This repository has no description
Something went wrong. Try again.
56 kB · 1646 lines
TypeScript
at main
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647// SPDX-License-Identifier: AGPL-3.0-or-later
import crypto from 'node:crypto';import type {LimitConfigSnapshot} from '@fluxer/limits/src/LimitTypes';import { type GatewayRolloutConfig, GatewayRolloutConfigSchema,} from '@fluxer/schema/src/domains/admin/GatewayRolloutSchemas';import type {IKVProvider, IKVSubscription} from '@pkgs/kv_client/src/IKVProvider';import {Config} from '../Config';import type {APIConfig, BlueskyOAuthConfig, BlueskyOAuthKeyConfig} from '../config/APIConfig';import {sanitizeLimitConfigForInstance} from '../constants/LimitConfig';import {fetchMany, fetchOne, upsertOne} from '../database/CassandraQueryExecution';import type {InstanceConfigurationRow} from '../database/types/InstanceConfigTypes';import {Logger} from '../Logger';import {InstanceConfiguration} from '../Tables';import {DEFAULT_DECAY_CONSTANTS, DEFAULT_RENEWAL_CONSTANTS} from '../utils/AttachmentDecay';import {isJsonRecord, parseJsonArray, parseJsonRecord} from '../utils/JsonBoundaryUtils';import {normalizeSsoAllowedEmailDomains} from './SsoConfigValidation';
const GATEWAY_ROLLOUT_CONFIG_KEY = 'gateway_rollout_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 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';const DEFAULT_GATEWAY_ROLLOUT_CONFIG: GatewayRolloutConfig = { session_rollout_percentage: 100, session_rollout_mode: 'modulo', guild_rollout_percentage: 100, rpc_request_timeout_ms: 10000, max_concurrent_session_starts: 512, max_concurrent_guild_starts: 256, gateway_dispatch_relay_shards: 32, gateway_dispatch_relay_max_queue: 50000, voice_e2ee_scope: 'guild_feature_only', voice_reconciliation_v3_percentage: 100, voice_reconciliation_v3_interval_ms: 2000,};export type InstanceRegistrationMode = 'open' | 'approval' | 'closed';export interface InstanceRegistrationConfig { mode: InstanceRegistrationMode; admin_registration_urls_enabled: boolean;}
export interface InstanceBrandingConfig { product_name: string; icon_url: string | null; symbol_url: string | null; logo_url: string | null; wordmark_url: string | null; favicon_url: string | null; theme_color: string | null;}
interface InstanceAppPublicConfig { branding: InstanceBrandingConfig; setup: { configured: boolean; }; legal: { terms_url: string | null; privacy_url: string | null; }; registration: { collect_date_of_birth: boolean; };}
export type InstancePremiumMode = 'mirror' | 'everyone';
export interface InstancePolicyConfig { single_community_enabled: boolean; single_community_locked: 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;}
interface InstanceCommunityPublicConfig { single_community: boolean; single_community_guild_id: string | null; direct_messages_disabled: boolean;}
interface InstanceServicesPublicConfig { gif_enabled: boolean; youtube_enabled: boolean; bluesky_enabled: boolean;}
export type InstanceGifProvider = 'tenor' | 'klipy';export type InstanceCaptchaProvider = 'hcaptcha' | 'turnstile' | 'none';type InstanceEmailProvider = 'smtp' | 'none';
interface InstanceGifIntegrationConfig { provider: InstanceGifProvider | null; tenor_api_key: string | null; 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;}
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<InstanceBlueskyKeyIntegrationConfig>;}
interface InstanceIntegrationsConfig { gif: InstanceGifIntegrationConfig; youtube: InstanceYoutubeIntegrationConfig; captcha: InstanceCaptchaIntegrationConfig; email: InstanceEmailIntegrationConfig; bluesky: InstanceBlueskyIntegrationConfig;}
export interface InstanceGifEffectiveConfig { provider: InstanceGifProvider; tenor_api_key: string | null; 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: { provider: InstanceGifProvider | null; effective_provider: InstanceGifProvider; tenor_api_key_set: boolean; 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; }; }; 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<InstanceGifIntegrationConfig>; youtube?: Partial<InstanceYoutubeIntegrationConfig>; captcha?: Partial<InstanceCaptchaIntegrationConfig>; email?: Partial<Omit<InstanceEmailIntegrationConfig, 'smtp'>> & { smtp?: Partial<InstanceEmailSmtpIntegrationConfig>; }; bluesky?: Partial<Omit<InstanceBlueskyIntegrationConfig, 'keys'>> & { keys?: Array<Partial<InstanceBlueskyKeyIntegrationConfig>>; };}
interface InstanceMediaConfigPatch { attachment_decay?: Partial<InstanceAttachmentDecayConfig>;}
export interface InstanceRegistrationUrl { id: string; label: string | null; code_hash: string; created_by_user_id: string; created_at: string; expires_at: string | null; max_uses: number | null; use_count: number; revoked_at: string | null; approval_required: boolean; last_used_at: string | null; last_used_by_user_id: string | null;}
type InstanceRegistrationUrlPublic = Omit<InstanceRegistrationUrl, 'code_hash'>;
interface InstancePendingRegistration { user_id: string; username: string; discriminator: number; global_name: string | null; email: string | null; requested_at: string; registration_url_id: string | null; client_ip: string | null;}
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();
function isStringArray(value: unknown): value is Array<string> { return Array.isArray(value) && value.every((entry) => typeof entry === 'string');}
function isRegistrationMode(value: unknown): value is InstanceRegistrationMode { return value === 'open' || value === 'approval' || value === 'closed';}
function normalizeNullableString(value: unknown): string | null { return typeof value === 'string' && value.trim().length > 0 ? value : null;}
function normalizePublicString(value: unknown): string | null { return typeof value === 'string' && value.trim().length > 0 ? value.trim() : null;}
function normalizeOptionalPublicString( value: Record<string, unknown>, key: string, fallback: string | null,): string | null { if (!Object.hasOwn(value, key)) { return fallback; } return normalizePublicString(value[key]);}
function getDefaultAppPublicConfig(): InstanceAppPublicConfig { return { branding: { product_name: Config.instance.branding.productName || 'Fluxer', icon_url: normalizePublicString(Config.instance.branding.iconUrl), symbol_url: normalizePublicString(Config.instance.branding.symbolUrl), logo_url: normalizePublicString(Config.instance.branding.logoUrl), wordmark_url: normalizePublicString(Config.instance.branding.wordmarkUrl), favicon_url: normalizePublicString(Config.instance.branding.faviconUrl), theme_color: normalizePublicString(Config.instance.branding.themeColor), }, setup: { configured: !Config.instance.selfHosted || Config.instance.setup.configured, }, legal: { terms_url: null, privacy_url: null, }, registration: { collect_date_of_birth: !Config.instance.selfHosted, }, };}
function normalizeAppPublicConfig(value: unknown): InstanceAppPublicConfig { const defaults = getDefaultAppPublicConfig(); if (!isJsonRecord(value)) { return defaults; } const branding = isJsonRecord(value.branding) ? value.branding : {}; const setup = isJsonRecord(value.setup) ? value.setup : {}; const legal = isJsonRecord(value.legal) ? value.legal : {}; const registration = isJsonRecord(value.registration) ? value.registration : {}; return { branding: { product_name: normalizePublicString(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), }, setup: { configured: typeof setup.configured === 'boolean' ? 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: typeof registration.collect_date_of_birth === 'boolean' ? registration.collect_date_of_birth : defaults.registration.collect_date_of_birth, }, };}
const DEFAULT_INSTANCE_POLICY_CONFIG: InstancePolicyConfig = { single_community_enabled: false, single_community_locked: false, single_community_guild_id: null, direct_messages_disabled: false, direct_messages_locked: false, premium_mode: 'everyone', gif_enabled: null, youtube_enabled: null, bluesky_enabled: null,};
function isPremiumMode(value: unknown): value is InstancePremiumMode { return value === 'mirror' || value === 'everyone';}
function normalizeNullableBoolean(value: unknown): boolean | null { return typeof value === 'boolean' ? value : null;}
function normalizeInstancePolicyConfig(value: unknown): InstancePolicyConfig { if (!isJsonRecord(value)) { return {...DEFAULT_INSTANCE_POLICY_CONFIG}; } return { single_community_enabled: value.single_community_enabled === true, single_community_locked: value.single_community_locked === true, single_community_guild_id: normalizeNullableString(value.single_community_guild_id), direct_messages_disabled: value.direct_messages_disabled === true, direct_messages_locked: value.direct_messages_locked === true, premium_mode: isPremiumMode(value.premium_mode) ? value.premium_mode : DEFAULT_INSTANCE_POLICY_CONFIG.premium_mode, gif_enabled: normalizeNullableBoolean(value.gif_enabled), youtube_enabled: normalizeNullableBoolean(value.youtube_enabled), bluesky_enabled: normalizeNullableBoolean(value.bluesky_enabled), };}
const DEFAULT_INSTANCE_INTEGRATIONS_CONFIG: InstanceIntegrationsConfig = { gif: { provider: null, tenor_api_key: null, klipy_api_key: null, }, youtube: { api_key: null, }, captcha: { provider: null, hcaptcha_site_key: null, hcaptcha_secret_key: null, turnstile_site_key: null, turnstile_secret_key: null, }, email: { enabled: null, provider: null, from_email: null, from_name: null, smtp: { host: null, port: null, username: null, password: null, secure: null, }, }, bluesky: { enabled: null, client_name: null, client_uri: null, logo_uri: null, tos_uri: null, policy_uri: null, keys: [], },};
const DEFAULT_INSTANCE_ATTACHMENT_DECAY_CONFIG: InstanceAttachmentDecayConfig = { enabled: null, min_size_mb: null, max_size_mb: null, max_eligible_size_mb: null, min_lifetime_days: null, max_lifetime_days: null, curve: null, renew_threshold_days: null, renew_window_days: null,};
const DEFAULT_INSTANCE_MEDIA_CONFIG: InstanceMediaConfig = { attachment_decay: DEFAULT_INSTANCE_ATTACHMENT_DECAY_CONFIG,};
function isGifProvider(value: unknown): value is InstanceGifProvider { return value === 'tenor' || value === 'klipy';}
function isCaptchaProvider(value: unknown): value is InstanceCaptchaProvider { return value === 'hcaptcha' || value === 'turnstile' || value === 'none';}
function isEmailProvider(value: unknown): value is InstanceEmailProvider { return value === 'smtp' || value === 'none';}
function normalizeSecretString(value: unknown): string | null { return typeof value === 'string' && value.trim().length > 0 ? value.trim() : null;}
function secretIsSet(value: unknown): boolean { return typeof value === 'string' && value.trim().length > 0;}
function normalizeNullablePort(value: unknown): number | null { if (typeof value !== 'number' || !Number.isInteger(value) || value < 1 || value > 65535) return null; return value;}
function normalizeNullablePositiveNumber(value: unknown): number | null { if (typeof value !== 'number' || !Number.isFinite(value) || value <= 0) return null; return value;}
function normalizeNullablePositiveInteger(value: unknown): number | null { if (typeof value !== 'number' || !Number.isInteger(value) || value < 1) return null; return value;}
function normalizeNullableCurve(value: unknown): number | null { if (typeof value !== 'number' || !Number.isFinite(value) || value < 0 || value > 1) return null; return value;}
function normalizeBlueskyKey(value: unknown): InstanceBlueskyKeyIntegrationConfig | null { if (!isJsonRecord(value)) return null; const kid = normalizePublicString(value.kid); if (!kid) return null; return { kid, private_key: normalizeSecretString(value.private_key), };}
function normalizeInstanceIntegrationsConfig(value: unknown): InstanceIntegrationsConfig { const defaults = DEFAULT_INSTANCE_INTEGRATIONS_CONFIG; if (!isJsonRecord(value)) { return structuredClone(defaults); } const gif = isJsonRecord(value.gif) ? value.gif : {}; const youtube = isJsonRecord(value.youtube) ? value.youtube : {}; const captcha = isJsonRecord(value.captcha) ? value.captcha : {}; const email = isJsonRecord(value.email) ? value.email : {}; const smtp = isJsonRecord(email.smtp) ? email.smtp : {}; const bluesky = isJsonRecord(value.bluesky) ? value.bluesky : {}; const blueskyKeys = Array.isArray(bluesky.keys) ? bluesky.keys.flatMap((entry) => { const normalized = normalizeBlueskyKey(entry); return normalized ? [normalized] : []; }) : defaults.bluesky.keys; return { gif: { provider: isGifProvider(gif.provider) ? gif.provider : defaults.gif.provider, tenor_api_key: normalizeSecretString(gif.tenor_api_key), klipy_api_key: normalizeSecretString(gif.klipy_api_key), }, youtube: { api_key: normalizeSecretString(youtube.api_key), }, captcha: { provider: isCaptchaProvider(captcha.provider) ? captcha.provider : defaults.captcha.provider, hcaptcha_site_key: normalizeSecretString(captcha.hcaptcha_site_key), hcaptcha_secret_key: normalizeSecretString(captcha.hcaptcha_secret_key), turnstile_site_key: normalizeSecretString(captcha.turnstile_site_key), turnstile_secret_key: normalizeSecretString(captcha.turnstile_secret_key), }, email: { enabled: normalizeNullableBoolean(email.enabled), provider: isEmailProvider(email.provider) ? email.provider : defaults.email.provider, from_email: normalizePublicString(email.from_email), from_name: normalizePublicString(email.from_name), smtp: { host: normalizePublicString(smtp.host), port: normalizeNullablePort(smtp.port), username: normalizePublicString(smtp.username), password: normalizeSecretString(smtp.password), secure: normalizeNullableBoolean(smtp.secure), }, }, bluesky: { enabled: normalizeNullableBoolean(bluesky.enabled), client_name: normalizePublicString(bluesky.client_name), client_uri: normalizePublicString(bluesky.client_uri), logo_uri: normalizePublicString(bluesky.logo_uri), tos_uri: normalizePublicString(bluesky.tos_uri), policy_uri: normalizePublicString(bluesky.policy_uri), keys: blueskyKeys, }, };}
function normalizeInstanceAttachmentDecayConfig(value: unknown): InstanceAttachmentDecayConfig { if (!isJsonRecord(value)) { return {...DEFAULT_INSTANCE_ATTACHMENT_DECAY_CONFIG}; } const minSizeMb = normalizeNullablePositiveNumber(value.min_size_mb); let maxSizeMb = normalizeNullablePositiveNumber(value.max_size_mb); let maxEligibleSizeMb = normalizeNullablePositiveNumber(value.max_eligible_size_mb); const minLifetimeDays = normalizeNullablePositiveInteger(value.min_lifetime_days); let maxLifetimeDays = normalizeNullablePositiveInteger(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 { enabled: normalizeNullableBoolean(value.enabled), min_size_mb: minSizeMb, max_size_mb: maxSizeMb, max_eligible_size_mb: maxEligibleSizeMb, min_lifetime_days: minLifetimeDays, max_lifetime_days: maxLifetimeDays, curve: normalizeNullableCurve(value.curve), renew_threshold_days: normalizeNullablePositiveInteger(value.renew_threshold_days), renew_window_days: normalizeNullablePositiveInteger(value.renew_window_days), };}
function normalizeInstanceMediaConfig(value: unknown): InstanceMediaConfig { if (!isJsonRecord(value)) { return structuredClone(DEFAULT_INSTANCE_MEDIA_CONFIG); } return { attachment_decay: normalizeInstanceAttachmentDecayConfig(value.attachment_decay), };}
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(), );}
function normalizeIsoDateString(value: unknown): string | null { if (typeof value !== 'string') return null; const time = Date.parse(value); return Number.isFinite(time) ? new Date(time).toISOString() : null;}
function normalizePositiveInteger(value: unknown): number | null { if (typeof value !== 'number' || !Number.isInteger(value) || value < 1) return null; return value;}
function normalizeNonNegativeInteger(value: unknown): number { if (typeof value !== 'number' || !Number.isInteger(value) || value < 0) return 0; return value;}
function normalizeRegistrationConfig(value: unknown): InstanceRegistrationConfig { if (!isJsonRecord(value)) { return {...DEFAULT_REGISTRATION_CONFIG}; } const mode = isRegistrationMode(value.mode) ? value.mode : DEFAULT_REGISTRATION_CONFIG.mode; const adminRegistrationUrlsEnabled = typeof value.admin_registration_urls_enabled === 'boolean' ? value.admin_registration_urls_enabled : typeof value.adminRegistrationUrlsEnabled === 'boolean' ? value.adminRegistrationUrlsEnabled : DEFAULT_REGISTRATION_CONFIG.admin_registration_urls_enabled; return { mode, admin_registration_urls_enabled: adminRegistrationUrlsEnabled, };}
function normalizeRegistrationUrl(value: unknown): InstanceRegistrationUrl | null { if (!isJsonRecord(value)) return null; if ( typeof value.id !== 'string' || typeof value.code_hash !== 'string' || typeof value.created_by_user_id !== 'string' ) { return null; } const createdAt = normalizeIsoDateString(value.created_at); if (!createdAt) return null; return { id: value.id, label: normalizeNullableString(value.label), code_hash: value.code_hash, created_by_user_id: value.created_by_user_id, created_at: createdAt, expires_at: normalizeIsoDateString(value.expires_at), max_uses: normalizePositiveInteger(value.max_uses), use_count: normalizeNonNegativeInteger(value.use_count), revoked_at: normalizeIsoDateString(value.revoked_at), approval_required: value.approval_required === true, last_used_at: normalizeIsoDateString(value.last_used_at), last_used_by_user_id: normalizeNullableString(value.last_used_by_user_id), };}
function normalizePendingRegistration(value: unknown): InstancePendingRegistration | null { if (!isJsonRecord(value)) return null; if (typeof value.user_id !== 'string' || typeof value.username !== 'string') return null; const requestedAt = normalizeIsoDateString(value.requested_at); if (!requestedAt) return null; return { user_id: value.user_id, username: value.username, discriminator: typeof value.discriminator === 'number' ? value.discriminator : 0, global_name: normalizeNullableString(value.global_name), email: normalizeNullableString(value.email), requested_at: requestedAt, registration_url_id: normalizeNullableString(value.registration_url_id), client_ip: normalizeNullableString(value.client_ip), };}
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;}
function isLimitRuleSnapshot(value: unknown): value is LimitConfigSnapshot['rules'][number] { if (!isJsonRecord(value) || typeof value.id !== 'string' || !isJsonRecord(value.limits)) return false; const filters = value.filters; return ( (filters === undefined || (isJsonRecord(filters) && (filters.traits === undefined || isStringArray(filters.traits)) && (filters.guildFeatures === undefined || isStringArray(filters.guildFeatures)))) && (value.modifiedFields === undefined || isStringArray(value.modifiedFields)) );}
function isLimitConfigSnapshot(value: unknown): value is LimitConfigSnapshot { return ( isJsonRecord(value) && (value.version === undefined || typeof value.version === 'number') && isStringArray(value.traitDefinitions) && Array.isArray(value.rules) && value.rules.every(isLimitRuleSnapshot) );}
export interface InstanceSsoConfig { enabled: boolean; enforced: boolean; 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<string>; autoProvision: boolean; redirectUri: string | null;}
export class InstanceConfigRepository { private readonly pubSubSourceId = crypto.randomUUID(); private readonly kvClient: IKVProvider | null; private configCache: Map<string, string> | null = null; private cacheInitializationPromise: Promise<void> | null = null; private refreshPromise: Promise<void> | null = null; private refreshRequested = false; private kvSubscription: IKVSubscription | null = null; private subscriberInitialized = false; private subscriberInitializationPromise: Promise<boolean> | null = null; private messageHandler: ((channel: string, message: string) => void) | null = null;
constructor(kvClient: IKVProvider | null = null) { this.kvClient = kvClient; }
async getConfig(key: string): Promise<string | null> { if (!(await this.ensureCacheReadable())) { return this.fetchConfigFromDatabase(key); } return this.configCache?.get(key) ?? null; }
async getAllConfigs(): Promise<Map<string, string>> { if (!(await this.ensureCacheReadable())) { return this.fetchAllConfigsFromDatabase(); } return new Map(this.configCache ?? []); }
async setConfig(key: string, value: string): Promise<void> { await this.writeConfig(key, value); this.updateCachedConfigs([[key, value]]); await this.publishRefresh(); }
private async setConfigs(entries: Array<[string, string]>): Promise<void> { if (entries.length === 0) return; await Promise.all(entries.map(([key, value]) => this.writeConfig(key, value))); this.updateCachedConfigs(entries); await this.publishRefresh(); }
private async writeConfig(key: string, value: string): Promise<void> { await upsertOne( InstanceConfiguration.upsertAll({ key, value, updated_at: new Date(), }), ); }
private async fetchConfigFromDatabase(key: string): Promise<string | null> { const row = await fetchOne<InstanceConfigurationRow>(FETCH_CONFIG_QUERY, {key}); return row?.value ?? null; }
private async fetchAllConfigsFromDatabase(): Promise<Map<string, string>> { const rows = await fetchMany<InstanceConfigurationRow>(FETCH_ALL_CONFIG_QUERY, {}); const configs = new Map<string, string>(); for (const row of rows) { if (row.value != null) { configs.set(row.key, row.value); } } return configs; }
private async ensureCacheReadable(): Promise<boolean> { if (!this.kvClient) { return false; } if (this.configCache !== null) { return true; } if (!this.cacheInitializationPromise) { this.cacheInitializationPromise = (async () => { const subscribed = await this.ensureSubscriberInitialized(); if (!subscribed) { return; } await this.refreshCacheFromDatabase(); })().finally(() => { this.cacheInitializationPromise = null; }); } await this.cacheInitializationPromise; return this.configCache !== null; }
private async ensureSubscriberInitialized(): Promise<boolean> { if (this.subscriberInitialized) { return true; } if (!this.kvClient) { return false; } if (!this.subscriberInitializationPromise) { this.subscriberInitializationPromise = this.startSubscriber() .then(() => true) .catch((error) => { Logger.error({error}, 'Failed to subscribe to instance config refresh channel'); this.closeSubscription(); return false; }) .finally(() => { this.subscriberInitializationPromise = null; }); } return this.subscriberInitializationPromise; }
private async startSubscriber(): Promise<void> { if (!this.kvClient) { return; } const subscription = this.kvClient.duplicate(); this.kvSubscription = subscription; this.messageHandler = (channel: string, message: string) => { if (channel !== INSTANCE_CONFIG_REFRESH_CHANNEL) { return; } if (this.isOwnRefreshMessage(message)) { return; } this.refreshCacheFromDatabase().catch((error) => { Logger.error({error}, 'Failed to refresh instance config cache from pubsub'); }); }; subscription.on('message', this.messageHandler); await subscription.connect(); await subscription.subscribe(INSTANCE_CONFIG_REFRESH_CHANNEL); this.subscriberInitialized = true; }
private async refreshCacheFromDatabase(): Promise<void> { if (this.refreshPromise) { this.refreshRequested = true; await this.refreshPromise; return; } this.refreshPromise = (async () => { do { this.refreshRequested = false; this.configCache = await this.fetchAllConfigsFromDatabase(); } while (this.refreshRequested); })().finally(() => { this.refreshPromise = null; }); await this.refreshPromise; }
private updateCachedConfigs(entries: Array<[string, string]>): void { if (!this.configCache) { return; } for (const [key, value] of entries) { this.configCache.set(key, value); } }
private async publishRefresh(): Promise<void> { if (!this.kvClient) { return; } await this.kvClient.publish( INSTANCE_CONFIG_REFRESH_CHANNEL, JSON.stringify({source_id: this.pubSubSourceId, type: 'refresh'}), ); }
private isOwnRefreshMessage(message: string): boolean { const parsed = parseJsonRecord(message); return parsed?.source_id === this.pubSubSourceId; }
private closeSubscription(): void { if (this.kvSubscription && this.messageHandler) { this.kvSubscription.off('message', this.messageHandler); } if (this.kvSubscription) { this.kvSubscription.disconnect(); this.kvSubscription = null; } this.messageHandler = null; this.subscriberInitialized = false; }
shutdown(): void { this.closeSubscription(); }
clearCacheForTesting(): void { this.closeSubscription(); this.configCache = null; this.cacheInitializationPromise = null; this.refreshPromise = null; this.refreshRequested = false; this.subscriberInitializationPromise = null; }
async isAdminBootstrapped(): Promise<boolean> { return (await this.getConfig(ADMIN_BOOTSTRAP_KEY)) === 'true'; }
async markAdminBootstrapped(): Promise<void> { await this.setConfig(ADMIN_BOOTSTRAP_KEY, 'true'); }
async getGatewayRolloutConfig(): Promise<GatewayRolloutConfig> { const raw = await this.getConfig(GATEWAY_ROLLOUT_CONFIG_KEY); if (!raw) { return {...DEFAULT_GATEWAY_ROLLOUT_CONFIG}; } try { const parsed = parseJsonRecord(raw); if (!parsed) { return {...DEFAULT_GATEWAY_ROLLOUT_CONFIG}; } const normalizedParsed: Record<string, unknown> = {...parsed}; if ( normalizedParsed.rpc_request_timeout_ms === undefined && typeof normalizedParsed.nats_request_timeout_ms === 'number' ) { normalizedParsed.rpc_request_timeout_ms = normalizedParsed.nats_request_timeout_ms; } return GatewayRolloutConfigSchema.parse({...DEFAULT_GATEWAY_ROLLOUT_CONFIG, ...normalizedParsed}); } catch (error) { Logger.error({error}, 'Invalid gateway rollout config'); throw error; } }
async setGatewayRolloutConfig(config: GatewayRolloutConfig): Promise<void> { await this.setConfig(GATEWAY_ROLLOUT_CONFIG_KEY, JSON.stringify(config)); }
async hasLimitConfig(): Promise<boolean> { const raw = await this.getConfig('limit_config'); return raw !== null; }
async getLimitConfig(): Promise<LimitConfigSnapshot | null> { const raw = await this.getConfig('limit_config'); if (!raw) { return null; } try { const parsed: unknown = JSON.parse(raw); if (!isLimitConfigSnapshot(parsed)) { return null; } const policy = await this.getInstancePolicyConfig(); return sanitizeLimitConfigForInstance(parsed, { selfHosted: Config.instance.selfHosted, premiumMode: policy.premium_mode, }); } catch (error) { Logger.warn({error}, 'Invalid limit config JSON, returning null'); return null; } }
async setLimitConfig(config: LimitConfigSnapshot): Promise<void> { await this.setConfig('limit_config', JSON.stringify(config)); }
async getAppPublicConfig(): Promise<InstanceAppPublicConfig> { const raw = await this.getConfig(APP_PUBLIC_CONFIG_KEY); if (!raw) { return getDefaultAppPublicConfig(); } try { const parsed: unknown = JSON.parse(raw); return normalizeAppPublicConfig(parsed); } catch (error) { Logger.warn({error}, 'Invalid app public config JSON, returning defaults'); return getDefaultAppPublicConfig(); } }
async setAppPublicConfig(config: { branding?: Partial<InstanceBrandingConfig>; setup?: Partial<InstanceAppPublicConfig['setup']>; legal?: Partial<InstanceAppPublicConfig['legal']>; registration?: Partial<InstanceAppPublicConfig['registration']>; }): Promise<InstanceAppPublicConfig> { const current = await this.getAppPublicConfig(); const next = normalizeAppPublicConfig({ 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)); return next; }
async getInstancePolicyConfig(): Promise<InstancePolicyConfig> { const raw = await this.getConfig(INSTANCE_POLICY_CONFIG_KEY); if (!raw) { return {...DEFAULT_INSTANCE_POLICY_CONFIG}; } return normalizeInstancePolicyConfig(parseJsonRecord(raw)); }
async setInstancePolicyConfig(config: Partial<InstancePolicyConfig>): Promise<InstancePolicyConfig> { const current = await this.getInstancePolicyConfig(); const next = normalizeInstancePolicyConfig({...current, ...config}); await this.setConfig(INSTANCE_POLICY_CONFIG_KEY, JSON.stringify(next)); return next; }
async getInstanceIntegrationsConfig(): Promise<InstanceIntegrationsConfig> { const raw = await this.getConfig(INSTANCE_INTEGRATIONS_CONFIG_KEY); if (!raw) { return normalizeInstanceIntegrationsConfig(null); } return normalizeInstanceIntegrationsConfig(parseJsonRecord(raw)); }
async setInstanceIntegrationsConfig(config: InstanceIntegrationsConfigPatch): Promise<InstanceIntegrationsConfig> { const current = await this.getInstanceIntegrationsConfig(); const next = normalizeInstanceIntegrationsConfig({ 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<InstanceMediaConfig> { const raw = await this.getConfig(INSTANCE_MEDIA_CONFIG_KEY); if (!raw) { return normalizeInstanceMediaConfig(null); } return normalizeInstanceMediaConfig(parseJsonRecord(raw)); }
async setInstanceMediaConfig(config: InstanceMediaConfigPatch): Promise<InstanceMediaConfig> { const current = await this.getInstanceMediaConfig(); const next = normalizeInstanceMediaConfig({ attachment_decay: { ...current.attachment_decay, ...(config.attachment_decay ?? {}), }, }); await this.setConfig(INSTANCE_MEDIA_CONFIG_KEY, JSON.stringify(next)); return next; }
async getEffectiveAttachmentDecayConfig(): Promise<InstanceAttachmentDecayEffectiveConfig> { 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 = configuredMaxSizeMb > minSizeMb ? configuredMaxSizeMb : Math.max(DEFAULT_DECAY_CONSTANTS.MAX_MB, minSizeMb + 1); 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<boolean> { return (await this.getEffectiveAttachmentDecayConfig()).enabled; }
async getInstanceMediaAdminConfig(): Promise<InstanceMediaAdminConfig> { const [media, attachmentDecay] = await Promise.all([ this.getInstanceMediaConfig(), this.getEffectiveAttachmentDecayConfig(), ]); return { attachment_decay: { ...media.attachment_decay, effective: attachmentDecay, }, }; }
async getEffectiveGifConfig(): Promise<InstanceGifEffectiveConfig> { const integrations = await this.getInstanceIntegrationsConfig(); const provider = integrations.gif.provider ?? Config.gif.provider; const tenorApiKey = integrations.gif.tenor_api_key ?? normalizeSecretString(Config.tenor.apiKey); const klipyApiKey = integrations.gif.klipy_api_key ?? normalizeSecretString(Config.klipy.apiKey); const activeApiKey = provider === 'tenor' ? tenorApiKey : klipyApiKey; return { provider, tenor_api_key: tenorApiKey, klipy_api_key: klipyApiKey, active_api_key: activeApiKey, available: Boolean(activeApiKey), }; }
async getEffectiveYoutubeApiKey(): Promise<string | null> { const integrations = await this.getInstanceIntegrationsConfig(); return integrations.youtube.api_key ?? normalizeSecretString(Config.youtube.apiKey); }
async getEffectiveCaptchaConfig(): Promise<InstanceCaptchaEffectiveConfig> { const integrations = await this.getInstanceIntegrationsConfig(); const provider = integrations.captcha.provider ?? (Config.captcha.enabled ? Config.captcha.provider : 'none'); const hcaptchaSiteKey = integrations.captcha.hcaptcha_site_key ?? normalizeSecretString(Config.captcha.hcaptcha?.siteKey); const hcaptchaSecretKey = integrations.captcha.hcaptcha_secret_key ?? normalizeSecretString(Config.captcha.hcaptcha?.secretKey); const turnstileSiteKey = integrations.captcha.turnstile_site_key ?? normalizeSecretString(Config.captcha.turnstile?.siteKey); const turnstileSecretKey = integrations.captcha.turnstile_secret_key ?? normalizeSecretString(Config.captcha.turnstile?.secretKey); const providerReady = provider === 'hcaptcha' ? Boolean(hcaptchaSiteKey && hcaptchaSecretKey) : provider === 'turnstile' ? Boolean(turnstileSiteKey && turnstileSecretKey) : false; return { enabled: provider !== 'none' && providerReady, provider, hcaptcha_site_key: hcaptchaSiteKey, hcaptcha_secret_key: hcaptchaSecretKey, turnstile_site_key: turnstileSiteKey, turnstile_secret_key: turnstileSecretKey, }; }
async getEffectiveEmailConfig(): Promise<APIConfig['email']> { 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<boolean> { return (await this.getEffectiveEmailConfig()).enabled; }
async getEffectiveBlueskyConfig(): Promise<BlueskyOAuthConfig> { const integrations = await this.getInstanceIntegrationsConfig(); const runtimeKeys = integrations.bluesky.keys.flatMap((key): Array<BlueskyOAuthKeyConfig> => { 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; return { ...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, }; }
async getInstanceIntegrationsAdminConfig(): Promise<InstanceIntegrationsAdminConfig> { const [integrations, gif, youtubeApiKey, captcha, email, bluesky] = await Promise.all([ this.getInstanceIntegrationsConfig(), this.getEffectiveGifConfig(), this.getEffectiveYoutubeApiKey(), this.getEffectiveCaptchaConfig(), this.getEffectiveEmailConfig(), this.getEffectiveBlueskyConfig(), ]); return { gif: { provider: integrations.gif.provider, effective_provider: gif.provider, tenor_api_key_set: secretIsSet(integrations.gif.tenor_api_key) || secretIsSet(Config.tenor.apiKey), 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, }, }, 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<InstanceCommunityPublicConfig> { 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<InstanceServicesPublicConfig> { 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<InstanceRegistrationConfig> { const raw = await this.getConfig(REGISTRATION_CONFIG_KEY); if (!raw) { return {...DEFAULT_REGISTRATION_CONFIG}; } const parsed = parseJsonRecord(raw); return normalizeRegistrationConfig(parsed); }
async setRegistrationConfig(config: Partial<InstanceRegistrationConfig>): Promise<InstanceRegistrationConfig> { const current = await this.getRegistrationConfig(); const next = normalizeRegistrationConfig({ 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<InstanceRegistrationConfig> { return this.getRegistrationConfig(); }
async getRegistrationUrls(): Promise<Array<InstanceRegistrationUrl>> { const raw = await this.getConfig(REGISTRATION_URLS_KEY); if (!raw) { return []; } const parsed = parseJsonArray(raw); if (!parsed) { Logger.warn('Invalid registration URL config JSON, returning empty list'); return []; } return parsed.flatMap((entry) => { const normalized = normalizeRegistrationUrl(entry); return normalized ? [normalized] : []; }); }
async getRegistrationUrlsForAdmin(): Promise<Array<InstanceRegistrationUrlPublic>> { 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<void> { 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<InstanceRegistrationUrl | null> { 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<void> { 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<Array<InstancePendingRegistration>> { const raw = await this.getConfig(REGISTRATION_PENDING_APPROVALS_KEY); if (!raw) { return []; } const parsed = parseJsonArray(raw); if (!parsed) { Logger.warn('Invalid pending registration config JSON, returning empty list'); return []; } return parsed .flatMap((entry) => { const normalized = normalizePendingRegistration(entry); return normalized ? [normalized] : []; }) .toSorted((a, b) => Date.parse(a.requested_at) - Date.parse(b.requested_at)); }
async addPendingRegistration(pendingRegistration: InstancePendingRegistration): Promise<void> { const pendingRegistrations = await this.getPendingRegistrations(); const next = [ pendingRegistration, ...pendingRegistrations.filter((entry) => entry.user_id !== pendingRegistration.user_id), ]; await this.setConfig(REGISTRATION_PENDING_APPROVALS_KEY, JSON.stringify(next)); }
async removePendingRegistration(userId: string): Promise<void> { const pendingRegistrations = await this.getPendingRegistrations(); await this.setConfig( REGISTRATION_PENDING_APPROVALS_KEY, JSON.stringify(pendingRegistrations.filter((entry) => entry.user_id !== userId)), ); }
async getSsoConfig(options?: {includeSecret?: boolean}): Promise<InstanceSsoConfig> { const configs = await this.getAllConfigs(); 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 allowedDomainsRaw = configs.get('sso_allowed_domains'); let allowedDomains: Array<string> = []; if (allowedDomainsRaw) { const parsed = parseJsonArray(allowedDomainsRaw); if (parsed) { allowedDomains = parsed.map((item) => String(item)).filter((item) => item.length > 0); } else { allowedDomains = allowedDomainsRaw .split(',') .map((s) => s.trim()) .filter((s) => s.length > 0); } } const clientSecret = read('sso_client_secret'); const enabled = configs.get('sso_enabled') === 'true'; const enforcedRaw = configs.get('sso_enforced'); return { enabled, enforced: enforcedRaw == null ? enabled : enforcedRaw === 'true', 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, autoProvision: configs.get('sso_auto_provision') !== 'false', redirectUri: null, }; }
async setSsoConfig(config: Partial<InstanceSsoConfig>): Promise<InstanceSsoConfig> { const current = await this.getSsoConfig({includeSecret: true}); const definedConfig = Object.fromEntries( Object.entries(config).filter(([, value]) => value !== undefined), ) as Partial<InstanceSsoConfig>; 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<string>; 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<InstanceRegistrationUrl>): Promise<void> { await this.setConfig(REGISTRATION_URLS_KEY, JSON.stringify(registrationUrls)); }
private hashRegistrationUrlCode(code: string): string { return crypto.createHash('sha256').update(code).digest('hex'); }}