agentinbox-cli 0.1.0-nightly.8 → 0.1.0-nightly.9

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.
Files changed (63) hide show
  1. package/dist/{audit-CNYWHcik.js → audit-bItbJYFP.js} +2 -2
  2. package/dist/{audit-CNYWHcik.js.map → audit-bItbJYFP.js.map} +1 -1
  3. package/dist/audit-gVdDzkAE.js +2 -0
  4. package/dist/{auth-D4yzhGUg.js → auth-0NrMvHdE.js} +2 -2
  5. package/dist/{auth-D4yzhGUg.js.map → auth-0NrMvHdE.js.map} +1 -1
  6. package/dist/{auth-X5SGuNNB.js → auth-CoHvbWgO.js} +1 -1
  7. package/dist/bus-Cz0flS37.js.map +1 -1
  8. package/dist/cli.js +5 -5
  9. package/dist/db-B6gLQGBK.js +2 -0
  10. package/dist/{db-B2pnfQW5.js → db-D0gvshCc.js} +35 -1
  11. package/dist/db-D0gvshCc.js.map +1 -0
  12. package/dist/instance-CgeL0q45.js.map +1 -1
  13. package/dist/{server-B97P3fiX.js → server-DwpxKa6g.js} +1670 -532
  14. package/dist/server-DwpxKa6g.js.map +1 -0
  15. package/dist/settings-CeEsv4CE.js +2 -0
  16. package/dist/{settings-BIuG-X7b.js → settings-DLUStIN7.js} +6 -4
  17. package/dist/settings-DLUStIN7.js.map +1 -0
  18. package/dist/setup-token-BO89xnpb.js.map +1 -1
  19. package/dist/web/assets/{_agentId-B1IT_oLM.js → _agentId-DAj08NDq.js} +1 -1
  20. package/dist/web/assets/{_app-BZC_ezNd.js → _app-B6q_HS9y.js} +1 -1
  21. package/dist/web/assets/{_app-D899up8Q.js → _app-CAQ6Mu1s.js} +1 -1
  22. package/dist/web/assets/{_webhookId-DL_FfpbN.js → _webhookId-CMgBUN2Z.js} +1 -1
  23. package/dist/web/assets/{account-status-B7xuoHdX.js → account-status-omoUlgBI.js} +1 -1
  24. package/dist/web/assets/{accounts-BaWYdoXm.js → accounts-DyLfACj_.js} +1 -1
  25. package/dist/web/assets/{accounts-list-SlLEx0Tj.js → accounts-list-DiXACW0-.js} +1 -1
  26. package/dist/web/assets/{agent-create-form-xY4c5aKn.js → agent-create-form-DKwaY1Dg.js} +1 -1
  27. package/dist/web/assets/{agents-BeT2jhye.js → agents-Drvjccgt.js} +1 -1
  28. package/dist/web/assets/{approvals-HoqCXgai.js → approvals-4kd0_J8X.js} +1 -1
  29. package/dist/web/assets/{audit-BKc-GvxS.js → audit-76H45orR.js} +1 -1
  30. package/dist/web/assets/{audit-bits-D5T99qmp.js → audit-bits-B1We-fQ3.js} +1 -1
  31. package/dist/web/assets/{chips-input-DnJH-tBN.js → chips-input--xgMZpg2.js} +1 -1
  32. package/dist/web/assets/{common-ZVp8Iqy7.js → common-PG6weTqi.js} +1 -1
  33. package/dist/web/assets/{connect-host-DwjH0Vjz.js → connect-host-DdkOJnMu.js} +1 -1
  34. package/dist/web/assets/{data-table-Cgc2dZGL.js → data-table-Ca9aQZKR.js} +1 -1
  35. package/dist/web/assets/{dialog-CtguxvM9.js → dialog-CxNRCZdE.js} +1 -1
  36. package/dist/web/assets/{event-row-DeVjUVeT.js → event-row-2xoyzgbP.js} +1 -1
  37. package/dist/web/assets/{events-DLL46jV7.js → events-Bizq3DPC.js} +1 -1
  38. package/dist/web/assets/{get-started-6sT3RVnC.js → get-started-DXUVmyNX.js} +1 -1
  39. package/dist/web/assets/{grants-CEtysWJ8.js → grants-BhXj1Pew.js} +1 -1
  40. package/dist/web/assets/{index-DIE0uwO-.js → index-Bd4Y5wSp.js} +2 -2
  41. package/dist/web/assets/{login-DM_C5rb-.js → login-aCr-cPqJ.js} +1 -1
  42. package/dist/web/assets/{mutations-DE96cXwv.js → mutations-DVlbDx0M.js} +1 -1
  43. package/dist/web/assets/{select-BZaLwu6u.js → select-C8tJBHb5.js} +1 -1
  44. package/dist/web/assets/{settings-ClkElIad.js → settings-H7ExHD9L.js} +1 -1
  45. package/dist/web/assets/{settings-form-BerjRzzv.js → settings-form-CNUionsI.js} +1 -1
  46. package/dist/web/assets/{setup-DB0QQETq.js → setup-DLbH5Dvl.js} +1 -1
  47. package/dist/web/assets/{sheet-C_M4zCN1.js → sheet-BQKpA2xa.js} +1 -1
  48. package/dist/web/assets/stream-C2Tr2d5O.js +1 -0
  49. package/dist/web/assets/{tabs-BGqmAVYU.js → tabs-ySiz7Mni.js} +1 -1
  50. package/dist/web/assets/{test-connection-nxVAS0Wx.js → test-connection-Bd2Gac0O.js} +1 -1
  51. package/dist/web/assets/{toggle-group-CCiYTSds.js → toggle-group-ne0pVDg6.js} +1 -1
  52. package/dist/web/assets/{useMutation-BA5lpPGT.js → useMutation-DeWvLw1a.js} +1 -1
  53. package/dist/web/assets/{webhook-sheet-GvYXTrKI.js → webhook-sheet-Cy_jHIqG.js} +1 -1
  54. package/dist/web/assets/{webhooks-CkT7bLVO.js → webhooks-CTAj5Ovp.js} +1 -1
  55. package/dist/web/index.html +1 -1
  56. package/package.json +1 -1
  57. package/dist/audit-Bf3TvAAN.js +0 -2
  58. package/dist/db-B2pnfQW5.js.map +0 -1
  59. package/dist/db-CXhlUuVP.js +0 -2
  60. package/dist/server-B97P3fiX.js.map +0 -1
  61. package/dist/settings-BIuG-X7b.js.map +0 -1
  62. package/dist/settings-BK7h99j7.js +0 -2
  63. package/dist/web/assets/stream-6OwrgVZw.js +0 -1
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { i as now, r as json } from "./db-B2pnfQW5.js";
2
+ import { i as now, r as json } from "./db-D0gvshCc.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-CNYWHcik.js.map
65
+ //# sourceMappingURL=audit-bItbJYFP.js.map
@@ -1 +1 @@
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
+ {"version":3,"file":"audit-bItbJYFP.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"}
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export { t as AuditLog } from "./audit-bItbJYFP.js";
@@ -545,6 +545,6 @@ function rejection(c, check) {
545
545
  return check.lockedMinutes ? lockedOut(c, check.lockedMinutes) : null;
546
546
  }
547
547
  //#endregion
548
- export { sessionToken as _, adminAuth as a, csrfGuard as c, hasSession as d, isClaimed as f, resetAdminPassword as g, requireAdmin as h, Semaphore as i, currentSetupToken as l, rejection as m, MAX_PASSWORD_LENGTH as n, checkSecret as o, lockedOut as p, SESSION_COOKIE as r, clientIp as s, CSRF_HEADER as t, endSession as u, startSession as v };
548
+ export { sessionToken as _, adminAuth as a, LoginLimiter as b, csrfGuard as c, hasSession as d, isClaimed as f, resetAdminPassword as g, requireAdmin as h, Semaphore as i, currentSetupToken as l, rejection as m, MAX_PASSWORD_LENGTH as n, checkSecret as o, lockedOut as p, SESSION_COOKIE as r, clientIp as s, CSRF_HEADER as t, endSession as u, startSession as v, clientKey as x, BAN_MS as y };
549
549
 
