/** * 环境配置 —— 只在 app/ 层读环境变量;模块内一律经 port 注入(§5.3)。 */ import { join } from 'node:path'; /** 统一鉴权中心(ADR-012)。本服务只验不调,**不配 client secret**(手册 13.2) */ export type KeycloakConfig = { readonly url: string; readonly realm: string; /** 校 aud 用 —— 填错全线 401 */ readonly clientId: string; readonly issuer: string; readonly jwksUrl: string; /** master realm 的 provisioning 服务账号(只读名册,view-users);可选 */ readonly provisioning?: { readonly clientId: string; readonly secret: string }; }; export type AppConfig = { readonly nodeEnv: 'development' | 'test' | 'production'; readonly port: number; readonly databaseUrl: string; /** 原件存档根目录 —— 信封的原件指针指向这里 */ readonly attachmentRoot: string; /** 派发器轮询间隔(毫秒) */ readonly dispatchIntervalMs: number; /** 邮件轮询工作者的 tick(毫秒);每个账号自己的间隔另算 */ readonly mailPollTickMs: number; /** 三样齐了才有;缺一样即 undefined —— main.ts 据此拒绝启动,集成测试可不配 */ readonly keycloak?: KeycloakConfig; /** 测试账号(Keycloak 用户名):首次建档 / 播种时落 TEST 岗 + 独立工作组 */ readonly testAccounts: readonly string[]; }; const DEFAULT_DATABASE_URL = 'postgres://jcai:jcai@localhost:54329/jcai_wos'; function nonEmpty(value: string | undefined): string | undefined { const trimmed = value?.trim(); return trimmed === undefined || trimmed === '' ? undefined : trimmed; } function keycloakOf(env: NodeJS.ProcessEnv): KeycloakConfig | undefined { const url = nonEmpty(env['KEYCLOAK_URL'])?.replace(/\/+$/, ''); const realm = nonEmpty(env['KEYCLOAK_REALM']); const clientId = nonEmpty(env['KEYCLOAK_CLIENT_ID']); if (url === undefined || realm === undefined || clientId === undefined) return undefined; const issuer = `${url}/realms/${realm}`; const provisioningId = nonEmpty(env['KEYCLOAK_PROVISIONING_CLIENT_ID']); const provisioningSecret = nonEmpty(env['KEYCLOAK_PROVISIONING_CLIENT_SECRET']); return { url, realm, clientId, issuer, jwksUrl: `${issuer}/protocol/openid-connect/certs`, ...(provisioningId === undefined || provisioningSecret === undefined ? {} : { provisioning: { clientId: provisioningId, secret: provisioningSecret } }), }; } export function loadConfig(env: NodeJS.ProcessEnv = process.env): AppConfig { const nodeEnv = env['NODE_ENV']; const keycloak = keycloakOf(env); return { nodeEnv: nodeEnv === 'production' || nodeEnv === 'test' ? nodeEnv : 'development', port: Number(env['PORT'] ?? 8787), databaseUrl: env['DATABASE_URL'] ?? DEFAULT_DATABASE_URL, attachmentRoot: env['JCAI_ATTACHMENT_ROOT'] ?? join(process.cwd(), '.data', 'attachments'), dispatchIntervalMs: Number(env['JCAI_DISPATCH_INTERVAL_MS'] ?? 2000), mailPollTickMs: Number(env['JCAI_MAIL_POLL_TICK_MS'] ?? 5000), ...(keycloak === undefined ? {} : { keycloak }), testAccounts: (env['JCAI_TEST_ACCOUNTS'] ?? '') .split(',') .map((s) => s.trim()) .filter((s) => s !== ''), }; }