/** * ai_working_help API server. * Routes: POST /v1/enqueue, GET /v1/response/:request_uid, GET /health, GET /v1/health. * Project id and env are resolved from the Bearer token by searching all * projects//.secrets//ia_token files; the matching project id is used for the spooler. * Spooler: projects//data/notary-ai/{pending,responded}. */ const express = require("express"); const bodyParser = require("body-parser"); const fs = require("fs"); const path = require("path"); const app = express(); app.use(bodyParser.json({ limit: "1mb" })); const PORT = process.env.AI_WORKING_HELP_PORT || 3020; const IA_DEV_ROOT = path.resolve(__dirname, ".."); const PROJECTS_DIR = path.join(IA_DEV_ROOT, "projects"); /** * Resolve project id and env from token by scanning projects//.secrets//ia_token. * @returns {{ projectId: string, env: string } | null} */ function resolveProjectAndEnvByToken(token) { if (!token || typeof token !== "string") return null; const t = token.trim(); if (!t) return null; const dirs = fs.readdirSync(PROJECTS_DIR, { withFileTypes: true }); for (const d of dirs) { if (!d.isDirectory()) continue; const secretsDir = path.join(PROJECTS_DIR, d.name, ".secrets"); try { if (!fs.existsSync(secretsDir) || !fs.statSync(secretsDir).isDirectory()) continue; const envDirs = fs.readdirSync(secretsDir, { withFileTypes: true }); for (const ed of envDirs) { if (!ed.isDirectory()) continue; const tokenPath = path.join(secretsDir, ed.name, "ia_token"); if (!fs.existsSync(tokenPath) || !fs.statSync(tokenPath).isFile()) continue; const content = fs.readFileSync(tokenPath, "utf8").trim(); const envName = ed.name; // Token is either full value in file (content) or base in file + env suffix: nicolecoffreio if (content === t || content + envName === t) return { projectId: d.name, env: envName }; } } catch (_) { // skip } } return null; } function resolveProjectIdByToken(token) { const resolved = resolveProjectAndEnvByToken(token); return resolved ? resolved.projectId : null; } function requireApiTokenAndResolveProject(req, res, next) { const authHeader = req.headers.authorization; if (!authHeader || typeof authHeader !== "string") { return res.status(401).json({ message: "Invalid or missing token" }); } const match = authHeader.match(/^Bearer\s+(.+)$/i); const token = match ? String(match[1]).trim() : ""; const resolved = resolveProjectAndEnvByToken(token); if (!resolved) { return res.status(401).json({ message: "Invalid or missing token" }); } req.projectId = resolved.projectId; req.projectEnv = resolved.env; next(); } function safeUid(uid) { return String(uid).replace(/[^a-zA-Z0-9-_]/g, "_").slice(0, 128) || "unknown"; } function projectDataDir(projectId) { if (!projectId || /[^a-zA-Z0-9-_]/.test(projectId)) return null; return path.join(PROJECTS_DIR, projectId, "data", "notary-ai"); } function findFileByRequestUid(dir, requestUid) { if (!fs.existsSync(dir)) return null; const files = fs.readdirSync(dir); for (const f of files) { if (!f.endsWith(".json")) continue; const filePath = path.join(dir, f); try { const data = JSON.parse(fs.readFileSync(filePath, "utf8")); if (data.request_uid === requestUid) return { path: filePath, data }; } catch (_) { // skip invalid json } } return null; } app.get("/health", (req, res) => { res.status(200).json({ status: "ok" }); }); app.get("/v1/health", (req, res) => { res.status(200).json({ status: "ok" }); }); app.post("/v1/enqueue", requireApiTokenAndResolveProject, (req, res) => { const dir = projectDataDir(req.projectId); if (!dir) { return res.status(400).json({ message: "Invalid project" }); } const pendingDir = path.join(dir, "pending"); const respondedDir = path.join(dir, "responded"); const body = req.body || {}; const requestUid = body.request_uid; if (!requestUid || typeof requestUid !== "string") { return res.status(400).json({ message: "Missing request_uid" }); } const payload = { request_uid: requestUid, folder_uid: body.folder_uid, office_uid: body.office_uid, user_id: body.user_id, question: body.question, folder_context: body.folder_context || {}, status: "pending", }; try { if (!fs.existsSync(pendingDir)) fs.mkdirSync(pendingDir, { recursive: true }); if (!fs.existsSync(respondedDir)) fs.mkdirSync(respondedDir, { recursive: true }); const safe = safeUid(requestUid); const filePath = path.join(pendingDir, `${safe}.json`); fs.writeFileSync(filePath, JSON.stringify(payload, null, 2), "utf8"); res.status(202).json({ request_uid: requestUid }); } catch (err) { console.error("[ai_working_help] enqueue write error", err); res.status(500).json({ message: "Write failed" }); } }); app.get("/v1/response/:request_uid", requireApiTokenAndResolveProject, (req, res) => { const requestUid = req.params.request_uid; const dir = projectDataDir(req.projectId); if (!dir) { return res.status(400).json({ message: "Invalid project" }); } const respondedDir = path.join(dir, "responded"); const foundResponded = findFileByRequestUid(respondedDir, requestUid); if (foundResponded && foundResponded.data.response) { return res.status(200).json({ status: "responded", response: foundResponded.data.response, }); } const pendingDir = path.join(dir, "pending"); const foundPending = findFileByRequestUid(pendingDir, requestUid); if (foundPending) { return res.status(200).json({ status: "pending" }); } res.status(200).json({ status: "pending" }); }); app.listen(PORT, () => { console.log(`[ai_working_help] listening on port ${PORT}`); });