toolroll 0.9.63 → 0.9.65

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 (292) hide show
  1. package/CHANGELOG.md +51 -0
  2. package/README.md +15 -5
  3. package/dist/acp-agents.d.ts +43 -0
  4. package/dist/acp-agents.js +62 -0
  5. package/dist/acp-attest.d.ts +44 -0
  6. package/dist/acp-attest.js +269 -0
  7. package/dist/acp-check.d.ts +28 -0
  8. package/dist/acp-check.js +410 -0
  9. package/dist/acp-client.d.ts +163 -0
  10. package/dist/acp-client.js +738 -0
  11. package/dist/acp-inspect.d.ts +23 -0
  12. package/dist/acp-inspect.js +109 -0
  13. package/dist/agent-board.d.ts +19 -6
  14. package/dist/agent-board.js +30 -9
  15. package/dist/agent-fence.d.ts +50 -2
  16. package/dist/agent-fence.js +157 -10
  17. package/dist/agent-pulse.js +9 -2
  18. package/dist/agent-state-words.d.ts +19 -0
  19. package/dist/agent-state-words.js +20 -0
  20. package/dist/agent-state.d.ts +65 -0
  21. package/dist/agent-state.js +229 -0
  22. package/dist/agentconfig.js +36 -13
  23. package/dist/agents-board.d.ts +34 -2
  24. package/dist/agents-board.js +110 -1
  25. package/dist/approval-rules-ui.js +2 -2
  26. package/dist/assignment-delivery.d.ts +14 -0
  27. package/dist/assignment-delivery.js +13 -0
  28. package/dist/assignment.js +13 -0
  29. package/dist/attest.js +7 -0
  30. package/dist/backup-ui.js +2 -2
  31. package/dist/browser/THIRD_PARTY_NOTICES.txt +48 -0
  32. package/dist/browser/terminal.css +37 -0
  33. package/dist/browser/terminal.js +117 -0
  34. package/dist/browser/workspace.css +2 -2
  35. package/dist/browser/workspace.js +42 -38
  36. package/dist/browser-shell.js +6 -3
  37. package/dist/browser-workspace.d.ts +235 -1
  38. package/dist/browser-workspace.js +60 -0
  39. package/dist/builder.d.ts +6 -1
  40. package/dist/builder.js +34 -13
  41. package/dist/chat-actions.d.ts +13 -1
  42. package/dist/chat-actions.js +197 -15
  43. package/dist/chat-ask.d.ts +4 -0
  44. package/dist/chat-ask.js +14 -0
  45. package/dist/chat-channel.js +20 -0
  46. package/dist/chat-continuity.js +5 -4
  47. package/dist/chat-decide.d.ts +0 -14
  48. package/dist/chat-decide.js +4 -1
  49. package/dist/chat-delivery.d.ts +9 -0
  50. package/dist/chat-delivery.js +45 -6
  51. package/dist/chat-messages.d.ts +8 -0
  52. package/dist/chat-messages.js +36 -4
  53. package/dist/check-levels-ui.js +2 -2
  54. package/dist/checkout-proof.d.ts +28 -0
  55. package/dist/checkout-proof.js +62 -0
  56. package/dist/claim.d.ts +12 -0
  57. package/dist/claim.js +19 -0
  58. package/dist/cli-help.d.ts +1 -1
  59. package/dist/cli-help.js +4 -1
  60. package/dist/cli.d.ts +3 -0
  61. package/dist/cli.js +21 -2
  62. package/dist/coding-ui.js +6 -4
  63. package/dist/console-instance.d.ts +124 -0
  64. package/dist/console-instance.js +337 -0
  65. package/dist/contracts/chat-actions.d.ts +16 -0
  66. package/dist/contracts/chat-actions.js +4 -0
  67. package/dist/contracts/chat-content.d.ts +19 -0
  68. package/dist/contracts/chat-content.js +6 -0
  69. package/dist/contracts/console-api.d.ts +12 -4
  70. package/dist/contracts/console-api.js +12 -4
  71. package/dist/contracts/flow-card.d.ts +4 -0
  72. package/dist/contracts/flow-card.js +3 -0
  73. package/dist/contracts/flow.d.ts +46 -0
  74. package/dist/contracts/flow.js +11 -2
  75. package/dist/contracts/lead-tools.d.ts +92 -0
  76. package/dist/contracts/lead-tools.js +13 -0
  77. package/dist/contracts/provider.d.ts +11 -1
  78. package/dist/contracts/provider.js +1 -1
  79. package/dist/contracts/route.d.ts +76 -1
  80. package/dist/contracts/route.js +1 -1
  81. package/dist/contracts/store-json.d.ts +2 -0
  82. package/dist/control-setup.d.ts +1 -1
  83. package/dist/control-ui.js +2 -2
  84. package/dist/converse.d.ts +16 -0
  85. package/dist/converse.js +32 -4
  86. package/dist/demo-chat.js +4 -4
  87. package/dist/demo.js +2 -2
  88. package/dist/desktop-update-gate.js +6 -0
  89. package/dist/desktop-update.d.ts +1 -1
  90. package/dist/discord-chat.js +4 -1
  91. package/dist/doctor.js +1 -1
  92. package/dist/egress-bridge.d.ts +17 -0
  93. package/dist/egress-bridge.js +111 -0
  94. package/dist/egress.d.ts +119 -0
  95. package/dist/egress.js +423 -0
  96. package/dist/exec.d.ts +78 -0
  97. package/dist/exec.js +367 -9
  98. package/dist/export-ui.js +2 -2
  99. package/dist/flow-engine.js +24 -5
  100. package/dist/flow-gallery-ui.js +2 -2
  101. package/dist/flow-gallery.js +27 -8
  102. package/dist/flow-share.js +16 -5
  103. package/dist/flow-triggers.d.ts +77 -8
  104. package/dist/flow-triggers.js +517 -70
  105. package/dist/flows-cli.js +8 -2
  106. package/dist/flows-ui.d.ts +0 -2
  107. package/dist/flows-ui.js +52 -4
  108. package/dist/flows.d.ts +4 -0
  109. package/dist/flows.js +9 -1
  110. package/dist/html.d.ts +9 -0
  111. package/dist/html.js +11 -0
  112. package/dist/invoke.d.ts +17 -0
  113. package/dist/invoke.js +217 -21
  114. package/dist/keys.d.ts +18 -14
  115. package/dist/keys.js +5 -0
  116. package/dist/lead-capabilities.js +3 -2
  117. package/dist/lead-contract.js +2 -0
  118. package/dist/lead-doors.d.ts +2 -0
  119. package/dist/lead-doors.js +7 -0
  120. package/dist/lead-follow.d.ts +2 -0
  121. package/dist/lead-follow.js +58 -2
  122. package/dist/lead-progress.js +1 -0
  123. package/dist/lead-queue-runtime.d.ts +46 -0
  124. package/dist/lead-queue-runtime.js +191 -0
  125. package/dist/lead-queue.d.ts +104 -0
  126. package/dist/lead-queue.js +187 -0
  127. package/dist/lead-sweep.d.ts +67 -0
  128. package/dist/lead-sweep.js +99 -0
  129. package/dist/lead-tools.d.ts +22 -0
  130. package/dist/lead-tools.js +152 -1
  131. package/dist/lead.d.ts +13 -5
  132. package/dist/lead.js +89 -29
  133. package/dist/limits-ui.d.ts +8 -1
  134. package/dist/limits-ui.js +26 -0
  135. package/dist/live-bus.d.ts +8 -0
  136. package/dist/live-bus.js +40 -4
  137. package/dist/live-detail.d.ts +27 -0
  138. package/dist/live-detail.js +57 -0
  139. package/dist/live-notes.d.ts +20 -0
  140. package/dist/live-notes.js +47 -0
  141. package/dist/live-records.d.ts +140 -0
  142. package/dist/live-records.js +935 -0
  143. package/dist/live-terminal.d.ts +258 -0
  144. package/dist/live-terminal.js +649 -0
  145. package/dist/live.d.ts +38 -27
  146. package/dist/live.js +142 -124
  147. package/dist/mcp-oauth.d.ts +6 -3
  148. package/dist/mcp-oauth.js +8 -4
  149. package/dist/memory-pass.js +2 -2
  150. package/dist/mobile-viewport.d.ts +5 -1
  151. package/dist/mobile-viewport.js +18 -2
  152. package/dist/model-catalog.js +4 -0
  153. package/dist/monitoring-ui.js +2 -2
  154. package/dist/needs-you.d.ts +1 -1
  155. package/dist/needs-you.js +11 -4
  156. package/dist/operate.d.ts +2 -0
  157. package/dist/operate.js +275 -17
  158. package/dist/passwordless.d.ts +23 -0
  159. package/dist/passwordless.js +26 -0
  160. package/dist/peek-cli.d.ts +12 -4
  161. package/dist/peek-cli.js +19 -11
  162. package/dist/phase-routing.js +10 -3
  163. package/dist/planner-source.d.ts +3 -0
  164. package/dist/planner-source.js +14 -0
  165. package/dist/planner.d.ts +2 -1
  166. package/dist/planner.js +7 -1
  167. package/dist/policy-ui.js +2 -2
  168. package/dist/policy.d.ts +1 -20
  169. package/dist/policy.js +29 -2
  170. package/dist/project-delete-ui.d.ts +12 -0
  171. package/dist/project-delete-ui.js +41 -9
  172. package/dist/project-delete.js +2 -2
  173. package/dist/project-memory.d.ts +3 -1
  174. package/dist/project-memory.js +57 -25
  175. package/dist/project-tools.d.ts +5 -3
  176. package/dist/project-tools.js +12 -9
  177. package/dist/provider-auth.d.ts +4 -3
  178. package/dist/provider-auth.js +5 -4
  179. package/dist/provider-connection.d.ts +2 -1
  180. package/dist/provider-connection.js +5 -0
  181. package/dist/provider.d.ts +25 -2
  182. package/dist/provider.js +63 -9
  183. package/dist/pull-request-ui.js +2 -2
  184. package/dist/push.js +1 -1
  185. package/dist/quick-start.d.ts +24 -0
  186. package/dist/quick-start.js +127 -0
  187. package/dist/release.d.ts +3 -0
  188. package/dist/release.js +14 -8
  189. package/dist/request-budget-ui.js +2 -2
  190. package/dist/result-review.d.ts +1 -1
  191. package/dist/result-review.js +9 -3
  192. package/dist/retention-ui.js +2 -2
  193. package/dist/retention.js +10 -1
  194. package/dist/run-efficiency.d.ts +73 -0
  195. package/dist/run-efficiency.js +228 -0
  196. package/dist/runner.d.ts +5 -1
  197. package/dist/runner.js +22 -1
  198. package/dist/scope.d.ts +170 -5
  199. package/dist/scope.js +251 -31
  200. package/dist/scout-net.d.ts +4 -1
  201. package/dist/scout-net.js +37 -12
  202. package/dist/scout.d.ts +20 -6
  203. package/dist/scout.js +27 -3
  204. package/dist/serve.js +247 -40
  205. package/dist/server/chat.d.ts +1 -1
  206. package/dist/server/chat.js +122 -31
  207. package/dist/server/chrome.d.ts +6 -1
  208. package/dist/server/chrome.js +18 -7
  209. package/dist/server/console.css +52 -38
  210. package/dist/server/flows.js +38 -8
  211. package/dist/server/guards.d.ts +1 -0
  212. package/dist/server/guards.js +23 -9
  213. package/dist/server/http.d.ts +7 -0
  214. package/dist/server/http.js +3 -2
  215. package/dist/server/live.d.ts +34 -1
  216. package/dist/server/live.js +39 -17
  217. package/dist/server/pages-assets.js +5 -0
  218. package/dist/server/pages.js +90 -13
  219. package/dist/server/people-tokens.d.ts +3 -0
  220. package/dist/server/people-tokens.js +55 -17
  221. package/dist/server/remote-hooks.js +12 -6
  222. package/dist/server/remote.d.ts +2 -1
  223. package/dist/server/remote.js +14 -5
  224. package/dist/server/render-chat.d.ts +12 -3
  225. package/dist/server/render-chat.js +47 -16
  226. package/dist/server/render-pages.d.ts +4 -1
  227. package/dist/server/render-pages.js +5 -4
  228. package/dist/server/render-people.d.ts +3 -2
  229. package/dist/server/render-people.js +3 -2
  230. package/dist/server/render-results.d.ts +18 -2
  231. package/dist/server/render-results.js +37 -28
  232. package/dist/server/render-settings.js +6 -5
  233. package/dist/server/render-tasks.d.ts +35 -10
  234. package/dist/server/render-tasks.js +110 -27
  235. package/dist/server/request-context.d.ts +2 -0
  236. package/dist/server/request-context.js +2 -1
  237. package/dist/server/route-table.d.ts +3 -0
  238. package/dist/server/route-table.js +46 -5
  239. package/dist/server/runtime.d.ts +33 -2
  240. package/dist/server/session.d.ts +25 -1
  241. package/dist/server/session.js +18 -4
  242. package/dist/server/settings.js +121 -10
  243. package/dist/server/sweeps.d.ts +4 -0
  244. package/dist/server/sweeps.js +58 -0
  245. package/dist/server/task-views.d.ts +2 -1
  246. package/dist/server/task-views.js +96 -6
  247. package/dist/server/tasks.js +320 -74
  248. package/dist/server/terminals.d.ts +31 -0
  249. package/dist/server/terminals.js +365 -0
  250. package/dist/setup-guide.js +3 -2
  251. package/dist/slack-chat.js +4 -1
  252. package/dist/spend-ui.js +3 -3
  253. package/dist/spend.js +2 -1
  254. package/dist/steer-capabilities.d.ts +32 -0
  255. package/dist/steer-capabilities.js +60 -0
  256. package/dist/steer-controls.d.ts +14 -0
  257. package/dist/steer-controls.js +36 -0
  258. package/dist/steer-status.js +2 -2
  259. package/dist/storage-ui.js +4 -4
  260. package/dist/store-baseline.d.ts +1 -1
  261. package/dist/store-baseline.js +1 -1
  262. package/dist/store.d.ts +430 -15
  263. package/dist/store.js +1041 -52
  264. package/dist/subagent-desk.js +1 -1
  265. package/dist/summary.js +3 -0
  266. package/dist/task-source.d.ts +58 -0
  267. package/dist/task-source.js +93 -0
  268. package/dist/team-leads.js +1 -1
  269. package/dist/teams-chat.js +4 -1
  270. package/dist/telegram-lead.d.ts +3 -0
  271. package/dist/telegram-lead.js +44 -7
  272. package/dist/telegram.d.ts +11 -2
  273. package/dist/telegram.js +51 -17
  274. package/dist/terminal-catalog.d.ts +11 -0
  275. package/dist/terminal-catalog.js +129 -0
  276. package/dist/text-limits.d.ts +2 -0
  277. package/dist/text-limits.js +2 -0
  278. package/dist/tool-broker.d.ts +2 -0
  279. package/dist/tool-broker.js +1 -0
  280. package/dist/toolroll-update-ui.js +2 -2
  281. package/dist/toolroll-update.js +13 -3
  282. package/dist/tools-ui.js +2 -2
  283. package/dist/webhook-limits.d.ts +23 -0
  284. package/dist/webhook-limits.js +30 -0
  285. package/dist/webhook-readers.d.ts +56 -0
  286. package/dist/webhook-readers.js +138 -0
  287. package/dist/webhook-signing.d.ts +27 -0
  288. package/dist/webhook-signing.js +119 -0
  289. package/dist/work-index.d.ts +29 -0
  290. package/dist/work-index.js +16 -2
  291. package/dist/workspace-revision.js +3 -1
  292. package/package.json +11 -2
