Repository navigation
AUTH-MAIL-002C2 — Handle verification links safely without exposing action codes #194
Description
Officer impact: Members get one plain verification-link page with a deliberate Verify email action and safe recovery wording. Officers never ask for, copy, or open a member's action link or code.
Officer documentation: Update docs/officers/EMERGENCY_AND_RECOVERY.md, docs/officers/EVENTS_SHOP_MEMBERS.md, and docs/officers/SYSTEM_MAPS.md. Keep the route SOURCE ONLY, NOT LIVE until exact website, Firebase template, sender, and synthetic staged evidence exist.
Deployment evidence: None at creation. This child proves source/tests/merge only. It does not configure Firebase templates, sender/domain/DNS, publish a website, or test a production mailbox/action link.
Parent, dependency, and exact claim
Focused AUTH-MAIL-002C2 child of #120. #99, #103, and #111 provide callback preservation, deterministic no-network tests, and diagnostic redaction. #145/#153/#155 are merged. #178 released its separate My Account phone/profile paths.
CLAIMED by /root at 2026-07-13T23:38:15Z on codex/issue-194-verify-email-action, based on exact main aafd15c04fbcd09341092a78f23d513b09cca5d2.
Owned paths only:
src/pages/login/VerifyEmailAction.tsx(new)src/pages/login/VerifyEmailAction.test.tsx(new)src/pages/login/VerifyEmailAction.css(new)src/services/identity/Identity.tssrc/services/identity/Identity.test.tssrc/App.jsxsrc/App.test.jsxsrc/services/monitoring/capabilityCallback.tssrc/services/monitoring/sentry.test.tssrc/services/firebase/FirebaseResources.test.tstests/spa-navigation.test.jsdocs/officers/EMERGENCY_AND_RECOVERY.mddocs/officers/EVENTS_SHOP_MEMBERS.mddocs/officers/SYSTEM_MAPS.md- minimum exact C2 status hunks in
SECURITY.md,OPERATIONS_RUNBOOK.md,GITHUB_ISSUES.md, andGITHUB_ISSUE_SLICES.md
Excluded: Account/profile/phone paths, Functions, Rules, packages, workflows, Firebase/Netlify/DNS/template/provider settings, password-entry/reset completion, recover-email mutation, Google login, roles/membership, Firestore data, and every other issue.
Problem
There is no website route for incoming Firebase Auth action links. A proposed one-time code in the query would currently fall through to 404 while Sentry and App Check/reCAPTCHA can initialize before React removes it. Automatically applying a code on page load is also unsafe because mail-security scanners can open links.
Firebase's official custom-handler design uses one handler for verifyEmail, resetPassword, and recoverEmail. This child is intentionally verification-only source. Do not point Firebase's project-wide custom action URL at this partial route. #119 must keep the default multi-mode handler or wait for separately reviewed coverage of every enabled mode.
Official design reference: https://firebase.google.com/docs/auth/custom-email-handler
Atomic outcome
Add a public /auth/action source route that:
- Captures exactly one bounded verification code into component memory after the hosting handoff. The existing SAFETY-001 — Preserve callbacks and fail closed on local Firebase isolation #99 Pages fallback may briefly use tab-local session storage as clarified below.
- Removes all query and fragment text from visible history before any provider call.
- Starts no Sentry/App Check outside client while the capability is present.
- Waits for one deliberate native Verify email activation; page load never mutates an account.
- Calls
checkActionCode, requires provider operationVERIFY_EMAIL, and only then callsapplyActionCode. - Returns only small public states: ready, working, verified, already complete when provable, different signed-in account when provable, unusable, or temporarily unavailable.
- Uses one fixed same-origin recovery action and never follows query
apiKeyorcontinueUrl.
Security and usability invariants
- Query
modeis untrusted; provider-returned operation authorizes the exact action. - Duplicate, blank, malformed, unsupported, control-character, or oversized parameters make no provider call.
- No email, code, query, fragment, action URL, provider error, UID, or raw metadata enters DOM, title/meta, logs, diagnostics, analytics, or screenshots. After the existing SAFETY-001 — Preserve callbacks and fail closed on local Firebase isolation #99 Pages restore reads and deletes its tab-local handoff, AUTH-MAIL-002C2 — Handle verification links safely without exposing action codes #194 writes no capability to storage and removes it from native history and React Router state before enabling the action. A failed Pages root load can leave the already-merged handoff value until tab close; direct-rewrite hosting avoids that residual.
- Invalid, expired, used, disabled, deleted, wrong-environment, malformed, and wrong-operation links share one honest result. Firebase cannot reliably distinguish all of them.
- Offline, throttled, network, and unknown provider failures share one temporary result and a deliberate retry.
- A different signed-in account blocks apply without rendering either address.
- Verification changes Firebase Auth email verification only. It never grants membership/admin, changes claims, refreshes roles, writes Firestore, calls Functions, creates/signs in an account, or sends mail.
- A synchronous in-flight guard prevents double activation. Late completion after unmount cannot update the page.
- The action is keyboard-operable, visibly focused, at least 48px high, and exposes busy/disabled state. Final status receives focus with polite atomic announcement.
- Tests use synthetic codes/mocks only and make no network or production request.
Acceptance criteria
-
/auth/actionrenders the handler rather than 404 and Pages deep-link restoration preserves the initial query only long enough for immediate scrub. - Search/hash and navigation state are removed from native history and React Router before the action is enabled or an action-code promise starts; after the existing SAFETY-001 — Preserve callbacks and fail closed on local Firebase isolation #99 Pages handoff is read and deleted, AUTH-MAIL-002C2 — Handle verification links safely without exposing action codes #194 retains the code only in its component ref.
- No
checkActionCode,applyActionCode, or account mutation occurs on page load, rerender, or mail-scanner-style visit without activation. Normal Firebase Auth client/session initialization is not represented as an action-code call. - Exactly one valid
VERIFY_EMAILcheck leads to exactly one apply; rapid repeat activation does not duplicate either call. - A provider operation mismatch, wrong signed-in account, or malformed/duplicate/oversized request never applies the code.
- Success, already-complete, unusable, and temporary results are truthful, fixed, accessible, and contain no account/token/provider detail.
- Hostile
apiKey,continueUrl, absolute/protocol-relative URL, fragment, email, token, and provider-error canaries cannot affect authority/navigation or appear in output/logs. - Sentry and App Check stay off for case, trailing-slash, encoded, query, and fragment forms of the verification callback.
- Retry, check/apply race, wrong-account, unmount, focus, keyboard, busy, disabled, and 48px target tests pass without network.
- Focused/full frontend, SPA, lint baseline, build, Functions, Rules, commerce-emulator, artifact/workflow, diff/link, and sensitive-pattern checks pass.
- Officer/system maps explain the deliberate-click path, safe stop conditions, proof, undo, escalation, and
NOT LIVEprovider boundary. - Independent security/test/officer reviews approve exact diff; merge only after exact-head hosted checks.
Migration, rollback, and live stop condition
No data migration. Roll back the route/service/guard/docs together if focused tests expose a regression. The route stays source-only unless #119 privately proves a safe provider design. Stop any provider cutover if the configured action URL would send resetPassword or recoverEmail to this verification-only handler.
Activity
daliu commentedon Jul 13, 2026
PRE-EDIT ROUTE REFINEMENT — 2026-07-13T23:41:00Z
Independent provider review confirmed Firebase configures one generic action-handler URL for all Auth email modes. Before any source edit, the claimed route changes from
/account/verify-emailto generic/auth/action. This C2 dispatcher still accepts only exactmode=verifyEmailplus provider-returnedVERIFY_EMAIL; every other mode fails closed with the fixed unusable result.The generic path can be extended by future separately claimed reset/recover slices without changing provider routing. It does not make the partial handler safe to enable globally now. All owned files and exclusions remain unchanged; the issue body now uses the corrected route.
daliu commentedon Jul 13, 2026
SCOPE AMENDMENT — 2026-07-13T23:43:30Z
Failing-test review found two necessary owned paths before editing them:
src/services/firebase/FirebaseResources.ts: capture the initial capability-bearing location at module load. The new page scrubs the URL in a layout effect before the provider's later initialization effect; checking only the then-current URL could otherwise start App Check/reCAPTCHA after the scrub and lose the initial callback safety signal.SYSTEM_DESIGN.md: add the minimum architecture truth for the bearer-capability route and project-wide Firebase handler boundary.This does not add Analytics, provider configuration, or another action mode. Sentry remains guarded synchronously; FirebaseResources only preserves the same shared guard decision through startup. All other ownership/exclusions remain unchanged.
daliu commentedon Jul 13, 2026
SCOPE AMENDMENT — FRONTEND LINT RECORD — 2026-07-13T23:49:00Z
The merged non-mutating lint gate deliberately fingerprints every scanned frontend file as well as every finding. Adding the two claimed clean TS/TSX source/test files makes the gate reject even with zero new lint findings; Identity/App line shifts may also move existing recorded findings.
Before editing it, #194 adds exact ownership of
.github/lint-baseline.jsonfor one audited mechanical update. Acceptance is strict: scanned files 102→104, configured findings remain exactly 125 errors / 7 warnings, every finding change must be only an understood line/column move caused by this issue, focused ESLint must add zero findings, andlint:cimust pass. The checker/workflow source, lint rules, severities, messages, and existing debt are unchanged. This is not permission to regenerate the record merely to make CI green.daliu commentedon Jul 13, 2026
SCOPE AMENDMENT — 2026-07-13T23:51:43Z
The exact repository workflow guard
tests/ci-workflow.test.jsis added to #194 ownership for one mechanical expectation update only: the complete frontend lint surface grows from 102 to 104 files because #194 addsVerifyEmailAction.tsxand its test. The expected lint errors (125) and warnings (7) do not change. No CI workflow, lint implementation, or acceptance rule changes.This was discovered by the required full workflow safety suite before staging.
daliu commentedon Jul 14, 2026
ACCEPTANCE CLARIFICATION — 2026-07-14T00:05:57Z
Independent exact-diff review found two overbroad phrases in the issue text. This comment narrows them without weakening the mutation boundary:
checkActionCode,applyActionCode, or account mutation before deliberate activation. The application still initializes its normal Firebase Auth client and persisted-session listener on startup.sessionStorage, then the first root-page script reads and deletes it before React. If the root page never loads, that value can remain until the tab closes. Direct-rewrite hosting avoids this residual. AUTH-MAIL-002C2 — Handle verification links safely without exposing action codes #194 adds no storage write, explicitly tests deletion, and now removes the capability from both native history and React Router state.The accepted #194 boundary is: no new capability persistence; immediate native/router scrub; no action-code check/apply on scanner-style load; and a documented existing Pages residual pending a direct SPA rewrite.
daliu commentedon Jul 14, 2026
EXTRACTION FINISHED — 2026-07-14T00:11:56Z
Branch
codex/issue-194-verify-email-actionis pushed at exact commit1eae2d14917406cd9ee5e05224c07ac47d1558ae. Exact staged diff SHA-256:8d82d831b734ba75823fa70cb447b6b07b668ed3436f34949412963567000180.Local evidence on Node 20 / Java 17:
Deployment truth: source and tests only. No website, Firebase, provider, mailbox, template, DNS, member data, or production behavior changed or was tested.
daliu commentedon Jul 14, 2026
DELIVERY COMPLETE — 2026-07-14T00:17:06Z
PR #195 merged as exact main
e76b1ee01a0f0ce9951fdcc6e454b66288394b93. Exact-head PR CI run 29295281279 passed frontend, Functions, Firestore Rules, commerce emulator, and artifact jobs; the Netlify PR preview was ready. Post-merge CI run 29295389626 passed the same five jobs.Three independent exact-diff reviews approved security, test completeness, and backup-officer usability. The final issue acceptance is source/test/merge only, as designed. The claim and
status:in-progresslabel are released.Deployment evidence: not published. The exact merge commit has only a CI run. The repository still has no protected
productionenvironment (onlygithub-pages) and no repository release variables/secrets.runmprc.comstill servesmain.58ede287.js, not the locally proven #194 buildmain.fdd058b4.js. No Firebase deployment, action-handler/template/sender/DNS setting, mailbox test, or live action-code behavior occurred. The route must remain out of Firebase's global custom-handler configuration until #119 proves all enabled modes are safe.daliu commentedon Jul 17, 2026
#335 reused #194's dual native-history/React-Router cleanup pattern for the separate Strava callback and merged via PR #336 as exact main
f9a3f3cac92ae1a38d4a02041cf7301355470f36. A review-driven distinction is explicit: unconfirmed Router cleanup waits with callback hooks unmounted; detected ineffective/throwing/path-divergent cleanup shows a fixed stop. #194 files and behavior were unchanged.Exact-main CI 29555712712 passed all five jobs; deployments are empty.