mirror of
https://github.com/Patiun/com.gregkilmer.www.git
synced 2026-11-08 04:08:24 +00:00
e3b0cf60c5
- 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
74 lines
3.0 KiB
TypeScript
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",
|
|
};
|