Skip to content

feat(account-claim): legacy-account-claim flow with email/password/staff proofs#46

Merged
themightychris merged 10 commits into
mainfrom
feat/account-claim
May 17, 2026
Merged

feat(account-claim): legacy-account-claim flow with email/password/staff proofs#46
themightychris merged 10 commits into
mainfrom
feat/account-claim

Conversation

@themightychris
Copy link
Copy Markdown
Member

Summary

  • Implements the legacy-account claim flow per specs/api/account-claim.md, specs/screens/account-claim.md, and specs/behaviors/account-migration.md.
  • API: 10 endpoints (candidates / confirm / decline / by-password / request-staff-review / legacy / legacy-request / staff queue / approve / deny), all coordinated via store.transact so public commits and private writes stay atomic.
  • Web: replaces the AccountClaimPlaceholder from github-oauth with the real /account-claim single + multi-candidate UI, by-password and request-staff-review sub-screens, the post-onboarding /account/claim-legacy search + merge submitter, and the staff queue at /staff/account-claim.
  • Storage: adds AccountClaimRequest as the third entity in the private store (account-claim-requests.jsonl); merge approvals re-point memberships / updates / buzz / help-wanted / interest from the requester Person to the legacy Person, hard-delete the requester, and write a 90-day slug-history redirect.
  • Anti-enumeration: by-password returns uniform 401 claim_credentials_invalid for unknown / already-claimed / wrong-password; request-staff-review always returns 202; the public commit trailer for staff-review submission carries no slug, email, or evidence.

Test plan

  • GET /api/account-claim/candidates returns 401 without cookie; returns matched candidates with matchedVia/matchedEmail for email and username matches
  • POST /confirm links Person, deletes LegacyPasswordCredential, issues session, refreshes private email
  • POST /confirm rejects not_a_candidate and email_match_required
  • POST /decline creates a fresh Person and leaves the candidate untouched
  • POST /by-password claims on correct password; returns uniform 401 claim_credentials_invalid for unknown slug + already-claimed
  • POST /request-staff-review returns 202 for nonexistent slug; creates AccountClaimRequest record
  • GET /staff/.../queue lists open requests; POST .../approve links GH on a pre-onboarding request
  • GET /api/account-claim/legacy?q= returns 0/1 candidate; never enumerates
  • POST /legacy/request + staff approve merges the requester into the legacy Person (GH transferred, requester hard-deleted, slug-history written)
  • Commit trailers for request-staff-review carry no slug/evidence/email
  • apps/api lint, type-check, and 182 tests pass

🤖 Generated with Claude Code

npm install -w apps/api bcryptjs
npm install -w apps/api -D @types/bcryptjs
…pport

Adds the third entity to the private store per
specs/api/account-claim.md#notes: account-claim-requests.jsonl alongside
profiles.jsonl and legacy-passwords.jsonl. The dual-store transact pipes
putClaimRequest through to the private-store flush so a single store.transact
covers the public commit and the claim-request write atomically.
Implements specs/api/account-claim.md and specs/behaviors/account-migration.md:
the legacy-account claim flow with three identity proofs (email, password,
staff approval), the post-onboarding /account/claim-legacy search + merge,
and the staff queue.

- routes/account-claim.ts — 10 endpoints, claim-pending JWT validation,
  uniform 401 for by-password (anti-enumeration), 202-always for
  request-staff-review.
- services/account-claim.ts — confirm/decline/byPassword/requestStaffReview
  plus the full post-onboarding merge: re-point memberships/updates/buzz/
  help-wanted/interest by author, hard-remove the requester Person, write
  90-day slug-history redirect, refresh PrivateProfile.
- auth/legacy-password.ts — bcrypt verifier dispatcher (bcryptjs).

Merge dedupes: when the claimed legacy Person already has a membership
or interest in the same project/role, the requester's duplicate is dropped
rather than creating two rows.

Notable spec edge case kept simple: pre-onboarding staff-approval seeds
Person.githubUserId/Login on the claimed legacy Person; the requester
gets a session next sign-in via the OAuth byGithubUserId hit.
Implements specs/screens/account-claim.md:
- /account-claim — single vs. multi candidate UI, with email-match auto-confirm
  and a username-only "verify with old password" hand-off
- /account-claim/by-password — slug + password form, uniform invalid message
- /account-claim/request-staff-review — evidence textarea + "continue as new"
- /account/claim-legacy — post-onboarding search + merge-request submission
- /staff/account-claim — staff queue with approve/deny + note

The AccountClaimPlaceholder shipped by github-oauth is removed; the route
points at the new AccountClaim screen.
Validates each endpoint per specs/api/account-claim.md, including the
anti-enumeration guarantees (uniform 401 for by-password, 202 for
request-staff-review) and the post-onboarding merge end-to-end (legacy
gains GH identity, fresh Person hard-deleted, slug-history entry written).
Mirrors the auto-claim paths: once the legacy Person has its GitHub
identity linked, the bcrypt hash is no longer needed and stays in the
private store only as a latent secret.
queryAll() on slug-history returns empty because the in-memory tree
isn't refreshed across sheet transactions in tests; verifying via
git ls-tree confirmed the file is committed. Switched to git show on
the blob path so the assertion isn't sensitive to that quirk.
@themightychris themightychris merged commit 11d6c0f into main May 17, 2026
1 check passed
@themightychris themightychris deleted the feat/account-claim branch May 17, 2026 01:52
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