/** * Konfigurace z environment variables. * AppFactory je predava containeru, viz AGENTS.md, sekce Variables a secrets. * Zadna hodnota se nehardcoduje a zadny secret se neloguje. * * Pravidlo: chybejici konfigurace nesmi shodit start aplikace. Container, * ktery nenastartuje, je pro AppFactory nefunkcni sluzba. */ import { randomBytes } from 'node:crypto'; import path from 'node:path'; const isProduction = process.env.NODE_ENV === 'production'; function positiveNumber(value: string | undefined, fallback: number): number { const parsed = Number(value); return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback; } /** * Tajny klic pro podpis tokenu. * * Kdyz promenna chybi, NEPADAME a ani nepouzijeme klic zapsany v kodu. * Aplikace musi nastartovat a odpovidat na /health, jinak ji AppFactory * vyhodnoti jako nefunkcni. Misto toho se vygeneruje nahodny klic pro * beh procesu a do logu jde hlasite varovani. * * Dusledek: po restartu containeru prestanou platit vydane tokeny * a uzivatele se musi prihlasit znovu. Proto se JWT_SECRET ma nastavit * jako promenna aplikace v AppFactory. */ function resolveJwtSecret(): string { const value = process.env.JWT_SECRET; if (value && value.trim().length > 0) return value; const generated = randomBytes(32).toString('base64url'); console.warn( '[config] JWT_SECRET neni nastavena. Pouzivam nahodny klic platny jen do restartu ' + 'containeru. Nastavte JWT_SECRET jako promennou aplikace v AppFactory.', ); return generated; } /** * Prefix verejne adresy, napr. "/apps/csbot-prototype". * Caddy ho pred predanim do containeru odstranuje (handle_path), ale prohlizec * ho vidi - proto se z nej sklada base pro SPA, odkazy, Swagger i webhooky. */ function normalizeRootPath(value: string | undefined): string { const trimmed = (value ?? '').trim(); if (trimmed.length === 0 || trimmed === '/') return ''; const withSlash = trimmed.startsWith('/') ? trimmed : `/${trimmed}`; return withSlash.replace(/\/+$/, ''); } export const config = { isProduction, /** Port urcuje AppFactory sablona, vychozi 3000. Nemenit bez upravy metadat. */ port: Number(process.env.PORT ?? 3000), rootPath: normalizeRootPath(process.env.ROOT_PATH), jwtSecret: resolveJwtSecret(), jwtExpiresIn: process.env.JWT_EXPIRES_IN ?? '8h', /** * Povolene originy pro CORS. V nasazeni bezi web i API na stejne domene, * takze se CORS neuplatni. Je tu kvuli lokalnimu vyvoji s Vite dev serverem. */ corsOrigins: (process.env.CORS_ORIGIN ?? 'http://localhost:5173,http://localhost:4173') .split(',') .map((o) => o.trim()) .filter(Boolean), /** * Verejna adresa bez prefixu, napr. "https://services.csbot.cz". * Sklada se z ni absolutni URL webhooku. Kdyz neni vyplnena, pouzije se * relativni tvar - nikdy se nehardcoduje produkcni domena. */ publicOrigin: (process.env.PUBLIC_ORIGIN ?? '').trim().replace(/\/+$/, ''), // ------------------------------------------------------------------ databaze /** * Pripojeni do Postgresu, napr. postgres://user:pass@host:5432/csbot * * Prazdna hodnota je platny stav: aplikace jede v pameti procesu. Container, * ktery nenastartuje kvuli chybejici promenne, je pro AppFactory nefunkcni * sluzba (AGENTS.md), takze se na tom nepada. */ databaseUrl: (process.env.DATABASE_URL ?? '').trim(), /** * Klic pro sifrovani pristupovych udaju konektoru. * * Ma to byt nahodny retezec, ne heslo. Jak ho vygenerovat je * v documentation/14-databaze.md. * * Bez nej se konektory neukladaji do databaze ani kdyz je nastavena - * plaintext v tabulce je horsi nez ztrata dat pri restartu. */ secretsKey: (process.env.SECRETS_KEY ?? '').trim(), /** * Kolik spojeni si smi vzit jedna instance. * * Nizke cislo je zamer: worker nesmi drzet spojeni po dobu volani ciziho API, * takze i pri stovce soubeznych kroku staci par spojeni. Podrobnosti * v documentation/10-runtime-a-kapacita.md. */ databasePoolMax: positiveNumber(process.env.DATABASE_POOL_MAX, 10), /** Spravovane databaze vyzaduji TLS. */ databaseSsl: process.env.DATABASE_SSL === 'true', /** * Slozka pro data mimo databazi. * * Bez `DATABASE_URL` se do ni uklada JSON, ktery prezije restart procesu * i containeru. Redeploy ho nezachova - filesystem containeru je docasny. * Prazdna hodnota vypne i tohle a jede se v ciste pameti. */ dataDir: (process.env.DATA_DIR ?? path.join(process.cwd(), 'data')).trim(), // ------------------------------------------------------- skripty konektoru /** * Adresar se skripty konektoru. Relativne k adresari, ze ktereho aplikace * bezi, aby to fungovalo v containeru (`/app/scripts`) i lokalne. */ scriptsDir: path.resolve(process.env.SCRIPTS_DIR ?? path.join(process.cwd(), 'scripts')), /** * Zaklad adres napojenych sluzeb, napr. "https://services.csbot.cz/apps". * Konkretni konektor lze presmerovat pres `_BASE_URL`. * Nikdy se nehardcoduje do logiky, viz AGENTS.md. */ servicesBaseUrl: (process.env.SERVICES_BASE_URL ?? 'https://services.csbot.cz/apps') .trim() .replace(/\/+$/, ''), /** * Odkud se zjistuje **odchozi IP adresa** tohoto containeru. * * Cizi sluzby maji seznamy povolenych IP a bez teto informace se neda rict, * jestli je na nich prave nase adresa - z containeru neni videt, jak ho vidi * protistrana. Vola se jen na vyzadani a vysledek se drzi v pameti. * * Prepisovatelne, aby se dalo ukazat na vlastni echo pod svou domenou misto * na cizi sluzbu. Prazdna hodnota funkci vypne. */ egressIpUrl: (process.env.EGRESS_IP_URL ?? 'https://api.ipify.org?format=json').trim(), /** Jak dlouho plati zjistena odchozi IP. Meni se nejvys pri presunu containeru. */ egressIpTtlMs: positiveNumber(process.env.EGRESS_IP_TTL_MS, 10 * 60_000), /** Strop na jeden beh skriptu, kdyz si ho manifest neurci sam. */ scriptTimeoutMs: positiveNumber(process.env.SCRIPT_TIMEOUT_MS, 15_000), /** Vetsi odpoved cizi sluzby se zahodi, misto aby snedla pamet procesu. */ scriptMaxResponseBytes: positiveNumber(process.env.SCRIPT_MAX_RESPONSE_BYTES, 1_000_000), /** * Strop na soubor odeslany z `ctx.http.postForm`. * * Soubor prochazi krokem stromu jako Base64, takze se cely drzi v pameti * a zapisuje se do zaznamu behu. Nizsi cislo nez u cizich sluzeb je zamer: * OpenAI zvladne stovky megabajtu, nas beh kroku ne. */ scriptMaxUploadBytes: positiveNumber(process.env.SCRIPT_MAX_UPLOAD_BYTES, 10_000_000), /** * Strop na text chybove odpovedi cizi sluzby. * * Zamerne velky. Odpoved na 401 nebo 400 obsahuje duvod a bez nej se chyba * nedá dohledat. Chyby jsou navic vzacne, takze objem neroste jako u logu * uspesnych kroku. */ errorDetailBytes: positiveNumber(process.env.SCRIPT_ERROR_DETAIL_BYTES, 8_000), /** * Strop na jednu strukturu (parametr typu object nebo list). * Radek s vystupem kroku je nejrychleji rostouci tabulka v systemu, takze * hranice patri do kontroly parametru, ne az do uklidu databaze. */ scriptMaxValueBytes: positiveNumber(process.env.SCRIPT_MAX_VALUE_BYTES, 256_000), /** * Povoli skriptum volat na localhost a do privatnich rozsahu IP. * Jen pro lokalni vyvoj, v nasazeni musi zustat vypnute. */ allowPrivateTargets: process.env.ALLOW_PRIVATE_TARGETS === 'true', /** * Zpracovava tenhle proces frontu behu? * * Vychozi ano, takze jedna instance umi obojí. Az bude potreba oddelit * vykon od API, spusti se tentyz obraz podruhe s `WORKER=1` a u API * se nastavi `WORKER=0`. */ workerEnabled: process.env.WORKER !== '0', /** * Nasypat ukazkova data? * * Vychozi **ne**. Ukazkove automatizace, tickety a incidenty jsou dobre na * predvedeni, ale na instanci, kde uz nekdo pracuje, jsou to cizi zaznamy, * ktere se po kazdem redeployi vraceji. Konfigurace (firmy, uzivatele, role, * resitele, typy) se nasypava vzdycky - bez ni je portal nepouzitelny. */ seedDemo: process.env.SEED_DEMO === '1', /** * Token webhooku automatizace, ktera je v seedu. * * Bez databaze token nikde neprezije redeploy, takze by se po kazdem * nasazeni menila adresa a odesilatel by ji musel prepisovat. Tohle to drzi: * token je v promenne aplikace, ne v datech ani v gitu. * * Kdyz neni nastaveny, vygeneruje se novy a v logu se rekne, ze se adresa * zmenila. */ seedWebhookToken: (process.env.WEBHOOK_TOKEN_TEST ?? '').trim(), /** * Jak casto plánovač hleda, co je na case (v sekundach). * * Casovane spoustece nemaji sekundovou presnost a nepotrebuji ji. Kratsi * interval znamena jen vic dotazu do fronty. */ schedulerIntervalSec: positiveNumber(process.env.SCHEDULER_INTERVAL_SEC, 30), }; /** Zaklad verejne adresy aplikace vcetne prefixu proxy. */ export function publicBaseUrl(): string { return `${config.publicOrigin}${config.rootPath}`; }