// SPDX-License-Identifier: AGPL-3.0-or-later import crypto from 'node:crypto'; import {Config} from '@app/api/Config'; import type {APIConfig, BlueskyOAuthConfig, BlueskyOAuthKeyConfig} from '@app/api/config/APIConfig'; import {fetchMany, fetchOne, upsertOne} from '@app/api/database/CassandraQueryExecution'; import type {InstanceConfigurationRow} from '@app/api/database/types/InstanceConfigTypes'; import { getDefaultDateOfBirthCollection, setCachedDateOfBirthCollection, } from '@app/api/instance/DateOfBirthCollectionCache'; import {InstanceConfigCache} from '@app/api/instance/InstanceConfigCache'; import {normalizeSsoAllowedEmailDomains} from '@app/api/instance/SsoConfigValidation'; import {Logger} from '@app/api/Logger'; import {isLimitConfigSnapshot} from '@app/api/limits/LimitConfigValidation'; import {resolveDeferredPhoneGateEnabled, setCachedDeferredPhoneGateEnabled} from '@app/api/risk/DeferredPhoneGateCache'; import {InstanceConfiguration} from '@app/api/Tables'; import {DEFAULT_DECAY_CONSTANTS, DEFAULT_RENEWAL_CONSTANTS} from '@app/api/utils/AttachmentDecay'; import {isJsonRecord} from '@app/api/utils/JsonBoundaryUtils'; import type {LimitConfigSnapshot} from '@fluxer/limits/src/LimitTypes'; import { InstanceConfigResponse, InstanceConfigUpdateRequest, type PendingRegistrationResponse, type RegistrationUrlResponse, } from '@fluxer/schema/src/domains/admin/AdminSchemas'; import { type GatewayRolloutConfig, GatewayRolloutConfigSchema, } from '@fluxer/schema/src/domains/admin/GatewayRolloutSchemas'; import { type VoiceNoiseSuppressionConfig, VoiceNoiseSuppressionConfigSchema, } from '@fluxer/schema/src/domains/admin/VoiceNoiseSuppressionSchemas'; import { type ExperimentDeliveryConfig, ExperimentDeliveryConfigSchema, } from '@fluxer/schema/src/domains/experiment/ExperimentSchemas'; import { type ScreenShareDeliveryConfig, ScreenShareDeliveryConfigSchema, } from '@fluxer/schema/src/domains/experiment/ScreenShareDeliverySchemas'; import { type InstanceAppPublic, InstanceAppPublicSchema, type InstanceBranding, type InstanceCaptchaProvider, InstanceCaptchaProviderSchema, type InstanceCommunity, type InstanceRegistration, InstanceRegistrationSchema, type InstanceServices, type InstanceSetup, } from '@fluxer/schema/src/domains/instance/InstanceSchemas'; import {normalizeString, SnowflakeType} from '@fluxer/schema/src/primitives/SchemaPrimitives'; import type {IKVProvider} from '@pkgs/kv_client/src/IKVProvider'; import {z} from 'zod'; const GATEWAY_ROLLOUT_CONFIG_KEY = 'gateway_rollout_config'; const VOICE_NOISE_SUPPRESSION_CONFIG_KEY = 'voice_noise_suppression_config'; const SCREEN_SHARE_DELIVERY_CONFIG_KEY = 'screen_share_delivery_config'; const EXPERIMENT_DELIVERY_CONFIG_KEY = 'experiment_delivery_config'; const REGISTRATION_CONFIG_KEY = 'registration_config'; const REGISTRATION_URLS_KEY = 'registration_urls'; const REGISTRATION_PENDING_APPROVALS_KEY = 'registration_pending_approvals'; const APP_PUBLIC_CONFIG_KEY = 'app_public_config'; const ADMIN_BOOTSTRAP_KEY = 'admin_bootstrapped'; const INSTANCE_POLICY_CONFIG_KEY = 'instance_policy_config'; const LIMIT_CONFIG_KEY = 'limit_config'; const INSTANCE_INTEGRATIONS_CONFIG_KEY = 'instance_integrations_config'; const INSTANCE_MEDIA_CONFIG_KEY = 'instance_media_config'; export const INSTANCE_CONFIG_REFRESH_CHANNEL = 'instance-config-refresh'; export const REGISTRATION_PENDING_APPROVAL_TRAIT = 'registration_pending_approval'; export const REGISTRATION_REJECTED_TRAIT = 'registration_rejected'; export type InstanceRegistrationConfig = InstanceRegistration; interface InstanceAppPublicConfig extends Omit { setup: Pick; } export type InstancePremiumMode = 'mirror' | 'everyone'; interface LimitConfigInputs { config: LimitConfigSnapshot | null; premiumMode: InstancePremiumMode; } export interface InstancePolicyConfig { single_community_enabled: boolean; single_community_guild_id: string | null; direct_messages_disabled: boolean; direct_messages_locked: boolean; premium_mode: InstancePremiumMode; gif_enabled: boolean | null; youtube_enabled: boolean | null; bluesky_enabled: boolean | null; deferred_phone_gate_enabled: boolean; deferred_phone_gate_window_hours: number; deferred_phone_gate_member_threshold: number; } type InstanceEmailProvider = 'smtp' | 'none'; interface InstanceGifIntegrationConfig { klipy_api_key: string | null; } interface InstanceYoutubeIntegrationConfig { api_key: string | null; } interface InstanceCaptchaIntegrationConfig { provider: InstanceCaptchaProvider | null; hcaptcha_site_key: string | null; hcaptcha_secret_key: string | null; turnstile_site_key: string | null; turnstile_secret_key: string | null; } interface InstanceEmailSmtpIntegrationConfig { host: string | null; port: number | null; username: string | null; password: string | null; secure: boolean | null; } interface InstanceEmailIntegrationConfig { enabled: boolean | null; provider: InstanceEmailProvider | null; from_email: string | null; from_name: string | null; smtp: InstanceEmailSmtpIntegrationConfig; disable_new_ip_authorization: boolean | null; } interface InstanceBlueskyKeyIntegrationConfig { kid: string; private_key: string | null; } interface InstanceBlueskyIntegrationConfig { enabled: boolean | null; client_name: string | null; client_uri: string | null; logo_uri: string | null; tos_uri: string | null; policy_uri: string | null; keys: Array; } interface InstanceIntegrationsConfig { gif: InstanceGifIntegrationConfig; youtube: InstanceYoutubeIntegrationConfig; captcha: InstanceCaptchaIntegrationConfig; email: InstanceEmailIntegrationConfig; bluesky: InstanceBlueskyIntegrationConfig; } interface InstanceGifEffectiveConfig { klipy_api_key: string | null; active_api_key: string | null; available: boolean; } export interface InstanceCaptchaEffectiveConfig { enabled: boolean; provider: InstanceCaptchaProvider; hcaptcha_site_key: string | null; hcaptcha_secret_key: string | null; turnstile_site_key: string | null; turnstile_secret_key: string | null; } interface InstanceIntegrationsAdminConfig { gif: { klipy_api_key_set: boolean; effective_available: boolean; }; youtube: { api_key_set: boolean; effective_available: boolean; }; captcha: { provider: InstanceCaptchaProvider | null; effective_provider: InstanceCaptchaProvider; hcaptcha_site_key: string | null; hcaptcha_secret_key_set: boolean; turnstile_site_key: string | null; turnstile_secret_key_set: boolean; effective_enabled: boolean; }; email: { enabled: boolean | null; effective_enabled: boolean; provider: InstanceEmailProvider | null; effective_provider: InstanceEmailProvider; from_email: string | null; from_name: string | null; smtp: { host: string | null; port: number | null; username: string | null; password_set: boolean; secure: boolean | null; }; disable_new_ip_authorization: boolean; effective_disable_new_ip_authorization: boolean; }; bluesky: { enabled: boolean | null; effective_enabled: boolean; client_name: string | null; client_uri: string | null; logo_uri: string | null; tos_uri: string | null; policy_uri: string | null; key_count: number; }; } interface InstanceAttachmentDecayConfig { enabled: boolean | null; min_size_mb: number | null; max_size_mb: number | null; max_eligible_size_mb: number | null; min_lifetime_days: number | null; max_lifetime_days: number | null; curve: number | null; renew_threshold_days: number | null; renew_window_days: number | null; } interface InstanceMediaConfig { attachment_decay: InstanceAttachmentDecayConfig; } export interface InstanceAttachmentDecayEffectiveConfig { enabled: boolean; min_size_mb: number; max_size_mb: number; max_eligible_size_mb: number; min_lifetime_days: number; max_lifetime_days: number; curve: number; renew_threshold_days: number; renew_window_days: number; } interface InstanceMediaAdminConfig { attachment_decay: InstanceAttachmentDecayConfig & { effective: InstanceAttachmentDecayEffectiveConfig; }; } interface InstanceIntegrationsConfigPatch { gif?: Partial; youtube?: Partial; captcha?: Partial; email?: Partial> & { smtp?: Partial; }; bluesky?: Partial> & { keys?: Array>; }; } interface InstanceMediaConfigPatch { attachment_decay?: Partial; } export interface InstanceRegistrationUrl extends RegistrationUrlResponse { code_hash: string; } type InstanceRegistrationUrlPublic = RegistrationUrlResponse; type InstancePendingRegistration = PendingRegistrationResponse; const DEFAULT_REGISTRATION_CONFIG: InstanceRegistrationConfig = { mode: 'open', admin_registration_urls_enabled: true, }; const FETCH_CONFIG_QUERY = InstanceConfiguration.selectCql({ where: InstanceConfiguration.where.eq('key'), limit: 1, }); const FETCH_ALL_CONFIG_QUERY = InstanceConfiguration.selectCql(); const FETCH_LIMIT_CONFIG_INPUTS_QUERY = InstanceConfiguration.selectCql({ where: InstanceConfiguration.where.in('key', 'keys'), }); function parseStoredLimitConfig(raw: string | null): LimitConfigSnapshot | null { if (raw === null) return null; let parsed: unknown; try { parsed = JSON.parse(raw); } catch (error) { Logger.error({error}, 'Stored limit configuration is not valid JSON, falling back to default limits'); return null; } if (!isLimitConfigSnapshot(parsed)) { Logger.error('Stored limit configuration has an invalid shape, falling back to default limits'); return null; } return parsed; } function normalizeOptionalString(value: unknown): string | null { return typeof value === 'string' && value.trim().length > 0 ? value.trim() : null; } function normalizeOptionalPublicString(value: string | null | undefined, fallback: string | null): string | null { return value === undefined ? fallback : normalizeOptionalString(value); } function getDefaultAppPublicConfig(): InstanceAppPublicConfig { return { branding: { product_name: Config.instance.branding.productName || 'Fluxer', icon_url: normalizeOptionalString(Config.instance.branding.iconUrl), symbol_url: normalizeOptionalString(Config.instance.branding.symbolUrl), logo_url: normalizeOptionalString(Config.instance.branding.logoUrl), wordmark_url: normalizeOptionalString(Config.instance.branding.wordmarkUrl), favicon_url: normalizeOptionalString(Config.instance.branding.faviconUrl), theme_color: normalizeOptionalString(Config.instance.branding.themeColor), status_page_url: normalizeOptionalString(Config.instance.branding.statusPageUrl), status_page_incident_history_url: normalizeOptionalString(Config.instance.branding.statusPageIncidentHistoryUrl), }, setup: { configured: !Config.instance.selfHosted || Config.instance.setup.configured, }, legal: { terms_url: null, privacy_url: null, }, registration: { collect_date_of_birth: getDefaultDateOfBirthCollection(), }, }; } type StoredConfigSection = | 'app public' | 'gateway rollout' | 'voice noise suppression' | 'screen share delivery' | 'experiment delivery' | 'instance policy' | 'integrations' | 'media' | 'registration' | 'registration URLs' | 'pending registrations' | 'SSO flags' | 'SSO allowed domains'; function parseStoredConfigValue(raw: string | null, section: StoredConfigSection): unknown { if (raw === null) return {}; try { return JSON.parse(raw); } catch { throw new Error(`Stored ${section} configuration is not valid JSON`); } } function describeInvalidFields(issues: ReadonlyArray<{path: ReadonlyArray}>, index?: number): string { const maxReportedIssues = 8; const invalidFields = new Set( issues.slice(0, maxReportedIssues).map((issue) => { const path = index === undefined ? issue.path : [index, ...issue.path]; return path.length === 0 ? '' : path.join('.'); }), ); const suffix = issues.length > maxReportedIssues ? ', ...' : ''; return `${[...invalidFields].join(', ')}${suffix}`; } function validateStoredConfig( schema: z.ZodType, value: unknown, section: StoredConfigSection, index?: number, ): T { const result = schema.safeParse(value); if (!result.success) { throw new Error( `Stored ${section} configuration has invalid fields: ${describeInvalidFields(result.error.issues, index)}`, ); } return result.data; } function removeStoredPaths(value: unknown, paths: ReadonlyArray>): void { const arrays = new Set>(); for (const path of paths) { let parent: unknown = value; for (const key of path.slice(0, -1)) { if (typeof parent !== 'object' || parent === null) break; parent = (parent as Record)[key]; } if (typeof parent !== 'object' || parent === null) continue; delete (parent as Record)[path.at(-1)!]; if (Array.isArray(parent)) arrays.add(parent); } for (const array of arrays) { array.splice(0, array.length, ...array.filter(() => true)); } } function salvageStoredConfig(schema: z.ZodType, value: unknown, section: StoredConfigSection): T { const strict = schema.safeParse(value); if (strict.success) return strict.data; Logger.error( {section, invalidFields: describeInvalidFields(strict.error.issues)}, 'Invalid stored instance configuration, falling back to defaults for the invalid fields', ); const signature = (issues: ReadonlyArray<{path: ReadonlyArray}>) => issues.map((issue) => issue.path.join('.')).join('|'); const salvaged = structuredClone(value); let issues = strict.error.issues; let trim = 0; for (let attempt = 0; attempt < 64; attempt++) { const paths = issues.map((issue) => issue.path.slice(0, Math.max(0, issue.path.length - trim))); if (paths.some((path) => path.length === 0)) break; removeStoredPaths(salvaged, paths); const result = schema.safeParse(salvaged); if (result.success) return result.data; trim = signature(result.error.issues) === signature(issues) ? trim + 1 : 0; issues = result.error.issues; } return schema.parse({}); } function parseStoredConfig(schema: z.ZodType, raw: string | null, section: StoredConfigSection): T { return validateStoredConfig(schema, parseStoredConfigValue(raw, section), section); } function readStoredConfigOrDefault(section: StoredConfigSection, decode: () => T, fallback: () => T): T { try { return decode(); } catch (error) { Logger.error({error, section}, 'Invalid stored instance configuration, falling back to defaults'); return fallback(); } } function parseStoredConfigOrDefault(schema: z.ZodType, raw: string | null, section: StoredConfigSection): T { return readStoredConfigOrDefault( section, () => parseStoredConfig(schema, raw, section), () => parseStoredConfig(schema, null, section), ); } function readStoredConfigValue(raw: string | null, section: StoredConfigSection): unknown { return readStoredConfigOrDefault( section, () => parseStoredConfigValue(raw, section), () => ({}), ); } function checkStoredConfig(section: StoredConfigSection, decode: () => unknown): void { readStoredConfigOrDefault(section, decode, () => null); } function decodeGatewayRolloutConfig(value: unknown): GatewayRolloutConfig { const input = isJsonRecord(value) && !Object.hasOwn(value, 'rpc_request_timeout_ms') && Object.hasOwn(value, 'nats_request_timeout_ms') ? {...value, rpc_request_timeout_ms: value.nats_request_timeout_ms} : value; return validateStoredConfig(GatewayRolloutConfigSchema, input, 'gateway rollout'); } function parseStoredGatewayRolloutConfig(raw: string | null): GatewayRolloutConfig { return decodeGatewayRolloutConfig(parseStoredConfigValue(raw, 'gateway rollout')); } function parseStoredVoiceNoiseSuppressionConfig(raw: string | null): VoiceNoiseSuppressionConfig { return parseStoredConfigOrDefault(VoiceNoiseSuppressionConfigSchema, raw, 'voice noise suppression'); } function parseStoredScreenShareDeliveryConfig(raw: string | null): ScreenShareDeliveryConfig { return parseStoredConfigOrDefault(ScreenShareDeliveryConfigSchema, raw, 'screen share delivery'); } function parseStoredExperimentDeliveryConfig(raw: string | null): ExperimentDeliveryConfig { return parseStoredConfigOrDefault(ExperimentDeliveryConfigSchema, raw, 'experiment delivery'); } function validateStoredCollection(schema: z.ZodType, value: unknown, section: StoredConfigSection): Array { if (!Array.isArray(value)) { throw new Error(`Stored ${section} configuration must be an array`); } return Array.from(value, (entry, index) => validateStoredConfig(schema, entry, section, index)); } function parseStoredCollection(schema: z.ZodType, raw: string | null, section: StoredConfigSection): Array { return raw === null ? [] : validateStoredCollection(schema, parseStoredConfigValue(raw, section), section); } const StoredInstanceAppPublicSchema = InstanceAppPublicSchema.extend({ branding: InstanceAppPublicSchema.shape.branding.partial().optional(), setup: InstanceAppPublicSchema.shape.setup.pick({configured: true}).partial().optional(), legal: InstanceAppPublicSchema.shape.legal.partial().optional(), registration: InstanceAppPublicSchema.shape.registration.partial().optional(), }); function parseStoredAppPublicConfig(raw: string | null): InstanceAppPublicConfig { return buildAppPublicConfig( salvageStoredConfig(StoredInstanceAppPublicSchema, readStoredConfigValue(raw, 'app public'), 'app public'), ); } function decodeAppPublicConfig(value: unknown): InstanceAppPublicConfig { return buildAppPublicConfig(validateStoredConfig(StoredInstanceAppPublicSchema, value, 'app public')); } function buildAppPublicConfig(config: z.infer): InstanceAppPublicConfig { const defaults = getDefaultAppPublicConfig(); const {branding = {}, setup = {}, legal = {}, registration = {}} = config; return { branding: { product_name: normalizeOptionalString(branding.product_name) ?? defaults.branding.product_name, icon_url: normalizeOptionalPublicString(branding.icon_url, defaults.branding.icon_url), symbol_url: normalizeOptionalPublicString(branding.symbol_url, defaults.branding.symbol_url), logo_url: normalizeOptionalPublicString(branding.logo_url, defaults.branding.logo_url), wordmark_url: normalizeOptionalPublicString(branding.wordmark_url, defaults.branding.wordmark_url), favicon_url: normalizeOptionalPublicString(branding.favicon_url, defaults.branding.favicon_url), theme_color: normalizeOptionalPublicString(branding.theme_color, defaults.branding.theme_color), status_page_url: normalizeOptionalPublicString(branding.status_page_url, defaults.branding.status_page_url), status_page_incident_history_url: normalizeOptionalPublicString( branding.status_page_incident_history_url, defaults.branding.status_page_incident_history_url, ), }, setup: { configured: setup.configured ?? defaults.setup.configured, }, legal: { terms_url: normalizeOptionalPublicString(legal.terms_url, defaults.legal.terms_url), privacy_url: normalizeOptionalPublicString(legal.privacy_url, defaults.legal.privacy_url), }, registration: { collect_date_of_birth: registration.collect_date_of_birth ?? defaults.registration.collect_date_of_birth, }, }; } const InstancePolicyUpdateSchema = InstanceConfigUpdateRequest.shape.policy.unwrap().unwrap(); const InstancePolicyServiceUpdateSchema = InstancePolicyUpdateSchema.shape.services.unwrap().unwrap(); const InstancePolicyPhoneGateUpdateSchema = InstancePolicyUpdateSchema.shape.deferred_phone_gate.unwrap().unwrap(); const StoredSnowflakeStringSchema = z .string() .refine((value) => value.length <= 19 && !/\D/.test(value) && SnowflakeType.safeParse(value).success); const StoredInstancePolicySchema = z.object({ single_community_enabled: InstancePolicyUpdateSchema.shape.single_community_enabled.default(false), single_community_guild_id: StoredSnowflakeStringSchema.nullable().default(null), direct_messages_disabled: InstancePolicyUpdateSchema.shape.direct_messages_disabled.default(false), direct_messages_locked: z.boolean().default(false), premium_mode: InstancePolicyUpdateSchema.shape.premium_mode.default('everyone'), gif_enabled: InstancePolicyServiceUpdateSchema.shape.gif_enabled.default(null), youtube_enabled: InstancePolicyServiceUpdateSchema.shape.youtube_enabled.default(null), bluesky_enabled: InstancePolicyServiceUpdateSchema.shape.bluesky_enabled.default(null), deferred_phone_gate_enabled: InstancePolicyPhoneGateUpdateSchema.shape.enabled.default(false), deferred_phone_gate_window_hours: InstancePolicyPhoneGateUpdateSchema.shape.window_hours.default(6), deferred_phone_gate_member_threshold: InstancePolicyPhoneGateUpdateSchema.shape.member_threshold.default(50), }) satisfies z.ZodType; function decodeInstancePolicyConfig(value: unknown): InstancePolicyConfig { return validateStoredConfig(StoredInstancePolicySchema, value, 'instance policy'); } function parseStoredInstancePolicyConfig(raw: string | null): InstancePolicyConfig { return salvageStoredConfig( StoredInstancePolicySchema, readStoredConfigValue(raw, 'instance policy'), 'instance policy', ); } const IntegrationUpdateSchema = InstanceConfigUpdateRequest.shape.integrations.unwrap().unwrap(); const EmailIntegrationUpdateSchema = IntegrationUpdateSchema.shape.email.unwrap().unwrap(); const SmtpIntegrationUpdateSchema = EmailIntegrationUpdateSchema.shape.smtp.unwrap().unwrap(); const StoredIntegrationStringSchema = z .string() .trim() .nullable() .default(null) .transform((value) => value || null); const StoredNullableBooleanSchema = z.boolean().nullable().default(null); const StoredBlueskyKeysSchema = z .array(z.object({kid: z.string().trim().min(1), private_key: StoredIntegrationStringSchema})) .default([]) .refine((keys) => { const activeIds = new Set(); for (const key of keys) { if (key.private_key === null) continue; if (activeIds.has(key.kid)) return false; activeIds.add(key.kid); } return true; }); const StoredInstanceIntegrationsSchema = z.object({ gif: z.object({klipy_api_key: StoredIntegrationStringSchema}).prefault({}), youtube: z.object({api_key: StoredIntegrationStringSchema}).prefault({}), captcha: z .object({ provider: InstanceCaptchaProviderSchema.nullable().default(null), hcaptcha_site_key: StoredIntegrationStringSchema, hcaptcha_secret_key: StoredIntegrationStringSchema, turnstile_site_key: StoredIntegrationStringSchema, turnstile_secret_key: StoredIntegrationStringSchema, }) .prefault({}), email: z .object({ enabled: StoredNullableBooleanSchema, provider: EmailIntegrationUpdateSchema.shape.provider.default(null), from_email: StoredIntegrationStringSchema, from_name: StoredIntegrationStringSchema, smtp: z .object({ host: StoredIntegrationStringSchema, port: SmtpIntegrationUpdateSchema.shape.port.default(null), username: StoredIntegrationStringSchema, password: StoredIntegrationStringSchema, secure: StoredNullableBooleanSchema, }) .prefault({}), disable_new_ip_authorization: StoredNullableBooleanSchema, }) .prefault({}), bluesky: z .object({ enabled: StoredNullableBooleanSchema, client_name: StoredIntegrationStringSchema, client_uri: StoredIntegrationStringSchema, logo_uri: StoredIntegrationStringSchema, tos_uri: StoredIntegrationStringSchema, policy_uri: StoredIntegrationStringSchema, keys: StoredBlueskyKeysSchema, }) .prefault({}), }) satisfies z.ZodType; function decodeInstanceIntegrationsConfig(value: unknown): InstanceIntegrationsConfig { return validateStoredConfig(StoredInstanceIntegrationsSchema, value, 'integrations'); } function parseStoredInstanceIntegrationsConfig(raw: string | null): InstanceIntegrationsConfig { return salvageStoredConfig( StoredInstanceIntegrationsSchema, readStoredConfigValue(raw, 'integrations'), 'integrations', ); } function secretIsSet(value: unknown): boolean { return typeof value === 'string' && value.trim().length > 0; } const AttachmentDecayUpdateSchema = InstanceConfigUpdateRequest.shape.media .unwrap() .unwrap() .shape.attachment_decay.unwrap() .unwrap(); const StoredAttachmentDecaySchema = z .object({ enabled: AttachmentDecayUpdateSchema.shape.enabled.default(null), min_size_mb: AttachmentDecayUpdateSchema.shape.min_size_mb.default(null), max_size_mb: AttachmentDecayUpdateSchema.shape.max_size_mb.default(null), max_eligible_size_mb: AttachmentDecayUpdateSchema.shape.max_eligible_size_mb.default(null), min_lifetime_days: AttachmentDecayUpdateSchema.shape.min_lifetime_days.default(null), max_lifetime_days: AttachmentDecayUpdateSchema.shape.max_lifetime_days.default(null), curve: AttachmentDecayUpdateSchema.shape.curve.default(null), renew_threshold_days: AttachmentDecayUpdateSchema.shape.renew_threshold_days.default(null), renew_window_days: AttachmentDecayUpdateSchema.shape.renew_window_days.default(null), }) .transform(normalizeAttachmentDecayBounds) .refine( (value) => { const minSizeMb = value.min_size_mb ?? DEFAULT_DECAY_CONSTANTS.MIN_MB; const maxSizeMb = computeAttachmentDecayMaxSize(minSizeMb, value.max_size_mb ?? DEFAULT_DECAY_CONSTANTS.MAX_MB); return Number.isFinite(maxSizeMb) && maxSizeMb > minSizeMb; }, {path: ['min_size_mb']}, ); const StoredInstanceMediaSchema = z.object({ attachment_decay: StoredAttachmentDecaySchema.prefault({}), }) satisfies z.ZodType; function normalizeAttachmentDecayBounds(value: InstanceAttachmentDecayConfig): InstanceAttachmentDecayConfig { const minSizeMb = value.min_size_mb; let maxSizeMb = value.max_size_mb; let maxEligibleSizeMb = value.max_eligible_size_mb; const minLifetimeDays = value.min_lifetime_days; let maxLifetimeDays = value.max_lifetime_days; if (minSizeMb !== null && maxSizeMb !== null && maxSizeMb <= minSizeMb) { maxSizeMb = null; } if (maxSizeMb !== null && maxEligibleSizeMb !== null && maxEligibleSizeMb < maxSizeMb) { maxEligibleSizeMb = null; } if (minLifetimeDays !== null && maxLifetimeDays !== null && maxLifetimeDays < minLifetimeDays) { maxLifetimeDays = null; } return { ...value, max_size_mb: maxSizeMb, max_eligible_size_mb: maxEligibleSizeMb, max_lifetime_days: maxLifetimeDays, }; } function computeAttachmentDecayMaxSize(minSizeMb: number, configuredMaxSizeMb: number): number { if (configuredMaxSizeMb > minSizeMb) return configuredMaxSizeMb; const incrementedMin = minSizeMb + 1; const largerMin = incrementedMin > minSizeMb ? incrementedMin : Math.min(Number.MAX_VALUE, minSizeMb + minSizeMb * Number.EPSILON); return Math.max(DEFAULT_DECAY_CONSTANTS.MAX_MB, largerMin); } function decodeInstanceMediaConfig(value: unknown): InstanceMediaConfig { return validateStoredConfig(StoredInstanceMediaSchema, value, 'media'); } function parseStoredInstanceMediaConfig(raw: string | null): InstanceMediaConfig { return decodeInstanceMediaConfig(parseStoredConfigValue(raw, 'media')); } function hasCompleteSmtpConfig(config: APIConfig['email']): boolean { if (config.provider !== 'smtp' || !config.smtp) return false; return Boolean( config.fromEmail.trim() && config.smtp.host.trim() && config.smtp.port && config.smtp.username.trim() && config.smtp.password.trim(), ); } const StoredRegistrationConfigSchema = InstanceRegistrationSchema.extend({ mode: InstanceRegistrationSchema.shape.mode.default(DEFAULT_REGISTRATION_CONFIG.mode), admin_registration_urls_enabled: InstanceRegistrationSchema.shape.admin_registration_urls_enabled.default( DEFAULT_REGISTRATION_CONFIG.admin_registration_urls_enabled, ), }); function decodeRegistrationConfig(value: unknown): InstanceRegistrationConfig { const input = isJsonRecord(value) && !Object.hasOwn(value, 'admin_registration_urls_enabled') && Object.hasOwn(value, 'adminRegistrationUrlsEnabled') ? {...value, admin_registration_urls_enabled: value.adminRegistrationUrlsEnabled} : value; return validateStoredConfig(StoredRegistrationConfigSchema, input, 'registration'); } function parseStoredRegistrationConfig(raw: string | null): InstanceRegistrationConfig { return decodeRegistrationConfig(parseStoredConfigValue(raw, 'registration')); } const RegistrationUrlSchema = InstanceConfigResponse.shape.registration.shape.urls.element; const PendingRegistrationSchema = InstanceConfigResponse.shape.registration.shape.pending_registrations.element; const StoredRegistrationTimestampSchema = z.iso .datetime({offset: true}) .refine((value) => value === value.trim() && Number.isFinite(Date.parse(value))) .transform((value): string => new Date(value).toISOString()) .pipe(RegistrationUrlSchema.shape.created_at); const StoredNullableStringSchema = z .string() .nullable() .default(null) .transform((value) => (value === null || value.trim().length === 0 ? null : value)); const StoredRegistrationUrlIdSchema = z .string() .min(1) .max(128) .refine((value) => normalizeString(value) === value); const StoredRegistrationUrlSchema = RegistrationUrlSchema.extend({ id: StoredRegistrationUrlIdSchema, label: StoredNullableStringSchema, code_hash: z .string() .length(64) .regex(/^[a-f0-9]{64}$/), created_by_user_id: StoredSnowflakeStringSchema, created_at: StoredRegistrationTimestampSchema, expires_at: StoredRegistrationTimestampSchema.nullable().default(null), max_uses: RegistrationUrlSchema.shape.max_uses.default(null), use_count: RegistrationUrlSchema.shape.use_count.default(0), revoked_at: StoredRegistrationTimestampSchema.nullable().default(null), approval_required: RegistrationUrlSchema.shape.approval_required.default(false), last_used_at: StoredRegistrationTimestampSchema.nullable().default(null), last_used_by_user_id: StoredNullableStringSchema.pipe(StoredSnowflakeStringSchema.nullable()), }) satisfies z.ZodType; const StoredPendingRegistrationSchema = PendingRegistrationSchema.extend({ user_id: StoredSnowflakeStringSchema, discriminator: PendingRegistrationSchema.shape.discriminator.default(0), global_name: StoredNullableStringSchema, email: StoredNullableStringSchema, requested_at: StoredRegistrationTimestampSchema, registration_url_id: StoredNullableStringSchema.pipe(StoredRegistrationUrlIdSchema.nullable()), client_ip: StoredNullableStringSchema, }) satisfies z.ZodType; function isRegistrationUrlUsable(registrationUrl: InstanceRegistrationUrl, now: Date): boolean { if (registrationUrl.revoked_at) return false; if (registrationUrl.expires_at && Date.parse(registrationUrl.expires_at) <= now.getTime()) return false; if (registrationUrl.max_uses !== null && registrationUrl.use_count >= registrationUrl.max_uses) return false; return true; } function redactRegistrationUrl(registrationUrl: InstanceRegistrationUrl): InstanceRegistrationUrlPublic { const {code_hash: _codeHash, ...redacted} = registrationUrl; return redacted; } interface InstanceSsoFlags { enabled: boolean; enforced: boolean; autoProvision: boolean; } export interface InstanceSsoConfig extends InstanceSsoFlags { displayName: string | null; issuer: string | null; authorizationUrl: string | null; tokenUrl: string | null; userInfoUrl: string | null; jwksUrl: string | null; clientId: string | null; clientSecret?: string | null; clientSecretSet?: boolean; scope: string | null; allowedEmailDomains: Array; redirectUri: string | null; } const MAX_SSO_ALLOWED_DOMAINS = 100; function readStoredSsoBoolean( configs: ReadonlyMap, key: 'sso_enabled' | 'sso_enforced' | 'sso_auto_provision', fallback: boolean, options?: {invalidFallback?: boolean; log?: boolean}, ): boolean { const value = configs.get(key); if (value === undefined) return fallback; if (value === 'true') return true; if (value === 'false') return false; if (options?.log) { Logger.warn({key}, 'Invalid stored SSO flag, falling back to its default'); } return options?.invalidFallback ?? fallback; } function readStoredSsoFlags(configs: ReadonlyMap, log = false): InstanceSsoFlags { const enabled = readStoredSsoBoolean(configs, 'sso_enabled', false, {log}); return { enabled, enforced: readStoredSsoBoolean(configs, 'sso_enforced', enabled, {invalidFallback: false, log}), autoProvision: readStoredSsoBoolean(configs, 'sso_auto_provision', true, {log}), }; } function parseStoredSsoAllowedEmailDomains(raw: string | undefined, log = false): Array { if (raw === undefined || raw.trim().length === 0) return []; let value: unknown; try { value = JSON.parse(raw); } catch { value = null; } const entries: ReadonlyArray = Array.isArray(value) ? value : raw.split(','); const domains = new Set(); const unusable = new Set(); for (const entry of entries) { try { for (const domain of normalizeSsoAllowedEmailDomains([entry])) { domains.add(domain); } } catch { const text = String(entry).trim(); if (text.length > 0) unusable.add(text); } } if (domains.size === 0 && unusable.size > 0) { if (log) { Logger.error( {unusable: unusable.size}, 'Every stored SSO allowed email domain is invalid, keeping them so the allowlist still matches nothing', ); } return Array.from(unusable).slice(0, MAX_SSO_ALLOWED_DOMAINS); } if (log && unusable.size > 0) { Logger.warn({dropped: unusable.size}, 'Dropped invalid stored SSO allowed email domains'); } if (log && domains.size > MAX_SSO_ALLOWED_DOMAINS) { Logger.warn( {dropped: domains.size - MAX_SSO_ALLOWED_DOMAINS}, 'Truncated the stored SSO allowed email domain list to its maximum length', ); } return Array.from(domains).slice(0, MAX_SSO_ALLOWED_DOMAINS); } export class InstanceConfigRepository { private readonly kvClient: IKVProvider | null; private configCache: InstanceConfigCache; private effectiveBlueskyConfig: BlueskyOAuthConfig | null = null; private effectiveBlueskyConfigSource: string | null = null; constructor(kvClient: IKVProvider | null = null) { this.kvClient = kvClient; this.configCache = this.createConfigCache(); } private createConfigCache(previousShutdown?: Promise): InstanceConfigCache { return new InstanceConfigCache( { provider: this.kvClient, channel: INSTANCE_CONFIG_REFRESH_CHANNEL, load: () => this.fetchAllConfigsFromDatabase(), onRefresh: (snapshot) => this.syncConfigCaches(snapshot), }, previousShutdown, ); } async initialize(): Promise { const cache = this.configCache; const snapshot = await cache.getSnapshot(); cache.assertActive(); if (snapshot !== null) return; const configs = await this.fetchAllConfigsFromDatabase(); cache.assertActive(); this.syncConfigCaches(configs); } async getConfig(key: string): Promise { const cache = this.configCache; const snapshot = await cache.getSnapshot(); cache.assertActive(); return snapshot === null ? this.fetchConfigFromDatabase(key) : (snapshot.get(key) ?? null); } async getAllConfigs(): Promise> { const cache = this.configCache; const snapshot = await cache.getSnapshot(); cache.assertActive(); return snapshot === null ? this.fetchAllConfigsFromDatabase() : new Map(snapshot); } setConfig(key: string, value: string): Promise { return this.setConfigs([[key, value]]); } private async setConfigs(entries: Array<[string, string]>): Promise { if (entries.length === 0) return; const cache = this.configCache; cache.assertActive(); const results = await Promise.allSettled( entries.map(async ([key, value]) => { await this.writeConfig(key, value); cache.update(key, value); }), ); const errors: Array = results.flatMap((result) => (result.status === 'rejected' ? [result.reason] : [])); if (results.some((result) => result.status === 'fulfilled')) { try { await this.publishRefresh(cache.sourceId); } catch (error) { errors.push(error); } } if (errors.length === 1) throw errors[0]; if (errors.length > 1) throw new AggregateError(errors, 'Failed to write or publish instance config'); } private async writeConfig(key: string, value: string): Promise { await upsertOne( InstanceConfiguration.upsertAll({ key, value, updated_at: new Date(), }), ); } private async fetchConfigFromDatabase(key: string): Promise { const row = await fetchOne(FETCH_CONFIG_QUERY, {key}); return row?.value ?? null; } private async fetchAllConfigsFromDatabase(): Promise> { const rows = await fetchMany(FETCH_ALL_CONFIG_QUERY, {}); const configs = new Map(); for (const row of rows) { if (row.value != null) { configs.set(row.key, row.value); } } return configs; } private syncConfigCaches(snapshot: ReadonlyMap): void { checkStoredConfig('gateway rollout', () => parseStoredGatewayRolloutConfig(snapshot.get(GATEWAY_ROLLOUT_CONFIG_KEY) ?? null), ); parseStoredVoiceNoiseSuppressionConfig(snapshot.get(VOICE_NOISE_SUPPRESSION_CONFIG_KEY) ?? null); parseStoredScreenShareDeliveryConfig(snapshot.get(SCREEN_SHARE_DELIVERY_CONFIG_KEY) ?? null); parseStoredExperimentDeliveryConfig(snapshot.get(EXPERIMENT_DELIVERY_CONFIG_KEY) ?? null); const policy = parseStoredInstancePolicyConfig(snapshot.get(INSTANCE_POLICY_CONFIG_KEY) ?? null); checkStoredConfig('registration', () => parseStoredRegistrationConfig(snapshot.get(REGISTRATION_CONFIG_KEY) ?? null), ); checkStoredConfig('registration URLs', () => parseStoredCollection( StoredRegistrationUrlSchema, snapshot.get(REGISTRATION_URLS_KEY) ?? null, 'registration URLs', ), ); checkStoredConfig('pending registrations', () => parseStoredCollection( StoredPendingRegistrationSchema, snapshot.get(REGISTRATION_PENDING_APPROVALS_KEY) ?? null, 'pending registrations', ), ); checkStoredConfig('SSO flags', () => readStoredSsoFlags(snapshot, true)); checkStoredConfig('SSO allowed domains', () => parseStoredSsoAllowedEmailDomains(snapshot.get('sso_allowed_domains'), true), ); checkStoredConfig('integrations', () => parseStoredInstanceIntegrationsConfig(snapshot.get(INSTANCE_INTEGRATIONS_CONFIG_KEY) ?? null), ); checkStoredConfig('media', () => parseStoredInstanceMediaConfig(snapshot.get(INSTANCE_MEDIA_CONFIG_KEY) ?? null)); const appPublic = parseStoredAppPublicConfig(snapshot.get(APP_PUBLIC_CONFIG_KEY) ?? null); setCachedDeferredPhoneGateEnabled(resolveDeferredPhoneGateEnabled(policy)); setCachedDateOfBirthCollection(appPublic.registration.collect_date_of_birth); } private async publishRefresh(sourceId: string): Promise { if (!this.kvClient) { return; } await this.kvClient.publish( INSTANCE_CONFIG_REFRESH_CHANNEL, JSON.stringify({source_id: sourceId, type: 'refresh'}), ); } shutdown(): Promise { return this.configCache.shutdown(); } clearCacheForTesting(): void { const shutdown = this.shutdown(); this.configCache = this.createConfigCache(shutdown); void shutdown.catch((error) => { Logger.error({error}, 'Failed to clear instance config cache'); }); } async isAdminBootstrapped(): Promise { return (await this.getConfig(ADMIN_BOOTSTRAP_KEY)) === 'true'; } async markAdminBootstrapped(): Promise { await this.setConfig(ADMIN_BOOTSTRAP_KEY, 'true'); } async getGatewayRolloutConfig(): Promise { const raw = await this.getConfig(GATEWAY_ROLLOUT_CONFIG_KEY); return parseStoredGatewayRolloutConfig(raw); } async setGatewayRolloutConfig(config: GatewayRolloutConfig): Promise { await this.setConfig(GATEWAY_ROLLOUT_CONFIG_KEY, JSON.stringify(decodeGatewayRolloutConfig(config))); } async getVoiceNoiseSuppressionConfig(): Promise { const raw = await this.getConfig(VOICE_NOISE_SUPPRESSION_CONFIG_KEY); return parseStoredVoiceNoiseSuppressionConfig(raw); } async setVoiceNoiseSuppressionConfig(config: VoiceNoiseSuppressionConfig): Promise { const validated = validateStoredConfig(VoiceNoiseSuppressionConfigSchema, config, 'voice noise suppression'); await this.setConfig(VOICE_NOISE_SUPPRESSION_CONFIG_KEY, JSON.stringify(validated)); } async getScreenShareDeliveryConfig(): Promise { const raw = await this.getConfig(SCREEN_SHARE_DELIVERY_CONFIG_KEY); return parseStoredScreenShareDeliveryConfig(raw); } async setScreenShareDeliveryConfig(config: ScreenShareDeliveryConfig): Promise { const validated = validateStoredConfig(ScreenShareDeliveryConfigSchema, config, 'screen share delivery'); await this.setConfig(SCREEN_SHARE_DELIVERY_CONFIG_KEY, JSON.stringify(validated)); } async getExperimentDeliveryConfig(): Promise { const raw = await this.getConfig(EXPERIMENT_DELIVERY_CONFIG_KEY); return parseStoredExperimentDeliveryConfig(raw); } async setExperimentDeliveryConfig(config: ExperimentDeliveryConfig): Promise { const validated = validateStoredConfig(ExperimentDeliveryConfigSchema, config, 'experiment delivery'); await this.setConfig(EXPERIMENT_DELIVERY_CONFIG_KEY, JSON.stringify(validated)); } async readLimitConfigInputs(): Promise { const cache = this.configCache; cache.assertActive(); const rows = await fetchMany(FETCH_LIMIT_CONFIG_INPUTS_QUERY, { keys: [LIMIT_CONFIG_KEY, INSTANCE_POLICY_CONFIG_KEY], }); cache.assertActive(); const values = new Map(rows.map((row) => [row.key, row.value])); const policyRaw = values.get(INSTANCE_POLICY_CONFIG_KEY) ?? null; const policy = parseStoredInstancePolicyConfig(policyRaw); return { config: parseStoredLimitConfig(values.get(LIMIT_CONFIG_KEY) ?? null), premiumMode: policy.premium_mode, }; } async setLimitConfig(config: LimitConfigSnapshot): Promise { await this.setConfig(LIMIT_CONFIG_KEY, JSON.stringify(config)); } async getAppPublicConfig(): Promise { const raw = await this.getConfig(APP_PUBLIC_CONFIG_KEY); const config = parseStoredAppPublicConfig(raw); setCachedDateOfBirthCollection(config.registration.collect_date_of_birth); return config; } async setAppPublicConfig(config: { branding?: Partial; setup?: Partial; legal?: Partial; registration?: Partial; }): Promise { const current = await this.getAppPublicConfig(); const next = decodeAppPublicConfig({ branding: { ...current.branding, ...(config.branding ?? {}), }, setup: { ...current.setup, ...(config.setup ?? {}), }, legal: { ...current.legal, ...(config.legal ?? {}), }, registration: { ...current.registration, ...(config.registration ?? {}), }, }); await this.setConfig(APP_PUBLIC_CONFIG_KEY, JSON.stringify(next)); setCachedDateOfBirthCollection(next.registration.collect_date_of_birth); return next; } async getInstancePolicyConfig(): Promise { const raw = await this.getConfig(INSTANCE_POLICY_CONFIG_KEY); const policy = parseStoredInstancePolicyConfig(raw); setCachedDeferredPhoneGateEnabled(resolveDeferredPhoneGateEnabled(policy)); return policy; } async readStoredInstancePolicyConfig(): Promise { const cache = this.configCache; cache.assertActive(); const raw = await this.fetchConfigFromDatabase(INSTANCE_POLICY_CONFIG_KEY); cache.assertActive(); return parseStoredInstancePolicyConfig(raw); } async setInstancePolicyConfig(config: Partial): Promise { const current = await this.readStoredInstancePolicyConfig(); const next = decodeInstancePolicyConfig({...current, ...config}); await this.setConfig(INSTANCE_POLICY_CONFIG_KEY, JSON.stringify(next)); setCachedDeferredPhoneGateEnabled(resolveDeferredPhoneGateEnabled(next)); return next; } async getInstanceIntegrationsConfig(): Promise { const raw = await this.getConfig(INSTANCE_INTEGRATIONS_CONFIG_KEY); return parseStoredInstanceIntegrationsConfig(raw); } async setInstanceIntegrationsConfig(config: InstanceIntegrationsConfigPatch): Promise { const current = await this.getInstanceIntegrationsConfig(); const next = decodeInstanceIntegrationsConfig({ gif: { ...current.gif, ...(config.gif ?? {}), }, youtube: { ...current.youtube, ...(config.youtube ?? {}), }, captcha: { ...current.captcha, ...(config.captcha ?? {}), }, email: { ...current.email, ...(config.email ?? {}), smtp: { ...current.email.smtp, ...(config.email?.smtp ?? {}), }, }, bluesky: { ...current.bluesky, ...(config.bluesky ?? {}), keys: config.bluesky?.keys ?? current.bluesky.keys, }, }); await this.setConfig(INSTANCE_INTEGRATIONS_CONFIG_KEY, JSON.stringify(next)); return next; } async getInstanceMediaConfig(): Promise { const raw = await this.getConfig(INSTANCE_MEDIA_CONFIG_KEY); return parseStoredInstanceMediaConfig(raw); } async setInstanceMediaConfig(config: InstanceMediaConfigPatch): Promise { const current = await this.getInstanceMediaConfig(); const next = decodeInstanceMediaConfig({ attachment_decay: { ...current.attachment_decay, ...(config.attachment_decay ?? {}), }, }); await this.setConfig(INSTANCE_MEDIA_CONFIG_KEY, JSON.stringify(next)); return next; } async getEffectiveAttachmentDecayConfig(): Promise { const media = await this.getInstanceMediaConfig(); const attachmentDecay = media.attachment_decay; const minSizeMb = attachmentDecay.min_size_mb ?? DEFAULT_DECAY_CONSTANTS.MIN_MB; const configuredMaxSizeMb = attachmentDecay.max_size_mb ?? DEFAULT_DECAY_CONSTANTS.MAX_MB; const maxSizeMb = computeAttachmentDecayMaxSize(minSizeMb, configuredMaxSizeMb); const configuredMaxEligibleSizeMb = attachmentDecay.max_eligible_size_mb ?? DEFAULT_DECAY_CONSTANTS.PLAN_MB; const maxEligibleSizeMb = Math.max(maxSizeMb, configuredMaxEligibleSizeMb); const minLifetimeDays = attachmentDecay.min_lifetime_days ?? DEFAULT_DECAY_CONSTANTS.MIN_DAYS; const configuredMaxLifetimeDays = attachmentDecay.max_lifetime_days ?? DEFAULT_DECAY_CONSTANTS.MAX_DAYS; const maxLifetimeDays = configuredMaxLifetimeDays >= minLifetimeDays ? configuredMaxLifetimeDays : Math.max(DEFAULT_DECAY_CONSTANTS.MAX_DAYS, minLifetimeDays); return { enabled: attachmentDecay.enabled ?? Config.attachmentDecayEnabled, min_size_mb: minSizeMb, max_size_mb: maxSizeMb, max_eligible_size_mb: maxEligibleSizeMb, min_lifetime_days: minLifetimeDays, max_lifetime_days: maxLifetimeDays, curve: attachmentDecay.curve ?? DEFAULT_DECAY_CONSTANTS.CURVE, renew_threshold_days: attachmentDecay.renew_threshold_days ?? DEFAULT_RENEWAL_CONSTANTS.RENEW_THRESHOLD_DAYS, renew_window_days: attachmentDecay.renew_window_days ?? DEFAULT_RENEWAL_CONSTANTS.RENEW_WINDOW_DAYS, }; } async isAttachmentDecayEnabled(): Promise { return (await this.getEffectiveAttachmentDecayConfig()).enabled; } async getInstanceMediaAdminConfig(): Promise { const [media, attachmentDecay] = await Promise.all([ this.getInstanceMediaConfig(), this.getEffectiveAttachmentDecayConfig(), ]); return { attachment_decay: { ...media.attachment_decay, effective: attachmentDecay, }, }; } async getEffectiveGifConfig(): Promise { const integrations = await this.getInstanceIntegrationsConfig(); const klipyApiKey = integrations.gif.klipy_api_key ?? normalizeOptionalString(Config.klipy.apiKey); return { klipy_api_key: klipyApiKey, active_api_key: klipyApiKey, available: Boolean(klipyApiKey), }; } async getEffectiveYoutubeApiKey(): Promise { const integrations = await this.getInstanceIntegrationsConfig(); return integrations.youtube.api_key ?? normalizeOptionalString(Config.youtube.apiKey); } async getEffectiveCaptchaConfig(): Promise { const integrations = await this.getInstanceIntegrationsConfig(); const provider = integrations.captcha.provider ?? (Config.captcha.enabled ? Config.captcha.provider : 'none'); const hcaptchaSiteKey = integrations.captcha.hcaptcha_site_key ?? normalizeOptionalString(Config.captcha.hcaptcha?.siteKey); const hcaptchaSecretKey = integrations.captcha.hcaptcha_secret_key ?? normalizeOptionalString(Config.captcha.hcaptcha?.secretKey); const turnstileSiteKey = integrations.captcha.turnstile_site_key ?? normalizeOptionalString(Config.captcha.turnstile?.siteKey); const turnstileSecretKey = integrations.captcha.turnstile_secret_key ?? normalizeOptionalString(Config.captcha.turnstile?.secretKey); const providerReady = provider === 'hcaptcha' ? Boolean(hcaptchaSiteKey && hcaptchaSecretKey) : provider === 'turnstile' ? Boolean(turnstileSiteKey && turnstileSecretKey) : false; return { enabled: providerReady, provider: providerReady ? provider : 'none', hcaptcha_site_key: hcaptchaSiteKey, hcaptcha_secret_key: hcaptchaSecretKey, turnstile_site_key: turnstileSiteKey, turnstile_secret_key: turnstileSecretKey, }; } async getEffectiveEmailConfig(): Promise { const integrations = await this.getInstanceIntegrationsConfig(); const provider = integrations.email.provider ?? Config.email.provider; const fromEmail = integrations.email.from_email ?? Config.email.fromEmail; const fromName = integrations.email.from_name ?? Config.email.fromName; const smtp = provider === 'smtp' ? { host: integrations.email.smtp.host ?? Config.email.smtp?.host ?? '', port: integrations.email.smtp.port ?? Config.email.smtp?.port ?? 587, username: integrations.email.smtp.username ?? Config.email.smtp?.username ?? '', password: integrations.email.smtp.password ?? Config.email.smtp?.password ?? '', secure: integrations.email.smtp.secure ?? Config.email.smtp?.secure ?? true, } : undefined; const next: APIConfig['email'] = { ...Config.email, enabled: integrations.email.enabled ?? Config.email.enabled, provider, fromEmail, fromName, smtp, }; return { ...next, enabled: next.enabled && hasCompleteSmtpConfig(next), }; } async isEmailEnabled(): Promise { return (await this.getEffectiveEmailConfig()).enabled; } async getEffectiveBlueskyConfig(): Promise { const raw = await this.getConfig(INSTANCE_INTEGRATIONS_CONFIG_KEY); const memoized = this.effectiveBlueskyConfig; if (memoized && this.effectiveBlueskyConfigSource === raw) { return memoized; } const integrations = parseStoredInstanceIntegrationsConfig(raw); const runtimeKeys = integrations.bluesky.keys.flatMap((key): Array => { if (!key.private_key) return []; return [{kid: key.kid, private_key: key.private_key}]; }); const keys = runtimeKeys.length > 0 ? runtimeKeys : Config.auth.bluesky.keys; const enabled = (integrations.bluesky.enabled ?? Config.auth.bluesky.enabled) && keys.length > 0; const effective: BlueskyOAuthConfig = { ...Config.auth.bluesky, enabled, client_name: integrations.bluesky.client_name ?? Config.auth.bluesky.client_name, client_uri: integrations.bluesky.client_uri ?? Config.auth.bluesky.client_uri, logo_uri: integrations.bluesky.logo_uri ?? Config.auth.bluesky.logo_uri, tos_uri: integrations.bluesky.tos_uri ?? Config.auth.bluesky.tos_uri, policy_uri: integrations.bluesky.policy_uri ?? Config.auth.bluesky.policy_uri, keys, }; this.effectiveBlueskyConfigSource = raw; this.effectiveBlueskyConfig = effective; return effective; } async getInstanceIntegrationsAdminConfig(): Promise { const [integrations, gif, youtubeApiKey, captcha, email, bluesky] = await Promise.all([ this.getInstanceIntegrationsConfig(), this.getEffectiveGifConfig(), this.getEffectiveYoutubeApiKey(), this.getEffectiveCaptchaConfig(), this.getEffectiveEmailConfig(), this.getEffectiveBlueskyConfig(), ]); return { gif: { klipy_api_key_set: secretIsSet(integrations.gif.klipy_api_key) || secretIsSet(Config.klipy.apiKey), effective_available: gif.available, }, youtube: { api_key_set: secretIsSet(integrations.youtube.api_key) || secretIsSet(Config.youtube.apiKey), effective_available: Boolean(youtubeApiKey), }, captcha: { provider: integrations.captcha.provider, effective_provider: captcha.provider, hcaptcha_site_key: captcha.hcaptcha_site_key, hcaptcha_secret_key_set: secretIsSet(integrations.captcha.hcaptcha_secret_key) || secretIsSet(Config.captcha.hcaptcha?.secretKey), turnstile_site_key: captcha.turnstile_site_key, turnstile_secret_key_set: secretIsSet(integrations.captcha.turnstile_secret_key) || secretIsSet(Config.captcha.turnstile?.secretKey), effective_enabled: captcha.enabled, }, email: { enabled: integrations.email.enabled, effective_enabled: email.enabled, provider: integrations.email.provider, effective_provider: email.provider, from_email: email.fromEmail || null, from_name: email.fromName || null, smtp: { host: email.smtp?.host || null, port: email.smtp?.port ?? null, username: email.smtp?.username || null, password_set: secretIsSet(integrations.email.smtp.password) || secretIsSet(Config.email.smtp?.password), secure: email.smtp?.secure ?? null, }, disable_new_ip_authorization: integrations.email.disable_new_ip_authorization ?? false, effective_disable_new_ip_authorization: integrations.email.disable_new_ip_authorization || !email.enabled, }, bluesky: { enabled: integrations.bluesky.enabled, effective_enabled: bluesky.enabled, client_name: bluesky.client_name || null, client_uri: bluesky.client_uri || null, logo_uri: bluesky.logo_uri || null, tos_uri: bluesky.tos_uri || null, policy_uri: bluesky.policy_uri || null, key_count: bluesky.keys.length, }, }; } async getInstanceCommunityPublicConfig(): Promise { const policy = await this.getInstancePolicyConfig(); return { single_community: policy.single_community_enabled, single_community_guild_id: policy.single_community_enabled ? policy.single_community_guild_id : null, direct_messages_disabled: policy.direct_messages_disabled, }; } async getResolvedServicesConfig(): Promise { const [policy, gif, youtubeApiKey, bluesky] = await Promise.all([ this.getInstancePolicyConfig(), this.getEffectiveGifConfig(), this.getEffectiveYoutubeApiKey(), this.getEffectiveBlueskyConfig(), ]); return { gif_enabled: policy.gif_enabled ?? gif.available, youtube_enabled: policy.youtube_enabled ?? Boolean(youtubeApiKey), bluesky_enabled: policy.bluesky_enabled ?? bluesky.enabled, }; } async getRegistrationConfig(): Promise { const raw = await this.getConfig(REGISTRATION_CONFIG_KEY); return parseStoredRegistrationConfig(raw); } async setRegistrationConfig(config: Partial): Promise { const current = await this.getRegistrationConfig(); const next = decodeRegistrationConfig({ mode: config.mode ?? current.mode, admin_registration_urls_enabled: config.admin_registration_urls_enabled ?? current.admin_registration_urls_enabled, }); await this.setConfig(REGISTRATION_CONFIG_KEY, JSON.stringify(next)); return next; } async getRegistrationPublicConfig(): Promise { return this.getRegistrationConfig(); } async getRegistrationUrls(): Promise> { const raw = await this.getConfig(REGISTRATION_URLS_KEY); return parseStoredCollection(StoredRegistrationUrlSchema, raw, 'registration URLs'); } async getRegistrationUrlsForAdmin(): Promise> { return (await this.getRegistrationUrls()) .toSorted((a, b) => Date.parse(b.created_at) - Date.parse(a.created_at)) .map(redactRegistrationUrl); } async createRegistrationUrl(params: { label: string | null; createdByUserId: string; expiresAt: Date | null; maxUses: number | null; approvalRequired: boolean; }): Promise<{registrationUrl: InstanceRegistrationUrlPublic; code: string}> { const id = crypto.randomUUID(); const code = id; const registrationUrl: InstanceRegistrationUrl = { id, label: params.label, code_hash: this.hashRegistrationUrlCode(code), created_by_user_id: params.createdByUserId, created_at: new Date().toISOString(), expires_at: params.expiresAt?.toISOString() ?? null, max_uses: params.maxUses, use_count: 0, revoked_at: null, approval_required: params.approvalRequired, last_used_at: null, last_used_by_user_id: null, }; const registrationUrls = await this.getRegistrationUrls(); await this.setRegistrationUrls([registrationUrl, ...registrationUrls]); return {registrationUrl: redactRegistrationUrl(registrationUrl), code}; } async revokeRegistrationUrl(id: string): Promise { const now = new Date().toISOString(); const registrationUrls = await this.getRegistrationUrls(); await this.setRegistrationUrls( registrationUrls.map((registrationUrl) => registrationUrl.id === id && !registrationUrl.revoked_at ? {...registrationUrl, revoked_at: now} : registrationUrl, ), ); } async resolveRegistrationUrlCode(code: string): Promise { const normalizedCode = code.trim(); if (!normalizedCode) return null; const hash = this.hashRegistrationUrlCode(normalizedCode); const now = new Date(); const registrationUrls = await this.getRegistrationUrls(); return ( registrationUrls.find( (registrationUrl) => (registrationUrl.id === normalizedCode || registrationUrl.code_hash === hash) && isRegistrationUrlUsable(registrationUrl, now), ) ?? null ); } async recordRegistrationUrlUse(id: string, userId: string): Promise { const now = new Date().toISOString(); const registrationUrls = await this.getRegistrationUrls(); await this.setRegistrationUrls( registrationUrls.map((registrationUrl) => registrationUrl.id === id ? { ...registrationUrl, use_count: registrationUrl.use_count + 1, last_used_at: now, last_used_by_user_id: userId, } : registrationUrl, ), ); } async getPendingRegistrations(): Promise> { const raw = await this.getConfig(REGISTRATION_PENDING_APPROVALS_KEY); return parseStoredCollection(StoredPendingRegistrationSchema, raw, 'pending registrations').toSorted( (a, b) => Date.parse(a.requested_at) - Date.parse(b.requested_at), ); } async addPendingRegistration(pendingRegistration: InstancePendingRegistration): Promise { const pendingRegistrations = await this.getPendingRegistrations(); const next = [ pendingRegistration, ...pendingRegistrations.filter((entry) => entry.user_id !== pendingRegistration.user_id), ]; await this.setPendingRegistrations(next); } async removePendingRegistration(userId: string): Promise { const pendingRegistrations = await this.getPendingRegistrations(); await this.setPendingRegistrations(pendingRegistrations.filter((entry) => entry.user_id !== userId)); } async getSsoConfig(options?: {includeSecret?: boolean}): Promise { const configs = await this.getAllConfigs(); const flags = readStoredSsoFlags(configs); const read = (key: string): string | null => { const v = configs.get(key); if (!v) return null; const trimmed = v.trim(); return trimmed.length === 0 ? null : trimmed; }; const allowedDomains = parseStoredSsoAllowedEmailDomains(configs.get('sso_allowed_domains')); const clientSecret = read('sso_client_secret'); return { ...flags, displayName: read('sso_display_name'), issuer: read('sso_issuer'), authorizationUrl: read('sso_authorization_url'), tokenUrl: read('sso_token_url'), userInfoUrl: read('sso_userinfo_url'), jwksUrl: read('sso_jwks_url'), clientId: read('sso_client_id'), clientSecret: options?.includeSecret ? clientSecret : undefined, clientSecretSet: Boolean(clientSecret), scope: read('sso_scope'), allowedEmailDomains: allowedDomains, redirectUri: null, }; } async setSsoConfig(config: Partial): Promise { const current = await this.getSsoConfig({includeSecret: true}); const definedConfig = Object.fromEntries( Object.entries(config).filter(([, value]) => value !== undefined), ) as Partial; const next: InstanceSsoConfig = { ...current, ...definedConfig, clientSecret: config.clientSecret !== undefined ? config.clientSecret : current.clientSecret, }; if (config.enabled === true && config.enforced === undefined && !current.enabled) { next.enforced = true; } let allowedEmailDomains: Array; try { allowedEmailDomains = normalizeSsoAllowedEmailDomains(next.allowedEmailDomains); } catch (error) { if (next.enabled) { throw error; } Logger.warn({error}, 'Clearing invalid SSO allowed domain config while SSO is disabled'); allowedEmailDomains = []; } const entries: Array<[string, string]> = [ ['sso_enabled', next.enabled ? 'true' : 'false'], ['sso_enforced', next.enforced ? 'true' : 'false'], ['sso_display_name', next.displayName ?? ''], ['sso_issuer', next.issuer ?? ''], ['sso_authorization_url', next.authorizationUrl ?? ''], ['sso_token_url', next.tokenUrl ?? ''], ['sso_userinfo_url', next.userInfoUrl ?? ''], ['sso_jwks_url', next.jwksUrl ?? ''], ['sso_client_id', next.clientId ?? ''], ['sso_scope', next.scope ?? ''], ['sso_allowed_domains', JSON.stringify(allowedEmailDomains)], ['sso_auto_provision', next.autoProvision ? 'true' : 'false'], ['sso_redirect_uri', ''], ]; if (config.clientSecret !== undefined) { entries.push(['sso_client_secret', config.clientSecret ?? '']); } await this.setConfigs(entries); return this.getSsoConfig({includeSecret: true}); } private async setRegistrationUrls(registrationUrls: Array): Promise { const validated = validateStoredCollection(StoredRegistrationUrlSchema, registrationUrls, 'registration URLs'); await this.setConfig(REGISTRATION_URLS_KEY, JSON.stringify(validated)); } private async setPendingRegistrations(pendingRegistrations: Array): Promise { const validated = validateStoredCollection( StoredPendingRegistrationSchema, pendingRegistrations, 'pending registrations', ); await this.setConfig(REGISTRATION_PENDING_APPROVALS_KEY, JSON.stringify(validated)); } private hashRegistrationUrlCode(code: string): string { return crypto.createHash('sha256').update(code).digest('hex'); } }