95 lines
3.8 KiB
TypeScript
95 lines
3.8 KiB
TypeScript
import { z } from 'zod';
|
|
|
|
const PLACEHOLDER_TENANT_ID = '01J00000000000000000000000';
|
|
const LOOPBACK_HOSTS = new Set(['localhost', '127.0.0.1', '::1']);
|
|
|
|
const configSchema = z.object({
|
|
NODE_ENV: z.enum(['development', 'test', 'production']).default('development'),
|
|
PORT: z.coerce.number().int().min(1).max(65535).default(8080),
|
|
HOST: z.string().min(1).default('127.0.0.1'),
|
|
LOG_LEVEL: z.enum(['fatal', 'error', 'warn', 'info', 'debug', 'trace', 'silent']).default('info'),
|
|
STAFF_RUNTIME_MODE: z.enum(['standalone', 'platform']).default('standalone'),
|
|
STANDALONE_ACCESS: z.enum(['employee', 'manager', 'terminal']).default('employee'),
|
|
STANDALONE_MANAGER_TOKEN: emptyStringAsUndefined(z.string().min(32).optional()),
|
|
STANDALONE_TERMINAL_TOKEN: emptyStringAsUndefined(z.string().min(32).optional()),
|
|
STANDALONE_TERMINAL_ACTOR_ID: emptyStringAsUndefined(
|
|
z
|
|
.string()
|
|
.regex(/^[0-9A-HJKMNP-TV-Z]{26}$/)
|
|
.optional(),
|
|
),
|
|
HTTPS_PFX_PATH: emptyStringAsUndefined(z.string().optional()),
|
|
HTTPS_PFX_PASSPHRASE: emptyStringAsUndefined(z.string().min(16).optional()),
|
|
DATABASE_URL: z.string().url(),
|
|
REDIS_URL: optionalUrl(),
|
|
PLATFORM_GATEWAY_URL: optionalUrl(),
|
|
PLATFORM_JWT_ISSUER: optionalUrl(),
|
|
PLATFORM_JWT_AUDIENCE: emptyStringAsUndefined(z.string().optional()),
|
|
PLATFORM_JWKS_URL: optionalUrl(),
|
|
STANDALONE_TENANT_ID: z
|
|
.string({ error: 'STANDALONE_TENANT_ID is required and must be a ULID' })
|
|
.regex(/^[0-9A-HJKMNP-TV-Z]{26}$/),
|
|
STANDALONE_ACTOR_ID: z.string().regex(/^[0-9A-HJKMNP-TV-Z]{26}$/),
|
|
VAPID_PUBLIC_KEY: emptyStringAsUndefined(z.string().optional()),
|
|
VAPID_PRIVATE_KEY: emptyStringAsUndefined(z.string().optional()),
|
|
VAPID_SUBJECT: emptyStringAsUndefined(z.string().optional()),
|
|
});
|
|
|
|
function emptyStringAsUndefined<T extends z.ZodTypeAny>(schema: T) {
|
|
return z.preprocess((value) => (value === '' ? undefined : value), schema);
|
|
}
|
|
|
|
function optionalUrl() {
|
|
return emptyStringAsUndefined(z.string().url().optional());
|
|
}
|
|
|
|
export type AppConfig = z.infer<typeof configSchema>;
|
|
|
|
export function loadConfig(env: NodeJS.ProcessEnv = process.env): AppConfig {
|
|
const config = configSchema.parse(env);
|
|
if (
|
|
config.STANDALONE_TENANT_ID === PLACEHOLDER_TENANT_ID &&
|
|
!(
|
|
config.NODE_ENV === 'test' ||
|
|
(config.NODE_ENV === 'development' && LOOPBACK_HOSTS.has(config.HOST.toLowerCase()))
|
|
)
|
|
) {
|
|
throw new Error(
|
|
'STANDALONE_TENANT_ID uses the development placeholder, which is allowed only in development on loopback',
|
|
);
|
|
}
|
|
if (Boolean(config.HTTPS_PFX_PATH) !== Boolean(config.HTTPS_PFX_PASSPHRASE)) {
|
|
throw new Error('HTTPS_PFX_PATH and HTTPS_PFX_PASSPHRASE must be configured together');
|
|
}
|
|
if (Boolean(config.STANDALONE_MANAGER_TOKEN) !== Boolean(config.STANDALONE_TERMINAL_TOKEN)) {
|
|
throw new Error('Both standalone manager and terminal tokens must be configured together');
|
|
}
|
|
if (Boolean(config.VAPID_PUBLIC_KEY) !== Boolean(config.VAPID_PRIVATE_KEY)) {
|
|
throw new Error('VAPID_PUBLIC_KEY and VAPID_PRIVATE_KEY must be configured together');
|
|
}
|
|
if (config.NODE_ENV === 'production' && config.STAFF_RUNTIME_MODE === 'standalone') {
|
|
if (
|
|
!config.HTTPS_PFX_PATH ||
|
|
!config.STANDALONE_MANAGER_TOKEN ||
|
|
!config.STANDALONE_TERMINAL_TOKEN
|
|
) {
|
|
throw new Error(
|
|
'STAFF_RUNTIME_MODE=standalone is not allowed in production without HTTPS and manager/terminal tokens',
|
|
);
|
|
}
|
|
}
|
|
if (config.STAFF_RUNTIME_MODE === 'platform') {
|
|
const required = [
|
|
'PLATFORM_GATEWAY_URL',
|
|
'PLATFORM_JWT_ISSUER',
|
|
'PLATFORM_JWT_AUDIENCE',
|
|
'PLATFORM_JWKS_URL',
|
|
] as const;
|
|
for (const key of required)
|
|
if (!config[key]) throw new Error(`${key} is required in platform mode`);
|
|
}
|
|
if (config.NODE_ENV === 'production' && !config.HTTPS_PFX_PATH) {
|
|
throw new Error('HTTPS is required in production');
|
|
}
|
|
return config;
|
|
}
|