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
@@ -18,7 +18,7 @@ import { assignFlowCard, commentOnFlowCard, watchFlowCard } from "../flow-people
18
18
  import { saveScript } from "../flow-scripts.js";
19
19
  import { chooseFlowCard } from "../flow-send.js";
20
20
  import { exportFlow, fetchFlowFile, FlowFileError, importFlow, parseFlowFile, planFlowImport } from "../flow-share.js";
21
- import { addFlowTriggerTo, checkFlowTriggerNow, pressFlowButton, removeFlowTrigger, removeLinearKey, setFlowTriggerOn, renewFlowHook, saveHooksBase, saveLinearKey, saveLinearSigningSecret, shareFlowButton, stopSharingFlowButton } from "../flow-triggers.js";
21
+ import { addFlowTriggerTo, checkFlowTriggerNow, pressFlowButton, removeFlowTrigger, removeLinearKey, setFlowTriggerOn, renewFlowHook, saveHooksBase, saveLinearKey, saveSigningSecret, setUpFlowHook, changeFlowHookSender, setFlowHookScope, shareFlowButton, stopSharingFlowButton, rawTakesDeliveries } from "../flow-triggers.js";
22
22
  import { FLOW_IMPORT_SCRIPT, flowFallbackHtml, flowImportHtml, flowsListHtml, flowView } from "../flows-ui.js";
23
23
  import { FLOW_TEMPLATES, FlowContractError, validateFlowDefinition, withZoneNames } from "../flows.js";
24
24
  import { prepareUnattendedCandidates, parseUnattendedAsk, revokeUnattended, signUnattended, untilWords } from "../flow-unattended.js";
@@ -43,7 +43,7 @@ import { redirect, respond, SAFETY, taskHref } from "./http.js";
43
43
  import { html } from "../html.js";
44
44
  import {} from "./session.js";
