diff --git a/.github/scripts/validate-staging-authority-context.sh b/.github/scripts/validate-staging-authority-context.sh new file mode 100755 index 00000000..ab125b76 --- /dev/null +++ b/.github/scripts/validate-staging-authority-context.sh @@ -0,0 +1,20 @@ +#!/usr/bin/env bash + +set -euo pipefail + +fail() { + printf '%s\n' 'staging_authority_context_invalid' >&2 + exit 1 +} + +[[ "${GITHUB_REPOSITORY:-}" == 'Run-MPRC/Run-MPRC.github.io' ]] || fail +[[ "${GITHUB_REPOSITORY_ID:-}" == '718285092' ]] || fail +[[ "${GITHUB_REPOSITORY_OWNER_ID:-}" == '150727922' ]] || fail +[[ "${GITHUB_REF:-}" == 'refs/heads/main' ]] || fail +[[ "${GITHUB_WORKFLOW_REF:-}" == 'Run-MPRC/Run-MPRC.github.io/.github/workflows/verify-staging-authority.yml@refs/heads/main' ]] || fail +[[ "${TARGET_ENVIRONMENT:-}" == 'staging' ]] || fail +[[ "${TARGET_PROJECT_ID:-}" == 'run-mprc-staging' ]] || fail +[[ "${DEPLOY_SERVICE_ACCOUNT:-}" == 'mprc-staging-deploy@run-mprc-staging.iam.gserviceaccount.com' ]] || fail +[[ "${WORKLOAD_IDENTITY_PROVIDER:-}" == 'projects/384996399199/locations/global/workloadIdentityPools/github-actions/providers/run-mprc-staging' ]] || fail + +printf '%s\n' 'staging_authority_context_valid' diff --git a/.github/scripts/verify-staging-project-read.sh b/.github/scripts/verify-staging-project-read.sh new file mode 100755 index 00000000..375a5f03 --- /dev/null +++ b/.github/scripts/verify-staging-project-read.sh @@ -0,0 +1,28 @@ +#!/usr/bin/env bash + +set -euo pipefail + +fail() { + printf '%s\n' 'staging_authority_read_failed' >&2 + exit 1 +} + +[[ "${TARGET_PROJECT_ID:-}" == 'run-mprc-staging' ]] || fail +[[ -n "${GCP_ACCESS_TOKEN:-}" ]] || fail + +response_file="$(mktemp)" +trap 'rm -f "$response_file"' EXIT + +curl --fail --silent --show-error --request GET \ + --header "Authorization: Bearer ${GCP_ACCESS_TOKEN}" \ + --header 'Accept: application/json' \ + 'https://cloudresourcemanager.googleapis.com/v3/projects/run-mprc-staging' \ + --output "$response_file" || fail + +jq -e ' + .projectId == "run-mprc-staging" + and .name == "projects/384996399199" + and .state == "ACTIVE" +' "$response_file" >/dev/null || fail + +printf '%s\n' 'staging_authority_read_verified' diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7713d0bb..eb545b45 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -37,8 +37,9 @@ jobs: - name: Validate protected release workflow run: | 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/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/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/workflows/verify-staging-authority.yml b/.github/workflows/verify-staging-authority.yml new file mode 100644 index 00000000..798ff51d --- /dev/null +++ b/.github/workflows/verify-staging-authority.yml @@ -0,0 +1,50 @@ +name: Verify staging cloud authority + +run-name: Verify protected staging authority from ${{ github.sha }} + +on: + workflow_dispatch: + +permissions: {} + +concurrency: + group: verify-staging-authority + cancel-in-progress: false + +jobs: + verify-staging-authority: + name: Read exact staging project through protected OIDC + runs-on: ubuntu-latest + timeout-minutes: 5 + environment: + name: staging + permissions: + contents: read + id-token: write + env: + TARGET_ENVIRONMENT: staging + TARGET_PROJECT_ID: ${{ vars.FIREBASE_PROJECT_ID }} + steps: + - name: Check out reviewed authority verifier + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6 + with: + persist-credentials: false + - name: Validate immutable repository and protected environment context + env: + DEPLOY_SERVICE_ACCOUNT: ${{ secrets.GCP_DEPLOY_SERVICE_ACCOUNT }} + WORKLOAD_IDENTITY_PROVIDER: ${{ secrets.GCP_WORKLOAD_IDENTITY_PROVIDER }} + run: .github/scripts/validate-staging-authority-context.sh + - id: auth + name: Obtain one short-lived read-only Google Cloud token + uses: google-github-actions/auth@7c6bc770dae815cd3e89ee6cdf493a5fab2cc093 # v3 + with: + token_format: access_token + access_token_lifetime: 300s + create_credentials_file: false + export_environment_variables: false + workload_identity_provider: ${{ secrets.GCP_WORKLOAD_IDENTITY_PROVIDER }} + service_account: ${{ secrets.GCP_DEPLOY_SERVICE_ACCOUNT }} + - name: Read and verify only the exact staging project identity + env: + GCP_ACCESS_TOKEN: ${{ steps.auth.outputs.access_token }} + run: .github/scripts/verify-staging-project-read.sh diff --git a/GITHUB_ISSUES.md b/GITHUB_ISSUES.md index b56191be..789dea25 100644 --- a/GITHUB_ISSUES.md +++ b/GITHUB_ISSUES.md @@ -144,6 +144,8 @@ Do not redesign routing or hosting in this issue; WEB-001 owns that. WEB-001A1 [ **Status:** Published as tracker [#105](https://github.com/Run-MPRC/Run-MPRC.github.io/issues/105). [#103](https://github.com/Run-MPRC/Run-MPRC.github.io/issues/103), [#124](https://github.com/Run-MPRC/Run-MPRC.github.io/issues/124), and [#126](https://github.com/Run-MPRC/Run-MPRC.github.io/issues/126) provide the local/hosted frontend and SPA baselines. CI-001B3 [#167](https://github.com/Run-MPRC/Run-MPRC.github.io/issues/167) owns the dedicated hosted command-journal Firestore transaction gate. CI-001B4 [#186](https://github.com/Run-MPRC/Run-MPRC.github.io/issues/186) merged through [PR #189](https://github.com/Run-MPRC/Run-MPRC.github.io/pull/189) as exact main `bec7d5e365eacb418563a172029f241f660d9768`; exact PR run `29291402007` and post-merge run `29291515653` passed. CI-001B4A [#227](https://github.com/Run-MPRC/Run-MPRC.github.io/issues/227), CI-001B4B [#239](https://github.com/Run-MPRC/Run-MPRC.github.io/issues/239), and PAY-004C1 [#359](https://github.com/Run-MPRC/Run-MPRC.github.io/issues/359) are focused reviewed reductions of that ledger. [PR #391](https://github.com/Run-MPRC/Run-MPRC.github.io/pull/391) and [PR #392](https://github.com/Run-MPRC/Run-MPRC.github.io/pull/392) add three TypeScript files while retiring four TSX errors during functional work. CI-001B4C [#449](https://github.com/Run-MPRC/Run-MPRC.github.io/issues/449) retires four stale unknown-rule directive records through comment-only same-line substitutions. MEMBERS-CONTENT-001B [#492](https://github.com/Run-MPRC/Run-MPRC.github.io/issues/492) removes one finding-free dormant JSX file. The gate now records 110 files, 113 configured errors, and 6 warnings. [#135](https://github.com/Run-MPRC/Run-MPRC.github.io/issues/135) owns the manual exact-commit, backend-first source gate and Git-triggered Netlify production containment. [#133](https://github.com/Run-MPRC/Run-MPRC.github.io/issues/133) owns protected OIDC/environment configuration, and [#136](https://github.com/Run-MPRC/Run-MPRC.github.io/issues/136) owns the actual profile-recovery release. Branch-required-check configuration, scanning, staging/live evidence, remaining lint-debt cleanup, and protected Netlify publication remain open, so the pipeline is still non-compliant overall. **Depends on:** SAFETY-001 +**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. + ### Problem Before #103, frontend tests failed because the Jest environment lacked `TextEncoder`. That child repaired the local baseline, and #124 adds it to hosted CI. #167 originally added a separate named Node 20/Java 17 job for the PAY-002B2A Firestore transaction suite and made both protected-release CI reads require it. SUPPLY-001D9/#455 moves the current source to Java 21 and exact Firebase CLI 15.24.0 while preserving one explicit demo project, Firestore-only scope, committed lockfiles, and no cloud/provider authority. CI-001B4/#186 replaces the mutating, ignored `lint:fix` step with a blocking exact-record check that initially covered 102 JS/JSX/TS/TSX files. It disables the repository-wide severity-masking `eslint-plugin-only-warn` hook inside the lint process. CI-001B4A/#227 removes exactly one reviewed `arrow-body-style` record without changing the rule or gate. CI-001B4B/#239 removes exactly one stale `AdminMembers` unknown-rule suppression record without changing executable statements, the rule set, or the gate. PAY-004C1/#359 removes one `no-alert` warning and two label-association errors by removing the obsolete reusable-link prompt and amount/tier controls. Intervening reviewed source additions expanded coverage from 102 to 108 files by #359. PR #391 then adds one TypeScript file and removes two TSX errors; PR #392 adds two TypeScript files and removes two more TSX errors. CI-001B4C/#449 replaces the four remaining stale `react-hooks/exhaustive-deps` directives with ordinary comments on the same lines, preserving every executable statement and dependency array. MEMBERS-CONTENT-001B/#492 then removes one finding-free dormant JSX file. The gate therefore scans 110 files and records 113 configured errors and 6 warnings as reviewable individual records. Any added, removed, moved, or changed record and every fatal parser/configuration failure rejects with one fixed diagnostic. The checker runs directly before application tests in an exact prefix that pins the hosted runner, read-only permission, credential-free checkout, Node version, lifecycle-disabled install, lint, and adjacent clean check. Global overrides and prefix changes reject. The clean check compares tracked files with `HEAD` plus non-ignored untracked files; ignored dependency/build paths are outside that proof. This is an explicit debt baseline, not a clean-lint claim. #135 removes the automatic/fail-open release source defects: merge and release are separate, exact CI checks are re-read, release targets are fixed, OIDC replaces JSON-key wiring, lockfile tooling is used, backend precedes Pages, and Git-triggered Netlify production builds stop. The protected identity/environments and actual staged/live release remain outside that source slice. diff --git a/GITHUB_ISSUE_SLICES.md b/GITHUB_ISSUE_SLICES.md index 66cd8dbf..0bb93992 100644 --- a/GITHUB_ISSUE_SLICES.md +++ b/GITHUB_ISSUE_SLICES.md @@ -20,6 +20,7 @@ An agent-ready child has one observable outcome, one trust boundary, explicit de | CI-001B1 | merged/closed as #124; CI-001A | Run the complete committed frontend Jest suite as a named blocking step in the hosted frontend CI job; update exact officer/engineering truth. | Exact pull-request and post-merge hosted steps passed; Firebase skipped and Netlify exact production commit remained unverified. | | CI-001B1A | merged/closed as #126; #99 and CI-001B1 | Run the standalone same-origin SPA callback suite as a separate blocking hosted frontend step. | Local deliberate-failure proof plus the named hosted step passing on the exact pull-request and post-merge commits; no provider callback claim. | | CI-001B2 | published as #135; CI-001B1 | Provide the tested manual exact-commit source gate: fixed Rules/Functions, lockfile CLI, pinned Actions, short-lived-auth wiring, backend-first Pages dependency, missing-config failure, no frontend credential, and Netlify Git-production containment. | Source/static negative tests and exact PR evidence. #133 separately owns protected OIDC/environment configuration; #136 owns staged/target release; WEB-001 owns protected Netlify publication. | +| CI-001C1 | in progress as [#667](https://github.com/Run-MPRC/Run-MPRC.github.io/issues/667); CI-001B2, WEB-001A2/#665 | Configure staging-only exact-repository/ref/environment/workflow OIDC, one keyless principal with project-identity read permission, two named environment reviewers, and a separate protected read-only verifier. | Source negative tests, cloud/environment readback, zero user-managed keys, one protected exact-main project-read run, and a wrong-ref rejection; no Firebase deploy, billing, backend, provider sandbox, or production claim. | | CI-001B3 | source/tests tracked in live [#167](https://github.com/Run-MPRC/Run-MPRC.github.io/issues/167), with current Java 21 compatibility tracked in [#455](https://github.com/Run-MPRC/Run-MPRC.github.io/issues/455); PAY-002B2A/#165 | Run the dedicated command-journal Firestore transaction suite as one named blocking Node 20/Java 21 job with exact demo project, explicit opt-in, Firestore-only scope, committed tooling, and no secrets/provider authority; require it at both protected-release CI reads. | Static negative workflow tests plus exact pull-request and post-merge hosted evidence; no Firebase deployment, Stripe, website, or live claim. | | CI-001B4 | merged/closed through [#186](https://github.com/Run-MPRC/Run-MPRC.github.io/issues/186) / [PR #189](https://github.com/Run-MPRC/Run-MPRC.github.io/pull/189) as `bec7d5e365eacb418563a172029f241f660d9768`; focused debt reductions tracked in [#227](https://github.com/Run-MPRC/Run-MPRC.github.io/issues/227), [#239](https://github.com/Run-MPRC/Run-MPRC.github.io/issues/239), functional removal in [#359](https://github.com/Run-MPRC/Run-MPRC.github.io/issues/359), functional/file-count changes in [PR #391](https://github.com/Run-MPRC/Run-MPRC.github.io/pull/391) and [PR #392](https://github.com/Run-MPRC/Run-MPRC.github.io/pull/392), CI-001B4C [#449](https://github.com/Run-MPRC/Run-MPRC.github.io/issues/449), and dormant-renderer removal [#492](https://github.com/Run-MPRC/Run-MPRC.github.io/issues/492); CI-001B1 | Replace the mutating fail-open frontend lint line with a named blocking JS/JSX/TS/TSX exact-record gate. The original gate covered 102 files with 125 configured errors and 7 warnings. #227 removes one reviewed `arrow-body-style` error, #239 removes one stale `AdminMembers` suppression error, and #359 removes one prompt warning plus two label errors by deleting the unsafe reusable-link controls. Intervening reviewed source additions expanded coverage to 108 files by #359. PR #391 adds one TypeScript file and removes two TSX errors; PR #392 adds two TypeScript files and removes two more TSX errors. #449 replaces the four remaining stale `react-hooks/exhaustive-deps` directives with ordinary comments on the same lines. #492 removes one finding-free dormant JSX file. The gate now scans 110 files with 113 errors and 6 warnings. Disable the repository-wide severity-masking hook inside the lint process. Any added, removed, moved, or changed record and every fatal parser/configuration failure rejects with a fixed diagnostic; lint may not rewrite tracked files or create non-ignored untracked files. | Exact #186 PR run `29291402007` and post-merge run `29291515653` passed lifecycle-disabled install, direct lint, clean proof, tests, and build; each later focused issue records its separate source/hosted evidence. Ignored dependency/build paths remain outside this proof, with no clean-lint, branch-protection, deployment, website, Firebase, or provider claim. | | SUPPLY-001A | published as [#127](https://github.com/Run-MPRC/Run-MPRC.github.io/issues/127); CI-001A | Prove `gpxparser` unused and remove its obsolete transitive chain. | `rg`/tree proof, before/after audits, root lockfile diff, full frontend checks. | diff --git a/IMPLEMENTATION_PLAN.md b/IMPLEMENTATION_PLAN.md index b36350e6..b14ed430 100644 --- a/IMPLEMENTATION_PLAN.md +++ b/IMPLEMENTATION_PLAN.md @@ -40,6 +40,8 @@ MPRC can open a race or merchandise item for sale only when the platform can: **WEB-001A2 isolated static staging:** [#665](https://github.com/Run-MPRC/Run-MPRC.github.io/issues/665) creates the club-owned `run-mprc-staging` Firebase project, one web app, and the static [run-mprc-staging.web.app](https://run-mprc-staging.web.app) Hosting site from exact merged source `a614c68d9a1a2be631a3be874a686d61e5d170a0`. The executable artifact scanner found the staging identity and rejected known production identities; provider, direct-route, desktop, and phone readback passed. This is signed-out visual staging only. It does not configure App Check, Auth test users, Firestore, Functions, Rules, indexes, Storage data, provider sandboxes, protected short-lived authority, a release marker, tested predecessor rollback, security headers, `production-web`, custom domains, DNS/TLS, or production publication. +**CI-001C1 protected staging identity proof:** [#667](https://github.com/Run-MPRC/Run-MPRC.github.io/issues/667) creates the protected `staging` GitHub environment, two named individual reviewers, exact-`main` policy, a dedicated GitHub OIDC pool/provider, and a keyless service account with only `resourcemanager.projects.get`. Its separate manual verifier fails on a wrong repository, immutable repository/owner ID, ref, environment, project, workflow, provider, or service account before authentication; after approval it reads only the exact staging project identity. It does not grant Firebase deployment roles, provision Auth/Firestore/Functions, configure outside providers, protect production, or make `.github/workflows/deploy.yml` available. + ## 3. Dependency map ```mermaid diff --git a/OFFICER_START_HERE.md b/OFFICER_START_HERE.md index 0c193a5f..1275195f 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). This is an **ENGINEERING STAGING SITE ONLY**. Desktop and phone public-route checks passed, but staging has no approved backend/provider test estate, App Check key, protected automatic release, release marker, tested predecessor rollback, security-header policy, custom domain, or officer publish control. Do not sign in or enter real information there. Netlify still answers `runmprc.com`; production Firebase Hosting remains empty, and no DNS, GitHub Pages, Netlify, backend, or production-data change occurred. +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. This remains an **ENGINEERING STAGING SITE ONLY**. Desktop and phone public-route checks passed, but staging has no approved backend/provider test estate, App Check key, protected automatic release, release marker, tested predecessor rollback, security-header policy, custom domain, or officer publish control. Do not sign in or enter real information there. Netlify still answers `runmprc.com`; production Firebase Hosting remains empty, and no DNS, GitHub Pages, Netlify, backend, or production-data change occurred. 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 e29c2a3b..72d36133 100644 --- a/OPERATIONS_RUNBOOK.md +++ b/OPERATIONS_RUNBOOK.md @@ -63,11 +63,13 @@ 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` | No provider sandbox or backend test configuration yet | `run-mprc-staging.web.app`; `dev.runmprc.com` not configured | No protected environment yet | Static signed-out engineering Hosting is available; backend/provider staging remains a launch blocker | +| Staging | `run-mprc-staging` | No provider sandbox or backend test configuration yet | `run-mprc-staging.web.app`; `dev.runmprc.com` not configured | `staging`: two reviewers, exact `main`, read-only OIDC verifier | Static signed-out Hosting and protected project-identity proof are available; backend/provider staging remains a launch blocker | | 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: App Check, Auth test configuration, Firestore, Functions, Rules, indexes, Storage data, provider sandboxes, protected deployment authority, `dev.runmprc.com`, release-marker readback, and tested rollback remain unavailable. +CI-001C1 [#667](https://github.com/Run-MPRC/Run-MPRC.github.io/issues/667) adds a separate staging-only authority check. The `staging` environment admits exact `main` and requires one of two named individual organization administrators. The OIDC provider additionally requires immutable organization/repository IDs, the environment, ref, and one verifier workflow. Its keyless service account has no user-managed key and only `resourcemanager.projects.get`; it has no Firebase deployment role. The verifier performs one fixed read of the exact active project identity and emits no response body. This proof does not enable or deploy Auth, Firestore, Rules, indexes, Functions, Hosting, App Check, Storage, an outside provider, or production. + 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 @@ -95,7 +97,7 @@ REACT_APP_STRAVA_CLIENT_ID The Firebase web configuration is public application configuration. It must still be environment-specific and paired with Auth restrictions, Firestore rules, and App Check. WEB-001A1 [#663](https://github.com/Run-MPRC/Run-MPRC.github.io/issues/663) makes controlled optimized builds choose `staging` or `production`; staging project IDs must include a distinct `staging` segment and the complete artifact check rejects known production identities. Production accepts only the exact reviewed public values. These values are not server credentials, but do not paste provider-console screenshots, private links, tokens, or any server credential into source or an issue. -`firebase.json` has a Hosting block for `build/` and missing-path SPA rewrites. Its predeploy chain rejects a Firebase CLI project that differs from the selected browser project, then rebuilds and scans executable JavaScript before upload. **Static engineering staging is available; protected and production publication are NOT AVAILABLE YET.** #665 used that chain for the first `run-mprc-staging` release. No WIF identity, protected production branch, custom domain, release marker, tested rollback, production release, or DNS/TLS change exists. +`firebase.json` has a Hosting block for `build/` and missing-path SPA rewrites. Its predeploy chain rejects a Firebase CLI project that differs from the selected browser project, then rebuilds and scans executable JavaScript before upload. **Static engineering staging and a protected read-only authority check are available; backend and production publication are NOT AVAILABLE YET.** #665 used that chain for the first `run-mprc-staging` release. #667's WIF identity can read only the project identity; it cannot run this deploy chain. No protected production authority, custom domain, release marker, tested rollback, production release, or DNS/TLS change exists. ### App Check release state — NOT AVAILABLE YET @@ -702,7 +704,7 @@ For expand-and-contract changes: 7. Run smoke and reconciliation checks. 8. Observe for the defined period before removing legacy fields/endpoints. -The GitHub release workflow is now manual and exact-current-commit. After protected approval it rechecks `main`, the newest exact CI run, and the retained credential-free artifact before cloud authentication. It fails on missing protected configuration, uses a lockfile Firebase CLI, verifies reviewed Rules and two named profile Functions before publishing the Pages branch, and never gives cloud authority to website preparation or publication. Future Pages artifacts omit the `runmprc.com` CNAME, but the existing Pages provider setting still claims that Netlify-served name until #136/WEB-001 publish and read it back. The source gate is not usable until #133 configures protected environments/OIDC. It does not publish the live Netlify host. Treat the full pipeline as incomplete until #105 closes. +The GitHub release workflow is now manual and exact-current-commit. After protected approval it rechecks `main`, the newest exact CI run, and the retained credential-free artifact before cloud authentication. It fails on missing protected configuration, uses a lockfile Firebase CLI, verifies reviewed Rules and two named profile Functions before publishing the Pages branch, and never gives cloud authority to website preparation or publication. Future Pages artifacts omit the `runmprc.com` CNAME, but the existing Pages provider setting still claims that Netlify-served name until #136/WEB-001 publish and read it back. #667's separate staging verifier proves protected short-lived authentication only; its principal has no deployment permission and `.github/workflows/deploy.yml` still rejects staging. The release source gate remains unavailable until #133 supplies the remaining staging deployment roles and protected production environment/identity. It does not publish the live Netlify host. Treat the full pipeline as incomplete until #105 closes. WEB-PRIVACY-001AA [#582](https://github.com/Run-MPRC/Run-MPRC.github.io/issues/582) changes only the executable Netlify release builder's outer caught-failure output. Source now uses an unbound catch, prints fixed `netlify_release_build_failed`, and preserves the failing exit. Synthetic black-box tests cover ordinary message detail, a hostile message getter, and a hostile prototype trap before any provider or release work can start. This proves only that the wrapper does not inspect or echo its caught value. At the #582 boundary, the child-process runner and two Git reads still inherited subprocess diagnostics; #596 below closes that separate residual inside the builder. The success evidence line, cleanup, authorization, build, artifact, and publication behavior are unchanged. Source, tests, and any merge do not prove a Netlify publication, website change, `runmprc.com` verification, Firebase deployment, provider configuration, production-data change, or live behavior; the fixed code says only that preparation failed. diff --git a/README.md b/README.md index aac22dff..1fbb1bac 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, direct `/events`, and empty-data checks passed. This is static engineering staging only: Auth, Firestore, Functions, App Check enforcement, outside-provider sandboxes, protected short-lived release authority, application security headers, a release marker, and a tested predecessor rollback are not configured. Do not sign in or use real data there. 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, direct `/events`, and empty-data 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. This is still static engineering staging: Auth, Firestore, Functions, App Check enforcement, outside-provider sandboxes, Firebase deployment authority, application security headers, a release marker, and a tested predecessor rollback are not configured. Do not sign in or use real data there. Production Firebase Hosting remains empty and `runmprc.com` remains on its existing Netlify deployment. ## Local setup status @@ -90,7 +90,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). [#133](https://github.com/Run-MPRC/Run-MPRC.github.io/issues/133) still owns protected environment/OIDC configuration; [#136](https://github.com/Run-MPRC/Run-MPRC.github.io/issues/136) owns the actual staged profile-recovery release. Required branch checks, 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. 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, 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). @@ -106,4 +106,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`. The manual workflow is fixed to an exact merged commit and reviewed target set, but it is **NOT AVAILABLE YET** until #133 configures protected environments and short-lived cloud authority. 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. The backend release workflow is fixed to an exact merged commit and reviewed target set, but 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 8bfb3862..19b78dea 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -325,6 +325,8 @@ Backups and exports must honor eventual deletion/anonymization schedules and hav - 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. +CI-001C1 [#667](https://github.com/Run-MPRC/Run-MPRC.github.io/issues/667) establishes the first external proof of this pattern in staging only. The protected `staging` environment requires one of two named individual organization administrators and exact `main`; its GitHub OIDC provider also binds immutable organization/repository IDs, the environment, ref, and one verifier workflow. The dedicated service account has no user-managed key and only a custom `resourcemanager.projects.get` permission. The verifier performs one fixed project `GET` with a five-minute token and emits fixed success/failure markers without response data. This is not Firebase deployment authority, production protection, backend isolation, or permission to inspect staging data. + ## 10. Incident response ### Severity examples diff --git a/SYSTEM_DESIGN.md b/SYSTEM_DESIGN.md index 996371fa..f42031a7 100644 --- a/SYSTEM_DESIGN.md +++ b/SYSTEM_DESIGN.md @@ -233,9 +233,21 @@ AUTH-MAIL-002C2 [#194](https://github.com/Run-MPRC/Run-MPRC.github.io/issues/194 The direct-rewrite hosting path does not need browser storage. #663 adds that catch-all Hosting rewrite to source, but it is **NOT DEPLOYED** and does not remove the current Pages bridge. The existing GitHub Pages fallback from #99 briefly places the complete return route in tab-local `sessionStorage`, then its first root-page script reads and deletes that value before React starts. If the root page never loads, the value can remain until the tab closes. #194 accepts that already-merged residual only for Pages compatibility; it does not call this “memory only.” A verified Firebase Hosting cutover should remove the bridge only after callback and rollback proof. The component itself never writes the code to storage or router state. +**CI-001C1 staging authority boundary:** [#667](https://github.com/Run-MPRC/Run-MPRC.github.io/issues/667) protects a separate manual verifier with the `staging` GitHub environment, two named individual organization administrators, and an exact `main` deployment branch policy. GitHub OIDC is admitted only for immutable organization/repository IDs, that environment, that ref, and `.github/workflows/verify-staging-authority.yml`. The dedicated keyless service account has one custom permission: `resourcemanager.projects.get`. The workflow obtains a five-minute token and reads only the exact active staging project identity. It cannot deploy Firebase, enable an API, inspect Firestore data, create a resource, reach an outside provider, or act in production. + +```mermaid +flowchart LR + Workflow["Manual verifier on exact main"] --> Review["Protected staging review\ntwo named officers"] + Review --> OIDC["GitHub OIDC\nimmutable repo and workflow claims"] + OIDC --> ServiceAccount["Keyless staging service account\none project-read permission"] + ServiceAccount --> ProjectRead["Read exact staging project identity"] +``` + +Text alternative: a manual verifier from exact `main` waits for a named staging reviewer, exchanges its tightly restricted GitHub identity for a short-lived keyless service-account token, and reads only the staging project identity. + ## 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 only the fail-closed build selector and inert Hosting rewrite configuration. Staging project ownership, protected short-lived deployment authority, automatic exact-commit release, readback, rollback, headers, and DNS/TLS cutover remain separate work under #113/#133/#460. The migration is not required to design or test the backend and is not live merely because its source configuration exists. +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. Backend deployment authority, automatic exact-commit release, readback, rollback, headers, production protection, and DNS/TLS cutover remain separate work under #113/#133/#460. The migration is not required to design or test the backend and is not live merely because its source configuration exists. ```mermaid flowchart TB @@ -1720,7 +1732,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 and one web app only | Not configured; no provider testing authorized | `run-mprc-staging.web.app`; `dev.runmprc.com` not configured | Signed-out public visual review only; no real data | +| Staging | `run-mprc-staging`; static Hosting, one web app, and protected read-only OIDC proof | Not configured; no provider testing authorized | `run-mprc-staging.web.app`; `dev.runmprc.com` not configured | Signed-out public visual review plus synthetic identity verification only; no real data | | Production | Dedicated production Firebase project | Live restricted keys and production webhook secret | `runmprc.com` | Real data under documented retention and access policies | ```mermaid diff --git a/docs/officers/ACCESS_CONTINUITY.md b/docs/officers/ACCESS_CONTINUITY.md index d076ef3d..ff8036f2 100644 --- a/docs/officers/ACCESS_CONTINUITY.md +++ b/docs/officers/ACCESS_CONTINUITY.md @@ -38,7 +38,7 @@ Until those six steps are recorded, this guide does not claim incapacitation cov | Club email provider | Notices and password recovery | Communications owner plus backup | | Social/community accounts | Public communication and member groups | Named owner plus backup | -WEB-001A1 [#663](https://github.com/Run-MPRC/Run-MPRC.github.io/issues/663) adds fail-closed Firebase Hosting build source. WEB-001A2 [#665](https://github.com/Run-MPRC/Run-MPRC.github.io/issues/665) records that the club account can reach production and owns the dedicated `run-mprc-staging` project and its static Hosting site. Continuity is still incomplete: name a second individual limited-access Firebase/Google Cloud administrator, configure the short-lived release identity and protected reviewers, and rehearse rollback after a second staging release exists. Do not create another project or use a personal project as a shortcut. +WEB-001A1 [#663](https://github.com/Run-MPRC/Run-MPRC.github.io/issues/663) adds fail-closed Firebase Hosting build source. WEB-001A2 [#665](https://github.com/Run-MPRC/Run-MPRC.github.io/issues/665) records that the club account can reach production and owns the dedicated `run-mprc-staging` project and its static Hosting site. CI-001C1 [#667](https://github.com/Run-MPRC/Run-MPRC.github.io/issues/667) protects a staging-only read-only identity check with individual reviewers Dave Liu and Jeff Chang; neither handles a cloud key. Continuity is still incomplete: name a second individual limited-access Firebase/Google Cloud administrator, protect production, add only reviewed backend deployment roles, and rehearse rollback after a second staging release exists. Do not create another project or use a personal project as a shortcut. ## Release access to record privately @@ -46,7 +46,7 @@ Record these facts without copying a credential or private provider identifier: 1. Dave Liu is the current primary production release approver. 2. Name the backup/security reviewer who can approve when Dave is unavailable. -3. Name two reviewers for the protected `staging` environment. +3. Record Dave Liu (`daliu`) and Jeff Chang (`jeffchang5`) as the two current individual reviewers for the protected `staging` environment. 4. Name two reviewers for the protected `production` environment. 5. Record that `Protected release` is the approved GitHub workflow. 6. Record the fixed environment-to-Firebase-project map: local `demo-mprc-local`, engineering staging `run-mprc-staging`, and production `mid-peninsula-running-club`. @@ -56,7 +56,45 @@ Record these facts without copying a credential or private provider identifier: 10. Record the latest backend-first rollback or safe roll-forward drill. 11. Record the Netlify team owner, live site, protected trigger, and rollback location. -The repository stores none of the cloud identity value, provider path, access token, password, or recovery code. The protected release is **NOT AVAILABLE YET** until #133 completes these records and tests the environment approvals. +The repository stores no access token, password, private key, or recovery code. The protected staging identity check is available after #667; the general protected release is **NOT AVAILABLE YET** until #133 adds reviewed Firebase deployment roles, protects production, and tests the complete release path. + +### Protected staging identity check — available after #667 + +**Purpose:** prove that one reviewed GitHub job can obtain a short-lived staging identity without a downloaded key and read only the approved project identity. + +**Approver:** Dave Liu or Jeff Chang as a named `staging` reviewer. The requester and reviewer should be different people when both are available. + +**Prerequisites:** the request comes from exact `main`; the workflow is **Verify staging cloud authority**; the environment is `staging`; and no backend, website, member, payment, or provider test is part of the request. + +```mermaid +flowchart LR + Request["Platform maintainer requests verifier from main"] --> Review{"Dave or Jeff approves staging?"} + Review -- "No" --> Stop["Stop with no cloud token"] + Review -- "Yes" --> Token["GitHub exchanges exact claims for a five-minute token"] + Token --> Read["Read only the staging project identity"] + Read --> Record["Record fixed success and the run link"] +``` + +Text alternative: the platform maintainer requests the exact verifier from `main`; Dave or Jeff may approve it; approval permits one five-minute keyless token and one project-identity read, while rejection stops before cloud access. + +1. Ask the platform maintainer for the GitHub run link. +2. Confirm the workflow name is **Verify staging cloud authority**. +3. Confirm the source branch shown by GitHub is `main`. +4. Confirm the environment shown by GitHub is `staging`. +5. Confirm the request says **read-only identity check** and names no deployment target. +6. Approve only if every fact above matches. +7. Wait for **Read exact staging project through protected OIDC** to finish. +8. Record the run link, approver, date, source commit, and fixed `staging_authority_read_verified` result. + +**Expected result:** one protected job succeeds after named approval. It deploys nothing and shows no provider locator, access token, cloud response, member data, or secret. + +**Stop conditions:** stop if the branch is not `main`; the environment is not `staging`; the workflow or project is unfamiliar; GitHub shows an unnamed reviewer; any step says deploy, create, update, delete, enable, or write; or anyone asks for a key, token, password, screenshot of private settings, member record, or payment data. + +**Success proof:** retain the public run link, exact commit, named approval, fixed success marker, and a dated private cloud readback stating that the service account has zero user-managed keys and one project-read permission. Do not copy the provider path or raw logs into the public record. + +**Undo:** open a reviewed security issue asking the platform owner to remove the two environment locator secrets and disable the staging federation provider or its service-account binding. Do not delete the Firebase project, service account, or unrelated IAM bindings as an improvised response. A later re-enable must repeat source review and the protected proof. + +**Escalation:** platform owner first; security owner second. If a token, private locator, or unexpected cloud response appears, stop and use the private incident path. ## Private service record diff --git a/docs/officers/PUBLISH_AND_CHECK.md b/docs/officers/PUBLISH_AND_CHECK.md index 5824c355..1259a771 100644 --- a/docs/officers/PUBLISH_AND_CHECK.md +++ b/docs/officers/PUBLISH_AND_CHECK.md @@ -54,13 +54,13 @@ As of **2026-07-13**, with the internal tooling note below checked from source o - Its only current release plan is the reviewed profile-recovery set: Firestore Rules, `createMemberOnSignUp`, and `ensureMemberProfile`. - A caller cannot type a Firebase project or deployment target into the release form. - 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 backend uses a short-lived cloud identity when #133 configures it. The website job receives public browser values only. +- 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. - 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. - A production Pages publication job cannot start until Firebase deployment and Function verification succeed. - The club-owned `run-mprc-staging` project now supplies one static signed-out Hosting surface. Its first release is exact source `a614c68d9a1a2be631a3be874a686d61e5d170a0`; direct `/events` rewriting and desktop/phone public rendering passed. -- #663 source requires complete staging-named public browser configuration and gives CI a synthetic staging artifact scan. #665's provider readback proves only the static staging host. It does not supply protected authority, backend/provider isolation, App Check, a release marker, predecessor rollback, or permission to test sign-in/private/admin/commerce behavior. +- #663 source requires complete staging-named public browser configuration and gives CI a synthetic staging artifact scan. #665's provider readback proves only the static staging host. #667 adds a protected read-only identity check, not Firebase deployment authority. These slices do not supply backend/provider isolation, App Check, a release marker, predecessor rollback, or permission to test sign-in/private/admin/commerce behavior. - `runmprc.com` is served by Netlify, not GitHub Pages. - GitHub Pages currently reports `runmprc.com` as its custom domain and redirects its normal address there. It is not an independently reachable copy today. - Future source stops writing that Pages domain claim. Only provider readback after #136/WEB-001 can prove it cleared. @@ -426,6 +426,30 @@ In words: the release preview and production must contain the same pinned fronte **Escalation:** platform owner first; website/content owner second. Escalate to the security owner if private data or an unexpected application version appears. +## Verify staging cloud identity — AVAILABLE AFTER #667 + +**Purpose:** prove protected keyless staging authentication without deploying or reading application data. + +**Approver:** Dave Liu or Jeff Chang as a named `staging` environment reviewer. + +1. Ask the platform maintainer to request **Verify staging cloud authority** from `main`. +2. Open the run link. +3. Confirm GitHub shows the `staging` environment and exact `main` source. +4. Confirm the run contains only context validation, short-lived authentication, and an exact project-identity read. +5. Approve the environment if those facts match. +6. Confirm the protected job finishes with `staging_authority_read_verified`. +7. Record the run link, commit, approver, and date. + +**Expected result:** the identity check succeeds after approval. Firebase, Hosting, Auth, Firestore, Rules, indexes, Functions, App Check, outside providers, and production remain unchanged. + +**Stop conditions:** stop if GitHub shows another branch, environment, workflow, project, or reviewer; a step proposes a write or deploy; output contains a provider locator, token, or response body; or anyone asks for private data or a credential. + +**Success proof:** keep the run link, exact commit, named approval, and fixed success marker. Pair it with private readback that the service account has no user-managed key and only project-identity read permission. + +**Undo:** ask the platform owner through one reviewed security issue to remove the staging locator secrets and disable the federation provider or service-account binding. Do not delete the Firebase project or edit IAM ad hoc. + +**Escalation:** platform owner, then security owner. Use the private incident path if output may contain a sensitive value. + ## Before a protected release — NOT AVAILABLE YET Do not use this section until #133 records that both GitHub environments are protected and tested. diff --git a/docs/officers/README.md b/docs/officers/README.md index c1d0a706..c5b694ec 100644 --- a/docs/officers/README.md +++ b/docs/officers/README.md @@ -67,7 +67,7 @@ Independent officer publishing to the live Netlify host is **NOT AVAILABLE YET** WEB-002D [#659](https://github.com/Run-MPRC/Run-MPRC.github.io/issues/659) completed one temporary accessibility artifact release on 2026-08-14. Deploy `6a7ece87c5ca4d0007c1a3fc` is live and pins source `7496fe0881fb52908c4ff2f40f488df09c94c908` with only the reviewed visible-focus, phone-menu, and client-side route-focus behavior. Signed-out route/menu checks passed. Repause attempt `6a7ed0ddb00a46000818878d` published nothing, retained the deploy, and left the manifest inactive. #623 deploy `6a7e072f8f346b0008510d29` is the rollback target. Officers performed no terminal, account, Firebase, provider-configuration, or production-data action. The completed audit record is in [Review, merge, release, and check a change](./PUBLISH_AND_CHECK.md). A preview, green check, or merged release control alone does not prove the website changed. -The protected GitHub release is also **NOT AVAILABLE YET** until #133 configures the environment approvers and short-lived cloud identity. Missing authority stops the release with a red failure before Firebase or website publication. +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. 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 b588bbfb..40815688 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-hosting": "node --test tests/firebase-hosting-foundation.test.js", + "test:staging-authority": "node --test tests/staging-authority-workflow.test.js", "test": "react-scripts test", "test:spa-navigation": "node --test tests/spa-navigation.test.js", "test:rules": "firebase emulators:exec --only firestore --project demo-rules-test \"npx --no-install jest --config tests/firestore-rules/jest.config.js\"", diff --git a/tests/staging-authority-workflow.test.js b/tests/staging-authority-workflow.test.js new file mode 100644 index 00000000..fc98bf25 --- /dev/null +++ b/tests/staging-authority-workflow.test.js @@ -0,0 +1,127 @@ +'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 WORKFLOW_PATH = path.join( + ROOT, + '.github/workflows/verify-staging-authority.yml', +); +const CONTEXT_CHECK_PATH = path.join( + ROOT, + '.github/scripts/validate-staging-authority-context.sh', +); +const PROJECT_READ_PATH = path.join( + ROOT, + '.github/scripts/verify-staging-project-read.sh', +); +const workflow = fs.readFileSync(WORKFLOW_PATH, 'utf8'); +const contextCheck = fs.readFileSync(CONTEXT_CHECK_PATH, 'utf8'); +const projectRead = fs.readFileSync(PROJECT_READ_PATH, 'utf8'); + +const VALID_CONTEXT = Object.freeze({ + GITHUB_REPOSITORY: 'Run-MPRC/Run-MPRC.github.io', + GITHUB_REPOSITORY_ID: '718285092', + GITHUB_REPOSITORY_OWNER_ID: '150727922', + GITHUB_REF: 'refs/heads/main', + GITHUB_WORKFLOW_REF: 'Run-MPRC/Run-MPRC.github.io/.github/workflows/verify-staging-authority.yml@refs/heads/main', + TARGET_ENVIRONMENT: 'staging', + TARGET_PROJECT_ID: 'run-mprc-staging', + DEPLOY_SERVICE_ACCOUNT: 'mprc-staging-deploy@run-mprc-staging.iam.gserviceaccount.com', + WORKLOAD_IDENTITY_PROVIDER: 'projects/384996399199/locations/global/workloadIdentityPools/github-actions/providers/run-mprc-staging', +}); + +function runContextCheck(overrides = {}, omitted = []) { + const env = { ...process.env, ...VALID_CONTEXT, ...overrides }; + omitted.forEach((name) => delete env[name]); + return spawnSync('bash', [CONTEXT_CHECK_PATH], { + cwd: ROOT, + encoding: 'utf8', + env, + }); +} + +test('staging authority workflow is manual, protected, pinned, and least-authority', () => { + assert.match(workflow, /\bon:\n workflow_dispatch:\n/); + assert.doesNotMatch(workflow, /\n (?:push|pull_request|schedule):/); + assert.match(workflow, /^permissions: \{\}$/m); + assert.match(workflow, /environment:\n name: staging/); + assert.match(workflow, /permissions:\n contents: read\n id-token: write/); + assert.match(workflow, /TARGET_PROJECT_ID: \$\{\{ vars\.FIREBASE_PROJECT_ID \}\}/); + assert.match(workflow, /secrets\.GCP_WORKLOAD_IDENTITY_PROVIDER/); + assert.match(workflow, /secrets\.GCP_DEPLOY_SERVICE_ACCOUNT/); + assert.doesNotMatch(workflow, /FIREBASE_TOKEN|FIREBASE_SERVICE_ACCOUNT|credentials_json/); + assert.doesNotMatch(workflow, /contents: write|actions: write|packages: write/); + + for (const action of workflow.matchAll(/^\s*uses:\s*([^\s#]+)/gm)) { + assert.match(action[1], /@[0-9a-f]{40}$/); + } +}); + +test('staging authority workflow validates context before requesting OIDC', () => { + const validatePosition = workflow.indexOf('Validate immutable repository'); + const authPosition = workflow.indexOf('google-github-actions/auth@'); + const readPosition = workflow.indexOf('verify-staging-project-read.sh'); + + assert.ok(validatePosition > 0); + assert.ok(authPosition > validatePosition); + assert.ok(readPosition > authPosition); + assert.match(contextCheck, /GITHUB_REPOSITORY_ID:-.*718285092/); + assert.match(contextCheck, /GITHUB_REPOSITORY_OWNER_ID:-.*150727922/); + assert.match(contextCheck, /GITHUB_REF:-.*refs\/heads\/main/); + assert.match(contextCheck, /TARGET_ENVIRONMENT:-.*staging/); + assert.match(contextCheck, /TARGET_PROJECT_ID:-.*run-mprc-staging/); +}); + +test('context verifier accepts only the exact reviewed authority context', () => { + const result = runContextCheck(); + assert.equal(result.status, 0); + assert.equal(result.stdout, 'staging_authority_context_valid\n'); + assert.equal(result.stderr, ''); +}); + +test('wrong ref and missing authority fail with one fixed diagnostic', () => { + const wrongRef = runContextCheck({ GITHUB_REF: 'refs/heads/not-main' }); + assert.notEqual(wrongRef.status, 0); + assert.equal(wrongRef.stdout, ''); + assert.equal(wrongRef.stderr, 'staging_authority_context_invalid\n'); + + for (const missingName of [ + 'DEPLOY_SERVICE_ACCOUNT', + 'WORKLOAD_IDENTITY_PROVIDER', + ]) { + const missing = runContextCheck({}, [missingName]); + assert.notEqual(missing.status, 0); + assert.equal(missing.stdout, ''); + assert.equal(missing.stderr, 'staging_authority_context_invalid\n'); + } +}); + +test('post-auth verifier performs only one fixed GET and emits no response data', () => { + assert.match(projectRead, /curl --fail --silent --show-error --request GET/); + assert.match( + projectRead, + /https:\/\/cloudresourcemanager\.googleapis\.com\/v3\/projects\/run-mprc-staging/, + ); + assert.match(projectRead, /\.projectId == "run-mprc-staging"/); + assert.match(projectRead, /\.name == "projects\/384996399199"/); + assert.match(projectRead, /\.state == "ACTIVE"/); + assert.doesNotMatch(projectRead, /--request (?:POST|PUT|PATCH|DELETE)/); + assert.doesNotMatch(projectRead, /gcloud|firebase|tee|cat .*response/); + assert.equal( + (projectRead.match(/staging_authority_read_verified/g) ?? []).length, + 1, + ); +}); + +test('verification source contains no deployment or cloud mutation command', () => { + const source = `${workflow}\n${contextCheck}\n${projectRead}`; + assert.doesNotMatch(source, /firebase\s+deploy/); + assert.doesNotMatch(source, /gcloud\s+(?:services|iam|projects)/); + assert.doesNotMatch(source, /--request (?:POST|PUT|PATCH|DELETE)/); + assert.doesNotMatch(source, /functions:|firestore:rules|hosting:/); +}); diff --git a/tests/test-artifact-safety.test.js b/tests/test-artifact-safety.test.js index aee3a83b..a55e244d 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/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/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(''); @@ -63,6 +63,7 @@ const EXPECTED_FRONTEND_VALIDATION_STEP = [ ' - name: Validate protected release workflow', ' run: |', ' 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)\'', ` ${FRONTEND_TEST_COMMAND}`, ].join('\n');