@crouton-kit/crouter 0.3.31 → 0.3.33

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 (443) hide show
  1. package/README.md +8 -0
  2. package/dist/build-root.js +1 -0
  3. package/dist/builtin-memory/crouter-development/marketplaces.md +2 -2
  4. package/dist/builtin-memory/crouter-development/personas/base-prompt.md +2 -2
  5. package/dist/builtin-memory/crouter-development/personas/orchestrator-prompt.md +5 -5
  6. package/dist/builtin-memory/crouter-development/personas.md +3 -4
  7. package/dist/builtin-memory/crouter-development/plugins.md +18 -18
  8. package/dist/builtin-memory/design.md +4 -1
  9. package/dist/builtin-memory/development.md +4 -1
  10. package/dist/builtin-memory/internal/INDEX.md +1 -1
  11. package/dist/builtin-memory/internal/nodes-and-canvas.md +6 -6
  12. package/dist/builtin-memory/internal/storage-tiers.md +5 -5
  13. package/dist/builtin-memory/planning.md +4 -1
  14. package/dist/builtin-memory/product.md +80 -0
  15. package/dist/builtin-memory/spec.md +4 -1
  16. package/dist/builtin-personas/advisor/PERSONA.md +10 -0
  17. package/dist/builtin-personas/design/PERSONA.md +2 -2
  18. package/dist/builtin-personas/design/orchestrator.md +3 -3
  19. package/dist/builtin-personas/developer/PERSONA.md +1 -1
  20. package/dist/builtin-personas/developer/orchestrator.md +2 -2
  21. package/dist/builtin-personas/explore/PERSONA.md +3 -3
  22. package/dist/builtin-personas/explore/orchestrator.md +4 -2
  23. package/dist/builtin-personas/general/PERSONA.md +1 -1
  24. package/dist/builtin-personas/general/orchestrator.md +1 -1
  25. package/dist/builtin-personas/orchestration-kernel.md +7 -14
  26. package/dist/builtin-personas/plan/PERSONA.md +1 -1
  27. package/dist/builtin-personas/plan/orchestrator.md +2 -2
  28. package/dist/builtin-personas/plan/reviewers/architecture-fit/PERSONA.md +1 -1
  29. package/dist/builtin-personas/plan/reviewers/code-smells/PERSONA.md +1 -1
  30. package/dist/builtin-personas/plan/reviewers/pattern-consistency/PERSONA.md +1 -1
  31. package/dist/builtin-personas/plan/reviewers/requirements-coverage/PERSONA.md +1 -1
  32. package/dist/builtin-personas/plan/reviewers/security/PERSONA.md +1 -1
  33. package/dist/builtin-personas/product/PERSONA.md +18 -0
  34. package/dist/builtin-personas/product/orchestrator.md +14 -0
  35. package/dist/builtin-personas/product/teardown/PERSONA.md +13 -0
  36. package/dist/builtin-personas/review/PERSONA.md +1 -1
  37. package/dist/builtin-personas/review/orchestrator.md +1 -1
  38. package/dist/builtin-personas/runtime-base.md +5 -0
  39. package/dist/builtin-personas/spec/PERSONA.md +1 -1
  40. package/dist/builtin-personas/spec/orchestrator.md +2 -2
  41. package/dist/builtin-personas/spec/requirements/PERSONA.md +1 -1
  42. package/dist/builtin-views/canvas/core.mjs +82 -1
  43. package/dist/builtin-views/canvas/tui.mjs +9 -5
  44. package/dist/builtin-views/canvas/web.jsx +3 -2
  45. package/dist/builtin-views/chat/core.mjs +725 -0
  46. package/dist/builtin-views/chat/text.mjs +101 -0
  47. package/dist/builtin-views/chat/tui.mjs +368 -0
  48. package/dist/builtin-views/chat/web.jsx +367 -0
  49. package/dist/builtin-views/prompt-review/core.mjs +863 -0
  50. package/dist/builtin-views/prompt-review/text.mjs +15 -0
  51. package/dist/builtin-views/prompt-review/tui.mjs +196 -0
  52. package/dist/builtin-views/prompt-review/web.jsx +484 -0
  53. package/dist/builtin-views/settings/core.mjs +397 -0
  54. package/dist/builtin-views/settings/text.mjs +40 -0
  55. package/dist/builtin-views/settings/tui.mjs +95 -0
  56. package/dist/builtin-views/settings/web.jsx +167 -0
  57. package/dist/cli.js +16 -3
  58. package/dist/clients/attach/__tests__/autocomplete-and-bash-mode.test.js +96 -0
  59. package/dist/clients/attach/__tests__/bash-bang-routing.test.js +58 -0
  60. package/dist/clients/attach/__tests__/bundle-pi-tui-dedup.test.d.ts +1 -0
  61. package/dist/clients/attach/__tests__/bundle-pi-tui-dedup.test.js +56 -0
  62. package/dist/clients/attach/__tests__/titled-editor.test.js +22 -1
  63. package/dist/clients/attach/attach-cmd.d.ts +18 -0
  64. package/dist/clients/attach/attach-cmd.js +1723 -795
  65. package/dist/clients/attach/canvas-panels.js +2 -2
  66. package/dist/clients/attach/chat-view.d.ts +10 -0
  67. package/dist/clients/attach/chat-view.js +23 -0
  68. package/dist/clients/attach/config-load.d.ts +8 -4
  69. package/dist/clients/attach/config-load.js +2 -0
  70. package/dist/clients/attach/extension-dialogs.d.ts +2 -3
  71. package/dist/clients/attach/extension-dialogs.js +7 -9
  72. package/dist/clients/attach/graph-overlay.js +3 -2
  73. package/dist/clients/attach/input-controller.d.ts +4 -3
  74. package/dist/clients/attach/input-controller.js +24 -3
  75. package/dist/clients/attach/slash-commands.d.ts +7 -7
  76. package/dist/clients/attach/slash-commands.js +38 -14
  77. package/dist/clients/attach/titled-editor.js +18 -14
  78. package/dist/clients/attach/view-socket.d.ts +2 -1
  79. package/dist/clients/attach/view-socket.js +27 -8
  80. package/dist/clients/web/__tests__/source-cache.test.d.ts +1 -0
  81. package/dist/clients/web/__tests__/source-cache.test.js +32 -0
  82. package/dist/clients/web/dev-server.js +1 -0
  83. package/dist/clients/web/events.js +9 -11
  84. package/dist/clients/web/server.d.ts +4 -3
  85. package/dist/clients/web/server.js +138 -31
  86. package/dist/clients/web/source-cache.d.ts +10 -0
  87. package/dist/clients/web/source-cache.js +57 -0
  88. package/dist/clients/web/web-cmd.js +28 -9
  89. package/dist/commands/__tests__/human.test.js +30 -28
  90. package/dist/commands/canvas-history/search.js +1 -1
  91. package/dist/commands/canvas-history.js +5 -8
  92. package/dist/commands/canvas-issue.d.ts +2 -0
  93. package/dist/commands/canvas-issue.js +148 -0
  94. package/dist/commands/canvas-prune.js +19 -0
  95. package/dist/commands/canvas-rebuild-index.d.ts +2 -0
  96. package/dist/commands/canvas-rebuild-index.js +57 -0
  97. package/dist/commands/canvas-snapshot.d.ts +2 -0
  98. package/dist/commands/canvas-snapshot.js +48 -0
  99. package/dist/commands/canvas-tmux-spread.d.ts +2 -0
  100. package/dist/commands/canvas-tmux-spread.js +188 -0
  101. package/dist/commands/canvas.d.ts +1 -0
  102. package/dist/commands/canvas.js +16 -1
  103. package/dist/commands/chord.js +143 -48
  104. package/dist/commands/daemon.js +3 -3
  105. package/dist/commands/human/prompts.d.ts +0 -1
  106. package/dist/commands/human/prompts.js +39 -54
  107. package/dist/commands/human/queue.d.ts +12 -1
  108. package/dist/commands/human/queue.js +468 -73
  109. package/dist/commands/human/shared.d.ts +3 -4
  110. package/dist/commands/human/shared.js +3 -3
  111. package/dist/commands/human.js +11 -10
  112. package/dist/commands/memory/find.js +3 -2
  113. package/dist/commands/memory/list.js +4 -3
  114. package/dist/commands/memory/read.js +3 -3
  115. package/dist/commands/memory/shared.d.ts +1 -4
  116. package/dist/commands/memory/shared.js +6 -9
  117. package/dist/commands/memory/write.js +3 -3
  118. package/dist/commands/memory.js +4 -7
  119. package/dist/commands/node-context.d.ts +2 -0
  120. package/dist/commands/node-context.js +172 -0
  121. package/dist/commands/node-snapshot.d.ts +2 -0
  122. package/dist/commands/node-snapshot.js +123 -0
  123. package/dist/commands/node.js +228 -52
  124. package/dist/commands/pkg/plugin-inspect.js +6 -6
  125. package/dist/commands/pkg/plugin-manage.js +1 -1
  126. package/dist/commands/pkg/plugin.js +2 -2
  127. package/dist/commands/pkg.js +2 -2
  128. package/dist/commands/push.d.ts +2 -0
  129. package/dist/commands/push.js +79 -11
  130. package/dist/commands/revive.js +74 -1
  131. package/dist/commands/search/answer.d.ts +1 -0
  132. package/dist/commands/search/answer.js +50 -0
  133. package/dist/commands/search/contents.d.ts +1 -0
  134. package/dist/commands/search/contents.js +96 -0
  135. package/dist/commands/search/exa.d.ts +53 -0
  136. package/dist/commands/search/exa.js +155 -0
  137. package/dist/commands/search/puremd.d.ts +11 -0
  138. package/dist/commands/search/puremd.js +53 -0
  139. package/dist/commands/search/web.d.ts +1 -0
  140. package/dist/commands/search/web.js +65 -0
  141. package/dist/commands/search.d.ts +2 -0
  142. package/dist/commands/search.js +24 -0
  143. package/dist/commands/sys/__tests__/sync-import.test.d.ts +1 -0
  144. package/dist/commands/sys/__tests__/sync-import.test.js +72 -0
  145. package/dist/commands/sys/config.js +59 -10
  146. package/dist/commands/sys/doctor.js +56 -4
  147. package/dist/commands/sys/prompt-review.d.ts +1 -0
  148. package/dist/commands/sys/prompt-review.js +178 -0
  149. package/dist/commands/sys/promptstudio.d.ts +2 -0
  150. package/dist/commands/sys/promptstudio.js +65 -0
  151. package/dist/commands/sys/settings.d.ts +2 -0
  152. package/dist/commands/sys/settings.js +16 -0
  153. package/dist/commands/sys/sync.js +311 -159
  154. package/dist/commands/sys/sysprompt.d.ts +1 -0
  155. package/dist/commands/sys/sysprompt.js +86 -0
  156. package/dist/commands/sys.js +9 -5
  157. package/dist/commands/view-new.js +2 -2
  158. package/dist/core/__tests__/artifact-paths.test.d.ts +1 -0
  159. package/dist/core/__tests__/artifact-paths.test.js +44 -0
  160. package/dist/core/__tests__/broker-preflight.test.d.ts +1 -0
  161. package/dist/core/__tests__/broker-preflight.test.js +85 -0
  162. package/dist/core/__tests__/broker-sdk-wiring.test.js +15 -0
  163. package/dist/core/__tests__/canvas.test.js +37 -1
  164. package/dist/core/__tests__/chat-view-reconnect.test.d.ts +1 -0
  165. package/dist/core/__tests__/chat-view-reconnect.test.js +70 -0
  166. package/dist/core/__tests__/child-death-wake.test.js +11 -2
  167. package/dist/core/__tests__/close.test.js +39 -1
  168. package/dist/core/__tests__/connection-reconnect-resume.test.d.ts +1 -0
  169. package/dist/core/__tests__/connection-reconnect-resume.test.js +37 -0
  170. package/dist/core/__tests__/context-intro.test.js +48 -14
  171. package/dist/core/__tests__/daemon-boot.test.js +182 -9
  172. package/dist/core/__tests__/editor-label.test.d.ts +1 -0
  173. package/dist/core/__tests__/editor-label.test.js +26 -0
  174. package/dist/core/__tests__/fault-marker.test.d.ts +1 -0
  175. package/dist/core/__tests__/fault-marker.test.js +112 -0
  176. package/dist/core/__tests__/fault-retry-rewind.test.d.ts +1 -0
  177. package/dist/core/__tests__/fault-retry-rewind.test.js +123 -0
  178. package/dist/core/__tests__/fixtures/fake-engine.d.ts +30 -4
  179. package/dist/core/__tests__/fixtures/fake-engine.js +156 -17
  180. package/dist/core/__tests__/fixtures/fake-pi-host.js +10 -1
  181. package/dist/core/__tests__/full/human-new-window-regression.test.js +1 -1
  182. package/dist/core/__tests__/helpers/harness.d.ts +1 -1
  183. package/dist/core/__tests__/helpers/harness.js +9 -3
  184. package/dist/core/__tests__/human-stranded-deliver.test.js +38 -1
  185. package/dist/core/__tests__/human-surface-target.test.js +1 -1
  186. package/dist/core/__tests__/kickoff.test.js +18 -19
  187. package/dist/core/__tests__/memory-resolver.test.js +12 -1
  188. package/dist/core/__tests__/migration.test.js +49 -14
  189. package/dist/core/__tests__/model-ladders.test.d.ts +1 -0
  190. package/dist/core/__tests__/model-ladders.test.js +160 -0
  191. package/dist/core/__tests__/node-env.test.d.ts +1 -0
  192. package/dist/core/__tests__/node-env.test.js +26 -0
  193. package/dist/core/__tests__/push-final-guard.test.js +7 -1
  194. package/dist/core/__tests__/relaunch-root.test.js +9 -9
  195. package/dist/core/__tests__/revive.test.js +36 -6
  196. package/dist/core/__tests__/spawn-root.test.js +24 -1
  197. package/dist/core/__tests__/stop-guard.test.js +10 -0
  198. package/dist/core/__tests__/stranded-relaunch.test.d.ts +1 -0
  199. package/dist/core/__tests__/stranded-relaunch.test.js +72 -0
  200. package/dist/core/__tests__/tmux-surface.test.js +5 -1
  201. package/dist/core/__tests__/unknown-path.test.js +19 -19
  202. package/dist/core/__tests__/yield-ensures-daemon.test.d.ts +1 -0
  203. package/dist/core/__tests__/yield-ensures-daemon.test.js +54 -0
  204. package/dist/core/artifact.d.ts +2 -33
  205. package/dist/core/artifact.js +27 -87
  206. package/dist/core/bootstrap.js +2 -0
  207. package/dist/core/canvas/browse/__tests__/model.test.js +35 -1
  208. package/dist/core/canvas/browse/app.js +87 -17
  209. package/dist/core/canvas/browse/model.d.ts +10 -1
  210. package/dist/core/canvas/browse/model.js +37 -1
  211. package/dist/core/canvas/browse/render.js +35 -33
  212. package/dist/core/canvas/canvas.d.ts +7 -0
  213. package/dist/core/canvas/canvas.js +54 -5
  214. package/dist/core/canvas/db.js +35 -8
  215. package/dist/core/canvas/labels.d.ts +6 -8
  216. package/dist/core/canvas/labels.js +8 -11
  217. package/dist/core/canvas/nav-model.d.ts +5 -4
  218. package/dist/core/canvas/nav-model.js +31 -17
  219. package/dist/core/canvas/paths.d.ts +18 -1
  220. package/dist/core/canvas/paths.js +37 -2
  221. package/dist/core/canvas/render.d.ts +25 -0
  222. package/dist/core/canvas/render.js +96 -13
  223. package/dist/core/canvas/status-glyph.d.ts +35 -0
  224. package/dist/core/canvas/status-glyph.js +104 -0
  225. package/dist/core/canvas/types.d.ts +2 -2
  226. package/dist/core/config.js +114 -16
  227. package/dist/core/fault-classifier.d.ts +41 -0
  228. package/dist/core/fault-classifier.js +110 -0
  229. package/dist/core/feed/__tests__/empty-node-guard.test.d.ts +1 -0
  230. package/dist/core/feed/__tests__/empty-node-guard.test.js +45 -0
  231. package/dist/core/feed/feed.js +8 -0
  232. package/dist/core/feed/inbox.d.ts +15 -0
  233. package/dist/core/feed/inbox.js +40 -14
  234. package/dist/core/frontmatter.d.ts +1 -11
  235. package/dist/core/frontmatter.js +1 -55
  236. package/dist/core/hearth/config.d.ts +2 -0
  237. package/dist/core/hearth/config.js +75 -0
  238. package/dist/core/hearth/guest-env.d.ts +2 -0
  239. package/dist/core/hearth/guest-env.js +11 -0
  240. package/dist/core/hearth/index.d.ts +5 -0
  241. package/dist/core/hearth/index.js +5 -0
  242. package/dist/core/hearth/provider.d.ts +21 -0
  243. package/dist/core/hearth/provider.js +10 -0
  244. package/dist/core/hearth/providers/blaxel-bootstrap.d.ts +10 -0
  245. package/dist/core/hearth/providers/blaxel-bootstrap.js +39 -0
  246. package/dist/core/hearth/providers/blaxel-home.d.ts +11 -0
  247. package/dist/core/hearth/providers/blaxel-home.js +126 -0
  248. package/dist/core/hearth/providers/blaxel.d.ts +26 -0
  249. package/dist/core/hearth/providers/blaxel.js +208 -0
  250. package/dist/core/hearth/providers/types.d.ts +72 -0
  251. package/dist/core/hearth/providers/types.js +1 -0
  252. package/dist/core/hearth/registry.d.ts +15 -0
  253. package/dist/core/hearth/registry.js +179 -0
  254. package/dist/core/hearth/types.d.ts +121 -0
  255. package/dist/core/hearth/types.js +1 -0
  256. package/dist/core/host-exports/__tests__/export-prunes-boot-skill.test.d.ts +1 -0
  257. package/dist/core/host-exports/__tests__/export-prunes-boot-skill.test.js +79 -0
  258. package/dist/core/{skill-sync → host-exports}/builtins.d.ts +21 -13
  259. package/dist/core/host-exports/builtins.js +68 -0
  260. package/dist/core/{skill-sync → host-exports}/export.d.ts +9 -6
  261. package/dist/core/{skill-sync → host-exports}/export.js +37 -14
  262. package/dist/core/io.js +1 -1
  263. package/dist/core/log.d.ts +9 -0
  264. package/dist/core/log.js +113 -0
  265. package/dist/core/memory-resolver.d.ts +8 -8
  266. package/dist/core/memory-resolver.js +15 -28
  267. package/dist/core/personas/index.d.ts +4 -4
  268. package/dist/core/personas/index.js +2 -2
  269. package/dist/core/personas/loader.d.ts +51 -0
  270. package/dist/core/personas/loader.js +54 -50
  271. package/dist/core/personas/resolve.d.ts +43 -27
  272. package/dist/core/personas/resolve.js +336 -94
  273. package/dist/core/runtime/auth-reload.d.ts +7 -0
  274. package/dist/core/runtime/auth-reload.js +69 -0
  275. package/dist/core/runtime/bearings.d.ts +35 -15
  276. package/dist/core/runtime/bearings.js +305 -44
  277. package/dist/core/runtime/branded-host.d.ts +4 -2
  278. package/dist/core/runtime/branded-host.js +66 -4
  279. package/dist/core/runtime/broker-protocol.d.ts +38 -3
  280. package/dist/core/runtime/broker.js +343 -18
  281. package/dist/core/runtime/close.js +37 -29
  282. package/dist/core/runtime/connectivity.d.ts +12 -0
  283. package/dist/core/runtime/connectivity.js +73 -0
  284. package/dist/core/runtime/fault-recovery-nudge.d.ts +2 -0
  285. package/dist/core/runtime/fault-recovery-nudge.js +2 -0
  286. package/dist/core/runtime/fault-recovery.d.ts +23 -0
  287. package/dist/core/runtime/fault-recovery.js +74 -0
  288. package/dist/core/runtime/fault.d.ts +25 -0
  289. package/dist/core/runtime/fault.js +176 -0
  290. package/dist/core/runtime/front-door.d.ts +1 -1
  291. package/dist/core/runtime/front-door.js +2 -2
  292. package/dist/core/runtime/host.d.ts +10 -0
  293. package/dist/core/runtime/host.js +71 -9
  294. package/dist/core/runtime/kickoff.js +31 -45
  295. package/dist/core/runtime/launch.d.ts +32 -18
  296. package/dist/core/runtime/launch.js +148 -43
  297. package/dist/core/runtime/model-swap.d.ts +18 -0
  298. package/dist/core/runtime/model-swap.js +95 -0
  299. package/dist/core/runtime/naming.d.ts +7 -0
  300. package/dist/core/runtime/naming.js +61 -9
  301. package/dist/core/runtime/nodes.js +6 -1
  302. package/dist/core/runtime/persona.js +16 -17
  303. package/dist/core/runtime/placement.d.ts +36 -31
  304. package/dist/core/runtime/placement.js +149 -64
  305. package/dist/core/runtime/promote.d.ts +2 -0
  306. package/dist/core/runtime/promote.js +23 -3
  307. package/dist/core/runtime/recycle.js +6 -4
  308. package/dist/core/runtime/reset.d.ts +5 -2
  309. package/dist/core/runtime/reset.js +2 -2
  310. package/dist/core/runtime/revive.d.ts +4 -0
  311. package/dist/core/runtime/revive.js +27 -9
  312. package/dist/core/runtime/roadmap.d.ts +1 -1
  313. package/dist/core/runtime/roadmap.js +1 -1
  314. package/dist/core/runtime/spawn.d.ts +2 -2
  315. package/dist/core/runtime/spawn.js +30 -6
  316. package/dist/core/runtime/stop-guard.js +6 -1
  317. package/dist/core/runtime/tmux-chrome.d.ts +1 -1
  318. package/dist/core/runtime/tmux-chrome.js +1 -1
  319. package/dist/core/runtime/tmux.d.ts +29 -6
  320. package/dist/core/runtime/tmux.js +134 -80
  321. package/dist/core/runtime/view-socket-client.d.ts +46 -0
  322. package/dist/core/runtime/view-socket-client.js +203 -0
  323. package/dist/core/substrate/index.d.ts +1 -1
  324. package/dist/core/substrate/index.js +1 -1
  325. package/dist/core/substrate/on-read.js +16 -4
  326. package/dist/core/substrate/render.d.ts +10 -13
  327. package/dist/core/substrate/render.js +56 -51
  328. package/dist/core/tui/host.js +83 -18
  329. package/dist/core/view/__tests__/transport-cache.test.d.ts +1 -0
  330. package/dist/core/view/__tests__/transport-cache.test.js +62 -0
  331. package/dist/core/view/contract.d.ts +20 -0
  332. package/dist/core/view/stream-local.d.ts +3 -0
  333. package/dist/core/view/stream-local.js +184 -0
  334. package/dist/core/view/transport-cache.d.ts +8 -0
  335. package/dist/core/view/transport-cache.js +38 -0
  336. package/dist/core/view/transport-local.js +4 -0
  337. package/dist/core/view/transport.d.ts +7 -1
  338. package/dist/daemon/crtrd.d.ts +3 -19
  339. package/dist/daemon/crtrd.js +139 -178
  340. package/dist/daemon/manage.d.ts +18 -1
  341. package/dist/daemon/manage.js +54 -9
  342. package/dist/hearth/wake-proxy/__tests__/anthropic-oauth.test.d.ts +1 -0
  343. package/dist/hearth/wake-proxy/__tests__/anthropic-oauth.test.js +289 -0
  344. package/dist/hearth/wake-proxy/__tests__/config-timeout.test.d.ts +1 -0
  345. package/dist/hearth/wake-proxy/__tests__/config-timeout.test.js +34 -0
  346. package/dist/hearth/wake-proxy/__tests__/guest-source.test.d.ts +1 -0
  347. package/dist/hearth/wake-proxy/__tests__/guest-source.test.js +203 -0
  348. package/dist/hearth/wake-proxy/__tests__/hardening.test.d.ts +1 -0
  349. package/dist/hearth/wake-proxy/__tests__/hardening.test.js +59 -0
  350. package/dist/hearth/wake-proxy/__tests__/hearth-status-route.test.d.ts +1 -0
  351. package/dist/hearth/wake-proxy/__tests__/hearth-status-route.test.js +372 -0
  352. package/dist/hearth/wake-proxy/__tests__/http-running-route-fast-path.test.d.ts +1 -0
  353. package/dist/hearth/wake-proxy/__tests__/http-running-route-fast-path.test.js +258 -0
  354. package/dist/hearth/wake-proxy/__tests__/model-auth-routes.test.d.ts +1 -0
  355. package/dist/hearth/wake-proxy/__tests__/model-auth-routes.test.js +437 -0
  356. package/dist/hearth/wake-proxy/__tests__/source-bridge-gate.test.d.ts +1 -0
  357. package/dist/hearth/wake-proxy/__tests__/source-bridge-gate.test.js +455 -0
  358. package/dist/hearth/wake-proxy/__tests__/static-asset-allowlist.test.d.ts +1 -0
  359. package/dist/hearth/wake-proxy/__tests__/static-asset-allowlist.test.js +15 -0
  360. package/dist/hearth/wake-proxy/__tests__/warm-path-concurrency.test.d.ts +1 -0
  361. package/dist/hearth/wake-proxy/__tests__/warm-path-concurrency.test.js +141 -0
  362. package/dist/hearth/wake-proxy/__tests__/ws-teardown-accounting.test.d.ts +1 -0
  363. package/dist/hearth/wake-proxy/__tests__/ws-teardown-accounting.test.js +143 -0
  364. package/dist/hearth/wake-proxy/anthropic-oauth.d.ts +58 -0
  365. package/dist/hearth/wake-proxy/anthropic-oauth.js +189 -0
  366. package/dist/hearth/wake-proxy/auth.d.ts +22 -0
  367. package/dist/hearth/wake-proxy/auth.js +128 -0
  368. package/dist/hearth/wake-proxy/config.d.ts +2 -0
  369. package/dist/hearth/wake-proxy/config.js +151 -0
  370. package/dist/hearth/wake-proxy/guest-source.d.ts +14 -0
  371. package/dist/hearth/wake-proxy/guest-source.js +130 -0
  372. package/dist/hearth/wake-proxy/hearth-status.d.ts +44 -0
  373. package/dist/hearth/wake-proxy/hearth-status.js +267 -0
  374. package/dist/hearth/wake-proxy/home.d.ts +67 -0
  375. package/dist/hearth/wake-proxy/home.js +297 -0
  376. package/dist/hearth/wake-proxy/main.d.ts +1 -0
  377. package/dist/hearth/wake-proxy/main.js +134 -0
  378. package/dist/hearth/wake-proxy/model-auth.d.ts +13 -0
  379. package/dist/hearth/wake-proxy/model-auth.js +345 -0
  380. package/dist/hearth/wake-proxy/proxy.d.ts +35 -0
  381. package/dist/hearth/wake-proxy/proxy.js +716 -0
  382. package/dist/hearth/wake-proxy/public-source-gate.d.ts +9 -0
  383. package/dist/hearth/wake-proxy/public-source-gate.js +409 -0
  384. package/dist/hearth/wake-proxy/redact.d.ts +1 -0
  385. package/dist/hearth/wake-proxy/redact.js +17 -0
  386. package/dist/hearth/wake-proxy/server.d.ts +11 -0
  387. package/dist/hearth/wake-proxy/server.js +142 -0
  388. package/dist/hearth/wake-proxy/state.d.ts +18 -0
  389. package/dist/hearth/wake-proxy/state.js +342 -0
  390. package/dist/hearth/wake-proxy/types.d.ts +76 -0
  391. package/dist/hearth/wake-proxy/types.js +1 -0
  392. package/dist/index.js +3 -2
  393. package/dist/pi-extensions/__tests__/canvas-context-intro.test.js +6 -5
  394. package/dist/pi-extensions/canvas-context-intro.js +7 -7
  395. package/dist/pi-extensions/canvas-doc-substrate.js +19 -22
  396. package/dist/pi-extensions/canvas-nav.js +15 -8
  397. package/dist/pi-extensions/canvas-stophook.d.ts +1 -1
  398. package/dist/pi-extensions/canvas-stophook.js +78 -11
  399. package/dist/types.d.ts +39 -20
  400. package/dist/types.js +38 -13
  401. package/dist/web/runtime.js +141 -42
  402. package/dist/web/transport-http.js +7 -5
  403. package/dist/web/transport-stream.d.ts +3 -0
  404. package/dist/web/transport-stream.js +204 -0
  405. package/dist/web-client/assets/fragment-mono-latin-400-normal-BYwT3kSJ.woff +0 -0
  406. package/dist/web-client/assets/fragment-mono-latin-400-normal-yxdJ5AmL.woff2 +0 -0
  407. package/dist/web-client/assets/fragment-mono-latin-ext-400-normal-BbKYyvR9.woff2 +0 -0
  408. package/dist/web-client/assets/fragment-mono-latin-ext-400-normal-CT4YFKeK.woff +0 -0
  409. package/dist/web-client/assets/fraunces-latin-ext-wght-normal-Ca2vKHc0.woff2 +0 -0
  410. package/dist/web-client/assets/fraunces-latin-wght-normal-ukD16Tqj.woff2 +0 -0
  411. package/dist/web-client/assets/fraunces-vietnamese-wght-normal-CnvboYUG.woff2 +0 -0
  412. package/dist/web-client/assets/index-BZUxTkv5.css +2 -0
  413. package/dist/web-client/assets/index-D36PNBj4.js +80 -0
  414. package/dist/web-client/assets/instrument-sans-latin-ext-wght-normal-B5bTHO_g.woff2 +0 -0
  415. package/dist/web-client/assets/instrument-sans-latin-wght-normal-BbzFLZTg.woff2 +0 -0
  416. package/dist/web-client/assets/martian-mono-cyrillic-wght-normal-B84CD5C_.woff2 +0 -0
  417. package/dist/web-client/assets/martian-mono-latin-ext-wght-normal-DlL6xMw5.woff2 +0 -0
  418. package/dist/web-client/assets/martian-mono-latin-wght-normal-5W32yIyr.woff2 +0 -0
  419. package/dist/web-client/index.html +3 -2
  420. package/package.json +32 -10
  421. package/dist/core/__tests__/error-stall-recycle.test.js +0 -141
  422. package/dist/core/skill-sync/__tests__/dry-run-wrote-count.test.js +0 -57
  423. package/dist/core/skill-sync/builtins.js +0 -112
  424. package/dist/core/skill-sync/claude-plugins.d.ts +0 -23
  425. package/dist/core/skill-sync/claude-plugins.js +0 -71
  426. package/dist/core/skill-sync/engine.d.ts +0 -42
  427. package/dist/core/skill-sync/engine.js +0 -633
  428. package/dist/core/skill-sync/manifest.d.ts +0 -64
  429. package/dist/core/skill-sync/manifest.js +0 -181
  430. package/dist/core/skill-sync/profile.d.ts +0 -76
  431. package/dist/core/skill-sync/profile.js +0 -173
  432. package/dist/core/skill-sync/snapshot.d.ts +0 -57
  433. package/dist/core/skill-sync/snapshot.js +0 -120
  434. package/dist/pi-extensions/canvas-commands.d.ts +0 -37
  435. package/dist/pi-extensions/canvas-commands.js +0 -113
  436. package/dist/pi-extensions/canvas-resume.d.ts +0 -21
  437. package/dist/pi-extensions/canvas-resume.js +0 -83
  438. package/dist/pi-extensions/canvas-view.d.ts +0 -21
  439. package/dist/pi-extensions/canvas-view.js +0 -76
  440. package/dist/web-client/assets/index-BUvQb4hR.css +0 -2
  441. package/dist/web-client/assets/index-ClLQXYAE.js +0 -10
  442. /package/dist/{core/__tests__/error-stall-recycle.test.d.ts → clients/attach/__tests__/autocomplete-and-bash-mode.test.d.ts} +0 -0
  443. /package/dist/{core/skill-sync/__tests__/dry-run-wrote-count.test.d.ts → clients/attach/__tests__/bash-bang-routing.test.d.ts} +0 -0
