mirror of
https://github.com/fluxerapp/fluxer
synced 2026-10-07 19:22:14 +09:00
refactor(self-hosting): forward every setting, drop dead config (#3047)
This commit is contained in:
@@ -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'),
|
||||
|
||||
@@ -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,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;
|
||||
|
||||
@@ -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},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user