Skip to content

DATA-001A3 — Require UID ownership for My Account registrations #374

Closed

Description

@daliu

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:

  1. queries registrations by the authenticated UID;
  2. reads context.auth.token.email and email_verified;
  3. when the email is verified, performs a second collection-group query by runner.email; and
  4. merges every email match into the My Account response, including a record with uid: null or 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:

  1. Preserve the existing App Check requirement and unauthenticated denial order.
  2. Read the authoritative Firebase UID from the authenticated context.
  3. Perform one registration collection-group query: exact stored uid equals that authenticated UID.
  4. Never read, normalize, compare, enumerate, coerce, or invoke token email or email_verified for registration ownership.
  5. Never query runner.email for My Account access.
  6. Return only the existing sanitized projection for exact-UID results.
  7. Load event summaries only for event IDs in those exact-UID results.
  8. Preserve existing result sorting and successful empty-result behavior.
  9. 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"]
Loading

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

  1. functions/listMyRegistrations.js
    • remove token email and verification reads;
    • remove the runner.email collection-group query and merge;
    • preserve App Check, Auth, UID query, sanitizer, event summary, sort, and response shape.
  2. New functions/listMyRegistrations.test.js
    • focused callable tests with synthetic records only.
  3. 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.
  4. 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.
  5. docs/officers/EVENTS_SHOP_MEMBERS.md
    • one separately named My Account registration ownership — SOURCE ONLY, NOT LIVE complete procedure, diagram, and text alternative.
  6. 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: null is 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.email query but must perform only the UID query;
  • getter canaries on token email and email_verified are 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

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

  1. added
    priority:P0Launch blocker or urgent security risk
    type:securitySecurity or privacy boundary
    area:authAuthentication and authorization
    area:firebaseFirebase services and data
    area:webWeb application and hosting
    size:SSmall focused issue
    type:testingTest infrastructure and quality gates
    area:privacyPersonal data, consent, minimization, retention, and privacy operations
    status:in-progressClaimed and actively being implemented
    on Jul 19, 2026
  2. self-assigned this
    on Jul 19, 2026
  3. daliu commented on Jul 19, 2026

    @daliu
    ContributorAuthor

    CLAIMED by /root at 2026-07-19T00:00:48Z; branch codex/issue-374-account-registration-uid; exact base df91460.

    Exact ownership:

    • functions/listMyRegistrations.js — remove token-email authority and the runner.email query only; preserve App Check/Auth/UID query/projection/events/sort.
    • new functions/listMyRegistrations.test.js — focused synthetic callable tests only.
    • src/pages/account/Account.tsx — upcoming-registration empty-state copy only.
    • src/pages/account/Account.test.tsx — one named empty-state truth block/support only.
    • docs/officers/EVENTS_SHOP_MEMBERS.md — one named My Account registration ownership source-only procedure/diagram/text alternative only.
    • SECURITY.md — one named DATA-001A3 source-ledger row only.

    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.

  4. daliu commented on Jul 19, 2026

    @daliu
    ContributorAuthor

    RED TEST EVIDENCE — 2026-07-19T00:07:01Z

    Exact untouched runtime base: df91460d4e155e22f6bdd4e149cdc9d01a32d0f7
    Test-only diff SHA-256: 1d61f8996fccdd9ac6c45caedd4719550b5438be1c5e2d73a4df8d4b2e29f72d
    Runtime: cached Node 20.19.5.

    Commands/results:

    • node ./node_modules/jest/bin/jest.js --runInBand --silent listMyRegistrations.test.js from functions/: 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.

  5. daliu commented on Jul 19, 2026

    @daliu
    ContributorAuthor

    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:

    • the !currentRegistrationsError display gate;
    • fixed Do not register or pay again recovery wording;
    • bounded status/live/atomic semantics for the asynchronously displayed empty result; and
    • focused tests proving an ordinary or hostile list rejection shows only the fixed load error, no empty result, no Browse events link, and no caught detail.

    The officer diagram label changes from Member to Signed-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.

  6. daliu commented on Jul 19, 2026

    @daliu
    ContributorAuthor

    IMPLEMENTATION CANDIDATE — 2026-07-19T00:19:31Z

    Exact base: df91460d4e155e22f6bdd4e149cdc9d01a32d0f7
    Exact pushed head: bb2d6ab474a20cff8161b521f72abeb739d5da73
    Exact six-file diff SHA-256: 3f811526319ef01a719832914deb3a3b21a87b7e1aab9c9b7573728cac2f4641
    Branch: codex/issue-374-account-registration-uid
    Runtime: Node 20.19.5; Java 17.0.19 for isolated Firestore emulators.

    Outcome:

    • My Account registration reads use one exact stored-UID query only.
    • Token email and email-verification claims are not read; runner.email is not queried.
    • Matching-email rows with no UID or another UID stay hidden.
    • Load failure and empty success are distinct; the empty result says not to register or pay again and is a polite atomic live region.
    • Officer and security documentation say SOURCE ONLY / NOT LIVE and prohibit manual repair.

    Verification:

    • Focused Functions: 8/8.
    • Full Functions: 2,976 passed / 64 skipped; Functions lint passed.
    • Focused Account: 91/91.
    • Full frontend: 591/591.
    • Frontend lint ledger: 108 files / 121 reviewed errors / 6 reviewed warnings.
    • Firestore Rules emulator: 378/378 on demo-rules-test.
    • Commerce emulator: 63/63 on demo-pay002b2-test.
    • SPA/workflow/release/artifact/dependency safety: 61/61.
    • TypeScript, diagnostic build, changed-document relative links, Mermaid block, sensitive-diff scan, and diff checks passed.
    • Two independent pre-commit reviews approved after the error/empty-state correction. Exact-commit reviews are now running.

    All fixtures are invented. No registration write/link, migration, Firebase/provider call, production-data access, protected release, website publication, or live action occurred.

  7. daliu commented on Jul 19, 2026

    @daliu
    ContributorAuthor

    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 registration createdAt; event summaries are keyed and promise no order. The sentence now says registration-created order. No runtime, test, authorization, response, UI, provider, or deployment behavior changed.

    Final pushed head: 2fd8ea48eeeec186b3dcad97837bfaee6d93a918
    Final six-file diff SHA-256: 888499edda1b7ba2aa15252ec2530c6e2fc70ba9384085ad6d16537185e1b11b
    Draft 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.

  8. daliu commented on Jul 19, 2026

    @daliu
    ContributorAuthor

    FINAL EXACT-HEAD REVIEWS — 2026-07-19T00:25:35Z

    Exact head: 2fd8ea48eeeec186b3dcad97837bfaee6d93a918
    Exact six-file diff SHA-256: 888499edda1b7ba2aa15252ec2530c6e2fc70ba9384085ad6d16537185e1b11b

    Three independent read-only reviews APPROVED with no findings:

    1. security/privacy and UID-only authorization;
    2. runtime compatibility and synthetic test coverage;
    3. backup-officer wording, accessibility, procedure completeness, links, diagram, and source/live truth.

    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.

  9. daliu commented on Jul 19, 2026

    @daliu
    ContributorAuthor

    COMPLETE / CLAIM RELEASED — 2026-07-19T00:30:02Z

    DATA-001A3 is complete as a source, test, review, and merge outcome.

    • PR: DATA-001A3: require UID ownership for My Account registrations #376
    • Final reviewed head: 2fd8ea48eeeec186b3dcad97837bfaee6d93a918
    • Exact six-file diff SHA-256: 888499edda1b7ba2aa15252ec2530c6e2fc70ba9384085ad6d16537185e1b11b
    • Admin-squash/main SHA: e414c828c3171f212737a5026d04aac05e61e1d1
    • Exact-head CI: 29666865110, all five required jobs passed.
    • Exact-main CI: 29666951610, all five required jobs passed.
    • Three independent exact-head reviews approved.
    • Exact-main workflow audit found CI only; GitHub deployment records are [].

    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.com revision 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, new functions/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.

  10. removed their assignment
    on Jul 19, 2026
  11. daliu commented on Aug 1, 2026

    @daliu
    ContributorAuthor

    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 30718228595 and exact-main CI 30718325990 passed all five jobs; deployments are []. This is merged source, not deployed Firebase behavior. Completion evidence: #553 (comment)

  12. daliu commented on Aug 1, 2026

    @daliu
    ContributorAuthor

    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)

  13. daliu commented on Aug 1, 2026

    @daliu
    ContributorAuthor

    DATA-001A6 child #566 is COMPLETE/CLOSED/CLAIM RELEASED at exact main 511d117972e2fcd951abc1c91f3e111e158acaee. PR #570 preserved the reviewed tree and exact patch; PR CI 30720996924 and exact-main CI 30721115711 passed 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.

  14. daliu commented on Aug 1, 2026

    @daliu
    ContributorAuthor

    DATA-001A7 child #575 is COMPLETE/CLOSED/CLAIM RELEASED at exact main 4c7cdd0d8b0ae28a388254924c96b6579e62b084 via PR #577. Exact-main CI 30722773830 passed 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)

  15. daliu commented on Aug 1, 2026

    @daliu
    ContributorAuthor

    DATA-001A8 child #579 is COMPLETE/CLOSED/CLAIM RELEASED at exact main a6b562b58f1d6527aeac77cabbc1199b2b70682f via PR #581. Exact-main CI 30723912362 passed 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 snapshot exists/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)

  16. daliu commented on Aug 2, 2026

    @daliu
    ContributorAuthor

    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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    area:authAuthentication and authorizationarea:firebaseFirebase services and dataarea:privacyPersonal data, consent, minimization, retention, and privacy operationsarea:webWeb application and hostingpriority:P0Launch blocker or urgent security risksize:SSmall focused issuetype:reliabilityReliability and recoverytype:securitySecurity or privacy boundarytype:testingTest infrastructure and quality gates

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions