toolroll 0.9.54 → 0.9.56

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 (350) hide show
  1. package/CHANGELOG.md +19 -0
  2. package/README.md +29 -12
  3. package/dist/action-ledger.d.ts +2 -4
  4. package/dist/action-ledger.js +2 -10
  5. package/dist/agent-onboard.js +4 -1
  6. package/dist/approval-policy.d.ts +5 -5
  7. package/dist/approval-policy.js +4 -4
  8. package/dist/approval-rules-ui.d.ts +3 -2
  9. package/dist/approval-rules-ui.js +17 -22
  10. package/dist/assignment-ui.d.ts +3 -2
  11. package/dist/assignment-ui.js +4 -11
  12. package/dist/backup-ui.d.ts +2 -1
  13. package/dist/backup-ui.js +16 -27
  14. package/dist/brand-mark.d.ts +3 -2
  15. package/dist/brand-mark.js +4 -4
  16. package/dist/browser/workspace.css +1 -1
  17. package/dist/browser/workspace.js +25 -25
  18. package/dist/browser-shell.d.ts +2 -1
  19. package/dist/browser-shell.js +9 -9
  20. package/dist/browser-workspace.d.ts +14 -15
  21. package/dist/browser-workspace.js +0 -3
  22. package/dist/budget-alerts.js +3 -3
  23. package/dist/chat-actions.d.ts +23 -18
  24. package/dist/chat-actions.js +110 -88
  25. package/dist/chat-ask.d.ts +2 -2
  26. package/dist/chat-ask.js +5 -5
  27. package/dist/chat-channel.d.ts +15 -15
  28. package/dist/chat-channel.js +20 -20
  29. package/dist/chat-controls.d.ts +0 -4
  30. package/dist/chat-controls.js +0 -1
  31. package/dist/chat-core.js +1 -1
  32. package/dist/chat-delivery-state.js +2 -2
  33. package/dist/chat-delivery.d.ts +2 -2
  34. package/dist/chat-delivery.js +27 -34
  35. package/dist/chat-flow.js +3 -3
  36. package/dist/chat-messages.d.ts +1 -1
  37. package/dist/chat-messages.js +4 -4
  38. package/dist/chat-migration.d.ts +1 -1
  39. package/dist/chat-migration.js +11 -11
  40. package/dist/chat-polish.d.ts +5 -4
  41. package/dist/chat-polish.js +9 -10
  42. package/dist/chat-rooms.js +3 -3
  43. package/dist/chat-warmth.d.ts +2 -2
  44. package/dist/check-levels-ui.d.ts +2 -1
  45. package/dist/check-levels-ui.js +26 -53
  46. package/dist/cli.d.ts +3 -1
  47. package/dist/cli.js +15 -10
  48. package/dist/coding-shipping-ui.d.ts +2 -1
  49. package/dist/coding-shipping-ui.js +6 -6
  50. package/dist/coding-ui.d.ts +3 -1
  51. package/dist/coding-ui.js +30 -29
  52. package/dist/contracts/chat-actions.d.ts +32 -27
  53. package/dist/contracts/chat-actions.js +29 -11
  54. package/dist/contracts/chat-content.d.ts +1 -1
  55. package/dist/contracts/chat-content.js +1 -1
  56. package/dist/contracts/chat-tables.js +4 -4
  57. package/dist/contracts/cli.d.ts +1 -3
  58. package/dist/contracts/console-api.d.ts +7 -7
  59. package/dist/contracts/console-api.js +13 -12
  60. package/dist/contracts/flow-propose.d.ts +3 -3
  61. package/dist/contracts/flow.d.ts +132 -125
  62. package/dist/contracts/flow.js +27 -12
  63. package/dist/contracts/health.d.ts +1 -3
  64. package/dist/contracts/lead-context.d.ts +1 -1
  65. package/dist/contracts/lead-context.js +2 -2
  66. package/dist/contracts/lead-tools.d.ts +20 -18
  67. package/dist/contracts/lead-tools.js +15 -15
  68. package/dist/contracts/stage-output.d.ts +1 -1
  69. package/dist/contracts/stage-output.js +1 -1
  70. package/dist/contracts/store-json.d.ts +23 -22
  71. package/dist/contracts/store-json.js +17 -17
  72. package/dist/contracts/{teammate-turn.d.ts → subagent-turn.d.ts} +5 -5
  73. package/dist/contracts/{teammate-turn.js → subagent-turn.js} +7 -7
  74. package/dist/control-ui.d.ts +5 -4
  75. package/dist/control-ui.js +9 -34
  76. package/dist/converse.d.ts +23 -23
  77. package/dist/converse.js +28 -28
  78. package/dist/coordinator-proposals.d.ts +2 -2
  79. package/dist/coordinator-proposals.js +1 -1
  80. package/dist/coordinator.js +1 -1
  81. package/dist/credentials-ui.d.ts +5 -4
  82. package/dist/credentials-ui.js +23 -24
  83. package/dist/demo-chat.d.ts +3 -3
  84. package/dist/demo-chat.js +29 -76
  85. package/dist/deprecations.d.ts +12 -0
  86. package/dist/deprecations.js +20 -0
  87. package/dist/desktop-update-gate.js +8 -4
  88. package/dist/desktop-update.js +6 -1
  89. package/dist/discord-chat.js +6 -6
  90. package/dist/discord-settings.d.ts +3 -2
  91. package/dist/discord-settings.js +17 -42
  92. package/dist/dispatch.d.ts +1 -1
  93. package/dist/dispatch.js +1 -1
  94. package/dist/envelope.js +10 -1
  95. package/dist/evidence-pack.d.ts +2 -1
  96. package/dist/evidence-pack.js +45 -49
  97. package/dist/export-ui.d.ts +7 -2
  98. package/dist/export-ui.js +4 -10
  99. package/dist/export.js +7 -7
  100. package/dist/flow-engine.d.ts +2 -2
  101. package/dist/flow-engine.js +21 -21
  102. package/dist/flow-gallery-ui.d.ts +4 -3
  103. package/dist/flow-gallery-ui.js +28 -43
  104. package/dist/flow-insights.d.ts +1 -1
  105. package/dist/flow-share.js +13 -8
  106. package/dist/flow-starters-ui.d.ts +2 -1
  107. package/dist/flow-starters-ui.js +12 -15
  108. package/dist/flow-steps.d.ts +5 -5
  109. package/dist/flow-steps.js +19 -19
  110. package/dist/flow-triggers.d.ts +2 -1
  111. package/dist/flow-triggers.js +17 -15
  112. package/dist/flows-cli.js +1 -1
  113. package/dist/flows-ui.d.ts +4 -3
  114. package/dist/flows-ui.js +38 -49
  115. package/dist/flows.js +21 -20
  116. package/dist/guides.js +112 -6
  117. package/dist/html.d.ts +76 -0
  118. package/dist/html.js +173 -0
  119. package/dist/integrations-ui.d.ts +2 -1
  120. package/dist/integrations-ui.js +19 -23
  121. package/dist/invoke.js +1 -1
  122. package/dist/kits-ui.d.ts +5 -4
  123. package/dist/kits-ui.js +22 -24
  124. package/dist/kits.d.ts +12 -12
  125. package/dist/kits.js +31 -31
  126. package/dist/knowledge-ui.d.ts +6 -5
  127. package/dist/knowledge-ui.js +26 -25
  128. package/dist/{mate-cli.d.ts → lead-cli.d.ts} +11 -11
  129. package/dist/{mate-cli.js → lead-cli.js} +38 -38
  130. package/dist/lead-commitments.js +4 -4
  131. package/dist/lead-context.d.ts +12 -2
  132. package/dist/lead-context.js +15 -17
  133. package/dist/{mate-contract.d.ts → lead-contract.d.ts} +3 -3
  134. package/dist/{mate-contract.js → lead-contract.js} +7 -6
  135. package/dist/{mate-doors.d.ts → lead-doors.d.ts} +6 -6
  136. package/dist/{mate-doors.js → lead-doors.js} +24 -24
  137. package/dist/lead-follow.d.ts +5 -5
  138. package/dist/lead-follow.js +9 -9
  139. package/dist/lead-people.d.ts +5 -5
  140. package/dist/lead-people.js +14 -14
  141. package/dist/{mate-progress.d.ts → lead-progress.d.ts} +9 -9
  142. package/dist/{mate-progress.js → lead-progress.js} +3 -3
  143. package/dist/{mate-tools.d.ts → lead-tools.d.ts} +19 -19
  144. package/dist/{mate-tools.js → lead-tools.js} +109 -101
  145. package/dist/{mate.d.ts → lead.d.ts} +27 -27
  146. package/dist/{mate.js → lead.js} +102 -102
  147. package/dist/ledger-view.d.ts +2 -1
  148. package/dist/ledger-view.js +27 -29
  149. package/dist/limits-ui.d.ts +3 -2
  150. package/dist/limits-ui.js +8 -8
  151. package/dist/live-bus.d.ts +50 -8
  152. package/dist/live-bus.js +133 -50
  153. package/dist/mcp-connect.d.ts +1 -1
  154. package/dist/mcp-connect.js +3 -3
  155. package/dist/mcp-core.js +1 -1
  156. package/dist/mcp-oauth.js +35 -36
  157. package/dist/memory-pass.d.ts +2 -2
  158. package/dist/memory-pass.js +7 -7
  159. package/dist/models-ui.d.ts +2 -1
  160. package/dist/models-ui.js +25 -35
  161. package/dist/monitoring-ui.d.ts +4 -3
  162. package/dist/monitoring-ui.js +13 -26
  163. package/dist/notification-rows.d.ts +2 -2
  164. package/dist/openrouter-models.d.ts +2 -1
  165. package/dist/openrouter-models.js +5 -9
  166. package/dist/operate.d.ts +3 -3
  167. package/dist/operate.js +46 -41
  168. package/dist/people-audit-ui.d.ts +2 -1
  169. package/dist/people-audit-ui.js +13 -13
  170. package/dist/policy-ui.d.ts +3 -2
  171. package/dist/policy-ui.js +17 -23
  172. package/dist/policy.d.ts +1 -1
  173. package/dist/principal.d.ts +1 -1
  174. package/dist/principal.js +2 -2
  175. package/dist/project-delete-ui.d.ts +4 -3
  176. package/dist/project-delete-ui.js +16 -28
  177. package/dist/project-delete.d.ts +1 -1
  178. package/dist/project-delete.js +26 -26
  179. package/dist/project-memory.js +1 -1
  180. package/dist/project-tools.d.ts +1 -1
  181. package/dist/project-tools.js +1 -1
  182. package/dist/pull-request-ui.d.ts +2 -1
  183. package/dist/pull-request-ui.js +17 -32
  184. package/dist/recipe-ui.d.ts +8 -7
  185. package/dist/recipe-ui.js +39 -32
  186. package/dist/release-adapters.d.ts +60 -0
  187. package/dist/release-adapters.js +435 -0
  188. package/dist/release-cli.d.ts +24 -0
  189. package/dist/release-cli.js +181 -0
  190. package/dist/release-gate.d.ts +80 -0
  191. package/dist/release-gate.js +150 -0
  192. package/dist/release-links.d.ts +29 -0
  193. package/dist/release-links.js +102 -0
  194. package/dist/release.d.ts +331 -0
  195. package/dist/release.js +550 -0
  196. package/dist/reply-shape.d.ts +2 -1
  197. package/dist/reply-shape.js +5 -5
  198. package/dist/repository-context-ui.d.ts +2 -1
  199. package/dist/repository-context-ui.js +2 -7
  200. package/dist/request-budget-ui.d.ts +2 -1
  201. package/dist/request-budget-ui.js +16 -16
  202. package/dist/result-review.d.ts +2 -3
  203. package/dist/result-review.js +3 -9
  204. package/dist/retention-ui.d.ts +7 -1
  205. package/dist/retention-ui.js +7 -11
  206. package/dist/retention.js +4 -4
  207. package/dist/scope.d.ts +2 -2
  208. package/dist/scout-report.d.ts +1 -1
  209. package/dist/scout-report.js +1 -1
  210. package/dist/serve.d.ts +11 -2
  211. package/dist/serve.js +105 -99
  212. package/dist/server/chat.d.ts +7 -10
  213. package/dist/server/chat.js +984 -1021
  214. package/dist/server/chrome.d.ts +291 -0
  215. package/dist/server/chrome.js +747 -0
  216. package/dist/server/console.css +2819 -0
  217. package/dist/server/flows.d.ts +2 -6
  218. package/dist/server/flows.js +854 -773
  219. package/dist/server/guards.d.ts +1 -1
  220. package/dist/server/guards.js +8 -5
  221. package/dist/server/handler-context.d.ts +1 -1
  222. package/dist/server/handler-context.js +1 -1
  223. package/dist/server/handler-registry.d.ts +8 -6
  224. package/dist/server/handler-registry.js +14 -2
  225. package/dist/server/http.d.ts +274 -0
  226. package/dist/server/http.js +239 -0
  227. package/dist/server/live.d.ts +34 -0
  228. package/dist/server/live.js +309 -0
  229. package/dist/server/pages.d.ts +2 -6
  230. package/dist/server/pages.js +808 -789
  231. package/dist/server/people-tokens.d.ts +1 -6
  232. package/dist/server/people-tokens.js +581 -573
  233. package/dist/server/remote.d.ts +4 -7
  234. package/dist/server/remote.js +39 -70
  235. package/dist/server/render-chat.d.ts +402 -0
  236. package/dist/server/render-chat.js +927 -0
  237. package/dist/server/render-pages.d.ts +535 -0
  238. package/dist/server/render-pages.js +1277 -0
  239. package/dist/server/render-people.d.ts +32 -0
  240. package/dist/server/render-people.js +121 -0
  241. package/dist/server/render-results.d.ts +744 -0
  242. package/dist/server/render-results.js +2237 -0
  243. package/dist/server/render-settings.d.ts +154 -0
  244. package/dist/server/render-settings.js +580 -0
  245. package/dist/server/render-tasks.d.ts +632 -0
  246. package/dist/server/render-tasks.js +2345 -0
  247. package/dist/server/request-context.d.ts +46 -0
  248. package/dist/server/request-context.js +17 -0
  249. package/dist/server/route-table.d.ts +25 -3
  250. package/dist/server/route-table.js +167 -77
  251. package/dist/server/runtime.d.ts +28 -20
  252. package/dist/server/runtime.js +7 -1
  253. package/dist/server/session.d.ts +138 -0
  254. package/dist/server/session.js +94 -0
  255. package/dist/server/settings.d.ts +1 -5
  256. package/dist/server/settings.js +2186 -2121
  257. package/dist/server/task-views.d.ts +11 -4
  258. package/dist/server/task-views.js +26 -22
  259. package/dist/server/tasks.d.ts +9 -9
  260. package/dist/server/tasks.js +1489 -1420
  261. package/dist/server-telemetry.d.ts +4 -3
  262. package/dist/server-telemetry.js +4 -8
  263. package/dist/setup-guide.d.ts +2 -2
  264. package/dist/skills-ui.d.ts +4 -3
  265. package/dist/skills-ui.js +14 -17
  266. package/dist/skills.d.ts +1 -1
  267. package/dist/skills.js +2 -1
  268. package/dist/slack-chat.d.ts +2 -2
  269. package/dist/slack-chat.js +6 -6
  270. package/dist/slack-settings.d.ts +3 -2
  271. package/dist/slack-settings.js +17 -42
  272. package/dist/spend-ui.d.ts +5 -4
  273. package/dist/spend-ui.js +17 -40
  274. package/dist/spend.d.ts +14 -14
  275. package/dist/spend.js +31 -31
  276. package/dist/sso-ui.d.ts +3 -2
  277. package/dist/sso-ui.js +26 -27
  278. package/dist/storage-ui.d.ts +2 -1
  279. package/dist/storage-ui.js +14 -33
  280. package/dist/store-baseline.d.ts +19 -0
  281. package/dist/store-baseline.js +2873 -0
  282. package/dist/store.d.ts +197 -249
  283. package/dist/store.js +747 -3084
  284. package/dist/subagent-admin.d.ts +53 -0
  285. package/dist/{teammate-admin.js → subagent-admin.js} +37 -37
  286. package/dist/{teammate-desk.d.ts → subagent-desk.d.ts} +22 -23
  287. package/dist/{teammate-desk.js → subagent-desk.js} +26 -35
  288. package/dist/{teammate-memory.d.ts → subagent-memory.d.ts} +17 -17
  289. package/dist/{teammate-memory.js → subagent-memory.js} +41 -41
  290. package/dist/{teammate-question.d.ts → subagent-question.d.ts} +6 -6
  291. package/dist/{teammate-question.js → subagent-question.js} +17 -17
  292. package/dist/{teammate-tools.d.ts → subagent-tools.d.ts} +18 -18
  293. package/dist/{teammate-tools.js → subagent-tools.js} +36 -36
  294. package/dist/{teammate-week.d.ts → subagent-week.d.ts} +10 -10
  295. package/dist/{teammate-week.js → subagent-week.js} +23 -23
  296. package/dist/{teammate-work.d.ts → subagent-work.d.ts} +21 -21
  297. package/dist/{teammate-work.js → subagent-work.js} +50 -50
  298. package/dist/subagents-ui.d.ts +22 -0
  299. package/dist/subagents-ui.js +137 -0
  300. package/dist/{teammates.d.ts → subagents.d.ts} +11 -11
  301. package/dist/{teammates.js → subagents.js} +21 -21
  302. package/dist/subscription-chat.d.ts +13 -13
  303. package/dist/subscription-chat.js +19 -19
  304. package/dist/surface.d.ts +19 -2
  305. package/dist/surface.js +20 -33
  306. package/dist/task-composer.d.ts +2 -1
  307. package/dist/task-composer.js +7 -7
  308. package/dist/task-status.d.ts +4 -3
  309. package/dist/task-status.js +15 -16
  310. package/dist/team-cli.d.ts +1 -0
  311. package/dist/team-cli.js +14 -2
  312. package/dist/team-http.d.ts +5 -9
  313. package/dist/team-http.js +5 -63
  314. package/dist/team-leads.js +13 -13
  315. package/dist/team-runtime.d.ts +2 -2
  316. package/dist/team-runtime.js +8 -8
  317. package/dist/team-ui.d.ts +2 -1
  318. package/dist/team-ui.js +2 -5
  319. package/dist/teams-chat.js +6 -6
  320. package/dist/teams-settings.d.ts +3 -2
  321. package/dist/teams-settings.js +20 -31
  322. package/dist/telegram-flow.d.ts +3 -3
  323. package/dist/telegram-flow.js +3 -3
  324. package/dist/{telegram-mate.d.ts → telegram-lead.d.ts} +5 -5
  325. package/dist/{telegram-mate.js → telegram-lead.js} +27 -27
  326. package/dist/telegram-settings.d.ts +3 -2
  327. package/dist/telegram-settings.js +14 -25
  328. package/dist/telegram-status.js +1 -1
  329. package/dist/telegram-team.d.ts +1 -1
  330. package/dist/telegram-team.js +5 -5
  331. package/dist/telegram.d.ts +1 -1
  332. package/dist/telegram.js +9 -30
  333. package/dist/text-limits.d.ts +18 -18
  334. package/dist/text-limits.js +18 -18
  335. package/dist/toolroll-update-ui.d.ts +3 -2
  336. package/dist/toolroll-update-ui.js +35 -39
  337. package/dist/toolroll-update.d.ts +8 -0
  338. package/dist/toolroll-update.js +45 -8
  339. package/dist/tools-ui.d.ts +5 -2
  340. package/dist/tools-ui.js +31 -31
  341. package/dist/when-html.d.ts +11 -3
  342. package/dist/when-html.js +5 -6
  343. package/dist/workspace-ui.d.ts +4 -2
  344. package/dist/workspace-ui.js +10 -10
  345. package/package.json +1 -2
  346. package/dist/server/shared.d.ts +0 -3151
  347. package/dist/server/shared.js +0 -12001
  348. package/dist/teammate-admin.d.ts +0 -53
  349. package/dist/teammates-ui.d.ts +0 -20
  350. package/dist/teammates-ui.js +0 -166