550
- //# sourceMappingURL=auth-D4yzhGUg.js.map
550
+ //# sourceMappingURL=auth-0NrMvHdE.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"auth-D4yzhGUg.js","names":[],"sources":["../server/auth/lockout.ts","../server/auth/proxy.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 { isIPv4, isIPv6 } from \"node:net\";\n\n/** An address as a family and a big integer, with IPv4-mapped IPv6 (::ffff:a.b.c.d) treated as IPv4. */\ninterface Addr {\n v6: boolean;\n n: bigint;\n}\n\nfunction parseV4(ip: string): bigint {\n return ip.split(\".\").reduce((acc, part) => (acc << 8n) | BigInt(Number(part)), 0n);\n}\n\nexport function parseAddress(raw: string): Addr | null {\n const ip = raw.trim().replace(/^\\[|\\]$/g, \"\").replace(/%.*$/, \"\").toLowerCase();\n if (isIPv4(ip)) return { v6: false, n: parseV4(ip) };\n if (!isIPv6(ip)) return null;\n const mapped = /^(?:0{0,4}:){0,5}:?ffff:(\\d+\\.\\d+\\.\\d+\\.\\d+)$/.exec(ip);\n if (mapped && isIPv4(mapped[1]!)) return { v6: false, n: parseV4(mapped[1]!) };\n // Expand an embedded IPv4 tail into two hextets, then \"::\".\n let text = ip;\n const v4Tail = /(\\d+\\.\\d+\\.\\d+\\.\\d+)$/.exec(text);\n if (v4Tail) {\n const n = parseV4(v4Tail[1]!);\n text = text.slice(0, -v4Tail[1]!.length) + `${(n >> 16n).toString(16)}:${(n & 0xffffn).toString(16)}`;\n }\n const [head = \"\", tail] = text.split(\"::\");\n const left = head ? head.split(\":\") : [];\n const right = tail ? tail.split(\":\") : [];\n const groups = tail === undefined ? left : [...left, ...Array<string>(8 - left.length - right.length).fill(\"0\"), ...right];\n if (groups.length !== 8) return null;\n return { v6: true, n: groups.reduce((acc, g) => (acc << 16n) | BigInt(Number.parseInt(g || \"0\", 16)), 0n) };\n}\n\ninterface Range {\n v6: boolean;\n base: bigint;\n bits: number;\n}\n\n/**\n * Parses \"10.0.0.5\", \"10.0.0.0/8\", \"fd00::/8\", \"::ffff:10.0.0.0/104\"; null when invalid. A /0\n * (trust everyone) is refused: it would let any client pick its own address.\n */\nexport function parseRange(entry: string): Range | null {\n const parts = entry.trim().split(\"/\");\n if (parts.length > 2) return null;\n const [ip = \"\", prefix] = parts;\n const addr = parseAddress(ip);\n if (!addr) return null;\n let bits = addr.v6 ? 128 : 32;\n if (prefix !== undefined) {\n if (!/^\\d{1,3}$/.test(prefix)) return null;\n bits = Number(prefix);\n // A mapped IPv4 range written in IPv6 form: drop the 96-bit ::ffff: prefix.\n if (!addr.v6 && ip.includes(\":\")) bits -= 96;\n if (bits <= 0 || bits > (addr.v6 ? 128 : 32)) return null;\n }\n const width = BigInt(addr.v6 ? 128 : 32);\n const mask = bits === 0 ? 0n : ((1n << BigInt(bits)) - 1n) << (width - BigInt(bits));\n return { v6: addr.v6, base: addr.n & mask, bits };\n}\n\nfunction inRange(addr: Addr, r: Range): boolean {\n if (addr.v6 !== r.v6) return false;\n const width = BigInt(r.v6 ? 128 : 32);\n const mask = r.bits === 0 ? 0n : ((1n << BigInt(r.bits)) - 1n) << (width - BigInt(r.bits));\n return (addr.n & mask) === r.base;\n}\n\nconst LOOPBACK = [parseRange(\"127.0.0.0/8\")!, parseRange(\"::1/128\")!];\n\nlet cache: { raw: string; ranges: Range[] } | undefined;\n\n/** TRUST_PROXY_FROM as ranges. Invalid entries are reported once and ignored. */\nfunction trustedRanges(raw: string): Range[] {\n if (cache?.raw === raw) return cache.ranges;\n const ranges: Range[] = [];\n const invalid: string[] = [];\n for (const entry of raw.split(/[,\\s]+/).filter(Boolean)) {\n const r = parseRange(entry);\n if (r) ranges.push(r);\n else invalid.push(entry);\n }\n if (invalid.length) process.stderr.write(`agentinbox: ignoring invalid TRUST_PROXY_FROM entries: ${invalid.join(\", \")}\\n`);\n cache = { raw, ranges };\n return ranges;\n}\n\n/**\n * Whether forwarding headers from this socket peer may be believed: loopback always (a proxy on\n * the same host), anything else only when listed in TRUST_PROXY_FROM (IPs or CIDRs, comma-separated).\n * The daemon usually binds 0.0.0.0, so trusting whole private ranges would let any LAN or tailnet\n * peer forge its address.\n */\nexport function isTrustedProxy(peer: string, trustFrom = process.env.TRUST_PROXY_FROM ?? \"\"): boolean {\n const addr = parseAddress(peer);\n if (!addr) return false;\n return [...LOOPBACK, ...trustedRanges(trustFrom)].some((r) => inRange(addr, r));\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 } 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 { isTrustedProxy } from \"./proxy.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\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 trusted proxy (loopback, or TRUST_PROXY_FROM); 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 */\n/** Some proxies (Azure, IIS) append \":port\" to X-Forwarded-For entries. */\nfunction stripPort(entry: string): string {\n const v6 = /^\\[([^\\]]+)\\](?::\\d+)?$/.exec(entry);\n if (v6) return v6[1]!;\n const v4 = /^(\\d{1,3}(?:\\.\\d{1,3}){3}):\\d+$/.exec(entry);\n return v4 ? v4[1]! : entry;\n}\n\nlet warnedUntrustedProxy = false;\n\n/** Upgrades from the old private-range rule silently lost proxy trust; say so once. */\nfunction warnUntrustedProxy(peer: string): void {\n if (warnedUntrustedProxy) return;\n warnedUntrustedProxy = true;\n process.stderr.write(\n `agentinbox: ignoring X-Forwarded-For from ${peer}: TRUST_PROXY only trusts loopback proxies unless TRUST_PROXY_FROM lists them (e.g. TRUST_PROXY_FROM=${peer}).\\n`,\n );\n}\n\nexport function clientIp(c: Context, trustProxy: boolean): string {\n const peer = socketAddress(c);\n if (trustProxy && peer && !isTrustedProxy(peer) && c.req.header(\"x-forwarded-for\")) warnUntrustedProxy(peer);\n if (trustProxy && peer && isTrustedProxy(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) => stripPort(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 && isTrustedProxy(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-0NrMvHdE.js","names":[],"sources":["../server/auth/lockout.ts","../server/auth/proxy.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 { isIPv4, isIPv6 } from \"node:net\";\n\n/** An address as a family and a big integer, with IPv4-mapped IPv6 (::ffff:a.b.c.d) treated as IPv4. */\ninterface Addr {\n v6: boolean;\n n: bigint;\n}\n\nfunction parseV4(ip: string): bigint {\n return ip.split(\".\").reduce((acc, part) => (acc << 8n) | BigInt(Number(part)), 0n);\n}\n\nexport function parseAddress(raw: string): Addr | null {\n const ip = raw.trim().replace(/^\\[|\\]$/g, \"\").replace(/%.*$/, \"\").toLowerCase();\n if (isIPv4(ip)) return { v6: false, n: parseV4(ip) };\n if (!isIPv6(ip)) return null;\n const mapped = /^(?:0{0,4}:){0,5}:?ffff:(\\d+\\.\\d+\\.\\d+\\.\\d+)$/.exec(ip);\n if (mapped && isIPv4(mapped[1]!)) return { v6: false, n: parseV4(mapped[1]!) };\n // Expand an embedded IPv4 tail into two hextets, then \"::\".\n let text = ip;\n const v4Tail = /(\\d+\\.\\d+\\.\\d+\\.\\d+)$/.exec(text);\n if (v4Tail) {\n const n = parseV4(v4Tail[1]!);\n text = text.slice(0, -v4Tail[1]!.length) + `${(n >> 16n).toString(16)}:${(n & 0xffffn).toString(16)}`;\n }\n const [head = \"\", tail] = text.split(\"::\");\n const left = head ? head.split(\":\") : [];\n const right = tail ? tail.split(\":\") : [];\n const groups = tail === undefined ? left : [...left, ...Array<string>(8 - left.length - right.length).fill(\"0\"), ...right];\n if (groups.length !== 8) return null;\n return { v6: true, n: groups.reduce((acc, g) => (acc << 16n) | BigInt(Number.parseInt(g || \"0\", 16)), 0n) };\n}\n\ninterface Range {\n v6: boolean;\n base: bigint;\n bits: number;\n}\n\n/**\n * Parses \"10.0.0.5\", \"10.0.0.0/8\", \"fd00::/8\", \"::ffff:10.0.0.0/104\"; null when invalid. A /0\n * (trust everyone) is refused: it would let any client pick its own address.\n */\nexport function parseRange(entry: string): Range | null {\n const parts = entry.trim().split(\"/\");\n if (parts.length > 2) return null;\n const [ip = \"\", prefix] = parts;\n const addr = parseAddress(ip);\n if (!addr) return null;\n let bits = addr.v6 ? 128 : 32;\n if (prefix !== undefined) {\n if (!/^\\d{1,3}$/.test(prefix)) return null;\n bits = Number(prefix);\n // A mapped IPv4 range written in IPv6 form: drop the 96-bit ::ffff: prefix.\n if (!addr.v6 && ip.includes(\":\")) bits -= 96;\n if (bits <= 0 || bits > (addr.v6 ? 128 : 32)) return null;\n }\n const width = BigInt(addr.v6 ? 128 : 32);\n const mask = bits === 0 ? 0n : ((1n << BigInt(bits)) - 1n) << (width - BigInt(bits));\n return { v6: addr.v6, base: addr.n & mask, bits };\n}\n\nfunction inRange(addr: Addr, r: Range): boolean {\n if (addr.v6 !== r.v6) return false;\n const width = BigInt(r.v6 ? 128 : 32);\n const mask = r.bits === 0 ? 0n : ((1n << BigInt(r.bits)) - 1n) << (width - BigInt(r.bits));\n return (addr.n & mask) === r.base;\n}\n\nconst LOOPBACK = [parseRange(\"127.0.0.0/8\")!, parseRange(\"::1/128\")!];\n\nlet cache: { raw: string; ranges: Range[] } | undefined;\n\n/** TRUST_PROXY_FROM as ranges. Invalid entries are reported once and ignored. */\nfunction trustedRanges(raw: string): Range[] {\n if (cache?.raw === raw) return cache.ranges;\n const ranges: Range[] = [];\n const invalid: string[] = [];\n for (const entry of raw.split(/[,\\s]+/).filter(Boolean)) {\n const r = parseRange(entry);\n if (r) ranges.push(r);\n else invalid.push(entry);\n }\n if (invalid.length) process.stderr.write(`agentinbox: ignoring invalid TRUST_PROXY_FROM entries: ${invalid.join(\", \")}\\n`);\n cache = { raw, ranges };\n return ranges;\n}\n\n/**\n * Whether forwarding headers from this socket peer may be believed: loopback always (a proxy on\n * the same host), anything else only when listed in TRUST_PROXY_FROM (IPs or CIDRs, comma-separated).\n * The daemon usually binds 0.0.0.0, so trusting whole private ranges would let any LAN or tailnet\n * peer forge its address.\n */\nexport function isTrustedProxy(peer: string, trustFrom = process.env.TRUST_PROXY_FROM ?? \"\"): boolean {\n const addr = parseAddress(peer);\n if (!addr) return false;\n return [...LOOPBACK, ...trustedRanges(trustFrom)].some((r) => inRange(addr, r));\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 } 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 { isTrustedProxy } from \"./proxy.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\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 trusted proxy (loopback, or TRUST_PROXY_FROM); 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 */\n/** Some proxies (Azure, IIS) append \":port\" to X-Forwarded-For entries. */\nfunction stripPort(entry: string): string {\n const v6 = /^\\[([^\\]]+)\\](?::\\d+)?$/.exec(entry);\n if (v6) return v6[1]!;\n const v4 = /^(\\d{1,3}(?:\\.\\d{1,3}){3}):\\d+$/.exec(entry);\n return v4 ? v4[1]! : entry;\n}\n\nlet warnedUntrustedProxy = false;\n\n/** Upgrades from the old private-range rule silently lost proxy trust; say so once. */\nfunction warnUntrustedProxy(peer: string): void {\n if (warnedUntrustedProxy) return;\n warnedUntrustedProxy = true;\n process.stderr.write(\n `agentinbox: ignoring X-Forwarded-For from ${peer}: TRUST_PROXY only trusts loopback proxies unless TRUST_PROXY_FROM lists them (e.g. TRUST_PROXY_FROM=${peer}).\\n`,\n );\n}\n\nexport function clientIp(c: Context, trustProxy: boolean): string {\n const peer = socketAddress(c);\n if (trustProxy && peer && !isTrustedProxy(peer) && c.req.header(\"x-forwarded-for\")) warnUntrustedProxy(peer);\n if (trustProxy && peer && isTrustedProxy(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) => stripPort(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 && isTrustedProxy(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":";;;;;;;;AAYA,MAAa,SAAS;;AAEtB,MAAa,cAAc;;;;;;;;;AAiB3B,IAAa,eAAb,MAA0B;CAIK;CAH7B,0BAA2B,IAAI,IAAmB;CAClD,SAAiB;EAAE,UAAU;EAAG,OAAO;CAAE;CAEzC,YAAY,QAAuC,KAAK,KAAK;EAAhC,KAAA,QAAA;CAAiC;;CAG9D,cAAc,IAAoB;EAChC,MAAM,IAAI,KAAK,MAAM;EACrB,MAAM,QAAQ,KAAK,QAAQ,IAAI,UAAU,EAAE,CAAC;EAC5C,MAAM,QAAQ,SAAS,MAAM,cAAc,IAAI,MAAM,cAAc;EACnE,OAAO,QAAQ,IAAI,KAAK,MAAM,QAAQ,KAAK,GAAM,IAAI;CACvD;;CAGA,mBAA4B;EAC1B,OAAO,KAAK,OAAO,YAAA,OAAmC,KAAK,MAAM,IAAI,KAAK,OAAO,QAAA;CACnF;;CAGA,SAAS,IAAY,QAAyB;EAE5C,OAAO,CAAC,CADM,KAAK,QAAQ,UAAU,EAAE,GAAG,KAAK,MAAM,CACxC,CAAC,EAAE,MAAM,IAAI,OAAO,MAAM,CAAC;CAC1C;;CAGA,KAAK,IAAY,QAAgB,OAA6B,CAAC,GAAY;EACzE,OAAO,KAAK,OAAO,IAAI,QAAQ,IAAI,CAAC,CAAC;CACvC;;;;;;CAOA,OAAO,IAAY,QAAgB,OAA6B,CAAC,GAAkD;EACjH,MAAM,IAAI,KAAK,MAAM;EACrB,MAAM,MAAM,UAAU,EAAE;EACxB,MAAM,QAAQ,KAAK,QAAQ,KAAK,CAAC,KAAK;GAAE,uBAAO,IAAI,IAAY;GAAG,UAAU;GAAG,OAAO;GAAG,aAAa;EAAE;EACxG,MAAM,OAAO,OAAO,MAAM;EAC1B,IAAI,kBAAkB;EACtB,IAAI,CAAC,MAAM,MAAM,IAAI,IAAI,GAAG;GAC1B,MAAM,MAAM,IAAI,IAAI;GACpB,IAAI,KAAK,WAAW,OAAO;IACzB,IAAI,IAAI,KAAK,OAAO,SAAA,KAAoB,KAAK,SAAS;KAAE,UAAU;KAAG,OAAO;IAAE;IAC9E,KAAK,OAAO;IACZ,kBAAkB;GACpB;EACF;EACA,MAAM;EACN,MAAM,SAAS,MAAM,MAAM,QAAA,KAAwB,MAAM,YAAA;EACzD,IAAI,QAAQ,MAAM,cAAc,IAAI;EACpC,KAAK,MAAM,KAAK,KAAK;EACrB,OAAO;GAAE;GAAQ;EAAgB;CACnC;CAEA,eAAqB;EACnB,IAAI,KAAK,OAAO,WAAW,GAAG,KAAK,OAAO;CAC5C;CAEA,MAAM,IAAkB;EACtB,KAAK,QAAQ,OAAO,UAAU,EAAE,CAAC;CACnC;;CAGA,QAAc;EACZ,KAAK,QAAQ,MAAM;EACnB,KAAK,SAAS;GAAE,UAAU;GAAG,OAAO;EAAE;CACxC;CAEA,IAAI,OAAe;EACjB,OAAO,KAAK,QAAQ;CACtB;;CAGA,QAAgB,KAAa,GAA8B;EACzD,MAAM,QAAQ,KAAK,QAAQ,IAAI,GAAG;EAClC,IAAI,CAAC,OAAO,OAAO,KAAA;EACnB,IAAI,MAAM,cAAc,KAAM,CAAC,MAAM,eAAe,IAAI,MAAM,QAAA,KAAoB,OAAO;EACzF,KAAK,QAAQ,OAAO,GAAG;CAEzB;;CAGA,MAAc,KAAa,OAAoB;EAC7C,KAAK,QAAQ,OAAO,GAAG;EACvB,KAAK,QAAQ,IAAI,KAAK,KAAK;EAC3B,OAAO,KAAK,QAAQ,OAAO,aAAa,KAAK,QAAQ,OAAO,KAAK,QAAQ,KAAK,CAAC,CAAC,KAAK,CAAC,CAAC,KAAM;CAC/F;AACF;;AAGA,SAAgB,UAAU,IAAoB;CAC5C,MAAM,OAAO,GAAG,QAAQ,QAAQ,EAAE,CAAC,CAAC,YAAY;CAChD,MAAM,SAAS,gCAAgC,KAAK,IAAI;CACxD,IAAI,QAAQ,OAAO,OAAO;CAC1B,IAAI,CAAC,OAAO,IAAI,GAAG,OAAO;CAC1B,MAAM,CAAC,OAAO,IAAI,QAAQ,KAAK,MAAM,IAAI;CACzC,MAAM,OAAO,OAAO,KAAK,MAAM,GAAG,IAAI,CAAC;CACvC,MAAM,QAAQ,OAAO,KAAK,MAAM,GAAG,IAAI,CAAC;CAExC,OAAO,IADQ,SAAS,KAAA,IAAY,OAAO;EAAC,GAAG;EAAM,GAAG,MAAM,IAAI,KAAK,SAAS,MAAM,MAAM,CAAC,CAAC,KAAK,GAAG;EAAG,GAAG;CAAK,EAAA,CAE9G,MAAM,GAAG,CAAC,CAAC,CACX,KAAK,MAAM,EAAE,QAAQ,YAAY,EAAE,CAAC,CAAC,CACrC,KAAK,GAAG,EAAE;AACf;;;AChIA,SAAS,QAAQ,IAAoB;CACnC,OAAO,GAAG,MAAM,GAAG,CAAC,CAAC,QAAQ,KAAK,SAAU,OAAO,KAAM,OAAO,OAAO,IAAI,CAAC,GAAG,EAAE;AACnF;AAEA,SAAgB,aAAa,KAA0B;CACrD,MAAM,KAAK,IAAI,KAAK,CAAC,CAAC,QAAQ,YAAY,EAAE,CAAC,CAAC,QAAQ,QAAQ,EAAE,CAAC,CAAC,YAAY;CAC9E,IAAI,OAAO,EAAE,GAAG,OAAO;EAAE,IAAI;EAAO,GAAG,QAAQ,EAAE;CAAE;CACnD,IAAI,CAAC,OAAO,EAAE,GAAG,OAAO;CACxB,MAAM,SAAS,gDAAgD,KAAK,EAAE;CACtE,IAAI,UAAU,OAAO,OAAO,EAAG,GAAG,OAAO;EAAE,IAAI;EAAO,GAAG,QAAQ,OAAO,EAAG;CAAE;CAE7E,IAAI,OAAO;CACX,MAAM,SAAS,wBAAwB,KAAK,IAAI;CAChD,IAAI,QAAQ;EACV,MAAM,IAAI,QAAQ,OAAO,EAAG;EAC5B,OAAO,KAAK,MAAM,GAAG,CAAC,OAAO,EAAE,CAAE,MAAM,IAAI,IAAI,KAAK,IAAA,CAAK,SAAS,EAAE,EAAE,IAAI,IAAI,OAAA,CAAS,SAAS,EAAE;CACpG;CACA,MAAM,CAAC,OAAO,IAAI,QAAQ,KAAK,MAAM,IAAI;CACzC,MAAM,OAAO,OAAO,KAAK,MAAM,GAAG,IAAI,CAAC;CACvC,MAAM,QAAQ,OAAO,KAAK,MAAM,GAAG,IAAI,CAAC;CACxC,MAAM,SAAS,SAAS,KAAA,IAAY,OAAO;EAAC,GAAG;EAAM,GAAG,MAAc,IAAI,KAAK,SAAS,MAAM,MAAM,CAAC,CAAC,KAAK,GAAG;EAAG,GAAG;CAAK;CACzH,IAAI,OAAO,WAAW,GAAG,OAAO;CAChC,OAAO;EAAE,IAAI;EAAM,GAAG,OAAO,QAAQ,KAAK,MAAO,OAAO,MAAO,OAAO,OAAO,SAAS,KAAK,KAAK,EAAE,CAAC,GAAG,EAAE;CAAE;AAC5G;;;;;AAYA,SAAgB,WAAW,OAA6B;CACtD,MAAM,QAAQ,MAAM,KAAK,CAAC,CAAC,MAAM,GAAG;CACpC,IAAI,MAAM,SAAS,GAAG,OAAO;CAC7B,MAAM,CAAC,KAAK,IAAI,UAAU;CAC1B,MAAM,OAAO,aAAa,EAAE;CAC5B,IAAI,CAAC,MAAM,OAAO;CAClB,IAAI,OAAO,KAAK,KAAK,MAAM;CAC3B,IAAI,WAAW,KAAA,GAAW;EACxB,IAAI,CAAC,YAAY,KAAK,MAAM,GAAG,OAAO;EACtC,OAAO,OAAO,MAAM;EAEpB,IAAI,CAAC,KAAK,MAAM,GAAG,SAAS,GAAG,GAAG,QAAQ;EAC1C,IAAI,QAAQ,KAAK,QAAQ,KAAK,KAAK,MAAM,KAAK,OAAO;CACvD;CACA,MAAM,QAAQ,OAAO,KAAK,KAAK,MAAM,EAAE;CACvC,MAAM,OAAO,SAAS,IAAI,MAAO,MAAM,OAAO,IAAI,KAAK,MAAQ,QAAQ,OAAO,IAAI;CAClF,OAAO;EAAE,IAAI,KAAK;EAAI,MAAM,KAAK,IAAI;EAAM;CAAK;AAClD;AAEA,SAAS,QAAQ,MAAY,GAAmB;CAC9C,IAAI,KAAK,OAAO,EAAE,IAAI,OAAO;CAC7B,MAAM,QAAQ,OAAO,EAAE,KAAK,MAAM,EAAE;CACpC,MAAM,OAAO,EAAE,SAAS,IAAI,MAAO,MAAM,OAAO,EAAE,IAAI,KAAK,MAAQ,QAAQ,OAAO,EAAE,IAAI;CACxF,QAAQ,KAAK,IAAI,UAAU,EAAE;AAC/B;AAEA,MAAM,WAAW,CAAC,WAAW,aAAa,GAAI,WAAW,SAAS,CAAE;AAEpE,IAAI;;AAGJ,SAAS,cAAc,KAAsB;CAC3C,IAAI,OAAO,QAAQ,KAAK,OAAO,MAAM;CACrC,MAAM,SAAkB,CAAC;CACzB,MAAM,UAAoB,CAAC;CAC3B,KAAK,MAAM,SAAS,IAAI,MAAM,QAAQ,CAAC,CAAC,OAAO,OAAO,GAAG;EACvD,MAAM,IAAI,WAAW,KAAK;EAC1B,IAAI,GAAG,OAAO,KAAK,CAAC;OACf,QAAQ,KAAK,KAAK;CACzB;CACA,IAAI,QAAQ,QAAQ,QAAQ,OAAO,MAAM,0DAA0D,QAAQ,KAAK,IAAI,EAAE,GAAG;CACzH,QAAQ;EAAE;EAAK;CAAO;CACtB,OAAO;AACT;;;;;;;AAQA,SAAgB,eAAe,MAAc,YAAY,QAAQ,IAAI,oBAAoB,IAAa;CACpG,MAAM,OAAO,aAAa,IAAI;CAC9B,IAAI,CAAC,MAAM,OAAO;CAClB,OAAO,CAAC,GAAG,UAAU,GAAG,cAAc,SAAS,CAAC,CAAC,CAAC,MAAM,MAAM,QAAQ,MAAM,CAAC,CAAC;AAChF;;;AC9FA,MAAa,iBAAiB;;AAE9B,MAAa,qBAAqB;;AAElC,MAAM,mBAAmB;;AASzB,IAAa,eAAb,MAA0B;CAEL;CACA;CAFnB,YACE,IACA,QAAuC,KAAK,KAC5C;EAFiB,KAAA,KAAA;EACA,KAAA,QAAA;CAChB;CAEH,OAAO,OAA0D,CAAC,GAAW;EAC3E,MAAM,QAAQ,YAAY,EAAE,CAAC,CAAC,SAAS,WAAW;EAClD,MAAM,IAAI,KAAK,MAAM;EACrB,KAAK,GAAG,IACN,0GACA,OAAO,KAAK,GACZ,IAAI,KAAK,CAAC,CAAC,CAAC,YAAY,GACxB,IAAI,KAAK,IAAI,cAAc,CAAC,CAAC,YAAY,GACzC,KAAK,MAAM,MACX,KAAK,WAAW,MAAM,GAAG,GAAG,KAAK,IACnC;EACA,OAAO;CACT;CAEA,MAAM,OAAgD;EACpD,IAAI,CAAC,OAAO,OAAO;GAAE,OAAO;GAAO,WAAW;EAAM;EACpD,MAAM,OAAO,OAAO,KAAK;EACzB,MAAM,MAAM,KAAK,GAAG,IAClB,0EACA,IACF;EACA,IAAI,CAAC,KAAK,OAAO;GAAE,OAAO;GAAO,WAAW;EAAM;EAClD,MAAM,IAAI,KAAK,MAAM;EACrB,MAAM,UAAU,KAAK,MAAM,IAAI,UAAU;EACzC,MAAM,YAAY,KAAK,MAAM,IAAI,UAAU,IAAI;EAC/C,IAAI,EAAE,UAAU,MAAM,EAAE,YAAY,IAAI;GACtC,KAAK,GAAG,IAAI,mDAAmD,IAAI;GACnE,OAAO;IAAE,OAAO;IAAO,WAAW;GAAM;EAC1C;EACA,MAAM,OAAO,KAAK,IAAI,IAAI,gBAAgB,SAAS;EACnD,IAAI,OAAO,UAAU,kBAAkB,OAAO;GAAE,OAAO;GAAM,WAAW;EAAM;EAC9E,KAAK,GAAG,IAAI,iEAAiE,IAAI,KAAK,IAAI,CAAC,CAAC,YAAY,GAAG,IAAI;EAC/G,OAAO;GAAE,OAAO;GAAM,WAAW;EAAK;CACxC;CAEA,OAAO,OAAqB;EAC1B,KAAK,GAAG,IAAI,mDAAmD,OAAO,KAAK,CAAC;CAC9E;;CAGA,UAAU,aAAqC;EAC7C,IAAI,aAAa,OAAO,KAAK,GAAG,IAAI,oDAAoD,OAAO,WAAW,CAAC,CAAC,CAAC;EAC7G,OAAO,KAAK,GAAG,IAAI,4BAA4B,CAAC,CAAC;CACnD;CAEA,eAAuB;EACrB,OAAO,KAAK,GAAG,IAAI,oDAAoD,IAAI,KAAK,KAAK,MAAM,CAAC,CAAC,CAAC,YAAY,CAAC,CAAC,CAAC;CAC/G;AACF;;;AC3DA,MAAa,iBAAiB;AAC9B,MAAa,cAAc;AAE3B,MAAa,sBAAsB;;AAQnC,IAAa,YAAb,MAAuB;CAIQ;CAH7B,SAAiB;CACjB,UAA2C,CAAC;CAE5C,YAAY,KAA8B;EAAb,KAAA,MAAA;CAAc;CAE3C,IAAI,QAAgB;EAClB,OAAO,KAAK;CACd;CAEA,MAAM,UAA+B;EACnC,IAAI,KAAK,SAAS,KAAK,KAAK,KAAK;OAC5B,MAAM,IAAI,SAAe,YAAY,KAAK,QAAQ,KAAK,OAAO,CAAC;EACpE,IAAI,WAAW;EACf,aAAa;GACX,IAAI,UAAU;GACd,WAAW;GACX,MAAM,OAAO,KAAK,QAAQ,MAAM;GAChC,IAAI,MAAM,KAAK;QACV,KAAK;EACZ;CACF;AACF;AAcA,MAAM,4BAAY,IAAI,QAA6B;AAEnD,SAAgB,UAAU,KAA0B;CAClD,IAAI,OAAO,UAAU,IAAI,GAAG;CAC5B,IAAI,CAAC,MAAM;EACT,OAAO;GAAE,UAAU,IAAI,aAAa,IAAI,EAAE;GAAG,SAAS,IAAI,aAAa;GAAG,uBAAO,IAAI,IAAI;GAAG,aAAa,IAAI,UAAA,CAAsC;EAAE;EACrJ,UAAU,IAAI,KAAK,IAAI;CACzB;CACA,OAAO;AACT;;AAGA,MAAa,aAAa,QAA2B,CAAC,CAAC,IAAI,SAAS,SAAS,oBAAoB;;AAGjG,SAAgB,kBAAkB,KAA8B;CAC9D,OAAO,UAAU,GAAG,IAAI,OAAO,iBAAiB,IAAI,OAAO,OAAO;AACpE;;AAGA,SAAgB,mBAAmB,KAAuB;CACxD,MAAM,OAAO,UAAU,GAAG;CAC1B,IAAI,GAAG,SAAS;EACd,IAAI,SAAS,YAAY,oBAAoB;EAC7C,KAAK,SAAS,UAAU;CAC1B,CAAC;CACD,KAAK,QAAQ,MAAM;CACnB,IAAI,MAAM,OAAO;EAAE,WAAW;EAAU,QAAQ;EAAwB,SAAS;CAAK,CAAC;CACvF,OAAO,qBAAqB,IAAI,OAAO,OAAO;AAChD;AAEA,SAAS,cAAc,GAA2B;CAChD,IAAI;EACF,OAAO,YAAY,CAAC,CAAC,CAAC,OAAO,WAAW;CAC1C,QAAQ;EAEN,OAAO;CACT;AACF;;;;;;;;AASA,SAAS,UAAU,OAAuB;CACxC,MAAM,KAAK,0BAA0B,KAAK,KAAK;CAC/C,IAAI,IAAI,OAAO,GAAG;CAClB,MAAM,KAAK,kCAAkC,KAAK,KAAK;CACvD,OAAO,KAAK,GAAG,KAAM;AACvB;AAEA,IAAI,uBAAuB;;AAG3B,SAAS,mBAAmB,MAAoB;CAC9C,IAAI,sBAAsB;CAC1B,uBAAuB;CACvB,QAAQ,OAAO,MACb,6CAA6C,KAAK,uGAAuG,KAAK,KAChK;AACF;AAEA,SAAgB,SAAS,GAAY,YAA6B;CAChE,MAAM,OAAO,cAAc,CAAC;CAC5B,IAAI,cAAc,QAAQ,CAAC,eAAe,IAAI,KAAK,EAAE,IAAI,OAAO,iBAAiB,GAAG,mBAAmB,IAAI;CAC3G,IAAI,cAAc,QAAQ,eAAe,IAAI,GAAG;EAC9C,MAAM,OAAO,KAAK,IAAI,GAAG,OAAO,SAAS,QAAQ,IAAI,oBAAoB,KAAK,EAAE,KAAK,CAAC;EACtF,MAAM,SAAS,EAAE,IAAI,OAAO,iBAAiB,KAAK,GAAA,CAC/C,MAAM,GAAG,CAAC,CACV,KAAK,MAAM,UAAU,EAAE,KAAK,CAAC,CAAC,CAAC,CAC/B,OAAO,OAAO;EACjB,MAAM,YAAY,MAAM,MAAM,SAAS;EACvC,IAAI,aAAa,KAAK,SAAS,GAAG,OAAO;CAC3C;CACA,OAAO,QAAQ;AACjB;AAEA,SAAS,SAAS,GAAY,YAA8B;CAC1D,IAAI,IAAI,IAAI,EAAE,IAAI,GAAG,CAAC,CAAC,aAAa,UAAU,OAAO;CACrD,MAAM,OAAO,cAAc,CAAC;CAC5B,OAAO,cAAc,CAAC,CAAC,QAAQ,eAAe,IAAI,KAAK,EAAE,IAAI,OAAO,mBAAmB,CAAC,EAAE,MAAM,GAAG,CAAC,CAAC,EAAE,EAAE,KAAK,CAAC,CAAC,YAAY,MAAM;AACpI;AAEA,SAAgB,aAAa,GAAgC;CAC3D,OAAO,UAAU,GAAA,aAAiB,KAAK,KAAA;AACzC;AAEA,SAAS,mBAAmB,GAAY,OAAe,YAA2B;CAChF,UAAU,GAAG,gBAAgB,OAAO;EAClC,UAAU;EACV,UAAU;EACV,MAAM;EACN,QAAQ,SAAS,GAAG,UAAU;EAC9B,QAAQ,KAAK,MAAM,iBAAiB,GAAI;CAC1C,CAAC;AACH;;AAGA,SAAgB,aAAa,GAAY,KAAqB;CAE5D,mBAAmB,GADL,UAAU,GAAG,CAAC,CAAC,SAAS,OAAO;EAAE,IAAI,SAAS,GAAG,IAAI,OAAO,UAAU;EAAG,WAAW,EAAE,IAAI,OAAO,YAAY;CAAE,CACnG,GAAG,IAAI,OAAO,UAAU;AACpD;AAEA,SAAgB,WAAW,GAAY,KAAqB;CAC1D,MAAM,QAAQ,aAAa,CAAC;CAC5B,IAAI,OAAO,UAAU,GAAG,CAAC,CAAC,SAAS,OAAO,KAAK;CAC/C,aAAa,GAAG,gBAAgB,EAAE,MAAM,IAAI,CAAC;AAC/C;;AAGA,SAAgB,WAAW,GAAY,KAAwB;CAC7D,MAAM,QAAQ,aAAa,CAAC;CAC5B,MAAM,QAAQ,UAAU,GAAG,CAAC,CAAC,SAAS,MAAM,KAAK;CACjD,IAAI,MAAM,aAAa,OAAO,mBAAmB,GAAG,OAAO,IAAI,OAAO,UAAU;CAChF,OAAO,MAAM;AACf;;AAGA,SAAgB,aAAa,KAA0C;CACrE,OAAO,OAAO,GAAG,SAAS;EACxB,IAAI,CAAC,WAAW,GAAG,GAAG,GAAG;GACvB,IAAI,CAAC,UAAU,GAAG,GAChB,MAAM,IAAI,UAAU,KAAK,kBAAkB,0CAA0C,uDAAuD;GAE9I,MAAM,IAAI,UAAU,KAAK,gBAAgB,qBAAqB;EAChE;EACA,EAAE,IAAI,SAAS,IAAI;EACnB,MAAM,KAAK;CACb;AACF;AAEA,MAAM,2BAAW,IAAI,IAAI;CAAC;CAAQ;CAAO;CAAS;AAAQ,CAAC;;;;;AAM3D,SAAgB,YAAuC;CACrD,OAAO,OAAO,GAAG,SAAS;EACxB,IAAI,SAAS,IAAI,EAAE,IAAI,MAAM,KAAK,EAAE,IAAI,OAAA,mBAAkB,MAAM,KAC9D,MAAM,IAAI,UAAU,KAAK,QAAQ,WAAW,YAAY,UAAU,SAAS,YAAY,uCAAuC;EAEhI,MAAM,KAAK;CACb;AACF;;AAGA,SAAgB,UAAU,GAAY,SAA2B;CAC/D,EAAE,OAAO,eAAe,OAAO,UAAU,EAAE,CAAC;CAC5C,MAAM,UAAU,0CAA0C,QAAQ,SAAS,YAAY,IAAI,KAAK,IAAI;CACpG,OAAO,EAAE,KAAK,EAAE,OAAO;EAAE,MAAM;EAAc;CAAQ,EAAE,GAAG,GAAG;AAC/D;;;;;;;;;;AAyBA,eAAsB,YACpB,KACA,IACA,QACA,QACA,OAAqB,CAAC,GACA;CACtB,MAAM,OAAO,UAAU,GAAG;CAC1B,MAAM,MAAM,UAAU,EAAE;CACxB,MAAM,OAAO,KAAK,MAAM,IAAI,GAAG,KAAK;EAAE,MAAM,QAAQ,QAAQ;EAAG,QAAQ;CAAE;CACzE,IAAI,KAAK,SAAA,GAAgC,OAAO;EAAE,IAAI;EAAO,eAAe;EAAG,WAAW;EAAO,MAAM;CAAK;CAC5G,KAAK;CACL,KAAK,MAAM,IAAI,KAAK,IAAI;CACxB,MAAM,WAAW,KAAK;CACtB,IAAI;CACJ,KAAK,OAAO,IAAI,SAAe,YAAa,OAAO,OAAQ;CAC3D,IAAI;EACF,MAAM;EACN,OAAO,MAAM,UAAU,MAAM,IAAI,QAAQ,QAAQ,IAAI;CACvD,UAAU;EACR,KAAK;EACL,IAAI,EAAE,KAAK,WAAW,GAAG,KAAK,MAAM,OAAO,GAAG;CAChD;AACF;AAEA,eAAe,UAAU,MAAiB,IAAY,QAAgB,QAA0C,MAA0C;CACxJ,MAAM,EAAE,YAAY;CACpB,MAAM,SAAS,KAAK,WAAW;CAC/B,IAAI,UAAU,QAAQ,iBAAiB,KAAK,CAAC,QAAQ,cAAc,EAAE,GACnE,MAAM,IAAI,SAAS,MAAM,WAAW,GAAG,KAAK,cAAA,GAAgC,CAAC;CAG/E,MAAM,SAAS,QAAQ,cAAc,EAAE;CACvC,IAAI,QAAQ,OAAO;EAAE,IAAI;EAAO,eAAe;EAAQ,WAAW;CAAM;CACxE,IAAI,QAAQ,SAAS,IAAI,MAAM,GAAG,OAAO;EAAE,IAAI;EAAO,eAAe;EAAG,WAAW,QAAQ,KAAK,IAAI,QAAQ,EAAE,OAAO,CAAC;CAAE;CACxH,MAAM,cAAc,QAAQ,OAAO,IAAI,QAAQ,EAAE,OAAO,CAAC;CACzD,MAAM,UAAU,MAAM,KAAK,YAAY,QAAQ;CAC/C,IAAI,KAAK;CACT,IAAI;EACF,KAAK,MAAM,OAAO;CACpB,UAAU;EACR,QAAQ;CACV;CACA,IAAI,CAAC,IAAI,OAAO;EAAE,IAAI;EAAO,eAAe;EAAG,WAAW,YAAY;CAAO;CAC7E,QAAQ,MAAM,EAAE;CAChB,IAAI,YAAY,iBAAiB,QAAQ,aAAa;CACtD,OAAO,EAAE,IAAI,KAAK;AACpB;;AAGA,SAAgB,UAAU,GAAY,OAAqC;CACzE,IAAI,MAAM,IAAI,OAAO;CACrB,IAAI,MAAM,MAAM;EACd,EAAE,OAAO,eAAe,GAAG;EAC3B,OAAO,EAAE,KAAK,EAAE,OAAO;GAAE,MAAM;GAAqB,SAAS;EAA2F,EAAE,GAAG,GAAG;CAClK;CACA,OAAO,MAAM,gBAAgB,UAAU,GAAG,MAAM,aAAa,IAAI;AACnE"}
@@ -1,2 +1,2 @@
1
1
  #!/usr/bin/env node
