import { createHash, randomBytes, timingSafeEqual } from "node:crypto"; import { mkdir, readFile, rename, writeFile } from "node:fs/promises"; import { join } from "node:path"; const STORE_VERSION = 1; const SETUP_TTL_MS = 15 * 60_000; const NO_STORE_WRITE = Symbol("no-store-write"); function nowIso(now) { return new Date(now()).toISOString(); } function cleanString(value, max = 240) { return String(value || "").trim().slice(0, max); } function cleanId(value, max = 180) { return cleanString(value, max).replace(/[^A-Za-z0-9_.:@-]/g, ""); } function cleanAvatarUrl(value) { const avatarUrl = cleanString(value, 550_000); if (!avatarUrl) return ""; if (/^https:\/\//i.test(avatarUrl) || /^data:image\/(?:png|jpeg|webp|gif);base64,/i.test(avatarUrl)) return avatarUrl; return ""; } function digest(value) { return createHash("sha256").update(String(value || ""), "utf8").digest("hex"); } function digestMatches(actual, expected) { const actualBuffer = Buffer.from(String(actual || ""), "hex"); const expectedBuffer = Buffer.from(String(expected || ""), "hex"); return actualBuffer.length === expectedBuffer.length && actualBuffer.length > 0 && timingSafeEqual(actualBuffer, expectedBuffer); } function randomToken(prefix, bytes = 32) { return `${prefix}_${randomBytes(bytes).toString("base64url")}`; } function emptyStore() { return { schemaVersion: STORE_VERSION, agents: [], setupCodes: [] }; } function normalizeDevice(value) { const id = cleanId(value?.id); const foundryTokenHash = cleanString(value?.foundryTokenHash, 128); const ontologyTokenHash = cleanString(value?.ontologyTokenHash, 128); if (!id || !foundryTokenHash || !ontologyTokenHash) return null; return { id, name: cleanString(value?.name, 160) || "Codex Desktop", foundryTokenHash, ontologyTokenHash, createdAt: cleanString(value?.createdAt, 80), lastUsedAt: cleanString(value?.lastUsedAt, 80) || null, lastCheckAt: cleanString(value?.lastCheckAt, 80) || null, revokedAt: cleanString(value?.revokedAt, 80) || null, }; } function normalizeAgent(value, now) { const id = cleanId(value?.id); const ownerId = cleanId(value?.ownerId); const ownerKey = cleanString(value?.ownerKey, 320); if (!id || !ownerId || !ownerKey) return null; return { id, ownerId, ownerKey, name: cleanString(value?.name, 160) || "Foundry Codex", avatarUrl: cleanAvatarUrl(value?.avatarUrl), status: value?.status === "revoked" ? "revoked" : "active", devices: Array.isArray(value?.devices) ? value.devices.map(normalizeDevice).filter(Boolean) : [], createdAt: cleanString(value?.createdAt, 80) || nowIso(now), updatedAt: cleanString(value?.updatedAt, 80) || nowIso(now), revokedAt: cleanString(value?.revokedAt, 80) || null, }; } function normalizeSetupCode(value) { const codeHash = cleanString(value?.codeHash, 128); const agentId = cleanId(value?.agentId); const ownerKey = cleanString(value?.ownerKey, 320); if (!codeHash || !agentId || !ownerKey) return null; return { codeHash, agentId, ownerKey, createdAt: cleanString(value?.createdAt, 80), expiresAt: cleanString(value?.expiresAt, 80), redeemedAt: cleanString(value?.redeemedAt, 80) || null, redeemedDeviceId: cleanId(value?.redeemedDeviceId) || null, }; } function normalizeStore(value, now) { return { schemaVersion: STORE_VERSION, agents: Array.isArray(value?.agents) ? value.agents.map((item) => normalizeAgent(item, now)).filter(Boolean) : [], setupCodes: Array.isArray(value?.setupCodes) ? value.setupCodes.map(normalizeSetupCode).filter(Boolean) : [], }; } function publicDevice(device) { return { id: device.id, name: device.name, createdAt: device.createdAt, lastUsedAt: device.lastUsedAt, lastCheckAt: device.lastCheckAt, revokedAt: device.revokedAt, }; } function publicAgent(agent) { return { id: agent.id, ownerId: agent.ownerId, name: agent.name, avatarUrl: agent.avatarUrl || null, status: agent.status, devices: agent.devices.map(publicDevice), deviceCount: agent.devices.filter((device) => !device.revokedAt).length, lastUsedAt: agent.devices.map((device) => device.lastUsedAt).filter(Boolean).sort().at(-1) || null, lastCheckAt: agent.devices.map((device) => device.lastCheckAt).filter(Boolean).sort().at(-1) || null, createdAt: agent.createdAt, updatedAt: agent.updatedAt, revokedAt: agent.revokedAt, }; } export function createFoundryAgentStore({ dataRoot, now = Date.now } = {}) { if (!dataRoot) throw new Error("foundry_agent_data_root_required"); const storePath = join(dataRoot, "agents.json"); const auditPath = join(dataRoot, "audit.ndjson"); let mutationQueue = Promise.resolve(); let auditQueue = Promise.resolve(); async function readStore() { try { return normalizeStore(JSON.parse(await readFile(storePath, "utf8")), now); } catch (error) { if (error?.code === "ENOENT") return emptyStore(); throw error; } } async function writeStore(store) { await mkdir(dataRoot, { recursive: true }); const tempPath = `${storePath}.${process.pid}.${randomBytes(6).toString("hex")}.tmp`; await writeFile(tempPath, `${JSON.stringify(normalizeStore(store, now), null, 2)}\n`, { encoding: "utf8", mode: 0o600 }); await rename(tempPath, storePath); } async function mutate(action) { const run = mutationQueue.then(async () => { const store = await readStore(); const result = await action(store); if (result !== NO_STORE_WRITE) await writeStore(store); return result; }); mutationQueue = run.catch(() => undefined); return run; } async function audit(event) { const run = auditQueue.then(async () => { const record = { schemaVersion: "nodedc.module-foundry.agent-audit.v1", at: nowIso(now), ...event, }; await mkdir(dataRoot, { recursive: true }); const current = await readFile(auditPath, "utf8").catch((error) => error?.code === "ENOENT" ? "" : Promise.reject(error)); const lines = `${current}${JSON.stringify(record)}\n`.trimEnd().split("\n").slice(-2_000); const tempPath = `${auditPath}.${process.pid}.${randomBytes(6).toString("hex")}.tmp`; await writeFile(tempPath, `${lines.join("\n")}\n`, { encoding: "utf8", mode: 0o600 }); await rename(tempPath, auditPath); }); auditQueue = run.catch(() => undefined); return run; } async function listAgents(ownerKey) { const normalizedOwner = cleanString(ownerKey, 320); const store = await readStore(); return store.agents.filter((agent) => agent.ownerKey === normalizedOwner).map(publicAgent); } async function createAgent({ ownerId, ownerKey, name, avatarUrl }) { const normalizedOwnerId = cleanId(ownerId); const normalizedOwnerKey = cleanString(ownerKey, 320); if (!normalizedOwnerId || !normalizedOwnerKey) throw new Error("foundry_agent_owner_required"); const agent = await mutate(async (store) => { const createdAt = nowIso(now); const next = { id: randomToken("foundry_agent", 12), ownerId: normalizedOwnerId, ownerKey: normalizedOwnerKey, name: cleanString(name, 160) || "Foundry Codex", avatarUrl: cleanAvatarUrl(avatarUrl), status: "active", devices: [], createdAt, updatedAt: createdAt, revokedAt: null, }; store.agents.push(next); return publicAgent(next); }); await audit({ event: "agent_created", ownerId: normalizedOwnerId, agentId: agent.id, outcome: "ok" }); return agent; } async function updateAgent(ownerKey, agentId, input = {}) { const normalizedOwner = cleanString(ownerKey, 320); const normalizedId = cleanId(agentId); const agent = await mutate(async (store) => { const target = store.agents.find((item) => item.id === normalizedId && item.ownerKey === normalizedOwner); if (!target) throw new Error("foundry_agent_not_found"); if (target.status === "revoked") throw new Error("foundry_agent_revoked"); if (Object.hasOwn(input, "name")) target.name = cleanString(input.name, 160) || target.name; if (Object.hasOwn(input, "avatarUrl")) target.avatarUrl = cleanAvatarUrl(input.avatarUrl); target.updatedAt = nowIso(now); return publicAgent(target); }); await audit({ event: "agent_updated", agentId: agent.id, outcome: "ok" }); return agent; } async function revokeAgent(ownerKey, agentId) { const normalizedOwner = cleanString(ownerKey, 320); const normalizedId = cleanId(agentId); const agent = await mutate(async (store) => { const target = store.agents.find((item) => item.id === normalizedId && item.ownerKey === normalizedOwner); if (!target) throw new Error("foundry_agent_not_found"); const revokedAt = target.revokedAt || nowIso(now); target.status = "revoked"; target.revokedAt = revokedAt; target.updatedAt = revokedAt; target.devices.forEach((device) => { device.revokedAt = device.revokedAt || revokedAt; }); return publicAgent(target); }); await audit({ event: "agent_revoked", agentId: agent.id, outcome: "ok" }); return agent; } async function issueSetupCode(ownerKey, agentId) { const normalizedOwner = cleanString(ownerKey, 320); const normalizedId = cleanId(agentId); const code = randomToken("fnd_setup", 24); const createdAtMs = now(); const expiresAt = new Date(createdAtMs + SETUP_TTL_MS).toISOString(); await mutate(async (store) => { const agent = store.agents.find((item) => item.id === normalizedId && item.ownerKey === normalizedOwner); if (!agent) throw new Error("foundry_agent_not_found"); if (agent.status !== "active") throw new Error("foundry_agent_revoked"); store.setupCodes = store.setupCodes.filter((item) => Date.parse(item.expiresAt || "") > createdAtMs && !item.redeemedAt); store.setupCodes.push({ codeHash: digest(code), agentId: agent.id, ownerKey: agent.ownerKey, createdAt: new Date(createdAtMs).toISOString(), expiresAt, redeemedAt: null, redeemedDeviceId: null, }); }); await audit({ event: "setup_code_issued", agentId: normalizedId, outcome: "ok" }); return { code, expiresAt }; } async function redeemSetupCode(code, deviceName) { const codeHash = digest(code); const foundryToken = randomToken("fnda", 36); const ontologyToken = randomToken("fndo", 36); const result = await mutate(async (store) => { const setup = store.setupCodes.find((item) => item.codeHash === codeHash); if (!setup) throw new Error("setup_code_invalid"); if (setup.redeemedAt) throw new Error("setup_code_already_redeemed"); if (Date.parse(setup.expiresAt || "") <= now()) throw new Error("setup_code_expired"); const agent = store.agents.find((item) => item.id === setup.agentId && item.ownerKey === setup.ownerKey); if (!agent || agent.status !== "active") throw new Error("foundry_agent_revoked"); const createdAt = nowIso(now); const device = { id: randomToken("foundry_device", 12), name: cleanString(deviceName, 160) || "Codex Desktop", foundryTokenHash: digest(foundryToken), ontologyTokenHash: digest(ontologyToken), createdAt, lastUsedAt: null, lastCheckAt: null, revokedAt: null, }; agent.devices.push(device); agent.updatedAt = createdAt; setup.redeemedAt = createdAt; setup.redeemedDeviceId = device.id; return { agent: publicAgent(agent), device: publicDevice(device) }; }); await audit({ event: "setup_code_redeemed", agentId: result.agent.id, deviceId: result.device.id, outcome: "ok" }); return { ...result, foundryToken, ontologyToken }; } async function authenticate(kind, token, { check = false } = {}) { const tokenHash = digest(token); return mutate(async (store) => { for (const agent of store.agents) { if (agent.status !== "active") continue; for (const device of agent.devices) { const expected = kind === "ontology" ? device.ontologyTokenHash : device.foundryTokenHash; if (!digestMatches(expected, tokenHash) || device.revokedAt) continue; const at = nowIso(now); device.lastUsedAt = at; if (check) device.lastCheckAt = at; agent.updatedAt = at; return { actor: { actorId: agent.ownerId, ownerKey: agent.ownerKey }, agent: publicAgent(agent), device: publicDevice(device), }; } } return NO_STORE_WRITE; }); } return { listAgents, createAgent, updateAgent, revokeAgent, issueSetupCode, redeemSetupCode, authenticateFoundryToken: async (token, options) => { const result = await authenticate("foundry", token, options); return result === NO_STORE_WRITE ? null : result; }, authenticateOntologyToken: async (token, options) => { const result = await authenticate("ontology", token, options); return result === NO_STORE_WRITE ? null : result; }, audit, }; }