import process from "node:process"; const REQUIRED = [ "AGENTS24_API_KEY", "AGENTS24_AGENTS", "AGENTS24_INTEGRATION_ID", "SESSION_COOKIE_SECRET", ] as const; function required(name: (typeof REQUIRED)[number]): string { const value = String(process.env[name] || "").trim(); if (!value) throw new Error(`${name} is required.`); return value; } type RuntimeTarget = "draft" | "published"; type ConfiguredAgent = ({ agentId: string; agentAlias?: never } | { agentAlias: string; agentId?: never }) & { agentVersionId?: string }; function agentsConfig(): { agents: ConfiguredAgent[]; impliedRuntimeTarget?: RuntimeTarget; } { let value: unknown; try { value = JSON.parse(required("AGENTS24_AGENTS")); } catch { throw new Error("AGENTS24_AGENTS must be valid JSON."); } if (!Array.isArray(value) || value.length < 1 || value.length > 50) throw new Error("AGENTS24_AGENTS must contain between one and 50 Agents."); const impliedTargets = new Set(); const agents = value.map((item): ConfiguredAgent => { if (!item || typeof item !== "object" || Array.isArray(item)) throw new Error("AGENTS24_AGENTS entries must be objects."); const record = item as Record; const agentId = String(record.agentId || "").trim(); const agentAlias = String(record.agentAlias || "").trim(); const agentVersion = String(record.agentVersion ?? "").trim().toLowerCase(); const legacyAgentVersionId = String(record.agentVersionId ?? "").trim(); if (Boolean(agentId) === Boolean(agentAlias)) throw new Error("Each AGENTS24_AGENTS entry requires exactly one of agentId or agentAlias."); if (agentAlias && !/^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$/.test(agentAlias)) throw new Error("Each agentAlias must be lowercase kebab-case."); if (agentVersion && legacyAgentVersionId) throw new Error("Use either agentVersion or legacy agentVersionId, not both."); if (agentVersion && !/^(?:draft|latest|v[1-9]\d*|[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})$/i.test(agentVersion)) { throw new Error("Each AGENTS24_AGENTS agentVersion must be draft, latest, a published label such as v1, or a UUID."); } if (legacyAgentVersionId && !/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(legacyAgentVersionId)) { throw new Error("Each legacy agentVersionId must be a UUID."); } const selector = agentVersion || legacyAgentVersionId; if (selector === "draft") impliedTargets.add("draft"); else if (selector) impliedTargets.add("published"); const agentVersionId = selector && !["draft", "latest"].includes(selector) ? selector : undefined; return agentAlias ? { agentAlias, ...(agentVersionId ? { agentVersionId } : {}) } : { agentId, ...(agentVersionId ? { agentVersionId } : {}) }; }); const selectors = agents.map((agent) => "agentAlias" in agent ? `alias:${agent.agentAlias}` : `id:${agent.agentId}`); if (new Set(selectors).size !== agents.length) throw new Error("AGENTS24_AGENTS must contain unique selectors."); if (impliedTargets.size > 1) throw new Error("AGENTS24_AGENTS cannot mix draft selectors with latest or pinned published selectors."); return { agents, impliedRuntimeTarget: impliedTargets.values().next().value }; } export function readServerEnvironment() { const parsedAgents = agentsConfig(); const explicitRuntimeTarget = String(process.env.AGENTS24_RUNTIME_TARGET || "").trim() || undefined; if (explicitRuntimeTarget && !["draft", "published"].includes(explicitRuntimeTarget)) throw new Error("AGENTS24_RUNTIME_TARGET must be draft or published."); if (explicitRuntimeTarget && parsedAgents.impliedRuntimeTarget && explicitRuntimeTarget !== parsedAgents.impliedRuntimeTarget) { throw new Error("AGENTS24_RUNTIME_TARGET conflicts with the configured agentVersion selectors."); } const env = { ...Object.fromEntries(REQUIRED.map((name) => [name, required(name)])) as Record<(typeof REQUIRED)[number], string>, AGENTS24_BASE_URL: String(process.env.AGENTS24_BASE_URL || "https://api.agents24.dev").trim(), AGENTS24_AGENTS: parsedAgents.agents, AGENTS24_RUNTIME_TARGET: explicitRuntimeTarget || parsedAgents.impliedRuntimeTarget, AGENTS24_INTEGRATION_NAME: String(process.env.AGENTS24_INTEGRATION_NAME || "").trim() || undefined, PORT: Number(process.env.PORT || 3001), }; if (!Number.isInteger(env.PORT) || env.PORT < 1 || env.PORT > 65535) throw new Error("PORT must be a valid port."); if (env.SESSION_COOKIE_SECRET.length < 32) throw new Error("SESSION_COOKIE_SECRET must contain at least 32 characters."); return env; } let environment: ReturnType | undefined; export function getServerEnvironment() { return environment ??= readServerEnvironment(); }