Files
csbot-prototype/src/config.ts
T
JiriUhlirandClaude Fable 5.1 ea9387bea9 Seed z repozitare, zatezove testy, worker bez spanku, dialogy bez rozmazani
Nastaveni prezije nasazeni: seed/records.json se pri prazdnem ulozisti
nacte misto ukazkovych dat (zive uloziste se nikdy neprepisuje). Soubor
nese soucasny stav produkce (firmy s provozovatelem, role, typ ticketu,
akce, widgety, skupiny, rozlozeni). Novy GET /api/admin/export a skript
npm run seed:export pro dalsi exporty, Dockerfile slozku kopiruje.

Vykonnostni testy (npm run test:perf) nad 200 firmami a 10 000 tickety
a zatezovy skript (npm run load) proti bezici instanci vcetne davky
udalosti na webhook. Mereni odhalilo strop workeru: po obsazeni vsech
mist spal sekundu, takze fronta odbavila nejvys 4 behy za sekundu. Ted
ceka na prvni dokonceny beh: 500 udalosti za 1,3 s (395 behu/s). Strop
posluchacu streamu zvednut na 2 000.

Dialogy: prekryv modalu a menu v portalu bez backdrop-blur, tecka Zive
pulzuje jen pri navazovani spojeni - rozmazani cele obrazovky pod trvalou
animaci sekalo video vedle portalu. Bublina udalosti drzi 0,5 s.

Dokumentace 14, 19, 20, 22, 04, 01, 03, 15 a 99 aktualizovana.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-09 20:41:42 +02:00

272 lines
11 KiB
TypeScript

/**
* 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(/\/+$/, '');
}
/** Nenastavena promenna = vychozi cesta, prazdna = vypnuto (`''`). */
function resolveSeedFile(value: string | undefined): string {
if (value === undefined) return path.join(process.cwd(), 'seed', 'records.json');
const trimmed = value.trim();
return trimmed === '' ? '' : path.resolve(trimmed);
}
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(),
/**
* Nastaveni z repozitare pro prazdne uloziste.
*
* Nasazeni bez databaze a bez svazku prijde pri kazdem redeployi o data
* a spravce platformy zadava firmy, ucty a widgety znovu. Soubor v repu
* (vystup `npm run seed:export`) drzi konfiguraci a pri startu se pouzije
* misto vychozi sady z kodu - **jen do prazdneho uloziste**, nikdy pres
* ziva data. Chybejici soubor je bezny stav (lokalni vyvoj). Spravne reseni
* je `DATABASE_URL`, tohle je nahrada do te doby. Prazdna hodnota soubor
* vypne uplne (stejne jako u `DATA_DIR`), testy tak nezavisi na obsahu repa.
* Viz documentation/14-databaze.md.
*/
seedFile: resolveSeedFile(process.env.SEED_FILE),
// --------------------------------------------------------------- znacka
/**
* Nazev produktu. Prejmenovani = zmena teto promenne, nic jineho.
*
* Server ho pouziva v titulku Swaggeru a v OpenAPI. Klient ma svoje
* `web/src/config/brand.ts`, ktere cte `VITE_BRAND_NAME` - dosadi se pri
* buildu, protoze do prohlizece runtime promenne containeru nedosahnou.
*
* Pozor na zamenu: firma **Automia v ukazkovych datech** je zaznam
* zakaznika, ne znacka. V pozvance se ukazuje jmeno firmy, do ktere
* pozvanka zve, a to se timhle nemeni.
*/
brandName: (process.env.BRAND_NAME ?? 'WorkNuke').trim() || 'WorkNuke',
// ------------------------------------------------------- skripty konektoru
/**
* Adresar se skripty konektoru. Relativne k adresari, ze ktereho aplikace
* bezi, aby to fungovalo v containeru (`/app/connectors`) i lokalne.
*/
scriptsDir: path.resolve(process.env.SCRIPTS_DIR ?? path.join(process.cwd(), 'connectors')),
/**
* Zaklad adres napojenych sluzeb, napr. "https://services.csbot.cz/apps".
* Konkretni konektor lze presmerovat pres `<KONEKTOR>_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',
/**
* Registr ARES pro zalozeni firmy podle IC nebo nazvu. Verejne API bez
* klice; promenna je tu jen pro testovaci prostredi nebo zrcadlo.
*/
aresBaseUrl: (process.env.ARES_BASE_URL ?? 'https://ares.gov.cz/ekonomicke-subjekty-v-be/rest')
.trim()
.replace(/\/+$/, ''),
/**
* 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),
/**
* Presmerovani jedne sluzby promennou `<SLUZBA>_BASE_URL`, napr.
* `OPENAI_BASE_URL`. K cemu to je, rika `runtime/scripts/connections.ts`.
*
* Jedine misto s dynamickym nazvem promenne: sluzby pribyvaji v katalogu
* a vypisovat kazdou sem by znamenalo dve mista, ktera se rozejdou. Nazev
* sklada volajici, tady se hodnota jen cte a normalizuje - bez mezer, bez
* lomitka na konci. Prazdna nebo chybejici promenna je `null`.
*/
serviceBaseUrlOverride(variable: string): string | null {
const value = (process.env[variable] ?? '').trim().replace(/\/+$/, '');
return value === '' ? null : value;
},
};
/** Zaklad verejne adresy aplikace vcetne prefixu proxy. */
export function publicBaseUrl(): string {
return `${config.publicOrigin}${config.rootPath}`;
}