diff --git a/.github/lint-baseline.json b/.github/lint-baseline.json
index 54a5ad49..d6df695e 100644
--- a/.github/lint-baseline.json
+++ b/.github/lint-baseline.json
@@ -10,12 +10,12 @@
"targets": [
"src"
],
- "scannedFileCount": 102,
+ "scannedFileCount": 104,
"scannedFilesByExtension": {
".js": 12,
".jsx": 35,
".ts": 31,
- ".tsx": 24
+ ".tsx": 26
},
"errorCount": 125,
"warningCount": 7,
@@ -31,7 +31,7 @@
".ts": 0,
".tsx": 7
},
- "fingerprint": "sha256:47721020c5ff47ef62494b6dcc6f7b806a4d446d32ef5f9193b1437a324c69b0",
+ "fingerprint": "sha256:7ddc6bb58cff269cf95dc05fc7e649f1f91c551e972afb5d7a0d1db5e0a1c964",
"scannedFiles": [
"src/App.jsx",
"src/App.test.jsx",
@@ -90,6 +90,8 @@
"src/pages/legal/Terms.jsx",
"src/pages/login/LoginForm.jsx",
"src/pages/login/LoginForm.test.jsx",
+ "src/pages/login/VerifyEmailAction.test.tsx",
+ "src/pages/login/VerifyEmailAction.tsx",
"src/pages/login/loginReturnPath.ts",
"src/pages/notFound/NotFound.jsx",
"src/pages/officers/Committee.jsx",
diff --git a/GITHUB_ISSUES.md b/GITHUB_ISSUES.md
index b39209c4..c59d615b 100644
--- a/GITHUB_ISSUES.md
+++ b/GITHUB_ISSUES.md
@@ -329,7 +329,7 @@ Both the legacy member synchronization function and admin role callable grant cl
AUTH-001A now rejects unverified targets at the two existing role-grant endpoints. Do not duplicate that merged slice. It is not proof that Firebase production runs the new code, and it does not complete the remaining parent requirements below.
-Adjacent account-email recovery is represented by canonical live parent [#120](https://github.com/Run-MPRC/Run-MPRC.github.io/issues/120), not by a new AUTH-001 role-grant child. AUTH-MAIL-002A [#145](https://github.com/Run-MPRC/Run-MPRC.github.io/issues/145) merged the account-creation/request split as `46557c7` but is not published. AUTH-MAIL-002B [#153](https://github.com/Run-MPRC/Run-MPRC.github.io/issues/153) merged the truthful My Account resend result and browser cooldown as `23bca8c8`, but protected release run `29252492614` stopped before build and published nothing. AUTH-MAIL-002C1 [#155](https://github.com/Run-MPRC/Run-MPRC.github.io/issues/155) tracks one neutral password-reset request result and repeat-safe browser cooldown. Incoming action-code states remain proposed as C2. None grants membership, proves provider delivery, or proves Firebase API enumeration protection.
+Adjacent account-email recovery is represented by canonical live parent [#120](https://github.com/Run-MPRC/Run-MPRC.github.io/issues/120), not by a new AUTH-001 role-grant child. AUTH-MAIL-002A [#145](https://github.com/Run-MPRC/Run-MPRC.github.io/issues/145) merged the account-creation/request split as `46557c7` but is not published. AUTH-MAIL-002B [#153](https://github.com/Run-MPRC/Run-MPRC.github.io/issues/153) merged the truthful My Account resend result and browser cooldown as `23bca8c8`, but protected release run `29252492614` stopped before build and published nothing. AUTH-MAIL-002C1 [#155](https://github.com/Run-MPRC/Run-MPRC.github.io/issues/155) tracks one neutral password-reset request result and repeat-safe browser cooldown. AUTH-MAIL-002C2 is represented by live child [#194](https://github.com/Run-MPRC/Run-MPRC.github.io/issues/194): a verification-only `/auth/action` source route that scrubs capability state, requires an explicit click and provider `VERIFY_EMAIL`, and exposes only fixed non-identifying results. It must not be configured as Firebase's project-wide custom handler while reset-password and recover-email modes remain unsupported. None grants membership, proves provider delivery, reconciles the Firestore verification mirror, or proves Firebase API enumeration protection.
### Scope
diff --git a/GITHUB_ISSUE_SLICES.md b/GITHUB_ISSUE_SLICES.md
index 46691735..b6bdf383 100644
--- a/GITHUB_ISSUE_SLICES.md
+++ b/GITHUB_ISSUE_SLICES.md
@@ -41,7 +41,7 @@ An agent-ready child has one observable outcome, one trust boundary, explicit de
| AUTH-MAIL-002A | source merged through [#145](https://github.com/Run-MPRC/Run-MPRC.github.io/issues/145) / PR #148 as `46557c7`; website/provider/live unproven | Separate account creation from verification-request acceptance/failure without exposing provider details. | Mocked create/accepted/unavailable tests; exact website and provider evidence stay separate. |
| AUTH-MAIL-002B | source/tests merged through [#153](https://github.com/Run-MPRC/Run-MPRC.github.io/issues/153) / PR #154 as `23bca8c8`; A, #118 source; release `29252492614` stopped before build; not published | Make My Account resend report request acceptance or generic unavailability, block rapid repeats, and show the same 60-second browser cooldown after either result. | No-network double-click, timer/retry, remount, UID-change, redaction, and accessibility tests; website/provider/live states remain separate. |
| AUTH-MAIL-002C1 | source/tests tracked in live [#155](https://github.com/Run-MPRC/Run-MPRC.github.io/issues/155); A/B; not published | Give every password-reset provider outcome one byte-equivalent neutral result, immediate repeat protection, and the same 60-second browser wait. Do not claim direct-API enumeration protection. | No-network success/failure equivalence, hostile canary, double-click, deadline/retry, unmount/remount, route/input, and accessibility tests; private provider-policy evidence stays #119. |
-| AUTH-MAIL-002C2 | proposed; C1 | Add generic expired, used, already-complete, wrong-account, malformed, offline, and provider-unavailable states for incoming Firebase Auth action-code links without leaking codes or account state. | No-network state/collision/navigation/accessibility tests with fixed public recovery outcomes and callback-token redaction. |
+| AUTH-MAIL-002C2 | source/tests tracked in live [#194](https://github.com/Run-MPRC/Run-MPRC.github.io/issues/194); C1; provider not configured | Add a verification-only `/auth/action` route that scrubs one bounded capability, makes no load-time mutation, requires checked `VERIFY_EMAIL`, blocks a different signed-in account, and returns fixed already-complete/unusable/temporary states without leaking codes or account details. Do not configure it as Firebase's global handler until every enabled mode is safe. | No-network check/apply/order/race/offline/wrong-account/navigation/accessibility tests; Sentry/App Check startup suppression, Pages restoration, hostile-parameter redaction, and explicit provider/live stop condition. |
| OAUTH-001A | proposed; represented within canonical live [#88](https://github.com/Run-MPRC/Run-MPRC.github.io/issues/88); SEC-001, AUTH-003B | Implement server-only, transaction/version-safe Strava token refresh so rotated refresh tokens cannot be lost. Do not publish a duplicate; claim/split only through #88. | Concurrent refresh/retry/conflict emulator tests. |
| OAUTH-001B | owner_action; represented within canonical live [#88](https://github.com/Run-MPRC/Run-MPRC.github.io/issues/88); OAUTH-001A | Minimize scopes, implement disconnect/provider revocation, record encryption decision, and add access/refresh/revoke audit. Do not publish a duplicate; claim/split only through #88. | Test-provider revoke/refresh evidence and approved threat decision. |
| OAUTH-001C | proposed; represented within canonical live [#88](https://github.com/Run-MPRC/Run-MPRC.github.io/issues/88); #99 | Create a server-safe Strava callback handoff that keeps `code`/`state` out of third parties while allowing App Check-protected `stravaExchangeCode`; retain state verification and server-only token exchange. Do not publish a duplicate; claim/split only through #88. | Encoded/case/trailing callback tests, wrong/replayed state denial, missing/invalid/valid App Check tests, and test-provider-only exchange evidence. |
diff --git a/OPERATIONS_RUNBOOK.md b/OPERATIONS_RUNBOOK.md
index cf91f047..6ff81a55 100644
--- a/OPERATIONS_RUNBOOK.md
+++ b/OPERATIONS_RUNBOOK.md
@@ -609,6 +609,10 @@ AUTH-MAIL-002B [#153](https://github.com/Run-MPRC/Run-MPRC.github.io/issues/153)
AUTH-MAIL-002C1 [#155](https://github.com/Run-MPRC/Run-MPRC.github.io/issues/155) tracks the password-reset request source state machine: idle, requesting, finished, and retry-ready. Provider success and failure enter the same `finished` DOM, copy, accessibility state, and 60-second deadline. The browser never says sent, delivered, accepted, failed, or whether an account exists. Missing local input makes no request. A synchronous ref blocks duplicate activation; unmount discards a late result and clears the timer. Remounting may reset this usability guard.
+AUTH-MAIL-002C2 [#194](https://github.com/Run-MPRC/Run-MPRC.github.io/issues/194) tracks the verification-only `/auth/action` source state machine: ready, checking, verified, already complete when provider metadata plus the current session prove it, different signed-in account, unusable, and temporarily unavailable. The query and fragment are removed from native and router locations before an action-code check or apply. Opening the page starts no action-code check/apply and changes no account; the member must choose one large **Verify email** button. The service checks the code, requires provider operation `VERIFY_EMAIL`, compares any signed-in account without returning either address, and then applies once. Invalid, expired, used, disabled, deleted, malformed, wrong-environment, and wrong-operation links share one fixed result because Firebase cannot reliably distinguish all of them. Offline, throttled, network, and unknown errors share one temporary result. No result changes membership, roles, claims, Firestore, Functions, or mail delivery.
+
+This route is **SOURCE ONLY, NOT LIVE**. Firebase configures one custom action handler for verification, password reset, and email recovery. The verification-only route must not become the project-wide handler. [#119](https://github.com/Run-MPRC/Run-MPRC.github.io/issues/119) must retain/prove the default multi-mode handler or wait for separately reviewed reset and recovery coverage before provider cutover.
+
The source and tests do not change Firebase Auth, email templates, DNS, sender branding, account-enumeration protection, or mailbox/provider settings. Tests use a mocked request and no network. Identical browser output does not prove the direct Firebase API hides account state; the identity owner must record that provider setting privately under [#119](https://github.com/Run-MPRC/Run-MPRC.github.io/issues/119). Do not test these flows with a production mailbox or a real member.
Release and evidence order:
@@ -621,7 +625,7 @@ Release and evidence order:
6. Use an isolated staging Firebase project and approved safe email sink before any end-to-end test.
7. Record request acceptance separately from message delivery, Spam placement, and sender configuration.
8. Keep provider/DNS/template work under [#119](https://github.com/Run-MPRC/Run-MPRC.github.io/issues/119).
-9. Keep password reset request work in AUTH-MAIL-002C1 [#155](https://github.com/Run-MPRC/Run-MPRC.github.io/issues/155) and incoming action-code work in the future AUTH-MAIL-002C2 under parent [#120](https://github.com/Run-MPRC/Run-MPRC.github.io/issues/120). #153/#155 change browser results and usability timers only; they do not prove provider delivery, enumeration protection, or a durable rate limit.
+9. Keep password reset request work in AUTH-MAIL-002C1 [#155](https://github.com/Run-MPRC/Run-MPRC.github.io/issues/155) and verification action-code source work in AUTH-MAIL-002C2 [#194](https://github.com/Run-MPRC/Run-MPRC.github.io/issues/194) under parent [#120](https://github.com/Run-MPRC/Run-MPRC.github.io/issues/120). #153/#155 change browser results and usability timers only. #194 handles only `verifyEmail`; it is not safe as the global custom handler. None proves provider delivery, enumeration protection, or a durable rate limit.
Rollback is a reviewed frontend revert or safe roll-forward. Do not delete or recreate an Auth account because the verification request failed.
@@ -651,4 +655,6 @@ For AUTH-MAIL-002B [#153](https://github.com/Run-MPRC/Run-MPRC.github.io/issues/
For AUTH-MAIL-002C1 [#155](https://github.com/Run-MPRC/Run-MPRC.github.io/issues/155), **source/tests** means mocked reset success/failure produce byte-equivalent neutral public output, immediate repeat protection, deadline/retry/unmount tests, return-state preservation, hostile-canary redaction, and accessible controls; **merge** is a separate `main` SHA; **website publication** and **`runmprc.com` revision verification** are separate; **Firebase/provider configuration** is unchanged; and **delivery plus API enumeration protection** remain unverified. A neutral page does not prove provider policy or direct-API behavior.
+For AUTH-MAIL-002C2 [#194](https://github.com/Run-MPRC/Run-MPRC.github.io/issues/194), **source/tests** means a no-network route, immediate history scrub, explicit-click guard, provider-operation check, fixed non-identifying results, wrong-account/no-apply handling, callback startup suppression, Pages restoration, hostile-parameter redaction, and keyboard/focus/target-size checks; **merge** is a separate `main` SHA; **website publication** and **`runmprc.com` revision verification** are separate; **Firebase template/action-domain configuration** is unchanged; and **link reachability, delivery, every non-verification action mode, provider behavior, the Firestore verification mirror, and live behavior** remain unverified. Never configure a project-wide custom handler from this source result alone.
+
Provider configuration, production secrets, Netlify publication, Firebase deployment, and live behavior always require separate dated evidence. Local tests and a green GitHub summary do not prove them.
diff --git a/SECURITY.md b/SECURITY.md
index a7e371b7..c50481f6 100644
--- a/SECURITY.md
+++ b/SECURITY.md
@@ -116,7 +116,7 @@ These entries are implementation evidence, not a production risk-acceptance deci
| Part of RISK-035 | The Jest environment is repaired, #124 adds the complete frontend suite as a named blocking hosted step, and #126 adds the standalone SPA callback suite. CI-001B3 [#167](https://github.com/Run-MPRC/Run-MPRC.github.io/issues/167) merged the lockfile-bound Node 20/Java 17 `Commerce command journal emulator` job using exact demo project `demo-pay002b2-test`, Firestore only, and explicit opt-in. CI-001B4 [#186](https://github.com/Run-MPRC/Run-MPRC.github.io/issues/186) merged through [PR #189](https://github.com/Run-MPRC/Run-MPRC.github.io/pull/189) as `bec7d5e365eacb418563a172029f241f660d9768`, adding the no-new-dependency exact-record checker over 102 frontend files, named blocking lint step, and clean-checkout step. Exact PR run `29291402007` and post-merge run `29291515653` passed every GitHub job. | The reviewable lint baseline deliberately records 125 configured errors and 7 warnings as individual file/line/column/rule/message records; it is not a clean-lint claim. Any added, removed, moved, or changed record requires review, every fatal parser/configuration failure rejects with a fixed diagnostic, and the baseline must not be regenerated just to pass. The clean check covers tracked and non-ignored untracked files; ignored dependency/build outputs are outside this proof. Hosted source checks do not configure branch protection, deploy Firebase, or prove Stripe/live behavior. #169, #173, and #182 expand the focused commerce suite; configure/prove branch-required checks and add the broader TEST-001 suite. |
| RISK-026, RISK-036 | #135 merged through PR #138 as `9eafab1217aff7058c42240aaba72d7b93f8ed24`, replacing automatic frontend-first/fail-open GitHub deployment with a tested manual exact-current-commit gate. Post-merge staging and synthetic production probes failed closed before authentication or mutation, and published neither Firebase nor Pages. | #133 must configure protected environments and least-privilege OIDC/WIF; #136 must prove staged/target deployment and clearing/readback of the existing Pages `runmprc.com` claim; a protected WEB-001 child must establish Netlify publication and rollback. No Firebase, Pages, live-host, or provider-setting change is proven by source/static tests alone. |
| RISK-039 | The #118 source slice uses an empty authenticated callable request, bounded Firebase Auth identity fields, one transactional create-only helper shared with signup, constant responses, generic failures, and a UI that hides Edit until setup and the Rules-protected read succeed. Signup and recovery never change custom claims; browser profile creation remains denied. | Source review/merge is not deployment. Under #105, deploy the exact #100 Rules plus both `createMemberOnSignUp` and `ensureMemberProfile` before the website, prove App Check policy, use a synthetic staged account, verify rollback, then record website, Function, Rules, and live behavior separately. Never repair a real profile manually. |
-| RISK-040 | AUTH-MAIL-002A [#145](https://github.com/Run-MPRC/Run-MPRC.github.io/issues/145) merged as `46557c7`: account creation returns `accepted` or `unavailable` without exposing provider details. AUTH-MAIL-002B [#153](https://github.com/Run-MPRC/Run-MPRC.github.io/issues/153) merged as `23bca8c8`: My Account makes no false “sent” claim, blocks rapid repeats, and applies the same 60-second browser cooldown after either outcome. Its protected release run `29252492614` stopped before build because the required public App Check key was absent, so neither frontend revision is published. AUTH-MAIL-002C1 [#155](https://github.com/Run-MPRC/Run-MPRC.github.io/issues/155) tracks one byte-equivalent password-reset result after provider success or failure, an immediate repeat guard, and the same browser wait. Synthetic tests use mocked calls and canary values only. | Publish and verify each exact frontend revision separately. #118 profile source is merged but live behavior is unproven. Keep delivery, Spam, DNS, templates, and private Firebase email-enumeration-protection readback under #119. Identical browser copy does not prove the direct Firebase API suppresses account-state differences. An accepted verification request does not prove delivery, and browser cooldowns can reset; they are not server abuse protection. Incoming action-code recovery remains AUTH-MAIL-002C2 under #120. |
+| RISK-040 | AUTH-MAIL-002A [#145](https://github.com/Run-MPRC/Run-MPRC.github.io/issues/145) merged as `46557c7`: account creation returns `accepted` or `unavailable` without exposing provider details. AUTH-MAIL-002B [#153](https://github.com/Run-MPRC/Run-MPRC.github.io/issues/153) merged as `23bca8c8`: My Account makes no false “sent” claim, blocks rapid repeats, and applies the same 60-second browser cooldown after either outcome. Its protected release run `29252492614` stopped before build because the required public App Check key was absent, so neither frontend revision is published. AUTH-MAIL-002C1 [#155](https://github.com/Run-MPRC/Run-MPRC.github.io/issues/155) tracks one byte-equivalent password-reset request result after provider success or failure. AUTH-MAIL-002C2 [#194](https://github.com/Run-MPRC/Run-MPRC.github.io/issues/194) tracks the source-only `/auth/action` verification path: the initial capability suppresses Sentry/App Check, the page removes native and router query/fragment state, a scanner-style page load makes no action-code check/apply and no account mutation, and one deliberate action requires provider `VERIFY_EMAIL` before apply. Fixed results expose no email, code, raw provider error, account identity, or provider-directed navigation. Synthetic tests use mocks and canary values only. | Publish and verify each exact frontend revision separately. The existing #99 Pages bridge briefly uses tab-local session storage for the return route and deletes it before React; a failed root load can leave it until tab close, while direct-rewrite hosting avoids that residual. #194 adds no storage write. #118 profile source is merged but live behavior is unproven. Keep delivery, Spam, DNS, templates, provider handler choice, and private Firebase email-enumeration-protection readback under #119. Firebase uses one custom handler for verification, password reset, and email recovery; never point its global action URL at the verification-only #194 route until every enabled mode is safely handled, or keep the default multi-mode handler. Accepted requests do not prove delivery, browser cooldowns are not abuse controls, Auth verification does not grant membership, and the Firestore verification mirror remains unfinished AUTH-001 work. |
## 5. Threat model
diff --git a/SYSTEM_DESIGN.md b/SYSTEM_DESIGN.md
index 7d293660..9cda5bef 100644
--- a/SYSTEM_DESIGN.md
+++ b/SYSTEM_DESIGN.md
@@ -130,6 +130,29 @@ flowchart LR
Text alternative: the 404 page temporarily carries the complete return route to the root page; an early referrer policy keeps the path/query/fragment out of subresource request headers, and the root page deletes the temporary value before accepting only a same-origin route. App Check, Analytics, and Sentry stay off on that initial capability-bearing callback. The bridge does not prove payment, OAuth state, or identity. Server/provider verification still decides the result.
+### Firebase Auth action link — source only, not live
+
+```mermaid
+flowchart LR
+ Mail["Private Firebase email link"] --> Bridge["Same-origin Pages handoff"]
+ Bridge --> Route["/auth/action captures one code in memory"]
+ Route --> Scrub["Remove query and fragment from browser history"]
+ Scrub --> Wait["Wait for the member to choose Verify email"]
+ Scanner["Mail scanner opens the page"] --> Wait
+ Wait --> Check{"Provider says VERIFY_EMAIL?"}
+ Check -- "No" --> Stop["Fixed unusable result; no account change"]
+ Check -- "Yes" --> Match{"Signed-in account is safe to continue?"}
+ Match -- "No" --> Stop
+ Match -- "Yes or signed out" --> Apply["Apply the one-time code"]
+ Apply --> Result["Fixed public result; no email or code shown"]
+```
+
+Text alternative: after the website hands off the action route, the app keeps one code only in component memory, removes it from both the visible address and router location, makes no account change when a scanner merely opens the page, and applies it only after a person chooses the button and Firebase confirms the verification operation. The result never grants membership or writes a member profile.
+
+AUTH-MAIL-002C2 [#194](https://github.com/Run-MPRC/Run-MPRC.github.io/issues/194) owns this verification-only source route. It ignores query-provided API keys and continuation URLs, suppresses Sentry and App Check startup while the initial capability is present, and returns only small non-identifying states. Its fixed `/account` exit performs a full clean-page load so App Check can start only after the private query is gone. Firebase configures one custom handler for verification, password reset, and email recovery. Therefore [#119](https://github.com/Run-MPRC/Run-MPRC.github.io/issues/119) must not point the project-wide action URL at this partial route. Keep Firebase's default multi-mode handler or wait for separately reviewed coverage of every enabled mode. Website publication, provider configuration, action-link reachability, and the Firestore verification mirror remain unproven.
+
+The direct-rewrite hosting path does not need browser storage. The existing GitHub Pages fallback from #99 briefly places the complete return route in tab-local `sessionStorage`, then its first root-page script reads and deletes that value before React starts. If the root page never loads, the value can remain until the tab closes. #194 accepts that already-merged residual only for Pages compatibility; it does not call this “memory only.” A future direct SPA rewrite should remove the bridge. The component itself never writes the code to storage or router state.
+
## 4. Target deployment topology
The target keeps React, Firebase, and Stripe, but places stronger boundaries around them. Migrating the frontend from GitHub Pages to Firebase Hosting is recommended before live commerce because it supports controlled SPA rewrites, preview channels, and security headers. That hosting migration is not required to design or test the backend, and must be executed as its own issue.
diff --git a/docs/officers/EMERGENCY_AND_RECOVERY.md b/docs/officers/EMERGENCY_AND_RECOVERY.md
index c6fa98cb..5fbf2c08 100644
--- a/docs/officers/EMERGENCY_AND_RECOVERY.md
+++ b/docs/officers/EMERGENCY_AND_RECOVERY.md
@@ -29,6 +29,7 @@
| Site unavailable or old after a merge | Ask the platform owner to check Netlify and GitHub Pages separately; do not change DNS. |
| Login or member access is wrong | Stop role requests and contact the identity/platform owners. |
| Account exists but a verification email request is unavailable | Ask the member to stop resending. Record only the time, route path `/login` or `/account`, and plain status. Omit all query/fragment text. Do not ask for the email address, password, code, action link, mailbox access, or screenshot. |
+| Verification link is unusable or temporarily unavailable | Ask the member to stop after one deliberate check and one retry. Record only the time, clean route `/auth/action`, and plain result. Never copy anything after `?` or `#`, and never ask for the link, code, email address, or screenshot. |
| Password reset does not lead to a private message | Ask the member to stop after one displayed wait and one retry. Record only the time, route path `/login`, and plain status. Do not ask which address they entered or whether an account exists. |
| Profile Save says permission is missing | Ask the member to stop, sign out, and wait. Open a new redacted incident through [Request a change](./REQUEST_A_CHANGE.md). Do not edit the database, delete the login, recreate the account, or grant a role. |
| Private member information is visible | Contact platform and privacy owners immediately; they choose containment through an approved service procedure. Do not change permissions yourself. |
@@ -114,6 +115,53 @@ After the exact #118 and #153 live proofs exist:
**Escalation:** membership lead plus identity/platform owner; communications owner for delivery or Spam.
+### Verification link page — SOURCE ONLY, NOT LIVE
+
+**Status:** [#194](https://github.com/Run-MPRC/Run-MPRC.github.io/issues/194) is a verification-only source change. It is not a live procedure until the exact website revision is published, `runmprc.com` is verified, and [#119](https://github.com/Run-MPRC/Run-MPRC.github.io/issues/119) proves a safe Firebase handler choice. Firebase uses one handler for verification, password reset, and email recovery. The partial route must not be enabled globally.
+
+**Purpose:** let a member check one private verification link without exposing its code or letting an automated mail scanner change the account.
+
+**Approver:** identity/platform owner plus membership lead. The communications owner and a second Firebase owner approve the provider/template decision.
+
+**Prerequisites:** exact #194 merge and website proof; protected release proof; private #119 proof that reset and recovery links still work; an isolated Firebase project; a made-up account and safe email sink. A source test, preview, or real member link is not enough.
+
+```mermaid
+flowchart LR
+ Open["Member opens private link"] --> Clean["Page removes code from the address"]
+ Clean --> Choose["Member chooses Verify email once"]
+ Scanner["Mail scanner only opens page"] --> NoChange["No account change"]
+ Choose --> Result{"Plain result"}
+ Result -- "Verified or already complete" --> Account["Continue to My Account"]
+ Result -- "Different account" --> SignOut["Sign out; reopen original link"]
+ Result -- "Cannot be used" --> New["Request one new email"]
+ Result -- "Temporary" --> Retry["Check connection; retry once"]
+```
+
+In words: opening the page removes the private code but changes nothing; the member chooses one button, then follows one plain result without sharing the link or account details.
+
+Officer steps after every prerequisite has proof:
+
+1. Ask the member to open the private message on their own device.
+2. Do not ask them to forward, copy, or show the link.
+3. Ask them to confirm the address ends with `/auth/action` and contains no `?` or `#` before choosing anything.
+4. Ask them to choose **Verify email** once.
+5. If the page says verified or already complete, ask them to continue to My Account.
+6. If it says a different account is signed in, ask them to open My Account, sign out, and reopen the original message.
+7. If the link cannot be used, ask them to request one new verification email from My Account.
+8. If verification is temporarily unavailable, ask them to check the connection and use the one retry.
+9. If the second attempt is not clear, stop and open a redacted incident.
+10. Record only the time, clean route `/auth/action`, and exact plain result.
+
+**Expected result:** page load makes no action-code check and changes no account; the private query and fragment disappear; one deliberate action gives a fixed result; no address, code, provider error, role, membership, payment, or profile value appears or changes.
+
+**Stop conditions:** the address still contains `?` or `#`; anyone asks for a link/code/address/screenshot; the page changes an account before the button; a non-verification action opens here; more than one retry; a real-member/provider test; or missing provider/live proof.
+
+**Success proof:** exact #194 pull request and merge; green synthetic no-network tests; dated keyboard and screen-reader check; exact website publication and `runmprc.com` revision; private #119 staging proof for verification, reset, recovery, reuse, expiration, and rollback. Keep each proof separate.
+
+**Undo:** publish one reviewed frontend revert or safe roll-forward. The Firebase owners separately restore the previously proven default/multi-mode handler. Never delete an account or edit verification state by hand.
+
+**Escalation:** identity/platform owner plus membership lead; add communications for sender/delivery, security for an exposed code or wrong mutation, and the provider owner for handler/template failure.
+
### Password reset request — NOT AVAILABLE YET
**Status:** [#155](https://github.com/Run-MPRC/Run-MPRC.github.io/issues/155) is the tracked source change. It is not a live procedure until the exact revision is merged, published, and verified on `runmprc.com`. The protected release is also blocked on approved App Check and deployment configuration. Sender, Spam, delivery, and Firebase account-enumeration settings remain unverified owner work under [#119](https://github.com/Run-MPRC/Run-MPRC.github.io/issues/119).
diff --git a/docs/officers/EVENTS_SHOP_MEMBERS.md b/docs/officers/EVENTS_SHOP_MEMBERS.md
index 582c0b66..0da0eedf 100644
--- a/docs/officers/EVENTS_SHOP_MEMBERS.md
+++ b/docs/officers/EVENTS_SHOP_MEMBERS.md
@@ -111,6 +111,8 @@ Officer steps after live proof:
Password reset is a separate recovery path. [#155](https://github.com/Run-MPRC/Run-MPRC.github.io/issues/155) tracks one neutral result and one browser wait; it must never reuse the verification flow's `accepted` or `unavailable` account-specific wording. Until its exact website and private provider proofs exist, use only [Password reset request — NOT AVAILABLE YET](./EMERGENCY_AND_RECOVERY.md#password-reset-request--not-available-yet). Do not ask a member which address they entered.
+The incoming verification link is another separate step. [#194](https://github.com/Run-MPRC/Run-MPRC.github.io/issues/194) tracks a deliberate-click `/auth/action` source route that removes the private code from the address and never grants membership. It is **NOT LIVE** and must not become Firebase's global handler while reset-password and email-recovery modes are unsupported. After every provider and website prerequisite is proven, use only [Verification link page — SOURCE ONLY, NOT LIVE](./EMERGENCY_AND_RECOVERY.md#verification-link-page--source-only-not-live). Officers never open, copy, or request the member's link or code.
+
## Checkout adjustment guard — SOURCE ONLY, NOT LIVE
**Purpose:** prevent an unknown discount, tax, or shipping charge from being treated as a valid payment.
diff --git a/docs/officers/SYSTEM_MAPS.md b/docs/officers/SYSTEM_MAPS.md
index d9523624..66fdfcf5 100644
--- a/docs/officers/SYSTEM_MAPS.md
+++ b/docs/officers/SYSTEM_MAPS.md
@@ -18,6 +18,7 @@ flowchart TD
Site --> Committee["Committee"]
Site --> Contact["Contact Us"]
Site --> Account["Login and Account"]
+ Account --> AuthAction["Verification link page — source only, not live"]
Site --> Legal["Terms and Privacy"]
Account --> Admin["Restricted /admin pages — direct link"]
```
@@ -102,6 +103,23 @@ In words: a signed-in member gets their existing profile unchanged or one new pe
Issue [#118](https://github.com/Run-MPRC/Run-MPRC.github.io/issues/118) owns this repair, and [#178](https://github.com/Run-MPRC/Run-MPRC.github.io/issues/178) owns the temporary phone-collection pause. Source code and local tests do not make either live. The website, server Function, database permissions, and made-up live check must each be proven separately.
+## Verification email action — SOURCE ONLY, NOT LIVE
+
+```mermaid
+flowchart LR
+ Email["Private Firebase email"] --> Page["/auth/action"]
+ Page --> Remove["Remove query and fragment"]
+ Remove --> Button["Wait for Verify email button"]
+ Scanner["Mail scanner opens page"] --> NoChange["No account change"]
+ Button --> Firebase["Firebase confirms verification operation"]
+ Firebase --> Plain["Plain result; no email or code"]
+ Plain -. "does not grant" .-> Access["Membership, payment, discounts, or admin"]
+```
+
+In words: the source page removes the private code, waits for the member to choose one button, and shows a plain result; simply opening the page does nothing and verification never grants club access.
+
+Issue [#194](https://github.com/Run-MPRC/Run-MPRC.github.io/issues/194) owns only the verification path. Firebase's single custom handler also receives password-reset and email-recovery links, so [#119](https://github.com/Run-MPRC/Run-MPRC.github.io/issues/119) must keep the default handler or wait for safe coverage of every mode before enabling this route. Website, provider, and live behavior remain separate proof.
+
## Emergency decision
```mermaid
diff --git a/src/App.jsx b/src/App.jsx
index fa852948..7be34ed0 100644
--- a/src/App.jsx
+++ b/src/App.jsx
@@ -12,6 +12,7 @@ import ErrorBoundary from './components/ErrorBoundary';
import Activities from './pages/activities/Activities';
import ServiceLocatorProvider from './services/ServiceLocatorProvider';
import LoginForm from './pages/login/LoginForm';
+import VerifyEmailAction from './pages/login/VerifyEmailAction';
import Events from './pages/events/Events';
import EventDetail from './pages/events/EventDetail';
import EventRegister from './pages/events/EventRegister';
@@ -86,6 +87,7 @@ function App() {
} />
} />
} />
+ } />
} />
} />
} />
diff --git a/src/App.test.jsx b/src/App.test.jsx
index aa91c3ff..4ed337e8 100644
--- a/src/App.test.jsx
+++ b/src/App.test.jsx
@@ -87,3 +87,25 @@ test('navigates from the wildcard route to home with future behavior enabled', a
})).toBeInTheDocument();
expect(window.location.pathname).toBe('/');
});
+
+test('renders the Auth action route and removes its query and fragment before use', () => {
+ window.history.pushState(
+ {},
+ '',
+ '/auth/action?mode=verifyEmail&oobCode=synthetic-action-code#private-fragment',
+ );
+
+ render();
+
+ expect(screen.getByRole('heading', {
+ level: 1,
+ name: /email verification/i,
+ })).toBeInTheDocument();
+ expect(screen.queryByRole('heading', {
+ level: 2,
+ name: /page not found/i,
+ })).not.toBeInTheDocument();
+ expect(window.location.pathname).toBe('/auth/action');
+ expect(window.location.search).toBe('');
+ expect(window.location.hash).toBe('');
+});
diff --git a/src/pages/login/VerifyEmailAction.css b/src/pages/login/VerifyEmailAction.css
new file mode 100644
index 00000000..105e8dbb
--- /dev/null
+++ b/src/pages/login/VerifyEmailAction.css
@@ -0,0 +1,67 @@
+.verify-email-panel {
+ background: #fff;
+ border: 1px solid #d1d5db;
+ border-radius: 0.5rem;
+ box-shadow: 0 1px 3px rgb(0 0 0 / 10%);
+ display: flex;
+ flex-direction: column;
+ gap: 1rem;
+ max-width: 30rem;
+ padding: 1.5rem;
+ width: 100%;
+}
+
+.verify-email-action,
+.verify-email-link {
+ align-items: center;
+ border-radius: 0.375rem;
+ display: inline-flex;
+ font-weight: 600;
+ justify-content: center;
+ padding: 0.75rem 1rem;
+}
+
+.verify-email-action {
+ background: #1d4ed8;
+ color: #fff;
+ min-height: 3rem;
+ width: 100%;
+}
+
+.verify-email-action:disabled {
+ background: #9ca3af;
+ cursor: not-allowed;
+}
+
+.verify-email-action:focus-visible {
+ outline: 3px solid #f59e0b;
+ outline-offset: 3px;
+}
+
+.verify-email-link {
+ background: #eff6ff;
+ color: #1d4ed8;
+ margin-top: 0.75rem;
+ min-height: 3rem;
+ text-decoration: underline;
+}
+
+.verify-email-link:focus-visible {
+ outline: 3px solid #f59e0b;
+ outline-offset: 3px;
+}
+
+.verify-email-result:focus {
+ outline: none;
+}
+
+.verify-email-result__title {
+ font-size: 1.125rem;
+ font-weight: 700;
+ margin-bottom: 0.5rem;
+}
+
+.verify-email-safety-note {
+ color: #4b5563;
+ font-size: 0.875rem;
+}
diff --git a/src/pages/login/VerifyEmailAction.test.tsx b/src/pages/login/VerifyEmailAction.test.tsx
new file mode 100644
index 00000000..6bee6416
--- /dev/null
+++ b/src/pages/login/VerifyEmailAction.test.tsx
@@ -0,0 +1,375 @@
+/* eslint-env jest */
+import React from 'react';
+import { readFileSync } from 'fs';
+import { join } from 'path';
+import {
+ act, fireEvent, render, screen,
+} from '@testing-library/react';
+import {
+ BrowserRouter, MemoryRouter, Route, Routes, useLocation, useNavigate,
+} from 'react-router-dom';
+import VerifyEmailAction from './VerifyEmailAction';
+import { useServiceLocator } from '../../services/ServiceLocatorContext';
+
+jest.mock('../../services/ServiceLocatorContext', () => ({
+ useServiceLocator: jest.fn(),
+}));
+
+jest.mock('../../components/Header', () => function Header({ title }: { title: string }) {
+ return