Something went wrong. Try again.
Monorepo for Tangled tangled.org
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235import * as env from "$app/env/private";import { knotOf, type KnotOrigin } from "#lib/api/syntax.ts";
// accepts comma-separated entries, ignoring emptiesconst list = (value: string | undefined): string[] => { const raw = value?.trim(); if (!raw) return []; return raw .split(",") .map((v) => v.trim()) .filter(Boolean);};
// accepts "0" to disable a limit; non-numeric values fall backconst limit = (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(/\/+$/, "");};
const fosterKnotOrigin = (value: string | undefined): KnotOrigin | "" => { const raw = value?.trim(); if (!raw) return ""; const parsed = URL.parse(raw); if (parsed === null || parsed.pathname !== "/") throw new Error( `FOSTER_KNOT_URL must be a bare origin like https://knot.oyster.cafe, got "${raw}"` ); return knotOf(parsed.host);};
const liarPdsUrl = (value: string | undefined): string => { const raw = cleanUrl(value, ""); if (!raw) return ""; const parsed = URL.parse(raw); if (parsed === null || (parsed.protocol !== "http:" && parsed.protocol !== "https:")) throw new Error( `LIAR_PDS_URL must be an http(s) URL like https://liar.tangled.sh, got "${raw}"` ); return raw;};
export type WebConfig = { bobbinUrl: string; apiUrl: string; deliberiUrl: string; delegationUrl: string; delegationDid: string; delegationHandleDomain: string; /** the domain user sites are served under, e.g. "tngl.io" */ sitesDomain: string; /** the sites Worker (claims + serving), e.g. "https://sites.tangled.org" */ sitesUrl: string; /** written into markdown as <url>/blob/<did>/<cid> for uploaded images */ blobUrl: string; camoUrl: string; avatarUrl: string; ogreUrl: string; /** the secrets camo and avatar sign with, so neither leaves the server */ camoSecret: string; avatarSecret: string; /** the email-did worker and the token it wants, both server-only */ emailDidUrl: string; emailDidToken: string; /** where /sitemap.xml comes from when there is no SITEMAP worker binding */ sitemapUrl: string; moderationServiceDid: string; primaryKnotHostname: string; primarySpindleHostname: string; turnstileSiteKey: string; /** see SSR_DIFF_MAX_BYTES */ ssrDiffMaxBytes: number; /** see UPSTREAM_TIMEOUT_MS */ upstreamTimeoutMs: number; migratorUrl: string; googleSiteVerification: string[]; /** empty turns the migration off */ liarPdsUrl: string; /** empty turns foster read routing off */ fosterKnotUrl: KnotOrigin | ""; /** fakes the liar pds and hydrant's unmark */ liarMock: boolean; /** empty turns analytics off; server-only */ posthogApiKey: string; posthogHost: string; posthogEnv: string;};
export type PublicWebConfig = Pick< WebConfig, | "bobbinUrl" | "apiUrl" | "deliberiUrl" | "delegationUrl" | "delegationDid" | "delegationHandleDomain" | "sitesDomain" | "sitesUrl" | "blobUrl" | "moderationServiceDid" | "primaryKnotHostname" | "primarySpindleHostname" | "turnstileSiteKey" | "ssrDiffMaxBytes" | "ogreUrl" | "migratorUrl" | "googleSiteVerification" | "liarPdsUrl" | "fosterKnotUrl" | "liarMock"> & { /** camo has a secret, so markup can route images through it */ camoEnabled: boolean;};
type WebConfigEnv = { BOBBIN_URL?: string; TANGLED_API_URL?: string; API_URL?: string; DELIBERI_URL?: string; DELEGATION_URL?: string; DELEGATION_DID?: string; DELEGATION_HANDLE_DOMAIN?: string; KNOT_RESOLVER_URL?: string; SITES_DOMAIN?: string; TANGLED_SITES_URL?: string; BLOB_URL?: string; CAMO_URL?: string; CAMO_SHARED_SECRET?: string; AVATAR_URL?: string; AVATAR_SHARED_SECRET?: string; EMAIL_DID_URL?: string; EMAIL_DID_TOKEN?: string; SITEMAP_URL?: string; OGRE_URL?: string; MODERATION_SERVICE_DID?: string; PRIMARY_KNOT?: string; PRIMARY_SPINDLE?: string; TURNSTILE_SITE_KEY?: string; MIGRATOR_URL?: string; GOOGLE_SITE_VERIFICATION?: string; LIAR_PDS_URL?: string; FOSTER_KNOT_URL?: string; LIAR_MOCK?: string; POSTHOG_API_KEY?: string; POSTHOG_HOST?: string; POSTHOG_ENV?: 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; /** how long a server side GET waits for an upstream's headers. "0" turns it off */ UPSTREAM_TIMEOUT_MS?: string;};
export const resolveConfig = (values: WebConfigEnv): WebConfig => ({ bobbinUrl: cleanUrl(values.BOBBIN_URL, "http://127.0.0.1:8090"), 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() ?? "", delegationHandleDomain: values.DELEGATION_HANDLE_DOMAIN?.trim().toLowerCase() ?? "", sitesDomain: values.SITES_DOMAIN?.trim() || "tngl.io", sitesUrl: cleanUrl(values.TANGLED_SITES_URL, "https://sites.tangled.org"), blobUrl: cleanUrl(values.BLOB_URL, "https://tangled.org"), 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"), migratorUrl: cleanUrl(values.MIGRATOR_URL, "https://migrator.tangled.sh"), camoSecret: values.CAMO_SHARED_SECRET?.trim() ?? "", avatarSecret: values.AVATAR_SHARED_SECRET?.trim() ?? "", emailDidUrl: cleanUrl(values.EMAIL_DID_URL, ""), emailDidToken: values.EMAIL_DID_TOKEN?.trim() ?? "", sitemapUrl: cleanUrl(values.SITEMAP_URL, ""), 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: limit(values.SSR_DIFF_MAX_BYTES, 5 * 1024 * 1024), upstreamTimeoutMs: limit(values.UPSTREAM_TIMEOUT_MS, 15_000), googleSiteVerification: list(values.GOOGLE_SITE_VERIFICATION), liarPdsUrl: liarPdsUrl(values.LIAR_PDS_URL), fosterKnotUrl: fosterKnotOrigin(values.FOSTER_KNOT_URL), liarMock: ["1", "true"].includes(values.LIAR_MOCK?.trim().toLowerCase() ?? ""), posthogApiKey: values.POSTHOG_API_KEY?.trim() ?? "", posthogHost: cleanUrl(values.POSTHOG_HOST, "https://eu.i.posthog.com"), posthogEnv: values.POSTHOG_ENV?.trim() || "dev"});
// every declared variable is present on $app/env/private, unset ones as undefined:// keep only the set ones, so they don't mask a platform binding of the same nameconst processEnv = (): WebConfigEnv => Object.fromEntries(Object.entries(env).filter(([, value]) => value !== undefined));
// process env takes precedence over platform bindings in devexport const getConfig = (platform?: App.Platform): WebConfig => resolveConfig({ ...((platform?.env as WebConfigEnv | undefined) ?? {}), ...processEnv() });
// listed one by one, so anything new in WebConfig stays private until it is// named hereexport const getPublicConfig = (platform?: App.Platform): PublicWebConfig => { const config = getConfig(platform); return { bobbinUrl: config.bobbinUrl, apiUrl: config.apiUrl, deliberiUrl: config.deliberiUrl, delegationUrl: config.delegationUrl, delegationDid: config.delegationDid, delegationHandleDomain: config.delegationHandleDomain, sitesDomain: config.sitesDomain, sitesUrl: config.sitesUrl, blobUrl: config.blobUrl, moderationServiceDid: config.moderationServiceDid, primaryKnotHostname: config.primaryKnotHostname, primarySpindleHostname: config.primarySpindleHostname, turnstileSiteKey: config.turnstileSiteKey, ssrDiffMaxBytes: config.ssrDiffMaxBytes, ogreUrl: config.ogreUrl, migratorUrl: config.migratorUrl, googleSiteVerification: config.googleSiteVerification, liarPdsUrl: config.liarPdsUrl, fosterKnotUrl: config.fosterKnotUrl, liarMock: config.liarMock, camoEnabled: config.camoSecret !== "" };};