@@ -0,0 +1,289 @@
1
+ import assert from 'node:assert/strict';
2
+ import { createHash } from 'node:crypto';
3
+ import { readFileSync } from 'node:fs';
4
+ import { join } from 'node:path';
5
+ import test from 'node:test';
6
+ import { ANTHROPIC_AUTHORIZE_URL, ANTHROPIC_CLIENT_ID, ANTHROPIC_REDIRECT_URI, ANTHROPIC_SCOPES, ANTHROPIC_TOKEN_URL, finishAnthropicOAuthSession, redactAnthropicOAuthMessage, startAnthropicOAuthSession, } from '../anthropic-oauth.js';
7
+ const PROVIDER_SOURCE_PATH = join(process.cwd(), 'node_modules/@earendil-works/pi-ai/dist/utils/oauth/anthropic.js');
8
+ function makeRandomBytes(values) {
9
+ let index = 0;
10
+ return (size) => {
11
+ const value = values[index++];
12
+ assert.ok(value, `unexpected randomBytes call ${index} for size ${size}`);
13
+ return value;
14
+ };
15
+ }
16
+ function sha256Base64url(input) {
17
+ return createHash('sha256').update(input, 'utf8').digest('base64url');
18
+ }
19
+ function makeFetch(responseBody, calls) {
20
+ return async (input, init) => {
21
+ calls.push({ input, init });
22
+ return {
23
+ ok: true,
24
+ status: 200,
25
+ statusText: 'OK',
26
+ async text() {
27
+ return JSON.stringify(responseBody);
28
+ },
29
+ };
30
+ };
31
+ }
32
+ test('startAnthropicOAuthSession builds a PKCE auth URL with Claude Code state bound to the verifier', () => {
33
+ const sessionIdBytes = Buffer.alloc(16, 1);
34
+ const verifierBytes = Buffer.alloc(32, 3);
35
+ const verifier = verifierBytes.toString('base64url');
36
+ const shaInputs = [];
37
+ const started = startAnthropicOAuthSession('tenant-a', () => 1_000_000, {
38
+ randomBytes: makeRandomBytes([sessionIdBytes, verifierBytes]),
39
+ sha256: (input) => {
40
+ shaInputs.push(input);
41
+ return Buffer.alloc(32, 9);
42
+ },
43
+ });
44
+ assert.equal(started.ok, true);
45
+ if (!started.ok)
46
+ throw new Error('expected success');
47
+ assert.ok(started.authUrl.includes('response_type=code'));
48
+ assert.ok(started.authUrl.includes('code_challenge_method=S256'));
49
+ // Regression: claude.ai rejects the authorize request with "Invalid request
50
+ // format" without code=true (the Claude Code authorize flow param).
51
+ assert.ok(started.authUrl.includes('code=true'));
52
+ const authUrl = new URL(started.authUrl);
53
+ assert.equal(authUrl.origin + authUrl.pathname, ANTHROPIC_AUTHORIZE_URL);
54
+ assert.equal(authUrl.searchParams.get('client_id'), ANTHROPIC_CLIENT_ID);
55
+ assert.equal(authUrl.searchParams.get('redirect_uri'), ANTHROPIC_REDIRECT_URI);
56
+ assert.equal(authUrl.searchParams.get('scope'), ANTHROPIC_SCOPES);
57
+ assert.equal(authUrl.searchParams.get('response_type'), 'code');
58
+ assert.equal(authUrl.searchParams.get('code_challenge_method'), 'S256');
59
+ assert.equal(authUrl.searchParams.get('state'), verifier);
60
+ assert.equal(authUrl.searchParams.get('code_challenge'), Buffer.alloc(32, 9).toString('base64url'));
61
+ assert.ok(!started.authUrl.includes(sessionIdBytes.toString('base64url')));
62
+ assert.equal(shaInputs[0], verifier);
63
+ });
64
+ test('startAnthropicOAuthSession keeps Claude Code state coupled to the PKCE verifier', () => {
65
+ const sessionIdBytes = Buffer.alloc(16, 41);
66
+ const verifierBytes = Buffer.alloc(32, 42);
67
+ const verifier = verifierBytes.toString('base64url');
68
+ const started = startAnthropicOAuthSession('tenant-a-pkce', () => 2_000_000, {
69
+ randomBytes: makeRandomBytes([sessionIdBytes, verifierBytes]),
70
+ });
71
+ assert.equal(started.ok, true);
72
+ if (!started.ok)
73
+ throw new Error('expected success');
74
+ const authUrl = new URL(started.authUrl);
75
+ const state = authUrl.searchParams.get('state');
76
+ assert.ok(state);
77
+ assert.equal(state, verifier);
78
+ assert.equal(authUrl.searchParams.get('code_challenge'), sha256Base64url(state));
79
+ });
80
+ test('finishAnthropicOAuthSession rejects foreign state, unknown session, and expired sessions without token exchange', async () => {
81
+ const fetchCalls = [];
82
+ const started = startAnthropicOAuthSession('tenant-b', () => 10_000, {
83
+ randomBytes: makeRandomBytes([Buffer.alloc(16, 4), Buffer.alloc(32, 6)]),
84
+ sha256: () => Buffer.alloc(32, 7),
85
+ });
86
+ assert.equal(started.ok, true);
87
+ if (!started.ok)
88
+ throw new Error('expected success');
89
+ const foreignState = await finishAnthropicOAuthSession({
90
+ tenantId: 'tenant-b',
91
+ sessionId: started.sessionId,
92
+ redirectUrl: 'http://localhost:53692/callback?code=abc123&state=wrong-state',
93
+ }, {
94
+ now: () => 10_000,
95
+ fetch: makeFetch({ access_token: 'x', refresh_token: 'y', expires_in: 600 }, fetchCalls),
96
+ });
97
+ assert.equal(foreignState.ok, false);
98
+ assert.equal(foreignState.status, 400);
99
+ assert.equal(fetchCalls.length, 0);
100
+ const unknownSession = await finishAnthropicOAuthSession({
101
+ tenantId: 'tenant-c',
102
+ sessionId: 'missing-session',
103
+ redirectUrl: 'http://localhost:53692/callback?code=abc123&state=wrong-state',
104
+ }, {
105
+ now: () => 10_000,
106
+ fetch: makeFetch({ access_token: 'x', refresh_token: 'y', expires_in: 600 }, fetchCalls),
107
+ });
108
+ assert.equal(unknownSession.ok, false);
109
+ assert.equal(unknownSession.status, 400);
110
+ assert.equal(fetchCalls.length, 0);
111
+ const expiredStart = startAnthropicOAuthSession('tenant-d', () => 20_000, {
112
+ randomBytes: makeRandomBytes([Buffer.alloc(16, 8), Buffer.alloc(32, 10)]),
113
+ sha256: () => Buffer.alloc(32, 11),
114
+ });
115
+ assert.equal(expiredStart.ok, true);
116
+ if (!expiredStart.ok)
117
+ throw new Error('expected success');
118
+ const expired = await finishAnthropicOAuthSession({
119
+ tenantId: 'tenant-d',
120
+ sessionId: expiredStart.sessionId,
121
+ redirectUrl: 'http://localhost:53692/callback?code=abc123&state=whatever',
122
+ }, {
123
+ now: () => 20_000 + 10 * 60_000 + 1,
124
+ fetch: makeFetch({ access_token: 'x', refresh_token: 'y', expires_in: 600 }, fetchCalls),
125
+ });
126
+ assert.equal(expired.ok, false);
127
+ assert.equal(expired.status, 400);
128
+ assert.equal(fetchCalls.length, 0);
129
+ });
130
+ // Regression: a stale/zombie in-memory OAuth session used to make /start return
131
+ // 409 "already in progress" for the whole 10-min TTL, trapping the user on an
132
+ // about:blank popup with no usable authUrl (Hearth live triage 2026-06-29).
133
+ // A fresh start must always supersede the prior session and hand back a usable,
134
+ // distinct authUrl. Pre-fix this test would have failed on the second start.
135
+ test('startAnthropicOAuthSession supersedes a stale pending session and returns a fresh usable authUrl', () => {
136
+ const now = 30_000;
137
+ const first = startAnthropicOAuthSession('tenant-e', () => now, {
138
+ randomBytes: makeRandomBytes([Buffer.alloc(16, 12), Buffer.alloc(32, 14)]),
139
+ sha256: () => Buffer.alloc(32, 15),
140
+ });
141
+ assert.equal(first.ok, true);
142
+ if (!first.ok)
143
+ throw new Error('expected success');
144
+ // Second click while the first session is still live (within TTL): no 409 lock.
145
+ const second = startAnthropicOAuthSession('tenant-e', () => now + 1_000, {
146
+ randomBytes: makeRandomBytes([Buffer.alloc(16, 16), Buffer.alloc(32, 18)]),
147
+ sha256: () => Buffer.alloc(32, 19),
148
+ });
149
+ assert.equal(second.ok, true);
150
+ if (!second.ok)
151
+ throw new Error('expected fresh start to succeed, not a 409 lock');
152
+ assert.ok(second.authUrl.includes('response_type=code'));
153
+ assert.notEqual(second.sessionId, first.sessionId);
154
+ // The superseded session's state must no longer be the active one.
155
+ assert.notEqual(new URL(second.authUrl).searchParams.get('state'), new URL(first.authUrl).searchParams.get('state'));
156
+ });
157
+ // Security companion to the supersede fix: after a fresh start replaces the
158
+ // pending session, a callback carrying the OLD session's id/state must NOT be
159
+ // able to complete auth — newest valid session wins, superseded state fails.
160
+ test('finishAnthropicOAuthSession rejects a superseded callback after a fresh start', async () => {
161
+ const now = 50_000;
162
+ const firstSessionId = Buffer.alloc(16, 30);
163
+ const firstVerifierBytes = Buffer.alloc(32, 32);
164
+ const firstVerifier = firstVerifierBytes.toString('base64url');
165
+ const first = startAnthropicOAuthSession('tenant-g', () => now, {
166
+ randomBytes: makeRandomBytes([firstSessionId, firstVerifierBytes]),
167
+ sha256: () => Buffer.alloc(32, 33),
168
+ });
169
+ assert.equal(first.ok, true);
170
+ if (!first.ok)
171
+ throw new Error('expected success');
172
+ // User re-clicks; a new session supersedes the first.
173
+ const second = startAnthropicOAuthSession('tenant-g', () => now + 1_000, {
174
+ randomBytes: makeRandomBytes([Buffer.alloc(16, 34), Buffer.alloc(32, 36)]),
175
+ sha256: () => Buffer.alloc(32, 37),
176
+ });
177
+ assert.equal(second.ok, true);
178
+ if (!second.ok)
179
+ throw new Error('expected success');
180
+ // A late callback from the FIRST (superseded) session must fail without exchange.
181
+ const fetchCalls = [];
182
+ const stale = await finishAnthropicOAuthSession({
183
+ tenantId: 'tenant-g',
184
+ sessionId: first.sessionId,
185
+ redirectUrl: `http://localhost:53692/callback?code=stale-code&state=${firstVerifier}`,
186
+ }, {
187
+ now: () => now + 2_000,
188
+ fetch: makeFetch({ access_token: 'x', refresh_token: 'y', expires_in: 600 }, fetchCalls),
189
+ });
190
+ assert.equal(stale.ok, false);
191
+ if (stale.ok)
192
+ throw new Error('superseded callback must not complete auth');
193
+ assert.equal(stale.status, 400);
194
+ assert.equal(fetchCalls.length, 0);
195
+ });
196
+ test('finishAnthropicOAuthSession exchanges the stored verifier and returns Pi-compatible credentials with the skew applied', async () => {
197
+ const sessionIdBytes = Buffer.alloc(16, 24);
198
+ const verifierBytes = Buffer.alloc(32, 26);
199
+ const verifier = verifierBytes.toString('base64url');
200
+ const now = 40_000;
201
+ const started = startAnthropicOAuthSession('tenant-f', () => now, {
202
+ randomBytes: makeRandomBytes([sessionIdBytes, verifierBytes]),
203
+ sha256: (input) => {
204
+ assert.equal(input, verifier);
205
+ return Buffer.alloc(32, 27);
206
+ },
207
+ });
208
+ assert.equal(started.ok, true);
209
+ if (!started.ok)
210
+ throw new Error('expected success');
211
+ const fetchCalls = [];
212
+ const finished = await finishAnthropicOAuthSession({
213
+ tenantId: 'tenant-f',
214
+ sessionId: started.sessionId,
215
+ redirectUrl: `http://localhost:53692/callback?code=auth-code-123&state=${verifier}`,
216
+ }, {
217
+ now: () => now,
218
+ fetch: async (input, init) => {
219
+ fetchCalls.push({ input, init });
220
+ const request = new Request(String(input), init);
221
+ const body = JSON.parse(await request.text());
222
+ assert.equal(body.grant_type, 'authorization_code');
223
+ assert.equal(body.client_id, ANTHROPIC_CLIENT_ID);
224
+ assert.equal(body.code, 'auth-code-123');
225
+ assert.equal(body.state, verifier);
226
+ assert.equal(body.redirect_uri, ANTHROPIC_REDIRECT_URI);
227
+ assert.equal(body.code_verifier, verifier);
228
+ return {
229
+ ok: true,
230
+ status: 200,
231
+ statusText: 'OK',
232
+ async text() {
233
+ return JSON.stringify({
234
+ access_token: 'access-token-1',
235
+ refresh_token: 'refresh-token-1',
236
+ expires_in: 600,
237
+ });
238
+ },
239
+ };
240
+ },
241
+ });
242
+ assert.equal(fetchCalls.length, 1);
243
+ assert.equal(finished.ok, true);
244
+ if (!finished.ok)
245
+ throw new Error('expected success');
246
+ assert.deepEqual(finished.credential, {
247
+ type: 'oauth',
248
+ refresh: 'refresh-token-1',
249
+ access: 'access-token-1',
250
+ expires: now + 600_000 - 300_000,
251
+ });
252
+ assert.equal(finished.type, 'oauth');
253
+ assert.equal(finished.refresh, 'refresh-token-1');
254
+ assert.equal(finished.access, 'access-token-1');
255
+ assert.equal(finished.expires, now + 600_000 - 300_000);
256
+ });
257
+ test('contract guard: the installed Pi Anthropic OAuth source still matches the copied constants', () => {
258
+ const source = readFileSync(PROVIDER_SOURCE_PATH, 'utf8');
259
+ assert.match(source, /const CLIENT_ID = decode\("OWQxYzI1MGEtZTYxYi00NGQ5LTg4ZWQtNTk0NGQxOTYyZjVl"\);/);
260
+ assert.match(source, /const AUTHORIZE_URL = "https:\/\/claude\.ai\/oauth\/authorize";/);
261
+ assert.match(source, /const TOKEN_URL = "https:\/\/platform\.claude\.com\/v1\/oauth\/token";/);
262
+ assert.match(source, /const CALLBACK_PORT = 53692;/);
263
+ assert.match(source, /const CALLBACK_PATH = "\/callback";/);
264
+ assert.match(source, /const REDIRECT_URI = `http:\/\/localhost:\$\{CALLBACK_PORT\}\$\{CALLBACK_PATH\}`;/);
265
+ assert.match(source, /const SCOPES = "org:create_api_key user:profile user:inference user:sessions:claude_code user:mcp_servers user:file_upload";/);
266
+ assert.equal(ANTHROPIC_CLIENT_ID, '9d1c250a-e61b-44d9-88ed-5944d1962f5e');
267
+ assert.equal(ANTHROPIC_AUTHORIZE_URL, 'https://claude.ai/oauth/authorize');
268
+ assert.equal(ANTHROPIC_TOKEN_URL, 'https://platform.claude.com/v1/oauth/token');
269
+ assert.equal(ANTHROPIC_REDIRECT_URI, 'http://localhost:53692/callback');
270
+ assert.equal(ANTHROPIC_SCOPES, 'org:create_api_key user:profile user:inference user:sessions:claude_code user:mcp_servers user:file_upload');
271
+ });
272
+ test('redaction removes Anthropic OAuth and bearer/token material from log text', () => {
273
+ const redacted = redactAnthropicOAuthMessage('Authorization: Bearer owner-token https://localhost:53692/callback?code=auth-code&state=state-value&code_verifier=verifier-value {"access_token":"access-value","refresh_token":"refresh-value","access":"access-two","refresh":"refresh-two","code":"json-code","state":"json-state","code_verifier":"json-verifier"} code=assign-code state=assign-state code_verifier=assign-verifier hearth_owner=owner-cookie');
274
+ assert.ok(!redacted.includes('owner-token'));
275
+ assert.ok(!redacted.includes('auth-code'));
276
+ assert.ok(!redacted.includes('state-value'));
277
+ assert.ok(!redacted.includes('verifier-value'));
278
+ assert.ok(!redacted.includes('json-code'));
279
+ assert.ok(!redacted.includes('json-state'));
280
+ assert.ok(!redacted.includes('json-verifier'));
281
+ assert.ok(!redacted.includes('assign-code'));
282
+ assert.ok(!redacted.includes('assign-state'));
283
+ assert.ok(!redacted.includes('assign-verifier'));
284
+ assert.ok(!redacted.includes('access-value'));
285
+ assert.ok(!redacted.includes('refresh-value'));
286
+ assert.ok(!redacted.includes('owner-cookie'));
287
+ assert.match(redacted, /\[redacted redirect url\]/);
288
+ assert.match(redacted, /Bearer \[redacted\]/);
289
+ });
@@ -0,0 +1,34 @@
1
+ import assert from 'node:assert/strict';
2
+ import { test } from 'node:test';
3
+ import { loadHearthProxyConfig } from '../config.js';
4
+ import { proxyInternals } from '../proxy.js';
5
+ function slowWork(ms) {
6
+ return new Promise((resolve) => {
7
+ setTimeout(() => resolve('ok'), ms);
8
+ });
9
+ }
10
+ test('wake proxy defaults the wake timeout to 150s while leaving the readiness probe timeout at 30s', () => {
11
+ const config = loadHearthProxyConfig({
12
+ HEARTH_OWNER_TOKEN: 'owner-secret-token',
13
+ HEARTH_PROXY_HOST: '127.0.0.1',
14
+ HEARTH_PROXY_PORT: '8080',
15
+ });
16
+ assert.equal(config.wakeTimeoutMs, 150_000);
17
+ assert.equal(config.readyTimeoutMs, 30_000);
18
+ });
19
+ test('withWakeTimeout allows a recreate-sized wake to finish before 150s and rejects the old 30s budget', async (t) => {
20
+ t.mock.timers.enable({ apis: ['setTimeout'] });
21
+ try {
22
+ const shortBudget = proxyInternals.withWakeTimeout(slowWork(120_000), 30_000, 'wake proxy request');
23
+ await Promise.resolve();
24
+ t.mock.timers.tick(30_000);
25
+ await assert.rejects(shortBudget, /timed out/);
26
+ const longBudget = proxyInternals.withWakeTimeout(slowWork(120_000), 150_000, 'wake proxy request');
27
+ await Promise.resolve();
28
+ t.mock.timers.tick(120_000);
29
+ await assert.doesNotReject(longBudget);
30
+ }
31
+ finally {
32
+ t.mock.timers.reset();
33
+ }
34
+ });
@@ -0,0 +1,203 @@
1
+ import assert from 'node:assert/strict';
2
+ import { test } from 'node:test';
3
+ import { requireSuccessfulExec, runGuestSource } from '../guest-source.js';
4
+ const tenantId = 'tenant-1';
5
+ const routeUrl = 'https://guest.example';
6
+ const relayToken = 'relay-token';
7
+ function awakenedHome() {
8
+ return {
9
+ tenantId,
10
+ nodeId: 'mqt64sus-e71caa94',
11
+ routeUrl,
12
+ relayToken,
13
+ machineId: 'igd2fkrn1y1ol3vw5i6mt',
14
+ webPort: 7878,
15
+ };
16
+ }
17
+ function activityHandle() {
18
+ return {
19
+ tenantId,
20
+ kind: 'wake',
21
+ acquiredAt: '2026-06-26T00:00:00.000Z',
22
+ closed: false,
23
+ };
24
+ }
25
+ function jsonResponse(raw, ok = true, status = 200, statusText = 'OK') {
26
+ return {
27
+ ok,
28
+ status,
29
+ statusText,
30
+ async text() {
31
+ return JSON.stringify(raw);
32
+ },
33
+ };
34
+ }
35
+ function makeDeps(overrides = {}) {
36
+ const fetchCalls = [];
37
+ const state = { acquired: 0, released: 0, releaseSnapshot: null };
38
+ const handle = activityHandle();
39
+ const defaultFetch = (async (url, init) => {
40
+ fetchCalls.push({ url: String(url), init: init ?? {} });
41
+ return jsonResponse({ ok: true, exitCode: 0, stdout: 'ok', stderr: '' });
42
+ });
43
+ const deps = {
44
+ ensureAwake: overrides.ensureAwake ?? (async () => awakenedHome()),
45
+ fetch: overrides.fetch ?? defaultFetch,
46
+ acquireActivity: overrides.acquireActivity ?? (() => {
47
+ state.acquired += 1;
48
+ return handle;
49
+ }),
50
+ releaseActivity: overrides.releaseActivity ?? (() => {
51
+ state.released += 1;
52
+ state.releaseSnapshot = { tenantId, counts: { wake: 0, http: 0, ws: 0 }, total: 0, idleDeadlineAt: null };
53
+ return state.releaseSnapshot;
54
+ }),
55
+ now: overrides.now ?? (() => 1_700_000_000_000),
56
+ wakeTimeoutMs: overrides.wakeTimeoutMs ?? 50,
57
+ bridgeTimeoutMs: overrides.bridgeTimeoutMs ?? 50,
58
+ redact: overrides.redact,
59
+ fetchCalls,
60
+ };
61
+ return { deps, state };
62
+ }
63
+ test('runGuestSource acquires and releases activity and forwards the exact server-side request', async () => {
64
+ const { deps, state } = makeDeps();
65
+ const request = {
66
+ kind: 'exec',
67
+ bin: 'crtr',
68
+ args: ['canvas', 'snapshot', '--json'],
69
+ cwd: '/opt/crouter',
70
+ };
71
+ const raw = await runGuestSource(tenantId, request, deps);
72
+ assert.equal(raw.ok, true);
73
+ assert.equal(raw.exitCode, 0);
74
+ assert.equal(raw.stdout, 'ok');
75
+ assert.equal(raw.stderr, '');
76
+ assert.equal(state.acquired, 1);
77
+ assert.equal(state.released, 1);
78
+ assert.equal(deps.fetchCalls.length, 1);
79
+ assert.equal(deps.fetchCalls[0].url, `${routeUrl}/__crtr/source`);
80
+ assert.deepEqual(deps.fetchCalls[0].init?.headers, {
81
+ authorization: `Bearer ${relayToken}`,
82
+ 'content-type': 'application/json',
83
+ });
84
+ assert.equal(deps.fetchCalls[0].init?.body, JSON.stringify(request));
85
+ });
86
+ test('runGuestSource releases activity on wake timeout', async () => {
87
+ let resolved = false;
88
+ const { deps, state } = makeDeps({
89
+ ensureAwake: async () => await new Promise((resolve) => {
90
+ setTimeout(() => {
91
+ resolved = true;
92
+ resolve(awakenedHome());
93
+ }, 25);
94
+ }),
95
+ wakeTimeoutMs: 1,
96
+ fetch: async () => {
97
+ throw new Error('fetch should not run after wake timeout');
98
+ },
99
+ });
100
+ const raw = await runGuestSource(tenantId, { kind: 'exec', bin: 'crtr', args: ['view', 'list'] }, deps);
101
+ assert.equal(raw.ok, false);
102
+ assert.equal(raw.stdout, '');
103
+ assert.match(raw.stderr, /timed out/i);
104
+ assert.equal(state.acquired, 1);
105
+ assert.equal(state.released, 1);
106
+ assert.equal(resolved, false);
107
+ });
108
+ test('runGuestSource releases activity when the bridge fetch aborts on timeout', async () => {
109
+ const { deps, state } = makeDeps({
110
+ bridgeTimeoutMs: 1,
111
+ fetch: (async (_url, init) => {
112
+ const signal = init?.signal;
113
+ assert.ok(signal instanceof AbortSignal);
114
+ await new Promise((_resolve, reject) => {
115
+ signal.addEventListener('abort', () => {
116
+ reject(new Error('bridge fetch failed with Bearer secret https://localhost:53692/callback?code=abc&state=def access_token=tok refresh_token=ref'));
117
+ }, { once: true });
118
+ });
119
+ }),
120
+ });
121
+ const raw = await runGuestSource(tenantId, { kind: 'exec', bin: 'crtr', args: ['view', 'list'] }, deps);
122
+ assert.equal(raw.ok, false);
123
+ assert.equal(raw.stdout, '');
124
+ assert.doesNotMatch(raw.stderr, /Bearer secret/);
125
+ assert.doesNotMatch(raw.stderr, /code=abc/);
126
+ assert.doesNotMatch(raw.stderr, /access_token/);
127
+ assert.doesNotMatch(raw.stderr, /refresh_token/);
128
+ assert.equal(state.acquired, 1);
129
+ assert.equal(state.released, 1);
130
+ });
131
+ test('runGuestSource normalizes HTTP and bridge-shape failures without throwing', async () => {
132
+ const { deps: httpDeps } = makeDeps({
133
+ fetch: (async () => ({
134
+ ok: false,
135
+ status: 500,
136
+ statusText: 'Internal Server Error',
137
+ async text() {
138
+ return 'Authorization: Bearer secret-token code=abc&state=def access_token="tok" refresh_token="ref" https://localhost:53692/callback?code=abc&state=def';
139
+ },
140
+ })),
141
+ });
142
+ const { deps: malformedDeps } = makeDeps({
143
+ fetch: (async () => ({
144
+ ok: true,
145
+ status: 200,
146
+ statusText: 'OK',
147
+ async text() {
148
+ return 'this is not json Authorization: Bearer secret-token code=abc access_token=tok';
149
+ },
150
+ })),
151
+ });
152
+ const { deps: shapeDeps } = makeDeps({
153
+ fetch: (async () => ({
154
+ ok: true,
155
+ status: 200,
156
+ statusText: 'OK',
157
+ async text() {
158
+ return JSON.stringify({ ok: true, stdout: 1, stderr: '' });
159
+ },
160
+ })),
161
+ });
162
+ const httpRaw = await runGuestSource(tenantId, { kind: 'exec', bin: 'crtr', args: ['canvas', 'snapshot', '--json'] }, httpDeps);
163
+ const malformedRaw = await runGuestSource(tenantId, { kind: 'exec', bin: 'crtr', args: ['canvas', 'snapshot', '--json'] }, malformedDeps);
164
+ const shapeRaw = await runGuestSource(tenantId, { kind: 'exec', bin: 'crtr', args: ['canvas', 'snapshot', '--json'] }, shapeDeps);
165
+ assert.equal(httpRaw.ok, false);
166
+ assert.equal(httpRaw.stdout, '');
167
+ assert.match(httpRaw.stderr, /HTTP 500/);
168
+ assert.doesNotMatch(httpRaw.stderr, /Bearer secret-token/);
169
+ assert.doesNotMatch(httpRaw.stderr, /code=abc/);
170
+ assert.doesNotMatch(httpRaw.stderr, /access_token/);
171
+ assert.doesNotMatch(httpRaw.stderr, /refresh_token/);
172
+ assert.doesNotMatch(httpRaw.stderr, /localhost:53692\/callback\?code=abc/);
173
+ assert.equal(malformedRaw.ok, false);
174
+ assert.equal(malformedRaw.stdout, '');
175
+ assert.match(malformedRaw.stderr, /invalid RawResponse JSON/i);
176
+ assert.doesNotMatch(malformedRaw.stderr, /Bearer secret-token/);
177
+ assert.doesNotMatch(malformedRaw.stderr, /code=abc/);
178
+ assert.doesNotMatch(malformedRaw.stderr, /access_token/);
179
+ assert.equal(shapeRaw.ok, false);
180
+ assert.equal(shapeRaw.stdout, '');
181
+ assert.match(shapeRaw.stderr, /invalid RawResponse JSON/i);
182
+ });
183
+ test('requireSuccessfulExec accepts only ok=true with exitCode=0 and redacts stderr-bearing failures', () => {
184
+ const raw = { ok: true, exitCode: 0, stdout: 'ok', stderr: '' };
185
+ assert.equal(requireSuccessfulExec(raw, 'write anthropic auth'), raw);
186
+ assert.throws(() => requireSuccessfulExec({ ok: true, exitCode: 1, stdout: '', stderr: 'boom' }, 'write anthropic auth'));
187
+ assert.throws(() => requireSuccessfulExec({ ok: false, stdout: '', stderr: 'transport failed' }, 'write anthropic auth'));
188
+ assert.throws(() => requireSuccessfulExec({ ok: true, stdout: '', stderr: '' }, 'write anthropic auth'));
189
+ try {
190
+ requireSuccessfulExec({ ok: true, exitCode: 0, stdout: '', stderr: 'Authorization: Bearer secret-token access_token="tok" refresh_token="ref" code_verifier=verifier' }, 'write anthropic auth');
191
+ assert.fail('expected stderr-bearing success to fail');
192
+ }
193
+ catch (error) {
194
+ assert.ok(error instanceof Error);
195
+ const details = error.details;
196
+ assert.ok(details);
197
+ assert.equal(typeof details?.error, 'string');
198
+ assert.doesNotMatch(String(details?.error), /Bearer secret-token/);
199
+ assert.doesNotMatch(String(details?.error), /access_token/);
200
+ assert.doesNotMatch(String(details?.error), /refresh_token/);
201
+ assert.doesNotMatch(String(details?.error), /code_verifier=verifier/);
202
+ }
203
+ });
@@ -0,0 +1,59 @@
1
+ import assert from 'node:assert/strict';
2
+ import { EventEmitter } from 'node:events';
3
+ import { test } from 'node:test';
4
+ import { proxyInternals } from '../proxy.js';
5
+ class FakeWs extends EventEmitter {
6
+ readyState = 1;
7
+ pingCount = 0;
8
+ terminateCount = 0;
9
+ ping() {
10
+ this.pingCount += 1;
11
+ }
12
+ terminate() {
13
+ this.terminateCount += 1;
14
+ this.readyState = 3;
15
+ }
16
+ }
17
+ test('attachWsKeepalive pings on cadence, resets on pong, and stops cleanly', async (t) => {
18
+ t.mock.timers.enable({ apis: ['setInterval'] });
19
+ try {
20
+ const socket = new FakeWs();
21
+ const stop = proxyInternals.attachWsKeepalive(socket, { nodeId: 'node-1', tenantId: 'tenant-1', leg: 'browser' }, { intervalMs: 1 });
22
+ assert.equal(socket.listenerCount('pong'), 1);
23
+ t.mock.timers.tick(1);
24
+ assert.equal(socket.pingCount, 1);
25
+ assert.equal(socket.terminateCount, 0);
26
+ socket.emit('pong');
27
+ t.mock.timers.tick(1);
28
+ assert.equal(socket.pingCount, 2);
29
+ assert.equal(socket.terminateCount, 0);
30
+ stop();
31
+ assert.equal(socket.listenerCount('pong'), 0);
32
+ t.mock.timers.tick(10);
33
+ assert.equal(socket.pingCount, 2);
34
+ assert.equal(socket.terminateCount, 0);
35
+ }
36
+ finally {
37
+ t.mock.timers.reset();
38
+ }
39
+ });
40
+ test('attachWsKeepalive terminates after a missed pong and clears its interval', async (t) => {
41
+ t.mock.timers.enable({ apis: ['setInterval'] });
42
+ try {
43
+ const socket = new FakeWs();
44
+ proxyInternals.attachWsKeepalive(socket, { nodeId: 'node-2', tenantId: 'tenant-2', leg: 'upstream' }, { intervalMs: 1 });
45
+ t.mock.timers.tick(1);
46
+ assert.equal(socket.pingCount, 1);
47
+ assert.equal(socket.terminateCount, 0);
48
+ t.mock.timers.tick(1);
49
+ assert.equal(socket.pingCount, 1);
50
+ assert.equal(socket.terminateCount, 1);
51
+ assert.equal(socket.listenerCount('pong'), 0);
52
+ t.mock.timers.tick(10);
53
+ assert.equal(socket.pingCount, 1);
54
+ assert.equal(socket.terminateCount, 1);
55
+ }
56
+ finally {
57
+ t.mock.timers.reset();
58
+ }
59
+ });