From 43ffa6c09aae38f7be701d40d75ff0b329d08ce5 Mon Sep 17 00:00:00 2001 From: Dave Liu <7david12liu@gmail.com> Date: Wed, 26 Aug 2026 14:31:50 -0700 Subject: [PATCH] CI-001D3: guard staging profile function deployment --- .github/workflows/ci.yml | 2 +- GITHUB_ISSUES.md | 2 +- IMPLEMENTATION_PLAN.md | 2 + OFFICER_START_HERE.md | 2 +- OPERATIONS_RUNBOOK.md | 6 +- README.md | 7 +- SECURITY.md | 2 +- SYSTEM_DESIGN.md | 8 +- docs/officers/PUBLISH_AND_CHECK.md | 3 +- docs/officers/README.md | 2 +- package.json | 3 + ...base-profile-functions-staging-contract.js | 259 ++++++++++++++++ ...profile-functions-staging-contract.test.js | 286 ++++++++++++++++++ tests/test-artifact-safety.test.js | 9 +- 14 files changed, 578 insertions(+), 15 deletions(-) create mode 100644 scripts/firebase-profile-functions-staging-contract.js create mode 100644 tests/firebase-profile-functions-staging-contract.test.js diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c24e312..5a776a6 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -39,7 +39,7 @@ jobs: ruby -e 'require "yaml"; YAML.safe_load(File.read(".github/workflows/deploy.yml"), permitted_classes: [], permitted_symbols: [], aliases: true)' ruby -e 'require "yaml"; YAML.safe_load(File.read(".github/workflows/verify-staging-authority.yml"), permitted_classes: [], permitted_symbols: [], aliases: true)' ruby -e 'require "yaml"; YAML.safe_load(File.read(".github/workflows/ci.yml"), permitted_classes: [], permitted_symbols: [], aliases: true)' - node --test tests/ci-workflow.test.js tests/release-workflow.test.js tests/firebase-release-verification.test.js tests/firebase-hosting-foundation.test.js tests/firebase-auth-staging-contract.test.js tests/staging-authority-workflow.test.js tests/test-artifact-safety.test.js tests/root-dependency-security.test.js + node --test tests/ci-workflow.test.js tests/release-workflow.test.js tests/firebase-release-verification.test.js tests/firebase-hosting-foundation.test.js tests/firebase-auth-staging-contract.test.js tests/firebase-profile-functions-staging-contract.test.js tests/staging-authority-workflow.test.js tests/test-artifact-safety.test.js tests/root-dependency-security.test.js - name: Build env: CI: 'true' diff --git a/GITHUB_ISSUES.md b/GITHUB_ISSUES.md index df937c6..54ae553 100644 --- a/GITHUB_ISSUES.md +++ b/GITHUB_ISSUES.md @@ -146,7 +146,7 @@ Do not redesign routing or hosting in this issue; WEB-001 owns that. WEB-001A1 [ **Current CI-001C boundary:** CI-001C1 [#667](https://github.com/Run-MPRC/Run-MPRC.github.io/issues/667) supplies the staging-only protected read-only OIDC proof: exact immutable repository/owner IDs, `main`, the `staging` environment, one verifier workflow, two named individual reviewers, and a keyless principal with only project-identity read permission. Parent #133 still owns Firebase deployment roles and production protection; #136 still owns an actual backend release. -**Current CI-001D boundary:** CI-001D1 [#669](https://github.com/Run-MPRC/Run-MPRC.github.io/issues/669) supplies the first staging data boundary: empty Native/Standard `(default)` Firestore in `us-west2`, delete protection enabled, PITR disabled, and exact Rules/indexes from `ee16bd16220ab58bd3a2add80dd2f39a1d514dd7`. The 418-case Rules suite passed, provider definitions matched, fixed anonymous server-only reads were denied, and root collection count remained zero. CI-001D2 [#671](https://github.com/Run-MPRC/Run-MPRC.github.io/issues/671) deploys the email/password-only Auth contract from exact merged source `42542303d043f87a8f1a04be2f0b4f2a88e0318c`. Provider readback and disposable API/browser checks prove password-required sign-in, improved email privacy, only staging domains, no other provider, billing disabled, and final cleanup to zero users/records. Functions, App Check, provider sandboxes, reusable deploy authority, application records, and production behavior remain absent. Parent #133 still owns keyless Firebase deployment roles and production protection; #136 still owns the compatible synthetic backend release. +**Current CI-001D boundary:** CI-001D1 [#669](https://github.com/Run-MPRC/Run-MPRC.github.io/issues/669) supplies the first staging data boundary: empty Native/Standard `(default)` Firestore in `us-west2`, delete protection enabled, PITR disabled, and exact Rules/indexes from `ee16bd16220ab58bd3a2add80dd2f39a1d514dd7`. The 418-case Rules suite passed, provider definitions matched, fixed anonymous server-only reads were denied, and root collection count remained zero. CI-001D2 [#671](https://github.com/Run-MPRC/Run-MPRC.github.io/issues/671) deploys the email/password-only Auth contract from exact merged source `42542303d043f87a8f1a04be2f0b4f2a88e0318c`. Provider readback and disposable API/browser checks prove password-required sign-in, improved email privacy, only staging domains, no other provider, billing disabled, and final cleanup to zero users/records. CI-001D3 [#674](https://github.com/Run-MPRC/Run-MPRC.github.io/issues/674) adds a source-only, argument-closed command for exactly `createMemberOnSignUp` and `ensureMemberProfile`; its tests reject broad/partial targets, another project/account, a credentials file, changed exports/scripts, extra arguments, and an unpinned CLI. Read-only preflight found project Owner authority, billing disabled, and zero open billing accounts visible to the club account. No Function/API/billing/data/provider mutation is part of #674. Functions, App Check, provider sandboxes, reusable deploy authority, application records, and production behavior remain absent. Parent #133 still owns keyless Firebase deployment roles and production protection; #136 still owns the compatible synthetic backend release. ### Problem diff --git a/IMPLEMENTATION_PLAN.md b/IMPLEMENTATION_PLAN.md index 657662d..ed02a32 100644 --- a/IMPLEMENTATION_PLAN.md +++ b/IMPLEMENTATION_PLAN.md @@ -46,6 +46,8 @@ MPRC can open a race or merchandise item for sale only when the platform can: **CI-001D2 staging Auth boundary:** [#671](https://github.com/Run-MPRC/Run-MPRC.github.io/issues/671) adds `emailPassword: true` under the Firebase Auth provider source, preserves the `demo-mprc-local` default alias, and pins a guarded staging-only `--only auth` command to Firebase CLI 15.24.0. The guard requires the exact staging project, quota project, club-account declaration, Auth-only scope, and short-lived token path while rejecting an ADC override. After exact source `42542303d043f87a8f1a04be2f0b4f2a88e0318c` reached `main`, the provider phase enabled password-required email/password only. Readback proved improved email privacy, only the two staging authorized domains, no other sign-in provider, billing disabled, and the instrumentless `IDENTITY_PLATFORM` subtype. Disposable API and browser sign-in/sign-out checks passed, requested no email/SMS, and returned Auth and Firestore to zero users/records. Functions, App Check, role/private-data behavior, reusable deployment authority, and production remain later work. +**CI-001D3 staging profile-Function guard:** [#674](https://github.com/Run-MPRC/Run-MPRC.github.io/issues/674) adds an argument-closed source command for exactly `createMemberOnSignUp` and `ensureMemberProfile`. It validates the demo-only default alias, single reviewed Functions codebase, exact exports, lockfile Firebase CLI 15.24.0, staging project and quota project, club-account declaration, exact target list, short-lived token path, and absence of credential-file overrides before invoking Firebase. Negative tests reject missing/redirected exports, broad or partial target sets, production/cross-project context, extra arguments, changed scripts, and an unpinned CLI. Read-only provider preflight found project Owner authority but billing disabled and zero visible open billing accounts. #674 therefore enables no API, links no billing, deploys no Function, creates no record, and leaves App Check, staging E2E, reusable release authority, and production to later approved children. + ## 3. Dependency map ```mermaid diff --git a/OFFICER_START_HERE.md b/OFFICER_START_HERE.md index 514aa3c..0b644a3 100644 --- a/OFFICER_START_HERE.md +++ b/OFFICER_START_HERE.md @@ -50,7 +50,7 @@ As of **2026-08-13**, a merge runs checks but does not start the GitHub release. As of **2026-08-14**, [#659](https://github.com/Run-MPRC/Run-MPRC.github.io/issues/659) completed one accessibility-only Netlify release. Deploy `6a7ece87c5ca4d0007c1a3fc` is live from frozen source `7496fe0881fb52908c4ff2f40f488df09c94c908`; its exact 62-file marker and signed-out desktop/phone route-focus and menu checks passed. Repause merge `3138a00c1c48e1d5d1dcda0b44722b09a2194ff7` made the manifest inactive. Its attempt `6a7ed0ddb00a46000818878d` published nothing and retained the verified deploy. #623 deploy `6a7e072f8f346b0008510d29` is the recorded rollback, not current production. #659 changed no Firebase, provider configuration, account, sign-in state, production data, payment, or connected directory behavior. Officers do not run commands, sign in, enter data, or approve this as a reusable release. Follow the completed record in [Review, merge, release, and check a change](./docs/officers/PUBLISH_AND_CHECK.md). -As of **2026-08-26**, WEB-001A1 [#663](https://github.com/Run-MPRC/Run-MPRC.github.io/issues/663) is merged, and WEB-001A2 [#665](https://github.com/Run-MPRC/Run-MPRC.github.io/issues/665) created the club-owned `run-mprc-staging` project and published exact source `a614c68d9a1a2be631a3be874a686d61e5d170a0` to [run-mprc-staging.web.app](https://run-mprc-staging.web.app). CI-001C1 [#667](https://github.com/Run-MPRC/Run-MPRC.github.io/issues/667) adds a separate protected read-only identity check: Dave Liu or Jeff Chang reviews exact `main`, and a keyless five-minute identity may read only the staging project identity. It cannot deploy Firebase. CI-001D1 [#669](https://github.com/Run-MPRC/Run-MPRC.github.io/issues/669) adds an empty, delete-protected staging Firestore database and the exact reviewed Rules/index definitions. Anonymous reads of three fixed server-only paths were denied, and the database still had zero root collections after verification. CI-001D2 [#671](https://github.com/Run-MPRC/Run-MPRC.github.io/issues/671) deploys password-required email/password Auth from exact merged source and proves it with disposable API and browser sign-in/sign-out checks. Readback shows improved email privacy, only the two staging domains, no other provider, billing disabled, and zero users/records after cleanup. This remains an **ENGINEERING STAGING SITE ONLY**. It has no trusted Functions, App Check, outside-provider sandbox, protected automatic release, release marker, tested predecessor rollback, complete security-header policy, custom domain, or officer publish control. Officers do not sign in, enter data, run commands, or treat narrow Auth proof as a usable member backend. Netlify still answers `runmprc.com`; production Firebase Hosting remains empty, and no DNS, GitHub Pages, Netlify, production Firebase, production data, or live website behavior changed. +As of **2026-08-26**, WEB-001A1 [#663](https://github.com/Run-MPRC/Run-MPRC.github.io/issues/663) is merged, and WEB-001A2 [#665](https://github.com/Run-MPRC/Run-MPRC.github.io/issues/665) created the club-owned `run-mprc-staging` project and published exact source `a614c68d9a1a2be631a3be874a686d61e5d170a0` to [run-mprc-staging.web.app](https://run-mprc-staging.web.app). CI-001C1 [#667](https://github.com/Run-MPRC/Run-MPRC.github.io/issues/667) adds a separate protected read-only identity check: Dave Liu or Jeff Chang reviews exact `main`, and a keyless five-minute identity may read only the staging project identity. It cannot deploy Firebase. CI-001D1 [#669](https://github.com/Run-MPRC/Run-MPRC.github.io/issues/669) adds an empty, delete-protected staging Firestore database and the exact reviewed Rules/index definitions. Anonymous reads of three fixed server-only paths were denied, and the database still had zero root collections after verification. CI-001D2 [#671](https://github.com/Run-MPRC/Run-MPRC.github.io/issues/671) deploys password-required email/password Auth from exact merged source and proves it with disposable API and browser sign-in/sign-out checks. Readback shows improved email privacy, only the two staging domains, no other provider, billing disabled, and zero users/records after cleanup. CI-001D3 [#674](https://github.com/Run-MPRC/Run-MPRC.github.io/issues/674) adds a code guard for a future deployment of only the two profile Functions. It does not deploy them. Read-only checks found no billing account available to the club account, and Firebase requires pay-as-you-go billing for Functions. This remains an **ENGINEERING STAGING SITE ONLY**. It has no trusted Functions, App Check, outside-provider sandbox, protected automatic release, release marker, tested predecessor rollback, complete security-header policy, custom domain, or officer publish control. Officers do not sign in, enter data, run commands, attach billing, or treat a command guard as a usable member backend. Netlify still answers `runmprc.com`; production Firebase Hosting remains empty, and no DNS, GitHub Pages, Netlify, production Firebase, production data, or live website behavior changed. The optional profile-photo and officer People-finder functions are still **NOT AVAILABLE YET**. #621 makes the frontend default an inert preview: My Account shows the future photo and separate finder-choice controls disabled, while the People finder stays behind the administrator guard and shows its name field and Search button disabled. The preview reads no saved photo or setting, accepts or uploads no photo, searches no name, and saves nothing; it shows no people or sample results. #623 published exactly that disabled interface as deploy `6a7e072f8f346b0008510d29`. Officers inspect the protected layouts only in synthetic local artifacts. The completed signed-out production review proved only the exact revision, normal sign-in and administrator guards, and absence of a member-directory network request. Do not sign in to production, choose a real photo, enter a real name, or treat the preview as a directory. #623 changed no Firebase, provider configuration, account, sign-in, or production data. #507 must later prove the privacy, authorization, staging, backend-first deployment, and readback gates before a separate reviewed source change may connect it. Follow the preview and source-review procedure in [Events, shop, members, and money](./docs/officers/EVENTS_SHOP_MEMBERS.md). diff --git a/OPERATIONS_RUNBOOK.md b/OPERATIONS_RUNBOOK.md index 315e65d..1f0da17 100644 --- a/OPERATIONS_RUNBOOK.md +++ b/OPERATIONS_RUNBOOK.md @@ -63,7 +63,7 @@ Maintain an internal inventory with project/account IDs and console links: | --- | --- | --- | --- | --- | --- | | Local source runtime | `demo-mprc-local` emulators | Providers remain separately gated | `localhost:3000` | None | Available for synthetic Firebase work after all three loopback emulators report ready | | CI | Separate named demo/emulator projects | Fixtures/test only | None | CI | Frontend, Functions, Rules, SPA, and commerce-journal checks run; broader gates remain partial | -| Staging | `run-mprc-staging` | Empty Firestore Rules/index boundary plus deployed password-required email/password Auth; no Functions, App Check, or provider sandbox | `run-mprc-staging.web.app`; `dev.runmprc.com` not configured | `staging`: two reviewers, exact `main`, read-only OIDC verifier | Static Hosting, protected project-identity proof, empty delete-protected Firestore, and disposable Auth proof are available; usable backend/provider staging remains a launch blocker | +| Staging | `run-mprc-staging` | Empty Firestore Rules/index boundary plus deployed password-required email/password Auth; source-only exact two-Function guard; no Functions, App Check, or provider sandbox | `run-mprc-staging.web.app`; `dev.runmprc.com` not configured | `staging`: two reviewers, exact `main`, read-only OIDC verifier | Static Hosting, protected project-identity proof, empty delete-protected Firestore, disposable Auth proof, and Function command guard are available; billing and usable backend/provider staging remain launch blockers | | Production | `mid-peninsula-running-club` (verify) | Live + unique endpoint | `runmprc.com` | `production` | Informational site only until gates close | WEB-001A2 [#665](https://github.com/Run-MPRC/Run-MPRC.github.io/issues/665) created and provider-read back `run-mprc-staging`, one web app, and Hosting version `3ffadcf2ac8cc760` from source `a614c68d9a1a2be631a3be874a686d61e5d170a0` on 2026-08-26. Always name that project explicitly; `.firebaserc` still defaults to the demo namespace. The static host is not complete environment isolation. @@ -76,6 +76,8 @@ CI-001D2 [#671](https://github.com/Run-MPRC/Run-MPRC.github.io/issues/671) first The bounded provider phase ran on 2026-08-26 from exact merged source `42542303d043f87a8f1a04be2f0b4f2a88e0318c` after all five required jobs in CI run `33011780449` passed. Live readback showed the instrumentless `IDENTITY_PLATFORM` subtype with billing disabled; password-required email/password and improved email privacy were enabled; authorized domains were exactly `run-mprc-staging.firebaseapp.com` and `run-mprc-staging.web.app`; and anonymous, phone, native federated, OIDC, and SAML providers were absent. A disposable reserved-domain user passed API create/sign-in, generic invalid-credential, anonymous-denial, and browser sign-in/sign-out checks. No out-of-band email or SMS was requested. Cleanup returned Auth to zero users and Firestore to zero root collections. Hosting remained version `3ffadcf2ac8cc760`; Functions and App Check remained unavailable; production was untouched. This evidence is not a reusable officer deployment control. +CI-001D3 [#674](https://github.com/Run-MPRC/Run-MPRC.github.io/issues/674) adds `npm run deploy:staging-profile-functions` as a source-only, argument-closed wrapper. Before Firebase CLI it checks the demo-only default alias, one reviewed Functions codebase, both exact profile exports, lockfile CLI 15.24.0, `run-mprc-staging` as both project and quota project, `runmprc@gmail.com` as the declared operator, an in-memory short-lived token, no credential-file override, and exactly `functions:createMemberOnSignUp,functions:ensureMemberProfile`. Do not add arguments or invoke the command directly from a branch. Read-only preflight on 2026-08-26 showed the club account is project Owner, project billing is disabled, and that account can see zero open billing accounts. Firebase requires Blaze for Function deployment, so do not enable APIs or run this command until a separately approved billing account, budget/alerts, exact-main source, and provider release issue exist. The guard itself deploys nothing and does not make App Check or staging profile behavior available. + The repository's only `.firebaserc` default is now `demo-mprc-local`, a deliberately non-deployable local namespace. It is an accidental-deploy brake, not staging. Every future deployment must still pass one exact privately approved project explicitly. Do not infer backend/provider isolation or release authority from the static host; #113/#133 must protect, map, and verify those remaining boundaries. ## 4. Configuration and secrets @@ -982,6 +984,8 @@ Never use destructive Git or Firestore reset operations on production as a rollb Issue [#118](https://github.com/Run-MPRC/Run-MPRC.github.io/issues/118) replaces manual repair with create-once signup/recovery Functions and a fail-closed account page. Do not use it as a production procedure until #105 supplies a protected staging and deployment path. +Source guard #674 may be reviewed before billing exists, but it is not deployment evidence. Stop before any provider mutation unless the board/platform owner separately approves a Cloud Billing account and spending controls. A billing link changes the project to pay-as-you-go even when expected Function usage is within no-cost quotas. + Required release order: 1. Confirm the exact merged #100 Rules revision is the reviewed dependency. diff --git a/README.md b/README.md index 3cf316a..d62a078 100644 --- a/README.md +++ b/README.md @@ -43,7 +43,7 @@ Historical developer/content/LLM guides remain under [`docs/`](./docs/README.md) **Deployment reality checked 2026-08-14:** merges run CI but do not start the manual release workflow. The protected gate accepts one exact current merged commit, rechecks its newest CI run after approval, uses one fixed Firebase target set, fails when protected authority/configuration is missing, verifies Firebase before publishing GitHub Pages, and gives no server credential to website preparation or publication. Ordinary Git-triggered Netlify production builds are paused. WEB-002D [#659](https://github.com/Run-MPRC/Run-MPRC.github.io/issues/659) completed one bounded accessibility release: exact merge `46e23647d8e0bf9fa3a574ea5c5f993be10a419d` published deploy `6a7ece87c5ca4d0007c1a3fc` from source `7496fe0881fb52908c4ff2f40f488df09c94c908`, tree `ccac4c189c195db8ab594e0eefe256ea9fa04996`, 62 files, and artifact digest `e4c26e6f0fbcd086663d86238675f0be228fb649a00628c1c97d1166612f49c7`. Signed-out desktop and phone route-focus/menu checks passed. Repause merge `3138a00c1c48e1d5d1dcda0b44722b09a2194ff7` passed exact-main CI run `31783808994`; attempt `6a7ed0ddb00a46000818878d` published nothing, deploy `6a7ece87c5ca4d0007c1a3fc` remained live, and the manifest is inactive. #623 deploy `6a7e072f8f346b0008510d29` remains rollback history and its inert directory interface is unchanged beneath the accessibility delta. No Firebase, Rules, Functions, indexes, provider configuration, account, sign-in state, role, member data, payment, or connected directory backend changed. The prior bounded #473 deploy `6a6dc9ea588b0c0008036312` remains older history. The source stops adding a Pages `CNAME`, but GitHub Pages currently still claims `runmprc.com` and its default URL redirects there; only a controlled #136/WEB-001 publication and provider readback can clear that conflict. Reusable protected publication to the live Netlify-served `runmprc.com` is not configured yet. Treat GitHub Pages, Netlify, `runmprc.com`, Firebase, and outside providers as separate states. -**Owned staging checked 2026-08-26:** WEB-001A2 [#665](https://github.com/Run-MPRC/Run-MPRC.github.io/issues/665) created the isolated `run-mprc-staging` Firebase project under the club account and published exact source `a614c68d9a1a2be631a3be874a686d61e5d170a0` as Hosting version `3ffadcf2ac8cc760` at [run-mprc-staging.web.app](https://run-mprc-staging.web.app). Signed-out desktop, phone, and direct `/events` checks passed. CI-001C1 [#667](https://github.com/Run-MPRC/Run-MPRC.github.io/issues/667) adds a protected staging-only OIDC verifier with two named reviewers and a keyless principal that can read only the exact project identity. CI-001D1 [#669](https://github.com/Run-MPRC/Run-MPRC.github.io/issues/669) adds the empty delete-protected `(default)` Firestore database in `us-west2` and deploys exact Rules/indexes from source `ee16bd16220ab58bd3a2add80dd2f39a1d514dd7`; provider definitions matched, fixed anonymous server-only reads were denied, and the root collection count remained zero. CI-001D2 [#671](https://github.com/Run-MPRC/Run-MPRC.github.io/issues/671) deployed exact merged source `42542303d043f87a8f1a04be2f0b4f2a88e0318c` to Auth only. Live readback shows instrumentless Firebase Authentication with Identity Platform, billing disabled, password-required email/password, improved email privacy, only the two staging authorized domains, and no anonymous, phone, native federated, OIDC, or SAML provider. Disposable API and browser sign-in/sign-out checks passed, both identities were removed, no email or SMS was requested, and the final user and Firestore root-collection counts were zero. This remains incomplete engineering staging: trusted Functions, App Check enforcement, outside-provider sandboxes, reusable Firebase deployment authority, application security headers, a release marker, and a tested predecessor rollback are not configured. Do not use real data or treat staging sign-in as an operational member system. Production Firebase Hosting remains empty and `runmprc.com` remains on its existing Netlify deployment. +**Owned staging checked 2026-08-26:** WEB-001A2 [#665](https://github.com/Run-MPRC/Run-MPRC.github.io/issues/665) created the isolated `run-mprc-staging` Firebase project under the club account and published exact source `a614c68d9a1a2be631a3be874a686d61e5d170a0` as Hosting version `3ffadcf2ac8cc760` at [run-mprc-staging.web.app](https://run-mprc-staging.web.app). Signed-out desktop, phone, and direct `/events` checks passed. CI-001C1 [#667](https://github.com/Run-MPRC/Run-MPRC.github.io/issues/667) adds a protected staging-only OIDC verifier with two named reviewers and a keyless principal that can read only the exact project identity. CI-001D1 [#669](https://github.com/Run-MPRC/Run-MPRC.github.io/issues/669) adds the empty delete-protected `(default)` Firestore database in `us-west2` and deploys exact Rules/indexes from source `ee16bd16220ab58bd3a2add80dd2f39a1d514dd7`; provider definitions matched, fixed anonymous server-only reads were denied, and the root collection count remained zero. CI-001D2 [#671](https://github.com/Run-MPRC/Run-MPRC.github.io/issues/671) deployed exact merged source `42542303d043f87a8f1a04be2f0b4f2a88e0318c` to Auth only. Live readback shows instrumentless Firebase Authentication with Identity Platform, billing disabled, password-required email/password, improved email privacy, only the two staging authorized domains, and no anonymous, phone, native federated, OIDC, or SAML provider. Disposable API and browser sign-in/sign-out checks passed, both identities were removed, no email or SMS was requested, and the final user and Firestore root-collection counts were zero. CI-001D3 [#674](https://github.com/Run-MPRC/Run-MPRC.github.io/issues/674) adds a source-only guard for exactly `createMemberOnSignUp` and `ensureMemberProfile`; it rejects a broad Function target, another project/account, a credential file, an unpinned CLI, or appended arguments before Firebase CLI. Read-only preflight found billing disabled and no open billing account visible to the club account, so no Function API was enabled and no Function was deployed. This remains incomplete engineering staging: trusted Functions, App Check enforcement, outside-provider sandboxes, reusable Firebase deployment authority, application security headers, a release marker, and a tested predecessor rollback are not configured. Do not use real data or treat staging sign-in as an operational member system. Production Firebase Hosting remains empty and `runmprc.com` remains on its existing Netlify deployment. ## Local setup status @@ -78,6 +78,7 @@ npm --prefix functions run test:run -- --runInBand CI=true npm test -- --watchAll=false --runInBand npm run test:spa-navigation npm run test:firebase-auth-staging +npm run test:firebase-profile-functions-staging npm run test:rules CI=true DISABLE_ESLINT_PLUGIN=true \ REACT_APP_FIREBASE_ENVIRONMENT=staging \ @@ -91,7 +92,7 @@ npx --no-install react-scripts build node scripts/firebase-hosting-contract.js verify build ``` -Rules and commerce-emulator tests require Java 21. The repository pins Firebase CLI 15.24.0; use only that lockfile copy with Node 20 and explicit `demo-*` projects. The synthetic values above authorize no provider call or deployment. The direct `react-scripts build` command avoids regenerating `public/sitemap.xml`; the following verifier proves that executable JavaScript selected the synthetic staging project and contains no known production Firebase identity. The normal `npm run build` validates its selected hosted environment and runs the sitemap generator. Hosted CI runs the frontend Jest suite under [#124](https://github.com/Run-MPRC/Run-MPRC.github.io/issues/124), the SPA callback suite under [#126](https://github.com/Run-MPRC/Run-MPRC.github.io/issues/126), and the release-gate source tests under [#135](https://github.com/Run-MPRC/Run-MPRC.github.io/issues/135). #667 supplies a separate protected staging project-identity check only. #669 proves the exact Rules/index files on an empty staging database but grants no reusable deploy authority. #671 adds a focused source test and an Auth-only deploy guard; its separate exact-main provider evidence proves the live narrow Auth configuration and disposable sign-in cleanup, while source and CI alone still prove neither fact. Parent [#133](https://github.com/Run-MPRC/Run-MPRC.github.io/issues/133) still owns Firebase deployment roles and production protection; [#136](https://github.com/Run-MPRC/Run-MPRC.github.io/issues/136) owns the actual staged profile-recovery release. Required branch checks, complete backend/provider isolation, protected publication, security headers, rollback proof, and hosting consolidation remain open under [#105](https://github.com/Run-MPRC/Run-MPRC.github.io/issues/105) and their atomic children. +Rules and commerce-emulator tests require Java 21. The repository pins Firebase CLI 15.24.0; use only that lockfile copy with Node 20 and explicit `demo-*` projects. The synthetic values above authorize no provider call or deployment. The direct `react-scripts build` command avoids regenerating `public/sitemap.xml`; the following verifier proves that executable JavaScript selected the synthetic staging project and contains no known production Firebase identity. The normal `npm run build` validates its selected hosted environment and runs the sitemap generator. Hosted CI runs the frontend Jest suite under [#124](https://github.com/Run-MPRC/Run-MPRC.github.io/issues/124), the SPA callback suite under [#126](https://github.com/Run-MPRC/Run-MPRC.github.io/issues/126), and the release-gate source tests under [#135](https://github.com/Run-MPRC/Run-MPRC.github.io/issues/135). #667 supplies a separate protected staging project-identity check only. #669 proves the exact Rules/index files on an empty staging database but grants no reusable deploy authority. #671 adds a focused source test and an Auth-only deploy guard; its separate exact-main provider evidence proves the live narrow Auth configuration and disposable sign-in cleanup, while source and CI alone still prove neither fact. #674 adds a separate source-only two-Function guard; passing it does not prove billing, API enablement, Function deployment, App Check, or backend behavior. Parent [#133](https://github.com/Run-MPRC/Run-MPRC.github.io/issues/133) still owns Firebase deployment roles and production protection; [#136](https://github.com/Run-MPRC/Run-MPRC.github.io/issues/136) owns the actual staged profile-recovery release. Required branch checks, complete backend/provider isolation, protected publication, security headers, rollback proof, and hosting consolidation remain open under [#105](https://github.com/Run-MPRC/Run-MPRC.github.io/issues/105) and their atomic children. These safety changes do not repair a missing member profile or prove deployed Firebase Rules/Functions. The reported profile-save failure remains [#118](https://github.com/Run-MPRC/Run-MPRC.github.io/issues/118). @@ -107,4 +108,4 @@ Business owners—not coding agents—must approve legal text, waiver/insurance ## Deployment warning -Pushing to `main` runs CI and does not start `.github/workflows/deploy.yml`. #667's separate manual verifier proves only short-lived staging project-identity access. #669's one-time bounded operation proves only the empty staging Firestore Rules/index boundary, and #671's one-time bounded operation proves only staging Auth; neither is a release button. The backend release workflow remains **NOT AVAILABLE YET** until #133 grants only the reviewed deployment roles and protects production. Ordinary Git-triggered Netlify production builds are paused. The completed #623 web-only exception is inactive and not reusable; it published no Firebase or provider change. The older #473 exception is retained only as rollback history. A reusable protected live-Netlify publication path is still **NOT AVAILABLE YET**. Production changes still require protected approval, a compatible backend-first rollout, separate live-host proof, a named observer, and the runbook's post-deploy/reconciliation steps. +Pushing to `main` runs CI and does not start `.github/workflows/deploy.yml`. #667's separate manual verifier proves only short-lived staging project-identity access. #669's one-time bounded operation proves only the empty staging Firestore Rules/index boundary, and #671's one-time bounded operation proves only staging Auth; neither is a release button. #674's guarded Function command is source only and must not be run while billing is unapproved. The backend release workflow remains **NOT AVAILABLE YET** until #133 grants only the reviewed deployment roles and protects production. Ordinary Git-triggered Netlify production builds are paused. The completed #623 web-only exception is inactive and not reusable; it published no Firebase or provider change. The older #473 exception is retained only as rollback history. A reusable protected live-Netlify publication path is still **NOT AVAILABLE YET**. Production changes still require protected approval, a compatible backend-first rollout, separate live-host proof, a named observer, and the runbook's post-deploy/reconciliation steps. diff --git a/SECURITY.md b/SECURITY.md index a563946..0622952 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -47,7 +47,7 @@ The findings below describe the repository at the start of the 2026-07-12 assess | RISK-007 | Repository source rejects unverified targets at the two existing role-grant endpoints. AUTH-001B [#196](https://github.com/Run-MPRC/Run-MPRC.github.io/issues/196) adds exact verified-email checks to role-based Firestore Rules, and AUTH-001C [#209](https://github.com/Run-MPRC/Run-MPRC.github.io/issues/209) tracks the matching gate at current Functions role consumers. Firebase deployment, verification-mirror refresh, revocation, authoritative membership, scoped capability/recent-auth controls, and replacement of the legacy static-key endpoint remain unproven. | Older deployed code or a remaining role-only server guard can let an unverified known-email account use member/admin authority. CSV export also retains separate minimization, recent-auth, and audit risks. | Deploy and verify the exact Rules/Functions through #105, complete the remaining AUTH-001 mirror/revocation work, require authoritative membership and scoped privileged access, harden roster export under #116, and retire 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 | #99 source preserves and validates the complete same-origin Pages callback route, applies `strict-origin` before subresources, and suppresses initial callback App Check/telemetry startup; #126 adds that standalone suite as a blocking hosted CI step. #443 adds only the source/test Strava same-document handoff after the native and Router callback locations are clean. No production Stripe/Strava callback, provider-backed App Check token behavior, or live custom-domain handoff has been rehearsed, and the live custom domain uses a different host. | Hosting, provider, configuration, or release drift can still break return links or the App Check handoff even when source tests pass. Enforcing a callback callable without its matching released client handoff and staged provider evidence can also break confirmation or Strava exchange. | Keep the hosted callback gate blocking; keep `lookupRegistration`/`lookupOrder` deferred until DATA-001A; keep native enforcement for `stravaExchangeCode` deferred to ABUSE-001A2 after the exact #443 website source and provider-backed Enterprise token behavior are proven in protected staging; verify test-mode callbacks only after provider gates; consolidate hosting under WEB-001. | -| RISK-010 | Development/test use synthetic loopback Auth, Firestore, and Functions. WEB-001A1 [#663](https://github.com/Run-MPRC/Run-MPRC.github.io/issues/663) binds controlled Hosting builds to an explicit environment, and WEB-001A2 [#665](https://github.com/Run-MPRC/Run-MPRC.github.io/issues/665) supplies owned static staging. CI-001D1 [#669](https://github.com/Run-MPRC/Run-MPRC.github.io/issues/669) adds an empty delete-protected Firestore database with exact Rules/indexes. CI-001D2 [#671](https://github.com/Run-MPRC/Run-MPRC.github.io/issues/671) deploys and verifies password-required email/password Auth with improved email privacy, but the project still has no Functions or App Check. Storage data, provider sandboxes, protected Firebase deployment authority, and a release marker remain absent; emulated Functions can still call outside providers. | A partially provisioned stage, bypassed build path, or provider-capable emulator can be mistaken for complete staging and affect production/provider systems. | Keep real identities and private actions out of previews; preserve the fixed project, exact Rules, and Auth-only guard/evidence; complete #113/#133/#136 Functions, App Check, short-lived deploy authority, and provider isolation; prove test keys, sinks, exact marker readback, and rollback before end-to-end flows. | +| RISK-010 | Development/test use synthetic loopback Auth, Firestore, and Functions. WEB-001A1 [#663](https://github.com/Run-MPRC/Run-MPRC.github.io/issues/663) binds controlled Hosting builds to an explicit environment, and WEB-001A2 [#665](https://github.com/Run-MPRC/Run-MPRC.github.io/issues/665) supplies owned static staging. CI-001D1 [#669](https://github.com/Run-MPRC/Run-MPRC.github.io/issues/669) adds an empty delete-protected Firestore database with exact Rules/indexes. CI-001D2 [#671](https://github.com/Run-MPRC/Run-MPRC.github.io/issues/671) deploys and verifies password-required email/password Auth with improved email privacy. CI-001D3 [#674](https://github.com/Run-MPRC/Run-MPRC.github.io/issues/674) narrows the future staging command to the two profile Functions, but billing is unapproved and the project still has no Functions or App Check. Storage data, provider sandboxes, protected Firebase deployment authority, and a release marker remain absent; emulated Functions can still call outside providers. | A partially provisioned stage, bypassed build path, or provider-capable emulator can be mistaken for complete staging and affect production/provider systems. | Keep real identities and private actions out of previews; preserve the fixed project, exact Rules, Auth-only evidence, and source-only two-Function guard; do not link billing merely to make a test pass; complete #113/#133/#136 Functions, App Check, short-lived deploy authority, and provider isolation; prove test keys, sinks, exact marker readback, and rollback before end-to-end flows. | | 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 | Repository source now denies every admitted positive late-registration amount before identifier/token allocation, registration writes, Stripe construction, or Product/Price/Payment Link calls, and the Admin screen exposes only the exact-zero compatibility path. Firebase deployment is unproven, and legacy reusable Payment Links may still exist. | Older deployed code or an existing link can still create unreconciled or repeated charges/Sessions for one logical registration. The exact-zero local record is also marked paid without Stripe and is not payment, free, comp, or membership authority. | Keep paid late registration unavailable; inventory, deactivate, and reconcile legacy links privately; then replace them with one-off idempotent Checkout Sessions through the normal registration/payment lifecycle. | | RISK-013 | The assessment baseline permitted Stripe promotions while Firestore stored base price. Repository source now disables promotion entry and automatic tax, requires a complete adjustment breakdown, and quarantines unknown or nonzero discount/tax/shipping amounts; deployment and provider state are unproven. | An older open Session, provider drift, skipped backend release, or later guard regression could still make reporting, refunds, and local payment confirmation disagree. | Privately inventory pre-change Sessions/provider settings, release through the protected gate, verify Firebase and Stripe separately, and keep every adjustment disabled until an approved authoritative model exists. | diff --git a/SYSTEM_DESIGN.md b/SYSTEM_DESIGN.md index 4c30e86..bfa0c6a 100644 --- a/SYSTEM_DESIGN.md +++ b/SYSTEM_DESIGN.md @@ -277,7 +277,7 @@ Text alternative: exact reviewed source permits only a guarded Auth deployment t ## 4. Target deployment topology -The target keeps React, Firebase, and Stripe, but places stronger boundaries around them. Migrating the frontend from GitHub Pages/Netlify to Firebase Hosting is recommended before live commerce because it supports controlled SPA rewrites, preview channels, and security headers. #663 supplies the fail-closed build selector and inert Hosting rewrite configuration. #665 owns the isolated static staging host. CI-001C1 [#667](https://github.com/Run-MPRC/Run-MPRC.github.io/issues/667) adds only a protected, read-only staging identity proof; it grants no Firebase deployment permission. CI-001D1 [#669](https://github.com/Run-MPRC/Run-MPRC.github.io/issues/669) adds only the empty staging Firestore database and exact Rules/index boundary. CI-001D2 [#671](https://github.com/Run-MPRC/Run-MPRC.github.io/issues/671) deploys and verifies the narrow email/password Auth configuration from exact merged source. The instrumentless project reports the `IDENTITY_PLATFORM` subtype and remains unlinked from billing; its current no-cost ceiling is an operational limit, not authorization to add enterprise providers. Backend Functions, App Check, reusable deployment authority, automatic exact-commit release, full backend readback, rollback, headers, production protection, outside-provider isolation, and DNS/TLS cutover remain separate work under #113/#133/#136/#460. The migration is not live merely because several staging layers exist. +The target keeps React, Firebase, and Stripe, but places stronger boundaries around them. Migrating the frontend from GitHub Pages/Netlify to Firebase Hosting is recommended before live commerce because it supports controlled SPA rewrites, preview channels, and security headers. #663 supplies the fail-closed build selector and inert Hosting rewrite configuration. #665 owns the isolated static staging host. CI-001C1 [#667](https://github.com/Run-MPRC/Run-MPRC.github.io/issues/667) adds only a protected, read-only staging identity proof; it grants no Firebase deployment permission. CI-001D1 [#669](https://github.com/Run-MPRC/Run-MPRC.github.io/issues/669) adds only the empty staging Firestore database and exact Rules/index boundary. CI-001D2 [#671](https://github.com/Run-MPRC/Run-MPRC.github.io/issues/671) deploys and verifies the narrow email/password Auth configuration from exact merged source. CI-001D3 [#674](https://github.com/Run-MPRC/Run-MPRC.github.io/issues/674) adds a source-only guard that can name only the two profile Functions in staging and cannot accept appended deployment arguments. The instrumentless project reports the `IDENTITY_PLATFORM` subtype and remains unlinked from billing; the club account currently sees no open billing account. The no-cost Auth ceiling is an operational limit, not authorization to link Cloud Billing. Backend Functions, App Check, reusable deployment authority, automatic exact-commit release, full backend readback, rollback, headers, production protection, outside-provider isolation, and DNS/TLS cutover remain separate work under #113/#133/#136/#460. The migration is not live merely because several staging layers and a source guard exist. ```mermaid flowchart TB @@ -1762,7 +1762,7 @@ Returning a `2xx` response to Stripe means the event has been durably accepted o | --- | --- | --- | --- | --- | | Local source runtime | Firebase Emulator Suite under `demo-mprc-local` | Not safe by Firebase emulation alone | `localhost:3000` | Synthetic data only; browser Firebase traffic is loopback-only | | CI | Ephemeral emulators and mocks | Stripe fixtures/signature tests; optional isolated test account | None | Synthetic data, no production secrets | -| Staging | `run-mprc-staging`; static Hosting, one web app, protected read-only OIDC proof, empty delete-protected Firestore with exact Rules/indexes, and deployed/verified password-required email Auth | Not configured; no outside-provider testing authorized | `run-mprc-staging.web.app`; `dev.runmprc.com` not configured | Public signed-out review plus issue-scoped disposable synthetic Auth checks; no real identity or data | +| Staging | `run-mprc-staging`; static Hosting, one web app, protected read-only OIDC proof, empty delete-protected Firestore with exact Rules/indexes, deployed/verified password-required email Auth, and a source-only paired profile-Function guard; no Function or App Check deployment | Not configured; no outside-provider testing authorized | `run-mprc-staging.web.app`; `dev.runmprc.com` not configured | Public signed-out review plus issue-scoped disposable synthetic Auth checks; no real identity or data | | Production | Dedicated production Firebase project | Live restricted keys and production webhook secret | `runmprc.com` | Real data under documented retention and access policies | ```mermaid @@ -1783,9 +1783,9 @@ Text alternative: the local browser uses only the three loopback Firebase emulat #99 source selects a fully synthetic Firebase configuration for development/test, connects Auth, Firestore, and Functions to the loopback ports above, stops startup when connection setup fails, and leaves App Check, Analytics, and Sentry off locally. #139 additionally keeps Firebase Analytics off in every source environment and removes the direct Waiver SDK bypass pending an approved #110 policy. The Firebase CLI must still report all three emulators ready before the app is opened. Mocked endpoint tests alone do not prove listening processes, website publication, provider behavior, or deletion of earlier provider data/cookies. -Optimized builds must select `staging` or `production` explicitly. WEB-001A1 #663 rejects missing, local, incomplete, or cross-environment Firebase web configuration, and its artifact check rejects known production identities from a staging executable. Ordinary Netlify deploy previews receive and scan only the reserved synthetic configuration; `run-mprc-staging.web.app` receives and scans the real isolated staging web-app identity; the pinned release builder receives and scans the exact reviewed public production web configuration. The owned staging project now has an empty Rules-protected Firestore boundary and verified password-required email/password Auth with improved email privacy. #671's disposable API and browser checks returned the project to zero users and records. It still lacks Functions, App Check, and outside-provider isolation. Netlify previews remain public, signed-out, read-only visual review only; the Firebase staging host permits only issue-scoped disposable engineering checks until #105/CONFIG supplies complete backend/provider evidence. Never use a real identity, private/admin record, or outside-provider action there. +Optimized builds must select `staging` or `production` explicitly. WEB-001A1 #663 rejects missing, local, incomplete, or cross-environment Firebase web configuration, and its artifact check rejects known production identities from a staging executable. Ordinary Netlify deploy previews receive and scan only the reserved synthetic configuration; `run-mprc-staging.web.app` receives and scans the real isolated staging web-app identity; the pinned release builder receives and scans the exact reviewed public production web configuration. The owned staging project now has an empty Rules-protected Firestore boundary and verified password-required email/password Auth with improved email privacy. #671's disposable API and browser checks returned the project to zero users and records. #674 narrows the future manual Function command to both profile Functions together, but billing is unapproved and no Function is deployed. The project still lacks Functions, App Check, and outside-provider isolation. Netlify previews remain public, signed-out, read-only visual review only; the Firebase staging host permits only issue-scoped disposable engineering checks until #105/CONFIG supplies complete backend/provider evidence. Never use a real identity, private/admin record, or outside-provider action there. -`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. `run-mprc-staging` now supplies the public web app, Hosting, empty Firestore Rules/index boundary, and deployed/verified narrow Auth boundary; end-to-end rollout still needs Functions, App Check, deployment authority, and outside-provider configuration and evidence. +`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. `run-mprc-staging` now supplies the public web app, Hosting, empty Firestore Rules/index boundary, and deployed/verified narrow Auth boundary. The #674 source guard is not a hosted backend. End-to-end rollout still needs approved billing, Functions, App Check, deployment authority, and outside-provider configuration and evidence. CONFIG-001A [#149](https://github.com/Run-MPRC/Run-MPRC.github.io/issues/149) adds the source boundary for current commerce and confirmation-mail Functions: local is exact loopback, tests use HTTPS `.test`, staging is exactly `https://dev.runmprc.com`, and production is exactly `https://runmprc.com`. Test/live expected mode must match the server-key marker on the four key-bound Functions. Webhook and mail receive no Stripe API key, and invalid settings stop before rate-limit, business, mail, event-ledger, or Stripe writes. This source boundary does not create staging, configure Secret Manager or Stripe, deploy Firebase, enable commerce, or prove live behavior. diff --git a/docs/officers/PUBLISH_AND_CHECK.md b/docs/officers/PUBLISH_AND_CHECK.md index d76b804..93c5453 100644 --- a/docs/officers/PUBLISH_AND_CHECK.md +++ b/docs/officers/PUBLISH_AND_CHECK.md @@ -10,7 +10,7 @@ **Protected release status:** **NOT AVAILABLE YET.** Issue #135 provides the fail-closed source gate. Issue #133 must still configure protected `staging` and `production` environments, their named reviewers, and a short-lived cloud identity. Public browser build values must be named repository or organization variables because artifact preparation has no protected-environment access; #133/#136 must record and verify them separately. Do not add a long-lived Firebase key as a shortcut. -**Firebase staging status:** **STATIC HOSTING, EMPTY FIRESTORE, AND DISPOSABLE EMAIL/PASSWORD AUTH PROOF AVAILABLE; OFFICER AND PRODUCTION RELEASE NOT AVAILABLE YET.** WEB-001A1 [#663](https://github.com/Run-MPRC/Run-MPRC.github.io/issues/663) requires controlled optimized builds to name staging or production and scans executable JavaScript for environment identity. WEB-001A2 [#665](https://github.com/Run-MPRC/Run-MPRC.github.io/issues/665) created the club-owned `run-mprc-staging` project and published exact merged source `a614c68d9a1a2be631a3be874a686d61e5d170a0` to [run-mprc-staging.web.app](https://run-mprc-staging.web.app). CI-001D1 [#669](https://github.com/Run-MPRC/Run-MPRC.github.io/issues/669) adds an empty delete-protected Firestore database and exact reviewed Rules/indexes; fixed anonymous server-only reads were denied and the root collection count remained zero. CI-001D2 [#671](https://github.com/Run-MPRC/Run-MPRC.github.io/issues/671) deploys password-required email/password Auth from exact merged source, verifies provider/privacy/domain settings, passes disposable API/browser sign-in checks, and returns the project to zero users and records. The site still has no Functions, App Check, provider sandbox, protected short-lived publication, release marker, tested predecessor rollback, complete security headers, custom domain, or DNS/TLS change. Officers do not sign in, enter information, or use the site as a publish control. Production Firebase Hosting remains empty, and Netlify still serves `runmprc.com`. +**Firebase staging status:** **STATIC HOSTING, EMPTY FIRESTORE, DISPOSABLE EMAIL/PASSWORD AUTH PROOF, AND A SOURCE-ONLY TWO-FUNCTION GUARD AVAILABLE; OFFICER AND PRODUCTION RELEASE NOT AVAILABLE YET.** WEB-001A1 [#663](https://github.com/Run-MPRC/Run-MPRC.github.io/issues/663) requires controlled optimized builds to name staging or production and scans executable JavaScript for environment identity. WEB-001A2 [#665](https://github.com/Run-MPRC/Run-MPRC.github.io/issues/665) created the club-owned `run-mprc-staging` project and published exact merged source `a614c68d9a1a2be631a3be874a686d61e5d170a0` to [run-mprc-staging.web.app](https://run-mprc-staging.web.app). CI-001D1 [#669](https://github.com/Run-MPRC/Run-MPRC.github.io/issues/669) adds an empty delete-protected Firestore database and exact reviewed Rules/indexes; fixed anonymous server-only reads were denied and the root collection count remained zero. CI-001D2 [#671](https://github.com/Run-MPRC/Run-MPRC.github.io/issues/671) deploys password-required email/password Auth from exact merged source, verifies provider/privacy/domain settings, passes disposable API/browser sign-in checks, and returns the project to zero users and records. CI-001D3 [#674](https://github.com/Run-MPRC/Run-MPRC.github.io/issues/674) adds only a fail-closed code guard for the future paired profile Functions. Billing remains disabled, no open billing account is visible to the club account, and no Function/API/App Check change occurred. Officers do not sign in, enter information, run the command, attach billing, or use the site as a publish control. The site still has no Functions, App Check, provider sandbox, protected short-lived publication, release marker, tested predecessor rollback, complete security headers, custom domain, or DNS/TLS change. Production Firebase Hosting remains empty, and Netlify still serves `runmprc.com`. **Live Netlify publication status:** a reusable protected release is **NOT AVAILABLE YET**. Ordinary Git-triggered production builds are paused by repository configuration. An overbroad #473 artifact was published and then rolled back on 2026-08-01; bounded #473 deploy `6a6dc9ea588b0c0008036312` is older history. #623 completed the inert-directory predecessor and remains the immediate rollback as deploy `6a7e072f8f346b0008510d29`, source `c2d87d1f69f15e128a0bc9b1b9f915b7c8417aec`. #659 completed one separate exact-artifact accessibility release. Deploy `6a7ece87c5ca4d0007c1a3fc`, source `7496fe0881fb52908c4ff2f40f488df09c94c908`, is production now. Its exact marker/artifact and signed-out route-focus/menu checks passed. The manifest is inactive, temporary refs are absent, and the rollback ref remains. Shop is the static catalog; Events and Calendar show a fixed retry-later notice; the directory remains inert. Event records remain unavailable because this did not deploy Firebase. GitHub Pages currently still claims the same custom domain; future source omits that claim, but #136/WEB-001 must publish and verify its removal. @@ -56,6 +56,7 @@ As of **2026-07-13**, with the internal tooling note below checked from source o - Missing environment configuration or cloud authority makes the release red before backend dependencies, cloud authentication, or deployment. A public website artifact may be prepared without cloud authority, but it cannot be published. - The separate #667 staging verifier uses a short-lived cloud identity after named approval. That identity can read only the staging project identity. The backend release still has no deployment authority. The website job receives public browser values only. - #669 used one bounded maintainer operation to provision empty staging Firestore and deploy exact Rules/indexes. It did not give the #667 identity or the general release workflow a deploy role. +- #674 adds an argument-closed source wrapper for exactly `createMemberOnSignUp` and `ensureMemberProfile`. Its tests and merge are not Function deployment evidence. Billing approval, API enablement, App Check, provider readback, and synthetic profile proof remain separate. - The Firebase CLI comes from the committed lockfile. The release does not install `latest`. - Source checked on 2026-07-22 pins the internal Firebase CLI to 15.24.0, and its emulator checks use Java 21. Officers do not install or run either tool; the platform maintainer owns them. This source change does not alter the release diagram or prove that Firebase was deployed. The provider and live-host facts below were not reverified for this tooling update. - Source checked on 2026-08-01 pins every root brace-expansion dependency family to reviewed compatible releases 1.1.18, 2.1.4, and 5.0.9 after a new advisory and follow-up bypass fixes. This is internal dependency evidence only. Officers do not install packages, run audit commands, or resolve dependency warnings; the platform maintainer owns those tasks. The separate minimatch finding remains open. This source change does not publish a website, deploy Firebase, change a provider, or prove live behavior. diff --git a/docs/officers/README.md b/docs/officers/README.md index 10a8c57..58ca79b 100644 --- a/docs/officers/README.md +++ b/docs/officers/README.md @@ -69,7 +69,7 @@ WEB-002D [#659](https://github.com/Run-MPRC/Run-MPRC.github.io/issues/659) compl The general protected GitHub release is still **NOT AVAILABLE YET**. #667 configures only a separate staging read-only identity check: Dave Liu or Jeff Chang reviews exact `main`, and a keyless five-minute identity may read only the staging project identity. It cannot deploy Firebase or publish a website. #133 still owns backend deployment roles and production protection. Missing authority stops the release with a red failure before Firebase or website publication. -The empty staging Firestore boundary is available after #669. Project `run-mprc-staging` has a delete-protected empty database with the exact reviewed Rules and indexes. Fixed signed-out requests to server-only paths were denied, and the final root collection count was zero. #671 deploys and verifies password-required email/password Auth from exact merged source. Improved email privacy is on; only the two staging domains are authorized; every other provider is absent; billing is disabled; disposable API/browser checks passed; and cleanup returned Auth and Firestore to zero users/records. This is not a usable member backend: Functions, App Check, provider sandboxes, reusable deployment authority, role/private-data proof, and officer operation remain **NOT AVAILABLE YET**. Officers do not sign in, enter records, run commands, edit the database, or deploy Auth. Use the no-terminal [staging Firestore review](./PUBLISH_AND_CHECK.md#review-the-empty-staging-firestore-boundary--available-after-669) and [staging Auth review](./PUBLISH_AND_CHECK.md#review-the-staging-auth-boundary--available-after-671). +The empty staging Firestore boundary is available after #669. Project `run-mprc-staging` has a delete-protected empty database with the exact reviewed Rules and indexes. Fixed signed-out requests to server-only paths were denied, and the final root collection count was zero. #671 deploys and verifies password-required email/password Auth from exact merged source. Improved email privacy is on; only the two staging domains are authorized; every other provider is absent; billing is disabled; disposable API/browser checks passed; and cleanup returned Auth and Firestore to zero users/records. #674 adds a code guard for a future deployment of only the two profile Functions. It is not a deployment: billing remains disabled, the club account currently sees no open billing account, and no Function or App Check policy is live. This is not a usable member backend: Functions, App Check, provider sandboxes, reusable deployment authority, role/private-data proof, and officer operation remain **NOT AVAILABLE YET**. Officers do not sign in, enter records, run commands, edit the database, deploy Auth, or attach billing. Use the no-terminal [staging Firestore review](./PUBLISH_AND_CHECK.md#review-the-empty-staging-firestore-boundary--available-after-669) and [staging Auth review](./PUBLISH_AND_CHECK.md#review-the-staging-auth-boundary--available-after-671). GitHub Pages currently still claims `runmprc.com`, so its normal address redirects to the Netlify-served name. The source stops adding that claim, but #136/WEB-001 must publish and verify the provider setting before officers call Pages an independent copy. diff --git a/package.json b/package.json index 6264f2e..b955da8 100644 --- a/package.json +++ b/package.json @@ -32,6 +32,7 @@ "lint:ci": "node .github/scripts/check-frontend-lint.cjs", "lint:fix": "eslint --fix --ext .js,.jsx .", "test:firebase-auth-staging": "node --test tests/firebase-auth-staging-contract.test.js", + "test:firebase-profile-functions-staging": "node --test tests/firebase-profile-functions-staging-contract.test.js", "test:firebase-hosting": "node --test tests/firebase-hosting-foundation.test.js", "test:staging-authority": "node --test tests/staging-authority-workflow.test.js", "test": "react-scripts test", @@ -40,6 +41,8 @@ "test:rules:watch": "firebase emulators:exec --only firestore --project demo-rules-test \"npx --no-install jest --config tests/firestore-rules/jest.config.js --watch\"", "predeploy:staging-auth": "node scripts/firebase-auth-staging-contract.js validate-deploy", "deploy:staging-auth": "npx --no-install firebase deploy --only auth --project run-mprc-staging --non-interactive", + "predeploy:staging-profile-functions": "node scripts/firebase-profile-functions-staging-contract.js validate-deploy", + "deploy:staging-profile-functions": "node scripts/firebase-profile-functions-staging-contract.js deploy", "eject": "react-scripts eject", "lint:all": "find . -name '*.{js,jsx,tsx,ts}' -exec pretty-quick {} + && eslint --fix ." }, diff --git a/scripts/firebase-profile-functions-staging-contract.js b/scripts/firebase-profile-functions-staging-contract.js new file mode 100644 index 0000000..e0f9d56 --- /dev/null +++ b/scripts/firebase-profile-functions-staging-contract.js @@ -0,0 +1,259 @@ +'use strict'; + +const { spawnSync } = require('node:child_process'); +const fs = require('node:fs'); +const path = require('node:path'); + +const REJECTION = 'firebase_profile_functions_staging_contract_rejected'; +const DEFAULT_PROJECT_ID = 'demo-mprc-local'; +const STAGING_PROJECT_ID = 'run-mprc-staging'; +const CLUB_ACCOUNT = 'runmprc@gmail.com'; +const FIREBASE_CLI_VERSION = '15.24.0'; +const FUNCTION_IDS = Object.freeze([ + 'createMemberOnSignUp', + 'ensureMemberProfile', +]); +const DEPLOY_SCOPE = FUNCTION_IDS + .map((functionId) => `functions:${functionId}`) + .join(','); +const TEST_SCRIPT = 'node --test tests/firebase-profile-functions-staging-contract.test.js'; +const PREDEPLOY_SCRIPT = 'node scripts/firebase-profile-functions-staging-contract.js validate-deploy'; +const DEPLOY_SCRIPT = 'node scripts/firebase-profile-functions-staging-contract.js deploy'; + +function reject() { + throw new Error(REJECTION); +} + +function isPlainObject(value) { + return value !== null + && typeof value === 'object' + && !Array.isArray(value) + && Object.getPrototypeOf(value) === Object.prototype; +} + +function hasExactKeys(value, expected) { + return isPlainObject(value) + && JSON.stringify(Object.keys(value).sort()) + === JSON.stringify([...expected].sort()); +} + +function hasExactArray(value, expected) { + return Array.isArray(value) + && JSON.stringify(value) === JSON.stringify(expected); +} + +function requiredValue(environment, key) { + const value = environment?.[key]; + if (typeof value !== 'string' + || value.length === 0 + || value.length > 8192 + || value.trim() !== value) { + reject(); + } + return value; +} + +function validateFunctionsConfig(firebaseConfig) { + if (!isPlainObject(firebaseConfig) + || !Array.isArray(firebaseConfig.functions) + || firebaseConfig.functions.length !== 1) { + reject(); + } + + const [functionsConfig] = firebaseConfig.functions; + if (!hasExactKeys( + functionsConfig, + ['source', 'codebase', 'ignore', 'predeploy'], + ) + || functionsConfig.source !== 'functions' + || functionsConfig.codebase !== 'default' + || !hasExactArray(functionsConfig.ignore, [ + 'node_modules', + '.git', + 'firebase-debug.log', + 'firebase-debug.*.log', + ]) + || !hasExactArray( + functionsConfig.predeploy, + ['npm --prefix "$RESOURCE_DIR" run build'], + )) { + reject(); + } +} + +function validateFunctionExports(functionIndex) { + if (typeof functionIndex !== 'string' || functionIndex.length === 0) reject(); + + const exportedNames = [...functionIndex.matchAll( + /^exports\.([A-Za-z][A-Za-z0-9_]*)\s*=/gmu, + )].map((match) => match[1]); + + const exactAssignments = [ + 'exports.createMemberOnSignUp = onSignUp;', + 'exports.ensureMemberProfile = ensureMemberProfile;', + ]; + for (const [index, functionId] of FUNCTION_IDS.entries()) { + if (exportedNames.filter((name) => name === functionId).length !== 1 + || functionIndex.split(exactAssignments[index]).length !== 2) { + reject(); + } + } +} + +function validatePackageContract(packageJson, packageLock) { + if (!isPlainObject(packageJson) + || packageJson.scripts?.['test:firebase-profile-functions-staging'] + !== TEST_SCRIPT + || packageJson.scripts?.['predeploy:staging-profile-functions'] + !== PREDEPLOY_SCRIPT + || packageJson.scripts?.['deploy:staging-profile-functions'] + !== DEPLOY_SCRIPT + || packageJson.devDependencies?.['firebase-tools'] !== FIREBASE_CLI_VERSION + || packageLock?.packages?.['']?.devDependencies?.['firebase-tools'] + !== FIREBASE_CLI_VERSION + || packageLock?.packages?.['node_modules/firebase-tools']?.version + !== FIREBASE_CLI_VERSION) { + reject(); + } +} + +function validateSourceContract({ + firebaseConfig, + aliases, + functionIndex, + packageJson, + packageLock, +}) { + validateFunctionsConfig(firebaseConfig); + if (!hasExactKeys(aliases, ['projects']) + || !hasExactKeys(aliases.projects, ['default']) + || aliases.projects.default !== DEFAULT_PROJECT_ID) { + reject(); + } + validateFunctionExports(functionIndex); + validatePackageContract(packageJson, packageLock); + + return Object.freeze({ + account: CLUB_ACCOUNT, + cliVersion: FIREBASE_CLI_VERSION, + defaultProject: DEFAULT_PROJECT_ID, + functions: FUNCTION_IDS, + projectId: STAGING_PROJECT_ID, + scope: DEPLOY_SCOPE, + }); +} + +function validateDeployEnvironment(environment = process.env) { + if (requiredValue(environment, 'GCLOUD_PROJECT') !== STAGING_PROJECT_ID + || requiredValue(environment, 'GOOGLE_CLOUD_QUOTA_PROJECT') + !== STAGING_PROJECT_ID + || requiredValue(environment, 'FIREBASE_DEPLOY_SCOPE') !== DEPLOY_SCOPE + || requiredValue(environment, 'RUN_MPRC_FIREBASE_ACCOUNT') !== CLUB_ACCOUNT + || requiredValue(environment, 'FIREBASE_TOKEN').length < 16 + || (environment.GOOGLE_APPLICATION_CREDENTIALS !== undefined + && environment.GOOGLE_APPLICATION_CREDENTIALS !== '') + || (environment.GOOGLE_GHA_CREDS_PATH !== undefined + && environment.GOOGLE_GHA_CREDS_PATH !== '') + || (environment.CLOUDSDK_AUTH_CREDENTIAL_FILE_OVERRIDE !== undefined + && environment.CLOUDSDK_AUTH_CREDENTIAL_FILE_OVERRIDE !== '') + || (environment.FIREBASE_SERVICE_ACCOUNT !== undefined + && environment.FIREBASE_SERVICE_ACCOUNT !== '')) { + reject(); + } + + return Object.freeze({ + account: CLUB_ACCOUNT, + projectId: STAGING_PROJECT_ID, + scope: DEPLOY_SCOPE, + }); +} + +function readJson(root, relativePath) { + try { + return JSON.parse(fs.readFileSync(path.join(root, relativePath), 'utf8')); + } catch { + return reject(); + } +} + +function validateRepository(root = path.resolve(__dirname, '..')) { + return validateSourceContract({ + firebaseConfig: readJson(root, 'firebase.json'), + aliases: readJson(root, '.firebaserc'), + functionIndex: fs.readFileSync(path.join(root, 'functions/index.js'), 'utf8'), + packageJson: readJson(root, 'package.json'), + packageLock: readJson(root, 'package-lock.json'), + }); +} + +function firebaseDeployArguments() { + return Object.freeze([ + '--no-install', + 'firebase', + 'deploy', + '--only', + DEPLOY_SCOPE, + '--project', + STAGING_PROJECT_ID, + '--non-interactive', + ]); +} + +function deploy(environment = process.env, spawn = spawnSync) { + validateRepository(); + validateDeployEnvironment(environment); + + const executable = process.platform === 'win32' ? 'npx.cmd' : 'npx'; + const version = spawn( + executable, + ['--no-install', 'firebase', '--version'], + { encoding: 'utf8', env: environment }, + ); + if (version.error + || version.status !== 0 + || version.stdout.trim() !== FIREBASE_CLI_VERSION) { + reject(); + } + + const result = spawn(executable, firebaseDeployArguments(), { + env: environment, + stdio: 'inherit', + }); + if (result.error || !Number.isInteger(result.status)) return 1; + return result.status; +} + +function runCli(args = process.argv.slice(2)) { + if (args.length === 1 && args[0] === 'validate') { + validateRepository(); + process.stdout.write('firebase_profile_functions_staging_contract_valid\n'); + return 0; + } + if (args.length === 1 && args[0] === 'validate-deploy') { + validateRepository(); + validateDeployEnvironment(); + process.stdout.write('firebase_profile_functions_staging_deploy_contract_valid\n'); + return 0; + } + if (args.length === 1 && args[0] === 'deploy') return deploy(); + return reject(); +} + +if (require.main === module) { + try { + process.exitCode = runCli(); + } catch { + process.stderr.write(`${REJECTION}\n`); + process.exitCode = 1; + } +} + +module.exports = { + DEPLOY_SCOPE, + FIREBASE_CLI_VERSION, + FUNCTION_IDS, + deploy, + firebaseDeployArguments, + validateDeployEnvironment, + validateSourceContract, +}; diff --git a/tests/firebase-profile-functions-staging-contract.test.js b/tests/firebase-profile-functions-staging-contract.test.js new file mode 100644 index 0000000..7e469cc --- /dev/null +++ b/tests/firebase-profile-functions-staging-contract.test.js @@ -0,0 +1,286 @@ +'use strict'; + +const assert = require('node:assert/strict'); +const { spawnSync } = require('node:child_process'); +const fs = require('node:fs'); +const path = require('node:path'); +const test = require('node:test'); + +const ROOT = path.resolve(__dirname, '..'); +const CONTRACT_PATH = path.join( + ROOT, + 'scripts/firebase-profile-functions-staging-contract.js', +); +const VALID_DEPLOY_ENVIRONMENT = Object.freeze({ + GCLOUD_PROJECT: 'run-mprc-staging', + GOOGLE_CLOUD_QUOTA_PROJECT: 'run-mprc-staging', + FIREBASE_DEPLOY_SCOPE: 'functions:createMemberOnSignUp,functions:ensureMemberProfile', + RUN_MPRC_FIREBASE_ACCOUNT: 'runmprc@gmail.com', + FIREBASE_TOKEN: 'synthetic-short-lived-credential', +}); + +function read(relativePath) { + return fs.readFileSync(path.join(ROOT, relativePath), 'utf8'); +} + +function readJson(relativePath) { + return JSON.parse(read(relativePath)); +} + +function clone(value) { + return JSON.parse(JSON.stringify(value)); +} + +function loadContract() { + // eslint-disable-next-line global-require, import/no-unresolved + return require('../scripts/firebase-profile-functions-staging-contract'); +} + +function sourceContract(overrides = {}) { + return { + firebaseConfig: readJson('firebase.json'), + aliases: readJson('.firebaserc'), + functionIndex: read('functions/index.js'), + packageJson: readJson('package.json'), + packageLock: readJson('package-lock.json'), + ...overrides, + }; +} + +test('CI-001D3 accepts only the exact staging profile Function source contract', () => { + const { validateSourceContract } = loadContract(); + + assert.deepEqual(validateSourceContract(sourceContract()), { + account: 'runmprc@gmail.com', + cliVersion: '15.24.0', + defaultProject: 'demo-mprc-local', + functions: ['createMemberOnSignUp', 'ensureMemberProfile'], + projectId: 'run-mprc-staging', + scope: 'functions:createMemberOnSignUp,functions:ensureMemberProfile', + }); +}); + +test('CI-001D3 rejects a changed deploy alias or Functions codebase', () => { + const { validateSourceContract } = loadContract(); + const firebaseConfig = readJson('firebase.json'); + + for (const defaultProject of [ + 'run-mprc-staging', + 'mid-peninsula-running-club', + 'unexpected-project', + ]) { + assert.throws( + () => validateSourceContract(sourceContract({ + aliases: { projects: { default: defaultProject } }, + })), + /firebase_profile_functions_staging_contract_rejected/u, + ); + } + + const invalidFunctions = [ + [], + [{ ...firebaseConfig.functions[0], source: 'another-source' }], + [{ ...firebaseConfig.functions[0], codebase: 'another-codebase' }], + [firebaseConfig.functions[0], clone(firebaseConfig.functions[0])], + [{ ...firebaseConfig.functions[0], extra: true }], + ]; + invalidFunctions.forEach((functions) => { + assert.throws( + () => validateSourceContract(sourceContract({ + firebaseConfig: { ...clone(firebaseConfig), functions }, + })), + /firebase_profile_functions_staging_contract_rejected/u, + ); + }); +}); + +test('CI-001D3 rejects missing or redirected profile exports', () => { + const { validateSourceContract } = loadContract(); + const functionIndex = read('functions/index.js'); + const invalidSources = [ + functionIndex.replace('exports.createMemberOnSignUp = onSignUp;', ''), + functionIndex.replace( + 'exports.ensureMemberProfile = ensureMemberProfile;', + 'exports.ensureMemberProfile = updateMemberRole;', + ), + `${functionIndex}\nexports.ensureMemberProfile = ensureMemberProfile;\n`, + ]; + + invalidSources.forEach((invalid) => { + assert.throws( + () => validateSourceContract(sourceContract({ functionIndex: invalid })), + /firebase_profile_functions_staging_contract_rejected/u, + ); + }); +}); + +test('CI-001D3 rejects changed scripts or an unpinned Firebase CLI', () => { + const { validateSourceContract } = loadContract(); + const packageJson = readJson('package.json'); + const packageLock = readJson('package-lock.json'); + const invalidPackages = [ + { + ...clone(packageJson), + scripts: { + ...packageJson.scripts, + 'deploy:staging-profile-functions': 'firebase deploy --only functions', + }, + }, + { + ...clone(packageJson), + devDependencies: { ...packageJson.devDependencies, 'firebase-tools': 'latest' }, + }, + ]; + + invalidPackages.forEach((invalid) => { + assert.throws( + () => validateSourceContract(sourceContract({ packageJson: invalid })), + /firebase_profile_functions_staging_contract_rejected/u, + ); + }); + + const invalidLock = clone(packageLock); + invalidLock.packages['node_modules/firebase-tools'].version = '15.23.0'; + assert.throws( + () => validateSourceContract(sourceContract({ packageLock: invalidLock })), + /firebase_profile_functions_staging_contract_rejected/u, + ); +}); + +test('CI-001D3 deploy context is exact, two-Function-only, and token-bound', () => { + const { validateDeployEnvironment } = loadContract(); + + assert.deepEqual(validateDeployEnvironment(VALID_DEPLOY_ENVIRONMENT), { + account: 'runmprc@gmail.com', + projectId: 'run-mprc-staging', + scope: 'functions:createMemberOnSignUp,functions:ensureMemberProfile', + }); + + const invalidEnvironments = [ + { ...VALID_DEPLOY_ENVIRONMENT, GCLOUD_PROJECT: 'mid-peninsula-running-club' }, + { ...VALID_DEPLOY_ENVIRONMENT, GOOGLE_CLOUD_QUOTA_PROJECT: 'another-project' }, + { ...VALID_DEPLOY_ENVIRONMENT, FIREBASE_DEPLOY_SCOPE: 'functions' }, + { ...VALID_DEPLOY_ENVIRONMENT, FIREBASE_DEPLOY_SCOPE: 'functions:ensureMemberProfile' }, + { ...VALID_DEPLOY_ENVIRONMENT, FIREBASE_DEPLOY_SCOPE: `${VALID_DEPLOY_ENVIRONMENT.FIREBASE_DEPLOY_SCOPE},functions:stripeWebhook` }, + { ...VALID_DEPLOY_ENVIRONMENT, FIREBASE_DEPLOY_SCOPE: `${VALID_DEPLOY_ENVIRONMENT.FIREBASE_DEPLOY_SCOPE},firestore:rules` }, + { ...VALID_DEPLOY_ENVIRONMENT, FIREBASE_DEPLOY_SCOPE: 'auth' }, + { ...VALID_DEPLOY_ENVIRONMENT, FIREBASE_DEPLOY_SCOPE: 'hosting' }, + { ...VALID_DEPLOY_ENVIRONMENT, FIREBASE_DEPLOY_SCOPE: 'storage' }, + { ...VALID_DEPLOY_ENVIRONMENT, RUN_MPRC_FIREBASE_ACCOUNT: 'another@example.invalid' }, + { ...VALID_DEPLOY_ENVIRONMENT, FIREBASE_TOKEN: '' }, + { ...VALID_DEPLOY_ENVIRONMENT, GOOGLE_APPLICATION_CREDENTIALS: '/tmp/credential.json' }, + { ...VALID_DEPLOY_ENVIRONMENT, GOOGLE_GHA_CREDS_PATH: '/tmp/github-credential.json' }, + { ...VALID_DEPLOY_ENVIRONMENT, CLOUDSDK_AUTH_CREDENTIAL_FILE_OVERRIDE: '/tmp/gcloud-credential.json' }, + { ...VALID_DEPLOY_ENVIRONMENT, FIREBASE_SERVICE_ACCOUNT: '{}' }, + ]; + + invalidEnvironments.forEach((invalid) => { + assert.throws( + () => validateDeployEnvironment(invalid), + /firebase_profile_functions_staging_contract_rejected/u, + ); + }); +}); + +test('CI-001D3 pins an argument-closed two-Function Firebase command in CI', () => { + const { + DEPLOY_SCOPE, + FIREBASE_CLI_VERSION, + FUNCTION_IDS, + firebaseDeployArguments, + } = loadContract(); + const packageJson = readJson('package.json'); + const ci = read('.github/workflows/ci.yml'); + + assert.equal(FIREBASE_CLI_VERSION, '15.24.0'); + assert.deepEqual(FUNCTION_IDS, [ + 'createMemberOnSignUp', + 'ensureMemberProfile', + ]); + assert.equal( + DEPLOY_SCOPE, + 'functions:createMemberOnSignUp,functions:ensureMemberProfile', + ); + assert.deepEqual(firebaseDeployArguments(), [ + '--no-install', + 'firebase', + 'deploy', + '--only', + DEPLOY_SCOPE, + '--project', + 'run-mprc-staging', + '--non-interactive', + ]); + assert.equal( + packageJson.scripts['deploy:staging-profile-functions'], + 'node scripts/firebase-profile-functions-staging-contract.js deploy', + ); + assert.match(ci, /tests\/firebase-profile-functions-staging-contract\.test\.js/u); + assert.doesNotMatch(ci, /deploy:staging-profile-functions|FIREBASE_TOKEN/u); +}); + +test('CI-001D3 checks the lockfile CLI before invoking the exact deployment', () => { + const { deploy, firebaseDeployArguments } = loadContract(); + const calls = []; + const fakeSpawn = (executable, args, options) => { + calls.push({ executable, args, stdio: options.stdio }); + if (args.includes('--version')) return { status: 0, stdout: '15.24.0\n' }; + return { status: 0 }; + }; + + assert.equal(deploy(VALID_DEPLOY_ENVIRONMENT, fakeSpawn), 0); + assert.equal(calls.length, 2); + assert.deepEqual(calls[0].args, [ + '--no-install', + 'firebase', + '--version', + ]); + assert.equal(calls[0].stdio, undefined); + assert.deepEqual(calls[1].args, firebaseDeployArguments()); + assert.equal(calls[1].stdio, 'inherit'); + + assert.throws( + () => deploy(VALID_DEPLOY_ENVIRONMENT, () => ({ + status: 0, + stdout: '99.0.0\n', + })), + /firebase_profile_functions_staging_contract_rejected/u, + ); +}); + +test('CI-001D3 CLI failures are fixed and never attempt deployment with extra arguments', () => { + const canary = 'private-profile-functions-context-canary'; + const invalidContext = spawnSync( + process.execPath, + [CONTRACT_PATH, 'validate-deploy'], + { + cwd: ROOT, + encoding: 'utf8', + env: { + ...process.env, + ...VALID_DEPLOY_ENVIRONMENT, + GCLOUD_PROJECT: canary, + FIREBASE_TOKEN: canary, + }, + }, + ); + const unexpectedArgument = spawnSync( + process.execPath, + [CONTRACT_PATH, 'deploy', '--only', 'functions'], + { + cwd: ROOT, + encoding: 'utf8', + env: { ...process.env, ...VALID_DEPLOY_ENVIRONMENT }, + }, + ); + + for (const result of [invalidContext, unexpectedArgument]) { + assert.equal(result.status, 1); + assert.equal(result.stdout, ''); + assert.equal( + result.stderr, + 'firebase_profile_functions_staging_contract_rejected\n', + ); + assert.doesNotMatch(`${result.stdout}${result.stderr}`, new RegExp(canary, 'u')); + } +}); diff --git a/tests/test-artifact-safety.test.js b/tests/test-artifact-safety.test.js index 825c185..8b819da 100644 --- a/tests/test-artifact-safety.test.js +++ b/tests/test-artifact-safety.test.js @@ -26,7 +26,7 @@ const temporaryDirectories = []; const JOB_ID = 'test-artifact-scrubber'; const JOB_NAME = 'Test artifact scrubber'; const TEST_COMMAND = 'node --test tests/test-artifact-safety.test.js'; -const FRONTEND_TEST_COMMAND = 'node --test tests/ci-workflow.test.js tests/release-workflow.test.js tests/firebase-release-verification.test.js tests/firebase-hosting-foundation.test.js tests/firebase-auth-staging-contract.test.js tests/staging-authority-workflow.test.js tests/test-artifact-safety.test.js tests/root-dependency-security.test.js'; +const FRONTEND_TEST_COMMAND = 'node --test tests/ci-workflow.test.js tests/release-workflow.test.js tests/firebase-release-verification.test.js tests/firebase-hosting-foundation.test.js tests/firebase-auth-staging-contract.test.js tests/firebase-profile-functions-staging-contract.test.js tests/staging-authority-workflow.test.js tests/test-artifact-safety.test.js tests/root-dependency-security.test.js'; const NEVER_RUN = ['$', '{{ false }}'].join(''); const ALWAYS_TRUE = ['$', '{{ true }}'].join(''); const BRACKET_SECRET = ['$', "{{ secrets['SYNTHETIC_CANARY'] }}"].join(''); @@ -1324,6 +1324,13 @@ test('frontend independently runs the exact workflow safety suite without a skip assert.notEqual(missingAuthTest, ciWorkflow); assert.notDeepEqual(frontendValidationErrors(missingAuthTest), []); + const missingProfileFunctionsTest = ciWorkflow.replace( + ' tests/firebase-profile-functions-staging-contract.test.js', + '', + ); + assert.notEqual(missingProfileFunctionsTest, ciWorkflow); + assert.notDeepEqual(frontendValidationErrors(missingProfileFunctionsTest), []); + const swallowedValidation = ciWorkflow.replace( FRONTEND_TEST_COMMAND, `${FRONTEND_TEST_COMMAND} || true`,