@@ -243,25 +328,55 @@ function AccountContent({ user }: { user: NonNullable
)}
{editing && (
-
-
- {saveError &&
{saveError}
}
+
+
+
+ Up to
+ {' '}
+ {MEMBER_PROFILE_LIMITS.fullName}
+ {' '}
+ characters.
+
+
+
+
+
+ Up to
+ {' '}
+ {MEMBER_PROFILE_LIMITS.phoneNumber}
+ {' '}
+ characters.
+
+
+ {saveError && (
+
{saveError}
+ )}
>
);
diff --git a/src/services/account/accountService.test.ts b/src/services/account/accountService.test.ts
new file mode 100644
index 00000000..2d9b6d46
--- /dev/null
+++ b/src/services/account/accountService.test.ts
@@ -0,0 +1,96 @@
+/* eslint-env jest */
+
+import {
+ doc, serverTimestamp, updateDoc,
+} from 'firebase/firestore';
+import { getFunctions, httpsCallable } from 'firebase/functions';
+import {
+ ensureMyProfile,
+ updateMyProfile,
+ validateMemberProfileFields,
+} from './accountService';
+
+jest.mock('firebase/firestore', () => ({
+ doc: jest.fn(() => ({ path: 'members/synthetic-user' })),
+ getDoc: jest.fn(),
+ updateDoc: jest.fn(),
+ serverTimestamp: jest.fn(() => ({ __serverTimestamp: true })),
+}));
+
+jest.mock('firebase/functions', () => ({
+ getFunctions: jest.fn(() => ({ name: 'synthetic-functions' })),
+ httpsCallable: jest.fn(),
+}));
+
+const firestore = { name: 'synthetic-firestore' } as any;
+const app = { name: 'synthetic-app' } as any;
+
+describe('account profile service', () => {
+ beforeEach(() => {
+ jest.clearAllMocks();
+ (doc as jest.Mock).mockReturnValue({ path: 'members/synthetic-user' });
+ (serverTimestamp as jest.Mock).mockReturnValue({ __serverTimestamp: true });
+ (getFunctions as jest.Mock).mockReturnValue({ name: 'synthetic-functions' });
+ (updateDoc as jest.Mock).mockResolvedValue(undefined);
+ });
+
+ test('calls the profile bootstrap with an empty request and returns uniform readiness', async () => {
+ const callable = jest.fn().mockResolvedValue({ data: { ready: true } });
+ (httpsCallable as jest.Mock).mockReturnValue(callable);
+
+ await expect(ensureMyProfile(app)).resolves.toEqual({ ready: true });
+
+ expect(getFunctions).toHaveBeenCalledWith(app);
+ expect(httpsCallable).toHaveBeenCalledWith(
+ { name: 'synthetic-functions' },
+ 'ensureMemberProfile',
+ );
+ expect(callable).toHaveBeenCalledWith({});
+ });
+
+ test('writes only normalized name, phone, and a server timestamp', async () => {
+ await updateMyProfile(firestore, 'synthetic-user', {
+ fullName: ' Synthetic Member ',
+ phoneNumber: ' 555-0100 ',
+ });
+
+ expect(doc).toHaveBeenCalledWith(firestore, 'members', 'synthetic-user');
+ expect(updateDoc).toHaveBeenCalledWith(
+ { path: 'members/synthetic-user' },
+ {
+ fullName: 'Synthetic Member',
+ phoneNumber: '555-0100',
+ updatedAt: { __serverTimestamp: true },
+ },
+ );
+ expect(serverTimestamp).toHaveBeenCalledTimes(1);
+ });
+
+ test('accepts the exact 200 and 40 unit boundaries, including emoji', async () => {
+ const fullName = 'π'.repeat(100);
+ const phoneNumber = 'π'.repeat(20);
+
+ expect(validateMemberProfileFields({ fullName, phoneNumber })).toEqual({
+ valid: true,
+ fields: { fullName, phoneNumber },
+ });
+ await expect(updateMyProfile(
+ firestore,
+ 'synthetic-user',
+ { fullName, phoneNumber },
+ )).resolves.toBeUndefined();
+ expect(updateDoc).toHaveBeenCalledTimes(1);
+ });
+
+ test.each([
+ ['an ASCII name', { fullName: 'n'.repeat(201), phoneNumber: '' }, 'Full name'],
+ ['an emoji name', { fullName: 'π'.repeat(101), phoneNumber: '' }, 'Full name'],
+ ['an ASCII phone', { fullName: '', phoneNumber: '1'.repeat(41) }, 'Phone'],
+ ['an emoji phone', { fullName: '', phoneNumber: 'π'.repeat(21) }, 'Phone'],
+ ])('rejects %s over the Rules limit before Firestore', async (_label, fields, field) => {
+ await expect(updateMyProfile(firestore, 'synthetic-user', fields))
+ .rejects.toThrow(`${field} must be`);
+ expect(updateDoc).not.toHaveBeenCalled();
+ expect(serverTimestamp).not.toHaveBeenCalled();
+ });
+});
diff --git a/src/services/account/accountService.ts b/src/services/account/accountService.ts
index 07ba6723..40f12134 100644
--- a/src/services/account/accountService.ts
+++ b/src/services/account/accountService.ts
@@ -5,6 +5,46 @@ import { FirebaseApp } from 'firebase/app';
import { getFunctions, httpsCallable } from 'firebase/functions';
import { Member, MemberEditableFields } from '../../types/member';
+export const MEMBER_PROFILE_LIMITS = {
+ fullName: 200,
+ phoneNumber: 40,
+} as const;
+
+export interface ValidatedMemberProfileFields {
+ fullName: string;
+ phoneNumber: string;
+}
+
+export type MemberProfileValidation =
+ | { valid: true; fields: ValidatedMemberProfileFields }
+ | { valid: false; message: string };
+
+export function validateMemberProfileFields(
+ fields: MemberEditableFields,
+): MemberProfileValidation {
+ const fullName = fields.fullName.trim();
+ const phoneNumber = fields.phoneNumber.trim();
+ if (fullName.length > MEMBER_PROFILE_LIMITS.fullName) {
+ return { valid: false, message: 'Full name must be 200 characters or fewer.' };
+ }
+ if (phoneNumber.length > MEMBER_PROFILE_LIMITS.phoneNumber) {
+ return { valid: false, message: 'Phone must be 40 characters or fewer.' };
+ }
+ return { valid: true, fields: { fullName, phoneNumber } };
+}
+
+export async function ensureMyProfile(
+ app: FirebaseApp,
+): Promise<{ ready: true }> {
+ const functions = getFunctions(app);
+ const callable = httpsCallable, { ready: true }>(
+ functions,
+ 'ensureMemberProfile',
+ );
+ const result = await callable({});
+ return result.data;
+}
+
export async function getMyProfile(db: Firestore, uid: string): Promise {
const ref = doc(db, 'members', uid);
const snap = await getDoc(ref);
@@ -29,10 +69,14 @@ export async function updateMyProfile(
uid: string,
fields: MemberEditableFields,
): Promise {
+ const validation = validateMemberProfileFields(fields);
+ if (!validation.valid) throw new Error(validation.message);
+ const { fullName, phoneNumber } = validation.fields;
+
const ref = doc(db, 'members', uid);
await updateDoc(ref, {
- fullName: fields.fullName.trim() || null,
- phoneNumber: fields.phoneNumber.trim() || '',
+ fullName: fullName || null,
+ phoneNumber,
updatedAt: serverTimestamp(),
});
}
diff --git a/tests/firestore-rules/members.test.js b/tests/firestore-rules/members.test.js
index 3c407971..465571dd 100644
--- a/tests/firestore-rules/members.test.js
+++ b/tests/firestore-rules/members.test.js
@@ -97,6 +97,38 @@ describe('members collection', () => {
}));
});
+ test('profile Unicode boundaries match the browser validator', async () => {
+ await seed('members/u1', SAMPLE_MEMBER);
+ const me = await db({ uid: 'u1', role: 'unverified' });
+
+ await assertSucceeds(me.doc('members/u1').update({
+ // Firestore Rules and browser maxlength both count these astral
+ // symbols as two UTF-16 units. Keep the client validation aligned
+ // with the behavior proven by this emulator suite.
+ fullName: 'π'.repeat(100),
+ phoneNumber: 'π'.repeat(20),
+ updatedAt: new Date(),
+ }));
+ });
+
+ test.each([
+ ['a Unicode name over the 200-unit limit', {
+ fullName: 'π'.repeat(101),
+ phoneNumber: '',
+ }],
+ ['a Unicode phone over the 40-unit limit', {
+ fullName: '',
+ phoneNumber: 'π'.repeat(21),
+ }],
+ ])('user CANNOT set %s', async (_label, fields) => {
+ await seed('members/u1', SAMPLE_MEMBER);
+ const me = await db({ uid: 'u1', role: 'unverified' });
+ await assertFails(me.doc('members/u1').update({
+ ...fields,
+ updatedAt: new Date(),
+ }));
+ });
+
test('user CANNOT update a missing profile document', async () => {
const me = await db({ uid: 'u1', role: 'unverified' });
await assertFails(me.doc('members/u1').update({
From 8dc0c0b9e3a212c322a87790f86faf44d80721ec Mon Sep 17 00:00:00 2001
From: Dave Liu <7david12liu@gmail.com>
Date: Mon, 13 Jul 2026 01:38:18 -0700
Subject: [PATCH 2/4] docs: explain safe profile recovery
---
SYSTEM_DESIGN.md | 44 ++++++++++++++++++++++-
docs/officers/EMERGENCY_AND_RECOVERY.md | 28 +++++++++++++++
docs/officers/EVENTS_SHOP_MEMBERS.md | 47 +++++++++++++++++++++++++
docs/officers/SYSTEM_MAPS.md | 19 ++++++++++
4 files changed, 137 insertions(+), 1 deletion(-)
diff --git a/SYSTEM_DESIGN.md b/SYSTEM_DESIGN.md
index c1f3f0f1..8817370a 100644
--- a/SYSTEM_DESIGN.md
+++ b/SYSTEM_DESIGN.md
@@ -227,7 +227,7 @@ The first release may continue using `admin` in the UI, but server endpoints and
| `events/{eventId}/registrations/{registrationId}` | Participant identity, waiver evidence, payment references, lifecycle | Cloud Functions and admins today; target Cloud Functions only | Restricted PII and financial metadata |
| `products/{productId}` | Merchandise catalog | Admin client today | Public plus internal configuration |
| `orders/{orderId}` | Buyer, shipping, payment, and fulfillment data | Cloud Functions and admins today; target Cloud Functions only | Restricted PII and financial metadata |
-| `members/{uid}` | Profile and role mirror | Signup function, self-service allowlist, admins | Confidential |
+| `members/{uid}` | Profile and role mirror | Create-once signup/recovery Functions; self-service name/phone allowlist; server role operations | Confidential |
| `members/{uid}/connections/{provider}` | Non-secret connection metadata | Cloud Functions | Confidential |
| `members/{uid}/secrets/{provider}` | OAuth tokens | Cloud Functions | Restricted secret |
| `promoCodes/{id}` | Intended promotion configuration | Admin only | Confidential; currently not integrated into checkout validation |
@@ -270,6 +270,48 @@ The following are correctness rules, not UI preferences:
## 8. Core workflows
+### 8.0 Account profile setup and recovery
+
+Issue [#118](https://github.com/Run-MPRC/Run-MPRC.github.io/issues/118) adds a create-once recovery path for accounts whose `members/{uid}` record is missing after the Firebase cutover. It is a source design until the exact Function, current Rules, and dependent website are deployed backend-first and proven with a synthetic account.
+
+```mermaid
+sequenceDiagram
+ actor M as Signed-in member
+ participant W as Account page
+ participant F as ensureMemberProfile Function
+ participant A as Firebase Auth Admin
+ participant D as Firestore
+
+ M->>W: Open My Account
+ W->>F: Empty request + verified Firebase sign-in
+ alt setup fails
+ F-->>W: Generic unavailable result
+ W-->>M: Hide Edit; show retry/sign-out path
+ else setup continues
+ F->>D: Read only members/{caller UID}
+ alt profile exists
+ D-->>F: Exists
+ F-->>W: ready=true; no write or claim change
+ else profile missing
+ F->>A: Load caller's authoritative Auth record
+ F->>D: Transactionally recheck and create pending profile once
+ F-->>W: ready=true
+ end
+ W->>D: Read caller profile through Firestore Rules
+ alt read succeeds
+ D-->>W: Profile
+ W-->>M: Show profile and name/phone Edit
+ else read fails
+ D-->>W: Generic failure
+ W-->>M: Hide Edit; show retry/sign-out path
+ end
+ end
+```
+
+The callable accepts no UID or profile fields. A missing record receives identity fields from Firebase Auth and `role: unverified`; it never copies a member/admin claim, dues, payment, or discount state. Existing records and claims remain unchanged. This may expose a pre-existing claim/profile mismatch instead of guessing how to repair it; the identity/membership workflow must resolve that mismatch explicitly. App Check is required by the shared boundary only when deployed enforcement is configured, so release evidence must prove that setting before calling App Check live.
+
+The server chooses initial timestamps. The current self-edit path sends a Firestore server timestamp, but the Rules source type-checks rather than independently proves that edit timestamp. Do not describe arbitrary profile edit timestamps as server-authoritative until a coordinated Rules/API issue closes that residual.
+
### 8.1 Paid race registration
```mermaid
diff --git a/docs/officers/EMERGENCY_AND_RECOVERY.md b/docs/officers/EMERGENCY_AND_RECOVERY.md
index 1ef71cc6..70710ebc 100644
--- a/docs/officers/EMERGENCY_AND_RECOVERY.md
+++ b/docs/officers/EMERGENCY_AND_RECOVERY.md
@@ -28,12 +28,40 @@
| Wrong public text, image, or link | Ask the platform owner for one reviewed revert pull request. |
| Site unavailable or old after a merge | Ask the platform owner to check Netlify and GitHub Pages separately; do not change DNS. |
| Login or member access is wrong | Stop role requests and contact the identity/platform owners. |
+| Profile Save says permission is missing | Ask the member to stop, sign out, and wait. Open a new redacted incident through [Request a change](./REQUEST_A_CHANGE.md). Do not edit the database, delete the login, recreate the account, or grant a role. |
| Private member information is visible | Contact platform and privacy owners immediately; they choose containment through an approved service procedure. Do not change permissions yourself. |
| Unexpected payment, refund, order, or signup | Contact treasurer and platform owners. There is no proven no-code payment kill switch. Do not test with real money. |
| Password, secret, or recovery code exposed | Contact the owning service's two approved owners. They revoke/rotate through the service-specific procedure and record evidence. Do not rotate it from this generic guide. |
Live commerce is not approved as of 2026-07-12. If the public site appears to accept a real payment, do not test it with a real card.
+### Missing profile or profile permission error
+
+**Status:** automatic repair is **NOT AVAILABLE YET**. These are current stop-and-escalate steps only.
+
+**Purpose:** prevent a failed account repair from changing access or private data.
+
+**Approver:** membership lead plus platform/security owner.
+
+**Prerequisites:** the time, the public `/account` address, and a redacted description with no member details.
+
+1. Ask the member to stop using Save.
+2. Ask the member to sign out.
+3. Open a new redacted incident by following [Request a change](./REQUEST_A_CHANGE.md).
+4. Link [#118](https://github.com/Run-MPRC/Run-MPRC.github.io/issues/118) only as past repair work. Do not reopen it or add member incidents to it.
+5. Share only the time and the plain symptom.
+6. Wait for a made-up account test and separate website, server Function, and database-permission proof.
+
+**Expected result:** no manual account or database change occurs. The member retries only after the automated repair is proven live.
+
+**Stop conditions:** stop if a step needs a real profile, private screenshot, direct database change, account deletion, account recreation, or role change.
+
+**Success proof:** use the checklist in [Events, shop, members, and money](./EVENTS_SHOP_MEMBERS.md#profile-permission-error).
+
+**Undo:** ask the platform owner to prepare, approve, publish, and verify a reviewed revert or safe roll-forward. Do not delete the account or profile.
+
+**Escalation:** membership lead plus identity/security owner; add the privacy owner if private details were exposed.
+
## Safe AI message
> Investigate this MPRC incident without changing production: **[symptom]** at **[URL]** starting **[time]**. Preserve evidence, redact private data, identify the last known-good version, and propose the smallest revert or safe roll-forward. Do not force-push, delete records, refund money, rotate secrets, change DNS, or deploy until the named owners approve.
diff --git a/docs/officers/EVENTS_SHOP_MEMBERS.md b/docs/officers/EVENTS_SHOP_MEMBERS.md
index 61d15b5c..8981f3bb 100644
--- a/docs/officers/EVENTS_SHOP_MEMBERS.md
+++ b/docs/officers/EVENTS_SHOP_MEMBERS.md
@@ -55,6 +55,53 @@ There is currently no proven no-code switch that safely stops all new Stripe pay
If any proof is missing, report the change as **not live**.
+## Profile permission error
+
+**Status: AUTOMATIC REPAIR NOT LIVE YET**
+
+**Purpose:** help a signed-in member whose profile is missing or cannot be read.
+
+**Approver:** membership lead plus platform/security owner.
+
+**Prerequisites:** a new redacted incident from [Request a change](./REQUEST_A_CHANGE.md), made-up test accounts, an isolated Firebase test project, and an approved release plan. Issues [#118](https://github.com/Run-MPRC/Run-MPRC.github.io/issues/118) and [#105](https://github.com/Run-MPRC/Run-MPRC.github.io/issues/105) are engineering references, not places to add member details.
+
+The planned safe flow is automatic. An officer does not create or edit the member record.
+
+```mermaid
+flowchart LR
+ A["Member opens My Account"] --> B["Server checks only this member's profile"]
+ B --> C{"Profile exists?"}
+ C -- "Yes" --> D["Keep it unchanged"]
+ C -- "No" --> E["Create one pending profile"]
+ D --> F["Read through normal permissions"]
+ E --> F
+ F -- "Success" --> G["Edit name and phone"]
+ F -- "Failure" --> H["Hide Edit and show Try again"]
+```
+
+In words: the server preserves an existing profile or creates one pending profile for the signed-in person; editing stays hidden unless the normal read succeeds.
+
+Safe officer steps:
+
+1. Ask the member to stop retrying Save.
+2. Record the time and the public `/account` page address.
+3. Do not record their name, email, phone, login code, or screenshot of profile details.
+4. Ask them to sign out.
+5. Open a new redacted incident through [Request a change](./REQUEST_A_CHANGE.md).
+6. Link #118 only as past repair work. Do not reopen it or add member incidents to it.
+7. Wait for the platform owner to test with a made-up account.
+8. Tell the member to retry only after the website, server Function, database permissions, and live behavior are each proven.
+
+**Expected result:** after all release proof is complete, the member sees a profile or a plain temporary-unavailable message. A missing profile is displayed as pending or unverified. The repair does not grant, remove, or change actual access. If displayed profile status and actual access disagree, stop and escalate.
+
+**Stop conditions:** stop if anyone proposes a direct database change, login-account deletion, account recreation, role grant, real-member test, or website-only release before the server Function is live.
+
+**Success proof:** name the merged pull request, website commit, Function deployment, database-permission deployment, made-up staged account test, `runmprc.com` check, and separate live-state check. A green workflow with βskipping Firebase deployβ is not proof.
+
+**Undo:** ask the platform owner to prepare, approve, publish, and verify a reviewed revert or safe roll-forward. Never undo by deleting a member profile or login account.
+
+**Escalation:** membership lead plus identity/security owner. Add the privacy owner if private information appeared. Email landing in spam is a separate delivery problem; do not treat it as proof of this permission failure.
+
## Admin screens β NOT AVAILABLE YET
Admin event and product editors exist in source, but their live permissions, backup, preview, and rollback behavior have not been approved. Saving can write directly to production Firestore. Officers must not use these screens as a continuity procedure yet.
diff --git a/docs/officers/SYSTEM_MAPS.md b/docs/officers/SYSTEM_MAPS.md
index b1e36a83..282bd7fd 100644
--- a/docs/officers/SYSTEM_MAPS.md
+++ b/docs/officers/SYSTEM_MAPS.md
@@ -82,6 +82,25 @@ flowchart TD
In words: people use their own accounts, at least two officers cover every service, and recovery information stays in the approved password manager rather than the repository or AI.
+## Member profile setup and recovery β NOT LIVE YET
+
+```mermaid
+flowchart TD
+ SignIn["Member signs in"] --> Ensure["Server checks this member's profile"]
+ Ensure --> Existing{"Profile already exists?"}
+ Existing -- "Yes" --> Preserve["Keep every field and role unchanged"]
+ Existing -- "No" --> Pending["Create one pending profile"]
+ Preserve --> Read["Website reads through normal Firebase permissions"]
+ Pending --> Read
+ Read -- "Read succeeds" --> Edit["Member may edit name and phone only"]
+ Read -- "Setup or read fails" --> Safe["Edit stays hidden; Try again or sign out"]
+ Ensure -. "does not grant, remove, or change" .-> Access["Membership, payment, discount, or admin access"]
+```
+
+In words: a signed-in member gets their existing profile unchanged or one new pending profile; the website permits only name and phone editing after the normal permission check succeeds. The repair does not change actual access. If displayed profile status and actual access disagree, stop and escalate.
+
+Issue [#118](https://github.com/Run-MPRC/Run-MPRC.github.io/issues/118) owns this repair. Source code and local tests do not make it live. The website, server Function, database permissions, and made-up live check must each be proven separately.
+
## Emergency decision
```mermaid
From 12285582e7c4ab30aac762587588f7c41803df3d Mon Sep 17 00:00:00 2001
From: Dave Liu <7david12liu@gmail.com>
Date: Mon, 13 Jul 2026 01:44:21 -0700
Subject: [PATCH 3/4] docs: record profile recovery release gate
---
OPERATIONS_RUNBOOK.md | 22 ++++++++++++++++++++++
SECURITY.md | 4 +++-
2 files changed, 25 insertions(+), 1 deletion(-)
diff --git a/OPERATIONS_RUNBOOK.md b/OPERATIONS_RUNBOOK.md
index 8eedd6e8..4c6efd87 100644
--- a/OPERATIONS_RUNBOOK.md
+++ b/OPERATIONS_RUNBOOK.md
@@ -484,6 +484,28 @@ For database/payment changes, roll-forward is often safer than reverting code be
Never use destructive Git or Firestore reset operations on production as a rollback.
+### Missing member profile repair β NOT AVAILABLE YET
+
+Issue [#118](https://github.com/Run-MPRC/Run-MPRC.github.io/issues/118) replaces manual repair with create-once signup/recovery Functions and a fail-closed account page. Do not use it as a production procedure until #105 supplies a protected staging and deployment path.
+
+Required release order:
+
+1. Confirm the exact merged #100 Rules revision is the reviewed dependency.
+2. Deploy those Rules plus both `createMemberOnSignUp` and `ensureMemberProfile` to isolated staging.
+3. Record the deployed App Check policy. When enforcement is enabled, prove missing or invalid App Check is rejected. When it is not enabled, record that open gate and do not call the boundary enforced.
+4. Use a made-up signed-in account with no profile.
+5. Prove one pending profile is created and retries preserve it byte-for-byte.
+6. Prove existing member/admin profiles and custom claims remain unchanged.
+7. Prove anonymous, injected-field, backend-down, missing-profile, Rules-denial, and cross-user paths fail safely. Prove App Check behavior matches the policy recorded in step 3.
+8. Record and rehearse either the last-compatible Rules/Functions/website rollback or a reviewed safe roll-forward in staging.
+9. Deploy and verify the exact #100 Rules plus both revised Functions in the approved target Firebase project.
+10. If that backend deployment is partial or verification fails, do not deploy the website. Restore the reviewed backend set or safely roll forward, then repeat verification.
+11. Deploy the dependent website only after target-backend proof.
+12. Check the exact website revision and the plain retry state.
+13. Repeat only the approved synthetic live smoke; never use a real member record.
+
+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.
+
## 17. Verification evidence boundaries
The earlier combined pre-extraction working tree reported 7 frontend tests, 4 SPA callback tests, 45 Functions tests, and 185 Firestore Rules tests. Those numbers combine queued #99, #100, #101, and #103 work. They are historical planning evidence only and are **not reproducible proof for #104 or current `main`**.
diff --git a/SECURITY.md b/SECURITY.md
index 78a3969a..bdb5b8a7 100644
--- a/SECURITY.md
+++ b/SECURITY.md
@@ -86,6 +86,7 @@ The findings below describe the repository at the start of the 2026-07-12 assess
| RISK-036 | #135 adds a manual exact-commit source gate, fixed profile-recovery targets, backend-first order, missing-config failure, and Git-triggered Netlify production containment. Protected environments/OIDC, isolated staging, live-Netlify publication, and rollback evidence are still absent. | Complete #133 and #136, provision isolated staging, protect the Netlify release path under WEB-001, and rehearse rollback/safe roll-forward before production. |
| 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. |
+| RISK-039 | Some authenticated accounts can lack `members/{uid}` after the Firebase cutover; the account screen hid the read failure and exposed an update that could only fail. Manual database/account repair could corrupt roles or private data. | Use an authenticated create-once server bootstrap, keep browser creation denied, fail the UI closed, and prove backend-first deployment with synthetic accounts. |
### Source remediation ledger
@@ -103,7 +104,8 @@ These entries are implementation evidence, not a production risk-acceptance deci
| Part of RISK-024 | SEC-001 protects token documents from every browser client. | OAUTH-001 must still add transactional refresh/versioning, scope/account binding, disconnect/revocation, IAM/encryption decision, and redacted lifecycle audit. |
| Part of RISK-027 | #99 keeps App Check, Firebase Analytics, and Sentry from initializing in local/test even when public provider configuration is present. Initial capability callback routes also fail closed before App Check, Firebase Analytics, or Sentry startup. | #111 still owns general hosted URL/event/log redaction, already-initialized SPA transitions, replay/consent/retention policy, payload inspection, and provider evidence. ABUSE-001A must defer the three callback callables; DATA-001A and OAUTH-001C within canonical tracker [#88](https://github.com/Run-MPRC/Run-MPRC.github.io/issues/88) own their safe protected handoffs. |
| Part of RISK-035 | The Jest environment is repaired, the stale frontend smoke test is replaced, #124 adds the complete frontend suite as a named blocking hosted step, and #126 adds the standalone SPA callback suite as its own named blocking step. | Keep exact hosted evidence on #124/#126; make lint non-mutating/fail-closed, prove required branch checks, and add the broader TEST-001 suite. |
-| RISK-026, RISK-036 | #135 replaces automatic frontend-first/fail-open GitHub deployment with a tested manual exact-current-commit gate. It rechecks newest CI and the retained artifact after approval, pins release Actions, uses the lockfile Firebase CLI, validates protected configuration before backend install/authentication, idempotently verifies fixed Rules/Function revisions before Pages publication, removes cloud credentials from website preparation/publication, stops Git-triggered Netlify production builds, and removes the Pages CNAME from future artifacts. | Before #135 closes, its merged source must pass the safe missing-authority dispatch with zero Firebase/Pages/Netlify publication. #133 must configure protected environments and least-privilege OIDC/WIF; #136 must prove staged/target deployment and clearing/readback of the existing Pages `runmprc.com` claim; a protected WEB-001 child must establish Netlify publication and rollback. No Firebase, Pages, live-host, or provider-setting change is proven by source/static tests alone. |
+| RISK-026, RISK-036 | #135 merged through PR #138 as `9eafab1217aff7058c42240aaba72d7b93f8ed24`, replacing automatic frontend-first/fail-open GitHub deployment with a tested manual exact-current-commit gate. Post-merge staging and synthetic production probes failed closed before authentication or mutation, and published neither Firebase nor Pages. | #133 must configure protected environments and least-privilege OIDC/WIF; #136 must prove staged/target deployment and clearing/readback of the existing Pages `runmprc.com` claim; a protected WEB-001 child must establish Netlify publication and rollback. No Firebase, Pages, live-host, or provider-setting change is proven by source/static tests alone. |
+| RISK-039 | The #118 source slice uses an empty authenticated callable request, bounded Firebase Auth identity fields, one transactional create-only helper shared with signup, constant responses, generic failures, and a UI that hides Edit until setup and the Rules-protected read succeed. Signup and recovery never change custom claims; browser profile creation remains denied. | Source review/merge is not deployment. Under #105, deploy the exact #100 Rules plus both `createMemberOnSignUp` and `ensureMemberProfile` before the website, prove App Check policy, use a synthetic staged account, verify rollback, then record website, Function, Rules, and live behavior separately. Never repair a real profile manually. |
## 5. Threat model
From 6381907a6e243335094cb25122ef773943f7f19d Mon Sep 17 00:00:00 2001
From: Dave Liu <7david12liu@gmail.com>
Date: Mon, 13 Jul 2026 03:02:37 -0700
Subject: [PATCH 4/4] docs: clarify profile incident reference
---
docs/officers/EMERGENCY_AND_RECOVERY.md | 2 +-
docs/officers/EVENTS_SHOP_MEMBERS.md | 2 +-
2 files changed, 2 insertions(+), 2 deletions(-)
diff --git a/docs/officers/EMERGENCY_AND_RECOVERY.md b/docs/officers/EMERGENCY_AND_RECOVERY.md
index 70710ebc..98f66acf 100644
--- a/docs/officers/EMERGENCY_AND_RECOVERY.md
+++ b/docs/officers/EMERGENCY_AND_RECOVERY.md
@@ -48,7 +48,7 @@ Live commerce is not approved as of 2026-07-12. If the public site appears to ac
1. Ask the member to stop using Save.
2. Ask the member to sign out.
3. Open a new redacted incident by following [Request a change](./REQUEST_A_CHANGE.md).
-4. Link [#118](https://github.com/Run-MPRC/Run-MPRC.github.io/issues/118) only as past repair work. Do not reopen it or add member incidents to it.
+4. Use [#118](https://github.com/Run-MPRC/Run-MPRC.github.io/issues/118) only as engineering context. Do not add member incidents or private details to it.
5. Share only the time and the plain symptom.
6. Wait for a made-up account test and separate website, server Function, and database-permission proof.
diff --git a/docs/officers/EVENTS_SHOP_MEMBERS.md b/docs/officers/EVENTS_SHOP_MEMBERS.md
index 8981f3bb..31baea4e 100644
--- a/docs/officers/EVENTS_SHOP_MEMBERS.md
+++ b/docs/officers/EVENTS_SHOP_MEMBERS.md
@@ -88,7 +88,7 @@ Safe officer steps:
3. Do not record their name, email, phone, login code, or screenshot of profile details.
4. Ask them to sign out.
5. Open a new redacted incident through [Request a change](./REQUEST_A_CHANGE.md).
-6. Link #118 only as past repair work. Do not reopen it or add member incidents to it.
+6. Use #118 only as engineering context. Do not add member incidents or private details to it.
7. Wait for the platform owner to test with a made-up account.
8. Tell the member to retry only after the website, server Function, database permissions, and live behavior are each proven.