Files
com.gregkilmer.www/services/content/src/config.ts
T
Claude e3b0cf60c5 Phase 6: sync jobs, inquiry and order forms
- Sync scheduler in the content service with a per-job cache (sync_cache),
  sync_runs reporting and Healthchecks pings. Jobs: Archidekt, BoardGameGeek,
  cover art (Open Library, TMDB, AniList), Suwayomi and Kavita reading
  progress, Bambuddy filament and Homebox material stock. A note's own value
  always wins over a synced one.
- PersonalReader: updates only Library notes with a matching anilistId,
  changes to Greg's notes are held for review; Reading series with no note
  show as suggestions on the admin Overview.
- /api/inquiries and /api/orders: Turnstile, rate limits, a saved copy in the
  content service (intake token) plus email over SMTP. Orders are re-checked
  against the live notes: accepted only when the category's ordersOpen is
  true and the line has capacity.
- Admin Inbox for saved submissions. Options' inStock is optional so synced
  stock can fill it. All settings from process.env; .env.example per service.
- Sources could not be reached from the build environment; parsers are tested
  against fixtures labelled as not live.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012aSbo2D1qP8cMFQCykf7kx
2026-10-11 23:50:06 +00:00

74 lines
3.0 KiB
TypeScript

/**
* 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";
import { syncSettings, type SyncSettings } from "./sync/settings.ts";
/** `intake` can only file submissions (the site's forms); it can read nothing. */
export type Role = "reader" | "agent" | "editor" | "admin" | "intake";
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<string, Role>;
/** 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;
/** Which sync jobs run, their intervals, Healthchecks URLs and keys. */
sync: SyncSettings;
/** Largest attachment a submission may carry, in bytes. */
submissionMaxBytes: number;
}
const roles = new Set<Role>(["reader", "agent", "editor", "admin", "intake"]);
/** CONTENT_TOKENS is a comma separated list of role:token pairs. */
export function parseTokens(raw: string | undefined): Map<string, Role> {
const tokens = new Map<string, Role>();
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),
sync: syncSettings(env),
submissionMaxBytes: Math.round(Number(env.SUBMISSION_MAX_MB ?? 10) * 1024 * 1024),
};
}
/** What each role may do. Checked by the HTTP API and the MCP server alike. */
export const can = {
readDrafts: (r: Role) => r !== "reader" && r !== "intake",
write: (r: Role) => r === "agent" || r === "editor" || r === "admin",
review: (r: Role) => r === "editor" || r === "admin",
rollback: (r: Role) => r === "admin",
};