import { env } from "$env/dynamic/private"; // accepts "0" to disable a limit; non-numeric values fall back const bytes = (value: string | undefined, fallback: number) => { const raw = value?.trim(); if (!raw) return fallback; const parsed = Number(raw); return Number.isFinite(parsed) && parsed >= 0 ? parsed : fallback; }; const cleanUrl = (value: string | undefined, fallback: string) => { const raw = value?.trim() || fallback; return raw.replace(/\/+$/, ""); }; export type WebConfig = { bobbinUrl: string; knotMirrorUrl: string; apiUrl: string; deliberiUrl: string; delegationUrl: string; delegationDid: string; /** the domain user sites are served under, e.g. "tngl.io" */ sitesDomain: string; camoUrl: string; avatarUrl: string; ogreUrl: string; /** the secrets camo and avatar sign with, so neither leaves the server */ camoSecret: string; avatarSecret: string; moderationServiceDid: string; primaryKnotHostname: string; primarySpindleHostname: string; turnstileSiteKey: string; /** see SSR_DIFF_MAX_BYTES */ ssrDiffMaxBytes: number; }; export type PublicWebConfig = Pick< WebConfig, | "bobbinUrl" | "knotMirrorUrl" | "apiUrl" | "deliberiUrl" | "delegationUrl" | "delegationDid" | "sitesDomain" | "moderationServiceDid" | "primaryKnotHostname" | "primarySpindleHostname" | "turnstileSiteKey" | "ssrDiffMaxBytes" | "ogreUrl" > & { /** camo has a secret, so markup can route images through it */ camoEnabled: boolean; }; type WebConfigEnv = { BOBBIN_URL?: string; KNOTMIRROR_URL?: string; TANGLED_API_URL?: string; API_URL?: string; DELIBERI_URL?: string; DELEGATION_URL?: string; DELEGATION_DID?: string; KNOT_RESOLVER_URL?: string; SITES_DOMAIN?: string; CAMO_URL?: string; CAMO_SHARED_SECRET?: string; AVATAR_URL?: string; AVATAR_SHARED_SECRET?: string; OGRE_URL?: string; MODERATION_SERVICE_DID?: string; PRIMARY_KNOT?: string; PRIMARY_SPINDLE?: string; TURNSTILE_SITE_KEY?: string; /** diff payloads above this many bytes are not buffered through the ssr * worker; the page refetches them from the browser instead. "0" disables */ SSR_DIFF_MAX_BYTES?: string; }; export const resolveConfig = (values: WebConfigEnv): WebConfig => ({ bobbinUrl: cleanUrl(values.BOBBIN_URL, "http://127.0.0.1:8090"), knotMirrorUrl: cleanUrl(values.KNOTMIRROR_URL, ""), apiUrl: cleanUrl(values.TANGLED_API_URL ?? values.API_URL, "http://127.0.0.1:8080"), deliberiUrl: cleanUrl(values.DELIBERI_URL, "https://notifs-dev.tangled.network"), delegationUrl: cleanUrl(values.DELEGATION_URL, ""), delegationDid: values.DELEGATION_DID?.trim() ?? "", sitesDomain: values.SITES_DOMAIN?.trim() || "tngl.io", camoUrl: cleanUrl(values.CAMO_URL, "https://camo.tangled.sh"), avatarUrl: cleanUrl(values.AVATAR_URL, "https://avatar.tangled.sh"), ogreUrl: cleanUrl(values.OGRE_URL, "https://ogre.tangled.network"), camoSecret: values.CAMO_SHARED_SECRET?.trim() ?? "", avatarSecret: values.AVATAR_SHARED_SECRET?.trim() ?? "", moderationServiceDid: values.MODERATION_SERVICE_DID?.trim() ?? "", primaryKnotHostname: values.PRIMARY_KNOT?.trim() ?? "knot1.tangled.sh", primarySpindleHostname: values.PRIMARY_SPINDLE?.trim() ?? "spindle.tangled.sh", turnstileSiteKey: values.TURNSTILE_SITE_KEY?.trim() ?? "", ssrDiffMaxBytes: bytes(values.SSR_DIFF_MAX_BYTES, 5 * 1024 * 1024) }); export const getConfig = (): WebConfig => resolveConfig(env as WebConfigEnv); // listed one by one, so anything new in WebConfig stays private until it is // named here export const getPublicConfig = (): PublicWebConfig => { const config = getConfig(); return { bobbinUrl: config.bobbinUrl, knotMirrorUrl: config.knotMirrorUrl, apiUrl: config.apiUrl, deliberiUrl: config.deliberiUrl, delegationUrl: config.delegationUrl, delegationDid: config.delegationDid, sitesDomain: config.sitesDomain, moderationServiceDid: config.moderationServiceDid, primaryKnotHostname: config.primaryKnotHostname, primarySpindleHostname: config.primarySpindleHostname, turnstileSiteKey: config.turnstileSiteKey, ssrDiffMaxBytes: config.ssrDiffMaxBytes, ogreUrl: config.ogreUrl, camoEnabled: config.camoSecret !== "" }; };