@@ -6,7 +6,7 @@ import { authenticateApprover as checkApproverPassword } from "../scope.js";
6
6
  import { SourceBudget } from "../sign-in-guard.js";
7
7
  import { type Run, type Store } from "../store.js";
8
8
  import type { RouteDeclaration } from './route-table.js';
9
- import { PersistentSessions, type Session, type Who } from "./shared.js";
9
+ import { PersistentSessions, type Session, type Who } from "./session.js";
10
10
  /** Shared browser, credential and project guards. Runtime access is lazy to preserve server initialization order. */
11
11
  export interface GuardsRuntime {
12
12
  store: Store;
@@ -9,7 +9,10 @@ import { canonicalProject, rowVisible } from "../project.js";
9
9
  import { authenticateAccount, authenticateApprover as checkApproverPassword } from "../scope.js";
10
10
  import { SourceBudget } from "../sign-in-guard.js";
11
11
  import {} from "../store.js";
12
- import { matchTaskPath, PersistentSessions, refuse, requestContext, SESSION_ABSOLUTE_MS, SESSION_COOKIE, SESSION_IDLE_MS } from "./shared.js";
12
+ import { requestContext } from "./request-context.js";
13
+ import { refuse } from "./chrome.js";
14
+ import { matchTaskPath } from "./http.js";
15
+ import { PersistentSessions, SESSION_ABSOLUTE_MS, SESSION_COOKIE, SESSION_IDLE_MS } from "./session.js";
13
16
  export function createSharedGuards(runtime) {
14
17
  // v99: wrong passwords. One address guessing across names runs out of tries here; each name
15
18
  // locks on its own (sign-in-guard.ts), and every lock, whatever road it came by, is kept.
@@ -163,7 +166,7 @@ export function createSharedGuards(runtime) {
163
166
  const path = url.pathname;
164
167
  const project = resolveRouteProject(source, url, who, request, body);
165
168
  if (restricted() && body !== null) {
166
- const repos = body.getAll("repo");
169
+ const repos = readForm(body, CONSOLE_FORMS.projectCeiling).getAll("repo");
167
170
  if (repos.length > 1 || repos.some(repo => repo.trim() !== "" && !visible(repo.trim()))) {
168
171
  refuse(response, who, 403, "That project is outside your access.", "/projects");
169
172
  return false;
@@ -203,7 +206,7 @@ export function createSharedGuards(runtime) {
203
206
  if (route?.limited === "self")
204
207
  return true;
205
208
  if (route?.limited === "proposal") {
206
- const proposal = runtime.store.getMateProposal(Number(path.split('/')[3])), action = proposal?.kind === 'action' ? sharedActionPayload(proposal.payload) : null;
209
+ const proposal = runtime.store.getLeadProposal(Number(path.split('/')[3])), action = proposal?.kind === 'action' ? sharedActionPayload(proposal.payload) : null;
207
210
  const shared = proposal ? runtime.store.handle.prepare('SELECT id FROM team_conversation WHERE thread=?').get(proposal.thread) : null;
208
211
  if (shared && who.via === 'cookie') {
209
212
  try {
@@ -213,10 +216,10 @@ export function createSharedGuards(runtime) {
213
216
  }
214
217
  catch { /* The same scoped refusal below also covers revoked membership. */ }
215
218
  }
216
- if (action && proposal && runtime.store.getMateThread(proposal.thread)?.approver === who.name && visible(action.repo) && runtime.store.accountCanAccess(who.name, action.repo))
219
+ if (action && proposal && runtime.store.getLeadThread(proposal.thread)?.approver === who.name && visible(action.repo) && runtime.store.accountCanAccess(who.name, action.repo))
217
220
  return true;
218
221
  // A card about the person themselves (what their lead knows about them) is in no project.
219
- if (action && proposal && action.repo === '' && OWNER_ACTIONS.has(action.operation) && runtime.store.getMateThread(proposal.thread)?.approver === who.name)
222
+ if (action && proposal && action.repo === '' && OWNER_ACTIONS.has(action.operation) && runtime.store.getLeadThread(proposal.thread)?.approver === who.name)
220
223
  return true;
221
224
  refuse(response, who, 404, 'No such action in your projects.', '/projects');
222
225
  return false;
@@ -1,6 +1,6 @@
1
1
  import type { IncomingMessage, ServerResponse } from 'node:http';
2
2
  import type { RouteDeclaration } from './route-table.js';
3
- import type { Who } from './shared.js';
3
+ import { type Who } from "./session.js";
4
4
  export interface HandlerContext {
5
5
  url: URL;
6
6
  who: Who;
@@ -1 +1 @@
1
- export {};
1
+ import {} from "./session.js";
@@ -1,19 +1,21 @@
1
1
  import type { EdgeContext, HandlerContext } from './handler-context.js';
2
- import { type Domain, type RouteDeclaration, type RouteMethod } from './route-table.js';
2
+ import { type Domain, type RouteDeclaration } from './route-table.js';
3
+ export type ConsoleHandler = (ctx: HandlerContext) => Promise<void>;
4
+ export type EdgeHandler = (ctx: EdgeContext) => Promise<void>;
3
5
  export type Registration = {
4
6
  id: string;
5
7
  domain: Domain;
6
8
  stage: 'console';
7
- method: RouteMethod;
8
- handle: (ctx: HandlerContext) => Promise<void>;
9
+ handle: ConsoleHandler;
9
10
  } | {
10
11
  id: string;
11
12
  domain: Domain;
12
13
  stage: 'edge';
13
- method: RouteMethod;
14
- handle: (ctx: EdgeContext) => Promise<void>;
14
+ handle: EdgeHandler;
15
15
  };
16
- /** Registrations are independent of policy. Missing, extra or incompatible entries fail at startup. */
16
+ /** One domain's handlers by route id: one function per row. */
17
+ export declare function handlersOf(domain: Domain, console: Readonly<Record<string, ConsoleHandler>>, edge?: Readonly<Record<string, EdgeHandler>>): Registration[];
18
+ /** Registrations are independent of policy. Missing, extra, incompatible or shared handlers fail at startup. */
17
19
  export declare function assertHandlerRegistry(registrations: readonly Registration[], routes?: readonly RouteDeclaration[]): void;
18
20
  export declare function createHandlerRegistry(registrations: readonly Registration[]): {
19
21
  console(route: RouteDeclaration, ctx: HandlerContext): Promise<void>;
@@ -1,14 +1,26 @@
1
1
  import { ROUTES } from './route-table.js';
2
- /** Registrations are independent of policy. Missing, extra or incompatible entries fail at startup. */
2
+ /** One domain's handlers by route id: one function per row. */
3
+ export function handlersOf(domain, console, edge = {}) {
4
+ return [
5
+ ...Object.entries(console).map(([id, handle]) => ({ id, domain, stage: 'console', handle })),
6
+ ...Object.entries(edge).map(([id, handle]) => ({ id, domain, stage: 'edge', handle })),
7
+ ];
8
+ }
9
+ /** Registrations are independent of policy. Missing, extra, incompatible or shared handlers fail at startup. */
3
10
  export function assertHandlerRegistry(registrations, routes = ROUTES) {
4
11
  const seen = new Set();
12
+ const callbacks = new Map();
5
13
  for (const handler of registrations) {
6
14
  if (seen.has(handler.id))
7
15
  throw new Error(`Duplicate handler: ${handler.id}`);
8
16
  seen.add(handler.id);
9
17
  const route = routes.find(row => row.id === handler.id);
10
- if (!route || route.domain !== handler.domain || route.method !== handler.method || route.stage !== handler.stage)
18
+ if (!route || route.domain !== handler.domain || route.stage !== handler.stage)
11
19
  throw new Error(`Handler has no compatible route: ${handler.id}`);
20
+ const other = callbacks.get(handler.handle);
21
+ if (other !== undefined)
22
+ throw new Error(`Handler shared by ${other} and ${handler.id}`);
23
+ callbacks.set(handler.handle, handler.id);
12
24
  }
13
25
  for (const route of routes)
14
26
  if (!seen.has(route.id))
@@ -0,0 +1,274 @@
1
+ /** HTTP answers every console route shares: server options, body limits, safe responses, redirects and return paths. */
2
+ import { type Html } from "../html.js";
3
+ import { type CliHttpOptions, type RunOperateAs } from "../cli-http.js";
4
+ import { type CodingWorkspace } from "../coding-workspace.js";
5
+ import { type run as execRun } from "../exec.js";
6
+ import { type MailSender } from "../flow-actions.js";
7
+ import { type FetchLike } from "../flow-share.js";
8
+ import { type TriggerIo } from "../flow-triggers.js";
9
+ import { type InstallMethod } from "../install-method.js";
10
+ import { type IntegrationIo } from "../integrations.js";
11
+ import { type VersionRunner } from "../model-catalog.js";
12
+ import { type cloneGithubRepo, type listGithubRepos, type previewGithubRepo } from "../onboard.js";
13
+ import { type PublishExec } from "../publish.js";
14
+ import { type ResultTab } from "../result-review.js";
15
+ import { type Store } from "../store.js";
16
+ import { type SubscriptionLeadRunner } from "../subscription-chat.js";
17
+ import { type launchRuntimeUpdate } from "../toolroll-update.js";
18
+ import { type IncomingMessage, type ServerResponse } from "node:http";
19
+ export type ServeOptions = {
20
+ /** Tests: runs a person's command for the HTTP MCP gateway instead of operate.ts's. */
21
+ runOperateAs?: import("../mcp-person.js").RunOperateAs;
22
+ /** `toolroll demo`: the scripted lead that answers Chat instead of a model. */
23
+ demoLead?: import("../demo.js").DemoLead;
24
+ /** Tests: the bin whose real path says how Toolroll was installed (Settings → Updates' command). */
25
+ installBin?: string;
26
+ /** Native coding workspace injection for isolated integration tests. */
27
+ codingWorkspace?: CodingWorkspace;
28
+ /** Read-only worker connections for heavy reads (read-executor.ts); 0 reads in-process. Default: off under tests. */
29
+ readWorkers?: number;
30
+ store: Store;
31
+ evidenceRoot: string;
32
+ clock?: () => Date;
33
+ /**
34
+ * The console's canonical https origin, when TLS terminates in front
35
+ * (arc 3 finding 2/16): EXACTLY an origin — no path, query, credentials.
36
+ * The one trust anchor for secure-context features: it joins the allowed
37
+ * hosts, its origin authorizes POSTs, cookies turn Secure, and the
38
+ * install/push cards light up. X-Forwarded-* is consulted only from a
39
+ * loopback peer, the same-host proxy (public-access.ts).
40
+ */
41
+ publicUrl?: string;
42
+ /** Tests: the shared command boundary `POST /api/cli` runs (default: operate.ts's runOperateAs). */
43
+ cliRunner?: RunOperateAs;
44
+ /** Tests: remote command metadata until the shared contract declares it. */
45
+ cliModeOf?: CliHttpOptions['modeOf'];
46
+ /** Tests: the clock request budgets for API tokens count by (request-budget.ts). */
47
+ requestBudgetClock?: () => number;
48
+ /** Where repos.json lives — every enrollment locks exactly this file. */
49
+ registryPath?: string;
50
+ /** This console fronts an `up` process: onboarding copy says how to watch. */
51
+ upConsole?: boolean;
52
+ /**
53
+ * The lead on by default (onboarding): with no lead set up, the agent CLI
54
+ * signed in on this computer runs it, once. `up` and `serve` turn this on;
55
+ * tests opt in.
56
+ */
57
+ leadByDefault?: boolean;
58
+ /** Test seam: the operating system the sign-in command is written for. */
59
+ platform?: NodeJS.Platform;
60
+ /** Extra Host values this server answers as (a Tailscale name, a LAN ip:port). */
61
+ allowedHosts?: readonly string[];
62
+ /**
63
+ * This computer's names on its tailnet (onboarding): read at start and every
64
+ * few minutes; the console answers to each on its own port without an
65
+ * --allow-host, and the phone card names the address. Absent = none.
66
+ */
67
+ tailnetNames?: () => Promise<readonly string[]>;
68
+ /**
69
+ * The first-account road (setup review): while NO approver exists, the
70
+ * login page offers "create the first account", gated by this code —
71
+ * printed once by the process that started the server, never stored.
72
+ * Five wrong codes close the road until the server restarts. The moment
73
+ * an approver exists, the page is the ordinary sign-in.
74
+ */
75
+ setupCode?: string;
76
+ /**
77
+ * Where the Telegram bot token lives when set from here. Present = the
78
+ * settings card renders; absent = no settings surface at all.
79
+ */
80
+ telegramTokenFile?: string;
81
+ /** Where messaging config files live (beside the database) — enables the
82
+ * primary-messenger selector on the settings screen. */
83
+ configDir?: string;
84
+ /** Test seam for the Slack setup handshake. */
85
+ slackFetcher?: typeof fetch;
86
+ /** Settings → Updates seams: the latest release, how this Toolroll was
87
+ * installed, its version, and how the updater job starts. Tests and
88
+ * screenshots inject these; production reads the registry and launchd. */
89
+ updates?: {
90
+ latest?: () => Promise<{
91
+ version: string;
92
+ }>;
93
+ method?: InstallMethod;
94
+ current?: string;
95
+ dist?: string;
96
+ launch?: typeof launchRuntimeUpdate;
97
+ };
98
+ discordFetcher?: typeof fetch;
99
+ /** Injected by tests: Microsoft sign-in, key metadata and Teams conversation calls. */
100
+ teamsFetcher?: typeof fetch;
101
+ /** Injected by tests: how "Check now" reaches GitHub (gh) and Linear (fetch). */
102
+ flowTriggerIo?: Partial<TriggerIo>;
103
+ /**
104
+ * The repo this console serves. Scopes run evidence to that repo's tasks
105
+ * (and unplaced ones) and turns on the gaps and capabilities views —
106
+ * without it those pages say so instead of guessing.
107
+ */
108
+ repo?: string;
109
+ /**
110
+ * The full authorization ceiling (v2 review, finding 1): `repos` this
111
+ * server may show and operate on, plus `projectRoots` under which any git
112
+ * repository qualifies. `repo` above is sugar for one entry in `repos`.
113
+ * No configuration at all is the legacy unscoped mode — everything
114
+ * visible, and stated as such where the code decides.
115
+ */
116
+ repos?: readonly string[];
117
+ projectRoots?: readonly string[];
118
+ /** Repositories the co-located `up` process has proved and is watching.
119
+ * Kept as a callback so projects added after startup appear immediately
120
+ * without turning the durable registry itself into an authorization source. */
121
+ currentRepos?: () => readonly string[];
122
+ /** Injected by tests: the fetch direct-API chat turns use, and where chat
123
+ * keys are read from (defaults to process.env). */
124
+ chatFetcher?: typeof fetch;
125
+ /** Subscription-backed mate transport; injected in tests so no real
126
+ * Codex or Claude membership turn is consumed. */
127
+ subscriptionChatRunner?: SubscriptionLeadRunner;
128
+ /** Tests: stands in for `codex mcp list --json` in the project (the Tools page's "Found on this computer"). */
129
+ codexToolList?: (cwd: string) => Promise<string | null>;
130
+ /** Tests: the home whose ~/.toolroll (or older ~/.standing-orders) tool-secrets and ~/.claude.json the Tools page uses. */
131
+ toolHome?: string;
132
+ /** v87: sends Send email steps' mail and the settings test (tests inject one). */
133
+ mailSender?: MailSender;
134
+ /** Injected by tests: how Flows → Import fetches a flow file's address. */
135
+ flowFetch?: FetchLike;
136
+ /** v89: Google's token endpoint (tests inject a scripted one). */
137
+ googleFetch?: typeof fetch;
138
+ /** Tests: every request a one-click connection makes (discovery, registration, tokens). */
139
+ connectFetch?: typeof fetch;
140
+ /** Tests: every request sign-in with the identity provider makes (discovery, keys, tokens). */
141
+ ssoFetch?: typeof fetch;
142
+ chatEnv?: Record<string, string | undefined>;
143
+ /**
144
+ * The live peek's locality ASSERTION (live-peek v3 §3): the administrator
145
+ * who starts serve names the runner this machine owns. This is documented
146
+ * as an assertion, not machine-bound credential enforcement — the product
147
+ * has none anywhere. Absent = the peek is off, and says so.
148
+ */
149
+ desktopIdentity?: string;
150
+ /** Trusted native admission callback; enrollment rows alone never supply this authority. */
151
+ additionalProjectRepos?: () => readonly string[];
152
+ /** The co-located `up`'s exact admitted repositories, read on every request: its startup projects plus those
153
+ * added since (`repos add`, the lead, the console) and minus those removed. When given it replaces `repos` as
154
+ * the exact part of the ceiling, so an addition or removal shows without a restart. */
155
+ admittedRepos?: () => readonly string[];
156
+ connectionProbe?: typeof execRun;
157
+ connectionHome?: string;
158
+ /** Test seam for Chat's first tasks: the `gh` and `git grep` reads. */
159
+ firstTaskRunner?: typeof execRun;
160
+ /** Test seam for Settings → Integrations: how its checks reach services (fetch, gh, mail servers). */
161
+ integrationIo?: Partial<IntegrationIo>;
162
+ modelCatalogFetcher?: typeof fetch;
163
+ /** Test seams for Settings → Models: CLI version probes and the PATH they search. */
164
+ modelRunner?: VersionRunner;
165
+ modelPath?: string;
166
+ localRunner?: string;
167
+ /** The checkout pool root the peek confines itself to (realpath-proved). */
168
+ poolRoot?: string;
169
+ /**
170
+ * Editor deep links (arc 6): a DEPLOYMENT capability, not an activation.
171
+ * vscode:// links open on the BROWSER's machine, so links render only
172
+ * when three statements align: the operator started serve with
173
+ * --editor vscode AND --runner (this machine owns the worktrees), the
174
+ * run belongs to that runner, and THIS session turned links on for
175
+ * this device. "vscode" is the only value; the scheme is never data.
176
+ */
177
+ editorLinks?: "vscode";
178
+ /** Injected by tests: the onboarding ceremony's gh-facing halves — the
179
+ * ceremony's gating, nonce, and enrollment logic is what the HTTP tests
180
+ * prove; gh itself is proved by onboard.test.ts. */
181
+ ghPreview?: typeof previewGithubRepo;
182
+ ghClone?: typeof cloneGithubRepo;
183
+ ghList?: typeof listGithubRepos;
184
+ /** Injected by tests: the git and gh calls pull-request setup and Merge make. */
185
+ publishExec?: PublishExec;
186
+ };
187
+ export declare const BODY_CAP: number;
188
+ /** How long a stop waits for subagents and the lead's follow pass before closing anyway. */
189
+ export declare const SHUTDOWN_WAIT_MS = 5000;
190
+ /**
191
+ * Match `/t/<id>` (suffix "") or `/t/<id>/<verb>` — the id percent-decoded
192
+ * exactly once, refused when it does not decode, is oversized, or carries
193
+ * control characters. Legacy CLI-created ids are free-form; the URL is not.
194
+ */
195
+ export declare function matchTaskPath(pathname: string, suffixPattern: string): {
196
+ taskId: string;
197
+ verb: string;
198
+ } | null;
199
+ export declare function taskHref(taskId: string): string;
200
+ export declare const SAFETY: {
201
+ readonly "Content-Security-Policy": "default-src 'none'; style-src 'self' 'unsafe-inline'; font-src 'self'; manifest-src 'self'; worker-src 'self'; img-src 'self'; form-action 'self'; base-uri 'none'; frame-ancestors 'none'";
202
+ readonly "X-Content-Type-Options": "nosniff";
203
+ readonly "Referrer-Policy": "no-referrer";
204
+ readonly "Cache-Control": "no-store";
205
+ };
206
+ /** A text/html answer carries markup (Html); every other type, its own text. */
207
+ export type ResponseBody<T extends string> = T extends `text/html${string}` ? Html : string;
208
+ export declare function respond<T extends string>(response: ServerResponse, status: number, type: T, body: ResponseBody<T>): void;
209
+ /**
210
+ * A page response. With a nonce, this response's CSP admits exactly the one
211
+ * inline script the shell stamped with the same value — generated per
212
+ * response, never shared, never 'unsafe-inline' (Codex board review,
213
+ * finding 9). Everything else keeps the constant script-free policy.
214
+ */
215
+ export declare function page(response: ServerResponse, status: number, document: Html, nonce?: string, fetches?: boolean): void;
216
+ /**
217
+ * An outside sign-in comes back only to the browser that started it: the
218
+ * start leaves the visit's state in this cookie (Lax, so it rides the
219
+ * service's redirect back; the session cookie is Strict and doesn't), and a
220
+ * return whose state isn't the one this browser holds is refused. So nobody
221
+ * can start a sign-in and have someone else finish it into their project.
222
+ */
223
+ /** A moment's page on the way to or back from another site's sign-in: no stylesheet may load there, so the palette rides inline. */
224
+ export declare const HANDOFF_STYLE: Html;
225
+ export declare const SIGN_IN_COOKIE = "so-sign-in";
226
+ export declare function startedHere(request: IncomingMessage, state: string): boolean;
227
+ export declare const signInSpent: (path: string) => string;
228
+ /**
229
+ * Off to another site's sign-in (Google, a service's one-click connection):
230
+ * a page that moves on by itself. Not a redirect: every page's form-action
231
+ * 'self' also covers where a form's answer redirects, so a browser stops a
232
+ * form that is answered with another site's address.
233
+ */
234
+ export declare function goOutside(response: ServerResponse, to: string, words: string, bind: {
235
+ state: string;
236
+ path: string;
237
+ secure: boolean;
238
+ }): void;
239
+ export declare function redirect(response: ServerResponse, to: string): void;
240
+ export declare const taskChatHref: (taskId: string) => string;
241
+ /** A project's own lead thread (v77). */
242
+ export declare const projectChatHref: (repo: string) => string;
243
+ export declare function safeReturn(raw: string | null | undefined): string;
244
+ /**
245
+ * The destination a sign-in returns to (a phone's deep link to an exact
246
+ * task or result): `safeReturn`, then narrower still — never the sign-in,
247
+ * sign-up, join or sign-out roads themselves (a loop, or a token in a
248
+ * path), never an encoded second scheme or host once the browser decodes
249
+ * it, never a query key that could carry a secret, and never a region
250
+ * fetch (`?fragment=`), which is a piece of a page and not a place to
251
+ * land. Only the app's own pages qualify: the task lens, the chat, the
252
+ * work and board views, and the fixed control destinations a phone
253
+ * button can name. Nothing here is a second redirect framework: one
254
+ * same-site path, or "/".
255
+ */
256
+ export declare const LOGIN_RETURN_PAGES: RegExp;
257
+ export declare function loginReturn(raw: string | null | undefined): string;
258
+ /** A request path fit for a log: the one-time sign-in link's code is left out. */
259
+ export declare function redactedPath(path: string): string;
260
+ /** The sign-in page that comes back to `path` afterwards, or plain /login when the path is not one to come back to. */
261
+ export declare function loginHref(path: string): string;
262
+ /** Chat actions may return only to the unified chat or one task-focused
263
+ * lens. Other same-site paths are valid elsewhere, but not for chat forms. */
264
+ export declare function safeChatReturn(raw: string | null | undefined): string;
265
+ export declare function chatReturnWithSaid(back: string, said: string): string;
266
+ export declare const chatReturnWithLatest: (back: string) => string;
267
+ /** The one result page's address: the task and its run, never a project path
268
+ * (the page finds the result's project itself and keeps the person's own). */
269
+ export declare const reviewHref: (taskId: string, runId?: number | null) => string;
270
+ /** The chat's result detail (package 3): the same panel the run page
271
+ * and the cockpit render, opened beside the conversation when the screen
272
+ * has room and as a dedicated view with Back to chat when it does not. */
273
+ export declare const chatResultHref: (taskId: string, runId: number, tab?: ResultTab) => string;
274
+ export declare function form(request: IncomingMessage, cap?: number): Promise<URLSearchParams>;
@@ -0,0 +1,239 @@
1
+ /** HTTP answers every console route shares: server options, body limits, safe responses, redirects and return paths. */
2
+ import { html, htmlString, isHtml } from "../html.js";
3
+ import { chatResultHref as sharedResultHref } from "../chat-controls.js";
4
+ import {} from "../cli-http.js";
5
+ import {} from "../coding-workspace.js";
6
+ import { hasForbiddenControls } from "../decision.js";
7
+ import {} from "../exec.js";
8
+ import {} from "../flow-actions.js";
9
+ import {} from "../flow-share.js";
10
+ import {} from "../flow-triggers.js";
11
+ import {} from "../install-method.js";
12
+ import {} from "../integrations.js";
13
+ import {} from "../model-catalog.js";
14
+ import {} from "../onboard.js";
15
+ import {} from "../publish.js";
16
+ import {} from "../result-review.js";
17
+ import {} from "../store.js";
18
+ import {} from "../subscription-chat.js";
19
+ import {} from "../toolroll-update.js";
20
+ import { timingSafeEqual } from "node:crypto";
21
+ import {} from "node:http";
22
+ import { SIGN_IN_LINK_PATH } from "./session.js";
23
+ // A note is at most NOTE_BYTE_CAP (16,000) UTF-8 bytes, which URL encoding
24
+ // can triple: room for one whole, before canonical text validation.
25
+ export const BODY_CAP = 64 * 1024;
26
+ /** How long a stop waits for subagents and the lead's follow pass before closing anyway. */
27
+ export const SHUTDOWN_WAIT_MS = 5_000;
28
+ // ---- path plumbing ---------------------------------------------------------
29
+ /**
30
+ * Match `/t/<id>` (suffix "") or `/t/<id>/<verb>` — the id percent-decoded
31
+ * exactly once, refused when it does not decode, is oversized, or carries
32
+ * control characters. Legacy CLI-created ids are free-form; the URL is not.
33
+ */
34
+ export function matchTaskPath(pathname, suffixPattern) {
35
+ const match = new RegExp(`^/t/([^/]+)${suffixPattern === "" ? "$" : suffixPattern}`).exec(pathname);
36
+ if (match === null)
37
+ return null;
38
+ let taskId;
39
+ try {
40
+ taskId = decodeURIComponent(match[1]);
41
+ }
42
+ catch {
43
+ return null;
44
+ }
45
+ if (taskId.length === 0 || taskId.length > 64 || hasForbiddenControls(taskId))
46
+ return null;
47
+ return { taskId, verb: match[2] ?? "" };
48
+ }
49
+ export function taskHref(taskId) {
50
+ return `/t/${encodeURIComponent(taskId)}`;
51
+ }
52
+ // ---- rendering -------------------------------------------------------------
53
+ export const SAFETY = {
54
+ "Content-Security-Policy": "default-src 'none'; style-src 'self' 'unsafe-inline'; font-src 'self'; manifest-src 'self'; worker-src 'self'; img-src 'self'; form-action 'self'; base-uri 'none'; frame-ancestors 'none'",
55
+ "X-Content-Type-Options": "nosniff",
56
+ "Referrer-Policy": "no-referrer",
57
+ "Cache-Control": "no-store",
58
+ };
59
+ export function respond(response, status, type, body) {
60
+ // A route that set its OWN policy (the service worker's default-src
61
+ // 'none') or its own caching (the pre-auth assets) keeps it —
62
+ // writeHead's headers would otherwise win.
63
+ const own = response.getHeader("content-security-policy");
64
+ const cache = response.getHeader("cache-control");
65
+ response.writeHead(status, {
66
+ ...SAFETY,
67
+ ...(own === undefined ? {} : { "content-security-policy": own }),
68
+ ...(cache === undefined ? {} : { "Cache-Control": cache }),
69
+ "Content-Type": type,
70
+ });
71
+ response.end(isHtml(body) ? htmlString(body) : body);
72
+ }
73
+ /**
74
+ * A page response. With a nonce, this response's CSP admits exactly the one
75
+ * inline script the shell stamped with the same value — generated per
76
+ * response, never shared, never 'unsafe-inline' (Codex board review,
77
+ * finding 9). Everything else keeps the constant script-free policy.
78
+ */
79
+ export function page(response, status, document, nonce, fetches) {
80
+ if (nonce === undefined)
81
+ return respond(response, status, "text/html; charset=utf-8", document);
82
+ response.writeHead(status, {
83
+ ...SAFETY,
84
+ "Content-Security-Policy": `default-src 'none'; style-src 'self' 'unsafe-inline'; font-src 'self'; manifest-src 'self'; worker-src 'self'; img-src 'self'; script-src 'nonce-${nonce}'; ` +
85
+ // connect-src only when the page's script actually fetches (a region
86
+ // poller, the full chrome beat, or the minimal sensitive-page beat).
87
+ `${fetches === true ? "connect-src 'self'; " : ""}form-action 'self'; base-uri 'none'; frame-ancestors 'none'`,
88
+ "Content-Type": "text/html; charset=utf-8",
89
+ });
90
+ response.end(htmlString(document));
91
+ }
92
+ /**
93
+ * An outside sign-in comes back only to the browser that started it: the
94
+ * start leaves the visit's state in this cookie (Lax, so it rides the
95
+ * service's redirect back; the session cookie is Strict and doesn't), and a
96
+ * return whose state isn't the one this browser holds is refused. So nobody
97
+ * can start a sign-in and have someone else finish it into their project.
98
+ */
99
+ /** A moment's page on the way to or back from another site's sign-in: no stylesheet may load there, so the palette rides inline. */
100
+ export const HANDOFF_STYLE = html `<style>body{font:15px/1.5 "Geist",system-ui,sans-serif;margin:2rem;background:#efefef;color:#171717}a{color:#171717;text-decoration-color:#8f8f8f;text-underline-offset:3px}@media(prefers-color-scheme:dark){body{background:#0b0b0b;color:#ededed}a{color:#ededed;text-decoration-color:#ff6fb5}}</style>`;
101
+ export const SIGN_IN_COOKIE = "so-sign-in";
102
+ export function startedHere(request, state) {
103
+ const held = new RegExp(`(?:^|;\\s*)${SIGN_IN_COOKIE}=([A-Za-z0-9_-]{16,128})`).exec(request.headers.cookie ?? "")?.[1];
104
+ return held !== undefined && held.length === state.length && timingSafeEqual(Buffer.from(held), Buffer.from(state));
105
+ }
106
+ export const signInSpent = (path) => `${SIGN_IN_COOKIE}=; Path=${path}; Max-Age=0; HttpOnly; SameSite=Lax`;
107
+ /**
108
+ * Off to another site's sign-in (Google, a service's one-click connection):
109
+ * a page that moves on by itself. Not a redirect: every page's form-action
110
+ * 'self' also covers where a form's answer redirects, so a browser stops a
111
+ * form that is answered with another site's address.
112
+ */
113
+ export function goOutside(response, to, words, bind) {
114
+ response.writeHead(200, { "content-type": "text/html; charset=utf-8", "cache-control": "no-store", "referrer-policy": "no-referrer", "x-content-type-options": "nosniff", "x-frame-options": "DENY",
115
+ "set-cookie": `${SIGN_IN_COOKIE}=${bind.state}; Path=${bind.path}; Max-Age=900; HttpOnly; SameSite=Lax${bind.secure ? "; Secure" : ""}`,
116
+ "content-security-policy": "default-src 'none'; style-src 'unsafe-inline'; frame-ancestors 'none'; base-uri 'none'; form-action 'none'" });
117
+ response.end(htmlString(html `<!doctype html><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><meta http-equiv="refresh" content="0;url=${to}"><title>Toolroll</title>${HANDOFF_STYLE}<p>${words} <a href="${to}">Continue</a></p>`));
118
+ }
119
+ export function redirect(response, to) {
120
+ response.writeHead(303, { ...SAFETY, Location: to });
121
+ response.end();
122
+ }
123
+ export const taskChatHref = (taskId) => `/chat?task=${encodeURIComponent(taskId)}`;
124
+ /** A project's own lead thread (v77). */
125
+ export const projectChatHref = (repo) => `/chat?project=${encodeURIComponent(repo)}`;
126
+ export function safeReturn(raw) {
127
+ if (raw === null || raw === undefined)
128
+ return "/";
129
+ // A backslash is a slash to a browser's URL parser (`/\evil` → `//evil`), so it is refused too (v3 review, finding 10).
130
+ if (!raw.startsWith("/") || raw.startsWith("//") || raw.includes("\\") || /[\r\n\t\u0000-\u001f]/.test(raw) || raw.length > 512)
131
+ return "/";
132
+ try {
133
+ const path = decodeURIComponent(new URL(raw, "http://standing-orders.local").pathname);
134
+ if (path.startsWith("//") || path.includes("\\") || /[\u0000-\u001f\u007f]/.test(path))
135
+ return "/";
136
+ }
137
+ catch {
138
+ return "/";
139
+ }
140
+ return raw;
141
+ }
142
+ /**
143
+ * The destination a sign-in returns to (a phone's deep link to an exact
144
+ * task or result): `safeReturn`, then narrower still — never the sign-in,
145
+ * sign-up, join or sign-out roads themselves (a loop, or a token in a
146
+ * path), never an encoded second scheme or host once the browser decodes
147
+ * it, never a query key that could carry a secret, and never a region
148
+ * fetch (`?fragment=`), which is a piece of a page and not a place to
149
+ * land. Only the app's own pages qualify: the task lens, the chat, the
150
+ * work and board views, and the fixed control destinations a phone
151
+ * button can name. Nothing here is a second redirect framework: one
152
+ * same-site path, or "/".
153
+ */
154
+ export const LOGIN_RETURN_PAGES = /^\/(oauth\/authorize|t\/[^/]+|r\/[1-9]\d*|code(?:\/[a-f0-9]{32}(?:\/ship)?)?|review|chat|work|board|projects|routines|recipes|fleet|settings(\/[a-z-]+)?|mode)$/;
155
+ export function loginReturn(raw) {
156
+ const safe = safeReturn(raw);
157
+ if (safe === "/")
158
+ return "/";
159
+ let parsed;
160
+ try {
161
+ parsed = new URL(safe, "http://standing-orders.local");
162
+ }
163
+ catch {
164
+ return "/";
165
+ }
166
+ if (parsed.origin !== "http://standing-orders.local" || parsed.username !== "" || parsed.password !== "")
167
+ return "/";
168
+ let decodedPath;
169
+ try {
170
+ decodedPath = decodeURIComponent(parsed.pathname);
171
+ }
172
+ catch {
173
+ return "/";
174
+ }
175
+ if (/^\/\/|\\|[\r\n\t\u0000-\u001f]/.test(decodedPath) || /^\/(login|logout|signup|join)(\/|$)/.test(decodedPath))
176
+ return "/";
177
+ if (!LOGIN_RETURN_PAGES.test(decodedPath) || parsed.searchParams.has("fragment"))
178
+ return "/";
179
+ for (const key of [...parsed.searchParams.keys()]) {
180
+ if (/token|password|secret|csrf|code|key|auth/i.test(key))
181
+ parsed.searchParams.delete(key);
182
+ }
183
+ const query = parsed.searchParams.toString();
184
+ return `${parsed.pathname}${query === "" ? "" : `?${query}`}`;
185
+ }
186
+ /** A request path fit for a log: the one-time sign-in link's code is left out. */
187
+ export function redactedPath(path) {
188
+ return path.startsWith(SIGN_IN_LINK_PATH) ? `${SIGN_IN_LINK_PATH}…` : path;
189
+ }
190
+ /** The sign-in page that comes back to `path` afterwards, or plain /login when the path is not one to come back to. */
191
+ export function loginHref(path) {
192
+ const back = loginReturn(path);
193
+ return back === "/" ? "/login" : `/login?return=${encodeURIComponent(back)}`;
194
+ }
195
+ /** Chat actions may return only to the unified chat or one task-focused
196
+ * lens. Other same-site paths are valid elsewhere, but not for chat forms. */
197
+ export function safeChatReturn(raw) {
198
+ const safe = safeReturn(raw);
199
+ try {
200
+ const parsed = new URL(safe, "http://standing-orders.local");
201
+ if (parsed.pathname !== "/chat")
202
+ return "/chat";
203
+ const conversation = parsed.searchParams.get('conversation');
204
+ if (conversation && /^[a-f0-9-]{36}$/.test(conversation))
205
+ return `/chat?conversation=${encodeURIComponent(conversation)}`;
206
+ if (parsed.searchParams.get('private') === '1')
207
+ return '/chat?private=1';
208
+ const task = parsed.searchParams.get("task");
209
+ return task !== null && task.length > 0 && task.length <= 64 && !hasForbiddenControls(task)
210
+ ? taskChatHref(task)
211
+ : "/chat";
212
+ }
213
+ catch {
214
+ return "/chat";
215
+ }
216
+ }
217
+ export function chatReturnWithSaid(back, said) {
218
+ return `${back}${back.includes("?") ? "&" : "?"}said=${encodeURIComponent(said)}`;
219
+ }
220
+ export const chatReturnWithLatest = (back) => `${back}#latest`;
221
+ /** The one result page's address: the task and its run, never a project path
222
+ * (the page finds the result's project itself and keeps the person's own). */
223
+ export const reviewHref = (taskId, runId = null) => `/review?result=${encodeURIComponent(taskId)}${runId === null ? "" : `&run=${runId}`}`;
224
+ /** The chat's result detail (package 3): the same panel the run page
225
+ * and the cockpit render, opened beside the conversation when the screen
226
+ * has room and as a dedicated view with Back to chat when it does not. */
227
+ export const chatResultHref = (taskId, runId, tab = "summary") => sharedResultHref(taskId, runId, tab);
228
+ // ---- request plumbing ------------------------------------------------------
229
+ export async function form(request, cap = BODY_CAP) {
230
+ const chunks = [];
231
+ let size = 0;
232
+ for await (const chunk of request) {
233
+ size += chunk.length;
234
+ if (size > cap)
235
+ throw new Error("body too large");
236
+ chunks.push(chunk);
237
+ }
238
+ return new URLSearchParams(Buffer.concat(chunks).toString("utf8"));
239
+ }