refactor(self-hosting): forward every setting, drop dead config (#3047)

This commit is contained in:
Hampus
2026-09-30 00:58:43 +02:00
committed by GitHub
parent 39f9beda5a
commit 2b8a743dc5
96 changed files with 1743 additions and 2391 deletions
+8 -48
View File
@@ -2,7 +2,7 @@
import {createECDH} from 'node:crypto';
import {isConfigObject} from '@fluxer/config/src/config_loader/ConfigObject';
import {buildNamedFluxerEnvOverrides} from '@fluxer/config/src/config_loader/EnvironmentOverrides';
import {buildNamedFluxerEnvOverrides, readEnvValue} from '@fluxer/config/src/config_loader/EnvironmentOverrides';
import {
buildUrl,
type DerivedEndpoints,
@@ -31,9 +31,7 @@ function defaultConfig(): MasterConfig {
base_domain: '',
public_origin: '',
public_scheme: 'http',
internal_scheme: 'http',
public_port: 8088,
internal_port: 8088,
static_cdn_domain: '',
invite_domain: '',
gift_domain: '',
@@ -46,18 +44,13 @@ function defaultConfig(): MasterConfig {
media: '',
static_cdn: '',
admin: '',
docs: '',
marketing: '',
invite: '',
gift: '',
},
internal: {
kv: 'redis://localhost:6379/0',
kv_provider: 'redis',
kv_mode: 'standalone',
kv_cluster_nodes: [],
kv_cluster_nat_map: {},
api: 'http://127.0.0.1:8080',
media_proxy: 'http://127.0.0.1:8082',
},
database: {
@@ -109,18 +102,6 @@ function defaultConfig(): MasterConfig {
presigned_harvest_downloads_enabled: true,
unfurl_ignored_hosts: [],
app_origin_aliases: [],
embeds: {
oembed_html_enabled: false,
oembed_html_allow_untrusted_on_self_hosted: false,
oembed_html_allowed_hosts: [],
cache_default_ttl_seconds: 86_400,
cache_max_ttl_seconds: 604_800,
cache_min_ttl_seconds: 300,
cache_respect_remote_ttl: true,
},
content_moderation: {
nsfw_threshold: 0.7,
},
storage_change_feed: {
enabled: false,
stream: 'STORAGE_CHANGES',
@@ -132,10 +113,7 @@ function defaultConfig(): MasterConfig {
auth_token: '',
},
media_proxy: {
host: '0.0.0.0',
port: 8082,
secret_key: '',
mode: 'upload',
upload_relay: {
endpoint: 'http://localhost:8088/media',
secret_base64: '',
@@ -148,19 +126,12 @@ function defaultConfig(): MasterConfig {
},
},
gateway: {
port: 8771,
rpc_auth_token: '',
},
admin: {
port: 3020,
base_path: '/admin',
secret_key_base: '',
oauth_client_secret: '',
},
app_proxy: {
port: 8773,
assets_dir: 'fluxer_app/dist',
},
},
auth: {
sudo_mode_secret: '',
@@ -200,7 +171,6 @@ function defaultConfig(): MasterConfig {
api_secret: '',
url: '',
internal_url: '',
webhook_url: '',
},
search: {
engine: 'elasticsearch',
@@ -253,10 +223,6 @@ function defaultConfig(): MasterConfig {
enabled: false,
apps: [],
},
fcm: {
enabled: false,
apps: [],
},
},
},
instance: {
@@ -314,13 +280,10 @@ function requireString(value: string | undefined, envName: string): void {
}
}
function validateUploadRelaySecret(value: string, mode: string): void {
function validateUploadRelaySecret(value: 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;
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');
@@ -537,9 +500,7 @@ function validatePublicEndpoints(endpoints: DerivedEndpoints): void {
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.domain.internal_scheme, ['http', 'https'], 'FLUXER_INTERNAL_SCHEME');
assertOneOf(config.database.backend, ['postgres', 'cassandra'], 'FLUXER_DATABASE_BACKEND');
assertOneOf(config.internal.kv_provider, ['redis'], 'FLUXER_KV_PROVIDER');
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');
@@ -563,7 +524,7 @@ function normalizeConfig(config: MasterConfig): MasterConfig {
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);
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');
@@ -622,10 +583,6 @@ function applyPublicPort(config: MasterConfig, endpoints: DerivedEndpoints): Mas
endpoint: normalize(config.services.media_proxy.upload_relay.endpoint),
},
},
gateway: {
...config.services.gateway,
media_proxy_endpoint: normalizeOptional(config.services.gateway.media_proxy_endpoint),
},
},
auth: {
...config.auth,
@@ -703,7 +660,10 @@ export async function loadConfig(): Promise<MasterConfig> {
const endpoints = {...derived, ...(normalized.endpoint_overrides ?? {})};
validatePublicEndpoints(endpoints);
const withPublicPort = applyPublicPort(normalized, endpoints);
normalizePasskeys(withPublicPort, process.env.FLUXER_PASSKEY_ADDITIONAL_ALLOWED_ORIGINS === undefined);
normalizePasskeys(
withPublicPort,
readEnvValue(process.env, 'FLUXER_PASSKEY_ADDITIONAL_ALLOWED_ORIGINS') === undefined,
);
cachedConfig = withPublicPort;
return cachedConfig;
}
@@ -1,13 +1,9 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
const DOCS_ENDPOINT = 'https://fluxer.dev';
export interface DomainConfig {
base_domain: string;
public_scheme: 'http' | 'https';
internal_scheme: 'http' | 'https';
public_port?: number;
internal_port?: number;
static_cdn_domain?: string;
invite_domain?: string;
gift_domain?: string;
@@ -29,7 +25,6 @@ export interface DerivedEndpoints {
media: string;
static_cdn: string;
admin: string;
docs: string;
marketing: string;
invite: string;
gift: string;
@@ -129,7 +124,6 @@ export function deriveDomain(
| 'media'
| 'static_cdn'
| 'admin'
| 'docs'
| 'marketing'
| 'invite'
| 'gift',
@@ -160,7 +154,6 @@ 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'),
-49
View File
@@ -26,9 +26,7 @@ export interface MasterConfig {
base_domain: string;
public_origin: string;
public_scheme: PublicScheme;
internal_scheme: PublicScheme;
public_port: number;
internal_port: number;
static_cdn_domain: string;
invite_domain: string;
gift_domain: string;
@@ -37,12 +35,7 @@ export interface MasterConfig {
endpoints: DerivedEndpoints;
internal: {
kv: string;
kv_provider: 'redis';
kv_mode: 'standalone' | 'cluster';
kv_cluster_nodes: Array<{host: string; port: number}>;
kv_cluster_nat_map: Record<string, {host: string; port: number}>;
api: string;
gateway?: string;
media_proxy: string;
};
database: {
@@ -95,18 +88,6 @@ export interface MasterConfig {
presigned_harvest_downloads_enabled: boolean;
unfurl_ignored_hosts: Array<string>;
app_origin_aliases: Array<string>;
embeds: {
oembed_html_enabled: boolean;
oembed_html_allow_untrusted_on_self_hosted: boolean;
oembed_html_allowed_hosts: Array<string>;
cache_default_ttl_seconds: number;
cache_max_ttl_seconds: number;
cache_min_ttl_seconds: number;
cache_respect_remote_ttl: boolean;
};
content_moderation?: {
nsfw_threshold?: number;
};
worker?: {
mode?: 'all_lanes' | 'single_lane' | 'single_task';
lane?: 'realtime' | 'unfurl' | 'lifecycle' | 'batch';
@@ -131,10 +112,7 @@ export interface MasterConfig {
auth_token?: string;
};
media_proxy: {
host: string;
port: number;
secret_key: string;
mode: string;
upload_relay: {
endpoint: string;
secret_base64: string;
@@ -147,21 +125,12 @@ export interface MasterConfig {
};
};
gateway: {
port: number;
rpc_auth_token?: string;
media_proxy_endpoint?: string;
api_rpc_endpoint?: string;
};
admin: {
port: number;
base_path: string;
secret_key_base: string;
oauth_client_secret: string;
};
app_proxy: {
port: number;
assets_dir: string;
};
};
auth: {
sudo_mode_secret: string;
@@ -213,7 +182,6 @@ export interface MasterConfig {
api_secret: string;
url: string;
internal_url: string;
webhook_url: string;
default_region?: {
id: string;
name: string;
@@ -280,27 +248,10 @@ export interface MasterConfig {
key_id?: string;
private_key?: string;
private_key_path?: string;
default_environment?: 'production' | 'development';
apps?: Array<{
app_id?: string;
topic?: string;
environment?: 'production' | 'development';
project_id?: string;
}>;
};
fcm: {
enabled: boolean;
project_id?: string;
client_email?: string;
private_key?: string;
private_key_path?: string;
service_account_json_path?: string;
token_uri?: string;
apps?: Array<{
app_id?: string;
topic?: string;
environment?: 'production' | 'development';
project_id?: string;
}>;
};
};
@@ -22,8 +22,6 @@ const MINIMAL_ENV: Record<string, string> = {
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_APP_PROXY_PORT: '8773',
FLUXER_GATEWAY_MEDIA_PROXY_ENDPOINT: 'http://127.0.0.1:8088/media',
FLUXER_GATEWAY_RPC_AUTH_TOKEN: 'test-gateway-token',
FLUXER_SUDO_MODE_SECRET: 'test-sudo-secret',
FLUXER_CONNECTION_INITIATION_SECRET: 'test-connection-secret',
@@ -243,9 +241,9 @@ describe('ConfigLoader', () => {
},
);
test('rejects an empty client API endpoint override', async () => {
test('an empty client API endpoint override falls back to the derived endpoint', async () => {
stubMinimalEnv({FLUXER_API_CLIENT_ENDPOINT: ''});
await expect(loadConfig()).rejects.toThrow('FLUXER_API_CLIENT_ENDPOINT is required');
expect((await loadConfig()).endpoints.api_client).toBe('http://localhost:8088/api');
});
test('defaults the passkey relying party to the deployment domain', async () => {
@@ -260,17 +258,36 @@ describe('ConfigLoader', () => {
expect(config.auth.passkeys.rp_id).toBe('chat.example.com');
});
test('uses only the app origin when the operator clears the default list', async () => {
test('a blank origin list keeps the built-in origins', async () => {
stubMinimalEnv({
FLUXER_BASE_DOMAIN: 'chat.example.com',
FLUXER_PUBLIC_SCHEME: 'https',
FLUXER_PUBLIC_PORT: '443',
FLUXER_PASSKEY_ADDITIONAL_ALLOWED_ORIGINS: '',
FLUXER_PASSKEY_ADDITIONAL_ALLOWED_ORIGINS: ' ',
});
const config = await loadConfig();
expect(config.auth.passkeys.additional_allowed_origins).toEqual(['https://chat.example.com']);
expect(config.auth.passkeys.additional_allowed_origins).toContain('https://web.fluxer.app');
expect(config.auth.passkeys.additional_allowed_origins).toContain('https://chat.example.com');
});
test('blank values fall back to the code defaults', async () => {
stubMinimalEnv({
FLUXER_API_PORT: '',
FLUXER_EMAIL_FROM_NAME: '',
FLUXER_KV_URL: ' ',
FLUXER_S3_FORCE_PATH_STYLE: '',
FLUXER_API_STORAGE_CHANGE_FEED_SKIP_BUCKETS: '',
});
const config = await loadConfig();
expect(config.services.api.port).toBe(8080);
expect(config.integrations.email.from_name).toBe('Fluxer');
expect(config.internal.kv).toBe('redis://localhost:6379/0');
expect(config.s3?.force_path_style).toBe(false);
expect(config.services.api.storage_change_feed?.skip_buckets).toBeUndefined();
});
test('keeps explicit passkey relying party values', async () => {
@@ -665,13 +682,11 @@ describe('ConfigLoader', () => {
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 () => {
test('rejects a missing upload relay secret', 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',
);
await expect(loadConfig()).rejects.toThrow('FLUXER_MEDIA_PROXY_UPLOAD_RELAY_SECRET_BASE64 is required');
});
test('rejects a non-base64 upload relay secret', async () => {
@@ -686,15 +701,6 @@ describe('ConfigLoader', () => {
);
});
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({
@@ -753,7 +759,6 @@ describe('ConfigLoader', () => {
test('inserts the public port into every other public url the config carries', async () => {
stubMinimalEnv({
FLUXER_GATEWAY_MEDIA_PROXY_ENDPOINT: 'http://localhost/media',
FLUXER_S3_PUBLIC_ENDPOINT: 'http://localhost/s3',
FLUXER_EMAIL_APP_BASE_URL: 'http://localhost',
FLUXER_AUTH_BLUESKY_CLIENT_URI: 'http://localhost',
@@ -764,7 +769,6 @@ describe('ConfigLoader', () => {
const config = await loadConfig();
expect(config.services.gateway.media_proxy_endpoint).toBe('http://localhost:8088/media');
expect(config.s3?.presigned_url_base).toBe('http://localhost:8088/s3');
expect(config.integrations.email.app_base_url).toBe('http://localhost:8088');
expect(config.auth.bluesky.client_uri).toBe('http://localhost:8088');
@@ -34,7 +34,6 @@ describe('deriveDomain', () => {
const baseConfig: DomainConfig = {
base_domain: 'fluxer.dev',
public_scheme: 'https',
internal_scheme: 'http',
};
test.each([
'api',
@@ -44,7 +43,6 @@ describe('deriveDomain', () => {
'media',
'static_cdn',
'admin',
'docs',
'marketing',
'invite',
'gift',
@@ -72,9 +70,7 @@ const endpointScenarios: Array<EndpointScenario> = [
config: {
base_domain: 'localhost',
public_scheme: 'http',
internal_scheme: 'http',
public_port: 8088,
internal_port: 8088,
},
expected: {
api: 'http://localhost:8088/api',
@@ -84,7 +80,6 @@ const endpointScenarios: Array<EndpointScenario> = [
media: 'http://localhost:8088/media',
static_cdn: 'http://localhost:8088',
admin: 'http://localhost:8088/admin',
docs: 'https://fluxer.dev',
marketing: 'http://localhost:8088/marketing',
invite: 'http://localhost:8088/invite',
gift: 'http://localhost:8088/gift',
@@ -95,9 +90,7 @@ const endpointScenarios: Array<EndpointScenario> = [
config: {
base_domain: 'fluxer.app',
public_scheme: 'https',
internal_scheme: 'http',
public_port: 443,
internal_port: 8080,
},
expected: {
api: 'https://fluxer.app/api',
@@ -107,7 +100,6 @@ const endpointScenarios: Array<EndpointScenario> = [
media: 'https://fluxer.app/media',
static_cdn: 'https://fluxer.app',
admin: 'https://fluxer.app/admin',
docs: 'https://fluxer.dev',
marketing: 'https://fluxer.app/marketing',
invite: 'https://fluxer.app/invite',
gift: 'https://fluxer.app/gift',
@@ -118,9 +110,7 @@ const endpointScenarios: Array<EndpointScenario> = [
config: {
base_domain: 'staging.fluxer.dev',
public_scheme: 'https',
internal_scheme: 'http',
public_port: 8443,
internal_port: 8080,
},
expected: {
api: 'https://staging.fluxer.dev:8443/api',
@@ -130,7 +120,6 @@ const endpointScenarios: Array<EndpointScenario> = [
media: 'https://staging.fluxer.dev:8443/media',
static_cdn: 'https://staging.fluxer.dev:8443',
admin: 'https://staging.fluxer.dev:8443/admin',
docs: 'https://fluxer.dev',
marketing: 'https://staging.fluxer.dev:8443/marketing',
invite: 'https://staging.fluxer.dev:8443/invite',
gift: 'https://staging.fluxer.dev:8443/gift',
@@ -141,7 +130,6 @@ const endpointScenarios: Array<EndpointScenario> = [
config: {
base_domain: 'fluxer.app',
public_scheme: 'https',
internal_scheme: 'http',
public_port: 443,
static_cdn_domain: 'cdn.fluxer.app',
},
@@ -153,7 +141,6 @@ const endpointScenarios: Array<EndpointScenario> = [
media: 'https://fluxer.app/media',
static_cdn: 'https://cdn.fluxer.app',
admin: 'https://fluxer.app/admin',
docs: 'https://fluxer.dev',
marketing: 'https://fluxer.app/marketing',
invite: 'https://fluxer.app/invite',
gift: 'https://fluxer.app/gift',
@@ -164,7 +151,6 @@ const endpointScenarios: Array<EndpointScenario> = [
config: {
base_domain: 'fluxer.app',
public_scheme: 'https',
internal_scheme: 'http',
public_port: 443,
invite_domain: 'fluxer.gg',
gift_domain: 'fluxer.gift',
@@ -177,7 +163,6 @@ const endpointScenarios: Array<EndpointScenario> = [
media: 'https://fluxer.app/media',
static_cdn: 'https://fluxer.app',
admin: 'https://fluxer.app/admin',
docs: 'https://fluxer.dev',
marketing: 'https://fluxer.app/marketing',
invite: 'https://fluxer.gg/invite',
gift: 'https://fluxer.gift/gift',
@@ -188,7 +173,6 @@ const endpointScenarios: Array<EndpointScenario> = [
config: {
base_domain: 'canary.fluxer.app',
public_scheme: 'https',
internal_scheme: 'http',
public_port: 443,
static_cdn_domain: 'cdn-canary.fluxer.app',
},
@@ -200,7 +184,6 @@ const endpointScenarios: Array<EndpointScenario> = [
media: 'https://canary.fluxer.app/media',
static_cdn: 'https://cdn-canary.fluxer.app',
admin: 'https://canary.fluxer.app/admin',
docs: 'https://fluxer.dev',
marketing: 'https://canary.fluxer.app/marketing',
invite: 'https://canary.fluxer.app/invite',
gift: 'https://canary.fluxer.app/gift',
@@ -211,7 +194,6 @@ const endpointScenarios: Array<EndpointScenario> = [
config: {
base_domain: 'example.com',
public_scheme: 'http',
internal_scheme: 'http',
public_port: 80,
},
expected: {
@@ -222,7 +204,6 @@ const endpointScenarios: Array<EndpointScenario> = [
media: 'http://example.com/media',
static_cdn: 'http://example.com',
admin: 'http://example.com/admin',
docs: 'https://fluxer.dev',
marketing: 'http://example.com/marketing',
invite: 'http://example.com/invite',
gift: 'http://example.com/gift',
@@ -233,7 +214,6 @@ const endpointScenarios: Array<EndpointScenario> = [
config: {
base_domain: 'example.com',
public_scheme: 'https',
internal_scheme: 'http',
},
expected: {
api: 'https://example.com/api',
@@ -243,7 +223,6 @@ const endpointScenarios: Array<EndpointScenario> = [
media: 'https://example.com/media',
static_cdn: 'https://example.com',
admin: 'https://example.com/admin',
docs: 'https://fluxer.dev',
marketing: 'https://example.com/marketing',
invite: 'https://example.com/invite',
gift: 'https://example.com/gift',
@@ -254,7 +233,6 @@ const endpointScenarios: Array<EndpointScenario> = [
config: {
base_domain: '127.0.0.1',
public_scheme: 'http',
internal_scheme: 'http',
public_port: 8088,
},
expected: {
@@ -265,7 +243,6 @@ const endpointScenarios: Array<EndpointScenario> = [
media: 'http://127.0.0.1:8088/media',
static_cdn: 'http://127.0.0.1:8088',
admin: 'http://127.0.0.1:8088/admin',
docs: 'https://fluxer.dev',
marketing: 'http://127.0.0.1:8088/marketing',
invite: 'http://127.0.0.1:8088/invite',
gift: 'http://127.0.0.1:8088/gift',
@@ -276,7 +253,6 @@ const endpointScenarios: Array<EndpointScenario> = [
config: {
base_domain: 'localhost',
public_scheme: 'http',
internal_scheme: 'http',
public_port: 8088,
static_cdn_domain: 'cdn.example.com',
},
@@ -288,7 +264,6 @@ const endpointScenarios: Array<EndpointScenario> = [
media: 'http://localhost:8088/media',
static_cdn: 'https://cdn.example.com',
admin: 'http://localhost:8088/admin',
docs: 'https://fluxer.dev',
marketing: 'http://localhost:8088/marketing',
invite: 'http://localhost:8088/invite',
gift: 'http://localhost:8088/gift',
@@ -469,10 +444,7 @@ describe('endpoints derived from a public origin', () => {
test('an origin with a non-standard port ports every derived endpoint', () => {
const origin = parsePublicOrigin('https://chat.example.com:29080');
assert.ok(origin);
const endpoints = deriveEndpointsFromDomain({
...origin,
internal_scheme: 'http',
});
const endpoints = deriveEndpointsFromDomain(origin);
expect(endpoints.api_client).toBe('https://chat.example.com:29080/api');
expect(endpoints.app).toBe('https://chat.example.com:29080');
expect(endpoints.gateway).toBe('wss://chat.example.com:29080/gateway');
@@ -481,10 +453,7 @@ describe('endpoints derived from a public origin', () => {
test('an origin written with an explicit :443 derives portless endpoints', () => {
const origin = parsePublicOrigin('https://chat.example.com:443');
assert.ok(origin);
const endpoints = deriveEndpointsFromDomain({
...origin,
internal_scheme: 'http',
});
const endpoints = deriveEndpointsFromDomain(origin);
expect(endpoints.admin).toBe('https://chat.example.com/admin');
expect(endpoints.app).toBe('https://chat.example.com');
expect(endpoints.gateway).toBe('wss://chat.example.com/gateway');
@@ -1,45 +1,8 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
import {
buildNamedFluxerEnvOverrides,
parseEnvValue,
setNestedValue,
} from '@fluxer/config/src/config_loader/EnvironmentOverrides';
import {buildNamedFluxerEnvOverrides, setNestedValue} from '@fluxer/config/src/config_loader/EnvironmentOverrides';
import {describe, expect, test} from 'vitest';
describe('parseEnvValue', () => {
test('parses boolean true', () => {
expect(parseEnvValue('true')).toBe(true);
expect(parseEnvValue(' true ')).toBe(true);
});
test('parses boolean false', () => {
expect(parseEnvValue('false')).toBe(false);
expect(parseEnvValue(' false ')).toBe(false);
});
test('parses integers', () => {
expect(parseEnvValue('42')).toBe(42);
expect(parseEnvValue('-7')).toBe(-7);
expect(parseEnvValue('0')).toBe(0);
});
test('parses floats', () => {
expect(parseEnvValue('3.14')).toBe(3.14);
expect(parseEnvValue('-0.5')).toBe(-0.5);
});
test('parses JSON objects', () => {
expect(parseEnvValue('{"key": "value"}')).toEqual({key: 'value'});
});
test('parses JSON arrays', () => {
expect(parseEnvValue('[1, 2, 3]')).toEqual([1, 2, 3]);
});
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');
expect(parseEnvValue('localhost')).toBe('localhost');
});
});
describe('setNestedValue', () => {
test('sets a top-level key', () => {
const target: Record<string, unknown> = {};
@@ -81,14 +44,13 @@ describe('setNestedValue', () => {
});
describe('buildNamedFluxerEnvOverrides', () => {
test('builds canonical split env overrides and preserves empty strings', () => {
test('builds canonical split env overrides', () => {
const overrides = buildNamedFluxerEnvOverrides({
FLUXER_BASE_DOMAIN: 'canonical.example',
FLUXER_API_ENDPOINT: 'https://canonical.example/api',
FLUXER_PASSKEY_ADDITIONAL_ALLOWED_ORIGINS: 'https://a.example, https://b.example',
FLUXER_S3_FORCE_PATH_STYLE: 'true',
FLUXER_AUTH_BLUESKY_KEYS: '[{"kid":"key-1","private_key_path":"/etc/fluxer/keys/bluesky.pem"}]',
FLUXER_ADMIN_BASE_PATH: '',
FLUXER_STRIPE_PRICE_MONTHLY_USD: 'price_monthly_usd',
});
@@ -100,17 +62,46 @@ describe('buildNamedFluxerEnvOverrides', () => {
bluesky: {keys: [{kid: 'key-1', private_key_path: '/etc/fluxer/keys/bluesky.pem'}]},
},
s3: {force_path_style: true},
services: {
admin: {base_path: ''},
},
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.each(['', ' ', '\t\n'])('treats %j as unset for every value type', (blank) => {
expect(
buildNamedFluxerEnvOverrides({
FLUXER_BASE_DOMAIN: blank,
FLUXER_API_PORT: blank,
FLUXER_S3_FORCE_PATH_STYLE: blank,
FLUXER_API_IP_BAN_EXEMPT_IPS: blank,
FLUXER_PASSKEY_ADDITIONAL_ALLOWED_ORIGINS: blank,
FLUXER_LIVEKIT_DEFAULT_REGION: blank,
FLUXER_AUTH_BLUESKY_KEYS: blank,
FLUXER_EMAIL_FROM_NAME: blank,
}),
).toEqual({});
});
test('a blank canonical name falls through to its alias', () => {
expect(
buildNamedFluxerEnvOverrides({
FLUXER_NATS_URL: '',
FLUXER_NATS_CORE_URL: 'nats://alias',
FLUXER_IPINFO_API_KEY: ' ',
FLUXER_RISK_IPINFO_API_KEY: 'alias-key',
}),
).toMatchObject({
services: {nats: {core_url: 'nats://alias'}},
integrations: {ipinfo: {api_key: 'alias-key'}},
});
});
test('a blank alias is unset too', () => {
expect(buildNamedFluxerEnvOverrides({FLUXER_NATS_URL: '', FLUXER_NATS_CORE_URL: ' '})).toEqual({});
});
test('none clears the storage change feed skip list', () => {
expect(buildNamedFluxerEnvOverrides({FLUXER_API_STORAGE_CHANGE_FEED_SKIP_BUCKETS: ' None '})).toEqual({
services: {api: {storage_change_feed: {skip_buckets: []}}},
});
});
@@ -120,10 +111,6 @@ describe('buildNamedFluxerEnvOverrides', () => {
);
});
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({
@@ -42,8 +42,6 @@ export function serviceEnvironment(name: string): Record<string, string> {
return value === undefined ? {} : environment(value, `services.${name}.environment`);
}
export const sharedEnvironment = environment(compose['x-fluxer-env'], 'x-fluxer-env');
export function serviceList(name: string, key: string): Array<string> {
const value = composeService(name)[key];
assert.ok(Array.isArray(value), `services.${name}.${key} must be a list`);
@@ -1,28 +1,90 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
import {serviceEnvironment, serviceNames, sharedEnvironment} from '@fluxer/config/src/__tests__/SelfHostingCompose';
import {serviceEnvironment, serviceNames} from '@fluxer/config/src/__tests__/SelfHostingCompose';
import {NAMED_FLUXER_ENV_NAMES} from '@fluxer/config/src/config_loader/EnvironmentOverrides';
import {describe, expect, test} from 'vitest';
describe('the shipped compose stack wires every service it starts', () => {
test('no service sizes a pool for a connection it cannot make', () => {
const pooledServices = serviceNames.filter((name) => 'FLUXER_POSTGRES_MAX_CONNECTIONS' in serviceEnvironment(name));
expect(pooledServices.length).toBeGreaterThan(0);
for (const name of pooledServices) {
expect(serviceEnvironment(name), name).toMatchObject({
FLUXER_DATABASE_BACKEND: 'postgres',
FLUXER_POSTGRES_HOST: expect.stringMatching(/\S/u),
FLUXER_POSTGRES_PORT: expect.stringMatching(/\S/u),
FLUXER_POSTGRES_DATABASE: expect.stringMatching(/\S/u),
FLUXER_POSTGRES_USERNAME: expect.stringMatching(/\S/u),
FLUXER_POSTGRES_PASSWORD: expect.stringMatching(/\S/u),
});
}
const API_SETTINGS_NOT_FORWARDED: Record<string, string> = {
FLUXER_CASSANDRA_HOSTS: 'the stack runs Postgres only',
FLUXER_CASSANDRA_PORT: 'the stack runs Postgres only',
FLUXER_CASSANDRA_KEYSPACE: 'the stack runs Postgres only',
FLUXER_CASSANDRA_LOCAL_DC: 'the stack runs Postgres only',
FLUXER_CASSANDRA_USERNAME: 'the stack runs Postgres only',
FLUXER_CASSANDRA_PASSWORD: 'the stack runs Postgres only',
FLUXER_API_WORKER_MODE: 'the one worker container runs every lane',
FLUXER_API_WORKER_LANE: 'the one worker container runs every lane',
FLUXER_API_WORKER_TASK: 'the one worker container runs every lane',
FLUXER_API_WORKER_ENABLE_CRON_SCHEDULER: 'the one worker container hosts cron',
FLUXER_RELAX_REGISTRATION_RATE_LIMITS: 'test and development only',
FLUXER_DISABLE_RATE_LIMITS: 'test and development only',
FLUXER_TEST_MODE_ENABLED: 'test and development only',
FLUXER_TEST_HARNESS_TOKEN: 'test and development only',
FLUXER_VALIDATE_RESPONSES: 'test and development only',
...Object.fromEntries(
['MONTHLY', 'YEARLY', 'GIFT_1_MONTH', 'GIFT_1_YEAR'].flatMap((slot) =>
['USD', 'EUR', 'BRL', 'DKK', 'INR', 'NOK', 'PLN', 'SEK', 'TRY'].map((currency) => [
`FLUXER_STRIPE_PRICE_${slot}_${currency}`,
'FLUXER_STRIPE_PRICES or the dashboard sets prices',
]),
),
),
};
const INPUT_NAMES: Record<string, string> = {
FLUXER_BASE_DOMAIN: 'FLUXER_DOMAIN',
FLUXER_POSTGRES_PASSWORD: 'POSTGRES_PASSWORD',
FLUXER_SEARCH_API_KEY: 'MEILI_MASTER_KEY',
FLUXER_S3_ACCESS_KEY_ID: 'FLUXER_S3_ACCESS_KEY',
FLUXER_S3_SECRET_ACCESS_KEY: 'FLUXER_S3_SECRET_KEY',
FLUXER_LIVEKIT_API_KEY: 'LIVEKIT_API_KEY',
FLUXER_LIVEKIT_API_SECRET: 'LIVEKIT_API_SECRET',
POSTGRES_DB: 'FLUXER_POSTGRES_DATABASE',
POSTGRES_USER: 'FLUXER_POSTGRES_USERNAME',
MEILI_ENV: 'FLUXER_MEILISEARCH_ENV',
MEILI_NO_ANALYTICS: 'FLUXER_MEILISEARCH_NO_ANALYTICS',
MEILI_MAX_INDEXING_MEMORY: 'FLUXER_MEILISEARCH_MAX_INDEXING_MEMORY',
GOMEMLIMIT: 'FLUXER_SEAWEEDFS_GOMEMLIMIT',
LIVEKIT_KEYS: 'LIVEKIT_API_KEY',
NODE_EXTRA_CA_CERTS: 'FLUXER_NODE_EXTRA_CA_CERTS',
};
const OWN_POOL_SIZES = new Set(['api', 'worker', 'users-shard', 'messages-shard']);
const INTERPOLATION = /^\$\{([A-Z][A-Z0-9_]*)/u;
function inputName(service: string, key: string): string {
if (key === 'FLUXER_POSTGRES_MAX_CONNECTIONS' && OWN_POOL_SIZES.has(service)) {
return `FLUXER_${service.toUpperCase().replace('-', '_')}_POSTGRES_MAX_CONNECTIONS`;
}
return INPUT_NAMES[key] ?? key;
}
function forwardedEntries(): Array<{service: string; key: string; name: string}> {
return serviceNames.flatMap((service) =>
Object.entries(serviceEnvironment(service)).flatMap(([key, value]) => {
const match = INTERPOLATION.exec(value.trim());
if (match == null) {
return [];
}
return [{service, key, name: match[1]}];
}),
);
}
describe('the shipped compose stack forwards settings from .env', () => {
test('the api is handed every setting its config loader reads', () => {
const api = serviceEnvironment('api');
const missing = NAMED_FLUXER_ENV_NAMES.filter((name) => !(name in api) && !(name in API_SETTINGS_NOT_FORWARDED));
expect(missing).toEqual([]);
expect(Object.keys(API_SETTINGS_NOT_FORWARDED).filter((name) => !NAMED_FLUXER_ENV_NAMES.includes(name))).toEqual(
[],
);
});
test('the shared block sets the client-IP trust the merged services read', () => {
expect(sharedEnvironment).toMatchObject({
FLUXER_TRUST_CLIENT_IP_HEADER: `\${FLUXER_TRUST_CLIENT_IP_HEADER:-true}`,
FLUXER_CLIENT_IP_HEADER_NAME: `\${FLUXER_CLIENT_IP_HEADER_NAME:-x-forwarded-for}`,
});
test('every forwarded setting is read from .env under the name the operator sets', () => {
const renamed = forwardedEntries()
.filter(({service, key, name}) => name !== inputName(service, key))
.map(({service, key, name}) => `${service}.${key} reads ${name}`);
expect(renamed).toEqual([]);
});
});
@@ -33,7 +33,7 @@ describe('the shipped object store checks the credentials the stack sends', () =
});
test('media-proxy signs its reads, which the store now refuses to serve unsigned', () => {
expect(serviceEnvironment('media-proxy').FLUXER_S3_READ_SIGNED).toBe('true');
expect(serviceEnvironment('media-proxy').FLUXER_S3_READ_SIGNED).toBe(`\${FLUXER_S3_READ_SIGNED:-true}`);
});
test('every service that reaches the store waits for the identity to exist', () => {
@@ -17,7 +17,6 @@ const NAMED_FLUXER_ENV_OVERRIDES: Record<string, NamedEnvOverride> = {
FLUXER_BASE_DOMAIN: {path: ['domain', 'base_domain']},
FLUXER_PUBLIC_ORIGIN: {path: ['domain', 'public_origin']},
FLUXER_PUBLIC_SCHEME: {path: ['domain', 'public_scheme']},
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']},
@@ -29,7 +28,6 @@ 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']},
@@ -54,10 +52,7 @@ const NAMED_FLUXER_ENV_OVERRIDES: Record<string, NamedEnvOverride> = {
FLUXER_POSTGRES_PREPARED_STATEMENTS: {path: ['database', 'postgres', 'prepared_statements'], parse: parseBoolean},
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_S3_ENDPOINT: {path: ['s3', 'endpoint']},
FLUXER_S3_PUBLIC_ENDPOINT: {path: ['s3', 'presigned_url_base']},
@@ -104,46 +99,11 @@ const NAMED_FLUXER_ENV_OVERRIDES: Record<string, NamedEnvOverride> = {
FLUXER_API_STORAGE_CHANGE_FEED_STREAM: {path: ['services', 'api', 'storage_change_feed', 'stream']},
FLUXER_API_STORAGE_CHANGE_FEED_SKIP_BUCKETS: {
path: ['services', 'api', 'storage_change_feed', 'skip_buckets'],
parse: parseCsv,
parse: parseBucketList,
},
FLUXER_API_UNFURL_IGNORED_HOSTS: {path: ['services', 'api', 'unfurl_ignored_hosts'], parse: parseCsv},
FLUXER_APP_ORIGIN_ALIASES: {path: ['services', 'api', 'app_origin_aliases'], parse: parseCsv},
FLUXER_API_EMBEDS_OEMBED_HTML_ENABLED: {
path: ['services', 'api', 'embeds', 'oembed_html_enabled'],
parse: parseBoolean,
},
FLUXER_API_EMBEDS_OEMBED_HTML_ALLOW_UNTRUSTED_ON_SELF_HOSTED: {
path: ['services', 'api', 'embeds', 'oembed_html_allow_untrusted_on_self_hosted'],
parse: parseBoolean,
},
FLUXER_API_EMBEDS_OEMBED_HTML_ALLOWED_HOSTS: {
path: ['services', 'api', 'embeds', 'oembed_html_allowed_hosts'],
parse: parseCsv,
},
FLUXER_API_EMBEDS_CACHE_DEFAULT_TTL_SECONDS: {
path: ['services', 'api', 'embeds', 'cache_default_ttl_seconds'],
parse: parseInteger,
},
FLUXER_API_EMBEDS_CACHE_MAX_TTL_SECONDS: {
path: ['services', 'api', 'embeds', 'cache_max_ttl_seconds'],
parse: parseInteger,
},
FLUXER_API_EMBEDS_CACHE_MIN_TTL_SECONDS: {
path: ['services', 'api', 'embeds', 'cache_min_ttl_seconds'],
parse: parseInteger,
},
FLUXER_API_EMBEDS_CACHE_RESPECT_REMOTE_TTL: {
path: ['services', 'api', 'embeds', 'cache_respect_remote_ttl'],
parse: parseBoolean,
},
FLUXER_API_CONTENT_MODERATION_NSFW_THRESHOLD: {
path: ['services', 'api', 'content_moderation', 'nsfw_threshold'],
parse: parseEnvValue,
},
FLUXER_MEDIA_PROXY_HOST: {path: ['services', 'media_proxy', 'host']},
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: {
@@ -162,47 +122,9 @@ const NAMED_FLUXER_ENV_OVERRIDES: Record<string, NamedEnvOverride> = {
path: ['services', 'media_proxy', 'attachment_urls', 'secrets_base64'],
parse: parseCsv,
},
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_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: 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_LOGGER_LEVEL: {path: ['services', 'gateway', 'logger_level']},
FLUXER_GATEWAY_HTTP_FAILURE_THRESHOLD: {
path: ['services', 'gateway', 'gateway_http_failure_threshold'],
parse: parseInteger,
},
FLUXER_GATEWAY_HTTP_RECOVERY_TIMEOUT_MS: {
path: ['services', 'gateway', 'gateway_http_recovery_timeout_ms'],
parse: parseInteger,
},
FLUXER_GATEWAY_HTTP_RPC_MAX_CONCURRENCY: {
path: ['services', 'gateway', 'gateway_http_rpc_max_concurrency'],
parse: parseInteger,
},
FLUXER_GATEWAY_NATS_RPC_MAX_HANDLERS: {
path: ['services', 'gateway', 'gateway_nats_rpc_max_handlers'],
parse: parseInteger,
},
FLUXER_GATEWAY_SHUTDOWN_DRAIN_WAIT_MS: {
path: ['services', 'gateway', 'shutdown_drain_wait_ms'],
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']},
FLUXER_GATEWAY_CLUSTER_DISCOVERY_NODE_BASENAME: {
path: ['services', 'gateway', 'cluster_discovery_node_basename'],
},
FLUXER_GATEWAY_CLUSTER_DISCOVERY_POLL_INTERVAL_MS: {
path: ['services', 'gateway', 'cluster_discovery_poll_interval_ms'],
parse: parseInteger,
},
FLUXER_SUDO_MODE_SECRET: {path: ['auth', 'sudo_mode_secret']},
FLUXER_CONNECTION_INITIATION_SECRET: {path: ['auth', 'connection_initiation_secret']},
FLUXER_SSO_ALLOW_PRIVATE_ADDRESSES: {path: ['auth', 'sso_allow_private_addresses'], parse: parseBoolean},
@@ -238,7 +160,6 @@ const NAMED_FLUXER_ENV_OVERRIDES: Record<string, NamedEnvOverride> = {
FLUXER_LIVEKIT_API_SECRET: {path: ['integrations', 'voice', 'api_secret']},
FLUXER_LIVEKIT_URL: {path: ['integrations', 'voice', 'url']},
FLUXER_LIVEKIT_INTERNAL_URL: {path: ['integrations', 'voice', 'internal_url']},
FLUXER_LIVEKIT_WEBHOOK_URL: {path: ['integrations', 'voice', 'webhook_url']},
FLUXER_LIVEKIT_DEFAULT_REGION: {path: ['integrations', 'voice', 'default_region'], parse: parseJsonObject},
FLUXER_SEARCH_ENGINE: {path: ['integrations', 'search', 'engine']},
FLUXER_SEARCH_URL: {path: ['integrations', 'search', 'url']},
@@ -272,10 +193,6 @@ const NAMED_FLUXER_ENV_OVERRIDES: Record<string, NamedEnvOverride> = {
FLUXER_STRIPE_PRICE_YEARLY_PLN: {path: ['integrations', 'stripe', 'prices', 'yearly_pln']},
FLUXER_STRIPE_PRICE_YEARLY_SEK: {path: ['integrations', 'stripe', 'prices', 'yearly_sek']},
FLUXER_STRIPE_PRICE_YEARLY_TRY: {path: ['integrations', 'stripe', 'prices', 'yearly_try']},
FLUXER_STRIPE_PRICE_VISIONARY_USD: {path: ['integrations', 'stripe', 'prices', 'visionary_usd']},
FLUXER_STRIPE_PRICE_VISIONARY_EUR: {path: ['integrations', 'stripe', 'prices', 'visionary_eur']},
FLUXER_STRIPE_PRICE_GIFT_VISIONARY_USD: {path: ['integrations', 'stripe', 'prices', 'gift_visionary_usd']},
FLUXER_STRIPE_PRICE_GIFT_VISIONARY_EUR: {path: ['integrations', 'stripe', 'prices', 'gift_visionary_eur']},
FLUXER_STRIPE_PRICE_GIFT_1_MONTH_USD: {path: ['integrations', 'stripe', 'prices', 'gift_1_month_usd']},
FLUXER_STRIPE_PRICE_GIFT_1_MONTH_EUR: {path: ['integrations', 'stripe', 'prices', 'gift_1_month_eur']},
FLUXER_STRIPE_PRICE_GIFT_1_MONTH_SEK: {path: ['integrations', 'stripe', 'prices', 'gift_1_month_sek']},
@@ -323,16 +240,7 @@ const NAMED_FLUXER_ENV_OVERRIDES: Record<string, NamedEnvOverride> = {
FLUXER_PUSH_APNS_KEY_ID: {path: ['integrations', 'push', 'apns', 'key_id']},
FLUXER_PUSH_APNS_PRIVATE_KEY: {path: ['integrations', 'push', 'apns', 'private_key']},
FLUXER_PUSH_APNS_PRIVATE_KEY_PATH: {path: ['integrations', 'push', 'apns', 'private_key_path']},
FLUXER_PUSH_APNS_DEFAULT_ENVIRONMENT: {path: ['integrations', 'push', 'apns', 'default_environment']},
FLUXER_PUSH_APNS_APPS: {path: ['integrations', 'push', 'apns', 'apps'], parse: parseJsonArray},
FLUXER_PUSH_FCM_ENABLED: {path: ['integrations', 'push', 'fcm', 'enabled'], parse: parseBoolean},
FLUXER_PUSH_FCM_PROJECT_ID: {path: ['integrations', 'push', 'fcm', 'project_id']},
FLUXER_PUSH_FCM_CLIENT_EMAIL: {path: ['integrations', 'push', 'fcm', 'client_email']},
FLUXER_PUSH_FCM_PRIVATE_KEY: {path: ['integrations', 'push', 'fcm', 'private_key']},
FLUXER_PUSH_FCM_PRIVATE_KEY_PATH: {path: ['integrations', 'push', 'fcm', 'private_key_path']},
FLUXER_PUSH_FCM_SERVICE_ACCOUNT_JSON_PATH: {path: ['integrations', 'push', 'fcm', 'service_account_json_path']},
FLUXER_PUSH_FCM_TOKEN_URI: {path: ['integrations', 'push', 'fcm', 'token_uri']},
FLUXER_PUSH_FCM_APPS: {path: ['integrations', 'push', 'fcm', 'apps'], parse: parseJsonArray},
FLUXER_SELF_HOSTED: {path: ['instance', 'self_hosted'], parse: parseBoolean},
FLUXER_AUTO_JOIN_INVITE_CODE: {path: ['instance', 'auto_join_invite_code']},
FLUXER_VISIONARIES_GUILD_ID: {path: ['instance', 'visionaries_guild_id']},
@@ -394,50 +302,27 @@ function parseBoolean(raw: string): boolean {
}
function parseJson(raw: string): unknown {
const trimmed = raw.trim();
if (trimmed.length === 0) return undefined;
try {
return JSON.parse(trimmed);
return JSON.parse(raw);
} catch {
throw new Error('must be valid JSON');
}
}
function parseJsonObject(raw: string): ConfigObject | undefined {
function parseJsonObject(raw: string): ConfigObject {
const value = parseJson(raw);
if (value === undefined || isConfigObject(value)) return value;
if (isConfigObject(value)) return value;
throw new Error('must be a JSON object');
}
function parseJsonArray(raw: string): Array<unknown> | undefined {
function parseJsonArray(raw: string): Array<unknown> {
const value = parseJson(raw);
if (value === undefined || Array.isArray(value)) return value;
if (Array.isArray(value)) return value;
throw new Error('must be a JSON array');
}
export function parseEnvValue(raw: string): unknown {
function parseInteger(raw: string): number {
const trimmed = raw.trim();
const lower = trimmed.toLowerCase();
if (lower === 'true' || lower === 'false') return parseBoolean(trimmed);
if (/^-?\d+$/.test(trimmed)) {
return parseInteger(trimmed);
}
if (/^-?\d+\.\d+$/.test(trimmed)) {
const value = Number(trimmed);
if (!Number.isFinite(value)) throw new Error('must be a finite number');
return value;
}
if (trimmed.startsWith('{') || trimmed.startsWith('[')) {
return parseJson(trimmed);
}
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)}`);
}
@@ -453,6 +338,10 @@ function parseCsv(raw: string): Array<string> {
.filter((part) => part.length > 0);
}
function parseBucketList(raw: string): Array<string> {
return raw.trim().toLowerCase() === 'none' ? [] : parseCsv(raw);
}
function parsePasskeyOrigins(raw: string): Array<string> {
if (/\p{Cc}/u.test(raw)) {
throw new Error('must not contain control characters');
@@ -486,11 +375,18 @@ const NAMED_FLUXER_ENV_ALIASES: Record<string, string | undefined> = {
FLUXER_IPINFO_API_KEY: 'FLUXER_RISK_IPINFO_API_KEY',
};
export const NAMED_FLUXER_ENV_NAMES = Object.keys(NAMED_FLUXER_ENV_OVERRIDES);
export function readEnvValue(env: NodeJS.ProcessEnv, name: string): string | undefined {
const value = env[name];
return value === undefined || value.trim().length === 0 ? undefined : value;
}
export function buildNamedFluxerEnvOverrides(env: NodeJS.ProcessEnv): ConfigObject {
const overrides: ConfigObject = {};
for (const [envKey, mapping] of Object.entries(NAMED_FLUXER_ENV_OVERRIDES)) {
const alias = NAMED_FLUXER_ENV_ALIASES[envKey];
const raw = env[envKey] ?? (alias === undefined ? undefined : env[alias]);
const raw = readEnvValue(env, envKey) ?? (alias === undefined ? undefined : readEnvValue(env, alias));
if (raw === undefined) {
continue;
}
@@ -500,9 +396,6 @@ export function buildNamedFluxerEnvOverrides(env: NodeJS.ProcessEnv): ConfigObje
} catch (error) {
throw new Error(`${envKey} ${error instanceof Error ? error.message : String(error)}`);
}
if (parsed === undefined) {
continue;
}
setNestedValue(overrides, mapping.path, parsed);
}
return overrides;
-1
View File
@@ -1,5 +1,4 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
export const DEFAULT_KV_TIMEOUT_MS = 5000;
export const DEFAULT_HTTP_WORKER_TIMEOUT_MS = 30000;
export const DEFAULT_SEARCH_CLIENT_TIMEOUT_MS = 30000;
-1
View File
@@ -12,7 +12,6 @@
"typecheck": "tsc --noEmit"
},
"dependencies": {
"@fluxer/config": "workspace:*",
"@fluxer/constants": "workspace:*",
"@fluxer/hono_types": "workspace:*",
"@fluxer/i18n": "workspace:*",
@@ -1,24 +1,15 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
import {getConfig} from '@fluxer/config/src/ConfigLoader';
import {APIErrorCodes} from '@fluxer/constants/src/ApiErrorCodes';
import {BadRequestError} from '@fluxer/errors/src/domains/core/BadRequestError';
export class MaxBookmarksError extends BadRequestError {
constructor(params: {
maxBookmarks: number;
isPremium?: boolean;
}) {
const {maxBookmarks, isPremium} = params;
const config = getConfig();
const selfHosted = 'self_hosted' in config ? config.self_hosted : false;
constructor(params: {maxBookmarks: number}) {
const {maxBookmarks} = params;
super({
code: APIErrorCodes.MAX_BOOKMARKS,
messageVariables: {count: maxBookmarks},
data: {
max_bookmarks: maxBookmarks,
...(selfHosted || isPremium === undefined ? {} : {is_premium: isPremium}),
},
data: {max_bookmarks: maxBookmarks},
});
}
}
+1 -1
View File
@@ -20,7 +20,7 @@ function isPinoLevel(value: string): value is pino.Level {
}
function resolveEnvironment(options: LoggerOptions): string {
return options.environment ?? process.env.FLUXER_ENV ?? 'production';
return options.environment ?? (process.env.FLUXER_ENV?.trim() || 'production');
}
function resolveLevel(options: LoggerOptions, isDev: boolean): pino.Level {