2
- export { l as currentSetupToken, f as isClaimed, g as resetAdminPassword } from "./auth-D4yzhGUg.js";
2
+ export { l as currentSetupToken, f as isClaimed, g as resetAdminPassword } from "./auth-0NrMvHdE.js";
@@ -1 +1 @@
1
- {"version":3,"file":"bus-Cz0flS37.js","names":[],"sources":["../server/bus.ts"],"sourcesContent":["import { EventEmitter } from \"node:events\";\nimport type { Account, Approval, AuditEntry, Delivery, MailEvent, Settings } from \"../shared/types.ts\";\n\n/**\n * In-process notifications. Fired *after* the change is committed to the database, so listeners\n * can read it back. Used by the console SSE stream, `wait_for_events`, the webhook worker and the\n * ingest loop (settings changes). Listeners must not throw.\n */\nexport interface BusEvents {\n /** A new event row was committed (and its webhook deliveries enqueued). */\n event: [MailEvent];\n /** A delivery was created or changed state. */\n delivery: [Delivery];\n /** An approval was created or decided. */\n approval: [Approval];\n audit: [AuditEntry];\n /** An account was connected or its status/sync state changed. */\n account: [Account];\n settings: [Settings];\n}\n\nexport type Bus = EventEmitter<BusEvents>;\n\nexport function createBus(): Bus {\n const bus = new EventEmitter<BusEvents>();\n // Every SSE client and pending wait_for_events call adds a listener.\n bus.setMaxListeners(0);\n return bus;\n}\n"],"mappings":";;;AAuBA,SAAgB,YAAiB;CAC/B,MAAM,MAAM,IAAI,aAAwB;CAExC,IAAI,gBAAgB,CAAC;CACrB,OAAO;AACT"}
1
+ {"version":3,"file":"bus-Cz0flS37.js","names":[],"sources":["../server/bus.ts"],"sourcesContent":["import { EventEmitter } from \"node:events\";\nimport type { Account, Approval, AuditEntry, Delivery, MailEvent, PairingRequest, Settings } from \"../shared/types.ts\";\n\n/**\n * In-process notifications. Fired *after* the change is committed to the database, so listeners\n * can read it back. Used by the console SSE stream, `wait_for_events`, the webhook worker and the\n * ingest loop (settings changes). Listeners must not throw.\n */\nexport interface BusEvents {\n /** A new event row was committed (and its webhook deliveries enqueued). */\n event: [MailEvent];\n /** A delivery was created or changed state. */\n delivery: [Delivery];\n /** An approval was created or decided. */\n approval: [Approval];\n audit: [AuditEntry];\n /** An account was connected or its status/sync state changed. */\n account: [Account];\n settings: [Settings];\n /**\n * An agent's access changed: live listener streams re-check the key/grants and close on revocation.\n * Emitted by policy on update (incl. disable), key rotation, deletion and grant upsert/delete.\n */\n agent: [{ agentId: string; reason: \"updated\" | \"key_rotated\" | \"deleted\" | \"grant_changed\" }];\n pairing: [PairingRequest];\n}\n\nexport type Bus = EventEmitter<BusEvents>;\n\nexport function createBus(): Bus {\n const bus = new EventEmitter<BusEvents>();\n // Every SSE client and pending wait_for_events call adds a listener.\n bus.setMaxListeners(0);\n return bus;\n}\n"],"mappings":";;;AA6BA,SAAgB,YAAiB;CAC/B,MAAM,MAAM,IAAI,aAAwB;CAExC,IAAI,gBAAgB,CAAC;CACrB,OAAO;AACT"}
package/dist/cli.js CHANGED
@@ -63,7 +63,7 @@ const configInput = (o) => ({
63
63
  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();
64
64
  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)").allowExcessArguments().action(async (o, cmd) => {
65
65
  if (cmd.args.length) throw new Error(unknownCommand(cmd.args[0]));
66
- const { runForeground } = await import("./server-B97P3fiX.js");
66
+ const { runForeground } = await import("./server-DwpxKa6g.js");
67
67
  await runForeground({
68
68
  ...configInput(o),
69
69
  dev: !!o.dev,
@@ -104,11 +104,11 @@ program.command("reset-password").description("remove the admin password and pri
104
104
  const dbPath = join(dataDir, "agentinbox.db");
105
105
  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.`);
106
106
  const { loadConfig } = await import("./config-CIvgMqLx.js");
107
- const { Db } = await import("./db-CXhlUuVP.js");
107
+ const { Db } = await import("./db-B6gLQGBK.js");
108
108
  const { createBus } = await import("./bus-OKIGut72.js");
109
- const { SettingsStore } = await import("./settings-BK7h99j7.js");
110
- const { AuditLog } = await import("./audit-Bf3TvAAN.js");
111
- const { currentSetupToken, isClaimed, resetAdminPassword } = await import("./auth-X5SGuNNB.js");
109
+ const { SettingsStore } = await import("./settings-CeEsv4CE.js");
110
+ const { AuditLog } = await import("./audit-gVdDzkAE.js");
111
+ const { currentSetupToken, isClaimed, resetAdminPassword } = await import("./auth-CoHvbWgO.js");
112
112
  const { runningInstance } = await import("./instance-CxS8D7b2.js");
113
113
  const { consoleUrls } = await import("./urls-Dp8pBGMU.js");
114
114
  const running = runningInstance(dataDir);
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export { t as Db } from "./db-D0gvshCc.js";
@@ -164,6 +164,40 @@ const MIGRATIONS = [`
164
164
  );
165
165
  CREATE INDEX audit_ts ON audit_log(ts);
166
166
  CREATE INDEX audit_actor ON audit_log(actor_id, ts);
167
+ `, `
168
+ CREATE TABLE pairings (
169
+ id TEXT PRIMARY KEY,
170
+ device_code_hash TEXT NOT NULL UNIQUE, -- sha256 of the CLI's polling secret
171
+ user_code TEXT NOT NULL, -- shown in the terminal and the console, e.g. "K7QM-2XPD"
172
+ name TEXT NOT NULL,
173
+ description TEXT,
174
+ requested TEXT NOT NULL DEFAULT '{}', -- JSON
175
+ client TEXT NOT NULL DEFAULT '{}', -- JSON
176
+ ip TEXT,
177
+ status TEXT NOT NULL DEFAULT 'pending',
178
+ agent_id TEXT REFERENCES agents(id) ON DELETE SET NULL,
179
+ api_key_enc TEXT, -- set on approval, wiped once delivered to the CLI
180
+ last_poll_at TEXT,
181
+ decided_at TEXT,
182
+ decision_note TEXT,
183
+ delivered_at TEXT,
184
+ created_at TEXT NOT NULL,
185
+ expires_at TEXT NOT NULL
186
+ );
187
+ CREATE INDEX pairings_status ON pairings(status, created_at);
188
+ CREATE INDEX pairings_user_code ON pairings(user_code);
189
+
190
+ -- Server-side cursor per agentinbox-listen consumer (MCP ack_events keeps using agent_cursors).
191
+ CREATE TABLE listener_cursors (
192
+ agent_id TEXT NOT NULL REFERENCES agents(id) ON DELETE CASCADE,
193
+ consumer TEXT NOT NULL,
194
+ acked_seq INTEGER NOT NULL DEFAULT 0,
195
+ client TEXT NOT NULL DEFAULT '{}',
196
+ connected INTEGER NOT NULL DEFAULT 0,
197
+ last_seen_at TEXT,
198
+ updated_at TEXT NOT NULL,
199
+ PRIMARY KEY (agent_id, consumer)
200
+ );
167
201
  `];
168
202
  //#endregion
169
203
  //#region server/db/index.ts
@@ -245,4 +279,4 @@ const now = () => (/* @__PURE__ */ new Date()).toISOString();
245
279
  //#endregion
246
280
  export { now as i, bool as n, json as r, Db as t };
247
281
 
248
- //# sourceMappingURL=db-B2pnfQW5.js.map
282
+ //# sourceMappingURL=db-D0gvshCc.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"db-D0gvshCc.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 /* 2: device-flow pairing + listener cursors */ `\n CREATE TABLE pairings (\n id TEXT PRIMARY KEY,\n device_code_hash TEXT NOT NULL UNIQUE, -- sha256 of the CLI's polling secret\n user_code TEXT NOT NULL, -- shown in the terminal and the console, e.g. \"K7QM-2XPD\"\n name TEXT NOT NULL,\n description TEXT,\n requested TEXT NOT NULL DEFAULT '{}', -- JSON\n client TEXT NOT NULL DEFAULT '{}', -- JSON\n ip TEXT,\n status TEXT NOT NULL DEFAULT 'pending',\n agent_id TEXT REFERENCES agents(id) ON DELETE SET NULL,\n api_key_enc TEXT, -- set on approval, wiped once delivered to the CLI\n last_poll_at TEXT,\n decided_at TEXT,\n decision_note TEXT,\n delivered_at TEXT,\n created_at TEXT NOT NULL,\n expires_at TEXT NOT NULL\n );\n CREATE INDEX pairings_status ON pairings(status, created_at);\n CREATE INDEX pairings_user_code ON pairings(user_code);\n\n -- Server-side cursor per agentinbox-listen consumer (MCP ack_events keeps using agent_cursors).\n CREATE TABLE listener_cursors (\n agent_id TEXT NOT NULL REFERENCES agents(id) ON DELETE CASCADE,\n consumer TEXT NOT NULL,\n acked_seq INTEGER NOT NULL DEFAULT 0,\n client TEXT NOT NULL DEFAULT '{}',\n connected INTEGER NOT NULL DEFAULT 0,\n last_seen_at TEXT,\n updated_at TEXT NOT NULL,\n PRIMARY KEY (agent_id, consumer)\n );\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;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;KAoKwB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAmClD;;;;ACpMA,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"}
@@ -1 +1 @@
1
- {"version":3,"file":"instance-CgeL0q45.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 /** Started by the background service (AGENTINBOX_SERVICE=1). */\n service?: boolean;\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 service: process.env.AGENTINBOX_SERVICE === \"1\",\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 const where = `(pid ${other.pid}${other.port ? `, port ${other.port}` : \"\"})`;\n throw new Error(\n other.service\n ? `agentinbox is already running as a background service with the data directory ${dataDir} ${where}. ` +\n `Use that one, stop it (\\`${cliCommand(\"service uninstall\")}\\` keeps your data), or use another --data directory.`\n : `agentinbox is already running with the data directory ${dataDir} ${where}. ` +\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":"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"}
1
+ {"version":3,"file":"instance-CgeL0q45.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 /** Accept device-flow pairing requests from `npx agentinbox-cli connect` (each still needs approval). */\n pairingEnabled: boolean;\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 /** `agentinbox listen` instances (one per consumer name) and their stream position. */\n listeners: ListenerInfo[];\n}\n\nexport interface ListenerInfo {\n consumer: string;\n connected: boolean;\n ackedSeq: number;\n lastSeenAt: string | null;\n client: { hostname?: string; platform?: string; version?: string };\n}\n\nexport type PairingStatus = \"pending\" | \"approved\" | \"denied\" | \"expired\";\n\n/** A device-flow pairing request from `npx agentinbox-cli connect`, shown in the approvals inbox. */\nexport interface PairingRequest {\n id: string; // par_…\n /** The code the agent's terminal shows; the human must match it before approving. */\n userCode: string;\n name: string;\n description: string | null;\n requested: { accounts?: string[]; capabilities?: Capability[]; sendMode?: SendMode; reason?: string };\n client: { hostname?: string; platform?: string; tool?: string; version?: string };\n /** Requester IP as seen by the daemon. */\n ip: string | null;\n status: PairingStatus;\n /** Set once approved. */\n agentId: string | null;\n createdAt: string;\n expiresAt: string;\n decidedAt: string | null;\n decisionNote: string | null;\n}\n\n/** POST /api/pairings/:id/approve. Grants default to sendMode draft_only in the console. */\nexport interface ApprovePairingInput {\n /** Typed by the human from the agent's terminal (not pre-filled); case and dashes are ignored. 409 on mismatch. */\n userCode: string;\n name?: string;\n grants: (GrantInput & { accountId: string })[];\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/pairings?status → PairingRequest[]\n// POST /api/pairings/:id/approve ApprovePairingInput → Agent (the key goes to the polling CLI, never to the console)\n// POST /api/pairings/:id/deny {note?} → PairingRequest\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// /agent/v1/* pairing + listener stream (see shared/agent-api.ts)\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 | { kind: \"pairing\"; data: PairingRequest };\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 /** Started by the background service (AGENTINBOX_SERVICE=1). */\n service?: boolean;\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 service: process.env.AGENTINBOX_SERVICE === \"1\",\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 const where = `(pid ${other.pid}${other.port ? `, port ${other.port}` : \"\"})`;\n throw new Error(\n other.service\n ? `agentinbox is already running as a background service with the data directory ${dataDir} ${where}. ` +\n `Use that one, stop it (\\`${cliCommand(\"service uninstall\")}\\` keeps your data), or use another --data directory.`\n : `agentinbox is already running with the data directory ${dataDir} ${where}. ` +\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":"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"}