@sentropic/h2a 0.85.25 → 0.86.0

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 (287) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/.codex-plugin/plugin.json +1 -1
  3. package/dist/bin.js +51 -2
  4. package/dist/bin.js.map +1 -1
  5. package/dist/cli-command-map.d.ts +119 -0
  6. package/dist/cli-command-map.d.ts.map +1 -0
  7. package/dist/cli-command-map.js +396 -0
  8. package/dist/cli-command-map.js.map +1 -0
  9. package/dist/cli-contract.d.ts.map +1 -1
  10. package/dist/cli-contract.js +38 -0
  11. package/dist/cli-contract.js.map +1 -1
  12. package/dist/cli.d.ts +25 -1
  13. package/dist/cli.d.ts.map +1 -1
  14. package/dist/cli.js +356 -7
  15. package/dist/cli.js.map +1 -1
  16. package/dist/identity.d.ts +37 -3
  17. package/dist/identity.d.ts.map +1 -1
  18. package/dist/identity.js +0 -0
  19. package/dist/identity.js.map +1 -1
  20. package/dist/index.d.ts +7 -2
  21. package/dist/index.d.ts.map +1 -1
  22. package/dist/index.js +9 -2
  23. package/dist/index.js.map +1 -1
  24. package/dist/runtime/enrollment/ceremony.d.ts +542 -0
  25. package/dist/runtime/enrollment/ceremony.d.ts.map +1 -0
  26. package/dist/runtime/enrollment/ceremony.js +777 -0
  27. package/dist/runtime/enrollment/ceremony.js.map +1 -0
  28. package/dist/runtime/enrollment/index.d.ts +16 -0
  29. package/dist/runtime/enrollment/index.d.ts.map +1 -0
  30. package/dist/runtime/enrollment/index.js +16 -0
  31. package/dist/runtime/enrollment/index.js.map +1 -0
  32. package/dist/runtime/feed/descriptors.d.ts +261 -0
  33. package/dist/runtime/feed/descriptors.d.ts.map +1 -0
  34. package/dist/runtime/feed/descriptors.js +425 -0
  35. package/dist/runtime/feed/descriptors.js.map +1 -0
  36. package/dist/runtime/feed/index.d.ts +12 -0
  37. package/dist/runtime/feed/index.d.ts.map +1 -0
  38. package/dist/runtime/feed/index.js +12 -0
  39. package/dist/runtime/feed/index.js.map +1 -0
  40. package/dist/runtime/identity/index.d.ts +3 -3
  41. package/dist/runtime/identity/index.d.ts.map +1 -1
  42. package/dist/runtime/identity/index.js +2 -2
  43. package/dist/runtime/identity/index.js.map +1 -1
  44. package/dist/runtime/identity/live.d.ts +68 -0
  45. package/dist/runtime/identity/live.d.ts.map +1 -1
  46. package/dist/runtime/identity/live.js +78 -2
  47. package/dist/runtime/identity/live.js.map +1 -1
  48. package/dist/runtime/identity/readers.d.ts +89 -5
  49. package/dist/runtime/identity/readers.d.ts.map +1 -1
  50. package/dist/runtime/identity/readers.js +242 -33
  51. package/dist/runtime/identity/readers.js.map +1 -1
  52. package/dist/runtime/local-files/presence.d.ts +16 -1
  53. package/dist/runtime/local-files/presence.d.ts.map +1 -1
  54. package/dist/runtime/local-files/presence.js +32 -20
  55. package/dist/runtime/local-files/presence.js.map +1 -1
  56. package/dist/runtime/loop/index.d.ts +5 -0
  57. package/dist/runtime/loop/index.d.ts.map +1 -1
  58. package/dist/runtime/loop/index.js +48 -5
  59. package/dist/runtime/loop/index.js.map +1 -1
  60. package/dist/runtime/mcp/agent-launch.d.ts +15 -1
  61. package/dist/runtime/mcp/agent-launch.d.ts.map +1 -1
  62. package/dist/runtime/mcp/agent-launch.js +45 -4
  63. package/dist/runtime/mcp/agent-launch.js.map +1 -1
  64. package/dist/runtime/mcp/index.d.ts +1 -1
  65. package/dist/runtime/mcp/index.d.ts.map +1 -1
  66. package/dist/runtime/mcp/index.js +1 -1
  67. package/dist/runtime/mcp/index.js.map +1 -1
  68. package/dist/runtime/mcp/server.d.ts +3 -1
  69. package/dist/runtime/mcp/server.d.ts.map +1 -1
  70. package/dist/runtime/mcp/server.js +7 -2
  71. package/dist/runtime/mcp/server.js.map +1 -1
  72. package/dist/runtime/mcp/sessions.d.ts +19 -0
  73. package/dist/runtime/mcp/sessions.d.ts.map +1 -1
  74. package/dist/runtime/mcp/sessions.js +44 -2
  75. package/dist/runtime/mcp/sessions.js.map +1 -1
  76. package/dist/runtime/mcp/stdio.d.ts +8 -0
  77. package/dist/runtime/mcp/stdio.d.ts.map +1 -1
  78. package/dist/runtime/mcp/stdio.js +42 -0
  79. package/dist/runtime/mcp/stdio.js.map +1 -1
  80. package/dist/runtime/mcp-registry/broker.d.ts +175 -0
  81. package/dist/runtime/mcp-registry/broker.d.ts.map +1 -0
  82. package/dist/runtime/mcp-registry/broker.js +203 -0
  83. package/dist/runtime/mcp-registry/broker.js.map +1 -0
  84. package/dist/runtime/mcp-registry/broker.typecheck.d.ts +2 -0
  85. package/dist/runtime/mcp-registry/broker.typecheck.d.ts.map +1 -0
  86. package/dist/runtime/mcp-registry/broker.typecheck.js +48 -0
  87. package/dist/runtime/mcp-registry/broker.typecheck.js.map +1 -0
  88. package/dist/runtime/mirror/accept.d.ts +43 -5
  89. package/dist/runtime/mirror/accept.d.ts.map +1 -1
  90. package/dist/runtime/mirror/accept.js +55 -7
  91. package/dist/runtime/mirror/accept.js.map +1 -1
  92. package/dist/runtime/mirror/build.d.ts +23 -4
  93. package/dist/runtime/mirror/build.d.ts.map +1 -1
  94. package/dist/runtime/mirror/build.js +34 -4
  95. package/dist/runtime/mirror/build.js.map +1 -1
  96. package/dist/runtime/mirror/index.d.ts +3 -0
  97. package/dist/runtime/mirror/index.d.ts.map +1 -1
  98. package/dist/runtime/mirror/index.js +3 -0
  99. package/dist/runtime/mirror/index.js.map +1 -1
  100. package/dist/runtime/mirror/ingest.d.ts +109 -0
  101. package/dist/runtime/mirror/ingest.d.ts.map +1 -0
  102. package/dist/runtime/mirror/ingest.js +117 -0
  103. package/dist/runtime/mirror/ingest.js.map +1 -0
  104. package/dist/runtime/mirror/push-daemon.d.ts +376 -0
  105. package/dist/runtime/mirror/push-daemon.d.ts.map +1 -0
  106. package/dist/runtime/mirror/push-daemon.js +644 -0
  107. package/dist/runtime/mirror/push-daemon.js.map +1 -0
  108. package/dist/runtime/mirror/sanitize.d.ts +477 -0
  109. package/dist/runtime/mirror/sanitize.d.ts.map +1 -0
  110. package/dist/runtime/mirror/sanitize.js +625 -0
  111. package/dist/runtime/mirror/sanitize.js.map +1 -0
  112. package/dist/runtime/mirror/serve.d.ts.map +1 -1
  113. package/dist/runtime/mirror/serve.js +104 -24
  114. package/dist/runtime/mirror/serve.js.map +1 -1
  115. package/dist/runtime/reporting/report-ai.d.ts.map +1 -1
  116. package/dist/runtime/reporting/report-ai.js +1 -0
  117. package/dist/runtime/reporting/report-ai.js.map +1 -1
  118. package/dist/runtime/reporting/stdin.d.ts +2 -0
  119. package/dist/runtime/reporting/stdin.d.ts.map +1 -0
  120. package/dist/runtime/reporting/stdin.js +8 -0
  121. package/dist/runtime/reporting/stdin.js.map +1 -0
  122. package/dist/status-surface.d.ts +167 -0
  123. package/dist/status-surface.d.ts.map +1 -0
  124. package/dist/status-surface.js +670 -0
  125. package/dist/status-surface.js.map +1 -0
  126. package/dist/types.d.ts +17 -0
  127. package/dist/types.d.ts.map +1 -1
  128. package/dist/types.js +3 -0
  129. package/dist/types.js.map +1 -1
  130. package/focus-app/client/_app/immutable/assets/{0.BdzHFb9q.css → 0.CgEVz9YW.css} +1 -1
  131. package/focus-app/client/_app/immutable/assets/0.CgEVz9YW.css.br +0 -0
  132. package/focus-app/client/_app/immutable/assets/0.CgEVz9YW.css.gz +0 -0
  133. package/focus-app/client/_app/immutable/assets/2.DaTmt6o7.css +1 -0
  134. package/focus-app/client/_app/immutable/assets/2.DaTmt6o7.css.br +0 -0
  135. package/focus-app/client/_app/immutable/assets/2.DaTmt6o7.css.gz +0 -0
  136. package/focus-app/client/_app/immutable/assets/3.vWQUAHjz.css +1 -0
  137. package/focus-app/client/_app/immutable/assets/3.vWQUAHjz.css.br +0 -0
  138. package/focus-app/client/_app/immutable/assets/3.vWQUAHjz.css.gz +0 -0
  139. package/focus-app/client/_app/immutable/assets/AppShell.CDPwj3zU.css +1 -0
  140. package/focus-app/client/_app/immutable/assets/AppShell.CDPwj3zU.css.br +0 -0
  141. package/focus-app/client/_app/immutable/assets/AppShell.CDPwj3zU.css.gz +0 -0
  142. package/focus-app/client/_app/immutable/chunks/{Cj6wo28J.js → 1NK_OeRJ.js} +1 -1
  143. package/focus-app/client/_app/immutable/chunks/1NK_OeRJ.js.br +0 -0
  144. package/focus-app/client/_app/immutable/chunks/1NK_OeRJ.js.gz +0 -0
  145. package/focus-app/client/_app/immutable/chunks/{Bs3f1UbD.js → B1veta3r.js} +2 -2
  146. package/focus-app/client/_app/immutable/chunks/B1veta3r.js.br +0 -0
  147. package/focus-app/client/_app/immutable/chunks/B1veta3r.js.gz +0 -0
  148. package/focus-app/client/_app/immutable/chunks/{DEmooz0s.js → BZ4lcC0v.js} +1 -1
  149. package/focus-app/client/_app/immutable/chunks/BZ4lcC0v.js.br +0 -0
  150. package/focus-app/client/_app/immutable/chunks/BZ4lcC0v.js.gz +0 -0
  151. package/focus-app/client/_app/immutable/chunks/Bu7F9NOv.js +2 -0
  152. package/focus-app/client/_app/immutable/chunks/Bu7F9NOv.js.br +0 -0
  153. package/focus-app/client/_app/immutable/chunks/Bu7F9NOv.js.gz +0 -0
  154. package/focus-app/client/_app/immutable/chunks/C6KhNEX3.js +1 -0
  155. package/focus-app/client/_app/immutable/chunks/C6KhNEX3.js.br +0 -0
  156. package/focus-app/client/_app/immutable/chunks/C6KhNEX3.js.gz +0 -0
  157. package/focus-app/client/_app/immutable/chunks/DIljKPJN.js +1 -0
  158. package/focus-app/client/_app/immutable/chunks/DIljKPJN.js.br +0 -0
  159. package/focus-app/client/_app/immutable/chunks/DIljKPJN.js.gz +0 -0
  160. package/focus-app/client/_app/immutable/chunks/iXQate3J.js +2 -0
  161. package/focus-app/client/_app/immutable/chunks/iXQate3J.js.br +0 -0
  162. package/focus-app/client/_app/immutable/chunks/iXQate3J.js.gz +0 -0
  163. package/focus-app/client/_app/immutable/chunks/{ZxFklh9T.js → kCE9wz0g.js} +1 -1
  164. package/focus-app/client/_app/immutable/chunks/kCE9wz0g.js.br +0 -0
  165. package/focus-app/client/_app/immutable/chunks/kCE9wz0g.js.gz +0 -0
  166. package/focus-app/client/_app/immutable/chunks/x-hq5fDG.js +1 -0
  167. package/focus-app/client/_app/immutable/chunks/x-hq5fDG.js.br +0 -0
  168. package/focus-app/client/_app/immutable/chunks/x-hq5fDG.js.gz +0 -0
  169. package/focus-app/client/_app/immutable/entry/app.C1sDdCg5.js +2 -0
  170. package/focus-app/client/_app/immutable/entry/app.C1sDdCg5.js.br +0 -0
  171. package/focus-app/client/_app/immutable/entry/app.C1sDdCg5.js.gz +0 -0
  172. package/focus-app/client/_app/immutable/entry/start.X3j_A5RN.js +1 -0
  173. package/focus-app/client/_app/immutable/entry/start.X3j_A5RN.js.br +2 -0
  174. package/focus-app/client/_app/immutable/entry/start.X3j_A5RN.js.gz +0 -0
  175. package/focus-app/client/_app/immutable/nodes/0.gLl44WzG.js +5 -0
  176. package/focus-app/client/_app/immutable/nodes/0.gLl44WzG.js.br +0 -0
  177. package/focus-app/client/_app/immutable/nodes/0.gLl44WzG.js.gz +0 -0
  178. package/focus-app/client/_app/immutable/nodes/1.DUrghq2x.js +1 -0
  179. package/focus-app/client/_app/immutable/nodes/1.DUrghq2x.js.br +0 -0
  180. package/focus-app/client/_app/immutable/nodes/1.DUrghq2x.js.gz +0 -0
  181. package/focus-app/client/_app/immutable/nodes/2.Bp7JE7Pz.js +1 -0
  182. package/focus-app/client/_app/immutable/nodes/2.Bp7JE7Pz.js.br +0 -0
  183. package/focus-app/client/_app/immutable/nodes/2.Bp7JE7Pz.js.gz +0 -0
  184. package/focus-app/client/_app/immutable/nodes/3.Cml8enOG.js +12 -0
  185. package/focus-app/client/_app/immutable/nodes/3.Cml8enOG.js.br +0 -0
  186. package/focus-app/client/_app/immutable/nodes/3.Cml8enOG.js.gz +0 -0
  187. package/focus-app/client/_app/version.json +1 -1
  188. package/focus-app/client/_app/version.json.br +0 -0
  189. package/focus-app/client/_app/version.json.gz +0 -0
  190. package/focus-app/h2a-focus.json +108 -80
  191. package/focus-app/handler.js +7 -7
  192. package/focus-app/index.js +7 -7
  193. package/focus-app/server/chunks/chunks/AppShell.js-6Pi87dO0.js +1353 -0
  194. package/focus-app/server/chunks/chunks/AppShell.js-6Pi87dO0.js.map +1 -0
  195. package/focus-app/server/chunks/chunks/agent-memory-dossier.js-nmFG_rgx.js +1036 -0
  196. package/focus-app/server/chunks/chunks/agent-memory-dossier.js-nmFG_rgx.js.map +1 -0
  197. package/focus-app/server/chunks/chunks/{exports.js-DUTkELZq.js → exports.js-CdqPbjm1.js} +3 -3
  198. package/focus-app/server/chunks/chunks/{exports.js-DUTkELZq.js.map → exports.js-CdqPbjm1.js.map} +1 -1
  199. package/focus-app/server/chunks/chunks/h2a-bus.js-bLEJ_Adi.js +233 -0
  200. package/focus-app/server/chunks/chunks/h2a-bus.js-bLEJ_Adi.js.map +1 -0
  201. package/focus-app/server/chunks/chunks/{index.js-Di21TltP.js → index.js-laGHLarB.js} +5 -2
  202. package/focus-app/server/chunks/chunks/index.js-laGHLarB.js.map +1 -0
  203. package/focus-app/server/chunks/chunks/{internal.js-33vbd70h.js → internal.js-C6T8nHkl.js} +4 -4
  204. package/focus-app/server/chunks/chunks/{internal.js-33vbd70h.js.map → internal.js-C6T8nHkl.js.map} +1 -1
  205. package/focus-app/server/chunks/chunks/{root.js-DO1m-gJA.js → root.js-CtVQOgDw.js} +2 -2
  206. package/focus-app/server/chunks/chunks/{root.js-DO1m-gJA.js.map → root.js-CtVQOgDw.js.map} +1 -1
  207. package/focus-app/server/chunks/entries/endpoints/api/decisions/inject/{_server.ts.js-j0NNcOSZ.js → _server.ts.js-hrbMnsV0.js} +13 -42
  208. package/focus-app/server/chunks/entries/endpoints/api/decisions/inject/_server.ts.js-hrbMnsV0.js.map +1 -0
  209. package/focus-app/server/chunks/entries/endpoints/api/dossiers/agent-memory/include/_server.ts.js-Dw4odGdV.js +138 -0
  210. package/focus-app/server/chunks/entries/endpoints/api/dossiers/agent-memory/include/_server.ts.js-Dw4odGdV.js.map +1 -0
  211. package/focus-app/server/chunks/entries/endpoints/api/h2a/targets/_server.ts.js-BLjYv7vg.js +41 -0
  212. package/focus-app/server/chunks/entries/endpoints/api/h2a/targets/_server.ts.js-BLjYv7vg.js.map +1 -0
  213. package/focus-app/server/chunks/entries/fallbacks/{error.svelte.js-Dj3sETSX.js → error.svelte.js-RbeaNF5a.js} +4 -4
  214. package/focus-app/server/chunks/entries/fallbacks/{error.svelte.js-Dj3sETSX.js.map → error.svelte.js-RbeaNF5a.js.map} +1 -1
  215. package/focus-app/server/chunks/entries/pages/{_layout.svelte.js-BoLsbuC2.js → _layout.svelte.js-e1E_I04z.js} +2 -2
  216. package/focus-app/server/chunks/entries/pages/{_layout.svelte.js-BoLsbuC2.js.map → _layout.svelte.js-e1E_I04z.js.map} +1 -1
  217. package/focus-app/server/chunks/entries/pages/{_page.svelte.js-QDldMYhk.js → _page.svelte.js-DbaEG23x.js} +31 -1373
  218. package/focus-app/server/chunks/entries/pages/_page.svelte.js-DbaEG23x.js.map +1 -0
  219. package/focus-app/server/chunks/entries/pages/dossier/agent-memory/_page.server.ts.js-CFC-wn6U.js +88 -0
  220. package/focus-app/server/chunks/entries/pages/dossier/agent-memory/_page.server.ts.js-CFC-wn6U.js.map +1 -0
  221. package/focus-app/server/chunks/entries/pages/dossier/agent-memory/_page.svelte.js-BpR26NMa.js +1574 -0
  222. package/focus-app/server/chunks/entries/pages/dossier/agent-memory/_page.svelte.js-BpR26NMa.js.map +1 -0
  223. package/focus-app/server/chunks/{handler-Cp_5hLyY.js → handler-Be3dyNi0.js} +3 -3
  224. package/focus-app/server/chunks/{handler-Cp_5hLyY.js.map → handler-Be3dyNi0.js.map} +1 -1
  225. package/focus-app/server/chunks/{index.js-CBh7Wjqp.js → index.js-siUxMMnK.js} +3 -3
  226. package/focus-app/server/chunks/{index.js-CBh7Wjqp.js.map → index.js-siUxMMnK.js.map} +1 -1
  227. package/focus-app/server/chunks/manifest.js-CXJqSHHn.js +78 -0
  228. package/focus-app/server/chunks/manifest.js-CXJqSHHn.js.map +1 -0
  229. package/focus-app/server/chunks/nodes/0.js-leU6sfRR.js +9 -0
  230. package/focus-app/server/chunks/nodes/{0.js-4wlmdsk8.js.map → 0.js-leU6sfRR.js.map} +1 -1
  231. package/focus-app/server/chunks/nodes/1.js-os6QXm6B.js +9 -0
  232. package/focus-app/server/chunks/nodes/{1.js-B-LpY-lC.js.map → 1.js-os6QXm6B.js.map} +1 -1
  233. package/focus-app/server/chunks/nodes/2.js-DAaLjh93.js +19 -0
  234. package/focus-app/server/chunks/nodes/2.js-DAaLjh93.js.map +1 -0
  235. package/focus-app/server/chunks/nodes/3.js-Cmgi6Uy0.js +18 -0
  236. package/focus-app/server/chunks/nodes/3.js-Cmgi6Uy0.js.map +1 -0
  237. package/package.json +2 -2
  238. package/skills/h2a-run/SKILL.md +100 -0
  239. package/focus-app/client/_app/immutable/assets/0.BdzHFb9q.css.br +0 -2
  240. package/focus-app/client/_app/immutable/assets/0.BdzHFb9q.css.gz +0 -0
  241. package/focus-app/client/_app/immutable/assets/2.Ddxq02y4.css +0 -1
  242. package/focus-app/client/_app/immutable/assets/2.Ddxq02y4.css.br +0 -0
  243. package/focus-app/client/_app/immutable/assets/2.Ddxq02y4.css.gz +0 -0
  244. package/focus-app/client/_app/immutable/chunks/Bs3f1UbD.js.br +0 -0
  245. package/focus-app/client/_app/immutable/chunks/Bs3f1UbD.js.gz +0 -0
  246. package/focus-app/client/_app/immutable/chunks/Bv8OL8ib.js +0 -1
  247. package/focus-app/client/_app/immutable/chunks/Bv8OL8ib.js.br +0 -0
  248. package/focus-app/client/_app/immutable/chunks/Bv8OL8ib.js.gz +0 -0
  249. package/focus-app/client/_app/immutable/chunks/CVq_IFFr.js +0 -2
  250. package/focus-app/client/_app/immutable/chunks/CVq_IFFr.js.br +0 -0
  251. package/focus-app/client/_app/immutable/chunks/CVq_IFFr.js.gz +0 -0
  252. package/focus-app/client/_app/immutable/chunks/Cj6wo28J.js.br +0 -0
  253. package/focus-app/client/_app/immutable/chunks/Cj6wo28J.js.gz +0 -0
  254. package/focus-app/client/_app/immutable/chunks/DEmooz0s.js.br +0 -0
  255. package/focus-app/client/_app/immutable/chunks/DEmooz0s.js.gz +0 -0
  256. package/focus-app/client/_app/immutable/chunks/DQIRXgdC.js +0 -2
  257. package/focus-app/client/_app/immutable/chunks/DQIRXgdC.js.br +0 -0
  258. package/focus-app/client/_app/immutable/chunks/DQIRXgdC.js.gz +0 -0
  259. package/focus-app/client/_app/immutable/chunks/ZxFklh9T.js.br +0 -0
  260. package/focus-app/client/_app/immutable/chunks/ZxFklh9T.js.gz +0 -0
  261. package/focus-app/client/_app/immutable/chunks/_wRuVGPs.js +0 -1
  262. package/focus-app/client/_app/immutable/chunks/_wRuVGPs.js.br +0 -0
  263. package/focus-app/client/_app/immutable/chunks/_wRuVGPs.js.gz +0 -0
  264. package/focus-app/client/_app/immutable/entry/app.6DKouWRt.js +0 -2
  265. package/focus-app/client/_app/immutable/entry/app.6DKouWRt.js.br +0 -0
  266. package/focus-app/client/_app/immutable/entry/app.6DKouWRt.js.gz +0 -0
  267. package/focus-app/client/_app/immutable/entry/start.DlLmkUb7.js +0 -1
  268. package/focus-app/client/_app/immutable/entry/start.DlLmkUb7.js.br +0 -2
  269. package/focus-app/client/_app/immutable/entry/start.DlLmkUb7.js.gz +0 -0
  270. package/focus-app/client/_app/immutable/nodes/0.CuSOF0Of.js +0 -5
  271. package/focus-app/client/_app/immutable/nodes/0.CuSOF0Of.js.br +0 -0
  272. package/focus-app/client/_app/immutable/nodes/0.CuSOF0Of.js.gz +0 -0
  273. package/focus-app/client/_app/immutable/nodes/1.D39jdnqm.js +0 -1
  274. package/focus-app/client/_app/immutable/nodes/1.D39jdnqm.js.br +0 -0
  275. package/focus-app/client/_app/immutable/nodes/1.D39jdnqm.js.gz +0 -0
  276. package/focus-app/client/_app/immutable/nodes/2.CnU8ifRl.js +0 -1
  277. package/focus-app/client/_app/immutable/nodes/2.CnU8ifRl.js.br +0 -0
  278. package/focus-app/client/_app/immutable/nodes/2.CnU8ifRl.js.gz +0 -0
  279. package/focus-app/server/chunks/chunks/index.js-Di21TltP.js.map +0 -1
  280. package/focus-app/server/chunks/entries/endpoints/api/decisions/inject/_server.ts.js-j0NNcOSZ.js.map +0 -1
  281. package/focus-app/server/chunks/entries/pages/_page.svelte.js-QDldMYhk.js.map +0 -1
  282. package/focus-app/server/chunks/manifest.js-Cm6cC7mC.js +0 -56
  283. package/focus-app/server/chunks/manifest.js-Cm6cC7mC.js.map +0 -1
  284. package/focus-app/server/chunks/nodes/0.js-4wlmdsk8.js +0 -9
  285. package/focus-app/server/chunks/nodes/1.js-B-LpY-lC.js +0 -9
  286. package/focus-app/server/chunks/nodes/2.js-F6jp5eD9.js +0 -19
  287. package/focus-app/server/chunks/nodes/2.js-F6jp5eD9.js.map +0 -1
