Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions OPERATIONS_RUNBOOK.md
Original file line number Diff line number Diff line change
Expand Up @@ -554,4 +554,6 @@ The #134 remediation-ledger entry is source/test evidence only. For OBS-001A1 [#

For OBS-001A2 [#139](https://github.com/Run-MPRC/Run-MPRC.github.io/issues/139), keep the same evidence discipline: **source/tests** means application runtime source and public HTML/JavaScript entrypoints contain no Firebase Analytics import, initialization, or emission and synthetic mocked/static tests pass; **merge** means the resulting `main` SHA; **website publication** means the exact revision has a hosting publication record; **`runmprc.com` verification** separately proves the live host serves that revision; **Firebase deployment** is unchanged and not required for this source-only issue; **Analytics provider configuration** remains unchanged and unverified for collection, cookies/storage, prior provider data, access, retention, consent, and deletion; **live network behavior** remains unverified until an approved public-page check at the exact website revision observes no Analytics request or cookie without signing in or using member data. Removing source calls does not delete provider history or browser storage. Do not use a production-configured preview for this check.

For OBS-001A3 [#142](https://github.com/Run-MPRC/Run-MPRC.github.io/issues/142), **source/tests** means application and public runtime source has no direct console path outside the closed five-outcome helper and synthetic console canaries pass; **merge** means the resulting `main` SHA; **website publication** and **`runmprc.com` verification** remain separate later evidence; **Firebase deployment** and **outside-provider configuration** are unchanged by this frontend-only source issue; **live behavior** and deletion of any earlier local browser-console history remain unverified. Never reproduce a member error or paste a browser console into an issue to prove this boundary.

Provider configuration, production secrets, Netlify publication, Firebase deployment, and live behavior always require separate dated evidence. Local tests and a green GitHub summary do not prove them.
2 changes: 1 addition & 1 deletion SECURITY.md
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,7 @@ These entries are implementation evidence, not a production risk-acceptance deci
| RISK-013 | PROMO-001 [#102](https://github.com/Run-MPRC/Run-MPRC.github.io/issues/102) sets both current Checkout creators to promotion entry off and automatic tax off. Exact payload and signed event tests require an explicit all-zero breakdown; malformed or nonzero discount, tax, or shipping data creates durable review evidence while definitive failed/expired Sessions still cancel. | Privately inventory pre-change Sessions/provider promotion settings, keep the guard through protected staging/deploy, verify provider and live state separately, and implement an approved authoritative discount/tax/shipping contract before ever enabling it. |
| Part of RISK-014 | SUPPLY-001A [#127](https://github.com/Run-MPRC/Run-MPRC.github.io/issues/127) removes the unused direct `gpxparser` dependency and 50 lockfile entries without changing any retained package version, source URL, or integrity hash. A fresh Node 20 install lowers the root production audit from 33 findings (3 critical, 9 high) to 22 (1 critical, 8 high), and the optimized production asset hashes remain identical. | Remediate or time-bound the remaining root and Functions findings through the separately tested router, Firebase, Stripe, build-system, and scanning slices; do not use a forced audit upgrade. |
| Part of RISK-024 | SEC-001 protects token documents from every browser client. | OAUTH-001 must still add transactional refresh/versioning, scope/account binding, disconnect/revocation, IAM/encryption decision, and redacted lifecycle audit. |
| Part of RISK-027 | #99 blocks App Check, Firebase Analytics, and Sentry in local/test and on initial capability-callback startup. OBS-001A1 [#134](https://github.com/Run-MPRC/Run-MPRC.github.io/issues/134) source disables Sentry replay, tracing, browser session tracking, client reports, member user context, breadcrumbs, attachments, transactions, and unapproved default integrations. OBS-001A2 [#139](https://github.com/Run-MPRC/Run-MPRC.github.io/issues/139) source removes every application runtime Firebase Analytics import, initialization, and emission, preserves existing wrapper calls as provider-free no-ops, and removes the direct Waiver SDK bypass. Synthetic tests use mocks/canaries and scan CRA runtime source plus public HTML/JavaScript entrypoints; they make no provider call. | #110/#111 still own any future approved Analytics purpose, consent, event/parameter schema, provider settings, and residual hosted telemetry boundary. Source/tests and merge evidence do not prove website publication, the exact `runmprc.com` revision, provider transport/collection, IP/cookie behavior, historical data deletion, retention, access, consent, deletion, or vendor terms. Sentry provider behavior is likewise unverified. Callback protected handoffs remain with ABUSE-001A, DATA-001A, and OAUTH-001C in #88. |
| Part of RISK-027 | #99 blocks App Check, Firebase Analytics, and Sentry in local/test and on initial capability-callback startup. OBS-001A1 [#134](https://github.com/Run-MPRC/Run-MPRC.github.io/issues/134) source disables Sentry replay, tracing, browser session tracking, client reports, member user context, breadcrumbs, attachments, transactions, and unapproved default integrations. OBS-001A2 [#139](https://github.com/Run-MPRC/Run-MPRC.github.io/issues/139) source removes every application runtime Firebase Analytics import, initialization, and emission, preserves existing wrapper calls as provider-free no-ops, and removes the direct Waiver SDK bypass. OBS-001A3 [#142](https://github.com/Run-MPRC/Run-MPRC.github.io/issues/142) source routes the remaining render, Auth-email, members-only, and App Check browser diagnostics through five fixed outcome identifiers and prohibits direct console calls elsewhere in application/public runtime source. Synthetic tests use mocks/canaries and make no provider call. | #110/#111 still own any future approved telemetry purpose, consent, event/parameter schema, provider settings, and residual hosted boundary. Source/tests and merge evidence do not prove website publication, the exact `runmprc.com` revision, provider transport/collection, IP/cookie behavior, console history, historical data deletion, retention, access, consent, deletion, or vendor terms. Sentry provider behavior is likewise unverified. Callback protected handoffs remain with ABUSE-001A, DATA-001A, and OAUTH-001C in #88. |
| Part of RISK-035 | The Jest environment is repaired, the stale frontend smoke test is replaced, #124 adds the complete frontend suite as a named blocking hosted step, and #126 adds the standalone SPA callback suite as its own named blocking step. | Keep exact hosted evidence on #124/#126; make lint non-mutating/fail-closed, prove required branch checks, and add the broader TEST-001 suite. |
| RISK-026, RISK-036 | #135 merged through PR #138 as `9eafab1217aff7058c42240aaba72d7b93f8ed24`, replacing automatic frontend-first/fail-open GitHub deployment with a tested manual exact-current-commit gate. Post-merge staging and synthetic production probes failed closed before authentication or mutation, and published neither Firebase nor Pages. | #133 must configure protected environments and least-privilege OIDC/WIF; #136 must prove staged/target deployment and clearing/readback of the existing Pages `runmprc.com` claim; a protected WEB-001 child must establish Netlify publication and rollback. No Firebase, Pages, live-host, or provider-setting change is proven by source/static tests alone. |
| RISK-039 | The #118 source slice uses an empty authenticated callable request, bounded Firebase Auth identity fields, one transactional create-only helper shared with signup, constant responses, generic failures, and a UI that hides Edit until setup and the Rules-protected read succeed. Signup and recovery never change custom claims; browser profile creation remains denied. | Source review/merge is not deployment. Under #105, deploy the exact #100 Rules plus both `createMemberOnSignUp` and `ensureMemberProfile` before the website, prove App Check policy, use a synthetic staged account, verify rollback, then record website, Function, Rules, and live behavior separately. Never repair a real profile manually. |
Expand Down
9 changes: 6 additions & 3 deletions src/components/ErrorBoundary.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
import React from 'react';
import { captureException } from '../services/monitoring/sentry';
import {
clientFailureEvents,
reportClientFailure,
} from '../services/monitoring/clientDiagnostics';

interface Props {
children: React.ReactNode;
Expand All @@ -19,10 +23,9 @@ class ErrorBoundary extends React.Component<Props, State> {
return { error };
}

componentDidCatch(error: Error, info: React.ErrorInfo) {
componentDidCatch(error: Error) {
captureException(error);
// eslint-disable-next-line no-console
console.error('ErrorBoundary caught:', error, info.componentStack);
reportClientFailure(clientFailureEvents.renderFailed);
}

handleReset = () => {
Expand Down
11 changes: 9 additions & 2 deletions src/components/MembersOnly.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,14 @@ import React, { useEffect, useState } from 'react';
import { doc, getDoc } from 'firebase/firestore';
import parse from 'html-react-parser';
import { useServiceLocator } from '../services/ServiceLocatorContext';
import {
clientFailureEvents,
reportClientFailure,
} from '../services/monitoring/clientDiagnostics';

// This legacy JSX component predates typed props; adding a dependency or
// migrating the component is outside this privacy-only change.
// eslint-disable-next-line react/prop-types
function MembersOnly({ dataKey, style }) {
const [data, setData] = useState([]);
const [loading, setLoading] = useState(true);
Expand All @@ -28,9 +35,9 @@ function MembersOnly({ dataKey, style }) {
} else {
setError('Document not found');
}
} catch (err) {
} catch {
setError('Failed to fetch data');
console.error('Error fetching members-only data:', err);
reportClientFailure(clientFailureEvents.membersOnlyFetchFailed);
} finally {
setLoading(false);
}
Expand Down
45 changes: 45 additions & 0 deletions src/services/firebase/FirebaseResources.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -186,6 +186,51 @@ describe('FirebaseResources environment isolation', () => {
);
});

test('production without an App Check key continues with one fixed diagnostic', () => {
const consoleWarn = jest.spyOn(console, 'warn').mockImplementation(() => {});

try {
const resources = createResourcesFor('production');

expect(resources.auth).toBe(mockAuth);
expect(resources.firestore).toBe(mockFirestore);
expect(resources.functions).toBe(mockFunctions);
expect(mockInitializeAppCheck).not.toHaveBeenCalled();
expect(consoleWarn).toHaveBeenCalledWith('[MPRC client] app_check_disabled');
} finally {
consoleWarn.mockRestore();
}
});

test('App Check failure continues without logging provider details', () => {
const canaries = [
'app-check-member@example.test',
'app-check-provider-response-canary',
'app-check-token-canary',
];
const providerError = Object.assign(new Error(canaries.join(' ')), {
response: canaries[1],
token: canaries[2],
});
mockInitializeAppCheck.mockImplementationOnce(() => { throw providerError; });
const consoleWarn = jest.spyOn(console, 'warn').mockImplementation(() => {});

try {
const resources = createResourcesFor('production', 'configured-public-site-key');

expect(resources.auth).toBe(mockAuth);
expect(resources.firestore).toBe(mockFirestore);
expect(resources.functions).toBe(mockFunctions);
expect(consoleWarn).toHaveBeenCalledWith(
'[MPRC client] app_check_initialization_failed',
);
const serializedConsole = JSON.stringify(consoleWarn.mock.calls);
canaries.forEach((canary) => expect(serializedConsole).not.toContain(canary));
} finally {
consoleWarn.mockRestore();
}
});

test.each([
'/account/strava/callback?code=example&state=example-state',
'/register/success?registration=r1&token=example-capability',
Expand Down
12 changes: 7 additions & 5 deletions src/services/firebase/FirebaseResources.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,10 @@ import {
} from 'firebase/functions';

import hasCapabilityCallbackState from '../monitoring/capabilityCallback';
import {
clientFailureEvents,
reportClientFailure,
} from '../monitoring/clientDiagnostics';

const isLocalRuntime = process.env.NODE_ENV !== 'production';
const LOCAL_FIREBASE_PROJECT_ID = 'demo-mprc-local';
Expand Down Expand Up @@ -82,18 +86,16 @@ class FirebaseResources {

const siteKey = process.env.REACT_APP_RECAPTCHA_SITE_KEY;
if (!siteKey) {
console.warn(
'App Check disabled: set REACT_APP_RECAPTCHA_SITE_KEY to enable',
);
reportClientFailure(clientFailureEvents.appCheckDisabled);
return;
}
try {
this._appCheck = initializeAppCheck(this.app, {
provider: new ReCaptchaV3Provider(siteKey),
isTokenAutoRefreshEnabled: true,
});
} catch (error) {
console.warn('App Check init failed:', error);
} catch {
reportClientFailure(clientFailureEvents.appCheckInitializationFailed);
}
}

Expand Down
8 changes: 6 additions & 2 deletions src/services/identity/Identity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,10 @@ import {
Unsubscribe,
IdTokenResult,
} from 'firebase/auth';
import {
clientFailureEvents,
reportClientFailure,
} from '../monitoring/clientDiagnostics';
import FirebaseResources from '../firebase/FirebaseResources';

export type UserRole = 'admin' | 'member' | 'unverified' | null;
Expand Down Expand Up @@ -132,8 +136,8 @@ class IdentityService {
this.currentUserRole = 'unverified';
try {
await sendEmailVerification(credential.user);
} catch (error) {
console.warn('Failed to send verification email:', error);
} catch {
reportClientFailure(clientFailureEvents.emailVerificationFailed);
}
return credential;
}
Expand Down
Loading
Loading