schulze 3343a6985f
Verify / verify (push) Failing after 14m57s
fix: stabilize integration test isolation
2026-08-05 12:24:36 +02:00
2026-07-29 22:56:30 +02:00
2026-07-17 15:47:29 +02:00

Poker foundation operations

Phase 1 provides one fixed-admin, passkey-only deployment. Public signup and non-passkey sign-in are disabled. Keep credentials, grant fragments, tokens, database URLs, device details, and acceptance evidence outside this public repository.

Build and operator commands

Install dependencies and build the five ESM operator programs before local use:

pnpm install --frozen-lockfile
pnpm build:operators
node dist/scripts/migrate.mjs
node dist/scripts/create-bootstrap-grant.mjs
node dist/scripts/create-recovery-grant.mjs --confirm
node dist/scripts/list-passkeys.mjs
node dist/scripts/revoke-passkey.mjs --passkey-id=<uuid> --confirm

Equivalent package commands are pnpm db:migrate, pnpm auth:bootstrap:create, pnpm auth:recovery:create -- --confirm, pnpm auth:passkey:list, and pnpm auth:passkey:revoke -- --passkey-id=<uuid> --confirm.

Each of four auth operators accepts --check. Check mode parses complete server environment, connects to database, reports only non-secret mode/count state, and never mutates. It needs neither bootstrap token, confirmation, nor passkey ID. Migration has no check mode; run it only as controlled release step.

lib/db/schema is the sole DDL source. Generator-owned drizzle/ files are never edited or formatted, and production images or startup never generate schema or run drizzle-kit push. Every migration change requires fresh clean regeneration in a temporary checkout plus human review of the complete SQL and metadata diff before commit. Regular CI runs pnpm db:check for schema consistency but does not reproduce generated migrations.

Migration and rollback

  1. Take and test database backup.
  2. Run node dist/scripts/migrate.mjs once from release image before application rollout.
  3. Block rollout if migration fails. Never let application startup mutate schema.
  4. On application failure, roll back application image first.

Initial auth migration creates tables and declarative constraints only. After any credential use, destructive down migration is forbidden. Database rollback means restoring tested pre-migration backup. Daily backups and disposable restore tests are required before Phase 2 data. Phase 4 owns full schedules, retention, alerts, and evidence automation.

Bootstrap

Set ADMIN_BOOTSTRAP_TOKEN only for bootstrap mutation while bootstrap remains pending. Run bootstrap operator and transfer returned fragment URL through confidential channel. Browser removes URL fragment immediately; navigation never sends it to server. Browser then intentionally sends raw grant once in same-origin HTTPS JSON exchange, which creates short-lived cookie/context. Request-body logging on exchange routes is forbidden, and fragments or grants must not be retained in source or logs. While bootstrap remains pending, --replace explicitly supersedes the pending generation and issues a new root grant; it does not continue a consumed root. Without --replace, subsequent root creation fails with BOOTSTRAP_ALREADY_INITIALIZED:

node dist/scripts/create-bootstrap-grant.mjs --replace

Registration grants belong to the current bootstrap or recovery generation. The first verified bootstrap registration atomically creates lifecycle and passkey state, consumes the grant, activates exactly one viable administrator passkey, deletes administrator sessions, increments the bounded authentication epoch once, and audits completion. No continuation or active-add path exists. A failed insert rolls back lifecycle, grant, passkey, session, epoch, mode, and audit changes; start a fresh official ceremony with the same still-valid grant rather than replaying the consumed challenge.

Credential lifecycle is durable: pending becomes registered, revoked, or expired. Retired credential identity remains permanently recorded.

The application requires exactly one viable administrator passkey. Passkey management is rename-only; application deletion is unavailable. Use recovery for replacement.

Recovery and revocation

  1. Run passkey list and record row IDs in private audit system.
  2. Enter recovery with node dist/scripts/create-recovery-grant.mjs --confirm. Explicit --confirm is required whenever any viable credential exists. Entry deletes all administrator sessions and increments authentication epoch exactly once.
  3. Register one replacement passkey. Its first verified registration atomically retires the old viable credential when present, retains that lifecycle identity as revoked, deletes its passkey child, invalidates sessions, activates exactly one new credential, increments the epoch once, and audits completion. Recovery from zero prior credentials follows the same flow without an old credential.
  4. Sign in fresh with the replacement.

Legacy state with more than one viable credential fails closed until an operator explicitly cleans it up. Operator revocation remains available only by exact row ID plus --confirm; it is audited, permanently retains revoked lifecycle identity, invalidates all administrator sessions, increments the epoch once, and may leave zero viable credentials. Missing confirmation or invalid/unknown ID fails without mutation. Never paste real operator output into repository issues or commits.

