@richhardry/codor 0.10.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 (388) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +293 -0
  3. package/bin/codor.mjs +7 -0
  4. package/node_modules/@codor/adapter-antigravity/dist/adapter.d.ts +41 -0
  5. package/node_modules/@codor/adapter-antigravity/dist/adapter.d.ts.map +1 -0
  6. package/node_modules/@codor/adapter-antigravity/dist/adapter.js +275 -0
  7. package/node_modules/@codor/adapter-antigravity/dist/adapter.js.map +1 -0
  8. package/node_modules/@codor/adapter-antigravity/dist/index.d.ts +2 -0
  9. package/node_modules/@codor/adapter-antigravity/dist/index.d.ts.map +1 -0
  10. package/node_modules/@codor/adapter-antigravity/dist/index.js +2 -0
  11. package/node_modules/@codor/adapter-antigravity/dist/index.js.map +1 -0
  12. package/node_modules/@codor/adapter-antigravity/package.json +18 -0
  13. package/node_modules/@codor/adapter-claude-code/dist/adapter.d.ts +94 -0
  14. package/node_modules/@codor/adapter-claude-code/dist/adapter.d.ts.map +1 -0
  15. package/node_modules/@codor/adapter-claude-code/dist/adapter.js +785 -0
  16. package/node_modules/@codor/adapter-claude-code/dist/adapter.js.map +1 -0
  17. package/node_modules/@codor/adapter-claude-code/dist/index.d.ts +8 -0
  18. package/node_modules/@codor/adapter-claude-code/dist/index.d.ts.map +1 -0
  19. package/node_modules/@codor/adapter-claude-code/dist/index.js +5 -0
  20. package/node_modules/@codor/adapter-claude-code/dist/index.js.map +1 -0
  21. package/node_modules/@codor/adapter-claude-code/dist/limits-probe.d.ts +18 -0
  22. package/node_modules/@codor/adapter-claude-code/dist/limits-probe.d.ts.map +1 -0
  23. package/node_modules/@codor/adapter-claude-code/dist/limits-probe.js +67 -0
  24. package/node_modules/@codor/adapter-claude-code/dist/limits-probe.js.map +1 -0
  25. package/node_modules/@codor/adapter-claude-code/dist/peek.d.ts +3 -0
  26. package/node_modules/@codor/adapter-claude-code/dist/peek.d.ts.map +1 -0
  27. package/node_modules/@codor/adapter-claude-code/dist/peek.js +128 -0
  28. package/node_modules/@codor/adapter-claude-code/dist/peek.js.map +1 -0
  29. package/node_modules/@codor/adapter-claude-code/dist/query.d.ts +9 -0
  30. package/node_modules/@codor/adapter-claude-code/dist/query.d.ts.map +1 -0
  31. package/node_modules/@codor/adapter-claude-code/dist/query.js +7 -0
  32. package/node_modules/@codor/adapter-claude-code/dist/query.js.map +1 -0
  33. package/node_modules/@codor/adapter-claude-code/dist/translate.d.ts +40 -0
  34. package/node_modules/@codor/adapter-claude-code/dist/translate.d.ts.map +1 -0
  35. package/node_modules/@codor/adapter-claude-code/dist/translate.js +505 -0
  36. package/node_modules/@codor/adapter-claude-code/dist/translate.js.map +1 -0
  37. package/node_modules/@codor/adapter-claude-code/package.json +18 -0
  38. package/node_modules/@codor/adapter-codex/dist/adapter.d.ts +105 -0
  39. package/node_modules/@codor/adapter-codex/dist/adapter.d.ts.map +1 -0
  40. package/node_modules/@codor/adapter-codex/dist/adapter.js +625 -0
  41. package/node_modules/@codor/adapter-codex/dist/adapter.js.map +1 -0
  42. package/node_modules/@codor/adapter-codex/dist/app-server-transport.d.ts +39 -0
  43. package/node_modules/@codor/adapter-codex/dist/app-server-transport.d.ts.map +1 -0
  44. package/node_modules/@codor/adapter-codex/dist/app-server-transport.js +186 -0
  45. package/node_modules/@codor/adapter-codex/dist/app-server-transport.js.map +1 -0
  46. package/node_modules/@codor/adapter-codex/dist/index.d.ts +8 -0
  47. package/node_modules/@codor/adapter-codex/dist/index.d.ts.map +1 -0
  48. package/node_modules/@codor/adapter-codex/dist/index.js +5 -0
  49. package/node_modules/@codor/adapter-codex/dist/index.js.map +1 -0
  50. package/node_modules/@codor/adapter-codex/dist/limits-probe.d.ts +18 -0
  51. package/node_modules/@codor/adapter-codex/dist/limits-probe.d.ts.map +1 -0
  52. package/node_modules/@codor/adapter-codex/dist/limits-probe.js +87 -0
  53. package/node_modules/@codor/adapter-codex/dist/limits-probe.js.map +1 -0
  54. package/node_modules/@codor/adapter-codex/dist/peek.d.ts +3 -0
  55. package/node_modules/@codor/adapter-codex/dist/peek.d.ts.map +1 -0
  56. package/node_modules/@codor/adapter-codex/dist/peek.js +98 -0
  57. package/node_modules/@codor/adapter-codex/dist/peek.js.map +1 -0
  58. package/node_modules/@codor/adapter-codex/dist/translate.d.ts +17 -0
  59. package/node_modules/@codor/adapter-codex/dist/translate.d.ts.map +1 -0
  60. package/node_modules/@codor/adapter-codex/dist/translate.js +286 -0
  61. package/node_modules/@codor/adapter-codex/dist/translate.js.map +1 -0
  62. package/node_modules/@codor/adapter-codex/package.json +17 -0
  63. package/node_modules/@codor/adapter-copilot/dist/adapter.d.ts +40 -0
  64. package/node_modules/@codor/adapter-copilot/dist/adapter.d.ts.map +1 -0
  65. package/node_modules/@codor/adapter-copilot/dist/adapter.js +190 -0
  66. package/node_modules/@codor/adapter-copilot/dist/adapter.js.map +1 -0
  67. package/node_modules/@codor/adapter-copilot/dist/index.d.ts +4 -0
  68. package/node_modules/@codor/adapter-copilot/dist/index.d.ts.map +1 -0
  69. package/node_modules/@codor/adapter-copilot/dist/index.js +3 -0
  70. package/node_modules/@codor/adapter-copilot/dist/index.js.map +1 -0
  71. package/node_modules/@codor/adapter-copilot/dist/translate.d.ts +11 -0
  72. package/node_modules/@codor/adapter-copilot/dist/translate.d.ts.map +1 -0
  73. package/node_modules/@codor/adapter-copilot/dist/translate.js +184 -0
  74. package/node_modules/@codor/adapter-copilot/dist/translate.js.map +1 -0
  75. package/node_modules/@codor/adapter-copilot/package.json +18 -0
  76. package/node_modules/@codor/adapter-cursor/dist/adapter.d.ts +32 -0
  77. package/node_modules/@codor/adapter-cursor/dist/adapter.d.ts.map +1 -0
  78. package/node_modules/@codor/adapter-cursor/dist/adapter.js +179 -0
  79. package/node_modules/@codor/adapter-cursor/dist/adapter.js.map +1 -0
  80. package/node_modules/@codor/adapter-cursor/dist/index.d.ts +9 -0
  81. package/node_modules/@codor/adapter-cursor/dist/index.d.ts.map +1 -0
  82. package/node_modules/@codor/adapter-cursor/dist/index.js +8 -0
  83. package/node_modules/@codor/adapter-cursor/dist/index.js.map +1 -0
  84. package/node_modules/@codor/adapter-cursor/dist/translate.d.ts +15 -0
  85. package/node_modules/@codor/adapter-cursor/dist/translate.d.ts.map +1 -0
  86. package/node_modules/@codor/adapter-cursor/dist/translate.js +164 -0
  87. package/node_modules/@codor/adapter-cursor/dist/translate.js.map +1 -0
  88. package/node_modules/@codor/adapter-cursor/package.json +17 -0
  89. package/node_modules/@codor/adapter-gemini/dist/adapter.d.ts +36 -0
  90. package/node_modules/@codor/adapter-gemini/dist/adapter.d.ts.map +1 -0
  91. package/node_modules/@codor/adapter-gemini/dist/adapter.js +245 -0
  92. package/node_modules/@codor/adapter-gemini/dist/adapter.js.map +1 -0
  93. package/node_modules/@codor/adapter-gemini/dist/index.d.ts +4 -0
  94. package/node_modules/@codor/adapter-gemini/dist/index.d.ts.map +1 -0
  95. package/node_modules/@codor/adapter-gemini/dist/index.js +3 -0
  96. package/node_modules/@codor/adapter-gemini/dist/index.js.map +1 -0
  97. package/node_modules/@codor/adapter-gemini/dist/translate.d.ts +12 -0
  98. package/node_modules/@codor/adapter-gemini/dist/translate.d.ts.map +1 -0
  99. package/node_modules/@codor/adapter-gemini/dist/translate.js +123 -0
  100. package/node_modules/@codor/adapter-gemini/dist/translate.js.map +1 -0
  101. package/node_modules/@codor/adapter-gemini/package.json +18 -0
  102. package/node_modules/@codor/adapter-opencode/dist/adapter.d.ts +42 -0
  103. package/node_modules/@codor/adapter-opencode/dist/adapter.d.ts.map +1 -0
  104. package/node_modules/@codor/adapter-opencode/dist/adapter.js +240 -0
  105. package/node_modules/@codor/adapter-opencode/dist/adapter.js.map +1 -0
  106. package/node_modules/@codor/adapter-opencode/dist/index.d.ts +4 -0
  107. package/node_modules/@codor/adapter-opencode/dist/index.d.ts.map +1 -0
  108. package/node_modules/@codor/adapter-opencode/dist/index.js +3 -0
  109. package/node_modules/@codor/adapter-opencode/dist/index.js.map +1 -0
  110. package/node_modules/@codor/adapter-opencode/dist/translate.d.ts +12 -0
  111. package/node_modules/@codor/adapter-opencode/dist/translate.d.ts.map +1 -0
  112. package/node_modules/@codor/adapter-opencode/dist/translate.js +144 -0
  113. package/node_modules/@codor/adapter-opencode/dist/translate.js.map +1 -0
  114. package/node_modules/@codor/adapter-opencode/package.json +18 -0
  115. package/node_modules/@codor/cli/dist/artifact.d.ts +33 -0
  116. package/node_modules/@codor/cli/dist/artifact.d.ts.map +1 -0
  117. package/node_modules/@codor/cli/dist/artifact.js +183 -0
  118. package/node_modules/@codor/cli/dist/artifact.js.map +1 -0
  119. package/node_modules/@codor/cli/dist/attach.d.ts +25 -0
  120. package/node_modules/@codor/cli/dist/attach.d.ts.map +1 -0
  121. package/node_modules/@codor/cli/dist/attach.js +133 -0
  122. package/node_modules/@codor/cli/dist/attach.js.map +1 -0
  123. package/node_modules/@codor/cli/dist/connection.d.ts +21 -0
  124. package/node_modules/@codor/cli/dist/connection.d.ts.map +1 -0
  125. package/node_modules/@codor/cli/dist/connection.js +130 -0
  126. package/node_modules/@codor/cli/dist/connection.js.map +1 -0
  127. package/node_modules/@codor/cli/dist/detect.d.ts +14 -0
  128. package/node_modules/@codor/cli/dist/detect.d.ts.map +1 -0
  129. package/node_modules/@codor/cli/dist/detect.js +99 -0
  130. package/node_modules/@codor/cli/dist/detect.js.map +1 -0
  131. package/node_modules/@codor/cli/dist/index.d.ts +17 -0
  132. package/node_modules/@codor/cli/dist/index.d.ts.map +1 -0
  133. package/node_modules/@codor/cli/dist/index.js +24 -0
  134. package/node_modules/@codor/cli/dist/index.js.map +1 -0
  135. package/node_modules/@codor/cli/dist/mirror.d.ts +4 -0
  136. package/node_modules/@codor/cli/dist/mirror.d.ts.map +1 -0
  137. package/node_modules/@codor/cli/dist/mirror.js +93 -0
  138. package/node_modules/@codor/cli/dist/mirror.js.map +1 -0
  139. package/node_modules/@codor/cli/dist/program.d.ts +17 -0
  140. package/node_modules/@codor/cli/dist/program.d.ts.map +1 -0
  141. package/node_modules/@codor/cli/dist/program.js +1068 -0
  142. package/node_modules/@codor/cli/dist/program.js.map +1 -0
  143. package/node_modules/@codor/cli/dist/runtime-install.d.ts +61 -0
  144. package/node_modules/@codor/cli/dist/runtime-install.d.ts.map +1 -0
  145. package/node_modules/@codor/cli/dist/runtime-install.js +122 -0
  146. package/node_modules/@codor/cli/dist/runtime-install.js.map +1 -0
  147. package/node_modules/@codor/cli/dist/runtime-paths.d.ts +24 -0
  148. package/node_modules/@codor/cli/dist/runtime-paths.d.ts.map +1 -0
  149. package/node_modules/@codor/cli/dist/runtime-paths.js +69 -0
  150. package/node_modules/@codor/cli/dist/runtime-paths.js.map +1 -0
  151. package/node_modules/@codor/cli/dist/setup-flow.d.ts +69 -0
  152. package/node_modules/@codor/cli/dist/setup-flow.d.ts.map +1 -0
  153. package/node_modules/@codor/cli/dist/setup-flow.js +110 -0
  154. package/node_modules/@codor/cli/dist/setup-flow.js.map +1 -0
  155. package/node_modules/@codor/cli/dist/setup-session.d.ts +105 -0
  156. package/node_modules/@codor/cli/dist/setup-session.d.ts.map +1 -0
  157. package/node_modules/@codor/cli/dist/setup-session.js +346 -0
  158. package/node_modules/@codor/cli/dist/setup-session.js.map +1 -0
  159. package/node_modules/@codor/cli/dist/setup-ui.d.ts +93 -0
  160. package/node_modules/@codor/cli/dist/setup-ui.d.ts.map +1 -0
  161. package/node_modules/@codor/cli/dist/setup-ui.js +249 -0
  162. package/node_modules/@codor/cli/dist/setup-ui.js.map +1 -0
  163. package/node_modules/@codor/cli/dist/setup.d.ts +108 -0
  164. package/node_modules/@codor/cli/dist/setup.d.ts.map +1 -0
  165. package/node_modules/@codor/cli/dist/setup.js +878 -0
  166. package/node_modules/@codor/cli/dist/setup.js.map +1 -0
  167. package/node_modules/@codor/cli/dist/terminal-keys.d.ts +26 -0
  168. package/node_modules/@codor/cli/dist/terminal-keys.d.ts.map +1 -0
  169. package/node_modules/@codor/cli/dist/terminal-keys.js +70 -0
  170. package/node_modules/@codor/cli/dist/terminal-keys.js.map +1 -0
  171. package/node_modules/@codor/cli/dist/terminal-qr.d.ts +2 -0
  172. package/node_modules/@codor/cli/dist/terminal-qr.d.ts.map +1 -0
  173. package/node_modules/@codor/cli/dist/terminal-qr.js +30 -0
  174. package/node_modules/@codor/cli/dist/terminal-qr.js.map +1 -0
  175. package/node_modules/@codor/cli/dist/up.d.ts +52 -0
  176. package/node_modules/@codor/cli/dist/up.d.ts.map +1 -0
  177. package/node_modules/@codor/cli/dist/up.js +178 -0
  178. package/node_modules/@codor/cli/dist/up.js.map +1 -0
  179. package/node_modules/@codor/cli/package.json +24 -0
  180. package/node_modules/@codor/cli/packaging/systemd/codor.service +21 -0
  181. package/node_modules/@codor/cli/runtime/web/assets/LandingPage-BOmG5kSw.js +31 -0
  182. package/node_modules/@codor/cli/runtime/web/assets/LedgerPage-CecuTS8-.js +1 -0
  183. package/node_modules/@codor/cli/runtime/web/assets/NoChannels-CYiBXGQd.js +11 -0
  184. package/node_modules/@codor/cli/runtime/web/assets/PairingCodeInput-Cr6XDWx-.js +1 -0
  185. package/node_modules/@codor/cli/runtime/web/assets/PairingPage-CQBWNA2L.js +6 -0
  186. package/node_modules/@codor/cli/runtime/web/assets/SettingsPage-DGLbSXE7.js +16 -0
  187. package/node_modules/@codor/cli/runtime/web/assets/StartupUnavailable-DdQBHip_.js +1 -0
  188. package/node_modules/@codor/cli/runtime/web/assets/arrow-left-BOpjMuRi.js +6 -0
  189. package/node_modules/@codor/cli/runtime/web/assets/browser-BWQYNOsD.js +6 -0
  190. package/node_modules/@codor/cli/runtime/web/assets/graph-toBZHlC0.js +1 -0
  191. package/node_modules/@codor/cli/runtime/web/assets/index-Cxi54Xim.js +408 -0
  192. package/node_modules/@codor/cli/runtime/web/assets/index-DjwJakJC.css +1 -0
  193. package/node_modules/@codor/cli/runtime/web/assets/qr-DpynCwP9.js +8 -0
  194. package/node_modules/@codor/cli/runtime/web/assets/react-C8w-UNLI.js +32 -0
  195. package/node_modules/@codor/cli/runtime/web/assets/sodium-DtsvMzX9.js +1 -0
  196. package/node_modules/@codor/cli/runtime/web/codor-192.png +0 -0
  197. package/node_modules/@codor/cli/runtime/web/codor-512.png +0 -0
  198. package/node_modules/@codor/cli/runtime/web/codor-apple-touch-180.png +0 -0
  199. package/node_modules/@codor/cli/runtime/web/codor-favicon.svg +86 -0
  200. package/node_modules/@codor/cli/runtime/web/codor-mark.svg +86 -0
  201. package/node_modules/@codor/cli/runtime/web/codor-maskable-512.png +0 -0
  202. package/node_modules/@codor/cli/runtime/web/codor-og.png +0 -0
  203. package/node_modules/@codor/cli/runtime/web/fonts/BitcountPropSingle-latin.woff2 +0 -0
  204. package/node_modules/@codor/cli/runtime/web/fonts/Geist-Variable.woff2 +0 -0
  205. package/node_modules/@codor/cli/runtime/web/fonts/GeistMono-Variable.woff2 +0 -0
  206. package/node_modules/@codor/cli/runtime/web/fonts/OFL.txt +93 -0
  207. package/node_modules/@codor/cli/runtime/web/index.html +53 -0
  208. package/node_modules/@codor/cli/runtime/web/manifest.webmanifest +1 -0
  209. package/node_modules/@codor/cli/runtime/web/sw.js +2 -0
  210. package/node_modules/@codor/protocol/dist/adapter.d.ts +118 -0
  211. package/node_modules/@codor/protocol/dist/adapter.d.ts.map +1 -0
  212. package/node_modules/@codor/protocol/dist/adapter.js +15 -0
  213. package/node_modules/@codor/protocol/dist/adapter.js.map +1 -0
  214. package/node_modules/@codor/protocol/dist/body.d.ts +14 -0
  215. package/node_modules/@codor/protocol/dist/body.d.ts.map +1 -0
  216. package/node_modules/@codor/protocol/dist/body.js +50 -0
  217. package/node_modules/@codor/protocol/dist/body.js.map +1 -0
  218. package/node_modules/@codor/protocol/dist/changelog.d.ts +30 -0
  219. package/node_modules/@codor/protocol/dist/changelog.d.ts.map +1 -0
  220. package/node_modules/@codor/protocol/dist/changelog.js +19 -0
  221. package/node_modules/@codor/protocol/dist/changelog.js.map +1 -0
  222. package/node_modules/@codor/protocol/dist/default-recipient.d.ts +8 -0
  223. package/node_modules/@codor/protocol/dist/default-recipient.d.ts.map +1 -0
  224. package/node_modules/@codor/protocol/dist/default-recipient.js +19 -0
  225. package/node_modules/@codor/protocol/dist/default-recipient.js.map +1 -0
  226. package/node_modules/@codor/protocol/dist/delivery.d.ts +42 -0
  227. package/node_modules/@codor/protocol/dist/delivery.d.ts.map +1 -0
  228. package/node_modules/@codor/protocol/dist/delivery.js +44 -0
  229. package/node_modules/@codor/protocol/dist/delivery.js.map +1 -0
  230. package/node_modules/@codor/protocol/dist/events.d.ts +284 -0
  231. package/node_modules/@codor/protocol/dist/events.d.ts.map +1 -0
  232. package/node_modules/@codor/protocol/dist/events.js +150 -0
  233. package/node_modules/@codor/protocol/dist/events.js.map +1 -0
  234. package/node_modules/@codor/protocol/dist/ids.d.ts +19 -0
  235. package/node_modules/@codor/protocol/dist/ids.d.ts.map +1 -0
  236. package/node_modules/@codor/protocol/dist/ids.js +27 -0
  237. package/node_modules/@codor/protocol/dist/ids.js.map +1 -0
  238. package/node_modules/@codor/protocol/dist/index.d.ts +14 -0
  239. package/node_modules/@codor/protocol/dist/index.d.ts.map +1 -0
  240. package/node_modules/@codor/protocol/dist/index.js +14 -0
  241. package/node_modules/@codor/protocol/dist/index.js.map +1 -0
  242. package/node_modules/@codor/protocol/dist/interaction.d.ts +45 -0
  243. package/node_modules/@codor/protocol/dist/interaction.d.ts.map +1 -0
  244. package/node_modules/@codor/protocol/dist/interaction.js +33 -0
  245. package/node_modules/@codor/protocol/dist/interaction.js.map +1 -0
  246. package/node_modules/@codor/protocol/dist/member.d.ts +146 -0
  247. package/node_modules/@codor/protocol/dist/member.d.ts.map +1 -0
  248. package/node_modules/@codor/protocol/dist/member.js +169 -0
  249. package/node_modules/@codor/protocol/dist/member.js.map +1 -0
  250. package/node_modules/@codor/protocol/dist/message.d.ts +166 -0
  251. package/node_modules/@codor/protocol/dist/message.d.ts.map +1 -0
  252. package/node_modules/@codor/protocol/dist/message.js +95 -0
  253. package/node_modules/@codor/protocol/dist/message.js.map +1 -0
  254. package/node_modules/@codor/protocol/dist/observability.d.ts +80 -0
  255. package/node_modules/@codor/protocol/dist/observability.d.ts.map +1 -0
  256. package/node_modules/@codor/protocol/dist/observability.js +41 -0
  257. package/node_modules/@codor/protocol/dist/observability.js.map +1 -0
  258. package/node_modules/@codor/protocol/dist/room.d.ts +430 -0
  259. package/node_modules/@codor/protocol/dist/room.d.ts.map +1 -0
  260. package/node_modules/@codor/protocol/dist/room.js +142 -0
  261. package/node_modules/@codor/protocol/dist/room.js.map +1 -0
  262. package/node_modules/@codor/protocol/dist/ws.d.ts +1348 -0
  263. package/node_modules/@codor/protocol/dist/ws.d.ts.map +1 -0
  264. package/node_modules/@codor/protocol/dist/ws.js +282 -0
  265. package/node_modules/@codor/protocol/dist/ws.js.map +1 -0
  266. package/node_modules/@codor/protocol/package.json +17 -0
  267. package/node_modules/@codor/switchboard/dist/adapter-registry.d.ts +14 -0
  268. package/node_modules/@codor/switchboard/dist/adapter-registry.d.ts.map +1 -0
  269. package/node_modules/@codor/switchboard/dist/adapter-registry.js +175 -0
  270. package/node_modules/@codor/switchboard/dist/adapter-registry.js.map +1 -0
  271. package/node_modules/@codor/switchboard/dist/authorization.d.ts +13 -0
  272. package/node_modules/@codor/switchboard/dist/authorization.d.ts.map +1 -0
  273. package/node_modules/@codor/switchboard/dist/authorization.js +96 -0
  274. package/node_modules/@codor/switchboard/dist/authorization.js.map +1 -0
  275. package/node_modules/@codor/switchboard/dist/blobs.d.ts +17 -0
  276. package/node_modules/@codor/switchboard/dist/blobs.d.ts.map +1 -0
  277. package/node_modules/@codor/switchboard/dist/blobs.js +54 -0
  278. package/node_modules/@codor/switchboard/dist/blobs.js.map +1 -0
  279. package/node_modules/@codor/switchboard/dist/collaboration.d.ts +115 -0
  280. package/node_modules/@codor/switchboard/dist/collaboration.d.ts.map +1 -0
  281. package/node_modules/@codor/switchboard/dist/collaboration.js +123 -0
  282. package/node_modules/@codor/switchboard/dist/collaboration.js.map +1 -0
  283. package/node_modules/@codor/switchboard/dist/continuation.d.ts +33 -0
  284. package/node_modules/@codor/switchboard/dist/continuation.d.ts.map +1 -0
  285. package/node_modules/@codor/switchboard/dist/continuation.js +177 -0
  286. package/node_modules/@codor/switchboard/dist/continuation.js.map +1 -0
  287. package/node_modules/@codor/switchboard/dist/crypto/browser-sessions.d.ts +22 -0
  288. package/node_modules/@codor/switchboard/dist/crypto/browser-sessions.d.ts.map +1 -0
  289. package/node_modules/@codor/switchboard/dist/crypto/browser-sessions.js +92 -0
  290. package/node_modules/@codor/switchboard/dist/crypto/browser-sessions.js.map +1 -0
  291. package/node_modules/@codor/switchboard/dist/crypto/challenge.d.ts +37 -0
  292. package/node_modules/@codor/switchboard/dist/crypto/challenge.d.ts.map +1 -0
  293. package/node_modules/@codor/switchboard/dist/crypto/challenge.js +132 -0
  294. package/node_modules/@codor/switchboard/dist/crypto/challenge.js.map +1 -0
  295. package/node_modules/@codor/switchboard/dist/crypto/keys.d.ts +41 -0
  296. package/node_modules/@codor/switchboard/dist/crypto/keys.d.ts.map +1 -0
  297. package/node_modules/@codor/switchboard/dist/crypto/keys.js +145 -0
  298. package/node_modules/@codor/switchboard/dist/crypto/keys.js.map +1 -0
  299. package/node_modules/@codor/switchboard/dist/crypto/pairing.d.ts +53 -0
  300. package/node_modules/@codor/switchboard/dist/crypto/pairing.d.ts.map +1 -0
  301. package/node_modules/@codor/switchboard/dist/crypto/pairing.js +182 -0
  302. package/node_modules/@codor/switchboard/dist/crypto/pairing.js.map +1 -0
  303. package/node_modules/@codor/switchboard/dist/crypto/roomkeys.d.ts +30 -0
  304. package/node_modules/@codor/switchboard/dist/crypto/roomkeys.d.ts.map +1 -0
  305. package/node_modules/@codor/switchboard/dist/crypto/roomkeys.js +101 -0
  306. package/node_modules/@codor/switchboard/dist/crypto/roomkeys.js.map +1 -0
  307. package/node_modules/@codor/switchboard/dist/daemon.d.ts +545 -0
  308. package/node_modules/@codor/switchboard/dist/daemon.d.ts.map +1 -0
  309. package/node_modules/@codor/switchboard/dist/daemon.js +3907 -0
  310. package/node_modules/@codor/switchboard/dist/daemon.js.map +1 -0
  311. package/node_modules/@codor/switchboard/dist/fake-adapter.d.ts +103 -0
  312. package/node_modules/@codor/switchboard/dist/fake-adapter.d.ts.map +1 -0
  313. package/node_modules/@codor/switchboard/dist/fake-adapter.js +228 -0
  314. package/node_modules/@codor/switchboard/dist/fake-adapter.js.map +1 -0
  315. package/node_modules/@codor/switchboard/dist/index.d.ts +44 -0
  316. package/node_modules/@codor/switchboard/dist/index.d.ts.map +1 -0
  317. package/node_modules/@codor/switchboard/dist/index.js +24 -0
  318. package/node_modules/@codor/switchboard/dist/index.js.map +1 -0
  319. package/node_modules/@codor/switchboard/dist/ledger/resolve.d.ts +12 -0
  320. package/node_modules/@codor/switchboard/dist/ledger/resolve.d.ts.map +1 -0
  321. package/node_modules/@codor/switchboard/dist/ledger/resolve.js +17 -0
  322. package/node_modules/@codor/switchboard/dist/ledger/resolve.js.map +1 -0
  323. package/node_modules/@codor/switchboard/dist/ledger/vault.d.ts +47 -0
  324. package/node_modules/@codor/switchboard/dist/ledger/vault.d.ts.map +1 -0
  325. package/node_modules/@codor/switchboard/dist/ledger/vault.js +175 -0
  326. package/node_modules/@codor/switchboard/dist/ledger/vault.js.map +1 -0
  327. package/node_modules/@codor/switchboard/dist/ledger/watch.d.ts +73 -0
  328. package/node_modules/@codor/switchboard/dist/ledger/watch.d.ts.map +1 -0
  329. package/node_modules/@codor/switchboard/dist/ledger/watch.js +232 -0
  330. package/node_modules/@codor/switchboard/dist/ledger/watch.js.map +1 -0
  331. package/node_modules/@codor/switchboard/dist/local-dirs.d.ts +14 -0
  332. package/node_modules/@codor/switchboard/dist/local-dirs.d.ts.map +1 -0
  333. package/node_modules/@codor/switchboard/dist/local-dirs.js +40 -0
  334. package/node_modules/@codor/switchboard/dist/local-dirs.js.map +1 -0
  335. package/node_modules/@codor/switchboard/dist/local-socket.d.ts +3 -0
  336. package/node_modules/@codor/switchboard/dist/local-socket.d.ts.map +1 -0
  337. package/node_modules/@codor/switchboard/dist/local-socket.js +16 -0
  338. package/node_modules/@codor/switchboard/dist/local-socket.js.map +1 -0
  339. package/node_modules/@codor/switchboard/dist/pricing.d.ts +32 -0
  340. package/node_modules/@codor/switchboard/dist/pricing.d.ts.map +1 -0
  341. package/node_modules/@codor/switchboard/dist/pricing.js +64 -0
  342. package/node_modules/@codor/switchboard/dist/pricing.js.map +1 -0
  343. package/node_modules/@codor/switchboard/dist/process-liveness.d.ts +2 -0
  344. package/node_modules/@codor/switchboard/dist/process-liveness.d.ts.map +1 -0
  345. package/node_modules/@codor/switchboard/dist/process-liveness.js +9 -0
  346. package/node_modules/@codor/switchboard/dist/process-liveness.js.map +1 -0
  347. package/node_modules/@codor/switchboard/dist/push/producer.d.ts +54 -0
  348. package/node_modules/@codor/switchboard/dist/push/producer.d.ts.map +1 -0
  349. package/node_modules/@codor/switchboard/dist/push/producer.js +168 -0
  350. package/node_modules/@codor/switchboard/dist/push/producer.js.map +1 -0
  351. package/node_modules/@codor/switchboard/dist/push/subscriptions.d.ts +29 -0
  352. package/node_modules/@codor/switchboard/dist/push/subscriptions.d.ts.map +1 -0
  353. package/node_modules/@codor/switchboard/dist/push/subscriptions.js +83 -0
  354. package/node_modules/@codor/switchboard/dist/push/subscriptions.js.map +1 -0
  355. package/node_modules/@codor/switchboard/dist/redact.d.ts +11 -0
  356. package/node_modules/@codor/switchboard/dist/redact.d.ts.map +1 -0
  357. package/node_modules/@codor/switchboard/dist/redact.js +65 -0
  358. package/node_modules/@codor/switchboard/dist/redact.js.map +1 -0
  359. package/node_modules/@codor/switchboard/dist/residency.d.ts +110 -0
  360. package/node_modules/@codor/switchboard/dist/residency.d.ts.map +1 -0
  361. package/node_modules/@codor/switchboard/dist/residency.js +587 -0
  362. package/node_modules/@codor/switchboard/dist/residency.js.map +1 -0
  363. package/node_modules/@codor/switchboard/dist/router.d.ts +120 -0
  364. package/node_modules/@codor/switchboard/dist/router.d.ts.map +1 -0
  365. package/node_modules/@codor/switchboard/dist/router.js +198 -0
  366. package/node_modules/@codor/switchboard/dist/router.js.map +1 -0
  367. package/node_modules/@codor/switchboard/dist/server.d.ts +50 -0
  368. package/node_modules/@codor/switchboard/dist/server.d.ts.map +1 -0
  369. package/node_modules/@codor/switchboard/dist/server.js +1425 -0
  370. package/node_modules/@codor/switchboard/dist/server.js.map +1 -0
  371. package/node_modules/@codor/switchboard/dist/store.d.ts +534 -0
  372. package/node_modules/@codor/switchboard/dist/store.d.ts.map +1 -0
  373. package/node_modules/@codor/switchboard/dist/store.js +2625 -0
  374. package/node_modules/@codor/switchboard/dist/store.js.map +1 -0
  375. package/node_modules/@codor/switchboard/dist/transport/hyperswarm.d.ts +59 -0
  376. package/node_modules/@codor/switchboard/dist/transport/hyperswarm.d.ts.map +1 -0
  377. package/node_modules/@codor/switchboard/dist/transport/hyperswarm.js +183 -0
  378. package/node_modules/@codor/switchboard/dist/transport/hyperswarm.js.map +1 -0
  379. package/node_modules/@codor/switchboard/dist/transport/peer.d.ts +87 -0
  380. package/node_modules/@codor/switchboard/dist/transport/peer.d.ts.map +1 -0
  381. package/node_modules/@codor/switchboard/dist/transport/peer.js +316 -0
  382. package/node_modules/@codor/switchboard/dist/transport/peer.js.map +1 -0
  383. package/node_modules/@codor/switchboard/dist/working-directory.d.ts +2 -0
  384. package/node_modules/@codor/switchboard/dist/working-directory.d.ts.map +1 -0
  385. package/node_modules/@codor/switchboard/dist/working-directory.js +26 -0
  386. package/node_modules/@codor/switchboard/dist/working-directory.js.map +1 -0
  387. package/node_modules/@codor/switchboard/package.json +33 -0
  388. package/package.json +58 -0
@@ -0,0 +1,1425 @@
1
+ import { chmodSync, createReadStream, createWriteStream, existsSync, lstatSync, mkdirSync, rmSync } from 'node:fs';
2
+ import { createServer as createHttpServer } from 'node:http';
3
+ import { connect as connectSocket } from 'node:net';
4
+ import { dirname } from 'node:path';
5
+ import { Transform } from 'node:stream';
6
+ import { pipeline } from 'node:stream/promises';
7
+ import fastifyStatic from '@fastify/static';
8
+ import Fastify from 'fastify';
9
+ import { WebSocketServer } from 'ws';
10
+ import { BROWSER_PROTOCOL_EPOCH, ClientFrameSchema, CreateRoomRequestSchema, } from '@codor/protocol';
11
+ import { assertAgentCapability, assertHumanCapability, roleAllows, } from './authorization.js';
12
+ import { constantTimeEqual, hashTranscript } from './crypto/challenge.js';
13
+ import { MAX_ATTACHMENT_BYTES } from './daemon.js';
14
+ import { listLocalDirectories, LocalDirectoryError } from './local-dirs.js';
15
+ import { isPipePath } from './local-socket.js';
16
+ const PAIRING_CODE_ATTEMPTS = 5;
17
+ const PAIRING_CODE_WINDOW_MS = 60_000;
18
+ function pairingCodeAttemptLimiter(now) {
19
+ const attempts = new WeakMap();
20
+ return (connection) => {
21
+ const cutoff = now() - PAIRING_CODE_WINDOW_MS;
22
+ const recent = (attempts.get(connection) ?? []).filter((timestamp) => timestamp > cutoff);
23
+ if (recent.length >= PAIRING_CODE_ATTEMPTS) {
24
+ attempts.set(connection, recent);
25
+ return false;
26
+ }
27
+ recent.push(now());
28
+ attempts.set(connection, recent);
29
+ return true;
30
+ };
31
+ }
32
+ async function prepareSocketPath(socketPath) {
33
+ // harn:assume unix-socket-parent-private-before-listen ref=unix-socket-parent-precondition
34
+ const parent = dirname(socketPath);
35
+ mkdirSync(parent, { recursive: true, mode: 0o700 });
36
+ const parentStat = lstatSync(parent);
37
+ if (!parentStat.isDirectory() || (parentStat.mode & 0o077) !== 0) {
38
+ throw new Error(`unix socket parent must be a private directory (mode 0700): ${parent}`);
39
+ }
40
+ // harn:end unix-socket-parent-private-before-listen
41
+ if (!existsSync(socketPath))
42
+ return;
43
+ if (!lstatSync(socketPath).isSocket()) {
44
+ throw new Error(`refusing to replace non-socket path ${socketPath}`);
45
+ }
46
+ await new Promise((resolve, reject) => {
47
+ const probe = connectSocket(socketPath);
48
+ probe.once('connect', () => {
49
+ probe.destroy();
50
+ reject(new Error(`unix socket already in use: ${socketPath}`));
51
+ });
52
+ probe.once('error', (error) => {
53
+ if (error.code === 'ECONNREFUSED' || error.code === 'ENOENT') {
54
+ rmSync(socketPath, { force: true });
55
+ resolve();
56
+ }
57
+ else {
58
+ reject(error);
59
+ }
60
+ });
61
+ });
62
+ }
63
+ async function listenUnix(server, socketPath) {
64
+ // harn:assume windows-named-pipe-shares-local-websocket-protocol ref=windows-pipe-server-listener
65
+ // A named pipe has no filesystem entry to prepare or chmod. Windows applies the
66
+ // creating user's default pipe security descriptor; tokenless local admission
67
+ // therefore still depends on the OS endpoint being unwritable by other profiles.
68
+ const pipe = isPipePath(socketPath);
69
+ if (!pipe)
70
+ await prepareSocketPath(socketPath);
71
+ await new Promise((resolve, reject) => {
72
+ server.once('error', reject);
73
+ server.listen(socketPath, () => {
74
+ server.off('error', reject);
75
+ if (!pipe)
76
+ chmodSync(socketPath, 0o600);
77
+ resolve();
78
+ });
79
+ });
80
+ // harn:end windows-named-pipe-shares-local-websocket-protocol
81
+ }
82
+ /**
83
+ * The API surface: one WebSocket (subscribe/post/act) + small REST (sync,
84
+ * blob fetch, room + member management). Everything served here has already
85
+ * passed the daemon's redaction projection.
86
+ */
87
+ export async function startServer(options) {
88
+ const { daemon, token } = options;
89
+ const minimumBrowserProtocol = options.minimumBrowserProtocol ?? 0;
90
+ if (!Number.isInteger(minimumBrowserProtocol) || minimumBrowserProtocol < 0) {
91
+ throw new Error('minimumBrowserProtocol must be a non-negative integer');
92
+ }
93
+ // harn:assume server-token-required ref=token-validation
94
+ if (typeof token !== 'string' || token.trim() === '') {
95
+ throw new Error('startServer requires a non-empty authentication token');
96
+ }
97
+ const configuredPrincipals = options.principals ?? [];
98
+ const principalTokens = new Set();
99
+ for (const principal of configuredPrincipals) {
100
+ if (principal.token.trim() === '')
101
+ throw new Error('principal tokens must be non-empty');
102
+ if (constantTimeEqual(principal.token, token) || principalTokens.has(principal.token)) {
103
+ throw new Error('principal tokens must be unique');
104
+ }
105
+ principalTokens.add(principal.token);
106
+ }
107
+ // harn:end server-token-required
108
+ const app = Fastify();
109
+ // Attachment uploads arrive as raw binary bodies (any content-type). This
110
+ // catch-all parser hands the route the untouched request stream to pipe to
111
+ // disk; it only fires for non-JSON bodies, which only the upload sends.
112
+ app.addContentTypeParser('*', (_req, payload, done) => done(null, payload));
113
+ const allowPairingCodeAttempt = pairingCodeAttemptLimiter(Date.now);
114
+ const browserAuthTranscript = options.crypto
115
+ ? hashTranscript(Buffer.from(`codor-browser-session-v1\0${options.crypto.keys.identity.device_id}`, 'utf8'))
116
+ : undefined;
117
+ const principalForToken = (candidate) => {
118
+ if (candidate === undefined)
119
+ return undefined;
120
+ if (constantTimeEqual(candidate, token))
121
+ return { kind: 'owner' };
122
+ const configured = configuredPrincipals.find((principal) => constantTimeEqual(candidate, principal.token));
123
+ if (configured)
124
+ return { kind: 'human', memberId: configured.member_id };
125
+ const deviceId = options.crypto?.browserSessions.authenticate(candidate);
126
+ if (deviceId)
127
+ return { kind: 'browser', deviceId };
128
+ // harn:assume agent-member-credentials-stay-secret ref=agent-principal-resolution
129
+ const agent = daemon.authenticateAgentToken(candidate);
130
+ return agent
131
+ ? { kind: 'agent', memberId: agent.member.id, room: agent.room }
132
+ : undefined;
133
+ // harn:end agent-member-credentials-stay-secret
134
+ };
135
+ const authed = (req, reply) => {
136
+ const header = req.headers.authorization;
137
+ const query = req.query.token;
138
+ const bearer = typeof header === 'string' && header.startsWith('Bearer ')
139
+ ? header.slice('Bearer '.length)
140
+ : undefined;
141
+ const principal = principalForToken(bearer) ?? principalForToken(query);
142
+ if (principal)
143
+ return principal;
144
+ void reply.code(401).send({ error: 'unauthorized' });
145
+ return undefined;
146
+ };
147
+ const memberForRoom = (principal, room) => {
148
+ if (principal.kind === 'owner' || principal.kind === 'browser')
149
+ return daemon.ownerOf(room);
150
+ if (principal.kind === 'agent' && principal.room !== room) {
151
+ throw new Error(`forbidden: agent credential belongs to room ${principal.room}`);
152
+ }
153
+ const member = daemon.store.getMember(room, principal.memberId);
154
+ if (member?.kind !== principal.kind) {
155
+ throw new Error(`principal is not a ${principal.kind} member of this room`);
156
+ }
157
+ return member;
158
+ };
159
+ const memberForGlobal = (principal) => {
160
+ if (principal.kind === 'owner' || principal.kind === 'browser')
161
+ return undefined;
162
+ if (principal.kind === 'agent')
163
+ throw new Error('forbidden: agent principal is room-scoped');
164
+ for (const room of daemon.store.listRooms()) {
165
+ const member = daemon.store.getMember(room.id, principal.memberId);
166
+ if (member?.kind === 'human')
167
+ return member;
168
+ }
169
+ throw new Error('principal is not a human member');
170
+ };
171
+ // harn:assume agent-network-authority-is-narrow ref=agent-room-authorization
172
+ const assertRoomCapability = (principal, room, capability) => {
173
+ if (!daemon.store.getRoom(room))
174
+ throw new Error(`no such room ${room}`);
175
+ const member = memberForRoom(principal, room);
176
+ if (principal.kind === 'agent')
177
+ assertAgentCapability(member, capability);
178
+ else
179
+ assertHumanCapability(member, capability);
180
+ return member;
181
+ };
182
+ const authorizeRoom = (principal, room, capability, reply) => {
183
+ if (!daemon.store.getRoom(room)) {
184
+ if (reply)
185
+ void reply.code(404).send({ error: `no such room ${room}` });
186
+ return undefined;
187
+ }
188
+ try {
189
+ return assertRoomCapability(principal, room, capability);
190
+ }
191
+ catch (error) {
192
+ if (reply)
193
+ void reply.code(403).send({ error: String(error) });
194
+ return undefined;
195
+ }
196
+ };
197
+ const authorizeGlobal = (principal, capability, reply) => {
198
+ try {
199
+ if (principal.kind === 'agent') {
200
+ throw new Error(`forbidden: agent cannot use global ${capability}`);
201
+ }
202
+ const member = memberForGlobal(principal);
203
+ if (member)
204
+ assertHumanCapability(member, capability);
205
+ else if (!roleAllows('owner', capability))
206
+ throw new Error(`forbidden: owner cannot ${capability}`);
207
+ return true;
208
+ }
209
+ catch (error) {
210
+ if (reply)
211
+ void reply.code(403).send({ error: String(error) });
212
+ return false;
213
+ }
214
+ };
215
+ const roomsFor = (principal) => daemon.store.listRooms().filter((room) => {
216
+ if (principal.kind === 'owner' || principal.kind === 'browser')
217
+ return true;
218
+ if (principal.kind === 'agent')
219
+ return room.id === principal.room;
220
+ return daemon.store.getMember(room.id, principal.memberId)?.kind === 'human';
221
+ });
222
+ // harn:end agent-network-authority-is-narrow
223
+ // harn:assume browser-protocol-epoch-blocks-only-stale-browser-ui ref=browser-protocol-compatibility-rest
224
+ const observedBrowserProtocols = new Set();
225
+ const observeBrowserProtocol = (protocol) => {
226
+ if (protocol === undefined || !Number.isInteger(protocol) || protocol < 1)
227
+ return;
228
+ options.onBrowserProtocolObserved?.(protocol);
229
+ if (observedBrowserProtocols.has(protocol))
230
+ return;
231
+ observedBrowserProtocols.add(protocol);
232
+ // This is the deploy precondition's live evidence. It contains no token,
233
+ // device id, room, or other browser identity.
234
+ if (options.onBrowserProtocolObserved === undefined) {
235
+ console.info(`[codor] observed browser protocol ${String(protocol)}`);
236
+ }
237
+ };
238
+ const browserCompatibility = (protocol) => ({
239
+ browser_protocol: BROWSER_PROTOCOL_EPOCH,
240
+ minimum_browser_protocol: minimumBrowserProtocol,
241
+ compatible: minimumBrowserProtocol === 0
242
+ || (protocol !== undefined && protocol >= minimumBrowserProtocol),
243
+ });
244
+ app.get('/api/client-compatibility', (req, reply) => {
245
+ const principal = authed(req, reply);
246
+ if (!principal)
247
+ return;
248
+ const query = req.query;
249
+ const parsed = query.browser_protocol === undefined
250
+ ? undefined
251
+ : Number(query.browser_protocol);
252
+ const protocol = Number.isInteger(parsed) && (parsed ?? 0) > 0 ? parsed : undefined;
253
+ const browserClient = principal.kind === 'browser'
254
+ || (principal.kind !== 'agent' && query.client_kind === 'browser');
255
+ if (browserClient)
256
+ observeBrowserProtocol(protocol);
257
+ const compatibility = browserClient
258
+ ? browserCompatibility(protocol)
259
+ : { ...browserCompatibility(protocol), compatible: true };
260
+ return reply
261
+ .header('cache-control', 'no-store')
262
+ .code(browserClient && !compatibility.compatible ? 426 : 200)
263
+ .send(compatibility);
264
+ });
265
+ // harn:end browser-protocol-epoch-blocks-only-stale-browser-ui
266
+ // harn:assume paired-browser-challenge-session ref=browser-device-session-rest
267
+ app.post('/api/auth/challenge', (req, reply) => {
268
+ if (!options.crypto || !browserAuthTranscript) {
269
+ return reply.code(404).send({ error: 'device authentication is not configured' });
270
+ }
271
+ try {
272
+ const body = req.body;
273
+ if (typeof body.device_id !== 'string' || body.device_id === '') {
274
+ throw new Error('device id is required');
275
+ }
276
+ const challenge = options.crypto.browserChallenges.issue(body.device_id, browserAuthTranscript);
277
+ return reply.header('cache-control', 'no-store').send({
278
+ challenge,
279
+ switchboard_device_id: options.crypto.keys.identity.device_id,
280
+ });
281
+ }
282
+ catch {
283
+ return reply.code(401).send({ error: 'device authentication failed' });
284
+ }
285
+ });
286
+ app.post('/api/auth/session', (req, reply) => {
287
+ if (!options.crypto) {
288
+ return reply.code(404).send({ error: 'device authentication is not configured' });
289
+ }
290
+ try {
291
+ const body = req.body;
292
+ if (typeof body.challenge_id !== 'string' || typeof body.signature !== 'string') {
293
+ throw new Error('challenge response is required');
294
+ }
295
+ const peer = options.crypto.browserChallenges.verify(body.challenge_id, body.signature);
296
+ if (peer.kind !== 'device')
297
+ throw new Error('only paired devices may open browser sessions');
298
+ return reply.header('cache-control', 'no-store').send(options.crypto.browserSessions.issue(peer.device_id));
299
+ }
300
+ catch {
301
+ return reply.code(401).send({ error: 'device authentication failed' });
302
+ }
303
+ });
304
+ // harn:end paired-browser-challenge-session
305
+ // harn:assume unpaired-browser-always-has-enrollment-path ref=trusted-pairing-status-rest
306
+ app.get('/api/pairing/status', (req, reply) => {
307
+ const tailnetLogin = req.headers['tailscale-user-login'];
308
+ return reply.header('cache-control', 'no-store').send({
309
+ trusted_enrollment: options.trustTailscaleServe === true &&
310
+ typeof tailnetLogin === 'string' &&
311
+ tailnetLogin.trim() !== '',
312
+ });
313
+ });
314
+ // harn:end unpaired-browser-always-has-enrollment-path
315
+ // harn:assume pairing-code-exchange-uniform-and-rate-limited ref=pairing-code-exchange-rest
316
+ app.post('/api/pairing/exchange', (req, reply) => {
317
+ const notFound = () => reply.header('cache-control', 'no-store')
318
+ .code(404).send({ error: 'pairing code not found' });
319
+ if (!options.crypto || !allowPairingCodeAttempt(req.raw.socket))
320
+ return notFound();
321
+ try {
322
+ const body = req.body;
323
+ if (typeof body.code !== 'string')
324
+ return notFound();
325
+ return reply.header('cache-control', 'no-store').send(options.crypto.pairing.exchange(body.code));
326
+ }
327
+ catch {
328
+ return notFound();
329
+ }
330
+ });
331
+ // harn:end pairing-code-exchange-uniform-and-rate-limited
332
+ app.post('/api/pairing/complete', (req, reply) => {
333
+ if (!options.crypto)
334
+ return reply.code(404).send({ error: 'pairing is not configured' });
335
+ const authorization = req.headers.authorization;
336
+ const pairingToken = authorization?.startsWith('Pairing ')
337
+ ? authorization.slice('Pairing '.length)
338
+ : undefined;
339
+ try {
340
+ // harn:assume tailnet-auto-pairing-explicit-trust ref=trusted-tailnet-pairing-rest
341
+ const tailnetLogin = req.headers['tailscale-user-login'];
342
+ if (!pairingToken) {
343
+ if (options.trustTailscaleServe !== true ||
344
+ typeof tailnetLogin !== 'string' ||
345
+ tailnetLogin.trim() === '') {
346
+ return reply.code(401).send({ error: 'pairing token required' });
347
+ }
348
+ return reply.header('cache-control', 'no-store').send(options.crypto.pairing.completeTrusted(req.body, tailnetLogin.trim()));
349
+ }
350
+ // harn:end tailnet-auto-pairing-explicit-trust
351
+ return reply.header('cache-control', 'no-store').send(options.crypto.pairing.complete(pairingToken, req.body));
352
+ }
353
+ catch (error) {
354
+ return reply.code(401).send({ error: String(error) });
355
+ }
356
+ });
357
+ app.post('/api/pairing/offers', (req, reply) => {
358
+ const principal = authed(req, reply);
359
+ if (!principal || !authorizeGlobal(principal, 'manage_devices', reply))
360
+ return;
361
+ if (!options.crypto)
362
+ return reply.code(404).send({ error: 'pairing is not configured' });
363
+ try {
364
+ const { endpoint } = req.body;
365
+ return reply.header('cache-control', 'no-store').send(options.crypto.pairing.issue(endpoint));
366
+ }
367
+ catch (error) {
368
+ return reply.code(400).send({ error: String(error) });
369
+ }
370
+ });
371
+ app.post('/api/devices/:deviceId/push-subscription', (req, reply) => {
372
+ const principal = authed(req, reply);
373
+ if (!principal || !authorizeGlobal(principal, 'manage_devices', reply))
374
+ return;
375
+ if (!options.pushSubscriptions) {
376
+ return reply.code(404).send({ error: 'push subscriptions are not configured' });
377
+ }
378
+ const { deviceId } = req.params;
379
+ try {
380
+ const body = req.body;
381
+ return reply.code(201).send({
382
+ subscription: options.pushSubscriptions.register(deviceId, body.subscription),
383
+ });
384
+ }
385
+ catch (error) {
386
+ return reply.code(400).send({ error: String(error) });
387
+ }
388
+ });
389
+ app.delete('/api/devices/:deviceId/push-subscription', (req, reply) => {
390
+ const principal = authed(req, reply);
391
+ if (!principal || !authorizeGlobal(principal, 'manage_devices', reply))
392
+ return;
393
+ if (!options.pushSubscriptions) {
394
+ return reply.code(404).send({ error: 'push subscriptions are not configured' });
395
+ }
396
+ const { deviceId } = req.params;
397
+ options.pushSubscriptions.remove(deviceId);
398
+ return reply.code(204).send();
399
+ });
400
+ app.get('/api/push/config', (req, reply) => {
401
+ const principal = authed(req, reply);
402
+ if (!principal || !authorizeGlobal(principal, 'read', reply))
403
+ return;
404
+ return reply.send({
405
+ enabled: Boolean(options.pushSubscriptions && options.pushVapidPublicKey && options.pushRelayEnabled),
406
+ ...(options.pushVapidPublicKey && { vapid_public_key: options.pushVapidPublicKey }),
407
+ });
408
+ });
409
+ // harn:assume unpair-purges-all-browser-state ref=device-revoke-rest
410
+ app.get('/api/devices', (req, reply) => {
411
+ const principal = authed(req, reply);
412
+ if (!principal || !authorizeGlobal(principal, 'manage_devices', reply))
413
+ return;
414
+ if (!options.crypto)
415
+ return reply.code(404).send({ error: 'pairing is not configured' });
416
+ return reply.send({
417
+ devices: options.crypto.keys.listPeers()
418
+ .filter((peer) => peer.kind === 'device')
419
+ .map((peer) => ({
420
+ device_id: peer.device_id,
421
+ label: peer.label,
422
+ paired_at: peer.paired_at,
423
+ push_enabled: options.pushSubscriptions?.get(peer.device_id) !== undefined,
424
+ })),
425
+ });
426
+ });
427
+ app.delete('/api/devices/:deviceId', (req, reply) => {
428
+ const principal = authed(req, reply);
429
+ if (!principal || !authorizeGlobal(principal, 'manage_devices', reply))
430
+ return;
431
+ if (!options.crypto)
432
+ return reply.code(404).send({ error: 'pairing is not configured' });
433
+ const { deviceId } = req.params;
434
+ const peer = options.crypto.keys.getPeer(deviceId);
435
+ if (!peer || peer.kind !== 'device')
436
+ return reply.code(404).send({ error: 'no such device' });
437
+ options.pushSubscriptions?.remove(deviceId);
438
+ const revoked = options.crypto.revokePeer(deviceId);
439
+ return reply.send({ revoked });
440
+ });
441
+ // harn:end unpair-purges-all-browser-state
442
+ app.get('/api/rooms', (req, reply) => {
443
+ const principal = authed(req, reply);
444
+ if (!principal)
445
+ return;
446
+ if (principal.kind === 'agent') {
447
+ if (!authorizeRoom(principal, principal.room, 'read', reply))
448
+ return;
449
+ }
450
+ else if (!authorizeGlobal(principal, 'read', reply))
451
+ return;
452
+ void reply.send({ rooms: roomsFor(principal) });
453
+ });
454
+ // harn:assume durable-room-summaries-stream-and-fallback ref=durable-room-summary
455
+ // Durable summaries share the exact RoomSupport builder used by streaming
456
+ // subscriptions. The caller-cursor mode stays byte-compatible for clients
457
+ // that have not opted into room-addressed support yet.
458
+ app.get('/api/rooms/summary', (req, reply) => {
459
+ const principal = authed(req, reply);
460
+ if (!principal)
461
+ return;
462
+ if (principal.kind === 'agent') {
463
+ if (!authorizeRoom(principal, principal.room, 'read', reply))
464
+ return;
465
+ }
466
+ else if (!authorizeGlobal(principal, 'read', reply))
467
+ return;
468
+ const query = req.query;
469
+ if (query.read_state !== undefined && query.read_state !== 'durable') {
470
+ return reply.code(400).send({ error: `unsupported read_state: ${query.read_state}` });
471
+ }
472
+ if (query.read_state === 'durable') {
473
+ if (principal.kind === 'agent') {
474
+ return reply.code(400).send({ error: 'durable room summaries require a human principal' });
475
+ }
476
+ return reply.send({
477
+ rooms: roomsFor(principal).map((room) => {
478
+ const actor = memberForRoom(principal, room.id);
479
+ return daemon.roomSupport(room.id, actor.id).summary;
480
+ }),
481
+ });
482
+ }
483
+ const cursors = new Map();
484
+ const rawCursors = query.cursors;
485
+ if (rawCursors !== undefined && rawCursors !== '') {
486
+ for (const entry of rawCursors.split(',')) {
487
+ const split = entry.lastIndexOf(':');
488
+ const id = split === -1 ? Number.NaN : Number(entry.slice(split + 1));
489
+ if (split <= 0 || !Number.isInteger(id) || id < 0) {
490
+ return reply.code(400).send({ error: `malformed cursor: ${entry}` });
491
+ }
492
+ cursors.set(entry.slice(0, split), id);
493
+ }
494
+ }
495
+ void reply.send({
496
+ rooms: roomsFor(principal).map((room) => {
497
+ const members = daemon.store.listMembers(room.id);
498
+ const latest = daemon.store.latestMessage(room.id, { ignoreAcks: true });
499
+ const latestRun = daemon.store.listRunMessages(room.id, { limit: 1 })[0];
500
+ const author = latest ? daemon.store.getMember(room.id, latest.author) : undefined;
501
+ const runAuthor = latestRun
502
+ ? daemon.store.getMember(room.id, latestRun.author)
503
+ : undefined;
504
+ const cursor = cursors.get(room.id);
505
+ const working = members.some((m) => m.kind === 'agent' && (m.state === 'running' || m.state === 'queued'));
506
+ return {
507
+ id: room.id,
508
+ name: room.name,
509
+ created_ts: room.created_ts,
510
+ color: room.config.color,
511
+ working,
512
+ attention: !working
513
+ && latestRun?.run?.status === 'failed'
514
+ && runAuthor?.kind === 'agent'
515
+ && runAuthor.state === 'dead',
516
+ ...(latest !== undefined && {
517
+ latest: {
518
+ id: latest.id,
519
+ ts: latest.ts,
520
+ kind: latest.kind,
521
+ author_handle: author?.handle ?? '',
522
+ author_kind: author?.kind ?? 'human',
523
+ preview: (latest.body.split('\n', 1)[0] ?? '').slice(0, 140),
524
+ },
525
+ }),
526
+ unread: cursor === undefined ? 0 : daemon.store.countMessagesAfter(room.id, cursor),
527
+ };
528
+ }),
529
+ });
530
+ });
531
+ // harn:end durable-room-summaries-stream-and-fallback
532
+ app.get('/api/adapters', (req, reply) => {
533
+ const principal = authed(req, reply);
534
+ if (!principal || !authorizeGlobal(principal, 'read', reply))
535
+ return;
536
+ // harn:assume model-catalogs-reach-a-browser-that-arrives-early ref=adapter-discovery-pending-rest
537
+ void reply.send({
538
+ adapters: daemon.registeredAdapters(),
539
+ discovering: daemon.modelDiscoveryPending(),
540
+ });
541
+ // harn:end model-catalogs-reach-a-browser-that-arrives-early
542
+ });
543
+ // harn:assume local-directory-listing-home-contained ref=local-dirs-rest-boundary
544
+ app.get('/api/local/dirs', (req, reply) => {
545
+ const principal = authed(req, reply);
546
+ if (!principal || !authorizeGlobal(principal, 'manage_agents', reply))
547
+ return;
548
+ const query = req.query;
549
+ try {
550
+ return reply.send(listLocalDirectories(query.path, query.hidden === '1', options.homeDir));
551
+ }
552
+ catch (error) {
553
+ if (error instanceof LocalDirectoryError) {
554
+ return reply.code(error.status).send({ error: error.message });
555
+ }
556
+ throw error;
557
+ }
558
+ });
559
+ // harn:end local-directory-listing-home-contained
560
+ // harn:assume channel-creation-derived-and-seeded ref=create-room-rest-contract
561
+ app.post('/api/rooms', (req, reply) => {
562
+ const principal = authed(req, reply);
563
+ if (!principal || !authorizeGlobal(principal, 'manage_rooms', reply))
564
+ return;
565
+ try {
566
+ const body = CreateRoomRequestSchema.parse(req.body);
567
+ const created = daemon.createRoom(body);
568
+ options.crypto?.roomKeys.ensureRoom(created.room.id);
569
+ // harn:assume browser-created-channel-delivers-its-room-key ref=browser-room-key-response
570
+ // Pairing can only seal rooms that already exist. A device that creates a
571
+ // later room therefore needs that one additive envelope before it opens
572
+ // the room; operator tokens have no device key and keep the old response.
573
+ const roomKey = principal.kind === 'browser'
574
+ ? options.crypto?.roomKeys.sealedFor(principal.deviceId)
575
+ .find((candidate) => candidate.room === created.room.id)
576
+ : undefined;
577
+ return reply.send({ ...created, ...(roomKey !== undefined && { room_key: roomKey }) });
578
+ // harn:end browser-created-channel-delivers-its-room-key
579
+ }
580
+ catch (error) {
581
+ return reply.code(400).send({ error: String(error) });
582
+ }
583
+ });
584
+ // harn:end channel-creation-derived-and-seeded
585
+ app.get('/api/rooms/:room/sync', (req, reply) => {
586
+ const principal = authed(req, reply);
587
+ if (!principal)
588
+ return;
589
+ const { room } = req.params;
590
+ if (!authorizeRoom(principal, room, 'read', reply))
591
+ return;
592
+ const sinceSeq = Number(req.query.since_seq ?? 0);
593
+ try {
594
+ void reply.send(daemon.sync(room, sinceSeq));
595
+ }
596
+ catch {
597
+ void reply.code(404).send({ error: `no such room ${room}` });
598
+ }
599
+ });
600
+ // harn:assume member-status-is-bounded-and-identity-safe ref=status-rest-boundary
601
+ app.get('/api/rooms/:room/members/:memberId/status', (req, reply) => {
602
+ const principal = authed(req, reply);
603
+ if (!principal)
604
+ return;
605
+ const { room, memberId } = req.params;
606
+ if (!authorizeRoom(principal, room, principal.kind === 'agent' ? 'member_status' : 'read', reply))
607
+ return;
608
+ try {
609
+ return reply.send(daemon.memberStatus(room, memberId));
610
+ }
611
+ catch (error) {
612
+ return reply.code(404).send({ error: String(error) });
613
+ }
614
+ });
615
+ // harn:end member-status-is-bounded-and-identity-safe
616
+ // harn:assume permalink-ids-stable ref=message-history-rest
617
+ const positiveInteger = (value, fallback, maximum, label) => {
618
+ if (value === undefined)
619
+ return fallback;
620
+ const parsed = Number(value);
621
+ if (!Number.isSafeInteger(parsed) || parsed < 1 || parsed > maximum) {
622
+ throw new Error(`${label} must be an integer from 1 to ${String(maximum)}`);
623
+ }
624
+ return parsed;
625
+ };
626
+ app.get('/api/rooms/:room/messages', (req, reply) => {
627
+ const principal = authed(req, reply);
628
+ if (!principal)
629
+ return;
630
+ const { room } = req.params;
631
+ if (!authorizeRoom(principal, room, 'read', reply))
632
+ return;
633
+ if (!daemon.store.getRoom(room))
634
+ return reply.code(404).send({ error: `no such room ${room}` });
635
+ try {
636
+ const query = req.query;
637
+ // Pins are few; the strip hydrates the whole set at once, ignoring paging.
638
+ if (query.pinned === '1') {
639
+ const pinned = daemon.store.listPinnedMessages(room);
640
+ return reply.send({ messages: daemon.project(room, pinned), has_more: false });
641
+ }
642
+ const limit = positiveInteger(query.limit, 50, 100, 'limit');
643
+ const before = positiveInteger(query.before, Number.MAX_SAFE_INTEGER, Number.MAX_SAFE_INTEGER, 'before');
644
+ const page = daemon.store.listMessages(room, { before, limit: limit + 1 });
645
+ const hasMore = page.length > limit;
646
+ const messages = hasMore ? page.slice(-limit) : page;
647
+ return reply.send({ messages: daemon.project(room, messages), has_more: hasMore });
648
+ }
649
+ catch (error) {
650
+ return reply.code(400).send({ error: String(error) });
651
+ }
652
+ });
653
+ app.get('/api/rooms/:room/search', (req, reply) => {
654
+ const principal = authed(req, reply);
655
+ if (!principal)
656
+ return;
657
+ const { room } = req.params;
658
+ if (!authorizeRoom(principal, room, principal.kind === 'agent' ? 'search' : 'read', reply))
659
+ return;
660
+ if (!daemon.store.getRoom(room))
661
+ return reply.code(404).send({ error: `no such room ${room}` });
662
+ try {
663
+ const query = req.query;
664
+ const needle = query.q?.trim();
665
+ if (!needle || needle.length > 200)
666
+ throw new Error('q must contain 1 to 200 characters');
667
+ if (query.include !== undefined && query.include !== 'runs') {
668
+ throw new Error('include must be runs when provided');
669
+ }
670
+ const includeRuns = query.include === 'runs';
671
+ const limit = positiveInteger(query.limit, 50, includeRuns ? 200 : 100, 'limit');
672
+ const messages = daemon.store.searchMessages(room, needle, { limit });
673
+ // harn:assume run-evidence-search-is-bounded-and-redacted ref=run-search-rest-boundary
674
+ return reply.send({
675
+ messages: daemon.project(room, messages),
676
+ ...(includeRuns && { runs: daemon.searchRunEvidence(room, needle, limit) }),
677
+ });
678
+ // harn:end run-evidence-search-is-bounded-and-redacted
679
+ }
680
+ catch (error) {
681
+ return reply.code(400).send({ error: String(error) });
682
+ }
683
+ });
684
+ // harn:end permalink-ids-stable
685
+ app.get('/api/rooms/:room/runs/:msgId', (req, reply) => {
686
+ const principal = authed(req, reply);
687
+ if (!principal)
688
+ return;
689
+ const { room, msgId } = req.params;
690
+ if (!authorizeRoom(principal, room, 'read', reply))
691
+ return;
692
+ void reply.send({ events: daemon.readRunBlob(room, Number(msgId)) });
693
+ });
694
+ // harn:assume room-git-state-read-only-from-known-cwds ref=room-git-state-contract
695
+ // The diff explorer's live git working-state. `cwd` is a SELECTOR into the
696
+ // room's known directories, never a free path — the daemon refuses anything
697
+ // outside that set before running any (read-only) git command.
698
+ app.get('/api/rooms/:room/git-diff', async (req, reply) => {
699
+ const principal = authed(req, reply);
700
+ if (!principal)
701
+ return;
702
+ const { room } = req.params;
703
+ if (!authorizeRoom(principal, room, 'read', reply))
704
+ return;
705
+ if (!daemon.store.getRoom(room))
706
+ return reply.code(404).send({ error: `no such room ${room}` });
707
+ try {
708
+ const { cwd } = req.query;
709
+ return reply.send(await daemon.gitWorkingState(room, cwd));
710
+ }
711
+ catch (error) {
712
+ return reply.code(400).send({ error: String(error) });
713
+ }
714
+ });
715
+ // harn:end room-git-state-read-only-from-known-cwds
716
+ // harn:assume attachments-are-capped-files-served-inert ref=attachment-contract
717
+ // Upload streams one file to disk under the room's attachment dir keyed by a
718
+ // server-issued id (the client filename is metadata only), refusing >25 MB.
719
+ app.post('/api/rooms/:room/attachments', { bodyLimit: MAX_ATTACHMENT_BYTES + 4096 }, async (req, reply) => {
720
+ const principal = authed(req, reply);
721
+ if (!principal)
722
+ return;
723
+ const { room } = req.params;
724
+ if (!authorizeRoom(principal, room, 'post', reply))
725
+ return;
726
+ if (!daemon.store.getRoom(room))
727
+ return reply.code(404).send({ error: `no such room ${room}` });
728
+ const declared = Number(req.headers['content-length'] ?? 0);
729
+ if (Number.isFinite(declared) && declared > MAX_ATTACHMENT_BYTES) {
730
+ return reply.code(413).send({ error: 'attachment exceeds 25 MB' });
731
+ }
732
+ const { name } = req.query;
733
+ const filename = (name ?? 'file').slice(0, 255) || 'file';
734
+ const mime = (req.headers['content-type'] ?? 'application/octet-stream').split(';')[0]?.trim()
735
+ || 'application/octet-stream';
736
+ const id = daemon.newAttachmentId();
737
+ daemon.ensureAttachmentDir(room);
738
+ const path = daemon.attachmentPath(room, id);
739
+ let size = 0;
740
+ let tooBig = false;
741
+ const counter = new Transform({
742
+ transform(chunk, _enc, callback) {
743
+ size += chunk.length;
744
+ if (size > MAX_ATTACHMENT_BYTES) {
745
+ tooBig = true;
746
+ callback(new Error('attachment exceeds 25 MB'));
747
+ return;
748
+ }
749
+ callback(null, chunk);
750
+ },
751
+ });
752
+ try {
753
+ await pipeline(req.body, counter, createWriteStream(path));
754
+ }
755
+ catch (error) {
756
+ rmSync(path, { force: true });
757
+ return reply.code(tooBig ? 413 : 400).send({ error: String(error) });
758
+ }
759
+ const meta = { id, name: filename, mime, size };
760
+ daemon.recordAttachment(room, meta);
761
+ return reply.send(meta);
762
+ });
763
+ // Serve a stored attachment with its recorded mime and name. The id is
764
+ // validated (hex handle) before it ever touches a path — traversal is dead.
765
+ app.get('/api/rooms/:room/attachments/:id', (req, reply) => {
766
+ const principal = authed(req, reply);
767
+ if (!principal)
768
+ return;
769
+ const { room, id } = req.params;
770
+ if (!authorizeRoom(principal, room, 'read', reply))
771
+ return;
772
+ const meta = daemon.getAttachmentMeta(room, id);
773
+ if (!meta)
774
+ return reply.code(404).send({ error: 'no such attachment' });
775
+ const path = daemon.attachmentPath(room, id);
776
+ if (!existsSync(path))
777
+ return reply.code(404).send({ error: 'no such attachment' });
778
+ // Inertness: the mime came from the uploader, so only raster images (not
779
+ // svg, which scripts) render inline with it; everything else downloads as
780
+ // an opaque octet-stream. Uploaded content can never execute same-origin.
781
+ const inline = /^image\/(png|jpe?g|gif|webp|avif)$/.test(meta.mime);
782
+ void reply
783
+ .header('x-content-type-options', 'nosniff')
784
+ .header('content-type', inline ? meta.mime : 'application/octet-stream')
785
+ .header('content-disposition', `${inline ? 'inline' : 'attachment'}; filename*=UTF-8''${encodeURIComponent(meta.name)}`)
786
+ .send(createReadStream(path));
787
+ });
788
+ // harn:end attachments-are-capped-files-served-inert
789
+ // harn:assume graph-derived-from-vault-links-readonly-v5 ref=ledger-graph-rest
790
+ app.get('/api/rooms/:room/ledger', (req, reply) => {
791
+ const principal = authed(req, reply);
792
+ if (!principal)
793
+ return;
794
+ const { room } = req.params;
795
+ if (!authorizeRoom(principal, room, 'read', reply))
796
+ return;
797
+ try {
798
+ return reply.send({ graph: daemon.project(room, daemon.ledgerGraph(room)) });
799
+ }
800
+ catch {
801
+ return reply.code(500).send({ error: 'ledger graph unavailable' });
802
+ }
803
+ });
804
+ // harn:end graph-derived-from-vault-links-readonly-v5
805
+ app.get('/api/rooms/:room/ledger/:name', (req, reply) => {
806
+ const principal = authed(req, reply);
807
+ if (!principal)
808
+ return;
809
+ const { room, name } = req.params;
810
+ if (!authorizeRoom(principal, room, 'read', reply))
811
+ return;
812
+ try {
813
+ const note = daemon.getLedgerNote(room, name);
814
+ if (!note)
815
+ return reply.code(404).send({ error: `no such ledger note ${name}` });
816
+ return reply.send({ note: daemon.project(room, note) });
817
+ }
818
+ catch (error) {
819
+ return reply.code(404).send({ error: String(error) });
820
+ }
821
+ });
822
+ // harn:assume bridge-enable-admin-or-owner ref=bridge-rest-boundary
823
+ app.post('/api/rooms/:room/bridges', (req, reply) => {
824
+ const principal = authed(req, reply);
825
+ if (!principal)
826
+ return;
827
+ const { room } = req.params;
828
+ if (!authorizeRoom(principal, room, 'enable_bridge', reply))
829
+ return;
830
+ try {
831
+ const body = req.body;
832
+ if (body.platform !== 'slack' && body.platform !== 'telegram') {
833
+ throw new Error('platform must be slack or telegram');
834
+ }
835
+ if (typeof body.channel !== 'string' || body.channel.trim() === '' || body.channel.length > 200) {
836
+ throw new Error('channel must contain 1 to 200 characters');
837
+ }
838
+ return reply.code(201).send(daemon.enableBridge(room, body.platform, body.channel));
839
+ }
840
+ catch (error) {
841
+ return reply.code(400).send({ error: String(error) });
842
+ }
843
+ });
844
+ app.post('/api/rooms/:room/bridges/:memberId/messages', (req, reply) => {
845
+ const principal = authed(req, reply);
846
+ if (!principal)
847
+ return;
848
+ const { room, memberId } = req.params;
849
+ if (!authorizeRoom(principal, room, 'enable_bridge', reply))
850
+ return;
851
+ try {
852
+ const body = req.body;
853
+ if (typeof body.body !== 'string' || body.body.trim() === '' || body.body.length > 100_000) {
854
+ throw new Error('body must contain 1 to 100000 characters');
855
+ }
856
+ return reply.send(daemon.postBridgeMessage(room, memberId, body.body, body.origin));
857
+ }
858
+ catch (error) {
859
+ return reply.code(400).send({ error: String(error) });
860
+ }
861
+ });
862
+ // harn:assume bridge-runtime-persists-delivery-progress ref=bridge-outbound-ready-window
863
+ app.get('/api/rooms/:room/bridges/:memberId/outbound', (req, reply) => {
864
+ const principal = authed(req, reply);
865
+ if (!principal)
866
+ return;
867
+ const { room, memberId } = req.params;
868
+ if (!authorizeRoom(principal, room, 'enable_bridge', reply))
869
+ return;
870
+ try {
871
+ const bridge = daemon.store.getMember(room, memberId);
872
+ if (bridge?.kind !== 'bridge')
873
+ return reply.code(404).send({ error: 'no such bridge' });
874
+ const query = req.query;
875
+ const after = query.after === undefined ? 0 : Number(query.after);
876
+ const limit = query.limit === undefined ? 100 : Number(query.limit);
877
+ if (!Number.isSafeInteger(after) || after < 0)
878
+ throw new Error('after must be a non-negative integer');
879
+ if (!Number.isSafeInteger(limit) || limit < 1 || limit > 100) {
880
+ throw new Error('limit must be an integer from 1 to 100');
881
+ }
882
+ const scanned = daemon.bridgeMessagesAfter(room, after, limit);
883
+ const platform = bridge.handle.slice(0, -'-bridge'.length);
884
+ const messages = [];
885
+ let nextAfter = after;
886
+ for (const message of scanned) {
887
+ if (message.kind === 'run' && message.run?.status === 'running')
888
+ break;
889
+ nextAfter = message.id;
890
+ if (message.kind === 'run' && message.body.trim() === '')
891
+ continue;
892
+ if (message.author === bridge.id && message.origin?.platform === platform)
893
+ continue;
894
+ messages.push(message);
895
+ }
896
+ return reply.send({
897
+ messages: daemon.project(room, messages),
898
+ next_after: nextAfter,
899
+ });
900
+ }
901
+ catch (error) {
902
+ return reply.code(400).send({ error: String(error) });
903
+ }
904
+ });
905
+ // harn:end bridge-runtime-persists-delivery-progress
906
+ // harn:end bridge-enable-admin-or-owner
907
+ app.post('/api/rooms/:room/members', (req, reply) => {
908
+ const principal = authed(req, reply);
909
+ if (!principal)
910
+ return;
911
+ const { room } = req.params;
912
+ if (!authorizeRoom(principal, room, 'spawn', reply))
913
+ return;
914
+ const body = req.body;
915
+ try {
916
+ return reply.send(daemon.spawnMember(room, body));
917
+ }
918
+ catch (error) {
919
+ return reply.code(400).send({ error: String(error) });
920
+ }
921
+ });
922
+ app.get('/api/rooms/:room/members', (req, reply) => {
923
+ const principal = authed(req, reply);
924
+ if (!principal)
925
+ return;
926
+ const { room } = req.params;
927
+ if (!authorizeRoom(principal, room, 'read', reply))
928
+ return;
929
+ void reply.send({ members: daemon.project(room, daemon.memberDetails(room)) });
930
+ });
931
+ app.patch('/api/rooms/:room/members/:memberId', (req, reply) => {
932
+ const principal = authed(req, reply);
933
+ if (!principal)
934
+ return;
935
+ const { room, memberId } = req.params;
936
+ if (!authorizeRoom(principal, room, 'rename', reply))
937
+ return;
938
+ const body = req.body;
939
+ void reply.send(daemon.renameMember(room, memberId, body.handle, body.display_name));
940
+ });
941
+ // harn:assume member-config-is-changed-not-respawned ref=configure-act-contract
942
+ app.post('/api/rooms/:room/members/:memberId/configure', (req, reply) => {
943
+ const principal = authed(req, reply);
944
+ if (!principal)
945
+ return;
946
+ const { room, memberId } = req.params;
947
+ if (!authorizeRoom(principal, room, 'configure', reply))
948
+ return;
949
+ const body = req.body;
950
+ const actor = memberForRoom(principal, room);
951
+ void reply.send(daemon.configureMember(room, memberId, body, { actor: actor.id }));
952
+ });
953
+ // harn:end member-config-is-changed-not-respawned
954
+ for (const action of ['revive', 'kill', 'pause', 'unpause']) {
955
+ app.post(`/api/rooms/:room/members/:memberId/${action}`, (req, reply) => {
956
+ const principal = authed(req, reply);
957
+ if (!principal)
958
+ return;
959
+ const { room, memberId } = req.params;
960
+ if (!authorizeRoom(principal, room, action, reply))
961
+ return;
962
+ const member = action === 'revive'
963
+ ? daemon.reviveMember(room, memberId)
964
+ : action === 'kill'
965
+ ? daemon.killMember(room, memberId)
966
+ : action === 'pause'
967
+ ? daemon.pauseMember(room, memberId)
968
+ : daemon.unpauseMember(room, memberId);
969
+ void reply.send(member);
970
+ });
971
+ }
972
+ if (options.staticRoot !== undefined) {
973
+ await app.register(fastifyStatic, { root: options.staticRoot });
974
+ app.setNotFoundHandler((req, reply) => {
975
+ if (req.url.startsWith('/api/'))
976
+ return reply.code(404).send({ error: 'not found' });
977
+ return reply.sendFile('index.html'); // SPA fallback
978
+ });
979
+ }
980
+ await app.listen({ host: options.host ?? '127.0.0.1', port: options.port ?? 0 });
981
+ const address = app.server.address();
982
+ const port = typeof address === 'object' && address !== null ? address.port : 0;
983
+ // ── WebSocket: subscribe / post / act ─────────────────────────────────
984
+ const wss = new WebSocketServer({ server: app.server, path: '/ws' });
985
+ let ipcServer;
986
+ let ipcWss;
987
+ const subscriptions = new Map();
988
+ const projectLiveFrame = (room, frame, subscription) => {
989
+ if (!subscription.roomAddressed || frame.type !== 'member')
990
+ return frame;
991
+ return { ...frame, room };
992
+ };
993
+ const pendingSupportRooms = new Set();
994
+ const sendRoomSupport = (socket, room, subscription, seq = daemon.store.currentSeq(room)) => {
995
+ if (subscription.memberId === undefined || socket.readyState !== socket.OPEN)
996
+ return;
997
+ const support = daemon.roomSupport(room, subscription.memberId);
998
+ const serialized = JSON.stringify(support);
999
+ if (serialized === subscription.lastSupport)
1000
+ return;
1001
+ subscription.lastSupport = serialized;
1002
+ socket.send(JSON.stringify({ type: 'room_support', seq, support }));
1003
+ };
1004
+ const scheduleRoomSupport = (room) => {
1005
+ if (pendingSupportRooms.has(room))
1006
+ return;
1007
+ pendingSupportRooms.add(room);
1008
+ queueMicrotask(() => {
1009
+ pendingSupportRooms.delete(room);
1010
+ for (const [socket, rooms] of subscriptions) {
1011
+ const subscription = rooms.get(room);
1012
+ if (subscription)
1013
+ sendRoomSupport(socket, room, subscription);
1014
+ }
1015
+ });
1016
+ };
1017
+ // harn:end room-support-is-bounded-recipient-scoped-state
1018
+ // harn:end multiplexed-subscriptions-identify-their-room
1019
+ const deviceSockets = new Map();
1020
+ // harn:assume paired-browser-challenge-session ref=browser-device-session-socket
1021
+ const stopDeviceRevocations = options.crypto?.keys.onPeerRevoked((deviceId) => {
1022
+ const sockets = deviceSockets.get(deviceId);
1023
+ if (!sockets)
1024
+ return;
1025
+ deviceSockets.delete(deviceId);
1026
+ for (const socket of sockets)
1027
+ socket.close(4403, 'device revoked');
1028
+ });
1029
+ // harn:end paired-browser-challenge-session
1030
+ // harn:assume multiplexed-subscriptions-identify-their-room ref=room-addressed-live-fanout
1031
+ const unsubscribeFrames = daemon.onFrame((room, frame) => {
1032
+ for (const [socket, rooms] of subscriptions) {
1033
+ const subscription = rooms.get(room);
1034
+ if (subscription && socket.readyState === socket.OPEN) {
1035
+ socket.send(JSON.stringify(projectLiveFrame(room, frame, subscription)));
1036
+ }
1037
+ }
1038
+ // harn:assume room-support-is-bounded-recipient-scoped-state ref=room-support-fanout
1039
+ if (frame.type === 'message'
1040
+ || frame.type === 'member'
1041
+ || frame.type === 'inbox'
1042
+ || frame.type === 'room') {
1043
+ scheduleRoomSupport(room);
1044
+ }
1045
+ // harn:end room-support-is-bounded-recipient-scoped-state
1046
+ });
1047
+ // harn:end multiplexed-subscriptions-identify-their-room
1048
+ // harn:assume unix-socket-same-protocol ref=unix-websocket-listener
1049
+ const bindProtocol = (server, authenticate) => {
1050
+ server.on('connection', (socket, req) => {
1051
+ const url = new URL(req.url ?? '/', 'http://localhost');
1052
+ const principal = authenticate(url);
1053
+ if (!principal) {
1054
+ socket.close(4401, 'unauthorized');
1055
+ return;
1056
+ }
1057
+ // harn:assume multiplexed-subscriptions-identify-their-room ref=room-addressed-live-fanout
1058
+ subscriptions.set(socket, new Map());
1059
+ // harn:end multiplexed-subscriptions-identify-their-room
1060
+ if (principal.kind === 'browser') {
1061
+ const sockets = deviceSockets.get(principal.deviceId) ?? new Set();
1062
+ sockets.add(socket);
1063
+ deviceSockets.set(principal.deviceId, sockets);
1064
+ }
1065
+ socket.on('close', () => {
1066
+ subscriptions.delete(socket);
1067
+ if (principal.kind !== 'browser')
1068
+ return;
1069
+ const sockets = deviceSockets.get(principal.deviceId);
1070
+ sockets?.delete(socket);
1071
+ if (sockets?.size === 0)
1072
+ deviceSockets.delete(principal.deviceId);
1073
+ });
1074
+ const send = (frame) => {
1075
+ socket.send(JSON.stringify(frame));
1076
+ };
1077
+ socket.on('message', (raw) => {
1078
+ let frame;
1079
+ try {
1080
+ frame = ClientFrameSchema.parse(JSON.parse(raw.toString()));
1081
+ }
1082
+ catch (error) {
1083
+ return send({ type: 'error', message: `invalid frame: ${String(error)}` });
1084
+ }
1085
+ try {
1086
+ if (frame.type === 'mirror_turn') {
1087
+ const joined = daemon.store.findMemberBySessionRef(frame.harness, frame.session_ref);
1088
+ if (!joined)
1089
+ throw new Error(`no mirrored member for ${frame.harness} session ${frame.session_ref}`);
1090
+ assertRoomCapability(principal, joined.room, 'mirror_turn');
1091
+ const mirrored = daemon.mirrorTurn(frame);
1092
+ send({
1093
+ type: 'mirror_ack',
1094
+ native_turn_id: frame.native_turn_id,
1095
+ message_id: mirrored.message.id,
1096
+ deduped: mirrored.deduped,
1097
+ });
1098
+ }
1099
+ else if (frame.type === 'mirror_session_end') {
1100
+ const joined = daemon.store.findMemberBySessionRef(frame.harness, frame.session_ref);
1101
+ if (!joined)
1102
+ throw new Error(`no mirrored member for ${frame.harness} session ${frame.session_ref}`);
1103
+ assertRoomCapability(principal, joined.room, 'mirror_session_end');
1104
+ send({
1105
+ type: 'mirror_ack',
1106
+ adopted: daemon.mirrorSessionEnd(frame.harness, frame.session_ref),
1107
+ });
1108
+ }
1109
+ else if (frame.type === 'list_rooms') {
1110
+ if (principal.kind === 'agent') {
1111
+ assertRoomCapability(principal, principal.room, 'read');
1112
+ }
1113
+ else if (!authorizeGlobal(principal, 'read')) {
1114
+ throw new Error('forbidden: principal cannot list rooms');
1115
+ }
1116
+ send({
1117
+ type: 'rooms',
1118
+ rooms: roomsFor(principal).map((room) => daemon.project(room.id, room)),
1119
+ });
1120
+ }
1121
+ else if (frame.type === 'subscribe') {
1122
+ // harn:assume browser-protocol-epoch-blocks-only-stale-browser-ui ref=browser-protocol-admission
1123
+ const browserClient = principal.kind === 'browser'
1124
+ || (principal.kind !== 'agent' && frame.client_kind === 'browser');
1125
+ if (browserClient)
1126
+ observeBrowserProtocol(frame.browser_protocol);
1127
+ if (browserClient
1128
+ && minimumBrowserProtocol > 0
1129
+ && (frame.browser_protocol ?? 0) < minimumBrowserProtocol) {
1130
+ send({
1131
+ type: 'upgrade_required',
1132
+ minimum_browser_protocol: minimumBrowserProtocol,
1133
+ current_browser_protocol: BROWSER_PROTOCOL_EPOCH,
1134
+ });
1135
+ socket.close(4406, 'browser upgrade required');
1136
+ return;
1137
+ }
1138
+ // harn:end browser-protocol-epoch-blocks-only-stale-browser-ui
1139
+ const actor = assertRoomCapability(principal, frame.room, 'read');
1140
+ // harn:assume multiplexed-subscriptions-identify-their-room ref=room-addressed-hydration
1141
+ const roomAddressed = frame.room_addressed === true;
1142
+ // harn:assume room-support-is-bounded-recipient-scoped-state ref=room-support-fanout
1143
+ const supportMemberId = roomAddressed && actor.kind === 'human'
1144
+ ? actor.id
1145
+ : undefined;
1146
+ const subscription = {
1147
+ roomAddressed,
1148
+ ...(supportMemberId !== undefined && { memberId: supportMemberId }),
1149
+ };
1150
+ subscriptions.get(socket).set(frame.room, subscription);
1151
+ // harn:end room-support-is-bounded-recipient-scoped-state
1152
+ const address = roomAddressed ? { room: frame.room } : {};
1153
+ // harn:end multiplexed-subscriptions-identify-their-room
1154
+ // The bound is the subscriber's own: passed straight through, honoured
1155
+ // only on a cold subscribe, and scoped to this actor so their unread
1156
+ // deliveries' messages ride along. Omit it and the replay is today's.
1157
+ const sync = daemon.sync(frame.room, frame.since_seq, {
1158
+ hydrateLimit: frame.hydrate_limit,
1159
+ subscriber: actor.id,
1160
+ strictTail: roomAddressed,
1161
+ supportFor: supportMemberId,
1162
+ });
1163
+ const hydrationCursor = frame.since_seq;
1164
+ // harn:assume multiplexed-subscriptions-identify-their-room ref=room-addressed-hydration
1165
+ send({ type: 'self', member_id: actor.id, ...address });
1166
+ send({ type: 'room', seq: hydrationCursor, room: sync.room });
1167
+ for (const member of sync.members) {
1168
+ send({ type: 'member', seq: hydrationCursor, member, ...address });
1169
+ }
1170
+ // harn:end multiplexed-subscriptions-identify-their-room
1171
+ for (const message of sync.messages)
1172
+ send({ type: 'message', seq: hydrationCursor, message });
1173
+ // harn:assume agent-sync-hydrates-only-own-queued-inbox ref=agent-own-queued-sync-overlay
1174
+ const inbox = principal.kind === 'agent'
1175
+ ? new Map([
1176
+ ...sync.inbox
1177
+ .filter((delivery) => delivery.recipient === actor.id)
1178
+ .map((delivery) => [delivery.id, delivery]),
1179
+ ...daemon.store.listDeliveries(frame.room, {
1180
+ recipient: actor.id,
1181
+ state: 'queued',
1182
+ }).map((delivery) => [delivery.id, delivery]),
1183
+ ]).values()
1184
+ : sync.inbox;
1185
+ for (const delivery of inbox)
1186
+ send({ type: 'inbox', seq: hydrationCursor, delivery });
1187
+ // harn:end agent-sync-hydrates-only-own-queued-inbox
1188
+ for (const meter of sync.meters)
1189
+ send({ type: 'meter', seq: hydrationCursor, meter });
1190
+ // harn:assume room-support-is-bounded-recipient-scoped-state ref=room-support-fanout
1191
+ if (sync.support !== undefined) {
1192
+ subscription.lastSupport = JSON.stringify(sync.support);
1193
+ send({ type: 'room_support', seq: hydrationCursor, support: sync.support });
1194
+ }
1195
+ // harn:end room-support-is-bounded-recipient-scoped-state
1196
+ send({
1197
+ type: 'sync_complete',
1198
+ seq: sync.seq,
1199
+ // harn:assume multiplexed-subscriptions-identify-their-room ref=room-addressed-hydration
1200
+ ...address,
1201
+ // harn:end multiplexed-subscriptions-identify-their-room
1202
+ // The floor is the server's, not a guess from what happened to arrive.
1203
+ ...(sync.history_floor !== undefined && { history_floor: sync.history_floor }),
1204
+ });
1205
+ }
1206
+ else if (frame.type === 'post') {
1207
+ const actor = assertRoomCapability(principal, frame.room, 'post');
1208
+ if (principal.kind === 'agent') {
1209
+ daemon.postAgentMessage(frame.room, actor.id, frame.body, frame.reply_to, frame.awaiting_reply);
1210
+ }
1211
+ else {
1212
+ // Resolve prior uploads to metadata (refuses unknown/cross-room ids
1213
+ // and over-count); refuse a post with neither body nor attachments.
1214
+ const attachments = daemon.resolveAttachmentsForPost(frame.room, frame.attachments);
1215
+ if (frame.body.trim().length === 0 && attachments.length === 0) {
1216
+ throw new Error('a post needs body text or at least one attachment');
1217
+ }
1218
+ daemon.postHumanMessage(frame.room, frame.body, {
1219
+ author: actor.id,
1220
+ reply_to: frame.reply_to,
1221
+ attachments,
1222
+ });
1223
+ }
1224
+ }
1225
+ else if (frame.type === 'act') {
1226
+ const act = frame.act;
1227
+ const actor = assertRoomCapability(principal, frame.room, act.act);
1228
+ if (act.act === 'answer_interaction') {
1229
+ void daemon
1230
+ .answerInteraction(frame.room, act.interaction_id, act.answer, actor.id)
1231
+ .catch((error) => send({ type: 'error', message: String(error), ref: 'answer_interaction' }));
1232
+ }
1233
+ else if (act.act === 'join') {
1234
+ daemon.joinMember(frame.room, {
1235
+ harness: act.harness,
1236
+ handle: act.handle,
1237
+ session_ref: act.session_ref,
1238
+ cwd: act.cwd,
1239
+ policy: act.policy,
1240
+ purpose: act.purpose,
1241
+ });
1242
+ }
1243
+ else if (act.act === 'adopt')
1244
+ daemon.adoptMember(frame.room, act.member_id);
1245
+ else if (act.act === 'attach_acquire') {
1246
+ void daemon.acquireAttachLease(frame.room, act.member_id, act.cli_pid)
1247
+ .then(({ lease, member }) => send({
1248
+ type: 'attach_lease',
1249
+ status: 'acquired',
1250
+ lease,
1251
+ member,
1252
+ }))
1253
+ .catch((error) => send({
1254
+ type: 'error',
1255
+ message: String(error),
1256
+ ref: 'attach_acquire',
1257
+ }));
1258
+ }
1259
+ else if (act.act === 'attach_child') {
1260
+ // harn:assume attach-lease-actions-room-bound ref=attach-lease-room-authorization
1261
+ const attachLease = daemon.store.getAttachLease(act.lease_id);
1262
+ if (!attachLease || attachLease.room !== frame.room) {
1263
+ throw new Error(`no such attach lease ${act.lease_id}`);
1264
+ }
1265
+ const { lease, member } = daemon.reportAttachChild(act.lease_id, act.child_pid, act.process_group_id);
1266
+ send({ type: 'attach_lease', status: 'child_recorded', lease, member });
1267
+ }
1268
+ else if (act.act === 'attach_heartbeat') {
1269
+ const attachLease = daemon.store.getAttachLease(act.lease_id);
1270
+ if (!attachLease || attachLease.room !== frame.room) {
1271
+ throw new Error(`no such attach lease ${act.lease_id}`);
1272
+ }
1273
+ daemon.heartbeatAttachLease(act.lease_id);
1274
+ }
1275
+ else if (act.act === 'attach_complete') {
1276
+ const attachLease = daemon.store.getAttachLease(act.lease_id);
1277
+ if (!attachLease || attachLease.room !== frame.room) {
1278
+ throw new Error(`no such attach lease ${act.lease_id}`);
1279
+ }
1280
+ const completed = daemon.completeAttachLease(act.lease_id);
1281
+ send({
1282
+ type: 'attach_lease',
1283
+ status: completed.status,
1284
+ lease: completed.lease,
1285
+ member: completed.member,
1286
+ });
1287
+ // harn:end attach-lease-actions-room-bound
1288
+ }
1289
+ else if (act.act === 'configure_room') {
1290
+ daemon.configureRoom(frame.room, {
1291
+ ...(act.turn_brake !== undefined && { turn_brake: act.turn_brake }),
1292
+ ...(act.spend_brake_usd !== undefined && {
1293
+ spend_brake_usd: act.spend_brake_usd,
1294
+ }),
1295
+ ...(act.stall_minutes !== undefined && { stall_minutes: act.stall_minutes }),
1296
+ });
1297
+ }
1298
+ else if (act.act === 'redeliver')
1299
+ daemon.redeliver(frame.room, act.delivery_id);
1300
+ else if (act.act === 'release_hold')
1301
+ daemon.releaseHold(frame.room, act.delivery_id);
1302
+ else if (act.act === 'mark_read')
1303
+ daemon.markRead(frame.room, act.delivery_id, actor.id);
1304
+ // harn:assume human-room-read-cursors-are-durable-and-monotonic ref=durable-room-read-storage
1305
+ else if (act.act === 'mark_room_read') {
1306
+ daemon.markRoomRead(frame.room, act.through_seq, actor.id);
1307
+ scheduleRoomSupport(frame.room);
1308
+ }
1309
+ // harn:end human-room-read-cursors-are-durable-and-monotonic
1310
+ // harn:assume live-delivery-consumption-is-idempotent ref=consume-act-dispatch
1311
+ else if (act.act === 'consume_delivery') {
1312
+ const consumed = daemon.consumeDelivery(frame.room, act.delivery_id, actor.id);
1313
+ send({ type: 'consume_result', ...consumed });
1314
+ }
1315
+ // harn:end live-delivery-consumption-is-idempotent
1316
+ // harn:assume live-agent-waits-are-transient ref=wait-act-dispatch
1317
+ else if (act.act === 'wait_begin') {
1318
+ if (principal.kind !== 'agent')
1319
+ throw new Error('forbidden: waits require an agent credential');
1320
+ daemon.beginWait(frame.room, actor.id, {
1321
+ reason: act.reason,
1322
+ peers: act.peers,
1323
+ until_ts: act.until_ts,
1324
+ });
1325
+ }
1326
+ else if (act.act === 'wait_end') {
1327
+ if (principal.kind !== 'agent')
1328
+ throw new Error('forbidden: waits require an agent credential');
1329
+ daemon.endWait(frame.room, actor.id);
1330
+ }
1331
+ // harn:end live-agent-waits-are-transient
1332
+ else if (act.act === 'spawn') {
1333
+ daemon.spawnMember(frame.room, {
1334
+ harness: act.harness,
1335
+ handle: act.handle,
1336
+ cwd: act.cwd,
1337
+ policy: act.policy,
1338
+ model: act.model,
1339
+ thinking: act.thinking,
1340
+ purpose: act.purpose,
1341
+ });
1342
+ }
1343
+ else if (act.act === 'configure') {
1344
+ daemon.configureMember(frame.room, act.member_id, { model: act.model, thinking: act.thinking, policy: act.policy }, { actor: actor.id });
1345
+ }
1346
+ else if (act.act === 'rename')
1347
+ daemon.renameMember(frame.room, act.member_id, act.handle, act.display_name);
1348
+ else if (act.act === 'revive')
1349
+ daemon.reviveMember(frame.room, act.member_id);
1350
+ else if (act.act === 'kill')
1351
+ daemon.killMember(frame.room, act.member_id);
1352
+ else if (act.act === 'remove')
1353
+ daemon.removeMember(frame.room, act.member_id);
1354
+ else if (act.act === 'pause')
1355
+ daemon.pauseMember(frame.room, act.member_id);
1356
+ else if (act.act === 'unpause')
1357
+ daemon.unpauseMember(frame.room, act.member_id);
1358
+ else if (act.act === 'interrupt')
1359
+ daemon.interruptMember(frame.room, act.member_id);
1360
+ // Compaction is a round trip to the engine: report its refusal or
1361
+ // failure back on this connection like any other act.
1362
+ else if (act.act === 'compact_member') {
1363
+ void daemon
1364
+ .compactMember(frame.room, act.member_id, actor.id)
1365
+ .catch((error) => send({ type: 'error', message: String(error), ref: 'compact_member' }));
1366
+ }
1367
+ else if (act.act === 'set_role')
1368
+ daemon.setHumanRole(frame.room, act.member_id, act.role);
1369
+ else if (act.act === 'pin_message')
1370
+ daemon.pinMessage(frame.room, act.message_id, act.pinned, actor.id);
1371
+ else if (act.act === 'delete_message')
1372
+ daemon.deleteMessage(frame.room, act.message_id, actor.id);
1373
+ else if (act.act === 'retry_run')
1374
+ daemon.retryRun(frame.room, act.message_id, actor.id);
1375
+ }
1376
+ }
1377
+ catch (error) {
1378
+ send({ type: 'error', message: String(error), ref: frame.type });
1379
+ }
1380
+ });
1381
+ });
1382
+ };
1383
+ bindProtocol(wss, (url) => principalForToken(url.searchParams.get('token') ?? undefined));
1384
+ if (options.socketPath !== undefined) {
1385
+ ipcServer = createHttpServer((_req, res) => res.writeHead(404).end());
1386
+ ipcWss = new WebSocketServer({ server: ipcServer, path: '/ws' });
1387
+ bindProtocol(ipcWss, (url) => {
1388
+ const candidate = url.searchParams.get('token') ?? undefined;
1389
+ return candidate === undefined ? { kind: 'owner' } : principalForToken(candidate);
1390
+ });
1391
+ try {
1392
+ await listenUnix(ipcServer, options.socketPath);
1393
+ }
1394
+ catch (error) {
1395
+ unsubscribeFrames();
1396
+ stopDeviceRevocations?.();
1397
+ ipcWss.close();
1398
+ wss.close();
1399
+ await app.close();
1400
+ throw error;
1401
+ }
1402
+ }
1403
+ // harn:end unix-socket-same-protocol
1404
+ return {
1405
+ app,
1406
+ port,
1407
+ socketPath: options.socketPath,
1408
+ observedBrowserProtocols: () => [...observedBrowserProtocols].sort((left, right) => left - right),
1409
+ close: async () => {
1410
+ unsubscribeFrames();
1411
+ stopDeviceRevocations?.();
1412
+ for (const socket of subscriptions.keys())
1413
+ socket.terminate();
1414
+ wss.close();
1415
+ ipcWss?.close();
1416
+ if (ipcServer?.listening) {
1417
+ await new Promise((resolve, reject) => ipcServer.close((error) => (error ? reject(error) : resolve())));
1418
+ }
1419
+ await app.close();
1420
+ if (options.socketPath !== undefined)
1421
+ rmSync(options.socketPath, { force: true });
1422
+ },
1423
+ };
1424
+ }
1425
+ //# sourceMappingURL=server.js.map