feat(identity): account-delete substrate (A-1) — migration 0023 + read-time refusal chokepoints - #232
Merged
Merged
Conversation
unforced
force-pushed
the
a1-account-delete-substrate
branch
from
July 28, 2026 00:43
55cdea1 to
5a632ef
Compare
…-time refusal chokepoints Adds the nullable deleted_at/delete_undo_hash/delete_notice_sent_at columns (migration 0023, claiming the slot ahead of the vault-delete train's PR-2a — see cloud#226) and wires the read-time refusal every account-acting surface needs so a tombstoned row can never still act: the account bearer gate (account-api.ts requireAccount, account-mcp-http.ts authenticate), the session JOIN (sessions.ts findActiveSession), both password-login paths (console.ts, oauth-authorize.ts), magic-link request + consume + the 2FA divert (auth-handlers.ts), the onboarding drip's three eligibility queries, and the billing/usage/snapshot sweep enumerations. Deliberately inert: no route sets these columns yet (A-3 owns the delete endpoint, A-4 the undo + convergence sweep). Mirrors migration 0011's suspended_at no-oracle posture rather than inventing a second style, except where deletion is stronger: requireAccount/authenticate answer a deleted owner with the exact "account not found" body a missing row gets, not the distinguishable account_suspended a suspended owner gets. Tests to follow in a separate commit.
…e-fix commit Adds the required coverage for the account-delete substrate (8e72fe6), one test (or set) per chokepoint: the account bearer gate (account-api.ts + account-mcp-http.ts), the session JOIN (via the account-token mint path), both password-login paths, magic-link request/consume + the 2FA divert, the signup-collision degrade (verification only — no new code), the drip's three eligibility queries, and the billing/usage/snapshot sweep enumerations. Verified red-then-green: reverted the 12 chokepoint src files (kept migration 0023) to the pre-fix commit, ran all 10 touched test files — 14 of 15 new tests failed for the stated reason (the 15th, signup collision, is a verification test with no new code to break, and passed as expected both before and after). Restored the fix; same 10 files now 463/463 passing. rc.128 -> rc.130 (re-bumped after rebasing onto main: #229 landed rc.128 first, #230 holds rc.129 ahead of this PR in the merge queue).
unforced
force-pushed
the
a1-account-delete-substrate
branch
from
July 28, 2026 00:48
5a632ef to
acd7d10
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
A-1 of the account-deletion train (24-hour undo window, ratified by Aaron). This PR is the substrate only — deliberately inert:
0023_account_delete.sql: nullableusers.deleted_at/delete_undo_hash/delete_notice_sent_at. Claims 0023 ahead of the vault-delete train's PR-2a, which re-bases onto 0024 (sequencing recorded on Hosted door has no vault-delete path — handleAccountVaultDelete is a 501 stub (DO + R2 teardown missing) #226).DELETE /accounthandler) and A-4 (undo + convergence sweep) own that.Chokepoints (what each prevents)
Mirrors migration 0011's
suspended_atno-oracle posture rather than inventing a second style, with one deliberate divergence: deletion is a stronger, one-way fact than suspension, so the account-bearer gates degrade all the way to the same "account not found" body a missing row gets — never the distinguishableaccount_suspendeda live-but-suspended owner gets.requireAccount(account-api.ts) +authenticate(account-mcp-http.ts, found during the sweep — same gate shape, same risk) → 401invalid_token, byte-identical to a missing row.findActiveSession(sessions.ts) —AND u.deleted_at IS NULLon the JOIN; the account-token mint path (account-token.test.ts) proves it end-to-end.console.ts/login,oauth-authorize.ts's inline authorize-login form) — the exact wrong-password message, even on the correct password.handleLogin2faPost's own re-check, for the case where deletion lands between the password step and the code step) — same neutral failure, no oracle.getUserByEmail, so the row is still found and the existing "already exists" branch fires) — no code change needed here, just the regression test.eligibleForqueries (welcome,connect-nudge,feedback) gainAND u.deleted_at IS NULL.runBillingSweep), usage (runUsageRollup, newly JOINsusers), and snapshot (runSnapshotSweep) sweeps all exclude a tombstoned owner.Verification
Red-then-green, watched, not assumed: reverted the 12 chokepoint
src/files to the pre-fix commit (d773237) while keeping migration 0023 (so the columns exist, just unread by app code), ran the 10 touched test files — 14 of 15 new tests failed for the stated reason (wrong status code, session actually minted, email actually sent, sweep actually touched the vault, etc.). The 15th (signup collision) passed both before and after, as expected — it's a verification test, not new logic. Restored the fix; same 10 files now 463 pass, 0 fail.bun run typecheck— clean, both states.Full suite: this session's local machine hit a reproducible port-exhaustion wall (
EADDRNOTAVAIL,Fallback service failed to fetch module) trying to run all 38 identity test files at once — confirmed environmental (uniform failures across files unrelated to this diff, live hub/vault stack stayed healthy at 200 throughout, elevatedTIME_WAITcount, symptom varied run to run). Abun installreinstall fixed a stale-cache module-resolution issue along the way, but the full-parallel run still couldn't complete locally. The 10 touched files plus a 14-file batch of untouched ones ran clean; CI (ci.yml, a fresh runner with no shared-machine contention) is the authoritative gate here and should be watched on this PR.Scope
Does not add: the
DELETE /accountroute, the undo-verify endpoint, or any sweep that acts ondeleted_atbeyond exclusion (A-3/A-4).rc
0.0.8-rc.127→rc.130— held atrc.130across two rebases:mainfirst re-bumped past it with #229 (rc.128), then again with #230 (rc.129,cloud#226 A-2billing teardown).rc.130is still correctly one above currentmain.Rebased onto current
maintwice (now includes #229cloud#226 PR-1+ #230cloud#226 A-2); both times the only conflict was the version line — took ours (rc.130) per the workspace rule that the version line always collides and doesn't count as real overlap. The fix/test commits are unchanged in content. Re-ran typecheck + the 10 touched test files (463/463) after each rebase; live hub/vault stack confirmed healthy before and after.🤖 Generated with Claude Code
https://claude.ai/code/session_01XLZtmuSs1RirWGMGyCB1QB