Skip to content

PayPal sandbox only. No real money moves.

BuildersBuildersPackages

@gavel/web

The browser client: routes, the transport seam, the payment approval port, the copy rules and the bundle gate.

Verifiedbacked by a real run or committed evidence

Page last changed 7 Nov 2026 (from the git history)

packages/web is the browser client: React 18, Vite and strict TypeScript. It renders the bidder, auctioneer console, public screen, audit and replay views from the wire types in @gavel/shared. All money is integer cents. All user text lives in src/copy.

It holds no business rules. Whether a bid stands, who wins and what is charged are decided by the engine and the payments saga on the server; the client shows the state it receives and sends commands.

Responsibilities

  • Routes. src/routes.tsx is the route table (first match wins, catch-all last). Routes that need a stored session token are marked needsToken; the audit route needs a reviewer token.
  • Live state. src/net wraps the WebSocket behind a Transport interface; connection.ts reconnects with backoff and resync.ts asks for a fresh snapshot after a gap. src/store reduces server messages into room state.
  • Payment approval. src/payments hides PayPal behind PaymentApprovalPort; there is a fake adapter and a PayPal sandbox adapter (paypalV6Adapter.ts).
  • Voice input. src/audio captures the microphone and sends it to the /audio socket for the auctioneer console.
  • Design system. src/design holds tokens, themes (including the projector theme), primitives and the sandbox and fake-payments labels.
  • Accessibility. src/a11y holds the announcer and focus helpers.

Routes

RouteScreenToken
/Landingnone
/demoDemo launchernone
/join/:roomIdJoinnone
/r/:roomId/register and /r/:roomId/returnRegister and the return from PayPal approvalnone
/r/:roomId/bidBidder screennone
/r/:roomId/receiptReceiptnone
/r/:roomId/consoleAuctioneer consoleauctioneer token
/r/:roomId/screenPublic screennone
/r/:roomId/auditAudit table and exportreviewer token
/opsOperations viewnone
/replay/:recordingIdRead-only replaynone
/galleryComponent gallerydev and preview builds only

Public interfaces

The package is an application, not a library, and exports nothing for other packages. Its seams are internal and are what the tests replace:

  • Transport (src/net/transport.ts): connect, send, onMessage, onStatus, close. The app never touches a WebSocket directly. A scenario transport and a scripted mode feed the same interface in tests and demos.
  • ControlPort (same file): the auctioneer commands open, pause, resume, extend, hammer and kill('STOP'), sent over HTTP.
  • PaymentApprovalPort (src/payments/port.ts): approval states idle, preparing, awaiting_buyer, confirming, held, cancelled, declined, error.
  • src/wire/index.ts: the single import seam for wire types, re-exported from @gavel/shared, so the client and the server cannot drift apart.

Key files

main.tsx
App.tsx
routes.tsx
config.ts
transport.ts
wsTransport.ts
connection.ts
resync.ts
vite.config.ts
FileWhat it holds
src/config.tsReads the VITE_* variables at start. A missing or invalid value stops the app with a ConfigError that lists names only. An endpoint must be the secure scheme; the plain scheme is accepted only for a loopback host
src/net/connection.tsReconnect backoff 500, 1000, 2000, 4000, 8000 ms with 20 percent jitter; the offline banner shows after 2 s. unauthorized, forbidden and invite_used end the session; room_full, auction_full, room_cap and rate_limited stop reconnecting until the bidder asks again
src/net/resync.tsRe-sends the join with the stored token and last seq until a snapshot arrives (retry every 3 s)
src/screens/console/shortcuts.tsConfirm queue keys Y, N, J and K; ignored while a field has focus or a modifier is held
src/copy/banned.tsWording rules: before the CAPTURED state no catalog string may say charged, pay, paid, payment, capture, debit, billed, deducted or fee (a negation such as "not charged" is allowed)
vite.config.tsThe sandbox guard and the Content-Security-Policy generator
scripts/check-size.mjsThe initial-route size gate

Configuration

Prop

Type

Invariants

  • Sandbox only. The Vite plugin gavel-sandbox-guard fails a production build unless VITE_SANDBOX is exactly true, and refuses VITE_PAYMENTS=fake without VITE_ALLOW_FAKE=true.
  • A fixed Content-Security-Policy. buildCsp generates the policy from the same config: scripts only from self and the PayPal sandbox, connect-src limited to self, the configured API and WS origins and the two PayPal sandbox origins, no objects, no inline script allowance.
  • A small first load. check-size.mjs fails the build when the gzip size of the initial route is over 180 KB, or when the PayPal SDK loader, the QR encoder or the replay player is part of that initial graph. The replay screen is loaded lazily.
  • No secret in the bundle. The root check npm run check:secrets builds the web app with canary values and scans the output (see Infra).
  • One wire source. Wire types come only through src/wire, which re-exports @gavel/shared. The web source imports @gavel/shared; the dependency rule would also allow @gavel/voice, but no web source file imports it today.
  • Honest wording. The banned-wording rules above are applied to the copy catalog by test/unit/copy.test.ts.

Tests that guard it

AreaTest files
Routing and shelltest/component/routes.test.tsx, mountedRoutes.test.tsx, App.test.tsx, shellBanner.test.tsx, boundaryRoute.test.tsx
Biddingtest/component/bid.test.tsx, bidderFlow.test.tsx, bidderFlows.test.tsx, Ladder.test.tsx
Registration and approvaltest/component/register.test.tsx, registerRouteBearer.test.tsx, registerPresetsSession.test.tsx, join.test.tsx
Console and voicetest/component/consoleRoute.test.tsx, ConsoleControls.test.tsx, consoleGate.test.tsx, liveConsole.test.tsx, micAudio.test.ts
Settlement and receipttest/component/receipt.test.tsx, receiptSettlement.test.tsx, holdRemainder.test.tsx, replayAudioSettlement.test.tsx
Audit and replaytest/component/auditRoute.test.tsx, auditExport.test.tsx, replayPlayer.test.tsx, ReplayScreen.test.tsx
Server contracttest/component/serverContract.test.ts
Copy rulestest/unit/copy.test.ts
Accessibilitytest/component/a11y.test.tsx; browser specs test/visual/a11y.spec.ts, keyboard.spec.ts, motion.spec.ts, responsive.spec.ts
Screen tourtest/screens/*.spec.ts (one image per reachable state, no pixel assertions)

Coverage floor from coverage.thresholds.json: 70 percent lines and 60 percent branches.

Scripts

ScriptWhat it does
npm run dev -w @gavel/webVite dev server (run it on your own machine, not on a shared one)
npm run build -w @gavel/webType check, then the Vite bundle
npm run test -w @gavel/webUnit and component tests
npm run test:a11y -w @gavel/webaxe-core and keyboard specs in a browser (CI)
npm run test:screens -w @gavel/webThe screen tour (CI workflow ui-screens)
npm run check:size -w @gavel/webThe initial-route size gate after a build
Edit on GitHub

Last updated on