Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 27 additions & 5 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ name: CI

on:
push:
branches: [main]
pull_request:

permissions:
Expand All @@ -10,7 +11,7 @@ permissions:
jobs:
validate:
runs-on: ubuntu-latest
timeout-minutes: 15
timeout-minutes: 30
services:
postgres:
image: postgres:18-alpine
Expand Down Expand Up @@ -49,11 +50,32 @@ jobs:
- name: Test
run: npm test

- name: Install browser fixture
run: npx playwright install --with-deps chromium
- name: Install browser fixtures
run: npx playwright install --with-deps chromium firefox webkit

- name: Test wallet connection
- name: Test cross-browser product journeys
run: npm run test:e2e --workspace @proofkey/web

- name: Build
- name: Build all workspaces
run: npm run build
env:
VITE_ETHEREUM_SEPOLIA_RPC_URL: https://sepolia.rpc.proofkey.invalid
VITE_CREDITCOIN_RPC_URL: https://creditcoin.rpc.proofkey.invalid
VITE_USAGE_PAYMENT_REGISTRY_ADDRESS: '0x0000000000000000000000000000000000000001'
VITE_MACHINE_REGISTRY_ADDRESS: '0x0000000000000000000000000000000000000002'
VITE_ACCESS_PASS_ADDRESS: '0x0000000000000000000000000000000000000004'
VITE_PROOFKEY_ASC_ADDRESS: '0x0000000000000000000000000000000000000005'
VITE_DEMO_MACHINE_ID: '0xc04beae61beb9471c4f24c8788a4624988d2948a5c3d3dd0b6ba1b7602875bcc'
VITE_PROOF_WORKER_URL: https://relay.proofkey.invalid

- name: Audit production configuration and bundle
run: npm run check:production
env:
VITE_ETHEREUM_SEPOLIA_RPC_URL: https://sepolia.rpc.proofkey.invalid
VITE_CREDITCOIN_RPC_URL: https://creditcoin.rpc.proofkey.invalid
VITE_USAGE_PAYMENT_REGISTRY_ADDRESS: '0x0000000000000000000000000000000000000001'
VITE_MACHINE_REGISTRY_ADDRESS: '0x0000000000000000000000000000000000000002'
VITE_ACCESS_PASS_ADDRESS: '0x0000000000000000000000000000000000000004'
VITE_PROOFKEY_ASC_ADDRESS: '0x0000000000000000000000000000000000000005'
VITE_DEMO_MACHINE_ID: '0xc04beae61beb9471c4f24c8788a4624988d2948a5c3d3dd0b6ba1b7602875bcc'
VITE_PROOF_WORKER_URL: https://relay.proofkey.invalid
39 changes: 39 additions & 0 deletions .github/workflows/production-smoke.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
name: Production smoke

on:
schedule:
- cron: '17 */6 * * *'
workflow_dispatch:

permissions:
contents: read

concurrency:
group: production-smoke
cancel-in-progress: true

jobs:
smoke:
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- name: Check out repository
uses: actions/checkout@v4

- name: Use Node.js
uses: actions/setup-node@v4
with:
node-version: 24
cache: npm

- name: Install dependencies
run: npm ci

- name: Install Chromium
run: npx playwright install --with-deps chromium

- name: Verify public deployment without writes
run: npm run test:smoke --workspace @proofkey/web
env:
PRODUCTION_WEB_URL: https://proofkey.vercel.app
PRODUCTION_RELAY_URL: https://proofkey-relay.onrender.com
21 changes: 16 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

ProofKey lets a customer pay for machine time on Ethereum Sepolia and unlocks a non-transferable access credential on Creditcoin. Attestcoin proves the source transaction to Creditcoin without bridging assets or trusting the relay worker.

**Status:** live testnet Product V1 · 94 automated tests · verified Sepolia-to-Creditcoin flow
**Status:** live testnet Product V1 · 143 automated checks · verified Sepolia-to-Creditcoin flow

[Launch ProofKey](https://proofkey.vercel.app) · [View the Sepolia payment](https://sepolia.etherscan.io/tx/0xb646bed97cd5ecafec256ea121a3ab7b5d147cce9c38e9e8f5f96cccfd17b967) · [View the Creditcoin authorization](https://creditcoin-testnet.blockscout.com/tx/0x45313262557698e745662272a1da814b74bcebb65b39990b44603c78cca64510)

Expand Down Expand Up @@ -187,19 +187,30 @@ Expected core result:
npm run check
```

The gate runs formatting, TypeScript checks, all automated tests, Solidity compilation, and production builds.
The gate runs formatting, TypeScript checks, all automated tests, Solidity compilation, and production builds. The browser journeys run deterministically in Chromium, Firefox, and WebKit in CI, including wallet recovery, a complete mocked payment-to-access journey, device authorization, keyboard navigation, and automated WCAG checks.

| Suite | Tests | Coverage focus |
| ---------------- | ------: | ------------------------------------------------------------------------------------------------------ |
| Solidity | 52 | Receipt semantics, proof tampering, replay, authorization, pricing, ownership, expiry, reentrancy |
| Relay worker | 23 | Leases, restart recovery, one-time handoffs, receipt signatures, CORS, readiness, secret-safe evidence |
| Customer web | 48 | Proof state, exact token math, operator workflow, session state machine, receipt tampering and expiry |
| Product browser | 7 | Multi-page rental, operator, proof, two-browser QR handoff, signed usage, and replay rejection |
| Customer web | 51 | Proof state, exact token math, operator workflow, diagnostics, privacy-safe telemetry, and sessions |
| Product browser | 11 | Multi-page rental, wallets, proof, QR handoff, accessibility, payment, and recovery journeys |
| Device simulator | 6 | Locked/unlocking/unlocked/expired states, tampered results, RPC failure |
| **Total** | **136** | |
| **Total** | **143** | |

The Solidity suite uses explicit verifier doubles at `0x0FD2` to isolate adversarial proof cases. Those tests are distinct from the committed live CC3 transaction, which executed against Creditcoin's real Native Query Verifier.

### Production readiness

`/diagnostics` gives operators a privacy-safe, read-only view of public configuration, relay readiness, and both required chain IDs. Client faults use stable codes and never record wallet addresses, transaction hashes, RPC URLs, or arbitrary error messages.

```bash
npm run check:production
npm run test:smoke --workspace @proofkey/web
```

The first command rejects incomplete or local-only configuration, secret-like client bundles, missing assets, source maps, and assets over the gzip budgets. The second performs a read-only deployment smoke test. See [`RELEASE.md`](RELEASE.md) for the release checklist and required environment values.

## Repository structure

| Path | Purpose |
Expand Down
32 changes: 32 additions & 0 deletions RELEASE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
# Production release gate

A ProofKey deployment is complete only after this checklist passes. Build success alone is not a release signal.

## Automated gates

- `npm run check` passes formatting, type checks, unit and contract tests, and all builds.
- `npm run check:production` rejects missing public configuration, credentials embedded in URLs, localhost coupling, source maps, secret-like bundle content, missing assets, and bundle-budget regressions.
- Mocked journeys pass in Chromium, Firefox, and WebKit, including wallet connection, checkout, relay state, activity recovery, proof exploration, operator onboarding, QR handoff, device refresh, signed usage, and replay rejection.
- Axe reports no serious or critical WCAG violations on the homepage and wallet dialog; keyboard navigation and a 390-pixel viewport are exercised.

## Performance budgets

| Metric | Enforced budget |
| --------------------------------- | ---------------: |
| Largest JavaScript chunk | 380 KiB gzip |
| Largest stylesheet | 20 KiB gzip |
| Production first contentful paint | 5 seconds |
| Target LCP | 2.5 seconds |
| Target INP | 200 milliseconds |
| Target CLS | 0.1 |

The chunk and paint limits fail automation. LCP, INP, and CLS are release targets checked in the Vercel production analytics view because representative field data cannot be manufactured in CI.

## Public smoke confirmation

1. Run `npm run test:smoke --workspace @proofkey/web` with `PRODUCTION_WEB_URL` and `PRODUCTION_RELAY_URL` set to the deployed HTTPS origins.
2. Confirm the homepage, machine deep link, wallet modal, recorded proof deep link, and every same-origin static asset return successfully.
3. Confirm relay `/health` returns HTTP 200 and `/ready` reports whether database, chains, and relayer are ready.
4. Open `/diagnostics` and confirm configuration, Sepolia, Creditcoin, and relay checks are green.
5. Confirm Vercel contains only public `VITE_*` configuration and Render contains server secrets. Never copy a worker key, RPC credential, or deployment token into Vercel.
6. Confirm the scheduled read-only smoke workflow has a recent green run before announcing the deployment.
60 changes: 60 additions & 0 deletions apps/web/e2e/accessibility.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import AxeBuilder from '@axe-core/playwright';
import { expect, test } from '@playwright/test';

test('homepage and wallet modal have no serious accessibility violations', async ({
page,
}) => {
await page.emulateMedia({ reducedMotion: 'reduce' });
await page.route('https://**.rpc.proofkey.invalid/**', (route) =>
route.abort(),
);
await page.goto('/', { waitUntil: 'domcontentloaded' });
const homepage = await new AxeBuilder({ page }).analyze();
expect(
homepage.violations.filter(({ impact }) =>
['serious', 'critical'].includes(impact ?? ''),
),
).toEqual([]);

await page.getByRole('button', { name: 'Connect wallet' }).click();
await expect(
page.getByRole('dialog', { name: 'Choose a wallet' }),
).toBeVisible();
const dialog = await new AxeBuilder({ page })
.include('.wallet-dialog')
.analyze();
expect(
dialog.violations.filter(({ impact }) =>
['serious', 'critical'].includes(impact ?? ''),
),
).toEqual([]);
await page.keyboard.press('Escape');
await expect(page.getByRole('dialog')).toBeHidden();
});

test('primary navigation is keyboard-operable and mobile layout does not overflow', async ({
page,
}) => {
await page.emulateMedia({ reducedMotion: 'reduce' });
await page.setViewportSize({ width: 390, height: 844 });
await page.route('https://**.rpc.proofkey.invalid/**', (route) =>
route.abort(),
);
await page.goto('/', { waitUntil: 'domcontentloaded' });
await page.getByRole('button', { name: 'Open navigation' }).focus();
await page.keyboard.press('Enter');
await expect(page.getByRole('navigation', { name: 'Primary' })).toHaveClass(
/open/,
);
await page
.getByRole('navigation', { name: 'Primary' })
.getByRole('link', { name: 'System', exact: true })
.focus();
await page.keyboard.press('Enter');
await expect(page).toHaveURL(/\/diagnostics$/);
const dimensions = await page.evaluate(() => ({
width: document.documentElement.clientWidth,
scrollWidth: document.documentElement.scrollWidth,
}));
expect(dimensions.scrollWidth).toBeLessThanOrEqual(dimensions.width + 1);
});
Loading
Loading