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

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

No files matched your search

+49 -1
View File
@@ -1,9 +1,10 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
import {loadConfig, resetConfig} from '@fluxer/config/src/ConfigLoader';
import type {MasterConfig} from '@fluxer/config/src/MasterConfig';
import {createServer} from '@fluxer/hono/src/Server';
import {Hono} from 'hono';
import {afterAll, afterEach, describe, expect, test, vi} from 'vitest';
import {afterAll, afterEach, beforeAll, describe, expect, it, test, vi} from 'vitest';
import {buildAPIConfigFromMaster, buildAPIServerOptions} from './Config';
interface ListeningServer {
@@ -63,3 +64,50 @@ describe('buildAPIServerOptions', () => {
expect(server.headersTimeout).toBe(45_000);
});
});
function withUploadRelaySecret(master: MasterConfig, secretBase64: string): MasterConfig {
return {
...master,
services: {
...master.services,
media_proxy: {
...master.services.media_proxy,
upload_relay: {
...master.services.media_proxy.upload_relay,
secret_base64: secretBase64,
},
},
},
};
}
describe('buildAPIConfigFromMaster upload relay secret', () => {
let master: MasterConfig;
beforeAll(async () => {
master = await loadConfig();
});
it('refuses to build without FLUXER_MEDIA_PROXY_UPLOAD_RELAY_SECRET_BASE64', () => {
expect(() => buildAPIConfigFromMaster(withUploadRelaySecret(master, ''))).toThrow(
/FLUXER_MEDIA_PROXY_UPLOAD_RELAY_SECRET_BASE64/,
);
});
it('refuses a secret that decodes to fewer than 32 bytes', () => {
const secret = Buffer.alloc(16, 7).toString('base64');
expect(() => buildAPIConfigFromMaster(withUploadRelaySecret(master, secret))).toThrow(/at least 32 bytes/);
});
it('accepts a secret that decodes to 32 bytes', () => {
const secret = Buffer.alloc(32, 7).toString('base64');
expect(
buildAPIConfigFromMaster(withUploadRelaySecret(master, secret)).mediaProxy.uploadRelay.relaySecretBase64,
).toBe(secret);
});
it('reads the relay secret from the loaded config rather than the environment', () => {
expect(buildAPIConfigFromMaster(master).mediaProxy.uploadRelay.relaySecretBase64).toBe(
master.services.media_proxy.upload_relay.secret_base64,
);
});
});
+20 -14
View File
@@ -113,17 +113,18 @@ function mapPushProviderApps(
project_id?: string;
}>
| undefined,
configName: string,
): APIConfig['push']['apns']['apps'] {
return (apps ?? []).flatMap((app) => {
if (!app.app_id) return [];
return [
{
appId: app.app_id,
topic: app.topic,
environment: app.environment,
projectId: app.project_id,
},
];
return (apps ?? []).map((app) => {
if (!app.app_id) {
throw new Error(`${configName} contains an entry with no app_id`);
}
return {
appId: app.app_id,
topic: app.topic,
environment: app.environment,
projectId: app.project_id,
};
});
}
@@ -139,7 +140,13 @@ export function buildAPIConfigFromMaster(master: MasterConfig): APIConfig {
serviceName: 'api',
});
const uploadRelayConfig = master.services.media_proxy.upload_relay;
const uploadRelaySecretBase64 = process.env.FLUXER_MEDIA_PROXY_UPLOAD_RELAY_SECRET_BASE64 ?? '';
const uploadRelaySecretBase64 = uploadRelayConfig.secret_base64;
if (uploadRelaySecretBase64.length === 0) {
throw new Error('FLUXER_MEDIA_PROXY_UPLOAD_RELAY_SECRET_BASE64 is required for the API');
}
if (Buffer.from(uploadRelaySecretBase64, 'base64').length < 32) {
throw new Error('FLUXER_MEDIA_PROXY_UPLOAD_RELAY_SECRET_BASE64 must decode to at least 32 bytes');
}
if (!s3Config) {
throw new Error('S3 configuration is required for the API');
}
@@ -149,7 +156,6 @@ export function buildAPIConfigFromMaster(master: MasterConfig): APIConfig {
downloads: '',
reports: '',
harvests: '',
static: '',
};
if (master.database.backend === 'cassandra' && !cassandraSource) {
throw new Error('Cassandra configuration is required.');
@@ -494,7 +500,7 @@ export function buildAPIConfigFromMaster(master: MasterConfig): APIConfig {
privateKey: master.integrations.push.apns.private_key,
privateKeyPath: master.integrations.push.apns.private_key_path,
defaultEnvironment: master.integrations.push.apns.default_environment ?? 'production',
apps: mapPushProviderApps(master.integrations.push.apns.apps),
apps: mapPushProviderApps(master.integrations.push.apns.apps, 'FLUXER_PUSH_APNS_APPS'),
},
fcm: {
enabled: master.integrations.push.fcm.enabled,
@@ -504,7 +510,7 @@ export function buildAPIConfigFromMaster(master: MasterConfig): APIConfig {
privateKeyPath: master.integrations.push.fcm.private_key_path,
serviceAccountJsonPath: master.integrations.push.fcm.service_account_json_path,
tokenUri: master.integrations.push.fcm.token_uri ?? 'https://oauth2.googleapis.com/token',
apps: mapPushProviderApps(master.integrations.push.fcm.apps),
apps: mapPushProviderApps(master.integrations.push.fcm.apps, 'FLUXER_PUSH_FCM_APPS'),
},
},
worker: {
@@ -0,0 +1,59 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
import {generateKeyPairSync} from 'node:crypto';
import {describe, expect, it} from 'vitest';
import type {BlueskyOAuthConfig} from '../config/APIConfig';
import {MockKVProvider} from '../test/mocks/MockKVProvider';
import {BlueskyOAuthService} from './BlueskyOAuthService';
const API_PUBLIC_ENDPOINT = 'https://chat.example.com';
function generateSigningKey(): string {
const {privateKey} = generateKeyPairSync('ec', {
namedCurve: 'P-256',
privateKeyEncoding: {type: 'pkcs8', format: 'pem'},
publicKeyEncoding: {type: 'spki', format: 'pem'},
});
return privateKey;
}
function buildConfig(overrides: Partial<BlueskyOAuthConfig> = {}): BlueskyOAuthConfig {
return {
enabled: true,
client_name: 'Fluxer',
client_uri: '',
logo_uri: '',
tos_uri: '',
policy_uri: '',
keys: [{kid: 'test-key', private_key: generateSigningKey()}],
...overrides,
};
}
async function serveClientMetadata(config: BlueskyOAuthConfig): Promise<Record<string, unknown>> {
const service = await BlueskyOAuthService.create(config, new MockKVProvider(), API_PUBLIC_ENDPOINT);
return JSON.parse(JSON.stringify(service.clientMetadata)) as Record<string, unknown>;
}
describe('BlueskyOAuthService', () => {
it('omits the legal URLs from the served client document when none are configured', async () => {
const metadata = await serveClientMetadata(buildConfig());
expect(metadata).not.toHaveProperty('tos_uri');
expect(metadata).not.toHaveProperty('policy_uri');
expect(metadata).not.toHaveProperty('logo_uri');
expect(metadata.client_id).toBe(`${API_PUBLIC_ENDPOINT}/connections/bluesky/client-metadata.json`);
});
it('echoes configured legal URLs verbatim', async () => {
const metadata = await serveClientMetadata(
buildConfig({
tos_uri: 'https://chat.example.com/terms',
policy_uri: 'https://chat.example.com/privacy',
}),
);
expect(metadata.tos_uri).toBe('https://chat.example.com/terms');
expect(metadata.policy_uri).toBe('https://chat.example.com/privacy');
});
});
-1
View File
@@ -153,7 +153,6 @@ export interface APIConfig {
reports: string;
harvests: string;
downloads: string;
static: string;
};
};
s3Downloads: ResolvedDownloadsProvider;
@@ -61,7 +61,6 @@ const MAX_DESKTOP_OBJECTS_PER_PREFIX = 10_000;
const DESKTOP_BUCKET_PREFIX = 'desktop';
const DESKTOP_TEST_BUCKET_PREFIX = 'desktop-test';
const DOWNLOAD_KEY_ALLOWED_PREFIXES = [`${DESKTOP_BUCKET_PREFIX}/`, `${DESKTOP_TEST_BUCKET_PREFIX}/`];
const DEFAULT_API_CLIENT_BASE_URL = 'https://api.fluxer.app';
const GITHUB_RELEASE_DOWNLOAD_BASE_URL = 'https://github.com/fluxerapp/fluxer/releases/download';
const GITHUB_RELEASE_MARKER_DIRECTORY = 'github-releases';
@@ -687,11 +686,7 @@ export class DownloadService {
}
private buildBaseUrl(baseUrl?: string): string {
const configuredBaseUrl = (baseUrl ?? Config.endpoints.apiClient).trim();
if (configuredBaseUrl.length > 0) {
return configuredBaseUrl.replace(/\/+$/u, '');
}
return DEFAULT_API_CLIENT_BASE_URL;
return (baseUrl ?? Config.endpoints.apiClient).trim().replace(/\/+$/u, '');
}
private buildDesktopVersionUrl(params: {
@@ -0,0 +1,62 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
import {Readable} from 'node:stream';
import {describe, expect, it} from 'vitest';
import {Config} from '../../Config';
import type {IStorageService} from '../../infrastructure/IStorageService';
import {DownloadService} from '../DownloadService';
const PREFIX = 'desktop/stable/darwin/x64';
const MANIFEST_KEY = `${PREFIX}/manifest.json`;
const FILENAME = 'Fluxer-1.3.0-mac-universal.dmg';
const SHA256 = 'a'.repeat(64);
const LATEST_PARAMS = {
channel: 'stable',
plat: 'darwin',
arch: 'x64',
} as const;
const MANIFEST_BODY = JSON.stringify({
channel: 'stable',
platform: 'darwin',
arch: 'x64',
version: '1.3.0',
pub_date: '2026-08-17T00:00:00Z',
files: {dmg: {filename: FILENAME, sha256: SHA256}},
});
function createService() {
const objectKeys = [`${PREFIX}/${FILENAME}`];
const storageService = {
streamObject: async (params: {key: string}) => {
if (params.key !== MANIFEST_KEY) {
return null;
}
const buffer = Buffer.from(MANIFEST_BODY, 'utf8');
return {body: Readable.from([buffer]), contentLength: buffer.byteLength};
},
listObjects: async () => objectKeys.map((key) => ({key})),
getObjectMetadata: async (_bucket: string, key: string) =>
objectKeys.includes(key) ? {contentLength: 1, contentType: 'application/x-apple-diskimage'} : null,
} as unknown as IStorageService;
return new DownloadService(storageService);
}
describe('desktop download base url', () => {
it('builds artifact urls from the configured client API endpoint', async () => {
const version = await createService().getLatestDesktopVersion({...LATEST_PARAMS});
const base = Config.endpoints.apiClient.replace(/\/+$/u, '');
expect(base.length).toBeGreaterThan(0);
expect(version?.files.dmg?.url).toBe(`${base}/dl/desktop/stable/darwin/x64/1.3.0/dmg`);
expect(version?.files.dmg?.checksum_url).toBe(`${base}/dl/desktop/stable/darwin/x64/1.3.0/dmg.sha256`);
});
it('prefers an explicit base url and strips its trailing slashes', async () => {
const version = await createService().getLatestDesktopVersion({
...LATEST_PARAMS,
baseUrl: 'https://chat.example.com/api//',
});
expect(version?.files.dmg?.url).toBe('https://chat.example.com/api/dl/desktop/stable/darwin/x64/1.3.0/dmg');
});
});
@@ -44,7 +44,6 @@ interface S3BucketConfigOverrides {
downloads?: string;
reports?: string;
harvests?: string;
static?: string;
}
interface S3ConfigOverrides {
@@ -115,6 +115,38 @@ describe('InstanceConfigRepository', () => {
});
});
it('reports the effective captcha provider as none while the selected pair is incomplete', async () => {
const executor = new CountingInMemoryCassandraQueryExecutor();
setCassandraQueryExecutorForTesting(executor);
const kvProvider = new MockKVProvider();
const repository = createRepository(kvProvider);
await repository.setInstanceIntegrationsConfig({
captcha: {
provider: 'turnstile',
hcaptcha_site_key: 'hcaptcha-site-key',
hcaptcha_secret_key: 'hcaptcha-secret-key',
},
});
await expect(repository.getEffectiveCaptchaConfig()).resolves.toMatchObject({
enabled: false,
provider: 'none',
});
await repository.setInstanceIntegrationsConfig({
captcha: {
turnstile_site_key: 'turnstile-site-key',
turnstile_secret_key: 'turnstile-secret-key',
},
});
await expect(repository.getEffectiveCaptchaConfig()).resolves.toMatchObject({
enabled: true,
provider: 'turnstile',
});
});
it('uses the registration URL id as the admin-visible registration code', async () => {
const executor = new CountingInMemoryCassandraQueryExecutor();
setCassandraQueryExecutorForTesting(executor);
@@ -1262,8 +1262,8 @@ export class InstanceConfigRepository {
? Boolean(turnstileSiteKey && turnstileSecretKey)
: false;
return {
enabled: provider !== 'none' && providerReady,
provider,
enabled: providerReady,
provider: providerReady ? provider : 'none',
hcaptcha_site_key: hcaptchaSiteKey,
hcaptcha_secret_key: hcaptchaSecretKey,
turnstile_site_key: turnstileSiteKey,
@@ -0,0 +1,99 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
import {Hono} from 'hono';
import {afterEach, describe, expect, it} from 'vitest';
import type {SsoService} from '../auth/services/SsoService';
import {setCassandraQueryExecutorForTesting} from '../database/CassandraQueryExecution';
import type {LimitConfigService} from '../limits/LimitConfigService';
import {InMemoryCassandraQueryExecutor} from '../test/InMemoryCassandraQueryExecutor';
import {MockKVProvider} from '../test/mocks/MockKVProvider';
import type {HonoEnv} from '../types/HonoEnv';
import {InstanceConfigRepository} from './InstanceConfigRepository';
import {InstanceController} from './InstanceController';
interface DiscoveryCaptcha {
provider: string;
hcaptcha_site_key: string | null;
turnstile_site_key: string | null;
}
describe('InstanceController discovery captcha', () => {
const repositories: Array<InstanceConfigRepository> = [];
afterEach(() => {
for (const repository of repositories) {
repository.shutdown();
}
repositories.length = 0;
});
function createRepository(): InstanceConfigRepository {
setCassandraQueryExecutorForTesting(new InMemoryCassandraQueryExecutor());
const repository = new InstanceConfigRepository(new MockKVProvider());
repositories.push(repository);
return repository;
}
function createApp(repository: InstanceConfigRepository): Hono<HonoEnv> {
const app = new Hono<HonoEnv>({strict: true});
app.use('*', async (ctx, next) => {
ctx.set('instanceConfigRepository', repository);
ctx.set('limitConfigService', {
getConfigWireFormat: () => ({version: 2, traitDefinitions: [], rules: [], defaultsHash: 'test'}),
} as unknown as LimitConfigService);
ctx.set('ssoService', {
getPublicStatus: async () => ({
enabled: false,
enforced: false,
display_name: null,
redirect_uri: '',
}),
} as unknown as SsoService);
await next();
});
InstanceController(app);
return app;
}
async function readCaptcha(repository: InstanceConfigRepository): Promise<DiscoveryCaptcha> {
const response = await createApp(repository).request('http://localhost/.well-known/fluxer');
expect(response.status).toBe(200);
return ((await response.json()) as {captcha: DiscoveryCaptcha}).captcha;
}
it('advertises no provider and no site key while the selected pair is incomplete', async () => {
const repository = createRepository();
await repository.setInstanceIntegrationsConfig({
captcha: {
provider: 'turnstile',
hcaptcha_site_key: 'hcaptcha-site-key',
hcaptcha_secret_key: 'hcaptcha-secret-key',
},
});
await expect(readCaptcha(repository)).resolves.toEqual({
provider: 'none',
hcaptcha_site_key: null,
turnstile_site_key: null,
});
});
it('advertises only the site key that matches the named provider', async () => {
const repository = createRepository();
await repository.setInstanceIntegrationsConfig({
captcha: {
provider: 'turnstile',
hcaptcha_site_key: 'hcaptcha-site-key',
hcaptcha_secret_key: 'hcaptcha-secret-key',
turnstile_site_key: 'turnstile-site-key',
turnstile_secret_key: 'turnstile-secret-key',
},
});
await expect(readCaptcha(repository)).resolves.toEqual({
provider: 'turnstile',
hcaptcha_site_key: null,
turnstile_site_key: 'turnstile-site-key',
});
});
});
@@ -57,8 +57,8 @@ function buildDiscoveryStaticInput(
},
captcha: {
provider: runtime.captcha.provider,
hcaptcha_site_key: runtime.captcha.hcaptcha_site_key,
turnstile_site_key: runtime.captcha.turnstile_site_key,
hcaptcha_site_key: runtime.captcha.provider === 'hcaptcha' ? runtime.captcha.hcaptcha_site_key : null,
turnstile_site_key: runtime.captcha.provider === 'turnstile' ? runtime.captcha.turnstile_site_key : null,
},
features: {
voice_enabled: Config.voice.enabled,
@@ -45,7 +45,7 @@ function resolveCaptchaProvider(
}
const secretKey = resolveProviderSecret(config, requestedProvider);
if (!secretKey) {
throw new Error(`Captcha provider ${requestedProvider} is enabled but has no configured secret key`);
throw new InvalidCaptchaError();
}
return createCaptchaProvider({mode: requestedProvider, secretKey});
}
@@ -0,0 +1,62 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
import {AppErrorHandler} from '@fluxer/errors/src/domains/core/ErrorHandlers';
import {Hono} from 'hono';
import {afterEach, beforeEach, describe, expect, it} from 'vitest';
import {Config} from '../../Config';
import type {InstanceCaptchaEffectiveConfig, InstanceConfigRepository} from '../../instance/InstanceConfigRepository';
import type {HonoEnv} from '../../types/HonoEnv';
import {CaptchaMiddleware} from '../CaptchaMiddleware';
const HCAPTCHA_ONLY: InstanceCaptchaEffectiveConfig = {
enabled: true,
provider: 'hcaptcha',
hcaptcha_site_key: 'hcaptcha-site-key',
hcaptcha_secret_key: 'hcaptcha-secret-key',
turnstile_site_key: null,
turnstile_secret_key: null,
};
function createHarness(
captcha: InstanceCaptchaEffectiveConfig,
): (headers: Record<string, string>) => Promise<Response> {
const repository = {
getEffectiveCaptchaConfig: async () => captcha,
} as unknown as InstanceConfigRepository;
const app = new Hono<HonoEnv>();
app.use(async (ctx, next) => {
ctx.set('instanceConfigRepository', repository);
await next();
});
app.use(CaptchaMiddleware);
app.post('/auth/register', (ctx) => ctx.text('ok'));
app.onError(AppErrorHandler);
return async (headers) => app.request('http://localhost/auth/register', {method: 'POST', headers});
}
describe('CaptchaMiddleware provider header', () => {
let previousTestModeEnabled: boolean;
beforeEach(() => {
previousTestModeEnabled = Config.dev.testModeEnabled;
Config.dev.testModeEnabled = false;
});
afterEach(() => {
Config.dev.testModeEnabled = previousTestModeEnabled;
});
it('rejects a header naming a provider the instance holds no secret key for with 400 INVALID_CAPTCHA', async () => {
const request = createHarness(HCAPTCHA_ONLY);
const response = await request({'x-captcha-token': 'solution', 'x-captcha-type': 'turnstile'});
expect(response.status).toBe(400);
expect(await response.json()).toMatchObject({code: 'INVALID_CAPTCHA'});
});
it('rejects a request with no proof with 400 CAPTCHA_REQUIRED', async () => {
const request = createHarness(HCAPTCHA_ONLY);
const response = await request({});
expect(response.status).toBe(400);
expect(await response.json()).toMatchObject({code: 'CAPTCHA_REQUIRED'});
});
});
@@ -2,18 +2,29 @@
import * as fs from 'node:fs';
import type {Hono} from 'hono';
import {Config} from '../Config';
import {RateLimitMiddleware} from '../middleware/RateLimitMiddleware';
import {RateLimitConfigs} from '../RateLimitConfig';
import type {HonoEnv} from '../types/HonoEnv';
import {resolveAssetPath} from '../utils/AssetPaths';
const SPEC_PATH = resolveAssetPath('openapi', 'openapi.json');
const SPEC_BODY = fs.readFileSync(SPEC_PATH, 'utf-8');
const SPEC_DOCUMENT = JSON.parse(fs.readFileSync(SPEC_PATH, 'utf-8')) as Record<string, unknown>;
let specBody: string | null = null;
export function buildOpenAPISpecBody(apiClientEndpoint: string): string {
return JSON.stringify({
...SPEC_DOCUMENT,
servers: [{url: `${apiClientEndpoint.trim().replace(/\/+$/u, '')}/v1`, description: 'This deployment'}],
});
}
export function OpenAPIController(app: Hono<HonoEnv>): void {
app.get('/openapi.json', RateLimitMiddleware(RateLimitConfigs.INSTANCE_INFO), (ctx) => {
specBody ??= buildOpenAPISpecBody(Config.endpoints.apiClient);
ctx.header('Access-Control-Allow-Origin', '*');
ctx.header('Content-Type', 'application/json; charset=utf-8');
return ctx.body(SPEC_BODY);
return ctx.body(specBody);
});
}
@@ -0,0 +1,43 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
import {Hono} from 'hono';
import {describe, expect, it} from 'vitest';
import {Config} from '../../Config';
import type {HonoEnv} from '../../types/HonoEnv';
import {buildOpenAPISpecBody, OpenAPIController} from '../OpenAPIController';
interface ServerEntry {
url: string;
description: string;
}
function parseServers(body: string): Array<ServerEntry> {
return (JSON.parse(body) as {servers: Array<ServerEntry>}).servers;
}
describe('OpenAPI server entry', () => {
it('declares the deployment client API endpoint', () => {
expect(parseServers(buildOpenAPISpecBody('https://chat.example.com/api'))).toEqual([
{url: 'https://chat.example.com/api/v1', description: 'This deployment'},
]);
});
it('strips trailing slashes from the configured endpoint', () => {
expect(parseServers(buildOpenAPISpecBody('https://chat.example.com/api//'))[0].url).toBe(
'https://chat.example.com/api/v1',
);
});
it('serves the configured endpoint instead of an upstream host', async () => {
const app = new Hono<HonoEnv>();
OpenAPIController(app);
const response = await app.request('/openapi.json');
expect(response.status).toBe(200);
const spec = (await response.json()) as {servers: Array<ServerEntry>; paths: Record<string, unknown>};
expect(spec.servers).toEqual([
{url: `${Config.endpoints.apiClient.replace(/\/+$/u, '')}/v1`, description: 'This deployment'},
]);
expect(spec.servers[0].url).not.toContain('api.fluxer.app');
expect(Object.keys(spec.paths).length).toBeGreaterThan(0);
});
});
+2 -3
View File
@@ -68,11 +68,10 @@ function setDefaultTestEnv(): void {
FLUXER_APP_PROXY_PORT: '8773',
FLUXER_GATEWAY_MEDIA_PROXY_ENDPOINT: 'http://127.0.0.1:8088/media',
FLUXER_GATEWAY_RPC_AUTH_TOKEN: 'test-gateway-rpc-token',
FLUXER_GATEWAY_PUSH_ENABLED: 'false',
FLUXER_SUDO_MODE_SECRET: 'test-sudo-secret',
FLUXER_CONNECTION_INITIATION_SECRET: 'test-connection-secret',
FLUXER_VAPID_PUBLIC_KEY: 'test-vapid-public-key',
FLUXER_VAPID_PRIVATE_KEY: 'test-vapid-private-key',
FLUXER_VAPID_PUBLIC_KEY: 'BB76bTFIuoqmxJtTfZX0yGTn1f_qu9H03B_nkj8OyExJFkN7Y-HBZZzShnHZoEhXKc5ZRy3jFu7OkBbnaQG-4aw',
FLUXER_VAPID_PRIVATE_KEY: 'Xgi-3P8J-I3Q6U1HlCcXMuc_tKLGAM9nIfznX3Hz68o',
FLUXER_VAPID_EMAIL: '[email protected]',
FLUXER_PASSKEY_RP_NAME: 'Fluxer Test',
FLUXER_PASSKEY_RP_ID: 'localhost',