diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..b070814 --- /dev/null +++ b/.env.example @@ -0,0 +1,20 @@ +# Public React build configuration. REACT_APP_* values are embedded in the +# browser bundle and MUST NOT contain secrets. + +# The current FirebaseResources implementation contains the production web +# config directly; these names document the intended environment-specific +# migration and match existing local files. Firebase web config is public, but +# every environment still needs correct Auth, Rules, and App Check controls. +REACT_APP_FIREBASE_API_KEY= +REACT_APP_FIREBASE_AUTH_DOMAIN= +REACT_APP_FIREBASE_PROJECT_ID= +REACT_APP_FIREBASE_STORAGE_BUCKET= +REACT_APP_FIREBASE_MESSAGING_SENDER_ID= +REACT_APP_FIREBASE_APP_ID= +REACT_APP_FIREBASE_MEASUREMENT_ID= + +# Public provider/application identifiers. +REACT_APP_RECAPTCHA_SITE_KEY= +REACT_APP_SENTRY_DSN= +REACT_APP_SENTRY_ENV=development +REACT_APP_STRAVA_CLIENT_ID= diff --git a/.github/ISSUE_TEMPLATE/officer-change-request.md b/.github/ISSUE_TEMPLATE/officer-change-request.md new file mode 100644 index 0000000..02c421e --- /dev/null +++ b/.github/ISSUE_TEMPLATE/officer-change-request.md @@ -0,0 +1,43 @@ +--- +name: Officer website change request +about: Request one clear website change without editing code or sharing private data +title: "[Officer change] " +labels: documentation +assignees: "" +--- + +## Desired result + + + +## Page or area + + + +## Approved source + + + +## Must not change + + + +## Timing and approval + +- Requested date: +- Approving officer role: +- Second approver required for money, legal, privacy, access, or security: + +## Safety + +- [ ] I did not include a password, login code, recovery code, secret, member list, private member detail, or payment information. +- [ ] This request does not ask AI to invent a price, waiver, policy, tax, refund, privacy, insurance, or access decision. +- [ ] I understand that a merge may start automatic website builds and is not proof that `runmprc.com` or Firebase changed. + +## Required handoff + +- Officer impact: +- Officer documentation: +- Deployment evidence: +- Preview or redacted screenshot: +- Plain-language undo plan: diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md new file mode 100644 index 0000000..7fee35c --- /dev/null +++ b/.github/pull_request_template.md @@ -0,0 +1,40 @@ +## Outcome + + + +## Scope + +- Issue: +- What changed: +- What did not change: + +## Officer handoff + +- Officer impact: +- Officer documentation: +- Approving role: +- Preview or redacted screenshot: +- Screenshot checked at: +- Plain-language undo plan: +- Deployment evidence: + +## Proof by surface + +- Source changed: +- Tests passed: +- Code merged: not yet +- Website published: not yet / not relevant +- Netlify intended commit verified: not yet / unknown / not relevant +- `runmprc.com` verified: not yet / not relevant +- Firebase deployed: not yet / not relevant +- Outside provider configured: not yet / not relevant +- Outside provider verified: not yet / not relevant +- Production behavior verified: not yet / not relevant + +## Safety review + +- [ ] No secrets, recovery codes, private member data, or payment data are included. +- [ ] Planned behavior is marked `NOT AVAILABLE YET`. +- [ ] Skipped checks or deployments are reported as incomplete, not green/live. +- [ ] Page/data/access/deployment diagrams were updated when their flow changed. +- [ ] A backup officer can follow the affected guide without a terminal, or the specialist-only step is explicit. diff --git a/.gitignore b/.gitignore index f1f21bb..3f4af8a 100644 --- a/.gitignore +++ b/.gitignore @@ -78,6 +78,8 @@ web_modules/ .env.test.local .env.production.local .env.local +.secret.local +functions/.secret.local # parcel-bundler cache (https://parceljs.org/) .cache @@ -145,4 +147,4 @@ dist /build # misc -.DS_Store \ No newline at end of file +.DS_Store diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..da121d4 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,125 @@ +# Repository Instructions for Coding Agents + +These instructions apply to all work in this repository. They supplement the issue being implemented; if an issue conflicts with a security invariant below, stop and escalate rather than weakening the invariant. + +## Read before changing code + +For architecture, security, commerce, or operations work, read the relevant root documents: + +- `SYSTEM_DESIGN.md` +- `STRIPE_COMMERCE_DESIGN.md` +- `SECURITY.md` +- `IMPLEMENTATION_PLAN.md` +- `OPERATIONS_RUNBOOK.md` +- The exact issue in `GITHUB_ISSUES.md` + +Existing files under `docs/` contain useful historical context but may predate the commerce platform. Root documents are authoritative for the target design. + +For every officer-visible or operational change, also read: + +- `OFFICER_START_HERE.md` +- `docs/officers/README.md` +- The short officer task guide affected by the issue + +## Officer continuity documentation + +Treat `OFFICER_START_HERE.md` and `docs/officers/` as product surfaces, not optional project notes. + +- Start every delivery summary with the outcome in plain language. +- Every issue and pull request must state: + - `Officer impact: ` + - `Officer documentation: ` or `None — ` + - `Deployment evidence: ` +- Update the relevant officer guide in the same pull request when work changes public content, navigation, admin duties, collected data, permissions, deployment, hosting, external accounts, payments, refunds, incidents, backup, or recovery. +- Officer guides describe current verified behavior. Mark future behavior `NOT AVAILABLE YET`; do not mix planned steps into a procedure that looks usable now. +- Use short sentences, ordinary words, and one action per numbered step. Define an unavoidable technical term once in the glossary. +- Every procedure must name its purpose, approver, prerequisites, steps, expected result, stop conditions, success proof, undo path, and escalation role. +- Do not make an officer edit source files, run terminal commands, handle secrets, or change production data as the primary continuity path. Use an issue, a small reviewed pull request, and named approval. +- Never ask an officer to paste passwords, recovery codes, secrets, customer/member data, payment data, or production records into an issue, screenshot, email, or AI tool. +- If page structure, data movement, permissions, account ownership, or deployment changes, update the matching Mermaid diagram and its one-sentence text alternative. +- Distinguish these states explicitly: source changed, tests passed, code merged, website published, `runmprc.com` verified, Firebase deployed, outside provider configured, and production behavior verified. +- A green workflow alone does not prove a website, Firebase, or provider change is live. Read job details for skipped steps and verify each affected surface. +- Redact screenshots, include the check date, and pair each visual with written steps. +- Before closing an issue, review the affected procedure as a backup officer with no terminal. Record any step that still requires a specialist. + +## Non-negotiable safety rules + +- Never use, request, print, commit, or copy production secrets or real customer/runner data. +- Never test checkout, refunds, role changes, email, Strava, migrations, or destructive behavior against production. +- In local development, Auth, Firestore, and Functions must all target emulators; Stripe must be in test mode. +- The browser never decides price, member eligibility, payment state, capacity, inventory, refund limits, or authorization. +- A redirect or client field never proves payment. Only verified Stripe state may confirm it. +- Every external create/refund/email and every webhook transition must be idempotent and retry-safe. +- Financial state, webhook events, audit records, rate limits, mail outbox, and OAuth secrets are server-only writes. Browser admins are not a trusted server boundary. +- Do not add an admin catch-all Firestore rule. +- Do not log request bodies, Checkout URLs, bearer/confirmation tokens, addresses, DOB, emergency contacts, Stripe/OAuth secrets, or raw ID tokens. +- Do not make legal, tax, insurance, waiver, retention, shipping, or refund-policy decisions. Implement only an approved decision or surface the blocker. +- Preserve unrelated worktree changes, especially generated or user-modified files such as `public/sitemap.xml`. + +## Issue workflow + +1. Work on one issue/outcome at a time and confirm its dependencies are complete. +2. Inspect the current implementation; documentation is a design, not proof the code already matches it. +3. State the invariant, allowed transitions, failure cases, and migration impact. +4. Add a test that demonstrates the defect or missing behavior before/with the fix. +5. Make the smallest backward-compatible change that satisfies the issue. +6. Use additive schema changes and idempotent, dry-runnable backfills. +7. Run focused checks, then the relevant full suite. +8. Review the diff for authorization, App Check, input bounds, PII/secrets, idempotency, impossible state transitions, and production fallback. +9. Update affected root documentation and the issue status/evidence. +10. Report commands/results and any residual risk; do not imply external production configuration was verified from source code. + +Split an issue before implementation if it cannot be safely reviewed as one focused pull request. + +## Repository map + +- `src/`: React web application and client services. Treat it as untrusted input/UI. +- `functions/`: trusted Firebase Functions boundary. Validate every request and external response. +- `firestore.rules`: browser access boundary. Admin SDK bypasses it, so also review server IAM/code. +- `tests/firestore-rules/`: required allow/deny tests for every rule change. +- `.github/workflows/`: CI and deployment trust boundary. +- `public/404.html` and `public/index.html`: current GitHub Pages SPA fallback. `public/spa-navigation.js` is queued #99 work and is **NOT AVAILABLE YET** on `main`. +- `docs/`: historical/developer/content context. +- `docs/officers/`: current plain-language officer procedures, maps, continuity, and emergency guidance. + +## Verification + +Use Node.js 20 and lockfile installs. + +Commands available on the current `main` baseline: + +```bash +npm ci --legacy-peer-deps +npm --prefix functions ci +npm --prefix functions run lint +npm --prefix functions run test:run -- --runInBand +npm run test:rules +CI=true DISABLE_ESLINT_PLUGIN=true npx --no-install react-scripts build +``` + +The Rules suite needs Java 17. The direct `react-scripts build` command is preferred for a diagnostic build because normal `npm run build` regenerates the sitemap. Run dependency audits for security/dependency issues, but do not apply forced automatic upgrades. + +The following are **NOT AVAILABLE YET** on `main`: + +- `npm run emulators` and fail-closed Auth/Firestore/Functions isolation — owned by #99. +- `npm run test:spa-navigation` — owned by #99. +- A trustworthy frontend Jest baseline/quality gate — owned by #103. + +Do not run Firebase-backed local development against the current production-configured client. Do not describe queued working-tree commands as merged or available. + +For Stripe lifecycle work, tests must include relevant negative and retry cases: invalid signature, duplicate/out-of-order Event, unpaid completion, amount/currency/environment mismatch, terminal-state protection, async success/failure, expiration, cancellation, and refund retry. Capacity/inventory work must include concurrent final-unit tests. + +## Code conventions + +- Preserve the current CommonJS style in `functions/` unless a dedicated migration issue changes it. +- Keep server-authoritative domain logic out of React components. +- Prefer small pure helpers for validation and state transitions so they can be exhaustively unit-tested. +- Use integer cents and explicit currency; never floating-point money. +- Use stable opaque IDs and schema-versioned minimal Stripe metadata; never put PII in metadata. +- Use structured, redacted logs with correlation IDs. +- Avoid mutable unbounded arrays for audit history; use append-oriented records. +- Do not add a new dependency when a small maintained-platform capability suffices. Any dependency change requires lockfile review and an audit. + +## Definition of done + +An issue is not complete until implementation, positive/negative tests, relevant full checks, migration/compatibility notes, redacted logging review, engineering documentation, and the applicable officer-facing handoff are complete. Provider Console, IAM, DNS, legal, or live-mode work also requires private evidence and named owner approval outside the repository. If officer documentation is not affected, state the specific reason in the pull request. diff --git a/GITHUB_ISSUES.md b/GITHUB_ISSUES.md new file mode 100644 index 0000000..457b7ce --- /dev/null +++ b/GITHUB_ISSUES.md @@ -0,0 +1,1285 @@ +# GitHub Issue Backlog: Secure Commerce Program + +**Prepared:** 2026-07-12 +**Status:** Dated design/assessment catalog; GitHub is canonical for execution +**Execution model:** One focused issue per branch/pull request +**Intended implementers:** Claude Sonnet, Terra/Luna, Codex, or a human engineer working under `AGENTS.md` + +GitHub publication began on 2026-07-12. Use the [Secure Commerce & Platform Hardening milestone](https://github.com/Run-MPRC/Run-MPRC.github.io/milestone/2) and each issue's current labels, dependencies, assignment, and comments as the live status source. This file preserves the broader system design and proposed issue bodies; its tables are an assessment snapshot, not an instruction to create duplicates. + +Published foundation mapping: + +- SAFETY-001 → [#99](https://github.com/Run-MPRC/Run-MPRC.github.io/issues/99) +- SEC-001 → [#100](https://github.com/Run-MPRC/Run-MPRC.github.io/issues/100) +- PAY-003A → [#101](https://github.com/Run-MPRC/Run-MPRC.github.io/issues/101) +- PROMO-001 → [#102](https://github.com/Run-MPRC/Run-MPRC.github.io/issues/102) +- CI-001A → [#103](https://github.com/Run-MPRC/Run-MPRC.github.io/issues/103) +- ARCH-001 documentation → [#104](https://github.com/Run-MPRC/Run-MPRC.github.io/issues/104) +- CI-001 tracker → [#105](https://github.com/Run-MPRC/Run-MPRC.github.io/issues/105) +- PAY-003 tracker → [#106](https://github.com/Run-MPRC/Run-MPRC.github.io/issues/106) + +Before creating or claiming anything from this catalog, search GitHub, confirm the predecessor is merged, then follow the timestamped claim protocol in the live issue. + +## Backlog conventions + +### Labels + +- Priority: `priority:P0`, `priority:P1`, `priority:P2` +- Type: `type:security`, `type:feature`, `type:reliability`, `type:compliance`, `type:operations`, `type:testing`, `type:maintenance` +- Area: `area:firebase`, `area:stripe`, `area:auth`, `area:race`, `area:shop`, `area:privacy`, `area:ci`, `area:web` +- Size: `size:S`, `size:M`, `size:L` +- Workflow: `status:ready`, `status:blocked-owner`, `needs-migration`, `needs-external-config` + +### Status + +- `proposed`: designed but dependency or owner decision remains. +- `ready`: dependencies and decisions are sufficient for implementation. +- `in_progress`: an isolated implementation is underway in the working tree. +- `implemented_locally`: the scoped code and local evidence exist, but review/merge/deploy evidence is still required. +- `partial_implemented_locally`: a risk-reducing slice exists, but the issue's full acceptance criteria are intentionally still open. +- `blocked_owner_decision`: legal/business/external authority is required. +- `done`: definition-of-done evidence is complete, not merely code written. + +### Shared requirements + +Every issue inherits `AGENTS.md` and the definition of done in `IMPLEMENTATION_PLAN.md`. In particular: no production access or real PII, preserve unrelated changes, add negative/security tests, use additive/idempotent migrations, review logs for PII/secrets, and update the affected root documentation. + +## Ordered index + +| Order | ID | Title | Priority | Size | Status | Depends on | +| ---: | --- | --- | --- | --- | --- | --- | +| 1 | SAFETY-001 | Preserve commerce/OAuth callbacks and isolate local Firebase Functions | P0 | S | implemented_locally | — | +| 2 | CI-001 | Repair test gates and secure the deployment pipeline | P0 | L | ready | SAFETY-001 | +| 3 | SUPPLY-001 | Remove vulnerable dependency chains and stage SDK/build upgrades | P0 | L | ready | CI-001 baseline | +| 4 | SEC-001 | Replace the Firestore admin catch-all with resource-specific rules | P0 | M | implemented_locally | — | +| 5 | CONFIG-001 | Fail closed on server environment and commerce configuration | P0 | M | ready | CI-001A recommended | +| 6 | AUTH-001 | Require verified email for member and privileged claims | P0 | M | partial: #98 merged; backend live unproven; parent open | SEC-001 recommended | +| 7 | AUTH-002 | Replace the legacy static-key membership synchronization endpoint | P0 | M | ready | AUTH-001 | +| 8 | OAUTH-001 | Make Strava token lifecycle server-only, transactional, and auditable | P1 | M | proposed | SEC-001, AUTH-003 capability model | +| 9 | AUTH-003 | Introduce scoped admin capabilities, MFA, and recent authentication | P1 | L | proposed | AUTH-001, AUTH-002, SEC-001 | +| 10 | ABUSE-001 | Enforce native App Check and privacy-preserving abuse limits | P0 | L | ready | CI-001 baseline | +| 11 | PAY-001 | Add strict request schemas and immutable monetary snapshots | P0 | L | ready | ABUSE-001 interface agreed | +| 12 | PROMO-001 | Disable unmodeled Stripe promotions until discounts are authoritative | P0 | S | partial_implemented_locally | PAY-001 for final discount contract | +| 13 | PAY-002 | Implement idempotent payment commands and explicit state machines | P0 | L | ready after PAY-001 | CONFIG-001, PAY-001 | +| 14 | PAY-003 | Build idempotent, async-aware Stripe webhook ingestion | P0 | L | partial_implemented_locally | CONFIG-001, PAY-001/PAY-002 target contract | +| 15 | RACE-001 | Add transactional race-capacity reservations | P0 | L | proposed | PAY-002, PAY-003 event contract | +| 16 | MERCH-001 | Add SKU variants and transactional inventory reservations | P0 | L | proposed | PAY-002, PAY-003 event contract | +| 17 | PAY-004 | Make cancellation authoritative and replace reusable late Payment Links | P0 | L | proposed | PAY-002, PAY-003, RACE-001 | +| 18 | PAY-005 | Build idempotent refunds, disputes, and reconciliation | P0 | L | proposed | PAY-002, PAY-003 | +| 19 | MAIL-001 | Build an escaped, idempotent transactional email outbox | P1 | M | proposed | PAY-003 | +| 20 | DATA-001 | Replace confirmation bearer URLs and implement PII minimization | P1 | L | proposed | PAY-002, PAY-003 | +| 21 | DATA-002 | Create immutable, truthful waiver evidence | P1 | M | blocked_owner_decision | RACE-001, LEGAL-001 decision | +| 22 | ADMIN-001 | Move sensitive admin operations behind scoped APIs and durable audit | P1 | L | proposed | SEC-001, AUTH-003, PAY-002 | +| 23 | MERCH-002 | Configure shipping, tax, returns, and order communications | P1 | L | blocked_owner_decision | MERCH-001, LEGAL-001 | +| 24 | WEB-001 | Move to controlled hosting and add browser security policy | P1 | L | proposed | SAFETY-001, CI-001 | +| 25 | OBS-001 | Add payment observability, alerts, SLOs, and redaction | P1 | M | proposed | PAY-003, PAY-005 | +| 26 | RESILIENCE-001 | Establish backup, restore, retention, and audited repair | P1 | L | proposed | DATA-001, PAY-005 | +| 27 | TEST-001 | Build the Stripe/Firebase security integration and E2E suite | P0 | L | proposed | core security/payment/data children | +| 28 | LEGAL-001 | Approve legal, tax, insurance, privacy, refund, and fulfillment policy | P0 | M | blocked_owner_decision | — | +| 29 | OPS-001 | Configure production systems and run a controlled live pilot | P0 | L | blocked by all launch gates | CI-001, TEST-001, LEGAL-001, OBS-001, RESILIENCE-001 | + +--- + +## SAFETY-001 — Preserve commerce/OAuth callbacks and isolate local Firebase Functions + +**Labels:** `priority:P0`, `type:security`, `type:reliability`, `area:web`, `area:firebase`, `size:S` +**Status:** `implemented_locally`; focused and integrated local checks pass, but review/merge/deploy evidence remains +**Depends on:** None + +### Problem + +GitHub Pages routes unknown SPA paths through `public/404.html`. The original bridge stored only `window.location.pathname`, dropping Stripe success/cancel query parameters, receipt capabilities, and Strava OAuth `code/state`. Separately, the Firebase client connected Auth and Firestore to local emulators but left Functions pointed at the deployed project. + +### Scope + +- Preserve same-origin pathname, search, and hash through the GitHub Pages root redirect. +- Clear temporary redirect state before restoration and reject malformed, protocol-relative, or cross-origin targets. +- Instantiate the shared Firebase Functions client and connect it to `127.0.0.1:5001` only in development. +- Add focused tests for callback preservation/rejection and development-versus-production emulator connections. + +### Acceptance criteria + +- [x] `/register/success?session_id=cs_test_x®=r1#done` restores exactly after the 404 bridge. +- [x] `/account/strava/callback?code=x&state=y` restores query parameters. +- [x] Cross-origin and protocol-relative stored targets are discarded. +- [x] Auth, Firestore, and Functions all connect to emulators in development. +- [x] None connects to an emulator in a production build. +- [x] Production build contains the bridge and passes without changing unrelated sitemap content. + +### Verification + +- Focused SPA navigation tests. +- Firebase resource environment tests with SDK calls mocked. +- Production compile invoked without sitemap-generating prebuild when only diagnosing. + +### Agent handoff + +Do not redesign routing or hosting in this issue; WEB-001 owns that. Do not include any real callback token in fixtures. + +--- + +## CI-001 — Repair test gates and secure the deployment pipeline + +**Labels:** `priority:P0`, `type:security`, `type:testing`, `area:ci`, `size:L`, `needs-external-config` +**Status:** Published as tracker [#105](https://github.com/Run-MPRC/Run-MPRC.github.io/issues/105); CI-001A [#103](https://github.com/Run-MPRC/Run-MPRC.github.io/issues/103) is claimed and queued. The current workflow remains non-compliant. +**Depends on:** SAFETY-001 + +### Problem + +Frontend tests fail because the Jest environment lacks `TextEncoder`. CI runs a mutating `lint:fix` command and ignores failure with `|| true`. Deployment runs independently of CI, gives a full Firebase service-account JSON to the frontend build, installs `firebase-tools@latest`, deploys frontend before backend, and can succeed while silently skipping Firebase deployment. + +### Scope + +- Repair Jest/browser polyfills without weakening application code or globally mocking business services. +- Add a non-mutating lint command covering JS/JSX/TS/TSX; make warnings/failures meaningful. +- Require frontend tests/build, Functions lint/tests, Rules emulator tests, dependency/secret checks before deploy. +- Remove Firebase service-account material from frontend build environment. +- Make production deployment protected, explicit, and fail closed when required credentials/config are absent. +- Use short-lived GitHub OIDC/Workload Identity Federation with a least-privilege deploy identity if supported; otherwise document a time-bounded interim credential and rotation. +- Pin/review Actions and Firebase CLI versions. +- Deploy additive backend/rules/indexes before dependent frontend. + +### Out of scope + +- Full CRA-to-Vite migration (SUPPLY-001). +- Live Stripe secret/configuration (OPS-001). + +### Acceptance criteria + +- [ ] `CI=true npm test -- --watchAll=false --runInBand` passes locally and in CI. +- [ ] Lint does not rewrite files and cannot be ignored. +- [ ] Rules tests run with Java 17 and include deny assertions from SEC-001. +- [ ] Deployment job cannot begin until the exact commit's required checks pass. +- [ ] Frontend build receives no service-account JSON or Stripe server secret. +- [ ] Missing production deployment authority fails the job visibly. +- [ ] GitHub production environment requires designated approval. +- [ ] Deployment order and rollback/roll-forward behavior are documented/tested in staging. + +### Tests/evidence + +- CI run on a pull request and protected main simulation. +- Negative job/config test showing absent credentials/config do not report success. +- Cloud IAM binding/export reviewed privately by two owners. + +### Agent handoff + +Repository changes can implement tests/workflow shape, but OIDC/IAM/environment protection requires an authorized owner. Split into `CI-001A` (test reliability) and `CI-001B` (deployment identity) if one PR would be too broad. + +--- + +## SUPPLY-001 — Remove vulnerable dependency chains and stage SDK/build upgrades + +**Labels:** `priority:P0`, `type:maintenance`, `type:security`, `area:web`, `area:firebase`, `area:stripe`, `size:L` +**Status:** Ready after CI baseline +**Depends on:** CI-001A + +### Problem + +The 2026-07-12 production audit reports 33 root advisories (3 critical, 9 high) and 9 Functions advisories (1 high). The direct router and Firebase client need patches; Firebase Admin needs a staged major upgrade. Unused `gpxparser` pulls obsolete `jsdom/request/form-data`. Create React App is unmaintained and contributes legacy dependency pressure. + +### Scope + +Deliver as small ordered PRs: + +1. Prove `gpxparser` is unused, remove it, refresh lockfile, and retest route behavior. +2. Upgrade `react-router-dom` to the safe compatible 6.x release; test all redirects/navigation. +3. Upgrade Firebase web SDK within compatibility, then evaluate/stage the next supported major. +4. Upgrade Firebase Admin/Functions with emulator and Auth/Firestore trigger tests. +5. Upgrade Stripe SDK and deliberately select/test the Stripe API version. +6. Plan and execute CRA migration to a maintained build tool without mixing payment behavior changes. +7. Add automated dependency updates, audit policy, reviewed lockfiles, and an SBOM/release inventory. + +### Acceptance criteria + +- [ ] No unused runtime dependency remains. +- [ ] No unexplained critical/high production advisory remains; any temporary exception has owner, reachability analysis, compensating control, and expiry. +- [ ] Auth, callable Functions, App Check, Firestore queries, routing, Stripe signature fixtures, and build pass after each upgrade. +- [ ] Bundle does not accidentally include server modules/secrets. +- [ ] Stripe API-version change has test-mode event fixtures and rollback notes. +- [ ] Build-system migration preserves SPA callbacks, SEO/sitemap behavior, code splitting, and deployment output. + +### Tests/evidence + +- Before/after `npm audit --omit=dev` for both lockfiles. +- Full CI and test-mode Stripe/Firebase smoke suite at every upgrade boundary. +- Bundle/dependency diff and SBOM artifact. + +### Agent handoff + +Never use `npm audit fix --force`. Do not combine all upgrades in one unreviewable lockfile diff. Stop if a major provider SDK change needs a product/runtime decision. + +--- + +## SEC-001 — Replace the Firestore admin catch-all with resource-specific rules + +**Labels:** `priority:P0`, `type:security`, `area:firebase`, `size:M` +**Status:** `implemented_locally`; rule and denial tests exist, with final review/deploy evidence still required +**Depends on:** None + +### Problem + +`match /{document=**}` grants browser admins read/write to every current and future document. Firestore rules are additive, so it overrides the apparent deny on member OAuth secrets and lets a compromised admin rewrite orders, registrations, audit arrays, mail, rate limits, and arbitrary collections. + +### Scope + +- Remove global admin access. +- Define reusable signed-in/member/admin helpers without dereferencing null auth. +- Preserve public event/product reads and members-only reads. +- Preserve the specific admin client access still required for event/product content management. +- Allow admins to read minimum member/registration/order data required by current admin UI during migration. +- Deny all client writes to registrations, orders, member roles, secrets, mail, rate limits, Stripe event inbox, audit/authz records, and future unmatched collections. +- Deny OAuth secret reads to every client, including admins. +- Reverse tests that intentionally assert unsafe access and add future-collection/mail/financial-write denial tests. + +### Acceptance criteria + +- [ ] Admin client cannot read `members/{uid}/secrets/*`. +- [ ] Admin client cannot create/update/delete registration/order payment state or audit data. +- [ ] Admin client cannot read/write an unmatched arbitrary collection. +- [ ] Admin client cannot create `mail`, modify rate limits, webhook ledgers, or authorization/audit records. +- [ ] Current admin event/product list/editor and member/order/registration read views retain only required access. +- [ ] Public, member, unverified, and owner/self-service behavior remains explicitly tested. +- [ ] Rules deploy/compile and all emulator tests pass. + +### Tests/evidence + +- Add deny tests for every protected collection and collection-group query. +- Test admin read versus write separately. +- Test legacy event drafts are not publicly readable. + +### Agent handoff + +Do not compensate by moving sensitive data into a different admin-readable document. ADMIN-001 later replaces remaining direct catalog/editor writes with scoped APIs. + +--- + +## CONFIG-001 — Fail closed on server environment and commerce configuration + +**Labels:** `priority:P0`, `type:security`, `type:reliability`, `area:firebase`, `area:stripe`, `size:M` +**Status:** Ready +**Depends on:** CI-001A recommended + +### Problem + +Several Functions default a missing `SITE_ORIGIN` to the production domain; until this pass the webhook accepted a missing expected Stripe mode. `ENVIRONMENT_NAME` and `COMMERCE_ENABLED` are documented but not validated or enforced. A missing/mistyped deployment variable can therefore create unsafe URLs, mix live/test behavior, or leave no operational kill switch. + +### Scope + +- Create one typed server-configuration module with an allowlisted environment name, required validated site origin, explicit expected Stripe livemode, commerce-enabled state, and safe emulator behavior. +- Reject production origin defaults; require HTTPS outside local/demo environments and an exact allowed origin/host. +- Verify local/demo cannot accept a live Stripe key/mode and production cannot accept test mode/key. +- Make every Session, refund, Payment Link/catalog create, and other new commerce command check a server-owned global and appropriate per-domain switch before any external side effect. +- Keep webhook ingestion, reconciliation, refunds needed for incident resolution, and read-only operations running while new commerce is disabled according to an explicit matrix. +- Validate configuration before request data, Firestore writes, email enqueue, or Stripe calls; return safe operator-visible error codes. +- Document typed parameters/Secret Manager bindings and a tested rotation/deploy sequence without logging values. + +### Acceptance criteria + +- [ ] Missing/malformed environment, site origin, expected mode, or commerce switch fails closed before external/local business writes. +- [ ] Local/demo execution rejects live Stripe configuration; production rejects test configuration. +- [ ] No server code silently falls back to `https://runmprc.com`. +- [ ] Disabled checkout blocks race, merchandise, late-payment, and catalog object creation with no Stripe side effect. +- [ ] Verified webhooks and reconciliation continue while checkout is disabled; incident refund behavior follows the approved matrix. +- [ ] Global/per-event/per-product switches are server-authoritative and cannot be bypassed by the browser. + +### Tests/evidence + +- Table-driven environment/config combinations, malicious origins, absent values, key/mode mismatch, and no-side-effect assertions. +- Emulator tests for kill-switch races and webhook/reconciliation continuity. +- Redacted staging parameter inventory and disable/enable drill; no production change in the coding issue. + +### Agent handoff + +Implement as CONFIG-001A/001B from `GITHUB_ISSUE_SLICES.md`. Never print or pattern-match an entire secret; use provider-safe key-mode metadata/prefix only where documented. External parameter/Secret Manager changes need an authorized owner. + +--- + +## AUTH-001 — Require verified email for member and privileged claims + +**Labels:** `priority:P0`, `type:security`, `area:auth`, `area:firebase`, `size:M` +**Status:** Partial. AUTH-001A [#98](https://github.com/Run-MPRC/Run-MPRC.github.io/issues/98) merged through [PR #107](https://github.com/Run-MPRC/Run-MPRC.github.io/pull/107) as `ce22c110f2132b157bd8a0d43b065585e0b43cb5`; focused source/tests are proven, but Firebase deployment skipped and the parent scope remains open. +**Depends on:** SEC-001 recommended + +### Problem + +Both the legacy member synchronization function and admin role callable grant claims based on email without requiring `userRecord.emailVerified`. An attacker can pre-register a known member address, not verify it, and later receive membership when that email is imported. Privileged guards and rules check only the role string. + +AUTH-001A now rejects unverified targets at the two existing role-grant endpoints. Do not duplicate that merged slice. It is not proof that Firebase production runs the new code, and it does not complete the remaining parent requirements below. + +### Scope + +- Reject member/admin/capability grants when the target Firebase user's email is unverified. +- Require verified email at sensitive member/admin server guards and Firestore rule helpers. +- Define behavior for non-email providers and accounts with no email. +- Reconcile/update the member profile's email-verification mirror after verification or token refresh. +- Revoke refresh tokens/force claim refresh on demotion and security-sensitive role changes. +- Add an audited error/result for skipped unverified membership imports without exposing account enumeration publicly. + +### Acceptance criteria + +- [ ] Unverified known-email account cannot receive member/admin claim through any endpoint. +- [ ] A forged request field cannot override Firebase's verified token/user-record value. +- [ ] Verified user can be granted an allowed role by an authorized actor. +- [ ] Sensitive rule/function access requires both capability and verified email where policy says so. +- [ ] Demotion has documented token revocation/propagation behavior. +- [ ] Existing tests with privileged contexts explicitly set verification state. + +### Tests/evidence + +- Attack-path unit and Rules emulator tests for unverified member/admin. +- Verified promotion/demotion and self-demotion protection tests. +- Emulator flow showing verification mirror/claim refresh. + +### Agent handoff + +Do not auto-promote a user merely because they verify email; authoritative club membership is a separate input. Do not put profile data in custom claims. + +--- + +## AUTH-002 — Replace the legacy static-key membership synchronization endpoint + +**Labels:** `priority:P0`, `type:security`, `area:auth`, `area:firebase`, `size:M`, `needs-external-config` +**Status:** Ready after AUTH-001 +**Depends on:** AUTH-001 + +### Problem + +`updateMemberRole` uses one shared `X-API-Key` from deprecated `functions.config()`, with no operator identity, replay protection, App Check, workload authentication, or durable audit. The API can change up to 100 roles per call and Firebase will decommission `functions.config()` deployments in March 2027. + +### Scope + +- Inventory the caller (`appengine/syncToFirestore.js` or another job) and decide whether it should be retired, moved to an authenticated admin import, or authorized through workload identity/OIDC. +- Remove the static shared-key public endpoint. +- Normalize/deduplicate input, require verified email, apply an allowlisted role/capability set, and cap batch size. +- Add a command ID/payload hash so retries are idempotent. +- Record actor/workload, source, old/new role, reason/import ID, outcome, and timestamp in append-oriented audit records. +- Migrate remaining configuration from `functions.config()` to Secret Manager/typed parameters only where still required. + +### Acceptance criteria + +- [ ] No public shared static key can change membership. +- [ ] Each import/action has a cryptographically verified caller and stable request ID. +- [ ] Unverified/missing accounts are reported safely and never promoted. +- [ ] Replaying the same import creates no duplicate changes/audit side effects. +- [ ] Admin grants are impossible through the membership importer. +- [ ] Legacy endpoint/config is removed and deployment succeeds without `functions.config()`. + +### Tests/evidence + +- Invalid/expired/wrong-audience caller tests. +- Duplicate import, mixed success, unverified target, role allowlist, and size-limit tests. +- Private IAM/workload configuration evidence. + +### Agent handoff + +External workload identity configuration needs an authorized owner. Do not replace one unauthenticated shared secret with a different long-lived shared secret and call the issue complete. + +--- + +## OAUTH-001 — Make Strava token lifecycle server-only, transactional, and auditable + +**Labels:** `priority:P1`, `type:security`, `type:reliability`, `area:auth`, `area:firebase`, `size:M`, `needs-external-config` +**Status:** Proposed +**Depends on:** SEC-001 and AUTH-003 capability model + +### Problem + +SEC-001 removes browser access to stored OAuth secrets, but that does not complete the token lifecycle. Tokens remain plaintext at rest, refresh-token rotation has no transaction/version guard, scopes/revocation/disconnect are not fully governed, and durable access/refresh audit is absent. Concurrent refresh can overwrite a newly rotated token and a compromised server identity may have more token access than required. + +### Scope + +- Keep authorization code exchange, access token use, refresh, and disconnect entirely server-side with strict owner/capability checks. +- Store token version/provider athlete reference, scope set, expiry, last refresh outcome, and minimum operational metadata separately from public member profile. +- Use transaction/compare-and-set or a lease/version protocol so concurrent refresh cannot lose a rotated refresh token. +- Minimize approved Strava scopes and reject unexpected returned scope/account binding. +- Implement idempotent disconnect with provider revocation where supported and local deletion/tombstone behavior. +- Record redacted connect/refresh/failure/revoke audit; never log codes or tokens. +- Make and record a threat-model decision on application-layer encryption/KMS versus Secret Manager/IAM-only protection, with rotation/recovery consequences. + +### Acceptance criteria + +- [ ] No browser/admin rule can read or write token material. +- [ ] Concurrent refresh preserves the newest valid rotated token and returns one coherent result. +- [ ] Exchange/refresh verifies account binding and exact allowed scopes. +- [ ] Disconnect revokes provider access when possible and makes local reuse impossible. +- [ ] Logs/audit contain no authorization code, access token, refresh token, or full secret document. +- [ ] Service IAM and encryption decision have named owner, rationale, and review date. + +### Tests/evidence + +- Concurrent refresh, stale-version, rotated-token, retry, provider rejection, scope mismatch, wrong-user, disconnect/reconnect, and redacted-log tests. +- Test-provider or mocked revocation evidence; private IAM/encryption review for hosted closure. + +### Agent handoff + +Use OAUTH-001A/B from the atomic slices. Never call the real Strava API with a member token in tests and never migrate/export production token documents into the workspace. + +--- + +## AUTH-003 — Introduce scoped admin capabilities, MFA, and recent authentication + +**Labels:** `priority:P1`, `type:security`, `area:auth`, `area:firebase`, `size:L`, `needs-migration`, `needs-external-config` +**Status:** Proposed +**Depends on:** AUTH-001, AUTH-002, SEC-001 + +### Problem + +One `admin` claim currently unlocks event content, registrations, members, products, orders, refunds, exports, roles, and—in the original rules—secrets. There is no repository evidence of MFA, re-authentication for refunds/role grants, rapid privilege revocation, or two-person protection for critical configuration. + +### Scope + +- Define capability claims: `event_manager`, `registrar`, `shop_manager`, `fulfillment`, `finance_admin`, `identity_admin`, `platform_admin` (names may be refined once). +- Map each function, Firestore read, admin route, export, and operation to the minimum capability. +- Keep `admin` temporarily as a migration alias, then remove it. +- Require verified email and MFA for privileged roles; enforce recent authentication for refunds, exports, role grants, and security changes. +- Add token revocation/refresh on demotion and a break-glass/last-platform-admin policy. +- Add append-oriented role-change audit and quarterly access review report. + +### Acceptance criteria + +- [ ] Race director cannot refund merchandise or grant roles. +- [ ] Shop lead cannot view DOB/emergency contacts or manage membership. +- [ ] Finance admin can execute approved refund/reconciliation without editing catalog/identity. +- [ ] Sensitive actions reject stale/non-MFA authentication according to approved threshold. +- [ ] Migration preserves access for named operators and has rollback/lockout recovery. +- [ ] Capability matrix is documented and covered by function/rules/route tests. + +### Tests/evidence + +- Table-driven authorization tests for every capability/action pair. +- Token demotion/revocation/refresh tests. +- Staging MFA/recent-auth demonstration and private access inventory. + +### Agent handoff + +Split claim migration, MFA/recent-auth, and UI route projection into separate PRs under the same epic if needed. Do not use Firestore profile fields as the trusted authorization source. + +--- + +## ABUSE-001 — Enforce native App Check and privacy-preserving abuse limits + +**Labels:** `priority:P0`, `type:security`, `type:reliability`, `area:firebase`, `size:L`, `needs-external-config` +**Status:** Ready +**Depends on:** CI-001A recommended + +### Problem + +App Check is optional on the client and enforced only by a custom `ENFORCE_APP_CHECK === true` branch that is not configured in repository workflows. Rate limiting trusts the first `X-Forwarded-For`, stores raw IP/email, depends on an unverified TTL policy, and lacks per-business/user/concurrency/cost controls. + +### Scope + +- Configure reCAPTCHA Enterprise for each web environment and observe App Check metrics. +- Replace custom fail-open checks with Firebase `enforceAppCheck: true` runtime options on sensitive callable functions. +- Evaluate limited-use/replay-protected tokens for checkout/refund commands after measuring web support and latency. +- Derive client address only from platform-trusted request metadata. +- HMAC rate-limit identifiers with a bound rotating secret; do not store raw email/IP. +- Add scopes for IP/pseudonymous email, authenticated UID, event/product, and command ID as appropriate. +- Provision and verify Firestore TTL; set function instances/concurrency/timeouts/budgets. +- Add metrics/alerts for App Check rejects, rate-limit rejects, Stripe/API object creation, and cloud cost anomalies. + +### Acceptance criteria + +- [ ] Hosted sensitive callables reject missing/invalid App Check without relying on an optional env toggle. +- [ ] Local emulator/CI behavior is explicit and cannot silently route to production. +- [ ] Rate-limit documents contain no raw email/IP and expire automatically. +- [ ] One attacker cannot block a victim solely by repeatedly submitting the victim's known email; mitigation/policy is documented. +- [ ] Valid retries with the same command ID do not consume duplicate external side effects. +- [ ] Abuse/cost alerts link to a runbook and named owner. + +### Tests/evidence + +- Missing/invalid/valid App Check tests in staging. +- Rate-limit boundary, concurrent transaction, expiry, HMAC rotation, and spoofed-header tests. +- Private console evidence for Enterprise key/domain, enforcement, TTL, budget, and alerts. + +### Agent handoff + +App Check is not authentication. Do not remove Auth/capability checks or rely on it as the only bot/fraud control. + +--- + +## PAY-001 — Add strict request schemas and immutable monetary snapshots + +**Labels:** `priority:P0`, `type:security`, `area:stripe`, `area:race`, `area:shop`, `size:L` +**Status:** Ready +**Depends on:** ABUSE-001 interface agreed + +### Problem + +Public/admin Functions validate only a few required strings. Names, phones, dates, notes, tracking values, custom fields, arrays, URLs, amounts, and nested payload size are weakly bounded. Event-required custom fields and allowed options are enforced only in HTML. Monetary records store one list amount but not an immutable expected/actual breakdown. + +### Scope + +- Select a maintained schema library compatible with CommonJS/Node 20 or implement small explicit validators if that is safer. +- Define versioned schemas for race checkout, merch checkout, lookup, role/admin actions, refunds, cancellation, substitutions, late/comp registration, exports, and Strava exchange. +- Bound total payload, object depth, field count, strings, arrays, enums, integer cents, dates, URLs, and quantities; reject unknown fields. +- Normalize email consistently; Unicode-normalize/bound names without banning legitimate names. +- Validate race custom fields against the server event/volunteer schema, including required fields and select options. +- Add immutable snapshots: expected subtotal, discount policy/version, currency, event/product/variant title/ID, price tier, waiver version/hash, and schema version. +- Centralize safe error codes/messages and redacted logging. + +### Acceptance criteria + +- [ ] Scripted clients cannot omit required event fields, submit an invalid option, or inject arbitrary nested extras. +- [ ] Floats, negative/unsafe/over-limit cents, invalid currency, excessive quantity, and oversized payloads fail before Firestore/Stripe calls. +- [ ] Merchandise requires a valid variant whenever variants exist. +- [ ] Admin notes/tracking/substitution/refund fields are bounded and typed. +- [ ] Stored records identify validation/schema version and expected-price snapshot. +- [ ] Public errors reveal no secret/provider internals or account-enumeration detail. + +### Tests/evidence + +- Table/fuzz-like tests for nulls, arrays-as-objects, prototype keys, deep nesting, Unicode length, hostile HTML, invalid dates/URLs, extra keys, and money boundaries. +- Tests proving Stripe/Firestore mocks are not called after validation failure. + +### Agent handoff + +Keep schemas close to the trusted Functions boundary and expose derived TypeScript/client types separately. Do not treat client TypeScript interfaces as runtime validation. + +--- + +## PROMO-001 — Disable unmodeled Stripe promotions until discounts are authoritative + +**Labels:** `priority:P0`, `type:security`, `area:stripe`, `size:S` +**Status:** `partial_implemented_locally`; creation and webhook defenses exist, but direct creator-payload tests and the future discount contract remain open +**Depends on:** PAY-001 for any future enabled discount model + +### Problem + +The original race and merchandise Checkout creators enabled arbitrary Dashboard promotion codes while local records stored only list/subtotal price. A mathematically consistent discounted Stripe total could therefore be fulfilled without a server-approved eligibility, policy/version, or expected total. Refund/reporting state could disagree with the charge. + +### Scope + +- Set `allow_promotion_codes: false` in every current Checkout Session creator. +- Treat any nonzero Stripe `amount_discount` as a permanent anomaly requiring review while discounts are disabled. +- Add creator payload tests and signed webhook tests, including a 100% discount and legacy outstanding Session. +- Inventory and disable/restrict active promotion configuration or outstanding Sessions in test/staging before release; production provider changes belong to OPS-001. +- If promotions are later desired, open a new issue under PAY-001/PAY-002 for server-approved code/campaign eligibility, immutable discount snapshot, actual total/refund/reconciliation, limits, expiry, audit, and migration before changing this guard. + +### Acceptance criteria + +- [ ] Neither race nor merchandise Session permits Stripe promotion-code entry. +- [ ] A nonzero discounted Session never becomes locally paid/fulfilled and is durably quarantined/alerted. +- [ ] Zero-discount Sessions continue through normal amount/currency validation. +- [ ] No UI or API claims a promotion is accepted. +- [ ] Provider inventory/release checklist accounts for Sessions created before the change. + +### Tests/evidence + +- Exact Stripe Session payload assertions for both creators. +- Signed zero, partial, and full-discount webhook fixtures with no-fulfillment assertions. +- Redacted test/staging inventory of outstanding promotion-enabled Sessions/codes before deployment. + +### Agent handoff + +The local webhook/creator patch is a safety default, not a complete discount feature. Do not remove the quarantine simply because the Stripe arithmetic balances. + +--- + +## PAY-002 — Implement idempotent payment commands and explicit state machines + +**Labels:** `priority:P0`, `type:reliability`, `type:security`, `area:stripe`, `area:firebase`, `size:L`, `needs-migration` +**Status:** Ready after PAY-001 +**Depends on:** CONFIG-001 and PAY-001 + +### Problem + +Checkout Session, Stripe Product/Price/Payment Link, and Refund creation have no idempotency keys. One overloaded `status` field mixes payment, registration, fulfillment, cancellation, refund, transfer, and dispute state. Network loss/retry can duplicate external operations; impossible transitions are currently allowed. + +### Scope + +- Define separate `paymentStatus`, `registrationStatus`, `fulfillmentStatus`, `refundStatus`, and `disputeStatus` with an allowed transition matrix. +- Keep/derive legacy `status` during an additive migration. +- Require client/admin `commandId` UUIDs; hash caller scope + ID into `checkoutRequests`/`commands` records with payload fingerprint. +- Reusing the same ID/payload returns the same result; different payload fails. +- Create local business/command record before external API call. +- Pass deterministic Stripe idempotency keys for Sessions, Products/Prices where applicable, refunds, and other creates. +- Persist saga step, attempt number, external ID, last error code, retryability, and timestamps. +- Add compensating actions and recovery rules for lost response and known failure. +- Move audit from mutable unbounded arrays toward append-oriented events or a bounded compatibility layer. + +### Acceptance criteria + +- [ ] Retrying a lost race/merch checkout response returns one business record and one active Session. +- [ ] Command ID reuse with a different payload is rejected. +- [ ] A paid/fulfilled/refunded/cancelled record cannot enter an impossible predecessor state. +- [ ] Refund retry cannot create a second refund for the same approved command. +- [ ] A crash between Firestore and Stripe is recoverable without guessing or manual field edits. +- [ ] Legacy records remain readable and the migration is dry-runnable/idempotent. + +### Tests/evidence + +- Pure table-driven transition tests. +- Concurrent duplicate command tests. +- Inject failure before/after external mock returns and before/after local persistence. +- Migration dry-run/backfill report on emulator fixtures representing every legacy status. + +### Agent handoff + +This is a shared domain foundation, not a UI task. Split state model/migration and command-idempotency infrastructure into sequential PRs if needed, but do not let race/shop implement incompatible keys or states. + +--- + +## PAY-003 — Build idempotent, async-aware Stripe webhook ingestion + +**Labels:** `priority:P0`, `type:security`, `type:reliability`, `area:stripe`, `area:firebase`, `size:L` +**Status:** `partial_implemented_locally`; a backward-compatible safety slice and unit evidence exist, while the PAY-001/PAY-002 migration and provider/emulator rehearsal remain open +**Depends on:** CONFIG-001 and PAY-001/PAY-002 target model; initial patch must remain compatible + +### Problem + +The current webhook verifies signatures but has no Event-ID ledger. It marks every Checkout completion paid without validating payment status, actual amount/currency, environment, metadata, or predecessor state; omits async outcomes; handles order expiry/disputes incompletely; and cannot resolve persistence-first/late metadata references reliably. + +### Scope + +- Verify method, signature over raw body, generic error response, expected environment/livemode, and supported event types. +- Create `stripeEvents/{event.id}` durable inbox/dedupe records without PII. +- Atomically apply business transition and processed marker where possible; define lease/retry/dead-letter behavior otherwise. +- Resolve business record from schema-versioned trusted metadata/client reference; support legacy Session/PaymentIntent query fallback during migration. +- Handle paid/unpaid `checkout.session.completed`, async success/failure, expiration for registration and order, refund totals, and disputes for both domains. +- Validate object/mode, Session/PaymentIntent ownership, expected amount, currency, discount policy, metadata, and allowed predecessor state. +- Store actual subtotal/discount/tax/shipping/total, PaymentIntent/Charge IDs, event/object references, and redacted transition audit. +- Quarantine permanent anomalies and return `5xx` only for retryable failures. +- Prevent completion from resurrecting terminal cancelled/refunded/fulfilled records. + +### Acceptance criteria + +- [ ] Exact duplicate Event ID produces no second transition/email/counter/inventory/refund effect. +- [ ] Distinct out-of-order Events reduce to a valid final state. +- [ ] `completed` with `payment_status != paid` stays pending/processing. +- [ ] Async success confirms; async failure fails/releases through the approved service. +- [ ] Wrong amount, currency, environment, metadata, or Stripe ownership never marks paid. +- [ ] Registration and merchandise expiry/refund/dispute paths are represented. +- [ ] Unknown/terminal transitions are safely ignored/quarantined and alerted. +- [ ] Successful processing and durable acceptance meet the defined latency/retry semantics. + +### Tests/evidence + +- Signed fixtures for immediate registration/order success, unpaid completion, async outcomes, duplicate/reordered events, mismatches, metadata-direct and legacy lookup, terminal-state protection, expiry, partial/full refunds, disputes, invalid signature, transient Firestore failure, and permanent quarantine. +- Emulator transaction integration test plus test-mode Stripe delivery rehearsal. + +### Agent handoff + +The in-progress slice may add safe confirmation and dedupe before the full PAY-002 migration. Clearly document remaining gaps; do not call partial unit coverage production-ready. + +--- + +## RACE-001 — Add transactional race-capacity reservations + +**Labels:** `priority:P0`, `type:feature`, `type:reliability`, `area:race`, `area:firebase`, `size:L`, `needs-migration` +**Status:** Proposed +**Depends on:** PAY-002 and PAY-003 event contract + +### Problem + +Capacity is a count-then-create query outside a transaction. Two concurrent final-seat requests can both succeed. Pending Sessions hold seats for their default lifetime but cleanup waits seven days. `registeredCount` is displayed and initialized but never updated. + +### Scope + +- Define participant capacity/reserved/paid counters and counter schema version on the event or dedicated counter record. +- Build an idempotent dry-run/backfill that counts legacy active participant registrations and reports drift. +- In one transaction, validate registration window/visibility/capacity, increment reserved once, and create the local pending/processing registration and checkout command. +- Set a deliberate Session/hold expiry and store it. +- Convert reserved to paid on verified payment; release once on expiry, async failure, authoritative cancellation, or policy-approved full refund. +- Make free participant and comp flows use the same counter; volunteers are separate. +- Define duplicate command reuse, admin late/comp behavior, event close/cancel behavior, and optional waitlist as follow-up. +- Replace UI-derived `registeredCount` with authoritative projected counts. + +### Acceptance criteria + +- [ ] In 20+ concurrent attempts for one remaining seat, at most one holds/succeeds. +- [ ] Duplicate request holds one seat. +- [ ] Expiry/cancellation/failure releases exactly one seat. +- [ ] Paid confirmation does not double-count; duplicate webhook has no effect. +- [ ] Volunteers do not consume participant capacity. +- [ ] Admin late/comp cannot silently exceed capacity; explicit override is permissioned/audited if approved. +- [ ] Counter backfill can rerun and reports no unexplained drift before cutover. + +### Tests/evidence + +- Firestore emulator concurrency tests for final seat, duplicate request, expiry/payment race, cancellation/payment race, comp/free/volunteer, and migration. +- Staging Stripe Session expiry rehearsal. + +### Agent handoff + +Do not hold a Firestore transaction open across a Stripe API call. Use the saga in `STRIPE_COMMERCE_DESIGN.md`. Waitlist can be a separate issue if not needed for launch. + +--- + +## MERCH-001 — Add SKU variants and transactional inventory reservations + +**Labels:** `priority:P0`, `type:feature`, `type:reliability`, `area:shop`, `area:firebase`, `area:stripe`, `size:L`, `needs-migration` +**Status:** Proposed +**Depends on:** PAY-002 and PAY-003 event contract + +### Problem + +Products contain independent size/color arrays and one price, with no canonical SKU or stock counts. The backend allows omitted size/color even when options exist and can sell the final item repeatedly. + +### Scope + +- Add `products/{id}/variants/{variantId}` with immutable SKU, option values, price/currency, sellable status, physical `onHand`, held `reserved`, and cumulative-reporting `sold`; availability is `onHand - reserved`. +- Build admin/import validation that prevents duplicate option combinations/SKUs and unsafe negative changes. +- Migrate existing product options in dry-run/report mode; require owner stock counts before activation. +- Require one valid variant and launch quantity policy in checkout. +- Transactionally reserve available stock with the local order/command. +- On verified payment, decrement `reserved` and `onHand` and increment `sold`; on expiry/failure/cancellation release only `reserved`. +- Separate returns/restock/write-off from financial refund. +- Derive product `sold_out` display from variants without using it as the integrity control. + +### Acceptance criteria + +- [ ] Every sellable option combination maps to one SKU/variant. +- [ ] Omitted, invalid, inactive, duplicate, or price-mismatched variant fails server-side. +- [ ] Concurrent final-unit attempts cannot make availability negative or create more than one hold. +- [ ] Migration/admin adjustment tests prove `sold` is never subtracted twice and approved restock increments physical `onHand` only. +- [ ] Duplicate request/webhook holds/sells once. +- [ ] Expiry/cancellation releases once; refund alone does not restock. +- [ ] Admin stock adjustment is scoped, reasoned, audited, and cannot erase history. + +### Tests/evidence + +- Emulator concurrency and transition tests for final unit, multiple variants, duplicate command, expiry/payment race, refund/return, and stock adjustment. +- Migration report verified by merchandise owner. + +### Agent handoff + +Start with quantity one if a multi-item cart is not explicitly required. Multi-line atomic reservation and shipping calculation should be a separate design. + +--- + +## PAY-004 — Make cancellation authoritative and replace reusable late Payment Links + +**Labels:** `priority:P0`, `type:security`, `type:reliability`, `area:stripe`, `area:race`, `area:shop`, `size:L` +**Status:** Proposed +**Depends on:** PAY-002, PAY-003, RACE-001; MERCH-001 for shop release + +### Problem + +Admin cancellation changes only Firestore; the Checkout Session remains payable and the webhook can reopen a cancelled record. Late registration creates a reusable Payment Link whose generated Sessions are not stored/resolved, so payment never reconciles and link reuse can charge multiple times. + +### Scope + +- Separate unpaid cancellation, paid cancellation-with-refund decision, event/product closure, and exception review. +- For unpaid active checkout, persist cancellation command, expire the exact Stripe Session, then release capacity/inventory once under a recoverable saga. +- Handle payment racing with cancellation by retrieving canonical Stripe state and entering paid-after-cancel review/refund policy. +- Expire open Sessions when an event/product/variant is disabled according to bounded batch/job behavior. +- Replace late-add Payment Links with one-off, expiring, idempotent Checkout Sessions tied to one registration and command. +- Communicate late checkout URL through an authorized, expiring channel; prevent multiple active attempts unless versioned. +- Migrate/deactivate legacy Payment Links and reconcile any charges. + +### Acceptance criteria + +- [ ] Cancelled unpaid record cannot subsequently be paid through the old Session. +- [ ] Cancellation retry is idempotent and hold releases once. +- [ ] Payment/cancel race never silently loses money or resurrects terminal state. +- [ ] One late registration produces at most one successful paid Session and reconciles through normal webhook logic. +- [ ] Legacy Payment Links are inventoried, deactivated, and reconciled. +- [ ] Paid cancellation requires explicit permission/reason/refund choice. + +### Tests/evidence + +- Stripe mock/test-mode races for cancel before/after completion, lost expire response, duplicate cancel, event closure batches, late link reuse, and payment-after-cancel. + +### Agent handoff + +Do not mark local cancelled before considering the live Stripe Session and recovery state. Never email a reusable unrestricted Payment Link as a shortcut. + +--- + +## PAY-005 — Build idempotent refunds, disputes, and reconciliation + +**Labels:** `priority:P0`, `type:security`, `type:reliability`, `area:stripe`, `size:L` +**Status:** Proposed +**Depends on:** PAY-002, PAY-003, AUTH-003 for final permissions + +### Problem + +Refund functions call Stripe without idempotency, immediately overwrite local status, weakly validate amount/current state/remaining balance, and do not track cumulative actual refund. Dispute handling is registration-only and audit-only. No scheduled reconciliation proves Stripe and Firestore agree. + +### Scope + +- Create refund command records with stable ID, actor/capability, reason, requested integer cents, payment snapshot, status, and Stripe idempotency key. +- Validate against Stripe's actual captured and cumulative refunded totals, not original list price alone. +- Model pending/succeeded/failed/cancelled refund and cumulative amount; let verified events finalize. +- Protect concurrent partial/full refunds and repeated clicks/timeouts. +- Handle disputes for orders and registrations across required lifecycle events; alert finance with no unnecessary PII. +- Build scheduled and operator-triggered reconciliation for Sessions/PaymentIntents/refunds/disputes/local records/counters/event inbox. +- Classify deterministic safe repair versus quarantine; create audited idempotent repair commands. + +### Acceptance criteria + +- [ ] Same refund command can be retried without a second refund. +- [ ] Two concurrent partial refunds cannot exceed remaining amount. +- [ ] Equal/over-limit/non-integer/invalid-state refund fails clearly. +- [ ] Local final totals come from verified Stripe state and preserve fulfillment/registration state separately. +- [ ] Race and merchandise disputes alert and reconcile through closure. +- [ ] Reconciliation detects orphan Stripe payment, false local paid, amount/currency mismatch, duplicate mapping, stale saga, and refund/dispute mismatch. +- [ ] Ambiguous mismatches are quarantined, not overwritten automatically. + +### Tests/evidence + +- Failure injection before/after Stripe response; duplicate/concurrent partials; pending/failed/succeeded events; full-after-partial; dispute lifecycle; reconciliation fixture for every category; repair idempotency. +- Staging finance review of report totals. + +### Agent handoff + +Separate refund command/state from the broader reconciliation worker into sequential PRs if necessary. Never hand-edit Firestore to make reconciliation pass. + +--- + +## MAIL-001 — Build an escaped, idempotent transactional email outbox + +**Labels:** `priority:P1`, `type:security`, `type:reliability`, `area:firebase`, `area:privacy`, `size:M`, `needs-external-config` +**Status:** Proposed +**Depends on:** PAY-003 + +### Problem + +Registration email triggers check a marker, add a random `mail` document, then update the marker. Trigger retries can enqueue duplicates. User/event values are interpolated into HTML without escaping, enabling club-branded malicious markup. “Sent” currently means queued, not delivered. Merchandise/refund/cancellation/fulfillment/dispute communications and bounce handling are absent. + +### Scope + +- Create deterministic outbox IDs such as `{businessId}:{messageType}:{version}` in a transaction with the triggering state. +- Escape every dynamic HTML value or use provider templates with escaping-by-default. +- Store delivery lifecycle separately: queued, provider accepted, delivered if available, bounced/failed/suppressed. +- Implement idempotent registration and order confirmation first; add approved cancellation/refund/fulfillment templates. +- Never include bearer tokens, emergency contacts, DOB, full addresses, or arbitrary admin notes. +- Rate-limit/abuse-protect free/volunteer flows that can target third-party email addresses. +- Configure and document sender domain, SPF, DKIM, DMARC, provider credentials, bounce/suppression handling, and monitoring. + +### Acceptance criteria + +- [ ] Trigger/event retry creates exactly one outbox message per type/version. +- [ ] Hostile names/event titles/locations render as text, not markup/link/image/script. +- [ ] Queue success is not mislabeled delivered. +- [ ] Payment confirmation is sent only after verified paid/non-payment confirmed state. +- [ ] Failure/bounce is visible and retry rules cannot spam the recipient. +- [ ] Browser admins cannot create arbitrary `mail` documents. + +### Tests/evidence + +- Hostile HTML/URL/control-character fixtures; concurrent/retried trigger; provider failure/retry; duplicate webhook; suppression/bounce fixture. +- Private DNS/provider verification evidence. + +### Agent handoff + +Keep templates generic until legal/communications copy is approved. Do not weaken escaping to allow Firestore-authored raw HTML. + +--- + +## DATA-001 — Replace confirmation bearer URLs and implement PII minimization + +**Labels:** `priority:P1`, `type:security`, `type:compliance`, `area:privacy`, `area:web`, `area:firebase`, `size:L`, `needs-migration` +**Status:** Proposed +**Depends on:** PAY-002, PAY-003 + +### Problem + +High-entropy confirmation tokens are stored plaintext with no expiry and placed in query strings. They can leak through history, screenshots, monitoring, analytics, logs, and referrers. Registrations/orders retain DOB, emergency contacts, shipping addresses, arbitrary extras, Stripe IDs, and audit emails without field-specific retention or deletion/export workflow. + +### Scope + +- For paid checkout, use `{CHECKOUT_SESSION_ID}` only and a server endpoint that retrieves/verifies Stripe/local ownership before returning a sanitized projection. +- For signed-in users, prefer UID ownership and verified-email linking policy. +- For anonymous free/legacy lookup, store only a token hash, add expiry/rotation/one-time exchange as appropriate, use URL fragment/session state, and scrub history immediately. +- Add restrictive referrer/analytics/error-monitoring behavior to confirmation/account/admin routes. +- Define data classification and owner-approved field retention; separate long-lived accounting/waiver evidence from short-lived operational PII. +- Implement report-only then active redaction/deletion jobs, user access/deletion export, and backup implications. +- Create role/purpose-specific projections and remove PII from logs/audit notes. + +### Acceptance criteria + +- [ ] Long-lived plaintext bearer capability is absent from query strings and stored records. +- [ ] Guessing a business ID/Session ID alone does not expose another person's PII. +- [ ] Confirmation projection includes only necessary name/item/status/amount fields. +- [ ] Analytics/Sentry/referrer behavior cannot capture capability or sensitive URL state. +- [ ] Retention job is idempotent, dry-runnable, auditable, and preserves legally required minimum evidence. +- [ ] Access/deletion requests authenticate the subject and do not erase required financial/audit evidence improperly. + +### Tests/evidence + +- Cross-user/expired/replayed/tampered token and Session tests; browser history/referrer/monitoring inspection; retention fixtures across statuses/dates; backup/restore deletion notes. + +### Agent handoff + +Retention durations require LEGAL-001 owner approval. Implement mechanisms and report-only modes without inventing legal periods. + +--- + +## DATA-002 — Create immutable, truthful waiver evidence + +**Labels:** `priority:P1`, `type:compliance`, `type:security`, `area:race`, `area:privacy`, `size:M`, `needs-migration` +**Status:** Blocked on owner/legal/insurance decisions +**Depends on:** RACE-001 and LEGAL-001 waiver decision + +### Problem + +Normal registration stores timestamp/version but no immutable text/hash/provenance. Admin comp and late-add paths currently set `waiverAcceptedAt=now` even though the participant did not accept anything. Substitution preserves or rewrites runner identity without collecting the substitute's acceptance. + +### Scope + +- Establish approved waiver versioning, effective date, canonical text/PDF location, content hash, event association, and retention. +- Record acceptance provenance: participant-authenticated or anonymous flow, timestamp, waiver hash/version, registration ID, and approved contextual evidence. +- Define minor/guardian behavior, if applicable, with counsel/insurance. +- Separate `waiver_required`, `waiver_pending`, and `waiver_accepted`; never fabricate acceptance on admin creation. +- Make comp/late/substitute flows send a one-time acceptance request before confirmed eligibility. +- Freeze old waiver evidence even when event waiver text changes. +- Provide audited admin view/export of minimum evidence. + +### Acceptance criteria + +- [ ] Every confirmed participant has required acceptance for the exact immutable waiver version or an approved documented exception. +- [ ] Admin-created record cannot claim participant acceptance. +- [ ] Substitute cannot see the prior runner's sensitive data and must accept independently. +- [ ] Editing an event creates a new waiver version rather than rewriting evidence. +- [ ] Evidence retention/access follows approved policy and survives required backup/restore. + +### Tests/evidence + +- Version change, comp/late, substitution, anonymous/authenticated, retry, expired acceptance link, minor/guardian decision, and immutable snapshot tests. +- Approved waiver/policy reference stored outside public issue as appropriate. + +### Agent handoff + +Stop until legal/insurance owners approve evidence and minor policy. Code agents must not write waiver language or decide whether IP/user-agent collection is appropriate. + +--- + +## ADMIN-001 — Move sensitive admin operations behind scoped APIs and durable audit + +**Labels:** `priority:P1`, `type:security`, `type:feature`, `area:firebase`, `area:auth`, `size:L`, `needs-migration` +**Status:** Proposed +**Depends on:** SEC-001, AUTH-003, PAY-002 + +### Problem + +Admin clients directly read/write events/products and, under the original catch-all, could directly alter any record. Some function actions still allow impossible transitions or unbounded notes. Inline mutable `auditLog` arrays can grow/contention-limit records. CSV export exposes a fixed broad set including DOB, emergency contacts, and Stripe IDs without purpose, re-auth, or export audit. + +### Scope + +- Build scoped server APIs for event/product configuration fields that affect money, capacity, inventory, visibility, waiver, Stripe catalog, or lifecycle. +- Keep harmless editorial direct writes only if rules and audit requirements justify them; otherwise move all admin mutations server-side. +- Enforce capability, verified email, App Check, recent auth where required, strict schema, current state, reason, and idempotent command. +- Replace inline audit arrays with append-oriented audit events protected from client mutation; define retention/query indexes. +- Return role-appropriate projections so race/shop/membership/finance roles see minimum data. +- Split exports into check-in roster, emergency sheet, finance report, and fulfillment report with minimum columns, bounded size/streaming, re-auth, reason, watermark/identifier, and durable export audit. +- Correct substitution ownership and waiver flow. + +### Acceptance criteria + +- [ ] No client can directly mutate price, payment, capacity/inventory counter, Stripe ID, refund, role, waiver evidence, audit, or secret fields. +- [ ] Every admin command has actor/capability/target/reason/old-new/result/correlation audit. +- [ ] Forbidden state/capability combinations are table-tested. +- [ ] Race director/shop lead/finance/membership projections contain no unrelated PII. +- [ ] Exports are purpose-specific, formula-safe, audited, and require approved authentication. +- [ ] Audit cannot be edited/deleted by ordinary browser admins. + +### Tests/evidence + +- Capability matrix tests, hostile/bounds inputs, duplicate commands, impossible transitions, audit immutability, export PII column snapshots/formula fixtures/large dataset. + +### Agent handoff + +This is an epic-sized issue. Split APIs by domain after the common auth/schema/audit middleware lands. Preserve current admin functionality through expand-and-contract deployment. + +--- + +## MERCH-002 — Configure shipping, tax, returns, and order communications + +**Labels:** `priority:P1`, `type:feature`, `type:compliance`, `area:shop`, `area:stripe`, `size:L`, `needs-external-config`, `status:blocked-owner` +**Status:** Blocked on owner/accountant policy +**Depends on:** MERCH-001, LEGAL-001, MAIL-001 + +### Problem + +Checkout collects US/Canada shipping address but the platform has no approved shipping rates/methods, pickup option, country/tax policy, return/restock process, customer delivery commitment, or order/refund/fulfillment communication. Local `amountCents` excludes discount/tax/shipping breakdown. + +### Scope + +- Obtain owner/accountant decisions for sellable jurisdictions, tax nexus/treatment, Stripe Tax, shipping/pickup methods/rates, delivery estimates, returns/exchanges/lost packages, and refund of shipping/fees. +- Model/store expected and actual subtotal, discount, tax, shipping, and total separately. +- Set Stripe product tax codes and automatic-tax/address behavior if approved. +- Configure allowed countries and shipping rates/methods server-side. +- Add fulfillment transitions, tracking validation, pickup flow, return inspection/restock/write-off, and customer notifications. +- Minimize shipping address after fulfillment/return/legal window. + +### Acceptance criteria + +- [ ] Checkout total/tax/shipping exactly reconciles with local actual breakdown. +- [ ] Unsupported destination/method cannot order. +- [ ] Fulfillment cannot occur before paid; refund does not automatically restock. +- [ ] Customer receives approved order, shipment/pickup, cancellation, return, and refund communication once. +- [ ] Address access/retention is role-limited and documented. +- [ ] Stripe Dashboard/tax/export configuration is reviewed by finance. + +### Tests/evidence + +- Supported/unsupported destination, tax on/off/error, shipping/pickup totals, lost payment response, fulfillment/refund/return, and retention tests. +- Private accountant/owner approval and Stripe configuration evidence. + +### Agent handoff + +Do not enable Stripe Tax, Canada shipping, or collect additional addresses until policy/finance decisions are explicit. + +--- + +## WEB-001 — Move to controlled hosting and add browser security policy + +**Labels:** `priority:P1`, `type:security`, `type:maintenance`, `area:web`, `size:L`, `needs-external-config` +**Status:** Proposed +**Depends on:** SAFETY-001, CI-001 + +### Problem + +GitHub Pages relies on a JavaScript 404 bridge for every deep link and offers limited application-controlled response headers. The app loads external fonts/images/maps/services and parses stored HTML with `html-react-parser` without sanitization. No deployed CSP, frame-ancestor, referrer, permissions, or nosniff policy is declared. + +### Scope + +- Evaluate and migrate production/staging to Firebase Hosting (preferred with current stack) or an equivalent owned CDN supporting SPA rewrites, preview channels, rollbacks, custom domain, and headers. +- Preserve canonical domain, redirects, SEO, sitemap, `404`, OAuth/Stripe callbacks, assets, and GitHub workflow protections. +- Inventory scripts/styles/images/fonts/connect/frame/form/network endpoints and create a restrictive tested CSP. +- Add HSTS, `X-Content-Type-Options`, `Referrer-Policy`, `Permissions-Policy`, frame protection via CSP, and safe caching. +- Remove arbitrary stored HTML in favor of structured content, or sanitize with a maintained allowlist and test it. +- Validate external URLs and prevent dangerous schemes/open redirects. +- Add preview/staging smoke and rollback test. + +### Acceptance criteria + +- [ ] Direct navigation/callback/reload works with server rewrite; no query/hash bridge loss. +- [ ] Security headers are visible on production/staging responses and do not require unsafe broad CSP exceptions. +- [ ] Stored hostile HTML cannot execute/inject active content. +- [ ] App Check/Auth/Stripe/Strava/Sentry/Analytics still function under documented policy. +- [ ] Canonical/robots/sitemap/custom domain/TLS and rollback are verified. +- [ ] Old GitHub Pages deployment cannot accidentally overwrite production after cutover. + +### Tests/evidence + +- Automated header/CSP tests; route/callback smoke; hostile HTML/URL fixtures; accessibility/build/SEO checks; DNS/TLS/rollback evidence. + +### Agent handoff + +Hosting cutover changes external state and needs DNS/owner approval. Implement configuration and staging validation first; do not redirect production autonomously. + +--- + +## OBS-001 — Add payment observability, alerts, SLOs, and redaction + +**Labels:** `priority:P1`, `type:reliability`, `type:operations`, `area:stripe`, `area:firebase`, `area:privacy`, `size:M`, `needs-external-config` +**Status:** Proposed +**Depends on:** PAY-003, PAY-005 interfaces + +### Problem + +Observability is optional client Sentry/Firebase Analytics. There are no structured payment metrics, webhook/reconciliation alerts, privileged-action alerts, redaction policy, or runbook-linked SLOs. Sentry records user email and enables replay on error, which may expose PII from registration/account/admin routes. + +### Scope + +- Define structured server log schema with environment, release, request/command/business/Stripe correlation IDs, state transition, latency, and sanitized error category. +- Add metrics for checkout attempts/results, reservations, verified paid propagation, webhook acceptance/process failures/quarantine/age, refunds/disputes, reconciliation mismatches, email failures, App Check/rate-limit rejects, and counter drift. +- Implement alerts and dashboards for objectives in `SYSTEM_DESIGN.md`; every alert links to an owner/runbook. +- Configure Sentry environments, sampling, retention, source maps, field/URL scrubbing, user pseudonymization, and replay masking/disablement on sensitive routes. +- Define analytics consent/purpose and prohibit PII/high-cardinality secrets in event parameters. +- Add deploy/release markers and a support-safe correlation code. + +### Acceptance criteria + +- [ ] Paid-but-unconfirmed, wrong-total, failed/quarantined webhook, dispute, refund failure, negative/drift counter, and stale reservation create actionable alerts. +- [ ] Logs/monitoring contain no raw body, secret, bearer token, Checkout URL, full email/IP/address/DOB/emergency contact. +- [ ] Sensitive browser form text/URL query state is not visible in Sentry replay/event payloads. +- [ ] Dashboards distinguish staging/test/live and release version. +- [ ] Alert test reaches primary and backup, and runbook drill resolves a synthetic incident. + +### Tests/evidence + +- Automated log-redaction snapshots with hostile/sensitive fixtures; synthetic alert tests; Sentry/analytics payload inspection; SLO dashboard screenshot/export without PII. + +### Agent handoff + +Do not add a high-cardinality metric label for email, business ID, Session ID, or Event ID. Keep those in restricted structured logs only as approved opaque references. + +--- + +## RESILIENCE-001 — Establish backup, restore, retention, and audited repair + +**Labels:** `priority:P1`, `type:reliability`, `type:compliance`, `area:firebase`, `area:privacy`, `size:L`, `needs-external-config` +**Status:** Proposed +**Depends on:** DATA-001, PAY-005 + +### Problem + +The repository has no verified Firestore/Auth/config backup inventory, restoration exercise, retention-aware backup policy, or standardized audited repair tooling. Payment records may need roll-forward rather than code rollback because Stripe continues emitting events. + +### Scope + +- Inventory recovery requirements for Firestore, Firebase Auth, Secret Manager versions, Stripe configuration/reports, email templates/suppression, DNS, GitHub environments, and source artifacts. +- Configure least-privilege encrypted Firestore backups/exports with owner-approved retention and monitoring. +- Document which external systems are authoritative and which configuration must be recreated rather than restored. +- Build isolated non-production restoration procedure and validate references/counters/indexes/rules. +- Run payment reconciliation after restore without contacting live Stripe from restored test data. +- Implement idempotent audited repair commands for reconciliation categories; no manual multi-field edits. +- Define release rollback/roll-forward compatibility, schema version, and checkout-disable behavior. +- Align backup deletion/minimization with approved retention and legal constraints. + +### Acceptance criteria + +- [ ] Scheduled backup succeeds and access/retention alerts are monitored. +- [ ] Restore into a new isolated project completes within approved recovery objective and passes integrity checks. +- [ ] Restored environment cannot send live email, call live Stripe, or serve production DNS. +- [ ] Counter/event/refund reconciliation reports expected results on restored fixtures. +- [ ] Repair commands are idempotent, permissioned, reasoned, and audited. +- [ ] Recovery gaps for Auth/Secrets/Stripe/DNS are explicitly documented with owners. + +### Tests/evidence + +- Dated restore drill, sampled record/hash/count report, rules/index verification, no-external-side-effect proof, repair dry-run/apply/rerun report. + +### Agent handoff + +Cloud backup configuration changes external state and needs an authorized owner. Never download a production backup into a local workspace for testing. + +--- + +## TEST-001 — Build the Stripe/Firebase security integration and E2E suite + +**Labels:** `priority:P0`, `type:testing`, `type:security`, `area:stripe`, `area:firebase`, `size:L` +**Status:** Proposed +**Depends on:** Core SEC/AUTH/ABUSE/PAY/RACE/MERCH/MAIL/DATA work + +### Problem + +Before this planning pass, Functions had seven tests and about 20% line coverage; webhook tests covered only invalid signature/method. Firestore rule tests cover many paths but intentionally asserted unsafe admin access. There is no test proving the cross-system sagas under concurrency/retry/provider failure. + +### Scope + +- Define test layers: pure schema/state reducers, Functions unit tests, Firestore/Auth/Functions emulator integration, signed Stripe fixtures/CLI, React flow tests, and isolated staging E2E. +- Build deterministic factories with synthetic PII and no live network fallback. +- Cover the full matrix in `STRIPE_COMMERCE_DESIGN.md`, including duplicate/out-of-order events, async, mismatches, idempotency failure injection, capacity/SKU concurrency, cancellation/payment races, refunds/disputes, email, confirmation, RBAC, App Check, and reconciliation. +- Add explicit production-host/key guard that fails tests if environment resembles production/live. +- Set meaningful coverage thresholds on trusted payment/auth/state modules; do not chase coverage on static UI. +- Make CI artifacts show test/coverage/reconciliation results without sensitive fixture data. + +### Acceptance criteria + +- [ ] Every P0 payment and auth invariant has at least one positive and one negative automated test. +- [ ] Concurrent last-seat/SKU tests are deterministic and pass repeatedly. +- [ ] Failure injection covers every Firestore-before/after-Stripe boundary and retry. +- [ ] Signed fixtures cover all configured Stripe event types, duplicates, ordering, livemode, amount/currency/reference mismatches. +- [ ] Rules tests prove protected paths for anonymous/member/every admin capability. +- [ ] Staging E2E passes with Stripe test mode and dedicated Firebase project. +- [ ] No test can invoke production or expose real PII/secrets. + +### Tests/evidence + +This issue is itself the test program. Produce a traceability table mapping each system invariant/risk ID to tests and attach the release test report. + +### Agent handoff + +Split by layer/domain into sequential PRs, starting with trusted state/schema and emulator fixtures. Do not make external network tests required for every fast unit-test run; keep a separate gated staging suite. + +--- + +## LEGAL-001 — Approve legal, tax, insurance, privacy, refund, and fulfillment policy + +**Labels:** `priority:P0`, `type:compliance`, `type:operations`, `status:blocked-owner`, `size:M` +**Status:** Blocked on MPRC leadership and qualified advisers +**Depends on:** None; runs in parallel + +### Problem + +Live Terms and Privacy pages explicitly say they are placeholders. Code cannot decide the club's refund/cancellation policy, waiver evidence/minor policy, merchandise tax nexus, shipping/returns, data retention, incident notification, accessibility obligations, or vendor agreements. + +### Scope + +Owners obtain appropriate legal, tax/accounting, insurance, and organizational review and record approved decisions for: + +- Terms, privacy notice, contact/security reporting, cookies/analytics/Sentry, vendors and international transfers if relevant. +- Event cancellation/reschedule/refund/transfer/substitution and processing-fee policy. +- Versioned waiver, evidence, minor/guardian requirements, and retention. +- Merchandise sales tax, supported jurisdictions, shipping/pickup, delivery, returns/exchanges/lost goods. +- Data collection necessity, subject access/deletion, field-specific retention, backup deletion, incident notification. +- Support, disputes, chargebacks, accessibility, acceptable use, and prohibited sales. + +Publish versioned approved content and configuration inputs without putting privileged advice/private data in the repository. + +### Acceptance criteria + +- [ ] Placeholder warnings and dates are replaced with approved versioned content before live mode. +- [ ] Every required data field has purpose/retention/access owner. +- [ ] Refund/transfer/cancellation and waiver decisions map to explicit state transitions. +- [ ] Tax/shipping/product decisions map to Stripe/catalog configuration. +- [ ] Vendor list/agreements and security/privacy contact are established. +- [ ] Leadership records approval and next review date privately. + +### Tests/evidence + +- Product/engineering traceability checklist from each approved rule to implementation issue/config/test. +- Content review for links, dates, contact, accessibility, and version/hash. + +### Agent handoff + +This issue cannot be completed autonomously. Agents may inventory questions, wire approved copy, and test versioning, but must not generate policy and mark it legally approved. + +--- + +## OPS-001 — Configure production systems and run a controlled live pilot + +**Labels:** `priority:P0`, `type:operations`, `area:stripe`, `area:firebase`, `needs-external-config`, `status:blocked-owner`, `size:L` +**Status:** Blocked by all launch gates +**Depends on:** CI-001, TEST-001, LEGAL-001, OBS-001, RESILIENCE-001 and every P0 commerce/security issue + +### Problem + +Repository code cannot prove external production configuration. MPRC needs owned accounts, isolated environments, MFA/roles, secrets, webhooks, App Check, IAM, DNS, email, Radar, alerts, backups, and a controlled live transaction before broadly accepting money. + +### Scope + +- Complete the ownership roster in `OPERATIONS_RUNBOOK.md` with primary/backup and least privilege. +- Create/verify dedicated staging Firebase project and Stripe test/sandbox endpoint; keep production isolated. +- Configure Secret Manager bindings, expected livemode/origin, commerce kill switch, App Check Enterprise/enforcement, TTL, budgets, IAM, protected GitHub environments/OIDC, email domain, Sentry redaction, backups, DNS/hosting, and Stripe event allowlist. +- Verify Stripe account legal/support/statement/payout settings, team MFA/passkeys/roles, Radar, tax/shipping decisions, webhook secret rotation, alert ownership, and reconciliation schedule. +- Run full staging dress rehearsal and incident/kill-switch/restore drills. +- Execute one approved low-value live purchase/registration, verify every system/report/email/counter, and optionally issue an approved refund. +- Observe and reconcile before broad enablement; document go/no-go with two-person approval. + +### Acceptance criteria + +- [ ] All P0 issues are `done` with evidence; no placeholder policy remains. +- [ ] Staging cannot reach live Stripe/production Firebase and local cannot reach either. +- [ ] Production secrets/roles/IAM/MFA/config are reviewed by two owners and rotation tested. +- [ ] Checkout kill switch, webhook failure, orphan payment, admin compromise, and restore drills pass. +- [ ] Controlled live transaction matches Stripe amount/currency/receipt/fee/payout, Firestore states/counters, email, and reconciliation. +- [ ] Any pilot refund/dispute path reconciles as expected. +- [ ] Broad launch has named coverage, alerting, freeze window, and rollback/roll-forward plan. + +### Tests/evidence + +- Private launch packet with commit/checks, non-secret configuration exports/screenshots, staging matrix, restore/incident drills, pilot Stripe/local reconciliation, approvals, and residual risks. + +### Agent handoff + +This issue changes live external state and requires explicit authorized owners. An agent may prepare checklists/scripts and analyze redacted results, but must not enable live checkout, alter DNS/payouts, or use production credentials without direct authority. + +--- + +## Suggested GitHub milestones + +1. **M0 — Safe development baseline:** SAFETY-001, CI-001A, CONFIG-001, initial SUPPLY-001. +2. **M1 — Trust boundaries:** SEC-001, AUTH-001, AUTH-002, OAUTH-001, ABUSE-001, PAY-001, PROMO-001. +3. **M2 — Payment integrity core:** PAY-002, PAY-003, RACE-001, MERCH-001. +4. **M3 — Complete lifecycle:** PAY-004, PAY-005, MAIL-001, DATA-001, DATA-002, MERCH-002. +5. **M4 — Operable platform:** AUTH-003, ADMIN-001, WEB-001, OBS-001, RESILIENCE-001, remaining CI/SUPPLY work. +6. **M5 — Launch qualification:** TEST-001, LEGAL-001, OPS-001. + +## Historical publication checklist + +The program is partially published. Treat unchecked rows as future governance work, not permission to bulk-create issues. + +- [x] Use an approved authenticated GitHub workflow. +- [ ] Create labels and milestones above. +- [ ] Create only dependency-ready, non-duplicate issues after searching the live milestone; never bulk-publish the snapshot. +- [ ] Add dependency links and project-board status. +- [x] Mark SAFETY-001, SEC-001, PAY-003, and PROMO-001 according to the verified working-tree result while keeping merge/deploy/full-acceptance gaps open. +- [ ] Assign business-owner issues to humans; do not assign LEGAL-001/OPS-001 solely to a coding agent. +- [ ] Link root design documents from every issue and link GitHub issue URLs back into this file. +- [ ] Do not include secrets, private account IDs, customer data, or confidential legal advice in issue bodies/evidence. diff --git a/GITHUB_ISSUE_SLICES.md b/GITHUB_ISSUE_SLICES.md new file mode 100644 index 0000000..aa9e8bf --- /dev/null +++ b/GITHUB_ISSUE_SLICES.md @@ -0,0 +1,130 @@ +# MPRC Atomic GitHub Issue Slices + +**Prepared:** 2026-07-12 +**Purpose:** Decompose the large trackers in `GITHUB_ISSUES.md` into one-agent, one-reviewable-PR implementation issues +**Implementers:** Claude Sonnet, Terra/Luna, Codex, or a human contributor following `AGENTS.md` + +> **Live status note (2026-07-12):** this is a design catalog. GitHub issues, labels, claims, and comments are authoritative. Foundation children/trackers are published as [#99–#106](https://github.com/Run-MPRC/Run-MPRC.github.io/issues?q=is%3Aissue%20milestone%3A%22Secure%20Commerce%20%26%20Platform%20Hardening%22); search before creating anything from a row. + +## Assignment contract + +Every `size:L` entry in `GITHUB_ISSUES.md` is a tracker, not a coding assignment. When a dependency-ready child is not already represented in GitHub, publish it as one focused issue, link it to the tracker, and assign only one active child at a time. Each child inherits the parent problem statement, security constraints, out-of-scope boundaries, and the definition of done in `IMPLEMENTATION_PLAN.md`. + +An agent-ready child has one observable outcome, one trust boundary, explicit dependencies, tests, and no unresolved owner decision. One child normally maps to one branch and one pull request. Split again if work crosses unrelated domains or migrations. `owner_action` permits an agent to prepare checklists/scripts and analyze redacted evidence, but only an explicitly authorized MPRC owner may change IAM, cloud accounts, provider settings, DNS, legal policy, production data, or live payments. + +## Baseline, CI, and supply chain + +| ID | Status/dependency | Bounded deliverable | Close evidence | +| --- | --- | --- | --- | +| CI-001A | ready; SAFETY-001 | Add non-mutating fail-closed frontend lint and one deterministic local/CI quality gate for SPA, frontend, Functions, Rules, and build. | Deliberate lint/test failure blocks; clean-worktree check; all local suites pass. | +| CI-001B | owner_action; CI-001A | Protect backend-first staging/production deployment; pin tools/Actions; remove frontend service-account JSON; configure OIDC/WIF, approvals, missing-config failure, smoke and roll-forward. | PR gate and negative credential run; private two-owner IAM review; staging deployment evidence. | +| SUPPLY-001A | ready; CI-001A | Prove `gpxparser` unused and remove its obsolete transitive chain. | `rg`/tree proof, before/after audits, root lockfile diff, full frontend checks. | +| SUPPLY-001B | ready; CI-001A | Patch React Router within compatible 6.x and resolve/test future flags without route redesign. | Navigation/callback tests, audit delta, build. | +| SUPPLY-001C | proposed; CI-001A | Upgrade Firebase web SDK within one compatibility boundary. | Auth/Firestore/Functions/App Check/emulator tests and bundle/audit diff. | +| SUPPLY-001D | proposed; CI-001A | Upgrade Firebase Admin/Functions in one supported set. | Node 20 lint/unit/emulator/trigger tests and deploy compatibility notes. | +| SUPPLY-001E | proposed; PAY-003C | Upgrade Stripe SDK and deliberately select/test an API version. | Signed fixture/test-mode diff and rollback notes; no live endpoint change. | +| SUPPLY-001F | proposed; CI-001B, A–C | Replace CRA while preserving env handling, SPA callbacks, sitemap/SEO, code splitting, and deploy output. | Full CI, bundle/SEO comparison, staging dry run. | +| SUPPLY-001G | proposed; CI-001A | Add dependency updates, audit exceptions, secret scan, SBOM, and lockfile review policy. | Safe sample update; deliberate secret detection; sanitized SBOM. | + +## Identity, OAuth, and abuse controls + +| ID | Status/dependency | Bounded deliverable | Close evidence | +| --- | --- | --- | --- | +| AUTH-003A | proposed; AUTH-001/002, SEC-001 | Define capability matrix, additive claims, role-change audit, and idempotent legacy-admin migration. | Table-driven claim/action tests and emulator dry-run/rollback. | +| AUTH-003B | proposed; AUTH-003A | Apply capabilities to server functions and Firestore projections/rules. | Every capability/action pair has allow/deny tests. | +| AUTH-003C | owner_action; AUTH-003B | Enforce privileged MFA, recent-auth thresholds, revocation, and break-glass/last-admin policy. | Staging failure/success demonstrations and private owner inventory. | +| AUTH-003D | proposed; AUTH-003B/C | Migrate admin UI/routes/token refresh, prove parity, then remove legacy alias. | Route/capability/demotion tests and staged operator sign-off. | +| OAUTH-001A | proposed; SEC-001, AUTH-003B | Implement server-only, transaction/version-safe Strava token refresh so rotated refresh tokens cannot be lost. | Concurrent refresh/retry/conflict emulator tests. | +| OAUTH-001B | owner_action; OAUTH-001A | Minimize scopes, implement disconnect/provider revocation, record encryption decision, and add access/refresh/revoke audit. | Test-provider revoke/refresh evidence and approved threat decision. | +| ABUSE-001A | ready; CI-001A | Replace custom fail-open App Check on an agreed sensitive-callable group with native runtime enforcement and explicit emulator behavior. | Missing/invalid/valid tests plus staged metrics before enforcement. | +| ABUSE-001B | proposed; PAY-001A | Replace raw IP/email rate keys with versioned HMAC, trusted address extraction, multi-scope transactional limits, rotation, and TTL-compatible records. | Spoof/boundary/concurrency/rotation tests; no raw identifiers. | +| ABUSE-001C | owner_action; A/B | Configure instance/concurrency/timeouts, budgets, TTL, and App Check/rate/cost alerts. | Synthetic alert and private cloud-setting review. | + +## Configuration, validation, and payment foundation + +| ID | Status/dependency | Bounded deliverable | Close evidence | +| --- | --- | --- | --- | +| CONFIG-001A | ready; CI-001A | Add typed server configuration requiring environment name, validated HTTPS/local origin, explicit Stripe mode, and environment/key compatibility; remove production fallbacks. | Missing/malformed/local-live tests fail before Firestore/Stripe/email. | +| CONFIG-001B | proposed; CONFIG-001A | Implement a server-owned global/per-domain commerce kill switch checked by every Session/refund/catalog command while webhook/reconciliation remain active. | Disabled-command tests prove no external call; webhook/reconcile tests still pass. | +| PROMO-001 | implemented_locally; PAY-001A for completion | Disable promotion codes in both Session creators and quarantine any nonzero Stripe discount until an approved snapshot contract exists. | Session payload assertions plus signed discounted-Session quarantine tests. | +| PAY-001A | ready; CI-001A | Add shared strict-object, bounds/depth, integer-cents/currency/date/URL/email primitives, safe errors, and redacted logging. | Prototype/deep/Unicode/money/unknown-key tests prove no external call. | +| PAY-001B | proposed; PAY-001A | Apply race/volunteer/free schemas, validate server event custom fields/options, and persist price/waiver snapshots. | Positive/hostile dynamic-field and snapshot tests. | +| PAY-001C | proposed; PAY-001A | Apply merchandise/variant/quantity schemas and immutable merchandise price snapshots. | Variant/quantity/money/payload tests. | +| PAY-001D | proposed; PAY-001A, AUTH-003A | Apply bounded schemas to lookup/refund/cancel/substitute/late/comp/role/tracking/export commands. | Endpoint matrix with hostile/boundary/no-side-effect tests. | +| PAY-002A | proposed; PAY-001A | Build pure separate payment/registration/fulfillment/refund/dispute reducers and additive legacy mapping. | Exhaustive allowed/forbidden/reordered transition tests and dry-run map. | +| PAY-002B | proposed; PAY-002A | Build caller-scoped command journal, UUID/payload fingerprint, same-result replay, conflict rejection, lease/attempt/error/audit, and Stripe key scheme. | Concurrent duplicate/conflict and injected-failure emulator tests. | +| PAY-002C | proposed; PAY-001B, PAY-002B, RACE-001B | Convert race checkout to persistence-first record/hold plus deterministic Stripe Session saga. | One record/Session on retry at every failure boundary. | +| PAY-002D | proposed; PAY-001C, PAY-002B, MERCH-001B | Convert merchandise/catalog creation to persistence-first order/hold plus deterministic Stripe keys. | One order/Session/catalog object on retry; failure recovery tests. | + +## Webhooks, capacity, inventory, and cancellation + +| ID | Status/dependency | Bounded deliverable | Close evidence | +| --- | --- | --- | --- | +| PAY-003A | implemented_locally | Review/land current ingress slice: signature/method, explicit livemode, payment mode, event ledger, reference resolution, money/discount checks, monotonic refunds, terminal guards, quarantine, generic errors, webhook-only secret. | Functions lint; signed positive/negative/duplicate/out-of-order unit suite and residual-gap notes. | +| PAY-003B | proposed; PAY-002A–D, RACE/MERCH reducers | Move event handling to strict versioned schemas and canonical reducers; atomically finalize/release counters and outbox projections. | Emulator transactions for both domains, distinct duplicates, reorder, async and terminal cases. | +| PAY-003C | owner_action; A/B, CI-001A | Add retry/quarantine worker/lease semantics, TTL/dead-letter monitoring, and Stripe CLI test-mode delivery rehearsal. | Redacted signed immediate/delayed/retry/rotation evidence and alerts. | +| RACE-001A | proposed; PAY-002A | Add capacity/hold schema, expiry/indexes, compatibility reader, and dry-run counter backfill. | Full/closed/legacy fixture report and idempotent rerun. | +| RACE-001B | proposed; A, PAY-002B | Implement transaction-safe reserve/confirm/release across paid/free/comp/volunteer/admin paths. | Repeated concurrent last-seat, duplicate, expiry and nonnegative-counter tests. | +| MERCH-001A | proposed; PAY-001C, PAY-002A | Add canonical SKU/variant and conventional `onHand`, `reserved`, `sold` schema plus catalog migration: `available = onHand - reserved`. | Legacy/size/color/zero-stock migration fixtures and invariant report. | +| MERCH-001B | proposed; A, PAY-002B | Implement deterministic multi-SKU reserve; on paid decrement `reserved` and `onHand`, increment `sold`; release only `reserved`; approved restock increments `onHand`. | Concurrent last-SKU/multi-line/retry/return tests; no negative stock. | +| PAY-004A | proposed; PAY-002B/C, RACE-001B | Implement one-record cancellation that expires Session/releases hold with already-paid/expired/network race recovery. | Retry and payment/cancel race tests; no resurrection. | +| PAY-004B | proposed; A, RACE/MERCH services | Implement bulk event/product closure as bounded idempotent jobs over active Sessions/holds. | Partial failure/resume/rerun report. | +| PAY-004C | proposed; PAY-002C, PAY-003B | Replace late reusable Payment Links with one-off Checkout; inventory/deactivate/reconcile legacy links. | Retry/reuse/expiry/payment tests; owner performs Dashboard deactivation. | + +## Refunds, privacy, mail, administration, and observability + +| ID | Status/dependency | Bounded deliverable | Close evidence | +| --- | --- | --- | --- | +| PAY-005A | proposed; PAY-001D, PAY-002A/B | Add idempotent refund commands, remaining-balance/concurrency validation, stable Stripe keys, and event-finalized totals. | Same/conflicting/concurrent partial/full/timeout tests. | +| PAY-005B | proposed; PAY-003B | Model dispute lifecycle for orders and registrations with finance alert references. | Duplicate/reordered created/updated/lost/won fixtures. | +| PAY-005C | proposed; A/B | Build read-only scheduled/operator reconciliation and mismatch/quarantine report. | Orphan/mismatch/refund/event/counter synthetic report. | +| PAY-005D | proposed; C, AUTH-003B | Build separately authorized idempotent repair commands with reason and append audit. | Dry-run/apply/rerun and forbidden-capability tests. | +| MAIL-001A | proposed; PAY-003B | Build deterministic escaped transactional outbox and truthful queued/delivery state for registration/order confirmation. | Retry/concurrency/hostile-HTML/provider-failure tests. | +| MAIL-001B | owner_action; A, LEGAL-001 | Add approved cancel/refund/fulfillment templates and sender/DNS/bounce/suppression operations. | Template snapshots and private SPF/DKIM/DMARC/provider evidence. | +| DATA-001A | proposed; PAY-002C/D | Replace confirmation query bearer tokens with verified Session/UID projection; hash/expire/scrub anonymous free-flow capabilities. | Cross-user/guess/replay/expiry/history/referrer tests. | +| DATA-001B | blocked_owner_decision; LEGAL-001 | Define classification/purpose projections and remove PII from logs/monitoring/admin views. | Owner-approved matrix and projection/redaction snapshots. | +| DATA-001C | blocked_owner_decision; B, RESILIENCE-001A | Add dry-run-first retention/minimization and authenticated access/deletion workflows. | Date/status/hold/cross-user/dry-run/apply/rerun tests. | +| ADMIN-001A | proposed; AUTH-003B, PAY-001A, PAY-002B | Build common privileged command middleware and append-only audit. | Capability/auth/schema/idempotency/audit tests. | +| ADMIN-001B | proposed; A, RACE-001A | Move event/registration configuration and lifecycle operations to scoped APIs. | Field/state/capability tests and UI parity. | +| ADMIN-001C | proposed; A, MERCH-001A | Move catalog/order/inventory/fulfillment operations to scoped APIs. | Field/state/capability/inventory tests and UI parity. | +| ADMIN-001D | proposed; A, DATA-001B | Add purpose-specific minimum check-in/emergency/finance/fulfillment exports with re-auth/reason/bounds/formula defense/audit. | Column/PII/large/formula/capability snapshots. | +| ADMIN-001E | proposed; B–D | Cut clients over and tighten remaining direct rules to server-only. | Full Rules/admin regression; no compatibility catch-all. | +| OBS-001A | proposed; PAY-003A | Add correlation-based structured logs with allowlisted fields/redaction. | Sensitive/hostile log snapshots. | +| OBS-001B | proposed; A, PAY-005C | Add low-cardinality payment/event/refund/reconcile metrics and SLO dashboards. | Synthetic metric and dashboard review. | +| OBS-001C | owner_action; A/B | Configure alerts, Sentry/analytics privacy policy, retention, and incident drill. | Redacted payload inspection and triggered alert/runbook handoff. | + +## Merchandise policy, hosting, resilience, test program, and launch + +| ID | Status/dependency | Bounded deliverable | Close evidence | +| --- | --- | --- | --- | +| MERCH-002A | blocked_owner_decision; LEGAL-001 | Record approved tax, shipping/pickup, countries, returns/exchanges, address retention, and support inputs. | Leadership/accountant traceability sign-off. | +| MERCH-002B | proposed; A, MERCH-001B | Apply server shipping/tax config and actual total snapshots/reconciliation. | Test-mode taxable/pickup/shipping/mismatch fixtures. | +| MERCH-002C | proposed; A/B, ADMIN-001C | Add fulfillment/return/restock state and audited operator actions. | Forbidden transitions and stock adjustment tests. | +| MERCH-002D | proposed; A–C, MAIL-001B | Add minimum customer communications/UI and address minimization. | Lifecycle/email/PII projection tests. | +| WEB-001A | owner_action; CI-001B | Provision isolated staging hosting, rewrites, canonical/sitemap behavior, deploy and rollback. | ADR and deep-route staging evidence. | +| WEB-001B | proposed; A | Add CSP/HSTS/frame/referrer/permissions/MIME/cache policy. | Automated header/CSP and browser flow smoke. | +| WEB-001C | proposed; A/B, PAY-001A | Sanitize/validate stored HTML/URLs and migrate unsafe content. | XSS/protocol/legacy content fixtures. | +| WEB-001D | owner_action; A–C | Execute controlled DNS cutover/rollback and retire old deploy path without enabling commerce. | Dated owner-approved cutover evidence. | +| RESILIENCE-001A | owner_action; DATA-001B, LEGAL-001 | Inventory RPO/RTO and configure least-privilege monitored backup/export retention. | Private access review and scheduled backup success. | +| RESILIENCE-001B | proposed; A, staging | Restore into isolated no-live-Stripe/email/DNS project and validate rules/indexes/counts/references/reconciliation. | Dated timed restore drill and no-external-side-effect proof. | +| TEST-001A | ready; CI-001A | Add synthetic factories, signed Stripe helpers, production-project/live-key/network guards, and artifact scrubber. | Deliberate guard and redaction failures. | +| TEST-001B | proposed; A | Add Auth/Firestore/Functions emulator saga, RBAC, Rules, and App Check integration harness. | Repeatable local/CI demo-project run. | +| TEST-001C | proposed; B, race/merch reducers | Add deterministic capacity/inventory concurrency and every local/Stripe failure-boundary test. | Repeated no-flake invariant matrix. | +| TEST-001D | proposed; B, PAY-003/PAY-005 | Add signed Stripe lifecycle, duplicate/reorder, async, refund/dispute/reconcile fixtures. | Risk/invariant traceability and coverage thresholds. | +| TEST-001E | owner_action; A–D, staging | Add browser/test-mode E2E and exact-commit release traceability report. | Redacted staging report; no live objects. | +| LEGAL-001A | owner_action | Approve privacy/terms/vendors/security contact/retention/access-deletion inputs. | Versioned approval and engineering traceability. | +| LEGAL-001B | owner_action | Approve race waiver/minor/evidence/refund/cancel/transfer/insurance inputs. | Versioned approval mapped to DATA/RACE states. | +| LEGAL-001C | owner_action | Approve merchandise tax/shipping/returns/refund/support inputs. | Accountant/leadership approval mapped to MERCH config. | +| OPS-001A | owner_action; all prerequisites ready | Complete ownership, MFA, access, secret rotation, and account inventories. | Private two-owner review. | +| OPS-001B | owner_action; A, TEST-001E | Configure isolated staging/provider systems and run full dress rehearsal. | Exact-commit matrix; any P0 gap blocks. | +| OPS-001C | owner_action; B, RESILIENCE-001B | Run kill-switch, webhook/orphan, admin-compromise, provider-outage, rotation, and restore drills. | Timed drill/corrective-action records. | +| OPS-001D | owner_action; every P0 done, C | Execute one explicitly approved bounded live pilot, reconcile/observe, then make two-person go/no-go decision. | Private financial/system evidence; default is disable on missing/anomalous evidence. | + +## Publication checklist for remaining, non-duplicate rows + +- [ ] Search the live milestone and issue history before creating anything. +- [ ] Publish large parents as trackers, never as one agent assignment. +- [ ] Publish only dependency-ready, unrepresented children; apply current status/owner labels accurately. +- [ ] Copy the parent constraints, this row, affected paths, and shared definition of done into each child. +- [ ] Link the exact PR/test/migration/config evidence before closing a child. +- [ ] Close a parent only after every child and every parent-level acceptance criterion passes. diff --git a/IMPLEMENTATION_PLAN.md b/IMPLEMENTATION_PLAN.md new file mode 100644 index 0000000..b1f38c4 --- /dev/null +++ b/IMPLEMENTATION_PLAN.md @@ -0,0 +1,301 @@ +# MPRC Secure Commerce Implementation Plan + +**Status:** Approved technical sequence and assessment snapshot; GitHub issues are the live execution/status source +**Last reviewed:** 2026-07-12 +**Live status:** [Secure Commerce & Platform Hardening milestone](https://github.com/Run-MPRC/Run-MPRC.github.io/milestone/2) +**Design catalog:** [GITHUB_ISSUES.md](./GITHUB_ISSUES.md) + +This plan turns the target architecture into a dependency-ordered delivery program. It is deliberately staged: payment correctness and trust boundaries come before features, and a controlled pilot comes before a broad launch. + +## 1. Program outcome + +MPRC can open a race or merchandise item for sale only when the platform can: + +- Prove one client request creates at most one active Checkout Session and business record. +- Prevent participant capacity and SKU stock from going below zero under concurrency. +- Mark payment successful only from verified Stripe state with matching amount, currency, environment, and business reference. +- Tolerate duplicate, delayed, and out-of-order Stripe events. +- Cancel/expire unpaid Sessions, issue idempotent refunds, handle disputes, and reconcile both systems. +- Enforce verified identity, scoped admin capabilities, App Check, narrow Firestore rules, and least-privilege cloud access. +- Protect/minimize PII, waiver evidence, exports, email, monitoring, and OAuth secrets. +- Pass automated security/correctness tests and a documented staging dress rehearsal. +- Operate under approved legal, tax, insurance, privacy, refund, and fulfillment policies. + +## 2. Delivery rules + +1. One issue is the unit of work. Avoid combining unrelated refactors with a security/payment issue. +2. Every issue begins from a passing or explicitly documented baseline and adds tests for its risk. +3. Schema changes are additive first. Backfills are idempotent, dry-runnable, and report counts/anomalies. +4. Backend/rules/indexes deploy before dependent clients. Legacy reads remain until migration evidence is complete. +5. No issue requires production secrets for development or CI. +6. Live-mode enabling must become a separate protected operation. The current `main` workflow automatically publishes Pages and attempts Firebase deployment on merge, so it is non-compliant until CI-001 is complete. +7. A provider Console setting is not complete until its non-secret configuration and verification evidence are recorded privately. +8. Security controls fail closed in hosted environments and remain developer-friendly only in explicit local/CI environments. +9. Do not trade payment integrity for UI responsiveness. Confirmation may say “processing”; it must not guess “paid.” +10. Legal/tax/insurance questions are escalated to qualified owners, not decided by an implementation agent. + +## 3. Dependency map + +```mermaid +flowchart TD + S0["SAFETY-001\nCallback + emulator isolation"] --> CI1["CI-001\nReliable gated CI/deploy"] + SUP["SUPPLY-001\nDependency remediation"] --> CI1 + + SEC["SEC-001\nFirestore trust boundaries"] --> ADMIN["ADMIN-001\nScoped admin APIs/audit"] + AUTH1["AUTH-001\nVerified privileged identity"] --> AUTH3["AUTH-003\nScoped RBAC + MFA"] + AUTH2["AUTH-002\nSecure membership sync"] --> AUTH3 + AUTH3 --> ADMIN + ABUSE["ABUSE-001\nApp Check + rate limits"] --> PAY1["PAY-001\nStrict schemas + money types"] + + PAY1 --> PAY2["PAY-002\nIdempotent commands + states"] + PAY2 --> PAY3["PAY-003\nWebhook inbox + reducer"] + PAY2 --> RACE["RACE-001\nCapacity reservations"] + PAY2 --> MERCH1["MERCH-001\nVariant inventory reservations"] + PAY3 --> PAY4["PAY-004\nCancellation + late checkout"] + PAY3 --> PAY5["PAY-005\nRefunds, disputes, reconciliation"] + RACE --> PAY4 + MERCH1 --> PAY4 + + PAY3 --> MAIL["MAIL-001\nIdempotent escaped email outbox"] + PAY3 --> DATA1["DATA-001\nConfirmation + retention"] + RACE --> DATA2["DATA-002\nWaiver evidence"] + MERCH1 --> MERCH2["MERCH-002\nShipping/tax/communications"] + + ADMIN --> TEST["TEST-001\nPayment/security integration suite"] + PAY4 --> TEST + PAY5 --> TEST + MAIL --> TEST + DATA1 --> TEST + + WEB["WEB-001\nOwned hosting + browser defenses"] --> OPS["OPS-001\nProduction configuration + pilot"] + OBS["OBS-001\nMetrics, alerts, SLOs"] --> OPS + BACKUP["RESILIENCE-001\nBackup/restore"] --> OPS + TEST --> OPS + LEGAL["LEGAL-001\nPolicy/tax/insurance approvals"] --> OPS + CI1 --> OPS +``` + +Parallel work is safe only across branches that do not share schemas or security boundaries. For example, legal policy decisions, hosting evaluation, dependency inventory, and initial observability design can proceed while payment state work is underway. Webhook, state, refund, capacity, and inventory changes require close sequencing. + +## 4. Phases and exit gates + +### Phase 0 — Stop accidental harm and establish a trustworthy baseline + +**Issues:** SAFETY-001, CONFIG-001, SUPPLY-001, first part of CI-001, LEGAL-001 discovery +**Purpose:** Ensure local work cannot touch production, external callbacks survive routing, known vulnerable dependency chains are reduced, and tests mean what they say. + +Exit gate: + +- Local Auth, Firestore, and Functions all use emulators with a regression test. +- GitHub Pages callbacks preserve path/query/hash or the hosting migration has replaced the workaround. +- Frontend test harness, Functions tests, and Rules tests pass in CI; lint failure is not ignored. +- Unused vulnerable dependency chains are removed and direct safe patches applied. +- Live checkout remains disabled and placeholder policies are visible as a launch blocker. + +**Assessment snapshot, not current `main`:** the combined working tree contains a locally verified SAFETY-001 candidate, but issue [#99](https://github.com/Run-MPRC/Run-MPRC.github.io/issues/99) is queued behind #104. Until #99 is isolated, reviewed, merged, and reverified, callback/emulator behavior is **NOT AVAILABLE YET**. + +### Phase 1 — Establish trust boundaries + +**Issues:** SEC-001, AUTH-001, AUTH-002, AUTH-003, OAUTH-001, ABUSE-001, PAY-001 +**Purpose:** No browser/admin claim can bypass secret/financial boundaries; identities and inputs have reliable meaning. + +Exit gate: + +- Browser admins cannot read OAuth secrets, send arbitrary mail, alter rate limits/audit events, or directly mutate financial state. +- Member/admin grants require verified email and authoritative club membership. +- Legacy shared-key member sync is removed/replaced. +- Privileged actions use scoped claims, MFA/recent authentication policy, and durable audit. +- Sensitive callables enforce App Check at the Firebase runtime. +- All public/admin request shapes are strict, bounded, server-validated, and covered by hostile-input tests. + +### Phase 2 — Build the payment integrity core + +**Issues:** PROMO-001, PAY-002, PAY-003, RACE-001, MERCH-001 +**Purpose:** Make Checkout, state transitions, webhooks, capacity, and inventory retry-safe and concurrency-safe. + +Exit gate: + +- Stable request/command IDs and Stripe idempotency keys prevent duplicate Sessions/refunds/products. +- Payment state is separate from registration/fulfillment state or an explicit compatible migration is in place. +- Stripe event inbox deduplicates Event IDs and business transitions tolerate distinct duplicate/out-of-order Events. +- Unpaid `completed` Sessions are not fulfilled; async success/failure works. +- Amount, currency, environment, metadata, business reference, and allowed predecessor state are verified. +- Concurrent last-seat and last-SKU tests prove no oversell. +- Session creation is persistence-first, and known failures release holds once. + +**Current work:** A PAY-003 webhook safety slice is implemented and unit-verified in this repository pass after the high-level design was recorded. It must not be treated as the complete payment core until PAY-001/PAY-002, capacity, inventory, provider/emulator integration, and production operations close. + +### Phase 3 — Complete lifecycle and customer safety + +**Issues:** PAY-004, PAY-005, MAIL-001, DATA-001, DATA-002, MERCH-002 +**Purpose:** Safely handle cancellation, late registration, refunds, disputes, communications, privacy, waivers, shipping, and tax. + +Exit gate: + +- Cancelling an unpaid record expires its Stripe Session and releases its hold exactly once. +- Reusable late Payment Links are removed or safely constrained; one registration cannot be charged repeatedly. +- Refunds use idempotent commands and verified cumulative totals; disputes alert finance for both domains. +- Daily reconciliation detects missing/mismatched Stripe/local state and safe repair is audited. +- Email is escaped, outbox-idempotent, delivery-aware, and has SPF/DKIM/DMARC operations. +- Confirmation capabilities are short-lived/hash-stored or identity-owned and scrubbed from browser history. +- Waiver evidence is immutable/versioned and never fabricated by admin creation. +- Shipping, return, tax, and customer communication policies are approved and implemented. + +### Phase 4 — Administrative, privacy, and platform hardening + +**Issues:** ADMIN-001, WEB-001, remaining SUPPLY-001/CI-001, OBS-001, RESILIENCE-001 +**Purpose:** Make routine operation least-privilege, observable, recoverable, and safe in the browser and supply chain. + +Exit gate: + +- Admin APIs return role-appropriate projections and append durable audit events. +- Exports are purpose-specific, minimal, re-authenticated, bounded, and audited. +- Commerce site has controlled SPA rewrites and CSP/HSTS/frame/referrer/permissions headers. +- Stored arbitrary HTML is removed or sanitized under strict policy. +- No unexplained critical/high production dependency findings. +- Production deployment uses protected environments and short-lived least-privilege cloud credentials. +- Payment SLOs, structured redacted logs, and actionable alerts are live. +- Backup restoration into isolated infrastructure succeeds and is documented. + +### Phase 5 — End-to-end qualification + +**Issue:** TEST-001 plus final closure evidence from all prior phases +**Purpose:** Prove the whole system, not only isolated functions. + +Exit gate: + +- Full staging matrix in `STRIPE_COMMERCE_DESIGN.md` passes. +- Deliberately missed, duplicated, delayed, and out-of-order events reconcile correctly. +- No PII/secrets appear in logs, monitoring, fixtures, URLs, or exports beyond approved projections. +- Firestore Rules emulator tests prove every deny boundary. +- Security/dependency/build/test reports attach to the release commit. +- A rollback/roll-forward and checkout-disable drill succeeds. + +### Phase 6 — Controlled production pilot + +**Issues:** OPS-001 and approved LEGAL-001 +**Purpose:** Configure external systems and introduce live risk gradually. + +Exit gate: + +- Two-person review of Stripe/Firebase/GitHub/DNS/email/Sentry ownership, MFA, roles, secrets, environment, and alerts. +- Approved terms, privacy, waiver, refund, tax, shipping, retention, and support policies are published and versioned. +- One authorized low-value live transaction is paid, fulfilled/confirmed, reconciled, and—if planned—refunded. +- Finance confirms Stripe Dashboard, local records, receipt, fee/refund, and payout reporting. +- Pilot observation window has zero unexplained mismatches before broad opening. + +## 5. Workstream details + +### Identity and access + +The current three-role model is retained only as a compatibility layer. First remove the global admin rule and require verified email. Then introduce capabilities (`event_manager`, `finance_admin`, `shop_manager`, `identity_admin`, `platform_admin`) through server-managed claims. Keep claims limited to access control. Every grant/demotion has actor, target, reason, timestamp, old/new capability set, and token revocation/refresh behavior. + +### Commerce core + +Define a shared domain library before implementing independent race and shop code. It owns money validation, identifiers, state transitions, audit event shape, Stripe metadata schema, and idempotency-key construction. Checkout services then compose domain-specific reservation adapters. The webhook reduces verified Stripe state into the same state machine; it does not implement a second conflicting set of transitions. + +### Data and privacy + +Separate operational high-risk fields from long-lived accounting/waiver evidence. Store only required fields, project narrow views to admins, and define field-specific minimization. Implement retention jobs in report-only mode first, review sample effects, then enable deletion/anonymization with audit. + +### Operations and reliability + +Use structured correlation across checkout request, business record, Stripe Session/PaymentIntent/Event, refund command, email outbox, and reconciliation report. Dashboards show rates and categories rather than PII. Alerts include a runbook and owner. + +## 6. First implementation tranche + +The first tranche deliberately combines one urgent safety repair with the most complex payment foundation: + +1. **SAFETY-001 — callback and emulator isolation:** preserve path/query/hash through GitHub Pages and connect local Functions to the emulator. This prevents paid/OAuth callbacks from breaking and local code from reaching production unexpectedly. +2. **PAY-003 — webhook event inbox and safe reducer:** add event replay protection, async-aware payment confirmation, expected total/currency validation, direct metadata lookup, terminal-state guards, and lifecycle tests. +3. **SEC-001 — Firestore trust boundaries:** remove recursive browser-admin authority, protect secrets/financial state, retain only allowlisted catalog administration, and reverse unsafe rule tests. This is implemented locally and awaits review/deployment evidence. +4. **PROMO-001 — fail-safe monetary policy:** disable promotion entry and quarantine unexplained discount/tax/shipping adjustments. This safety default is implemented locally; creator tests/provider inventory remain. +5. **Identity/config next:** AUTH-001A [#98](https://github.com/Run-MPRC/Run-MPRC.github.io/issues/98) merged at `ce22c110`, proving source/tests for unverified-target rejection at the existing role-grant endpoints. Its Firebase deployment skipped, so backend-live status is unproven and the remaining AUTH-001 parent work stays open. Continue CONFIG-001 and the uncompleted AUTH-001 guards/Rules/refresh/audit scope without duplicating #98. +6. **Then:** PAY-001/PAY-002 so checkout creation, refunds, and every later handler share strict schemas and idempotent state. +7. **Then:** RACE-001 and MERCH-001 transactional reservations. + +Why this order: SAFETY-001 removes immediate accidental-production and callback hazards without schema impact. PAY-003 closes the most dangerous “unpaid looks paid” and replay gaps while establishing test fixtures. The subsequent trust-boundary and command/state work completes the surrounding architecture; no live launch is authorized by the partial webhook patch alone. + +## 7. Issue readiness requirements + +An issue is ready for Claude Sonnet, Terra/Luna, or another implementation agent when it contains: + +- One concrete outcome and why it matters. +- Exact dependencies and known affected files/surfaces. +- Current behavior with evidence. +- In-scope and explicitly out-of-scope work. +- Data migration/backward compatibility expectations. +- Acceptance criteria phrased as observable results. +- Required positive, negative, retry, concurrency, and authorization tests. +- Commands/fixtures/environment that do not require production access. +- Security/privacy/logging constraints. +- Documentation and operational handoff updates. +- Size small enough for one focused pull request; otherwise split first. + +Small/medium issue templates in [GITHUB_ISSUES.md](./GITHUB_ISSUES.md) and the one-agent children in [GITHUB_ISSUE_SLICES.md](./GITHUB_ISSUE_SLICES.md) meet this standard. Large parents are trackers and must not be assigned wholesale. + +## 8. Definition of done + +Every issue is done only when: + +- Implementation and data changes match the acceptance criteria. +- New logic has positive and negative tests; security/payment logic includes retry/idempotency tests. +- Existing relevant tests, lint, Rules tests, and production build pass. +- No test reaches production or uses real PII/secrets. +- Logs/errors are reviewed for PII/secret leakage. +- Rules/IAM/provider configuration is verified in the intended non-production environment. +- Backward compatibility, rollout, rollback/roll-forward, and migration reports are documented. +- Root design/runbook/backlog status is updated if architecture or operations changed. +- Pull request explains residual risk and follow-up issues. +- For external configuration, a named owner stores private verification evidence. + +A passing unit test alone is not sufficient for a payment or authorization issue. + +## 9. Agent execution protocol + +For future autonomous agents: + +1. Read `AGENTS.md`, this plan, the linked issue, and only the relevant design/runbook sections. +2. Inspect current code and tests; do not assume this 2026 assessment is still current. +3. Confirm the worktree and preserve unrelated/user changes. +4. State the intended invariant and failure cases before editing. +5. Implement the smallest compatible slice; avoid unrelated formatting/dependency churn. +6. Use Firebase emulators and Stripe test fixtures/CLI only. +7. Add tests that fail before and pass after the change. +8. Run focused checks, then the broader required suite. +9. Review the diff for direct Firestore financial writes, missing App Check/Auth, client price trust, PII logs, secret values, non-idempotent external calls, and impossible state transitions. +10. Report exact evidence, remaining risks, and which issue should run next. + +Agents must stop and request an owner decision for legal text, tax, insurance/waiver policy, live credentials, destructive production migration, account/DNS ownership, and any choice that changes customer money or data policy beyond the issue. + +## 10. Estimation and parallelism + +Sizes in the backlog are relative: + +- **S:** narrow code/config change with focused tests, usually one subsystem. +- **M:** multiple files with a small schema/API change or emulator integration. +- **L:** cross-boundary state/data migration with failure recovery and integration tests; split if acceptance cannot fit one focused PR. + +Suggested parallel lanes after Phase 0: + +- Lane A: Firestore rules, identity, admin RBAC. +- Lane B: payment state, webhook, checkout idempotency. +- Lane C: CI/dependencies/hosting/observability. +- Lane D: legal/tax/insurance/privacy decisions and operational ownership. + +Race capacity and merchandise inventory can proceed in parallel only after the shared payment/idempotency model is stable. Refund/dispute/reconciliation should follow webhook state work, not compete with it. + +## 11. Progress tracking + +Canonical assignment, dependencies, claims, and status now belong in GitHub. Use the [live milestone](https://github.com/Run-MPRC/Run-MPRC.github.io/milestone/2) and issue comments/labels. This document and [GITHUB_ISSUES.md](./GITHUB_ISSUES.md) are dated design snapshots and must not be used to duplicate or claim work without checking GitHub first. Status values remain: + +- `proposed` +- `ready` +- `in_progress` +- `implemented_locally` +- `partial_implemented_locally` +- `blocked_owner_decision` +- `in_review` +- `done` + +Do not mark an issue done because code exists on a branch; require the definition-of-done evidence and deployment/configuration step appropriate to the issue. diff --git a/OFFICER_HANDBOOK.md b/OFFICER_HANDBOOK.md new file mode 100644 index 0000000..f25776c --- /dev/null +++ b/OFFICER_HANDBOOK.md @@ -0,0 +1,87 @@ +# MPRC Website Officer Handbook + +**Purpose:** help a backup officer request, approve, verify, or undo a website change without learning to code. + +Start with [OFFICER_START_HERE.md](./OFFICER_START_HERE.md) if you have less than five minutes. + +The private Google Doc entry card titled **MPRC Website — Officer Start Here** is created and readback-verified but currently owner-only. Its private link is not stored in this public repository. Sharing to at least two backup officers remains a required board action. + +## The whole process + +```mermaid +flowchart LR + Ask["Describe the result"] --> Track["One issue and pull request"] + Track --> Preview["Preview and checks"] + Preview --> Approve{"Approve merge and its automatic deployment attempt?"} + Approve -- "No" --> Track + Approve -- "Yes" --> Merged["Merged to main"] + Merged --> Auto["Automatic Pages publication and Firebase attempt"] + Auto --> Verify["Check runmprc.com, Firebase, and providers separately"] + Verify --> Record["Record proof and undo plan"] +``` + +In words: ask and preview first; approving a merge also authorizes today's automatic deployment attempt; then verify each real service separately. + +## One-line request for AI + +> Please update the MPRC website so **[describe the result]** on **[page]** by **[date, if any]**. Read `OFFICER_START_HERE.md` and `AGENTS.md`, use one issue and one small pull request, show a preview and proof, update officer documentation, and do not publish, deploy, use secrets, or change real member/payment data until **[approver]** explicitly approves. + +## Find the right procedure + +| Need | Procedure | Current status | +| --- | --- | --- | +| Any website change | [Request a change](./docs/officers/REQUEST_A_CHANGE.md) | Available | +| Public words, links, photos, or officer list | [Update public content](./docs/officers/UPDATE_PUBLIC_CONTENT.md) | Available through an issue and reviewed pull request | +| Events, products, members, race signup, money, waiver, or privacy | [Events, shop, members, and money](./docs/officers/EVENTS_SHOP_MEMBERS.md) | Source exists; live behavior unverified; live commerce unavailable | +| Merge, publish, and prove the result is live | [Publish and check](./docs/officers/PUBLISH_AND_CHECK.md) | Requires a platform maintainer while hosting is split | +| Outage, wrong page, privacy/security concern, or unexpected payment | [Emergency and recovery](./docs/officers/EMERGENCY_AND_RECOVERY.md) | Available as an escalation and evidence guide | +| Backup access and officer transition | [Access continuity](./docs/officers/ACCESS_CONTINUITY.md) | Private owner action required | +| Pages, data, deployment, and emergency flow | [Simple system maps](./docs/officers/SYSTEM_MAPS.md) | Current as of 2026-07-12 | +| Unfamiliar word | [Plain-language glossary](./docs/officers/GLOSSARY.md) | Available | + +## Current facts that prevent false confidence + +- Use the canonical [`main` branch](https://github.com/Run-MPRC/Run-MPRC.github.io/tree/main). The repository currently opens stale `dev` by default. +- `runmprc.com` is currently served by Netlify. +- GitHub also publishes a separate Pages copy. +- A `main` merge automatically publishes the Pages copy; there is no separate Pages approval today. +- Independent officer publishing to the live Netlify host is **NOT AVAILABLE YET**. +- A green workflow can include “skipping Firebase deploy.” +- Live race registration, merchandise payments, and refunds are not approved. +- There is no proven no-code switch that safely stops every new Stripe payment. + +## Never share + +- Passwords, login codes, recovery codes, private keys, or service secrets. +- Full member lists or private member details. +- Card, bank, payout, refund, health, or emergency-contact information. +- Private edit links, owner links, or screenshots showing private screens. + +Use the club's approved password manager for access. Use public links, redacted screenshots, and made-up test data when asking for help. + +## Approval rules + +| Change | Minimum approval | +| --- | --- | +| Ordinary public wording, link, or approved photo | Content-owning officer | +| Event date, registration window, capacity, member access, or admin duty | Business owner + platform owner | +| Price, discount, tax, order, refund, payout, or Stripe | Treasurer + platform owner | +| Waiver, Terms, Privacy, insurance, or data retention | Club officer + approved legal/privacy owner | +| Domain, Netlify, Firebase, GitHub ownership, secret, or security rule | Service owner + backup/security reviewer | + +## Before anyone says “done” + +Record separate answers: + +1. What source changed? +2. What tests passed? +3. What pull request merged? +4. Was a website copy published? +5. Did Netlify identify the intended commit, or is that still unknown? +6. Was the exact result seen on `runmprc.com`? +7. Did Firebase actually deploy, without a skip message? +8. Was each affected outside provider configured and checked directly? +9. Who approved and checked the result? +10. How can the change be undone safely? + +The expanded handbook and task index is [docs/officers/README.md](./docs/officers/README.md). diff --git a/OFFICER_START_HERE.md b/OFFICER_START_HERE.md new file mode 100644 index 0000000..10e4b03 --- /dev/null +++ b/OFFICER_START_HERE.md @@ -0,0 +1,46 @@ +# MPRC Website: Officer Start Here + +**Use this page if Dave or the usual code owner is unavailable.** You do not need to know how to code. + +**Google Doc entry card:** a private native Google Doc titled **MPRC Website — Officer Start Here** has been connector-readback verified with the safe request and warnings. Its private edit link is intentionally not published here. It is currently owner-only; continuity is incomplete until the board moves it to the board-owned Drive and shares it with at least two backup officers. + +## Copy this one sentence into an AI assistant + +> Please update the MPRC website so **[describe the result in plain words]**. Read `OFFICER_START_HERE.md` and `AGENTS.md`, make the smallest safe change, show me a preview and proof, update the officer guide, and do not publish, deploy, use secrets, or change real member/payment data until an officer explicitly approves. + +Attach the exact wording, public link, or approved photo when you have it. The AI should ask questions if anything is unclear. + +## Choose the closest task + +| What you need | Open this short guide | +| --- | --- | +| Ask for any website change | [Request a change](./docs/officers/REQUEST_A_CHANGE.md) | +| Change public text, a public link, a photo, or the officer list | [Update public content](./docs/officers/UPDATE_PUBLIC_CONTENT.md) | +| Change an event, member access, race signup, shop item, price, refund, waiver, or privacy wording | [Events, shop, members, and money](./docs/officers/EVENTS_SHOP_MEMBERS.md) | +| Approve a change and check whether it is really live | [Publish and check](./docs/officers/PUBLISH_AND_CHECK.md) | +| The site is wrong, down, unsafe, or showing private information | [Emergency and recovery](./docs/officers/EMERGENCY_AND_RECOVERY.md) | +| Prepare backup officers and account access | [Access continuity](./docs/officers/ACCESS_CONTINUITY.md) | +| Understand the pages and services | [Simple system maps](./docs/officers/SYSTEM_MAPS.md) | +| Understand an unfamiliar word | [Plain-language glossary](./docs/officers/GLOSSARY.md) | + +## Never paste these into AI, GitHub, email, or screenshots + +- Passwords, recovery codes, private keys, or login codes. +- Stripe, Firebase, GitHub, Netlify, domain, or email secrets. +- Full member lists or private member details. +- Payment card, bank, payout, refund, emergency-contact, or health information. + +Use the club's approved password manager for access. Share only a public link or a made-up example when asking for help. + +## A change is complete only when each line is answered + +- **Code changed:** What changed? +- **Tests passed:** What was checked? +- **Code merged:** Which pull request was approved? +- **Website live:** Was the exact change seen on [runmprc.com](https://runmprc.com)? +- **Backend live:** If Firebase changed, did the log show a real Firebase deployment rather than “skipping” it? +- **Outside service verified:** If Stripe, Netlify, DNS, Google, or email changed, was that service checked separately? + +As of **2026-07-12**, GitHub Pages builds and the live Netlify site are separate. A green GitHub check does **not** by itself prove that `runmprc.com` or Firebase changed. + +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 new file mode 100644 index 0000000..a634e09 --- /dev/null +++ b/OPERATIONS_RUNBOOK.md @@ -0,0 +1,465 @@ +# MPRC Platform Operations Runbook + +**Status:** Target operating procedure; several controls are not implemented yet +**Last reviewed:** 2026-07-12 +**Audience:** Engineers, race directors, merchandise/fulfillment leads, membership admins, treasurer/finance admins, and incident responders + +This runbook explains how to operate the website, Firebase services, and Stripe integration safely. Unless a procedure is explicitly marked **available now**, it describes the **target** operating model and must not be represented as implemented or deployed. The implementation source of truth is [GITHUB_ISSUES.md](./GITHUB_ISSUES.md); any procedure whose owning issue is not `done` is unavailable for production use. + +Club officers and backup maintainers should start with [OFFICER_START_HERE.md](./OFFICER_START_HERE.md). It converts this technical runbook into short request, approval, verification, access, and emergency steps without terminal commands. + +## 1. Ownership roster + +Complete this table in the private operations system before live payments. Do not put personal phone numbers or recovery codes in this public repository. + +| Area | Primary role | Backup role | Required access | +| --- | --- | --- | --- | +| Incident commander | Platform lead | Club president/designee | Checkout kill switch, status communication | +| Stripe account and payouts | Treasurer | Finance backup | Stripe finance/admin with MFA | +| Refunds and disputes | Finance admin | Treasurer | Stripe refunds/disputes; scoped MPRC finance capability | +| Race operations | Race director | Registrar backup | Event/roster capability; no payout settings | +| Merchandise fulfillment | Shop lead | Fulfillment backup | Catalog/order projection; no member roles | +| Membership and identity | Membership lead | Identity backup | Membership verification; no refunds | +| Firebase/GCP | Platform lead | Platform backup | Scoped project and deployment access | +| GitHub/release | Maintainer | Maintainer backup | Protected environment deployment | +| DNS/domain | Domain owner | Domain backup | Registrar/DNS with MFA and recovery plan | +| Email deliverability | Communications lead | Platform backup | Provider, SPF/DKIM/DMARC, bounce monitoring | +| Privacy/legal response | Club officer/counsel | Named backup | Approved policy and notification channel | + +Review access quarterly, when an officer changes, and after every incident. Remove departed users immediately. + +## 2. Emergency controls + +Before launch, implement and test a checkout kill switch that does not require a code deployment. Recommended controls: + +- `commerceEnabled=false` globally in server-controlled configuration. +- Per-event `checkoutEnabled=false` and per-product/variant non-sellable status. +- Server functions check the switch before reserving capacity/inventory or calling Stripe. +- The public site displays a neutral maintenance message and support path. +- Existing paid records and webhook/reconciliation processing continue while new Sessions are disabled. + +Changing only the UI is not a kill switch. Existing Checkout Sessions may remain payable; incident procedures must also expire active Sessions when required. + +## 3. Environment inventory + +Maintain an internal inventory with project/account IDs and console links: + +| Environment | Firebase project | Stripe mode/account | Web domain | GitHub environment | Status | +| --- | --- | --- | --- | --- | --- | +| Local | Emulator project ID | Test + Stripe CLI | `localhost:3000` | None | **NOT AVAILABLE YET** — blocked on #99 fail-closed emulator isolation | +| CI | Demo/emulator project | Fixtures/test only | None | CI | Partially implemented | +| Staging | **Create dedicated project** | Test/sandbox + unique endpoint | `dev.runmprc.com` | `staging` | Launch blocker | +| Production | `mid-peninsula-running-club` (verify) | Live + unique endpoint | `runmprc.com` | `production` | Informational site only until gates close | + +The repository currently includes only one `.firebaserc` project mapping. Do not infer staging isolation from the historical documentation; create and verify it. + +## 4. Configuration and secrets + +### Browser/build-time values + +Commit a `.env.example` containing names and safe descriptions, never real secret values. Values prefixed `REACT_APP_` are embedded in the public bundle and must not be secrets. + +Expected public/environment-specific names include: + +```text +REACT_APP_RECAPTCHA_SITE_KEY +REACT_APP_SENTRY_DSN +REACT_APP_SENTRY_ENV +REACT_APP_STRAVA_CLIENT_ID +``` + +The Firebase web configuration is public application configuration. It should still be environment-specific and paired with Auth restrictions, Firestore rules, and App Check. + +### Server secrets + +Store server secrets in Google Secret Manager and bind only to functions that need them: + +```text +STRIPE_SECRET_KEY +STRIPE_WEBHOOK_SECRET +STRAVA_CLIENT_ID +STRAVA_CLIENT_SECRET +RATE_LIMIT_HMAC_KEY # target +``` + +Validated non-secret server parameters: + +```text +SITE_ORIGIN +STRIPE_LIVEMODE_EXPECTED +COMMERCE_ENABLED +ENVIRONMENT_NAME +``` + +`functions.config()` is deprecated and scheduled for decommissioning in March 2027. The legacy member-sync API key must be retired or migrated as its own security issue, not copied into a new generic secret without redesign. + +### Secret rotation record + +For each secret, record in a private inventory: + +- Owner and backup. +- Environments and exact function consumers. +- Creation/last-rotation date. +- Rotation and rollback procedure. +- How to test the new version. +- Emergency revoke procedure. +- Evidence that the old version was disabled after overlap. + +Never print secret values into CI output, issue bodies, screenshots, chat, or this runbook. + +## 5. Local development + +### Prerequisites + +- Node.js 20 matching Functions runtime. +- npm matching the lockfiles. +- Java 17 for the Firestore emulator/rules tests. +- Firebase CLI from the repository dependency where possible. +- Stripe CLI for signed local webhook testing. + +### Install — available now + +```bash +npm ci --legacy-peer-deps +npm --prefix functions ci +``` + +The legacy peer-dependency flag is currently needed by the frontend dependency tree. Removing that requirement belongs to the build-system/dependency issue. + +### Local secret override — available now + +Use Firebase's ignored `functions/.secret.local` behavior for emulator-only fake/test values. Confirm the file is ignored before adding it. Use Stripe test keys only. Do not download or reuse production service-account JSON. + +Example names—not values: + +```text +STRIPE_SECRET_KEY=sk_test_... +STRIPE_WEBHOOK_SECRET= +STRAVA_CLIENT_ID=test-or-development-id +STRAVA_CLIENT_SECRET=development-secret +``` + +### Start emulators and app — NOT AVAILABLE YET + +Issue [#99](https://github.com/Run-MPRC/Run-MPRC.github.io/issues/99) owns the demo-project script, Auth/Firestore/Functions emulator wiring, local monitoring isolation, and fail-closed startup. Those changes exist only in queued working-tree work and are not part of the current `main` baseline. + +Until #99 merges, do not start the app for Firebase-backed account, admin, member, event, shop, payment, or private-data testing. The current client can inherit production Firebase configuration. Historical commands are not a safe workaround. + +After #99 merges, replace this warning with the exact merged commands and proof. The finished procedure must confirm the demo project and all three emulator connections before test data is created. + +### Forward Stripe events + +Use the Stripe CLI logged into the intended test workspace: + +```bash +stripe listen --forward-to http://127.0.0.1:5001/demo-mprc-local/us-central1/stripeWebhook +``` + +Copy the CLI's temporary webhook signing secret only into the ignored local secret override. Do not use the Dashboard production endpoint secret locally. + +Trigger or complete test-mode Checkout through the application when testing end-to-end. Synthetic webhook triggers are useful for individual handlers but do not replace a real test Checkout flow with metadata and totals. + +### Local safety checks + +- Browser/network calls target localhost for Auth, Firestore, and Functions. +- Stripe objects show test mode. +- No production customer email, address, DOB, or emergency-contact data is copied into local Firestore. +- Email extension is disabled or points to a safe sink/test mailbox. +- Sentry and analytics use development environments or are disabled. + +## 6. Verification commands + +### Checks available on current `main` + +```bash +npm --prefix functions run lint +npm --prefix functions run test:run -- --runInBand +``` + +The SPA navigation command is **NOT AVAILABLE YET** and belongs to #99. A dependable frontend Jest baseline/quality gate is **NOT AVAILABLE YET** and belongs to [#103](https://github.com/Run-MPRC/Run-MPRC.github.io/issues/103). + +### Firestore Rules + +```bash +npm run test:rules +``` + +This requires Java. Rules tests must prove both allowed behavior and explicit denial of secret/financial/arbitrary paths for browser admins. + +### Production build without changing generated sitemap during a diagnostic check + +```bash +CI=true DISABLE_ESLINT_PLUGIN=true npx --no-install react-scripts build +``` + +The normal `npm run build` executes `prebuild` and may update `public/sitemap.xml`. Use the normal command only when generated sitemap changes are intended and reviewed. + +### Dependency review + +```bash +npm audit --omit=dev +npm --prefix functions audit --omit=dev +``` + +Do not run `npm audit fix --force` blindly. Review reachability, upgrade direct dependencies deliberately, inspect lockfile changes, and rerun all checks. + +### Target payment integration suite + +CI/staging must eventually include: + +- Duplicate and out-of-order signed webhook fixtures. +- Immediate and asynchronous payment outcomes. +- Amount/currency/environment mismatch. +- Concurrent final-seat and final-SKU attempts. +- Session expiry and cancellation. +- Full/multiple partial refunds and disputes. +- Reconciliation after a deliberately missed webhook. + +## 7. Change and release process + +### Before merge + +1. Link one issue from [GITHUB_ISSUES.md](./GITHUB_ISSUES.md) or the corresponding GitHub issue. +2. Record data/schema/API changes and backward-compatibility behavior. +3. Add negative/security tests, not only the happy path. +4. Verify no secret, production identifier, PII fixture, generated build, emulator export, or debug log was added. +5. Obtain review from an owner appropriate to the risk: platform/security, finance/payment, race operations, or privacy. +6. Require CI to pass without ignored failures. + +### Deployment order + +For expand-and-contract changes: + +1. Backup/export or snapshot the affected operational data according to the approved method. +2. Deploy additive indexes/rules/backend functions compatible with the current client. +3. Run migrations/backfills in dry-run mode; review counts and anomalies. +4. Execute the idempotent migration and record its report. +5. Verify backend health in staging. +6. Deploy the dependent frontend. +7. Run smoke and reconciliation checks. +8. Observe for the defined period before removing legacy fields/endpoints. + +The current workflow deploys the frontend before Functions and can silently skip Firebase deployment when credentials are absent. Treat it as non-compliant until CI-001 is complete. + +### Production approvals + +Production commerce changes require: + +- Passing required CI/security checks. +- Protected GitHub production environment approval. +- Named operator for post-deploy observation. +- Rollback/roll-forward plan. +- No unrelated schema migration during a race-registration opening. +- Finance approval when totals, discounts, tax, refunds, or reconciliation change. + +## 8. Stripe event-destination setup + +In Stripe Workbench/Dashboard, verify the production endpoint: + +- URL points to the intended production Firebase function. +- Mode/account matches production. +- TLS is valid. +- Only required event types are selected. +- Signing secret version matches Secret Manager. +- Recent deliveries return `2xx` only after durable acceptance. +- Alerts/owners watch failed deliveries. + +Required initial event set is documented in [STRIPE_COMMERCE_DESIGN.md](./STRIPE_COMMERCE_DESIGN.md). When the handler changes, update its tests and the Dashboard allowlist together. + +For secret rotation, support the provider-approved overlap or controlled switchover, send a signed test event, verify the new secret, then retire the old secret and record evidence. + +## 9. Staging dress rehearsal + +Before every major payment release: + +1. Create a capped test race and a product with one unit in a staging Firebase project. +2. Confirm non-member and verified-member pricing from separate accounts. +3. Race two final-seat and final-unit checkouts; confirm only one reservation succeeds. +4. Complete a card payment and, if supported, a delayed payment fixture. +5. Replay the same webhook and deliver relevant events out of order. +6. Cancel an unpaid Session and prove Stripe can no longer accept payment. +7. Allow another Session to expire and prove the hold is released once. +8. Perform partial and final refunds using a retry/timeout scenario. +9. Trigger a dispute fixture and verify finance alert/state. +10. Temporarily interrupt webhook processing, restore it, and run reconciliation. +11. Verify confirmation email is emitted once and hostile name/content is escaped. +12. Review Firestore records, counters, event inbox, audit entries, Stripe totals, and redacted logs. + +Capture the commit, test IDs, timestamps, screenshots without secrets/PII, and the final reconciliation report. + +## 10. Go-live procedure + +### T-7 days or earlier + +- Complete the launch checklist in `STRIPE_COMMERCE_DESIGN.md`. +- Verify account owners/MFA, support coverage, legal text, tax/shipping/refund/waiver decisions, inventory, capacity, and email deliverability. +- Verify backups and restoration drill. +- Confirm dependency/security scan and branch/environment protection. +- Announce the maintenance/incident contact internally. + +### T-24 hours + +- Freeze unrelated production changes. +- Verify event/product configuration and expected prices with two reviewers. +- Verify Stripe event destination and no failed backlog. +- Run reconciliation and confirm zero unexplained mismatches. +- Review cloud/Stripe budgets and alerts. +- Confirm kill-switch access and operator coverage. + +### Pilot + +- Start with one low-value product or small capped event. +- Perform one controlled live transaction using an authorized real payment method. +- Verify Stripe amount/currency/receipt, Firestore paid state, counters, confirmation, and payout reporting. +- Refund if this was an approved test purchase and verify both systems. +- Do not broaden until finance and platform owners sign off on reconciliation. + +### Opening registration/sales + +- Enable server-side commerce switch. +- Monitor checkout errors, App Check/rate-limit rejects, webhook latency/failures, reservations, paid conversions, counter drift, email failures, Stripe Radar reviews, refunds, and disputes. +- Run reconciliation frequently during the first launch window. + +## 11. Routine operations + +### Daily while commerce is active + +- Review failed/quarantined Stripe events and reconciliation mismatches. +- Review stale reservations and negative/near-zero capacity/inventory. +- Review Stripe disputes, Radar reviews, refunds, and payout anomalies. +- Review transactional email failures/bounces. +- Confirm no production deployment failed or silently skipped. + +### Weekly + +- Reconcile Stripe gross/discount/tax/shipping/refunds/fees to local order/registration reports and finance records. +- Review privileged actions and exports. +- Review cloud costs, rate-limit spikes, App Check invalid traffic, and Sentry redaction. +- Patch time-sensitive high-risk dependencies through reviewed PRs. + +### Monthly/quarterly + +- Test checkout kill switch and a recovery path in staging. +- Review access and remove unnecessary roles. +- Review secret age/rotation schedule. +- Test backup restoration on non-production infrastructure. +- Review retention/deletion job reports. +- Revisit fraud, refund, chargeback, and support trends. + +## 12. Refund procedure + +1. Authenticate with finance capability and satisfy recent-auth/MFA policy. +2. Locate the local business record and corresponding Stripe PaymentIntent/Charge. +3. Confirm identity using the approved support procedure without asking for card data. +4. Review policy, prior refunds, actual captured/refunded totals, fulfillment/attendance state, and dispute state. +5. Enter amount in integer cents, reason, and internal note. +6. Submit once. If the response is lost, retry the same refund command ID; never create a new request blindly. +7. Treat the record as refund-pending until Stripe confirms. +8. Verify webhook/reconciliation updates cumulative refunded total and customer communication. +9. Release race capacity or process returned inventory only under the explicit policy. + +Never edit Firestore totals/status manually to simulate a refund. + +## 13. Reconciliation procedure + +The target reconciliation job produces categories: + +- Stripe paid / no local record. +- Local paid / Stripe not paid. +- Amount or currency mismatch. +- Duplicate local records for one Stripe object. +- Refund/dispute mismatch. +- Stale checkout/reservation. +- Capacity or inventory counter drift. +- Failed/quarantined event inbox item. + +For an anomaly: + +1. Disable only the affected sellable flow if ongoing harm is possible. +2. Preserve record versions, Stripe Event IDs, logs, and timestamps. +3. Retrieve canonical Stripe objects using server tooling. +4. Classify deterministic repair versus finance/security review. +5. Run an idempotent repair command; do not hand-edit multiple fields. +6. Re-run reconciliation and attach the before/after report to the private incident/operations record. + +## 14. Incident playbooks + +### Suspected Stripe key exposure + +1. Disable new checkout/refund commands. +2. Restrict or roll the key in Stripe; create a replacement through the approved rotation path. +3. Update Secret Manager binding and deploy only affected functions. +4. Review Stripe logs for unexpected API calls, refunds, customers, Sessions, Products/Prices, webhook changes, payout/account changes, and restricted-key use. +5. Reconcile all activity in the exposure window. +6. Determine notification obligations and complete post-incident review. + +### Webhook signing-secret exposure + +1. Disable or roll the event destination secret. +2. Ensure the endpoint continues to fail closed for invalid signatures. +3. Review invalid/valid event logs and event inbox for anomalies. +4. Retrieve canonical Stripe objects before repairing any suspicious local state. + +### Paid customer not confirmed + +1. Ask for email/order reference—not card details. +2. Locate the Stripe Session/PaymentIntent and local record. +3. Inspect event delivery and event inbox. +4. Run reconciliation/repair using canonical Stripe state. +5. Send confirmation once through the idempotent outbox and document support resolution. + +### Local paid but Stripe unpaid/mismatched + +1. Disable fulfillment/access for the record without deleting evidence. +2. Quarantine it and alert finance/platform owners. +3. Confirm the source event/signature, environment, amount, currency, and IDs. +4. Treat as a possible integrity incident; expand review to adjacent records/events. + +### Capacity or inventory oversell + +1. Disable new checkout for the event/variant. +2. Reconcile successful payment time, reservation time, and policy priority. +3. Contact affected customers using approved language; issue idempotent refunds if required. +4. Repair counters only with an audited tool and add a regression test. + +### Compromised admin account + +1. Disable/revoke the account and refresh tokens; remove privileged claims. +2. Review role grants, financial actions, exports, catalog/events, mail, secrets access, GitHub/cloud activity, and Stripe actions. +3. Rotate exposed secrets and notify affected users/providers as required. +4. Restore through audited repair; require MFA/recovery remediation before re-enabling. + +## 15. Backup, restoration, and retention + +Before live commerce: + +- Enable an approved Firestore backup/export schedule with restricted storage/IAM. +- Document whether Firebase Auth, Secret Manager versions, Stripe configuration, email templates, and DNS require separate backup/export. +- Encrypt and retention-limit backups; monitor access. +- Test restoring into an isolated non-production project. +- Validate restored referential integrity and run reconciliation in test mode. +- Ensure deletion/retention policy eventually reaches backups according to legal and platform constraints. + +A backup that has never been restored is not verified. + +## 16. Rollback and repair + +For database/payment changes, roll-forward is often safer than reverting code because external Stripe events continue and records may use the new schema. Every release identifies: + +- Last compatible frontend/backend versions. +- Additive schema and backfill version. +- Whether old code can read new state. +- How to disable new writes while webhook/reconciliation continue. +- Idempotent repair path for partially completed sagas. + +Never use destructive Git or Firestore reset operations on production as a rollback. + +## 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`**. + +Each isolated issue must publish its own exact base commit, commands, counts, and results. A documentation-only #104 branch starts from `main` with the merged #98 role-grant behavior; it must not claim the queued emulator, webhook, Rules, or frontend-test outcomes. + +On the isolated #104 branch based at `ce22c110`, Functions lint and 17/17 Functions tests pass, 97/97 Firestore Rules tests pass with Java 17, and the diagnostic production build passes. Frontend Jest runs 15 Login tests but the App suite fails on the known missing `TextEncoder`; #103 owns that baseline repair. These are branch checks, not deployment evidence. + +Provider configuration, production secrets, Netlify publication, Firebase deployment, and live behavior always require separate dated evidence. Local tests and a green GitHub summary do not prove them. diff --git a/README.md b/README.md index 08be823..cc0bcd5 100644 --- a/README.md +++ b/README.md @@ -1,37 +1,82 @@ -# run-mprc.github.io -Live sites are [runmprc.com](runmprc.com) and [dev.runmprc.com](dev.runmprc.com) for changes made in dev branch. +# Mid-Peninsula Running Club Platform -### Useful subdirectories +The MPRC platform is a React single-page application backed by Firebase Authentication, Cloud Firestore, and Firebase Cloud Functions. It publishes club content and events and contains an in-progress platform for member accounts, race registration, Stripe-hosted payments, merchandise orders, administration, and Strava connections. -- src/ - - images/ (Most of the images are resized to fit on the webpage and stored here.) - - text/ (Most of the basic template text is here. If you can't find certain subtext here, it's likely under `pages/`) - - pages/ (Most of the formatting CSS documents and aggregation .jsx files for each file. JSX files will centralize imported resources like images and templatized text.) +- Production informational site: [runmprc.com](https://runmprc.com) +- Repository: [Run-MPRC/Run-MPRC.github.io](https://github.com/Run-MPRC/Run-MPRC.github.io) -## Available Scripts +> **Commerce status:** the repository contains a substantial Stripe/race/shop prototype, but live payments are not production-ready. Do not configure or enable live Stripe keys until the P0 gates in [STRIPE_COMMERCE_DESIGN.md](./STRIPE_COMMERCE_DESIGN.md) and [SECURITY.md](./SECURITY.md) are closed and a controlled pilot is approved. -In the project directory, you can run: +## Choose your starting point -### `npm start` +| You are… | Start here | +| --- | --- | +| A club officer or backup maintainer | [OFFICER_START_HERE.md](./OFFICER_START_HERE.md) — no coding required | +| An AI agent working a claimed issue | [AGENTS.md](./AGENTS.md) — safety, ownership, documentation, and proof rules | +| A developer | Continue with the technical documents below | -Runs the app in the development mode.\ -Open [http://localhost:3000](http://localhost:3000) to view it in your browser. +## Technical documents -The page will reload when you make changes.\ -You may also see any lint errors in the console. +| Document | Purpose | +| --- | --- | +| [SYSTEM_DESIGN.md](./SYSTEM_DESIGN.md) | Current and target architecture, boundaries, data model, invariants, workflows, and decisions | +| [STRIPE_COMMERCE_DESIGN.md](./STRIPE_COMMERCE_DESIGN.md) | Stripe configuration, checkout saga, webhooks, capacity, inventory, refunds, and launch checklist | +| [SECURITY.md](./SECURITY.md) | Dated risk register, threat model, required controls, privacy, secure delivery, and incident response | +| [IMPLEMENTATION_PLAN.md](./IMPLEMENTATION_PLAN.md) | Dependency graph, phases, gates, first implementation tranche, and definition of done | +| [GITHUB_ISSUES.md](./GITHUB_ISSUES.md) | Ordered system-level trackers and directly assignable small/medium issues | +| [GITHUB_ISSUE_SLICES.md](./GITHUB_ISSUE_SLICES.md) | Atomic one-agent child tickets for every large tracker, with dependencies and required proof | +| [OPERATIONS_RUNBOOK.md](./OPERATIONS_RUNBOOK.md) | Environments, secrets, testing, deployment, launch, reconciliation, refunds, incidents, and restore | +| [AGENTS.md](./AGENTS.md) | Repository-specific safety and execution instructions for coding agents | +| [OFFICER_START_HERE.md](./OFFICER_START_HERE.md) | Plain-language change, approval, live-verification, access, and emergency guides | +| [OFFICER_HANDBOOK.md](./OFFICER_HANDBOOK.md) | Concise officer decision guide and index to the short step-by-step procedures | -### `npm run build` +Historical developer/content/LLM guides remain under [`docs/`](./docs/README.md). They predate portions of the commerce implementation; use the root documents above for target architecture and launch decisions. -Builds the app for production to the `build` folder.\ -It correctly bundles React in production mode and optimizes the build for the best performance. +## System at a glance -The build is minified and the filenames include the hashes.\ -Your app is ready to be deployed! +- `src/`: React 18 UI, routes, services, Firebase client, account/admin/event/shop experiences. +- `functions/`: Node.js 20 Firebase Functions for identity, checkout, Stripe webhooks, admin commands, exports, email, rate limiting, and Strava. +- `firestore.rules` and `firestore.indexes.json`: browser data boundary and query indexes. +- `tests/firestore-rules/`: emulator-based allow/deny coverage. +- `.github/workflows/`: frontend, Functions, Rules CI and deployment automation. +- `public/404.html`: current GitHub Pages SPA fallback. The safer shared callback helper and its tests are **NOT AVAILABLE YET** and belong to #99. -See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information. +**Deployment reality checked 2026-07-12:** the repository publishes a GitHub Pages copy, while `runmprc.com` is currently served by a separate Netlify deployment. The Firebase workflow can remain green while skipping backend deployment when its service-account secret is absent. Treat website, Firebase, and provider deployment as separate states until CI-001 and hosting consolidation are complete. -### `npm run deploy` +## Local setup status -Run this under the `main` branch to kick-off the `pages build and deployment` Action to deploy to https://run-mprc.github.io/ -- changes will update in `gh-pages` branch -- more info: https://github.com/Run-MPRC/Run-MPRC.github.io/settings/pages +**Firebase-backed local development is NOT AVAILABLE YET on `main`.** Issue [#99](https://github.com/Run-MPRC/Run-MPRC.github.io/issues/99) owns the missing demo-project emulator script, Auth/Firestore/Functions wiring, and fail-closed startup. Until #99 merges, `npm start` can use production Firebase configuration; do not use it for account, admin, member, event, shop, payment, or private-data testing. + +Node.js 20 lockfile installation remains the baseline for maintainers preparing isolated, non-Firebase checks: + +```bash +npm ci --legacy-peer-deps +npm --prefix functions ci +``` + +After #99 merges, the runbook and this section must be updated from its merged tests rather than from the pre-merge working tree. + +## Verification + +```bash +npm --prefix functions run lint +npm --prefix functions run test:run -- --runInBand +npm run test:rules +CI=true DISABLE_ESLINT_PLUGIN=true npx --no-install react-scripts build +``` + +Rules tests require Java 17. The direct `react-scripts build` command is useful for a diagnostic compile because the normal `npm run build` runs the sitemap generator and may intentionally update `public/sitemap.xml`. The SPA navigation command belongs to queued #99, and a trustworthy frontend Jest baseline belongs to queued [#103](https://github.com/Run-MPRC/Run-MPRC.github.io/issues/103); do not claim either from this documentation pull request. + +## Working on the platform + +1. Select one ready `S`/`M` issue from [GITHUB_ISSUES.md](./GITHUB_ISSUES.md), or one atomic child from [GITHUB_ISSUE_SLICES.md](./GITHUB_ISSUE_SLICES.md), and confirm its dependencies. +2. Follow [AGENTS.md](./AGENTS.md), including no production credentials/data and preserving unrelated worktree changes. +3. Add positive, negative, retry, and authorization/concurrency tests appropriate to the risk. +4. Use additive/idempotent migrations and backend-first expand-and-contract deployment. +5. Update the affected design/runbook and record residual risk. + +Business owners—not coding agents—must approve legal text, waiver/insurance policy, tax, shipping/returns, retention, live credentials, account ownership, DNS, and the live-mode pilot. + +## Deployment warning + +Pushing to `main` currently triggers GitHub Pages deployment and may deploy Firebase resources when configured. The existing workflow is itself scheduled for hardening in CI-001; do not assume a merge is a safe commerce release. Production changes require protected approval, passing checks, compatible backend-first rollout, a named observer, and the runbook's post-deploy/reconciliation steps. diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 0000000..6238d56 --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,292 @@ +# MPRC Security, Privacy, and Risk Plan + +**Status:** Pre-production security assessment +**Assessment date:** 2026-07-12 +**Scope:** This repository's React application, Firebase configuration/functions/rules, Stripe integration, Strava integration, CI/CD, operational data, and documented deployment model + +This is both the repository security policy and the current engineering risk register. It is not a penetration-test report, legal opinion, PCI attestation, privacy certification, or guarantee that deployed cloud settings match the repository. External Firebase, GCP, Stripe, GitHub, DNS, Sentry, and email-provider configuration must be reviewed separately. + +## 1. Production readiness decision + +The public informational website can continue operating with normal care. **Live race or merchandise payments should remain disabled until every P0 launch blocker below is closed and verified in a staging dress rehearsal.** The existing payment implementation is a useful prototype, but several correctness defects could mark unpaid transactions paid, lose payment callbacks, oversell race capacity, fail to reconcile late-payment links, or allow overly broad administrative access. + +## 2. Reporting a vulnerability + +Do not disclose a suspected vulnerability, secret, customer record, payment reference, or exploit in a public GitHub issue. Until MPRC publishes a dedicated security address: + +1. Use the private contact channel listed on the live MPRC contact page. +2. State that the message is a security report and request a secure reply channel. +3. Include the affected URL/component, impact, reproduction with synthetic data, and any suggested mitigation. +4. Do not access, change, download, or retain data beyond what is necessary to demonstrate the issue. + +An implementation issue must establish `security@runmprc.com` or another monitored private address, named primary/backup responders, an acknowledgement target, and a disclosure policy before commerce launch. + +## 3. Severity and launch gates + +- **P0 — launch blocker:** credible risk of incorrect money state, privilege compromise, secret/PII exposure, oversell, or inability to recover safely. No live payments. +- **P1 — high:** material abuse, privacy, availability, or operational-control weakness. Complete before broad launch or explicitly accept with a time-bounded compensating control. +- **P2 — medium:** defense-in-depth, maintainability, or bounded operational risk. Schedule soon after the secure pilot. +- **P3 — improvement:** useful hardening with lower immediate impact. + +Closing an item requires code/configuration, automated or documented tests, deployed-environment evidence, monitoring, and an owner—not only a code diff. + +## 4. Assessment-baseline risk register + +The findings below describe the repository at the start of the 2026-07-12 assessment. The IDs use the `RISK-*` namespace so they cannot be confused with the independently publishable issue IDs in `GITHUB_ISSUES.md`. A working-tree fix lowers exposure only after review, merge, deployment, external configuration, and evidence; the remediation ledger after the tables records that distinction. + +### P0 launch blockers + +| ID | Finding and evidence | Impact | Required treatment | +| --- | --- | --- | --- | +| RISK-001 | `stripeWebhook.js` marks every `checkout.session.completed` record paid without validating `payment_status`, `amount_total`, currency, environment, metadata schema, or allowed prior state. | Delayed or anomalous payments can be fulfilled as paid; local totals can disagree with Stripe. | Implement verified payment reduction, async success/failure handling, quarantine, and reconciliation. | +| RISK-002 | Stripe Event IDs are not durably deduplicated; object transitions and emails are not comprehensively idempotent. Existing webhook tests cover only invalid signatures/method. | Duplicate/retried/out-of-order events can repeat side effects or corrupt audit/state. | Add a durable event inbox, atomic transitions, and duplicate/out-of-order tests. | +| RISK-003 | Paid Checkout Sessions are created before their registration/order record is persisted. The webhook only queries for the stored Session ID. | A fast webhook or function crash can leave a paid Stripe transaction with no local confirmation. | Use a persistence-first, idempotent checkout saga and metadata-direct lookup. | +| RISK-004 | Race capacity uses a count-then-create sequence outside a transaction; `registeredCount` is displayed but not maintained. | Concurrent users can exceed capacity; stale pending Sessions can block capacity for up to seven days. | Introduce transactional capacity counters/reservations, matching expiry, backfill, and concurrency tests. | +| RISK-005 | Merchandise has product status but no SKU/variant inventory reservation. | The club can sell unavailable size/color combinations or oversell stock. | Add canonical variants and atomic inventory holds before live merchandise. | +| RISK-006 | Admin catch-all Firestore rule grants any `admin` browser read/write access to every document. It overrides the explicit deny on `members/*/secrets`, allows direct financial-state/audit mutation, and tests intentionally codify this. | A compromised admin session can read all Strava refresh tokens, rewrite orders/registrations, or erase audit evidence. | Remove the global catch-all; create explicit capability/resource rules; make secrets and financial state server-only even for browser admins. | +| RISK-007 | Member promotion by email does not require the Firebase account's email to be verified. The legacy static-key bulk endpoint can grant `member` to a pre-registered known address. | An attacker who registers another member's email first can obtain membership access/discount after sync. | Require verified email and authoritative membership match; retire or strongly redesign the static-key endpoint. | +| RISK-008 | `requireAppCheck` is controlled by optional `ENFORCE_APP_CHECK`; repository/deployment config does not prove it is true. Missing site key also disables client App Check. | Public callable functions may be scripted directly, enabling abuse and cloud/Stripe cost amplification. | Use Firebase runtime `enforceAppCheck: true` for sensitive callables, reCAPTCHA Enterprise, staged metrics, and no environment fail-open. | +| RISK-009 | GitHub Pages `404.html` stores only `pathname`; `index.html` restores only that value. Stripe and Strava deep-link callbacks lose query/hash data. | Paid success pages can lose Session/order/token references; OAuth loses `code`/`state`; users see failure despite successful external action. | Preserve same-origin path, search, and hash safely; add callback routing tests; prefer owned hosting rewrites. | +| RISK-010 | Local Firebase setup connects Auth and Firestore emulators but not the Functions emulator; every `getFunctions(app)` can target the deployed project. | Developers can accidentally invoke production checkout/admin/Strava functions while believing they are local. | Connect Functions emulator unconditionally in development and add environment safety tests. | +| RISK-011 | Local cancellation only changes Firestore; it does not expire the active Stripe Session. The webhook can later move a cancelled record back to paid. | Customers can pay a supposedly cancelled registration/order; capacity/inventory and support state diverge. | Add explicit cancellation saga with Stripe Session expiry and allowed-state transitions. | +| RISK-012 | Late registration creates reusable Stripe Payment Links, but records have no Session ID and the webhook searches only by Session/PaymentIntent. | Late payments do not reconcile; reuse can produce multiple charges/Sessions for one logical registration. | Replace with one-off idempotent Checkout Sessions or fully constrain/map Payment Links. | +| RISK-013 | `allow_promotion_codes: true` permits Stripe discounts while Firestore stores base price and webhook does not validate/store discount/actual total. | Reporting, refund limits, and local payment confirmation can be wrong. | Disable promotions until a server-approved discount and total-reconciliation model exists. | +| RISK-014 | Production dependency audit reports 33 root advisories (3 critical, 9 high) and 9 Functions advisories (1 high). Direct affected dependencies include the router, Firebase client, and Firebase Admin; unused `gpxparser` pulls obsolete `request/jsdom/form-data`. | Known vulnerable or unmaintained runtime/build chains increase XSS, request, DoS, and supply-chain exposure. | Remove unused dependency chains, patch direct dependencies, then stage SDK/build-system upgrades with tests and continuous scanning. | +| RISK-015 | `Privacy.jsx` and `Terms.jsx` explicitly say `REPLACE WITH DATE` and `Placeholder template`; tax, refund, shipping, waiver, retention, and customer-support policies are not approved. | Users are asked for sensitive data and money without final disclosures/policies; disputes and regulatory obligations are unmanaged. | Obtain appropriate legal/tax/insurance review, approve versioned policies, and block live-mode configuration until complete. | + +### P1 high-priority risks + +| ID | Finding | Required treatment | +| --- | --- | --- | +| RISK-016 | Success/lookup bearer tokens are stored plaintext in Firestore and placed in query strings, which can enter history, screenshots, logs, analytics, and referrers. | Prefer authenticated ownership or server-verified Session IDs; hash anonymous tokens, expire them, use fragment/session state, and scrub history. | +| RISK-017 | Input validation checks only a few required fields. Names, phones, dates, notes, tracking values, custom field maps, arrays, URLs, and payload sizes are weakly bounded or unbounded. | Add shared strict schemas, allowlists, normalization, size limits, URL policy, and hostile-input tests before any write or external call. | +| RISK-018 | Event-defined required custom fields and allowed options are enforced by HTML, not by the checkout function. | A scripted client can omit required answers or inject arbitrary data into protected records/exports. | Validate against the server event schema and reject unknown/missing/invalid fields. | +| RISK-019 | Confirmation email HTML directly interpolates runner and event values; email outbox creation and sent-marker update are separate writes. | HTML injection/phishing-like content and duplicate messages on trigger retries. | Escape HTML, create deterministic outbox IDs in a transaction, and test hostile input/retries. | +| RISK-020 | Refund endpoints lack stable Stripe idempotency keys, comprehensive remaining-balance validation, and a pending/canonical webhook model. | A timeout/retry or repeated click can create ambiguous or multiple refund attempts. | Create local refund operations with stable keys and let verified Stripe events finalize totals. | +| RISK-021 | Admin state transitions accept impossible combinations: cancel paid without refund decision, fulfill unpaid/cancelled orders, substitute without new waiver, or comp without proven waiver. | Financial, legal, fulfillment, and waiver records can be inconsistent. | Centralize allowed state transitions and authorization; require reason/evidence and test every forbidden edge. | +| RISK-022 | Rate-limit documents contain raw IP/email keys and values; fixed windows are distributed only by those identifiers and cleanup depends on an externally configured TTL. | Additional PII storage, unbounded growth if TTL is absent, email-targeted denial of service, and easy distributed abuse. | HMAC identifiers with a rotating secret, verify TTL, add per-user/business budgets, and monitor rejects/cost. | +| 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 are stored plaintext and browser admins can read them under current rules; refresh writes have no concurrency control. | Token theft exposes member activity data; concurrent refresh can lose a rotated refresh token. | Make secrets server/IAM-only, encrypt where risk analysis requires, transact refresh versions, minimize scopes, and audit access. | +| 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-026 | Firestore Admin SDK bypasses rules and the Functions service identity's IAM scope is not documented. GitHub deploy uses a broad service-account secret when present. | Server or CI compromise may expose the entire project. | Inventory IAM, create least-privilege service/deployer identities, use workload identity/OIDC where practical, and remove long-lived keys. | +| 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. | +| RISK-029 | Hosting on GitHub Pages limits controlled response headers and relies on custom JavaScript SPA fallback. | Weaker CSP/clickjacking/referrer controls and fragile OAuth/payment routes. | Migrate to Firebase Hosting or another owned edge with SPA rewrites, CSP, HSTS, frame protection, referrer and permissions policy. | + +### P2 defense-in-depth and operational risks + +| ID | Finding | Required treatment | +| --- | --- | --- | +| RISK-030 | Stripe API version is pinned to `2023-10-16` with an old Stripe SDK and no documented upgrade cadence. | Schedule test-mode API/SDK upgrades, inspect breaking changes, and pin intentionally. | +| RISK-031 | Lazy Stripe Product creation is concurrency-prone and reachable from anonymous checkout traffic. | Move catalog synchronization to authenticated/idempotent management functions. | +| 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 | Frontend tests currently fail because `TextEncoder` is absent in Jest; CI's frontend lint step is allowed to fail with `|| true`; Functions have only seven unit tests and about 20% line coverage. | Repair the harness, make non-mutating lint/test gates mandatory, and add domain/integration coverage. | +| RISK-036 | Production and staging deployment topology is inconsistent in docs/workflows; backend deploy follows frontend and can silently skip when a secret is absent. | Define environment-protected deployments, deploy compatible backend first, fail required production stages closed, and add rollback evidence. | +| 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. | + +### Working-tree remediation ledger + +These entries are implementation evidence, not a production risk-acceptance decision: + +| Risks | Working-tree result on 2026-07-12 | Remaining closure evidence | +| --- | --- | --- | +| RISK-001, RISK-002, RISK-034 | PAY-003 safety slice adds durable Stripe Event deduplication, transactional transition/processed marking, paid/amount/currency/Checkout-mode/reference-shape and explicitly configured livemode checks, zero-discount enforcement, async outcomes, monotonic refund/terminal guards, quarantine, generic signature failures, and a webhook-secret-only binding. | Complete CONFIG/PAY-001/PAY-002 contracts, metadata schema allowlist/migration, canonical reducers/reservations/outbox, retry/dead-letter/TTL/alerts, emulator integration, Stripe test-mode rehearsal, deploy, and reconcile. | +| RISK-006, part of RISK-024/RISK-032 | SEC-001 removes the recursive browser-admin rule. OAuth secrets and server-owned financial/operational collections are denied; current catalog writes are narrowly allowlisted. | Review and deploy rules, capture deployed-rule evidence, then move remaining catalog mutation to scoped APIs under ADMIN-001. | +| RISK-009 | SAFETY-001 preserves same-origin path, query, and hash through the GitHub Pages fallback and rejects unsafe stored targets. | Review/deploy and run real test-mode Stripe/Strava callback smoke tests; WEB-001 still owns controlled hosting. | +| RISK-010 | SAFETY-001 uses a `demo-mprc-local` app, connects the shared Functions/Auth/Firestore instances to local emulators in development/test, fails startup on connection configuration errors, and adds environment-isolation tests. | Confirm the full emulator workflow and CI gate; no production invocation was performed in this pass. | +| Part of RISK-007 | AUTH-001A [#98](https://github.com/Run-MPRC/Run-MPRC.github.io/issues/98) was merged through [PR #107](https://github.com/Run-MPRC/Run-MPRC.github.io/pull/107) as `ce22c110f2132b157bd8a0d43b065585e0b43cb5`. Source and focused tests reject unverified targets at the two existing role-grant endpoints. | The workflow skipped Firebase because `FIREBASE_SERVICE_ACCOUNT` was absent, so backend-live behavior is unproven. Remaining AUTH-001 guards, Rules, verification mirror/refresh, demotion propagation, audit, and authoritative membership work stay open. | +| RISK-013 | PROMO-001 sets both current Checkout creators to `allow_promotion_codes: false`; the webhook quarantines nonzero discounts plus unconfigured tax/shipping totals. | Add exact creator-payload tests, inventory pre-change Sessions/provider promotion configuration, keep the guard through staging/deploy, and implement an authoritative discount/tax/shipping contract before ever enabling it. | +| 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 | Firebase Analytics and Sentry do not initialize in local/test runtime in the working tree, even if production-looking public config is present. | Approve hosted consent/redaction/retention/replay policy, environment-specific configuration, payload inspection, and deploy evidence under OBS/DATA work. | +| Part of RISK-035 | The Jest environment is repaired, the stale frontend smoke test is replaced, and webhook lifecycle tests materially increase Functions coverage. | CI still needs fail-closed lint/test/rules gates and the broader TEST-001 suite. | + +## 5. Threat model + +### Assets + +- Stripe secret keys, webhook secrets, refunds, payout/account settings, and financial reports. +- Firebase/GCP service credentials, deployment authority, Auth role claims, and App Check configuration. +- Runner identity, date of birth, phone, emergency contact, waiver evidence, and attendance. +- Buyer identity, shipping address, order history, and tracking information. +- Member profiles and Strava access/refresh tokens and activity data. +- Race capacity, merchandise inventory, discounts, email reputation, and cloud budget. +- GitHub repository, workflows, DNS/custom domain, production artifacts, and audit history. + +### Relevant threat actors + +- Opportunistic unauthenticated bots testing public functions and credential stuffing. +- A customer manipulating price, member tier, custom fields, checkout retries, refunds, or inventory races. +- A compromised member or admin account. +- An insider with more access than their job needs. +- A leaked CI/cloud/Stripe/OAuth secret. +- A compromised dependency, GitHub Action, browser extension, or operator device. +- Accidental operator/developer action against production. +- Webhook duplication, delay, reordering, provider outage, or partial network failure without malicious intent. + +### High-risk abuse cases + +1. Submit a member tier while unauthenticated or using an unverified known member email. +2. Race two last-seat/last-SKU requests. +3. Reuse a client request after a timeout to create multiple Sessions or refunds. +4. Pay a Session after the operator cancels the local record. +5. Replay or reorder a valid Stripe event. +6. Apply an unmodeled Stripe promotion and obtain goods while local records show another total. +7. Steal an admin token and read OAuth secrets or directly rewrite financial records. +8. Script public callables without App Check to create Stripe objects, Firestore writes, or cloud cost. +9. Inject markup/formulas/oversized data through names, notes, custom fields, email, or exports. +10. Cause local development to call production Functions. + +## 6. Required security architecture + +### Authentication + +- Firebase Auth verifies identity; email ownership must be verified before member/admin grants based on email. +- Privileged accounts require MFA and a documented account-recovery process. +- Sensitive operations require recent authentication where supported. +- Auth error messages do not reveal whether an account exists beyond Firebase's approved behavior. +- Role changes force token refresh/revocation and emit a durable audit event. + +### Authorization + +- Server code and Firestore rules check signed token capabilities; the UI is never the enforcement point. +- Separate catalog/content editing from financial state, identity administration, exports, and secrets. +- No client—including browser admins—can write webhook inbox, payment state, refund IDs, Stripe IDs, audit events, rate limits, or OAuth secrets. +- Server service accounts use IAM least privilege because Admin SDK bypasses Firestore rules. + +### App Check and abuse resistance + +- Use reCAPTCHA Enterprise for web App Check and observe metrics before enforcing. +- Set runtime `enforceAppCheck: true` on every sensitive callable instead of a custom fail-open environment test. +- Consider limited-use/replay-protected tokens only for the most abuse-sensitive callable after measuring latency and SDK support. +- Keep rate limits as defense in depth; App Check is not user authorization and does not stop all valid-browser abuse. +- Apply cloud budgets/alerts and Stripe rate/error alerts so cost abuse is visible. + +### Input and output safety + +- Centralize strict request schemas with maximum object depth, field count, string length, array length, total bytes, integer ranges, enum values, and URL allowlists. +- Normalize email consistently; do not over-normalize names or assume ASCII. +- Validate event custom fields against the stored schema. +- Escape HTML email content, neutralize spreadsheet formulas, and let React escape browser rendering. +- Avoid rendering arbitrary HTML from Firestore. If business requirements demand it, sanitize with a maintained allowlist library and a restrictive CSP. +- Return generic errors to unauthenticated clients; log structured error codes without secrets/PII. + +### Secrets + +- Store server secrets in Google Secret Manager and bind each only to functions that need it. +- Never use `REACT_APP_*` for a secret; CRA embeds those values in public JavaScript. +- Keep local overrides ignored (`.secret.local`, `.env.local`) and commit only name/format examples. +- Rotate Stripe and webhook secrets independently, with an overlap plan where the provider supports it. +- Inventory owners, creation dates, consumers, last rotation, and emergency revocation steps. +- Remove long-lived GitHub service-account JSON in favor of workload identity/OIDC when feasible. + +### Payment integrity + +The binding design is in [STRIPE_COMMERCE_DESIGN.md](./STRIPE_COMMERCE_DESIGN.md). Its mandatory controls are server price authority, persistence-first checkout, Stripe and application idempotency, atomic capacity/inventory, verified async-aware webhooks, allowed state transitions, idempotent refunds, and reconciliation. + +## 7. Data classification and retention + +| Class | Examples | Baseline controls | +| --- | --- | --- | +| Public | Published event/product content, officer information intentionally on site | Integrity review; no secrets or hidden fields in same readable document | +| Internal | Catalog drafts, operational counts, non-sensitive audit metrics | Authenticated role-limited access; normal logs/backup | +| Confidential PII | Member email/phone, order contact, Strava profile metadata | Need-to-know access, redacted logs, retention limit, access/deletion process | +| Restricted PII | DOB, emergency contact, shipping address, waiver evidence | Narrow server/admin capability, export audit, encryption in transit/at rest, aggressive field-specific retention | +| Restricted secret | Stripe keys, webhook secrets, OAuth refresh tokens, deploy credentials | Secret Manager or server-only encrypted store, no browser read, rotation, access audit | +| Financial metadata | Expected/paid/refunded totals, Stripe IDs, disputes | Finance/server capability, immutable audit, accounting retention, reconciliation | + +Proposed retention values must be approved by counsel, finance, insurance, and operations. A reasonable minimization starting point for discussion is: + +- Unpaid/expired checkout PII: delete or anonymize within 30 days after final reconciliation. +- Emergency contact and DOB: delete after the event plus a short operational/incident window unless insurance/counsel requires longer. +- Shipping address: remove from routine operator access after fulfillment/return window; retain only fields legally required for accounting/tax. +- OAuth tokens: delete immediately on disconnect/account deletion; rotate on suspected exposure. +- Rate-limit identifiers: expire shortly after the enforcement window and store HMACs, not raw values. +- Webhook inbox: retain non-PII identifiers long enough for reconciliation/audit, then expire under policy. +- Payment/accounting and waiver evidence: retain only for the approved legal/accounting/insurance period, separately from unnecessary operational PII. + +Backups and exports must honor eventual deletion/anonymization schedules and have documented access. + +## 8. Logging and monitoring rules + +### Log + +- Correlation/request ID, function name, environment, code version, local business ID, Stripe Event/type/object ID, state transition, latency, and sanitized error code. +- Authentication UID for privileged actions, capability used, target resource, reason, and result. +- Reconciliation counts and anonymous mismatch categories. + +### Do not log + +- Stripe or OAuth secrets, bearer tokens, full webhook payloads, Checkout URLs, password/reset data, raw ID tokens, session cookies, full addresses, DOB, emergency contacts, or arbitrary request bodies. +- Full customer email/IP in rate-limit and security logs; use a keyed pseudonymous identifier where correlation is needed. + +### Alert + +- Invalid webhook signature spikes. +- Event inbox failed/quarantined/old items. +- Paid Stripe Session with no local record or amount/currency mismatch. +- Local paid record not verified in Stripe. +- Refund/dispute creation and failed refund reconciliation. +- Capacity/inventory counter drift or negative availability. +- Elevated callable rejects, App Check failures, Auth abuse, cloud budget anomalies, deploy failures, secret access, and role grants. + +## 9. Secure delivery controls + +- Protect `main` and production environments with pull-request review and required passing checks. +- CI must run non-mutating lint, frontend tests, Functions tests, Firestore Rules emulator tests, production build, secret scan, and dependency scan. +- Do not use `lint:fix` as the CI lint command or mask it with `|| true`. +- Pin GitHub Actions to reviewed major versions or immutable SHAs according to the project's maintenance capacity. +- Dependabot/Renovate updates are small, reviewed, and tested; do not blindly `npm audit fix --force`. +- Generate an SBOM/release dependency inventory for live commerce builds. +- Restrict production deployment to protected environment approval; use short-lived federated cloud credentials. +- Deploy backward-compatible backend/rules/indexes before dependent frontend changes. +- Record release commit, migration, function version, Stripe webhook/API version, test evidence, and rollback point. + +## 10. Incident response + +### Severity examples + +- **SEV-1:** Stripe/GCP/GitHub secret exposure; unauthorized refund/payout/role change; confirmed PII exfiltration; active payment misclassification at scale. +- **SEV-2:** Webhook backlog or reconciliation mismatch affecting customers; capacity/inventory oversell; admin account compromise without confirmed data access. +- **SEV-3:** Bounded availability failure, attempted abuse blocked by controls, or non-sensitive data issue. + +### First response + +1. Name the incident commander and scribe; preserve timestamps and evidence. +2. Stop harm: disable new checkout, revoke/rotate affected credentials, revoke sessions/roles, or isolate the deployment. +3. Do not destroy logs or silently rewrite financial records. +4. Compare Stripe and Firestore using the reconciliation runbook. +5. Notify Stripe/Firebase/GitHub or other vendors through their security/support channels when relevant. +6. Determine legal, insurance, customer, and regulatory notification with qualified advisers. +7. Restore through an audited repair, test it, monitor, and document every changed record. +8. Complete a blameless post-incident review with owners and deadlines. + +Specific playbooks live in [OPERATIONS_RUNBOOK.md](./OPERATIONS_RUNBOOK.md). + +## 11. Verification evidence required before live mode + +- Passing CI link and commit SHA. +- Dependency scan with zero unaccepted critical/high runtime findings. +- Firestore Rules test report proving browser admins cannot read secrets or directly mutate financial records. +- App Check metrics and enforcement proof for each callable. +- Stripe test-mode event delivery showing valid, duplicate, invalid, async, expired, refund, and dispute paths. +- Concurrency test showing no capacity/SKU oversell. +- Reconciliation report with zero unexplained mismatches. +- Secret/IAM/role inventory reviewed by two owners. +- Approved policies/waiver versions and retention matrix. +- Backup restore exercise and checkout-disable drill. +- Limited live pilot report and finance confirmation that Stripe payouts/totals match local records. + +## 12. Current first-party references + +- [Stripe webhook best practices](https://docs.stripe.com/webhooks) +- [Stripe Checkout fulfillment and delayed methods](https://docs.stripe.com/checkout/fulfillment) +- [Stripe idempotent requests](https://docs.stripe.com/api/idempotent_requests) +- [Firebase App Check enforcement](https://firebase.google.com/docs/app-check/cloud-functions) +- [Firebase Secret Manager and environment configuration](https://firebase.google.com/docs/functions/config-env) +- [Firestore IAM and least privilege](https://firebase.google.com/docs/firestore/security/iam) +- [Firestore Security Rules conditions](https://firebase.google.com/docs/firestore/security/rules-conditions) + +Re-check first-party documentation during implementation because provider behavior and recommended controls change. diff --git a/STRIPE_COMMERCE_DESIGN.md b/STRIPE_COMMERCE_DESIGN.md new file mode 100644 index 0000000..6d65167 --- /dev/null +++ b/STRIPE_COMMERCE_DESIGN.md @@ -0,0 +1,505 @@ +# Stripe, Race Registration, and Merchandise Design + +**Status:** Required production design; current code is a test-mode prototype +**Last reviewed:** 2026-07-12 +**Related:** [SYSTEM_DESIGN.md](./SYSTEM_DESIGN.md), [SECURITY.md](./SECURITY.md), [OPERATIONS_RUNBOOK.md](./OPERATIONS_RUNBOOK.md) + +This document defines how MPRC should configure Stripe and how race-registration and merchandise money flows must behave. It deliberately separates repository implementation from external Stripe account configuration: source code can declare required secret names and behavior, but it cannot prove that the production Stripe account, event destination, bank account, roles, tax settings, Radar controls, or secrets are correctly configured. + +## 1. Launch status + +**Do not enable or advertise live checkout yet.** The initial 2026-07-12 assessment found unsafe payment confirmation, no Event-ID ledger, enabled unmodeled promotions, and callback loss. A working-tree safety slice now validates paid/amount/currency/mode/explicit livemode, rejects discounts, deduplicates Events, protects terminal/refund state, and preserves callbacks. That slice is not deployed evidence and does not close the payment architecture. These residual conditions still prevent safe production use: + +- Checkout creation lacks validated fail-closed environment configuration, command idempotency, a persistence-first saga, and the canonical versioned state/schema contract. +- The webhook still needs canonical reducer/reservation/outbox integration, explicit metadata schema allowlisting/migration, retry/dead-letter operations, TTL/alerts, emulator integration, and Stripe test-mode delivery rehearsal. +- A webhook can arrive before the registration/order record is written. +- Late-registration Payment Links cannot be reliably mapped back to their registration and are reusable. +- Race capacity is checked with a non-atomic count; merchandise has no SKU inventory reservation. +- Cancelling locally does not expire an open Stripe Session, so a later payment can reopen the record. +- Promotion codes are disabled and discounted Sessions quarantined in the working tree, but exact creator tests and outstanding pre-change Session/provider inventory remain PROMO-001. +- Success credentials remain in query strings even though the working-tree Pages fallback now preserves them; DATA-001 must remove long-lived plaintext capabilities and scrub browser/monitoring state. +- Production App Check enforcement is optional and fail-open. +- Legal, privacy, cancellation/refund, tax, shipping, and waiver content is not approved for launch. + +The issue sequence in [GITHUB_ISSUES.md](./GITHUB_ISSUES.md) closes these gaps in dependency order. + +## 2. Stripe account and environment model + +### Required account controls + +- The Stripe account must be owned by MPRC, not an individual developer. +- Require MFA for every Stripe team member; prefer phishing-resistant security keys for administrators and finance roles. +- Assign least-privilege Stripe roles. Only a small finance group should issue refunds or change payout settings. +- Enable payout and bank-account change notifications to more than one trusted officer. +- Record legal entity, tax, statement descriptor, public support contact, website, and fulfillment/refund policies accurately. +- Configure Radar and review rules before live sales. Start with Stripe defaults and add rules only from observed abuse, not guesses. +- Restrict webhook destinations to the event types this application handles. +- Maintain two named owners for key rotation, webhook delivery failures, disputes, and account recovery. + +### Environment isolation + +| Concern | Local/CI | Staging | Production | +| --- | --- | --- | --- | +| API mode | Stripe test objects and fixtures | Stripe sandbox/test mode | Stripe live mode | +| Secret key | Local secret override, never committed | Staging-scoped secret | Production-scoped secret | +| Webhook secret | Stripe CLI signing secret | Staging endpoint secret | Production endpoint secret | +| Firebase project | Emulator | Dedicated staging project | Dedicated production project | +| Site origin | `http://localhost:3000` | `https://dev.runmprc.com` | `https://runmprc.com` | +| Data | Synthetic | Synthetic/approved test data | Real customer and runner data | + +Never let a development build fall back to production Functions. Never share a webhook signing secret between endpoints or modes. A test-mode Event cannot mutate a production business record; the webhook processor must compare the incoming Stripe `livemode` flag to its environment configuration. + +### Secret inventory + +| Name | Consumer | Storage | Notes | +| --- | --- | --- | --- | +| `STRIPE_SECRET_KEY` | Checkout, refund, reconciliation functions | Google Secret Manager, bound only to those functions | Prefer the narrowest key Stripe supports for the required API resources; never expose it to React. | +| `STRIPE_WEBHOOK_SECRET` | Webhook ingress only | Google Secret Manager | Unique per event destination and environment. | +| `STRIPE_LIVEMODE_EXPECTED` | Webhook/reconciler | Non-secret environment parameter | Explicit `true` or `false`; fail deployment/startup if absent. | +| `SITE_ORIGIN` | Checkout and email links | Validated environment parameter | Exact allowlisted HTTPS origin in hosted environments. | +| `REACT_APP_RECAPTCHA_SITE_KEY` | Browser App Check | Build environment | Public site key; not a secret. Use a separate key per environment. | +| `STRAVA_CLIENT_ID` / `STRAVA_CLIENT_SECRET` | Strava functions | Secret Manager | Separate from Stripe and bound only to Strava functions. | + +The Firebase web API configuration is not a server secret, but its project, Auth, Firestore, and App Check configuration must still be restricted and monitored. + +## 3. Payment method policy + +For the first live release, enable card-class immediate methods only unless the team explicitly implements and tests delayed methods. Wallets delivered through the card payment method can still be available through Stripe Checkout. If ACH, bank debit, cash vouchers, or any delayed-notification method is enabled, `checkout.session.completed` can represent authorization rather than settled payment. Fulfillment must wait for `checkout.session.async_payment_succeeded` or a verified successful PaymentIntent, and failure must be handled. + +The backend should remain correct if a delayed method appears accidentally: + +- `checkout.session.completed` + `payment_status=paid` may confirm payment. +- `checkout.session.completed` + `payment_status=unpaid` stays in `processing`/`pending_payment` and does not consume final fulfillment side effects. +- `checkout.session.async_payment_succeeded` confirms payment. +- `checkout.session.async_payment_failed` transitions to a failed state and releases the reservation if policy allows. + +## 4. Money representation + +- Store all amounts as integer minor units: `amountExpectedCents`, `amountPaidCents`, `amountRefundedCents`, and `discountAmountCents`. +- Store a lowercase ISO currency code alongside every amount; launch with `usd` only. +- Reject `NaN`, floats, negative values, unsafe integers, and values above a documented maximum. +- Never infer the amount paid from the catalog after checkout. Catalog price can change; the business record stores the expected snapshot and Stripe stores the charged total. +- Keep Stripe IDs as references, not proof: Session, PaymentIntent, Charge, Refund, Product, Price, and Event IDs each have different semantics. +- Do not put PII, waiver text, emergency contacts, or secrets in Stripe metadata. Use opaque local IDs and a schema version. + +## 5. Catalog model + +### Race pricing + +The server reads an event pricing snapshot and selects one of the configured tiers: + +- `member`: requires a current server-verified member or authorized admin claim. +- `non_member`: public default. +- `early_bird`: requires a server-side cutoff comparison. +- `comp`: created only by an authorized event manager under an explicit waiver policy. +- `free`: a configured zero-price participant registration. +- `volunteer`: no race payment and a distinct capacity policy. + +The current client-computed tier is display-only. The server selection is final. An unavailable or malformed tier fails closed. + +### Merchandise variants + +Products need SKU-level variants rather than independent `sizes[]` and `colors[]` arrays: + +```text +products/{productId} + title, description, status, images, taxCode, shippingPolicy + +products/{productId}/variants/{variantId} + sku + optionValues: { size, color } + priceCents + currency + onHand + reserved + sold + status + stripeProductId + stripePriceId (optional) +``` + +Every sellable combination has one canonical variant ID. Here `onHand` means physical sellable stock currently held, `reserved` is the subset temporarily held for unpaid Checkout, and `sold` is a cumulative reporting counter. Availability is `onHand - reserved`; do not subtract cumulative `sold` again. + +### Stripe Products and Prices + +Do not lazily create Stripe Products from an anonymous checkout request. Concurrent checkouts can create duplicates and give public traffic permission to drive catalog writes. Product/Price creation should occur in an authenticated catalog-management function or an idempotent deployment/synchronization job. + +Two valid implementation patterns exist: + +1. Persist Stripe Price IDs per event tier/SKU and use them in Checkout. This provides clear Stripe reporting and immutable price snapshots. +2. Use server-authored `price_data` for each Session while persisting an MPRC/Stripe Product mapping. This is simpler for infrequent races but creates many inline prices. + +For merchandise, persistent Products and Prices are recommended. For one-off races, either is acceptable if updates are idempotent and reporting is clear. The choice must not move price authority to the browser. + +## 6. Checkout request contract + +Every checkout request includes a client-generated UUID `requestId`. The server binds it to a normalized payload fingerprint and caller scope, stores only a hash as the Firestore key, and rejects reuse with different data. + +### Race request + +```json +{ + "requestId": "uuid", + "eventId": "opaque-id", + "signupType": "participant", + "requestedPriceTier": "member", + "runner": { + "firstName": "...", + "lastName": "...", + "email": "...", + "phone": "...", + "dateOfBirth": "YYYY-MM-DD", + "emergencyContactName": "...", + "emergencyContactPhone": "..." + }, + "customFields": {}, + "waiver": { + "accepted": true, + "version": "..." + } +} +``` + +The server validates field presence, type, Unicode-normalized length, allowed options, date format/range, event-defined custom field schema, waiver version, registration window, event visibility, member eligibility, rate limit, and payload size. Unknown custom keys are rejected rather than stored. + +### Merchandise request + +```json +{ + "requestId": "uuid", + "productId": "opaque-id", + "variantId": "opaque-id", + "quantity": 1, + "buyer": { + "firstName": "...", + "lastName": "...", + "email": "...", + "phone": "..." + } +} +``` + +Launch with quantity `1` unless cart and multi-line inventory semantics are explicitly implemented. The server ignores client price/title values because none are accepted in this contract. + +### Response + +```json +{ + "businessId": "registration-or-order-id", + "state": "checkout_ready", + "checkoutUrl": "https://checkout.stripe.com/...", + "expiresAt": "timestamp" +} +``` + +Returning the same request returns the same active Session. If the Session expired, a versioned new attempt can be created against the same business record under an explicit transition. + +## 7. Persistence-first checkout saga + +External Stripe calls cannot be part of a Firestore transaction. Use this sequence: + +1. Validate the request and read server-controlled catalog/event state. +2. Derive the idempotency-record key and payload fingerprint. +3. In one Firestore transaction: + - Reuse a matching prior request or reject a conflicting reuse. + - Lock/read the event capacity counter or SKU variant. + - Verify availability. + - Increment `reserved` exactly once when applicable. + - Create the registration/order in `checkout_creating` with an immutable expected-price snapshot. + - Store `capacityHeld` or `inventoryHeld` so release is idempotent. +4. Create the Stripe Checkout Session with an idempotency key such as `registration:{id}:session:{attempt}` or `order:{id}:session:{attempt}`. +5. Include `client_reference_id`, `metadata.type`, `metadata.schemaVersion`, and the opaque local ID(s). +6. Store Session ID, URL, expiry, and attempt state. +7. Return the URL. +8. If Stripe definitively rejects creation, run a compensating transaction that marks the attempt failed and releases the hold once. +9. If the function loses its response after Stripe creates the Session, a retry uses the same Stripe key, receives the same Session, and completes step 6. + +Create the local business record before calling Stripe. That lets a very fast webhook resolve metadata directly and eliminates the current record-not-found race. + +Set a deliberate Checkout expiry appropriate for scarce inventory. Stripe's allowed range and current API behavior must be verified during implementation. The application cleanup threshold must match the actual Session expiry rather than waiting seven days. + +## 8. Capacity reservation design + +Use counters stored on the event document or a dedicated counter document updated in the same transaction as registration creation: + +```text +participantCapacity +participantReservedCount +participantPaidCount +participantReleasedCount (optional audit metric) +counterVersion +``` + +Before enabling the counter, run an idempotent backfill that counts existing active participant records (`pending`, `processing`, `paid`, `comp` according to migration policy), writes a versioned baseline, and reports discrepancies. Every subsequent transition updates both registration and counter in one transaction. + +Rules: + +- Volunteers do not change participant counters. +- Free participants reserve a seat and move directly to confirmed. +- Pending/processing Sessions hold a seat until payment, explicit cancellation, async failure, or expiry. +- Full refund/cancellation releases a seat only if event policy and cutoff permit it. +- A release checks `capacityHeld == true`, sets it false, and decrements once. +- Partial refunds do not release automatically. +- Admin comps and late registrations use the same reservation service; they cannot bypass capacity silently. + +## 9. Inventory reservation design + +In the variant transaction: + +```text +available = onHand - reserved +require available >= quantity +reserved += quantity +order.inventoryHeld = true +``` + +On paid webhook, decrement both `reserved` and `onHand` by the quantity and increment cumulative `sold`. On Session expiry, async failure, or pre-payment cancellation, decrement only `reserved`. A refund does not automatically return stock: a separate approved return/inspection action increments `onHand` for resellable stock or records damaged/write-off disposition. Migration and admin adjustment tests must assert these semantics explicitly. + +Do not represent inventory solely with `product.status == active|sold_out`. Product status is merchandising; the SKU counter is the sellability constraint. + +## 10. Checkout Session configuration + +For every Session: + +- `mode: payment`. +- Server-controlled line items and currency. +- A validated same-origin HTTPS success URL and cancel URL. +- `client_reference_id` equal to the local business ID. +- Minimal metadata: `type`, `schemaVersion`, local ID, and event/product/variant ID as needed. +- `customer_email` only when needed and validated. +- Shipping collection only for flows that ship physical goods, with an approved country list. +- Explicit payment-method policy. +- Deliberate expiration for reserved capacity/inventory. +- Promotion codes disabled under PROMO-001 until PAY-001 defines server-approved monetary snapshots and PAY-002 implements the corresponding idempotent eligibility/discount/state contract. The working tree sets `allow_promotion_codes: false` and quarantines nonzero discounts; creator-payload tests and pre-change Session/provider inventory remain open. +- A stable Stripe idempotency key supplied in request options. + +Do not put the confirmation bearer token in `success_url`. Use Stripe's `{CHECKOUT_SESSION_ID}` placeholder. The success API retrieves and validates the Session server-side and returns a sanitized business projection. The success page is informational; fulfillment remains webhook-driven. + +For free anonymous registrations, return an opaque receipt token, store only a cryptographic hash, place it in a URL fragment or session state rather than the query, remove it from browser history immediately, apply expiry, and rate-limit lookups. Authenticated users should use UID ownership instead. + +## 11. Webhook endpoint and event inbox + +### Ingress requirements + +- Accept `POST` only. +- Read the exact raw bytes. +- Require the `Stripe-Signature` header. +- Verify with the endpoint-specific secret and Stripe's maintained library. +- Reject malformed or invalid signatures with `400` and a generic response. +- Enforce a reasonable body size at the platform/edge where possible. +- Do not log full payloads, checkout URLs, customer data, or secrets. +- Listen only for required event types. + +### Required event types + +Initial set: + +- `checkout.session.completed` +- `checkout.session.async_payment_succeeded` +- `checkout.session.async_payment_failed` +- `checkout.session.expired` +- `charge.refunded` or the selected refund event model +- `charge.dispute.created` +- dispute lifecycle events required by the finance runbook + +The exact refund/dispute event set should be selected against the Stripe API version during implementation. + +### Durable processing + +Use `stripeEvents/{event.id}` as a durable inbox/dedupe record. Store event type, Stripe object ID, livemode, received/processed timestamps, attempt count, status, code version, and local business path—no customer PII. + +A successful business transition and `processed` event marker must be atomic where possible. If using a queue, webhook ingress durably enqueues/creates the event before returning `2xx`; a worker leases and processes it. Duplicate Event IDs produce no duplicate transition. Because Stripe can generate distinct Events for the same object transition, the business state machine must also be idempotent by object ID and transition. + +Stripe does not guarantee delivery order. Never assume Checkout completion arrives before a refund or dispute. When state is ambiguous, retrieve the canonical Stripe object and reduce it into the current local state. + +### Verification before marking paid + +At minimum verify: + +- Expected `livemode` for this deployment. +- Session `mode` and object type. +- Metadata schema and local record reference. +- Session ID ownership (or attach it exactly once from trusted metadata for a persistence-first record). +- Payment status appropriate to the event. +- Currency equals expected currency. +- `amount_total` equals the allowed expected total after a validated discount policy. +- Local record is in an allowed predecessor state. +- PaymentIntent is not already attached to another local business record. + +Store actual total, discount, tax, shipping, Stripe customer reference if required, PaymentIntent ID, and Charge reference. An anomaly enters `payment_review`/quarantine and alerts operations; it never silently marks paid. + +## 12. Payment and business state machines + +Payment and operational state should be separate. + +### Payment state + +```mermaid +stateDiagram-v2 + [*] --> not_required + [*] --> checkout_creating + checkout_creating --> checkout_open + checkout_creating --> checkout_failed + checkout_open --> processing + checkout_open --> paid + checkout_open --> expired + checkout_open --> cancelled + processing --> paid + processing --> failed + paid --> refund_pending + refund_pending --> partially_refunded + refund_pending --> refunded + paid --> disputed + partially_refunded --> disputed + disputed --> paid: won/closed without loss + disputed --> refunded: lost/returned +``` + +### Registration state + +`reserved -> confirmed -> attended|no_show|transferred|cancelled`. Waiver acceptance and eligibility are attributes/evidence, not inferred from payment state. A substitute must accept the applicable waiver; an admin cannot silently transfer the original person's acceptance. + +### Fulfillment state + +`unfulfilled -> picking -> packed -> shipped|ready_for_pickup -> delivered|picked_up`, with separate `cancelled`, `return_requested`, `returned`, and `written_off` paths as needed. + +During migration, the existing single `status` remains as a derived compatibility field. New code writes explicit states first and derives the legacy field in one place. + +## 13. Cancellation, expiry, and late registration + +### Cancellation + +- Cancelling an unpaid record expires the active Stripe Session before or as part of the saga, then releases the hold. +- If Session expiry fails transiently, keep a cancellation-pending state and retry; do not merely mark local cancelled while payment remains possible. +- Cancelling a paid record requires an explicit refund/no-refund policy and permission. It cannot be the same operation as unpaid cancellation. + +### Expiry + +- Handle `checkout.session.expired` for both registrations and orders. +- Release the hold exactly once. +- A scheduled sweeper finds records beyond `expiresAt + grace period`, retrieves the Stripe Session, and repairs local state. + +### Late registration + +Do not create a reusable Payment Link per registrant. Prefer a one-off Checkout Session created through the same idempotent registration service, then communicate that URL through an authorized channel. If Payment Links are retained, restrict them, define quantity/expiry behavior, map every generated Session through trusted metadata, and prevent multiple paid Sessions from confirming or charging one registration. The simpler first-release decision is to replace the current late-registration Payment Link flow. + +## 14. Promotion, tax, shipping, and receipts + +### Promotions + +The assessment baseline's `allow_promotion_codes: true` was not a complete promotion system. PROMO-001 now disables it in the working tree; before enabling discounts in any future issue: + +- Decide whether codes live in Stripe, Firestore, or both. +- Restrict eligible products/events, redemption counts, dates, currencies, and customer scope. +- Validate the applied Stripe discount on the webhook. +- Store expected, discounted, tax, shipping, and final totals separately. +- Include discounts in reconciliation and exports. +- Audit code creation and changes. + +Until that work is complete, keep `allow_promotion_codes: false` and quarantine any nonzero discount. + +### Tax + +MPRC leadership and a qualified adviser must determine sales-tax obligations. If Stripe Tax is used, enable it intentionally per sellable item and set correct product tax codes and addresses. Do not assume race fees and merchandise share tax treatment. + +### Shipping + +Define supported countries, carriers, costs, pickup options, delivery expectations, lost-package handling, and return policy. Checkout shipping collection alone is not fulfillment logic. Store only the address fields needed for fulfillment, restrict access, and delete/minimize them after the retention period. + +### Receipts and confirmations + +Stripe can send payment receipts. MPRC sends a separate registration/order confirmation only after verified local transition. Each email has an idempotency/outbox key, escaped user-controlled content, no secret URL, and no sensitive emergency/contact details. + +## 15. Refund and dispute design + +### Refund request + +- Require a finance-authorized user, App Check, and a recent-authentication/MFA policy. +- Validate current Stripe and local state. +- Validate integer amount against `amountPaidCents - amountRefundedCents`. +- Require an operator reason and optional customer-visible note. +- Generate a stable idempotency key per approved refund request. +- Create a local `refund_pending` operation record before or with the Stripe call. +- Let the verified Stripe event confirm final refunded totals. +- Never label a refund full merely because the request omitted `amount`; retrieve/verify totals. + +### Disputes + +- Record disputes for registrations and merchandise. +- Alert finance immediately with an opaque business reference and Stripe Dashboard link pattern, not full PII in chat/email. +- Preserve required evidence under the approved retention policy. +- Track opened, needs-response, won, lost, warning-closed, and funds-reinstated outcomes as supported by Stripe. +- Reconciliation must detect a Stripe dispute with no local record. + +## 16. Reconciliation + +Run a scheduled, idempotent reconciliation job and an operator-triggered version: + +- Local pending/processing records past expected time -> retrieve Session/PaymentIntent. +- Local paid record -> verify Stripe paid total/currency and no unexpected refund/dispute. +- Stripe successful Session in the integration's time window -> verify one local business record. +- Local refund totals -> compare Stripe refund totals. +- Capacity/inventory counters -> compare source records and report drift. +- Webhook inbox -> alert failed, quarantined, or long-running items. + +The job writes a report and metrics; it does not automatically overwrite ambiguous financial state. Safe deterministic repairs may be automated and must be logged. + +## 17. Test matrix + +Every commerce release must cover: + +- Immediate successful card payment. +- Free participant and volunteer registration. +- Member price accepted and rejected. +- Registration before/after window and members-only visibility. +- Concurrent last-seat attempts; exactly one succeeds. +- Concurrent last-SKU attempts; no negative stock. +- Duplicate checkout request and mismatched request-ID reuse. +- Function timeout after Stripe creates the Session. +- Webhook arriving before the client redirect. +- Duplicate and out-of-order webhook events. +- Invalid signature and wrong webhook secret. +- Wrong livemode, amount, currency, metadata, Session, or PaymentIntent. +- `completed` with unpaid/processing status. +- Async payment success and failure. +- Session expiry and local cancellation with Stripe expiry. +- Full and multiple partial refunds, retry after timeout, and excessive refund rejection. +- Registration and merchandise disputes. +- Promotion disabled; later, allowed and disallowed discounts. +- Late-registration one-time payment. +- Confirmation email emitted once with hostile HTML-like input safely escaped. +- Reconciliation repairs a missed webhook and reports an irreconcilable anomaly. + +Use Stripe test clocks/fixtures where applicable, Stripe CLI for signed local delivery, Firebase emulators for integration tests, and a separate staging end-to-end suite. No automated test uses live keys or production data. + +## 18. Go-live checklist + +Live checkout remains disabled until every P0 gate is evidenced: + +- [ ] Production and staging Firebase/Stripe environments are isolated. +- [ ] Secret Manager bindings and rotation owners are verified. +- [ ] App Check is enforced at the function runtime and monitored. +- [ ] Checkout creation is persistence-first and idempotent. +- [ ] Capacity and SKU inventory reservations pass concurrency tests. +- [ ] Webhook inbox, async handling, amount/currency/reference verification, and duplicates pass tests. +- [ ] Local cancellation expires Stripe Sessions. +- [ ] Refunds are idempotent and permissioned. +- [ ] Promotion codes are disabled or fully reconciled. +- [ ] Success routing preserves callbacks and no long-lived bearer token remains in query/history. +- [ ] Reconciliation and payment alerts are live. +- [ ] Critical/high runtime dependency findings are remediated or explicitly risk-accepted with compensating controls. +- [ ] Terms, privacy, refund/cancellation, tax, shipping, and waiver decisions are approved. +- [ ] Stripe/Firebase/GitHub/DNS administrators use MFA and named backup owners. +- [ ] A test-mode dress rehearsal and limited live pilot complete successfully. + +## 19. Primary technical references + +Implementation should be checked against current first-party documentation at the time of each issue: + +- [Stripe Checkout fulfillment](https://docs.stripe.com/checkout/fulfillment) +- [Stripe webhook behavior and best practices](https://docs.stripe.com/webhooks) +- [Stripe idempotent requests](https://docs.stripe.com/api/idempotent_requests) +- [Firebase App Check enforcement for Cloud Functions](https://firebase.google.com/docs/app-check/cloud-functions) +- [Firebase environment configuration and Secret Manager](https://firebase.google.com/docs/functions/config-env) + +These links are guidance, not proof that the production account is configured correctly. The runbook requires screenshots or exported non-secret configuration evidence for launch review. diff --git a/SYSTEM_DESIGN.md b/SYSTEM_DESIGN.md new file mode 100644 index 0000000..a699267 --- /dev/null +++ b/SYSTEM_DESIGN.md @@ -0,0 +1,379 @@ +# MPRC Platform System Design + +**Status:** Target architecture and current-state assessment +**Last reviewed:** 2026-07-12 +**Owners:** MPRC product, engineering, operations, and finance leads + +This document is the system-level map for the Mid-Peninsula Running Club platform. It describes what exists in this repository, which parts are safe to rely on today, and the architecture required before accepting live race-registration or merchandise payments. + +The repository already contains a substantial prototype: a public React site, Firebase Authentication, Firestore-backed events and products, member and admin experiences, Stripe Checkout creation, a signed Stripe webhook, order and registration administration, Firestore rules, and a small automated test suite. That is a useful foundation, but it is not yet a production-ready commerce system. In particular, payment reconciliation, replay safety, capacity and inventory reservations, environment isolation, legal content, dependency health, and operational controls are incomplete. See [SECURITY.md](./SECURITY.md) for the risk register and [IMPLEMENTATION_PLAN.md](./IMPLEMENTATION_PLAN.md) for the ordered path to launch. + +## 1. Goals and non-goals + +### Goals + +- Publish public club content, events, and merchandise. +- Support anonymous and signed-in race registration. +- Apply member pricing only when the server verifies a current member role. +- Support free, paid, complimentary, and volunteer registration paths. +- Sell physical merchandise with explicit variant and inventory tracking. +- Keep card data out of MPRC systems by using Stripe-hosted Checkout. +- Give authorized operators safe tools for registrations, orders, refunds, fulfillment, members, and event configuration. +- Make every money-affecting operation idempotent, auditable, observable, and reconcilable. +- Minimize collection and retention of personal data. +- Separate development, test, and production systems so local work cannot change production data. + +### Non-goals for the first production release + +- Storing or processing raw card numbers. +- Building a custom payment form. +- A multi-vendor marketplace, subscriptions, split payments, or Stripe Connect. +- International tax automation, multi-currency pricing, or international fulfillment unless approved as a separate project. +- Complex discount stacking or a public coupon engine. +- General-purpose content management. +- Replacing Stripe's receipts, dispute console, fraud tooling, or financial reports. + +## 2. Architectural principles + +1. **Stripe is authoritative for money; Firestore is authoritative for club operations.** A redirect, browser state, or client request never proves payment. A verified Stripe event transitions the local business record. +2. **The browser proposes; the server decides.** Prices, eligibility, capacity, inventory, refund limits, roles, and state transitions are recomputed or validated in trusted server code. +3. **Reserve scarce resources before opening Checkout.** Race capacity and merchandise inventory are held atomically in Firestore, then a Stripe Session is created as the next step of a recoverable saga. +4. **Every external side effect is idempotent.** Checkout creation, refunds, webhook consumption, emails, and reconciliation use stable business keys and tolerate retries. +5. **Financial mutations use narrow server endpoints.** Admin users may manage catalog and editorial content directly only where rules explicitly permit it. They do not directly edit payment state, OAuth secrets, event ledgers, or rate-limit data. +6. **Fail closed and reconcile.** If an amount, currency, reference, or transition is unexpected, do not mark it paid. Record the anomaly, alert an operator, and resolve it through reconciliation. +7. **Collect the minimum data for the minimum time.** Emergency contacts, birth dates, shipping addresses, OAuth tokens, analytics, and support logs require explicit purpose and retention rules. +8. **Production is never a development dependency.** Local clients use emulators and Stripe test mode; staging and production use different Firebase projects, Stripe sandboxes/accounts or keys, webhook secrets, and domains. + +## 3. Current system context + +```mermaid +flowchart LR + Visitor["Visitor or member browser"] + Admin["Club administrator browser"] + Web["React single-page app\nNetlify live; separate GitHub Pages copy"] + Auth["Firebase Authentication"] + FS["Cloud Firestore"] + Fn["Firebase Cloud Functions\nNode.js 20"] + Stripe["Stripe-hosted Checkout\nand Stripe API"] + Hook["Stripe webhook endpoint"] + Mail["Firestore Email extension\nand mail provider"] + Strava["Strava OAuth and API"] + Sentry["Sentry"] + Analytics["Firebase Analytics"] + + Visitor --> Web + Admin --> Web + Web --> Auth + Web --> FS + Web --> Fn + Fn --> FS + Fn --> Stripe + Stripe --> Hook + Hook --> FS + FS --> Mail + Fn --> Strava + Web --> Sentry + Web --> Analytics +``` + +### Current component inventory + +| Layer | Current implementation | Primary locations | Assessment | +| --- | --- | --- | --- | +| Public and account UI | React 18, React Router 6, mixed JS/TS, Create React App | `src/App.jsx`, `src/pages`, `src/components` | Functional, but the build stack and several dependencies are stale. | +| Identity | Firebase email/password Auth and custom role claims | `src/services/identity`, `functions/signup.js`, `functions/setMemberRole.js` | Reasonable base; admin assurance and role-change audit controls need strengthening. | +| Operational data | Cloud Firestore | `src/services`, `firestore.rules`, `firestore.indexes.json` | Appropriate for current scale; counters and state transitions require transactional design. | +| Server API | First-generation Firebase callable/HTTP/trigger functions | `functions/` | Prototype covers most workflows; validation, idempotency, and isolation are incomplete. | +| Payments | Stripe Checkout Sessions, Payment Links, refunds, signed webhook | `functions/createCheckoutSession.js`, `createMerchCheckout.js`, `stripeWebhook.js` | Not ready for live payments until P0 issues are complete. | +| Hosting | Netlify currently answers `runmprc.com`; GitHub Actions also publishes a separate Pages copy | `netlify.toml`, `.github/workflows/deploy.yml`, `public/404.html` | Split and drifting as verified 2026-07-12. A Pages success does not prove the live Netlify site changed. Hosting authority, preview, DNS, headers, and rollback remain open CI/WEB work. | +| Email | Firestore `mail` outbox designed for the Firebase Trigger Email extension | `functions/sendConfirmationEmail.js` | Extension/provider deployment is unverified; outbox creation is not transactionally idempotent and HTML needs escaping. | +| Observability | Optional Sentry and Firebase Analytics | `src/services/monitoring`, `src/services/analytics` | Local initialization is disabled in the working tree; hosted policy/config is unverified and no payment-specific alerts, metrics, or reconciliation dashboard exists. | +| Third-party fitness | Strava OAuth tokens and statistics | `functions/strava.js`, `src/services/strava` | Functional prototype. Working-tree Rules deny browser token access, but transactional refresh, scopes/revocation, IAM/encryption decision, and audit remain OAUTH-001. | + +The repository workflow presents GitHub Pages plus Firebase as a release path, but public DNS and response headers show that the live custom domain is served by Netlify. The two copies currently contain different bundles. The Firebase job can also finish green while explicitly skipping deployment when `FIREBASE_SERVICE_ACCOUNT` is absent. Treat Pages publication, Netlify production, and Firebase deployment as separate evidence until one hosting/release authority is chosen. The App Engine synchronization script is another surface that must be documented as active or retired. + +## 4. Target deployment topology + +The target keeps React, Firebase, and Stripe, but places stronger boundaries around them. Migrating the frontend from GitHub Pages to Firebase Hosting is recommended before live commerce because it supports controlled SPA rewrites, preview channels, and security headers. That hosting migration is not required to design or test the backend, and must be executed as its own issue. + +```mermaid +flowchart TB + subgraph Client["Untrusted client boundary"] + Web["React web application"] + end + + subgraph Edge["Owned web edge"] + Hosting["Firebase Hosting or equivalent CDN\nTLS, CSP, HSTS, SPA rewrites"] + AppCheck["Firebase App Check\nreCAPTCHA Enterprise"] + end + + subgraph Identity["Identity boundary"] + FirebaseAuth["Firebase Authentication"] + Claims["Server-managed role claims"] + end + + subgraph Application["Trusted application boundary"] + PublicAPI["Public callable functions\nvalidation, rate limits, App Check"] + AdminAPI["Admin callable functions\nAuth, role, recent-auth policy"] + Webhook["Stripe webhook ingress\nsignature verification only"] + Worker["Idempotent payment processor\nand reconciliation jobs"] + Mailer["Transactional email outbox"] + end + + subgraph Data["Data boundary"] + Firestore["Firestore domain records"] + EventInbox["Stripe event inbox"] + Audit["Append-oriented audit events"] + Secrets["Google Secret Manager"] + end + + subgraph Payment["Payment boundary"] + Checkout["Stripe-hosted Checkout"] + StripeAPI["Stripe API and event destination"] + end + + Web --> Hosting + Web --> FirebaseAuth + Web --> AppCheck + Web --> PublicAPI + Web --> AdminAPI + PublicAPI --> Firestore + AdminAPI --> Firestore + PublicAPI --> StripeAPI + AdminAPI --> StripeAPI + StripeAPI --> Checkout + StripeAPI --> Webhook + Webhook --> EventInbox + EventInbox --> Worker + Worker --> Firestore + Worker --> Audit + Worker --> Mailer + PublicAPI --> Secrets + AdminAPI --> Secrets + Webhook --> Secrets + FirebaseAuth --> Claims +``` + +## 5. Trust boundaries and authorization + +| Boundary | Trusted assertions | Never trust directly | +| --- | --- | --- | +| Browser | A Firebase ID token after SDK/server verification; an App Check token after Firebase verification | Price, member status, registration status, order status, redirect query parameters, product availability, capacity, inventory, or admin UI visibility | +| Firebase Auth | UID, verified token claims, token timestamps | A role copied into Firestore or sent in the request body | +| Stripe webhook | Event payload only after raw-body signature verification with the endpoint's secret | An unsigned request, a browser success redirect, or a client-provided Session ID without server retrieval/validation | +| Firestore client SDK | Reads and writes allowed by the deployed ruleset | The presence of a hidden UI control as authorization | +| Cloud Functions/Admin SDK | Application code and IAM-scoped service identity | Firestore rules as protection; Admin SDK bypasses those rules | +| GitHub Actions | Pinned workflow and environment-scoped secrets | Pull-request code with production secrets or a shared, long-lived deployment token | + +### Roles + +The current `unverified`, `member`, and `admin` claims should evolve toward capabilities rather than one all-powerful role. A practical first split is: + +| Capability | Example users | Allowed operations | +| --- | --- | --- | +| `member` | Verified club member | Members-only content and member price | +| `event_manager` | Race director | Event content, roster, comps, substitutions; no global member roles or merchandise refunds | +| `shop_manager` | Merchandise lead | Catalog, orders, tracking, fulfillment; no event or membership administration | +| `finance_admin` | Treasurer | Refunds, reconciliation, disputes, reports | +| `identity_admin` | Membership lead | Membership verification and role administration | +| `platform_admin` | Very small break-glass group | Security configuration and role grants | + +The first release may continue using `admin` in the UI, but server endpoints and Firestore rules should be narrowed by resource now so future capability claims can be introduced without rewriting payment logic. + +## 6. Domain model and ownership + +### Current collections + +| Path | Purpose | Source of writes | Sensitivity | +| --- | --- | --- | --- | +| `events/{eventId}` | Event content, schedule, pricing configuration, capacity, waiver version | Admin client today; target event-management API for sensitive fields | Public or members-only | +| `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}/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 | +| `ratelimits/{bucket}` | Abuse-control counters | Cloud Functions | Confidential operational data | +| `mail/{id}` | Transactional email outbox | Cloud Functions and email extension | Restricted PII | + +### Target additions and changes + +| Path or field | Purpose | +| --- | --- | +| `stripeEvents/{stripeEventId}` | Durable, non-PII webhook inbox/deduplication record with processing status and business reference | +| `checkoutRequests/{idempotencyKeyHash}` | Stable client request mapping and payload fingerprint for retry-safe Session creation | +| `auditEvents/{eventId}` or bounded per-record audit subcollections | Append-oriented operational and security audit trail without unbounded arrays | +| `events/{id}.capacityCounters` | Transactionally maintained participant reservations, paid seats, and released seats | +| `products/{id}/variants/{variantId}` | SKU, option values, price, on-hand, reserved, and sold counts | +| `orders.paymentStatus` and `orders.fulfillmentStatus` | Separate money state from physical fulfillment state | +| `registrations.paymentStatus` and `registrations.registrationStatus` | Separate payment lifecycle from attendance/transfer/cancellation lifecycle | +| `retentionJobs/{jobId}` | Optional operational record of scheduled minimization/deletion work | + +Large `auditLog` arrays on registration and order documents should be replaced before they approach Firestore document-size and write-contention limits. New audit data should be append-oriented. + +## 7. Business invariants + +The following are correctness rules, not UI preferences: + +1. `amountExpectedCents`, `currency`, and sellable item are read from server-controlled data. +2. `amountPaidCents` comes from a verified Stripe object and is stored separately from expected list price. +3. A record becomes paid only when `payment_status == paid` or an equivalent successful PaymentIntent state is verified. +4. Each Stripe event is applied at most once; applying it again produces the same final state and no duplicate email, seat, stock decrement, or refund. +5. One checkout request maps to one business record and at most one active Stripe Session. +6. A member price requires a currently verified `member` or authorized admin claim at checkout time. +7. Participant reservations never exceed event capacity. Volunteers do not consume participant capacity unless an event explicitly configures a volunteer cap. +8. Variant reservations never exceed sellable on-hand inventory. A product-wide `active` flag does not imply every size/color is available. +9. Full refunds, terminal cancellations, and expired unpaid Sessions release capacity or inventory exactly once according to policy. +10. Partial refunds do not silently release a seat or fulfilled product. +11. A paid record cannot be changed to cancelled without an explicit policy decision about refunding or retaining funds. +12. Financial state cannot be edited directly from a Firestore client. +13. A waiver record identifies the exact waiver version/text hash accepted, timestamp, event, registrant, and acceptance context. +14. Confirmation pages display only sanitized fields and do not rely on bearer credentials left in URLs or referrer logs. + +## 8. Core workflows + +### 8.1 Paid race registration + +```mermaid +sequenceDiagram + actor R as Runner + participant W as Web app + participant F as Registration function + participant D as Firestore + participant S as Stripe + participant H as Webhook processor + + R->>W: Submit identity, waiver acceptance, request ID + W->>F: Callable request + Auth/App Check when available + F->>D: Transaction: validate event snapshot, reserve capacity, create pending registration + F->>S: Create Checkout Session with stable idempotency key and business metadata + F->>D: Attach Session ID and expiry + F-->>W: Stripe-hosted Checkout URL + W->>S: Redirect to Checkout + S-->>H: Signed Checkout event + H->>D: Atomic dedupe + amount/currency/reference validation + state transition + H->>D: Enqueue confirmation email exactly once + S-->>W: Redirect using Session ID only + W->>F: Request sanitized checkout result + F->>S: Retrieve/verify Session if reconciliation is needed + F-->>W: Confirmed, processing, failed, or support-required state +``` + +If Stripe Session creation fails, the function releases the reservation in a compensating transaction. If the function crashes after Stripe creates the Session, retrying the same request returns the same Session through Stripe and local idempotency keys. A scheduled job releases abandoned reservations and reconciles records that missed a webhook. + +### 8.2 Free or volunteer registration + +The same server validation and Firestore transaction are used, but no Stripe call occurs. The registration moves directly to its confirmed non-payment state. Anonymous confirmation uses a short-lived or hash-stored opaque receipt token that is removed from browser history immediately; signed-in users can use ownership by UID. + +### 8.3 Merchandise purchase + +Merchandise uses the same checkout saga but reserves a specific SKU/variant. Stripe shipping collection is copied into the order only after a verified paid event. Payment status and fulfillment status remain separate. A paid order may be `unfulfilled`, `packed`, `shipped`, `delivered`, `cancelled`, or `returned` without corrupting the payment ledger. + +### 8.4 Refund + +An authorized finance action creates a refund request with a stable idempotency key. Stripe executes the money movement; the webhook is the canonical confirmation. The local record may show `refund_pending` while waiting. Repeated clicks or network retries must return the original refund rather than create another one. A full registration refund may release capacity according to event policy; a merchandise refund does not modify inventory until the return/stock disposition is explicitly recorded. + +### 8.5 Webhook processing + +The ingress verifies method, raw payload, signature, and secret. Relevant event types are written or claimed using the Stripe Event ID. Business processing validates object type, livemode/environment, metadata schema version, business reference, Session/PaymentIntent ownership, currency, and amount. Unknown transitions are quarantined for review. Stripe does not guarantee event order, so each transition is based on current domain state and the retrieved Stripe object when necessary. + +## 9. Consistency and failure model + +Stripe and Firestore cannot share a distributed transaction. Checkout and refunds are therefore explicit sagas: + +- A Firestore transaction protects local uniqueness and scarce-resource counters. +- A stable Stripe idempotency key protects external creation. +- The business record stores the saga step and external ID. +- A compensating transaction releases a reservation after a known failure. +- A webhook advances successful external state. +- A scheduled reconciler repairs missed, delayed, or out-of-order outcomes. +- Operators receive alerts for records that remain in intermediate or review states beyond their service-level threshold. + +Returning a `2xx` response to Stripe means the event has been durably accepted or safely processed. A transient storage failure returns `5xx` so Stripe retries. A permanent validation anomaly is durably quarantined and acknowledged so it does not create an endless retry storm. + +## 10. Environment model + +| Environment | Firebase | Stripe | Domain | Data policy | +| --- | --- | --- | --- | --- | +| Local | Firebase Emulator Suite | Stripe CLI and test objects | `localhost` | Synthetic data only; no production network fallback | +| CI | Ephemeral emulators and mocks | Stripe fixtures/signature tests; optional isolated test account | None | Synthetic data, no production secrets | +| Staging | Dedicated non-production Firebase project | Stripe sandbox/test keys and its own webhook endpoint | `dev.runmprc.com` | Synthetic or consented test records only | +| Production | Dedicated production Firebase project | Live restricted keys and production webhook secret | `runmprc.com` | Real data under documented retention and access policies | + +`SITE_ORIGIN`, Firebase project IDs, App Check keys, Stripe keys, webhook endpoints, Sentry environments, and email providers must be environment-scoped. Live and test webhook secrets are never interchangeable. The current repository points to one Firebase project and needs an explicit staging project before end-to-end rollout. + +## 11. Security, privacy, and compliance posture + +Hosted Checkout reduces MPRC's card-data scope because payment details are entered on Stripe's domain, but it does not make the surrounding system automatically secure or compliant. MPRC still owns access control, application security, privacy disclosures, waiver evidence, fulfillment, refunds, disputes, incident response, vendor configuration, and data retention. + +Before launch, MPRC leadership must approve: + +- Terms, privacy policy, refund/cancellation policy, fulfillment/shipping policy, and waiver language reviewed by appropriate counsel. +- Stripe account ownership, MFA, least-privilege team roles, bank/payout controls, Radar policy, support contacts, and webhook ownership. +- Tax nexus and sales-tax handling for merchandise and race fees. +- Record-retention periods for financial records, waiver evidence, emergency contacts, birth dates, shipping addresses, support logs, and analytics. +- A named incident commander and a way to disable checkout quickly. + +Technical controls and the current findings are detailed in [SECURITY.md](./SECURITY.md). This repository documentation is engineering guidance, not legal, tax, insurance, or PCI certification advice. + +## 12. Reliability and service objectives + +Initial objectives should be modest and measurable: + +| Objective | Initial target | +| --- | --- | +| Checkout API availability | 99.9% during open registration windows, excluding Stripe/Firebase outages | +| Webhook durable acceptance | 99% under 10 seconds; 99.9% under 60 seconds | +| Paid-to-confirmed propagation | 99% under 60 seconds for immediate methods | +| Duplicate fulfillment/refund | Zero tolerated | +| Capacity or inventory oversell caused by application race | Zero tolerated | +| Reconciliation backlog | No unresolved paid/missing-local or local-paid/missing-Stripe item older than 15 minutes during launches | +| Critical security alert response | Acknowledged within 30 minutes during an active paid event window | + +These targets require structured logs, alerting, Stripe delivery monitoring, a reconciliation job, and a documented on-call owner. See [OPERATIONS_RUNBOOK.md](./OPERATIONS_RUNBOOK.md). + +## 13. Deployment and migration strategy + +Use expand-and-contract changes so the deployed frontend and backend remain compatible: + +1. Add new fields, endpoints, ledgers, and state handling while continuing to read old records. +2. Deploy backend functions, rules, and indexes before clients that depend on them. +3. Backfill counters, state fields, and business references with a dry-run report and an idempotent migration. +4. Switch new checkout creation to the new path in Stripe test mode. +5. Exercise webhook retries, duplicates, out-of-order events, refunds, expired Sessions, and reconciliation. +6. Run a limited live pilot with one low-risk product or capped event. +7. Observe and reconcile before broad launch. +8. Remove legacy status fields and direct-write permissions only after all active records and clients have migrated. + +Never test migration or reconciliation logic for the first time against production records. + +## 14. Architecture decisions + +| ID | Decision | Rationale | Status | +| --- | --- | --- | --- | +| ADR-001 | Use Stripe-hosted Checkout rather than custom card UI | Keeps raw card data out of MPRC systems and uses Stripe's optimized payment surface | Accepted | +| ADR-002 | Keep Firebase/Firestore for the first commerce release | Existing implementation and operational scale fit; changing databases would add risk without fixing payment correctness | Accepted | +| ADR-003 | Treat Stripe webhooks plus reconciliation as payment truth | Redirects are lossy and spoofable; webhooks are retryable but can be delayed or duplicated | Accepted | +| ADR-004 | Use application idempotency records and Stripe idempotency keys | Required for safe retries across the Firestore/Stripe boundary | Accepted | +| ADR-005 | Atomically reserve capacity and SKU inventory before Checkout | Prevents overselling under concurrent requests | Accepted | +| ADR-006 | Separate payment state from registration/fulfillment state | Avoids impossible overloaded states such as a fulfilled order becoming merely `refunded` | Proposed for phased migration | +| ADR-007 | Move financial and secret mutations behind narrow functions | The current admin catch-all is too broad for least privilege and auditability | Accepted target | +| ADR-008 | Launch with card-class immediate methods only unless delayed methods are fully tested | Simplifies initial fulfillment while the webhook still handles asynchronous outcomes safely | Proposed; finance/product approval required | +| ADR-009 | Prefer Firebase Hosting for the commerce frontend | Controlled rewrites, preview environments, and response headers are operationally safer than the current GitHub Pages fallback | Proposed | + +## 15. Open product and governance decisions + +The following cannot be safely decided from code alone and are tracked as launch-gate issues: + +- Who owns Stripe, Firebase/GCP, DNS, GitHub, email delivery, and Sentry accounts? +- Who may grant roles, issue refunds, view participant PII, export rosters, and access payouts? +- Are guest registrations allowed for every race, and which fields are truly required? +- Does a refund release a race spot automatically? What happens after bib assignment or an event cutoff? +- Are transfers/substitutions allowed, and how is waiver acceptance re-collected from the substitute? +- Will merchandise be shipped, picked up, or both? Which countries and tax jurisdictions are supported? +- Are discounts required at launch? If so, who can create them and how are they represented in reporting? +- What is the retention period for waiver evidence and accounting records, and when are high-risk operational fields deleted? +- What uptime and response coverage will exist during an event launch? + +Until these decisions are recorded, implementation should use the safest narrow behavior: no unverified member discount, no inventory below zero, no paid-to-cancelled transition without an explicit refund decision, no delayed fulfillment on an unpaid Session, no live promotion codes, and no broad export or secret access. diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index fd707f2..072c2d1 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -1,5 +1,7 @@ # Architecture Overview +> **Historical overview—do not execute commands from this file:** this file predates significant account, registration, Stripe, merchandise, admin, security-rule, and operations work. Use [`SYSTEM_DESIGN.md`](../SYSTEM_DESIGN.md) and the other root design documents for the current/target architecture and launch gates. Some Firebase/secret commands below are unsafe or retired and remain only as project history. + ## Technology Stack | Layer | Technology | diff --git a/docs/CONTENT_GUIDE.md b/docs/CONTENT_GUIDE.md index 259bb72..e3c494b 100644 --- a/docs/CONTENT_GUIDE.md +++ b/docs/CONTENT_GUIDE.md @@ -1,143 +1,47 @@ -# Content Management Guide - -This guide explains how to update content on the MPRC website. - -## Updating Text Content - -All text content is centralized in the `src/text/` directory for easy updates. - -### File Locations - -| File | Content | -|------|---------| -| `Home.js` | Home page text | -| `JoinUs.js` | Join Us page, membership info, benefits | -| `Activities.js` | Activities page content | -| `Committee.js` | Committee page intro text | -| `ContactUs.js` | Contact page content | -| `Footer.js` | Footer text and copyright | -| `externalLinks.js` | External URLs (forms, social media) | - -### Example: Update Membership Fees - -Edit `src/text/JoinUs.js`: - -```javascript -export const MEMBERSHIP_STANDARD_INDIVIDUAL = 'Individual membership: $25'; -export const MEMBERSHIP_STANDARD_HOUSEHOLD = 'Household membership: $30'; -``` - -## Updating Officers - -### 1. Add Officer Photos - -Place photos in `src/images/committee/`: -- Recommended size: 400x400px or similar square ratio -- Formats: .jpg, .jpeg, .png -- Naming: lowercase, no spaces (e.g., `john_doe.jpg`) - -### 2. Update Committee.jsx - -Edit `src/pages/officers/Committee.jsx`: - -```javascript -// Add import for new photo -const OfficerNewPerson = require('../../images/committee/new_person.jpg'); - -// Add to officers array -const officers = [ - { - id: 1, - image: OfficerNewPerson, - name: 'New Person', - job: 'President', - }, - // ... other officers -]; -``` - -### 3. Update Structured Data - -Also update the `structuredData` object in the same file for SEO. - -## Updating External Links - -Edit `src/text/externalLinks.js`: - -```javascript -export const GOOGLE_FORM_LINK = 'https://docs.google.com/forms/...'; -export const RENEWAL_FORM_2026_LINK = 'https://docs.google.com/forms/...'; -export const WAIVER_FORM_LINK = 'https://docs.google.com/forms/.../viewform?embedded=true'; -``` - -**Note**: For embedded forms (like the waiver), add `?embedded=true` to the URL. - -## Updating Images - -### Image Locations - -| Directory | Purpose | -|-----------|---------| -| `src/images/home/` | Home page images | -| `src/images/about/` | About page images | -| `src/images/activities/` | Activities page images | -| `src/images/committee/` | Officer photos | -| `src/images/contact/` | Contact page images | -| `src/images/joinus/` | Join Us page images | - -### Adding New Images - -1. Add image file to appropriate directory -2. Import in the component: - ```javascript - import NewImage from '../../images/section/new_image.jpg'; - ``` -3. Use in JSX: - ```jsx - Description - ``` - -## Updating the Waiver Form - -The waiver uses an embedded Google Form. To change: - -1. Create/update your Google Form -2. Get the embed URL (Send > Embed icon > copy `src` URL) -3. Update in `src/text/externalLinks.js`: - ```javascript - export const WAIVER_FORM_LINK = 'https://docs.google.com/forms/d/e/YOUR_FORM_ID/viewform?embedded=true'; - ``` - -## Adding New Pages - -1. Create page component in `src/pages/newpage/NewPage.jsx` -2. Add route in `src/App.jsx`: - ```jsx - } /> - ``` -3. Add navigation link in `src/data.jsx` -4. Create text file `src/text/NewPage.js` for content - -## SEO Updates - -Each page has SEO configuration via the `` component: - -```jsx - -``` - -Update structured data for rich search results (Google knowledge panels, etc.). - -## Testing Changes - -1. Run `npm start` to test locally -2. Check all affected pages -3. Test on mobile viewport (responsive design) -4. Run `npm run build` to verify production build works +# Website Content Source Map + +**For club officers:** use [OFFICER_START_HERE.md](../OFFICER_START_HERE.md) and [Update public content](./officers/UPDATE_PUBLIC_CONTENT.md). Do not edit these files or run deployment commands yourself. + +**For AI agents and maintainers:** this page identifies likely source locations. Inspect the current page before editing because not all text is centralized. + +## Public page sources + +| Public area | Main source locations | Important note | +| --- | --- | --- | +| Home | `src/text/Home.js`, `src/pages/home/Home.jsx`, `src/images/home/` | Most prose is centralized. | +| About | `src/pages/about/About.jsx`, `src/images/about/` | Inspect the component; not every word is in `src/text/`. | +| Join Us | `src/text/JoinUs.js`, `src/pages/joinUs/`, `src/text/externalLinks.js`, `src/images/joinus/` | Includes membership, waiver, form, map, and price-like wording. Requires owner review. | +| Activities | `src/text/Activities.js`, `src/pages/activities/Activities.jsx`, `src/images/activities/` | Significant text, videos, and layout are hardcoded in the component. | +| Events | Firestore event records plus `src/pages/events/` | Admin saves can affect production data immediately. Live behavior is not verified. | +| Shop | Firestore product records plus `src/pages/shop/` | Live commerce is not approved. | +| Committee | `src/text/Committee.js`, `src/pages/officers/Committee.jsx`, `src/images/committee/` | Names/titles and duplicate search metadata live in the component. | +| Contact | `src/text/ContactUs.js`, `src/pages/contact/Contact.jsx`, `src/images/contact/` | Some text and email parts are hardcoded. | +| Footer | `src/text/Footer.js`, footer components | Legal/privacy-style wording requires an approved source. | +| Forms, map, and social links | `src/text/externalLinks.js` | Use public view/submission links, never private edit links. | +| Terms and Privacy | `src/pages/legal/` | Do not ask AI to invent policy. Require approved owner text. | +| Navigation | `src/data.jsx`, `src/App.jsx` | Route changes require the page map and officer guide to change. | + +## Safe maintainer workflow + +1. Read the officer's one-line request and identify the approving role. +2. Inspect the live page and every likely source listed above. +3. Open or use one claimed issue and one focused branch. +4. Change the smallest possible source area. +5. Update search metadata, links, image descriptions, and duplicated visible text when applicable. +6. Run focused checks and a production build without exposing secrets or using production data. +7. Provide a preview or redacted screenshot. +8. Update the affected officer task guide and map. +9. Obtain approval before merge. +10. Follow [Publish and check](./officers/PUBLISH_AND_CHECK.md); do not call a green workflow “live.” + +## Images + +- Prefer JPG or PNG files sized for the page. +- Use lowercase names without spaces. +- Obtain permission before publishing a person's photo. +- Provide meaningful alternative text. +- Remove private metadata and avoid names, addresses, badges, plates, or private screens in the image. + +## Stop conditions + +Stop and escalate if the content changes a price, capacity, discount, waiver, Terms, Privacy, member benefit/access, payment, refund, tax, insurance, retention rule, or private data. Those require the owners and evidence in [Events, shop, members, and money](./officers/EVENTS_SHOP_MEMBERS.md). diff --git a/docs/DEVELOPER_GUIDE.md b/docs/DEVELOPER_GUIDE.md index 8386875..bb329c3 100644 --- a/docs/DEVELOPER_GUIDE.md +++ b/docs/DEVELOPER_GUIDE.md @@ -1,144 +1,13 @@ -# Developer Guide +# Developer Guide — Retired -## Prerequisites +The former guide described an older branch, deployment, Firebase, and dependency workflow. Its commands are unsafe or inaccurate for the current platform, so they are no longer active instructions. -- Node.js v20+ -- npm v9+ -- Git -- Firebase CLI (`npm install -g firebase-tools`) +Use these current sources instead: -## Getting Started +1. [README.md](../README.md) for supported setup and checks. +2. [AGENTS.md](../AGENTS.md) for repository safety and issue workflow. +3. [SYSTEM_DESIGN.md](../SYSTEM_DESIGN.md) for current and target architecture. +4. [OPERATIONS_RUNBOOK.md](../OPERATIONS_RUNBOOK.md) for technical operations. +5. [OFFICER_START_HERE.md](../OFFICER_START_HERE.md) for nontechnical continuity. -### 1. Clone the Repository - -```bash -git clone https://github.com/Run-MPRC/Run-MPRC.github.io.git -cd Run-MPRC.github.io -``` - -### 2. Install Dependencies - -```bash -# Install frontend dependencies -npm install - -# Install Cloud Functions dependencies -cd functions && npm install && cd .. -``` - -### 3. Start Development Server - -```bash -npm start -``` - -The site will be available at http://localhost:3000 - -### 4. Start Firebase Emulators (Optional) - -For testing Firebase Auth and Firestore locally: - -```bash -firebase emulators:start -``` - -Emulator ports: -- Auth: http://localhost:9099 -- Firestore: http://localhost:8080 -- Functions: http://localhost:5001 - -## Project Structure - -``` -Run-MPRC.github.io/ -├── src/ -│ ├── components/ # Reusable UI components -│ ├── pages/ # Page-level components -│ ├── services/ # Firebase, auth, and utility services -│ ├── text/ # Text content (easy to update) -│ ├── images/ # Image assets organized by section -│ └── index.css # Global styles -├── functions/ # Firebase Cloud Functions -├── public/ # Static files (index.html, sitemap, etc.) -└── docs/ # Documentation -``` - -## Available Scripts - -| Command | Description | -|---------|-------------| -| `npm start` | Start development server | -| `npm run build` | Create production build | -| `npm run lint:fix` | Fix linting issues | -| `npm test` | Run tests | -| `npm run deploy` | Deploy to GitHub Pages | - -## Git Workflow - -### Branches - -- `main` - Production branch (deploys to runmprc.com) -- `dev` - Development branch (deploys to dev.runmprc.com) - -### Making Changes - -1. Create feature branch from `dev` -2. Make changes and test locally -3. Commit with descriptive message -4. Push and create PR to `dev` -5. After testing on dev site, merge `dev` to `main` - -## Deployment - -### Automatic Deployment - -- Pushing to `main` triggers deployment to production -- Pushing to `dev` triggers deployment to dev site - -### Manual Deployment - -```bash -# Deploy to GitHub Pages -npm run deploy - -# Deploy Firebase Functions -cd functions && npm run deploy -``` - -## Firebase Configuration - -### Setting API Keys (Cloud Functions) - -```bash -firebase functions:config:set api.key="your-secret-key" -``` - -### Firestore Security Rules - -Rules are defined in `firestore.rules` and deployed with: - -```bash -firebase deploy --only firestore:rules -``` - -## Troubleshooting - -### Port 3000 in use - -```bash -lsof -i :3000 -kill -9 -``` - -### Firebase emulator issues - -```bash -firebase emulators:start --clear-data -``` - -### Build failures - -```bash -rm -rf node_modules package-lock.json -npm install -``` +The old document remains available in Git history for archaeology. Do not copy commands from an old revision without revalidating them against the current repository and claimed issue. diff --git a/docs/LLM_CONTEXT.md b/docs/LLM_CONTEXT.md index df6f9db..95931f6 100644 --- a/docs/LLM_CONTEXT.md +++ b/docs/LLM_CONTEXT.md @@ -1,279 +1,15 @@ -# LLM Context Guide +# LLM Context Guide — Retired -This document provides comprehensive context for AI assistants working with the MPRC website codebase. +This snapshot is retired because it described an older code, hosting, branch, and Firebase model. -## Project Overview +AI agents must read, in order: -**Project**: Mid-Peninsula Running Club (MPRC) Website -**Type**: React Single Page Application with Firebase Backend -**Purpose**: Club website with member authentication, events, and content management -**Live URL**: https://runmprc.com -**Dev URL**: https://dev.runmprc.com -**Repository**: https://github.com/Run-MPRC/Run-MPRC.github.io +1. [AGENTS.md](../AGENTS.md). +2. [OFFICER_START_HERE.md](../OFFICER_START_HERE.md) when an officer or operational workflow is affected. +3. The exact claimed GitHub issue. +4. The relevant root design, security, implementation, or runbook document. +5. Current code and tests; documentation is not proof that behavior is deployed. -## Tech Stack Summary +Every handoff must separate source changes, tests, merge state, website publication, `runmprc.com` verification, Firebase deployment, and outside-provider verification. Never summarize those states as one green or “done” status. -- **Frontend**: React 18.2.0, React Router 6.21.3, TypeScript (partial) -- **Styling**: Tailwind CSS + Custom CSS (index.css) -- **Backend**: Firebase 10.7.1 (Auth, Firestore, Analytics, Cloud Functions) -- **Build**: Create React App (react-scripts 5.0.1) -- **Linting**: ESLint with Airbnb config -- **Deployment**: GitHub Pages, Netlify (alternative) - -## Directory Structure - -``` -src/ -├── components/ # Reusable UI components (18 files) -│ ├── index.ts # Barrel export for all components -│ ├── Navbar.jsx # Navigation bar -│ ├── Footer.jsx # Site footer -│ ├── Header.jsx # Page headers with background image -│ ├── SEO.jsx # Meta tags and structured data -│ ├── Card.jsx # Content card wrapper -│ ├── Officer.jsx # Officer profile card -│ └── ... -│ -├── pages/ # Route-level page components -│ ├── home/Home.jsx -│ ├── about/About.jsx -│ ├── activities/Activities.jsx -│ ├── events/Events.tsx # TypeScript, uses Firestore -│ ├── joinUs/ -│ │ ├── JoinUs.jsx # Main join page -│ │ ├── JoinUsConditionalRoute.jsx # Waiver gate -│ │ ├── Waiver.jsx # Embedded Google Form waiver -│ │ └── Route.jsx # Leaflet map component -│ ├── officers/Committee.jsx # Officer listing -│ ├── contact/Contact.jsx -│ ├── login/LoginForm.jsx -│ ├── admin/Admin.jsx # Placeholder admin panel -│ └── notFound/NotFound.jsx -│ -├── services/ # Business logic and integrations -│ ├── index.ts # Barrel export -│ ├── ServiceLocatorContext.ts # DI context with hooks -│ ├── ServiceLocatorProvider.tsx # DI provider -│ ├── firebase/ -│ │ └── FirebaseResources.ts # Firebase SDK singleton -│ ├── identity/ -│ │ └── Identity.ts # Auth service -│ ├── hooks/ -│ │ ├── index.ts -│ │ └── useAuth.ts # Auth state hook -│ └── seo/ -│ ├── index.ts -│ └── structuredData.ts # SEO schema builders -│ -├── text/ # Centralized text content -│ ├── index.ts # Barrel export -│ ├── Home.js -│ ├── JoinUs.js # Membership info, benefits -│ ├── Activities.js -│ ├── Committee.js -│ ├── ContactUs.js -│ ├── Footer.js -│ └── externalLinks.js # URLs for forms, social media -│ -├── images/ # Image assets by section -│ ├── home/ -│ ├── about/ -│ ├── activities/ -│ ├── committee/ # Officer photos -│ ├── contact/ -│ ├── joinus/ -│ └── navbar/ -│ -├── App.jsx # Root component with routing -├── index.js # Entry point -└── index.css # Global styles and CSS variables - -functions/ # Firebase Cloud Functions -├── index.js # Function exports -├── signup.js # Create member on auth signup -├── updatemembers.js # Batch update member roles -└── package.json -``` - -## Key Patterns - -### Service Locator Pattern - -Services are initialized once and provided via React Context: - -```typescript -// Access services in components -const { services, isReady } = useServiceLocator(); -if (!isReady) return ; -const { identityService, firebaseResources } = services; -``` - -### Available Hooks - -```typescript -import { useServiceLocator } from '../services/ServiceLocatorContext'; -import { useAuth } from '../services/hooks/useAuth'; - -// useServiceLocator - raw service access -const { services, isReady } = useServiceLocator(); - -// useAuth - simplified auth state -const { user, isLoading, isMember, isAdmin, signIn, signOut, register } = useAuth(); -``` - -### Text Content Pattern - -All user-facing text is in `src/text/` for easy updates: - -```javascript -// src/text/JoinUs.js -export const MEMBERSHIP_STANDARD_INDIVIDUAL = 'Individual membership: $25'; - -// Usage in component -import { MEMBERSHIP_STANDARD_INDIVIDUAL } from '../../text/JoinUs'; -``` - -### SEO Pattern - -Every page includes SEO component with structured data: - -```jsx - -``` - -## Authentication & Authorization - -### User Roles (Custom Claims) - -- `unverified` - New user, not yet verified -- `member` - Verified club member -- `admin` - Full administrative access - -### Role Check Methods - -```typescript -// IdentityService methods -await identityService.checkMembership(); // true if member or admin -await identityService.checkAdmin(); // true if admin only -``` - -### Protected Content - -Members-only content is: -1. Stored in Firestore `members_only` collection -2. Filtered by `member_only` flag on events -3. Gated by `MembersOnly` component - -## Firestore Collections - -| Collection | Purpose | Access | -|------------|---------|--------| -| `members` | User profiles | Admin only | -| `events` | Club events | Public (filtered) | -| `members_only` | Private content | Members only | - -## Cloud Functions - -### createMemberOnSignUp - -Triggered on Firebase Auth user creation: -- Creates member document in Firestore -- Sets `role: 'unverified'` custom claim - -### updateMemberRole - -HTTP endpoint to batch update member roles: -- Requires API key (from Firebase config) -- Updates custom claims and Firestore docs - -## Common Tasks - -### Adding a New Page - -1. Create `src/pages/newpage/NewPage.jsx` -2. Add route in `src/App.jsx` -3. Add text in `src/text/NewPage.js` -4. Add to navigation in `src/data.jsx` - -### Updating Officers - -1. Add photo to `src/images/committee/` -2. Update `src/pages/officers/Committee.jsx`: - - Add require() for image - - Update officers array - - Update structuredData - -### Updating Membership Fees - -Edit `src/text/JoinUs.js`: -- `MEMBERSHIP_EARLY_BIRD_*` constants -- `MEMBERSHIP_STANDARD_*` constants -- `LI_AFFORDABLE_MEMBERSHIP_FEES` - -### Changing External Links - -Edit `src/text/externalLinks.js`: -- Form links, social media URLs, etc. - -## File Naming Conventions - -- **Components**: PascalCase (`Navbar.jsx`, `SEO.jsx`) -- **Pages**: PascalCase in directories (`Home.jsx`) -- **Services**: PascalCase (`Identity.ts`) -- **Text files**: PascalCase (`JoinUs.js`) -- **CSS**: lowercase (`navbar.css`, `joinUs.css`) -- **Images**: lowercase with underscores (`header_bg_1.jpg`) - -## Import Conventions - -```javascript -// Use barrel exports where available -import { useAuth } from '../services'; -import { SEO, Header, Card } from '../components'; -import { GOOGLE_FORM_LINK, ARM_URI } from '../text'; - -// Direct imports when needed -import FirebaseResources from '../services/firebase/FirebaseResources'; -``` - -## Testing Locally - -```bash -npm start # Start dev server (port 3000) -firebase emulators:start # Start Firebase emulators -npm run build # Test production build -npm run lint:fix # Fix linting issues -``` - -## Deployment - -- Push to `dev` branch → deploys to dev.runmprc.com -- Push to `main` branch → deploys to runmprc.com -- Manual: `npm run deploy` for GitHub Pages - -## Known Considerations - -1. **Mixed JS/TS**: Codebase uses both JavaScript and TypeScript -2. **Firebase Config**: API keys are in code (Firebase keys are safe to expose) -3. **CSS Approach**: Mix of Tailwind utilities and custom CSS -4. **Image Optimization**: Images are not automatically optimized -5. **Waiver Flow**: Uses localStorage to track waiver acknowledgment - -## Quick Reference - -| Task | File(s) to Edit | -|------|-----------------| -| Update text content | `src/text/*.js` | -| Update officers | `src/pages/officers/Committee.jsx` | -| Update external links | `src/text/externalLinks.js` | -| Add new route | `src/App.jsx` | -| Modify auth logic | `src/services/identity/Identity.ts` | -| Update waiver form | `src/text/externalLinks.js` (WAIVER_FORM_LINK) | -| Modify Firebase functions | `functions/*.js` | -| Update global styles | `src/index.css` | +The former snapshot is available in Git history if historical comparison is needed. Do not execute its old commands. diff --git a/docs/PROPOSAL_MEMBER_FEATURES.md b/docs/PROPOSAL_MEMBER_FEATURES.md index 72c7772..5e69074 100644 --- a/docs/PROPOSAL_MEMBER_FEATURES.md +++ b/docs/PROPOSAL_MEMBER_FEATURES.md @@ -1,5 +1,7 @@ # Proposal: Newsletter & Member Login Features +> **Historical proposal—do not use as current-state or security guidance:** deployment claims, effort estimates, role assumptions, and “no additional security concerns” language below predate the current platform assessment. Use the repository-root system design, security plan, issue backlog, and operations runbook. Do not execute or implement this proposal without a current scoped issue and privacy/security review. + ## Executive Summary This document outlines options for implementing newsletter subscriptions and enhancing the existing member login system. The good news: **significant infrastructure already exists** in the codebase. diff --git a/docs/README.md b/docs/README.md index 1cc1648..c1d8efe 100644 --- a/docs/README.md +++ b/docs/README.md @@ -2,21 +2,29 @@ Welcome to the Mid-Peninsula Running Club website documentation. +> **Start here:** club officers and backup maintainers should use the root [Officer Start Here](../OFFICER_START_HERE.md) and the short [officer handbook](./officers/README.md). The remaining files in this folder are technical or historical context. + ## Documentation Index -### For Humans -- [Developer Guide](./DEVELOPER_GUIDE.md) - Setup, development workflow, and deployment -- [Content Management](./CONTENT_GUIDE.md) - How to update text, images, and officers -- [Architecture Overview](./ARCHITECTURE.md) - System design and component structure +### For Club Officers + +- [Officer handbook](./officers/README.md) — request, preview, approval, publishing proof, access, and emergencies +- [Content source map](./CONTENT_GUIDE.md) — technical reference for AI/maintainers; officers should request changes rather than edit code + +### Technical and Historical + +- [Retired Developer Guide](./DEVELOPER_GUIDE.md) — redirect only; old commands are available in Git history +- [Architecture Overview](./ARCHITECTURE.md) — historical component overview; see root system design for current architecture ### For AI/LLMs -- [LLM Context Guide](./LLM_CONTEXT.md) - Comprehensive codebase context for AI assistants + +- [Retired LLM Context Guide](./LLM_CONTEXT.md) — redirect to current `AGENTS.md` and root designs ### Diagrams -- [Waiver Flow Diagram](./diagrams/waiver-flow.md) - State machine for the waiver process +- [Waiver Flow Diagram](./diagrams/waiver-flow.md) - Legacy Join Us content gate, not race-registration waiver evidence ## Quick Links - **Live Site**: https://runmprc.com -- **Dev Site**: https://dev.runmprc.com +- **Preview status**: `dev.runmprc.com` exists but is stale/untrusted until hosting consolidation is complete - **Repository**: https://github.com/Run-MPRC/Run-MPRC.github.io diff --git a/docs/diagrams/waiver-flow.md b/docs/diagrams/waiver-flow.md index 097cf3b..e876bde 100644 --- a/docs/diagrams/waiver-flow.md +++ b/docs/diagrams/waiver-flow.md @@ -1,5 +1,7 @@ # Waiver Flow State Machine +> **Legacy Join Us content gate only:** this diagram describes a browser `localStorage` acknowledgement around an embedded membership Google Form. It is not proof of race-waiver acceptance and must never gate or fabricate race eligibility. Race registration requires immutable, versioned, truthful evidence under [DATA-002](../../GITHUB_ISSUES.md) and the root [commerce design](../../STRIPE_COMMERCE_DESIGN.md), subject to LEGAL-001 owner approval. + ## Overview The waiver flow gates access to the Join Us page content. Users must acknowledge the waiver (via embedded Google Form) before viewing membership information. diff --git a/docs/officers/ACCESS_CONTINUITY.md b/docs/officers/ACCESS_CONTINUITY.md new file mode 100644 index 0000000..4c6030d --- /dev/null +++ b/docs/officers/ACCESS_CONTINUITY.md @@ -0,0 +1,95 @@ +# Access Continuity + +**Purpose:** ensure at least two named officers can reach every important service if the usual owner is unavailable. +**Status:** setup is **INCOMPLETE** until the entry document is in a board-owned Drive and two backup officers complete a sign-in drill. +**Approver:** club president plus the officer responsible for the service. + +## Entry document and private locator + +A private Google Doc titled **MPRC Website — Officer Start Here** has been created and connector-readback verified. Its private link is intentionally absent from this public repository. Metadata currently reports owner-only access, so backup-officer sharing is not complete. It contains no passwords or member/payment data. + +Owner action still required: + +1. Create a board-owned Google Drive folder named **Website Continuity**. +2. Move the entry document into that folder. +3. Share the folder with at least two current officers. +4. Confirm both officers can open the document using their own accounts. +5. Add the approved password-manager vault name to the private document. +6. Do not add a password, recovery code, API key, or private member/payment record. + +Until those six steps are recorded, this guide does not claim incapacitation coverage is complete. + +## Before you start + +- Select two individual officers; do not share Dave's login. +- Select the club-approved password manager. +- Require two-person approval for owner, billing, recovery, or security changes. + +## Systems to cover + +| System | Why it matters | Minimum backup | +| --- | --- | --- | +| GitHub `Run-MPRC` organization | Source, issues, reviews, workflows | Two individual organization owners with secure two-factor authentication | +| Netlify | Current live website host | Two team owners; site and branch recorded | +| Domain registrar and DNS | Controls `runmprc.com` | Two owners; renewal and recovery email verified | +| Firebase / Google Cloud | Login, database, Functions, rules | Two limited-access administrators | +| Stripe | Payments, refunds, disputes, payouts | Treasurer plus finance backup; strong two-factor authentication | +| Google Workspace / Drive / Forms | Club documents and forms | Two Workspace/Drive owners | +| Club email provider | Notices and password recovery | Communications owner plus backup | +| Social/community accounts | Public communication and member groups | Named owner plus backup | + +## Private service record + +For each system, record only: + +- Public sign-in URL. +- Primary officer role. +- Backup officer role. +- Club-owned account email. +- Password-manager vault name. +- Last successful sign-in date. +- Billing or renewal owner. +- Approved removal/recovery procedure location. +- Two-factor method type: passkey, authenticator, or hardware key. + +## Quarterly check + +1. Ask the primary owner to sign in. +2. Ask the backup owner to sign in. +3. Record the date of both successful checks. +4. Confirm recovery information is stored in the password manager. +5. Confirm billing and renewal notices reach a club-owned address. +6. List former officers, unused integrations, and old tokens for review. +7. Have two owners approve any removal. +8. Test the service-specific recovery or rollback procedure before removing access. +9. Confirm GitHub, Netlify, DNS, and Firebase agree on the intended production path. + +## Expected result and success proof + +- Two current officers can open the private entry document. +- Two current officers can sign in to every system with their own accounts. +- The password manager, billing owner, and recovery procedure are named without exposing their contents. +- The drill date and approving officers are recorded privately. + +## Stop conditions + +Stop if a service has only one owner, uses Dave's personal recovery channel, has no tested rollback, or asks for a password/code in a shared document. Do not remove an account, token, integration, fork, or billing method until two owners complete a dependency check. + +## Undo and recovery + +If an access change blocks a valid officer, stop further removals. Use the service's approved recovery procedure with two owners. Restore only the minimum former access, record why, and schedule a new least-access review. + +## Escalation + +- GitHub, Netlify, Firebase, DNS, or security: platform owner plus backup. +- Stripe, billing, refunds, or payouts: treasurer plus finance backup. +- Google Drive or club email: Workspace/communications owner plus backup. +- No reachable owner: club president initiates provider account recovery; do not create an unofficial shared login. + +## Special GitHub note + +`Run-MPRC` is the club organization. `runmprc` is a separate personal user and stale fork owner. Do not retire it from this guide alone. First open a claimed cleanup issue, inventory outside dependencies, prepare rollback, and obtain two-owner approval. + +## Annual tabletop exercise + +Ask a backup officer who did not write these guides to request one harmless text change without Dave's help. Record every confusing step and update the guides before the exercise is closed. diff --git a/docs/officers/EMERGENCY_AND_RECOVERY.md b/docs/officers/EMERGENCY_AND_RECOVERY.md new file mode 100644 index 0000000..1ef71cc --- /dev/null +++ b/docs/officers/EMERGENCY_AND_RECOVERY.md @@ -0,0 +1,73 @@ +# Emergency and Recovery + +**Purpose:** preserve evidence and reach the right owners when the site is down, wrong, unsafe, or exposing private information. +**Approver for production action:** platform owner plus one backup; add the privacy owner for private data and the treasurer for money. +**Expected first result:** the problem is recorded, no additional risky changes are made, and the correct owners are responding. + +## Before you start + +- Use your own officer account. +- Open the private continuity record if you can. +- Do not open or copy private records merely to investigate. + +## First five minutes + +1. Stop making changes. +2. Write down the time. +3. Copy the affected public URL. +4. Save one screenshot with names, emails, codes, and private details covered. +5. Describe the visitor action that exposed the problem. +6. Contact the platform owner. +7. Contact the platform backup. +8. Add the privacy owner or treasurer when the table below requires it. + +## Choose the symptom + +| Symptom | Safe first response | +| --- | --- | +| 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. | +| 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. + +## 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. + +## Stop conditions + +- Stop if anyone asks for a password, code, secret, private record, or payment record in chat. +- Stop if a proposed fix changes several services at once. +- Stop if rollback, backup, owner, or affected production surface is unknown. +- Stop if the only available action is a force-push, database deletion, direct payment-state edit, or unreviewed DNS change. + +## Success proof + +Record: + +- What happened and when. +- Who approved each response action. +- The issue, pull request, commit, or provider change. +- What was checked on `runmprc.com`. +- Whether Netlify identified the intended commit. +- Whether Firebase actually deployed. +- Whether each outside provider was checked. +- What follow-up prevents the same problem. + +## Undo and restoration + +Do not undo containment merely because the page looks normal. The same two owners must verify the cause is removed, affected services agree, and monitoring is stable. Restore one surface at a time and record the result. + +## Escalation + +- Public website or deployment: platform owner plus backup. +- Private member information: platform owner plus privacy owner. +- Login, role, or admin access: identity owner plus platform/security backup. +- Payment, refund, order, payout, or dispute: treasurer plus platform owner. +- Secret or account takeover: owning service's two owners; use provider support if recovery fails. + +After recovery, update the matching officer guide while the lesson is fresh. diff --git a/docs/officers/EVENTS_SHOP_MEMBERS.md b/docs/officers/EVENTS_SHOP_MEMBERS.md new file mode 100644 index 0000000..1423cc4 --- /dev/null +++ b/docs/officers/EVENTS_SHOP_MEMBERS.md @@ -0,0 +1,90 @@ +# Events, Shop, Members, and Money + +**Current status:** live commerce is **NOT AVAILABLE YET**. +**Use this guide when:** a request touches registrations, members-only access, products, payments, refunds, waivers, or private data. + +**Prerequisites:** a claimed issue, named business owner and backup, approved policy/value, isolated test data, and a platform specialist. +**Expected result:** a reviewed plan or test-only demonstration; no real payment, member, registration, order, or production-data change. + +The repository contains screens for these jobs. Their presence does not prove the full system is safely configured or live. + +There is currently no proven no-code switch that safely stops all new Stripe payments. Hiding a button, closing an event, or marking a product sold out may leave an already-created Stripe Checkout page payable. + +## Approval table + +| Change | Required owners | +| --- | --- | +| Public event description only | Event lead + communications lead | +| Registration dates or capacity | Event lead + platform lead | +| Price, discount, tax, product, inventory, refund, or payout | Treasurer + platform lead | +| Member access or admin role | Membership lead + platform/security lead | +| Waiver, Terms, Privacy, retention, insurance, or consent | Club officer + approved legal/privacy owner | +| Stripe, Firebase, Netlify, domain, email, or secret | Named service owner + backup | + +## Safe request process + +1. Open a GitHub issue before changing anything. +2. Name the business owner and backup owner. +3. Write the exact approved policy or value. Do not ask AI to invent it. +4. Ask AI to list every affected screen, data record, email, report, and outside service. +5. Require a test-only demonstration with made-up people and Stripe test mode. +6. Require negative tests: who must be denied, what retries, and what happens if a step fails. +7. Review the preview and the simple data/deployment diagram. +8. Require a rollback or safe roll-forward plan. +9. Require staging evidence before any production approval. +10. Approve a small, named live pilot only after the security launch gates are closed. + +## Do not do these jobs manually + +- Do not change Firestore records to “paid.” +- Do not grant admin access from the database console. +- Do not paste live Stripe or Firebase keys into code or chat. +- Do not delete registrations, orders, webhook events, or audit records to fix a display. +- Do not issue a refund in both Stripe and the website unless the approved procedure explicitly requires it. +- Do not open registration or sales because a screen appears to work locally. + +## What proof is required + +- Tests used fake people and test-mode payments. +- The exact commit and pull request are named. +- The website deployment is verified separately. +- Firebase logs prove Functions/rules/indexes deployed; no step says “skipping Firebase deploy.” +- Stripe or another provider is verified directly when involved. +- Counts and money reconcile after the change. +- The named officer signs off. + +If any proof is missing, report the change as **not live**. + +## 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. + +Before an officer click guide may be added, a claimed issue must prove all of the following with made-up data in an isolated staging project: + +1. Only the intended officer role can open and save each screen. +2. A draft stays private to ordinary visitors. +3. A second officer can preview without changing production. +4. Backup and restore are tested. +5. Every field has an approved owner and validation rule. +6. Publishing requires a separate, explicit approval. +7. Closing an event or product has a documented effect on existing Checkout Sessions. +8. A real no-code checkout kill switch is implemented and tested. +9. Audit records show who changed what and when. +10. The rollback procedure is tested before production access is granted. + +Until that issue closes, request event/product changes through [Request a change](./REQUEST_A_CHANGE.md). Use a reviewed pull request or a specialist-run, test-only demonstration; do not enter real members, registrations, products, prices, or payment details. + +## Stop conditions + +Stop if staging is not isolated, the owner/policy is missing, a test uses real people or money, Firebase deployment skipped, rollback is untested, or the requested action directly edits payment/member state. + +## Undo + +Because this guide authorizes no production write, the safe undo is to close or revise the issue before release. If production data changed unexpectedly, stop and use [Emergency and recovery](./EMERGENCY_AND_RECOVERY.md); do not delete or overwrite the record. + +## Escalation + +- Event/capacity: event lead plus platform owner. +- Member/admin access: membership lead plus identity/security owner. +- Price/order/refund/Stripe: treasurer plus platform owner. +- Waiver/Terms/Privacy/retention: club officer plus approved legal/privacy owner. diff --git a/docs/officers/GLOSSARY.md b/docs/officers/GLOSSARY.md new file mode 100644 index 0000000..0cf9def --- /dev/null +++ b/docs/officers/GLOSSARY.md @@ -0,0 +1,34 @@ +# Plain-Language Glossary + +| Word | Plain meaning | +| --- | --- | +| AI assistant | A tool that can read instructions and help prepare a change. It still needs review and approval. | +| GitHub | The service that stores the website's source and change history. | +| Repository | The MPRC website project stored in GitHub. | +| Issue | A tracked request describing one problem or result. | +| Branch | A separate working copy used to prepare one change safely. | +| Commit | A named snapshot of source changes. It helps identify exactly which version was built. | +| Pull request | A review page showing a proposed change before it joins the main version. | +| Review | A person or review agent checks the change for mistakes and risk. | +| Merge | GitHub accepts an approved pull request into `main`. This does not always mean the change is live. | +| Publish or deploy | A service receives a new version. Each service must be checked separately. | +| Production or live | The version real visitors or officers actually use. | +| Frontend | The pages people see in a browser. | +| Backend | Private services behind the pages, including login, data, and server actions. | +| Firebase | The service used for login, database records, and backend Functions. | +| Firestore | The database inside Firebase. Saving an Admin form can change its records immediately. | +| Function | A protected backend action, such as checking access or handling a payment event. | +| Netlify | The service currently answering requests for `runmprc.com`. | +| GitHub Pages | A second website copy built from GitHub. It is not currently the custom-domain production copy. | +| Stripe | The outside payment service. MPRC live commerce is not approved yet. | +| Secret | A password-like value, key, token, or recovery code that must not be put in source, issues, AI, email, or screenshots. | +| Token | A password-like value that lets a person or service act. Treat it as a secret. | +| Service account | A non-human account used by automation. Its key is a secret and must have limited access. | +| Two-factor authentication (2FA) | A second sign-in check, preferably a passkey, authenticator, or hardware key rather than a text message. | +| Least privilege | Giving a person or service only the access needed for its job. | +| Staging | A separate safe environment used to test before production. MPRC does not yet have a proven isolated staging environment. | +| Index | A database helper that makes an approved query work. It is not the website home page. | +| Test mode | A safe provider mode that uses fake payments and test data. | +| Rollback or revert | A reviewed change that restores the last known-good version. | +| DNS | The settings that direct `runmprc.com` to a website host. DNS changes can take the whole site offline. | +| CI or workflow | Automated checks and build jobs in GitHub. Green means those jobs passed, not necessarily that production changed. | diff --git a/docs/officers/PUBLISH_AND_CHECK.md b/docs/officers/PUBLISH_AND_CHECK.md new file mode 100644 index 0000000..3399eff --- /dev/null +++ b/docs/officers/PUBLISH_AND_CHECK.md @@ -0,0 +1,93 @@ +# Review, Merge, and Check a Change + +**Use this when:** a preview is approved and someone asks whether the change is live. +**Approver:** content owner for ordinary content; specialist owners listed in [Events, shop, members, and money](./EVENTS_SHOP_MEMBERS.md) for higher-risk work. + +**Independent officer publication status:** **NOT AVAILABLE YET.** A platform maintainer must own the merge and live verification until the Netlify repository, branch, owner, build, preview, and rollback path are recorded and tested. + +## Current deployment truth + +As of **2026-07-12**: + +- Merging to `main` starts GitHub workflows. +- Merging therefore also authorizes an automatic GitHub Pages publication; there is no separate Pages approval today. +- The GitHub repository currently opens stale, unprotected `dev` by default; use the explicit `main` branch and do not merge `dev` into `main` as a shortcut. +- GitHub Pages builds a copy of the website. +- `runmprc.com` is currently served by Netlify, not that Pages copy. +- The Firebase step can say success while skipping deployment when its service-account secret is absent. +- Therefore, a green workflow does not prove the public site or backend changed. + +## Before merge + +1. Open the pull request and confirm its destination says `main`. +2. Confirm it names one issue and one outcome. +3. Confirm another person or review agent approved it. +4. Confirm required tests are green without “ignored” failures. +5. Confirm the officer guide was updated when needed. +6. Read the rollback note. +7. Confirm you understand that a merge automatically publishes the GitHub Pages copy. +8. Ask the platform maintainer whether any outside automation may also publish from the merge. +9. Do not merge if either automatic publication is unacceptable or unknown. + +## Netlify publication — NOT AVAILABLE YET + +Before a backup officer can publish independently, a claimed hosting issue must record and test: + +1. The exact Netlify team and site name. +2. The two officer accounts that own it. +3. The connected GitHub repository and branch. +4. The event that starts a production deploy. +5. The build command and public configuration names. +6. A safe preview URL for the intended commit. +7. Where Netlify displays the deployed commit. +8. How to restore the previous known-good deploy. +9. Which DNS records point `runmprc.com` to Netlify. +10. A dated drill proving the procedure without changing member or payment data. + +## After merge + +1. Record the pull request number and merged commit. +2. Wait for the GitHub workflow to finish. +3. Read the job details, not only the green summary. +4. Look for “skipping Firebase deploy.” If present, mark the backend **not deployed**. +5. Ask the platform maintainer which commit Netlify says it deployed. +6. Open [runmprc.com](https://runmprc.com) in a private/incognito window. +7. Visit the exact changed page directly. +8. Check the requested text, link, image, or behavior. +9. Check one phone-sized view. +10. Check one normal computer view. +11. If Firebase or an outside service changed, ask its owner for separate dated proof. +12. Record the result using the checklist below. + +## Delivery record + +```text +Issue: +Pull request: +Merged commit: +Tests passed: +GitHub Pages published: yes / no / not relevant +Netlify intended commit verified: yes / no / unknown +runmprc.com verified: yes / no +Firebase deployed: yes / no / not relevant +Outside provider configured: yes / no / not relevant +Outside provider verified: yes / no / not relevant +Production behavior verified: yes / no +Checked by: +Checked at (date and time): +Known remaining problem: +``` + +## If the change is not on the live site + +1. Do not repeatedly merge or redeploy. +2. Save the live URL, time, and a redacted screenshot. +3. Ask AI to compare the merged version, GitHub Pages copy, and Netlify copy. +4. Treat the problem as a hosting/deployment issue. +5. Do not change DNS until the Netlify site, owner, branch, environment, and rollback are confirmed. + +## Undo + +Ask for a revert pull request. Do not force-push, reset shared branches, delete data, or make a second unrelated change while the first problem is being investigated. + +**Escalation:** platform owner and backup for website hosting; Firebase owner for backend changes; treasurer plus platform owner for commerce. diff --git a/docs/officers/README.md b/docs/officers/README.md new file mode 100644 index 0000000..7af855a --- /dev/null +++ b/docs/officers/README.md @@ -0,0 +1,61 @@ +# Officer Website Handbook + +**Audience:** MPRC officers and backup maintainers with little or no coding experience +**Last checked:** 2026-07-12 +**Start page:** [OFFICER_START_HERE.md](../../OFFICER_START_HERE.md) + +The safe pattern is always the same: + +```mermaid +flowchart LR + A["Officer describes the result"] --> B["AI opens one tracked change"] + B --> C["AI shows preview and checks"] + C --> D{"Approve merge and its automatic deployment attempt?"} + D -- "No" --> B + D -- "Yes" --> E["Merged to main"] + E --> P["Automatic Pages publication and Firebase attempt"] + P --> F["Check the real site and services"] + F --> G["Record proof and close"] +``` + +In words: ask and preview first; approving a merge also authorizes today's automatic deployment attempt; then check the real result on every affected service. + +## Short guides + +1. [Request a change](./REQUEST_A_CHANGE.md) +2. [Update public content](./UPDATE_PUBLIC_CONTENT.md) +3. [Events, shop, members, and money](./EVENTS_SHOP_MEMBERS.md) +4. [Publish and check](./PUBLISH_AND_CHECK.md) +5. [Emergency and recovery](./EMERGENCY_AND_RECOVERY.md) +6. [Access continuity](./ACCESS_CONTINUITY.md) +7. [Simple system maps](./SYSTEM_MAPS.md) +8. [Plain-language glossary](./GLOSSARY.md) + +## Five rules + +1. Describe the result. Do not guess which file or service to edit. +2. Ask for a preview. Do not approve a change you cannot see. +3. Approve one step at a time. A `main` merge automatically publishes GitHub Pages today; live Netlify and Firebase still need separate proof. +4. Check the real website or service. A green check is not the same as “live.” +5. Stop when money, private data, account access, legal wording, or security is involved. + +## Words every delivery report must use + +| Status | What it proves | +| --- | --- | +| Drafted | A change exists only in a working copy. | +| Tests passed | Automated checks passed for the named change. | +| Merged | GitHub accepted the code into `main`. | +| Pages published | GitHub automatically built its website copy after merge. | +| Netlify commit verified | The live host identifies the intended merged commit. | +| Website verified | The exact result was seen on `runmprc.com`. | +| Firebase deployed | The backend deployment actually ran; it did not skip. | +| Provider verified | Stripe, Netlify, DNS, Google, or another outside service was checked directly. | + +Never shorten several of these states to “done.” + +Independent officer publishing to the live Netlify host is **NOT AVAILABLE YET**. Use a platform maintainer until the Netlify connection and rollback path are documented and tested. + +## Current safety warning + +The repository contains race, shop, registration, payment, refund, and admin work that is **not approved for live commerce**. Do not enter live Stripe keys or accept real payments until the launch blockers in the root security documents are closed and officers approve a controlled launch. diff --git a/docs/officers/REQUEST_A_CHANGE.md b/docs/officers/REQUEST_A_CHANGE.md new file mode 100644 index 0000000..ef2b4d5 --- /dev/null +++ b/docs/officers/REQUEST_A_CHANGE.md @@ -0,0 +1,72 @@ +# Request a Website Change + +**Use this when:** you know what should look or behave differently. +**Approver:** the officer responsible for that content; a second approver for money, legal, privacy, access, or security. + +## Before you start + +Have one of these ready: + +- The exact new wording. +- A public link that should replace an old link. +- An approved photo and permission to publish it. +- A screenshot with private information covered. +- A plain description of what a visitor should be able to do. + +## The one-line request + +> Please update the MPRC website so **[desired result]** on **[page or area]** by **[date, if any]**. Read `OFFICER_START_HERE.md` and `AGENTS.md`, show a preview and proof, update officer documentation, and do not publish or use secrets or real member/payment data until **[approver]** approves. + +## Steps + +1. Open an AI assistant that can read the MPRC repository. +2. Paste the one-line request. +3. Attach only public or redacted source material. +4. Ask the AI to repeat the request in plain words. +5. Confirm what must change and what must stay the same. +6. Require one GitHub issue and one small pull request based on `main`. +7. Ask for the affected page link and a preview or clear before/after view. +8. Review spelling, dates, links, mobile layout, and unintended changes. +9. If the preview is correct, ask a platform maintainer to explain which automatic builds a merge will start. +10. Approve the merge only if those automatic builds are also acceptable. +11. Use [Publish and check](./PUBLISH_AND_CHECK.md) before calling the change live. + +If you open GitHub yourself, use the [canonical repository on `main`](https://github.com/Run-MPRC/Run-MPRC.github.io/tree/main), then select **Issues**. The normal repository landing page currently opens stale `dev`; do not use `dev` as the source for a new request. + +GitHub automatically publishes a Pages copy after a `main` merge. Other outside automation may also react to a merge. There is no safe “merge but guarantee nothing publishes” option in the current workflow. + +## If you cannot open an AI assistant + +1. Sign in to GitHub with your own officer account, not Dave's account. +2. Open the [canonical repository on `main`](https://github.com/Run-MPRC/Run-MPRC.github.io/tree/main). +3. Select **Issues**. +4. Select **New issue**. +5. Choose **Officer website change request** if that choice appears. +6. Paste the one-line request. +7. Assign the issue to an approved maintainer or ask the platform backup to do so. + +A screenshot guide for a chosen AI tool is **NOT AVAILABLE YET** because the board has not selected one supported tool/account path for backup officers. Do not share Dave's login as a workaround. + +## What the AI must return + +- A one-sentence result. +- A link to the issue and pull request. +- The pages or services affected. +- The checks that passed. +- The officer guide that changed, or a plain reason none changed. +- Separate answers for merged, website live, Firebase live, and outside-service verified. +- A safe undo plan. + +## Stop and ask for a specialist if + +- The request changes a price, payment, refund, payout, capacity, or inventory. +- The request changes a waiver, Terms, Privacy notice, tax, insurance, or retention rule. +- The request changes a member role, account owner, secret, domain, or security rule. +- The AI asks for a password, code, secret, private member record, or payment record. +- The AI proposes a direct production edit, force-push, broad merge, or skipped test. + +## If the result is wrong + +Do not keep making random edits. Ask: + +> Prepare a small revert pull request for the last approved change. Do not force-push, delete records, change DNS, or deploy until an officer reviews the rollback. diff --git a/docs/officers/SYSTEM_MAPS.md b/docs/officers/SYSTEM_MAPS.md new file mode 100644 index 0000000..d71690b --- /dev/null +++ b/docs/officers/SYSTEM_MAPS.md @@ -0,0 +1,93 @@ +# Simple System Maps + +These diagrams show where pages, information, and deployments go. Each diagram includes a one-sentence text version. + +## Public page map + +```mermaid +flowchart TD + Site["runmprc.com"] --> Home["Home"] + Site --> About["About"] + Site --> Join["Join Us"] + Site --> Activities["Activities"] + Site --> Events["Events"] + Events --> Calendar["Calendar"] + Events --> Event["Event details"] + Event --> Registration["Race registration — not approved for live payments"] + Site --> Shop["Shop — not approved for live payments"] + Site --> Committee["Committee"] + Site --> Contact["Contact Us"] + Site --> Account["Login and Account"] + Site --> Legal["Terms and Privacy"] + Account --> Admin["Restricted /admin pages — direct link"] +``` + +In words: public information, account/admin pages, and unfinished commerce screens share one website; seeing a screen does not mean it is approved for live use. + +## Where information lives + +```mermaid +flowchart LR + Public["Public text and photos in GitHub"] --> Build["Website build"] + Build --> Visitor["Website visitor"] + Officer["Authorized officer"] --> Admin["Restricted admin pages"] + Visitor --> Auth["Firebase login"] + Admin --> Auth + Auth --> Data["Firebase database"] + Admin --> Functions["Firebase Functions"] + Visitor --> Forms["External Google Forms and public links"] + Functions -. "future live commerce only" .-> Stripe["Stripe"] +``` + +In words: public content comes from GitHub; private accounts and operational records use Firebase; Google Forms are separate; Stripe must remain test-only until approved. + +## How a change reaches people today + +```mermaid +flowchart TD + PR["Approved pull request"] --> Main["Merge to main"] + Main --> Workflow["GitHub workflow"] + Workflow --> Pages["GitHub Pages copy"] + Workflow --> Firebase{"Firebase credential present?"} + Firebase -- "No" --> Skip["Backend deploy skipped"] + Firebase -- "Yes and job succeeds" --> Backend["Firebase deployed"] + Main -. "connection and trigger not verified" .-> Netlify + Netlify["Netlify deployment — current live host"] --> Live["runmprc.com"] + Pages -. "currently not the live custom-domain copy" .-> Live + Dev["dev — stale default branch"] -. "do not use for new release work" .-> PR +``` + +In words: GitHub can report success while the live Netlify site stays old or Firebase is skipped, so each surface needs separate proof. + +## Account and permission ownership + +```mermaid +flowchart TD + Board["MPRC board"] --> People["At least two named officers"] + People --> GitHub["Individual GitHub accounts"] + People --> Netlify["Individual Netlify accounts"] + People --> Cloud["Scoped Firebase and cloud accounts"] + People --> Finance["Scoped Stripe finance accounts"] + People --> Domain["Domain and DNS access"] + Vault["Approved password manager"] --> Recovery["Recovery location — no secrets in guides"] + Recovery --> People + AI["Approved AI tool"] --> GitHub +``` + +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. + +## Emergency decision + +```mermaid +flowchart TD + Problem["Problem noticed"] --> Private{"Private data, access, money, or secret involved?"} + Private -- "Yes" --> Stop["Stop changes and contact specialist owners"] + Private -- "No" --> Public{"Only wrong public content?"} + Public -- "Yes" --> Revert["Prepare one reviewed revert"] + Public -- "No" --> Host["Check Netlify, GitHub Pages, and Firebase separately"] + Stop --> Verify["Preserve redacted evidence and verify recovery"] + Revert --> Verify + Host --> Verify +``` + +In words: stop and escalate anything involving privacy, access, money, or secrets; otherwise use one reviewed rollback and check every affected service. diff --git a/docs/officers/UPDATE_PUBLIC_CONTENT.md b/docs/officers/UPDATE_PUBLIC_CONTENT.md new file mode 100644 index 0000000..7ad15d4 --- /dev/null +++ b/docs/officers/UPDATE_PUBLIC_CONTENT.md @@ -0,0 +1,72 @@ +# Update Public Text, Links, Photos, or Officers + +**Use this when:** the change is public information and does not affect money, private data, access, legal wording, or security. +**Approver:** communications lead or the officer who owns the page. + +**Before you start:** have exact approved wording, a public link, or an approved photo; know the page and intended date. +**Expected result:** one reviewed public-content change with no account, data, payment, policy, or security effect. + +## Text + +1. Copy the current sentence from the live page. +2. Write the replacement sentence. +3. Name the page and heading where it appears. +4. Ask AI to update every matching public description, including search text when relevant. +5. Review the preview on a phone-sized view. +6. Review the preview on a normal computer view. +7. Confirm no price, date, address, or policy changed by accident. + +Helpful request: + +> On the MPRC **[page]**, replace **[old text]** with **[approved new text]**. Keep the rest of the page unchanged and show me the page preview before publishing. + +## Public links and Google Forms + +1. Open the new link in a private/incognito window. +2. Confirm it is a public viewing or submission link. +3. Never send an edit link, owner link, or link containing a private token. +4. Give AI the old public link and the new public link. +5. Ask AI to find every visible place using the old link. +6. Test each changed button from the preview. + +## Photos + +1. Get permission to publish the photo. +2. Confirm the correct name and role of each person shown. +3. Use a clear JPG or PNG; a square photo works best for officers. +4. Remove private location or device information when possible. +5. Tell AI where the photo should appear and provide a short description for screen readers. +6. Review the crop on phone and computer views. + +Do not publish photos of minors, private events, name badges, addresses, license plates, or private screens without specific approval. + +## Officer list + +1. Obtain the approved officer name, title, display order, and photo permission. +2. State the date the change should take effect. +3. Ask AI to update the visible list and search-engine description together. +4. Check spelling, title, photo, order, and old-officer removal. +5. Ask AI to confirm no account permissions changed. Website display and GitHub/Firebase access are separate. + +## Success check + +- The exact change appears on `runmprc.com`. +- Every new link opens correctly without requiring editor access. +- Photos have correct names and descriptions. +- No unrelated page changed. +- The delivery report separates “merged” from “verified live.” + +## Stop here instead + +Use [Events, shop, members, and money](./EVENTS_SHOP_MEMBERS.md) if the change mentions a signup, price, waiver, member benefit, discount, race, product, order, refund, or private page. + +## Undo + +Ask the platform maintainer for one revert pull request. Do not edit `main`, delete content records, change DNS, or bundle a second change into the rollback. + +## Escalation + +- Wording, public link, or approved photo: communications lead. +- Officer name/title: club president or secretary plus communications lead. +- Unexpected layout, deployment, or live-site mismatch: platform owner plus backup. +- Any money, policy, access, privacy, or security effect: stop and use the specialist guide linked above. diff --git a/functions/.env.example b/functions/.env.example new file mode 100644 index 0000000..9b75b76 --- /dev/null +++ b/functions/.env.example @@ -0,0 +1,12 @@ +# Non-secret Functions parameters for local/staging configuration. +# Production values must be environment-scoped and validated. Do not copy this +# file to production without following OPERATIONS_RUNBOOK.md. + +ENVIRONMENT_NAME=local +SITE_ORIGIN=http://localhost:3000 +STRIPE_LIVEMODE_EXPECTED=false +COMMERCE_ENABLED=false + +# Legacy compatibility only. Sensitive callable functions must migrate to the +# native Firebase `enforceAppCheck` runtime option before live commerce. +ENFORCE_APP_CHECK=false diff --git a/functions/.secret.local.example b/functions/.secret.local.example new file mode 100644 index 0000000..343fb4b --- /dev/null +++ b/functions/.secret.local.example @@ -0,0 +1,9 @@ +# Names only for Firebase Emulator Suite local secret overrides. +# Copy to the ignored `functions/.secret.local` and use test/development values. +# Never put live keys here, commit the copied file, or share its contents. + +STRIPE_SECRET_KEY=sk_test_replace_me +# Leave blank until `stripe listen` gives you a temporary test webhook secret. +STRIPE_WEBHOOK_SECRET= +STRAVA_CLIENT_ID=development_client_id +STRAVA_CLIENT_SECRET=development_client_secret diff --git a/netlify.toml b/netlify.toml index 2d228aa..a725bb2 100644 --- a/netlify.toml +++ b/netlify.toml @@ -1,4 +1,13 @@ +[build] + command = "npm run build" + publish = "build" + +[build.environment] + NODE_VERSION = "20" + NPM_FLAGS = "--legacy-peer-deps" + DISABLE_ESLINT_PLUGIN = "true" + [[redirects]] from = "/*" to = "index.html" - status = 200 \ No newline at end of file + status = 200