Skip to content

feat: make first-user bootstrap atomic (TIN-2821) - #41

Draft
Jess Sullivan (Jesssullivan) wants to merge 4 commits into
mainfrom
codex/tin-2821-atomic-bootstrap-hardened-20260713
Draft

feat: make first-user bootstrap atomic (TIN-2821)#41
Jess Sullivan (Jesssullivan) wants to merge 4 commits into
mainfrom
codex/tin-2821-atomic-bootstrap-hardened-20260713

Conversation

@Jesssullivan

Copy link
Copy Markdown
Contributor

Scope\n\n- Add a tenant-scoped atomic first-user bootstrap claim/finalize/receipt protocol.\n- Keep inert claims authority-free and make exact finalization replay idempotent.\n- Move password hashes, raw TOTP material, backup codes, and prepared finalization into server-side attempt custody.\n- Make session identity immutable so existing sessions cannot be rebound to the claimed actor.\n- Add reusable storage and attempt-store conformance suites for built-in and external adapters.\n- Update the file, memory, fixed-tenant, MVP, and operator documentation surfaces.\n\n## Security properties\n\n- Browser state is only a version plus a 192-bit opaque attempt id.\n- Completion never returns backup codes; one-time disclosure occurs during initiation.\n- TOTP ciphertext is round-trip decrypted and timing-safe compared before finalization.\n- Prepared attempts expire, reject stale profile snapshots, freeze after preparation, and permit replacement after expiry.\n- Finalized bootstrap actors, factors, and backup-code authority cannot be independently deleted.\n\n## Hard gates\n\nSOURCE-ONLY DRAFT. DO NOT MERGE, TAG, RELEASE, PROMOTE, OR ADOPT YET.\n\nThe unreleased 0.8 interface is intentionally breaking. Current PostgreSQL and Redis adapter releases do not implement the mandatory atomic methods or durable attempt-store contract. Merge/release/adoption remain blocked on:\n\n- TIN-2828: PostgreSQL implementation plus conformance.\n- TIN-2829: Redis implementation plus conformance.\n- Release metadata and registry promotion for a coordinated 0.8 wave.\n- Mothership adoption and an attended first-user canary.\n- TIN-2822 RBAC convergence after this source contract is staged.\n\ntinyland.dev #731 remains held. This PR does not mutate runtime state, SOPS, CI secrets, tags, registry packages, or deployments.\n\n## Verification\n\n- pnpm typecheck\n- focused bootstrap/storage suite: 76/76\n- pnpm build\n- pnpm check:release-metadata\n- pnpm check:invitation-authority\n- pnpm check:package (passes; existing sideEffects suggestion only)\n- Bazel //:test and //:typecheck with a 60-second Vitest case timeout: passed\n- git diff --check\n- independent adversarial review: no remaining source-draft blockers\n\nThe default host-constrained full suite previously reached 446/449 with only three unchanged bcrypt cases hitting their 30-second timeout; the isolated password suite passed 20/20, and the final Bazel run passed with the explicit timeout above.\n\nTracks TIN-2821. It does not close the ticket because adapter adoption, release, and attended runtime proof remain outstanding.

@Jesssullivan

Copy link
Copy Markdown
Contributor Author

Review gate: preserve this PR as the canonical design, but keep it draft until the following are repaired and re-reviewed:

  • Enforce exact claim/finalization schemas before any commit. Unknown, missing, non-JSON, sparse, undefined, and negative-zero values must fail closed.
  • Make completed-replay identity injective and validate material before comparing its digest.
  • Replace filesystem stale-lock recovery with ownership-preserving or fail-closed semantics; the current check-then-rename/check-then-remove sequence can steal a replacement lock.
  • Bind file contents to the expected tenant filename and normalize tenant identity before forwarding through fixed-tenant adapters.
  • Pass the generated tenant into external-adapter conformance harnesses and add exact-schema, digest-edge, filename-swap, and deterministic contention tests.
  • Keep the release boundary honest: this is unreleased 0.8 source work; PG/Redis adoption remains blocked on their durable CAS implementations.

The competing local implementation will not replace this PR: it regresses browser-state custody, lost-response recovery, and immutable session identity. A clean follow-up worktree based on this exact head is repairing only the gaps above.

@Jesssullivan

Copy link
Copy Markdown
Contributor Author

RBAC/invitation interoperability review confirms this PR should stay focused on atomic bootstrap. The separate RBAC worktree preserves the intended model: management rank is a strict order, while specialist capabilities remain intentionally non-monotone. It is not release-ready yet.

Blocking follow-ons, in order:

  1. Finish this bootstrap transaction and adapters first. General user creation/invitations stay disabled until the immutable first-super-admin receipt exists.
  2. Fix invitation authority separately: tinyland-invitation 0.2.5 has a duplicate role order that lets editor/event_manager issue moderator invites, trusts caller-supplied createdByRole, and only serializes acceptance per process. Acceptance needs trusted-actor resolution plus distributed CAS that atomically creates the user, consumes the invite, and writes an idempotent receipt. Keep the >=0.2.5 floor until that replacement release exists.
  3. Add one canonical assignRole transaction in tinyland-auth: reload actor and target, require the actor above both current and requested roles, constrain explicit grants, preserve one active super_admin, audit, and revoke sessions or bump an authorization epoch atomically. Rank gates are for management ordering only; feature routes use requiredPermission.
  4. Cut coordinated 0.8 package/adaptor releases only after those transactions land, then run attended concurrent invite, demotion, and immediate-session-invalidation canaries.

Claim boundary: current RBAC work is source-contract review only, not merge, release, registry promotion, downstream adoption, or live correctness.

@Jesssullivan

Copy link
Copy Markdown
Contributor Author

Follow-up hardening is now on the PR head as signed commit eafdbb9e056677347a608a449b6fff143e3069be.

Evidence before push:

  • fresh independent review: PASS
  • focused boundary/conformance checks: 118/118
  • full suite: 485/485
  • typecheck, build, package/publint, runtime exports: pass
  • Bazel //:test, //:typecheck, //:pkg: pass locally
  • exact 10-minute semantics: 599999 and 600000 accepted; 600001 rejected
  • adversarial adapter cannot certify expired finalization
  • cross-owner inode-safe release and two-sample lock observation retained

Claim boundary remains unchanged: source-only draft at package version 0.7.1; no release, adapter rollout, or live two-replica canary.

@Jesssullivan

Copy link
Copy Markdown
Contributor Author

Review follow-up ffc1b0ab2d7244551485a5df5baa9ff8604a24f4 closes the file-backed session lost-update race found after the original draft.

  • deleteSession, deleteUserSessions, and cleanupExpiredSessions now hold the same bootstrap filesystem lock across read/modify/atomic-write.
  • Deterministic red-before/green-after matrix: all 3 stale-privileged-survivor cases fail against the archived pre-repair source and pass after the repair.
  • Isolated file-storage suite: 72/72.
  • Offline Bazel //:pkg, //:test, and //:typecheck: pass.
  • Fresh independent source review: SHIP, no P0-P2 findings.
  • The required legacy 0.7 file-store fixture is tracked in the commit.

Boundary unchanged: this remains a source-only draft. The file lock is single-host/cooperative-filesystem authority; PostgreSQL and Redis remain blocked on TIN-2828/TIN-2829. Do not merge, tag, release, promote, adopt, or touch mothership runtime state from this PR.

Comment thread src/storage/firstUserBootstrap.ts Fixed
Comment thread src/storage/firstUserBootstrap.ts Fixed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants