Files
csbot-prototype/src/config.ts
T
JiriUhlirandClaude Opus 5 a771834e57 Realne sluzby, OpenAI, odesilani e-mailu a helpdesk
Katalog srovnany s tim, co opravdu bezi na services.csbot.cz/apps:
trinact sluzeb dostalo pristupove udaje a levne cteci overeni, opravena
appId, ktera nikam nevedla (ppl, microsoft365, transcription), a GA4,
Search Console, Google Ads i Sklik ted stoji na aplikaci analytics,
kazda s vlastnimi udaji. Nove sluzby SAP Business One, Google Workspace
a Meta Ads. K tomu 23 skriptu, ktere s nimi opravdu neco delaji.

OpenAI jako prvni sluzba, ktera nebezi u nas: Service.baseUrl s absolutni
adresou, prepis pres <SLUZBA>_BASE_URL nebo adresu u konektoru, predpona
hlavicky u pole udaju (uzivatel vlepi holy klic, Bearer dopise runtime).
Dotaz na model, nahrani souboru, otazka nad souborem, prepis zvuku.
Skript umi odeslat soubor pres ctx.http.postForm (multipart, obsah Base64).

Sluzba E-mail pres SMTP. Neni to skript, ale vnitrni krok - SMTP neni HTTP.
Konektor nese schranku firmy, krok ma HTML telo, ve kterem se dosazene
hodnoty escapuji (znacky autora sablony jsou zamer, ostre zavorky od
zakaznika ne). Overeni konektoru se prihlasi na server a nic neodesle.

Helpdesk: Ticket.helpdeskSourceId drzi firmu, ktera pozadavek poslala,
vlastnikem zustava ta, ktera ho resi - jinak by ho resitel nemel ve sve
fronte. Komu pozadavek pripadne, urcuje Tenant.helpdeskProviderId.
Zadavatel vidi jen svoje pozadavky a smi k nim pripsat komentar.

Opravy v portalu:
- hlasky o ulozisti a odchozi IP vidi jen spravce platformy
- typ ticketu se v automatizaci vybira ze seznamu firmy, nebo dosadi z dat
- stav ticketu je otevreny naseptavac, ne ciselnik
- ticket jde zalozit rucne, zakaznik u nej neni povinny
- kanal se prejmenoval a parametry u webhooku jsou oznacene jako nepovinne
- srovnane markdown tabulky v cele dokumentaci

Co z teto davky jeste neni: prepinac firmy je porad jen stav uvnitr stranky
Prehled, takze se prepnuti neprojevi v Lidech ani jinde.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-26 07:40:16 +02:00

217 lines
8.8 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(/\/+$/, '');
}
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 `<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',
/**
* 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}`;
}