Files
gongxue-base/apps/taro/src/env.ts
2026-07-01 02:37:43 +08:00

167 lines
5.0 KiB
TypeScript

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<string, string | undefined>;
};
type ProcessLike = {
env?: Record<string, string | undefined>;
};
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<string, unknown>, 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<string>(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<AppEnv> | null = null;
export function applyRuntimeConfig(input: RuntimeConfigInput, source = 'runtime config') {
assertNoForbiddenKeys(input as Record<string, unknown>, 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';
}