agentinbox-cli 0.0.0 → 0.1.0-nightly.10

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 (176) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +337 -0
  3. package/dist/admin-CXkmdTYo.js +196 -0
  4. package/dist/admin-CXkmdTYo.js.map +1 -0
  5. package/dist/audit-bItbJYFP.js +65 -0
  6. package/dist/audit-bItbJYFP.js.map +1 -0
  7. package/dist/audit-gVdDzkAE.js +2 -0
  8. package/dist/auth-0NrMvHdE.js +550 -0
  9. package/dist/auth-0NrMvHdE.js.map +1 -0
  10. package/dist/auth-CoHvbWgO.js +2 -0
  11. package/dist/bus-Cz0flS37.js +12 -0
  12. package/dist/bus-Cz0flS37.js.map +1 -0
  13. package/dist/bus-OKIGut72.js +2 -0
  14. package/dist/cli.js +220 -0
  15. package/dist/cli.js.map +1 -0
  16. package/dist/commands-DZ60j8_y.js +103 -0
  17. package/dist/commands-DZ60j8_y.js.map +1 -0
  18. package/dist/config-CIvgMqLx.js +2 -0
  19. package/dist/config-v975gxv4.js +44 -0
  20. package/dist/config-v975gxv4.js.map +1 -0
  21. package/dist/connect-BHC2trBi.js +292 -0
  22. package/dist/connect-BHC2trBi.js.map +1 -0
  23. package/dist/content-BXblC_Qs.js +1162 -0
  24. package/dist/content-BXblC_Qs.js.map +1 -0
  25. package/dist/content-worker.js +28 -0
  26. package/dist/content-worker.js.map +1 -0
  27. package/dist/crypto-DYZ4R7c2.js +74 -0
  28. package/dist/crypto-DYZ4R7c2.js.map +1 -0
  29. package/dist/db-B6gLQGBK.js +2 -0
  30. package/dist/db-D0gvshCc.js +282 -0
  31. package/dist/db-D0gvshCc.js.map +1 -0
  32. package/dist/errors-FbMS3QBo.js +39 -0
  33. package/dist/errors-FbMS3QBo.js.map +1 -0
  34. package/dist/filter-DGVz6-P1.js +140 -0
  35. package/dist/filter-DGVz6-P1.js.map +1 -0
  36. package/dist/instance-BBzEsYZO.js +175 -0
  37. package/dist/instance-BBzEsYZO.js.map +1 -0
  38. package/dist/instance-DLNTj9nm.js +2 -0
  39. package/dist/listen-D8DkEhqM.js +1724 -0
  40. package/dist/listen-D8DkEhqM.js.map +1 -0
  41. package/dist/listen-DT-omyD6.js +2 -0
  42. package/dist/mcpheader-By2dIyZq.js +2 -0
  43. package/dist/mcpheader-D9rrI-aa.js +65 -0
  44. package/dist/mcpheader-D9rrI-aa.js.map +1 -0
  45. package/dist/mcpsetup-7KxroOCp.js +2 -0
  46. package/dist/mcpsetup-DJ7ebFGJ.js +266 -0
  47. package/dist/mcpsetup-DJ7ebFGJ.js.map +1 -0
  48. package/dist/paths-BCBp8vrG.js +2 -0
  49. package/dist/paths-TLp8hiMt.js +40 -0
  50. package/dist/paths-TLp8hiMt.js.map +1 -0
  51. package/dist/profile-CgGgJ4Dm.js +181 -0
  52. package/dist/profile-CgGgJ4Dm.js.map +1 -0
  53. package/dist/profile-RbN2FRz7.js +2 -0
  54. package/dist/recipes-7d_Xws4M.js +2 -0
  55. package/dist/recipes-UalOc_LB.js +100 -0
  56. package/dist/recipes-UalOc_LB.js.map +1 -0
  57. package/dist/send-Dk-t8ia-.js +108 -0
  58. package/dist/send-Dk-t8ia-.js.map +1 -0
  59. package/dist/server-Hunjpt8n.js +8103 -0
  60. package/dist/server-Hunjpt8n.js.map +1 -0
  61. package/dist/service-BAgZDh22.js +2 -0
  62. package/dist/service-DYdD92CC.js +2 -0
  63. package/dist/service-D_ZEs3Zk.js +508 -0
  64. package/dist/service-D_ZEs3Zk.js.map +1 -0
  65. package/dist/service-whbugeQp.js +295 -0
  66. package/dist/service-whbugeQp.js.map +1 -0
  67. package/dist/settings-CeEsv4CE.js +2 -0
  68. package/dist/settings-DLUStIN7.js +109 -0
  69. package/dist/settings-DLUStIN7.js.map +1 -0
  70. package/dist/setup-token-BO89xnpb.js +48 -0
  71. package/dist/setup-token-BO89xnpb.js.map +1 -0
  72. package/dist/skill/SKILL.md +277 -0
  73. package/dist/skill-BJFn2bHK.js +2 -0
  74. package/dist/skill-WKbeyNct.js +43 -0
  75. package/dist/skill-WKbeyNct.js.map +1 -0
  76. package/dist/types-3eEsBFVj.js +37 -0
  77. package/dist/types-3eEsBFVj.js.map +1 -0
  78. package/dist/ui-B8Sd4Y7Y.js +81 -0
  79. package/dist/ui-B8Sd4Y7Y.js.map +1 -0
  80. package/dist/urls-Dp8pBGMU.js +2 -0
  81. package/dist/urls-U_LfYV61.js +27 -0
  82. package/dist/urls-U_LfYV61.js.map +1 -0
  83. package/dist/web/assets/_agentId-DAj08NDq.js +1 -0
  84. package/dist/web/assets/_app-B6q_HS9y.js +1 -0
  85. package/dist/web/assets/_app-CAQ6Mu1s.js +63 -0
  86. package/dist/web/assets/_webhookId-CMgBUN2Z.js +1 -0
  87. package/dist/web/assets/account-status-omoUlgBI.js +1 -0
  88. package/dist/web/assets/accounts-DyLfACj_.js +1 -0
  89. package/dist/web/assets/accounts-list-DiXACW0-.js +1 -0
  90. package/dist/web/assets/activity-zrckFhPk.js +1 -0
  91. package/dist/web/assets/agent-create-form-DKwaY1Dg.js +1 -0
  92. package/dist/web/assets/agents-Drvjccgt.js +1 -0
  93. package/dist/web/assets/approvals-4kd0_J8X.js +2 -0
  94. package/dist/web/assets/arrow-left-DVJV4Zx7.js +1 -0
  95. package/dist/web/assets/arrow-right-4PFX6MSS.js +1 -0
  96. package/dist/web/assets/audit-76H45orR.js +1 -0
  97. package/dist/web/assets/audit-bits-B1We-fQ3.js +1 -0
  98. package/dist/web/assets/bot-DVw15lBn.js +1 -0
  99. package/dist/web/assets/card-EQTpqntn.js +1 -0
  100. package/dist/web/assets/checkbox-DDZZurgK.js +1 -0
  101. package/dist/web/assets/chevron-down-892ylB4R.js +1 -0
  102. package/dist/web/assets/chevron-right-CbBKS6-T.js +1 -0
  103. package/dist/web/assets/chips-input--xgMZpg2.js +1 -0
  104. package/dist/web/assets/circle-x-Bi4IGwky.js +1 -0
  105. package/dist/web/assets/common-PG6weTqi.js +1 -0
  106. package/dist/web/assets/connect-host-DdkOJnMu.js +1 -0
  107. package/dist/web/assets/data-table-Ca9aQZKR.js +1 -0
  108. package/dist/web/assets/dialog-CxNRCZdE.js +1 -0
  109. package/dist/web/assets/dist-B0e3Ju3r.js +1 -0
  110. package/dist/web/assets/empty-CE_WZGEC.js +1 -0
  111. package/dist/web/assets/es2015-DPaldoxG.js +41 -0
  112. package/dist/web/assets/event-row-2xoyzgbP.js +1 -0
  113. package/dist/web/assets/events-Bizq3DPC.js +1 -0
  114. package/dist/web/assets/eye-off-DjpaihHf.js +1 -0
  115. package/dist/web/assets/field-CcyMn3ey.js +1 -0
  116. package/dist/web/assets/geist-cyrillic-ext-wght-normal-DjL33-gN.woff2 +0 -0
  117. package/dist/web/assets/geist-cyrillic-wght-normal-BEAKL7Jp.woff2 +0 -0
  118. package/dist/web/assets/geist-latin-ext-wght-normal-DC-KSUi6.woff2 +0 -0
  119. package/dist/web/assets/geist-latin-wght-normal-BgDaEnEv.woff2 +0 -0
  120. package/dist/web/assets/geist-mono-cyrillic-ext-wght-normal-X_5orZeX.woff2 +0 -0
  121. package/dist/web/assets/geist-mono-cyrillic-wght-normal-DiZS0aHC.woff2 +0 -0
  122. package/dist/web/assets/geist-mono-latin-ext-wght-normal-Bwz-egvJ.woff2 +0 -0
  123. package/dist/web/assets/geist-mono-latin-wght-normal-XN7g48iV.woff2 +0 -0
  124. package/dist/web/assets/geist-mono-symbols2-wght-normal-CO5SzqOn.woff2 +0 -0
  125. package/dist/web/assets/geist-mono-vietnamese-wght-normal-DadHysG0.woff2 +0 -0
  126. package/dist/web/assets/geist-vietnamese-wght-normal-6IgcOCM7.woff2 +0 -0
  127. package/dist/web/assets/get-started-DXUVmyNX.js +1 -0
  128. package/dist/web/assets/grants-BhXj1Pew.js +1 -0
  129. package/dist/web/assets/index-Bd4Y5wSp.js +10 -0
  130. package/dist/web/assets/index-akvt_1UI.css +2 -0
  131. package/dist/web/assets/input-CMhmmHiH.js +1 -0
  132. package/dist/web/assets/input-group-DRs3TNaT.js +1 -0
  133. package/dist/web/assets/key-round-CVIZRCLM.js +1 -0
  134. package/dist/web/assets/labels-C_Pwium9.js +1 -0
  135. package/dist/web/assets/login-aCr-cPqJ.js +1 -0
  136. package/dist/web/assets/mail-CAVmuESo.js +1 -0
  137. package/dist/web/assets/mutations-DVlbDx0M.js +1 -0
  138. package/dist/web/assets/paperclip-QFjM4y6n.js +1 -0
  139. package/dist/web/assets/password-input-_m9Ycphv.js +1 -0
  140. package/dist/web/assets/pencil-Bf1JhsG0.js +1 -0
  141. package/dist/web/assets/play-DiPk49JH.js +1 -0
  142. package/dist/web/assets/plus-CETmK0SB.js +1 -0
  143. package/dist/web/assets/preload-helper-BFuRUfdt.js +1 -0
  144. package/dist/web/assets/progress-3ZOsbLSK.js +6 -0
  145. package/dist/web/assets/queries-a3M5_loa.js +2 -0
  146. package/dist/web/assets/refresh-cw-DOn9kD7g.js +1 -0
  147. package/dist/web/assets/search-gcYjitzV.js +1 -0
  148. package/dist/web/assets/select-C8tJBHb5.js +1 -0
  149. package/dist/web/assets/send-BhQwhuJo.js +1 -0
  150. package/dist/web/assets/settings-H7ExHD9L.js +1 -0
  151. package/dist/web/assets/settings-form-CNUionsI.js +1 -0
  152. package/dist/web/assets/setup-DLbH5Dvl.js +1 -0
  153. package/dist/web/assets/sheet-BQKpA2xa.js +1 -0
  154. package/dist/web/assets/shield-alert-CbFgd06X.js +1 -0
  155. package/dist/web/assets/stamp-CH2OvjH2.js +1 -0
  156. package/dist/web/assets/stream-C2Tr2d5O.js +1 -0
  157. package/dist/web/assets/tabs-ySiz7Mni.js +1 -0
  158. package/dist/web/assets/tag-CP72Kk6Y.js +1 -0
  159. package/dist/web/assets/test-connection-Bd2Gac0O.js +1 -0
  160. package/dist/web/assets/textarea-CCokBZIw.js +1 -0
  161. package/dist/web/assets/toggle-group-ne0pVDg6.js +1 -0
  162. package/dist/web/assets/trash-DxdMNg8B.js +1 -0
  163. package/dist/web/assets/use-sign-out-BuzlTb0Q.js +1 -0
  164. package/dist/web/assets/useInfiniteQuery-DE6WChlb.js +1 -0
  165. package/dist/web/assets/useMatch-BKUnm54_.js +1 -0
  166. package/dist/web/assets/useMutation-DeWvLw1a.js +1 -0
  167. package/dist/web/assets/useQuery-DAhf1YDH.js +1 -0
  168. package/dist/web/assets/useSelector-D70eKNYs.js +1 -0
  169. package/dist/web/assets/webhook-BWT75M78.js +1 -0
  170. package/dist/web/assets/webhook-sheet-Cy_jHIqG.js +34 -0
  171. package/dist/web/assets/webhooks-CTAj5Ovp.js +1 -0
  172. package/dist/web/assets/with-selector-CkNyXYMW.js +1 -0
  173. package/dist/web/assets/x-BHjDLXNO.js +1 -0
  174. package/dist/web/favicon.svg +1 -0
  175. package/dist/web/index.html +31 -0
  176. package/package.json +95 -1
