@songsid/agend 2.1.12-beta.9 → 2.2.0-alpha.1

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 (358) hide show
  1. package/README.md +2 -2
  2. package/README.zh-TW.md +2 -2
  3. package/dist/auth/one-time-code.d.ts +19 -0
  4. package/dist/auth/one-time-code.js +49 -0
  5. package/dist/auth/one-time-code.js.map +1 -0
  6. package/dist/auth-api.d.ts +52 -0
  7. package/dist/auth-api.js +346 -0
  8. package/dist/auth-api.js.map +1 -0
  9. package/dist/backend/claude-code.d.ts +4 -0
  10. package/dist/backend/claude-code.js +97 -0
  11. package/dist/backend/claude-code.js.map +1 -1
  12. package/dist/backend/codex.js +59 -30
  13. package/dist/backend/codex.js.map +1 -1
  14. package/dist/backend/kiro-agent.d.ts +66 -0
  15. package/dist/backend/kiro-agent.js +302 -0
  16. package/dist/backend/kiro-agent.js.map +1 -0
  17. package/dist/backend/kiro-identity.d.ts +80 -0
  18. package/dist/backend/kiro-identity.js +537 -0
  19. package/dist/backend/kiro-identity.js.map +1 -0
  20. package/dist/backend/kiro-v2-store-worker.d.ts +1 -0
  21. package/dist/backend/kiro-v2-store-worker.js +87 -0
  22. package/dist/backend/kiro-v2-store-worker.js.map +1 -0
  23. package/dist/backend/kiro-v2-store.d.ts +54 -0
  24. package/dist/backend/kiro-v2-store.js +131 -0
  25. package/dist/backend/kiro-v2-store.js.map +1 -0
  26. package/dist/backend/kiro-v3-identity.d.ts +5 -0
  27. package/dist/backend/kiro-v3-identity.js +22 -0
  28. package/dist/backend/kiro-v3-identity.js.map +1 -1
  29. package/dist/backend/kiro.d.ts +83 -7
  30. package/dist/backend/kiro.js +418 -91
  31. package/dist/backend/kiro.js.map +1 -1
  32. package/dist/backend/types.d.ts +45 -3
  33. package/dist/backend/types.js.map +1 -1
  34. package/dist/channel/adapters/discord.d.ts +1 -0
  35. package/dist/channel/adapters/discord.js +29 -10
  36. package/dist/channel/adapters/discord.js.map +1 -1
  37. package/dist/channel/adapters/telegram.d.ts +1 -0
  38. package/dist/channel/adapters/telegram.js +13 -1
  39. package/dist/channel/adapters/telegram.js.map +1 -1
  40. package/dist/channel/types.d.ts +9 -1
  41. package/dist/classic-channel-manager.d.ts +18 -10
  42. package/dist/classic-channel-manager.js +45 -18
  43. package/dist/classic-channel-manager.js.map +1 -1
  44. package/dist/cli.js +225 -127
  45. package/dist/cli.js.map +1 -1
  46. package/dist/command-table.d.ts +5 -3
  47. package/dist/command-table.js +29 -27
  48. package/dist/command-table.js.map +1 -1
  49. package/dist/config-validator.js +46 -0
  50. package/dist/config-validator.js.map +1 -1
  51. package/dist/config.js +12 -0
  52. package/dist/config.js.map +1 -1
  53. package/dist/connection-secrets.d.ts +6 -0
  54. package/dist/connection-secrets.js +4 -0
  55. package/dist/connection-secrets.js.map +1 -1
  56. package/dist/daemon.d.ts +89 -4
  57. package/dist/daemon.js +661 -77
  58. package/dist/daemon.js.map +1 -1
  59. package/dist/darwin-memory.d.ts +6 -1
  60. package/dist/darwin-memory.js +17 -6
  61. package/dist/darwin-memory.js.map +1 -1
  62. package/dist/delivery-consumed-watch.d.ts +85 -0
  63. package/dist/delivery-consumed-watch.js +244 -0
  64. package/dist/delivery-consumed-watch.js.map +1 -0
  65. package/dist/delivery-outbox.d.ts +61 -0
  66. package/dist/delivery-outbox.js +120 -2
  67. package/dist/delivery-outbox.js.map +1 -1
  68. package/dist/delivery-reconciliation.d.ts +14 -5
  69. package/dist/delivery-reconciliation.js +27 -4
  70. package/dist/delivery-reconciliation.js.map +1 -1
  71. package/dist/delivery-reconciliation.test.js +35 -1
  72. package/dist/delivery-reconciliation.test.js.map +1 -1
  73. package/dist/event-loop-watch.d.ts +28 -0
  74. package/dist/event-loop-watch.js +75 -3
  75. package/dist/event-loop-watch.js.map +1 -1
  76. package/dist/fleet-context.d.ts +22 -2
  77. package/dist/fleet-level-config.d.ts +2 -2
  78. package/dist/fleet-level-config.js +6 -1
  79. package/dist/fleet-level-config.js.map +1 -1
  80. package/dist/fleet-manager.d.ts +293 -12
  81. package/dist/fleet-manager.js +2364 -449
  82. package/dist/fleet-manager.js.map +1 -1
  83. package/dist/host-memory.d.ts +4 -0
  84. package/dist/host-memory.js.map +1 -1
  85. package/dist/instance-config-impact.js +5 -0
  86. package/dist/instance-config-impact.js.map +1 -1
  87. package/dist/instance-lifecycle.d.ts +10 -5
  88. package/dist/instance-lifecycle.js +162 -53
  89. package/dist/instance-lifecycle.js.map +1 -1
  90. package/dist/locale.js +150 -16
  91. package/dist/locale.js.map +1 -1
  92. package/dist/login-controller.d.ts +2 -0
  93. package/dist/login-controller.js +17 -5
  94. package/dist/login-controller.js.map +1 -1
  95. package/dist/memory-pressure.d.ts +7 -8
  96. package/dist/memory-pressure.js +76 -15
  97. package/dist/memory-pressure.js.map +1 -1
  98. package/dist/monotonic-budget.d.ts +2 -0
  99. package/dist/monotonic-budget.js +28 -0
  100. package/dist/monotonic-budget.js.map +1 -0
  101. package/dist/needs-you-hub.d.ts +114 -0
  102. package/dist/needs-you-hub.js +370 -0
  103. package/dist/needs-you-hub.js.map +1 -0
  104. package/dist/needs-you-live.d.ts +116 -0
  105. package/dist/needs-you-live.js +228 -0
  106. package/dist/needs-you-live.js.map +1 -0
  107. package/dist/needs-you.d.ts +132 -0
  108. package/dist/needs-you.js +164 -0
  109. package/dist/needs-you.js.map +1 -0
  110. package/dist/node-version-guard.d.ts +30 -0
  111. package/dist/node-version-guard.js +50 -0
  112. package/dist/node-version-guard.js.map +1 -0
  113. package/dist/operator-control-paths.d.ts +4 -0
  114. package/dist/operator-control-paths.js +24 -0
  115. package/dist/operator-control-paths.js.map +1 -0
  116. package/dist/outbound-handlers.d.ts +2 -0
  117. package/dist/outbound-handlers.js +9 -2
  118. package/dist/outbound-handlers.js.map +1 -1
  119. package/dist/outbound-schemas.d.ts +5 -5
  120. package/dist/profile-control.js +4 -20
  121. package/dist/profile-control.js.map +1 -1
  122. package/dist/public-web-gateway.d.ts +18 -0
  123. package/dist/public-web-gateway.js +141 -0
  124. package/dist/public-web-gateway.js.map +1 -0
  125. package/dist/public-web-link.d.ts +53 -0
  126. package/dist/public-web-link.js +175 -0
  127. package/dist/public-web-link.js.map +1 -0
  128. package/dist/quickstart-api.d.ts +8 -1
  129. package/dist/quickstart-api.js +86 -48
  130. package/dist/quickstart-api.js.map +1 -1
  131. package/dist/quickstart.d.ts +1 -0
  132. package/dist/quickstart.js +6 -2
  133. package/dist/quickstart.js.map +1 -1
  134. package/dist/scheduler/db.d.ts +7 -1
  135. package/dist/scheduler/db.js +38 -0
  136. package/dist/scheduler/db.js.map +1 -1
  137. package/dist/scheduler/index.d.ts +1 -1
  138. package/dist/scheduler/retry-label.d.ts +11 -0
  139. package/dist/scheduler/retry-label.js +24 -0
  140. package/dist/scheduler/retry-label.js.map +1 -0
  141. package/dist/scheduler/scheduler.d.ts +55 -2
  142. package/dist/scheduler/scheduler.js +220 -8
  143. package/dist/scheduler/scheduler.js.map +1 -1
  144. package/dist/scheduler/types.d.ts +22 -0
  145. package/dist/scheduler/types.js.map +1 -1
  146. package/dist/secret-store.d.ts +5 -0
  147. package/dist/secret-store.js +20 -0
  148. package/dist/secret-store.js.map +1 -1
  149. package/dist/service-activation.d.ts +110 -0
  150. package/dist/service-activation.js +302 -0
  151. package/dist/service-activation.js.map +1 -0
  152. package/dist/service-installer.d.ts +7 -0
  153. package/dist/service-installer.js +7 -4
  154. package/dist/service-installer.js.map +1 -1
  155. package/dist/service-restart-selection.d.ts +6 -5
  156. package/dist/service-restart-selection.js +6 -5
  157. package/dist/service-restart-selection.js.map +1 -1
  158. package/dist/settings-api.d.ts +10 -2
  159. package/dist/settings-api.js +175 -72
  160. package/dist/settings-api.js.map +1 -1
  161. package/dist/settings-baseline.d.ts +20 -0
  162. package/dist/settings-baseline.js +104 -0
  163. package/dist/settings-baseline.js.map +1 -0
  164. package/dist/settings-change.d.ts +21 -0
  165. package/dist/settings-change.js +133 -0
  166. package/dist/settings-change.js.map +1 -0
  167. package/dist/settings-confirmation.d.ts +105 -0
  168. package/dist/settings-confirmation.js +232 -0
  169. package/dist/settings-confirmation.js.map +1 -0
  170. package/dist/settings-control.d.ts +41 -0
  171. package/dist/settings-control.js +294 -0
  172. package/dist/settings-control.js.map +1 -0
  173. package/dist/settings-effect.d.ts +24 -0
  174. package/dist/settings-effect.js +169 -0
  175. package/dist/settings-effect.js.map +1 -0
  176. package/dist/settings-http-confirmation.d.ts +29 -0
  177. package/dist/settings-http-confirmation.js +123 -0
  178. package/dist/settings-http-confirmation.js.map +1 -0
  179. package/dist/settings-instance-patch.d.ts +4 -0
  180. package/dist/settings-instance-patch.js +26 -0
  181. package/dist/settings-instance-patch.js.map +1 -0
  182. package/dist/settings-request-capability.d.ts +16 -0
  183. package/dist/settings-request-capability.js +32 -0
  184. package/dist/settings-request-capability.js.map +1 -0
  185. package/dist/settings-transaction.d.ts +65 -0
  186. package/dist/settings-transaction.js +223 -0
  187. package/dist/settings-transaction.js.map +1 -0
  188. package/dist/setup-auth.d.ts +10 -18
  189. package/dist/setup-auth.js +14 -38
  190. package/dist/setup-auth.js.map +1 -1
  191. package/dist/setup-form.d.ts +1 -1
  192. package/dist/setup-form.js +75 -18
  193. package/dist/setup-form.js.map +1 -1
  194. package/dist/setup-guide.d.ts +13 -0
  195. package/dist/setup-guide.js +25 -0
  196. package/dist/setup-guide.js.map +1 -0
  197. package/dist/setup-host.d.ts +16 -0
  198. package/dist/setup-host.js +197 -32
  199. package/dist/setup-host.js.map +1 -1
  200. package/dist/setup-wizard.js +3 -0
  201. package/dist/setup-wizard.js.map +1 -1
  202. package/dist/slash-authz.d.ts +17 -3
  203. package/dist/slash-authz.js +5 -0
  204. package/dist/slash-authz.js.map +1 -1
  205. package/dist/spawn-gate.js +3 -3
  206. package/dist/spawn-gate.js.map +1 -1
  207. package/dist/statusline-watcher.d.ts +5 -0
  208. package/dist/statusline-watcher.js +2 -0
  209. package/dist/statusline-watcher.js.map +1 -1
  210. package/dist/steer-capability.d.ts +6 -0
  211. package/dist/steer-capability.js +8 -0
  212. package/dist/steer-capability.js.map +1 -1
  213. package/dist/sync-work-attribution.d.ts +14 -0
  214. package/dist/sync-work-attribution.js +43 -8
  215. package/dist/sync-work-attribution.js.map +1 -1
  216. package/dist/tips.js +10 -5
  217. package/dist/tips.js.map +1 -1
  218. package/dist/tmux-control.d.ts +35 -5
  219. package/dist/tmux-control.js +291 -57
  220. package/dist/tmux-control.js.map +1 -1
  221. package/dist/tmux-manager.d.ts +20 -6
  222. package/dist/tmux-manager.js +90 -47
  223. package/dist/tmux-manager.js.map +1 -1
  224. package/dist/tmux-read.d.ts +59 -0
  225. package/dist/tmux-read.js +213 -0
  226. package/dist/tmux-read.js.map +1 -0
  227. package/dist/topic-commands.d.ts +21 -10
  228. package/dist/topic-commands.js +191 -58
  229. package/dist/topic-commands.js.map +1 -1
  230. package/dist/transcript-delta-reader.js +2 -1
  231. package/dist/transcript-delta-reader.js.map +1 -1
  232. package/dist/tunnel/cloudflared-install.d.ts +2 -0
  233. package/dist/tunnel/cloudflared-install.js +1 -1
  234. package/dist/tunnel/cloudflared-install.js.map +1 -1
  235. package/dist/tunnel/purpose-lane.d.ts +14 -0
  236. package/dist/tunnel/purpose-lane.js +43 -0
  237. package/dist/tunnel/purpose-lane.js.map +1 -0
  238. package/dist/turn-reply-guard.d.ts +2 -0
  239. package/dist/turn-reply-guard.js +10 -1
  240. package/dist/turn-reply-guard.js.map +1 -1
  241. package/dist/types.d.ts +33 -0
  242. package/dist/ui/app.html +18 -0
  243. package/dist/ui/chat-render.js +478 -0
  244. package/dist/ui/chat-store.js +289 -0
  245. package/dist/ui/chat-strings.js +69 -0
  246. package/dist/ui/chat-thread.js +209 -0
  247. package/dist/ui/chat-tour.js +116 -0
  248. package/dist/ui/fleet-org.js +142 -0
  249. package/dist/ui/panel-chat.js +418 -0
  250. package/dist/ui/panel-fleet.js +413 -0
  251. package/dist/ui/panel-needs.js +202 -0
  252. package/dist/ui/panel-settings.js +750 -0
  253. package/dist/ui/preview.js +266 -0
  254. package/dist/ui/settings-apply.js +239 -0
  255. package/dist/ui/settings-confirm.js +186 -0
  256. package/dist/ui/settings-dialogs.js +546 -0
  257. package/dist/ui/settings-model.js +277 -0
  258. package/dist/ui/settings-strings.js +652 -0
  259. package/dist/ui/settings-wizard.js +140 -0
  260. package/dist/ui/shared/agend-auth.js +93 -0
  261. package/dist/ui/shared/app-ctx.js +80 -0
  262. package/dist/ui/shared/app-html.js +9 -0
  263. package/dist/ui/shared/app-i18n.js +95 -0
  264. package/dist/ui/shared/app-nav.js +73 -0
  265. package/dist/ui/shared/app-needs.js +114 -0
  266. package/dist/ui/shared/app-route.js +73 -0
  267. package/dist/ui/shared/app-session.js +66 -0
  268. package/dist/ui/shared/app-shell.js +344 -0
  269. package/dist/ui/shared/app-store.js +51 -0
  270. package/dist/ui/shared/app-stream.js +201 -0
  271. package/dist/ui/shared/app.css +676 -0
  272. package/dist/ui/shared/app.js +88 -0
  273. package/dist/ui/shared/fonts/OFL.txt +92 -0
  274. package/dist/ui/shared/fonts/README.md +21 -0
  275. package/dist/ui/shared/fonts/inter.woff2 +0 -0
  276. package/dist/ui/shared/htm.module.js +1 -0
  277. package/dist/ui/shared/panel-view.js +655 -0
  278. package/dist/ui/shared/preact-hooks.module.js +2 -0
  279. package/dist/ui/shared/preact.module.js +2 -0
  280. package/dist/ui/shared/signin.js +130 -0
  281. package/dist/ui/shared/theme.js +29 -0
  282. package/dist/ui/shared/tokens.css +153 -0
  283. package/dist/ui/shared/ui-confirm.js +43 -0
  284. package/dist/ui/shared/ui-dialog.js +45 -0
  285. package/dist/ui/shared/ui-icons.js +61 -0
  286. package/dist/ui/shared/ui-menu.js +39 -0
  287. package/dist/ui/shared/ui-states.js +20 -0
  288. package/dist/ui/shared/ui-toast.js +18 -0
  289. package/dist/ui/shared/vendor/htm.LICENSE +202 -0
  290. package/dist/ui/shared/vendor/preact.LICENSE +21 -0
  291. package/dist/ui/shared/vendor/vendor.json +39 -0
  292. package/dist/ui/shared/view-strings.js +309 -0
  293. package/dist/ui/signin.html +51 -0
  294. package/dist/update-check.d.ts +6 -2
  295. package/dist/update-check.js +12 -3
  296. package/dist/update-check.js.map +1 -1
  297. package/dist/update-install.d.ts +71 -0
  298. package/dist/update-install.js +162 -0
  299. package/dist/update-install.js.map +1 -0
  300. package/dist/upgrade-notices.d.ts +17 -0
  301. package/dist/upgrade-notices.js +93 -0
  302. package/dist/upgrade-notices.js.map +1 -0
  303. package/dist/usage/statusline-usage.d.ts +2 -0
  304. package/dist/usage/statusline-usage.js +14 -10
  305. package/dist/usage/statusline-usage.js.map +1 -1
  306. package/dist/view-api.d.ts +6 -1
  307. package/dist/view-api.js +62 -51
  308. package/dist/view-api.js.map +1 -1
  309. package/dist/web-api.d.ts +71 -14
  310. package/dist/web-api.js +492 -77
  311. package/dist/web-api.js.map +1 -1
  312. package/dist/web-auth.d.ts +71 -27
  313. package/dist/web-auth.js +169 -69
  314. package/dist/web-auth.js.map +1 -1
  315. package/dist/web-body.d.ts +3 -0
  316. package/dist/web-body.js +55 -0
  317. package/dist/web-body.js.map +1 -0
  318. package/dist/web-channel-echo.d.ts +9 -0
  319. package/dist/web-channel-echo.js +55 -0
  320. package/dist/web-channel-echo.js.map +1 -0
  321. package/dist/web-chat-history.d.ts +125 -0
  322. package/dist/web-chat-history.js +184 -0
  323. package/dist/web-chat-history.js.map +1 -0
  324. package/dist/web-continuation.d.ts +7 -0
  325. package/dist/web-continuation.js +22 -0
  326. package/dist/web-continuation.js.map +1 -0
  327. package/dist/web-host-guard.d.ts +34 -0
  328. package/dist/web-host-guard.js +57 -1
  329. package/dist/web-host-guard.js.map +1 -1
  330. package/dist/web-login.d.ts +72 -0
  331. package/dist/web-login.js +132 -0
  332. package/dist/web-login.js.map +1 -0
  333. package/dist/web-org.d.ts +55 -0
  334. package/dist/web-org.js +67 -0
  335. package/dist/web-org.js.map +1 -0
  336. package/dist/web-preview.d.ts +77 -0
  337. package/dist/web-preview.js +240 -0
  338. package/dist/web-preview.js.map +1 -0
  339. package/dist/web-request-context.d.ts +10 -0
  340. package/dist/web-request-context.js +13 -0
  341. package/dist/web-request-context.js.map +1 -0
  342. package/dist/web-session.d.ts +167 -0
  343. package/dist/web-session.js +389 -0
  344. package/dist/web-session.js.map +1 -0
  345. package/dist/web-shell-routes.d.ts +59 -0
  346. package/dist/web-shell-routes.js +96 -0
  347. package/dist/web-shell-routes.js.map +1 -0
  348. package/dist/web-upload.d.ts +183 -0
  349. package/dist/web-upload.js +468 -0
  350. package/dist/web-upload.js.map +1 -0
  351. package/dist/website-tips-renderer.d.ts +2 -2
  352. package/dist/website-tips-renderer.js +3 -3
  353. package/dist/website-tips-renderer.js.map +1 -1
  354. package/package.json +8 -5
  355. package/scripts/preinstall-guard.cjs +36 -0
  356. package/dist/ui/dashboard.html +0 -774
  357. package/dist/ui/settings.html +0 -2165
  358. package/dist/ui/view.html +0 -1139
