Repository navigation
DATA-001A3 — Require UID ownership for My Account registrations #374
Description
Officer impact: My Account will show only race registrations already linked to the signed-in website account. A matching email address will no longer reveal or silently claim a registration. A registration made while signed out may stay absent until a future reviewed officer-association workflow exists.
Officer documentation: docs/officers/EVENTS_SHOP_MEMBERS.md — one named source-only procedure, Mermaid diagram, and text alternative for the My Account registration-ownership boundary.
Deployment evidence: None at issue creation. Source, tests, review, merge, website publication, exact runmprc.com revision, Firebase Functions deployment, production data, and live behavior must be recorded separately. A merge or green workflow does not make the Functions change live.
Outcome
Make exact stored Firebase UID ownership the only authority used by listMyRegistrations.
My Account may return a registration only when its stored primitive uid exactly matches the authenticated Firebase UID. A matching email address, even when verified, is not read authority and must not start a registration query. Any later association of an older or signed-out registration belongs to the explicit, audited officer workflow in #115.
Current defect
On exact main df91460d4e155e22f6bdd4e149cdc9d01a32d0f7, functions/listMyRegistrations.js:
- queries registrations by the authenticated UID;
- reads
context.auth.token.emailandemail_verified; - when the email is verified, performs a second collection-group query by
runner.email; and - merges every email match into the My Account response, including a record with
uid: nullor a different UID.
The response can include runner name, email, shirt size, amount, status, timestamps, event title, date, and location.
Concrete failure: account A registers runner B while signed in. The record stores account A's UID and runner B's email. If runner B later verifies that address, both accounts can read the same registration. Older anonymous registrations, reassigned addresses, shared addresses, and mistyped addresses create the same risk.
This contradicts #115: a normalized email match may be shown to an authorized officer as a suggestion, but it never auto-links, grants access, or silently resolves an identity collision.
Security and privacy invariant
For one authenticated callable request:
- Preserve the existing App Check requirement and unauthenticated denial order.
- Read the authoritative Firebase UID from the authenticated context.
- Perform one registration collection-group query: exact stored
uidequals that authenticated UID. - Never read, normalize, compare, enumerate, coerce, or invoke token
emailoremail_verifiedfor registration ownership. - Never query
runner.emailfor My Account access. - Return only the existing sanitized projection for exact-UID results.
- Load event summaries only for event IDs in those exact-UID results.
- Preserve existing result sorting and successful empty-result behavior.
- Make no write, link, claim, migration, role, payment, provider, or audit-state change.
A future reviewed officer association may deliberately add or correct UID ownership after authoritative evidence. This containment must not invent that workflow.
Allowed data flow
flowchart LR
A["Authenticated Firebase UID"] --> B["Exact registration uid query"]
B --> C["Existing sanitized owned results"]
D["Matching runner email"] --> E["No read authority"]
E --> F["Future explicit officer review under #115"]
Text alternative: the authenticated Firebase UID may retrieve registrations with that exact stored UID. A matching runner email retrieves nothing and cannot associate a record; a future reviewed officer workflow owns any deliberate association.
Exact ownership
functions/listMyRegistrations.js- remove token email and verification reads;
- remove the
runner.emailcollection-group query and merge; - preserve App Check, Auth, UID query, sanitizer, event summary, sort, and response shape.
- New
functions/listMyRegistrations.test.js- focused callable tests with synthetic records only.
src/pages/account/Account.tsx- only the upcoming-registration empty-state copy;
- say no upcoming registration is linked to this account;
- say a signed-out registration may not appear and the member should ask the event lead for help rather than retrying or assuming email linkage.
src/pages/account/Account.test.tsx- one separately named empty-state truth test and minimum support only;
- preserve all profile, verification email, sign-out, registration lifecycle, and Strava tests.
docs/officers/EVENTS_SHOP_MEMBERS.md- one separately named
My Account registration ownership — SOURCE ONLY, NOT LIVEcomplete procedure, diagram, and text alternative.
- one separately named
SECURITY.md- one separately named DATA-001A3 source-ledger row only.
SYSTEM_DESIGN.md is excluded because draft PRs #346, #365, #369, and #372 own separate active sections there. The officer diagram records this bounded data-flow change without colliding with those claims.
Required RED proof
Against exact old runtime df91460d4e155e22f6bdd4e149cdc9d01a32d0f7, using only additive synthetic tests and the empty-state assertion:
- a matching-email record with
uid: nullis returned but must be absent; - a matching-email record owned by another UID is returned but must be absent;
- the callable performs a second
runner.emailquery but must perform only the UID query; - getter canaries on token
emailandemail_verifiedare invoked but must remain untouched; - the current empty state says the person has not registered but must say only that no upcoming registration is linked.
Record the test-only diff hash and intended failures. Do not use a production account, real runner, real email address, Firebase network, or production record.
Required green proof
- exact own-UID registrations retain the same sanitized fields, sort order, and event summaries;
- matching-email records with no UID or another UID are absent;
- token email and verification fields are not read;
- no email query occurs;
- unauthenticated requests stop before Firestore;
- App Check behavior is unchanged;
- an empty UID result performs no event lookup;
- malformed, accessor-backed, and proxied email claims are inert because they are never read;
- no write, association, backfill, payment, provider, analytics, or log occurs;
- the My Account empty-state copy is truthful and accessible;
- focused Functions and Account tests pass;
- full Functions and frontend suites, Functions lint, TypeScript, non-mutating lint, Rules, the demo-only commerce emulator, repository-safety checks, diagnostic build, links, sensitive-output scan, and diff check pass;
- three independent exact-head reviews approve security/privacy, compatibility/tests, and officer continuity.
Dependencies and coordination
- MEMBERS-ADMIN-001 — Add audited officer membership association and manual activation #115 is the authoritative product decision: email is a suggestion only and association must be explicit, officer-confirmed, and audited.
- BACKEND-001 — Audit the Firebase cutover and approve legacy data disposition #113 owns private legacy-source inventory and disposition.
- DATA-001B — Inventory personal data and approve a minimization/retention matrix #110 owns data purpose, retention, deletion, and access policy.
- DATA-001A siblings DATA-001A1 — Bind registration confirmation polling to the current capability route #319 and DATA-001A2 — Bind shop confirmation polling to the current capability route #321 cover confirmation-capability route lifecycle; they do not own My Account registration ownership.
- Parent AUTH-MAIL-002 — Make account-email delivery states honest and recoverable #120 has no current Account source claim. Preserve all merged verification-email, resend, action-handler, sign-out, profile, and sender behavior.
- Draft PRs MEMBERS-DUES-001A: immutable membership term/evidence receipt ledger (#345) #346, PAY-003B1: pure side-effect outbox delivery-state contract (#364) #365, MEMBERS-IDENTITY-001C — provider-neutral external-account link + collision contract (source only, unused) #369, and MEMBERS-IDENTITY-001D — provider-neutral versioned consent-state derivation (source only, unused) #372 own only their new Functions contract files/tests and named
SYSTEM_DESIGN.mdsections. Their paths are immutable.
Explicit exclusions and residual risk
No registration write/schema/backfill; no confirmation-token or lookup-capability change; no Stripe or payment-state change; no Firestore Rules/index deployment; no membership or payment authority; no officer-association implementation; no role, Auth, App Check configuration, provider, production data, deployment, or live verification.
Existing stored UID values are trusted for this narrow read path. How older records received or should receive a UID, how an officer proves an association, and how a mistaken association is audited and undone remain #115/#113 work.
Migration and compatibility
- Registrations made while signed in remain visible because the current checkout source stores
context.auth.uid. - Anonymous, older, mistyped-email, shared-email, or differently owned records stop appearing automatically.
- No record is changed or backfilled.
- Confirmation-capability behavior remains unchanged.
- Do not remove the existing email index in this slice; index/provider cleanup requires separate review.
This is deliberate fail-closed containment. It may remove previously visible email-matched rows until a future explicit association exists.
Acceptance checklist
- Timestamped exact claim exists before repository edits.
- RED evidence is recorded against the exact old runtime.
- My Account registration access uses exact stored UID only.
- Existing exact-UID response compatibility remains green.
- Empty-state and officer wording are truthful and complete.
- Required full Node 20 checks pass.
- Three independent exact-head reviews approve.
- Exact-head hosted CI passes before merge.
- Exact-main CI and deployment records are audited after merge.
- Claim, assignee, active label, remote branch, and worktree are released after closure evidence.
Claim protocol
Creation is not a claim. Before editing: confirm exact main, create and push a unique codex/ branch, assign this issue, add status:in-progress, and post a timestamped CLAIMED marker naming the exact branch, base, files/hunks, and immutable exclusions. Earliest valid claim wins.
Activity
daliu commentedon Jul 19, 2026
CLAIMED by /root at 2026-07-19T00:00:48Z; branch codex/issue-374-account-registration-uid; exact base df91460.
Exact ownership:
Immutable exclusions: all other Account/profile/resend/sign-out/registration-lifecycle/Strava hunks; all registration writes/schema/backfill, confirmation/lookup capability, Stripe/payment, Rules/indexes, membership association, providers, production data, deployment, and live behavior; all #345/#364/#367/#370 files and SYSTEM_DESIGN sections. No repository edit existed before this marker. Earliest valid claim wins.
daliu commentedon Jul 19, 2026
RED TEST EVIDENCE — 2026-07-19T00:07:01Z
Exact untouched runtime base:
df91460d4e155e22f6bdd4e149cdc9d01a32d0f7Test-only diff SHA-256:
1d61f8996fccdd9ac6c45caedd4719550b5438be1c5e2d73a4df8d4b2e29f72dRuntime: cached Node
20.19.5.Commands/results:
node ./node_modules/jest/bin/jest.js --runInBand --silent listMyRegistrations.test.jsfromfunctions/: RED as intended, 4 failed / 4 passed. The old runtime returned both matching-email records (one unowned, one owned by another UID) and read both accessor-backed and proxied token email claims.CI=true node ./node_modules/react-scripts/bin/react-scripts.js test --watchAll=false --runInBand --silent src/pages/account/Account.test.tsx: RED as intended, 1 failed / 89 passed. The old page said the person had not registered rather than stating that no registration is linked to this account.git diff --check: passed.All fixtures are synthetic. No network, Firebase/provider, production-data, association, migration, write, deployment, or live action occurred.
daliu commentedon Jul 19, 2026
SCOPE AMENDMENT — 2026-07-19T00:11:44Z
Two independent read-only reviews found that the already claimed Account empty-result block also rendered after a registration-load rejection. That can show an unproven no-link statement beside the load error and invite a duplicate signup/payment.
Within the same six claimed paths only, the Account empty-state hunk now also owns:
!currentRegistrationsErrordisplay gate;Do not register or pay againrecovery wording;The officer diagram label changes from
MembertoSigned-in person. No request, service, Function, registration lifecycle, payment, write, migration, provider, deployment, data, or live behavior is added. All other Account and shared-document hunks remain immutable.daliu commentedon Jul 19, 2026
IMPLEMENTATION CANDIDATE — 2026-07-19T00:19:31Z
Exact base:
df91460d4e155e22f6bdd4e149cdc9d01a32d0f7Exact pushed head:
bb2d6ab474a20cff8161b521f72abeb739d5da73Exact six-file diff SHA-256:
3f811526319ef01a719832914deb3a3b21a87b7e1aab9c9b7573728cac2f4641Branch:
codex/issue-374-account-registration-uidRuntime: Node 20.19.5; Java 17.0.19 for isolated Firestore emulators.
Outcome:
runner.emailis not queried.Verification:
All fixtures are invented. No registration write/link, migration, Firebase/provider call, production-data access, protected release, website publication, or live action occurred.
daliu commentedon Jul 19, 2026
CANDIDATE REFRESH — 2026-07-19T00:23:36Z
Independent compatibility review found one documentation-only P3: the officer checklist said
event order, but runtime preserves registration order by registrationcreatedAt; event summaries are keyed and promise no order. The sentence now saysregistration-created order. No runtime, test, authorization, response, UI, provider, or deployment behavior changed.Final pushed head:
2fd8ea48eeeec186b3dcad97837bfaee6d93a918Final six-file diff SHA-256:
888499edda1b7ba2aa15252ec2530c6e2fc70ba9384085ad6d16537185e1b11bDraft PR: #376
The prior exact-hash approvals and CI are historical. All three final exact-head reviews and fresh hosted checks are being rerun against
2fd8ea48.daliu commentedon Jul 19, 2026
FINAL EXACT-HEAD REVIEWS — 2026-07-19T00:25:35Z
Exact head:
2fd8ea48eeeec186b3dcad97837bfaee6d93a918Exact six-file diff SHA-256:
888499edda1b7ba2aa15252ec2530c6e2fc70ba9384085ad6d16537185e1b11bThree independent read-only reviews APPROVED with no findings:
The only refresh after the earlier candidate was the reviewer-requested phrase
event order→registration-created order. Runtime, tests, Account UI, and SECURITY remained byte-identical. PR #376 is now ready for review; fresh exact-head hosted checks are still authoritative for merge.daliu commentedon Jul 19, 2026
COMPLETE / CLAIM RELEASED — 2026-07-19T00:30:02Z
DATA-001A3 is complete as a source, test, review, and merge outcome.
2fd8ea48eeeec186b3dcad97837bfaee6d93a918888499edda1b7ba2aa15252ec2530c6e2fc70ba9384085ad6d16537185e1b11be414c828c3171f212737a5026d04aac05e61e1d129666865110, all five required jobs passed.29666951610, all five required jobs passed.[].Merged behavior: My Account registration reads use only exact stored UID ownership; matching email grants no read and token email claims are not inspected. A load failure cannot masquerade as an empty result or offer a repeat-registration link. The successful empty result says not to register or pay again and points to officer help.
Deployment truth: no protected release, Firebase Function deployment/readback, website publication,
runmprc.comrevision check, provider action, production-data action, association, migration, or live verification occurred. This fix is merged but NOT LIVE/UNPROVEN until a protected backend-first release separately verifies the Function and website.Residuals: #115 owns explicit audited association; #113 owns legacy inventory; #110 owns data policy. Existing stored UID values remain the narrow read authority, and the email index remains for separate review.
Released ownership:
functions/listMyRegistrations.js, newfunctions/listMyRegistrations.test.js, the named Account empty/error hunks, the named officer procedure, and the DATA-001A3 SECURITY ledger row. The remote branch is deleted. All fixtures were synthetic.daliu commentedon Aug 1, 2026
Follow-up DATA-001A4 / #553 is COMPLETE/CLOSED/CLAIM RELEASED through PR #557 at exact main
87d0f7caf6715cb43a36ad6de8d62592964eaa21.The exact-UID/App Check/Auth/event/sort/read-only boundary from #374 remains unchanged. The follow-up removes only the unused nested runner name/email/shirt-size projection from the My Account response and matching client type. PR CI
30718228595and exact-main CI30718325990passed all five jobs; deployments are[]. This is merged source, not deployed Firebase behavior. Completion evidence: #553 (comment)daliu commentedon Aug 1, 2026
Follow-up DATA-001A5 / #560 is COMPLETE/CLOSED/CLAIM RELEASED through PR #564 at exact main 22404fe.\n\nThe exact-UID/App Check/Auth/success/empty/event/sort/read-only boundary from #374 remains unchanged. The follow-up contains only exceptional Firestore construction, collectionGroup, exact-UID where, and query get outcomes behind one fixed unavailable result before registration projection or event reads. PR CI 30719602322 and exact-main CI 30719710554 passed all five jobs. GitHub deployments are []; the exact Netlify main attempt was unpublished, and Firebase was not deployed. This is merged source, not live Firebase behavior.\n\nCompletion evidence: #560 (comment)
daliu commentedon Aug 1, 2026
DATA-001A6 child #566 is COMPLETE/CLOSED/CLAIM RELEASED at exact main
511d117972e2fcd951abc1c91f3e111e158acaee. PR #570 preserved the reviewed tree and exact patch; PR CI30720996924and exact-main CI30721115711passed all five jobs. GitHub deployments are empty, the exact Netlify main attempt failed unpublished, Firebase was not deployed, and the public #473 marker is unchanged. Durable completion: #566 (comment)The exact UID authority, runner minimization, query-acquisition containment, and snapshot-processing containment now compose in merged source. Event acquisition, event snapshot/projection, concurrent sibling reads, event-ID derivation, and sorting failures remain separate residuals; no live behavior is claimed.
daliu commentedon Aug 1, 2026
DATA-001A7 child #575 is COMPLETE/CLOSED/CLAIM RELEASED at exact main
4c7cdd0d8b0ae28a388254924c96b6579e62b084via PR #577. Exact-main CI30722773830passed all five jobs; deployments are empty; the Netlify attempt was unpublished; Firebase/provider/data/live behavior did not change. This child contains only event collection/document/get acquisition failures. Event snapshot/projection and later sort residuals remain separate work. Durable release evidence: #575 (comment)daliu commentedon Aug 1, 2026
DATA-001A8 child #579 is COMPLETE/CLOSED/CLAIM RELEASED at exact main
a6b562b58f1d6527aeac77cabbc1199b2b70682fvia PR #581. Exact-main CI30723912362passed all five jobs; deployments are empty; the exact Netlify attempt was unpublished; Firebase/provider/data/live behavior did not change. This child contains only acquired event snapshotexists/data()/retained-field projection exceptions. Event-ID derivation/validation, malformed nonthrowing values, sibling cancellation/order, later sort failures, and response/platform serialization remain separate. Durable release: #579 (comment)daliu commentedon Aug 2, 2026
DATA-001A9 FOLLOW-UP COMPLETE
Atomic child #598 merged through PR #599 as exact main 57ac85b. Exact-main CI 30732611095 passed all five jobs. The final in-function registration sort stage now maps exceptional nested timestamp access/coercion to the existing fixed registration-load failure without binding the caught value.
This does not change #374's deployment/live status. GitHub deployments are empty; the exact-main Netlify production attempt failed unpublished; Firebase was not deployed/read back; runmprc.com remains the earlier #473 release; no production data or live behavior was accessed.
Remaining separately claimable residuals include malformed-but-nonthrowing timestamp semantics, event-ID derivation/validation, sibling cancellation/order, response/platform serialization/logging, retries, and protected deployment/readback/live proof.