fix(config)!: validate and derive config, drop the unread keys (#2482)

This commit is contained in:
Hampus Kraft committed 2026-09-06 15:02:20 +02:00
1 parent ea93ef5352
commit 5bcaa7cfac
46 files changed
+1310 -199

No files matched your search

+98 -11
View File
@@ -1,5 +1,6 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
import {createPrivateKey, createPublicKey} from 'node:crypto';
import {buildNamedFluxerEnvOverrides} from '@fluxer/config/src/config_loader/EnvironmentOverrides';
import {
type DerivedEndpoints,
@@ -41,6 +42,7 @@ function defaultConfig(): MasterConfig {
media: '',
static_cdn: '',
admin: '',
docs: '',
marketing: '',
invite: '',
gift: '',
@@ -90,7 +92,6 @@ function defaultConfig(): MasterConfig {
downloads: 'fluxer-downloads',
reports: 'fluxer-reports',
harvests: 'fluxer-harvests',
static: 'fluxer-static',
},
},
services: {
@@ -130,14 +131,14 @@ function defaultConfig(): MasterConfig {
mode: 'upload',
upload_relay: {
endpoint: 'http://localhost:8088/media',
max_body_bytes: 268_435_456,
secret_base64: '',
max_body_bytes: 524_288_000,
token_ttl_secs: 900,
keep_direct_countries: [],
},
},
gateway: {
port: 8771,
push_enabled: false,
rpc_auth_token: '',
},
admin: {
@@ -163,7 +164,7 @@ function defaultConfig(): MasterConfig {
sso_allow_private_addresses: false,
passkeys: {
rp_name: 'Fluxer',
rp_id: 'fluxer.app',
rp_id: '',
additional_allowed_origins: DEFAULT_PASSKEY_ORIGINS,
},
vapid: {
@@ -172,12 +173,12 @@ function defaultConfig(): MasterConfig {
email: '',
},
bluesky: {
enabled: true,
enabled: false,
client_name: 'Fluxer',
client_uri: '',
logo_uri: '',
tos_uri: 'https://fluxer.app/terms',
policy_uri: 'https://fluxer.app/privacy',
tos_uri: '',
policy_uri: '',
keys: [],
},
},
@@ -335,6 +336,22 @@ function requireString(value: string | undefined, envName: string): void {
}
}
function validateUploadRelaySecret(value: string, mode: string): void {
const trimmed = value.trim();
if (trimmed.length === 0) {
if (mode === 'upload') {
throw new Error('FLUXER_MEDIA_PROXY_UPLOAD_RELAY_SECRET_BASE64 is required in upload mode');
}
return;
}
if (!/^[A-Za-z0-9+/]+={0,2}$/u.test(trimmed)) {
throw new Error('FLUXER_MEDIA_PROXY_UPLOAD_RELAY_SECRET_BASE64 must be base64');
}
if (Buffer.from(trimmed, 'base64').length < 32) {
throw new Error('FLUXER_MEDIA_PROXY_UPLOAD_RELAY_SECRET_BASE64 must decode to at least 32 bytes');
}
}
function assertBoolean(value: unknown, envName: string): asserts value is boolean {
if (typeof value !== 'boolean') {
throw new Error(`${envName} must be true or false`);
@@ -353,6 +370,35 @@ function assertIdentifier(value: string, envName: string): void {
}
}
function validateVapidConfig(config: MasterConfig): void {
requireString(config.auth.vapid.public_key, 'FLUXER_VAPID_PUBLIC_KEY');
requireString(config.auth.vapid.private_key, 'FLUXER_VAPID_PRIVATE_KEY');
const pub = Buffer.from(config.auth.vapid.public_key, 'base64url');
const priv = Buffer.from(config.auth.vapid.private_key, 'base64url');
if (pub.length !== 65 || pub[0] !== 0x04) {
throw new Error('FLUXER_VAPID_PUBLIC_KEY must be the base64url 65-byte uncompressed P-256 point');
}
if (priv.length !== 32) {
throw new Error('FLUXER_VAPID_PRIVATE_KEY must be the base64url 32-byte P-256 scalar');
}
const jwk = {
kty: 'EC',
crv: 'P-256',
x: pub.subarray(1, 33).toString('base64url'),
y: pub.subarray(33, 65).toString('base64url'),
d: priv.toString('base64url'),
};
let derived: {x?: string; y?: string};
try {
derived = createPublicKey(createPrivateKey({key: jwk, format: 'jwk'})).export({format: 'jwk'});
} catch {
throw new Error('FLUXER_VAPID_PRIVATE_KEY does not match FLUXER_VAPID_PUBLIC_KEY');
}
if (derived.x !== jwk.x || derived.y !== jwk.y) {
throw new Error('FLUXER_VAPID_PRIVATE_KEY does not match FLUXER_VAPID_PUBLIC_KEY');
}
}
function validatePostgresConfig(config: MasterConfig): void {
const postgres = config.database.postgres;
assertIntegerInRange(postgres.port, 'FLUXER_POSTGRES_PORT', 1, 65535);
@@ -380,6 +426,24 @@ function validatePostgresConfig(config: MasterConfig): void {
}
}
function validateCaptchaConfig(config: MasterConfig): void {
const captcha = config.integrations.captcha;
if (!captcha.enabled) {
return;
}
if (captcha.provider === 'hcaptcha') {
requireString(captcha.hcaptcha?.site_key, 'FLUXER_CAPTCHA_HCAPTCHA_SITE_KEY');
requireString(captcha.hcaptcha?.secret_key, 'FLUXER_CAPTCHA_HCAPTCHA_SECRET_KEY');
return;
}
if (captcha.provider === 'turnstile') {
requireString(captcha.turnstile?.site_key, 'FLUXER_CAPTCHA_TURNSTILE_SITE_KEY');
requireString(captcha.turnstile?.secret_key, 'FLUXER_CAPTCHA_TURNSTILE_SECRET_KEY');
return;
}
throw new Error('FLUXER_CAPTCHA_PROVIDER must be hcaptcha or turnstile when FLUXER_CAPTCHA_ENABLED is true');
}
function validateApiWorkerConfig(config: MasterConfig): void {
const worker = config.services.api?.worker;
if (!worker) {
@@ -412,6 +476,7 @@ function normalizeConfig(config: MasterConfig): MasterConfig {
'FLUXER_ABUSE_DIRECT_CONTACT_SPAM_ACTION',
);
validatePostgresConfig(config);
validateCaptchaConfig(config);
validateApiWorkerConfig(config);
assertIntegerInRange(config.services.api.max_inflight_requests, 'FLUXER_API_MAX_INFLIGHT_REQUESTS', 1, 100_000);
assertIntegerInRange(config.services.api.headers_timeout_ms, 'FLUXER_API_HEADERS_TIMEOUT_MS', 1_000, 3_600_000);
@@ -419,11 +484,11 @@ function normalizeConfig(config: MasterConfig): MasterConfig {
requireString(config.domain.base_domain, 'FLUXER_BASE_DOMAIN');
requireString(config.auth.sudo_mode_secret, 'FLUXER_SUDO_MODE_SECRET');
requireString(config.auth.connection_initiation_secret, 'FLUXER_CONNECTION_INITIATION_SECRET');
requireString(config.auth.vapid.public_key, 'FLUXER_VAPID_PUBLIC_KEY');
requireString(config.auth.vapid.private_key, 'FLUXER_VAPID_PRIVATE_KEY');
validateVapidConfig(config);
requireString(config.s3?.access_key_id, 'FLUXER_S3_ACCESS_KEY_ID');
requireString(config.s3?.secret_access_key, 'FLUXER_S3_SECRET_ACCESS_KEY');
requireString(config.services.media_proxy.secret_key, 'FLUXER_MEDIA_PROXY_SECRET_KEY');
validateUploadRelaySecret(config.services.media_proxy.upload_relay.secret_base64, config.services.media_proxy.mode);
requireString(config.services.admin.secret_key_base, 'FLUXER_ADMIN_SECRET_KEY_BASE');
requireString(config.services.admin.oauth_client_secret, 'FLUXER_ADMIN_OAUTH_CLIENT_SECRET');
if (!config.instance.self_hosted) {
@@ -463,15 +528,37 @@ function applyPublicPort(config: MasterConfig, endpoints: DerivedEndpoints): Mas
};
}
function resolveAppOrigin(appEndpoint: string): string {
try {
return new URL(appEndpoint).origin;
} catch {
throw new Error(`FLUXER_APP_ENDPOINT must be a valid URL: ${appEndpoint}`);
}
}
function applyPasskeyDefaults(config: MasterConfig, endpoints: DerivedEndpoints): void {
const passkeys = config.auth.passkeys;
if (passkeys.rp_id.trim().length === 0) {
passkeys.rp_id = config.domain.base_domain;
}
if (passkeys.additional_allowed_origins.length === 0) {
passkeys.additional_allowed_origins = [resolveAppOrigin(endpoints.app)];
}
}
export async function loadConfig(): Promise<MasterConfig> {
if (cachedConfig) {
return cachedConfig;
}
const merged = mergeConfig(defaultConfig(), buildNamedFluxerEnvOverrides(process.env));
const overrides = buildNamedFluxerEnvOverrides(process.env);
const merged = mergeConfig(defaultConfig(), overrides);
const normalized = normalizeConfig(merged);
const derived = deriveEndpointsFromDomain(normalized.domain);
const endpoints = {...derived, ...(normalized.endpoint_overrides ?? {})};
cachedConfig = applyPublicPort(normalized, endpoints);
requireString(endpoints.api_client, 'FLUXER_API_CLIENT_ENDPOINT');
const withPublicPort = applyPublicPort(normalized, endpoints);
applyPasskeyDefaults(withPublicPort, withPublicPort.endpoints);
cachedConfig = withPublicPort;
return cachedConfig;
}
@@ -1,5 +1,7 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
const DOCS_ENDPOINT = 'https://fluxer.dev';
export interface DomainConfig {
base_domain: string;
public_scheme: 'http' | 'https';
@@ -19,6 +21,7 @@ export interface DerivedEndpoints {
media: string;
static_cdn: string;
admin: string;
docs: string;
marketing: string;
invite: string;
gift: string;
@@ -83,6 +86,7 @@ export function deriveDomain(
| 'media'
| 'static_cdn'
| 'admin'
| 'docs'
| 'marketing'
| 'invite'
| 'gift',
@@ -113,6 +117,7 @@ export function deriveEndpointsFromDomain(config: DomainConfig): DerivedEndpoint
? buildUrl('https', deriveDomain('static_cdn', config), undefined)
: buildUrl(public_scheme, deriveDomain('static_cdn', config), public_port),
admin: buildUrl(public_scheme, deriveDomain('admin', config), public_port, '/admin'),
docs: DOCS_ENDPOINT,
marketing: buildUrl(public_scheme, deriveDomain('marketing', config), public_port, '/marketing'),
invite: buildUrl(public_scheme, deriveDomain('invite', config), public_port, '/invite'),
gift: buildUrl(public_scheme, deriveDomain('gift', config), public_port, '/gift'),
+1 -2
View File
@@ -77,7 +77,6 @@ export interface MasterConfig {
downloads: string;
reports: string;
harvests: string;
static: string;
};
};
s3_downloads?: {
@@ -146,6 +145,7 @@ export interface MasterConfig {
mode: string;
upload_relay: {
endpoint: string;
secret_base64: string;
max_body_bytes: number;
token_ttl_secs: number;
keep_direct_countries: Array<string>;
@@ -156,7 +156,6 @@ export interface MasterConfig {
rpc_auth_token?: string;
media_proxy_endpoint?: string;
api_rpc_endpoint?: string;
push_enabled: boolean;
};
admin: {
port: number;
@@ -1,5 +1,6 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
import {generateKeyPairSync} from 'node:crypto';
import {getConfig, loadConfig, resetConfig} from '@fluxer/config/src/ConfigLoader';
import {afterEach, beforeEach, describe, expect, test, vi} from 'vitest';
@@ -18,6 +19,7 @@ const MINIMAL_ENV: Record<string, string> = {
FLUXER_S3_ACCESS_KEY_ID: 'test-key',
FLUXER_S3_SECRET_ACCESS_KEY: 'test-secret',
FLUXER_MEDIA_PROXY_SECRET_KEY: 'test-media-secret',
FLUXER_MEDIA_PROXY_UPLOAD_RELAY_SECRET_BASE64: 'AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8=',
FLUXER_ADMIN_SECRET_KEY_BASE: 'test-admin-secret',
FLUXER_ADMIN_OAUTH_CLIENT_SECRET: 'test-admin-oauth-secret',
FLUXER_MARKETING_SECRET_KEY_BASE: 'test-marketing-secret',
@@ -26,10 +28,22 @@ const MINIMAL_ENV: Record<string, string> = {
FLUXER_GATEWAY_RPC_AUTH_TOKEN: 'test-gateway-token',
FLUXER_SUDO_MODE_SECRET: 'test-sudo-secret',
FLUXER_CONNECTION_INITIATION_SECRET: 'test-connection-secret',
FLUXER_VAPID_PUBLIC_KEY: 'test-vapid-public-key',
FLUXER_VAPID_PRIVATE_KEY: 'test-vapid-private-key',
FLUXER_VAPID_PUBLIC_KEY: 'BB76bTFIuoqmxJtTfZX0yGTn1f_qu9H03B_nkj8OyExJFkN7Y-HBZZzShnHZoEhXKc5ZRy3jFu7OkBbnaQG-4aw',
FLUXER_VAPID_PRIVATE_KEY: 'Xgi-3P8J-I3Q6U1HlCcXMuc_tKLGAM9nIfznX3Hz68o',
};
function generateVapidPair(): {publicKey: string; privateKey: string} {
const {privateKey} = generateKeyPairSync('ec', {namedCurve: 'prime256v1'});
const jwk = privateKey.export({format: 'jwk'});
const x = Buffer.from(jwk.x ?? '', 'base64url');
const y = Buffer.from(jwk.y ?? '', 'base64url');
const d = Buffer.from(jwk.d ?? '', 'base64url');
return {
publicKey: Buffer.concat([Buffer.from([0x04]), x, y]).toString('base64url'),
privateKey: d.toString('base64url'),
};
}
function stubMinimalEnv(overrides: Record<string, string> = {}): void {
for (const [key, value] of Object.entries({...MINIMAL_ENV, ...overrides})) {
vi.stubEnv(key, value);
@@ -201,6 +215,51 @@ describe('ConfigLoader', () => {
]);
});
test('rejects an empty client API endpoint override', async () => {
stubMinimalEnv({FLUXER_API_CLIENT_ENDPOINT: ''});
await expect(loadConfig()).rejects.toThrow('FLUXER_API_CLIENT_ENDPOINT is required');
});
test('defaults the passkey relying party to the deployment domain', async () => {
stubMinimalEnv({
FLUXER_BASE_DOMAIN: 'chat.example.com',
FLUXER_PUBLIC_SCHEME: 'https',
FLUXER_PUBLIC_PORT: '443',
});
const config = await loadConfig();
expect(config.auth.passkeys.rp_id).toBe('chat.example.com');
});
test('derives the passkey origin only when the operator clears the default list', async () => {
stubMinimalEnv({
FLUXER_BASE_DOMAIN: 'chat.example.com',
FLUXER_PUBLIC_SCHEME: 'https',
FLUXER_PUBLIC_PORT: '443',
FLUXER_PASSKEY_ADDITIONAL_ALLOWED_ORIGINS: '',
});
const config = await loadConfig();
expect(config.auth.passkeys.additional_allowed_origins).toEqual(['https://chat.example.com']);
});
test('keeps explicit passkey relying party values', async () => {
stubMinimalEnv({
FLUXER_BASE_DOMAIN: 'chat.example.com',
FLUXER_PUBLIC_SCHEME: 'https',
FLUXER_PUBLIC_PORT: '443',
FLUXER_PASSKEY_RP_ID: 'example.com',
FLUXER_PASSKEY_ADDITIONAL_ALLOWED_ORIGINS: 'https://example.com,https://app.example.com',
});
const config = await loadConfig();
expect(config.auth.passkeys.rp_id).toBe('example.com');
expect(config.auth.passkeys.additional_allowed_origins).toEqual(['https://example.com', 'https://app.example.com']);
});
test('parses typed named environment variables', async () => {
stubMinimalEnv({
FLUXER_API_PORT: '9090',
@@ -212,7 +271,6 @@ describe('ConfigLoader', () => {
FLUXER_POSTGRES_PREPARED_STATEMENTS: 'false',
FLUXER_API_WORKER_MODE: 'single_task',
FLUXER_API_WORKER_TASK: 'processStripeWebhook',
FLUXER_GATEWAY_PUSH_ENABLED: 'false',
FLUXER_ACCOUNT_POLICY_DSL: '{"version":1,"id":"env_policy","rules":[]}',
FLUXER_LIVEKIT_ENABLED: 'true',
FLUXER_LIVEKIT_DEFAULT_REGION:
@@ -230,7 +288,6 @@ describe('ConfigLoader', () => {
expect(config.database.postgres.prepared_statements).toBe(false);
expect(config.services.api.worker?.mode).toBe('single_task');
expect(config.services.api.worker?.task).toBe('processStripeWebhook');
expect(config.services.gateway.push_enabled).toBe(false);
expect(config.integrations.risk_integration.account_policy_dsl).toEqual({
version: 1,
id: 'env_policy',
@@ -239,6 +296,14 @@ describe('ConfigLoader', () => {
expect(config.integrations.voice.default_region?.id).toBe('local');
});
test('ignores FLUXER_GATEWAY_PUSH_ENABLED, which only the gateway reads', async () => {
stubMinimalEnv({FLUXER_GATEWAY_PUSH_ENABLED: 'false'});
const config = await loadConfig();
expect(config.services.gateway).not.toHaveProperty('push_enabled');
});
test('rejects single task worker mode without task env', async () => {
stubMinimalEnv({FLUXER_API_WORKER_MODE: 'single_task'});
await expect(loadConfig()).rejects.toThrow('FLUXER_API_WORKER_TASK');
@@ -249,6 +314,16 @@ describe('ConfigLoader', () => {
await expect(loadConfig()).rejects.toThrow('FLUXER_POSTGRES_PORT');
});
test('rejects a non-integer port', async () => {
stubMinimalEnv({FLUXER_API_PORT: '80a'});
await expect(loadConfig()).rejects.toThrow('FLUXER_API_PORT must be an integer, got "80a"');
});
test('rejects malformed JSON for a JSON-shaped variable', async () => {
stubMinimalEnv({FLUXER_LIVEKIT_DEFAULT_REGION: '{bad'});
await expect(loadConfig()).rejects.toThrow('FLUXER_LIVEKIT_DEFAULT_REGION must be valid JSON');
});
test('keeps Postgres prepared statements on by default', async () => {
stubMinimalEnv();
expect((await loadConfig()).database.postgres.prepared_statements).toBe(true);
@@ -283,6 +358,16 @@ describe('ConfigLoader', () => {
await expect(loadConfig()).rejects.toThrow('FLUXER_API_REQUEST_TIMEOUT_MS');
});
test('applies the KV mode from the environment', async () => {
stubMinimalEnv({FLUXER_KV_MODE: 'cluster'});
expect((await loadConfig()).internal.kv_mode).toBe('cluster');
});
test('rejects an unknown KV mode', async () => {
stubMinimalEnv({FLUXER_KV_MODE: 'sentinel'});
await expect(loadConfig()).rejects.toThrow('Invalid FLUXER_KV_MODE: sentinel');
});
test('rejects unsafe production Postgres defaults', async () => {
stubMinimalEnv({FLUXER_ENV: 'production'});
await expect(loadConfig()).rejects.toThrow('FLUXER_POSTGRES_HOST');
@@ -405,6 +490,158 @@ describe('ConfigLoader', () => {
});
});
test('rejects an enabled captcha with no keys for the selected provider', async () => {
stubMinimalEnv({FLUXER_CAPTCHA_ENABLED: 'true', FLUXER_CAPTCHA_PROVIDER: 'hcaptcha'});
await expect(loadConfig()).rejects.toThrow('FLUXER_CAPTCHA_HCAPTCHA_SITE_KEY is required');
});
test('rejects an enabled captcha with a site key but no secret key', async () => {
stubMinimalEnv({
FLUXER_CAPTCHA_ENABLED: 'true',
FLUXER_CAPTCHA_PROVIDER: 'turnstile',
FLUXER_CAPTCHA_TURNSTILE_SITE_KEY: 'turnstile-site-key',
});
await expect(loadConfig()).rejects.toThrow('FLUXER_CAPTCHA_TURNSTILE_SECRET_KEY is required');
});
test('rejects an enabled captcha with no provider', async () => {
stubMinimalEnv({FLUXER_CAPTCHA_ENABLED: 'true'});
await expect(loadConfig()).rejects.toThrow(
'FLUXER_CAPTCHA_PROVIDER must be hcaptcha or turnstile when FLUXER_CAPTCHA_ENABLED is true',
);
});
test('accepts an enabled captcha with both keys for the selected provider', async () => {
stubMinimalEnv({
FLUXER_CAPTCHA_ENABLED: 'true',
FLUXER_CAPTCHA_PROVIDER: 'hcaptcha',
FLUXER_CAPTCHA_HCAPTCHA_SITE_KEY: 'hcaptcha-site-key',
FLUXER_CAPTCHA_HCAPTCHA_SECRET_KEY: 'hcaptcha-secret-key',
});
const config = await loadConfig();
expect(config.integrations.captcha.enabled).toBe(true);
expect(config.integrations.captcha.hcaptcha?.secret_key).toBe('hcaptcha-secret-key');
});
test('leaves a disabled captcha unvalidated', async () => {
stubMinimalEnv({FLUXER_CAPTCHA_PROVIDER: 'hcaptcha'});
expect((await loadConfig()).integrations.captcha.enabled).toBe(false);
});
test('leaves Bluesky login off with no legal URLs by default', async () => {
stubMinimalEnv();
const config = await loadConfig();
expect(config.auth.bluesky.enabled).toBe(false);
expect(config.auth.bluesky.tos_uri).toBe('');
expect(config.auth.bluesky.policy_uri).toBe('');
expect(config.auth.bluesky.keys).toEqual([]);
});
test('applies explicit Bluesky legal URLs from the environment', async () => {
stubMinimalEnv({
FLUXER_AUTH_BLUESKY_ENABLED: 'true',
FLUXER_AUTH_BLUESKY_TOS_URI: 'https://chat.example.com/terms',
FLUXER_AUTH_BLUESKY_POLICY_URI: 'https://chat.example.com/privacy',
});
const config = await loadConfig();
expect(config.auth.bluesky.enabled).toBe(true);
expect(config.auth.bluesky.tos_uri).toBe('https://chat.example.com/terms');
expect(config.auth.bluesky.policy_uri).toBe('https://chat.example.com/privacy');
});
test('reads the upload relay secret through the override table', async () => {
const secret = Buffer.alloc(32, 9).toString('base64');
stubMinimalEnv({FLUXER_MEDIA_PROXY_UPLOAD_RELAY_SECRET_BASE64: secret});
const config = await loadConfig();
expect(config.services.media_proxy.upload_relay.secret_base64).toBe(secret);
});
test('defaults the upload relay body limit to the media proxy ceiling', async () => {
stubMinimalEnv();
expect((await loadConfig()).services.media_proxy.upload_relay.max_body_bytes).toBe(524_288_000);
});
test('rejects a missing upload relay secret in upload mode', async () => {
stubMinimalEnv();
vi.stubEnv('FLUXER_MEDIA_PROXY_UPLOAD_RELAY_SECRET_BASE64', '');
await expect(loadConfig()).rejects.toThrow(
'FLUXER_MEDIA_PROXY_UPLOAD_RELAY_SECRET_BASE64 is required in upload mode',
);
});
test('rejects a non-base64 upload relay secret', async () => {
stubMinimalEnv({FLUXER_MEDIA_PROXY_UPLOAD_RELAY_SECRET_BASE64: 'not base64!'});
await expect(loadConfig()).rejects.toThrow('FLUXER_MEDIA_PROXY_UPLOAD_RELAY_SECRET_BASE64 must be base64');
});
test('rejects an upload relay secret shorter than 32 bytes', async () => {
stubMinimalEnv({FLUXER_MEDIA_PROXY_UPLOAD_RELAY_SECRET_BASE64: Buffer.alloc(16, 7).toString('base64')});
await expect(loadConfig()).rejects.toThrow(
'FLUXER_MEDIA_PROXY_UPLOAD_RELAY_SECRET_BASE64 must decode to at least 32 bytes',
);
});
test('leaves the upload relay secret optional outside upload mode', async () => {
stubMinimalEnv({FLUXER_MEDIA_PROXY_MODE: 'mp'});
vi.stubEnv('FLUXER_MEDIA_PROXY_UPLOAD_RELAY_SECRET_BASE64', '');
const config = await loadConfig();
expect(config.services.media_proxy.upload_relay.secret_base64).toBe('');
});
test('rejects a VAPID public key that is not a 65-byte uncompressed point', async () => {
const {privateKey} = generateVapidPair();
stubMinimalEnv({
FLUXER_VAPID_PUBLIC_KEY: Buffer.alloc(64, 4).toString('base64url'),
FLUXER_VAPID_PRIVATE_KEY: privateKey,
});
await expect(loadConfig()).rejects.toThrow(
'FLUXER_VAPID_PUBLIC_KEY must be the base64url 65-byte uncompressed P-256 point',
);
});
test('rejects a VAPID private key that is not a 32-byte scalar', async () => {
const {publicKey} = generateVapidPair();
stubMinimalEnv({
FLUXER_VAPID_PUBLIC_KEY: publicKey,
FLUXER_VAPID_PRIVATE_KEY: Buffer.alloc(31, 9).toString('base64url'),
});
await expect(loadConfig()).rejects.toThrow('FLUXER_VAPID_PRIVATE_KEY must be the base64url 32-byte P-256 scalar');
});
test('rejects a well formed VAPID scalar that does not derive the public point', async () => {
const {publicKey} = generateVapidPair();
const other = generateVapidPair();
stubMinimalEnv({
FLUXER_VAPID_PUBLIC_KEY: publicKey,
FLUXER_VAPID_PRIVATE_KEY: other.privateKey,
});
await expect(loadConfig()).rejects.toThrow('FLUXER_VAPID_PRIVATE_KEY does not match FLUXER_VAPID_PUBLIC_KEY');
});
test('accepts a generated VAPID pair', async () => {
const pair = generateVapidPair();
stubMinimalEnv({
FLUXER_VAPID_PUBLIC_KEY: pair.publicKey,
FLUXER_VAPID_PRIVATE_KEY: pair.privateKey,
});
const config = await loadConfig();
expect(config.auth.vapid.public_key).toBe(pair.publicKey);
expect(config.auth.vapid.private_key).toBe(pair.privateKey);
});
test('requires a complete environment', async () => {
vi.stubEnv('FLUXER_ENV', 'test');
await expect(loadConfig()).rejects.toThrow();
@@ -31,8 +31,8 @@ describe('parseEnvValue', () => {
test('parses JSON arrays', () => {
expect(parseEnvValue('[1, 2, 3]')).toEqual([1, 2, 3]);
});
test('returns raw string for invalid JSON-like values', () => {
expect(parseEnvValue('{not json}')).toBe('{not json}');
test('rejects invalid JSON-like values', () => {
expect(() => parseEnvValue('{not json}')).toThrow('must be valid JSON');
});
test('returns raw string for plain strings', () => {
expect(parseEnvValue('hello')).toBe('hello');
@@ -106,4 +106,53 @@ describe('buildNamedFluxerEnvOverrides', () => {
integrations: {stripe: {prices: {monthly_usd: 'price_monthly_usd'}}},
});
});
test('maps the internal scheme and KV provider names', () => {
expect(buildNamedFluxerEnvOverrides({FLUXER_INTERNAL_SCHEME: 'https', FLUXER_KV_PROVIDER: 'redis'})).toMatchObject({
domain: {internal_scheme: 'https'},
internal: {kv_provider: 'redis'},
});
});
test('rejects a non-integer value for an integer override', () => {
expect(() => buildNamedFluxerEnvOverrides({FLUXER_API_PORT: '80a'})).toThrow(
'FLUXER_API_PORT must be an integer, got "80a"',
);
});
test('leaves the default in place for a blank integer override', () => {
expect(buildNamedFluxerEnvOverrides({FLUXER_API_PORT: ''})).toEqual({});
});
test('the canonical name wins over its alias regardless of declaration order', () => {
expect(
buildNamedFluxerEnvOverrides({
FLUXER_MEDIA_PROXY_ENDPOINT: 'http://alias',
FLUXER_INTERNAL_MEDIA_PROXY_ENDPOINT: 'http://canonical',
FLUXER_NATS_CORE_URL: 'nats://alias',
FLUXER_NATS_URL: 'nats://canonical',
}),
).toMatchObject({
internal: {media_proxy: 'http://canonical'},
services: {nats: {core_url: 'nats://canonical'}},
});
});
test('an alias alone still applies', () => {
expect(
buildNamedFluxerEnvOverrides({
FLUXER_MEDIA_PROXY_ENDPOINT: 'http://alias',
FLUXER_NATS_CORE_URL: 'nats://alias',
}),
).toMatchObject({
internal: {media_proxy: 'http://alias'},
services: {nats: {core_url: 'nats://alias'}},
});
});
test('rejects malformed JSON for a JSON-shaped override', () => {
expect(() => buildNamedFluxerEnvOverrides({FLUXER_LIVEKIT_DEFAULT_REGION: '{bad'})).toThrow(
'FLUXER_LIVEKIT_DEFAULT_REGION must be valid JSON',
);
});
});
@@ -11,7 +11,7 @@ const base: Pick<MasterConfig, 's3' | 's3_downloads'>['s3'] = {
region: 'us-east-1',
access_key_id: 'MAIN_KEY',
secret_access_key: 'MAIN_SECRET',
buckets: {cdn: 'cdn', uploads: 'uploads', downloads: 'downloads', reports: 'r', harvests: 'h', static: 's'},
buckets: {cdn: 'cdn', uploads: 'uploads', downloads: 'downloads', reports: 'r', harvests: 'h'},
};
describe('resolveDownloadsProvider', () => {
@@ -15,7 +15,8 @@ const NAMED_FLUXER_ENV_OVERRIDES: Record<string, NamedEnvOverride> = {
FLUXER_ENV: {path: ['env']},
FLUXER_BASE_DOMAIN: {path: ['domain', 'base_domain']},
FLUXER_PUBLIC_SCHEME: {path: ['domain', 'public_scheme']},
FLUXER_PUBLIC_PORT: {path: ['domain', 'public_port'], parse: parseEnvValue},
FLUXER_INTERNAL_SCHEME: {path: ['domain', 'internal_scheme']},
FLUXER_PUBLIC_PORT: {path: ['domain', 'public_port'], parse: parseInteger},
FLUXER_STATIC_CDN_DOMAIN: {path: ['domain', 'static_cdn_domain']},
FLUXER_INVITE_DOMAIN: {path: ['domain', 'invite_domain']},
FLUXER_GIFT_DOMAIN: {path: ['domain', 'gift_domain']},
@@ -26,34 +27,36 @@ const NAMED_FLUXER_ENV_OVERRIDES: Record<string, NamedEnvOverride> = {
FLUXER_MEDIA_ENDPOINT: {path: ['endpoint_overrides', 'media']},
FLUXER_STATIC_CDN_ENDPOINT: {path: ['endpoint_overrides', 'static_cdn']},
FLUXER_ADMIN_ENDPOINT: {path: ['endpoint_overrides', 'admin']},
FLUXER_DOCS_ENDPOINT: {path: ['endpoint_overrides', 'docs']},
FLUXER_MARKETING_ENDPOINT: {path: ['endpoint_overrides', 'marketing']},
FLUXER_INVITE_ENDPOINT: {path: ['endpoint_overrides', 'invite']},
FLUXER_GIFT_ENDPOINT: {path: ['endpoint_overrides', 'gift']},
FLUXER_TRUST_CLIENT_IP_HEADER: {path: ['proxy', 'trust_client_ip_header'], parse: parseEnvValue},
FLUXER_CLIENT_IP_HEADER_NAME: {path: ['proxy', 'client_ip_header']},
FLUXER_CASSANDRA_HOSTS: {path: ['database', 'cassandra', 'hosts'], parse: parseCsv},
FLUXER_CASSANDRA_PORT: {path: ['database', 'cassandra', 'port'], parse: parseEnvValue},
FLUXER_CASSANDRA_PORT: {path: ['database', 'cassandra', 'port'], parse: parseInteger},
FLUXER_CASSANDRA_KEYSPACE: {path: ['database', 'cassandra', 'keyspace']},
FLUXER_CASSANDRA_LOCAL_DC: {path: ['database', 'cassandra', 'local_dc']},
FLUXER_CASSANDRA_USERNAME: {path: ['database', 'cassandra', 'username']},
FLUXER_CASSANDRA_PASSWORD: {path: ['database', 'cassandra', 'password']},
FLUXER_POSTGRES_URL: {path: ['database', 'postgres', 'url']},
FLUXER_POSTGRES_HOST: {path: ['database', 'postgres', 'host']},
FLUXER_POSTGRES_PORT: {path: ['database', 'postgres', 'port'], parse: parseEnvValue},
FLUXER_POSTGRES_PORT: {path: ['database', 'postgres', 'port'], parse: parseInteger},
FLUXER_POSTGRES_DATABASE: {path: ['database', 'postgres', 'database']},
FLUXER_POSTGRES_USERNAME: {path: ['database', 'postgres', 'username']},
FLUXER_POSTGRES_PASSWORD: {path: ['database', 'postgres', 'password']},
FLUXER_POSTGRES_SSL: {path: ['database', 'postgres', 'ssl'], parse: parseEnvValue},
FLUXER_POSTGRES_SSL_CA: {path: ['database', 'postgres', 'ssl_ca']},
FLUXER_POSTGRES_MAX_CONNECTIONS: {path: ['database', 'postgres', 'max_connections'], parse: parseEnvValue},
FLUXER_POSTGRES_MAX_CONNECTIONS: {path: ['database', 'postgres', 'max_connections'], parse: parseInteger},
FLUXER_POSTGRES_KV_TABLE: {path: ['database', 'postgres', 'kv_table']},
FLUXER_POSTGRES_PREPARED_STATEMENTS: {path: ['database', 'postgres', 'prepared_statements'], parse: parseEnvValue},
FLUXER_DATABASE_BACKEND: {path: ['database', 'backend']},
FLUXER_KV_URL: {path: ['internal', 'kv']},
FLUXER_KV_PROVIDER: {path: ['internal', 'kv_provider']},
FLUXER_KV_MODE: {path: ['internal', 'kv_mode']},
FLUXER_INTERNAL_API_ENDPOINT: {path: ['internal', 'api']},
FLUXER_INTERNAL_GATEWAY_ENDPOINT: {path: ['internal', 'gateway']},
FLUXER_INTERNAL_MEDIA_PROXY_ENDPOINT: {path: ['internal', 'media_proxy']},
FLUXER_MEDIA_PROXY_ENDPOINT: {path: ['internal', 'media_proxy']},
FLUXER_S3_ENDPOINT: {path: ['s3', 'endpoint']},
FLUXER_S3_PUBLIC_ENDPOINT: {path: ['s3', 'presigned_url_base']},
FLUXER_S3_FORCE_PATH_STYLE: {path: ['s3', 'force_path_style'], parse: parseEnvValue},
@@ -65,7 +68,6 @@ const NAMED_FLUXER_ENV_OVERRIDES: Record<string, NamedEnvOverride> = {
FLUXER_S3_BUCKET_DOWNLOADS: {path: ['s3', 'buckets', 'downloads']},
FLUXER_S3_BUCKET_REPORTS: {path: ['s3', 'buckets', 'reports']},
FLUXER_S3_BUCKET_HARVESTS: {path: ['s3', 'buckets', 'harvests']},
FLUXER_S3_BUCKET_STATIC: {path: ['s3', 'buckets', 'static']},
FLUXER_S3_DOWNLOADS_ENDPOINT: {path: ['s3_downloads', 'endpoint']},
FLUXER_S3_DOWNLOADS_PUBLIC_ENDPOINT: {path: ['s3_downloads', 'presigned_url_base']},
FLUXER_S3_DOWNLOADS_FORCE_PATH_STYLE: {path: ['s3_downloads', 'force_path_style'], parse: parseEnvValue},
@@ -73,13 +75,12 @@ const NAMED_FLUXER_ENV_OVERRIDES: Record<string, NamedEnvOverride> = {
FLUXER_S3_DOWNLOADS_ACCESS_KEY_ID: {path: ['s3_downloads', 'access_key_id']},
FLUXER_S3_DOWNLOADS_SECRET_ACCESS_KEY: {path: ['s3_downloads', 'secret_access_key']},
FLUXER_NATS_URL: {path: ['services', 'nats', 'core_url']},
FLUXER_NATS_CORE_URL: {path: ['services', 'nats', 'core_url']},
FLUXER_NATS_JETSTREAM_URL: {path: ['services', 'nats', 'jetstream_url']},
FLUXER_NATS_AUTH_TOKEN: {path: ['services', 'nats', 'auth_token']},
FLUXER_API_PORT: {path: ['services', 'api', 'port'], parse: parseEnvValue},
FLUXER_API_HEADERS_TIMEOUT_MS: {path: ['services', 'api', 'headers_timeout_ms'], parse: parseEnvValue},
FLUXER_API_REQUEST_TIMEOUT_MS: {path: ['services', 'api', 'request_timeout_ms'], parse: parseEnvValue},
FLUXER_API_MAX_INFLIGHT_REQUESTS: {path: ['services', 'api', 'max_inflight_requests'], parse: parseEnvValue},
FLUXER_API_PORT: {path: ['services', 'api', 'port'], parse: parseInteger},
FLUXER_API_HEADERS_TIMEOUT_MS: {path: ['services', 'api', 'headers_timeout_ms'], parse: parseInteger},
FLUXER_API_REQUEST_TIMEOUT_MS: {path: ['services', 'api', 'request_timeout_ms'], parse: parseInteger},
FLUXER_API_MAX_INFLIGHT_REQUESTS: {path: ['services', 'api', 'max_inflight_requests'], parse: parseInteger},
FLUXER_API_IP_BAN_EXEMPT_IPS: {path: ['services', 'api', 'ip_ban_exempt_ips'], parse: parseCsv},
FLUXER_API_DESKTOP_GITHUB_REDIRECT_COUNTRIES: {
path: ['services', 'api', 'desktop_github_redirect_countries'],
@@ -110,27 +111,27 @@ const NAMED_FLUXER_ENV_OVERRIDES: Record<string, NamedEnvOverride> = {
},
FLUXER_API_WORKER_VOICE_RECONCILIATION_INTERVAL_MS: {
path: ['services', 'api', 'worker', 'voice_reconciliation', 'interval_ms'],
parse: parseEnvValue,
parse: parseInteger,
},
FLUXER_API_WORKER_VOICE_RECONCILIATION_STAGGER_DELAY_MS: {
path: ['services', 'api', 'worker', 'voice_reconciliation', 'stagger_delay_ms'],
parse: parseEnvValue,
parse: parseInteger,
},
FLUXER_API_WORKER_VOICE_RECONCILIATION_LOCK_TTL_SECONDS: {
path: ['services', 'api', 'worker', 'voice_reconciliation', 'lock_ttl_seconds'],
parse: parseEnvValue,
parse: parseInteger,
},
FLUXER_API_WORKER_VOICE_RECONCILIATION_CADENCE_TTL_SECONDS: {
path: ['services', 'api', 'worker', 'voice_reconciliation', 'cadence_ttl_seconds'],
parse: parseEnvValue,
parse: parseInteger,
},
FLUXER_API_WORKER_VOICE_RECONCILIATION_GATEWAY_ONLY_GRACE_MS: {
path: ['services', 'api', 'worker', 'voice_reconciliation', 'gateway_only_grace_ms'],
parse: parseEnvValue,
parse: parseInteger,
},
FLUXER_API_WORKER_VOICE_RECONCILIATION_LIVEKIT_ONLY_GRACE_MS: {
path: ['services', 'api', 'worker', 'voice_reconciliation', 'livekit_only_grace_ms'],
parse: parseEnvValue,
parse: parseInteger,
},
FLUXER_API_WORKER_LANE_CONCURRENCY_OVERRIDES: {
path: ['services', 'api', 'worker', 'lane_concurrency_overrides'],
@@ -151,15 +152,15 @@ const NAMED_FLUXER_ENV_OVERRIDES: Record<string, NamedEnvOverride> = {
},
FLUXER_API_EMBEDS_CACHE_DEFAULT_TTL_SECONDS: {
path: ['services', 'api', 'embeds', 'cache_default_ttl_seconds'],
parse: parseEnvValue,
parse: parseInteger,
},
FLUXER_API_EMBEDS_CACHE_MAX_TTL_SECONDS: {
path: ['services', 'api', 'embeds', 'cache_max_ttl_seconds'],
parse: parseEnvValue,
parse: parseInteger,
},
FLUXER_API_EMBEDS_CACHE_MIN_TTL_SECONDS: {
path: ['services', 'api', 'embeds', 'cache_min_ttl_seconds'],
parse: parseEnvValue,
parse: parseInteger,
},
FLUXER_API_EMBEDS_CACHE_RESPECT_REMOTE_TTL: {
path: ['services', 'api', 'embeds', 'cache_respect_remote_ttl'],
@@ -170,58 +171,58 @@ const NAMED_FLUXER_ENV_OVERRIDES: Record<string, NamedEnvOverride> = {
parse: parseEnvValue,
},
FLUXER_MEDIA_PROXY_HOST: {path: ['services', 'media_proxy', 'host']},
FLUXER_MEDIA_PROXY_PORT: {path: ['services', 'media_proxy', 'port'], parse: parseEnvValue},
FLUXER_MEDIA_PROXY_PORT: {path: ['services', 'media_proxy', 'port'], parse: parseInteger},
FLUXER_MEDIA_PROXY_SECRET_KEY: {path: ['services', 'media_proxy', 'secret_key']},
FLUXER_MEDIA_PROXY_MODE: {path: ['services', 'media_proxy', 'mode']},
FLUXER_MEDIA_PROXY_UPLOAD_RELAY_ENDPOINT: {path: ['services', 'media_proxy', 'upload_relay', 'endpoint']},
FLUXER_MEDIA_PROXY_UPLOAD_RELAY_SECRET_BASE64: {path: ['services', 'media_proxy', 'upload_relay', 'secret_base64']},
FLUXER_MEDIA_PROXY_UPLOAD_RELAY_MAX_BODY_BYTES: {
path: ['services', 'media_proxy', 'upload_relay', 'max_body_bytes'],
parse: parseEnvValue,
parse: parseInteger,
},
FLUXER_MEDIA_PROXY_UPLOAD_RELAY_TOKEN_TTL_SECS: {
path: ['services', 'media_proxy', 'upload_relay', 'token_ttl_secs'],
parse: parseEnvValue,
parse: parseInteger,
},
FLUXER_MEDIA_PROXY_UPLOAD_RELAY_KEEP_DIRECT_COUNTRIES: {
path: ['services', 'media_proxy', 'upload_relay', 'keep_direct_countries'],
parse: parseCsv,
},
FLUXER_ADMIN_PORT: {path: ['services', 'admin', 'port'], parse: parseEnvValue},
FLUXER_ADMIN_PORT: {path: ['services', 'admin', 'port'], parse: parseInteger},
FLUXER_ADMIN_BASE_PATH: {path: ['services', 'admin', 'base_path']},
FLUXER_ADMIN_SECRET_KEY_BASE: {path: ['services', 'admin', 'secret_key_base']},
FLUXER_ADMIN_OAUTH_CLIENT_SECRET: {path: ['services', 'admin', 'oauth_client_secret']},
FLUXER_MARKETING_HOST: {path: ['services', 'marketing', 'host']},
FLUXER_MARKETING_PORT: {path: ['services', 'marketing', 'port'], parse: parseEnvValue},
FLUXER_MARKETING_PORT: {path: ['services', 'marketing', 'port'], parse: parseInteger},
FLUXER_MARKETING_BASE_PATH: {path: ['services', 'marketing', 'base_path']},
FLUXER_MARKETING_SECRET_KEY_BASE: {path: ['services', 'marketing', 'secret_key_base']},
FLUXER_APP_PROXY_PORT: {path: ['services', 'app_proxy', 'port'], parse: parseEnvValue},
FLUXER_APP_PROXY_PORT: {path: ['services', 'app_proxy', 'port'], parse: parseInteger},
FLUXER_STATIC_DIR: {path: ['services', 'app_proxy', 'assets_dir']},
FLUXER_GATEWAY_PORT: {path: ['services', 'gateway', 'port'], parse: parseEnvValue},
FLUXER_GATEWAY_PORT: {path: ['services', 'gateway', 'port'], parse: parseInteger},
FLUXER_GATEWAY_ROLE: {path: ['services', 'gateway', 'gateway_role']},
FLUXER_GATEWAY_MEDIA_PROXY_ENDPOINT: {path: ['services', 'gateway', 'media_proxy_endpoint']},
FLUXER_GATEWAY_API_RPC_ENDPOINT: {path: ['services', 'gateway', 'api_rpc_endpoint']},
FLUXER_GATEWAY_RPC_AUTH_TOKEN: {path: ['services', 'gateway', 'rpc_auth_token']},
FLUXER_GATEWAY_PUSH_ENABLED: {path: ['services', 'gateway', 'push_enabled'], parse: parseEnvValue},
FLUXER_GATEWAY_LOGGER_LEVEL: {path: ['services', 'gateway', 'logger_level']},
FLUXER_GATEWAY_HTTP_FAILURE_THRESHOLD: {
path: ['services', 'gateway', 'gateway_http_failure_threshold'],
parse: parseEnvValue,
parse: parseInteger,
},
FLUXER_GATEWAY_HTTP_RECOVERY_TIMEOUT_MS: {
path: ['services', 'gateway', 'gateway_http_recovery_timeout_ms'],
parse: parseEnvValue,
parse: parseInteger,
},
FLUXER_GATEWAY_HTTP_RPC_MAX_CONCURRENCY: {
path: ['services', 'gateway', 'gateway_http_rpc_max_concurrency'],
parse: parseEnvValue,
parse: parseInteger,
},
FLUXER_GATEWAY_NATS_RPC_MAX_HANDLERS: {
path: ['services', 'gateway', 'gateway_nats_rpc_max_handlers'],
parse: parseEnvValue,
parse: parseInteger,
},
FLUXER_GATEWAY_SHUTDOWN_DRAIN_WAIT_MS: {
path: ['services', 'gateway', 'shutdown_drain_wait_ms'],
parse: parseEnvValue,
parse: parseInteger,
},
FLUXER_GATEWAY_CLUSTER_ENABLED: {path: ['services', 'gateway', 'cluster_enabled'], parse: parseEnvValue},
FLUXER_GATEWAY_CLUSTER_DISCOVERY_DNS_NAME: {path: ['services', 'gateway', 'cluster_discovery_dns_name']},
@@ -230,7 +231,7 @@ const NAMED_FLUXER_ENV_OVERRIDES: Record<string, NamedEnvOverride> = {
},
FLUXER_GATEWAY_CLUSTER_DISCOVERY_POLL_INTERVAL_MS: {
path: ['services', 'gateway', 'cluster_discovery_poll_interval_ms'],
parse: parseEnvValue,
parse: parseInteger,
},
FLUXER_SUDO_MODE_SECRET: {path: ['auth', 'sudo_mode_secret']},
FLUXER_CONNECTION_INITIATION_SECRET: {path: ['auth', 'connection_initiation_secret']},
@@ -258,7 +259,7 @@ const NAMED_FLUXER_ENV_OVERRIDES: Record<string, NamedEnvOverride> = {
FLUXER_EMAIL_APP_BASE_URL: {path: ['integrations', 'email', 'app_base_url']},
FLUXER_EMAIL_WEBHOOK_SECRET: {path: ['integrations', 'email', 'webhook_secret']},
FLUXER_EMAIL_SMTP_HOST: {path: ['integrations', 'email', 'smtp', 'host']},
FLUXER_EMAIL_SMTP_PORT: {path: ['integrations', 'email', 'smtp', 'port'], parse: parseEnvValue},
FLUXER_EMAIL_SMTP_PORT: {path: ['integrations', 'email', 'smtp', 'port'], parse: parseInteger},
FLUXER_EMAIL_SMTP_USERNAME: {path: ['integrations', 'email', 'smtp', 'username']},
FLUXER_EMAIL_SMTP_PASSWORD: {path: ['integrations', 'email', 'smtp', 'password']},
FLUXER_EMAIL_SMTP_SECURE: {path: ['integrations', 'email', 'smtp', 'secure'], parse: parseEnvValue},
@@ -330,14 +331,14 @@ const NAMED_FLUXER_ENV_OVERRIDES: Record<string, NamedEnvOverride> = {
FLUXER_NCMEC_REPORTER_EMAIL: {path: ['integrations', 'ncmec', 'reporter_email']},
FLUXER_CLAMAV_ENABLED: {path: ['integrations', 'clamav', 'enabled'], parse: parseEnvValue},
FLUXER_CLAMAV_HOST: {path: ['integrations', 'clamav', 'host']},
FLUXER_CLAMAV_PORT: {path: ['integrations', 'clamav', 'port'], parse: parseEnvValue},
FLUXER_CLAMAV_PORT: {path: ['integrations', 'clamav', 'port'], parse: parseInteger},
FLUXER_CLAMAV_FAIL_OPEN: {path: ['integrations', 'clamav', 'fail_open'], parse: parseEnvValue},
FLUXER_KLIPY_API_KEY: {path: ['integrations', 'klipy', 'api_key']},
FLUXER_YOUTUBE_API_KEY: {path: ['integrations', 'youtube', 'api_key']},
FLUXER_BUNNY_PURGE_ENABLED: {path: ['integrations', 'bunny', 'purge_enabled'], parse: parseEnvValue},
FLUXER_BLOCKLIST_FEEDS_ENABLED: {path: ['integrations', 'blocklist_feeds', 'enabled'], parse: parseEnvValue},
FLUXER_BUNNY_API_KEY: {path: ['integrations', 'bunny', 'api_key']},
FLUXER_BUNNY_PULL_ZONE_ID: {path: ['integrations', 'bunny', 'pull_zone_id'], parse: parseEnvValue},
FLUXER_BUNNY_PULL_ZONE_ID: {path: ['integrations', 'bunny', 'pull_zone_id'], parse: parseInteger},
FLUXER_RISK_INTEGRATION_ENABLED: {path: ['integrations', 'risk_integration', 'enabled'], parse: parseEnvValue},
FLUXER_RISK_IPINFO_API_KEY: {path: ['integrations', 'risk_integration', 'ipinfo_api_key']},
FLUXER_ACCOUNT_POLICY_DSL: {
@@ -354,7 +355,7 @@ const NAMED_FLUXER_ENV_OVERRIDES: Record<string, NamedEnvOverride> = {
},
FLUXER_RISK_TOR_REVERSE_DNS_TIMEOUT_MS: {
path: ['integrations', 'risk_integration', 'tor', 'reverse_dns_timeout_ms'],
parse: parseEnvValue,
parse: parseInteger,
},
FLUXER_PUSH_APNS_ENABLED: {path: ['integrations', 'push', 'apns', 'enabled'], parse: parseEnvValue},
FLUXER_PUSH_APNS_TEAM_ID: {path: ['integrations', 'push', 'apns', 'team_id']},
@@ -401,18 +402,18 @@ const NAMED_FLUXER_ENV_OVERRIDES: Record<string, NamedEnvOverride> = {
},
FLUXER_ABUSE_DIRECT_CONTACT_SPAM_DISTINCT_TARGET_THRESHOLD: {
path: ['instance', 'abuse_policy', 'direct_contact_spam', 'distinct_target_threshold'],
parse: parseEnvValue,
parse: parseInteger,
},
FLUXER_ABUSE_DIRECT_CONTACT_SPAM_TARGET_WINDOW_MS: {
path: ['instance', 'abuse_policy', 'direct_contact_spam', 'target_window_ms'],
parse: parseEnvValue,
parse: parseInteger,
},
FLUXER_ABUSE_DIRECT_CONTACT_SPAM_ACTION: {
path: ['instance', 'abuse_policy', 'direct_contact_spam', 'action'],
},
FLUXER_DISCOVERY_ENABLED: {path: ['discovery', 'enabled'], parse: parseEnvValue},
FLUXER_DISCOVERY_MIN_MEMBER_COUNT: {path: ['discovery', 'min_member_count'], parse: parseEnvValue},
FLUXER_DELETION_GRACE_PERIOD_HOURS: {path: ['deletion_grace_period_hours'], parse: parseEnvValue},
FLUXER_DISCOVERY_MIN_MEMBER_COUNT: {path: ['discovery', 'min_member_count'], parse: parseInteger},
FLUXER_DELETION_GRACE_PERIOD_HOURS: {path: ['deletion_grace_period_hours'], parse: parseInteger},
FLUXER_RELAX_REGISTRATION_RATE_LIMITS: {path: ['dev', 'relax_registration_rate_limits'], parse: parseEnvValue},
FLUXER_DISABLE_RATE_LIMITS: {path: ['dev', 'disable_rate_limits'], parse: parseEnvValue},
FLUXER_TEST_MODE_ENABLED: {path: ['dev', 'test_mode_enabled'], parse: parseEnvValue},
@@ -467,16 +468,27 @@ export function parseEnvValue(raw: string): unknown {
if (/^-?\d+\.\d+$/.test(trimmed)) {
return Number.parseFloat(trimmed);
}
if ((trimmed.startsWith('{') && trimmed.endsWith('}')) || (trimmed.startsWith('[') && trimmed.endsWith(']'))) {
if (trimmed.startsWith('{') || trimmed.startsWith('[')) {
try {
return JSON.parse(trimmed);
} catch {
return raw;
} catch (error) {
throw new Error(`must be valid JSON: ${error instanceof Error ? error.message : String(error)}`);
}
}
return raw;
}
function parseInteger(raw: string): number | undefined {
const trimmed = raw.trim();
if (trimmed.length === 0) {
return undefined;
}
if (!/^-?\d+$/.test(trimmed)) {
throw new Error(`must be an integer, got ${JSON.stringify(raw)}`);
}
return Number.parseInt(trimmed, 10);
}
function parseCsv(raw: string): Array<string> {
return raw
.split(',')
@@ -500,14 +512,29 @@ export function setNestedValue(target: ConfigContainer, keys: Array<ConfigPathKe
setNestedValue(toChildContainer(getChildValue(target, first), rest[0]), rest, value);
}
const NAMED_FLUXER_ENV_ALIASES: Record<string, string | undefined> = {
FLUXER_INTERNAL_MEDIA_PROXY_ENDPOINT: 'FLUXER_MEDIA_PROXY_ENDPOINT',
FLUXER_NATS_URL: 'FLUXER_NATS_CORE_URL',
};
export function buildNamedFluxerEnvOverrides(env: NodeJS.ProcessEnv): ConfigObject {
const overrides: ConfigObject = {};
for (const [envKey, mapping] of Object.entries(NAMED_FLUXER_ENV_OVERRIDES)) {
const raw = env[envKey];
const alias = NAMED_FLUXER_ENV_ALIASES[envKey];
const raw = env[envKey] ?? (alias === undefined ? undefined : env[alias]);
if (raw === undefined) {
continue;
}
setNestedValue(overrides, mapping.path, (mapping.parse ?? ((value: string) => value))(raw));
let parsed: unknown;
try {
parsed = (mapping.parse ?? ((value: string) => value))(raw);
} catch (error) {
throw new Error(`${envKey} ${error instanceof Error ? error.message : String(error)}`);
}
if (parsed === undefined) {
continue;
}
setNestedValue(overrides, mapping.path, parsed);
}
return overrides;
}