@@ -0,0 +1,129 @@
1
+ /**
2
+ * Live terminals, written down for the updater (docs/plans/live-terminal.md, review round 2 finding 7). Terminals live
3
+ * in the console's memory, but an update is decided by another process, so each one is a row in a small catalog
4
+ * beside the database (`<db>.terminals.sqlite`), as the coding workspace keeps its own:
5
+ *
6
+ * - a start inserts its row before any work, and an update's gate (a trigger) refuses that insert while the update
7
+ * owns admission: no terminal starts during a drain;
8
+ * - a row is `starting` or `running` until the terminal's sweep ends, then deleted — or kept as `uncertain` when the
9
+ * sweep couldn't prove its processes gone, until the terminal is removed; the updater counts rows whose console is
10
+ * still alive as active work, so "update when idle" waits for running and unproved terminals;
11
+ * - rows of a console that is gone are stale (its programs went with it, or are swept at its next start).
12
+ */
13
+ import { chmodSync, lstatSync } from "node:fs";
14
+ import { processMayBeAlive } from "./process-liveness.js";
15
+ import { sqliteRuntime } from "./sqlite-runtime.js";
16
+ const gatePrefix = "so_terminal_update_";
17
+ const PAUSED = "Toolroll is updating. Running terminals keep going; new ones can start after the update.";
18
+ const SCHEMA = "CREATE TABLE IF NOT EXISTS live_terminal (id TEXT PRIMARY KEY, console_pid INTEGER NOT NULL, state TEXT NOT NULL CHECK (state IN ('starting','running','uncertain')), at TEXT NOT NULL)";
19
+ export function terminalCatalogPath(orders) {
20
+ const main = orders.prepare("PRAGMA database_list").all().find(row => row["name"] === "main")?.["file"];
21
+ return typeof main === "string" && main !== "" ? `${main}.terminals.sqlite` : null;
22
+ }
23
+ function exists(file) {
24
+ try {
25
+ return lstatSync(file).isFile();
26
+ }
27
+ catch {
28
+ return false;
29
+ }
30
+ }
31
+ function open(file, readOnly) {
32
+ if (exists(file) && lstatSync(file).isSymbolicLink())
33
+ throw Error("The terminal catalog is linked, not a regular database.");
34
+ const db = new (sqliteRuntime().DatabaseSync)(file, { readOnly });
35
+ db.exec("PRAGMA busy_timeout=1000");
36
+ return db;
37
+ }
38
+ /** The updater's read: never creates the catalog. */
39
+ function withCatalog(orders, absent, readOnly, action) {
40
+ const file = terminalCatalogPath(orders);
41
+ if (file === null || !exists(file))
42
+ return absent;
43
+ const db = open(file, readOnly);
44
+ try {
45
+ return action(db);
46
+ }
47
+ finally {
48
+ db.close();
49
+ }
50
+ }
51
+ /** The console's ledger: creates the catalog, drops rows of consoles that are gone, and writes its own. */
52
+ export function openTerminalLedger(orders, alive = pid => processMayBeAlive(pid, false)) {
53
+ const file = terminalCatalogPath(orders);
54
+ if (file === null)
55
+ return null;
56
+ const db = open(file, false);
57
+ chmodSync(file, 0o600);
58
+ db.exec(SCHEMA);
59
+ for (const row of db.prepare("SELECT DISTINCT console_pid FROM live_terminal").all()) {
60
+ const pid = Number(row["console_pid"]);
61
+ if (pid !== process.pid && !alive(pid))
62
+ db.prepare("DELETE FROM live_terminal WHERE console_pid=?").run(pid);
63
+ }
64
+ return {
65
+ reserve(id) {
66
+ // Fail closed even where this catalog had no gate to install (it didn't exist yet when the update began): the main
67
+ // database's own admission pause refuses too.
68
+ if (orders.prepare("SELECT 1 FROM sqlite_master WHERE type='trigger' AND name GLOB 'so_desktop_update_*' LIMIT 1").get() !== undefined)
69
+ throw new Error(PAUSED);
70
+ try {
71
+ db.prepare("INSERT INTO live_terminal (id, console_pid, state, at) VALUES (?, ?, 'starting', ?)").run(id, process.pid, new Date().toISOString());
72
+ }
73
+ catch (error) {
74
+ throw new Error(error instanceof Error && error.message.includes(PAUSED) ? PAUSED : "No terminal can start right now (its record couldn't be written).");
75
+ }
76
+ },
77
+ running(id) { db.prepare("UPDATE live_terminal SET state='running', at=? WHERE id=?").run(new Date().toISOString(), id); },
78
+ // A sweep that couldn't prove its processes gone stays written down: the updater counts it while this console lives.
79
+ release(id, cleanup) {
80
+ try {
81
+ if (cleanup === "uncertain")
82
+ db.prepare("UPDATE live_terminal SET state='uncertain', at=? WHERE id=?").run(new Date().toISOString(), id);
83
+ else
84
+ db.prepare("DELETE FROM live_terminal WHERE id=?").run(id);
85
+ }
86
+ catch { /* the next start drops it as stale */ }
87
+ },
88
+ forget(id) { try {
89
+ db.prepare("DELETE FROM live_terminal WHERE id=?").run(id);
90
+ }
91
+ catch { /* stale later */ } },
92
+ };
93
+ }
94
+ function gateStatement(id) {
95
+ if (!/^[a-f0-9-]{36}$/.test(id))
96
+ throw Error("Invalid update identity.");
97
+ return `CREATE TRIGGER ${gatePrefix}start BEFORE INSERT ON live_terminal BEGIN SELECT RAISE(ABORT, '${PAUSED} [${id}]'); END`;
98
+ }
99
+ function changeGate(db, id, install) {
100
+ const wanted = gateStatement(id);
101
+ db.exec("BEGIN IMMEDIATE");
102
+ try {
103
+ db.exec(SCHEMA);
104
+ const rows = db.prepare("SELECT name, sql FROM sqlite_master WHERE type='trigger' AND name GLOB 'so_terminal_update_*'").all();
105
+ if (rows.some(row => String(row["sql"]) !== wanted))
106
+ throw Error("Another or unrecognized update owns terminal admission. Its gate was preserved.");
107
+ if (install && rows.length === 0)
108
+ db.exec(wanted);
109
+ if (!install)
110
+ for (const row of rows)
111
+ db.exec(`DROP TRIGGER "${String(row["name"])}"`);
112
+ db.exec("COMMIT");
113
+ }
114
+ catch (error) {
115
+ db.exec("ROLLBACK");
116
+ throw error;
117
+ }
118
+ }
119
+ export function installTerminalUpdateGate(orders, id) { withCatalog(orders, undefined, false, db => changeGate(db, id, true)); }
120
+ export function removeTerminalUpdateGate(orders, id) { withCatalog(orders, undefined, false, db => changeGate(db, id, false)); }
121
+ /** Terminals starting or running in a console that is still alive: active work an update waits for. */
122
+ export function activeTerminalUpdateWork(orders, alive = pid => processMayBeAlive(pid, false)) {
123
+ return withCatalog(orders, { terminals: 0 }, true, db => {
124
+ if (!db.prepare("SELECT 1 FROM sqlite_master WHERE type='table' AND name='live_terminal'").get())
125
+ return { terminals: 0 };
126
+ const rows = db.prepare("SELECT console_pid FROM live_terminal").all();
127
+ return { terminals: rows.filter(row => alive(Number(row["console_pid"]))).length };
128
+ });
129
+ }
@@ -68,6 +68,8 @@ export declare const TEXT_LIMITS: {
68
68
  readonly triggerState: 40;
69
69
  readonly triggerWebhookTitle: 120;
70
70
  readonly triggerWebhookField: 80;
71
+ /** v126: a Sentry or Vercel project (or Vercel team) a signed webhook is scoped to. */
72
+ readonly triggerWebhookScope: 120;
71
73
  readonly triggerFolder: 100;
72
74
  readonly triggerSender: 300;
73
75
  readonly triggerSubject: 100;
@@ -68,6 +68,8 @@ export const TEXT_LIMITS = {
68
68
  triggerState: 40,
69
69
  triggerWebhookTitle: 120,
70
70
  triggerWebhookField: 80,
71
+ /** v126: a Sentry or Vercel project (or Vercel team) a signed webhook is scoped to. */
72
+ triggerWebhookScope: 120,
71
73
  triggerFolder: 100,
72
74
  triggerSender: 300,
73
75
  triggerSubject: 100,
@@ -22,6 +22,8 @@ export type ToolBroker = {
22
22
  /** The launch's token: the agent's only key to its tools. */
23
23
  token: string;
24
24
  url: (name: string) => string;
25
+ /** Its loopback port: the one a confined network leaves reachable for its agent (agent-fence.ts NetworkFence). */
26
+ port: number;
25
27
  /** The local servers running now (the fence tests read them). */
26
28
  pids: () => number[];
27
29
  /** Revoke the token, stop every server, close the port. */
@@ -357,6 +357,7 @@ export async function startToolBroker(tools, options = {}) {
357
357
  return {
358
358
  token,
359
359
  url: name => `http://127.0.0.1:${port}/mcp/${name}`,
360
+ port,
360
361
  pids: () => [...local.values()].map(one => one.pid()).filter((pid) => pid !== null),
361
362
  close: () => {
362
363
  revoked = true;
@@ -4,7 +4,7 @@
4
4
  * and a one-time "What's new" card afterwards. The work itself is the
5
5
  * `toolroll update` command (toolroll-update.ts), started as its own job.
6
6
  */
7
- import { html, postForm, replaceMarkup } from "./html.js";
7
+ import { html, postForm, replaceMarkup, reauth } from "./html.js";
8
8
  import { STEP_WORDS, UPDATE_STEPS, runtimeUpdateTerminal } from "./toolroll-update.js";
9
9
  const clock = (iso) => { const d = new Date(iso); return `${String(d.getHours()).padStart(2, "0")}:${String(d.getMinutes()).padStart(2, "0")}`; };
10
10
  export const UPDATES_CSS = `.updates{max-width:640px;min-width:0;overflow-wrap:anywhere}.updates .card{margin:0 0 16px}.updates h2{margin:0;font-size:1.0625rem}.updates .card>p{margin:4px 0 0}` +
@@ -69,7 +69,7 @@ export function updatesHtml(view, notice) {
69
69
  : latest === null ? html `<h2>Toolroll ${view.current}</h2><p class="meta">Couldn’t check for a newer release: ${"problem" in view.latest ? view.latest.problem : ""}</p>`
70
70
  : html `<h2>Toolroll ${view.current} is up to date</h2>`;
71
71
  const form = newer && !["npx", "source", "desktop"].includes(view.method.kind)
72
- ? postForm("/settings/updates", html `<label class="step-up">Your Toolroll password<input type="password" name="password" autocomplete="current-password" required></label><div class="update-actions"><button type="submit" name="when" value="now" class="primary">Update now</button><button type="submit" name="when" value="when-idle">When idle</button><button type="submit" name="when" value="tonight">Tonight (03:00)</button></div>`, { hidden: { version: latest } })
72
+ ? postForm("/settings/updates", html `${reauth(html `<label class="step-up">Your Toolroll password<input type="password" name="password" autocomplete="current-password" required></label>`)}<div class="update-actions"><button type="submit" name="when" value="now" class="primary">Update now</button><button type="submit" name="when" value="when-idle">When idle</button><button type="submit" name="when" value="tonight">Tonight (03:00)</button></div>`, { hidden: { version: latest } })
73
73
  : "";
74
74
  const rollback = view.rollbackTo !== null ? html `<p class="meta">To go back to ${view.rollbackTo}: <code>toolroll update --rollback</code></p>` : "";
75
75
  return html `<section class="updates">${note}${whatsNew}${j ? outcomeHtml(j) : ""}<div class="card" data-update-state="${newer ? "available" : "current"}">${state}${form}${rollback}</div></section>`;
@@ -32,10 +32,11 @@ import { createRequire } from "node:module";
32
32
  import { clearWords, durableJson, LINGERING_LIMIT_MS, lingeringRun, lingeringWords, settleLeftoverRecords, sqliteLock, desktopUpdateWaitingOf, stoppedWaitingWords, updateWaitingWords, verifiedDatabaseBackup } from "./desktop-update.js";
33
33
  import { activeUpdateWork, freezeUpdateGate, installUpdateGate, removeUpdateGate, updateAdmissionPaused, updateGateOwned } from "./desktop-update-gate.js";
34
34
  import { processMayBeAlive } from "./process-liveness.js";
35
+ import { removeTerminalUpdateGate } from "./terminal-catalog.js";
35
36
  import { assertCodingUpdateStopped, backupCodingCatalog, codingCatalogExists, releaseStaleCodingOwner, removeCodingUpdateGate } from "./coding-update.js";
36
37
  import { installLaunchdService, launchdPlist, stopLaunchdService, writeFileDurably } from "./daemon.js";
37
38
  import { NAME } from "./names.js";
38
- import { orphanKey, updateSafeSchema, V117_RENAMED_COLUMNS, V117_RENAMED_TABLES, V118_MERGED_TABLES, V119_ADDED_COLUMNS, V120_ADDED_COLUMNS, V121_ADDED_COLUMNS, V122_ADDED_COLUMNS, V123_ADDED_COLUMNS } from "./store.js";
39
+ import { orphanKey, updateSafeSchema, V117_RENAMED_COLUMNS, V117_RENAMED_TABLES, V118_MERGED_TABLES, V119_ADDED_COLUMNS, V120_ADDED_COLUMNS, V121_ADDED_COLUMNS, V122_ADDED_COLUMNS, V123_ADDED_COLUMNS, V125_ADDED_COLUMNS, V126_ADDED_COLUMNS, V128_ADDED_COLUMNS, V129_ADDED_COLUMNS } from "./store.js";
39
40
  import { isNewer, REGISTRY } from "./releases.js";
40
41
  import { markNeverIndex } from "./never-index.js";
41
42
  import { readRuntimeUpdateJournal, RUNTIME_PHASES, RUNTIME_UPDATE_STEPS, stagedStartedAt, updaterStartingOf } from "./contracts/update-journal.js";
@@ -373,6 +374,8 @@ function addedColumns(...versions) {
373
374
  }
374
375
  return rules;
375
376
  }
377
+ /** Rules on tables that later gained declared columns (addedColumns merges into these instead of replacing them). */
378
+ const EARLIER_RULES = { flow_trigger: { fromRoutines: "trigger" } };
376
379
  const HISTORY_RULES = {
377
380
  ...Object.fromEntries(Object.entries(CHAT_MOVES).map(([table, moved]) => [table, { moved }])),
378
381
  action_ledger: { appendOnly: true }, ledger_seal: { appendOnly: true }, ledger_checkpoint: { appendOnly: true },
@@ -401,7 +404,11 @@ const HISTORY_RULES = {
401
404
  // v121: live steering's columns and a run's attempt key read none, and no sends, the same way.
402
405
  // v122: a project's recorded remote reads none the same way.
403
406
  // v123: a plan authorization's basis reads the operating mode, and a trigger firing reads not on time and no script ran.
404
- ...addedColumns(V119_ADDED_COLUMNS, V120_ADDED_COLUMNS, V121_ADDED_COLUMNS, V122_ADDED_COLUMNS, V123_ADDED_COLUMNS),
407
+ // v126: a receipt's state, acceptance time and held item, a trigger's credential generation and turned-away count,
408
+ // and who made a card read none, 0, or none. A table with an earlier rule (flow_trigger's v115 move) keeps it too.
409
+ // v128: a lead message's pending flag reads waiting exactly for a team queue's unadmitted operator message.
410
+ ...Object.fromEntries(Object.entries(addedColumns(V119_ADDED_COLUMNS, V120_ADDED_COLUMNS, V121_ADDED_COLUMNS, V122_ADDED_COLUMNS, V123_ADDED_COLUMNS, V125_ADDED_COLUMNS, V126_ADDED_COLUMNS, V128_ADDED_COLUMNS, V129_ADDED_COLUMNS))
411
+ .map(([table, rule]) => [table, { ...EARLIER_RULES[table], ...rule }])),
405
412
  };
406
413
  /** The tables a migration may remove whole (v115's removed features). */
407
414
  export const RETIRED_TABLES = Object.freeze(Object.keys(HISTORY_RULES).filter(name => HISTORY_RULES[name].retired));
@@ -908,8 +915,10 @@ function ungate(j) {
908
915
  db.exec("PRAGMA busy_timeout=5000");
909
916
  if (updateGateOwned(db, j.id))
910
917
  removeUpdateGate(db, j.id);
911
- else
918
+ else {
912
919
  removeCodingUpdateGate(db, j.id);
920
+ removeTerminalUpdateGate(db, j.id);
921
+ }
913
922
  }
914
923
  finally {
915
924
  db.close();
@@ -920,6 +929,7 @@ function ungateCoding(j) {
920
929
  try {
921
930
  db.exec("PRAGMA busy_timeout=5000");
922
931
  removeCodingUpdateGate(db, j.id);
932
+ removeTerminalUpdateGate(db, j.id);
923
933
  }
924
934
  finally {
925
935
  db.close();
package/dist/tools-ui.js CHANGED
@@ -1,6 +1,6 @@
1
1
  /** Settings → Tools: one project's MCP servers, in plain words. Secrets are
2
2
  * entered here and nowhere else, and never shown again. */
3
- import { html, postForm } from "./html.js";
3
+ import { html, postForm, reauth } from "./html.js";
4
4
  import { brandMarkHtml, TILE_PHONE } from "./brand-mark.js";
5
5
  import { signedIn } from "./oauth-run-token.js";
6
6
  import { credentialModeOf, toolCommandLine, toolStanding } from "./project-tools.js";
@@ -15,7 +15,7 @@ export function toolsProjectPicker(projects, chosen, keep = {}) {
15
15
  }
16
16
  /** Choosing another project marks every form on the page as stale before it reloads, so a tap racing the reload is refused. */
17
17
  export const TOOLS_PROJECT_SCRIPT = `(function(){var s=document.querySelector('select[data-tools-project]');if(!s)return;s.addEventListener('change',function(){document.querySelectorAll('input[name=shown]').forEach(function(i){i.value=s.value;});});})();`;
18
- const password = html `<label>Your Toolroll password<input type="password" name="password" autocomplete="current-password" required></label>`;
18
+ const password = html `${reauth(html `<label>Your Toolroll password<input type="password" name="password" autocomplete="current-password" required></label>`)}`;
19
19
  function toolCard(one, base, manage, project, research = null) {
20
20
  const { tool, secretsSet } = one;
21
21
  const standing = toolStanding(tool, secretsSet);
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Webhook floods (docs/plans/webhook-tasks.md §4): how many deliveries a trigger's address may fail in a minute. Held
3
+ * in memory per store (a restart forgets it, which is harmless: it protects the process and the database's write rate,
4
+ * not anything durable). It counts only what doesn't prove itself — a bad, missing or stale signature, an address that
5
+ * isn't set up, an unknown address, and every delivery to an unsigned webhook (nothing proves those) — so a stranger
6
+ * holding a leaked address can't crowd out the deliveries that are signed.
7
+ */
8
+ import type { Store } from "./store.js";
9
+ /** At most this many unproven deliveries per address a minute; over it, further ones are answered 429 and do nothing. */
10
+ export declare const UNPROVEN_PER_MINUTE = 60;
11
+ /** Addresses that match no trigger share one count. */
12
+ export declare const UNKNOWN_ADDRESS = "unknown";
13
+ /** Count one unproven delivery to `address` at `now`. Whether it is over the minute's limit, and when to try again. */
14
+ export declare function countUnproven(store: Store, address: string, now: Date): {
15
+ over: false;
16
+ } | {
17
+ over: true;
18
+ retryAfterSeconds: number;
19
+ };
20
+ /** At most this many cards a trigger makes in a rolling hour (counted when each card is made). */
21
+ export declare const CARDS_PER_HOUR = 30;
22
+ /** At most this many signed deliveries a trigger holds while it waits for room; beyond it, deliveries are turned away. */
23
+ export declare const HELD_AT_MOST = 200;
@@ -0,0 +1,30 @@
1
+ /** At most this many unproven deliveries per address a minute; over it, further ones are answered 429 and do nothing. */
2
+ export const UNPROVEN_PER_MINUTE = 60;
3
+ const WINDOW_MS = 60_000;
4
+ /** Addresses that match no trigger share one count. */
5
+ export const UNKNOWN_ADDRESS = "unknown";
6
+ const counters = new WeakMap();
7
+ /** Count one unproven delivery to `address` at `now`. Whether it is over the minute's limit, and when to try again. */
8
+ export function countUnproven(store, address, now) {
9
+ let counter = counters.get(store);
10
+ if (counter === undefined) {
11
+ counter = new Map();
12
+ counters.set(store, counter);
13
+ }
14
+ const at = now.getTime();
15
+ const recent = (counter.get(address) ?? []).filter(one => one > at - WINDOW_MS);
16
+ recent.push(at);
17
+ // Keep at most one over the limit: the count only needs to know it is over, and memory stays bounded per address.
18
+ counter.set(address, recent.slice(-(UNPROVEN_PER_MINUTE + 1)));
19
+ // Bounded across addresses too: the oldest addresses are forgotten first.
20
+ if (counter.size > 10_000)
21
+ counter.delete(counter.keys().next().value);
22
+ if (recent.length <= UNPROVEN_PER_MINUTE)
23
+ return { over: false };
24
+ const oldest = recent[recent.length - UNPROVEN_PER_MINUTE - 1];
25
+ return { over: true, retryAfterSeconds: Math.max(1, Math.ceil((oldest + WINDOW_MS - at) / 1000)) };
26
+ }
27
+ /** At most this many cards a trigger makes in a rolling hour (counted when each card is made). */
28
+ export const CARDS_PER_HOUR = 30;
29
+ /** At most this many signed deliveries a trigger holds while it waits for room; beyond it, deliveries are turned away. */
30
+ export const HELD_AT_MOST = 200;
@@ -0,0 +1,56 @@
1
+ import type { WebhookSender } from "./webhook-signing.js";
2
+ /** A webhook trigger's reading settings (its saved config, defaults filled). */
3
+ export type ReaderSettings = {
4
+ sender: WebhookSender;
5
+ title: string;
6
+ titleField: string | null;
7
+ bodyField: string | null;
8
+ idField: string | null;
9
+ urlField: string | null;
10
+ project: string | null;
11
+ team: string | null;
12
+ };
13
+ /** What one delivery is, once read: an item (with the outside thing's identity and link, when it has them), a delivery
14
+ * whose configured identity couldn't be used (made into nothing, with why), or one that isn't for this flow. */
15
+ export type ReadDelivery = {
16
+ kind: "item";
17
+ identity: string | null;
18
+ url: string | null;
19
+ title: string;
20
+ description: string;
21
+ label: string;
22
+ } | {
23
+ kind: "skip";
24
+ why: string;
25
+ } | {
26
+ kind: "ignored";
27
+ why: string;
28
+ };
29
+ /** Whether outside words could hold a key or password (the same check cards' words get). */
30
+ export declare const looksSecret: (value: string) => boolean;
31
+ /** Where a sender's links may point: its own host, and (for Sentry) the organisation subdomains under it. */
32
+ export type LinkHosts = readonly {
33
+ host: string;
34
+ subdomains: boolean;
35
+ }[];
36
+ export declare const SENTRY_HOSTS: LinkHosts;
37
+ export declare const VERCEL_HOSTS: LinkHosts;
38
+ /**
39
+ * A link kept from a delivery: absolute https, at most 2,000 characters, no user information, and with its query and
40
+ * fragment dropped (where tokens travel). `hosts` narrows it to a sender's own site (and its subdomains). Null when it
41
+ * isn't one, or when what is left could still hold a secret.
42
+ */
43
+ export declare function safeLink(value: unknown, hosts: LinkHosts | null): string | null;
44
+ /** The identity words a key may carry: at most 120 characters, nothing hidden, and nothing that looks like a secret. */
45
+ export declare function identityOf(value: unknown): {
46
+ ok: true;
47
+ id: string;
48
+ } | {
49
+ ok: false;
50
+ why: string;
51
+ };
52
+ /** Whether each provider's reader has read a real, captured delivery (test/fixtures/webhooks/*.captured.json), or only
53
+ * its sender's documented examples so far: the panel says which until a captured one is in the suite. */
54
+ export declare const READER_CAPTURED: Record<"sentry" | "vercel", boolean>;
55
+ /** What a proven delivery says, by its sender's reader. */
56
+ export declare function readDelivery(body: unknown, settings: ReaderSettings): ReadDelivery;
@@ -0,0 +1,138 @@
1
+ /**
2
+ * What a proven webhook delivery says, read by its sender's reader (docs/plans/webhook-tasks.md §2). A signature proves
3
+ * who sent a delivery, not that it belongs in this flow: Sentry's and Vercel's readers check its shape, its action and
4
+ * the configured scope first, and only then give it an identity. Anything else is ignored, with why, and claims no
5
+ * identity. The generic senders pick their fields by dot-paths. Every outside value is screened for secrets before it
6
+ * is used anywhere (a key, a held item, a card, a task), and every link is made safe before it is kept.
7
+ */
8
+ import { scanForSecrets } from "./evidence.js";
9
+ const HIDDEN = /[\u0000-\u001f\u007f​-‏‪-‮⁦-⁩]/;
10
+ const record = (value) => value !== null && typeof value === "object" && !Array.isArray(value) ? value : {};
11
+ const pick = (body, path) => path.split(".").reduce((node, part) => record(node)[part], body);
12
+ const text = (value) => typeof value === "string" && value.trim() !== "" ? value.trim() : null;
13
+ /** A string or a safe integer, as an identity's words exactly as sent; anything else isn't one. */
14
+ const idWords = (value) => typeof value === "string" ? value : typeof value === "number" && Number.isSafeInteger(value) ? String(value) : null;
15
+ /** Whether outside words could hold a key or password (the same check cards' words get). */
16
+ export const looksSecret = (value) => scanForSecrets(value).length > 0;
17
+ export const SENTRY_HOSTS = [{ host: "sentry.io", subdomains: true }];
18
+ export const VERCEL_HOSTS = [{ host: "vercel.com", subdomains: false }];
19
+ /**
20
+ * A link kept from a delivery: absolute https, at most 2,000 characters, no user information, and with its query and
21
+ * fragment dropped (where tokens travel). `hosts` narrows it to a sender's own site (and its subdomains). Null when it
22
+ * isn't one, or when what is left could still hold a secret.
23
+ */
24
+ export function safeLink(value, hosts) {
25
+ if (typeof value !== "string" || value.length > 2000 || HIDDEN.test(value))
26
+ return null;
27
+ let url;
28
+ try {
29
+ url = new URL(value);
30
+ }
31
+ catch {
32
+ return null;
33
+ }
34
+ if (url.protocol !== "https:" || url.username !== "" || url.password !== "")
35
+ return null;
36
+ const host = url.hostname.toLowerCase();
37
+ if (hosts !== null && !hosts.some(one => host === one.host || (one.subdomains && host.endsWith(`.${one.host}`))))
38
+ return null;
39
+ const kept = `${url.origin}${url.pathname}`;
40
+ // Judged as it will be kept: an address that only normalises past 2,000 characters (escaped non-ASCII) isn't kept.
41
+ if (kept.length > 2000)
42
+ return null;
43
+ // Screened as kept and as read: a token spelled with percent-escapes is the same token. A path that won't decode isn't kept.
44
+ let decoded;
45
+ try {
46
+ decoded = decodeURIComponent(url.pathname);
47
+ }
48
+ catch {
49
+ return null;
50
+ }
51
+ return looksSecret(kept) || looksSecret(decoded) || HIDDEN.test(decoded) ? null : kept;
52
+ }
53
+ /** The identity words a key may carry: at most 120 characters, nothing hidden, and nothing that looks like a secret. */
54
+ export function identityOf(value) {
55
+ const sent = idWords(value);
56
+ if (sent === null || sent.trim() === "")
57
+ return { ok: false, why: "its identity is missing" };
58
+ // Judged as sent, before anything is trimmed: padding, hidden characters or length make it unusable, never another id.
59
+ if (sent.length > 120 || HIDDEN.test(sent) || sent !== sent.trim())
60
+ return { ok: false, why: "its identity isn't usable" };
61
+ if (looksSecret(sent))
62
+ return { ok: false, why: "it looked like it held a key or password" };
63
+ return { ok: true, id: sent };
64
+ }
65
+ /** Sentry's API address for an event names its project's slug (…/api/0/projects/<org>/<project>/events/<id>/): read
66
+ * only from that exact shape on Sentry's own host. */
67
+ function sentrySlugOf(value) {
68
+ const link = typeof value === "string" ? value : "";
69
+ const match = /^https:\/\/(?:[a-z0-9-]+\.)?sentry\.io\/api\/0\/projects\/[a-z0-9_-]+\/([a-z0-9_-]+)\/events\/[A-Za-z0-9-]+\/?$/.exec(link);
70
+ return match?.[1] ?? null;
71
+ }
72
+ /** Sentry: an issue alert (action "triggered", with the event) or a new issue (action "created", with the issue). */
73
+ function readSentry(body, settings) {
74
+ const action = text(record(body)["action"]);
75
+ const data = record(record(body)["data"]);
76
+ const alert = action === "triggered" && Object.keys(record(data["event"])).length > 0;
77
+ const created = action === "created" && Object.keys(record(data["issue"])).length > 0;
78
+ // A fixed reason: nothing the sender wrote is kept for an event Toolroll doesn't take.
79
+ if (!alert && !created)
80
+ return { kind: "ignored", why: "not an issue alert or a new issue" };
81
+ const thing = record(alert ? data["event"] : data["issue"]);
82
+ // Scope from the body alone (the resource header isn't signed): the project's id or slug, as Sentry sends it.
83
+ if (settings.project !== null) {
84
+ const project = record(thing["project"]);
85
+ // An alert's event names its project by number, and by slug only in its API address; a new issue names both.
86
+ const names = [idWords(thing["project"]), idWords(project["id"]), text(project["slug"]), sentrySlugOf(thing["url"])].filter((one) => one !== null);
87
+ if (!names.some(one => one.toLowerCase() === settings.project.toLowerCase()))
88
+ return { kind: "ignored", why: "from another Sentry project" };
89
+ }
90
+ const identity = identityOf(alert ? thing["issue_id"] : thing["id"]);
91
+ if (!identity.ok)
92
+ return { kind: "skip", why: identity.why };
93
+ const title = text(thing["title"]) ?? settings.title;
94
+ const culprit = text(thing["culprit"]);
95
+ return { kind: "item", identity: identity.id, url: safeLink(thing["web_url"], SENTRY_HOSTS), title, description: culprit === null ? "From a Sentry issue." : `From a Sentry issue, in ${culprit}.`, label: "Sentry issue" };
96
+ }
97
+ /** Vercel: a failed deployment only, from the configured project and team. */
98
+ function readVercel(body, settings) {
99
+ const type = text(record(body)["type"]);
100
+ if (type !== "deployment.error")
101
+ return { kind: "ignored", why: "not a failed deployment" };
102
+ const payload = record(record(body)["payload"]);
103
+ if (settings.project !== null && idWords(record(payload["project"])["id"]) !== settings.project)
104
+ return { kind: "ignored", why: "from another Vercel project" };
105
+ if (settings.team !== null && idWords(record(payload["team"])["id"]) !== settings.team)
106
+ return { kind: "ignored", why: "from another Vercel team" };
107
+ const deployment = record(payload["deployment"]);
108
+ const identity = identityOf(deployment["id"]);
109
+ if (!identity.ok)
110
+ return { kind: "skip", why: identity.why };
111
+ const name = text(deployment["name"]);
112
+ return { kind: "item", identity: identity.id, url: safeLink(record(payload["links"])["deployment"], VERCEL_HOSTS), title: name === null ? settings.title : `${name} failed to deploy`,
113
+ description: "From a failed Vercel deployment.", label: "Vercel deployment" };
114
+ }
115
+ /** Any JSON: the trigger's own paths, then the usual names; shown whole when they find no text. */
116
+ function readGeneric(body, settings) {
117
+ const at = (path) => path === null ? undefined : pick(body, path);
118
+ let identity = null;
119
+ if (settings.idField !== null) {
120
+ const read = identityOf(at(settings.idField));
121
+ // A configured identity that can't be used makes nothing: it never falls back to some other identity.
122
+ if (!read.ok)
123
+ return { kind: "skip", why: read.why };
124
+ identity = read.id;
125
+ }
126
+ const title = at(settings.titleField ?? "title") ?? at("summary") ?? at("message");
127
+ const details = at(settings.bodyField ?? "description") ?? at("body") ?? at("text");
128
+ return { kind: "item", identity, url: settings.urlField === null ? null : safeLink(at(settings.urlField), null),
129
+ title: typeof title === "string" && title.trim() !== "" ? title : settings.title,
130
+ description: `From a webhook:\n\n${typeof details === "string" ? details : JSON.stringify(body, null, 2)}`.slice(0, 4000), label: "Webhook" };
131
+ }
132
+ /** Whether each provider's reader has read a real, captured delivery (test/fixtures/webhooks/*.captured.json), or only
133
+ * its sender's documented examples so far: the panel says which until a captured one is in the suite. */
134
+ export const READER_CAPTURED = { sentry: false, vercel: false };
135
+ /** What a proven delivery says, by its sender's reader. */
136
+ export function readDelivery(body, settings) {
137
+ return settings.sender === "sentry" ? readSentry(body, settings) : settings.sender === "vercel" ? readVercel(body, settings) : readGeneric(body, settings);
138
+ }
@@ -0,0 +1,27 @@
1
+ import { WEBHOOK_SENDERS } from "./contracts/flow.js";
2
+ export { WEBHOOK_SENDERS };
3
+ export type WebhookSender = (typeof WEBHOOK_SENDERS)[number];
4
+ /** The senders whose deliveries carry a signature (everything but `none`). */
5
+ export type SigningSender = Exclude<WebhookSender, "none">;
6
+ export declare const SENDER_WORDS: Record<WebhookSender, string>;
7
+ /** Node's `headersDistinct`: every header's values, one per line it arrived on. */
8
+ export type DistinctHeaders = Record<string, string[] | undefined>;
9
+ export type Proof = {
10
+ ok: true;
11
+ signedId: string | null;
12
+ } | {
13
+ ok: false;
14
+ said: string;
15
+ };
16
+ /** A Standard Webhooks timestamp may be this far from now, either way. */
17
+ export declare const STANDARD_TOLERANCE_MS: number;
18
+ /** A `whsec_` secret's key bytes, or null when it isn't one. */
19
+ export declare function standardKey(secret: string): Buffer | null;
20
+ /** Whether a delivery was signed with this secret by this sender. `none` proves nothing and is never passed here. */
21
+ export declare function proveDelivery(sender: SigningSender, secret: string, headers: DistinctHeaders, body: Buffer, now: Date): Proof;
22
+ /** Whether a pasted secret has its sender's shape (Sentry's and Vercel's come from them; the others can be made here). */
23
+ export declare function secretShapeOk(sender: SigningSender, secret: string): boolean;
24
+ /** A new secret for a sender Toolroll can make one for; Sentry's and Vercel's are pasted from them. */
25
+ export declare function makeSecret(sender: "standard" | "github-style"): string;
26
+ /** Whether Toolroll makes this sender's secret (and shows it once), rather than asking for the sender's own. */
27
+ export declare const makesSecret: (sender: SigningSender) => sender is "standard" | "github-style";