157 lines
5.3 KiB
TypeScript
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;
|
|
}
|