@@ -0,0 +1,240 @@
1
+ /**
2
+ * #1306: the preview listener — where an agent's HTML may run, away from the dashboard's authority.
3
+ * Design: docs/design/1306-inline-html-preview.md (r3, approved).
4
+ *
5
+ * A second loopback listener (default health_port + 1) serves one static document, `GET /frame`: a shim that
6
+ * receives HTML by postMessage only from the dashboard origins it was built with, and writes it into its own
7
+ * opaque, sandboxed document. Everything else is an empty 404. It reads no cookie, no header but Host, and logs
8
+ * nothing about a request. The dashboard decides per /ui load whether a preview origin is reachable from the
9
+ * browser's address (Host only — never X-Forwarded-Host), and the page re-checks that against location.origin.
10
+ *
11
+ * What this does NOT promise: that a preview cannot send data out (WebRTC and self-navigation are known ways
12
+ * around the restrictions — design §4.3). It promises the account boundary only (G1): the preview cannot act as
13
+ * the signed-in person.
14
+ */
15
+ import { randomBytes } from "node:crypto";
16
+ import { createServer } from "node:http";
17
+ import { allowedHostNames, hostnameOf, LOOPBACK_HOST_NAMES } from "./web-host-guard.js";
18
+ /** The most HTML a preview takes (UTF-8 bytes): the parent refuses more before sending, the shim refuses it too. */
19
+ export const PREVIEW_HTML_MAX_BYTES = 1024 * 1024;
20
+ /** The settings a fleet runs with: `web.preview`, `web.preview_port` (default health_port + 1), `web.preview_origin`. */
21
+ export function previewSettings(web, healthPort) {
22
+ // An ephemeral web listener (port 0: tests, harnesses) gets an ephemeral preview listener; its real port is filled
23
+ // in once it listens (createPreviewListener). Otherwise the fixed default, so an SSH user can forward it too.
24
+ const port = typeof web?.preview_port === "number" && Number.isInteger(web.preview_port) ? web.preview_port
25
+ : healthPort === 0 ? 0 : healthPort + 1 <= 65535 ? healthPort + 1 : null;
26
+ const origin = typeof web?.preview_origin === "string" ? normalizeOrigin(web.preview_origin) : null;
27
+ return { enabled: web?.preview !== false, port, origin };
28
+ }
29
+ /** scheme://host[:port] for an http(s) URL with nothing else in it; null otherwise. A default port is dropped. */
30
+ export function normalizeOrigin(value) {
31
+ let u;
32
+ try {
33
+ u = new URL(value.trim());
34
+ }
35
+ catch {
36
+ return null;
37
+ }
38
+ if (u.protocol !== "http:" && u.protocol !== "https:")
39
+ return null;
40
+ if (u.username || u.password || u.search || u.hash || (u.pathname !== "/" && u.pathname !== ""))
41
+ return null;
42
+ if (!u.hostname || u.hostname.includes("*"))
43
+ return null;
44
+ // The raw text must not carry a query/fragment marker either ("https://x/?" parses to an empty search).
45
+ if (/[?#]/.test(value))
46
+ return null;
47
+ return u.origin;
48
+ }
49
+ /**
50
+ * Why a `web.preview_origin` cannot be used, or null when it can (design §3.1). It must be a bare http(s) origin
51
+ * (no path, query, credentials or wildcard), and its host must not be one the dashboard answers to — that would be
52
+ * a same-origin preview by another name.
53
+ */
54
+ export function previewOriginProblem(value, config) {
55
+ if (typeof value !== "string")
56
+ return "must be an origin such as https://preview.example.net";
57
+ const origin = normalizeOrigin(value);
58
+ if (!origin)
59
+ return "must be a bare http(s) origin — scheme, host and optional port; no path, query, fragment, credentials or wildcard";
60
+ const host = hostnameOf(new URL(origin).host);
61
+ if (!host || allowedHostNames(config).has(host))
62
+ return "must be a separate host name, not one the dashboard answers to (loopback names, hostname, web.allowed_hosts)";
63
+ return null;
64
+ }
65
+ const DEFAULT_PORT = { "http:": "80", "https:": "443" };
66
+ /** scheme://host[:port] from a Host header the host guard already accepted; a default port is dropped. */
67
+ export function dashboardOriginFor(hostHeader, secure) {
68
+ if (typeof hostHeader !== "string" || hostnameOf(hostHeader) === null)
69
+ return null;
70
+ const scheme = secure ? "https:" : "http:";
71
+ try {
72
+ return new URL(`${scheme}//${hostHeader.trim().toLowerCase()}`).origin;
73
+ }
74
+ catch {
75
+ return null;
76
+ }
77
+ // (URL drops a default port: http://x:80 → http://x, so it matches what the browser reports as location.origin.)
78
+ }
79
+ /**
80
+ * For one /ui request: a loopback Host and no preview_origin → the same loopback name on the preview port;
81
+ * preview_origin set → that; anything else (a tunnel, a proxy, `hostname`, `allowed_hosts`) → disabled.
82
+ * Only Host is read; X-Forwarded-Host never is.
83
+ */
84
+ export function previewAvailability(settings, hostHeader, secure, accepted) {
85
+ const dashboardOrigin = dashboardOriginFor(hostHeader, secure);
86
+ const off = (reason) => ({ dashboardOrigin, previewOrigin: null, reason });
87
+ if (!settings || !settings.enabled)
88
+ return off("Previews are turned off for this fleet (web.preview: false).");
89
+ if (!dashboardOrigin)
90
+ return off("This address cannot be checked.");
91
+ if (settings.port === null && !settings.origin)
92
+ return off("Previews have no port: set web.preview_port (health_port is the highest port).");
93
+ // The page must be at an origin the shim takes HTML from (and that may frame it) — exactly, or a frame would never
94
+ // render. A name in web.allowed_hosts served on a non-default port must be listed with that port.
95
+ if (accepted && !accepted.includes(dashboardOrigin))
96
+ return off(`Previews are not offered at ${dashboardOrigin}: list this address (with its port) in web.allowed_hosts, and set web.preview_origin.`);
97
+ let preview;
98
+ if (settings.origin)
99
+ preview = settings.origin;
100
+ else {
101
+ const host = hostnameOf(hostHeader);
102
+ if (!LOOPBACK_HOST_NAMES.includes(host))
103
+ return off("Previews need web.preview_origin when the dashboard is reached through a tunnel or proxy.");
104
+ preview = `http://${host}:${settings.port}`;
105
+ }
106
+ // An https page cannot frame an http preview (mixed content).
107
+ if (dashboardOrigin.startsWith("https:") && preview.startsWith("http:"))
108
+ return off("Previews need an https web.preview_origin when the dashboard is reached over https.");
109
+ if (preview === dashboardOrigin)
110
+ return off("The preview origin cannot be the dashboard's own.");
111
+ return { dashboardOrigin, previewOrigin: preview, reason: null };
112
+ }
113
+ /**
114
+ * The exact dashboard origins a preview may be framed by, and accept HTML from (design §5.1): the loopback names on
115
+ * the dashboard's port, plus — when a preview_origin is set — `https://<name>` for each name the dashboard answers to
116
+ * besides loopback (`hostname`, `web.allowed_hosts`). Never 'self', never *.
117
+ */
118
+ export function dashboardOrigins(settings, healthPort, config) {
119
+ // As the browser writes location.origin: a default port is dropped (http://127.0.0.1:80 → http://127.0.0.1).
120
+ const origin = (u) => { try {
121
+ return new URL(u).origin;
122
+ }
123
+ catch {
124
+ return null;
125
+ } };
126
+ const out = LOOPBACK_HOST_NAMES.map(h => origin(`http://${h}:${healthPort}`)).filter((o) => !!o);
127
+ if (settings.origin) {
128
+ // Each extra dashboard name as written — with its port when it has one (fleet.example.net:8443).
129
+ const extra = [config?.hostname, ...(Array.isArray(config?.web?.allowed_hosts) ? config.web.allowed_hosts : [])];
130
+ for (const entry of extra) {
131
+ if (typeof entry !== "string")
132
+ continue;
133
+ const name = hostnameOf(entry);
134
+ if (!name || LOOPBACK_HOST_NAMES.includes(name))
135
+ continue;
136
+ const o = origin(`https://${entry.trim().toLowerCase()}`);
137
+ if (o)
138
+ out.push(o);
139
+ }
140
+ }
141
+ return [...new Set(out)];
142
+ }
143
+ // ── The listener ────────────────────────────────────────────────────────────────────────────────────────────
144
+ const PERMISSIONS_POLICY = "camera=(), microphone=(), geolocation=(), clipboard-read=(), clipboard-write=(), usb=(), serial=(), hid=(), bluetooth=(), payment=(), display-capture=(), fullscreen=()";
145
+ /** The /frame response's Content-Security-Policy (design §5.1): exact, no 'self', no *. */
146
+ export function previewFrameCsp(ancestors) {
147
+ return [
148
+ "default-src 'none'", "script-src 'unsafe-inline'", "style-src 'unsafe-inline'",
149
+ "img-src data: blob:", "font-src data:", "media-src data: blob:",
150
+ "connect-src 'none'", "worker-src 'none'", "frame-src 'none'", "child-src 'none'",
151
+ "form-action 'none'", "base-uri 'none'", "manifest-src 'none'", "object-src 'none'",
152
+ "webrtc 'block'",
153
+ `frame-ancestors ${ancestors.join(" ")}`,
154
+ "sandbox allow-scripts",
155
+ ].join("; ");
156
+ }
157
+ /**
158
+ * The shim (design §4): posts `ready` with the listener's boot id; takes ONE `render` — from its parent window, from
159
+ * an exact listed dashboard origin (never "null"), with exactly {v, type, ch, html}, html ≤ 1 MiB — removes the WebRTC
160
+ * entry points from its realm (defence in depth only), and writes a heartbeat/height prologue and the HTML into its
161
+ * own document. Every later message is ignored.
162
+ */
163
+ export function buildShim(bootId, origins) {
164
+ const script = `(function () {
165
+ "use strict";
166
+ var BOOT = ${JSON.stringify(bootId)}, ORIGINS = ${JSON.stringify([...origins])}, MAX = ${PREVIEW_HTML_MAX_BYTES};
167
+ var done = false;
168
+ function bytes(s) { try { return new TextEncoder().encode(s).length; } catch (e) { return s.length * 3; } }
169
+ window.addEventListener("message", function (e) {
170
+ if (done || e.source !== window.parent || ORIGINS.indexOf(e.origin) < 0) return;
171
+ var d = e.data;
172
+ if (!d || typeof d !== "object" || Object.getPrototypeOf(d) !== Object.prototype) return;
173
+ if (Object.keys(d).sort().join(",") !== "ch,html,type,v" || d.v !== 1 || d.type !== "render") return;
174
+ if (typeof d.ch !== "string" || !/^[0-9a-f]{16,64}$/.test(d.ch) || typeof d.html !== "string" || bytes(d.html) > MAX) return;
175
+ done = true;
176
+ ["RTCPeerConnection", "webkitRTCPeerConnection", "RTCDataChannel", "RTCIceTransport", "RTCSctpTransport"].forEach(function (k) {
177
+ try { delete window[k]; } catch (x) { /* not configurable */ }
178
+ try { if (window[k]) Object.defineProperty(window, k, { value: undefined, configurable: false, writable: false }); } catch (x) { /* best effort */ }
179
+ });
180
+ var prologue = "<script>(function(){var ch=" + JSON.stringify(d.ch) + ",p=window.parent,last=-1;" +
181
+ "function size(){var h=Math.ceil(Math.max(document.documentElement?document.documentElement.scrollHeight:0,document.body?document.body.scrollHeight:0));" +
182
+ "if(h!==last){last=h;p.postMessage({v:1,type:\\"resize\\",ch:ch,height:h},\\"*\\");}}" +
183
+ "setInterval(function(){p.postMessage({v:1,type:\\"heartbeat\\",ch:ch},\\"*\\");},2000);" +
184
+ "p.postMessage({v:1,type:\\"heartbeat\\",ch:ch},\\"*\\");" +
185
+ "try{new ResizeObserver(size).observe(document.documentElement);}catch(e){setInterval(size,500);}" +
186
+ "addEventListener(\\"load\\",size);setTimeout(size,0);})();<\\/script>";
187
+ document.open();
188
+ document.write(prologue + d.html);
189
+ document.close();
190
+ });
191
+ window.parent.postMessage({ v: 1, type: "ready", ch: null, boot: BOOT }, "*");
192
+ })();`;
193
+ return `<!doctype html><html><head><meta charset="utf-8"><title>Preview</title></head><body><script>${script}</script></body></html>`;
194
+ }
195
+ /**
196
+ * The preview listener: GET /frame (no query) → the shim with the §5.1 headers; anything else → empty 404; a Host
197
+ * it does not answer to → empty 403. No X-Frame-Options (it must be framable by the dashboard), no cookie or
198
+ * header parsing beyond Host, nothing logged per request. Built once: the shim and its headers never change.
199
+ */
200
+ export function createPreviewListener(opts) {
201
+ const bootId = opts.bootId ?? randomBytes(16).toString("hex");
202
+ const origins = dashboardOrigins(opts.settings, opts.healthPort, opts.config);
203
+ const body = Buffer.from(buildShim(bootId, origins), "utf8");
204
+ const headers = {
205
+ "Content-Type": "text/html; charset=utf-8",
206
+ "Content-Security-Policy": previewFrameCsp(origins),
207
+ "Permissions-Policy": PERMISSIONS_POLICY,
208
+ "Referrer-Policy": "no-referrer",
209
+ "Cross-Origin-Resource-Policy": "same-origin",
210
+ "X-Content-Type-Options": "nosniff",
211
+ "Cache-Control": "no-store",
212
+ "Content-Length": String(body.length),
213
+ };
214
+ const hosts = new Set(LOOPBACK_HOST_NAMES);
215
+ if (opts.settings.origin) {
216
+ const h = hostnameOf(new URL(opts.settings.origin).host);
217
+ if (h)
218
+ hosts.add(h);
219
+ }
220
+ const empty = (res, status) => {
221
+ res.writeHead(status, { "Content-Length": "0", "Cache-Control": "no-store", "X-Content-Type-Options": "nosniff", "Referrer-Policy": "no-referrer" });
222
+ res.end();
223
+ };
224
+ const server = createServer((req, res) => {
225
+ // Host only. The cookie a same-host port receives (cookies are not port-isolated) is never read.
226
+ const host = typeof req.headers.host === "string" ? hostnameOf(req.headers.host) : null;
227
+ if (!host || !hosts.has(host))
228
+ return empty(res, 403);
229
+ if (req.method !== "GET" || req.url !== "/frame")
230
+ return empty(res, 404);
231
+ res.writeHead(200, headers);
232
+ res.end(body);
233
+ });
234
+ const settings = { ...opts.settings };
235
+ // A listener on port 0 learns its real port when it binds: that is the one a /ui load must be offered.
236
+ server.on("listening", () => { const a = server.address(); if (a && typeof a === "object")
237
+ settings.port = a.port; });
238
+ return { server, bootId, settings, origins, close: () => { server.close(); } };
239
+ }
240
+ //# sourceMappingURL=web-preview.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"web-preview.js","sourceRoot":"","sources":["../src/web-preview.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,10 @@
1
+ /** Listener-owned provenance. HTTP headers can never select a public surface. */
2
+ export interface GatewayRequestContext {
3
+ readonly surface: "gateway";
4
+ readonly exposureId: string;
5
+ readonly expectedOrigin: string;
6
+ readonly isCurrent: () => boolean;
7
+ }
8
+ export declare function bindGatewayRequest(req: object, context: GatewayRequestContext): void;
9
+ export declare function gatewayRequestContext(req: object): GatewayRequestContext | undefined;
10
+ export declare function isWebRequestCurrent(req: object): boolean;
@@ -0,0 +1,13 @@
1
+ const contexts = new WeakMap();
2
+ export function bindGatewayRequest(req, context) {
3
+ if (contexts.has(req))
4
+ throw new Error("request context already bound");
5
+ contexts.set(req, Object.freeze({ ...context }));
6
+ }
7
+ export function gatewayRequestContext(req) {
8
+ return contexts.get(req);
9
+ }
10
+ export function isWebRequestCurrent(req) {
11
+ return contexts.get(req)?.isCurrent() ?? true;
12
+ }
13
+ //# sourceMappingURL=web-request-context.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"web-request-context.js","sourceRoot":"","sources":["../src/web-request-context.ts"],"names":[],"mappings":"AAOA,MAAM,QAAQ,GAAG,IAAI,OAAO,EAAiC,CAAC;AAC9D,MAAM,UAAU,kBAAkB,CAAC,GAAW,EAAE,OAA8B;IAC5E,IAAI,QAAQ,CAAC,GAAG,CAAC,GAAG,CAAC;QAAE,MAAM,IAAI,KAAK,CAAC,+BAA+B,CAAC,CAAC;IACxE,QAAQ,CAAC,GAAG,CAAC,GAAG,EAAE,MAAM,CAAC,MAAM,CAAC,EAAE,GAAG,OAAO,EAAE,CAAC,CAAC,CAAC;AACnD,CAAC;AACD,MAAM,UAAU,qBAAqB,CAAC,GAAW;IAC/C,OAAO,QAAQ,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;AAC3B,CAAC;AACD,MAAM,UAAU,mBAAmB,CAAC,GAAW;IAC7C,OAAO,QAAQ,CAAC,GAAG,CAAC,GAAG,CAAC,EAAE,SAAS,EAAE,IAAI,IAAI,CAAC;AAChD,CAAC"}
@@ -0,0 +1,167 @@
1
+ import { renameSync, unlinkSync, writeFileSync } from "node:fs";
2
+ export type SessionSurface = "local" | "gateway";
3
+ /** `admin` may do everything the panel can; `read` is reserved for the read-only tier (design D6). */
4
+ export type SessionTier = "admin" | "read";
5
+ export interface SessionPolicy {
6
+ /** Hard cap from creation; activity never extends it. */
7
+ readonly absoluteMs: number;
8
+ /** Slides with activity, never past the absolute cap. */
9
+ readonly idleMs: number;
10
+ }
11
+ /** Design D4. The gateway numbers are shorter because a session there is reachable from the internet. */
12
+ export declare const DEFAULT_SESSION_POLICY: Readonly<Record<SessionSurface, SessionPolicy>>;
13
+ /** More than this and the least recently used one is dropped. */
14
+ export declare const MAX_WEB_SESSIONS = 8;
15
+ /** `lastSeen` is only written to disk this often; creation and revocation always write. */
16
+ export declare const SESSION_PERSIST_DEBOUNCE_MS: number;
17
+ export interface SessionRecord {
18
+ /** Opaque, safe to show and to name in a revoke request. Not the id and not its hash. */
19
+ readonly handle: string;
20
+ readonly idHash: string;
21
+ readonly created: number;
22
+ lastSeen: number;
23
+ readonly absoluteExpiry: number;
24
+ idleExpiry: number;
25
+ readonly tier: SessionTier;
26
+ readonly surface: SessionSurface;
27
+ readonly exposureId?: string;
28
+ /** What the person sees in the device list. Display only, never an input to a decision. */
29
+ readonly label: string;
30
+ /** `sha256(web.token)` when the session was made; a mismatch means the token was rotated. */
31
+ readonly tokenEpoch: string;
32
+ }
33
+ export interface SessionSummary {
34
+ readonly handle: string;
35
+ readonly created: number;
36
+ readonly lastSeen: number;
37
+ readonly absoluteExpiry: number;
38
+ readonly idleExpiry: number;
39
+ readonly tier: SessionTier;
40
+ readonly surface: SessionSurface;
41
+ readonly label: string;
42
+ readonly current: boolean;
43
+ }
44
+ export declare function sessionIdHash(sessionId: string): string;
45
+ /** Binds a session to the `web.token` it was issued under. */
46
+ export declare function tokenEpoch(webToken: string): string;
47
+ /**
48
+ * The value a page sends back in `X-Agend-CSRF`.
49
+ *
50
+ * Derived from the session id so it needs no storage, and different from the
51
+ * cookie value so it can be handed to script that must never see the cookie.
52
+ */
53
+ export declare function csrfTokenFor(sessionId: string): string;
54
+ export declare function isWellFormedSessionId(value: string): boolean;
55
+ /** Printable, bounded, and never empty: a label is drawn in a page by other people's code. */
56
+ export declare function sanitizeLabel(raw: string): string;
57
+ /** "Chrome on macOS" out of a User-Agent, coarse on purpose — it is a hint for a person, not a fingerprint. */
58
+ export declare function labelFromUserAgent(userAgent: string | undefined): string;
59
+ /** The three file operations the store makes, so a test can make one of them fail. */
60
+ export interface SessionFileOps {
61
+ writeFileSync: typeof writeFileSync;
62
+ renameSync: typeof renameSync;
63
+ unlinkSync: typeof unlinkSync;
64
+ }
65
+ export interface WebSessionStoreOptions {
66
+ /** Where `web-sessions.json` lives. Omit for a memory-only store (tests). */
67
+ readonly dataDir?: string;
68
+ /** Override individual file operations (tests only). */
69
+ readonly fileOps?: Partial<SessionFileOps>;
70
+ readonly now?: () => number;
71
+ readonly policy?: Partial<Record<SessionSurface, SessionPolicy>>;
72
+ readonly maxSessions?: number;
73
+ readonly onWarn?: (message: string) => void;
74
+ }
75
+ export declare class WebSessionStore {
76
+ private readonly pending;
77
+ private readonly byHash;
78
+ private readonly now;
79
+ private readonly policy;
80
+ private readonly maxSessions;
81
+ private readonly path;
82
+ private readonly warn;
83
+ private dirty;
84
+ private lastPersist;
85
+ private readonly ops;
86
+ /** The sessions the file on disk holds right now, as far as this process knows. */
87
+ private persisted;
88
+ constructor(opts?: WebSessionStoreOptions);
89
+ get size(): number;
90
+ /** How long a session on this surface may sit unused before it ends. For showing people, not for deciding. */
91
+ idleWindowMs(surface: SessionSurface): number;
92
+ /**
93
+ * Make a session. The id is returned once and never stored; the caller puts it
94
+ * in a cookie and forgets it.
95
+ */
96
+ create(input: {
97
+ tier: SessionTier;
98
+ surface: SessionSurface;
99
+ label: string;
100
+ tokenEpoch: string;
101
+ exposureId?: string;
102
+ pending?: boolean;
103
+ }): {
104
+ sessionId: string;
105
+ record: SessionRecord;
106
+ };
107
+ /**
108
+ * The session this id belongs to, if it is still good.
109
+ *
110
+ * Expiry and a rotated `web.token` are decided here, on the server, on every
111
+ * call — nothing is taken on the cookie's word. An expired or stale record is
112
+ * deleted as it is found so it cannot be revived by a clock or config change.
113
+ *
114
+ * `touch: false` is for a stream that re-checks itself on a timer: counting
115
+ * that as activity would keep an idle session alive for as long as a tab is
116
+ * open.
117
+ */
118
+ authenticate(sessionId: string | undefined, currentEpoch: string, opts?: {
119
+ touch?: boolean;
120
+ surface?: SessionSurface;
121
+ exposureId?: string;
122
+ }): SessionRecord | null;
123
+ /** Recheck a retained server record without retaining/replaying its bearer cookie. */
124
+ isCurrent(record: SessionRecord, currentEpoch: string): boolean;
125
+ /** A candidate has no usable credential until the public notice and final fence passed. */
126
+ activate(sessionId: string): boolean;
127
+ revokeExposure(exposureId: string): {
128
+ count: number;
129
+ durable: boolean;
130
+ };
131
+ /** Every live session, newest activity first. `currentIdHash` marks the caller's own. */
132
+ list(currentIdHash?: string): SessionSummary[];
133
+ revokeByHandle(handle: string): {
134
+ found: boolean;
135
+ durable: boolean;
136
+ };
137
+ revokeById(sessionId: string): {
138
+ removed: boolean;
139
+ durable: boolean;
140
+ };
141
+ /** Every session: how many there were, and whether the revocation is durable. */
142
+ revokeAll(): {
143
+ count: number;
144
+ durable: boolean;
145
+ };
146
+ /** Write anything pending. Called on shutdown. */
147
+ flush(): void;
148
+ private purgeExpired;
149
+ private evictLeastRecentlyUsed;
150
+ private persistIfDue;
151
+ /**
152
+ * Write the store. Returns whether it is DURABLE: whether a restart is now unable to bring back a
153
+ * session memory has dropped — true when the file was replaced, when the old file held nothing memory
154
+ * dropped (it is merely behind), or when it was removed; false only when a stale file is still there.
155
+ *
156
+ * The failure path matters more than the success path. Memory forgets a revoked
157
+ * session at once; if the file cannot be replaced it still holds that session, and a
158
+ * restart would read it back — a revoked session, alive again. So when a write fails:
159
+ * the store stays owing the disk a write (`dirty`, retried by the next change, the
160
+ * next debounce and shutdown), and if the old file holds anything memory has since
161
+ * dropped, the old file is removed. A restart then finds no sessions and everyone
162
+ * signs in again, which is the failure that is safe; it says so, loudly, when even
163
+ * that removal is impossible.
164
+ */
165
+ private persistNow;
166
+ private load;
167
+ }