45
45
  export function createFlowsHandlers(runtime) {
46
- const { store, clock, sendScreen, chromeFor, consoleProjects, options, visible, flowRooms, identify, liveCeiling, providerHome, toolHome, restricted, evidenceRoot, authenticateApprover, projectOf, unscopedMode, ceiling, authorizeMutation } = runtime;
46
+ const { store, clock, sendScreen, chromeFor, consoleProjects, options, visible, flowRooms, identify, liveCeiling, providerHome, toolHome, restricted, evidenceRoot, authenticateApprover, authenticateCredential, projectOf, unscopedMode, ceiling, authorizeMutation } = runtime;
47
47
  // ---- reads ---------------------------------------------------------------
48
48
  /** A gallery template's page (Flows → New → Use this): its questions, the preview of these answers, and Create. */
49
49
  function sendGalleryUse(response, who, template, projects, repo, given, name, notice, said = null) {
@@ -572,7 +572,11 @@ export function createFlowsHandlers(runtime) {
572
572
  catch {
573
573
  return answer(400, { ok: false, said: "That trigger couldn't be read." });
574
574
  }
575
- const made = addFlowTriggerTo(store, flow, raw, who.name, now, dir);
575
+ // A trigger that takes deliveries mints an address: a credential act (docs/plans/passwordless.md).
576
+ const ingress = rawTakesDeliveries(raw);
577
+ if (ingress && !authenticateCredential(who, body.get("password") ?? "").ok)
578
+ return answer(403, { ok: false, said: "Enter your Toolroll password to make a webhook address." });
579
+ const made = addFlowTriggerTo(store, flow, raw, who.name, now, dir, ingress ? { ingress: "proved" } : {});
576
580
  if (made.ok)
577
581
  await prepareUnattendedCandidates(store, flow.id);
578
582
  return made.ok ? settle(made.said, made.reveal === null ? {} : { reveal: made.reveal }) : answer(400, { ok: false, said: made.message });
@@ -729,6 +733,10 @@ export function createFlowsHandlers(runtime) {
729
733
  removeFlowTrigger(store, trigger, now, dir, who.name);
730
734
  return settle("Trigger removed.");
731
735
  }
736
+ // A public form link and a new webhook address each open a way in: credential acts, a password typed now.
737
+ if ((verb === "share" || verb === "renew") && !authenticateCredential(who, body.get("password") ?? "").ok) {
738
+ return answer(403, { ok: false, said: verb === "share" ? "Enter your Toolroll password to share a form link." : "Enter your Toolroll password to make a new address." });
739
+ }
732
740
  if (verb === "share") {
733
741
  const shared = shareFlowButton(store, trigger, now, dir);
734
742
  return shared.ok ? settle(shared.said, { reveal: shared.reveal }) : answer(409, { ok: false, said: shared.message });
@@ -737,6 +745,11 @@ export function createFlowsHandlers(runtime) {
737
745
  stopSharingFlowButton(store, trigger, now);
738
746
  return settle("The form link no longer works.");
739
747
  }
748
+ // v126: a person has seen that deliveries were turned away (the banner stays until then).
749
+ if (verb === "dropped") {
750
+ store.clearFlowDeliveriesDropped(trigger.id);
751
+ return settle("Cleared.");
752
+ }
740
753
  if (verb === "press") {
741
754
  let answers;
742
755
  try {
@@ -764,11 +777,24 @@ export function createFlowsHandlers(runtime) {
764
777
  const renewed = renewFlowHook(store, trigger, now, dir);
765
778
  return renewed.ok ? settle(renewed.said, { reveal: renewed.reveal }) : answer(409, { ok: false, said: renewed.message });
766
779
  }
767
- // Linear's signing secret, pasted on this secure panel behind the password — never in chat.
768
- if (!authenticateApprover(who, body.get("password") ?? "").ok)
769
- return answer(403, { ok: false, said: "Enter your Toolroll password to save a secret." });
770
- const saved = saveLinearSigningSecret(trigger, body.get("secret") ?? "", dir);
771
- return saved.ok ? settle("Signing secret saved. Linear's deliveries can be proved now.") : answer(400, { ok: false, said: saved.message });
780
+ // A webhook's credentials change only on this secure panel, behind the password — never in chat (v126): a secret
781
+ // pasted (Linear's, Sentry's, Vercel's), one made and shown once with a new address, or who signs its deliveries.
782
+ if (!authenticateCredential(who, body.get("password") ?? "").ok)
783
+ return answer(403, { ok: false, said: verb === "secret" ? "Enter your Toolroll password to save a secret." : "Enter your Toolroll password to change how this webhook is proved." });
784
+ if (verb === "setup") {
785
+ const made = setUpFlowHook(store, trigger, now, dir);
786
+ return made.ok ? settle(made.said, { reveal: made.reveal }) : answer(409, { ok: false, said: made.message });
787
+ }
788
+ if (verb === "scope") {
789
+ const scoped = setFlowHookScope(store, trigger, { project: body.get("project"), team: body.get("team") }, who.name, now);
790
+ return scoped.ok ? settle(scoped.said) : answer(400, { ok: false, said: scoped.message });
791
+ }
792
+ if (verb === "sender") {
793
+ const changed = changeFlowHookSender(store, trigger, body.get("sender"), who.name, now, dir);
794
+ return changed.ok ? settle(changed.said) : answer(400, { ok: false, said: changed.message });
795
+ }
796
+ const saved = saveSigningSecret(store, trigger, body.get("secret") ?? "", now, dir);
797
+ return saved.ok ? settle(saved.said) : answer(400, { ok: false, said: saved.message });
772
798
  };
773
799
  const recipeAct = (step) => async (ctx) => {
774
800
  const { who, request, response, now, posted } = ctx;
@@ -924,6 +950,10 @@ export function createFlowsHandlers(runtime) {
924
950
  "flow.trigger.press": triggerAct("press"),
925
951
  "flow.trigger.renew": triggerAct("renew"),
926
952
  "flow.trigger.secret": triggerAct("secret"),
953
+ "flow.trigger.setup": triggerAct("setup"),
954
+ "flow.trigger.sender": triggerAct("sender"),
955
+ "flow.trigger.scope": triggerAct("scope"),
956
+ "flow.trigger.dropped": triggerAct("dropped"),
927
957
  "flow.trigger.share": triggerAct("share"),
928
958
  "flow.trigger.unshare": triggerAct("unshare"),
929
959
  "recipes.prepare-send": recipeAct("prepare"),
@@ -41,6 +41,7 @@ export interface GuardsRuntime {
41
41
  }
42
42
  export declare function createSharedGuards(runtime: GuardsRuntime): {
43
43
  authenticateApprover: (who: Who, token: string, repo?: string | null) => ReturnType<typeof checkApproverPassword>;
44
+ authenticateCredential: (who: Who, token: string, repo?: string | null) => ReturnType<typeof checkApproverPassword>;
44
45
  authorizeMutation: (request: IncomingMessage, who: Who, body: FormView<FormFieldOf<"mutationGuard">>) => {
45
46
  status: number;
46
47
  message: string;
@@ -6,23 +6,29 @@ import { tokenLive, tokenProjects } from "../api-tokens.js";
6
6
  import { oauthProjects, oauthTokenAllowed } from "../mcp-oauth.js";
7
7
  import { REMOTE_MESSAGES, reproveRemote } from "../operate-remote.js";
8
8
  import { canonicalProject, rowVisible } from "../project.js";
9
- import { authenticateAccount, authenticateApprover as checkApproverPassword } from "../scope.js";
9
+ import { authenticateAccount, authenticateApprover as checkApproverPassword, credentialProof } from "../scope.js";
10
10
  import { SourceBudget } from "../sign-in-guard.js";
11
11
  import {} from "../store.js";
12
12
  import { requestContext } from "./request-context.js";
13
13
  import { refuse } from "./chrome.js";
14
14
  import { matchTaskPath } from "./http.js";
15
- import { PersistentSessions, SESSION_ABSOLUTE_MS, SESSION_COOKIE, SESSION_IDLE_MS } from "./session.js";
15
+ import { PersistentSessions, SESSION_COOKIE, sessionWithinLifetime } from "./session.js";
16
16
  export function createSharedGuards(runtime) {
17
17
  // v99: wrong passwords. One address guessing across names runs out of tries here; each name
18
18
  // locks on its own (sign-in-guard.ts), and every lock, whatever road it came by, is kept.
19
19
  /**
20
- * Every step-up in the console (v100): a person the identity provider
21
- * checked in the last ten minutes confirms with that (an empty password
22
- * field); anyone else types their password. Standing (an approver, with
23
- * access to the project) is checked either way.
20
+ * Every step-up in the console: a password typed now, or (an empty field) an explicit identity-provider re-check
21
+ * made on request and not yet spent, or — inside an allowlisted act only — a remembered password (cockpit B2,
22
+ * scope.ts proveAccountNow / freshnessFor, reading the request's live facts). Standing (an approver, with access to
23
+ * the project) is checked either way.
24
24
  */
25
- // v100: the shared check honours a fresh identity-provider sign-in itself (freshIdentitySignIn, set per request below).
25
+ /** A credential act (docs/plans/passwordless.md): only a password typed now (or a one-use provider re-check) proves it. */
26
+ function authenticateCredential(who, token, repo) {
27
+ if (who.via !== "cookie")
28
+ return { ok: false, reason: "not-an-approver" };
29
+ const proved = checkApproverPassword(runtime.store, who.name, token, repo, { credential: true });
30
+ return !proved.ok || credentialProof(proved.method) ? proved : { ok: false, reason: "not-an-approver" };
31
+ }
26
32
  function authenticateApprover(who, token, repo) {
27
33
  // Re-entering a password never upgrades a machine credential into a browser ceremony.
28
34
  if (who.via !== "cookie")
@@ -332,7 +338,7 @@ export function createSharedGuards(runtime) {
332
338
  }
333
339
  for (const [id, session] of found === null ? [] : [found]) {
334
340
  const now = Date.now();
335
- if (now - session.lastSeen > SESSION_IDLE_MS || now - session.createdAt > SESSION_ABSOLUTE_MS) {
341
+ if (!sessionWithinLifetime(session, now)) {
336
342
  runtime.sessions.delete(id);
337
343
  return null;
338
344
  }
@@ -340,10 +346,18 @@ export function createSharedGuards(runtime) {
340
346
  runtime.sessions.delete(id);
341
347
  return null;
342
348
  }
349
+ // Ended in another process (or from Sessions): the cached copy is dropped, never used.
350
+ if (!runtime.sessions.stillSaved(id)) {
351
+ Map.prototype.delete.call(runtime.sessions, id);
352
+ return null;
353
+ }
343
354
  if (touch) {
344
355
  session.lastSeen = now;
345
356
  runtime.sessions.persist(session, true);
346
357
  }
358
+ // A touch that found the session ended elsewhere dropped it: this request is not signed in by it.
359
+ if (touch && !runtime.sessions.holds(session))
360
+ return null;
347
361
  return session;
348
362
  }
349
363
  return null;
@@ -356,5 +370,5 @@ export function createSharedGuards(runtime) {
356
370
  function runVisible(run) {
357
371
  return visible(taskRepoOf(run.taskRef));
358
372
  }
359
- return { authenticateApprover, authorizeMutation, projectOf, resolveRouteProject, projectRequestAllowed, identify, lookupSession, restricted, visible, admissionList, codingActorAllowed, codingProjectAllowed, workAccess, familyOf, taskRepoOf, runVisible };
373
+ return { authenticateApprover, authenticateCredential, authorizeMutation, projectOf, resolveRouteProject, projectRequestAllowed, identify, lookupSession, restricted, visible, admissionList, codingActorAllowed, codingProjectAllowed, workAccess, familyOf, taskRepoOf, runVisible };
360
374
  }
@@ -1,4 +1,5 @@
1
1
  /** HTTP answers every console route shares: server options, body limits, safe responses, redirects and return paths. */
2
+ import type { TerminalHost } from "../live-terminal.js";
2
3
  import { type Html } from "../html.js";
3
4
  import { type CliHttpOptions, type RunOperateAs } from "../cli-http.js";
4
5
  import { type CodingWorkspace } from "../coding-workspace.js";
@@ -25,6 +26,8 @@ export type ServeOptions = {
25
26
  installBin?: string;
26
27
  /** Native coding workspace injection for isolated integration tests. */
27
28
  codingWorkspace?: CodingWorkspace;
29
+ /** Live terminals (tests stand their own in; null turns them off). Default: one on a machine that runs agents. */
30
+ terminalHost?: TerminalHost | null;
28
31
  /** Read-only worker connections for heavy reads (read-executor.ts); 0 reads in-process. Default: off under tests. */
29
32
  readWorkers?: number;
30
33
  store: Store;
@@ -141,6 +144,10 @@ export type ServeOptions = {
141
144
  connectFetch?: typeof fetch;
142
145
  /** Tests: every request sign-in with the identity provider makes (discovery, keys, tokens). */
143
146
  ssoFetch?: typeof fetch;
147
+ /** Cockpit B1: this `up` console's instance secret (null: none — the instance route answers 404). */
148
+ instanceSecret?: () => string | null;
149
+ /** Cockpit E: how Finder is asked to reveal a proved checkout (tests pass their own; never a launch). True: shown. */
150
+ revealPath?: (path: string) => Promise<boolean> | boolean;
144
151
  chatEnv?: Record<string, string | undefined>;
145
152
  /**
146
153
  * The live peek's locality ASSERTION (live-peek v3 §3): the administrator
@@ -1,4 +1,3 @@
1
- /** HTTP answers every console route shares: server options, body limits, safe responses, redirects and return paths. */
2
1
  import { html, htmlString, isHtml } from "../html.js";
3
2
  import { chatResultHref as sharedResultHref } from "../chat-controls.js";
4
3
  import {} from "../cli-http.js";
@@ -117,7 +116,9 @@ export function goOutside(response, to, words, bind) {
117
116
  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
117
  }
119
118
  export function redirect(response, to) {
120
- response.writeHead(303, { ...SAFETY, Location: to });
119
+ // Set as a header (not only in writeHead) so the answer can be read back after: a refused act's redirect names it.
120
+ response.setHeader("Location", to);
121
+ response.writeHead(303, { ...SAFETY });
121
122
  response.end();
122
123
  }
123
124
  export const taskChatHref = (taskId) => `/chat?task=${encodeURIComponent(taskId)}`;
@@ -1,10 +1,32 @@
1
+ /**
2
+ * GET /live: one stream per page, joined to every room the page shows.
3
+ *
4
+ * room=workspace the reader's workspace may have changed (any meaningful write, or a lapse)
5
+ * room=task:<id> the task family changed; who else has it open
6
+ * room=flow:<id>?card=&editing= the flow changed; who else is on it, and on which card
7
+ * room=chat[?task=|?project=] the reader's own lead reply, as it runs (the only room that carries content)
8
+ * room=team[?conversation=] the team conversation's cursor moved
9
+ * room=agent:<run> one agent on the Agents board changed; while open, this person watches it (no pings)
10
+ * room=summary the reader's project states (the sidebar): each change carries the snapshot; never watching or seeing
11
+ *
12
+ * Every room is validated before anything is looked up, and admitted with
13
+ * the same checks and answers as the page it belongs to, before the stream
14
+ * opens; each is re-proved on every signal and its safety-net check, and a
15
+ * viewer who no longer passes hears `gone`. Events name their room. A page
16
+ * that falls behind gets one `reload` and the stream ends, so its
17
+ * EventSource reconnects (live-bus.ts).
18
+ */
19
+ import type { IncomingMessage } from "node:http";
1
20
  import { type LiveBus } from "../live-bus.js";
2
21
  import type { WorkspaceRevision } from "../workspace-revision.js";
3
22
  import type { ServerRuntime } from "./runtime.js";
23
+ import type { Who } from "./session.js";
4
24
  /** A page asks for a few rooms at once: its workspace, what it shows, its conversation. */
5
- export declare const LIVE_ROOMS_PER_PAGE = 5;
25
+ export declare const LIVE_ROOMS_PER_PAGE = 6;
6
26
  export type LiveRoom = {
7
27
  kind: "workspace";
28
+ } | {
29
+ kind: "summary";
8
30
  } | {
9
31
  kind: "task";
10
32
  task: string;
@@ -26,10 +48,21 @@ export type LiveRoom = {
26
48
  };
27
49
  /** A room's name, read strictly: anything else is not a room. */
28
50
  export declare function parseLiveRoom(name: string): LiveRoom | null;
51
+ /** The same session, still signed in, from the cookie and the account (never this request's context): a live room's and
52
+ * a live terminal's re-proof. Null when it was signed out, ended elsewhere, revoked, rotated or re-roled. */
53
+ export declare function rereadBrowser(runtime: Pick<ServerRuntime, "identify" | "sessions" | "store">, request: IncomingMessage, who: Who & {
54
+ via: "cookie";
55
+ }): (Who & {
56
+ via: "cookie";
57
+ }) | null;
29
58
  export declare function createLiveHandlers(runtime: ServerRuntime, live: {
30
59
  bus: LiveBus;
31
60
  workspaceRevision: WorkspaceRevision;
32
61
  chatProjectOf: (projects: string[]) => string | null | undefined;
62
+ /** One account's sidebar snapshot for one scope (serve.ts projectSummaryFor); null: gone, or the scope moved. */
63
+ projectSummaryFor: (account: string, scope: string) => string | null;
64
+ /** What one account may see now (serve.ts projectScopeFor); null: no such account. */
65
+ projectScopeFor: (account: string) => string | null;
33
66
  }): {
34
67
  registrations: import("./handler-registry.js").Registration[];
35
68
  turnStarted: (thread: number) => void;
@@ -10,8 +10,8 @@ import { respond } from "./http.js";
10
10
  import { adapterPolicy } from "./route-policy.js";
11
11
  import { requestContext } from "./request-context.js";
12
12
  /** A page asks for a few rooms at once: its workspace, what it shows, its conversation. */
13
- export const LIVE_ROOMS_PER_PAGE = 5;
14
- const ROOM_NAME = /^(workspace|task|flow|chat|team|agent)(?::([^?]{1,400}))?(?:\?([^#]{0,600}))?$/;
13
+ export const LIVE_ROOMS_PER_PAGE = 6;
14
+ const ROOM_NAME = /^(workspace|task|flow|chat|team|agent|summary)(?::([^?]{1,400}))?(?:\?([^#]{0,600}))?$/;
15
15
  /** At most one workspace nudge a second: a burst of writes is one re-read, the first heard at once. */
16
16
  const WORKSPACE_SETTLE_MS = 1_000;
17
17
  /** A running reply's preview: at most one snapshot every 80 ms; the last one always goes out. */
@@ -33,6 +33,7 @@ export function parseLiveRoom(name) {
33
33
  const only = (...allowed) => [...params.keys()].every(one => allowed.includes(one)) && allowed.every(one => params.getAll(one).length <= 1);
34
34
  switch (kind) {
35
35
  case "workspace": return key === null && query === undefined ? { kind } : null;
36
+ case "summary": return key === null && query === undefined ? { kind } : null;
36
37
  case "task": return key !== null && key !== "" && query === undefined ? { kind, task: key } : null;
37
38
  case "flow": {
38
39
  const flow = Number(key), card = params.get("card");
@@ -53,11 +54,34 @@ export function parseLiveRoom(name) {
53
54
  }
54
55
  return null;
55
56
  }
57
+ /** The same session, still signed in, from the cookie and the account (never this request's context): a live room's and
58
+ * a live terminal's re-proof. Null when it was signed out, ended elsewhere, revoked, rotated or re-roled. */
59
+ export function rereadBrowser(runtime, request, who) {
60
+ const again = runtime.identify(request, false);
61
+ if (again?.via !== "cookie" || again.name !== who.name || again.session.generation !== who.session.generation)
62
+ return null;
63
+ // The in-memory session may still exist after another process ends the saved session.
64
+ const hash = runtime.sessions.hashOf(again.session);
65
+ const saved = hash === null ? null : runtime.store.webSession(hash), account = runtime.store.accountOf(again.name);
66
+ if (saved === null || account === null || account.revokedAt !== null || account.generation !== again.session.generation
67
+ || saved.generation !== account.generation || saved.role !== account.role || again.role !== account.role)
68
+ return null;
69
+ return again;
70
+ }
56
71
  const json = (response, status, value) => respond(response, status, "application/json", JSON.stringify(value));
57
72
  export function createLiveHandlers(runtime, live) {
58
73
  const { store, identify, liveCeiling, familyOf, taskRooms, flowRooms, clock, leadPrincipal, taskChatFocus, chatScopeOf, liveTurns, team, visible } = runtime;
59
74
  // The reader's workspace: one room for everyone, nudged by any meaningful write or a clock-bound lapse.
60
75
  const workspaceRooms = createLiveRooms(() => `${live.workspaceRevision.current()}:${live.workspaceRevision.expiresAt(clock())}`, null, { bus: live.bus, minIntervalMs: WORKSPACE_SETTLE_MS });
76
+ // The reader's project states, per account AND scope: the fingerprint IS the snapshot, so each `change` carries it
77
+ // (`at`). Every delivery re-proves the viewer's scope, so a snapshot read for a wider scope (a cached one, a held
78
+ // one, one a newcomer would be handed on joining) never reaches a narrowed reader: it hears `gone` instead. Reads
79
+ // settle: a burst of writes is one read, while every signal still re-proves at once.
80
+ const summaryKey = (account, scope) => JSON.stringify([account, scope]);
81
+ const summaryRooms = createLiveRooms(key => {
82
+ const [account, scope] = JSON.parse(key);
83
+ return live.projectSummaryFor(account, scope);
84
+ }, null, { bus: live.bus, minIntervalMs: WORKSPACE_SETTLE_MS, readSettleMs: WORKSPACE_SETTLE_MS });
61
85
  // A team audience is per actor (and per token): its key carries who reads, the room keeps how to read as them.
62
86
  const teamActors = new Map();
63
87
  const teamRooms = createLiveRooms(key => {
@@ -68,19 +92,7 @@ export function createLiveHandlers(runtime, live) {
68
92
  const chatViewers = new Set();
69
93
  // One agent's open view (Agents board): what it shows, keyed by the exact run.
70
94
  const agentRooms = createLiveRooms(run => agentRunFingerprint(store, run), null, { bus: live.bus });
71
- /** The same session, still signed in, from the cookie and the account (never this request's context). */
72
- const readBrowser = (request, who) => {
73
- const again = identify(request, false);
74
- if (again?.via !== "cookie" || again.name !== who.name || again.session.generation !== who.session.generation)
75
- return null;
76
- // The in-memory session may still exist after another process ends the saved session.
77
- const hash = runtime.sessions.hashOf(again.session);
78
- const saved = hash === null ? null : store.webSession(hash), account = store.accountOf(again.name);
79
- if (saved === null || account === null || account.revokedAt !== null || account.generation !== again.session.generation
80
- || saved.generation !== account.generation || saved.role !== account.role || again.role !== account.role)
81
- return null;
82
- return again;
83
- };
95
+ const readBrowser = (request, who) => rereadBrowser(runtime, request, who);
84
96
  const sameBrowser = (request, who) => () => readBrowser(request, who) !== null;
85
97
  function admitted(viewer, thread) {
86
98
  let valid = false;
@@ -90,7 +102,7 @@ export function createLiveHandlers(runtime, live) {
90
102
  catch { /* Failed proof stops this room. */ }
91
103
  if (!valid) {
92
104
  viewer.connection.send(viewer.room, "gone");
93
- viewer.leave();
105
+ viewer.connection.refuse(() => viewer.leave());
94
106
  }
95
107
  return valid;
96
108
  }
@@ -184,7 +196,7 @@ export function createLiveHandlers(runtime, live) {
184
196
  if ([...url.searchParams.keys()].some(key => key !== "room") || names.length === 0 || names.length > LIVE_ROOMS_PER_PAGE || new Set(names).size !== names.length)
185
197
  return json(response, 400, { error: "room" });
186
198
  const rooms = names.map(name => ({ name, room: parseLiveRoom(name) }));
187
- if (rooms.some(one => one.room === null) || rooms.filter(one => one.room?.kind === "agent").length > 1 || rooms.filter(one => one.room?.kind === "workspace").length > 1 || rooms.filter(one => one.room?.kind === "chat").length > 1 || rooms.filter(one => one.room?.kind === "team").length > 1)
199
+ if (rooms.some(one => one.room === null) || rooms.filter(one => one.room?.kind === "agent").length > 1 || rooms.filter(one => one.room?.kind === "workspace").length > 1 || rooms.filter(one => one.room?.kind === "chat").length > 1 || rooms.filter(one => one.room?.kind === "team").length > 1 || rooms.filter(one => one.room?.kind === "summary").length > 1)
188
200
  return json(response, 400, { error: "room" });
189
201
  // Admit every room before the stream opens, with the answers each page's own stream gave.
190
202
  const joins = [];
@@ -195,6 +207,15 @@ export function createLiveHandlers(runtime, live) {
195
207
  const browser = who;
196
208
  joins.push(connection => workspaceRooms.join("workspace", { name: who.name, room: name, connection, valid: sameBrowser(request, browser) }));
197
209
  }
210
+ else if (room.kind === "summary") {
211
+ const browser = who;
212
+ const scope = live.projectScopeFor(who.name);
213
+ if (scope === null)
214
+ return json(response, 404, { error: "summary" });
215
+ const signedIn = sameBrowser(request, browser);
216
+ joins.push(connection => summaryRooms.join(summaryKey(who.name, scope), { name: who.name, room: name, connection,
217
+ valid: () => signedIn() && live.projectScopeFor(who.name) === scope }));
218
+ }
198
219
  else if (room.kind === "task") {
199
220
  const browser = who;
200
221
  const family = familyOf(room.task);
@@ -333,6 +354,7 @@ export function createLiveHandlers(runtime, live) {
333
354
  close() {
334
355
  detachChat();
335
356
  workspaceRooms.close();
357
+ summaryRooms.close();
336
358
  teamRooms.close();
337
359
  agentRooms.close();
338
360
  for (const viewer of [...chatViewers])
@@ -6,6 +6,9 @@ const PWA_ICON_192 = "iVBORw0KGgoAAAANSUhEUgAAAMAAAADACAIAAADdvvtQAAAB1klEQVR42u
6
6
  const PWA_ICON_512 = "iVBORw0KGgoAAAANSUhEUgAAAgAAAAIACAIAAAB7GkOtAAAJF0lEQVR42u3VwQ0AEBBFQXU4KUD/TW0R3JzcRLJhfqYCwivDzMy+XHEEZmYCYGZmAmBmZgJgZmYCYGZmAmBmZgJgZmYCYGZmAmBmZgJgZmYCYGZmAmBmZgJgZmYCYGZmAmBmZgJgZmYCYGZmAmBmZgJgZmYCYGZmAmBmZgJgZmYCYGZmAmBmJgBmZiYAZmYmAGZmJgBmZiYAZmYmAGZmJgDb1dYBOCEAAAIgAAACIAAAAiAAAAIgAAACIAAAAiAAAAIgAAACIAAAAiAAAAIgAAACIAAAAiAAAAIgAAACIAAAAiAAAAIgAAACAIAAACAAAAgAAAIAgAAAIAAAAiAAAAIgAAACIAAAAiAAAAIgAAACIAAAAiAAAAIgAAACIAAAAiAAAAIgAAACIAAAAiAAAAIgAAACIAAAAiAAAAIgAAACAIAAACAAAAgAAAIAgAAACIAAAAiAAAAIgAAACIAAAAiAAAAIgAAACIAAAAiAAAAIgAAACIAAAAiAAAAIgAAACIAAAAiAAAAIgAAACIAAAAiAAAAIAAACAIAAACAAAAgAAAIAgAAACIAAAAiAAAAIgAAACIAAAAiAAAAIgAAACIAAAAiAAAAIgAAACIAAAAiAAAAIgAAACIAAAAiAAAAIgAAACIAAAAiAAAAIAAACAIAAACAAAAgAAAIAIAACACAAAgAgAAIAIAACACAAAgAgAAIAIAACACAAAgAgAAIAIAACACAAAgAgAAIAIAACACAAAgAgAAIAIAACcF+Y5Z5HKgACIAAmAAgAAmACgAAgACYACAACYAKAACAAJgAIAAJgAoAAIAAmAAgAAmACIAACgP/FBEAABEAAzARAAARAAMwEQAAEQADMBEAABEAAzARAAARAAMwEQAAEQADMBEAABEAAzARAAARAAMwEQAAEQADMBEAABEAAzARAAARAAMwEQAAEQADMBEAABEAAzARAAARAAMwEQAAEQABMABAAARAAEwAEAAEwAUAAEAATAAQAATABQAAQABMABAABMAFAABAAEwABEAAEwARAAARAAMwEQAAEQADMBEAABEAAzARAAARAAMwEQAAEQADMBEAABEAAzARAAARAAMwEQAAEQADMBEAABEAAzARAAARAAMwEQAAEQADMBEAABEAAzARAAARAAMwEQAAEAEAABABAAAQAQAAAEAAABAAAAQBAAAAQAAAEAEAABABAAAQAQAAEAEAABABAAAQAQAAEAEAABABAAAQAQAAEAEAABABAAAQAQAAEAEAABABAAAQAQAAEAEAABABAANwcgAAAIAAACAAAAgCAAAAgAAACIAAAAiAAAAIgAAACIAAAAiAAAAIgAAACIAAAAiAAAAIgAAACIAAAAiAAAAIgAAACIAAAAiAAAAIgAAACIAAAAiAAAAIAgAAAIAAACAAAAgCAAAAgAAACIAC/C7Pc80gFQAAEwAQAAUAATAAQAATABAABQABMABAABMAEAAFAAEwAEAAEwAQAAUAATAAEQADwv5gACIAACICZAAiAAAiAmQAIgAAIgJkACIAACICZAAiAAAiAmQAIgAAIgJkACIAACICZAAiAAAiAmQAIgAAIgJkACIAACICZAAiAAAiAmQAIgAAIgJkACIAACICZAAiAAAiAmQAIgAAIgAkAAiAAAmACgAAgACYACAACYAKAACAAJgAIAAJgAoAAIAAmAAgAAmACIAACgACYAAiAAAiAmQAIgAAIgJkACIAACICZAAiAAAiAmQAIgAAIgJkACIAACICZAAiAAAiAmQAIgAAIgJkACIAACICZAAiAAAiAmQAIgAAIgJkACIAACICZAAiAAAiAmQAIgAAACIAAAAiAAAAIAAACAIAAACAAAAgAAAIAgAAACIAAAAiAAAAIgAAACIAAAAiAAAAIgAAACIAAAAiAAAAIgAAACIAAAAiAAAAIgAAACIAAAAiAAAAIgAAACIAAAAiAmwMQAAAEAAABAEAAABAAAAQAQAAEAEAABABAAAQAQAAEAEAABABAAAQAQAAEAEAABABAAAQAQAAEAEAABABAAAQAQAAEAEAABABAAAQAQAAEAEAABABAAAAQAAAEAAABAEAAABAAAASAJcxyzyMVAAEQABMABAABMAFAABAAEwAEAAEwAUAAEAATAAQAATABQAAQABMABAABMAEQAAHA/2ICIAACIABmAiAAAiAAZgIgAAIgAGYCIAACIABmAiAAAiAAZgIgAAIgAGYCIAACIABmAiAAAiAAZgIgAAIgAGYCIAACIABmAiAAAiAAZgIgAAIgAGYCIAACIABmAiAAAiAAZgIgAAIgACYACIAACIAJAAKAAJgAIAAIgAkAAoAAmAAgAAiACQACgACYACAACIAJgAAIAAJgAiAAAiAAZgIgAAIgAGYCIAACIABmAiAAAiAAZgIgAAIgAGYCIAACIABmAiAAAiAAZgIgAAIgAGYCIAACIABmAiAAAiAAZgIgAAIgAGYCIAACIABmAiAAAiAAZgIgAAIAIAACACAAAgAgAAAIAAACAIAAACAAAAgAAAIAIAACACAAAgAgAAIAIAACACAAAgAgAAIAIAACACAAAgAgAAIAIAACACAAAgAgAAIAIAACACAAAgAgAAIAIAACACAAbg5AAAAQAAAEAAABAEAAABAAAAEQAAABEAAAARAAAAEQAAABEAAAARAAAAEQAAABEAAAARAAAAEQAAABEAAAARAAAAEQAAABEAAAARAAAAEQAAABEAAAAQBAAAAQAAAEAAABAEAAABAAAAEQAAABEAAAARAAAAEQAAABEAAAARAAAAEQAAABEAAAARAAAAEQAAABEAAAARAAAAEQAAABEAAAARAAAAEQAAABcHkAAgCAAAAgAAAIAAACAIAAAAiAAAAIgAAACIAAAAiAAAAIgAAACIAAAAiAAAAIgAAACIAAAAiAAAAIgJmZPT4BMDMTADMzEwAzMxMAMzMTADMzEwAzMxMAMzMTADMzEwAzMxMAMzMTADMzEwAzMxMAMzMTADMzEwAzMxMAMzMTADMzEwAzMxMAMzMTADMzEwAzMxMAMzMTADMzEwAzMwEwMzMBMDMzATAzMwEwMzMBMDOzJzYBVJhD+Nnu218AAAAASUVORK5CYII=";
7
7
  const PWA_ICON_APPLE = "iVBORw0KGgoAAAANSUhEUgAAALQAAAC0CAIAAACyr5FlAAABqklEQVR42u3aMQ0AIAxFwepgQgD+TSECFHQiYWjv5ylobmwcs2ThBAaHwWFwGBwGh8FhcBgcBofBkW7MparBITgEh+AQHIJDcAgOwQEHHHDAAYfgEByCQ3AIDsEhOASHI8IBBxxwCA7BITgEh+AQHIJDcAgOOOCAQ3AIDsEhOASH4BAcguNPu9PggAMOOOCAAw444IADDjjggAMOOOCAAw444IADDjjggAMOOOCAAw44PPsIDsEhOASH4BAcgkNwCA444IADDjgEh+AQHIJDcAgOwSE44IDDg7EHYzjggAMOOOCAAw444IADDjjggAMOOOCAAw444IADDjjggAMOOOCAAw559hEcgkNwwAEHHHDAITgEh+AQHIJDcAgOwQEHHHDAAYfgEByCQ3B4MIYDDjjggAMOOOCAAw444IADDjjggAMOOOCAAw444IADDjjggAMOOODw7OPZBw7BITgEh+AQHIJDcAgOOOCAAw44BIfgEByCQ3AIDsEhOAQHHHDAITgEh+AQHIJDcAgOwSE44IADDjgecVjbwWFwGBwGh8FhcBgcBofBYeV3AaohX51oqNRKAAAAAElFTkSuQmCC";
8
8
  const PWA_ICON_SVG = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"><rect x="6" y="6" width="88" height="88" rx="14" fill="#1a202c"/><rect x="28" y="28" width="44" height="8" rx="4" fill="#ebebeb"/><rect x="28" y="47" width="44" height="8" rx="4" fill="#ebebeb"/><rect x="28" y="66" width="44" height="8" rx="4" fill="#ebebeb"/></svg>`;
9
+ /** The icon with the attention dot (cockpit §3): a tab whose reader has something waiting shows it. Same origin, so
10
+ * the page's img-src 'self' allows it; nothing about the reader rides the asset. */
11
+ const PWA_ICON_ATTENTION_SVG = PWA_ICON_SVG.replace("</svg>", `<circle cx="78" cy="22" r="20" fill="#bb2649" stroke="#ffffff" stroke-width="6"/></svg>`);
9
12
  /** The typefaces by route: exact names only, served pre-auth like the icons. */
10
13
  const FONT_FILES = {
11
14
  "/fonts/geist-sans-400.woff2": GEIST_SANS_400,
@@ -87,6 +90,8 @@ export function serveInstallAsset(response, pathname, respond) {
87
90
  return asset("image/png", Buffer.from(PWA_ICON_APPLE, "base64"));
88
91
  if (pathname === "/icon.svg")
89
92
  return asset("image/svg+xml", PWA_ICON_SVG);
93
+ if (pathname === "/icon-attention.svg")
94
+ return asset("image/svg+xml", PWA_ICON_ATTENTION_SVG);
90
95
  if (pathname === "/icon-192.png")
91
96
  return asset("image/png", Buffer.from(PWA_ICON_192, "base64"));
92
97
  if (pathname === "/icon-512.png")