diff --git a/.github/lint-baseline.json b/.github/lint-baseline.json index ca82b437..f6a143fa 100644 --- a/.github/lint-baseline.json +++ b/.github/lint-baseline.json @@ -10,11 +10,11 @@ "targets": [ "src" ], - "scannedFileCount": 119, + "scannedFileCount": 120, "scannedFilesByExtension": { ".js": 12, ".jsx": 35, - ".ts": 42, + ".ts": 43, ".tsx": 30 }, "errorCount": 113, @@ -31,7 +31,7 @@ ".ts": 0, ".tsx": 6 }, - "fingerprint": "sha256:bda71336dd9fbb242334c9aaf7f2be6d58bc55814540f730d90fe4809ec7e829", + "fingerprint": "sha256:b62532df877e1a1599273d0bdbf9876e75dae8be6e60ecadce601f854c283b6a", "scannedFiles": [ "src/App.jsx", "src/App.test.jsx", @@ -113,6 +113,7 @@ "src/services/account/accountService.test.ts", "src/services/account/accountService.ts", "src/services/account/adminMembersService.ts", + "src/services/account/memberDirectoryAvailability.ts", "src/services/account/memberDirectorySearchService.test.ts", "src/services/account/memberDirectorySearchService.ts", "src/services/account/memberDirectoryService.test.ts", @@ -247,9 +248,9 @@ }, { "file": "src/pages/admin/AdminHome.tsx", - "line": 196, + "line": 197, "column": 1, - "endLine": 196, + "endLine": 197, "endColumn": 114, "severity": 2, "ruleId": "max-len", diff --git a/IMPLEMENTATION_PLAN.md b/IMPLEMENTATION_PLAN.md index b1ee69c2..c2cc8cca 100644 --- a/IMPLEMENTATION_PLAN.md +++ b/IMPLEMENTATION_PLAN.md @@ -169,7 +169,7 @@ Exit gate: - Payment SLOs, structured redacted logs, and actionable alerts are live. - Backup restoration into isolated infrastructure succeeds and is documented. -**Current optional-directory boundary:** Parent [#504](https://github.com/Run-MPRC/Run-MPRC.github.io/issues/504) defines a private, opt-in officer people finder as name search with voluntary thumbnails—not facial recognition. MEMBERS-DIRECTORY-001A [#505](https://github.com/Run-MPRC/Run-MPRC.github.io/issues/505) owns the signed-in person's server-only processed thumbnail and independent default-off preference. MEMBERS-DIRECTORY-001B [#506](https://github.com/Run-MPRC/Run-MPRC.github.io/issues/506) adds source for the minimum server-only projection, current-state reconciler, bounded verified-admin name-prefix callable, query-free audit, and separate `/admin/member-directory` gallery. It returns at most 24 current opted-in display-name/optional-thumbnail cards and has no image query, facial recognition, cursor, total, export, or membership authority. [#507](https://github.com/Run-MPRC/Run-MPRC.github.io/issues/507) still owns #110 policy completion, #133 protected authority, required backend/index deployment and readback, website publication, and live proof. Do not reuse the current browser-side full-account filter or describe #505/#506 source as an available directory. +**Current optional-directory boundary:** Parent [#504](https://github.com/Run-MPRC/Run-MPRC.github.io/issues/504) defines a private, opt-in officer people finder as name search with voluntary thumbnails—not facial recognition. MEMBERS-DIRECTORY-001A [#505](https://github.com/Run-MPRC/Run-MPRC.github.io/issues/505) owns the signed-in person's server-only processed thumbnail and independent default-off preference. MEMBERS-DIRECTORY-001B [#506](https://github.com/Run-MPRC/Run-MPRC.github.io/issues/506) adds source for the minimum server-only projection, current-state reconciler, bounded verified-admin name-prefix callable, query-free audit, and separate `/admin/member-directory` gallery. It returns at most 24 current opted-in display-name/optional-thumbnail cards and has no image query, facial recognition, cursor, total, export, or membership authority. MEMBERS-DIRECTORY-001D [#621](https://github.com/Run-MPRC/Run-MPRC.github.io/issues/621) makes the shipped frontend boundary default unavailable: My Account and the guarded officer route show only visibly described, disabled layouts and perform zero directory reads, uploads, searches, saves, request-ID creation, or service calls. [#507](https://github.com/Run-MPRC/Run-MPRC.github.io/issues/507) still owns #110 policy completion, scoped authorization, #133 protected authority, isolated staging, required backend/index deployment and readback, and a later separately reviewed source flip before connected website publication and live proof. Do not reuse the current browser-side full-account filter or describe a disabled preview or #505/#506 source as an available directory. ### Phase 5 — End-to-end qualification diff --git a/OFFICER_START_HERE.md b/OFFICER_START_HERE.md index 127cf3fb..4e91c2e5 100644 --- a/OFFICER_START_HERE.md +++ b/OFFICER_START_HERE.md @@ -48,6 +48,6 @@ Use the club's approved password manager for access. Share only a public link or As of **2026-08-01**, a merge runs checks but does not start the GitHub release. The protected release is **NOT AVAILABLE YET** until its short-lived cloud identity and named environment approvers are configured under issue #133. Ordinary Git-triggered Netlify production builds are paused. An overbroad #473 web artifact was published and immediately rolled back. The bounded replacement is now live as Netlify deploy `6a6dc9ea588b0c0008036312`: Shop is the static in-person catalog, while Events and Calendar show a fixed retry-later notice instead of a raw provider error. Event records are still unavailable because no Firebase repair was deployed. The temporary release manifest is inactive again. This does not change sign-in, expose protected event offers, add officer editing, or make commerce safe. GitHub Pages still reports `runmprc.com` as its custom domain even though Netlify serves that name; source removal is not provider proof. A green test or workflow does **not** by itself prove that GitHub Pages, `runmprc.com`, Firebase, or that domain setting changed. -The optional profile photo and officer **People finder** are also **NOT AVAILABLE YET**. #505/#506 source keeps the choice off by default and limits the source-only officer page to explicit name search with voluntary thumbnails. It is not a membership roster and does not recognize faces. Do not open production Account/Admin pages, upload a real photo, search a real name, or treat a source merge or preview as live proof. Follow the source-only procedure in [Events, shop, members, and money](./docs/officers/EVENTS_SHOP_MEMBERS.md). +The optional profile-photo and officer People-finder functions are still **NOT AVAILABLE YET**. #621 makes the frontend default an inert preview: My Account shows the future photo and separate finder-choice controls disabled, while the People finder stays behind the administrator guard and shows its name field and Search button disabled. The preview reads no saved photo or setting, accepts or uploads no photo, searches no name, and saves nothing; it shows no people or sample results. Officers inspect those protected disabled layouts only in synthetic local artifacts. After publication, signed-out public review proves only the exact revision, normal sign-in and administrator guards, and absence of a member-directory network request. Do not choose a real photo, enter a real name, or treat the preview as a directory. #507 must later prove the privacy, authorization, staging, backend-first deployment, and readback gates before a separate reviewed source change may connect it. Follow the preview and source-review procedure in [Events, shop, members, and money](./docs/officers/EVENTS_SHOP_MEMBERS.md). For the concise handbook, see [OFFICER_HANDBOOK.md](./OFFICER_HANDBOOK.md). The expanded task index is [docs/officers/README.md](./docs/officers/README.md). diff --git a/OPERATIONS_RUNBOOK.md b/OPERATIONS_RUNBOOK.md index e73e6dfd..20b17992 100644 --- a/OPERATIONS_RUNBOOK.md +++ b/OPERATIONS_RUNBOOK.md @@ -267,7 +267,7 @@ node .github/scripts/check-frontend-lint.cjs node --test tests/test-artifact-safety.test.js ``` -The frontend Jest command is a dependable local baseline and hosted CI runs it as the blocking `Run frontend Jest tests` step under [#124](https://github.com/Run-MPRC/Run-MPRC.github.io/issues/124). Hosted CI also runs the separate Node SPA suite as `Run SPA callback safety tests` under [#126](https://github.com/Run-MPRC/Run-MPRC.github.io/issues/126). CI-001B3 [#167](https://github.com/Run-MPRC/Run-MPRC.github.io/issues/167) adds the blocking `Commerce command journal emulator` job. That job uses exact demo project `demo-pay002b2-test`, Firestore only, an explicit opt-in, and the focused transaction suite. 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`. It adds the named blocking `Run non-mutating frontend lint` step using the checker directly, before application tests can change its process environment. Its trusted prefix pins the hosted runner, read-only job permission, credential-free checkout, Node 20 setup, install with lifecycle scripts disabled, lint, and the immediately following clean check. With WEB-SUGGESTIONS-001A [#618](https://github.com/Run-MPRC/Run-MPRC.github.io/issues/618), the checker covers 119 frontend files: 12 JS, 35 JSX, 42 TS, and 30 TSX. The lint process disables the repository-wide `eslint-plugin-only-warn` hook so the original configured severities remain visible. CI-001B4A [#227](https://github.com/Run-MPRC/Run-MPRC.github.io/issues/227) removes one reviewed `arrow-body-style` record, CI-001B4B [#239](https://github.com/Run-MPRC/Run-MPRC.github.io/issues/239) removes one stale `AdminMembers` unknown-rule suppression record, and PAY-004C1 [#359](https://github.com/Run-MPRC/Run-MPRC.github.io/issues/359) removes one prompt warning plus two label-association errors with the unsafe reusable-link controls. Reviewed functional changes in [PR #391](https://github.com/Run-MPRC/Run-MPRC.github.io/pull/391) and [PR #392](https://github.com/Run-MPRC/Run-MPRC.github.io/pull/392) add three TypeScript files and retire four TSX errors. CI-001B4C [#449](https://github.com/Run-MPRC/Run-MPRC.github.io/issues/449) replaces exactly four stale `react-hooks/exhaustive-deps` directives with ordinary comments on the same lines and changes no executable code. MEMBERS-CONTENT-001B [#492](https://github.com/Run-MPRC/Run-MPRC.github.io/issues/492) removes one finding-free dormant JSX file, #505 adds four finding-free TypeScript/TSX files, #506 adds four more finding-free TypeScript/TSX files, and #618 adds one finding-free JSX page, leaving 113 configured errors and 6 warnings. This is accepted debt, not a clean-lint claim. Any finding added, removed, moved, or changed must be understood and the exact record updated in the same reviewed pull request; a fatal parser, configuration, or unexpected process failure always stops with one fixed diagnostic. Never regenerate the baseline only to make a check green. The clean-checkout step compares tracked files with `HEAD` and rejects non-ignored untracked files. Ignored dependency and build paths are outside this proof. The original #186 PR run `29291402007` and post-merge run `29291515653` passed the install, lint, clean, test, workflow-validation, build, Functions, Rules, commerce, and artifact jobs; each later focused issue records its separate source and hosted evidence. These runs prove source execution only: branch protection, production publication, Firebase, Stripe, and provider state remain unchanged. TEST-001A2 [#177](https://github.com/Run-MPRC/Run-MPRC.github.io/issues/177) adds the exact `Test artifact scrubber` job. It deliberately creates bounded synthetic output and proves that the dependency-free scanner accepts reserved examples, rejects the documented recognizable sensitive patterns with fixed messages, ignores source outside an explicit output root, and protects both release job lists. Current CI uploads no test report. Later report-producing work must scan its explicit output before upload. A green scrubber proves only that this deliberate source test ran. It is not a general data-loss-prevention guarantee or proof that an unknown or newly obfuscated sensitive value is absent, that an artifact was uploaded, that all runtime logs are safe, or that Firebase, Stripe, the website, or another provider changed. The manual-release source invariants remain under [#135](https://github.com/Run-MPRC/Run-MPRC.github.io/issues/135). Protected environment/OIDC configuration, actual staged/live deployment, branch-required-check configuration, and controlled Netlify publication remain **NOT AVAILABLE YET** under [#105](https://github.com/Run-MPRC/Run-MPRC.github.io/issues/105), [#133](https://github.com/Run-MPRC/Run-MPRC.github.io/issues/133), [#136](https://github.com/Run-MPRC/Run-MPRC.github.io/issues/136), and WEB-001. +The frontend Jest command is a dependable local baseline and hosted CI runs it as the blocking `Run frontend Jest tests` step under [#124](https://github.com/Run-MPRC/Run-MPRC.github.io/issues/124). Hosted CI also runs the separate Node SPA suite as `Run SPA callback safety tests` under [#126](https://github.com/Run-MPRC/Run-MPRC.github.io/issues/126). CI-001B3 [#167](https://github.com/Run-MPRC/Run-MPRC.github.io/issues/167) adds the blocking `Commerce command journal emulator` job. That job uses exact demo project `demo-pay002b2-test`, Firestore only, an explicit opt-in, and the focused transaction suite. 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`. It adds the named blocking `Run non-mutating frontend lint` step using the checker directly, before application tests can change its process environment. Its trusted prefix pins the hosted runner, read-only job permission, credential-free checkout, Node 20 setup, install with lifecycle scripts disabled, lint, and the immediately following clean check. With WEB-SUGGESTIONS-001A [#618](https://github.com/Run-MPRC/Run-MPRC.github.io/issues/618) and MEMBERS-DIRECTORY-001D [#621](https://github.com/Run-MPRC/Run-MPRC.github.io/issues/621), the checker covers 120 frontend files: 12 JS, 35 JSX, 43 TS, and 30 TSX. The lint process disables the repository-wide `eslint-plugin-only-warn` hook so the original configured severities remain visible. CI-001B4A [#227](https://github.com/Run-MPRC/Run-MPRC.github.io/issues/227) removes one reviewed `arrow-body-style` record, CI-001B4B [#239](https://github.com/Run-MPRC/Run-MPRC.github.io/issues/239) removes one stale `AdminMembers` unknown-rule suppression record, and PAY-004C1 [#359](https://github.com/Run-MPRC/Run-MPRC.github.io/issues/359) removes one prompt warning plus two label-association errors with the unsafe reusable-link controls. Reviewed functional changes in [PR #391](https://github.com/Run-MPRC/Run-MPRC.github.io/pull/391) and [PR #392](https://github.com/Run-MPRC/Run-MPRC.github.io/pull/392) add three TypeScript files and retire four TSX errors. CI-001B4C [#449](https://github.com/Run-MPRC/Run-MPRC.github.io/issues/449) replaces exactly four stale `react-hooks/exhaustive-deps` directives with ordinary comments on the same lines and changes no executable code. MEMBERS-CONTENT-001B [#492](https://github.com/Run-MPRC/Run-MPRC.github.io/issues/492) removes one finding-free dormant JSX file, #505 adds four finding-free TypeScript/TSX files, #506 adds four more finding-free TypeScript/TSX files, #618 adds one finding-free JSX page, and #621 adds one finding-free TypeScript availability module, leaving 113 configured errors and 6 warnings. This is accepted debt, not a clean-lint claim. Any finding added, removed, moved, or changed must be understood and the exact record updated in the same reviewed pull request; a fatal parser, configuration, or unexpected process failure always stops with one fixed diagnostic. Never regenerate the baseline only to make a check green. The clean-checkout step compares tracked files with `HEAD` and rejects non-ignored untracked files. Ignored dependency and build paths are outside this proof. The original #186 PR run `29291402007` and post-merge run `29291515653` passed the install, lint, clean, test, workflow-validation, build, Functions, Rules, commerce, and artifact jobs; each later focused issue records its separate source and hosted evidence. These runs prove source execution only: branch protection, production publication, Firebase, Stripe, and provider state remain unchanged. TEST-001A2 [#177](https://github.com/Run-MPRC/Run-MPRC.github.io/issues/177) adds the exact `Test artifact scrubber` job. It deliberately creates bounded synthetic output and proves that the dependency-free scanner accepts reserved examples, rejects the documented recognizable sensitive patterns with fixed messages, ignores source outside an explicit output root, and protects both release job lists. Current CI uploads no test report. Later report-producing work must scan its explicit output before upload. A green scrubber proves only that this deliberate source test ran. It is not a general data-loss-prevention guarantee or proof that an unknown or newly obfuscated sensitive value is absent, that an artifact was uploaded, that all runtime logs are safe, or that Firebase, Stripe, the website, or another provider changed. The manual-release source invariants remain under [#135](https://github.com/Run-MPRC/Run-MPRC.github.io/issues/135). Protected environment/OIDC configuration, actual staged/live deployment, branch-required-check configuration, and controlled Netlify publication remain **NOT AVAILABLE YET** under [#105](https://github.com/Run-MPRC/Run-MPRC.github.io/issues/105), [#133](https://github.com/Run-MPRC/Run-MPRC.github.io/issues/133), [#136](https://github.com/Run-MPRC/Run-MPRC.github.io/issues/136), and WEB-001. ### CI-001B5 Functions job credential boundary — SOURCE ONLY @@ -958,11 +958,21 @@ Required release order: If the website arrives before `ensureMemberProfile` or the matching Rules, the intended safe result is a temporary-unavailable message with Edit hidden. Roll back the website or safely roll forward the reviewed backend. Never release the callable while the old signup trigger remains: that trigger can overwrite a recovered profile or replace claims. Deploy the two revised Functions together, or stop and roll back the partial backend release. Do not create, delete, overwrite, or re-role a production account in Firebase Console. -### Profile thumbnail and officer people finder — SOURCE ONLY, NOT LIVE +### Profile thumbnail and officer people finder — INERT FRONTEND PREVIEW; BACKEND NOT LIVE -Issue [#505](https://github.com/Run-MPRC/Run-MPRC.github.io/issues/505) adds the private profile-photo/default-off-preference slice of the optional people finder in [#504](https://github.com/Run-MPRC/Run-MPRC.github.io/issues/504). Issue [#506](https://github.com/Run-MPRC/Run-MPRC.github.io/issues/506) adds the minimum server-only projection, current-state reconciler, bounded officer name search, and separate gallery. Do not deploy or publish either slice independently. Neither issue authorizes real photos, real-name searches, or production testing. +Issue [#505](https://github.com/Run-MPRC/Run-MPRC.github.io/issues/505) adds the private profile-photo/default-off-preference source slice of the optional people finder in [#504](https://github.com/Run-MPRC/Run-MPRC.github.io/issues/504). Issue [#506](https://github.com/Run-MPRC/Run-MPRC.github.io/issues/506) adds the minimum server-only projection, current-state reconciler, bounded officer name-search source, and separate gallery. Issue [#621](https://github.com/Run-MPRC/Run-MPRC.github.io/issues/621) keeps that connected implementation behind one source-controlled availability boundary that defaults unavailable. Inspect its protected layouts only through synthetic local artifacts. If the exact #621 frontend is published, anonymous readback is limited to the revision, sign-in and administrator guards, and absence of a public member-directory request. Do not deploy or publish the connected #505/#506 behavior independently. None of these issues authorizes a real photo, real-name search, Firebase deployment, or production-data test. -Source review must prove all of these with generated non-face images and made-up accounts: +#621 frontend-preview review must prove all of these without entering any name or choosing any file: + +1. My Account shows a visible preview notice and the future thumbnail placeholder, file control, and independent finder choice are natively disabled. +2. The default account path does not mount the connected profile component, read a file or saved setting, create a request ID, or initialize or call a member-directory service. +3. `/admin/member-directory` remains behind `AdminGuard`; its name field and Search button are visibly described and natively disabled. +4. The default officer path accepts no name, creates no request ID, initializes no search service, and renders no sample, fake, or result card. +5. Preview copy uses future tense for backend behavior, says a future upload never opts a person in, says a future result is not membership proof, and states that there is no photo query or facial recognition. +6. Focused synthetic tests exercise the preserved connected branches through an explicit test seam; the production source default remains unavailable. +7. The disabled layouts remain readable at 320 pixels and expose useful accessible names, descriptions, status text, keyboard order, and contrast without horizontal overflow. + +The preserved #505 connected-source review must prove all of these with generated non-face images and made-up accounts: 1. Missing preference returns hidden with revision zero. 2. Uploading one valid image creates only a bounded 256-pixel WebP thumbnail and does not turn on discoverability. @@ -975,7 +985,7 @@ Source review must prove all of these with generated non-face images and made-up 9. Every profile callable is declared with native App Check enforcement and fails closed unless its private response can carry explicit no-store headers. 10. If an opted-in person later clears their display name, My Account keeps opt-out available and explains that no officer result may include them until a valid name is restored; the #506 server response enforces that exclusion. -#506 source review must also prove all of these with made-up names and generated non-face images: +The preserved #506 connected-source review must also prove all of these with made-up names and generated non-face images: 1. A browser cannot directly read or write `memberDirectoryEntries`, preferences, photos, search audits, nested records, or rate state. 2. A missing, false, malformed, or later-disabled preference has no result; upload alone creates no searchable entry. @@ -990,7 +1000,7 @@ Source review must prove all of these with generated non-face images and made-up 11. `/admin/members` is visibly labeled as website-account administration: its broad name/email filter does not honor finder consent and is not a roster. It links to the separate People finder instead of being reused. 12. Search permits at most 30 attempts per verified-admin account in one hour. Rate state uses a separate domain-separated account pseudonym and the private TTL-dependent limiter. Prefix digests, entry references, rate keys, and audit identifiers are linkable pseudonyms, not anonymization, and none may enter browser logs, URLs, analytics, or screenshots. -The approved release order remains blocked on [#507](https://github.com/Run-MPRC/Run-MPRC.github.io/issues/507), the #110 privacy decision, protected short-lived deployment authority under #133, and isolated staging. When those dependencies are complete, deploy and verify the exact Rules plus `getMyMemberDirectoryProfile`, `setMyMemberDirectoryVisibility`, `setMyMemberDirectoryPhoto`, `removeMyMemberDirectoryPhoto`, `searchMemberDirectory`, and `syncMemberDirectoryEntryOnMemberWrite` before publishing the dependent website. Deploy and read back any required index with that backend set, and verify the trigger's retry policy. Before initial publication, prove that no older preference rows need projection or run an approved idempotent, dry-run-first current-state repair; retain that owned repair path for schema/normalization upgrades. A skipped or partial backend publishes nothing. Verify default-hidden, upload-without-opt-in, opt-in name search, opt-out absence, name change, stale-entry withholding, and photo removal with synthetic records only. Never test with a real member photo, inspect production profiles, widen Rules, or repair a record manually. +Synthetic local artifacts and tests prove that the protected #621 layouts are disabled and their default branches make zero directory calls. Publishing and anonymously reading back the exact frontend proves only its revision, normal sign-in and administrator guards, and absence of a public member-directory request; it does not require or authorize a Firebase change. The connected release remains blocked on [#507](https://github.com/Run-MPRC/Run-MPRC.github.io/issues/507), the #110 privacy decision, scoped authorization, protected short-lived deployment authority under #133, and isolated staging. When those dependencies are complete, deploy and verify the exact Rules plus `getMyMemberDirectoryProfile`, `setMyMemberDirectoryVisibility`, `setMyMemberDirectoryPhoto`, `removeMyMemberDirectoryPhoto`, `searchMemberDirectory`, and `syncMemberDirectoryEntryOnMemberWrite` while the frontend stays unavailable. Deploy and read back any required index with that backend set, and verify the trigger's retry policy. Before connected publication, prove that no older preference rows need projection or run an approved idempotent, dry-run-first current-state repair; retain that owned repair path for schema/normalization upgrades. Verify default-hidden, upload-without-opt-in, opt-in name search, opt-out absence, name change, stale-entry withholding, and photo removal with synthetic records only. Only after those gates pass may a separate reviewed source change turn frontend availability on and publish the connected website. A skipped or partial backend keeps the preview unavailable. Never test with a real member photo, inspect production profiles, widen Rules, or repair a record manually. This feature performs no face recognition, image-to-person query, embedding, similarity scoring, or biometric template creation. A request for any of those is a stop condition and needs a separate owner/legal/privacy decision. diff --git a/SECURITY.md b/SECURITY.md index 98716425..f89b2a81 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -68,7 +68,7 @@ The findings below describe the repository at the start of the 2026-07-12 assess | RISK-023 | The legacy `functions.config().api.key` endpoint uses a static shared key with no replay control, source restriction, App Check, authenticated operator, or request audit. The API is also scheduled for Firebase decommissioning in March 2027. | Key theft permits bulk role changes and future deployment failure. | Retire it in favor of authenticated admin workflow or narrowly authenticated scheduled import; migrate any remaining config to Secret Manager. | | RISK-024 | OAuth tokens remain plaintext. Repository Rules deny browser access, but protected deployment and live behavior are unproven; least-privilege runtime IAM and an encryption decision remain open. OAUTH-001A1H [#606](https://github.com/Run-MPRC/Run-MPRC.github.io/issues/606) source transactionally rereads the exact access-token, refresh-token, and expiry tuple before persisting a refresh response. A retry rereads the tuple; changed, missing, or malformed state returns one fixed failure without a stale write or downstream bearer call. OAUTH-001B9 [#608](https://github.com/Run-MPRC/Run-MPRC.github.io/issues/608) source records the disconnect secret's Firestore update time before optional revocation, then transactionally rereads it and deletes the local secret/connection pair only when the secret is absent or still has that exact version. A newer version, including byte-identical replacement, is preserved with one fixed unknown result; a transaction conflict rereads without repeating provider work. OAUTH-001B10 [#610](https://github.com/Run-MPRC/Run-MPRC.github.io/issues/610) browser source admits only a fulfilled response value that presents exactly one own enumerable data field, `ok` equal to `true`; it returns a new frozen confirmation and routes other fulfilled results or exceptional inspection through the existing fixed unknown-result view without hiding the connection. Browser JavaScript cannot distinguish a transparent Proxy that faithfully presents that contract, so the received object is never retained. Synthetic tests use no real token, account, or provider call. Provider calls are still not serialized; an exact browser confirmation does not independently prove provider revocation, and provider/local and lost-acknowledgement reconciliation, a trusted connection-only write that does not also version the secret, deployment, and live behavior remain unproven. | Token theft exposes member activity data; an undeployed boundary or an unhandled provider/acknowledgement race can still lose availability or leave local and provider state ambiguous. | Deploy and read back server/IAM-only secret access, decide encryption, serialize or otherwise reconcile refresh and disconnect operations and lost acknowledgements, minimize scopes, audit access, and verify only with approved made-up staged accounts. | | RISK-025 | Admin authentication relies on password Auth plus a long-lived role token; no repository evidence of MFA, recent-auth checks, re-auth for refunds/role grants, or rapid revocation workflow. | A stolen admin session has broad durable impact. | Require MFA for privileged accounts, recent-auth for sensitive actions, capability roles, short sessions/forced refresh, and break-glass procedures. | -| RISK-042 | Profile photos and an officer people finder have no approved privacy notice, retention/backup treatment, scoped capability, or production release path. The existing Admin Members page downloads complete account documents and cannot enforce an opt-out promise. #505/#506 source now supplies the private processed thumbnail, default-off preference, minimum server-only projection, and bounded audited verified-admin name search, but none is deployed. | A photo, name, query, preference, prefix digest, or stable entry reference could be exposed, retained, enumerated, correlated, or mistaken for membership evidence; automated face matching would add unapproved biometric processing. | Keep the feature default-hidden and separate from the official roster. Preserve current-source revalidation, direct Rules denials, fixed query/result bounds, no-store responses, pseudonymous rate keys, query-free audit, and synthetic race/opt-out/removal proof. Never store the original, persist raw queries, or add face recognition. Complete #110 policy inputs, AUTH-003 scoped authorization follow-up, #133 protected authority, and #507 backend-first release/readback before live use. | +| RISK-042 | Profile photos and an officer people finder have no approved privacy notice, retention/backup treatment, scoped capability, or production release path. The existing Admin Members page downloads complete account documents and cannot enforce an opt-out promise. #505/#506 source supplies the private processed thumbnail, default-off preference, minimum server-only projection, and bounded audited verified-admin name search, but none is deployed. MEMBERS-DIRECTORY-001D [#621](https://github.com/Run-MPRC/Run-MPRC.github.io/issues/621) makes the frontend default an inert, disabled preview with zero directory reads, uploads, searches, saves, request IDs, or service calls; it does not make the backend available. | A photo, name, query, preference, prefix digest, or stable entry reference could be exposed, retained, enumerated, correlated, or mistaken for membership evidence if the connected path is enabled prematurely; automated face matching would add unapproved biometric processing. A browser availability constant is not authorization. | Keep the source-controlled availability boundary off and the feature separate from the official roster. Preserve current-source revalidation, direct Rules denials, fixed query/result bounds, no-store responses, pseudonymous rate keys, query-free audit, and synthetic race/opt-out/removal proof. Never store the original, persist raw queries, or add face recognition. Complete #110 policy inputs, AUTH-003 scoped authorization follow-up, #133 protected authority, and #507 staging plus backend-first deployment/readback before a reviewed source flip and connected website publication. | | RISK-026 | Firestore Admin SDK bypasses rules and the Functions runtime IAM scope is not documented. #135 removes the long-lived service-account JSON path from release source, but the least-privilege short-lived deploy identity is not configured yet. | Server or CI compromise may expose the entire project; missing provider configuration also blocks release. | Complete the private IAM inventory and #133 OIDC/WIF configuration; keep runtime and deploy identities separate; never restore a JSON key shortcut. | | RISK-027 | Sentry optionally records user email and enables 100% replay on error; analytics/privacy consent, redaction, retention, and field-deny policies are not evidenced. | Forms and account flows contain PII that may be sent to monitoring vendors unexpectedly. | Disable replay on sensitive routes or configure strict masking, avoid email user context, set scrubbing/retention, consent policy, and vendor agreements. | | RISK-028 | No payment reconciliation job, dead-letter/quarantine workflow, or alert proves paid Stripe objects match Firestore. | Missed webhooks and partial failures can persist unnoticed. | Add scheduled reconciliation, alert thresholds, operator repair tooling, and a daily finance report. | @@ -84,7 +84,7 @@ The findings below describe the repository at the start of the 2026-07-12 assess | RISK-032 | `auditLog` arrays grow within primary documents and are editable by broad admins. | Move to append-oriented, immutable-by-client audit records with retention/export rules. | | RISK-033 | CSV export mitigates formula injection, but roster export has no explicit re-auth, download audit, row limit/streaming limit, or data-minimization profiles. | Add scoped exports, reason/re-auth, audit, minimum columns, safe filename IDs, and large-export controls. | | RISK-034 | Webhook error response includes the Stripe library's signature error detail. | Return generic client errors; keep sanitized structured diagnostics server-side. | -| RISK-035 | The deterministic frontend Jest suite and standalone SPA callback suite run as separate blocking hosted CI steps. CI-001B4/#186 merged a non-mutating frontend lint gate as `bec7d5e365eacb418563a172029f241f660d9768`; exact PR and post-merge runs passed. CI-001B4A [#227](https://github.com/Run-MPRC/Run-MPRC.github.io/issues/227) removes one reviewed `arrow-body-style` error, CI-001B4B [#239](https://github.com/Run-MPRC/Run-MPRC.github.io/issues/239) removes one stale `AdminMembers` unknown-rule suppression record, and PAY-004C1 [#359](https://github.com/Run-MPRC/Run-MPRC.github.io/issues/359) retires one `no-alert` warning plus two label-association errors with the unsafe reusable-link controls. Reviewed functional changes in [PR #391](https://github.com/Run-MPRC/Run-MPRC.github.io/pull/391) and [PR #392](https://github.com/Run-MPRC/Run-MPRC.github.io/pull/392) add one and two TypeScript files respectively while retiring two TSX errors each. CI-001B4C [#449](https://github.com/Run-MPRC/Run-MPRC.github.io/issues/449) replaces the four remaining stale `react-hooks/exhaustive-deps` directives with ordinary same-line comments, without changing executable code or the gate. MEMBERS-CONTENT-001B [#492](https://github.com/Run-MPRC/Run-MPRC.github.io/issues/492) removes one finding-free dormant JSX file. MEMBERS-DIRECTORY-001A [#505](https://github.com/Run-MPRC/Run-MPRC.github.io/issues/505) adds four finding-free TypeScript/TSX files, and MEMBERS-DIRECTORY-001B [#506](https://github.com/Run-MPRC/Run-MPRC.github.io/issues/506) adds four more. WEB-SUGGESTIONS-001A [#618](https://github.com/Run-MPRC/Run-MPRC.github.io/issues/618) adds one finding-free JSX page. The current baseline scans 119 files and still records 113 configured errors and 6 warnings after the lint process disables the repository's severity-masking `eslint-plugin-only-warn` hook. Branch protection, remaining lint-debt cleanup, and broader domain/integration coverage remain incomplete. | Continue reducing the reviewed finding baseline in focused changes, prove required branch checks, and add domain/integration coverage. Never regenerate the baseline merely to make CI green. | +| RISK-035 | The deterministic frontend Jest suite and standalone SPA callback suite run as separate blocking hosted CI steps. CI-001B4/#186 merged a non-mutating frontend lint gate as `bec7d5e365eacb418563a172029f241f660d9768`; exact PR and post-merge runs passed. CI-001B4A [#227](https://github.com/Run-MPRC/Run-MPRC.github.io/issues/227) removes one reviewed `arrow-body-style` error, CI-001B4B [#239](https://github.com/Run-MPRC/Run-MPRC.github.io/issues/239) removes one stale `AdminMembers` unknown-rule suppression record, and PAY-004C1 [#359](https://github.com/Run-MPRC/Run-MPRC.github.io/issues/359) retires one `no-alert` warning plus two label-association errors with the unsafe reusable-link controls. Reviewed functional changes in [PR #391](https://github.com/Run-MPRC/Run-MPRC.github.io/pull/391) and [PR #392](https://github.com/Run-MPRC/Run-MPRC.github.io/pull/392) add one and two TypeScript files respectively while retiring two TSX errors each. CI-001B4C [#449](https://github.com/Run-MPRC/Run-MPRC.github.io/issues/449) replaces the four remaining stale `react-hooks/exhaustive-deps` directives with ordinary same-line comments, without changing executable code or the gate. MEMBERS-CONTENT-001B [#492](https://github.com/Run-MPRC/Run-MPRC.github.io/issues/492) removes one finding-free dormant JSX file. MEMBERS-DIRECTORY-001A [#505](https://github.com/Run-MPRC/Run-MPRC.github.io/issues/505) adds four finding-free TypeScript/TSX files, MEMBERS-DIRECTORY-001B [#506](https://github.com/Run-MPRC/Run-MPRC.github.io/issues/506) adds four more, WEB-SUGGESTIONS-001A [#618](https://github.com/Run-MPRC/Run-MPRC.github.io/issues/618) adds one finding-free JSX page, and MEMBERS-DIRECTORY-001D [#621](https://github.com/Run-MPRC/Run-MPRC.github.io/issues/621) adds one finding-free TypeScript availability module. The current baseline scans 120 files and still records 113 configured errors and 6 warnings after the lint process disables the repository's severity-masking `eslint-plugin-only-warn` hook. Branch protection, remaining lint-debt cleanup, and broader domain/integration coverage remain incomplete. | Continue reducing the reviewed finding baseline in focused changes, prove required branch checks, and add domain/integration coverage. Never regenerate the baseline merely to make CI green. | | RISK-036 | #135 adds a manual exact-commit source gate, fixed profile-recovery targets, backend-first order, missing-config failure, and ordinary Git-triggered Netlify production containment. WEB-UX-001A [#457](https://github.com/Run-MPRC/Run-MPRC.github.io/issues/457) used a temporary exact-artifact exception. On 2026-08-01 an overbroad WEB-002A artifact was merged after a late blocker and published; exact rollback merge `1099ee8` restored source `ed1b0833`, and `dee7951` paused the manifest. Bounded replacement [#473](https://github.com/Run-MPRC/Run-MPRC.github.io/issues/473) then published exact source `39ab8649` as deploy `6a6dc9ea588b0c0008036312`; its delta is only Shop and Events/Calendar failure containment, and its public checks passed. Final control `cb6a8f0` made the manifest inactive; Netlify attempt `6a6dcdd47bc81e000859a249` stopped unpublished and left the bounded deploy live. Protected environments/OIDC, isolated staging, a reusable live-Netlify path, and provider-owned atomic rollback remain unverified. | Require a final blocker re-read and an executable delta from the live artifact before every release merge. Preserve the exact #473 marker and rollback evidence, keep ordinary publication paused, and keep the reviewed Git rollback projection available. Complete #133 and #136, provision isolated staging, protect the reusable Netlify release path under WEB-001, and rehearse provider rollback before broader production work. | | RISK-037 | Account/registration deletion, export, retention, backup, and restore procedures are incomplete. | Approve retention matrix, automate minimization, support access/deletion requests, and test backup restoration. | | RISK-038 | Source-controlled secret scan is ad hoc; no continuous secret scanner, dependency update bot, SBOM, provenance, or branch protection is documented. | Add secret/dependency/code scanning, reviewed lockfile updates, protected environments/branches, and artifact provenance appropriate to project scale. | @@ -99,6 +99,7 @@ These entries are implementation evidence, not a production risk-acceptance deci | --- | --- | --- | | Source-only containment for RISK-008, RISK-017, RISK-022, RISK-032, and RISK-042 | MEMBERS-DIRECTORY-001A [#505](https://github.com/Run-MPRC/Run-MPRC.github.io/issues/505) adds four caller-only profile callables with native App Check enforcement and fail-closed private no-store responses. Strict requests, revisions, current-name opt-in, normalized 256-by-256 WebP output, metadata/animation/size rejection, separate default-off visibility, server-only preference/photo records, atomic append-oriented audits, and direct-browser denials are covered with generated non-face data. No original image, filename, provider photo URL, image-derived digest, face matching, or membership authority is stored or returned. The exact `sharp@0.35.0` dependency has no finding in the 2026-08-01 production audits; the audits still report 4 existing root findings (1 high, 2 moderate, 1 low) and 10 existing Functions findings (9 moderate, 1 low). | This slice is not live and by itself supplies no officer search; #506 adds that separate source boundary. Every upload or retry still writes stable, domain-separated, SHA-256 account-key abuse-control bookkeeping, consumes quota, and depends on an unverified TTL; that pseudonym is linkable and is not anonymization. Profile/photo/audit state alone is read-only on an exact retry. Complete #110 privacy/retention/backup decisions, #133 protected short-lived authority, #507 isolated staging and backend-first deployment/readback, approved public notice wording, synthetic opt-out/removal/cache proof, website publication, and `runmprc.com` verification before use. Do not inspect real profiles or enable biometric processing. | | Source-only containment for RISK-008, RISK-017, RISK-022, RISK-032, and RISK-042 | MEMBERS-DIRECTORY-001B [#506](https://github.com/Run-MPRC/Run-MPRC.github.io/issues/506) adds one separate officer people-finder page, one native-App-Check verified-admin callable, and one current-state projection reconciler with idempotent background retry. The browser submits only an explicit UUID/name-prefix command and accepts at most 24 exact display-name/optional-thumbnail projections. Direct Firestore access is denied. The server rejects Unicode controls/formats and invalid canonical bounds, stores only prefix digests under a proven 272-entry ceiling, permits 30 searches per verified-admin account/hour, queries at most 48 candidates, re-reads each current member/preference/photo record before returning it, and atomically records one minimal query-free audit. An opted-out, missing-name, malformed, stale, or now-nonmatching candidate is hidden. The existing full-account admin filter is visibly separate. | The projection's internal UID key, prefix digests, entry reference, rate key, and audit identity remain stable/linkable server-side pseudonyms, not anonymization. A response still contains personal data for its approved purpose. Exact request-ID reuse gets a fixed denial instead of retained-result replay and creates no second audit; the rate limiter may still consume an attempt. The current verified `admin` role is only a documented compatibility boundary, not a scoped people-finder capability. Source/tests/preview do not configure TTL, privacy notice, retention/backups, repair/backfill ownership, staging, Firebase Rules/Functions/indexes or trigger policy, Netlify, or production behavior. Complete #110, AUTH-003, #133, and #507 before use; never test against real profiles or add image queries, facial recognition, embeddings, similarity, analytics, or raw-query logs. | +| Frontend-only containment for RISK-042 | MEMBERS-DIRECTORY-001D [#621](https://github.com/Run-MPRC/Run-MPRC.github.io/issues/621) adds one source-controlled availability boundary that defaults unavailable. The account page and `AdminGuard`-protected People finder render the future layout as visibly described, natively disabled controls. The default path does not mount the connected components, read a file or saved state, accept a name, create a request ID, initialize or call a directory service, or render sample or result cards. Focused tests preserve the connected source behind an explicit seam using only synthetic data. | This is an accidental-call and misleading-interface containment, not an authorization, privacy, retention, backend, provider, deployment, or live-behavior control. A later build must not turn availability on through configuration drift or an unreviewed environment value. #507 must complete approved notice and backup/removal wording, scoped authorization, protected authority, isolated staging, backend-first Rules/Functions/index deployment and readback, synthetic privacy/race proof, and then a separately reviewed source flip before connected website publication. Until then, no real name or photo may be entered, read, uploaded, searched, or saved. | | Immediate Product-binding containment part of RISK-031 | PAY-PRODUCT-001A is tracked in live [#353](https://github.com/Run-MPRC/Run-MPRC.github.io/issues/353). One dependency-free projection is used by the paid race and Shop checkout paths. A present stored Product link must be an own primitive non-empty string from 1 through 255 JavaScript code units and is copied without trimming, conversion, character restrictions, or `prod_` inference. Only a genuinely missing own field retains the compatibility Product-create path. Before any mapping write, a resolved Product must provide a bounded custom ID, exact Product kind, expected declared mode, and an installed-SDK 2xx response marker. Malformed stored links stop before token, registration/order identifier, Product-link or business-record write, or Stripe work; earlier access and request-count checks and their safety-counter writes may already have run. Malformed created results stop after at most one Product attempt but before mapping, Checkout Session, or business-record writes. | This structural containment does not prove provider origin, Stripe account ownership, intended catalog identity, metadata binding, Product status, price, dispatch, delivery, or reconciliation. A rejected create result may leave an orphaned Product; do not retry automatically. Anonymous clean-missing creation remains concurrency-prone and reachable from public traffic. Complete #113 inventory/disposition, authenticated idempotent catalog synchronization, Product-specific plan/pre-send/result/lost-acknowledgement/reconciliation controls, isolated staging, protected Firebase deployment, and provider proof before live commerce. | | Immediate current-handler containment part of RISK-003 | PAY-SESSION-001A is tracked in live [#357](https://github.com/Run-MPRC/Run-MPRC.github.io/issues/357). The current paid race and Shop handlers build one immutable expectation before the Session call, catch rejection without opening it, and immediately project only a closed installed-SDK result. A result must match declared test/live mode, payment/open/unpaid state, exact cents, USD, buyer email, exact closed metadata, exact callbacks, a mode-compatible bounded Session ID, one canonical HTTPS capability at the hard-coded default `checkout.stripe.com` origin, and an installed-SDK 200 marker. Only copied ID/URL values continue; records store the ID but never the URL. Invalid results create no registration/order record and return one fixed unknown-result message. The website makes rejection or a missing paid URL terminal for that page visit, retains the form, and blocks a second direct handler call. | This is a narrow legacy compatibility stop, not trusted provider/account origin, deterministic business idempotency, dispatch/delivery proof, durable result evidence, payment proof, or adoption of the unused C4 chain. The Session call occurs first, so a rejected result may leave a payable orphan; earlier rate-counter, token/identifier, and lazy Product-mapping effects may remain. Reload, another tab/device, or a scripted caller bypasses the page lock. A configured Stripe custom checkout domain is blocked until #113 and a protected configuration boundary approve it. Complete PAY-002C/D persistence-first sagas, trusted C4 controller/result persistence, reconciliation, protected staging/deployment, provider readback, and exact website/Firebase/live proof. | | Immediate pre-render browser containment part of RISK-003 | PAY-SESSION-001B is tracked in live [#503](https://github.com/Run-MPRC/Run-MPRC.github.io/issues/503). Each current public race and Shop page now keeps one component-local in-memory marker. After existing local checks admit a request, the handler sets that marker synchronously before analytics or its first Checkout promise can settle. A same-action or later submission on that mounted page is inert, including the gap before React renders the existing pending disabled button. Focused actual-route tests prove one service/analytics attempt across same-action and post-render repeats; preserve free-participant, volunteer-without-price-tier, and both paid-link navigation branches; and prove local waiver/native-disabled paths do not consume an attempt. | This browser marker is not a server boundary, durable idempotency key, provider dispatch/result record, business-state lock, payment proof, or reconciliation. It never releases during the mounted page visit because the admitted result must navigate or enter #357's terminal unknown-result state. Existing form controls other than the submit button remain editable. Reload, a remount, another tab/device, a script, or a direct service caller can bypass it. A same-mounted route change instead stays locked, and this slice does not fence an older pending result from that changed route. Complete PAY-002C/D persistence-first deterministic commands, durable replay/reconciliation, protected deployment, and exact website/Firebase/Stripe/live proof. | @@ -145,7 +146,7 @@ These entries are implementation evidence, not a production risk-acceptance deci | RISK-009, part of RISK-024 | OAUTH-001C1J [#612](https://github.com/Run-MPRC/Run-MPRC.github.io/issues/612) browser source admits a fulfilled Strava exchange result only when it presents exactly two own fields, each an enumerable data field: primitive `ok` equal to `true` and a positive safe-integer `athleteId`. It returns a fresh frozen confirmation. Every other fulfillment or exceptional result inspection throws one fixed local response error; a current callback uses the existing fixed failure screen without navigating, while existing lifecycle guards keep obsolete completions inert. Callable transport rejection remains uninspected and identity-preserving. Browser JavaScript cannot distinguish a transparent Proxy that faithfully presents the contract, so the received object is never retained. The check relies on the page realm's built-in reflection, numeric, and freeze methods; code that replaces those methods before admission can change what this browser-only boundary observes. Synthetic tests use no real callback, account, token, provider, or Firebase project. | This client acknowledgement check does not prove Strava origin, provider exchange, paired Firestore persistence, athlete ownership, approved scopes, server integrity, Firebase or website deployment, or live behavior. A compromised server or browser realm can forge the exact result. Provider response-byte and timeout limits, scope/account policy, provider/local reconciliation, audit, IAM/encryption, configured Enterprise evidence, native enforcement, release order, and provider/live proof remain open under [#88](https://github.com/Run-MPRC/Run-MPRC.github.io/issues/88). | | RISK-017, part of RISK-024 | OAUTH-001A2K [#614](https://github.com/Run-MPRC/Run-MPRC.github.io/issues/614) Function source admits a successful Strava authorization-exchange or refresh body only through the captured native Node `Response`, `Headers`, and stream-reader methods. It permits at most 65,536 content-decoded bytes and 4,096 chunks before copy, treats a valid `Content-Length` only as an early ceiling check, counts the actual chunks even when that header is missing or lower, best-effort cancels an unfinished failure without awaiting or exposing cleanup, then uses fatal UTF-8 decoding and captured JSON parsing before the existing strict projector. It never calls the response's `json`, `text`, `arrayBuffer`, or `bytes` method, clone, or tee. Invalid body transport/parsing keeps the existing fixed unavailable result; a bounded invalid schema keeps the fixed internal result. Exchange state stays consumed but no token/connection write follows a body failure; refresh keeps the prior secret and starts no bearer data request. The direct projector seam remains absent from `functions/index.js` and preserves hostile-JavaScript tests that real JSON bytes cannot construct. Synthetic tests use no real token, account, provider, or Firebase project. | This application ceiling covers only content-decoded chunks consumed by this source. It does not bound headers, compressed-wire bytes, Undici buffers, one already-allocated provider chunk, parsed-object heap growth, CPU, or request/connect/read time. Cancellation cannot undo a provider exchange or rotation, so a lost acknowledgement can still require reconciliation and must not be retried blindly. Activities/statistics response bounds and their concurrently started body cleanup remain open, as do scope/account policy, refresh/revoke reconciliation, audit, IAM/encryption, Firebase deployment, provider configuration, production data, and provider/live proof under [#88](https://github.com/Run-MPRC/Run-MPRC.github.io/issues/88). | | Part of RISK-027 | #99 blocks App Check, Firebase Analytics, and Sentry in local/test and on initial capability-callback startup. OBS-001A1 [#134](https://github.com/Run-MPRC/Run-MPRC.github.io/issues/134) source disables Sentry replay, tracing, browser session tracking, client reports, member user context, breadcrumbs, attachments, transactions, and unapproved default integrations. OBS-001A2 [#139](https://github.com/Run-MPRC/Run-MPRC.github.io/issues/139) source removes every application runtime Firebase Analytics import, initialization, and emission, preserves existing wrapper calls as provider-free no-ops, and removes the direct Waiver SDK bypass. OBS-001A3 [#142](https://github.com/Run-MPRC/Run-MPRC.github.io/issues/142) routed the remaining render, Auth-email, members-only, and App Check browser diagnostics through five fixed outcome identifiers and prohibited direct console calls elsewhere in application/public runtime source. OBS-001A5 [#494](https://github.com/Run-MPRC/Run-MPRC.github.io/issues/494) removes the dormant members-only identifier after its sole caller was retired, leaving the current helper with four fixed outcomes and requiring the old literal to be dropped as unknown input. Synthetic tests use mocks/canaries and make no provider call. | #110/#111 still own any future approved telemetry purpose, consent, event/parameter schema, provider settings, and residual hosted boundary. Source/tests and merge evidence do not prove website publication, the exact `runmprc.com` revision, provider transport/collection, IP/cookie behavior, console history, historical data deletion, retention, access, consent, deletion, or vendor terms. Sentry provider behavior is likewise unverified. #443 completes only the source/test Strava client handoff; native enforcement and the DATA-001A confirmation handoffs remain with ABUSE-001A and their canonical trackers. | -| 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, named blocking lint step, and clean-checkout step. Exact PR run `29291402007` and post-merge run `29291515653` passed every GitHub job. CI-001B4A [#227](https://github.com/Run-MPRC/Run-MPRC.github.io/issues/227), CI-001B4B [#239](https://github.com/Run-MPRC/Run-MPRC.github.io/issues/239), and PAY-004C1 [#359](https://github.com/Run-MPRC/Run-MPRC.github.io/issues/359) retire four errors and one warning in focused reviewed changes. [PR #391](https://github.com/Run-MPRC/Run-MPRC.github.io/pull/391) and [PR #392](https://github.com/Run-MPRC/Run-MPRC.github.io/pull/392) add three TypeScript files while retiring four TSX errors during reviewed functional work. CI-001B4C [#449](https://github.com/Run-MPRC/Run-MPRC.github.io/issues/449) retires exactly four stale unknown-rule directive records with comment-only same-line substitutions. #492 removes one finding-free dormant JSX file, #505 adds four finding-free TypeScript/TSX files, and #506 adds four more; #618 adds one finding-free JSX page. | The checker now scans 119 frontend files. Its reviewable baseline deliberately records 113 configured errors and 6 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, #182, #206, #226, #232, and #238 expand the focused commerce suite; configure/prove branch-required checks and add the broader TEST-001 suite. | +| 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, named blocking lint step, and clean-checkout step. Exact PR run `29291402007` and post-merge run `29291515653` passed every GitHub job. CI-001B4A [#227](https://github.com/Run-MPRC/Run-MPRC.github.io/issues/227), CI-001B4B [#239](https://github.com/Run-MPRC/Run-MPRC.github.io/issues/239), and PAY-004C1 [#359](https://github.com/Run-MPRC/Run-MPRC.github.io/issues/359) retire four errors and one warning in focused reviewed changes. [PR #391](https://github.com/Run-MPRC/Run-MPRC.github.io/pull/391) and [PR #392](https://github.com/Run-MPRC/Run-MPRC.github.io/pull/392) add three TypeScript files while retiring four TSX errors during reviewed functional work. CI-001B4C [#449](https://github.com/Run-MPRC/Run-MPRC.github.io/issues/449) retires exactly four stale unknown-rule directive records with comment-only same-line substitutions. #492 removes one finding-free dormant JSX file, #505 adds four finding-free TypeScript/TSX files, #506 adds four more, #618 adds one finding-free JSX page, and #621 adds one finding-free TypeScript availability module. | The checker now scans 120 frontend files. Its reviewable baseline deliberately records 113 configured errors and 6 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, #182, #206, #226, #232, and #238 expand the focused commerce suite; configure/prove branch-required checks and add the broader TEST-001 suite. | | DATA-001A3 / registration confidentiality | DATA-001A3 [#374](https://github.com/Run-MPRC/Run-MPRC.github.io/issues/374) limits the My Account registration callable to one exact stored-UID query. Source no longer reads token email or verification claims and no longer searches registrations by runner email. Matching-email records with no UID or another account's UID are excluded; exact-UID records retain their existing sanitized summary, event lookup, and sort. The page's empty result says only that no upcoming registration is linked and that a signed-out registration may not appear. Synthetic tests use getter and Proxy canaries and make no write or provider call. | This source containment is not deployed or live and does not associate, migrate, repair, or change a registration. Anonymous, historical, mistyped-email, shared-email, and differently owned records stay hidden until a separately approved, audited server association under [#115](https://github.com/Run-MPRC/Run-MPRC.github.io/issues/115). Keep the email index until separate inventory and migration review. Complete protected Function deployment/readback, website publication, exact `runmprc.com` verification, and a made-up account check before describing the boundary as live. | | DATA-001A4 / registration response minimization | DATA-001A4 [#553](https://github.com/Run-MPRC/Run-MPRC.github.io/issues/553) removes the unused nested runner projection from the exact-UID My Account registration response and its browser type. Source does not access the matching document's `runner` property or serialize runner name, email, or shirt size; it preserves every non-runner registration field, the event summary, event reads, registration-created sort, and empty result. A synthetic accessor-backed record proves the runner property is untouched and invented canaries do not cross the response. | This source is not deployed or live. The Function still retrieves the complete matching registration document from Firestore, and stored runner data plus separate event, admin, and confirmation contracts remain unchanged. This slice makes no retention, deletion, field-mask, schema, association, or migration decision. Complete a protected Function deployment/readback and made-up account check before describing the narrower response as live. | | DATA-001A5 / registration-query failure containment | DATA-001A5 [#560](https://github.com/Run-MPRC/Run-MPRC.github.io/issues/560) catches without binding any exceptional Firestore construction, collection-group, exact-UID `where`, or query `get` outcome and returns only fixed `unavailable / Registration data could not be loaded.` before registration projection or event reads. Synthetic stage failures prove that no caught-value getter, coercion, serialization, formatting hook, or application logger is reached while App Check/Auth and successful query behavior stay unchanged. | This source is not deployed or live. It does not contain exceptional registration snapshot iteration/projection or event-document reads, change stored data or field retrieval, add retries, or prove Firebase logging outside application code. Complete a protected Function deployment/readback and made-up account check before describing the fixed boundary as live; harden later data/event failure stages separately. | diff --git a/SYSTEM_DESIGN.md b/SYSTEM_DESIGN.md index 5a7540e2..4933c27c 100644 --- a/SYSTEM_DESIGN.md +++ b/SYSTEM_DESIGN.md @@ -677,9 +677,11 @@ Name matching uses normalization version 1: NFKC, locale-independent lowercase, The projection supplies candidates only. In the same bounded transaction that creates the search audit, the server re-reads each candidate's current member, preference, and photo records, derives every returned name and thumbnail from those current records, and checks that the current normalized name still matches. Stale or malformed candidates are withheld. All reads happen before the audit create. A search transaction ordered after a completed opt-out conflicts/retries as needed and observes the hidden state. A search that committed before opt-out remains a pre-opt-out snapshot and its network response cannot be retroactively recalled; no-store prevents reusable cache storage but not an officer seeing or capturing an already-authorized result. #110 notice/removal wording must disclose that boundary. The audit document ID is a domain-separated digest of actor UID and UUID request ID; it is checked before the candidate query, so one reused command receives a fixed duplicate denial and creates no second audit rather than replaying retained personal results. The event contains only actor UID, fixed action and purpose, request ID, query-length bucket, result count, fixed outcome, and trusted time. Search rate state uses a separate domain-separated account pseudonym and the existing TTL-dependent private limiter. Trigger `syncMemberDirectoryEntryOnMemberWrite` and the #505 profile mutations both reconcile this same current-state contract. -The page sends nothing while the person types. Search happens only after the explicit action and never uses the existing `/admin/members` browser-side full-account filter. The client accepts only the exact bounded response projection, binds pending work to the current application/admin identity, clears stale results when input or identity changes, disables browser autocomplete, and supplies loading, empty, fixed-failure, missing-photo, and narrow-screen states without implying membership. Firestore Rules deny every direct browser read or write to directory entries, preferences, photos, nested records, search audits, and rate state. +The preserved connected branch sends nothing while the person types. Search happens only after the explicit action and never uses the existing `/admin/members` browser-side full-account filter. The client accepts only the exact bounded response projection, binds pending work to the current application/admin identity, clears stale results when input or identity changes, disables browser autocomplete, and supplies loading, empty, fixed-failure, missing-photo, and narrow-screen states without implying membership. Firestore Rules deny every direct browser read or write to directory entries, preferences, photos, nested records, search audits, and rate state. -This source has no production index, Rules, Function, website, privacy-notice, retention/backup approval, or live proof. [#507](https://github.com/Run-MPRC/Run-MPRC.github.io/issues/507) must complete the #110 policy decision, #133 protected short-lived authority, any required index and backend-first deployment/readback (including the trigger retry policy), isolated synthetic staging checks, website publication, `runmprc.com` verification, and backup-officer procedure before use. It must also own an idempotent, dry-runnable projection repair/backfill for any later schema/normalization upgrade, or prove no older preference data exists for the initial target. A green source build or deploy preview is not authorization to search a real name or inspect a real photo. +**MEMBERS-DIRECTORY-001D frontend boundary:** [#621](https://github.com/Run-MPRC/Run-MPRC.github.io/issues/621) puts both directory interfaces behind one source-controlled availability boundary that defaults unavailable. In that default state, My Account renders the future thumbnail placeholder, file control, and independent finder choice as natively disabled controls under a visible preview notice. It does not mount the connected profile branch, read a file or saved setting, create a request ID, or initialize or call a member-directory service. The separate `/admin/member-directory` route remains behind `AdminGuard`, but its name field and Search button are disabled; it accepts no name, creates no request ID, initializes no search service, and renders no sample, fake, or result card. This is an inert layout preview, not client-side authorization and not a connected feature. + +The backend source still has no production index, Rules, Function, privacy-notice, retention/backup approval, or live proof. Synthetic local artifacts and tests prove that the protected #621 layouts are disabled and their default branches make zero directory calls. Publishing and anonymously reading back the exact frontend may prove only its revision, normal sign-in and administrator guards, and absence of a public member-directory request. [#507](https://github.com/Run-MPRC/Run-MPRC.github.io/issues/507) must complete the #110 policy decision, scoped authorization, #133 protected short-lived authority, any required index and backend-first deployment/readback (including the trigger retry policy), isolated synthetic staging checks, and backup-officer procedure before a separately reviewed source change turns availability on. Only then may the connected website be published and verified on `runmprc.com`. #507 must also own an idempotent, dry-runnable projection repair/backfill for any later schema/normalization upgrade, or prove no older preference data exists for the initial target. A green source build, disabled preview, merge, or frontend publication is not authorization to search a real name or inspect a real photo. ### 8.0a Provider-neutral membership authority and entitlement — SOURCE ONLY, UNUSED diff --git a/docs/officers/EVENTS_SHOP_MEMBERS.md b/docs/officers/EVENTS_SHOP_MEMBERS.md index f89acf44..9833fd61 100644 --- a/docs/officers/EVENTS_SHOP_MEMBERS.md +++ b/docs/officers/EVENTS_SHOP_MEMBERS.md @@ -2414,30 +2414,61 @@ Officer review steps after every prerequisite has proof: **Escalation:** treasurer plus platform/security owner. Use the private incident path if any unexpected refund or real record was involved. -## Optional profile photo and officer people finder — SOURCE ONLY, NOT LIVE +## Optional profile photo and officer people finder — INERT FRONTEND PREVIEW; BACKEND NOT LIVE -**Purpose:** let a signed-in person choose one private profile thumbnail and, separately, choose whether the People finder may show their display name and thumbnail. Under the current temporary access boundary, a verified website administrator searches by name and may compare the voluntary thumbnails visually. A finder-specific permission is still required before release. The system does not search a photo or recognize a face. +**Purpose:** let officers review the future profile-photo, independent finder-choice, and People finder layouts without connecting the private backend. The current #621 frontend default is an inert preview: every related control is disabled; it reads no saved photo or setting, accepts or uploads no photo, searches no name, and saves nothing. After a later approved connection, a signed-in person could choose one private profile thumbnail and, separately, choose whether the People finder may show their display name and thumbnail. A properly authorized officer could search by name and compare voluntary thumbnails visually. The system will not search a photo or recognize a face. -**Approver:** membership lead, privacy owner, and platform/security owner. The privacy owner must approve the final notice and backup/removal wording before publication. +**Approver:** membership lead, privacy owner, and platform/security owner. The privacy owner must approve the final notice and backup/removal wording before connected publication. -**Prerequisites:** parent [#504](https://github.com/Run-MPRC/Run-MPRC.github.io/issues/504), account source child [#505](https://github.com/Run-MPRC/Run-MPRC.github.io/issues/505), and search source child [#506](https://github.com/Run-MPRC/Run-MPRC.github.io/issues/506) are reviewed. Use only made-up accounts, made-up names, and generated non-face images. Protected release [#507](https://github.com/Run-MPRC/Run-MPRC.github.io/issues/507), the approved #110 privacy entry, #133 protected release authority, and isolated staging are still required before live use. +**Prerequisites:** parent [#504](https://github.com/Run-MPRC/Run-MPRC.github.io/issues/504), account source child [#505](https://github.com/Run-MPRC/Run-MPRC.github.io/issues/505), search source child [#506](https://github.com/Run-MPRC/Run-MPRC.github.io/issues/506), and frontend-preview child [#621](https://github.com/Run-MPRC/Run-MPRC.github.io/issues/621) are reviewed. Review the protected disabled layouts only through synthetic local artifacts. After the exact #621 frontend revision is published and read back, inspect only its signed-out public guards. Use only made-up accounts, made-up names, and generated non-face images for the preserved connected-source tests. Protected release [#507](https://github.com/Run-MPRC/Run-MPRC.github.io/issues/507), the approved #110 privacy entry, scoped authorization, #133 protected release authority, and isolated staging are still required before any backend connection or live use. ```mermaid flowchart TD - Person["Signed-in person"] --> Photo["Upload or remove private processed thumbnail"] - Person --> Choice{"Allow optional officer finder?"} - Choice -- "No or missing" --> Hidden["Hidden from optional finder"] - Choice -- "Yes, with current name" --> Private["Minimum private search entry"] - Photo -. "does not turn on" .-> Choice - Officer["Verified admin types a name and selects Search"] --> Server["Server checks access, limits, and current choice"] - Server --> Private - Server --> Results["Up to 24 voluntary name/photo cards"] - Results -. "does not prove or change" .-> Official["Membership, role, payment, or official records"] + Person["Signed-in person's My Account preview"] --> Account["Photo and separate finder-choice controls disabled"] + Guard["Existing administrator guard"] --> Officer["People finder preview"] + Officer --> Search["Name field and Search button disabled"] + Account --> None["No directory name or photo read, upload, remove, search, or save"] + Search --> None + Later["Later #507 reviewed source flip"] --> Gates["Privacy, scoped authorization, staging, and backend-first readback"] + Gates --> Connected["Future optional name search with voluntary thumbnails"] + Connected -. "never photo search or proof" .-> Official["Membership, role, payment, or official records"] ``` -Text alternative: the person controls a private processed thumbnail and a separate default-off finder choice. A verified admin types a name and receives at most 24 currently opted-in name/photo cards. Uploading does not opt a person in, and no card proves or changes an official record. - -Officer review steps for the #505 source only: +Text alternative: the current signed-in Account preview and administrator-guarded People finder show their future layouts with every directory control disabled. Their directory branches read, accept, search, and save nothing; the administrator guard still uses the normal account and role context. Only a later #507 source flip after privacy, authorization, staging, and backend-first readback could connect the optional name search and voluntary thumbnails. It will not search a photo, and a result will not prove or change an official record. + +Officer review steps for the #621 frontend preview: + +1. Keep the profile photo and People finder backend marked **NOT AVAILABLE YET**. +2. Ask the platform owner for the exact #621 pull request, commit, and frontend test record. +3. Confirm the source-controlled availability boundary defaults unavailable. +4. Confirm the default My Account path mounts no connected profile component. +5. Ask the platform owner for dated synthetic local screenshots of both protected layouts at desktop and 320-pixel widths. +6. Confirm those screenshots contain only a made-up local account and no real name or photo. +7. Confirm the local My Account layout shows a visible interface-preview notice. +8. Confirm its thumbnail placeholder, file control, and finder choice are disabled. +9. Confirm the preview says no directory photo or finder setting is read, uploaded, searched, or saved. +10. Confirm the default account test creates no request number and calls no directory service. +11. Confirm the People finder route still requires the administrator guard and normal account/role check. +12. Confirm the local People finder layout shows a visible interface-preview notice. +13. Confirm its name field and Search button are disabled. +14. Confirm the preview accepts no finder name and loads no directory profile, sample, or result card. +15. Confirm the default officer test creates no request number and calls no directory search service. +16. Confirm both synthetic layouts remain readable at 320 pixels without horizontal overflow. +17. Confirm keyboard focus does not offer an enabled directory action. +18. Confirm the visible notices programmatically describe the disabled controls. +19. Confirm the copy says a future upload will not opt a person in. +20. Confirm the copy says a future result will not prove membership. +21. Confirm the copy says there is no photo query or facial recognition. +22. Confirm synthetic tests still cover the preserved connected branches through an explicit test seam. +23. After publication, ask the platform owner for the exact frontend hosting record. +24. Ask the platform owner for the matching public revision readback. +25. While signed out, open `/account` and confirm the normal sign-in boundary remains. +26. While signed out, open `/admin/member-directory` and confirm the administrator boundary remains. +27. Ask the platform owner for the anonymous public network record. +28. Confirm that record contains no member-directory callable request. +29. Stop. Do not sign in to production, choose a file, enter a name, call Firebase directly, or use production member data. + +Officer review steps for the preserved #505 connected source only: 1. Keep the complete photo and people-finder feature marked **NOT AVAILABLE YET**. 2. Ask the platform owner for the exact #505 pull request, commit, and synthetic test record. @@ -2449,9 +2480,9 @@ Officer review steps for the #505 source only: 8. Confirm turning the choice off does not delete or change an official account, role, membership, registration, or payment record. 9. Confirm removing the photo is a separate action and deletes the active thumbnail. 10. Confirm an uncertain change hides further controls until the person deliberately reloads the settings; a fixed server rejection instead refetches current settings and leaves controls available. -11. Stop the #505 review. Do not open the production Account screen, upload a real photo, or edit Firebase. +11. Stop the #505 review. Do not enable the production Account controls, upload a real photo, or edit Firebase. -Officer review steps for the #506 source only: +Officer review steps for the preserved #506 connected source only: 1. Keep the complete feature marked **NOT AVAILABLE YET**. 2. Ask the platform owner for the exact #506 pull request, commit, and synthetic test record. @@ -2492,15 +2523,15 @@ Officer review steps for the #506 source only: 37. Confirm direct browser access cannot list the private search entries, preferences, photos, audits, or rate counters. 38. Set the source page to a 320-pixel width with only synthetic data. 39. Confirm loading, no-result, fixed-error, and missing-photo states remain readable. -40. Stop. Do not open production Account or Admin pages, search a real name, inspect a real profile, or edit Firebase. +40. Stop. Do not enable connected production Account or Admin controls, search a real name, inspect a real profile, or edit Firebase. -**Expected result:** source and synthetic tests prove the separate default-off account controls and a bounded officer name search whose results are rechecked against current private state. There is no public directory, official roster, public photo URL, Firebase Storage object, photo-as-query path, face recognition, similarity score, embedding, biometric template, export, result total, or pagination. +**Expected result:** the frontend defaults to a visibly disabled preview that makes zero directory calls, accepts no file or name, and shows no person. Separate synthetic tests still prove the preserved default-off account controls and bounded officer name search whose future results would be rechecked against current private state. The backend and connected behavior remain unavailable. There is no public directory, official roster, public photo URL, Firebase Storage object, photo-as-query path, face recognition, similarity score, embedding, biometric template, export, result total, or pagination. -**Stop conditions:** a real person, name, photo, member record, production account, or production Firebase project; a public/permanent photo URL; an upload that silently opts in; a result from a search transaction ordered after completed opt-out; a notice that fails to explain that an earlier-committed response may still arrive; raw image, name, query, result identity, or provider detail in a log, issue, screenshot, message, email, or AI tool; a request for photo search, face recognition, similarity matching, or biometric processing; missing privacy approval; or a claim that source, tests, merge, or preview means the feature is live. +**Stop conditions:** an enabled preview control; a preview that reads saved directory state, accepts a file or finder name, creates a directory request number, calls a directory service, or shows a sample or result card; a real person, name, photo, member record, production sign-in, direct production Firebase access, production data change, or member-directory callable request; a public/permanent photo URL; an upload that silently opts in; a result from a search transaction ordered after completed opt-out; a notice that fails to explain that an earlier-committed response may still arrive; raw image, name, query, result identity, or provider detail in a log, issue, screenshot, message, email, or AI tool; a request for photo search, face recognition, similarity matching, or biometric processing; missing privacy approval for connected publication; an unreviewed availability flip; or a claim that source, tests, merge, frontend publication, or preview means the backend feature is live. -**Success proof:** record the exact #505 and #506 pull requests and merge commits, green focused and full checks, dependency review, independent privacy/security and backup-officer reviews, and explicit statements that Firebase deployment, website publication, `runmprc.com`, privacy notice, production data, and live behavior were not performed. Final live proof belongs to #507. +**Success proof:** record the exact #505, #506, and #621 pull requests and merge commits; green focused and full checks; dependency review; independent privacy/security and backup-officer reviews; and explicit statements that Firebase, providers, and production data were unchanged. Record the exact frontend publication and `runmprc.com` readback if they occurred, or the exact external blocker if they did not. Synthetic artifacts and tests prove only the disabled protected layouts and default zero-directory-call branches. Publication and anonymous readback prove only the exact revision, signed-out guards, and absence of a public member-directory request. Record separately that the privacy notice, backend deployment, connected behavior, and live directory were not performed. Final connected live proof belongs to #507. -**Undo:** before publication, use one reviewed source revert or safe roll-forward. After a future approved release, use the documented backend-first release path and verify opt-out/removal with a made-up account. Never undo by deleting or editing a real account or Firebase record manually. +**Undo:** before or after frontend-preview publication, use one reviewed frontend revert or safe roll-forward and read back the exact disabled state. The #621 preview changes no Firebase record to undo. After a future approved backend release, use the documented backend-first release path and verify opt-out/removal with a made-up account. Never undo by deleting or editing a real account or Firebase record manually. **Escalation:** membership lead plus privacy and platform/security owners. Use the private incident path if a real photo/name appeared, a visibility choice was wrong, or deletion could not be confirmed. diff --git a/docs/officers/README.md b/docs/officers/README.md index 8d6e948e..c52d3f9f 100644 --- a/docs/officers/README.md +++ b/docs/officers/README.md @@ -69,7 +69,7 @@ The protected GitHub release is also **NOT AVAILABLE YET** until #133 configures GitHub Pages currently still claims `runmprc.com`, so its normal address redirects to the Netlify-served name. The source stops adding that claim, but #136/WEB-001 must publish and verify the provider setting before officers call Pages an independent copy. -The optional profile photo and officer **People finder** are **NOT AVAILABLE YET**. #505/#506 source supports a separate default-off choice and explicit name search with voluntary thumbnails; it is not the official roster and performs no photo query or facial recognition. Use only the synthetic source-review steps in [Events, shop, members, and money](./EVENTS_SHOP_MEMBERS.md). Do not open the production Account/Admin flow or use a real name or photo until #507 records privacy approval and backend-first live proof. +The optional profile-photo and officer People-finder functions are **NOT AVAILABLE YET**. #621 makes the frontend default an inert preview. My Account shows the future photo and independent finder-choice controls disabled. The administrator-guarded People finder shows its name field and Search button disabled. Neither preview reads saved state, accepts a file or name, creates a request, calls the directory services, or shows people. Officers inspect those protected disabled layouts only in synthetic local artifacts. After publication, signed-out public review proves only the exact revision, normal sign-in and administrator guards, and absence of a member-directory network request. Follow [Events, shop, members, and money](./EVENTS_SHOP_MEMBERS.md), and do not use a real name or photo. #507 must later prove privacy approval, scoped authorization, isolated staging, backend-first deployment and readback, and a separate reviewed source flip before connected use. The future connected feature is not the official roster and performs no photo query or facial recognition. ## Current safety warning diff --git a/src/App.test.jsx b/src/App.test.jsx index d60fa97f..cd3ec654 100644 --- a/src/App.test.jsx +++ b/src/App.test.jsx @@ -5782,7 +5782,7 @@ describe('Admin dashboard summary load-failure boundary', () => { 'Website accountsManage website roles; not a people finder', ); expect(document.querySelector('a[href="/admin/member-directory"]')).toHaveTextContent( - 'People finderSearch opted-in names; view voluntary photos', + 'People finderInterface preview; search not connected', ); expect(document.querySelector('a[href="/admin/products"]')).not.toBeNull(); expect(document.querySelector('a[href="/admin/orders"]')).not.toBeNull(); @@ -9694,7 +9694,7 @@ describe('Admin People finder route', () => { }); }); - test('routes an authorized officer to the separate explicit-submit People finder', async () => { + test('routes an authorized officer to the disconnected People finder preview', async () => { renderAdminMemberDirectory(); expect(await screen.findByRole('heading', { level: 1, name: 'People finder' })) @@ -9702,7 +9702,12 @@ describe('Admin People finder route', () => { expect(window.location.pathname).toBe('/admin/member-directory'); expect(screen.getByRole('textbox', { name: 'Search opted-in people by name' })) .toHaveValue(''); - expect(screen.getByRole('button', { name: 'Search' })).toBeEnabled(); + expect(screen.getByRole('textbox', { name: 'Search opted-in people by name' })) + .toBeDisabled(); + expect(screen.getByRole('button', { name: 'Search' })).toBeDisabled(); + expect(screen.getByRole('status')).toHaveTextContent( + 'Interface preview — search is not connected.', + ); expect(screen.getByText(/does not accept a photo as a query/i)).toBeInTheDocument(); }); }); diff --git a/src/pages/account/Account.css b/src/pages/account/Account.css index 126b9b2f..ea8d0673 100644 --- a/src/pages/account/Account.css +++ b/src/pages/account/Account.css @@ -199,16 +199,36 @@ gap: 0.75rem; } +.member-directory-profile__controls { + min-width: 0; + align-items: stretch; +} + .member-directory-profile__controls, .member-directory-profile__warning { margin-top: 1rem; } +.member-directory-profile__preview { + display: flex; + flex-direction: column; + gap: 0.25rem; + margin: 1rem 0; + padding: 0.75rem; + color: #422006; + background: #fffbeb; + border: 2px solid #b45309; + border-radius: 0.375rem; + line-height: 1.5; +} + .member-directory-profile__photo { display: flex; flex-wrap: wrap; align-items: center; gap: 1rem; + width: 100%; + min-width: 0; } .member-directory-profile__photo img, @@ -233,6 +253,7 @@ } .member-directory-profile__photo-actions label, +.member-directory-profile__control-label, .member-directory-profile__visibility label { font-weight: 700; } @@ -250,9 +271,16 @@ max-width: 100%; } +.member-directory-profile__photo-actions { + flex: 1 1 15rem; + width: 100%; +} + .member-directory-profile__visibility { padding-top: 1rem; border-top: 1px solid #d1d5db; + min-width: 0; + overflow-wrap: anywhere; } .member-directory-profile__visibility input { diff --git a/src/pages/account/MemberDirectoryProfile.test.tsx b/src/pages/account/MemberDirectoryProfile.test.tsx index f6537edb..e0d55834 100644 --- a/src/pages/account/MemberDirectoryProfile.test.tsx +++ b/src/pages/account/MemberDirectoryProfile.test.tsx @@ -68,16 +68,19 @@ function renderProfile({ firebaseApp = app, uid = 'synthetic-user', hasDisplayName = true, + backendAvailable = true, }: { firebaseApp?: typeof app; uid?: string; hasDisplayName?: boolean; + backendAvailable?: boolean; } = {}) { return render( , ); } @@ -108,6 +111,87 @@ describe('My Account member directory profile', () => { }); }); + test('defaults to an inert, explicitly unavailable interface preview', () => { + const hostileApp = new Proxy({}, { + get() { + throw new Error('preview-must-not-inspect-app'); + }, + }) as any; + render( + , + ); + + expect(screen.getByRole('heading', { + level: 2, + name: 'Profile photo and officer finder', + })).toBeInTheDocument(); + expect(screen.getByRole('status')).toHaveTextContent( + 'Interface preview — not connected yet.', + ); + expect(screen.getByRole('status')).toHaveTextContent( + 'No photo or finder setting is read, uploaded, searched, or saved', + ); + expect(screen.getByLabelText('Profile photo preview')).toBeInTheDocument(); + const file = screen.getByLabelText('Add profile photo (not available yet)'); + const checkbox = screen.getByRole('checkbox', { + name: 'Let authorized officers find me by name (not available yet)', + }); + expect(file).toBeDisabled(); + expect(file).toHaveAttribute('accept', 'image/jpeg,image/png,image/webp'); + expect(file.getAttribute('aria-describedby')).toContain( + 'member-directory-preview-status', + ); + expect(checkbox).toBeDisabled(); + expect(checkbox).not.toBeChecked(); + expect(checkbox.getAttribute('aria-describedby')).toContain( + 'member-directory-preview-status', + ); + expect(screen.getByText(/uploading, replacing, or removing it will not turn on/i)) + .toBeInTheDocument(); + expect(screen.getByText(/will not accept a photo as a query/i)).toBeInTheDocument(); + expect(screen.getByText(/will not.*facial recognition or image matching/i)) + .toBeInTheDocument(); + + fireEvent.change(file, { + target: { + files: [new File(['synthetic'], 'synthetic.png', { type: 'image/png' })], + }, + }); + fireEvent.click(checkbox); + + expect(createMemberDirectoryRequestId).not.toHaveBeenCalled(); + expect(getMyMemberDirectoryProfile).not.toHaveBeenCalled(); + expect(setMyMemberDirectoryPhoto).not.toHaveBeenCalled(); + expect(removeMyMemberDirectoryPhoto).not.toHaveBeenCalled(); + expect(setMyMemberDirectoryVisibility).not.toHaveBeenCalled(); + expect(document.body).not.toHaveTextContent('preview-must-not-inspect-app'); + }); + + test('describes the future name prerequisite without enabling preview controls', () => { + render( + , + ); + + const checkbox = screen.getByRole('checkbox', { + name: 'Let authorized officers find me by name (not available yet)', + }); + expect(checkbox).toBeDisabled(); + expect(checkbox.getAttribute('aria-describedby')).toContain( + 'member-directory-name-required-preview', + ); + expect(screen.getByText(/full name in the Profile section will also be required/i)) + .toBeInTheDocument(); + expect(getMyMemberDirectoryProfile).not.toHaveBeenCalled(); + }); + test('loads missing settings as private by default with separate photo controls', async () => { renderProfile(); @@ -497,6 +581,7 @@ describe('My Account member directory profile', () => { app={otherApp} uid="other-synthetic-user" hasDisplayName + backendAvailable />, ); @@ -524,6 +609,7 @@ describe('My Account member directory profile', () => { app={otherApp} uid="other-synthetic-user" hasDisplayName + backendAvailable />, ); expect(await screen.findByRole('img', { name: 'Your current profile thumbnail' })) @@ -564,12 +650,21 @@ describe('My Account member directory profile', () => { expect(screen.getByRole('button', { name: 'Remove profile photo' })).toBeEnabled(); }); - test('keeps the light panel heading readable and bounds the native file input', () => { + test('keeps the light panel readable and contains the native file input at 320px', () => { const css = readFileSync(join(__dirname, 'Account.css'), 'utf8'); expect(css).toMatch(/\.member-directory-profile h2\s*\{[\s\S]*color:\s*#111827;/); expect(css).toMatch( /\.member-directory-profile__photo-actions,\s*\.member-directory-profile__photo-actions input\s*\{[\s\S]*min-width:\s*0;[\s\S]*max-width:\s*100%;/, ); + expect(css).toMatch( + /\.member-directory-profile__controls\s*\{[\s\S]*min-width:\s*0;[\s\S]*align-items:\s*stretch;/, + ); + expect(css).toMatch( + /\.member-directory-profile__photo\s*\{[\s\S]*width:\s*100%;[\s\S]*min-width:\s*0;/, + ); + expect(css).toMatch( + /\.member-directory-profile__photo-actions\s*\{[\s\S]*flex:\s*1 1 15rem;[\s\S]*width:\s*100%;/, + ); }); }); diff --git a/src/pages/account/MemberDirectoryProfile.tsx b/src/pages/account/MemberDirectoryProfile.tsx index ed4a208a..46fa223c 100644 --- a/src/pages/account/MemberDirectoryProfile.tsx +++ b/src/pages/account/MemberDirectoryProfile.tsx @@ -1,5 +1,6 @@ import { FirebaseApp } from 'firebase/app'; import React, { useEffect, useRef, useState } from 'react'; +import MEMBER_DIRECTORY_BACKEND_AVAILABLE from '../../services/account/memberDirectoryAvailability'; import { createMemberDirectoryRequestId, getMyMemberDirectoryProfile, @@ -18,6 +19,98 @@ const UNKNOWN_CHANGE_MESSAGE = 'We could not confirm that change. Do not make an const REJECTED_CHANGE_MESSAGE = 'That change was rejected before it was saved. Review the requirements and try again.'; const REQUEST_UNAVAILABLE_MESSAGE = 'This browser could not safely start that change. No setting was changed. Reload the page and try again.'; +function MemberDirectoryProfilePreview({ + hasDisplayName, +}: { + hasDisplayName: boolean; +}) { + return ( +
+

Profile photo and officer finder

+
+ Interface preview — not connected yet. + + No photo or finder setting is read, uploaded, searched, or saved from + this preview. + +
+

+ When connected, you will be able to add an optional profile photo. + Uploading, replacing, or removing it will not turn on the officer finder. +

+

+ When connected, authorized officers will be able to search by name and + see a voluntary thumbnail only after you turn on the separate finder + choice. A result will not prove current club membership, payment, or + eligibility. The finder will not accept a photo as a query or use facial + recognition or image matching. +

+ +
+
+
+ Photo preview +
+
+ + Add profile photo (not available yet) + + + + JPG, PNG, or WebP up to 2 MiB will be supported after the protected + backend is connected. + +
+
+ +
+ + {!hasDisplayName && ( +

+ A full name in the Profile section will also be required when the + finder is connected. +

+ )} +
+
+
+ ); +} + const appIdentities = new WeakMap(); let nextAppIdentity = 1; @@ -473,11 +566,17 @@ export default function MemberDirectoryProfile({ app, uid, hasDisplayName, + backendAvailable = MEMBER_DIRECTORY_BACKEND_AVAILABLE, }: { app: FirebaseApp; uid: string; hasDisplayName: boolean; + backendAvailable?: boolean; }) { + if (!backendAvailable) { + return ; + } + return (
People finder
- Search opted-in names; view voluntary photos + {MEMBER_DIRECTORY_BACKEND_AVAILABLE + ? 'Search opted-in names; view voluntary photos' + : 'Interface preview; search not connected'}
- + , ); } @@ -102,6 +106,57 @@ describe('Admin People finder', () => { }); }); + test('defaults to a guarded inert preview without obtaining directory context', () => { + (useServiceLocator as jest.Mock).mockImplementation(() => { + throw new Error('preview-must-not-obtain-directory-context'); + }); + render( + + + , + ); + + expect(screen.getByRole('heading', { level: 1, name: 'People finder' })) + .toBeInTheDocument(); + expect(screen.getByRole('status')).toHaveTextContent( + 'Interface preview — search is not connected.', + ); + expect(screen.getByRole('status')).toHaveTextContent( + 'No finder name is collected or sent, and no member-directory profiles or results are loaded.', + ); + const input = queryInput(); + const search = screen.getByRole('button', { name: 'Search' }); + expect(input).toBeDisabled(); + expect(input).toHaveValue(''); + expect(input).toHaveAttribute('autocomplete', 'off'); + expect(input.getAttribute('aria-describedby')).toContain( + 'member-directory-search-preview-status', + ); + expect(search).toBeDisabled(); + expect(search.getAttribute('aria-describedby')).toContain( + 'member-directory-search-preview-status', + ); + expect(screen.getByText(/when connected, search by the beginning/i)) + .toBeInTheDocument(); + expect(screen.getByText(/does not accept a photo as a query/i)).toBeInTheDocument(); + expect(screen.queryByRole('heading', { level: 2, name: 'Opted-in people' })) + .not.toBeInTheDocument(); + expect(document.querySelector('input[type="file"]')).toBeNull(); + + fireEvent.change(input, { target: { value: 'Synthetic Runner' } }); + fireEvent.submit(input.closest('form') as HTMLFormElement); + + expect(useServiceLocator).not.toHaveBeenCalled(); + expect(createMemberDirectorySearchRequestId).not.toHaveBeenCalled(); + expect(searchMemberDirectory).not.toHaveBeenCalled(); + expect(document.body).not.toHaveTextContent( + 'preview-must-not-obtain-directory-context', + ); + }); + test('explains the bounded opt-in finder and waits for explicit submit', () => { renderDirectory(); @@ -253,7 +308,7 @@ describe('Admin People finder', () => { future={{ v7_relativeSplatPath: true, v7_startTransition: true }} initialEntries={['/admin/member-directory']} > - + , ); expect(queryInput()).toHaveValue(''); diff --git a/src/pages/admin/members/AdminMemberDirectory.tsx b/src/pages/admin/members/AdminMemberDirectory.tsx index dacf2595..565230bb 100644 --- a/src/pages/admin/members/AdminMemberDirectory.tsx +++ b/src/pages/admin/members/AdminMemberDirectory.tsx @@ -4,6 +4,7 @@ import React, { } from 'react'; import { Link } from 'react-router-dom'; import SEO from '../../../components/SEO'; +import MEMBER_DIRECTORY_BACKEND_AVAILABLE from '../../../services/account/memberDirectoryAvailability'; import { useServiceLocator } from '../../../services/ServiceLocatorContext'; import { createMemberDirectorySearchRequestId, @@ -249,7 +250,69 @@ function SearchAttempt({ app }: { app: FirebaseApp }) { ); } -function MemberDirectoryRoute() { +function MemberDirectoryPreview() { + return ( + <> +
+ Interface preview — search is not connected. + + No finder name is collected or sent, and no member-directory profiles or + results are loaded. + +
+
event.preventDefault()} + > +
+ + +
+
+ + ); +} + +function ConnectedMemberDirectoryRoute() { const { services, isReady } = useServiceLocator(); const { user } = useAuth(); const app = isReady && services ? services.firebaseResources.app : null; @@ -276,7 +339,17 @@ function MemberDirectoryRoute() { ); } -function Inner() { +function MemberDirectoryRoute({ backendAvailable }: { backendAvailable: boolean }) { + return backendAvailable + ? + : ; +} + +function Inner({ backendAvailable }: { backendAvailable: boolean }) { + const introduction = backendAvailable + ? 'Search by the beginning of a person\'s current display name or any name part. Only website-account holders who turned on the optional officer finder can appear. A result does not prove current club membership, payment, eligibility, or a website role.' + : 'When connected, search by the beginning of a person\'s current display name or any name part. Only website-account holders who turned on the optional officer finder can appear. A result does not prove current club membership, payment, eligibility, or a website role.'; + return ( <> @@ -286,25 +359,26 @@ function Inner() {

People finder

- Search by the beginning of a person's current display name or any name - part. Only website-account holders who turned on the optional officer finder - can appear. A result does not prove current club membership, payment, - eligibility, or a website role. + {introduction}

Photos are voluntary. This page does not accept a photo as a query and does not use facial recognition, image matching, fuzzy matching, or a full account list.

- + ); } -export default function AdminMemberDirectory() { +export default function AdminMemberDirectory({ + backendAvailable = MEMBER_DIRECTORY_BACKEND_AVAILABLE, +}: { + backendAvailable?: boolean; +}) { return ( - + ); } diff --git a/src/pages/admin/members/AdminMembers.tsx b/src/pages/admin/members/AdminMembers.tsx index 4c3f75fc..e5119733 100644 --- a/src/pages/admin/members/AdminMembers.tsx +++ b/src/pages/admin/members/AdminMembers.tsx @@ -3,6 +3,7 @@ import React, { } from 'react'; import { Link } from 'react-router-dom'; import SEO from '../../../components/SEO'; +import MEMBER_DIRECTORY_BACKEND_AVAILABLE from '../../../services/account/memberDirectoryAvailability'; import AdminGuard from '../AdminGuard'; import { useServiceLocator } from '../../../services/ServiceLocatorContext'; import { useAuth } from '../../../services/hooks/useAuth'; @@ -203,7 +204,9 @@ function Inner() { People finder {' '} - only to search people who opted in. + {MEMBER_DIRECTORY_BACKEND_AVAILABLE + ? 'only to search people who opted in.' + : 'to review the interface. Search is not connected yet.'}

{showAccountResults && ( diff --git a/src/services/account/memberDirectoryAvailability.ts b/src/services/account/memberDirectoryAvailability.ts new file mode 100644 index 00000000..295909b0 --- /dev/null +++ b/src/services/account/memberDirectoryAvailability.ts @@ -0,0 +1,9 @@ +/** + * Frontend routing only. This is not an authorization boundary. + * + * Keep the production default fail-closed until the protected backend-first + * release has deployed and read back every required directory dependency. + */ +const MEMBER_DIRECTORY_BACKEND_AVAILABLE: boolean = false; + +export default MEMBER_DIRECTORY_BACKEND_AVAILABLE; diff --git a/tests/ci-workflow.test.js b/tests/ci-workflow.test.js index 186d65c5..8611c74d 100644 --- a/tests/ci-workflow.test.js +++ b/tests/ci-workflow.test.js @@ -43,7 +43,7 @@ const CLEAN_COMMANDS = Object.freeze([ 'test -z "$frontend_git_status"', ]); const LINT_SCRIPT = 'node .github/scripts/check-frontend-lint.cjs'; -const EXPECTED_LINT_FILES = 119; +const EXPECTED_LINT_FILES = 120; const EXPECTED_LINT_ERRORS = 113; const EXPECTED_LINT_WARNINGS = 6; const YAML_TO_JSON = [