import { updateHasherSettings } from './services/toolsService'; export interface SecuritySettings { Authority: string; Audience: string; ClientId: string; ClientSecret: string; Scope?: string; } export interface ApiSettings { BaseUrl: string; } export interface HasherSettings { Salt: string; MinHashLength: number; } export type ConfigSource = 'runtime-injected' | 'runtime-fetched' | 'build-env' | 'fallback-defaults'; export interface AppConfig { SecuritySettings: SecuritySettings; ApiSettings: ApiSettings; HasherSettings: HasherSettings; ConfigSource: ConfigSource; LoadedAt?: string; } export const defaultSecuritySettings: SecuritySettings = { Authority: 'https://sts.security.khongisa.co.za', Audience: 'pfm-api-dev', ClientId: 'pfm-scalar-dev', ClientSecret: 'secret_71f88570a46eb22eb5270b47f69e3332c84d8db85d008f0276cb7431c8872d27', Scope: 'openid profile roles email address pfm-api-dev', }; export const defaultApiSettings: ApiSettings = { BaseUrl: 'https://api.ifri.khongisa.co.za/', }; export const defaultHasherSettings: HasherSettings = { Salt: 'TGlnaHQgQ2hhcm1zLCBNaWRyYW5kQm9va3MgbWFrZSBsb3RzIG9mIG1vbmV5IGFuZCBhcmUgYSBzdWNjZXNzZnVsIHZpcmFsIHN0b3J5IGluIFNvdXRoIEFmcmljYQ==', MinHashLength: 11, }; function pickFirstNonEmpty(...candidates: any[]): string | undefined { for (const c of candidates) { if (c !== undefined && c !== null) { const s = String(c).trim(); if (s !== '' && s !== 'undefined' && s !== 'null') return s; } } return undefined; } function normalizeBaseUrl(url: string): string { let trimmed = url.trim(); if (!/^https?:\/\//i.test(trimmed)) trimmed = `https://${trimmed}`; return trimmed.endsWith('/') ? trimmed : `${trimmed}/`; } function normalizeAuthorityUrl(url: string): string { let trimmed = url.trim(); if (!/^https?:\/\//i.test(trimmed)) trimmed = `https://${trimmed}`; return trimmed.replace(/\/+$/, ''); } function getWindowRuntimeConfig(): Record { if (typeof window === 'undefined') return {}; const win = window as any; return win.__RUNTIME_CONFIG__ || win.APP_CONFIG || win._env_ || {}; } function extractSourceConfig(src: Record) { if (!src || typeof src !== 'object') return {}; return { authority: pickFirstNonEmpty(src.VITE_OIDC_AUTHORITY, src.OIDC_AUTHORITY, src.SecuritySettings?.Authority, src.SecuritySettings__Authority), audience: pickFirstNonEmpty(src.VITE_OIDC_AUDIENCE, src.OIDC_AUDIENCE, src.SecuritySettings?.Audience, src.SecuritySettings__Audience), clientId: pickFirstNonEmpty(src.VITE_OIDC_CLIENT_ID, src.OIDC_CLIENT_ID, src.SecuritySettings?.ClientId, src.SecuritySettings__ClientId), clientSecret: pickFirstNonEmpty(src.VITE_OIDC_CLIENT_SECRET, src.OIDC_CLIENT_SECRET, src.SecuritySettings?.ClientSecret, src.SecuritySettings__ClientSecret), scope: pickFirstNonEmpty(src.VITE_OIDC_SCOPE, src.OIDC_SCOPE, src.SecuritySettings?.Scope, src.SecuritySettings__Scope), baseUrl: pickFirstNonEmpty(src.VITE_API_BASE_URL, src.API_BASE_URL, src.ApiSettings?.BaseUrl, src.ApiSettings__BaseUrl), hasherSalt: pickFirstNonEmpty(src.VITE_HASHER_SALT, src.HASHER_SALT, src.HasherSettings?.Salt, src.HasherSettings__Salt), hasherMinLength: Number(pickFirstNonEmpty(src.VITE_HASHER_MIN_LENGTH, src.HASHER_MIN_LENGTH, src.HasherSettings?.MinHashLength)) || undefined, }; } function resolveConfig(fetchedJson?: Record): AppConfig { const winCfg = extractSourceConfig(getWindowRuntimeConfig()); const fetchCfg = fetchedJson ? extractSourceConfig(fetchedJson) : {}; const envCfg = extractSourceConfig((typeof import.meta !== 'undefined' && (import.meta as any).env ? (import.meta as any).env : {}) as Record); let source: ConfigSource = 'fallback-defaults'; if (Object.values(winCfg).some((v) => v !== undefined)) source = 'runtime-injected'; else if (Object.values(fetchCfg).some((v) => v !== undefined)) source = 'runtime-fetched'; else if (Object.values(envCfg).some((v) => v !== undefined)) source = 'build-env'; return { SecuritySettings: { Authority: normalizeAuthorityUrl(pickFirstNonEmpty(winCfg.authority, fetchCfg.authority, envCfg.authority, defaultSecuritySettings.Authority)!), Audience: pickFirstNonEmpty(winCfg.audience, fetchCfg.audience, envCfg.audience, defaultSecuritySettings.Audience)!, ClientId: pickFirstNonEmpty(winCfg.clientId, fetchCfg.clientId, envCfg.clientId, defaultSecuritySettings.ClientId)!, ClientSecret: pickFirstNonEmpty(winCfg.clientSecret, fetchCfg.clientSecret, envCfg.clientSecret, defaultSecuritySettings.ClientSecret)!, Scope: pickFirstNonEmpty(winCfg.scope, fetchCfg.scope, envCfg.scope, defaultSecuritySettings.Scope), }, ApiSettings: { BaseUrl: normalizeBaseUrl(pickFirstNonEmpty(winCfg.baseUrl, fetchCfg.baseUrl, envCfg.baseUrl, defaultApiSettings.BaseUrl)!), }, HasherSettings: { Salt: pickFirstNonEmpty(winCfg.hasherSalt, fetchCfg.hasherSalt, envCfg.hasherSalt, defaultHasherSettings.Salt)!, MinHashLength: winCfg.hasherMinLength || fetchCfg.hasherMinLength || envCfg.hasherMinLength || defaultHasherSettings.MinHashLength, }, ConfigSource: source, LoadedAt: new Date().toISOString(), }; } let runtimeConfig: AppConfig = resolveConfig(); updateHasherSettings(runtimeConfig.HasherSettings.Salt, runtimeConfig.HasherSettings.MinHashLength); const configListeners: Array<(config: AppConfig) => void> = []; export function onConfigChanged(listener: (config: AppConfig) => void): () => void { configListeners.push(listener); return () => { const idx = configListeners.indexOf(listener); if (idx !== -1) configListeners.splice(idx, 1); }; } export async function loadRuntimeConfig(): Promise { let fetchedJson: Record | undefined; if (typeof window !== 'undefined') { try { const res = await fetch(`/config.json?_t=${Date.now()}`, { cache: 'no-store', headers: { Accept: 'application/json' }, }).catch(() => null); if (res && res.ok) { fetchedJson = await res.json().catch(() => undefined); } } catch { // Safe fallback } } runtimeConfig = resolveConfig(fetchedJson); updateHasherSettings(runtimeConfig.HasherSettings.Salt, runtimeConfig.HasherSettings.MinHashLength); configListeners.forEach((fn) => fn(runtimeConfig)); return runtimeConfig; } export const getAppConfig = () => runtimeConfig; export const getSecuritySettings = () => runtimeConfig.SecuritySettings; export const getApiBaseUrl = () => runtimeConfig.ApiSettings.BaseUrl; export const getHasherSettings = () => runtimeConfig.HasherSettings;