If recovery entry, activation, or operator revocation would increment epoch beyond 9007199254740991, operation fails atomically before deleting sessions or changing mode, grants, lifecycle, or audit state. Stop and escalate; never repair epoch with manual SQL.

Deployment security

Production uses exact HTTPS origin; only local development may use http://localhost. WebAuthn RP ID must equal origin hostname. Hostname or RP changes strand existing credentials: plan credential migration/re-enrollment before cutover.

Grant and rate-limit secrets are stored only as keyed HMAC digests. Bootstrap and recovery plaintext grants appear once in URL fragment. Browser clears fragment immediately, never sends it during navigation, and exchanges it only through same-origin HTTPS JSON; do not log fragments, grants, cookies, or request bodies on exchange routes.

AUTH_IP_ADDRESS_HEADERS trusts only configured reverse-proxy headers. Proxy must strip client-supplied values, set every listed header itself, and prevent direct ingress to application. CI's x-forwarded-for trust exists only inside isolated loopback/container networking.

Better Auth 1.6.23 requests user verification for registration options but cannot enforce it during registration verification. Authentication internally requests preferred UV and offers no supported mandatory-UV option. Record real-device UV results without claiming server enforcement. Protocol fields also cannot prove physical credential independence.

Product routes

Administrator routes are:

  • Dashboard /; game setup /game/new and /game/{gameId}/setup; live game /game/live; closing /game/close/count, /game/close/reconcile, /game/close/review, /game/close/settle, and /game/close/complete.
  • History /history, historical entry /history/new, detail /history/{gameId}, correction /history/{gameId}/correct, and leaderboard /leaderboard.
  • Settings /settings/players, /settings/chips, /settings/presets, and /settings/share; passkey management /security/passkeys.

Hand rankings are public at /hand-rankings. Read-only shared routes are /shared/history, /shared/history/{gameId}, and /shared/leaderboard. /share/{token} is only the transient exchange shape that establishes reader access and redirects to a clean URL. Never log or bookmark it.

Browser verification

With local Compose PostgreSQL running, use:

TEST_DATABASE_URL=postgres://poker:local-only@127.0.0.1:55432/poker pnpm test:browser

The runner creates, migrates, and drops a strictly disposable child database named poker_browser_<random>. Browser helpers exist under tests only. The application has no fixture token, authentication bypass, or production switch.

Never point browser verification at production, a remote database, or a production SSH tunnel.

Stable HTTPS product acceptance

Use an already-provisioned active administrator on stable HTTPS:

  1. On phone and laptop, complete a full poker night: setup, start, late join, rebuy and undo, every close step, history and correction, and leaderboard.
  2. Rotate reader access, exchange it, revoke it, and prove old access is rejected after both rotation and revocation.
  3. Verify keyboard order, focus, announcements, screen-reader names, and absence of page overflow.
  4. Verify public and reader hand rankings.

Bootstrap and recovery ceremony acceptance are excluded; Phase 4 owns them. Record date, browser, and device evidence outside source. Omit origins, tokens, credential IDs, operator output, device identifiers, and private screenshots.

Real HTTPS acceptance

Run against exact production HTTPS origin. Store evidence privately when it includes operator/device details.

  1. Recreate any disposable manual database produced by the obsolete multi-registration flow. Bootstrap from fragment URL, register one passkey, prove activation and old-session invalidation, then sign in fresh.
  2. Rename the passkey and verify no application add or delete control exists.
  3. Start recovery with confirmation, prove sessions and authentication are blocked, register one replacement, prove the old credential is retired and recovery atomically activates exactly one new credential, then sign in fresh.
  4. In a separate disposable run, verify recovery from zero prior credentials. Seed legacy state with more than one viable credential only in disposable operator testing; prove fail-closed behavior, then clean up by exact-ID confirmed operator revocation.
  5. Attempt revoked authenticator re-registration and authentication; prove both fail. Verify an exact-ID confirmed operator revocation can leave zero.
  6. Restart between options/verification and session checks; prove same secret, database, and origin preserve challenge and state behavior.
  7. Record date, devices/browser versions, exact origin, requested UV result, and pass/fail outside source when evidence contains operator details. Use one real passkey at a time; do not claim physical independence.
S
Description
poker scoreboard
Readme
1.1 MiB
Languages
TypeScript 97.5%
CSS 2.1%
JavaScript 0.3%