agentinbox-cli 0.1.0-nightly.1 → 0.1.0-nightly.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/audit-Bf3TvAAN.js +2 -0
- package/dist/{audit-DETqygzG.js → audit-CNYWHcik.js} +2 -2
- package/dist/{audit-DETqygzG.js.map → audit-CNYWHcik.js.map} +1 -1
- package/dist/{auth-BniddcRD.js → auth-B3ukRgpB.js} +3 -3
- package/dist/{auth-BniddcRD.js.map → auth-B3ukRgpB.js.map} +1 -1
- package/dist/{auth-C2v7bEmQ.js → auth-B9dqCyaj.js} +1 -1
- package/dist/cli.js +9 -9
- package/dist/{db-DdagFmUY.js → db-B2pnfQW5.js} +3 -2
- package/dist/{db-DdagFmUY.js.map → db-B2pnfQW5.js.map} +1 -1
- package/dist/db-CXhlUuVP.js +2 -0
- package/dist/{errors-CJLUPllS.js → errors-FbMS3QBo.js} +3 -2
- package/dist/{errors-CJLUPllS.js.map → errors-FbMS3QBo.js.map} +1 -1
- package/dist/instance-BEEO0AHU.js +2 -0
- package/dist/{instance-Dy2jeyZc.js → instance-BnbMB2EK.js} +11 -2
- package/dist/instance-BnbMB2EK.js.map +1 -0
- package/dist/server-BvX0c9MQ.js +4682 -0
- package/dist/server-BvX0c9MQ.js.map +1 -0
- package/dist/{service-63pdSmhO.js → service-lFSuvmgq.js} +3 -3
- package/dist/{service-63pdSmhO.js.map → service-lFSuvmgq.js.map} +1 -1
- package/dist/{settings-Dq2ETFsd.js → settings-DkA2sgzt.js} +3 -3
- package/dist/{settings-Dq2ETFsd.js.map → settings-DkA2sgzt.js.map} +1 -1
- package/dist/settings-DoPS659W.js +2 -0
- package/dist/{setup-token-Bkwk84l7.js → setup-token-BO89xnpb.js} +3 -2
- package/dist/{setup-token-Bkwk84l7.js.map → setup-token-BO89xnpb.js.map} +1 -1
- package/dist/web/assets/{_agentId-il4y7OCG.js → _agentId-Cq2tty9r.js} +1 -1
- package/dist/web/assets/{_app-wlLL4MrD.js → _app-Cb1baRA0.js} +1 -1
- package/dist/web/assets/{_app-BIO8WIYB.js → _app-DU_bk7n8.js} +1 -1
- package/dist/web/assets/{_webhookId-CiwDrkY4.js → _webhookId-B55gK0Lb.js} +1 -1
- package/dist/web/assets/{account-status-at9Bv3_T.js → account-status-C08BBEsU.js} +1 -1
- package/dist/web/assets/{accounts-B4rfPwCV.js → accounts-Bk1s8Uzv.js} +1 -1
- package/dist/web/assets/{accounts-list-e--JWxHH.js → accounts-list-F-kO3jiT.js} +1 -1
- package/dist/web/assets/{agent-create-form-DaWPR6kx.js → agent-create-form-BxTkdezg.js} +1 -1
- package/dist/web/assets/{agents-D4gPq4dt.js → agents-CmbZ2lwf.js} +1 -1
- package/dist/web/assets/{approvals-DaoZ6wuf.js → approvals-3DYaHsIJ.js} +1 -1
- package/dist/web/assets/{audit-bAdLe-px.js → audit-CDXxi4Hk.js} +1 -1
- package/dist/web/assets/{audit-bits-DQHo74i6.js → audit-bits-D8AkB5ZU.js} +1 -1
- package/dist/web/assets/{chips-input-Btdc_f7b.js → chips-input-D_rUs_lc.js} +1 -1
- package/dist/web/assets/{common-h6596gEC.js → common-Cc3JNp4n.js} +1 -1
- package/dist/web/assets/{connect-host-Co31qIYr.js → connect-host-BV4t499t.js} +1 -1
- package/dist/web/assets/{data-table-DsKrM62X.js → data-table-CyhIs-ZM.js} +1 -1
- package/dist/web/assets/{dialog-CgZr27yw.js → dialog-Da2XSU3Y.js} +1 -1
- package/dist/web/assets/{event-row-C0qRfWuV.js → event-row-BsPXPNKL.js} +1 -1
- package/dist/web/assets/{events-Svyij7mp.js → events-eSr5JR5h.js} +1 -1
- package/dist/web/assets/{get-started-Cxfw97sJ.js → get-started-CGYjo7e7.js} +1 -1
- package/dist/web/assets/{grants-BilXCDps.js → grants-BOBZxwBG.js} +1 -1
- package/dist/web/assets/index-C0ZXKtiq.css +2 -0
- package/dist/web/assets/{index-TbxC5ZgP.js → index-MbDFbrbR.js} +2 -2
- package/dist/web/assets/{login-DUJia7lg.js → login-DTteGF6c.js} +1 -1
- package/dist/web/assets/{mutations-D_GluJ0j.js → mutations-C9NgE3M4.js} +1 -1
- package/dist/web/assets/{select-_tULk-vs.js → select-B7o4J-XI.js} +1 -1
- package/dist/web/assets/{settings-Dl4wb4f0.js → settings-CundYHSp.js} +1 -1
- package/dist/web/assets/{settings-form-D21eKBh-.js → settings-form-BDGVJnAI.js} +1 -1
- package/dist/web/assets/{setup-DYnNhnfH.js → setup-BHRb8np_.js} +1 -1
- package/dist/web/assets/{sheet--2lKU0jN.js → sheet-CbWxq7yd.js} +1 -1
- package/dist/web/assets/{tabs-CiAUEj_y.js → tabs-ChnOxkjp.js} +1 -1
- package/dist/web/assets/{test-connection-F2tMowbv.js → test-connection-AUx4zhwY.js} +1 -1
- package/dist/web/assets/{toggle-group-D1pC7sJr.js → toggle-group-BjRrXeJH.js} +1 -1
- package/dist/web/assets/{useMutation-DGl4PaZ1.js → useMutation-BGhPR1Vu.js} +1 -1
- package/dist/web/assets/{webhook-sheet-4U_6Rw8I.js → webhook-sheet-DQJqHwl0.js} +1 -1
- package/dist/web/assets/{webhooks-C1JvTI88.js → webhooks-DS8Xc6Gc.js} +1 -1
- package/dist/web/index.html +2 -2
- package/package.json +2 -1
- package/dist/audit-CQe-_dPQ.js +0 -2
- package/dist/db-CfoMO5zW.js +0 -2
- package/dist/instance-DZMV78rI.js +0 -2
- package/dist/instance-Dy2jeyZc.js.map +0 -1
- package/dist/server-CWqKeCGn.js +0 -981
- package/dist/server-CWqKeCGn.js.map +0 -1
- package/dist/settings-qtn5WTBM.js +0 -2
- package/dist/web/assets/index-DgIgw66p.css +0 -2
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import {
|
|
2
|
+
import { i as now, r as json } from "./db-B2pnfQW5.js";
|
|
3
3
|
//#region server/audit.ts
|
|
4
4
|
const SELECT = `SELECT a.*, CASE a.actor_type WHEN 'agent' THEN ag.name WHEN 'admin' THEN 'admin' ELSE NULL END AS actor_name
|
|
5
5
|
FROM audit_log a LEFT JOIN agents ag ON a.actor_type = 'agent' AND ag.id = a.actor_id`;
|
|
@@ -62,4 +62,4 @@ var AuditLog = class {
|
|
|
62
62
|
//#endregion
|
|
63
63
|
export { AuditLog as t };
|
|
64
64
|
|
|
65
|
-
//# sourceMappingURL=audit-
|
|
65
|
+
//# sourceMappingURL=audit-CNYWHcik.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"audit-
|
|
1
|
+
{"version":3,"file":"audit-CNYWHcik.js","names":[],"sources":["../server/audit.ts"],"sourcesContent":["import type { ActorType, AuditEntry, AuditOutcome, Page } from \"../shared/types.ts\";\nimport type { Bus } from \"./bus.ts\";\nimport { type Db, json, now } from \"./db/index.ts\";\n\nexport interface AuditInput {\n actorType: ActorType;\n actorId?: string | null;\n /** MCP tool name or console action, e.g. \"send_message\", \"agent.create\", \"account.connect\". */\n action: string;\n accountId?: string | null;\n target?: string | null;\n outcome: AuditOutcome;\n /** Required for denials: the guardrail that blocked it (\"allow_to\", \"label_scope\", \"rate_limit\", …). */\n reason?: string | null;\n durationMs?: number | null;\n /** Small, non-sensitive context. Never message bodies, keys or tokens. */\n detail?: Record<string, unknown> | null;\n}\n\nexport interface AuditQuery {\n agentId?: string;\n accountId?: string;\n outcome?: AuditOutcome;\n action?: string;\n /** Opaque cursor from a previous page. */\n cursor?: string | null;\n limit?: number;\n}\n\ninterface AuditRow {\n id: number;\n ts: string;\n actor_type: ActorType;\n actor_id: string | null;\n actor_name: string | null;\n action: string;\n account_id: string | null;\n target: string | null;\n outcome: AuditOutcome;\n reason: string | null;\n duration_ms: number | null;\n detail: string | null;\n}\n\nconst SELECT = `SELECT a.*, CASE a.actor_type WHEN 'agent' THEN ag.name WHEN 'admin' THEN 'admin' ELSE NULL END AS actor_name\n FROM audit_log a LEFT JOIN agents ag ON a.actor_type = 'agent' AND ag.id = a.actor_id`;\n\nconst toEntry = (r: AuditRow): AuditEntry => ({\n id: r.id,\n ts: r.ts,\n actorType: r.actor_type,\n actorId: r.actor_id,\n actorName: r.actor_name,\n action: r.action,\n accountId: r.account_id,\n target: r.target,\n outcome: r.outcome,\n reason: r.reason,\n durationMs: r.duration_ms,\n detail: json.parse<Record<string, unknown> | null>(r.detail, null),\n});\n\n/** Append-only audit log. Every agent tool call and every admin mutation is recorded. */\nexport class AuditLog {\n constructor(\n private readonly db: Db,\n private readonly bus: Bus,\n ) {}\n\n record(input: AuditInput): AuditEntry {\n const { lastInsertRowid } = this.db.run(\n `INSERT INTO audit_log (ts, actor_type, actor_id, action, account_id, target, outcome, reason, duration_ms, detail)\n VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,\n now(),\n input.actorType,\n input.actorId ?? null,\n input.action,\n input.accountId ?? null,\n input.target ?? null,\n input.outcome,\n input.reason ?? null,\n input.durationMs ?? null,\n input.detail ? json.str(input.detail) : null,\n );\n const entry = toEntry(this.db.get<AuditRow>(`${SELECT} WHERE a.id = ?`, lastInsertRowid)!);\n this.bus.emit(\"audit\", entry);\n return entry;\n }\n\n /** Newest first. */\n list(q: AuditQuery = {}): Page<AuditEntry> {\n const where: string[] = [];\n const params: (string | number)[] = [];\n const add = (clause: string, value: string | number) => {\n where.push(clause);\n params.push(value);\n };\n if (q.agentId) add(\"a.actor_id = ?\", q.agentId);\n if (q.accountId) add(\"a.account_id = ?\", q.accountId);\n if (q.outcome) add(\"a.outcome = ?\", q.outcome);\n if (q.action) add(\"a.action = ?\", q.action);\n const before = Number(q.cursor);\n if (q.cursor && Number.isInteger(before)) add(\"a.id < ?\", before);\n const limit = Math.min(Math.max(q.limit ?? 50, 1), 200);\n const rows = this.db.all<AuditRow>(\n `${SELECT} ${where.length ? \"WHERE \" + where.join(\" AND \") : \"\"} ORDER BY a.id DESC LIMIT ?`,\n ...params,\n limit + 1,\n );\n const items = rows.slice(0, limit).map(toEntry);\n return { items, nextCursor: rows.length > limit ? String(items.at(-1)!.id) : null };\n }\n\n /** Deletes entries older than `beforeIso`. Returns the number removed. */\n prune(beforeIso: string): number {\n return this.db.run(\"DELETE FROM audit_log WHERE ts < ?\", beforeIso).changes;\n }\n}\n"],"mappings":";;;AA4CA,MAAM,SAAS;;AAGf,MAAM,WAAW,OAA6B;CAC5C,IAAI,EAAE;CACN,IAAI,EAAE;CACN,WAAW,EAAE;CACb,SAAS,EAAE;CACX,WAAW,EAAE;CACb,QAAQ,EAAE;CACV,WAAW,EAAE;CACb,QAAQ,EAAE;CACV,SAAS,EAAE;CACX,QAAQ,EAAE;CACV,YAAY,EAAE;CACd,QAAQ,KAAK,MAAsC,EAAE,QAAQ,IAAI;AACnE;;AAGA,IAAa,WAAb,MAAsB;CAED;CACA;CAFnB,YACE,IACA,KACA;EAFiB,KAAA,KAAA;EACA,KAAA,MAAA;CAChB;CAEH,OAAO,OAA+B;EACpC,MAAM,EAAE,oBAAoB,KAAK,GAAG,IAClC;+CAEA,IAAI,GACJ,MAAM,WACN,MAAM,WAAW,MACjB,MAAM,QACN,MAAM,aAAa,MACnB,MAAM,UAAU,MAChB,MAAM,SACN,MAAM,UAAU,MAChB,MAAM,cAAc,MACpB,MAAM,SAAS,KAAK,IAAI,MAAM,MAAM,IAAI,IAC1C;EACA,MAAM,QAAQ,QAAQ,KAAK,GAAG,IAAc,GAAG,OAAO,kBAAkB,eAAe,CAAE;EACzF,KAAK,IAAI,KAAK,SAAS,KAAK;EAC5B,OAAO;CACT;;CAGA,KAAK,IAAgB,CAAC,GAAqB;EACzC,MAAM,QAAkB,CAAC;EACzB,MAAM,SAA8B,CAAC;EACrC,MAAM,OAAO,QAAgB,UAA2B;GACtD,MAAM,KAAK,MAAM;GACjB,OAAO,KAAK,KAAK;EACnB;EACA,IAAI,EAAE,SAAS,IAAI,kBAAkB,EAAE,OAAO;EAC9C,IAAI,EAAE,WAAW,IAAI,oBAAoB,EAAE,SAAS;EACpD,IAAI,EAAE,SAAS,IAAI,iBAAiB,EAAE,OAAO;EAC7C,IAAI,EAAE,QAAQ,IAAI,gBAAgB,EAAE,MAAM;EAC1C,MAAM,SAAS,OAAO,EAAE,MAAM;EAC9B,IAAI,EAAE,UAAU,OAAO,UAAU,MAAM,GAAG,IAAI,YAAY,MAAM;EAChE,MAAM,QAAQ,KAAK,IAAI,KAAK,IAAI,EAAE,SAAS,IAAI,CAAC,GAAG,GAAG;EACtD,MAAM,OAAO,KAAK,GAAG,IACnB,GAAG,OAAO,GAAG,MAAM,SAAS,WAAW,MAAM,KAAK,OAAO,IAAI,GAAG,8BAChE,GAAG,QACH,QAAQ,CACV;EACA,MAAM,QAAQ,KAAK,MAAM,GAAG,KAAK,CAAC,CAAC,IAAI,OAAO;EAC9C,OAAO;GAAE;GAAO,YAAY,KAAK,SAAS,QAAQ,OAAO,MAAM,GAAG,EAAE,CAAC,CAAE,EAAE,IAAI;EAAK;CACpF;;CAGA,MAAM,WAA2B;EAC/B,OAAO,KAAK,GAAG,IAAI,sCAAsC,SAAS,CAAC,CAAC;CACtE;AACF"}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { t as HttpError } from "./errors-
|
|
2
|
+
import { t as HttpError } from "./errors-FbMS3QBo.js";
|
|
3
3
|
import { i as sha256 } from "./crypto-DYZ4R7c2.js";
|
|
4
|
-
import { i as regenerateSetupToken, n as ensureSetupToken } from "./setup-token-
|
|
4
|
+
import { i as regenerateSetupToken, n as ensureSetupToken } from "./setup-token-BO89xnpb.js";
|
|
5
5
|
import { isIP, isIPv4, isIPv6 } from "node:net";
|
|
6
6
|
import { getConnInfo } from "@hono/node-server/conninfo";
|
|
7
7
|
import { deleteCookie, getCookie, setCookie } from "hono/cookie";
|
|
@@ -444,4 +444,4 @@ function rejection(c, check) {
|
|
|
444
444
|
//#endregion
|
|
445
445
|
export { resetAdminPassword as _, adminAuth as a, csrfGuard as c, hasSession as d, isClaimed as f, requireAdmin as g, rejection as h, Semaphore as i, currentSetupToken as l, lockedOut as m, MAX_PASSWORD_LENGTH as n, checkSecret as o, isPrivateAddress as p, SESSION_COOKIE as r, clientIp as s, CSRF_HEADER as t, endSession as u, sessionToken as v, startSession as y };
|
|
446
446
|
|
|
447
|
-
//# sourceMappingURL=auth-
|
|
447
|
+
//# sourceMappingURL=auth-B3ukRgpB.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"auth-BniddcRD.js","names":[],"sources":["../server/auth/lockout.ts","../server/auth/sessions.ts","../server/auth/index.ts"],"sourcesContent":["import { isIPv6 } from \"node:net\";\nimport { sha256 } from \"../crypto.ts\";\n\n/** Distinct wrong secrets per client within the window before a ban. */\nexport const MAX_FAILURES = 5;\n/** Raw failed attempts per client within the window (repeats of the same wrong secret included). */\nexport const MAX_ATTEMPTS = 30;\n/** Distinct wrong secrets across all clients within the window before every attempt is slowed down. */\nexport const GLOBAL_MAX_FAILURES = 100;\n/** Delay per attempt while the global budget is exceeded. */\nexport const GLOBAL_SLOWDOWN_MS = 2000;\nexport const WINDOW_MS = 15 * 60_000;\nexport const BAN_MS = 15 * 60_000;\n/** Tracked clients; the least recently seen is dropped beyond this. */\nexport const MAX_ENTRIES = 10_000;\n\ninterface Entry {\n tried: Set<string>;\n attempts: number;\n since: number;\n bannedUntil: number;\n}\n\n/**\n * Failed sign-in limiter. Per client (IPv4 address or IPv6 /64): 5 distinct wrong secrets or 30\n * attempts within 15 minutes ban it for 15 minutes. A secret already known to be wrong is\n * rejected without hashing (`knownBad`) and counts only as an attempt, so a client retrying a stale\n * password can't spend every attempt, and can't make the server hash it again either. Past a global\n * budget of distinct failures, every attempt is slowed down (never refused, so many clients can't\n * lock the owner out).\n */\nexport class LoginLimiter {\n private readonly entries = new Map<string, Entry>();\n private global = { failures: 0, since: 0 };\n\n constructor(private readonly clock: () => number = Date.now) {}\n\n /** Minutes until this client may try again, or 0. */\n bannedMinutes(ip: string): number {\n const t = this.clock();\n const entry = this.entries.get(clientKey(ip));\n const until = entry && entry.bannedUntil > t ? entry.bannedUntil : 0;\n return until > t ? Math.ceil((until - t) / 60_000) : 0;\n }\n\n /** The global failure budget is exhausted: slow every attempt down. */\n overGlobalBudget(): boolean {\n return this.global.failures >= GLOBAL_MAX_FAILURES && this.clock() - this.global.since < WINDOW_MS;\n }\n\n /** The client already failed with exactly this secret in the current window. */\n knownBad(ip: string, secret: string): boolean {\n const entry = this.current(clientKey(ip), this.clock());\n return !!entry?.tried.has(sha256(secret));\n }\n\n /** Records a failure; returns true when it started a ban for this client. `global: false` keeps it out of the global budget. */\n fail(ip: string, secret: string, opts: { global?: boolean } = {}): boolean {\n return this.record(ip, secret, opts).banned;\n }\n\n /**\n * Counts an attempt with this secret now, before it is verified, so concurrent requests can't\n * all pass the ban check first. `countedGlobally` says whether it used global budget (refund it\n * with `refundGlobal` when the secret turns out to be right).\n */\n record(ip: string, secret: string, opts: { global?: boolean } = {}): { banned: boolean; countedGlobally: boolean } {\n const t = this.clock();\n const key = clientKey(ip);\n const entry = this.current(key, t) ?? { tried: new Set<string>(), attempts: 0, since: t, bannedUntil: 0 };\n const hash = sha256(secret);\n let countedGlobally = false;\n if (!entry.tried.has(hash)) {\n entry.tried.add(hash);\n if (opts.global !== false) {\n if (t - this.global.since >= WINDOW_MS) this.global = { failures: 0, since: t };\n this.global.failures++;\n countedGlobally = true;\n }\n }\n entry.attempts++;\n const banned = entry.tried.size >= MAX_FAILURES || entry.attempts >= MAX_ATTEMPTS;\n if (banned) entry.bannedUntil = t + BAN_MS;\n this.touch(key, entry);\n return { banned, countedGlobally };\n }\n\n refundGlobal(): void {\n if (this.global.failures > 0) this.global.failures--;\n }\n\n clear(ip: string): void {\n this.entries.delete(clientKey(ip));\n }\n\n /** After the password changes, previously wrong secrets may be right. */\n reset(): void {\n this.entries.clear();\n this.global = { failures: 0, since: 0 };\n }\n\n get size(): number {\n return this.entries.size;\n }\n\n /** The client's entry if its window (or ban) is still running. */\n private current(key: string, t: number): Entry | undefined {\n const entry = this.entries.get(key);\n if (!entry) return undefined;\n if (entry.bannedUntil > t || (!entry.bannedUntil && t - entry.since < WINDOW_MS)) return entry;\n this.entries.delete(key);\n return undefined;\n }\n\n /** Map order doubles as LRU order: re-insert on use, evict from the front. */\n private touch(key: string, entry: Entry): void {\n this.entries.delete(key);\n this.entries.set(key, entry);\n while (this.entries.size > MAX_ENTRIES) this.entries.delete(this.entries.keys().next().value!);\n }\n}\n\n/** IPv4 addresses as is (also when IPv4-mapped); IPv6 by /64, since one client usually owns the whole prefix. */\nexport function clientKey(ip: string): string {\n const addr = ip.replace(/%.*$/, \"\").toLowerCase();\n const mapped = /^::ffff:(\\d+\\.\\d+\\.\\d+\\.\\d+)$/.exec(addr);\n if (mapped) return mapped[1]!;\n if (!isIPv6(addr)) return addr;\n const [head = \"\", tail] = addr.split(\"::\");\n const left = head ? head.split(\":\") : [];\n const right = tail ? tail.split(\":\") : [];\n const groups = tail === undefined ? left : [...left, ...Array(8 - left.length - right.length).fill(\"0\"), ...right];\n return `${groups\n .slice(0, 4)\n .map((g) => g.replace(/^0+(?=.)/, \"\"))\n .join(\":\")}::/64`;\n}\n","import { randomBytes } from \"node:crypto\";\nimport { sha256 } from \"../crypto.ts\";\nimport type { Db } from \"../db/index.ts\";\n\nexport const SESSION_TTL_MS = 30 * 24 * 3600_000;\n/** Sliding refresh never extends a session past this age: sign in again after 90 days. */\nexport const SESSION_MAX_AGE_MS = 90 * 24 * 3600_000;\n/** Sliding expiry is pushed forward at most once per this interval, to avoid a write per request. */\nconst REFRESH_AFTER_MS = 24 * 3600_000;\n\nexport interface SessionCheck {\n valid: boolean;\n /** The expiry was extended: re-send the cookie so the browser keeps it as long. */\n refreshed: boolean;\n}\n\n/** Admin console sessions. Only sha256(token) is stored. */\nexport class SessionStore {\n constructor(\n private readonly db: Db,\n private readonly clock: () => number = Date.now,\n ) {}\n\n create(meta: { ip?: string | null; userAgent?: string | null } = {}): string {\n const token = randomBytes(32).toString(\"base64url\");\n const t = this.clock();\n this.db.run(\n \"INSERT INTO admin_sessions (token_hash, created_at, expires_at, ip, user_agent) VALUES (?, ?, ?, ?, ?)\",\n sha256(token),\n new Date(t).toISOString(),\n new Date(t + SESSION_TTL_MS).toISOString(),\n meta.ip ?? null,\n meta.userAgent?.slice(0, 300) ?? null,\n );\n return token;\n }\n\n check(token: string | undefined | null): SessionCheck {\n if (!token) return { valid: false, refreshed: false };\n const hash = sha256(token);\n const row = this.db.get<{ created_at: string; expires_at: string }>(\n \"SELECT created_at, expires_at FROM admin_sessions WHERE token_hash = ?\",\n hash,\n );\n if (!row) return { valid: false, refreshed: false };\n const t = this.clock();\n const expires = Date.parse(row.expires_at);\n const hardLimit = Date.parse(row.created_at) + SESSION_MAX_AGE_MS;\n if (!(expires > t) || !(hardLimit > t)) {\n this.db.run(\"DELETE FROM admin_sessions WHERE token_hash = ?\", hash);\n return { valid: false, refreshed: false };\n }\n const next = Math.min(t + SESSION_TTL_MS, hardLimit);\n if (next - expires < REFRESH_AFTER_MS) return { valid: true, refreshed: false };\n this.db.run(\"UPDATE admin_sessions SET expires_at = ? WHERE token_hash = ?\", new Date(next).toISOString(), hash);\n return { valid: true, refreshed: true };\n }\n\n delete(token: string): void {\n this.db.run(\"DELETE FROM admin_sessions WHERE token_hash = ?\", sha256(token));\n }\n\n /** Revokes every session, optionally keeping one (the caller's own). */\n deleteAll(exceptToken?: string | null): number {\n if (exceptToken) return this.db.run(\"DELETE FROM admin_sessions WHERE token_hash != ?\", sha256(exceptToken)).changes;\n return this.db.run(\"DELETE FROM admin_sessions\").changes;\n }\n\n pruneExpired(): number {\n return this.db.run(\"DELETE FROM admin_sessions WHERE expires_at <= ?\", new Date(this.clock()).toISOString()).changes;\n }\n}\n","import { isIP, isIPv4, isIPv6 } from \"node:net\";\nimport { getConnInfo } from \"@hono/node-server/conninfo\";\nimport type { Context, MiddlewareHandler } from \"hono\";\nimport { deleteCookie, getCookie, setCookie } from \"hono/cookie\";\nimport type { AppEnv } from \"../api/util.ts\";\nimport type { CoreContext } from \"../context.ts\";\nimport { HttpError } from \"../util/errors.ts\";\nimport { clientKey, GLOBAL_SLOWDOWN_MS, LoginLimiter } from \"./lockout.ts\";\nimport { SESSION_TTL_MS, SessionStore } from \"./sessions.ts\";\nimport { ensureSetupToken, regenerateSetupToken } from \"./setup-token.ts\";\n\nexport const SESSION_COOKIE = \"aib_session\";\nexport const CSRF_HEADER = \"x-agentinbox-csrf\";\nexport const MIN_PASSWORD_LENGTH = 10;\nexport const MAX_PASSWORD_LENGTH = 1024;\n\n/** Secret checks (scrypt) allowed at once: libuv's default thread pool size. */\nexport const MAX_CONCURRENT_VERIFICATIONS = 4;\n/** Requests from one client that may wait behind its in-flight check; more get 429. */\nexport const MAX_QUEUED_PER_CLIENT = 4;\n\n/** Hands out at most `max` slots; waiters are served in order. */\nexport class Semaphore {\n private active = 0;\n private readonly waiters: (() => void)[] = [];\n\n constructor(private readonly max: number) {}\n\n get inUse(): number {\n return this.active;\n }\n\n async acquire(): Promise<() => void> {\n if (this.active < this.max) this.active++;\n else await new Promise<void>((resolve) => this.waiters.push(resolve));\n let released = false;\n return () => {\n if (released) return;\n released = true;\n const next = this.waiters.shift();\n if (next) next();\n else this.active--;\n };\n }\n}\n\n/** Per-context admin auth state: the session store, the failed-login limiter and verification queues. */\nexport interface AdminAuth {\n sessions: SessionStore;\n limiter: LoginLimiter;\n /** One secret check per client at a time: the tail of each client's queue. */\n lanes: Map<string, { tail: Promise<void>; queued: number }>;\n verifySlots: Semaphore;\n}\n\n/** What admin auth needs from the context (the CLI builds this without loading the master key). */\nexport type AuthCore = Pick<CoreContext, \"db\" | \"settings\" | \"audit\" | \"config\">;\n\nconst instances = new WeakMap<AuthCore, AdminAuth>();\n\nexport function adminAuth(ctx: AuthCore): AdminAuth {\n let auth = instances.get(ctx);\n if (!auth) {\n auth = { sessions: new SessionStore(ctx.db), limiter: new LoginLimiter(), lanes: new Map(), verifySlots: new Semaphore(MAX_CONCURRENT_VERIFICATIONS) };\n instances.set(ctx, auth);\n }\n return auth;\n}\n\n/** Claimed = an admin password exists. Before that only the setup-token claim works. */\nexport const isClaimed = (ctx: AuthCore): boolean => !!ctx.settings.getState(\"admin.passwordHash\");\n\n/** The current setup token while unclaimed (created on demand), otherwise null. */\nexport function currentSetupToken(ctx: AuthCore): string | null {\n return isClaimed(ctx) ? null : ensureSetupToken(ctx.config.dataDir);\n}\n\n/** `agentinbox reset-password`: back to the unclaimed state with a fresh setup link. */\nexport function resetAdminPassword(ctx: AuthCore): string {\n const auth = adminAuth(ctx);\n ctx.db.tx(() => {\n ctx.settings.deleteState(\"admin.passwordHash\");\n auth.sessions.deleteAll();\n });\n auth.limiter.reset();\n ctx.audit.record({ actorType: \"system\", action: \"admin.password_reset\", outcome: \"ok\" });\n return regenerateSetupToken(ctx.config.dataDir);\n}\n\n/** Loopback, RFC 1918, CGNAT (100.64/10) and IPv6 ULA (fc00::/7): where a reverse proxy can be. */\nexport function isPrivateAddress(ip: string): boolean {\n const addr = ip.replace(/%.*$/, \"\").toLowerCase().replace(/^::ffff:(?=\\d+\\.\\d+\\.\\d+\\.\\d+$)/, \"\");\n if (isIPv4(addr)) {\n const [a = 0, b = 0] = addr.split(\".\").map(Number);\n return a === 127 || a === 10 || (a === 172 && b >= 16 && b <= 31) || (a === 192 && b === 168) || (a === 100 && b >= 64 && b <= 127);\n }\n if (isIPv6(addr)) return addr === \"::1\" || /^f[cd][0-9a-f]{0,2}:/.test(addr);\n return false;\n}\n\nfunction socketAddress(c: Context): string | null {\n try {\n return getConnInfo(c).remote.address ?? null;\n } catch {\n // No Node socket (e.g. app.request() without bindings).\n return null;\n }\n}\n\n/**\n * The client address. X-Forwarded-For is honored only with TRUST_PROXY and only when the\n * connection comes from a loopback or private address (where the proxy runs); it is read from the\n * right, since proxies append the address they saw and entries further left are client-controlled.\n * TRUST_PROXY_HOPS (default 1) is the number of proxies in front of agentinbox.\n */\nexport function clientIp(c: Context, trustProxy: boolean): string {\n const peer = socketAddress(c);\n if (trustProxy && peer && isPrivateAddress(peer)) {\n const hops = Math.max(1, Number.parseInt(process.env.TRUST_PROXY_HOPS ?? \"1\", 10) || 1);\n const chain = (c.req.header(\"x-forwarded-for\") ?? \"\")\n .split(\",\")\n .map((s) => s.trim())\n .filter(Boolean);\n const forwarded = chain[chain.length - hops];\n if (forwarded && isIP(forwarded)) return forwarded;\n }\n return peer ?? \"unknown\";\n}\n\nfunction isSecure(c: Context, trustProxy: boolean): boolean {\n if (new URL(c.req.url).protocol === \"https:\") return true;\n const peer = socketAddress(c);\n return trustProxy && !!peer && isPrivateAddress(peer) && c.req.header(\"x-forwarded-proto\")?.split(\",\")[0]?.trim().toLowerCase() === \"https\";\n}\n\nexport function sessionToken(c: Context): string | undefined {\n return getCookie(c, SESSION_COOKIE) || undefined;\n}\n\nfunction writeSessionCookie(c: Context, token: string, trustProxy: boolean): void {\n setCookie(c, SESSION_COOKIE, token, {\n httpOnly: true,\n sameSite: \"Lax\",\n path: \"/\",\n secure: isSecure(c, trustProxy),\n maxAge: Math.floor(SESSION_TTL_MS / 1000),\n });\n}\n\n/** Creates a session for the requester and sets the cookie. */\nexport function startSession(c: Context, ctx: AuthCore): void {\n const token = adminAuth(ctx).sessions.create({ ip: clientIp(c, ctx.config.trustProxy), userAgent: c.req.header(\"user-agent\") });\n writeSessionCookie(c, token, ctx.config.trustProxy);\n}\n\nexport function endSession(c: Context, ctx: AuthCore): void {\n const token = sessionToken(c);\n if (token) adminAuth(ctx).sessions.delete(token);\n deleteCookie(c, SESSION_COOKIE, { path: \"/\" });\n}\n\n/** Whether the request carries a valid session; refreshes the cookie when the expiry slid forward. */\nexport function hasSession(c: Context, ctx: AuthCore): boolean {\n const token = sessionToken(c);\n const check = adminAuth(ctx).sessions.check(token);\n if (check.refreshed && token) writeSessionCookie(c, token, ctx.config.trustProxy);\n return check.valid;\n}\n\n/** Requires an admin session on every route it guards. */\nexport function requireAdmin(ctx: AuthCore): MiddlewareHandler<AppEnv> {\n return async (c, next) => {\n if (!hasSession(c, ctx)) {\n if (!isClaimed(ctx)) {\n throw new HttpError(401, \"setup_required\", \"This instance has not been claimed yet\", \"Open the setup link printed by agentinbox on startup.\");\n }\n throw new HttpError(401, \"unauthorized\", \"Sign in to continue\");\n }\n c.set(\"admin\", true);\n await next();\n };\n}\n\nconst MUTATING = new Set([\"POST\", \"PUT\", \"PATCH\", \"DELETE\"]);\n\n/**\n * CSRF guard: a custom header can't be sent cross-site without a CORS preflight (which this server\n * never approves), and SameSite=Lax already keeps the cookie off cross-site POSTs.\n */\nexport function csrfGuard(): MiddlewareHandler<AppEnv> {\n return async (c, next) => {\n if (MUTATING.has(c.req.method) && c.req.header(CSRF_HEADER) !== \"1\") {\n throw new HttpError(403, \"csrf\", `Missing ${CSRF_HEADER} header`, `Send \"${CSRF_HEADER}: 1\" with every mutating /api request.`);\n }\n await next();\n };\n}\n\n/** 429 response for a locked-out address. */\nexport function lockedOut(c: Context, minutes: number): Response {\n c.header(\"Retry-After\", String(minutes * 60));\n const message = `Too many failed attempts. Try again in ${minutes} minute${minutes === 1 ? \"\" : \"s\"}.`;\n return c.json({ error: { code: \"locked_out\", message } }, 429);\n}\n\nexport type CheckResult =\n | { ok: true }\n | { ok: false; lockedMinutes: number; nowBanned: boolean; /** Too many concurrent attempts from this client. */ busy?: boolean };\n\nexport interface CheckOptions {\n /**\n * Count toward (and be slowed down by) the instance-wide failure budget. Off for the setup claim:\n * it's only open while unclaimed and needs the token.\n */\n global?: boolean;\n /** Test hook: how long to wait while over the global budget. */\n slowdownMs?: number;\n}\n\n/**\n * Verifies a secret under the failed-login limiter.\n * - Each client gets one check at a time (a few more may queue, the rest get `busy`), and at most\n * MAX_CONCURRENT_VERIFICATIONS run at once overall.\n * - The attempt is recorded before `verify` (scrypt) runs and refunded if it succeeds, so\n * concurrent requests can't all slip past the ban.\n * - Locked-out clients and secrets this client already failed with never reach `verify`.\n * - While the instance-wide budget is exhausted, each attempt waits a little first; it is never refused.\n */\nexport async function checkSecret(\n ctx: AuthCore,\n ip: string,\n secret: string,\n verify: () => boolean | Promise<boolean>,\n opts: CheckOptions = {},\n): Promise<CheckResult> {\n const auth = adminAuth(ctx);\n const key = clientKey(ip);\n const lane = auth.lanes.get(key) ?? { tail: Promise.resolve(), queued: 0 };\n if (lane.queued > MAX_QUEUED_PER_CLIENT) return { ok: false, lockedMinutes: 0, nowBanned: false, busy: true };\n lane.queued++;\n auth.lanes.set(key, lane);\n const previous = lane.tail;\n let done!: () => void;\n lane.tail = new Promise<void>((resolve) => (done = resolve));\n try {\n await previous;\n return await checkOnce(auth, ip, secret, verify, opts);\n } finally {\n done();\n if (--lane.queued === 0) auth.lanes.delete(key);\n }\n}\n\nasync function checkOnce(auth: AdminAuth, ip: string, secret: string, verify: () => boolean | Promise<boolean>, opts: CheckOptions): Promise<CheckResult> {\n const { limiter } = auth;\n const global = opts.global !== false;\n if (global && limiter.overGlobalBudget() && !limiter.bannedMinutes(ip)) {\n await new Promise((r) => setTimeout(r, opts.slowdownMs ?? GLOBAL_SLOWDOWN_MS));\n }\n // From here to the reservation everything is synchronous.\n const locked = limiter.bannedMinutes(ip);\n if (locked) return { ok: false, lockedMinutes: locked, nowBanned: false };\n if (limiter.knownBad(ip, secret)) return { ok: false, lockedMinutes: 0, nowBanned: limiter.fail(ip, secret, { global }) };\n const reservation = limiter.record(ip, secret, { global });\n const release = await auth.verifySlots.acquire();\n let ok = false;\n try {\n ok = await verify();\n } finally {\n release();\n }\n if (!ok) return { ok: false, lockedMinutes: 0, nowBanned: reservation.banned };\n limiter.clear(ip);\n if (reservation.countedGlobally) limiter.refundGlobal();\n return { ok: true };\n}\n\n/** The 429 for a rejected check (locked out or too many at once), or null to continue. */\nexport function rejection(c: Context, check: CheckResult): Response | null {\n if (check.ok) return null;\n if (check.busy) {\n c.header(\"Retry-After\", \"2\");\n return c.json({ error: { code: \"too_many_requests\", message: \"Another sign-in attempt from your address is still being checked. Try again in a moment.\" } }, 429);\n }\n return check.lockedMinutes ? lockedOut(c, check.lockedMinutes) : null;\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"auth-B3ukRgpB.js","names":[],"sources":["../server/auth/lockout.ts","../server/auth/sessions.ts","../server/auth/index.ts"],"sourcesContent":["import { isIPv6 } from \"node:net\";\nimport { sha256 } from \"../crypto.ts\";\n\n/** Distinct wrong secrets per client within the window before a ban. */\nexport const MAX_FAILURES = 5;\n/** Raw failed attempts per client within the window (repeats of the same wrong secret included). */\nexport const MAX_ATTEMPTS = 30;\n/** Distinct wrong secrets across all clients within the window before every attempt is slowed down. */\nexport const GLOBAL_MAX_FAILURES = 100;\n/** Delay per attempt while the global budget is exceeded. */\nexport const GLOBAL_SLOWDOWN_MS = 2000;\nexport const WINDOW_MS = 15 * 60_000;\nexport const BAN_MS = 15 * 60_000;\n/** Tracked clients; the least recently seen is dropped beyond this. */\nexport const MAX_ENTRIES = 10_000;\n\ninterface Entry {\n tried: Set<string>;\n attempts: number;\n since: number;\n bannedUntil: number;\n}\n\n/**\n * Failed sign-in limiter. Per client (IPv4 address or IPv6 /64): 5 distinct wrong secrets or 30\n * attempts within 15 minutes ban it for 15 minutes. A secret already known to be wrong is\n * rejected without hashing (`knownBad`) and counts only as an attempt, so a client retrying a stale\n * password can't spend every attempt, and can't make the server hash it again either. Past a global\n * budget of distinct failures, every attempt is slowed down (never refused, so many clients can't\n * lock the owner out).\n */\nexport class LoginLimiter {\n private readonly entries = new Map<string, Entry>();\n private global = { failures: 0, since: 0 };\n\n constructor(private readonly clock: () => number = Date.now) {}\n\n /** Minutes until this client may try again, or 0. */\n bannedMinutes(ip: string): number {\n const t = this.clock();\n const entry = this.entries.get(clientKey(ip));\n const until = entry && entry.bannedUntil > t ? entry.bannedUntil : 0;\n return until > t ? Math.ceil((until - t) / 60_000) : 0;\n }\n\n /** The global failure budget is exhausted: slow every attempt down. */\n overGlobalBudget(): boolean {\n return this.global.failures >= GLOBAL_MAX_FAILURES && this.clock() - this.global.since < WINDOW_MS;\n }\n\n /** The client already failed with exactly this secret in the current window. */\n knownBad(ip: string, secret: string): boolean {\n const entry = this.current(clientKey(ip), this.clock());\n return !!entry?.tried.has(sha256(secret));\n }\n\n /** Records a failure; returns true when it started a ban for this client. `global: false` keeps it out of the global budget. */\n fail(ip: string, secret: string, opts: { global?: boolean } = {}): boolean {\n return this.record(ip, secret, opts).banned;\n }\n\n /**\n * Counts an attempt with this secret now, before it is verified, so concurrent requests can't\n * all pass the ban check first. `countedGlobally` says whether it used global budget (refund it\n * with `refundGlobal` when the secret turns out to be right).\n */\n record(ip: string, secret: string, opts: { global?: boolean } = {}): { banned: boolean; countedGlobally: boolean } {\n const t = this.clock();\n const key = clientKey(ip);\n const entry = this.current(key, t) ?? { tried: new Set<string>(), attempts: 0, since: t, bannedUntil: 0 };\n const hash = sha256(secret);\n let countedGlobally = false;\n if (!entry.tried.has(hash)) {\n entry.tried.add(hash);\n if (opts.global !== false) {\n if (t - this.global.since >= WINDOW_MS) this.global = { failures: 0, since: t };\n this.global.failures++;\n countedGlobally = true;\n }\n }\n entry.attempts++;\n const banned = entry.tried.size >= MAX_FAILURES || entry.attempts >= MAX_ATTEMPTS;\n if (banned) entry.bannedUntil = t + BAN_MS;\n this.touch(key, entry);\n return { banned, countedGlobally };\n }\n\n refundGlobal(): void {\n if (this.global.failures > 0) this.global.failures--;\n }\n\n clear(ip: string): void {\n this.entries.delete(clientKey(ip));\n }\n\n /** After the password changes, previously wrong secrets may be right. */\n reset(): void {\n this.entries.clear();\n this.global = { failures: 0, since: 0 };\n }\n\n get size(): number {\n return this.entries.size;\n }\n\n /** The client's entry if its window (or ban) is still running. */\n private current(key: string, t: number): Entry | undefined {\n const entry = this.entries.get(key);\n if (!entry) return undefined;\n if (entry.bannedUntil > t || (!entry.bannedUntil && t - entry.since < WINDOW_MS)) return entry;\n this.entries.delete(key);\n return undefined;\n }\n\n /** Map order doubles as LRU order: re-insert on use, evict from the front. */\n private touch(key: string, entry: Entry): void {\n this.entries.delete(key);\n this.entries.set(key, entry);\n while (this.entries.size > MAX_ENTRIES) this.entries.delete(this.entries.keys().next().value!);\n }\n}\n\n/** IPv4 addresses as is (also when IPv4-mapped); IPv6 by /64, since one client usually owns the whole prefix. */\nexport function clientKey(ip: string): string {\n const addr = ip.replace(/%.*$/, \"\").toLowerCase();\n const mapped = /^::ffff:(\\d+\\.\\d+\\.\\d+\\.\\d+)$/.exec(addr);\n if (mapped) return mapped[1]!;\n if (!isIPv6(addr)) return addr;\n const [head = \"\", tail] = addr.split(\"::\");\n const left = head ? head.split(\":\") : [];\n const right = tail ? tail.split(\":\") : [];\n const groups = tail === undefined ? left : [...left, ...Array(8 - left.length - right.length).fill(\"0\"), ...right];\n return `${groups\n .slice(0, 4)\n .map((g) => g.replace(/^0+(?=.)/, \"\"))\n .join(\":\")}::/64`;\n}\n","import { randomBytes } from \"node:crypto\";\nimport { sha256 } from \"../crypto.ts\";\nimport type { Db } from \"../db/index.ts\";\n\nexport const SESSION_TTL_MS = 30 * 24 * 3600_000;\n/** Sliding refresh never extends a session past this age: sign in again after 90 days. */\nexport const SESSION_MAX_AGE_MS = 90 * 24 * 3600_000;\n/** Sliding expiry is pushed forward at most once per this interval, to avoid a write per request. */\nconst REFRESH_AFTER_MS = 24 * 3600_000;\n\nexport interface SessionCheck {\n valid: boolean;\n /** The expiry was extended: re-send the cookie so the browser keeps it as long. */\n refreshed: boolean;\n}\n\n/** Admin console sessions. Only sha256(token) is stored. */\nexport class SessionStore {\n constructor(\n private readonly db: Db,\n private readonly clock: () => number = Date.now,\n ) {}\n\n create(meta: { ip?: string | null; userAgent?: string | null } = {}): string {\n const token = randomBytes(32).toString(\"base64url\");\n const t = this.clock();\n this.db.run(\n \"INSERT INTO admin_sessions (token_hash, created_at, expires_at, ip, user_agent) VALUES (?, ?, ?, ?, ?)\",\n sha256(token),\n new Date(t).toISOString(),\n new Date(t + SESSION_TTL_MS).toISOString(),\n meta.ip ?? null,\n meta.userAgent?.slice(0, 300) ?? null,\n );\n return token;\n }\n\n check(token: string | undefined | null): SessionCheck {\n if (!token) return { valid: false, refreshed: false };\n const hash = sha256(token);\n const row = this.db.get<{ created_at: string; expires_at: string }>(\n \"SELECT created_at, expires_at FROM admin_sessions WHERE token_hash = ?\",\n hash,\n );\n if (!row) return { valid: false, refreshed: false };\n const t = this.clock();\n const expires = Date.parse(row.expires_at);\n const hardLimit = Date.parse(row.created_at) + SESSION_MAX_AGE_MS;\n if (!(expires > t) || !(hardLimit > t)) {\n this.db.run(\"DELETE FROM admin_sessions WHERE token_hash = ?\", hash);\n return { valid: false, refreshed: false };\n }\n const next = Math.min(t + SESSION_TTL_MS, hardLimit);\n if (next - expires < REFRESH_AFTER_MS) return { valid: true, refreshed: false };\n this.db.run(\"UPDATE admin_sessions SET expires_at = ? WHERE token_hash = ?\", new Date(next).toISOString(), hash);\n return { valid: true, refreshed: true };\n }\n\n delete(token: string): void {\n this.db.run(\"DELETE FROM admin_sessions WHERE token_hash = ?\", sha256(token));\n }\n\n /** Revokes every session, optionally keeping one (the caller's own). */\n deleteAll(exceptToken?: string | null): number {\n if (exceptToken) return this.db.run(\"DELETE FROM admin_sessions WHERE token_hash != ?\", sha256(exceptToken)).changes;\n return this.db.run(\"DELETE FROM admin_sessions\").changes;\n }\n\n pruneExpired(): number {\n return this.db.run(\"DELETE FROM admin_sessions WHERE expires_at <= ?\", new Date(this.clock()).toISOString()).changes;\n }\n}\n","import { isIP, isIPv4, isIPv6 } from \"node:net\";\nimport { getConnInfo } from \"@hono/node-server/conninfo\";\nimport type { Context, MiddlewareHandler } from \"hono\";\nimport { deleteCookie, getCookie, setCookie } from \"hono/cookie\";\nimport type { AppEnv } from \"../api/util.ts\";\nimport type { CoreContext } from \"../context.ts\";\nimport { HttpError } from \"../util/errors.ts\";\nimport { clientKey, GLOBAL_SLOWDOWN_MS, LoginLimiter } from \"./lockout.ts\";\nimport { SESSION_TTL_MS, SessionStore } from \"./sessions.ts\";\nimport { ensureSetupToken, regenerateSetupToken } from \"./setup-token.ts\";\n\nexport const SESSION_COOKIE = \"aib_session\";\nexport const CSRF_HEADER = \"x-agentinbox-csrf\";\nexport const MIN_PASSWORD_LENGTH = 10;\nexport const MAX_PASSWORD_LENGTH = 1024;\n\n/** Secret checks (scrypt) allowed at once: libuv's default thread pool size. */\nexport const MAX_CONCURRENT_VERIFICATIONS = 4;\n/** Requests from one client that may wait behind its in-flight check; more get 429. */\nexport const MAX_QUEUED_PER_CLIENT = 4;\n\n/** Hands out at most `max` slots; waiters are served in order. */\nexport class Semaphore {\n private active = 0;\n private readonly waiters: (() => void)[] = [];\n\n constructor(private readonly max: number) {}\n\n get inUse(): number {\n return this.active;\n }\n\n async acquire(): Promise<() => void> {\n if (this.active < this.max) this.active++;\n else await new Promise<void>((resolve) => this.waiters.push(resolve));\n let released = false;\n return () => {\n if (released) return;\n released = true;\n const next = this.waiters.shift();\n if (next) next();\n else this.active--;\n };\n }\n}\n\n/** Per-context admin auth state: the session store, the failed-login limiter and verification queues. */\nexport interface AdminAuth {\n sessions: SessionStore;\n limiter: LoginLimiter;\n /** One secret check per client at a time: the tail of each client's queue. */\n lanes: Map<string, { tail: Promise<void>; queued: number }>;\n verifySlots: Semaphore;\n}\n\n/** What admin auth needs from the context (the CLI builds this without loading the master key). */\nexport type AuthCore = Pick<CoreContext, \"db\" | \"settings\" | \"audit\" | \"config\">;\n\nconst instances = new WeakMap<AuthCore, AdminAuth>();\n\nexport function adminAuth(ctx: AuthCore): AdminAuth {\n let auth = instances.get(ctx);\n if (!auth) {\n auth = { sessions: new SessionStore(ctx.db), limiter: new LoginLimiter(), lanes: new Map(), verifySlots: new Semaphore(MAX_CONCURRENT_VERIFICATIONS) };\n instances.set(ctx, auth);\n }\n return auth;\n}\n\n/** Claimed = an admin password exists. Before that only the setup-token claim works. */\nexport const isClaimed = (ctx: AuthCore): boolean => !!ctx.settings.getState(\"admin.passwordHash\");\n\n/** The current setup token while unclaimed (created on demand), otherwise null. */\nexport function currentSetupToken(ctx: AuthCore): string | null {\n return isClaimed(ctx) ? null : ensureSetupToken(ctx.config.dataDir);\n}\n\n/** `agentinbox reset-password`: back to the unclaimed state with a fresh setup link. */\nexport function resetAdminPassword(ctx: AuthCore): string {\n const auth = adminAuth(ctx);\n ctx.db.tx(() => {\n ctx.settings.deleteState(\"admin.passwordHash\");\n auth.sessions.deleteAll();\n });\n auth.limiter.reset();\n ctx.audit.record({ actorType: \"system\", action: \"admin.password_reset\", outcome: \"ok\" });\n return regenerateSetupToken(ctx.config.dataDir);\n}\n\n/** Loopback, RFC 1918, CGNAT (100.64/10) and IPv6 ULA (fc00::/7): where a reverse proxy can be. */\nexport function isPrivateAddress(ip: string): boolean {\n const addr = ip.replace(/%.*$/, \"\").toLowerCase().replace(/^::ffff:(?=\\d+\\.\\d+\\.\\d+\\.\\d+$)/, \"\");\n if (isIPv4(addr)) {\n const [a = 0, b = 0] = addr.split(\".\").map(Number);\n return a === 127 || a === 10 || (a === 172 && b >= 16 && b <= 31) || (a === 192 && b === 168) || (a === 100 && b >= 64 && b <= 127);\n }\n if (isIPv6(addr)) return addr === \"::1\" || /^f[cd][0-9a-f]{0,2}:/.test(addr);\n return false;\n}\n\nfunction socketAddress(c: Context): string | null {\n try {\n return getConnInfo(c).remote.address ?? null;\n } catch {\n // No Node socket (e.g. app.request() without bindings).\n return null;\n }\n}\n\n/**\n * The client address. X-Forwarded-For is honored only with TRUST_PROXY and only when the\n * connection comes from a loopback or private address (where the proxy runs); it is read from the\n * right, since proxies append the address they saw and entries further left are client-controlled.\n * TRUST_PROXY_HOPS (default 1) is the number of proxies in front of agentinbox.\n */\nexport function clientIp(c: Context, trustProxy: boolean): string {\n const peer = socketAddress(c);\n if (trustProxy && peer && isPrivateAddress(peer)) {\n const hops = Math.max(1, Number.parseInt(process.env.TRUST_PROXY_HOPS ?? \"1\", 10) || 1);\n const chain = (c.req.header(\"x-forwarded-for\") ?? \"\")\n .split(\",\")\n .map((s) => s.trim())\n .filter(Boolean);\n const forwarded = chain[chain.length - hops];\n if (forwarded && isIP(forwarded)) return forwarded;\n }\n return peer ?? \"unknown\";\n}\n\nfunction isSecure(c: Context, trustProxy: boolean): boolean {\n if (new URL(c.req.url).protocol === \"https:\") return true;\n const peer = socketAddress(c);\n return trustProxy && !!peer && isPrivateAddress(peer) && c.req.header(\"x-forwarded-proto\")?.split(\",\")[0]?.trim().toLowerCase() === \"https\";\n}\n\nexport function sessionToken(c: Context): string | undefined {\n return getCookie(c, SESSION_COOKIE) || undefined;\n}\n\nfunction writeSessionCookie(c: Context, token: string, trustProxy: boolean): void {\n setCookie(c, SESSION_COOKIE, token, {\n httpOnly: true,\n sameSite: \"Lax\",\n path: \"/\",\n secure: isSecure(c, trustProxy),\n maxAge: Math.floor(SESSION_TTL_MS / 1000),\n });\n}\n\n/** Creates a session for the requester and sets the cookie. */\nexport function startSession(c: Context, ctx: AuthCore): void {\n const token = adminAuth(ctx).sessions.create({ ip: clientIp(c, ctx.config.trustProxy), userAgent: c.req.header(\"user-agent\") });\n writeSessionCookie(c, token, ctx.config.trustProxy);\n}\n\nexport function endSession(c: Context, ctx: AuthCore): void {\n const token = sessionToken(c);\n if (token) adminAuth(ctx).sessions.delete(token);\n deleteCookie(c, SESSION_COOKIE, { path: \"/\" });\n}\n\n/** Whether the request carries a valid session; refreshes the cookie when the expiry slid forward. */\nexport function hasSession(c: Context, ctx: AuthCore): boolean {\n const token = sessionToken(c);\n const check = adminAuth(ctx).sessions.check(token);\n if (check.refreshed && token) writeSessionCookie(c, token, ctx.config.trustProxy);\n return check.valid;\n}\n\n/** Requires an admin session on every route it guards. */\nexport function requireAdmin(ctx: AuthCore): MiddlewareHandler<AppEnv> {\n return async (c, next) => {\n if (!hasSession(c, ctx)) {\n if (!isClaimed(ctx)) {\n throw new HttpError(401, \"setup_required\", \"This instance has not been claimed yet\", \"Open the setup link printed by agentinbox on startup.\");\n }\n throw new HttpError(401, \"unauthorized\", \"Sign in to continue\");\n }\n c.set(\"admin\", true);\n await next();\n };\n}\n\nconst MUTATING = new Set([\"POST\", \"PUT\", \"PATCH\", \"DELETE\"]);\n\n/**\n * CSRF guard: a custom header can't be sent cross-site without a CORS preflight (which this server\n * never approves), and SameSite=Lax already keeps the cookie off cross-site POSTs.\n */\nexport function csrfGuard(): MiddlewareHandler<AppEnv> {\n return async (c, next) => {\n if (MUTATING.has(c.req.method) && c.req.header(CSRF_HEADER) !== \"1\") {\n throw new HttpError(403, \"csrf\", `Missing ${CSRF_HEADER} header`, `Send \"${CSRF_HEADER}: 1\" with every mutating /api request.`);\n }\n await next();\n };\n}\n\n/** 429 response for a locked-out address. */\nexport function lockedOut(c: Context, minutes: number): Response {\n c.header(\"Retry-After\", String(minutes * 60));\n const message = `Too many failed attempts. Try again in ${minutes} minute${minutes === 1 ? \"\" : \"s\"}.`;\n return c.json({ error: { code: \"locked_out\", message } }, 429);\n}\n\nexport type CheckResult =\n | { ok: true }\n | { ok: false; lockedMinutes: number; nowBanned: boolean; /** Too many concurrent attempts from this client. */ busy?: boolean };\n\nexport interface CheckOptions {\n /**\n * Count toward (and be slowed down by) the instance-wide failure budget. Off for the setup claim:\n * it's only open while unclaimed and needs the token.\n */\n global?: boolean;\n /** Test hook: how long to wait while over the global budget. */\n slowdownMs?: number;\n}\n\n/**\n * Verifies a secret under the failed-login limiter.\n * - Each client gets one check at a time (a few more may queue, the rest get `busy`), and at most\n * MAX_CONCURRENT_VERIFICATIONS run at once overall.\n * - The attempt is recorded before `verify` (scrypt) runs and refunded if it succeeds, so\n * concurrent requests can't all slip past the ban.\n * - Locked-out clients and secrets this client already failed with never reach `verify`.\n * - While the instance-wide budget is exhausted, each attempt waits a little first; it is never refused.\n */\nexport async function checkSecret(\n ctx: AuthCore,\n ip: string,\n secret: string,\n verify: () => boolean | Promise<boolean>,\n opts: CheckOptions = {},\n): Promise<CheckResult> {\n const auth = adminAuth(ctx);\n const key = clientKey(ip);\n const lane = auth.lanes.get(key) ?? { tail: Promise.resolve(), queued: 0 };\n if (lane.queued > MAX_QUEUED_PER_CLIENT) return { ok: false, lockedMinutes: 0, nowBanned: false, busy: true };\n lane.queued++;\n auth.lanes.set(key, lane);\n const previous = lane.tail;\n let done!: () => void;\n lane.tail = new Promise<void>((resolve) => (done = resolve));\n try {\n await previous;\n return await checkOnce(auth, ip, secret, verify, opts);\n } finally {\n done();\n if (--lane.queued === 0) auth.lanes.delete(key);\n }\n}\n\nasync function checkOnce(auth: AdminAuth, ip: string, secret: string, verify: () => boolean | Promise<boolean>, opts: CheckOptions): Promise<CheckResult> {\n const { limiter } = auth;\n const global = opts.global !== false;\n if (global && limiter.overGlobalBudget() && !limiter.bannedMinutes(ip)) {\n await new Promise((r) => setTimeout(r, opts.slowdownMs ?? GLOBAL_SLOWDOWN_MS));\n }\n // From here to the reservation everything is synchronous.\n const locked = limiter.bannedMinutes(ip);\n if (locked) return { ok: false, lockedMinutes: locked, nowBanned: false };\n if (limiter.knownBad(ip, secret)) return { ok: false, lockedMinutes: 0, nowBanned: limiter.fail(ip, secret, { global }) };\n const reservation = limiter.record(ip, secret, { global });\n const release = await auth.verifySlots.acquire();\n let ok = false;\n try {\n ok = await verify();\n } finally {\n release();\n }\n if (!ok) return { ok: false, lockedMinutes: 0, nowBanned: reservation.banned };\n limiter.clear(ip);\n if (reservation.countedGlobally) limiter.refundGlobal();\n return { ok: true };\n}\n\n/** The 429 for a rejected check (locked out or too many at once), or null to continue. */\nexport function rejection(c: Context, check: CheckResult): Response | null {\n if (check.ok) return null;\n if (check.busy) {\n c.header(\"Retry-After\", \"2\");\n return c.json({ error: { code: \"too_many_requests\", message: \"Another sign-in attempt from your address is still being checked. Try again in a moment.\" } }, 429);\n }\n return check.lockedMinutes ? lockedOut(c, check.lockedMinutes) : null;\n}\n"],"mappings":"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"}
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
export { l as currentSetupToken, f as isClaimed, _ as resetAdminPassword } from "./auth-
|
|
2
|
+
export { l as currentSetupToken, f as isClaimed, _ as resetAdminPassword } from "./auth-B3ukRgpB.js";
|
package/dist/cli.js
CHANGED
|
@@ -41,7 +41,7 @@ const configInput = (o) => ({
|
|
|
41
41
|
});
|
|
42
42
|
const program = new Command("agentinbox").description("Self-hosted Gmail broker for AI agents: MCP tools, webhooks, guardrails and a web console.").version(version, "-v, --version", "print the version").helpOption("-h, --help", "show help").addHelpText("after", "\nWithout installing: npx agentinbox-cli [command]. To install the command: npm i -g agentinbox-cli").showSuggestionAfterError();
|
|
43
43
|
withServerOptions(program.command("start", { isDefault: true }).description("run agentinbox in the foreground (default)")).option("--no-open", "don't open a browser on first run").option("--dev", "development: API only, the console runs on Vite (:5173)").action(async (o) => {
|
|
44
|
-
const { runForeground } = await import("./server-
|
|
44
|
+
const { runForeground } = await import("./server-BvX0c9MQ.js");
|
|
45
45
|
await runForeground({
|
|
46
46
|
...configInput(o),
|
|
47
47
|
dev: !!o.dev,
|
|
@@ -52,16 +52,16 @@ withServerOptions(program.command("start", { isDefault: true }).description("run
|
|
|
52
52
|
const service = program.command("service").description("run agentinbox as a background service (systemd / launchd)");
|
|
53
53
|
withServerOptions(service.command("install").description("install and start the service; re-run to update")).action(async (o) => {
|
|
54
54
|
const { loadConfig } = await import("./config-CIvgMqLx.js");
|
|
55
|
-
const { installService } = await import("./service-
|
|
55
|
+
const { installService } = await import("./service-lFSuvmgq.js");
|
|
56
56
|
await installService(loadConfig(configInput(o), version));
|
|
57
57
|
});
|
|
58
58
|
service.command("uninstall").description("stop and remove the service (keeps your data)").option("-d, --data <dir>", "data directory (default ~/.agentinbox, env AGENTINBOX_DATA)").action(async (o) => {
|
|
59
59
|
const { loadConfig } = await import("./config-CIvgMqLx.js");
|
|
60
|
-
const { uninstallService } = await import("./service-
|
|
60
|
+
const { uninstallService } = await import("./service-lFSuvmgq.js");
|
|
61
61
|
uninstallService(loadConfig(configInput(o), version));
|
|
62
62
|
});
|
|
63
63
|
service.command("status").description("show service status").action(async () => {
|
|
64
|
-
const { serviceStatus } = await import("./service-
|
|
64
|
+
const { serviceStatus } = await import("./service-lFSuvmgq.js");
|
|
65
65
|
serviceStatus();
|
|
66
66
|
});
|
|
67
67
|
const skill = program.command("skill").description("the agentinbox agent skill (SKILL.md)");
|
|
@@ -82,12 +82,12 @@ withServerOptions(program.command("reset-password").description("remove the admi
|
|
|
82
82
|
const dbPath = join(dataDir, "agentinbox.db");
|
|
83
83
|
if (!existsSync(dbPath)) throw new Error(`No agentinbox data in ${dataDir}. If agentinbox runs as a system service, run this with sudo; otherwise pass the data directory it uses with --data.`);
|
|
84
84
|
const { loadConfig } = await import("./config-CIvgMqLx.js");
|
|
85
|
-
const { Db } = await import("./db-
|
|
85
|
+
const { Db } = await import("./db-CXhlUuVP.js");
|
|
86
86
|
const { createBus } = await import("./bus-OKIGut72.js");
|
|
87
|
-
const { SettingsStore } = await import("./settings-
|
|
88
|
-
const { AuditLog } = await import("./audit-
|
|
89
|
-
const { currentSetupToken, isClaimed, resetAdminPassword } = await import("./auth-
|
|
90
|
-
const { runningInstance } = await import("./instance-
|
|
87
|
+
const { SettingsStore } = await import("./settings-DoPS659W.js");
|
|
88
|
+
const { AuditLog } = await import("./audit-Bf3TvAAN.js");
|
|
89
|
+
const { currentSetupToken, isClaimed, resetAdminPassword } = await import("./auth-B9dqCyaj.js");
|
|
90
|
+
const { runningInstance } = await import("./instance-BEEO0AHU.js");
|
|
91
91
|
const { consoleUrls } = await import("./urls-CNYhbeUQ.js");
|
|
92
92
|
const running = runningInstance(dataDir);
|
|
93
93
|
const config = loadConfig({
|
|
@@ -240,8 +240,9 @@ const json = {
|
|
|
240
240
|
return JSON.stringify(v);
|
|
241
241
|
}
|
|
242
242
|
};
|
|
243
|
+
const bool = (v) => v === 1 || v === 1n || v === true;
|
|
243
244
|
const now = () => (/* @__PURE__ */ new Date()).toISOString();
|
|
244
245
|
//#endregion
|
|
245
|
-
export {
|
|
246
|
+
export { now as i, bool as n, json as r, Db as t };
|
|
246
247
|
|
|
247
|
-
//# sourceMappingURL=db-
|
|
248
|
+
//# sourceMappingURL=db-B2pnfQW5.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"db-DdagFmUY.js","names":[],"sources":["../server/db/schema.ts","../server/db/index.ts"],"sourcesContent":["// Ordered migrations. Append only — never edit a shipped migration.\n// Conventions: ids are prefixed TEXT, timestamps are ISO-8601 TEXT (UTC), JSON is TEXT,\n// booleans are INTEGER 0/1, secrets are AES-GCM ciphertext TEXT (see server/crypto.ts).\n\nexport const MIGRATIONS: string[] = [\n /* 1: initial schema */ `\n CREATE TABLE settings (\n key TEXT PRIMARY KEY,\n value TEXT NOT NULL\n );\n\n CREATE TABLE admin_sessions (\n token_hash TEXT PRIMARY KEY,\n created_at TEXT NOT NULL,\n expires_at TEXT NOT NULL,\n ip TEXT,\n user_agent TEXT\n );\n\n CREATE TABLE accounts (\n id TEXT PRIMARY KEY,\n email TEXT NOT NULL UNIQUE,\n display_name TEXT,\n status TEXT NOT NULL DEFAULT 'active',\n last_error TEXT,\n refresh_token_enc TEXT NOT NULL,\n access_token_enc TEXT,\n access_expires_at TEXT,\n scopes TEXT NOT NULL DEFAULT '[]',\n history_id TEXT,\n watch_expires_at TEXT,\n last_sync_at TEXT,\n created_at TEXT NOT NULL\n );\n\n -- Pending OAuth flows (state → PKCE verifier). Short-lived.\n CREATE TABLE oauth_states (\n state TEXT PRIMARY KEY,\n code_verifier TEXT NOT NULL,\n redirect_uri TEXT NOT NULL,\n reauth_account_id TEXT,\n created_at TEXT NOT NULL\n );\n\n CREATE TABLE agents (\n id TEXT PRIMARY KEY,\n name TEXT NOT NULL,\n description TEXT,\n key_prefix TEXT NOT NULL,\n key_hash TEXT NOT NULL UNIQUE,\n disabled INTEGER NOT NULL DEFAULT 0,\n last_used_at TEXT,\n created_at TEXT NOT NULL\n );\n\n CREATE TABLE grants (\n id TEXT PRIMARY KEY,\n agent_id TEXT NOT NULL REFERENCES agents(id) ON DELETE CASCADE,\n account_id TEXT NOT NULL REFERENCES accounts(id) ON DELETE CASCADE,\n capabilities TEXT NOT NULL, -- JSON Capability[]\n label_scope TEXT, -- JSON string[] | NULL (= whole mailbox)\n send_mode TEXT NOT NULL DEFAULT 'draft_only',\n allow_to TEXT NOT NULL DEFAULT '[]', -- JSON string[]\n reply_only INTEGER NOT NULL DEFAULT 0,\n max_sends_per_hour INTEGER,\n max_sends_per_day INTEGER,\n created_at TEXT NOT NULL,\n UNIQUE (agent_id, account_id)\n );\n\n -- Agent event-feed cursor for the MCP pull tools (list_events / wait_for_events / ack_events).\n CREATE TABLE agent_cursors (\n agent_id TEXT PRIMARY KEY REFERENCES agents(id) ON DELETE CASCADE,\n acked_seq INTEGER NOT NULL DEFAULT 0,\n updated_at TEXT NOT NULL\n );\n\n CREATE TABLE events (\n seq INTEGER PRIMARY KEY AUTOINCREMENT,\n id TEXT NOT NULL UNIQUE,\n type TEXT NOT NULL,\n account_id TEXT NOT NULL,\n gmail_message_id TEXT,\n gmail_thread_id TEXT,\n label_ids TEXT NOT NULL DEFAULT '[]', -- JSON: message labels at event time (used for scope + filters)\n message TEXT, -- JSON MessageSummary | NULL\n changed_label_ids TEXT, -- JSON string[] for label.added/removed\n detail TEXT, -- JSON\n dedupe_key TEXT NOT NULL UNIQUE, -- e.g. \"acc_x:msgid:message.received\" — Pub/Sub is at-least-once\n created_at TEXT NOT NULL\n );\n CREATE INDEX events_account_seq ON events(account_id, seq);\n CREATE INDEX events_thread ON events(account_id, gmail_thread_id);\n CREATE INDEX events_created ON events(created_at);\n\n -- Threads the account has sent into; drives thread.reply_received.\n CREATE TABLE sent_threads (\n account_id TEXT NOT NULL,\n gmail_thread_id TEXT NOT NULL,\n last_sent_at TEXT NOT NULL,\n PRIMARY KEY (account_id, gmail_thread_id)\n );\n\n CREATE TABLE webhooks (\n id TEXT PRIMARY KEY,\n agent_id TEXT NOT NULL REFERENCES agents(id) ON DELETE CASCADE,\n name TEXT NOT NULL,\n url TEXT NOT NULL,\n secret_enc TEXT NOT NULL,\n secret_prefix TEXT NOT NULL,\n event_types TEXT NOT NULL, -- JSON EventType[]\n filter TEXT NOT NULL DEFAULT '{}', -- JSON WebhookFilter\n include_body INTEGER NOT NULL DEFAULT 0,\n enabled INTEGER NOT NULL DEFAULT 1,\n created_at TEXT NOT NULL\n );\n\n CREATE TABLE deliveries (\n id TEXT PRIMARY KEY,\n webhook_id TEXT NOT NULL REFERENCES webhooks(id) ON DELETE CASCADE,\n event_seq INTEGER NOT NULL,\n status TEXT NOT NULL DEFAULT 'pending',\n attempts INTEGER NOT NULL DEFAULT 0,\n next_attempt_at TEXT,\n last_status_code INTEGER,\n last_error TEXT,\n last_response TEXT,\n duration_ms INTEGER,\n created_at TEXT NOT NULL,\n updated_at TEXT NOT NULL,\n UNIQUE (webhook_id, event_seq)\n );\n CREATE INDEX deliveries_due ON deliveries(status, next_attempt_at);\n CREATE INDEX deliveries_webhook ON deliveries(webhook_id, created_at);\n\n CREATE TABLE approvals (\n id TEXT PRIMARY KEY,\n agent_id TEXT NOT NULL REFERENCES agents(id) ON DELETE CASCADE,\n account_id TEXT NOT NULL REFERENCES accounts(id) ON DELETE CASCADE,\n kind TEXT NOT NULL,\n status TEXT NOT NULL DEFAULT 'pending',\n draft_id TEXT NOT NULL,\n payload TEXT NOT NULL, -- JSON {to,cc,bcc,subject,body,threadId,attachments,reason}\n decided_at TEXT,\n decision_note TEXT,\n sent_message_id TEXT,\n error TEXT,\n created_at TEXT NOT NULL,\n expires_at TEXT NOT NULL\n );\n CREATE INDEX approvals_status ON approvals(status, created_at);\n\n CREATE TABLE audit_log (\n id INTEGER PRIMARY KEY AUTOINCREMENT,\n ts TEXT NOT NULL,\n actor_type TEXT NOT NULL,\n actor_id TEXT,\n action TEXT NOT NULL,\n account_id TEXT,\n target TEXT,\n outcome TEXT NOT NULL,\n reason TEXT,\n duration_ms INTEGER,\n detail TEXT\n );\n CREATE INDEX audit_ts ON audit_log(ts);\n CREATE INDEX audit_actor ON audit_log(actor_id, ts);\n `,\n];\n","import { chmodSync } from \"node:fs\";\nimport { DatabaseSync, type SQLInputValue } from \"node:sqlite\";\nimport { MIGRATIONS } from \"./schema.ts\";\n\nexport type Param = SQLInputValue;\nexport type Row = Record<string, unknown>;\n\n/** Thin synchronous wrapper over node:sqlite. All repos go through this. */\nexport class Db {\n readonly raw: DatabaseSync;\n private depth = 0;\n\n constructor(path: string) {\n this.raw = new DatabaseSync(path);\n this.raw.exec(\"PRAGMA journal_mode = WAL; PRAGMA foreign_keys = ON; PRAGMA busy_timeout = 5000;\");\n this.migrate();\n // The database holds (encrypted) tokens and the audit log: owner-only, whatever the umask.\n if (path !== \":memory:\") {\n for (const file of [path, `${path}-wal`, `${path}-shm`]) {\n try {\n chmodSync(file, 0o600);\n } catch {\n // Not created (yet) or not ours to change.\n }\n }\n }\n }\n\n get<T = Row>(sql: string, ...params: Param[]): T | undefined {\n return this.raw.prepare(sql).get(...params) as T | undefined;\n }\n\n all<T = Row>(sql: string, ...params: Param[]): T[] {\n return this.raw.prepare(sql).all(...params) as T[];\n }\n\n run(sql: string, ...params: Param[]): { changes: number; lastInsertRowid: number } {\n const r = this.raw.prepare(sql).run(...params);\n return { changes: Number(r.changes), lastInsertRowid: Number(r.lastInsertRowid) };\n }\n\n /** Nested calls join the outer transaction (savepoints). */\n tx<T>(fn: () => T): T {\n const sp = `sp${this.depth++}`;\n this.raw.exec(`SAVEPOINT ${sp}`);\n try {\n const out = fn();\n this.raw.exec(`RELEASE ${sp}`);\n return out;\n } catch (err) {\n this.raw.exec(`ROLLBACK TO ${sp}; RELEASE ${sp}`);\n throw err;\n } finally {\n this.depth--;\n }\n }\n\n close(): void {\n this.raw.close();\n }\n\n private migrate(): void {\n this.raw.exec(\"CREATE TABLE IF NOT EXISTS _migrations (version INTEGER PRIMARY KEY, applied_at TEXT NOT NULL)\");\n const row = this.get<{ v: number | null }>(\"SELECT MAX(version) AS v FROM _migrations\");\n const current = row?.v ?? 0;\n MIGRATIONS.forEach((sql, i) => {\n const version = i + 1;\n if (version <= current) return;\n this.tx(() => {\n this.raw.exec(sql);\n this.run(\"INSERT INTO _migrations (version, applied_at) VALUES (?, ?)\", version, new Date().toISOString());\n });\n });\n }\n}\n\nexport const json = {\n parse<T>(v: unknown, fallback: T): T {\n if (typeof v !== \"string\" || v === \"\") return fallback;\n try {\n return JSON.parse(v) as T;\n } catch {\n return fallback;\n }\n },\n str(v: unknown): string {\n return JSON.stringify(v);\n },\n};\n\nexport const bool = (v: unknown): boolean => v === 1 || v === 1n || v === true;\nexport const now = (): string => new Date().toISOString();\n"],"mappings":";;;;AAIA,MAAa,aAAuB,CACV;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAmK1B;;;;AChKA,IAAa,KAAb,MAAgB;CACd;CACA,QAAgB;CAEhB,YAAY,MAAc;EACxB,KAAK,MAAM,IAAI,aAAa,IAAI;EAChC,KAAK,IAAI,KAAK,kFAAkF;EAChG,KAAK,QAAQ;EAEb,IAAI,SAAS,YACX,KAAK,MAAM,QAAQ;GAAC;GAAM,GAAG,KAAK;GAAO,GAAG,KAAK;EAAK,GACpD,IAAI;GACF,UAAU,MAAM,GAAK;EACvB,QAAQ,CAER;CAGN;CAEA,IAAa,KAAa,GAAG,QAAgC;EAC3D,OAAO,KAAK,IAAI,QAAQ,GAAG,CAAC,CAAC,IAAI,GAAG,MAAM;CAC5C;CAEA,IAAa,KAAa,GAAG,QAAsB;EACjD,OAAO,KAAK,IAAI,QAAQ,GAAG,CAAC,CAAC,IAAI,GAAG,MAAM;CAC5C;CAEA,IAAI,KAAa,GAAG,QAA+D;EACjF,MAAM,IAAI,KAAK,IAAI,QAAQ,GAAG,CAAC,CAAC,IAAI,GAAG,MAAM;EAC7C,OAAO;GAAE,SAAS,OAAO,EAAE,OAAO;GAAG,iBAAiB,OAAO,EAAE,eAAe;EAAE;CAClF;;CAGA,GAAM,IAAgB;EACpB,MAAM,KAAK,KAAK,KAAK;EACrB,KAAK,IAAI,KAAK,aAAa,IAAI;EAC/B,IAAI;GACF,MAAM,MAAM,GAAG;GACf,KAAK,IAAI,KAAK,WAAW,IAAI;GAC7B,OAAO;EACT,SAAS,KAAK;GACZ,KAAK,IAAI,KAAK,eAAe,GAAG,YAAY,IAAI;GAChD,MAAM;EACR,UAAU;GACR,KAAK;EACP;CACF;CAEA,QAAc;EACZ,KAAK,IAAI,MAAM;CACjB;CAEA,UAAwB;EACtB,KAAK,IAAI,KAAK,gGAAgG;EAE9G,MAAM,UADM,KAAK,IAA0B,2CACzB,CAAC,EAAE,KAAK;EAC1B,WAAW,SAAS,KAAK,MAAM;GAC7B,MAAM,UAAU,IAAI;GACpB,IAAI,WAAW,SAAS;GACxB,KAAK,SAAS;IACZ,KAAK,IAAI,KAAK,GAAG;IACjB,KAAK,IAAI,+DAA+D,0BAAS,IAAI,KAAK,EAAA,CAAE,YAAY,CAAC;GAC3G,CAAC;EACH,CAAC;CACH;AACF;AAEA,MAAa,OAAO;CAClB,MAAS,GAAY,UAAgB;EACnC,IAAI,OAAO,MAAM,YAAY,MAAM,IAAI,OAAO;EAC9C,IAAI;GACF,OAAO,KAAK,MAAM,CAAC;EACrB,QAAQ;GACN,OAAO;EACT;CACF;CACA,IAAI,GAAoB;EACtB,OAAO,KAAK,UAAU,CAAC;CACzB;AACF;AAGA,MAAa,6BAAoB,IAAI,KAAK,EAAA,CAAE,YAAY"}
|
|
1
|
+
{"version":3,"file":"db-B2pnfQW5.js","names":[],"sources":["../server/db/schema.ts","../server/db/index.ts"],"sourcesContent":["// Ordered migrations. Append only — never edit a shipped migration.\n// Conventions: ids are prefixed TEXT, timestamps are ISO-8601 TEXT (UTC), JSON is TEXT,\n// booleans are INTEGER 0/1, secrets are AES-GCM ciphertext TEXT (see server/crypto.ts).\n\nexport const MIGRATIONS: string[] = [\n /* 1: initial schema */ `\n CREATE TABLE settings (\n key TEXT PRIMARY KEY,\n value TEXT NOT NULL\n );\n\n CREATE TABLE admin_sessions (\n token_hash TEXT PRIMARY KEY,\n created_at TEXT NOT NULL,\n expires_at TEXT NOT NULL,\n ip TEXT,\n user_agent TEXT\n );\n\n CREATE TABLE accounts (\n id TEXT PRIMARY KEY,\n email TEXT NOT NULL UNIQUE,\n display_name TEXT,\n status TEXT NOT NULL DEFAULT 'active',\n last_error TEXT,\n refresh_token_enc TEXT NOT NULL,\n access_token_enc TEXT,\n access_expires_at TEXT,\n scopes TEXT NOT NULL DEFAULT '[]',\n history_id TEXT,\n watch_expires_at TEXT,\n last_sync_at TEXT,\n created_at TEXT NOT NULL\n );\n\n -- Pending OAuth flows (state → PKCE verifier). Short-lived.\n CREATE TABLE oauth_states (\n state TEXT PRIMARY KEY,\n code_verifier TEXT NOT NULL,\n redirect_uri TEXT NOT NULL,\n reauth_account_id TEXT,\n created_at TEXT NOT NULL\n );\n\n CREATE TABLE agents (\n id TEXT PRIMARY KEY,\n name TEXT NOT NULL,\n description TEXT,\n key_prefix TEXT NOT NULL,\n key_hash TEXT NOT NULL UNIQUE,\n disabled INTEGER NOT NULL DEFAULT 0,\n last_used_at TEXT,\n created_at TEXT NOT NULL\n );\n\n CREATE TABLE grants (\n id TEXT PRIMARY KEY,\n agent_id TEXT NOT NULL REFERENCES agents(id) ON DELETE CASCADE,\n account_id TEXT NOT NULL REFERENCES accounts(id) ON DELETE CASCADE,\n capabilities TEXT NOT NULL, -- JSON Capability[]\n label_scope TEXT, -- JSON string[] | NULL (= whole mailbox)\n send_mode TEXT NOT NULL DEFAULT 'draft_only',\n allow_to TEXT NOT NULL DEFAULT '[]', -- JSON string[]\n reply_only INTEGER NOT NULL DEFAULT 0,\n max_sends_per_hour INTEGER,\n max_sends_per_day INTEGER,\n created_at TEXT NOT NULL,\n UNIQUE (agent_id, account_id)\n );\n\n -- Agent event-feed cursor for the MCP pull tools (list_events / wait_for_events / ack_events).\n CREATE TABLE agent_cursors (\n agent_id TEXT PRIMARY KEY REFERENCES agents(id) ON DELETE CASCADE,\n acked_seq INTEGER NOT NULL DEFAULT 0,\n updated_at TEXT NOT NULL\n );\n\n CREATE TABLE events (\n seq INTEGER PRIMARY KEY AUTOINCREMENT,\n id TEXT NOT NULL UNIQUE,\n type TEXT NOT NULL,\n account_id TEXT NOT NULL,\n gmail_message_id TEXT,\n gmail_thread_id TEXT,\n label_ids TEXT NOT NULL DEFAULT '[]', -- JSON: message labels at event time (used for scope + filters)\n message TEXT, -- JSON MessageSummary | NULL\n changed_label_ids TEXT, -- JSON string[] for label.added/removed\n detail TEXT, -- JSON\n dedupe_key TEXT NOT NULL UNIQUE, -- e.g. \"acc_x:msgid:message.received\" — Pub/Sub is at-least-once\n created_at TEXT NOT NULL\n );\n CREATE INDEX events_account_seq ON events(account_id, seq);\n CREATE INDEX events_thread ON events(account_id, gmail_thread_id);\n CREATE INDEX events_created ON events(created_at);\n\n -- Threads the account has sent into; drives thread.reply_received.\n CREATE TABLE sent_threads (\n account_id TEXT NOT NULL,\n gmail_thread_id TEXT NOT NULL,\n last_sent_at TEXT NOT NULL,\n PRIMARY KEY (account_id, gmail_thread_id)\n );\n\n CREATE TABLE webhooks (\n id TEXT PRIMARY KEY,\n agent_id TEXT NOT NULL REFERENCES agents(id) ON DELETE CASCADE,\n name TEXT NOT NULL,\n url TEXT NOT NULL,\n secret_enc TEXT NOT NULL,\n secret_prefix TEXT NOT NULL,\n event_types TEXT NOT NULL, -- JSON EventType[]\n filter TEXT NOT NULL DEFAULT '{}', -- JSON WebhookFilter\n include_body INTEGER NOT NULL DEFAULT 0,\n enabled INTEGER NOT NULL DEFAULT 1,\n created_at TEXT NOT NULL\n );\n\n CREATE TABLE deliveries (\n id TEXT PRIMARY KEY,\n webhook_id TEXT NOT NULL REFERENCES webhooks(id) ON DELETE CASCADE,\n event_seq INTEGER NOT NULL,\n status TEXT NOT NULL DEFAULT 'pending',\n attempts INTEGER NOT NULL DEFAULT 0,\n next_attempt_at TEXT,\n last_status_code INTEGER,\n last_error TEXT,\n last_response TEXT,\n duration_ms INTEGER,\n created_at TEXT NOT NULL,\n updated_at TEXT NOT NULL,\n UNIQUE (webhook_id, event_seq)\n );\n CREATE INDEX deliveries_due ON deliveries(status, next_attempt_at);\n CREATE INDEX deliveries_webhook ON deliveries(webhook_id, created_at);\n\n CREATE TABLE approvals (\n id TEXT PRIMARY KEY,\n agent_id TEXT NOT NULL REFERENCES agents(id) ON DELETE CASCADE,\n account_id TEXT NOT NULL REFERENCES accounts(id) ON DELETE CASCADE,\n kind TEXT NOT NULL,\n status TEXT NOT NULL DEFAULT 'pending',\n draft_id TEXT NOT NULL,\n payload TEXT NOT NULL, -- JSON {to,cc,bcc,subject,body,threadId,attachments,reason}\n decided_at TEXT,\n decision_note TEXT,\n sent_message_id TEXT,\n error TEXT,\n created_at TEXT NOT NULL,\n expires_at TEXT NOT NULL\n );\n CREATE INDEX approvals_status ON approvals(status, created_at);\n\n CREATE TABLE audit_log (\n id INTEGER PRIMARY KEY AUTOINCREMENT,\n ts TEXT NOT NULL,\n actor_type TEXT NOT NULL,\n actor_id TEXT,\n action TEXT NOT NULL,\n account_id TEXT,\n target TEXT,\n outcome TEXT NOT NULL,\n reason TEXT,\n duration_ms INTEGER,\n detail TEXT\n );\n CREATE INDEX audit_ts ON audit_log(ts);\n CREATE INDEX audit_actor ON audit_log(actor_id, ts);\n `,\n];\n","import { chmodSync } from \"node:fs\";\nimport { DatabaseSync, type SQLInputValue } from \"node:sqlite\";\nimport { MIGRATIONS } from \"./schema.ts\";\n\nexport type Param = SQLInputValue;\nexport type Row = Record<string, unknown>;\n\n/** Thin synchronous wrapper over node:sqlite. All repos go through this. */\nexport class Db {\n readonly raw: DatabaseSync;\n private depth = 0;\n\n constructor(path: string) {\n this.raw = new DatabaseSync(path);\n this.raw.exec(\"PRAGMA journal_mode = WAL; PRAGMA foreign_keys = ON; PRAGMA busy_timeout = 5000;\");\n this.migrate();\n // The database holds (encrypted) tokens and the audit log: owner-only, whatever the umask.\n if (path !== \":memory:\") {\n for (const file of [path, `${path}-wal`, `${path}-shm`]) {\n try {\n chmodSync(file, 0o600);\n } catch {\n // Not created (yet) or not ours to change.\n }\n }\n }\n }\n\n get<T = Row>(sql: string, ...params: Param[]): T | undefined {\n return this.raw.prepare(sql).get(...params) as T | undefined;\n }\n\n all<T = Row>(sql: string, ...params: Param[]): T[] {\n return this.raw.prepare(sql).all(...params) as T[];\n }\n\n run(sql: string, ...params: Param[]): { changes: number; lastInsertRowid: number } {\n const r = this.raw.prepare(sql).run(...params);\n return { changes: Number(r.changes), lastInsertRowid: Number(r.lastInsertRowid) };\n }\n\n /** Nested calls join the outer transaction (savepoints). */\n tx<T>(fn: () => T): T {\n const sp = `sp${this.depth++}`;\n this.raw.exec(`SAVEPOINT ${sp}`);\n try {\n const out = fn();\n this.raw.exec(`RELEASE ${sp}`);\n return out;\n } catch (err) {\n this.raw.exec(`ROLLBACK TO ${sp}; RELEASE ${sp}`);\n throw err;\n } finally {\n this.depth--;\n }\n }\n\n close(): void {\n this.raw.close();\n }\n\n private migrate(): void {\n this.raw.exec(\"CREATE TABLE IF NOT EXISTS _migrations (version INTEGER PRIMARY KEY, applied_at TEXT NOT NULL)\");\n const row = this.get<{ v: number | null }>(\"SELECT MAX(version) AS v FROM _migrations\");\n const current = row?.v ?? 0;\n MIGRATIONS.forEach((sql, i) => {\n const version = i + 1;\n if (version <= current) return;\n this.tx(() => {\n this.raw.exec(sql);\n this.run(\"INSERT INTO _migrations (version, applied_at) VALUES (?, ?)\", version, new Date().toISOString());\n });\n });\n }\n}\n\nexport const json = {\n parse<T>(v: unknown, fallback: T): T {\n if (typeof v !== \"string\" || v === \"\") return fallback;\n try {\n return JSON.parse(v) as T;\n } catch {\n return fallback;\n }\n },\n str(v: unknown): string {\n return JSON.stringify(v);\n },\n};\n\nexport const bool = (v: unknown): boolean => v === 1 || v === 1n || v === true;\nexport const now = (): string => new Date().toISOString();\n"],"mappings":";;;;AAIA,MAAa,aAAuB,CACV;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAmK1B;;;;AChKA,IAAa,KAAb,MAAgB;CACd;CACA,QAAgB;CAEhB,YAAY,MAAc;EACxB,KAAK,MAAM,IAAI,aAAa,IAAI;EAChC,KAAK,IAAI,KAAK,kFAAkF;EAChG,KAAK,QAAQ;EAEb,IAAI,SAAS,YACX,KAAK,MAAM,QAAQ;GAAC;GAAM,GAAG,KAAK;GAAO,GAAG,KAAK;EAAK,GACpD,IAAI;GACF,UAAU,MAAM,GAAK;EACvB,QAAQ,CAER;CAGN;CAEA,IAAa,KAAa,GAAG,QAAgC;EAC3D,OAAO,KAAK,IAAI,QAAQ,GAAG,CAAC,CAAC,IAAI,GAAG,MAAM;CAC5C;CAEA,IAAa,KAAa,GAAG,QAAsB;EACjD,OAAO,KAAK,IAAI,QAAQ,GAAG,CAAC,CAAC,IAAI,GAAG,MAAM;CAC5C;CAEA,IAAI,KAAa,GAAG,QAA+D;EACjF,MAAM,IAAI,KAAK,IAAI,QAAQ,GAAG,CAAC,CAAC,IAAI,GAAG,MAAM;EAC7C,OAAO;GAAE,SAAS,OAAO,EAAE,OAAO;GAAG,iBAAiB,OAAO,EAAE,eAAe;EAAE;CAClF;;CAGA,GAAM,IAAgB;EACpB,MAAM,KAAK,KAAK,KAAK;EACrB,KAAK,IAAI,KAAK,aAAa,IAAI;EAC/B,IAAI;GACF,MAAM,MAAM,GAAG;GACf,KAAK,IAAI,KAAK,WAAW,IAAI;GAC7B,OAAO;EACT,SAAS,KAAK;GACZ,KAAK,IAAI,KAAK,eAAe,GAAG,YAAY,IAAI;GAChD,MAAM;EACR,UAAU;GACR,KAAK;EACP;CACF;CAEA,QAAc;EACZ,KAAK,IAAI,MAAM;CACjB;CAEA,UAAwB;EACtB,KAAK,IAAI,KAAK,gGAAgG;EAE9G,MAAM,UADM,KAAK,IAA0B,2CACzB,CAAC,EAAE,KAAK;EAC1B,WAAW,SAAS,KAAK,MAAM;GAC7B,MAAM,UAAU,IAAI;GACpB,IAAI,WAAW,SAAS;GACxB,KAAK,SAAS;IACZ,KAAK,IAAI,KAAK,GAAG;IACjB,KAAK,IAAI,+DAA+D,0BAAS,IAAI,KAAK,EAAA,CAAE,YAAY,CAAC;GAC3G,CAAC;EACH,CAAC;CACH;AACF;AAEA,MAAa,OAAO;CAClB,MAAS,GAAY,UAAgB;EACnC,IAAI,OAAO,MAAM,YAAY,MAAM,IAAI,OAAO;EAC9C,IAAI;GACF,OAAO,KAAK,MAAM,CAAC;EACrB,QAAQ;GACN,OAAO;EACT;CACF;CACA,IAAI,GAAoB;EACtB,OAAO,KAAK,UAAU,CAAC;CACzB;AACF;AAEA,MAAa,QAAQ,MAAwB,MAAM,KAAK,MAAM,MAAM,MAAM;AAC1E,MAAa,6BAAoB,IAAI,KAAK,EAAA,CAAE,YAAY"}
|
|
@@ -20,6 +20,7 @@ var HttpError = class extends Error {
|
|
|
20
20
|
}
|
|
21
21
|
};
|
|
22
22
|
const badRequest = (message, hint) => new HttpError(400, "bad_request", message, hint);
|
|
23
|
+
const notFound = (what) => new HttpError(404, "not_found", `${what} not found`);
|
|
23
24
|
const conflict = (message, hint) => new HttpError(409, "conflict", message, hint);
|
|
24
25
|
/** Thrown by policy checks. `reason` is the guardrail name recorded in the audit log. */
|
|
25
26
|
var PolicyDenied = class extends Error {
|
|
@@ -33,6 +34,6 @@ function errorMessage(err) {
|
|
|
33
34
|
return err instanceof Error ? err.message : String(err);
|
|
34
35
|
}
|
|
35
36
|
//#endregion
|
|
36
|
-
export { errorMessage as a, conflict as i, PolicyDenied as n, badRequest as r, HttpError as t };
|
|
37
|
+
export { errorMessage as a, conflict as i, PolicyDenied as n, notFound as o, badRequest as r, HttpError as t };
|
|
37
38
|
|
|
38
|
-
//# sourceMappingURL=errors-
|
|
39
|
+
//# sourceMappingURL=errors-FbMS3QBo.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"errors-
|
|
1
|
+
{"version":3,"file":"errors-FbMS3QBo.js","names":[],"sources":["../server/util/errors.ts"],"sourcesContent":["import type { ApiError } from \"../../shared/types.ts\";\n\n/** Throw from anywhere in request handling; the API layer turns it into an ApiError response. */\nexport class HttpError extends Error {\n constructor(\n readonly status: number,\n readonly code: string,\n message: string,\n readonly hint?: string,\n ) {\n super(message);\n }\n\n toJSON(): ApiError {\n return { error: { code: this.code, message: this.message, ...(this.hint ? { hint: this.hint } : {}) } };\n }\n}\n\nexport const badRequest = (message: string, hint?: string) => new HttpError(400, \"bad_request\", message, hint);\nexport const notFound = (what: string) => new HttpError(404, \"not_found\", `${what} not found`);\nexport const conflict = (message: string, hint?: string) => new HttpError(409, \"conflict\", message, hint);\n\n/** Thrown by policy checks. `reason` is the guardrail name recorded in the audit log. */\nexport class PolicyDenied extends Error {\n constructor(\n readonly reason: string,\n message: string,\n ) {\n super(message);\n }\n}\n\nexport function errorMessage(err: unknown): string {\n return err instanceof Error ? err.message : String(err);\n}\n"],"mappings":";;;AAGA,IAAa,YAAb,cAA+B,MAAM;CAExB;CACA;CAEA;CAJX,YACE,QACA,MACA,SACA,MACA;EACA,MAAM,OAAO;EALJ,KAAA,SAAA;EACA,KAAA,OAAA;EAEA,KAAA,OAAA;CAGX;CAEA,SAAmB;EACjB,OAAO,EAAE,OAAO;GAAE,MAAM,KAAK;GAAM,SAAS,KAAK;GAAS,GAAI,KAAK,OAAO,EAAE,MAAM,KAAK,KAAK,IAAI,CAAC;EAAG,EAAE;CACxG;AACF;AAEA,MAAa,cAAc,SAAiB,SAAkB,IAAI,UAAU,KAAK,eAAe,SAAS,IAAI;AAC7G,MAAa,YAAY,SAAiB,IAAI,UAAU,KAAK,aAAa,GAAG,KAAK,WAAW;AAC7F,MAAa,YAAY,SAAiB,SAAkB,IAAI,UAAU,KAAK,YAAY,SAAS,IAAI;;AAGxG,IAAa,eAAb,cAAkC,MAAM;CAE3B;CADX,YACE,QACA,SACA;EACA,MAAM,OAAO;EAHJ,KAAA,SAAA;CAIX;AACF;AAEA,SAAgB,aAAa,KAAsB;CACjD,OAAO,eAAe,QAAQ,IAAI,UAAU,OAAO,GAAG;AACxD"}
|
|
@@ -7,6 +7,15 @@ import { execFileSync } from "node:child_process";
|
|
|
7
7
|
//#region shared/types.ts
|
|
8
8
|
/** The npm package (`npx agentinbox-cli`); the command it installs is `agentinbox`. */
|
|
9
9
|
const NPM_PACKAGE = "agentinbox-cli";
|
|
10
|
+
const EVENT_TYPES = [
|
|
11
|
+
"message.received",
|
|
12
|
+
"message.sent",
|
|
13
|
+
"thread.reply_received",
|
|
14
|
+
"label.added",
|
|
15
|
+
"label.removed",
|
|
16
|
+
"account.auth_failed",
|
|
17
|
+
"sync.gap"
|
|
18
|
+
];
|
|
10
19
|
//#endregion
|
|
11
20
|
//#region server/instance.ts
|
|
12
21
|
/**
|
|
@@ -169,6 +178,6 @@ function assertMasterKeyUsable(config, envKey = process.env.AGENTINBOX_MASTER_KE
|
|
|
169
178
|
}
|
|
170
179
|
}
|
|
171
180
|
//#endregion
|
|
172
|
-
export { lockPath as a,
|
|
181
|
+
export { lockPath as a, EVENT_TYPES as c, cliCommand as i, NPM_PACKAGE as l, assertMasterKeyUsable as n, processStartTime as o, bootId as r, runningInstance as s, acquireInstanceLock as t };
|
|
173
182
|
|
|
174
|
-
//# sourceMappingURL=instance-
|
|
183
|
+
//# sourceMappingURL=instance-BnbMB2EK.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"instance-BnbMB2EK.js","names":[],"sources":["../shared/types.ts","../server/instance.ts"],"sourcesContent":["// Shared contract between the server (server/) and the web console (src/).\n// Everything crossing the HTTP boundary of the console API (/api/*) is typed here.\n// Timestamps are ISO-8601 strings in UTC. IDs are prefixed opaque strings (e.g. \"agt_…\").\n\n/** The npm package (`npx agentinbox-cli`); the command it installs is `agentinbox`. */\nexport const NPM_PACKAGE = \"agentinbox-cli\";\n\n// ───────────────────────────── Domain enums ─────────────────────────────\n\n/** What an agent may do with a mailbox. `read` also grants search, labels, attachments and the event feed. */\nexport const CAPABILITIES = [\"read\", \"draft\", \"send\", \"organize\", \"trash\"] as const;\nexport type Capability = (typeof CAPABILITIES)[number];\n\n/**\n * How `send`/`reply`/`forward` behave for a grant.\n * - direct: sent immediately (still subject to allowlist + rate limits)\n * - draft_only: a Gmail draft is created instead; a human sends it from Gmail\n * - approval: a draft is created and an approval request is queued in the console;\n * approving sends the draft, rejecting deletes it\n */\nexport const SEND_MODES = [\"direct\", \"draft_only\", \"approval\"] as const;\nexport type SendMode = (typeof SEND_MODES)[number];\n\nexport const EVENT_TYPES = [\n \"message.received\", // new message landed in the mailbox (not sent by the account)\n \"message.sent\", // message sent from the account (by an agent or by a human)\n \"thread.reply_received\", // received message in a thread the account previously sent into\n \"label.added\",\n \"label.removed\",\n \"account.auth_failed\", // refresh token revoked/expired; account needs re-auth\n \"sync.gap\", // history was too old; a bounded resync ran and events may be missing\n] as const;\nexport type EventType = (typeof EVENT_TYPES)[number];\n\nexport type AccountStatus = \"active\" | \"needs_reauth\" | \"paused\" | \"error\";\nexport type IngestMode = \"pubsub\" | \"poll\";\nexport type DeliveryStatus = \"pending\" | \"succeeded\" | \"failed\" | \"dead\";\nexport type ApprovalStatus = \"pending\" | \"approved\" | \"rejected\" | \"sent\" | \"failed\" | \"expired\";\nexport type AuditOutcome = \"ok\" | \"denied\" | \"error\";\nexport type ActorType = \"agent\" | \"admin\" | \"system\";\n\n/** OAuth consent configuration the operator chose in Google Cloud. Drives setup guidance only. */\nexport type GoogleAppType = \"internal\" | \"external\";\n\n// ───────────────────────────── Setup / system ─────────────────────────────\n\nexport interface HealthResponse {\n status: \"ok\" | \"degraded\";\n version: string;\n setupComplete: boolean;\n}\n\n/** GET /api/setup/status (unauthenticated). Drives the console's first route decision: claim, login or app. */\nexport interface SetupStatus {\n claimed: boolean;\n /** The request carries a valid admin session. */\n authenticated: boolean;\n}\n\n/** Live state of the ingest pipeline (Pub/Sub pull loop or history polling). */\nexport interface IngestStatus {\n mode: IngestMode;\n running: boolean;\n lastPullAt: string | null;\n lastError: string | null;\n}\n\n/** GET /api/system: facts about this instance for the settings page and connect snippets. */\nexport interface SystemInfo {\n version: string;\n nodeVersion: string;\n dataDir: string;\n /** Running under `agentinbox service install` (systemd/launchd). */\n service: boolean;\n /** URLs this machine is reachable at (localhost first, then LAN addresses). */\n addresses: string[];\n publicUrl: string;\n ingest: IngestStatus;\n}\n\n/** GET /api/system/latest: update notice. */\nexport interface UpdateInfo {\n current: string;\n latest: string | null;\n updateAvailable: boolean;\n /** Shell command that runs (or reinstalls the service with) the latest version using this instance's options. */\n command: string;\n}\n\n/** Ordered setup checklist; the console's setup wizard and overview both render this. */\nexport interface SetupState {\n /** True once an admin password exists. Before that, only /api/setup/claim with the setup token works. */\n claimed: boolean;\n googleClientConfigured: boolean;\n /** Google project id (needed for Pub/Sub). */\n gcpProjectId: string | null;\n googleAppType: GoogleAppType | null;\n pubsub: PubsubState;\n accountsConnected: number;\n agentsCreated: number;\n /** All required steps done (claimed + client configured + ≥1 account). Pub/Sub is optional (poll fallback). */\n complete: boolean;\n}\n\nexport interface PubsubState {\n mode: IngestMode;\n topic: string | null; // projects/{p}/topics/{t}\n subscription: string | null; // projects/{p}/subscriptions/{s}\n /** Last provisioning/health error, human readable, with remediation hint. */\n lastError: string | null;\n /** Last time a Pub/Sub message was pulled (proves the pipeline works end-to-end). */\n lastMessageAt: string | null;\n}\n\nexport interface Settings {\n /** Externally reachable base URL of this instance, e.g. https://mail.example.com. Used in MCP snippets + OAuth redirect. */\n publicUrl: string;\n ingestMode: IngestMode;\n pollIntervalSeconds: number;\n /** Max characters of message body returned to agents per message (0 = unlimited). */\n maxBodyChars: number;\n /** Days to keep events, deliveries and audit entries. */\n retentionDays: number;\n /** Approval requests auto-expire after this many hours. */\n approvalTtlHours: number;\n /**\n * SSRF guard: webhook targets that resolve to private, loopback or link-local addresses are refused\n * unless their host matches one of these entries (hostnames, IPs or CIDRs, e.g. \"127.0.0.0/8\",\n * \"10.0.0.0/8\", \"n8n.internal\"). Defaults to loopback only.\n */\n webhookAllowlist: string[];\n}\n\nexport interface GoogleClientConfig {\n clientId: string;\n /** Write-only: never returned by the API. */\n clientSecret?: string;\n projectId: string;\n appType: GoogleAppType;\n}\n\n/** Returned by GET /api/google/client. */\nexport interface GoogleClientView {\n clientId: string | null;\n hasSecret: boolean;\n projectId: string | null;\n appType: GoogleAppType | null;\n /** The exact redirect URI the operator must register on the OAuth client. */\n redirectUri: string;\n /**\n * \"direct\": Google redirects straight back to this console (localhost or https publicUrl).\n * \"paste\": publicUrl is plain-http non-localhost; Google can't redirect here, so the console\n * asks the user to paste the URL they land on after consent.\n */\n callbackMode: \"direct\" | \"paste\";\n}\n\n// ───────────────────────────── Accounts ─────────────────────────────\n\nexport interface Account {\n id: string; // acc_…\n email: string;\n displayName: string | null;\n status: AccountStatus;\n lastError: string | null;\n historyId: string | null;\n watchExpiresAt: string | null;\n lastSyncAt: string | null;\n createdAt: string;\n}\n\nexport interface GmailLabel {\n id: string; // Gmail label id, e.g. \"INBOX\" or \"Label_123\"\n name: string;\n type: \"system\" | \"user\";\n}\n\n/** POST /api/accounts/connect → start OAuth. */\nexport interface PubsubTestResult {\n ok: boolean;\n message: string;\n}\n\nexport interface ConnectAccountResponse {\n authUrl: string;\n /** Echoed state; in paste mode the console POSTs the pasted URL to /api/accounts/connect/complete. */\n state: string;\n callbackMode: \"direct\" | \"paste\";\n}\n\n// ───────────────────────────── Agents & grants ─────────────────────────────\n\nexport interface Agent {\n id: string; // agt_…\n name: string;\n description: string | null;\n /** First chars of the key, for identification (e.g. \"aib_7Hk2\"). The full key is shown once at creation/rotation. */\n keyPrefix: string;\n disabled: boolean;\n lastUsedAt: string | null;\n createdAt: string;\n grants: Grant[];\n}\n\nexport interface Grant {\n id: string; // grt_…\n agentId: string;\n accountId: string;\n capabilities: Capability[];\n /** Gmail label ids the agent is confined to (thread-level: a thread is visible if any message carries one). null = whole mailbox. */\n labelScope: string[] | null;\n sendMode: SendMode;\n /** Recipient allowlist: exact addresses or \"*@domain.com\" globs. Empty = any recipient. */\n allowTo: string[];\n /** Only allow sending replies inside threads the agent can already see. */\n replyOnly: boolean;\n maxSendsPerHour: number | null;\n maxSendsPerDay: number | null;\n createdAt: string;\n}\n\nexport type GrantInput = Omit<Grant, \"id\" | \"agentId\" | \"createdAt\">;\n\nexport interface CreateAgentInput {\n name: string;\n description?: string | null;\n grants?: GrantInput[];\n}\n\n/** POST /api/agents/:id/test: runs MCP initialize + tools/list as the agent, in-process. */\nexport interface AgentTestResult {\n ok: boolean;\n tools: string[];\n message: string;\n}\n\n/** Returned once on create and on key rotation. */\nexport interface AgentWithKey {\n agent: Agent;\n apiKey: string; // aib_…\n}\n\n// ───────────────────────────── Events ─────────────────────────────\n\nexport interface Address {\n name: string | null;\n email: string;\n}\n\n/** Normalized message metadata carried in events. No bodies are stored server-side. */\nexport interface MessageSummary {\n id: string; // Gmail message id\n threadId: string;\n from: Address | null;\n to: Address[];\n cc: Address[];\n subject: string;\n snippet: string;\n date: string; // ISO\n labelIds: string[];\n hasAttachments: boolean;\n}\n\nexport interface MailEvent {\n id: string; // evt_…\n seq: number; // monotonic, used as cursor\n type: EventType;\n accountId: string;\n accountEmail: string;\n message: MessageSummary | null;\n /** For label.added / label.removed. */\n labelIds?: string[];\n /** Free-form detail for account/sync events. */\n detail?: Record<string, unknown>;\n createdAt: string;\n}\n\n// ───────────────────────────── Webhooks ─────────────────────────────\n\nexport interface WebhookFilter {\n accountIds?: string[];\n /** Match if the message has any of these label ids. */\n labelIds?: string[];\n /** Glob on sender address, e.g. \"*@stripe.com\". Any match passes. */\n from?: string[];\n /** Glob on any to/cc address. */\n to?: string[];\n /** Case-insensitive regular expression tested against the subject. */\n subject?: string;\n hasAttachments?: boolean;\n}\n\nexport interface Webhook {\n id: string; // whk_…\n agentId: string;\n name: string;\n url: string;\n eventTypes: EventType[];\n filter: WebhookFilter;\n /** Include the cleaned message body (markdown) in the payload. */\n includeBody: boolean;\n enabled: boolean;\n /** Shown once at creation and on rotation (\"whsec_…\", Standard Webhooks format). */\n secretPrefix: string;\n createdAt: string;\n stats: { succeeded24h: number; failed24h: number; lastDeliveryAt: string | null };\n}\n\nexport type WebhookInput = Omit<Webhook, \"id\" | \"secretPrefix\" | \"createdAt\" | \"stats\">;\n\nexport interface WebhookWithSecret {\n webhook: Webhook;\n secret: string;\n}\n\nexport interface Delivery {\n id: string; // dlv_…\n webhookId: string;\n eventId: string;\n eventType: EventType;\n status: DeliveryStatus;\n attempts: number;\n nextAttemptAt: string | null;\n lastStatusCode: number | null;\n lastError: string | null;\n /** First ~500 chars of the receiver's response body. */\n lastResponse: string | null;\n durationMs: number | null;\n createdAt: string;\n updatedAt: string;\n}\n\n/**\n * Webhook HTTP request body. Signed per https://www.standardwebhooks.com:\n * headers webhook-id, webhook-timestamp, webhook-signature (\"v1,<base64 hmac-sha256>\").\n */\nexport interface WebhookPayload {\n type: EventType;\n timestamp: string;\n data: MailEvent & {\n /** Present only when the webhook has includeBody and the event concerns a message. */\n content?: MessageContent;\n /** Set instead of `content` when includeBody was requested but the body could not be fetched. */\n contentError?: string;\n };\n}\n\n// ───────────────────────────── Message content (returned to agents) ─────────────────────────────\n\n/** Signals attached to content. Informational: the content itself is still delivered (cleaned, not censored). */\nexport interface ContentSignals {\n /** Invisible content (display:none, zero-size/transparent text, HTML comments, zero-width/tag unicode) was removed. */\n hiddenContentRemoved: boolean;\n /** Phrases that commonly appear in prompt-injection attempts (\"ignore previous instructions\", …). */\n suspiciousInstructions: string[];\n /** SPF/DKIM/DMARC results parsed from Authentication-Results. null when the header is absent. */\n auth: { spf: string | null; dkim: string | null; dmarc: string | null } | null;\n /** Sender is outside the account's own domain. */\n external: boolean;\n}\n\nexport interface Attachment {\n id: string; // Gmail attachment id\n filename: string;\n mimeType: string;\n size: number;\n}\n\nexport interface MessageContent {\n /** Body converted to markdown (from HTML) or the plain-text part. */\n body: string;\n truncated: boolean;\n attachments: Attachment[];\n signals: ContentSignals;\n}\n\n// ───────────────────────────── Approvals ─────────────────────────────\n\nexport interface Approval {\n id: string; // apr_…\n agentId: string;\n agentName: string;\n accountId: string;\n accountEmail: string;\n kind: \"send\" | \"reply\" | \"forward\";\n status: ApprovalStatus;\n draftId: string;\n to: Address[];\n cc: Address[];\n bcc: Address[];\n subject: string;\n /** Full body as the agent wrote it (markdown/plain). */\n body: string;\n threadId: string | null;\n attachments: { filename: string; size: number }[];\n /** Why the agent says it is sending (optional `reason` tool arg). */\n reason: string | null;\n decidedAt: string | null;\n decisionNote: string | null;\n sentMessageId: string | null;\n error: string | null;\n createdAt: string;\n expiresAt: string;\n}\n\n// ───────────────────────────── Audit ─────────────────────────────\n\nexport interface AuditEntry {\n id: number;\n ts: string;\n actorType: ActorType;\n actorId: string | null;\n actorName: string | null;\n action: string; // MCP tool name or console action, e.g. \"send_message\", \"agent.create\"\n accountId: string | null;\n target: string | null; // message/thread/draft id\n outcome: AuditOutcome;\n /** For denials: the guardrail that blocked it, e.g. \"allow_to\", \"label_scope\", \"rate_limit\". */\n reason: string | null;\n durationMs: number | null;\n detail: Record<string, unknown> | null;\n}\n\n// ───────────────────────────── Overview ─────────────────────────────\n\nexport interface OverviewStats {\n accounts: { total: number; active: number; needsAttention: number };\n agents: { total: number; activeLast24h: number };\n events24h: number;\n deliveries24h: { succeeded: number; failed: number };\n pendingApprovals: number;\n toolCalls24h: number;\n denied24h: number;\n /** Hourly buckets for the last 24h, oldest first. */\n activity: { hour: string; events: number; toolCalls: number }[];\n}\n\n// ───────────────────────────── Pagination & errors ─────────────────────────────\n\nexport interface Page<T> {\n items: T[];\n /** Opaque cursor for the next page; null when exhausted. */\n nextCursor: string | null;\n}\n\n/** All non-2xx console API responses have this body. */\nexport interface ApiError {\n error: { code: string; message: string; hint?: string };\n}\n\n// ───────────────────────────── Console API route map ─────────────────────────────\n// Session: cookie `aib_session` (HttpOnly, SameSite=Lax). Mutating requests must send header\n// `x-agentinbox-csrf: 1` (simple CSRF guard; the console fetch wrapper always sets it).\n//\n// Unauthenticated:\n// GET /api/health → HealthResponse\n// GET /api/setup/status → SetupStatus\n// POST /api/setup/claim {token,password} → 204 + session cookie (only while unclaimed)\n// POST /api/auth/login {password} → 204 + session cookie (5 failures/IP → 15 min lockout)\n// POST /api/auth/logout → 204\n// GET /oauth/callback?code&state → HTML (redirects to console /accounts?connected=…)\n//\n// Authenticated (admin session):\n// GET /api/setup/state → SetupState\n// GET /api/system → SystemInfo\n// GET /api/system/latest → UpdateInfo (npm registry check, cached 30 min)\n// GET /api/settings → Settings\n// PATCH /api/settings Partial<Settings> → Settings\n// POST /api/auth/password {current,next} → 204\n// GET /api/google/client → GoogleClientView\n// PUT /api/google/client GoogleClientConfig → GoogleClientView\n// POST /api/google/pubsub/provision → PubsubState (creates topic + IAM + subscription using a connected account's token)\n// POST /api/google/pubsub/test → PubsubTestResult\n// GET /api/accounts → Account[]\n// POST /api/accounts/connect → ConnectAccountResponse\n// POST /api/accounts/connect/complete {url} → Account (paste mode)\n// POST /api/accounts/:id/resync → Account\n// POST /api/accounts/:id/pause | /resume → Account\n// DELETE /api/accounts/:id → 204 (stops watch, revokes token, deletes grants)\n// GET /api/accounts/:id/labels → GmailLabel[]\n// GET /api/agents → Agent[]\n// POST /api/agents CreateAgentInput → AgentWithKey\n// GET /api/agents/:id → Agent\n// PATCH /api/agents/:id {name?,description?,disabled?} → Agent\n// POST /api/agents/:id/rotate-key → AgentWithKey\n// DELETE /api/agents/:id → 204\n// PUT /api/agents/:id/grants/:accountId GrantInput → Grant (upsert; one grant per agent×account)\n// DELETE /api/agents/:id/grants/:accountId → 204\n// POST /api/agents/:id/test → AgentTestResult (runs MCP initialize+tools/list as the agent)\n// GET /api/webhooks?agentId → Webhook[]\n// POST /api/webhooks WebhookInput → WebhookWithSecret\n// PATCH /api/webhooks/:id Partial<WebhookInput> → Webhook\n// POST /api/webhooks/:id/rotate-secret → WebhookWithSecret\n// POST /api/webhooks/:id/test → Delivery (sends a synthetic event synchronously)\n// DELETE /api/webhooks/:id → 204\n// GET /api/deliveries?webhookId&status&cursor → Page<Delivery>\n// POST /api/deliveries/:id/replay → Delivery\n// GET /api/events?accountId&type&cursor → Page<MailEvent>\n// GET /api/events/stream → text/event-stream of ConsoleStreamMessage (SSE `data:` = JSON)\n// GET /api/approvals?status → Approval[]\n// POST /api/approvals/:id/approve {note?} → Approval\n// POST /api/approvals/:id/reject {note?} → Approval\n// GET /api/audit?agentId&outcome&cursor → Page<AuditEntry>\n// GET /api/overview → OverviewStats\n//\n// Agent-facing (bearer = agent API key):\n// POST/GET/DELETE /mcp MCP streamable HTTP (stateless)\n// GET /skill.md the agent skill (public, no secrets)\n// GET /health HealthResponse (alias of /api/health for uptime monitors; 503 when degraded)\n\nexport type ConsoleStreamMessage =\n | { kind: \"event\"; data: MailEvent }\n | { kind: \"delivery\"; data: Delivery }\n | { kind: \"approval\"; data: Approval }\n | { kind: \"audit\"; data: AuditEntry }\n | { kind: \"account\"; data: Account };\n","import { execFileSync } from \"node:child_process\";\nimport { existsSync, readFileSync, rmSync, writeFileSync } from \"node:fs\";\nimport { join } from \"node:path\";\nimport { DatabaseSync } from \"node:sqlite\";\nimport { NPM_PACKAGE } from \"../shared/types.ts\";\nimport type { Config } from \"./config.ts\";\nimport { Crypto } from \"./crypto.ts\";\n\n/**\n * How to run this CLI in printed hints: `npx agentinbox-cli` when run through npx (and in service\n * logs, read by someone who most likely installed that way), else the installed `agentinbox`.\n */\nexport function cliCommand(args: string): string {\n const viaNpx = process.env.npm_command === \"exec\" || /[\\\\/]_npx[\\\\/]/.test(process.argv[1] ?? \"\") || process.env.AGENTINBOX_SERVICE === \"1\";\n return `${viaNpx ? `npx ${NPM_PACKAGE}` : \"agentinbox\"} ${args}`;\n}\n\n// ───────────── Single instance per data directory ─────────────\n\nexport interface InstanceInfo {\n pid: number;\n host: string;\n port: number;\n startedAt: string;\n /** Kernel boot id (Linux), so a lock from before a reboot is never mistaken for a live one. */\n bootId?: string | null;\n /** The process's own start time as the OS reports it, so a reused pid doesn't look alive. */\n processStart?: string | null;\n}\n\nexport const lockPath = (dataDir: string) => join(dataDir, \"agentinbox.lock\");\n\n/** Data dirs locked by this process: a lock naming our own pid is live only if it's in here. */\nconst heldByUs = new Set<string>();\n\nexport function bootId(): string | null {\n try {\n return readFileSync(\"/proc/sys/kernel/random/boot_id\", \"utf8\").trim() || null;\n } catch {\n return null;\n }\n}\n\n/** Process start time: /proc (Linux, in clock ticks since boot) or `ps` elsewhere; null if unknown. */\nexport function processStartTime(pid: number): string | null {\n try {\n const stat = readFileSync(`/proc/${pid}/stat`, \"utf8\");\n // Fields after the parenthesized command name; starttime is field 22, i.e. index 19 here.\n return stat.slice(stat.lastIndexOf(\")\") + 2).split(\" \")[19] ?? null;\n } catch {\n // No /proc (macOS) or no such process.\n }\n if (process.platform === \"linux\" || process.platform === \"win32\") return null;\n try {\n return execFileSync(\"ps\", [\"-o\", \"lstart=\", \"-p\", String(pid)], { encoding: \"utf8\", stdio: [\"ignore\", \"pipe\", \"ignore\"] }).trim() || null;\n } catch {\n return null;\n }\n}\n\nfunction isAlive(pid: number): boolean {\n try {\n process.kill(pid, 0);\n return true;\n } catch (err) {\n return (err as NodeJS.ErrnoException).code === \"EPERM\";\n }\n}\n\n/**\n * Whether the process a lock names is still the one that wrote it. Our own pid is live only if we\n * hold the lock (in containers every start is pid 1); otherwise the pid must exist, on the same\n * boot, with the same start time (when both are known).\n */\nfunction isLive(info: InstanceInfo, dataDir: string): boolean {\n if (info.pid === process.pid) return heldByUs.has(dataDir);\n if (!isAlive(info.pid)) return false;\n const boot = bootId();\n if (info.bootId && boot && info.bootId !== boot) return false;\n if (info.processStart) {\n const start = processStartTime(info.pid);\n if (start && start !== info.processStart) return false;\n }\n return true;\n}\n\nfunction readLock(dataDir: string): InstanceInfo | null {\n try {\n const info = JSON.parse(readFileSync(lockPath(dataDir), \"utf8\")) as InstanceInfo;\n return Number.isInteger(info.pid) && info.pid > 0 ? info : null;\n } catch {\n return null;\n }\n}\n\n/** The running instance for this data directory, if any. */\nexport function runningInstance(dataDir: string): InstanceInfo | null {\n const info = readLock(dataDir);\n return info && isLive(info, dataDir) ? info : null;\n}\n\nexport interface InstanceLock {\n /** Records the bound port (for `reset-password` and error messages). */\n update(port: number): void;\n release(): void;\n}\n\n/**\n * Takes `<data>/agentinbox.lock`. Two processes on one data directory would both run ingest and\n * deliver every webhook twice, even on different ports. A lock whose process is gone (dead,\n * rebooted, or its pid reused by something else) is replaced.\n */\nexport function acquireInstanceLock(config: Pick<Config, \"dataDir\" | \"host\" | \"port\">): InstanceLock {\n const dataDir = config.dataDir;\n const file = lockPath(dataDir);\n const info: InstanceInfo = {\n pid: process.pid,\n host: config.host,\n port: config.port,\n startedAt: new Date().toISOString(),\n bootId: bootId(),\n processStart: processStartTime(process.pid),\n };\n for (let attempt = 0; ; attempt++) {\n try {\n writeFileSync(file, JSON.stringify(info), { flag: \"wx\", mode: 0o600 });\n break;\n } catch (err) {\n if ((err as NodeJS.ErrnoException).code !== \"EEXIST\" || attempt > 0) throw err;\n const other = runningInstance(dataDir);\n if (other) {\n throw new Error(\n `agentinbox is already running with the data directory ${dataDir} (pid ${other.pid}${other.port ? `, port ${other.port}` : \"\"}). ` +\n `Stop it first, or use another --data directory. If no such process exists, delete ${file}.`,\n );\n }\n rmSync(file, { force: true });\n }\n }\n heldByUs.add(dataDir);\n const ours = () => {\n const current = readLock(dataDir);\n return current?.pid === process.pid && current.startedAt === info.startedAt;\n };\n return {\n update(port) {\n info.port = port;\n if (ours()) writeFileSync(file, JSON.stringify(info), { mode: 0o600 });\n },\n release() {\n if (ours()) rmSync(file, { force: true });\n heldByUs.delete(dataDir);\n },\n };\n}\n\n// ───────────── Master key guard ─────────────\n\n/** One ciphertext from the database, or null when nothing encrypted is stored. */\nfunction sampleCiphertext(dbPath: string): string | null {\n if (!existsSync(dbPath)) return null;\n const db = new DatabaseSync(dbPath);\n try {\n const queries = [\n \"SELECT refresh_token_enc AS v FROM accounts WHERE refresh_token_enc LIKE 'v1:%' LIMIT 1\",\n \"SELECT secret_enc AS v FROM webhooks WHERE secret_enc LIKE 'v1:%' LIMIT 1\",\n \"SELECT json_extract(value, '$.clientSecretEnc') AS v FROM settings WHERE key = 'google.client'\",\n ];\n for (const sql of queries) {\n try {\n const v = (db.prepare(sql).get() as { v?: unknown } | undefined)?.v;\n if (typeof v === \"string\" && v.startsWith(\"v1:\")) return v;\n } catch {\n // Table not created yet.\n }\n }\n return null;\n } finally {\n db.close();\n }\n}\n\n/**\n * Refuses to start with a master key that can't read the stored secrets: a missing master.key would\n * otherwise be regenerated, silently making every refresh token and webhook secret unreadable.\n * `envKey` overrides AGENTINBOX_MASTER_KEY (used by `service install` to check the key it will install).\n */\nexport function assertMasterKeyUsable(config: Pick<Config, \"dbPath\" | \"masterKeyPath\">, envKey = process.env.AGENTINBOX_MASTER_KEY): void {\n const sample = sampleCiphertext(config.dbPath);\n if (!sample) return;\n let raw: string | null = envKey || null;\n const source = raw ? \"AGENTINBOX_MASTER_KEY\" : config.masterKeyPath;\n if (!raw && existsSync(config.masterKeyPath)) raw = readFileSync(config.masterKeyPath, \"utf8\").trim();\n if (!raw) {\n throw new Error(\n `${config.dbPath} holds encrypted secrets, but there is no master key: ${config.masterKeyPath} is missing and AGENTINBOX_MASTER_KEY is not set. ` +\n \"Restore master.key from your backup (or set AGENTINBOX_MASTER_KEY to the key used before). \" +\n \"agentinbox won't create a new key, because the stored Google tokens and secrets could never be read again.\",\n );\n }\n try {\n new Crypto(Buffer.from(raw, \"base64\")).decrypt(sample);\n } catch {\n throw new Error(\n `The master key from ${source} can't decrypt the secrets stored in ${config.dbPath}. ` +\n \"Use the key this data directory was created with (master.key from your backup, or the previous AGENTINBOX_MASTER_KEY).\",\n );\n }\n}\n"],"mappings":";;;;;;;;AAKA,MAAa,cAAc;AAkB3B,MAAa,cAAc;CACzB;CACA;CACA;CACA;CACA;CACA;CACA;AACF;;;;;;;ACnBA,SAAgB,WAAW,MAAsB;CAE/C,OAAO,GADQ,QAAQ,IAAI,gBAAgB,UAAU,iBAAiB,KAAK,QAAQ,KAAK,MAAM,EAAE,KAAK,QAAQ,IAAI,uBAAuB,MACrH,OAAO,gBAAgB,aAAa,GAAG;AAC5D;AAeA,MAAa,YAAY,YAAoB,KAAK,SAAS,iBAAiB;;AAG5E,MAAM,2BAAW,IAAI,IAAY;AAEjC,SAAgB,SAAwB;CACtC,IAAI;EACF,OAAO,aAAa,mCAAmC,MAAM,CAAC,CAAC,KAAK,KAAK;CAC3E,QAAQ;EACN,OAAO;CACT;AACF;;AAGA,SAAgB,iBAAiB,KAA4B;CAC3D,IAAI;EACF,MAAM,OAAO,aAAa,SAAS,IAAI,QAAQ,MAAM;EAErD,OAAO,KAAK,MAAM,KAAK,YAAY,GAAG,IAAI,CAAC,CAAC,CAAC,MAAM,GAAG,CAAC,CAAC,OAAO;CACjE,QAAQ,CAER;CACA,IAAI,QAAQ,aAAa,WAAW,QAAQ,aAAa,SAAS,OAAO;CACzE,IAAI;EACF,OAAO,aAAa,MAAM;GAAC;GAAM;GAAW;GAAM,OAAO,GAAG;EAAC,GAAG;GAAE,UAAU;GAAQ,OAAO;IAAC;IAAU;IAAQ;GAAQ;EAAE,CAAC,CAAC,CAAC,KAAK,KAAK;CACvI,QAAQ;EACN,OAAO;CACT;AACF;AAEA,SAAS,QAAQ,KAAsB;CACrC,IAAI;EACF,QAAQ,KAAK,KAAK,CAAC;EACnB,OAAO;CACT,SAAS,KAAK;EACZ,OAAQ,IAA8B,SAAS;CACjD;AACF;;;;;;AAOA,SAAS,OAAO,MAAoB,SAA0B;CAC5D,IAAI,KAAK,QAAQ,QAAQ,KAAK,OAAO,SAAS,IAAI,OAAO;CACzD,IAAI,CAAC,QAAQ,KAAK,GAAG,GAAG,OAAO;CAC/B,MAAM,OAAO,OAAO;CACpB,IAAI,KAAK,UAAU,QAAQ,KAAK,WAAW,MAAM,OAAO;CACxD,IAAI,KAAK,cAAc;EACrB,MAAM,QAAQ,iBAAiB,KAAK,GAAG;EACvC,IAAI,SAAS,UAAU,KAAK,cAAc,OAAO;CACnD;CACA,OAAO;AACT;AAEA,SAAS,SAAS,SAAsC;CACtD,IAAI;EACF,MAAM,OAAO,KAAK,MAAM,aAAa,SAAS,OAAO,GAAG,MAAM,CAAC;EAC/D,OAAO,OAAO,UAAU,KAAK,GAAG,KAAK,KAAK,MAAM,IAAI,OAAO;CAC7D,QAAQ;EACN,OAAO;CACT;AACF;;AAGA,SAAgB,gBAAgB,SAAsC;CACpE,MAAM,OAAO,SAAS,OAAO;CAC7B,OAAO,QAAQ,OAAO,MAAM,OAAO,IAAI,OAAO;AAChD;;;;;;AAaA,SAAgB,oBAAoB,QAAiE;CACnG,MAAM,UAAU,OAAO;CACvB,MAAM,OAAO,SAAS,OAAO;CAC7B,MAAM,OAAqB;EACzB,KAAK,QAAQ;EACb,MAAM,OAAO;EACb,MAAM,OAAO;EACb,4BAAW,IAAI,KAAK,EAAA,CAAE,YAAY;EAClC,QAAQ,OAAO;EACf,cAAc,iBAAiB,QAAQ,GAAG;CAC5C;CACA,KAAK,IAAI,UAAU,IAAK,WACtB,IAAI;EACF,cAAc,MAAM,KAAK,UAAU,IAAI,GAAG;GAAE,MAAM;GAAM,MAAM;EAAM,CAAC;EACrE;CACF,SAAS,KAAK;EACZ,IAAK,IAA8B,SAAS,YAAY,UAAU,GAAG,MAAM;EAC3E,MAAM,QAAQ,gBAAgB,OAAO;EACrC,IAAI,OACF,MAAM,IAAI,MACR,yDAAyD,QAAQ,QAAQ,MAAM,MAAM,MAAM,OAAO,UAAU,MAAM,SAAS,GAAG,uFACvC,KAAK,EAC9F;EAEF,OAAO,MAAM,EAAE,OAAO,KAAK,CAAC;CAC9B;CAEF,SAAS,IAAI,OAAO;CACpB,MAAM,aAAa;EACjB,MAAM,UAAU,SAAS,OAAO;EAChC,OAAO,SAAS,QAAQ,QAAQ,OAAO,QAAQ,cAAc,KAAK;CACpE;CACA,OAAO;EACL,OAAO,MAAM;GACX,KAAK,OAAO;GACZ,IAAI,KAAK,GAAG,cAAc,MAAM,KAAK,UAAU,IAAI,GAAG,EAAE,MAAM,IAAM,CAAC;EACvE;EACA,UAAU;GACR,IAAI,KAAK,GAAG,OAAO,MAAM,EAAE,OAAO,KAAK,CAAC;GACxC,SAAS,OAAO,OAAO;EACzB;CACF;AACF;;AAKA,SAAS,iBAAiB,QAA+B;CACvD,IAAI,CAAC,WAAW,MAAM,GAAG,OAAO;CAChC,MAAM,KAAK,IAAI,aAAa,MAAM;CAClC,IAAI;EAMF,KAAK,MAAM,OAAO;GAJhB;GACA;GACA;EAEsB,GACtB,IAAI;GACF,MAAM,IAAK,GAAG,QAAQ,GAAG,CAAC,CAAC,IAAI,CAAC,EAAkC;GAClE,IAAI,OAAO,MAAM,YAAY,EAAE,WAAW,KAAK,GAAG,OAAO;EAC3D,QAAQ,CAER;EAEF,OAAO;CACT,UAAU;EACR,GAAG,MAAM;CACX;AACF;;;;;;AAOA,SAAgB,sBAAsB,QAAkD,SAAS,QAAQ,IAAI,uBAA6B;CACxI,MAAM,SAAS,iBAAiB,OAAO,MAAM;CAC7C,IAAI,CAAC,QAAQ;CACb,IAAI,MAAqB,UAAU;CACnC,MAAM,SAAS,MAAM,0BAA0B,OAAO;CACtD,IAAI,CAAC,OAAO,WAAW,OAAO,aAAa,GAAG,MAAM,aAAa,OAAO,eAAe,MAAM,CAAC,CAAC,KAAK;CACpG,IAAI,CAAC,KACH,MAAM,IAAI,MACR,GAAG,OAAO,OAAO,wDAAwD,OAAO,cAAc,wPAGhG;CAEF,IAAI;EACF,IAAI,OAAO,OAAO,KAAK,KAAK,QAAQ,CAAC,CAAC,CAAC,QAAQ,MAAM;CACvD,QAAQ;EACN,MAAM,IAAI,MACR,uBAAuB,OAAO,uCAAuC,OAAO,OAAO,yHAErF;CACF;AACF"}
|