/** * Settings come from the environment, so the same image runs on Cosmos and in * tests. Nothing here is read from the vault: an agent that can edit notes * cannot change who may write or what the rules allow. */ import { resolve } from "node:path"; export type Role = "reader" | "agent" | "editor" | "admin"; export interface Config { /** The vault's Site folder. */ siteDir: string; /** The SQLite file holding accepted notes, history and the review queue. */ dbFile: string; port: number; host: string; /** Bearer token to role. */ tokens: Map; /** Pinged after every full scan, so a stopped watcher is noticed. */ healthcheckUrl?: string; /** Receives a JSON message whenever a change is held. */ notifyWebhookUrl?: string; allowedEmails: string[]; scanMinutes: number; } const roles = new Set(["reader", "agent", "editor", "admin"]); /** CONTENT_TOKENS is a comma separated list of role:token pairs. */ export function parseTokens(raw: string | undefined): Map { const tokens = new Map(); for (const pair of (raw ?? "").split(",").map((s) => s.trim()).filter(Boolean)) { const at = pair.indexOf(":"); const role = pair.slice(0, at) as Role; const token = pair.slice(at + 1); if (at < 1 || !roles.has(role)) throw new Error(`CONTENT_TOKENS: "${pair.slice(0, Math.max(at, 0))}" is not a role`); if (token.length < 24) throw new Error(`CONTENT_TOKENS: the ${role} token is shorter than 24 characters`); tokens.set(token, role); } return tokens; } export function loadConfig(env: NodeJS.ProcessEnv = process.env): Config { if (!env.SITE_DIR) throw new Error("SITE_DIR is required: the vault's Site folder"); const dataDir = resolve(env.DATA_DIR ?? "data"); return { siteDir: resolve(env.SITE_DIR), dbFile: resolve(dataDir, "content.db"), port: Number(env.PORT ?? 4310), host: env.HOST ?? "0.0.0.0", tokens: parseTokens(env.CONTENT_TOKENS), healthcheckUrl: env.HEALTHCHECK_URL || undefined, notifyWebhookUrl: env.NOTIFY_WEBHOOK_URL || undefined, allowedEmails: (env.ALLOWED_EMAILS ?? "").split(",").map((s) => s.trim().toLowerCase()).filter(Boolean), scanMinutes: Number(env.SCAN_MINUTES ?? 5), }; } /** What each role may do. Checked by the HTTP API and the MCP server alike. */ export const can = { readDrafts: (r: Role) => r !== "reader", write: (r: Role) => r === "agent" || r === "editor" || r === "admin", review: (r: Role) => r === "editor" || r === "admin", rollback: (r: Role) => r === "admin", };