forked from wangziqi/gongxue-base
167 lines
5.0 KiB
TypeScript
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';
|
|
}
|