Skip to content

feat: issue tracked migration credentials and guard activation (TECH-681 plan 011) - #28

Open
Ashex wants to merge 1 commit into
tech-681/saved-user-lifecyclefrom
tech-681/migration-credentials-readiness
Open

Ashex wants to merge 1 commit into
tech-681/saved-user-lifecyclefrom
tech-681/migration-credentials-readiness

Conversation

@Ashex

@Ashex Ashex commented Nov 12, 2026

Copy link
Copy Markdown
Contributor

Part of TECH-681. Stacked on #27. Implements plan 011: tracked migration credentials and one guarded activation operation.

Everything here is dormant. No routes are mounted, nothing is operator-visible yet, and there is no schema change, so there is no Changeset. Plan 012 mounts it and carries the Changeset.

What changes

composeIncomingArrival now takes a required legacy argument and adds these callables:

Callable Purpose
issueArrivalSession First tracked family for the saved browser user
issueCreationSession Cookie-less first family, as the continuation of createAccount
refreshArrivalSession Refresh a tracked family of a bound, unfinished arrival
recoverArrivalSession Saved-user recovery with a fresh email code (M3)
authorizeArrivalAccess Map a bearer to its arrival, owner and family
activateArrivalAsUser Activation by the saved browser user
activateArrivalWithCredential Activation by a tracked credential

Credentials

  • Family creation, arrival association and revocation of prior families commit in one authority transaction under the live fence. Tokens are signed first and returned only after commit; a rollback leaves no session or refresh rows.
  • Recovery replaces every earlier refresh family for the arrival. Already-issued stock access tokens stay usable until they expire (5 minutes).
  • Access tokens are at+jwt with the placement PDS DID as audience; refresh tokens use the Entryway service DID. No migration-only scope.
  • Ordinary login, refresh and account checks are unchanged and still refuse a pending account. The ordinary refreshSession now shares a helper with the arrival refresh; its behaviour is unchanged.

Activation

  • Checked before anything is journaled or sent: canonical PLC head, endpoint, repository key, ordered rotation keys, handle, placement, saved-user binding, backend present and paused, and no other unresolved attempt.
  • Sequence: journal the attempt, activate through the admin updateSubjectStatus hook, observe the backend active, then settle the attempt and complete the arrival in one transaction.
  • A lost acknowledgement stays uncertain until isolation or drain recovery. A lost local commit resumes the same operation with no second backend write. A backend already active with no journaled activation is not adopted.
  • No client "ready" flag or inventory check gates activation (C4).

Changes to shared and previously reviewed code

These need review from both developers.

  1. Plan 009 storage (migration-arrival.ts, account-storage.ts, the two ports), code only:
    • completeArrivalActivation now requires the exact placed account (provisioning, same email, handle and PDS) and activates it in the same transaction through a new shared activatePlacedAccount, which goes through saveAccount.
    • settleArrivalAttempt also accepts a receipt already resolved by an approved recovery (unapplied or replay-safe), so a superseded activation attempt is settled at completion.
    • Two read-only family reads and a new incoming-activation attempt step.
  2. Plan 010 path: arrivalBackend now throws OperationConflict on a placement change, where provisioning previously got a plain Error with the same message.
  3. requestDigest is exported from operation-ownership.ts and reused.
  4. Test fixture: the incoming arrival fixture moved to tests/support/helpers/incoming-arrival-fixture.mjs. The live and restart helpers pass the new legacy argument and activation callback.

Decisions recorded in docs/incoming-migration.md

  • Cookie-less issuance gating. The current, unexpired arrival code plus an accepted createAccount admission on a bound arrival is enough to obtain the first family. The source JWT is not re-checked, because it cannot verify after cutover. A leaked code could therefore win the first issuance; the window closes at first issuance or code expiry, and recovery revokes that family.
  • A fresh email code is required for recovery and for browser activation, with separate incoming-recover and incoming-activate budgets at the existing limits.
  • A second issuance is OperationConflict; a lost first response is recovered through recoverArrivalSession.
  • Refresh stays allowed while an external attempt is unresolved; issue and recover are refused.
  • A credential retry after a completed activation returns the committed result.

Validation

Run natively on Node 24 at this head:

  • npm run build, check, lint, format:check, schema:check: exit 0. typecheck and check:architecture (157 files, 0 violations) passed in review.
  • npm run test:unit: 756 tests, 737 pass, 0 fail, 19 skipped.
  • incoming-arrival-storage: 169 pass, 2 skipped. incoming-credentials: 33 pass. incoming-activation: 50 pass.
  • npm run test:incoming-reconciliation: 23 pass, 0 fail.
  • Mutation checks: 32 guards were removed one at a time. All fail a test except four redundant checks and three unreachable reconciliation-required branches in the completion transaction.

Not run locally: PostgreSQL. This PR changes storage code, so the plan's both-dialect gate is unmet until the databases job passes here. The live, Atmosphere in a Box and browser suites were also not run.

Not in this PR

  • Routes, pages, pending guards and the browser, XRPC and stock-callback consumers (plan 012).
  • admitIncoming still takes its owner from a browser session, so the cookie-less createAccount flow does not work end to end until plan 012 adds a tool-facing admission.
  • Live stock-PDS callback and recovery evidence, and the public journey with records and blobs.

🤖 Generated with Claude Code

Add dormant tracked credential and activation operations to the
incoming arrival composition.

Credentials: a bound, pending arrival can be issued one tracked
access/refresh family, either to the saved browser user or as the
cookie-less continuation of an accepted createAccount admission
presented with the current arrival code. Family creation, arrival
association and revocation of prior families commit in one authority
transaction; tokens are returned only after commit. Saved-user recovery
with a fresh email code replaces earlier refresh families. Ordinary
login, refresh and account checks are unchanged and still refuse a
pending account.

Activation: one fenced operation checks canonical PLC head, endpoint,
repository key, rotation custody, handle, placement, saved-user binding
and backend state before anything is journaled, activates through the
admin status hook, observes the backend active, then settles the
attempt and completes the arrival in one transaction. A lost
acknowledgement stays uncertain until isolation or drain recovery.

Storage: completeArrivalActivation now requires the exact placed
provisioning account and activates it in the same transaction, and
settleArrivalAttempt accepts receipts already resolved by an approved
recovery. No schema change. No routes are mounted.

This work was assisted by a machine
@coderabbitai

coderabbitai Bot commented Nov 12, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

Run configuration

  • Source mode: Auto
  • Repository: Unavailable
  • Branch: Unavailable
  • File: No configuration file from this repository applied
  • Commit: Unavailable
  • Layers: defaults
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: 407f27c6-4c92-4db6-9387-3878b203122f

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • Trigger review
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

Share

@sonarqubecloud

Copy link
Copy Markdown

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.

1 participant