@@ -0,0 +1,777 @@
1
+ /**
2
+ * h2a's side of the PRINCIPAL↔AGENT enrollment ceremony (P1, step 3).
3
+ *
4
+ * Implements Part B of the ratified contract
5
+ * `docs/superpowers/specs/2026-07-24-h2a-feed-contract-for-sentropic.md`
6
+ * (RATIFIED by the sentropic architect 2026-07-24, amended 2026-07-25 and again
7
+ * 2026-07-25 by the SIGNED-COMPOSITE amendment recorded in Part B), and step 3 of
8
+ * `docs/specs/2026-07-25-p1-joint-plan-h2a-sessions-in-sentropic-ui.md`.
9
+ *
10
+ * AUTHORITY MODEL — read this before touching anything below.
11
+ * ----------------------------------------------------------
12
+ * **The 39-auth PRINCIPAL is the authorizing authority. This module only
13
+ * proves that the local agent controls its ed25519 key.** A valid signature
14
+ * proves *authorship* (this key produced this payload); it NEVER proves
15
+ * *authorization* (this key may appear in this principal's feed). Those are two
16
+ * different checks and the code keeps them structurally separate:
17
+ *
18
+ * - h2a produces a proof of key control. That is the whole of h2a's part.
19
+ * - sentropic decides what the proof authorizes, by looking up an ACTIVE
20
+ * `(principalSub, agentPubKey)` binding row in ITS OWN store.
21
+ *
22
+ * **SIGNED ≠ AUTHORIZED.** The signature now covers `instance` as well as the
23
+ * nonce and the key (see {@link enrollmentProofSignedPayload}). That makes the
24
+ * *provenance* of `instance` trustworthy — it does **NOT** make `instance` an
25
+ * authorization input, and nothing downstream may start treating it as one.
26
+ * Authorization stays the active-binding lookup on
27
+ * `(principalSub, agentPubKey)`, with the instance re-resolved live at read time
28
+ * (Part B: `agentInstanceIdAtBinding` is "provenance only — NEVER re-used as
29
+ * authority at read time"). This is the same line as authorship ≠ authorization,
30
+ * one field further in: a signature widens what you may *believe about the
31
+ * payload*, never what the payload may *reach*.
32
+ *
33
+ * Four consequences that are load-bearing, not stylistic:
34
+ *
35
+ * 1. **No binding store here.** h2a has no concept of a 39-auth principal, so
36
+ * it cannot own the binding record (`H2APrincipalAgentBinding` — Part B,
37
+ * "Binding record": owned and stored by sentropic). Nothing in this module
38
+ * writes, caches or reads a binding.
39
+ * 2. **The agent never receives a principal identifier at all.** The challenge
40
+ * type has no `principalSub`, and the CLI refuses a challenge document that
41
+ * carries one. The agent signs a nonce; it has no functional need for the
42
+ * principal's id, and putting one into an agent process and context window
43
+ * buys nothing. *Minimal disclosure beats verified non-retention* — so this
44
+ * is enforced at receipt, not merely proven absent from the output.
45
+ * 3. **Every field the proof carries is signed.** A proof must attest to
46
+ * everything it carries: a reader sees a signature and reasonably infers it
47
+ * covers the payload, so an unsigned-but-present field is a claim wider than
48
+ * its evidence — in the one artifact whose whole job is to be exactly as wide
49
+ * as its evidence. If a field does not deserve signing, it must be REMOVED
50
+ * from the proof rather than carried unsigned. Pinned by a test that derives
51
+ * the covered set from the proof's own keys.
52
+ * 4. **No outward transport.** This module never opens a socket. Not behind a
53
+ * flag, not behind a default-off flag. The proof is returned to the caller;
54
+ * delivering it to the gateway is the gateway lane's step, and the seam for
55
+ * it ({@link EnrollmentProofSubmitter}) has NO default implementation — an
56
+ * unsupplied submitter is reported as such, never silently attempted.
57
+ *
58
+ * Per the amendment accepted after ratification (joint plan, finding 2),
59
+ * enrollment on the AUTH side requires an active **first-party session** and is
60
+ * deliberately unreachable by bearer tokens — otherwise any relying party the
61
+ * owner ever consented to could mint a durable binding for its own key,
62
+ * escalating "read the owner's data" into "permanently be the owner's agent".
63
+ * That gate is the auth lane's. Nothing here assumes, carries or accepts a
64
+ * bearer token; there is no credential input to this module at all.
65
+ *
66
+ * REUSE, not new crypto: `signCanonical` / `verifyCanonical`
67
+ * (`packages/h2a/src/signature.ts`) over the existing identity keypair at
68
+ * `<root>/keys/<instance>.key.pem` — the same primitive already used for reclaim
69
+ * proof-of-possession (`runtime/identity/live.ts` `provesLocalKey`,
70
+ * `runtime/identity/bindings.ts` `verifyReclaimProof`). `signCanonical` already
71
+ * takes `unknown`, so signing a composite is the same primitive with a different
72
+ * argument: no new key, no new algorithm, no new file, no extra round-trip.
73
+ *
74
+ * ONE EXCEPTION CONSIDERED AND REFUSED — recorded so nobody re-litigates it.
75
+ * -------------------------------------------------------------------------
76
+ * A natural-looking proposal is to WARN when a challenge has an inherited
77
+ * `expiresAt` it does not carry —
78
+ * `!Object.hasOwn(c, "expiresAt") && "expiresAt" in c` — on the grounds that it is
79
+ * diagnostic only and never control flow. **It was considered and REFUSED.**
80
+ *
81
+ * A diagnostic-only prototype-chain read is exactly the shape of harmless-looking
82
+ * thing that a later refactor promotes into control flow, and the module's rule is
83
+ * stronger as an ABSOLUTE — *this module never reads through the prototype chain* —
84
+ * than as *"never, except for warnings"*. An absolute is verifiable by reading the
85
+ * file; an exception has to be policed forever. Refusing the exception once is
86
+ * cheaper than guarding it indefinitely, so it is refused here rather than
87
+ * admitted and watched. Do not add it back without reopening this with the
88
+ * approver.
89
+ */
90
+ import { createPrivateKey } from "node:crypto";
91
+ import { existsSync, readFileSync, readdirSync } from "node:fs";
92
+ import { join } from "node:path";
93
+ // Siblings and leaf primitives are imported RELATIVELY. `index.ts` re-exports
94
+ // this barrel, so importing `@sentropic/h2a` here would close a self-referential
95
+ // cycle through the package root.
96
+ import { signCanonical, verifyCanonical } from "../../signature.js";
97
+ import { H2A_CLI_DECLARED_CAPABILITIES, publicKeyFingerprint, resolveLiveIdentity } from "../identity/live.js";
98
+ import { createLocalStore } from "../local-files/store.js";
99
+ /**
100
+ * THE NONCE BRACKET — four bounds, each labelled with what it is FOR.
101
+ * =================================================================
102
+ *
103
+ * The general form, which is the correction to the first draft of this contract:
104
+ * **a positive specification does not mean a single value — it means every
105
+ * accepted-set boundary is stated, and each one says what it is for.** A floor
106
+ * that protects strength and a ceiling that protects against blobs are different
107
+ * parameters and must not be described in the same breath. The first attempt
108
+ * specified `fixed ~43`, which pinned an issuer that DOES NOT EXIST YET to an
109
+ * entropy choice made on its behalf: if the auth lane later picks 384 or 512 bits
110
+ * — the *safer* choice — a fixed verifier turns their improvement into our
111
+ * outage.
112
+ *
113
+ * 1. {@link H2A_ENROLLMENT_NONCE_PATTERN} — alphabet base64url.
114
+ * **POSITIVE, SECURITY-BEARING.**
115
+ * 2. {@link H2A_ENROLLMENT_NONCE_MIN_BITS} / {@link H2A_ENROLLMENT_NONCE_MIN_LENGTH}
116
+ * — minimum 256 bits. **POSITIVE, SECURITY-BEARING.**
117
+ * 3. {@link H2A_ENROLLMENT_NONCE_MAX_BITS} / {@link H2A_ENROLLMENT_NONCE_MAX_LENGTH}
118
+ * — maximum 1024 bits. **SANITY CEILING, EXPLICITLY NOT A SECURITY
119
+ * PARAMETER.** It means *"beyond this it is not a nonce"*. It must NEVER be
120
+ * read as *"this much entropy is enough"* — the floor is the only bound that
121
+ * speaks about strength.
122
+ * 4. {@link H2A_ENROLLMENT_MAX_NONCE_LENGTH} — 4096 chars, pre-parse DoS guard
123
+ * only. Not a definition of anything.
124
+ */
125
+ /**
126
+ * Minimum entropy a gateway nonce must carry. **POSITIVE, SECURITY-BEARING.**
127
+ * Below this a "nonce" is guessable, and a guessable challenge is one an attacker
128
+ * can pre-compute a proof for.
129
+ */
130
+ export const H2A_ENROLLMENT_NONCE_MIN_BITS = 256;
131
+ /**
132
+ * Upper end of the bracket. **SANITY CEILING — NOT A SECURITY PARAMETER.**
133
+ *
134
+ * It says "beyond this it is not a nonce", nothing about sufficiency.
135
+ *
136
+ * Set at 1024 rather than 512 for HEADROOM — and the arithmetic has to be stated
137
+ * correctly, because an earlier draft of this comment said "88 base64url
138
+ * characters" and then drew a conclusion that only the wrong number supports.
139
+ * Base64url packs 6 bits per character, so a perfectly reasonable 64-byte nonce is
140
+ * 512 bits / **86** characters — and a 512-bit ceiling derives to `ceil(512 / 6) =
141
+ * 86` as well. It would therefore land EXACTLY on such a nonce and ACCEPT it, not
142
+ * reject it. So "512 would reject a 64-byte nonce" was never true; the real
143
+ * objection is that it leaves ZERO margin. A bound never meant to bound entropy
144
+ * must not sit flush against a legitimate value, where one more byte of issuer
145
+ * entropy — or base64 padding the issuer does not strip — turns a sanity bound
146
+ * into an outage. 1024 buys that margin. The corrected number does not weaken the
147
+ * choice; it removes a justification that misreported its own arithmetic, which is
148
+ * the defect class this module exists to argue against.
149
+ */
150
+ export const H2A_ENROLLMENT_NONCE_MAX_BITS = 1024;
151
+ /**
152
+ * The nonce's accepted alphabet, stated POSITIVELY: base64url characters only.
153
+ * **POSITIVE, SECURITY-BEARING** — it is what refuses free text, JSON, a URL, or
154
+ * a message borrowed from some other protocol, rather than enumerating those.
155
+ */
156
+ export const H2A_ENROLLMENT_NONCE_PATTERN = /^[A-Za-z0-9_-]+$/;
157
+ /**
158
+ * Minimum nonce length, DERIVED not asserted: base64url packs 6 bits per
159
+ * character, so `ceil(256 / 6) = 43` characters is the 256-bit floor.
160
+ * **POSITIVE, SECURITY-BEARING.**
161
+ *
162
+ * A MINIMUM, never an exact length — see the bracket note above for why pinning
163
+ * an as-yet-unbuilt issuer to one value is the error this replaces.
164
+ */
165
+ export const H2A_ENROLLMENT_NONCE_MIN_LENGTH = Math.ceil(H2A_ENROLLMENT_NONCE_MIN_BITS / 6);
166
+ /**
167
+ * Maximum nonce length, derived the same way: `ceil(1024 / 6) = 171` characters.
168
+ * **SANITY CEILING, NOT A SECURITY PARAMETER** — see
169
+ * {@link H2A_ENROLLMENT_NONCE_MAX_BITS}.
170
+ */
171
+ export const H2A_ENROLLMENT_NONCE_MAX_LENGTH = Math.ceil(H2A_ENROLLMENT_NONCE_MAX_BITS / 6);
172
+ /**
173
+ * Cheap PRE-PARSE cap, and nothing more.
174
+ *
175
+ * A DoS sanity bound, not a definition of a nonce — the definition is the
176
+ * alphabet plus the floor, with the ceiling as a separate sanity bound. Kept
177
+ * distinct so none of the four is mistaken for another.
178
+ */
179
+ export const H2A_ENROLLMENT_MAX_NONCE_LENGTH = 4096;
180
+ /**
181
+ * The ONLY keys a challenge document may carry. An ALLOWLIST, not a blocklist.
182
+ *
183
+ * The nonce was specified positively while the challenge *object* was left
184
+ * specified negatively — a blocklist of one key, `principalSub`. That control did
185
+ * not cover the harm it was written for: the harm is *"a principal id reaching an
186
+ * agent process and context window"*, and `{ nonce, meta: { principalSub } }` or
187
+ * `{ nonce, "__proto__": { principalSub } }` both do exactly that while passing a
188
+ * top-level `"principalSub" in challenge` check. A blocklist of one key stops one
189
+ * spelling of one field; nesting is a different spelling.
190
+ *
191
+ * So the same positive-specification move is applied one level up: only `nonce`
192
+ * and `expiresAt` may appear, both must be strings, and anything else is refused
193
+ * without needing to know what it means. No nesting is reachable, so no
194
+ * deep scan for a forbidden name is needed — that would be the blocklist again,
195
+ * one level deeper.
196
+ */
197
+ export const H2A_ENROLLMENT_CHALLENGE_KEYS = ["nonce", "expiresAt"];
198
+ /**
199
+ * The domain-separation tag carried and signed by every proof — see
200
+ * {@link H2AEnrollmentProof.type}.
201
+ *
202
+ * Versioned so a format change is distinguishable rather than silently
203
+ * reinterpreted. Bump this and the verifier stops accepting the old shape, which
204
+ * is the entire point of it being here.
205
+ */
206
+ export const H2A_ENROLLMENT_PROOF_TYPE = "h2a-enrollment-proof-v1";
207
+ /**
208
+ * The exact object the signature covers: **every field the proof carries except
209
+ * the signature itself**.
210
+ *
211
+ * Single definition on purpose. The gateway's verification (Part B flow step 5b,
212
+ * as amended) is `verifyCanonical(enrollmentProofSignedPayload(proof),
213
+ * proof.signature, proof.publicKeyPem)`, and field ordering is irrelevant
214
+ * because `canonicalize` normalizes it — so an independent implementation cannot
215
+ * disagree about what was signed by guessing at key order.
216
+ *
217
+ * The rule is STRUCTURAL, not asserted. Coverage is a rest-spread that removes
218
+ * exactly one field, so "every field except the signature" is what the code
219
+ * *does* rather than a list that has to be kept in step — the same shape as
220
+ * `envelope.ts` `envelopeSigningView` (`const { signatures: _omit, ...rest }`).
221
+ * Three things follow, and the third is why this beats an enumeration:
222
+ *
223
+ * 1. `Omit<…, "signature">` on the parameter makes **tsc** reject a caller that
224
+ * does not hold every non-signature field, so a new required field on
225
+ * {@link H2AEnrollmentProof} is a compile error, not a test failure.
226
+ * 2. A new field flows into the signature automatically. There is no second list
227
+ * to forget.
228
+ * 3. A field carried on the proof but NOT signed becomes impossible to emit:
229
+ * signing sees the unsigned view, verification re-derives it from the finished
230
+ * proof, so any extra field makes the two disagree and
231
+ * {@link signEnrollmentChallenge}'s self-verification throws. The rule holds
232
+ * even with every test deleted.
233
+ *
234
+ * The keys test is kept anyway — it documents the intent and costs nothing.
235
+ */
236
+ export function enrollmentProofSignedPayload(
237
+ // `Omit<…, "signature">` states the rule in the type itself: the signature is
238
+ // the ONE field a signature cannot cover, and the only field this function is
239
+ // allowed not to see. A full proof satisfies this parameter too, which is why
240
+ // `signature` is accepted-and-stripped rather than merely absent: verification
241
+ // hands in the finished proof.
242
+ proof) {
243
+ const { signature: _omit, ...rest } = proof;
244
+ return rest;
245
+ }
246
+ /**
247
+ * Verify a proof the way the gateway must: the signature over the whole
248
+ * composite, checked against the key the proof ships.
249
+ *
250
+ * **This answers "did this key produce this payload", and NOTHING else.** It is
251
+ * not an authorization check and must never be used as one: a `true` here on a
252
+ * key with no active binding row still authorizes nothing (Part B, fail-closed
253
+ * item 3 — "verifying the signature must never, by itself, cause any row to be
254
+ * returned"). Provided so the two lanes verify the same bytes, not so a caller
255
+ * can shortcut a binding lookup.
256
+ */
257
+ export function verifyEnrollmentProof(proof) {
258
+ // CHECK THE TAG, or it is decoration. A signature over a payload whose `type`
259
+ // says something else is a valid signature over a DIFFERENT message, and this
260
+ // verifier speaks v1 only — a `-v2` proof must fail here rather than be
261
+ // reinterpreted as v1. That is what versioning the tag is for.
262
+ if (proof.type !== H2A_ENROLLMENT_PROOF_TYPE)
263
+ return false;
264
+ return verifyCanonical(enrollmentProofSignedPayload(proof), proof.signature, proof.publicKeyPem);
265
+ }
266
+ /**
267
+ * Refuse to prove control of a key h2a itself does not list as ACTIVE.
268
+ *
269
+ * h2a-side key validity is *necessary but not sufficient* for exposure — the
270
+ * binding governs that (Part B, fail-closed item 4) — but a key h2a considers
271
+ * revoked must never be offered up for a NEW binding.
272
+ *
273
+ * This is DEFENCE IN DEPTH, and its call site is unreachable in practice: when
274
+ * the live key has been revoked, `resolveLiveIdentity`'s reclaim proof
275
+ * (`provesLocalKey`, which requires at least one active key) fails, so it MINTS
276
+ * a fresh identity instead of returning the revoked one — verified by the
277
+ * "a locally revoked key is never the key proved" test. The guard exists so that
278
+ * remains a CHECKED fact rather than a relied-upon one, and it is exported so it
279
+ * can be exercised directly instead of sitting untested behind a path that
280
+ * cannot reach it.
281
+ *
282
+ * An empty key list is an ERROR here, never a silent pass: "no active keys" is
283
+ * not the fact "this key is fine".
284
+ */
285
+ export function assertKeyIsLocallyActive(input) {
286
+ if (input.activeKeys.length === 0) {
287
+ throw new Error(`h2a enrollment: instance "${input.instance}" has NO active key in the local registry — ` +
288
+ "refusing to prove control of a key h2a itself does not list as active");
289
+ }
290
+ if (!input.activeKeys.includes(input.publicKeyPem)) {
291
+ throw new Error(`h2a enrollment: the resolved public key of "${input.instance}" is not active in the local ` +
292
+ "registry (revoked, or superseded) — re-run `h2a connect` before enrolling");
293
+ }
294
+ }
295
+ /**
296
+ * Resolve the CURRENT live identity and load its keypair.
297
+ *
298
+ * Why this resolves live, every single time, and takes no instance id:
299
+ * **a memory that names an instance-id ROTS.** That is a documented failure in
300
+ * this project (a stale recorded mapping sent a consultation to the wrong
301
+ * instance), and it is the same failure the identity re-anchor of 2026-06-07
302
+ * created for enrollment: the stability unit moved from `(host, workspaceId)`
303
+ * to the provider conversation UUID, so an agent enrolled before it now
304
+ * presents a DIFFERENT instance handle and a DIFFERENT keypair (Part B,
305
+ * "Re-enrollment of a post-re-anchor key"). The previously enrolled
306
+ * `agentPubKey` is simply no longer produced by any live agent — which is why a
307
+ * push today is rejected 401.
308
+ *
309
+ * So there is deliberately **no `instance` parameter anywhere in this module's
310
+ * public surface**. The id cannot be passed in, therefore a stale one cannot be
311
+ * used. `resolveLiveIdentity` is called without `explicitInstance` (which would
312
+ * short-circuit before any keypair exists — `identity/live.ts`), and the key is
313
+ * then read from the paths THAT resolution returned.
314
+ *
315
+ * WHAT ACTUALLY HAPPENS WHEN THE LOCAL KEY IS UNUSABLE — stated because the
316
+ * obvious reading ("it fails closed") is wrong, and an overstated guard comment
317
+ * is worse than none:
318
+ *
319
+ * - **Corrupt, truncated or passphrase-protected private key** → `provesLocalKey`
320
+ * catches its own failure and returns `false`, so `resolveLiveIdentity` MINTS a
321
+ * fresh identity. The ceremony then succeeds, with exit 0, proving control of a
322
+ * BRAND-NEW key — the corrupt file is left on disk and the old instance stays
323
+ * listed active. It does not fail, and it does not prove the damaged key. The
324
+ * mint is the same self-healing mechanism as the revoked-key case, but here it
325
+ * would MASK TAMPERING, so silence is the defect: the CLI names any unusable
326
+ * key file on stderr before this runs (see {@link listUnusablePrivateKeys}),
327
+ * and `identityAction` reports the mint. Pinned by an OBSERVED BEHAVIOUR test.
328
+ * - **Unreadable private key (EACCES)** → `readKeypair` reads the same file
329
+ * earlier, inside `resolveLiveIdentity` and outside any `try`, so the raw errno
330
+ * escapes from there rather than from the guard below. That is why the whole
331
+ * resolution call is wrapped: the failure is re-thrown with the root named,
332
+ * instead of surfacing as a bare `EACCES` with no indication of which store it
333
+ * came from.
334
+ * - **The two guards below are NARROWING, not behaviour.** The
335
+ * `privateKeyPath === undefined` branch exists because the type is optional
336
+ * (`explicitInstance` is the only resolution path that omits the paths, and
337
+ * this function never passes one), and the `readFileSync` branch is belt and
338
+ * braces behind the earlier read. Neither is reachable on this path; they are
339
+ * not the reason a broken key is safe. {@link assertKeyIsLocallyActive} is in
340
+ * the same position and says so itself.
341
+ *
342
+ * DOCUMENTED LIMIT on what the returned `instance` contains: an instance id is
343
+ * `<host>:<label>:<uuid>` and its label is the host-native session name or the
344
+ * workspace directory's BASENAME (`identity/live.ts` `deriveInstanceId`). So the
345
+ * proof carries a workspace *label*, which Part A's opacity boundary explicitly
346
+ * permits ("never a filesystem path beyond a human label") — but never a path.
347
+ * It is free text the owner controls, so a consumer must escape it like any
348
+ * user content; see joint plan §9.
349
+ */
350
+ export function resolveEnrollmentIdentity(input) {
351
+ let identity;
352
+ try {
353
+ identity = resolveLiveIdentity({
354
+ root: input.root,
355
+ host: input.host,
356
+ cwd: input.cwd,
357
+ // Display-only list, exactly as `h2a connect` declares it. Never an
358
+ // authorization input (feed contract ratification condition #3).
359
+ declaredCapabilities: H2A_CLI_DECLARED_CAPABILITIES
360
+ // No `explicitInstance`: see the doc comment. A named id is the rot.
361
+ });
362
+ }
363
+ catch (error) {
364
+ // `resolveLiveIdentity` reads the keypair internally, outside any try, so an
365
+ // unreadable key surfaces HERE as a bare errno. Name the source rather than
366
+ // letting an `EACCES` escape with no indication of which store produced it.
367
+ throw new Error(`h2a enrollment: live identity resolution failed under root "${input.root}" ` +
368
+ `(${error.message})`);
369
+ }
370
+ // NARROWING, not a fail-closed behaviour: the paths are optional on the type
371
+ // because `explicitInstance` resolution omits them, and this function never
372
+ // passes one. See the doc comment for what actually happens to a broken key.
373
+ if (identity.privateKeyPath === undefined || identity.publicKeyPath === undefined) {
374
+ throw new Error(`h2a enrollment: live identity resolution for "${identity.instance}" returned no keypair ` +
375
+ `(action=${identity.action})`);
376
+ }
377
+ let privateKeyPem;
378
+ let publicKeyPem;
379
+ try {
380
+ privateKeyPem = readFileSync(identity.privateKeyPath, "utf8");
381
+ publicKeyPem = readFileSync(identity.publicKeyPath, "utf8");
382
+ }
383
+ catch (error) {
384
+ // Belt and braces behind the read that already happened inside the
385
+ // resolution above; unreachable on this path.
386
+ throw new Error(`h2a enrollment: cannot read the identity keypair of "${identity.instance}" ` +
387
+ `(${error.message})`);
388
+ }
389
+ assertKeyIsLocallyActive({
390
+ instance: identity.instance,
391
+ publicKeyPem,
392
+ activeKeys: createLocalStore({ root: input.root }).listInstanceKeys(identity.instance)
393
+ });
394
+ return {
395
+ instance: identity.instance,
396
+ privateKeyPem,
397
+ publicKeyPem,
398
+ identityAction: identity.action
399
+ };
400
+ }
401
+ /**
402
+ * Every private key file under `<root>/keys` that exists but cannot be loaded as
403
+ * a private key — corrupt, truncated, or passphrase-protected.
404
+ *
405
+ * This exists because of what does NOT happen when the live key is one of those:
406
+ * live resolution's reclaim proof fails silently and MINTS a fresh identity, so
407
+ * the ceremony succeeds with exit 0 on a brand-new key while a damaged file sits
408
+ * on disk. The mint is defensible; doing it silently is not, because it is
409
+ * indistinguishable from tampering. So the caller can name the file.
410
+ *
411
+ * Read-only and total: it never throws, and a missing `keys` directory yields an
412
+ * empty list from an EXPLICIT branch — "there is no key directory" is an
413
+ * established fact, not a default.
414
+ */
415
+ export function listUnusablePrivateKeys(root) {
416
+ const keysDir = join(root, "keys");
417
+ if (!existsSync(keysDir))
418
+ return [];
419
+ let entries;
420
+ try {
421
+ entries = readdirSync(keysDir);
422
+ }
423
+ catch {
424
+ // The directory exists but cannot be listed: report nothing rather than
425
+ // claim health. The caller's real check is the ceremony itself.
426
+ return [];
427
+ }
428
+ const unusable = [];
429
+ for (const entry of entries) {
430
+ if (!entry.endsWith(".key.pem"))
431
+ continue;
432
+ const path = join(keysDir, entry);
433
+ try {
434
+ createPrivateKey({ key: readFileSync(path, "utf8"), format: "pem" });
435
+ }
436
+ catch {
437
+ unusable.push(path);
438
+ }
439
+ }
440
+ return unusable.sort();
441
+ }
442
+ /**
443
+ * What a challenge document actually CARRIES: its allowlisted OWN properties,
444
+ * copied once into a fresh null-prototype object.
445
+ *
446
+ * WHY OWN-NESS IS ESTABLISHED HERE AND NOT AT EACH CONSUMER — this is the
447
+ * generalisation of a defect that was first fixed one field at a time, and the
448
+ * reason it is now structural.
449
+ *
450
+ * `Object.keys` (the allowlist) cannot see an inherited field, but plain property
451
+ * access (`challenge.expiresAt`) walks the prototype chain. So the allowlist and
452
+ * the consumers can disagree about what the document says. Guarding each consumer
453
+ * with `Object.hasOwn` only fixes the consumers you remembered: the first version
454
+ * of this code remembered `nonce` and forgot `expiresAt`, so an own `nonce` plus an
455
+ * INHERITED `expiresAt` was read by the validator *and* copied in as an own field
456
+ * by the sanitizer. **An allowlist is only as strong as the accessor the consumer
457
+ * uses afterwards** — and a rule applied to one of two allowlisted fields is a
458
+ * habit, not a guarantee.
459
+ *
460
+ * The durable fix is therefore not a second `hasOwn` at the second consumer. It is
461
+ * to make own-ness a property of the OBJECT everything downstream reads. The two
462
+ * reads below are the only prototype-chain-capable reads of a challenge left in
463
+ * this module, each immediately gated by the `Object.hasOwn` above it; after this
464
+ * function returns, its result has no prototype, so no later read — including one
465
+ * a future edit adds — can walk a chain. Validation and consumption cannot diverge
466
+ * again because there is only one object left for them to disagree about.
467
+ *
468
+ * Refuses rather than trims: an unexpected key, or a `nonce` that is not the
469
+ * document's own, throws here. Field VALUES are not checked yet — that is
470
+ * {@link assertCarriedChallengeIsSignable}, which reads only this copy.
471
+ */
472
+ function carriedChallengeFields(challenge) {
473
+ // ALLOWLIST FIRST, over OWN ENUMERABLE KEYS — `Object.keys`, never `in`.
474
+ //
475
+ // `in` walks the prototype chain, so an allowlist built on it would ask the
476
+ // wrong question twice over: it would miss an own `"__proto__"` key (which
477
+ // `JSON.parse` DEFINES as an own property rather than reassigning the
478
+ // prototype) while being confused by inherited names. `Object.keys` sees
479
+ // exactly what a parsed document actually carries. That makes the
480
+ // `__proto__` case — a prototype-pollution vector on a `JSON.parse` result, not
481
+ // merely a disclosure one — refused rather than ignored.
482
+ //
483
+ // REFUSE-THE-REST MEANS REFUSE, NOT IGNORE. Silently dropping unknown fields
484
+ // would let a document that says something we do not understand be treated as
485
+ // one that says nothing.
486
+ //
487
+ // This runs on the LIBRARY path, not only in the CLI: a TS caller cannot add an
488
+ // excess key to a typed literal, but a parsed document can, and every entry
489
+ // point validates its own input rather than assuming an upstream check.
490
+ const unexpected = Object.keys(challenge).filter((key) => !H2A_ENROLLMENT_CHALLENGE_KEYS.includes(key));
491
+ if (unexpected.length > 0) {
492
+ const named = unexpected.map((key) => `"${key}"`).join(", ");
493
+ throw new Error(`h2a enrollment: challenge carries unexpected field(s) ${named} — only ` +
494
+ `${H2A_ENROLLMENT_CHALLENGE_KEYS.join(" and ")} are accepted. The agent signs a nonce; ` +
495
+ "any other field is refused so that a principal identifier (or anything else) cannot ride " +
496
+ "in nested inside one, which is what a top-level principalSub check alone would miss" +
497
+ (unexpected.includes("principalSub")
498
+ ? ". principalSub in particular MUST NOT be sent to the agent: the gateway already knows " +
499
+ "which session it issued the nonce to"
500
+ : ""));
501
+ }
502
+ // An inherited nonce carries nothing of its own: it sails through the allowlist
503
+ // (`Object.keys` is empty) and would then be signed anyway. Refused by name
504
+ // here, before the copy, so the message says what is actually wrong rather than
505
+ // surfacing later as "no nonce" on a copy that never received one.
506
+ if (!Object.hasOwn(challenge, "nonce")) {
507
+ throw new Error("h2a enrollment: the challenge carries no nonce of its own — an inherited value is not a " +
508
+ "carried field, and only carried fields are signed");
509
+ }
510
+ const carried = Object.create(null);
511
+ carried.nonce = challenge.nonce;
512
+ // THE SAME QUESTION, asked of the other allowlisted field — this is the field the
513
+ // one-at-a-time fix missed. An inherited `expiresAt` is not carried, so it is not
514
+ // copied, so nothing downstream can see it. The extra `!== undefined` keeps an
515
+ // own-but-undefined `expiresAt` from becoming a key, preserving the "absent
516
+ // optional stays absent" property of the sanitized object.
517
+ //
518
+ // THE FRAMING THIS BEHAVIOUR MUST BE READ IN. Two inputs now pass that once threw:
519
+ // an inherited PAST `expiresAt` (once refused as expired) and an inherited
520
+ // non-string `expiresAt` (once refused as not an instant). This is the line a
521
+ // future auditor will try to "restore", so the correct description is recorded at
522
+ // the field rather than left to be rediscovered. It is NOT *"used to refuse, now
523
+ // accepts"*. It is: **an inherited field is not carried, therefore not present; the
524
+ // behaviour follows from the carriage rule and is not a special case for
525
+ // `expiresAt`.** Nothing was relaxed about expiry — the set of fields that exist
526
+ // got smaller, and every rule about expiry still applies in full to every expiry
527
+ // that is actually there.
528
+ //
529
+ // THE CONSEQUENCE OF RESTORING THE REFUSAL, stated explicitly because it is the
530
+ // whole cost: **restoring the refusal means reintroducing a second reader.** To
531
+ // refuse an inherited value you must first look at it, and looking at it is a
532
+ // prototype-chain read of the caller's object — precisely the divergence between
533
+ // the allowlist and its consumers that this function exists to remove. You cannot
534
+ // say *"I never read inherited fields, except to reject them"* without being two
535
+ // readers again. The refusal and the guarantee cannot both be had; this module
536
+ // keeps the guarantee.
537
+ //
538
+ // THE SECURITY CHECK, on the record because this flip is TOWARD ACCEPTANCE and
539
+ // that direction earns one. The agent-side expiry check is ADVISORY by the
540
+ // architect's own ruling, and the GATEWAY REMAINS THE TTL AUTHORITY (Part B flow
541
+ // step 5a). An attacker who suppresses the advisory check by hanging `expiresAt`
542
+ // on a prototype must ALREADY CONTROL the challenge object, and the authoritative
543
+ // check is server-side and wholly unaffected by anything reachable from here. So
544
+ // the trade is: one bypassable defence-in-depth layer, against a party who already
545
+ // owns the input, in exchange for eliminating an entire defect class — validator
546
+ // and consumer disagreeing about what a document says.
547
+ //
548
+ // THE ASYMMETRY WITH `nonce` IS PRINCIPLED, NOT INCONSISTENT. An inherited `nonce`
549
+ // throws just above; an inherited `expiresAt` is ignored here. Both fields collapse
550
+ // *inherited* to *absent* — that is one rule, applied identically. The only thing
551
+ // that differs is REQUIREDNESS, never the rule: `nonce` is required, so absence is
552
+ // an error however it arose; `expiresAt` is optional, so absence is legal. Read the
553
+ // two branches as a single carriage rule meeting two different obligations.
554
+ if (Object.hasOwn(challenge, "expiresAt") && challenge.expiresAt !== undefined) {
555
+ carried.expiresAt = challenge.expiresAt;
556
+ }
557
+ return carried;
558
+ }
559
+ /**
560
+ * Reject a challenge we must not sign, reading ONLY the carried copy. Narrowing
561
+ * only — this can refuse a signature, never authorize one.
562
+ *
563
+ * The parameter is deliberately {@link CarriedChallengeFields} and not
564
+ * `H2AEnrollmentChallenge`: every read below is then own-by-construction, and the
565
+ * validator has no access to the caller's object to accidentally read through.
566
+ */
567
+ function assertCarriedChallengeIsSignable(carried, nowMs) {
568
+ if (carried.expiresAt !== undefined && typeof carried.expiresAt !== "string") {
569
+ throw new Error("h2a enrollment: challenge expiresAt must be an ISO-8601 STRING — a non-string cannot be an " +
570
+ "instant, and would be a place for structure to hide");
571
+ }
572
+ if (typeof carried.nonce !== "string" || carried.nonce.length === 0) {
573
+ throw new Error("h2a enrollment: the challenge carries no nonce — nothing to prove key control over");
574
+ }
575
+ // Cheap pre-parse bound first, so a hostile blob is dropped before any regex
576
+ // walks it. This is a DoS cap, NOT the nonce's definition.
577
+ if (carried.nonce.length > H2A_ENROLLMENT_MAX_NONCE_LENGTH) {
578
+ throw new Error(`h2a enrollment: challenge nonce is ${carried.nonce.length} chars, over the ` +
579
+ `${H2A_ENROLLMENT_MAX_NONCE_LENGTH}-char pre-parse cap — refusing to read an unbounded blob`);
580
+ }
581
+ // The definition, stated positively: base64url of at least 256 bits. Free
582
+ // text, JSON, a URL, or a message borrowed from another protocol all fail here
583
+ // because they are not the specified shape — not because they were enumerated.
584
+ if (!H2A_ENROLLMENT_NONCE_PATTERN.test(carried.nonce)) {
585
+ throw new Error("h2a enrollment: challenge nonce is not base64url ([A-Za-z0-9_-]) — a nonce is a random " +
586
+ "value of a specified shape, and anything else is refused rather than signed");
587
+ }
588
+ // FLOOR — the only bound here that speaks about strength.
589
+ if (carried.nonce.length < H2A_ENROLLMENT_NONCE_MIN_LENGTH) {
590
+ throw new Error(`h2a enrollment: challenge nonce is ${carried.nonce.length} base64url chars, under the ` +
591
+ `${H2A_ENROLLMENT_NONCE_MIN_LENGTH} needed for ${H2A_ENROLLMENT_NONCE_MIN_BITS} bits — ` +
592
+ "a guessable challenge is one an attacker can pre-compute a proof for");
593
+ }
594
+ // CEILING — a SANITY bound, not a security one. It says "beyond this it is not
595
+ // a nonce"; it says nothing about how much entropy is enough.
596
+ if (carried.nonce.length > H2A_ENROLLMENT_NONCE_MAX_LENGTH) {
597
+ throw new Error(`h2a enrollment: challenge nonce is ${carried.nonce.length} base64url chars, over the ` +
598
+ `${H2A_ENROLLMENT_NONCE_MAX_LENGTH}-char sanity ceiling (${H2A_ENROLLMENT_NONCE_MAX_BITS} ` +
599
+ "bits) — beyond this it is not a nonce. This is NOT a statement that less entropy is enough");
600
+ }
601
+ if (carried.expiresAt !== undefined) {
602
+ const expiresAtMs = Date.parse(carried.expiresAt);
603
+ if (Number.isNaN(expiresAtMs)) {
604
+ throw new Error(`h2a enrollment: challenge expiresAt "${carried.expiresAt}" is not an ISO-8601 instant`);
605
+ }
606
+ if (expiresAtMs <= nowMs) {
607
+ throw new Error(`h2a enrollment: challenge expired at ${carried.expiresAt} — ask the gateway for a new one`);
608
+ }
609
+ }
610
+ }
611
+ /**
612
+ * Reject a challenge we must not sign. Narrowing only — this can refuse a
613
+ * signature, never authorize one. Throws with a message naming the reason.
614
+ *
615
+ * Exported so a caller that classifies errors (the CLI, which owes a distinct
616
+ * exit code for "your challenge is bad" vs "my local key state is bad") can
617
+ * check the input it was handed BEFORE touching any identity.
618
+ * {@link signEnrollmentChallenge} validates again regardless: each entry point
619
+ * validates its own input, and an upstream check is never assumed.
620
+ *
621
+ * IMPLEMENTED AS the sanitize path with the result discarded, deliberately. The
622
+ * validator and the sanitizer used to be two independent readers of the caller's
623
+ * object, which is precisely how `expiresAt` came to be validated *through the
624
+ * prototype chain* while the allowlist looked only at own keys. One reader means
625
+ * they cannot disagree.
626
+ */
627
+ export function assertSignableEnrollmentChallenge(challenge, nowMs) {
628
+ sanitizeEnrollmentChallenge(challenge, nowMs);
629
+ }
630
+ /**
631
+ * Validate a parsed challenge document and return a FRESH, NULL-PROTOTYPE object
632
+ * carrying only the allowlisted keys.
633
+ *
634
+ * Three distinct jobs, all load-bearing:
635
+ *
636
+ * 1. It takes the OWN-ONLY view first ({@link carriedChallengeFields}), so what is
637
+ * validated and what is returned are the same object. An inherited field is
638
+ * invisible to both.
639
+ * 2. It refuses ({@link assertCarriedChallengeIsSignable}). Refuse-the-rest means
640
+ * refuse, not ignore.
641
+ * 3. What flows onward is a **new object with `null` prototype**, so the
642
+ * `JSON.parse` result — which may carry an own `"__proto__"` key, a
643
+ * prototype-pollution vector the moment anything spreads or assigns it into
644
+ * another object — never propagates past this boundary. Even though such a
645
+ * document is already refused above, nothing downstream has to depend on that
646
+ * having happened.
647
+ *
648
+ * Order matters and is the point: sanitize-then-validate, never validate-then-copy.
649
+ * The copy is what removes the second accessor, so validating the caller's object
650
+ * first would put the bug back.
651
+ *
652
+ * Use this at any boundary where the challenge came from parsed input. The CLI
653
+ * does, and so does {@link signEnrollmentChallenge}.
654
+ */
655
+ export function sanitizeEnrollmentChallenge(challenge, nowMs) {
656
+ const carried = carriedChallengeFields(challenge);
657
+ assertCarriedChallengeIsSignable(carried, nowMs);
658
+ return carried;
659
+ }
660
+ /**
661
+ * Sign a gateway-issued challenge with the agent's identity key and return the
662
+ * Part B proof payload. PURE apart from the injected clock: no I/O, no network,
663
+ * no store.
664
+ *
665
+ * The signed message is the CANONICAL COMPOSITE
666
+ * {@link enrollmentProofSignedPayload} — `{ type, nonce, instance, publicKeyPem }`
667
+ * — per the signed-composite amendment. The earlier shape signed the bare nonce
668
+ * (Part B flow steps 3 and 5b as originally written), which was safe only
669
+ * because nothing yet consumed `instance`: safety derived from the *absence* of a
670
+ * consumer, which expires the moment someone adds one.
671
+ *
672
+ * The rule, in its amended form: **a proof must attest to everything it carries
673
+ * AND to what it is.** Content-completeness plus context-binding — the first
674
+ * without the second leaves the interpretation attacker-chosen.
675
+ *
676
+ * A structural consequence worth knowing, because it removes a whole finding
677
+ * rather than guarding against it: the reclaim proof-of-possession signs a
678
+ * STRING (`identity-reclaim:<instance>:<fingerprint>`), and this signs an
679
+ * OBJECT. `canonicalize` type-tags the two differently, so no enrollment
680
+ * signature can ever satisfy `verifyReclaimProof` — the signing-oracle collision
681
+ * is impossible by construction, not by refusal. There is deliberately no guard
682
+ * against it: a guard that cannot fire is the defect this PR spent its time
683
+ * finding. The property is pinned by a regression test instead.
684
+ *
685
+ * Note that that separation was ACCIDENTAL — it held because the two payload
686
+ * types differ, not because either said what it was. The signed `type` field is
687
+ * what makes it deliberate, and it generalizes: no h2a signing site can collide
688
+ * with this one now, whatever shape it later adopts.
689
+ *
690
+ * Before returning, the proof is VERIFIED against the public key it ships. A
691
+ * proof we cannot verify ourselves is never emitted: that is what catches a
692
+ * mismatched keypair locally instead of at the gateway, where the failure is
693
+ * indistinguishable from an attack.
694
+ */
695
+ export function signEnrollmentChallenge(input) {
696
+ const now = input.now ?? Date.now;
697
+ // Validate and take the own-only view in ONE step, then sign what THAT view says.
698
+ // Reading `input.challenge.nonce` below instead would be another accessor on the
699
+ // caller's object — own today only because the validator happens to have rejected
700
+ // an inherited nonce first, i.e. correct by ordering rather than by construction.
701
+ // The signed nonce comes off the sanitized copy so the ordering cannot matter.
702
+ const challenge = sanitizeEnrollmentChallenge(input.challenge, now());
703
+ const { instance, privateKeyPem, publicKeyPem } = input.identity;
704
+ // Build the unsigned proof first, then sign the payload DERIVED from it, so the
705
+ // signed bytes and the shipped fields cannot drift apart.
706
+ //
707
+ // `type` sits INSIDE this object — the spread SOURCE — so it is covered by the
708
+ // very same mechanism as every other field. A tag added after the spread, or
709
+ // carried on the proof but excluded from it, would be an UNSIGNED field
710
+ // asserting the message's own identity: the worst possible field to leave
711
+ // unsigned, and worse than having no tag at all.
712
+ const unsigned = {
713
+ type: H2A_ENROLLMENT_PROOF_TYPE,
714
+ nonce: challenge.nonce,
715
+ instance,
716
+ publicKeyPem
717
+ };
718
+ const signature = signCanonical(enrollmentProofSignedPayload(unsigned), {
719
+ by: instance,
720
+ privateKeyPem
721
+ });
722
+ const proof = { ...unsigned, signature };
723
+ if (!verifyEnrollmentProof(proof)) {
724
+ throw new Error(`h2a enrollment: the proof for "${instance}" does not verify against its own public key ` +
725
+ "(private/public key mismatch) — refusing to emit an unverifiable proof");
726
+ }
727
+ return proof;
728
+ }
729
+ /**
730
+ * Resolve the live identity, sign the challenge, return the proof.
731
+ *
732
+ * SYNCHRONOUS and network-free by construction: there is no transport in this
733
+ * function at all, so the CLI path (`h2a keys prove-control`) cannot reach one.
734
+ * Re-enrollment needs no separate entry point — this IS the re-enrollment path,
735
+ * because it resolves the current identity every time and can therefore only
736
+ * ever produce a proof for the key that is live now. A changed controlling key
737
+ * means the owner re-runs this and the gateway mints a NEW binding, revoking the
738
+ * old row (Part B, "Re-enrollment of a post-re-anchor key"); nothing here
739
+ * reuses or rotates anything.
740
+ */
741
+ export function buildEnrollmentProof(options) {
742
+ const resolveIdentity = options.resolveIdentityImpl ?? resolveEnrollmentIdentity;
743
+ const identity = resolveIdentity({
744
+ root: options.root,
745
+ host: options.host,
746
+ cwd: options.cwd
747
+ });
748
+ const proof = signEnrollmentChallenge({
749
+ challenge: options.challenge,
750
+ identity,
751
+ ...(options.now !== undefined ? { now: options.now } : {})
752
+ });
753
+ return {
754
+ proof,
755
+ instance: proof.instance,
756
+ publicKeyFingerprint: publicKeyFingerprint(proof.publicKeyPem),
757
+ ...(identity.identityAction !== undefined
758
+ ? { identityAction: identity.identityAction }
759
+ : {})
760
+ };
761
+ }
762
+ /**
763
+ * Run the ceremony end to end: resolve the live identity, sign the challenge,
764
+ * and — only if a transport was injected — hand the proof over.
765
+ *
766
+ * With no `submitImpl` this makes NO network call of any kind. That is not a
767
+ * default-off flag; there is no hosted endpoint in this file to turn on.
768
+ */
769
+ export async function runEnrollmentCeremony(options) {
770
+ const built = buildEnrollmentProof(options);
771
+ if (options.submitImpl === undefined) {
772
+ return { ...built, submission: { attempted: false, reason: "no-transport-configured" } };
773
+ }
774
+ const response = await options.submitImpl(built.proof);
775
+ return { ...built, submission: { attempted: true, response } };
776
+ }
777
+ //# sourceMappingURL=ceremony.js.map