Files
fluxer/packages/config/src/ConfigLoader.ts
T

738 lines
23 KiB
TypeScript

// SPDX-License-Identifier: AGPL-3.0-or-later
import {createECDH} from 'node:crypto';
import {isConfigObject} from '@fluxer/config/src/config_loader/ConfigObject';
import {buildNamedFluxerEnvOverrides, readEnvValue} from '@fluxer/config/src/config_loader/EnvironmentOverrides';
import {
buildUrl,
type DerivedEndpoints,
deriveEndpointsFromDomain,
normalizePublicEndpoint,
parsePublicOrigin,
parseWebOrigin,
} from '@fluxer/config/src/EndpointDerivation';
import {CACHE_PURGE_ADAPTER_NAMES, type MasterConfig, STORE_PRODUCT_SLOT_NAMES} from '@fluxer/config/src/MasterConfig';
let cachedConfig: MasterConfig | null = null;
const DEFAULT_PASSKEY_ORIGINS = [
'https://fluxer.app',
'https://web.fluxer.app',
'https://web.canary.fluxer.app',
'https://fluxer.com',
'https://canary.fluxer.com',
'android:apk-key-hash:keSY4bimyLqZQV7bKXgpa2xYuqXi0qZJzsYtp6gpx7w',
];
function defaultConfig(): MasterConfig {
return {
env: 'development',
domain: {
base_domain: '',
public_origin: '',
public_scheme: 'http',
public_port: 8088,
static_cdn_domain: '',
invite_domain: '',
gift_domain: '',
},
endpoints: {
api: '',
api_client: '',
app: '',
gateway: '',
media: '',
static_cdn: '',
admin: '',
marketing: '',
invite: '',
gift: '',
},
internal: {
kv: 'redis://localhost:6379/0',
kv_mode: 'standalone',
media_proxy: 'http://127.0.0.1:8082',
},
database: {
backend: 'postgres',
cassandra: {
hosts: ['127.0.0.1'],
port: 9042,
keyspace: 'fluxer',
local_dc: 'datacenter1',
username: '',
password: '',
},
postgres: {
url: '',
host: '127.0.0.1',
port: 5432,
database: 'fluxer',
username: 'fluxer',
password: 'fluxer',
ssl: false,
ssl_ca: '',
max_connections: 20,
kv_table: 'fluxer_kv',
prepared_statements: true,
},
},
s3: {
endpoint: 'http://localhost:3900',
force_path_style: false,
region: 'local',
access_key_id: '',
secret_access_key: '',
buckets: {
cdn: 'fluxer',
uploads: 'fluxer-uploads',
reports: 'fluxer-reports',
harvests: 'fluxer-harvests',
},
},
services: {
api: {
port: 8080,
headers_timeout_ms: 30_000,
request_timeout_ms: 120_000,
max_inflight_requests: 512,
ip_ban_exempt_ips: [],
donation_proxy_key: '',
presigned_attachment_uploads_enabled: false,
presigned_harvest_downloads_enabled: true,
unfurl_ignored_hosts: [],
app_origin_aliases: [],
storage_change_feed: {
enabled: false,
stream: 'STORAGE_CHANGES',
},
},
nats: {
core_url: 'nats://127.0.0.1:4222',
jetstream_url: 'nats://127.0.0.1:4222',
auth_token: '',
},
media_proxy: {
secret_key: '',
upload_relay: {
endpoint: 'http://localhost:8088/media',
secret_base64: '',
max_body_bytes: 524_288_000,
token_ttl_secs: 900,
keep_direct_countries: [],
},
attachment_urls: {
secrets_base64: [],
},
},
gateway: {
rpc_auth_token: '',
},
admin: {
secret_key_base: '',
oauth_client_secret: '',
},
},
auth: {
sudo_mode_secret: '',
connection_initiation_secret: '',
sso_allow_private_addresses: false,
passkeys: {
rp_name: 'Fluxer',
rp_id: '',
additional_allowed_origins: DEFAULT_PASSKEY_ORIGINS,
},
vapid: {
public_key: '',
private_key: '',
email: '',
},
bluesky: {
enabled: false,
client_name: 'Fluxer',
client_uri: '',
logo_uri: '',
tos_uri: '',
policy_uri: '',
keys: [],
},
},
integrations: {
email: {
enabled: false,
provider: 'none',
from_email: '',
from_name: 'Fluxer',
app_base_url: '',
},
voice: {
enabled: false,
api_key: '',
api_secret: '',
url: '',
internal_url: '',
},
search: {
engine: 'elasticsearch',
url: 'http://127.0.0.1:9200',
api_key: '',
username: '',
password: '',
tls_reject_unauthorized: true,
},
stripe: {
enabled: false,
secret_key: '',
webhook_secret: '',
prices: {},
legacy_prices: {},
},
ncmec: {
enabled: false,
base_url: '',
username: '',
password: '',
},
clamav: {
enabled: false,
host: '127.0.0.1',
port: 3310,
fail_open: false,
},
klipy: {
api_key: '',
},
youtube: {
api_key: '',
},
cache_purge: {
adapter: 'none',
http: {
endpoint: '',
token: '',
timeout_ms: 10_000,
},
},
blocklist_feeds: {},
breached_password_check: {},
push: {
apns: {
enabled: false,
apps: [],
},
},
app_store: {
enabled: false,
apps: [],
products: {},
},
google_play: {
enabled: false,
packages: [],
token_uri: 'https://oauth2.googleapis.com/token',
products: {},
},
store_billing: {
sandbox_user_ids: [],
sandbox_entitles_all: false,
},
},
instance: {
self_hosted: false,
branding: {
product_name: 'Fluxer',
},
setup: {
configured: false,
},
},
dev: {
relax_registration_rate_limits: false,
disable_rate_limits: false,
test_mode_enabled: false,
},
geoip: {
maxmind_db_path: '',
},
proxy: {
trust_client_ip_header: false,
client_ip_header: 'x-forwarded-for',
},
discovery: {
enabled: true,
min_member_count: 1,
},
attachment_decay_enabled: true,
deletion_grace_period_hours: 336,
inactivity_deletion_threshold_days: 365,
};
}
function mergeConfig<T>(base: T, overrides: unknown): T {
if (!isConfigObject(base) || !isConfigObject(overrides)) {
return overrides === undefined ? base : (overrides as T);
}
const out = new Map(Object.entries(base));
for (const [key, value] of Object.entries(overrides)) {
const current = out.get(key);
out.set(key, isConfigObject(current) && isConfigObject(value) ? mergeConfig(current, value) : value);
}
return Object.fromEntries(out) as T;
}
function assertOneOf<T extends string>(value: string, allowed: ReadonlyArray<T>, path: string): asserts value is T {
if (!allowed.includes(value as T)) {
throw new Error(`Invalid ${path}: ${value}`);
}
}
function requireString(value: string | undefined, envName: string): void {
if (!value || value.trim().length === 0) {
throw new Error(`${envName} is required`);
}
}
function validateUploadRelaySecret(value: string): void {
const trimmed = value.trim();
if (trimmed.length === 0) {
throw new Error('FLUXER_MEDIA_PROXY_UPLOAD_RELAY_SECRET_BASE64 is required');
}
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 isCanonicalStandardBase64(value: string): boolean {
if (!/^[A-Za-z0-9+/]+={0,2}$/u.test(value)) {
return false;
}
return Buffer.from(value, 'base64').toString('base64') === value;
}
function validateAttachmentUrlSecrets(values: Array<string>): void {
for (const value of values) {
const trimmed = value.trim();
if (trimmed.length === 0) {
continue;
}
if (!isCanonicalStandardBase64(trimmed)) {
throw new Error('FLUXER_MEDIA_PROXY_ATTACHMENT_URL_SECRETS_BASE64 entries must be standard base64');
}
if (Buffer.from(trimmed, 'base64').length < 32) {
throw new Error('FLUXER_MEDIA_PROXY_ATTACHMENT_URL_SECRETS_BASE64 entries 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`);
}
}
function assertIntegerInRange(value: unknown, envName: string, min: number, max: number): asserts value is number {
if (typeof value !== 'number' || !Number.isInteger(value) || value < min || value > max) {
throw new Error(`${envName} must be an integer between ${min} and ${max}`);
}
}
function assertIdentifier(value: string, envName: string): void {
if (!/^[A-Za-z_][A-Za-z0-9_]*$/u.test(value)) {
throw new Error(`${envName} must be a safe Postgres identifier`);
}
}
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');
}
let derived: Buffer;
try {
const curve = createECDH('prime256v1');
curve.setPrivateKey(priv);
derived = curve.getPublicKey();
} catch {
throw new Error('FLUXER_VAPID_PRIVATE_KEY does not match FLUXER_VAPID_PUBLIC_KEY');
}
if (!derived.equals(pub)) {
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);
assertIntegerInRange(postgres.max_connections, 'FLUXER_POSTGRES_MAX_CONNECTIONS', 1, 1000);
assertBoolean(postgres.ssl, 'FLUXER_POSTGRES_SSL');
assertIdentifier(postgres.kv_table, 'FLUXER_POSTGRES_KV_TABLE');
assertBoolean(postgres.prepared_statements, 'FLUXER_POSTGRES_PREPARED_STATEMENTS');
if (config.env !== 'production' || config.database.backend !== 'postgres') {
return;
}
if (!postgres.url) {
requireString(postgres.host, 'FLUXER_POSTGRES_HOST');
requireString(postgres.database, 'FLUXER_POSTGRES_DATABASE');
requireString(postgres.username, 'FLUXER_POSTGRES_USERNAME');
requireString(postgres.password, 'FLUXER_POSTGRES_PASSWORD');
if (['127.0.0.1', 'localhost'].includes(postgres.host.trim().toLowerCase())) {
throw new Error('FLUXER_POSTGRES_HOST must be explicitly configured for production');
}
if (postgres.password === 'fluxer') {
throw new Error('FLUXER_POSTGRES_PASSWORD must not use the development default in production');
}
}
if (!postgres.ssl && !config.instance.self_hosted) {
throw new Error('FLUXER_POSTGRES_SSL must be true in production');
}
}
function validateApiWorkerConfig(config: MasterConfig): void {
const worker = config.services.api?.worker;
if (!worker) {
return;
}
if (worker.mode !== undefined) {
assertOneOf(worker.mode, ['all_lanes', 'single_lane', 'single_task'], 'FLUXER_API_WORKER_MODE');
}
if (worker.lane !== undefined) {
assertOneOf(worker.lane, ['realtime', 'unfurl', 'lifecycle', 'batch'], 'FLUXER_API_WORKER_LANE');
}
if (worker.mode === 'single_task') {
requireString(worker.task, 'FLUXER_API_WORKER_TASK');
}
}
function validateStorageChangeFeedConfig(config: MasterConfig): void {
const feed = config.services.api?.storage_change_feed;
if (!feed?.enabled) {
return;
}
if (feed.stream === undefined || !/^[A-Za-z0-9_-]+$/u.test(feed.stream)) {
throw new Error(
'FLUXER_API_STORAGE_CHANGE_FEED_STREAM must be letters, digits, underscores or hyphens when the storage change feed is enabled',
);
}
}
function normalizeAppOriginAliases(config: MasterConfig): void {
const api = config.services.api;
api.app_origin_aliases = [
...new Set(
api.app_origin_aliases.map((alias, index) => {
const origin = parseWebOrigin(alias);
if (!origin) {
throw new Error(`FLUXER_APP_ORIGIN_ALIASES entry ${index + 1} must be an HTTP(S) origin`);
}
return origin.origin;
}),
),
];
}
function validateCachePurgeConfig(config: MasterConfig): void {
const cachePurge = config.integrations.cache_purge;
if (cachePurge.adapter !== 'http') {
return;
}
requireString(cachePurge.http.endpoint, 'FLUXER_CACHE_PURGE_HTTP_ENDPOINT');
const endpoint = URL.parse(cachePurge.http.endpoint);
if (
endpoint === null ||
(endpoint.protocol !== 'http:' && endpoint.protocol !== 'https:') ||
endpoint.username !== '' ||
endpoint.password !== ''
) {
throw new Error('FLUXER_CACHE_PURGE_HTTP_ENDPOINT must be an absolute http or https URL without credentials');
}
requireString(cachePurge.http.token, 'FLUXER_CACHE_PURGE_HTTP_TOKEN');
if (!/^[\x21-\x7e]+$/u.test(cachePurge.http.token)) {
throw new Error('FLUXER_CACHE_PURGE_HTTP_TOKEN must contain only visible ASCII characters');
}
assertIntegerInRange(cachePurge.http.timeout_ms, 'FLUXER_CACHE_PURGE_HTTP_TIMEOUT_MS', 1_000, 10_000);
}
function validateStoreProductSlots(
products: Record<string, string> | undefined,
envName: string,
isValidKey: (productKey: string, slot: string) => boolean,
): void {
for (const [productKey, slot] of Object.entries(products ?? {})) {
assertOneOf(slot, STORE_PRODUCT_SLOT_NAMES, `${envName} slot for ${productKey}`);
if (!isValidKey(productKey, slot)) {
throw new Error(`${envName} key ${productKey} does not match its ${slot} slot`);
}
}
}
function validateStoreBillingConfig(config: MasterConfig): void {
const appStore = config.integrations.app_store;
for (const [index, app] of (appStore.apps ?? []).entries()) {
if (!isConfigObject(app) || typeof app.bundle_id !== 'string' || app.bundle_id.trim().length === 0) {
throw new Error(`FLUXER_APP_STORE_APPS entry ${index + 1} must have a bundle_id`);
}
if (typeof app.app_apple_id !== 'number' || !Number.isSafeInteger(app.app_apple_id) || app.app_apple_id <= 0) {
throw new Error(`FLUXER_APP_STORE_APPS entry ${index + 1} must have a numeric app_apple_id`);
}
}
validateStoreProductSlots(appStore.products, 'FLUXER_APP_STORE_PRODUCTS', (productKey) => productKey.length > 0);
validateStoreProductSlots(
config.integrations.google_play.products,
'FLUXER_GOOGLE_PLAY_PRODUCTS',
(productKey, slot) => {
const parts = productKey.split(':');
const isSubscription = slot === 'monthly' || slot === 'yearly';
if (isSubscription) {
return parts.length === 2 && parts.every((part) => part.length > 0);
}
return parts.length === 1 && productKey.length > 0;
},
);
for (const userId of config.integrations.store_billing.sandbox_user_ids ?? []) {
if (!/^\d+$/.test(userId)) {
throw new Error('FLUXER_STORE_BILLING_SANDBOX_USER_IDS must be a comma separated list of user ids');
}
}
assertBoolean(config.integrations.store_billing.sandbox_entitles_all, 'FLUXER_STORE_BILLING_SANDBOX_ENTITLES_ALL');
}
function validateDomain(value: string, envName: string): void {
if (value === '') return;
const parsed = URL.parse(`http://${value}/`);
const hasPort = value.startsWith('[') ? !value.endsWith(']') : value.includes(':');
if (
!parsed ||
hasPort ||
/[\s\p{Cc}/\\?#@%]/u.test(value) ||
!parsed.hostname ||
parsed.port ||
parsed.username ||
parsed.password ||
parsed.pathname !== '/' ||
parsed.search ||
parsed.hash
) {
throw new Error(`${envName} must be a hostname without a scheme, port, credentials, or path`);
}
}
function validatePublicEndpoints(endpoints: DerivedEndpoints): void {
for (const [key, value] of Object.entries(endpoints)) {
const envName = `FLUXER_${key.toUpperCase()}_ENDPOINT`;
requireString(value, envName);
const gateway = key === 'gateway';
const parsed = URL.parse(value);
if (parsed?.port === '0') throw new Error(`${envName} must not use port 0`);
const authority = /^[a-z]+:\/\/([^/?#]+)/i.exec(value)?.[1];
const allowedProtocols = gateway ? ['ws:', 'wss:'] : ['http:', 'https:'];
if (
!parsed ||
!authority ||
!allowedProtocols.includes(parsed.protocol) ||
!parsed.hostname ||
/[\s\p{Cc}\\]/u.test(value) ||
authority.includes('@') ||
authority.endsWith(':') ||
value.includes('#') ||
(!gateway && value.includes('?'))
) {
const expected = gateway
? 'ws/wss URL without credentials or a fragment'
: 'http/https base URL without credentials, a query, or a fragment';
throw new Error(`${envName} must be an absolute ${expected}`);
}
}
}
function normalizeConfig(config: MasterConfig): MasterConfig {
assertOneOf(config.env, ['development', 'production', 'test'], 'FLUXER_ENV');
assertOneOf(config.domain.public_scheme, ['http', 'https'], 'FLUXER_PUBLIC_SCHEME');
assertOneOf(config.database.backend, ['postgres', 'cassandra'], 'FLUXER_DATABASE_BACKEND');
assertOneOf(config.internal.kv_mode, ['standalone', 'cluster'], 'FLUXER_KV_MODE');
assertOneOf(config.integrations.email.provider, ['smtp', 'none'], 'FLUXER_EMAIL_PROVIDER');
assertOneOf(config.integrations.search.engine, ['elasticsearch', 'meilisearch'], 'FLUXER_SEARCH_ENGINE');
assertOneOf(config.integrations.cache_purge.adapter, CACHE_PURGE_ADAPTER_NAMES, 'FLUXER_CACHE_PURGE_ADAPTER');
validatePostgresConfig(config);
validateApiWorkerConfig(config);
validateStorageChangeFeedConfig(config);
validateCachePurgeConfig(config);
validateStoreBillingConfig(config);
normalizeAppOriginAliases(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);
assertIntegerInRange(config.services.api.request_timeout_ms, 'FLUXER_API_REQUEST_TIMEOUT_MS', 1_000, 3_600_000);
assertIntegerInRange(config.domain.public_port, 'FLUXER_PUBLIC_PORT', 1, 65_535);
requireString(config.domain.base_domain, 'FLUXER_BASE_DOMAIN');
for (const key of ['base_domain', 'static_cdn_domain', 'invite_domain', 'gift_domain'] as const) {
validateDomain(config.domain[key], `FLUXER_${key.toUpperCase()}`);
}
requireString(config.auth.sudo_mode_secret, 'FLUXER_SUDO_MODE_SECRET');
requireString(config.auth.connection_initiation_secret, 'FLUXER_CONNECTION_INITIATION_SECRET');
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);
validateAttachmentUrlSecrets(config.services.media_proxy.attachment_urls.secrets_base64);
requireString(config.services.admin.secret_key_base, 'FLUXER_ADMIN_SECRET_KEY_BASE');
requireString(config.services.admin.oauth_client_secret, 'FLUXER_ADMIN_OAUTH_CLIENT_SECRET');
requireString(config.services.gateway.rpc_auth_token, 'FLUXER_GATEWAY_RPC_AUTH_TOKEN');
return config;
}
function applyPublicOrigin(config: MasterConfig): MasterConfig {
const raw = config.domain.public_origin;
if (raw.trim().length === 0 && !/\p{Cc}/u.test(raw)) {
return config;
}
const origin = parsePublicOrigin(raw);
if (!origin) {
throw new Error(
'FLUXER_PUBLIC_ORIGIN must be a scheme, host and optional port such as https://chat.example.com:8443',
);
}
return {
...config,
domain: {
...config.domain,
base_domain: origin.base_domain,
public_scheme: origin.public_scheme,
public_port: origin.public_port,
},
};
}
function applyPublicPort(config: MasterConfig, endpoints: DerivedEndpoints): MasterConfig {
const {base_domain, public_port} = config.domain;
const comparisonHost = new URL(buildUrl('http', base_domain)).hostname;
const normalize = (url: string) => normalizePublicEndpoint(url, comparisonHost, public_port);
const normalizeOptional = (url: string | undefined) => (url === undefined ? undefined : normalize(url));
const normalizedEndpoints = {...endpoints};
for (const key of Object.keys(normalizedEndpoints) as Array<keyof DerivedEndpoints>) {
normalizedEndpoints[key] = normalize(normalizedEndpoints[key]);
}
const {bluesky, passkeys} = config.auth;
const {branding} = config.instance;
const {email, voice} = config.integrations;
return {
...config,
domain: {
...config.domain,
public_origin: buildUrl(config.domain.public_scheme, base_domain, public_port),
},
endpoints: normalizedEndpoints,
s3: config.s3 && {...config.s3, presigned_url_base: normalizeOptional(config.s3.presigned_url_base)},
services: {
...config.services,
media_proxy: {
...config.services.media_proxy,
upload_relay: {
...config.services.media_proxy.upload_relay,
endpoint: normalize(config.services.media_proxy.upload_relay.endpoint),
},
},
},
auth: {
...config.auth,
passkeys: {
...passkeys,
additional_allowed_origins: passkeys.additional_allowed_origins.map(normalize),
},
bluesky: {
...bluesky,
client_uri: normalize(bluesky.client_uri),
logo_uri: normalize(bluesky.logo_uri),
tos_uri: normalize(bluesky.tos_uri),
policy_uri: normalize(bluesky.policy_uri),
},
},
integrations: {
...config.integrations,
email: {...email, app_base_url: normalize(email.app_base_url)},
voice: {...voice, url: normalize(voice.url)},
},
instance: {
...config.instance,
branding: {
...branding,
icon_url: normalizeOptional(branding.icon_url),
symbol_url: normalizeOptional(branding.symbol_url),
logo_url: normalizeOptional(branding.logo_url),
wordmark_url: normalizeOptional(branding.wordmark_url),
favicon_url: normalizeOptional(branding.favicon_url),
status_page_url: normalizeOptional(branding.status_page_url),
status_page_incident_history_url: normalizeOptional(branding.status_page_incident_history_url),
},
},
};
}
function normalizePasskeyOrigin(origin: string, index: number): string {
const webOrigin = parseWebOrigin(origin);
if (webOrigin) {
return webOrigin.origin;
}
const fingerprint = /^android:apk-key-hash:([A-Za-z0-9_-]{43})$/.exec(origin)?.[1];
if (fingerprint) {
const bytes = Buffer.from(fingerprint, 'base64url');
if (bytes.length === 32 && bytes.toString('base64url') === fingerprint) {
return origin;
}
}
throw new Error(
`FLUXER_PASSKEY_ADDITIONAL_ALLOWED_ORIGINS entry ${index + 1} must be an HTTP(S) origin or a canonical Android signing-certificate origin`,
);
}
function normalizePasskeys(config: MasterConfig, useDefaultOrigins: boolean): void {
const passkeys = config.auth.passkeys;
passkeys.additional_allowed_origins = passkeys.additional_allowed_origins.map(normalizePasskeyOrigin);
if (passkeys.rp_id.trim().length === 0) {
passkeys.rp_id = config.domain.base_domain;
}
if (useDefaultOrigins || passkeys.additional_allowed_origins.length === 0) {
passkeys.additional_allowed_origins = [
...new Set([...passkeys.additional_allowed_origins, new URL(config.endpoints.app).origin]),
];
}
}
export async function loadConfig(): Promise<MasterConfig> {
if (cachedConfig) {
return cachedConfig;
}
const overrides = buildNamedFluxerEnvOverrides(process.env);
const merged = applyPublicOrigin(mergeConfig(defaultConfig(), overrides));
const normalized = normalizeConfig(merged);
const derived = deriveEndpointsFromDomain(normalized.domain);
const endpoints = {...derived, ...(normalized.endpoint_overrides ?? {})};
validatePublicEndpoints(endpoints);
const withPublicPort = applyPublicPort(normalized, endpoints);
normalizePasskeys(
withPublicPort,
readEnvValue(process.env, 'FLUXER_PASSKEY_ADDITIONAL_ALLOWED_ORIGINS') === undefined,
);
cachedConfig = withPublicPort;
return cachedConfig;
}
export function getConfig(): MasterConfig {
if (!cachedConfig) {
throw new Error('Config not loaded. Call loadConfig() first.');
}
return cachedConfig;
}
export function resetConfig(): void {
cachedConfig = null;
}