MotorSignal-App/website/lib/firebase-config.ts

157 lines
5.3 KiB
TypeScript

/**
* MotorSignal Firebase Configuration and Isolation Guards
*
* Enforces strict environment separation between production (motoapp-d22e0)
* and staging (motorsignal-staging), preventing staging environments from
* accidentally initializing against production Firebase resources.
*/
export const PRODUCTION_FIREBASE_PROJECT_ID = 'motoapp-d22e0';
export const STAGING_FIREBASE_PROJECT_ID = 'motorsignal-staging';
export interface FirebaseClientOptions {
apiKey: string;
authDomain: string;
projectId: string;
storageBucket: string;
messagingSenderId?: string;
appId?: string;
}
/**
* Determines whether the current execution context is a staging deployment.
* Inspects standard environment variables and browser hostname.
*/
export function isStagingEnvironment(): boolean {
// Check standard environment descriptors
const motorsignalEnv = (process.env.MOTORSIGNAL_ENV || '').toLowerCase().trim();
const appEnv = (process.env.APP_ENV || process.env.NEXT_PUBLIC_APP_ENV || '').toLowerCase().trim();
// If explicitly flagged as production, it is never staging
if (motorsignalEnv === 'production' || appEnv === 'production') {
return false;
}
// Explicit staging flags
if (motorsignalEnv === 'staging' || appEnv === 'staging') {
return true;
}
if (process.env.STAGING === 'true' || process.env.NEXT_PUBLIC_STAGING === 'true') {
return true;
}
const nodeEnv = (process.env.NODE_ENV || '').toLowerCase().trim();
if (nodeEnv === 'staging') {
return true;
}
// Staging API base URL indicator (e.g. https://api-staging.motorsignal.app)
const apiBaseUrl = (process.env.MOTORSIGNAL_API_BASE_URL || '').toLowerCase();
if (apiBaseUrl.includes('api-staging.motorsignal.app') || apiBaseUrl.includes('//staging.')) {
return true;
}
// Staging site URL indicator (e.g. https://staging.motorsignal.app)
const siteUrl = (process.env.NEXT_PUBLIC_SITE_URL || '').toLowerCase();
if (siteUrl.includes('staging.motorsignal.app')) {
return true;
}
// Browser hostname inspection
if (typeof window !== 'undefined' && window.location?.hostname) {
const hostname = window.location.hostname.toLowerCase();
if (hostname === 'staging.motorsignal.app') {
return true;
}
}
return false;
}
/**
* Staging safety guard: strictly prevents any staging deployment from
* initializing Firebase against the production project (motoapp-d22e0).
*/
export function assertStagingSafety(projectId: string): void {
if (isStagingEnvironment() && projectId === PRODUCTION_FIREBASE_PROJECT_ID) {
throw new Error(
`Staging safety guard violation: Staging deployment cannot initialize Firebase against production project "${PRODUCTION_FIREBASE_PROJECT_ID}". ` +
`Expected staging project "${STAGING_FIREBASE_PROJECT_ID}".`
);
}
}
/**
* Validates that server-side and browser-side Firebase project IDs agree.
* Throws if both are provided but do not match.
*/
export function assertProjectIdsMatch(clientProjectId?: string, serverProjectId?: string): void {
if (clientProjectId && serverProjectId && clientProjectId !== serverProjectId) {
throw new Error(
`Firebase project ID mismatch: Client project ID "${clientProjectId}" ` +
`does not match server project ID "${serverProjectId}".`
);
}
}
/**
* Validates and retrieves the client-side Firebase configuration options
* from environment variables.
*
* Throws if required configuration is missing or if the staging safety guard is violated.
*/
export function getFirebaseClientConfig(): FirebaseClientOptions {
const apiKey = process.env.NEXT_PUBLIC_FIREBASE_API_KEY;
const authDomain = process.env.NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN;
const projectId = process.env.NEXT_PUBLIC_FIREBASE_PROJECT_ID;
const storageBucket = process.env.NEXT_PUBLIC_FIREBASE_STORAGE_BUCKET;
const messagingSenderId = process.env.NEXT_PUBLIC_FIREBASE_MESSAGING_SENDER_ID;
const appId = process.env.NEXT_PUBLIC_FIREBASE_APP_ID;
if (!projectId || !apiKey) {
throw new Error(
'Missing required Firebase client configuration: NEXT_PUBLIC_FIREBASE_PROJECT_ID ' +
'and NEXT_PUBLIC_FIREBASE_API_KEY must be set.'
);
}
// Enforce staging safety guard
assertStagingSafety(projectId);
// Enforce client/server project ID consistency if server env var is visible
if (process.env.FIREBASE_PROJECT_ID) {
assertProjectIdsMatch(projectId, process.env.FIREBASE_PROJECT_ID);
}
return {
apiKey,
authDomain: authDomain || `${projectId}.firebaseapp.com`,
projectId,
storageBucket: storageBucket || `${projectId}.firebasestorage.app`,
...(messagingSenderId ? { messagingSenderId } : {}),
...(appId ? { appId } : {}),
};
}
/**
* Validates and retrieves the server-side Firebase project ID.
* Requires explicit FIREBASE_PROJECT_ID; fails closed without falling back to production.
*/
export function getServerFirebaseProjectId(): string {
const projectId = process.env.FIREBASE_PROJECT_ID;
if (!projectId) {
throw new Error('FIREBASE_PROJECT_ID is required for Firebase Admin SDK initialization.');
}
// Enforce staging safety guard
assertStagingSafety(projectId);
// Enforce client/server project ID consistency if client env var is set
if (process.env.NEXT_PUBLIC_FIREBASE_PROJECT_ID) {
assertProjectIdsMatch(process.env.NEXT_PUBLIC_FIREBASE_PROJECT_ID, projectId);
}
return projectId;
}