export type Portal = 'student' | 'tenant-admin' | 'platform-admin'; export interface AppEnv { portal: Portal; apiBaseUrl: string; supabaseUrl: string; supabasePublishableKey: string; tenantCode: string; } export interface RuntimeConfigInput { portal?: string; apiBaseUrl?: string; supabaseUrl?: string; supabasePublishableKey?: string; tenantCode?: string; TARO_APP_PORTAL?: string; TARO_APP_API_BASE_URL?: string; TARO_APP_SUPABASE_URL?: string; TARO_APP_SUPABASE_PUBLISHABLE_KEY?: string; TARO_APP_TENANT_CODE?: string; } declare const process: { env: Record; }; type ProcessLike = { env?: Record; }; function envValue(key: string) { const runtimeProcess = typeof process === 'undefined' ? undefined : (process as ProcessLike); return runtimeProcess?.env?.[key]; } const forbiddenFrontendKeys = [ 'SUPABASE_SERVICE_ROLE_KEY', 'SUPABASE_SECRET_KEY', 'DATABASE_URL', 'ALIYUN_OSS_ACCESS_KEY_SECRET', 'TENCENT_COS_SECRET_KEY', 'WECHAT_PAY_PRIVATE_KEY', 'ALIPAY_APP_PRIVATE_KEY', 'AUTH_SESSION_SECRET', 'PLATFORM_ADMIN_API_KEY', ] as const; const allowedRuntimeConfigKeys = [ 'portal', 'apiBaseUrl', 'supabaseUrl', 'supabasePublishableKey', 'tenantCode', 'TARO_APP_PORTAL', 'TARO_APP_API_BASE_URL', 'TARO_APP_SUPABASE_URL', 'TARO_APP_SUPABASE_PUBLISHABLE_KEY', 'TARO_APP_TENANT_CODE', ] as const; function normalizeString(value: unknown) { return typeof value === 'string' ? value.trim() : ''; } function normalizePortal(value: unknown): Portal | null { if (value === 'student' || value === 'tenant-admin' || value === 'platform-admin') return value; return null; } function assertNoForbiddenKeys(input: Record, source: string) { const leaked = forbiddenFrontendKeys.filter(key => Object.prototype.hasOwnProperty.call(input, key)); if (leaked.length) { throw new Error(`Forbidden secret key in ${source}: ${leaked.join(', ')}`); } const allowed = new Set(allowedRuntimeConfigKeys); const unknown = Object.keys(input).filter(key => !allowed.has(key)); if (unknown.length) { throw new Error(`Unknown key in ${source}: ${unknown.join(', ')}`); } } export const appEnv: AppEnv = { portal: (envValue('TARO_APP_PORTAL') || 'student') as Portal, apiBaseUrl: envValue('TARO_APP_API_BASE_URL') || 'http://127.0.0.1:8787', supabaseUrl: envValue('TARO_APP_SUPABASE_URL') || '', supabasePublishableKey: envValue('TARO_APP_SUPABASE_PUBLISHABLE_KEY') || '', tenantCode: envValue('TARO_APP_TENANT_CODE') || '', }; let runtimeConfigPromise: Promise | null = null; export function applyRuntimeConfig(input: RuntimeConfigInput, source = 'runtime config') { assertNoForbiddenKeys(input as Record, source); const portal = normalizePortal(input.portal || input.TARO_APP_PORTAL); if (portal) appEnv.portal = portal; const apiBaseUrl = normalizeString(input.apiBaseUrl || input.TARO_APP_API_BASE_URL); if (apiBaseUrl) appEnv.apiBaseUrl = apiBaseUrl.replace(/\/+$/, ''); const supabaseUrl = normalizeString(input.supabaseUrl || input.TARO_APP_SUPABASE_URL); if (supabaseUrl) appEnv.supabaseUrl = supabaseUrl.replace(/\/+$/, ''); const supabasePublishableKey = normalizeString(input.supabasePublishableKey || input.TARO_APP_SUPABASE_PUBLISHABLE_KEY); if (supabasePublishableKey) appEnv.supabasePublishableKey = supabasePublishableKey; const tenantCode = normalizeString(input.tenantCode || input.TARO_APP_TENANT_CODE); if (tenantCode) appEnv.tenantCode = tenantCode; return appEnv; } export async function loadRuntimeConfig() { if (!isH5Runtime() || typeof window === 'undefined' || typeof window.fetch !== 'function') { return appEnv; } const runtimeConfigUrl = `${window.location.origin}/runtime-config.json`; let response: Response; try { response = await window.fetch(runtimeConfigUrl, { cache: 'no-store', credentials: 'same-origin', }); } catch { return appEnv; } if (!response.ok) return appEnv; const text = (await response.text()).trim(); if (!text || !text.startsWith('{')) return appEnv; let config: RuntimeConfigInput; try { config = JSON.parse(text) as RuntimeConfigInput; } catch (error) { throw new Error(`Invalid Taro runtime-config.json: ${(error as Error).message}`); } return applyRuntimeConfig(config, 'runtime-config.json'); } export function ensureRuntimeConfigLoaded() { if (!runtimeConfigPromise) runtimeConfigPromise = loadRuntimeConfig(); return runtimeConfigPromise; } export function assertFrontendSecretsAreAbsent() { const leaked = forbiddenFrontendKeys.filter(key => envValue(key)); if (leaked.length) { throw new Error(`Forbidden secret env in Taro build: ${leaked.join(', ')}`); } } export function taroRuntimeEnv() { return envValue('TARO_ENV') || ''; } export function isH5Runtime() { return taroRuntimeEnv() === 'h5' || (typeof window !== 'undefined' && typeof document !== 'undefined'); } export function isWeappRuntime() { return taroRuntimeEnv() === 'weapp'; }