@@ -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"}
@@ -0,0 +1,39 @@
1
+ #!/usr/bin/env node
2
+ //#region server/util/errors.ts
3
+ /** Throw from anywhere in request handling; the API layer turns it into an ApiError response. */
4
+ var HttpError = class extends Error {
5
+ status;
6
+ code;
7
+ hint;
8
+ constructor(status, code, message, hint) {
9
+ super(message);
10
+ this.status = status;
11
+ this.code = code;
12
+ this.hint = hint;
13
+ }
14
+ toJSON() {
15
+ return { error: {
16
+ code: this.code,
17
+ message: this.message,
18
+ ...this.hint ? { hint: this.hint } : {}
19
+ } };
20
+ }
21
+ };
22
+ const badRequest = (message, hint) => new HttpError(400, "bad_request", message, hint);
23
+ const notFound = (what) => new HttpError(404, "not_found", `${what} not found`);
24
+ const conflict = (message, hint) => new HttpError(409, "conflict", message, hint);
25
+ /** Thrown by policy checks. `reason` is the guardrail name recorded in the audit log. */
26
+ var PolicyDenied = class extends Error {
27
+ reason;
28
+ constructor(reason, message) {
29
+ super(message);
30
+ this.reason = reason;
31
+ }
32
+ };
33
+ function errorMessage(err) {
34
+ return err instanceof Error ? err.message : String(err);
35
+ }
36
+ //#endregion
37
+ export { errorMessage as a, conflict as i, PolicyDenied as n, notFound as o, badRequest as r, HttpError as t };
38
+
39
+ //# sourceMappingURL=errors-FbMS3QBo.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"errors-FbMS3QBo.js","names":[],"sources":["../server/util/errors.ts"],"sourcesContent":["import type { ApiError } from \"../../shared/types.ts\";\n\n/** Throw from anywhere in request handling; the API layer turns it into an ApiError response. */\nexport class HttpError extends Error {\n constructor(\n readonly status: number,\n readonly code: string,\n message: string,\n readonly hint?: string,\n ) {\n super(message);\n }\n\n toJSON(): ApiError {\n return { error: { code: this.code, message: this.message, ...(this.hint ? { hint: this.hint } : {}) } };\n }\n}\n\nexport const badRequest = (message: string, hint?: string) => new HttpError(400, \"bad_request\", message, hint);\nexport const notFound = (what: string) => new HttpError(404, \"not_found\", `${what} not found`);\nexport const conflict = (message: string, hint?: string) => new HttpError(409, \"conflict\", message, hint);\n\n/** Thrown by policy checks. `reason` is the guardrail name recorded in the audit log. */\nexport class PolicyDenied extends Error {\n constructor(\n readonly reason: string,\n message: string,\n ) {\n super(message);\n }\n}\n\nexport function errorMessage(err: unknown): string {\n return err instanceof Error ? err.message : String(err);\n}\n"],"mappings":";;;AAGA,IAAa,YAAb,cAA+B,MAAM;CAExB;CACA;CAEA;CAJX,YACE,QACA,MACA,SACA,MACA;EACA,MAAM,OAAO;EALJ,KAAA,SAAA;EACA,KAAA,OAAA;EAEA,KAAA,OAAA;CAGX;CAEA,SAAmB;EACjB,OAAO,EAAE,OAAO;GAAE,MAAM,KAAK;GAAM,SAAS,KAAK;GAAS,GAAI,KAAK,OAAO,EAAE,MAAM,KAAK,KAAK,IAAI,CAAC;EAAG,EAAE;CACxG;AACF;AAEA,MAAa,cAAc,SAAiB,SAAkB,IAAI,UAAU,KAAK,eAAe,SAAS,IAAI;AAC7G,MAAa,YAAY,SAAiB,IAAI,UAAU,KAAK,aAAa,GAAG,KAAK,WAAW;AAC7F,MAAa,YAAY,SAAiB,SAAkB,IAAI,UAAU,KAAK,YAAY,SAAS,IAAI;;AAGxG,IAAa,eAAb,cAAkC,MAAM;CAE3B;CADX,YACE,QACA,SACA;EACA,MAAM,OAAO;EAHJ,KAAA,SAAA;CAIX;AACF;AAEA,SAAgB,aAAa,KAAsB;CACjD,OAAO,eAAe,QAAQ,IAAI,UAAU,OAAO,GAAG;AACxD"}
@@ -0,0 +1,140 @@
1
+ #!/usr/bin/env node
2
+ import { n as EVENT_TYPES } from "./types-3eEsBFVj.js";
3
+ import { z } from "zod";
4
+ import { RE2JS } from "re2js";
5
+ //#region server/util/glob.ts
6
+ /**
7
+ * Case-insensitive address globs used by grant allowlists (`allowTo`) and webhook filters
8
+ * (`from`, `to`). `*` matches any run of characters, everything else is literal:
9
+ * "*@example.com", "billing@*", "alice@example.com".
10
+ *
11
+ * Case folding is ASCII-only: Unicode folding would let look-alikes such as the Kelvin sign (U+212A)
12
+ * match "k". Domains are NFKC-normalized first (as IDNA does), local parts are compared as written.
13
+ */
14
+ const asciiLower = (s) => s.replace(/[A-Z]/g, (c) => c.toLowerCase());
15
+ function normalize(address) {
16
+ const s = address.trim();
17
+ const at = s.lastIndexOf("@");
18
+ if (at < 0) return asciiLower(s);
19
+ return asciiLower(s.slice(0, at)) + "@" + asciiLower(s.slice(at + 1).normalize("NFKC"));
20
+ }
21
+ function globToRegExp(glob) {
22
+ const body = normalize(glob).split("*").map((part) => part.replace(/[.+?^${}()|[\]\\]/g, "\\$&")).join(".*");
23
+ return new RegExp(`^${body}$`, "s");
24
+ }
25
+ function matchesGlob(value, glob) {
26
+ return globToRegExp(glob).test(normalize(value));
27
+ }
28
+ function matchesAnyGlob(value, globs) {
29
+ return globs.some((g) => matchesGlob(value, g));
30
+ }
31
+ //#endregion
32
+ //#region server/webhooks/regex.ts
33
+ /**
34
+ * Webhook subject filters run synchronously on attacker-controlled subjects during events.emit, so
35
+ * they use RE2 (re2js): matching is linear in the input, with no catastrophic backtracking. RE2
36
+ * syntax is JavaScript's minus lookarounds and backreferences.
37
+ */
38
+ const RE2_SYNTAX_HINT = "Subject patterns use RE2 syntax, which runs in linear time: lookarounds ((?=…), (?!…), (?<=…), (?<!…)) and backreferences (\\1) aren't supported.";
39
+ /** Compiled patterns by source; null marks a pattern RE2 rejects. Bounded so it can't grow forever. */
40
+ const cache = /* @__PURE__ */ new Map();
41
+ const CACHE_MAX = 1e3;
42
+ /** RE2's reason for rejecting the pattern, or null when it compiles. */
43
+ function subjectPatternError(pattern) {
44
+ try {
45
+ RE2JS.compile(pattern, RE2JS.CASE_INSENSITIVE);
46
+ return null;
47
+ } catch (err) {
48
+ return (err instanceof Error ? err.message : String(err)).replace(/^error parsing regexp: /, "");
49
+ }
50
+ }
51
+ /** Case-insensitive search. A pattern RE2 rejects (e.g. stored before this check existed) never matches. */
52
+ function subjectMatches(pattern, subject) {
53
+ let re = cache.get(pattern);
54
+ if (re === void 0) {
55
+ try {
56
+ re = RE2JS.compile(pattern, RE2JS.CASE_INSENSITIVE);
57
+ } catch {
58
+ re = null;
59
+ }
60
+ if (cache.size >= CACHE_MAX) cache.delete(cache.keys().next().value);
61
+ cache.set(pattern, re);
62
+ }
63
+ return re ? re.matcher(subject).find() : false;
64
+ }
65
+ //#endregion
66
+ //#region server/webhooks/filter.ts
67
+ /** Events about the account itself rather than a message: filters only narrow them by account. */
68
+ const ACCOUNT_EVENT_TYPES = ["account.auth_failed", "sync.gap"];
69
+ /** Bounds glob backtracking: filters run on attacker-controlled addresses during every emit. */
70
+ const MAX_GLOB_STARS = 8;
71
+ const list = (max, label) => z.array(z.string().trim().min(1, `${label} entries must not be empty`).max(320)).max(max, `At most ${max} ${label} entries`).optional();
72
+ const globList = (max, label) => list(max, label).refine((globs) => !globs?.some((g) => g.split("*").length - 1 > MAX_GLOB_STARS), `${label} patterns may use at most ${MAX_GLOB_STARS} "*"`);
73
+ /** Strict, so a misspelled criterion is an error rather than a filter that silently matches everything. */
74
+ const webhookFilterSchema = z.object({
75
+ accountIds: list(100, "account"),
76
+ labelIds: list(100, "label"),
77
+ from: globList(100, "from"),
78
+ to: globList(100, "to"),
79
+ subject: z.string().max(200, "Subject pattern must be at most 200 characters").optional().superRefine((s, issues) => {
80
+ const error = s === void 0 ? null : subjectPatternError(s);
81
+ if (error) issues.addIssue({
82
+ code: "custom",
83
+ message: `Subject pattern is not a valid regular expression (${error}). ${RE2_SYNTAX_HINT}`
84
+ });
85
+ }),
86
+ hasAttachments: z.boolean().optional()
87
+ }).strict();
88
+ const webhookFields = {
89
+ agentId: z.string().min(1, "Choose an agent"),
90
+ name: z.string().trim().min(1, "Name is required").max(100, "Name must be at most 100 characters"),
91
+ url: z.string().trim().max(2048).refine((u) => URL.canParse(u) && /^https?:$/.test(new URL(u).protocol), "URL must be a full http(s) URL, e.g. https://example.com/hooks/mail").refine((u) => !URL.canParse(u) || !new URL(u).username && !new URL(u).password, "URL must not contain credentials; verify requests with the signing secret instead"),
92
+ eventTypes: z.array(z.enum(EVENT_TYPES)).min(1, "Pick at least one event type"),
93
+ filter: webhookFilterSchema,
94
+ includeBody: z.boolean(),
95
+ enabled: z.boolean()
96
+ };
97
+ const webhookInputSchema = z.object({
98
+ ...webhookFields,
99
+ filter: webhookFilterSchema.default({}),
100
+ includeBody: z.boolean().default(false),
101
+ enabled: z.boolean().default(true)
102
+ });
103
+ const webhookPatchSchema = z.object(webhookFields).partial();
104
+ /** Drops empty lists and blank values so stored filters stay minimal. */
105
+ function normalizeFilter(f) {
106
+ const out = {};
107
+ for (const key of [
108
+ "accountIds",
109
+ "labelIds",
110
+ "from",
111
+ "to"
112
+ ]) {
113
+ const v = f[key]?.map((s) => s.trim()).filter(Boolean);
114
+ if (v?.length) out[key] = [...new Set(v)];
115
+ }
116
+ if (f.subject?.trim()) out.subject = f.subject;
117
+ if (typeof f.hasAttachments === "boolean") out.hasAttachments = f.hasAttachments;
118
+ return out;
119
+ }
120
+ /** Whether an event passes a webhook filter. Every set criterion must pass. */
121
+ function matchesFilter(f, ev) {
122
+ if (f.accountIds?.length && !f.accountIds.includes(ev.accountId)) return false;
123
+ if (ACCOUNT_EVENT_TYPES.includes(ev.type)) return true;
124
+ const hasMessageCriteria = !!(f.labelIds?.length || f.from?.length || f.to?.length || f.subject || f.hasAttachments !== void 0);
125
+ const m = ev.message;
126
+ if (!m) return !hasMessageCriteria;
127
+ if (f.labelIds?.length) {
128
+ const labels = /* @__PURE__ */ new Set([...m.labelIds, ...ev.labelIds ?? []]);
129
+ if (!f.labelIds.some((l) => labels.has(l))) return false;
130
+ }
131
+ if (f.from?.length && !(m.from && matchesAnyGlob(m.from.email, f.from))) return false;
132
+ if (f.to?.length && ![...m.to, ...m.cc].some((a) => matchesAnyGlob(a.email, f.to))) return false;
133
+ if (f.subject && !subjectMatches(f.subject, m.subject.slice(0, 256))) return false;
134
+ if (f.hasAttachments !== void 0 && m.hasAttachments !== f.hasAttachments) return false;
135
+ return true;
136
+ }
137
+ //#endregion
138
+ export { webhookPatchSchema as a, matchesGlob as c, webhookInputSchema as i, normalizeFilter as n, subjectPatternError as o, webhookFilterSchema as r, matchesAnyGlob as s, matchesFilter as t };
139
+
140
+ //# sourceMappingURL=filter-DGVz6-P1.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"filter-DGVz6-P1.js","names":[],"sources":["../server/util/glob.ts","../server/webhooks/regex.ts","../server/webhooks/filter.ts"],"sourcesContent":["/**\n * Case-insensitive address globs used by grant allowlists (`allowTo`) and webhook filters\n * (`from`, `to`). `*` matches any run of characters, everything else is literal:\n * \"*@example.com\", \"billing@*\", \"alice@example.com\".\n *\n * Case folding is ASCII-only: Unicode folding would let look-alikes such as the Kelvin sign (U+212A)\n * match \"k\". Domains are NFKC-normalized first (as IDNA does), local parts are compared as written.\n */\nconst asciiLower = (s: string) => s.replace(/[A-Z]/g, (c) => c.toLowerCase());\n\nfunction normalize(address: string): string {\n const s = address.trim();\n const at = s.lastIndexOf(\"@\");\n if (at < 0) return asciiLower(s);\n return asciiLower(s.slice(0, at)) + \"@\" + asciiLower(s.slice(at + 1).normalize(\"NFKC\"));\n}\n\nexport function globToRegExp(glob: string): RegExp {\n const body = normalize(glob)\n .split(\"*\")\n .map((part) => part.replace(/[.+?^${}()|[\\]\\\\]/g, \"\\\\$&\"))\n .join(\".*\");\n return new RegExp(`^${body}$`, \"s\");\n}\n\nexport function matchesGlob(value: string, glob: string): boolean {\n return globToRegExp(glob).test(normalize(value));\n}\n\nexport function matchesAnyGlob(value: string, globs: readonly string[]): boolean {\n return globs.some((g) => matchesGlob(value, g));\n}\n","import { RE2JS } from \"re2js\";\n\n/**\n * Webhook subject filters run synchronously on attacker-controlled subjects during events.emit, so\n * they use RE2 (re2js): matching is linear in the input, with no catastrophic backtracking. RE2\n * syntax is JavaScript's minus lookarounds and backreferences.\n */\n\nexport const RE2_SYNTAX_HINT =\n \"Subject patterns use RE2 syntax, which runs in linear time: lookarounds ((?=…), (?!…), (?<=…), (?<!…)) and backreferences (\\\\1) aren't supported.\";\n\n/** Compiled patterns by source; null marks a pattern RE2 rejects. Bounded so it can't grow forever. */\nconst cache = new Map<string, RE2JS | null>();\nconst CACHE_MAX = 1000;\n\n/** RE2's reason for rejecting the pattern, or null when it compiles. */\nexport function subjectPatternError(pattern: string): string | null {\n try {\n RE2JS.compile(pattern, RE2JS.CASE_INSENSITIVE);\n return null;\n } catch (err) {\n return (err instanceof Error ? err.message : String(err)).replace(/^error parsing regexp: /, \"\");\n }\n}\n\n/** Case-insensitive search. A pattern RE2 rejects (e.g. stored before this check existed) never matches. */\nexport function subjectMatches(pattern: string, subject: string): boolean {\n let re = cache.get(pattern);\n if (re === undefined) {\n try {\n re = RE2JS.compile(pattern, RE2JS.CASE_INSENSITIVE);\n } catch {\n re = null;\n }\n if (cache.size >= CACHE_MAX) cache.delete(cache.keys().next().value!);\n cache.set(pattern, re);\n }\n return re ? re.matcher(subject).find() : false;\n}\n","import { z } from \"zod\";\nimport { EVENT_TYPES, type EventType, type WebhookFilter } from \"../../shared/types.ts\";\nimport type { StoredEvent } from \"../events/types.ts\";\nimport { matchesAnyGlob } from \"../util/glob.ts\";\nimport { RE2_SYNTAX_HINT, subjectMatches, subjectPatternError } from \"./regex.ts\";\n\n/** Events about the account itself rather than a message: filters only narrow them by account. */\nexport const ACCOUNT_EVENT_TYPES: readonly EventType[] = [\"account.auth_failed\", \"sync.gap\"];\n\n/** Bounds glob backtracking: filters run on attacker-controlled addresses during every emit. */\nconst MAX_GLOB_STARS = 8;\n/** Only this much of the subject is tested against the pattern (bounds matching cost). */\nexport const MAX_SUBJECT_TESTED = 256;\n\nconst list = (max: number, label: string) =>\n z\n .array(z.string().trim().min(1, `${label} entries must not be empty`).max(320))\n .max(max, `At most ${max} ${label} entries`)\n .optional();\n\nconst globList = (max: number, label: string) =>\n list(max, label).refine((globs) => !globs?.some((g) => g.split(\"*\").length - 1 > MAX_GLOB_STARS), `${label} patterns may use at most ${MAX_GLOB_STARS} \"*\"`);\n\n/** Strict, so a misspelled criterion is an error rather than a filter that silently matches everything. */\nexport const webhookFilterSchema = z\n .object({\n accountIds: list(100, \"account\"),\n labelIds: list(100, \"label\"),\n from: globList(100, \"from\"),\n to: globList(100, \"to\"),\n subject: z\n .string()\n .max(200, \"Subject pattern must be at most 200 characters\")\n .optional()\n .superRefine((s, issues) => {\n const error = s === undefined ? null : subjectPatternError(s);\n if (error) issues.addIssue({ code: \"custom\", message: `Subject pattern is not a valid regular expression (${error}). ${RE2_SYNTAX_HINT}` });\n }),\n hasAttachments: z.boolean().optional(),\n })\n .strict();\n\nconst webhookFields = {\n agentId: z.string().min(1, \"Choose an agent\"),\n name: z.string().trim().min(1, \"Name is required\").max(100, \"Name must be at most 100 characters\"),\n url: z\n .string()\n .trim()\n .max(2048)\n .refine((u) => URL.canParse(u) && /^https?:$/.test(new URL(u).protocol), \"URL must be a full http(s) URL, e.g. https://example.com/hooks/mail\")\n .refine((u) => !URL.canParse(u) || (!new URL(u).username && !new URL(u).password), \"URL must not contain credentials; verify requests with the signing secret instead\"),\n eventTypes: z.array(z.enum(EVENT_TYPES)).min(1, \"Pick at least one event type\"),\n filter: webhookFilterSchema,\n includeBody: z.boolean(),\n enabled: z.boolean(),\n};\n\nexport const webhookInputSchema = z\n .object({\n ...webhookFields,\n filter: webhookFilterSchema.default({}),\n includeBody: z.boolean().default(false),\n enabled: z.boolean().default(true),\n });\n\nexport const webhookPatchSchema = z.object(webhookFields).partial();\n\n/** Drops empty lists and blank values so stored filters stay minimal. */\nexport function normalizeFilter(f: WebhookFilter): WebhookFilter {\n const out: WebhookFilter = {};\n const lists = [\"accountIds\", \"labelIds\", \"from\", \"to\"] as const;\n for (const key of lists) {\n const v = f[key]?.map((s) => s.trim()).filter(Boolean);\n if (v?.length) out[key] = [...new Set(v)];\n }\n if (f.subject?.trim()) out.subject = f.subject;\n if (typeof f.hasAttachments === \"boolean\") out.hasAttachments = f.hasAttachments;\n return out;\n}\n\n/** Whether an event passes a webhook filter. Every set criterion must pass. */\nexport function matchesFilter(f: WebhookFilter, ev: StoredEvent): boolean {\n if (f.accountIds?.length && !f.accountIds.includes(ev.accountId)) return false;\n if (ACCOUNT_EVENT_TYPES.includes(ev.type)) return true;\n const hasMessageCriteria = !!(f.labelIds?.length || f.from?.length || f.to?.length || f.subject || f.hasAttachments !== undefined);\n const m = ev.message;\n if (!m) return !hasMessageCriteria;\n if (f.labelIds?.length) {\n // Label events also match on the labels that changed (a removed label is no longer on the message).\n const labels = new Set([...m.labelIds, ...(ev.labelIds ?? [])]);\n if (!f.labelIds.some((l) => labels.has(l))) return false;\n }\n if (f.from?.length && !(m.from && matchesAnyGlob(m.from.email, f.from))) return false;\n if (f.to?.length && ![...m.to, ...m.cc].some((a) => matchesAnyGlob(a.email, f.to!))) return false;\n if (f.subject && !subjectMatches(f.subject, m.subject.slice(0, MAX_SUBJECT_TESTED))) return false;\n if (f.hasAttachments !== undefined && m.hasAttachments !== f.hasAttachments) return false;\n return true;\n}\n"],"mappings":"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"}
@@ -0,0 +1,175 @@
1
+ #!/usr/bin/env node
2
+ import { t as Crypto } from "./crypto-DYZ4R7c2.js";
3
+ import { r as NPM_PACKAGE } from "./types-3eEsBFVj.js";
4
+ import { existsSync, readFileSync, rmSync, writeFileSync } from "node:fs";
5
+ import { join } from "node:path";
6
+ import { DatabaseSync } from "node:sqlite";
7
+ import { execFileSync } from "node:child_process";
8
+ //#region server/instance.ts
9
+ /**
10
+ * How to run this CLI in printed hints: `npx agentinbox-cli` when run through npx (and in service
11
+ * logs, read by someone who most likely installed that way), else the installed `agentinbox`.
12
+ */
13
+ function cliCommand(args) {
14
+ return `${process.env.npm_command === "exec" || /[\\/]_npx[\\/]/.test(process.argv[1] ?? "") || process.env.AGENTINBOX_SERVICE === "1" ? `npx ${NPM_PACKAGE}` : "agentinbox"} ${args}`;
15
+ }
16
+ const lockPath = (dataDir) => join(dataDir, "agentinbox.lock");
17
+ /** Data dirs locked by this process: a lock naming our own pid is live only if it's in here. */
18
+ const heldByUs = /* @__PURE__ */ new Set();
19
+ function bootId() {
20
+ try {
21
+ return readFileSync("/proc/sys/kernel/random/boot_id", "utf8").trim() || null;
22
+ } catch {
23
+ return null;
24
+ }
25
+ }
26
+ /** Process start time: /proc (Linux, in clock ticks since boot) or `ps` elsewhere; null if unknown. */
27
+ function processStartTime(pid) {
28
+ try {
29
+ const stat = readFileSync(`/proc/${pid}/stat`, "utf8");
30
+ return stat.slice(stat.lastIndexOf(")") + 2).split(" ")[19] ?? null;
31
+ } catch {}
32
+ if (process.platform === "linux" || process.platform === "win32") return null;
33
+ try {
34
+ return execFileSync("ps", [
35
+ "-o",
36
+ "lstart=",
37
+ "-p",
38
+ String(pid)
39
+ ], {
40
+ encoding: "utf8",
41
+ stdio: [
42
+ "ignore",
43
+ "pipe",
44
+ "ignore"
45
+ ]
46
+ }).trim() || null;
47
+ } catch {
48
+ return null;
49
+ }
50
+ }
51
+ function isAlive(pid) {
52
+ try {
53
+ process.kill(pid, 0);
54
+ return true;
55
+ } catch (err) {
56
+ return err.code === "EPERM";
57
+ }
58
+ }
59
+ /**
60
+ * Whether the process a lock names is still the one that wrote it. Our own pid is live only if we
61
+ * hold the lock (in containers every start is pid 1); otherwise the pid must exist, on the same
62
+ * boot, with the same start time (when both are known).
63
+ */
64
+ function isLive(info, dataDir) {
65
+ if (info.pid === process.pid) return heldByUs.has(dataDir);
66
+ if (!isAlive(info.pid)) return false;
67
+ const boot = bootId();
68
+ if (info.bootId && boot && info.bootId !== boot) return false;
69
+ if (info.processStart) {
70
+ const start = processStartTime(info.pid);
71
+ if (start && start !== info.processStart) return false;
72
+ }
73
+ return true;
74
+ }
75
+ function readLock(dataDir) {
76
+ try {
77
+ const info = JSON.parse(readFileSync(lockPath(dataDir), "utf8"));
78
+ return Number.isInteger(info.pid) && info.pid > 0 ? info : null;
79
+ } catch {
80
+ return null;
81
+ }
82
+ }
83
+ /** The running instance for this data directory, if any. */
84
+ function runningInstance(dataDir) {
85
+ const info = readLock(dataDir);
86
+ return info && isLive(info, dataDir) ? info : null;
87
+ }
88
+ /**
89
+ * Takes `<data>/agentinbox.lock`. Two processes on one data directory would both run ingest and
90
+ * deliver every webhook twice, even on different ports. A lock whose process is gone (dead,
91
+ * rebooted, or its pid reused by something else) is replaced.
92
+ */
93
+ function acquireInstanceLock(config) {
94
+ const dataDir = config.dataDir;
95
+ const file = lockPath(dataDir);
96
+ const info = {
97
+ pid: process.pid,
98
+ host: config.host,
99
+ port: config.port,
100
+ startedAt: (/* @__PURE__ */ new Date()).toISOString(),
101
+ bootId: bootId(),
102
+ processStart: processStartTime(process.pid),
103
+ service: process.env.AGENTINBOX_SERVICE === "1"
104
+ };
105
+ for (let attempt = 0;; attempt++) try {
106
+ writeFileSync(file, JSON.stringify(info), {
107
+ flag: "wx",
108
+ mode: 384
109
+ });
110
+ break;
111
+ } catch (err) {
112
+ if (err.code !== "EEXIST" || attempt > 0) throw err;
113
+ const other = runningInstance(dataDir);
114
+ if (other) {
115
+ const where = `(pid ${other.pid}${other.port ? `, port ${other.port}` : ""})`;
116
+ throw new Error(other.service ? `agentinbox is already running as a background service with the data directory ${dataDir} ${where}. Use that one, stop it (\`${cliCommand("service uninstall")}\` keeps your data), or use another --data directory.` : `agentinbox is already running with the data directory ${dataDir} ${where}. Stop it first, or use another --data directory. If no such process exists, delete ${file}.`);
117
+ }
118
+ rmSync(file, { force: true });
119
+ }
120
+ heldByUs.add(dataDir);
121
+ const ours = () => {
122
+ const current = readLock(dataDir);
123
+ return current?.pid === process.pid && current.startedAt === info.startedAt;
124
+ };
125
+ return {
126
+ update(port) {
127
+ info.port = port;
128
+ if (ours()) writeFileSync(file, JSON.stringify(info), { mode: 384 });
129
+ },
130
+ release() {
131
+ if (ours()) rmSync(file, { force: true });
132
+ heldByUs.delete(dataDir);
133
+ }
134
+ };
135
+ }
136
+ /** One ciphertext from the database, or null when nothing encrypted is stored. */
137
+ function sampleCiphertext(dbPath) {
138
+ if (!existsSync(dbPath)) return null;
139
+ const db = new DatabaseSync(dbPath);
140
+ try {
141
+ for (const sql of [
142
+ "SELECT refresh_token_enc AS v FROM accounts WHERE refresh_token_enc LIKE 'v1:%' LIMIT 1",
143
+ "SELECT secret_enc AS v FROM webhooks WHERE secret_enc LIKE 'v1:%' LIMIT 1",
144
+ "SELECT json_extract(value, '$.clientSecretEnc') AS v FROM settings WHERE key = 'google.client'"
145
+ ]) try {
146
+ const v = db.prepare(sql).get()?.v;
147
+ if (typeof v === "string" && v.startsWith("v1:")) return v;
148
+ } catch {}
149
+ return null;
150
+ } finally {
151
+ db.close();
152
+ }
153
+ }
154
+ /**
155
+ * Refuses to start with a master key that can't read the stored secrets: a missing master.key would
156
+ * otherwise be regenerated, silently making every refresh token and webhook secret unreadable.
157
+ * `envKey` overrides AGENTINBOX_MASTER_KEY (used by `service install` to check the key it will install).
158
+ */
159
+ function assertMasterKeyUsable(config, envKey = process.env.AGENTINBOX_MASTER_KEY) {
160
+ const sample = sampleCiphertext(config.dbPath);
161
+ if (!sample) return;
162
+ let raw = envKey || null;
163
+ const source = raw ? "AGENTINBOX_MASTER_KEY" : config.masterKeyPath;
164
+ if (!raw && existsSync(config.masterKeyPath)) raw = readFileSync(config.masterKeyPath, "utf8").trim();
165
+ if (!raw) throw new Error(`${config.dbPath} holds encrypted secrets, but there is no master key: ${config.masterKeyPath} is missing and AGENTINBOX_MASTER_KEY is not set. Restore master.key from your backup (or set AGENTINBOX_MASTER_KEY to the key used before). agentinbox won't create a new key, because the stored Google tokens and secrets could never be read again.`);
166
+ try {
167
+ new Crypto(Buffer.from(raw, "base64")).decrypt(sample);
168
+ } catch {
169
+ throw new Error(`The master key from ${source} can't decrypt the secrets stored in ${config.dbPath}. Use the key this data directory was created with (master.key from your backup, or the previous AGENTINBOX_MASTER_KEY).`);
170
+ }
171
+ }
172
+ //#endregion
173
+ export { lockPath as a, cliCommand as i, assertMasterKeyUsable as n, processStartTime as o, bootId as r, runningInstance as s, acquireInstanceLock as t };
174
+
175
+ //# sourceMappingURL=instance-BBzEsYZO.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"instance-BBzEsYZO.js","names":[],"sources":["../server/instance.ts"],"sourcesContent":["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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"}
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export { s as runningInstance } from "./instance-BBzEsYZO.js";