Archived
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TM4kUBvekALu9qqmp7D3wr
78 lines
3.1 KiB
TypeScript
78 lines
3.1 KiB
TypeScript
/**
|
|
* 环境配置 —— 只在 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 !== ''),
|
|
};
|
|
}
|