mirror of
https://github.com/fluxerapp/fluxer
synced 2026-10-18 05:25:40 +09:00
fix(self-host): tie the public address to a single origin (#2605)
This commit is contained in:
1 parent
6c36d934f7
commit
8cc485cf81
17 files changed
+903
-61
No files matched your search
@@ -3,9 +3,11 @@
|
||||
import {createECDH} from 'node:crypto';
|
||||
import {buildNamedFluxerEnvOverrides} from '@fluxer/config/src/config_loader/EnvironmentOverrides';
|
||||
import {
|
||||
buildUrl,
|
||||
type DerivedEndpoints,
|
||||
deriveEndpointsFromDomain,
|
||||
normalizePublicEndpoint,
|
||||
parsePublicOrigin,
|
||||
} from '@fluxer/config/src/EndpointDerivation';
|
||||
import type {MasterConfig} from '@fluxer/config/src/MasterConfig';
|
||||
|
||||
@@ -26,6 +28,7 @@ function defaultConfig(): MasterConfig {
|
||||
env: 'development',
|
||||
domain: {
|
||||
base_domain: '',
|
||||
public_origin: '',
|
||||
public_scheme: 'http',
|
||||
internal_scheme: 'http',
|
||||
public_port: 8088,
|
||||
@@ -476,6 +479,7 @@ function normalizeConfig(config: MasterConfig): MasterConfig {
|
||||
assertIntegerInRange(config.services.api.max_inflight_requests, 'FLUXER_API_MAX_INFLIGHT_REQUESTS', 1, 100_000);
|
||||
assertIntegerInRange(config.services.api.headers_timeout_ms, 'FLUXER_API_HEADERS_TIMEOUT_MS', 1_000, 3_600_000);
|
||||
assertIntegerInRange(config.services.api.request_timeout_ms, 'FLUXER_API_REQUEST_TIMEOUT_MS', 1_000, 3_600_000);
|
||||
assertIntegerInRange(config.domain.public_port, 'FLUXER_PUBLIC_PORT', 1, 65_535);
|
||||
requireString(config.domain.base_domain, 'FLUXER_BASE_DOMAIN');
|
||||
requireString(config.auth.sudo_mode_secret, 'FLUXER_SUDO_MODE_SECRET');
|
||||
requireString(config.auth.connection_initiation_secret, 'FLUXER_CONNECTION_INITIATION_SECRET');
|
||||
@@ -493,16 +497,51 @@ function normalizeConfig(config: MasterConfig): MasterConfig {
|
||||
return config;
|
||||
}
|
||||
|
||||
function applyPublicOrigin(config: MasterConfig): MasterConfig {
|
||||
const raw = config.domain.public_origin.trim();
|
||||
if (raw.length === 0) {
|
||||
return config;
|
||||
}
|
||||
const origin = parsePublicOrigin(raw);
|
||||
if (!origin) {
|
||||
throw new Error(
|
||||
`FLUXER_PUBLIC_ORIGIN must be a scheme, host and optional port such as https://chat.example.com:8443, got ${raw}`,
|
||||
);
|
||||
}
|
||||
return {
|
||||
...config,
|
||||
domain: {
|
||||
...config.domain,
|
||||
base_domain: origin.base_domain,
|
||||
public_scheme: origin.public_scheme,
|
||||
public_port: origin.public_port,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function applyPublicPort(config: MasterConfig, endpoints: DerivedEndpoints): MasterConfig {
|
||||
const {base_domain, public_port} = config.domain;
|
||||
const normalize = (url: string) => normalizePublicEndpoint(url, base_domain, public_port);
|
||||
const normalizeOptional = (url: string | undefined) => (url === undefined ? undefined : normalize(url));
|
||||
const normalizedEndpoints = {...endpoints};
|
||||
for (const key of Object.keys(normalizedEndpoints) as Array<keyof DerivedEndpoints>) {
|
||||
normalizedEndpoints[key] = normalize(normalizedEndpoints[key]);
|
||||
}
|
||||
const {bluesky, passkeys} = config.auth;
|
||||
const {branding} = config.instance;
|
||||
const {email, sms, voice} = config.integrations;
|
||||
return {
|
||||
...config,
|
||||
domain: {
|
||||
...config.domain,
|
||||
public_origin: buildUrl(config.domain.public_scheme, base_domain, public_port),
|
||||
},
|
||||
endpoints: normalizedEndpoints,
|
||||
s3: config.s3 && {...config.s3, presigned_url_base: normalizeOptional(config.s3.presigned_url_base)},
|
||||
s3_downloads: config.s3_downloads && {
|
||||
...config.s3_downloads,
|
||||
presigned_url_base: normalizeOptional(config.s3_downloads.presigned_url_base),
|
||||
},
|
||||
services: {
|
||||
...config.services,
|
||||
media_proxy: {
|
||||
@@ -512,12 +551,40 @@ 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,
|
||||
passkeys: {
|
||||
...config.auth.passkeys,
|
||||
additional_allowed_origins: config.auth.passkeys.additional_allowed_origins.map(normalize),
|
||||
...passkeys,
|
||||
additional_allowed_origins: passkeys.additional_allowed_origins.map(normalize),
|
||||
},
|
||||
bluesky: {
|
||||
...bluesky,
|
||||
client_uri: normalize(bluesky.client_uri),
|
||||
logo_uri: normalize(bluesky.logo_uri),
|
||||
tos_uri: normalize(bluesky.tos_uri),
|
||||
policy_uri: normalize(bluesky.policy_uri),
|
||||
},
|
||||
},
|
||||
integrations: {
|
||||
...config.integrations,
|
||||
email: {...email, app_base_url: normalize(email.app_base_url)},
|
||||
sms: {...sms, inbound_webhook_public_url: normalizeOptional(sms.inbound_webhook_public_url)},
|
||||
voice: {...voice, url: normalize(voice.url)},
|
||||
},
|
||||
instance: {
|
||||
...config.instance,
|
||||
branding: {
|
||||
...branding,
|
||||
icon_url: normalizeOptional(branding.icon_url),
|
||||
symbol_url: normalizeOptional(branding.symbol_url),
|
||||
logo_url: normalizeOptional(branding.logo_url),
|
||||
wordmark_url: normalizeOptional(branding.wordmark_url),
|
||||
favicon_url: normalizeOptional(branding.favicon_url),
|
||||
},
|
||||
},
|
||||
};
|
||||
@@ -546,7 +613,7 @@ export async function loadConfig(): Promise<MasterConfig> {
|
||||
return cachedConfig;
|
||||
}
|
||||
const overrides = buildNamedFluxerEnvOverrides(process.env);
|
||||
const merged = mergeConfig(defaultConfig(), overrides);
|
||||
const merged = applyPublicOrigin(mergeConfig(defaultConfig(), overrides));
|
||||
const normalized = normalizeConfig(merged);
|
||||
const derived = deriveEndpointsFromDomain(normalized.domain);
|
||||
const endpoints = {...derived, ...(normalized.endpoint_overrides ?? {})};
|
||||
|
||||
@@ -13,6 +13,14 @@ export interface DomainConfig {
|
||||
gift_domain?: string;
|
||||
}
|
||||
|
||||
type PublicOriginScheme = 'http' | 'https';
|
||||
|
||||
interface PublicOrigin {
|
||||
public_scheme: PublicOriginScheme;
|
||||
base_domain: string;
|
||||
public_port: number;
|
||||
}
|
||||
|
||||
export interface DerivedEndpoints {
|
||||
api: string;
|
||||
api_client: string;
|
||||
@@ -36,8 +44,45 @@ function isStandardPort(scheme: string, port: number): boolean {
|
||||
);
|
||||
}
|
||||
|
||||
function stripTrailingDot(host: string): string {
|
||||
return host.endsWith('.') ? host.slice(0, -1) : host;
|
||||
export function canonicalizeDomain(value: string): string {
|
||||
const trimmed = value.trim().toLowerCase();
|
||||
return trimmed.endsWith('.') ? trimmed.slice(0, -1) : trimmed;
|
||||
}
|
||||
|
||||
function defaultPortForScheme(scheme: PublicOriginScheme): number {
|
||||
return scheme === 'https' ? 443 : 80;
|
||||
}
|
||||
|
||||
export function parsePublicOrigin(origin: string): PublicOrigin | null {
|
||||
const trimmed = origin.trim();
|
||||
if (trimmed.length === 0) {
|
||||
return null;
|
||||
}
|
||||
let parsed: URL;
|
||||
try {
|
||||
parsed = new URL(trimmed);
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
const scheme = parsed.protocol.slice(0, -1);
|
||||
if (scheme !== 'http' && scheme !== 'https') {
|
||||
return null;
|
||||
}
|
||||
if (parsed.pathname !== '/' || parsed.search.length > 0 || parsed.hash.length > 0) {
|
||||
return null;
|
||||
}
|
||||
if (parsed.username.length > 0 || parsed.password.length > 0) {
|
||||
return null;
|
||||
}
|
||||
const base_domain = canonicalizeDomain(parsed.hostname);
|
||||
if (base_domain.length === 0) {
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
public_scheme: scheme,
|
||||
base_domain,
|
||||
public_port: parsed.port.length === 0 ? defaultPortForScheme(scheme) : Number.parseInt(parsed.port, 10),
|
||||
};
|
||||
}
|
||||
|
||||
export function buildUrl(scheme: string, domain: string, port?: number, path?: string): string {
|
||||
@@ -47,7 +92,7 @@ export function buildUrl(scheme: string, domain: string, port?: number, path?: s
|
||||
}
|
||||
|
||||
export function normalizePublicEndpoint(url: string, baseDomain: string, publicPort?: number): string {
|
||||
const domain = stripTrailingDot(baseDomain.trim().toLowerCase());
|
||||
const domain = canonicalizeDomain(baseDomain);
|
||||
if (domain.length === 0 || !publicPort) {
|
||||
return url;
|
||||
}
|
||||
@@ -57,7 +102,7 @@ export function normalizePublicEndpoint(url: string, baseDomain: string, publicP
|
||||
} catch {
|
||||
return url;
|
||||
}
|
||||
if (stripTrailingDot(parsed.hostname.toLowerCase()) !== domain) {
|
||||
if (canonicalizeDomain(parsed.hostname) !== domain) {
|
||||
return url;
|
||||
}
|
||||
if (isStandardPort(parsed.protocol.slice(0, -1), publicPort)) {
|
||||
|
||||
@@ -20,6 +20,7 @@ export interface MasterConfig {
|
||||
env: RuntimeEnv;
|
||||
domain: {
|
||||
base_domain: string;
|
||||
public_origin: string;
|
||||
public_scheme: PublicScheme;
|
||||
internal_scheme: PublicScheme;
|
||||
public_port: number;
|
||||
|
||||
@@ -646,4 +646,183 @@ describe('ConfigLoader', () => {
|
||||
vi.stubEnv('FLUXER_ENV', 'test');
|
||||
await expect(loadConfig()).rejects.toThrow();
|
||||
});
|
||||
|
||||
test('inserts the public port into the LiveKit url', async () => {
|
||||
stubMinimalEnv({FLUXER_LIVEKIT_URL: 'http://localhost/livekit'});
|
||||
|
||||
const config = await loadConfig();
|
||||
|
||||
expect(config.integrations.voice.url).toBe('http://localhost:8088/livekit');
|
||||
});
|
||||
|
||||
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_SMS_INBOUND_WEBHOOK_PUBLIC_URL: 'http://localhost/webhooks/sms',
|
||||
FLUXER_AUTH_BLUESKY_CLIENT_URI: 'http://localhost',
|
||||
FLUXER_AUTH_BLUESKY_TOS_URI: 'http://localhost/terms',
|
||||
FLUXER_APP_ICON_URL: 'http://localhost/icon.png',
|
||||
FLUXER_LIVEKIT_INTERNAL_URL: 'http://livekit:7880',
|
||||
});
|
||||
|
||||
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.integrations.sms.inbound_webhook_public_url).toBe('http://localhost:8088/webhooks/sms');
|
||||
expect(config.auth.bluesky.client_uri).toBe('http://localhost:8088');
|
||||
expect(config.auth.bluesky.tos_uri).toBe('http://localhost:8088/terms');
|
||||
expect(config.instance.branding.icon_url).toBe('http://localhost:8088/icon.png');
|
||||
expect(config.integrations.voice.internal_url).toBe('http://livekit:7880');
|
||||
});
|
||||
|
||||
test('rejects a public port outside the valid range', async () => {
|
||||
stubMinimalEnv({FLUXER_PUBLIC_PORT: '70000'});
|
||||
await expect(loadConfig()).rejects.toThrow('FLUXER_PUBLIC_PORT must be an integer between 1 and 65535');
|
||||
});
|
||||
});
|
||||
|
||||
describe('FLUXER_PUBLIC_ORIGIN', () => {
|
||||
beforeEach(() => {
|
||||
resetConfig();
|
||||
clearFluxerEnv();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
resetConfig();
|
||||
vi.unstubAllEnvs();
|
||||
});
|
||||
|
||||
test('a ported origin ports every derived endpoint and every compose override', async () => {
|
||||
stubMinimalEnv({
|
||||
FLUXER_BASE_DOMAIN: 'chat.example.com',
|
||||
FLUXER_PUBLIC_SCHEME: 'https',
|
||||
FLUXER_PUBLIC_PORT: '',
|
||||
FLUXER_PUBLIC_ORIGIN: 'https://chat.example.com:29080',
|
||||
FLUXER_MARKETING_ENDPOINT: 'https://chat.example.com:29080',
|
||||
FLUXER_MEDIA_ENDPOINT: 'https://chat.example.com:29080/media',
|
||||
FLUXER_MEDIA_PROXY_UPLOAD_RELAY_ENDPOINT: 'https://chat.example.com:29080/media',
|
||||
FLUXER_LIVEKIT_URL: 'https://chat.example.com:29080/livekit',
|
||||
FLUXER_PASSKEY_ADDITIONAL_ALLOWED_ORIGINS: 'https://chat.example.com:29080',
|
||||
});
|
||||
|
||||
const config = await loadConfig();
|
||||
|
||||
expect(config.domain.public_port).toBe(29080);
|
||||
expect(config.domain.public_origin).toBe('https://chat.example.com:29080');
|
||||
expect(config.endpoints.api_client).toBe('https://chat.example.com:29080/api');
|
||||
expect(config.endpoints.app).toBe('https://chat.example.com:29080');
|
||||
expect(config.endpoints.gateway).toBe('wss://chat.example.com:29080/gateway');
|
||||
expect(config.endpoints.admin).toBe('https://chat.example.com:29080/admin');
|
||||
expect(config.endpoints.marketing).toBe('https://chat.example.com:29080');
|
||||
expect(config.endpoints.media).toBe('https://chat.example.com:29080/media');
|
||||
expect(config.services.media_proxy.upload_relay.endpoint).toBe('https://chat.example.com:29080/media');
|
||||
expect(config.integrations.voice.url).toBe('https://chat.example.com:29080/livekit');
|
||||
expect(config.auth.passkeys.additional_allowed_origins).toEqual(['https://chat.example.com:29080']);
|
||||
});
|
||||
|
||||
test('the origin port wins over a FLUXER_PUBLIC_PORT that disagrees', async () => {
|
||||
stubMinimalEnv({
|
||||
FLUXER_BASE_DOMAIN: 'chat.example.com',
|
||||
FLUXER_PUBLIC_SCHEME: 'https',
|
||||
FLUXER_PUBLIC_PORT: '443',
|
||||
FLUXER_PUBLIC_ORIGIN: 'https://chat.example.com:29080',
|
||||
});
|
||||
|
||||
const config = await loadConfig();
|
||||
|
||||
expect(config.domain.public_port).toBe(29080);
|
||||
expect(config.endpoints.app).toBe('https://chat.example.com:29080');
|
||||
expect(config.endpoints.api_client).toBe('https://chat.example.com:29080/api');
|
||||
expect(config.endpoints.gateway).toBe('wss://chat.example.com:29080/gateway');
|
||||
expect(config.endpoints.admin).toBe('https://chat.example.com:29080/admin');
|
||||
});
|
||||
|
||||
test('accepts an origin whose port matches FLUXER_PUBLIC_PORT', async () => {
|
||||
stubMinimalEnv({
|
||||
FLUXER_BASE_DOMAIN: 'chat.example.com',
|
||||
FLUXER_PUBLIC_SCHEME: 'https',
|
||||
FLUXER_PUBLIC_PORT: '29080',
|
||||
FLUXER_PUBLIC_ORIGIN: 'https://chat.example.com:29080',
|
||||
});
|
||||
|
||||
expect((await loadConfig()).endpoints.gateway).toBe('wss://chat.example.com:29080/gateway');
|
||||
});
|
||||
|
||||
test('normalizes an origin written with an explicit default port', async () => {
|
||||
stubMinimalEnv({
|
||||
FLUXER_BASE_DOMAIN: 'chat.example.com',
|
||||
FLUXER_PUBLIC_SCHEME: 'https',
|
||||
FLUXER_PUBLIC_PORT: '443',
|
||||
FLUXER_PUBLIC_ORIGIN: 'https://chat.example.com:443',
|
||||
FLUXER_ADMIN_ENDPOINT: 'https://chat.example.com/admin',
|
||||
});
|
||||
|
||||
const config = await loadConfig();
|
||||
|
||||
expect(config.domain.public_port).toBe(443);
|
||||
expect(config.domain.public_origin).toBe('https://chat.example.com');
|
||||
expect(config.endpoints.admin).toBe('https://chat.example.com/admin');
|
||||
expect(config.endpoints.app).toBe('https://chat.example.com');
|
||||
expect(config.endpoints.gateway).toBe('wss://chat.example.com/gateway');
|
||||
});
|
||||
|
||||
test('takes the scheme and the domain from the origin when neither is set', async () => {
|
||||
stubMinimalEnv({
|
||||
FLUXER_BASE_DOMAIN: '',
|
||||
FLUXER_PUBLIC_SCHEME: '',
|
||||
FLUXER_PUBLIC_PORT: '',
|
||||
FLUXER_PUBLIC_ORIGIN: 'https://chat.example.com:29080',
|
||||
});
|
||||
|
||||
const config = await loadConfig();
|
||||
|
||||
expect(config.domain.base_domain).toBe('chat.example.com');
|
||||
expect(config.domain.public_scheme).toBe('https');
|
||||
expect(config.domain.public_port).toBe(29080);
|
||||
});
|
||||
|
||||
test('the origin scheme wins over a FLUXER_PUBLIC_SCHEME that disagrees', async () => {
|
||||
stubMinimalEnv({
|
||||
FLUXER_BASE_DOMAIN: 'chat.example.com',
|
||||
FLUXER_PUBLIC_SCHEME: 'http',
|
||||
FLUXER_PUBLIC_PORT: '',
|
||||
FLUXER_PUBLIC_ORIGIN: 'https://chat.example.com',
|
||||
});
|
||||
|
||||
const config = await loadConfig();
|
||||
|
||||
expect(config.domain.public_scheme).toBe('https');
|
||||
expect(config.endpoints.app).toBe('https://chat.example.com');
|
||||
expect(config.endpoints.gateway).toBe('wss://chat.example.com/gateway');
|
||||
});
|
||||
|
||||
test('the origin host wins over a FLUXER_BASE_DOMAIN that disagrees', async () => {
|
||||
stubMinimalEnv({
|
||||
FLUXER_BASE_DOMAIN: 'chat.example.com',
|
||||
FLUXER_PUBLIC_SCHEME: 'https',
|
||||
FLUXER_PUBLIC_PORT: '',
|
||||
FLUXER_PUBLIC_ORIGIN: 'https://other.example.com',
|
||||
});
|
||||
|
||||
const config = await loadConfig();
|
||||
|
||||
expect(config.domain.base_domain).toBe('other.example.com');
|
||||
expect(config.endpoints.app).toBe('https://other.example.com');
|
||||
});
|
||||
|
||||
test('refuses to boot on an origin that is not a bare origin', async () => {
|
||||
stubMinimalEnv({FLUXER_PUBLIC_ORIGIN: 'https://chat.example.com/app'});
|
||||
await expect(loadConfig()).rejects.toThrow(
|
||||
'FLUXER_PUBLIC_ORIGIN must be a scheme, host and optional port such as https://chat.example.com:8443, got https://chat.example.com/app',
|
||||
);
|
||||
});
|
||||
|
||||
test('leaves the derived origin canonical when nothing is set', async () => {
|
||||
stubMinimalEnv();
|
||||
expect((await loadConfig()).domain.public_origin).toBe('http://localhost:8088');
|
||||
});
|
||||
});
|
||||
@@ -2,10 +2,12 @@
|
||||
|
||||
import {
|
||||
buildUrl,
|
||||
canonicalizeDomain,
|
||||
type DomainConfig,
|
||||
deriveDomain,
|
||||
deriveEndpointsFromDomain,
|
||||
normalizePublicEndpoint,
|
||||
parsePublicOrigin,
|
||||
} from '@fluxer/config/src/EndpointDerivation';
|
||||
import {describe, expect, test} from 'vitest';
|
||||
|
||||
@@ -394,3 +396,103 @@ describe('normalizePublicEndpoint', () => {
|
||||
expect(normalizePublicEndpoint('https://fluxer.dev/media', ' ', 8443)).toBe('https://fluxer.dev/media');
|
||||
});
|
||||
});
|
||||
|
||||
describe('canonicalizeDomain', () => {
|
||||
test('lowercases, trims and drops the root dot', () => {
|
||||
expect(canonicalizeDomain(' CHAT.Example.COM. ')).toBe('chat.example.com');
|
||||
});
|
||||
test('leaves an empty value empty', () => {
|
||||
expect(canonicalizeDomain(' ')).toBe('');
|
||||
});
|
||||
});
|
||||
|
||||
describe('parsePublicOrigin', () => {
|
||||
test('reads scheme, host and a non-standard port', () => {
|
||||
expect(parsePublicOrigin('https://chat.example.com:8443')).toEqual({
|
||||
public_scheme: 'https',
|
||||
base_domain: 'chat.example.com',
|
||||
public_port: 8443,
|
||||
});
|
||||
expect(parsePublicOrigin('http://chat.example.com:19080')).toEqual({
|
||||
public_scheme: 'http',
|
||||
base_domain: 'chat.example.com',
|
||||
public_port: 19080,
|
||||
});
|
||||
});
|
||||
test('fills in the standard port for a portless origin', () => {
|
||||
expect(parsePublicOrigin('https://chat.example.com')).toEqual({
|
||||
public_scheme: 'https',
|
||||
base_domain: 'chat.example.com',
|
||||
public_port: 443,
|
||||
});
|
||||
expect(parsePublicOrigin('http://chat.example.com')).toEqual({
|
||||
public_scheme: 'http',
|
||||
base_domain: 'chat.example.com',
|
||||
public_port: 80,
|
||||
});
|
||||
});
|
||||
test('normalizes an explicitly written standard port to the portless form', () => {
|
||||
const origin = parsePublicOrigin('https://chat.example.com:443');
|
||||
expect(origin).toEqual({public_scheme: 'https', base_domain: 'chat.example.com', public_port: 443});
|
||||
expect(buildUrl(origin?.public_scheme ?? 'https', origin?.base_domain ?? '', origin?.public_port)).toBe(
|
||||
'https://chat.example.com',
|
||||
);
|
||||
expect(parsePublicOrigin('http://chat.example.com:80')?.public_port).toBe(80);
|
||||
});
|
||||
test('canonicalizes the host', () => {
|
||||
expect(parsePublicOrigin(' https://CHAT.Example.com.:8443 ')).toEqual({
|
||||
public_scheme: 'https',
|
||||
base_domain: 'chat.example.com',
|
||||
public_port: 8443,
|
||||
});
|
||||
});
|
||||
test('keeps an IPv6 literal bracketed', () => {
|
||||
expect(parsePublicOrigin('http://[::1]:19080')).toEqual({
|
||||
public_scheme: 'http',
|
||||
base_domain: '[::1]',
|
||||
public_port: 19080,
|
||||
});
|
||||
});
|
||||
test('accepts a bare trailing slash', () => {
|
||||
expect(parsePublicOrigin('https://chat.example.com:8443/')?.public_port).toBe(8443);
|
||||
});
|
||||
test('rejects anything that is not a bare origin', () => {
|
||||
expect(parsePublicOrigin('')).toBeNull();
|
||||
expect(parsePublicOrigin(' ')).toBeNull();
|
||||
expect(parsePublicOrigin('not a url')).toBeNull();
|
||||
expect(parsePublicOrigin('chat.example.com:8443')).toBeNull();
|
||||
expect(parsePublicOrigin('wss://chat.example.com')).toBeNull();
|
||||
expect(parsePublicOrigin('https://chat.example.com/media')).toBeNull();
|
||||
expect(parsePublicOrigin('https://chat.example.com?a=1')).toBeNull();
|
||||
expect(parsePublicOrigin('https://chat.example.com#top')).toBeNull();
|
||||
expect(parsePublicOrigin('https://user:[email protected]')).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
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');
|
||||
const endpoints = deriveEndpointsFromDomain({
|
||||
base_domain: origin?.base_domain ?? '',
|
||||
public_scheme: origin?.public_scheme ?? 'https',
|
||||
internal_scheme: 'http',
|
||||
public_port: origin?.public_port,
|
||||
});
|
||||
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');
|
||||
expect(endpoints.admin).toBe('https://chat.example.com:29080/admin');
|
||||
});
|
||||
test('an origin written with an explicit :443 derives portless endpoints', () => {
|
||||
const origin = parsePublicOrigin('https://chat.example.com:443');
|
||||
const endpoints = deriveEndpointsFromDomain({
|
||||
base_domain: origin?.base_domain ?? '',
|
||||
public_scheme: origin?.public_scheme ?? 'https',
|
||||
internal_scheme: 'http',
|
||||
public_port: origin?.public_port,
|
||||
});
|
||||
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');
|
||||
});
|
||||
});
|
||||
@@ -14,6 +14,7 @@ interface NamedEnvOverride {
|
||||
const NAMED_FLUXER_ENV_OVERRIDES: Record<string, NamedEnvOverride> = {
|
||||
FLUXER_ENV: {path: ['env']},
|
||||
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},
|
||||
|
||||
Reference in new issue
Block a user