@serfab/cadre-host 0.7.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (300) hide show
  1. package/README.md +101 -0
  2. package/dist/auth/duration.d.ts +1 -0
  3. package/dist/auth/duration.js +32 -0
  4. package/dist/auth/duration.js.map +1 -0
  5. package/dist/auth/index.d.ts +9 -0
  6. package/dist/auth/index.js +8 -0
  7. package/dist/auth/index.js.map +1 -0
  8. package/dist/auth/trust-circle-store.d.ts +26 -0
  9. package/dist/auth/trust-circle-store.js +129 -0
  10. package/dist/auth/trust-circle-store.js.map +1 -0
  11. package/dist/auth/trust-circle.d.ts +91 -0
  12. package/dist/auth/trust-circle.js +230 -0
  13. package/dist/auth/trust-circle.js.map +1 -0
  14. package/dist/auth/types.d.ts +81 -0
  15. package/dist/auth/types.js +24 -0
  16. package/dist/auth/types.js.map +1 -0
  17. package/dist/bin/host.d.ts +13 -0
  18. package/dist/bin/host.js +826 -0
  19. package/dist/bin/host.js.map +1 -0
  20. package/dist/index.d.ts +32 -0
  21. package/dist/index.js +27 -0
  22. package/dist/index.js.map +1 -0
  23. package/dist/installer/browser.d.ts +14 -0
  24. package/dist/installer/browser.js +35 -0
  25. package/dist/installer/browser.js.map +1 -0
  26. package/dist/installer/config.d.ts +43 -0
  27. package/dist/installer/config.js +113 -0
  28. package/dist/installer/config.js.map +1 -0
  29. package/dist/installer/identity.d.ts +23 -0
  30. package/dist/installer/identity.js +36 -0
  31. package/dist/installer/identity.js.map +1 -0
  32. package/dist/installer/index.d.ts +83 -0
  33. package/dist/installer/index.js +204 -0
  34. package/dist/installer/index.js.map +1 -0
  35. package/dist/installer/paths.d.ts +33 -0
  36. package/dist/installer/paths.js +84 -0
  37. package/dist/installer/paths.js.map +1 -0
  38. package/dist/installer/platform.d.ts +9 -0
  39. package/dist/installer/platform.js +21 -0
  40. package/dist/installer/platform.js.map +1 -0
  41. package/dist/installer/service-host/index.d.ts +11 -0
  42. package/dist/installer/service-host/index.js +16 -0
  43. package/dist/installer/service-host/index.js.map +1 -0
  44. package/dist/installer/service-host/launchd.d.ts +16 -0
  45. package/dist/installer/service-host/launchd.js +79 -0
  46. package/dist/installer/service-host/launchd.js.map +1 -0
  47. package/dist/installer/service-host/nssm.d.ts +20 -0
  48. package/dist/installer/service-host/nssm.js +88 -0
  49. package/dist/installer/service-host/nssm.js.map +1 -0
  50. package/dist/installer/service-host/systemd.d.ts +16 -0
  51. package/dist/installer/service-host/systemd.js +78 -0
  52. package/dist/installer/service-host/systemd.js.map +1 -0
  53. package/dist/installer/service-host/template.d.ts +6 -0
  54. package/dist/installer/service-host/template.js +20 -0
  55. package/dist/installer/service-host/template.js.map +1 -0
  56. package/dist/installer/service-host/types.d.ts +40 -0
  57. package/dist/installer/service-host/types.js +6 -0
  58. package/dist/installer/service-host/types.js.map +1 -0
  59. package/dist/installer/wizard.d.ts +31 -0
  60. package/dist/installer/wizard.js +67 -0
  61. package/dist/installer/wizard.js.map +1 -0
  62. package/dist/nat/address-resolver.d.ts +22 -0
  63. package/dist/nat/address-resolver.js +31 -0
  64. package/dist/nat/address-resolver.js.map +1 -0
  65. package/dist/nat/ddns/duckdns.d.ts +12 -0
  66. package/dist/nat/ddns/duckdns.js +57 -0
  67. package/dist/nat/ddns/duckdns.js.map +1 -0
  68. package/dist/nat/ddns/index.d.ts +30 -0
  69. package/dist/nat/ddns/index.js +21 -0
  70. package/dist/nat/ddns/index.js.map +1 -0
  71. package/dist/nat/ddns/updater.d.ts +68 -0
  72. package/dist/nat/ddns/updater.js +194 -0
  73. package/dist/nat/ddns/updater.js.map +1 -0
  74. package/dist/nat/external-ip.d.ts +62 -0
  75. package/dist/nat/external-ip.js +114 -0
  76. package/dist/nat/external-ip.js.map +1 -0
  77. package/dist/nat/index.d.ts +22 -0
  78. package/dist/nat/index.js +16 -0
  79. package/dist/nat/index.js.map +1 -0
  80. package/dist/nat/nat-service.d.ts +101 -0
  81. package/dist/nat/nat-service.js +284 -0
  82. package/dist/nat/nat-service.js.map +1 -0
  83. package/dist/nat/nat-store.d.ts +19 -0
  84. package/dist/nat/nat-store.js +136 -0
  85. package/dist/nat/nat-store.js.map +1 -0
  86. package/dist/nat/port-mapper.d.ts +98 -0
  87. package/dist/nat/port-mapper.js +226 -0
  88. package/dist/nat/port-mapper.js.map +1 -0
  89. package/dist/nat/reachability.d.ts +19 -0
  90. package/dist/nat/reachability.js +29 -0
  91. package/dist/nat/reachability.js.map +1 -0
  92. package/dist/nat/secrets/file-store.d.ts +23 -0
  93. package/dist/nat/secrets/file-store.js +102 -0
  94. package/dist/nat/secrets/file-store.js.map +1 -0
  95. package/dist/nat/secrets/index.d.ts +36 -0
  96. package/dist/nat/secrets/index.js +56 -0
  97. package/dist/nat/secrets/index.js.map +1 -0
  98. package/dist/nat/secrets/keytar-store.d.ts +18 -0
  99. package/dist/nat/secrets/keytar-store.js +52 -0
  100. package/dist/nat/secrets/keytar-store.js.map +1 -0
  101. package/dist/nat/types.d.ts +120 -0
  102. package/dist/nat/types.js +24 -0
  103. package/dist/nat/types.js.map +1 -0
  104. package/dist/orchestrator/host-process-orchestrator.d.ts +87 -0
  105. package/dist/orchestrator/host-process-orchestrator.js +557 -0
  106. package/dist/orchestrator/host-process-orchestrator.js.map +1 -0
  107. package/dist/orchestrator/index.d.ts +3 -0
  108. package/dist/orchestrator/index.js +2 -0
  109. package/dist/orchestrator/index.js.map +1 -0
  110. package/dist/orchestrator/log-rotator.d.ts +25 -0
  111. package/dist/orchestrator/log-rotator.js +56 -0
  112. package/dist/orchestrator/log-rotator.js.map +1 -0
  113. package/dist/orchestrator/pid-liveness.d.ts +11 -0
  114. package/dist/orchestrator/pid-liveness.js +26 -0
  115. package/dist/orchestrator/pid-liveness.js.map +1 -0
  116. package/dist/orchestrator/port-allocator.d.ts +16 -0
  117. package/dist/orchestrator/port-allocator.js +38 -0
  118. package/dist/orchestrator/port-allocator.js.map +1 -0
  119. package/dist/orchestrator/state-store.d.ts +32 -0
  120. package/dist/orchestrator/state-store.js +44 -0
  121. package/dist/orchestrator/state-store.js.map +1 -0
  122. package/dist/orchestrator/types.d.ts +85 -0
  123. package/dist/orchestrator/types.js +31 -0
  124. package/dist/orchestrator/types.js.map +1 -0
  125. package/dist/server/error-handler.d.ts +26 -0
  126. package/dist/server/error-handler.js +95 -0
  127. package/dist/server/error-handler.js.map +1 -0
  128. package/dist/server/events/bus.d.ts +17 -0
  129. package/dist/server/events/bus.js +36 -0
  130. package/dist/server/events/bus.js.map +1 -0
  131. package/dist/server/events/sse-route.d.ts +22 -0
  132. package/dist/server/events/sse-route.js +65 -0
  133. package/dist/server/events/sse-route.js.map +1 -0
  134. package/dist/server/events/types.d.ts +25 -0
  135. package/dist/server/events/types.js +8 -0
  136. package/dist/server/events/types.js.map +1 -0
  137. package/dist/server/index.d.ts +67 -0
  138. package/dist/server/index.js +149 -0
  139. package/dist/server/index.js.map +1 -0
  140. package/dist/server/origin-guard.d.ts +24 -0
  141. package/dist/server/origin-guard.js +72 -0
  142. package/dist/server/origin-guard.js.map +1 -0
  143. package/dist/server/routes/nat.d.ts +16 -0
  144. package/dist/server/routes/nat.js +42 -0
  145. package/dist/server/routes/nat.js.map +1 -0
  146. package/dist/server/routes/nodes.d.ts +23 -0
  147. package/dist/server/routes/nodes.js +128 -0
  148. package/dist/server/routes/nodes.js.map +1 -0
  149. package/dist/server/routes/settings.d.ts +26 -0
  150. package/dist/server/routes/settings.js +97 -0
  151. package/dist/server/routes/settings.js.map +1 -0
  152. package/dist/server/routes/status.d.ts +42 -0
  153. package/dist/server/routes/status.js +76 -0
  154. package/dist/server/routes/status.js.map +1 -0
  155. package/dist/server/routes/trust-circle.d.ts +17 -0
  156. package/dist/server/routes/trust-circle.js +35 -0
  157. package/dist/server/routes/trust-circle.js.map +1 -0
  158. package/dist/server/routes/update.d.ts +16 -0
  159. package/dist/server/routes/update.js +29 -0
  160. package/dist/server/routes/update.js.map +1 -0
  161. package/dist/server/server.d.ts +40 -0
  162. package/dist/server/server.js +90 -0
  163. package/dist/server/server.js.map +1 -0
  164. package/dist/server/settings-store.d.ts +24 -0
  165. package/dist/server/settings-store.js +27 -0
  166. package/dist/server/settings-store.js.map +1 -0
  167. package/dist/server/static.d.ts +25 -0
  168. package/dist/server/static.js +177 -0
  169. package/dist/server/static.js.map +1 -0
  170. package/dist/ui/assets/index-BwU5mJGF.css +1 -0
  171. package/dist/ui/assets/index-C9uChk2P.js +14 -0
  172. package/dist/ui/assets/index-C9uChk2P.js.map +1 -0
  173. package/dist/ui/index.html +14 -0
  174. package/dist/update/apply.d.ts +37 -0
  175. package/dist/update/apply.js +102 -0
  176. package/dist/update/apply.js.map +1 -0
  177. package/dist/update/index.d.ts +101 -0
  178. package/dist/update/index.js +228 -0
  179. package/dist/update/index.js.map +1 -0
  180. package/dist/update/manifest.d.ts +48 -0
  181. package/dist/update/manifest.js +152 -0
  182. package/dist/update/manifest.js.map +1 -0
  183. package/dist/update/release-key.d.ts +24 -0
  184. package/dist/update/release-key.js +52 -0
  185. package/dist/update/release-key.js.map +1 -0
  186. package/dist/update/store.d.ts +22 -0
  187. package/dist/update/store.js +81 -0
  188. package/dist/update/store.js.map +1 -0
  189. package/dist/update/types.d.ts +90 -0
  190. package/dist/update/types.js +22 -0
  191. package/dist/update/types.js.map +1 -0
  192. package/dist/update/version.d.ts +18 -0
  193. package/dist/update/version.js +76 -0
  194. package/dist/update/version.js.map +1 -0
  195. package/package.json +80 -0
  196. package/service/README.md +86 -0
  197. package/service/cadre-host.service.tmpl +16 -0
  198. package/service/com.serfab.cadre-host.plist.tmpl +35 -0
  199. package/service/install-service.ps1 +98 -0
  200. package/service/uninstall-service.ps1 +19 -0
  201. package/src/__tests__/cli-invite.smoke.test.ts +187 -0
  202. package/src/__tests__/cli-nat.smoke.test.ts +207 -0
  203. package/src/__tests__/cli.smoke.test.ts +41 -0
  204. package/src/__tests__/orchestrator.test.ts +570 -0
  205. package/src/auth/__tests__/duration.test.ts +28 -0
  206. package/src/auth/__tests__/trust-circle-integration.test.ts +128 -0
  207. package/src/auth/__tests__/trust-circle-store.test.ts +115 -0
  208. package/src/auth/__tests__/trust-circle.test.ts +334 -0
  209. package/src/auth/duration.ts +33 -0
  210. package/src/auth/index.ts +19 -0
  211. package/src/auth/trust-circle-store.ts +155 -0
  212. package/src/auth/trust-circle.ts +304 -0
  213. package/src/auth/types.ts +96 -0
  214. package/src/bin/host.ts +905 -0
  215. package/src/index.ts +167 -0
  216. package/src/installer/__tests__/config.test.ts +86 -0
  217. package/src/installer/__tests__/identity.test.ts +34 -0
  218. package/src/installer/__tests__/installer.smoke.test.ts +124 -0
  219. package/src/installer/__tests__/paths.test.ts +47 -0
  220. package/src/installer/__tests__/service-host.test.ts +62 -0
  221. package/src/installer/__tests__/wizard.test.ts +60 -0
  222. package/src/installer/browser.ts +42 -0
  223. package/src/installer/config.ts +153 -0
  224. package/src/installer/identity.ts +49 -0
  225. package/src/installer/index.ts +303 -0
  226. package/src/installer/paths.ts +109 -0
  227. package/src/installer/platform.ts +27 -0
  228. package/src/installer/service-host/index.ts +21 -0
  229. package/src/installer/service-host/launchd.ts +90 -0
  230. package/src/installer/service-host/nssm.ts +100 -0
  231. package/src/installer/service-host/systemd.ts +88 -0
  232. package/src/installer/service-host/template.ts +21 -0
  233. package/src/installer/service-host/types.ts +48 -0
  234. package/src/installer/wizard.ts +92 -0
  235. package/src/nat/__tests__/address-resolver.test.ts +82 -0
  236. package/src/nat/__tests__/ddns-duckdns.test.ts +83 -0
  237. package/src/nat/__tests__/ddns-updater.test.ts +253 -0
  238. package/src/nat/__tests__/external-ip.test.ts +124 -0
  239. package/src/nat/__tests__/nat-service.test.ts +305 -0
  240. package/src/nat/__tests__/nat-store.test.ts +93 -0
  241. package/src/nat/__tests__/port-mapper.test.ts +191 -0
  242. package/src/nat/__tests__/reachability.test.ts +24 -0
  243. package/src/nat/__tests__/secrets.test.ts +156 -0
  244. package/src/nat/address-resolver.ts +51 -0
  245. package/src/nat/ddns/duckdns.ts +76 -0
  246. package/src/nat/ddns/index.ts +55 -0
  247. package/src/nat/ddns/updater.ts +243 -0
  248. package/src/nat/external-ip.ts +154 -0
  249. package/src/nat/index.ts +71 -0
  250. package/src/nat/nat-service.ts +375 -0
  251. package/src/nat/nat-store.ts +154 -0
  252. package/src/nat/port-mapper.ts +329 -0
  253. package/src/nat/reachability.ts +34 -0
  254. package/src/nat/secrets/file-store.ts +130 -0
  255. package/src/nat/secrets/index.ts +82 -0
  256. package/src/nat/secrets/keytar-store.ts +67 -0
  257. package/src/nat/types.ts +157 -0
  258. package/src/orchestrator/host-process-orchestrator.ts +626 -0
  259. package/src/orchestrator/index.ts +3 -0
  260. package/src/orchestrator/log-rotator.ts +53 -0
  261. package/src/orchestrator/pid-liveness.ts +24 -0
  262. package/src/orchestrator/port-allocator.ts +43 -0
  263. package/src/orchestrator/state-store.ts +72 -0
  264. package/src/orchestrator/types.ts +95 -0
  265. package/src/server/__tests__/bus.test.ts +38 -0
  266. package/src/server/__tests__/error-handler.test.ts +96 -0
  267. package/src/server/__tests__/nodes-route.test.ts +179 -0
  268. package/src/server/__tests__/origin-guard.test.ts +72 -0
  269. package/src/server/__tests__/publishers.test.ts +216 -0
  270. package/src/server/__tests__/server.smoke.test.ts +178 -0
  271. package/src/server/__tests__/settings-route.test.ts +177 -0
  272. package/src/server/__tests__/sse-route.test.ts +119 -0
  273. package/src/server/__tests__/status-route.test.ts +113 -0
  274. package/src/server/error-handler.ts +108 -0
  275. package/src/server/events/bus.ts +42 -0
  276. package/src/server/events/sse-route.ts +81 -0
  277. package/src/server/events/types.ts +22 -0
  278. package/src/server/index.ts +209 -0
  279. package/src/server/origin-guard.ts +84 -0
  280. package/src/server/routes/nat.ts +63 -0
  281. package/src/server/routes/nodes.ts +147 -0
  282. package/src/server/routes/settings.ts +115 -0
  283. package/src/server/routes/status.ts +108 -0
  284. package/src/server/routes/trust-circle.ts +49 -0
  285. package/src/server/routes/update.ts +43 -0
  286. package/src/server/server.ts +122 -0
  287. package/src/server/settings-store.ts +37 -0
  288. package/src/server/static.ts +191 -0
  289. package/src/update/__tests__/apply.test.ts +101 -0
  290. package/src/update/__tests__/manifest.test.ts +93 -0
  291. package/src/update/__tests__/store.test.ts +81 -0
  292. package/src/update/__tests__/update-service.integration.test.ts +221 -0
  293. package/src/update/__tests__/version.test.ts +31 -0
  294. package/src/update/apply.ts +148 -0
  295. package/src/update/index.ts +303 -0
  296. package/src/update/manifest.ts +172 -0
  297. package/src/update/release-key.ts +55 -0
  298. package/src/update/store.ts +101 -0
  299. package/src/update/types.ts +109 -0
  300. package/src/update/version.ts +80 -0
@@ -0,0 +1,56 @@
1
+ import { existsSync, renameSync, unlinkSync, } from 'node:fs';
2
+ import { join } from 'node:path';
3
+ /**
4
+ * On-disk log rotation helper.
5
+ *
6
+ * Children spawned by the orchestrator inherit a direct file descriptor to
7
+ * their `node.log` (see `host-process-orchestrator.ts`), so rotation can no
8
+ * longer be driven by an in-process byte counter — renaming the path under a
9
+ * running child would leave it writing to the renamed inode. Instead the
10
+ * orchestrator calls `rotateOnDisk(...)` at spawn time, before the child is
11
+ * launched and its fd is opened. This is the only safe point to rotate
12
+ * without coordinating with the running process.
13
+ *
14
+ * Within a workdir there is at most one live child, so spawn-time rotation
15
+ * is sufficient for v1: a child that exceeds `logMaxBytes` mid-run will
16
+ * keep appending to the active file until it (or its successor) is restarted.
17
+ */
18
+ /**
19
+ * Perform a rotation cascade against on-disk paths: drop the oldest, then
20
+ * shift `.N-1 → .N`, …, `.1 → .2`, and finally `filePath → .1`.
21
+ *
22
+ * Caller is responsible for deciding when to rotate (e.g. when
23
+ * `statSync(filePath).size >= logMaxBytes`). No fd is held by this helper.
24
+ */
25
+ export function rotateOnDisk(filePath, maxFiles) {
26
+ if (maxFiles < 1)
27
+ return;
28
+ const oldest = `${filePath}.${maxFiles}`;
29
+ if (existsSync(oldest)) {
30
+ try {
31
+ unlinkSync(oldest);
32
+ }
33
+ catch { /* ignore */ }
34
+ }
35
+ for (let i = maxFiles - 1; i >= 1; i--) {
36
+ const from = `${filePath}.${i}`;
37
+ const to = `${filePath}.${i + 1}`;
38
+ if (existsSync(from)) {
39
+ try {
40
+ renameSync(from, to);
41
+ }
42
+ catch { /* ignore */ }
43
+ }
44
+ }
45
+ if (existsSync(filePath)) {
46
+ try {
47
+ renameSync(filePath, `${filePath}.1`);
48
+ }
49
+ catch { /* ignore */ }
50
+ }
51
+ }
52
+ /** Build the standard log file path for a workdir. */
53
+ export function defaultLogPath(workdir) {
54
+ return join(workdir, 'node.log');
55
+ }
56
+ //# sourceMappingURL=log-rotator.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"log-rotator.js","sourceRoot":"","sources":["../../src/orchestrator/log-rotator.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,UAAU,EACV,UAAU,EACV,UAAU,GACX,MAAM,SAAS,CAAC;AACjB,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AAEjC;;;;;;;;;;;;;;GAcG;AAEH;;;;;;GAMG;AACH,MAAM,UAAU,YAAY,CAAC,QAAgB,EAAE,QAAgB;IAC7D,IAAI,QAAQ,GAAG,CAAC;QAAE,OAAO;IAEzB,MAAM,MAAM,GAAG,GAAG,QAAQ,IAAI,QAAQ,EAAE,CAAC;IACzC,IAAI,UAAU,CAAC,MAAM,CAAC,EAAE,CAAC;QACvB,IAAI,CAAC;YAAC,UAAU,CAAC,MAAM,CAAC,CAAC;QAAC,CAAC;QAAC,MAAM,CAAC,CAAC,YAAY,CAAC,CAAC;IACpD,CAAC;IACD,KAAK,IAAI,CAAC,GAAG,QAAQ,GAAG,CAAC,EAAE,CAAC,IAAI,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC;QACvC,MAAM,IAAI,GAAG,GAAG,QAAQ,IAAI,CAAC,EAAE,CAAC;QAChC,MAAM,EAAE,GAAG,GAAG,QAAQ,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC;QAClC,IAAI,UAAU,CAAC,IAAI,CAAC,EAAE,CAAC;YACrB,IAAI,CAAC;gBAAC,UAAU,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC;YAAC,CAAC;YAAC,MAAM,CAAC,CAAC,YAAY,CAAC,CAAC;QACtD,CAAC;IACH,CAAC;IACD,IAAI,UAAU,CAAC,QAAQ,CAAC,EAAE,CAAC;QACzB,IAAI,CAAC;YAAC,UAAU,CAAC,QAAQ,EAAE,GAAG,QAAQ,IAAI,CAAC,CAAC;QAAC,CAAC;QAAC,MAAM,CAAC,CAAC,YAAY,CAAC,CAAC;IACvE,CAAC;AACH,CAAC;AAED,sDAAsD;AACtD,MAAM,UAAU,cAAc,CAAC,OAAe;IAC5C,OAAO,IAAI,CAAC,OAAO,EAAE,UAAU,CAAC,CAAC;AACnC,CAAC"}
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Cross-platform PID liveness check using only stdlib.
3
+ *
4
+ * `process.kill(pid, 0)` sends signal 0, which performs error checking
5
+ * without actually delivering a signal:
6
+ * - returns successfully when the process exists and we have permission
7
+ * - throws EPERM when the process exists but is owned by another user
8
+ * (common on Windows where signals don't translate cleanly)
9
+ * - throws ESRCH when the process does not exist
10
+ */
11
+ export declare function isPidAlive(pid: number): boolean;
@@ -0,0 +1,26 @@
1
+ /**
2
+ * Cross-platform PID liveness check using only stdlib.
3
+ *
4
+ * `process.kill(pid, 0)` sends signal 0, which performs error checking
5
+ * without actually delivering a signal:
6
+ * - returns successfully when the process exists and we have permission
7
+ * - throws EPERM when the process exists but is owned by another user
8
+ * (common on Windows where signals don't translate cleanly)
9
+ * - throws ESRCH when the process does not exist
10
+ */
11
+ export function isPidAlive(pid) {
12
+ if (!Number.isInteger(pid) || pid <= 0) {
13
+ return false;
14
+ }
15
+ try {
16
+ process.kill(pid, 0);
17
+ return true;
18
+ }
19
+ catch (err) {
20
+ const code = err.code;
21
+ // EPERM means the process exists but we lack permission to signal it.
22
+ // That's still "alive" from a liveness perspective.
23
+ return code === 'EPERM';
24
+ }
25
+ }
26
+ //# sourceMappingURL=pid-liveness.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"pid-liveness.js","sourceRoot":"","sources":["../../src/orchestrator/pid-liveness.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AACH,MAAM,UAAU,UAAU,CAAC,GAAW;IACpC,IAAI,CAAC,MAAM,CAAC,SAAS,CAAC,GAAG,CAAC,IAAI,GAAG,IAAI,CAAC,EAAE,CAAC;QACvC,OAAO,KAAK,CAAC;IACf,CAAC;IACD,IAAI,CAAC;QACH,OAAO,CAAC,IAAI,CAAC,GAAG,EAAE,CAAC,CAAC,CAAC;QACrB,OAAO,IAAI,CAAC;IACd,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,MAAM,IAAI,GAAI,GAA6B,CAAC,IAAI,CAAC;QACjD,sEAAsE;QACtE,oDAAoD;QACpD,OAAO,IAAI,KAAK,OAAO,CAAC;IAC1B,CAAC;AACH,CAAC"}
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Range-based port tracker. Allocations are in-memory; on orchestrator
3
+ * restart the caller rehydrates the used-set from persisted state via
4
+ * `markUsed(port)` before serving new allocations.
5
+ */
6
+ export declare class PortAllocator {
7
+ private readonly start;
8
+ private readonly end;
9
+ private readonly usedPorts;
10
+ constructor(start: number, end: number);
11
+ allocate(): number;
12
+ release(port: number): void;
13
+ /** Used during restart rehydration to reserve ports already in use. */
14
+ markUsed(port: number): void;
15
+ has(port: number): boolean;
16
+ }
@@ -0,0 +1,38 @@
1
+ /**
2
+ * Range-based port tracker. Allocations are in-memory; on orchestrator
3
+ * restart the caller rehydrates the used-set from persisted state via
4
+ * `markUsed(port)` before serving new allocations.
5
+ */
6
+ export class PortAllocator {
7
+ constructor(start, end) {
8
+ this.start = start;
9
+ this.end = end;
10
+ this.usedPorts = new Set();
11
+ if (!Number.isInteger(start) || !Number.isInteger(end) || start <= 0 || end < start) {
12
+ throw new Error(`Invalid port range: ${start}..${end}`);
13
+ }
14
+ }
15
+ allocate() {
16
+ for (let port = this.start; port <= this.end; port++) {
17
+ if (!this.usedPorts.has(port)) {
18
+ this.usedPorts.add(port);
19
+ return port;
20
+ }
21
+ }
22
+ throw new Error(`No available ports in range ${this.start}..${this.end}`);
23
+ }
24
+ release(port) {
25
+ this.usedPorts.delete(port);
26
+ }
27
+ /** Used during restart rehydration to reserve ports already in use. */
28
+ markUsed(port) {
29
+ if (port < this.start || port > this.end) {
30
+ return;
31
+ }
32
+ this.usedPorts.add(port);
33
+ }
34
+ has(port) {
35
+ return this.usedPorts.has(port);
36
+ }
37
+ }
38
+ //# sourceMappingURL=port-allocator.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"port-allocator.js","sourceRoot":"","sources":["../../src/orchestrator/port-allocator.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AACH,MAAM,OAAO,aAAa;IAGxB,YACmB,KAAa,EACb,GAAW;QADX,UAAK,GAAL,KAAK,CAAQ;QACb,QAAG,GAAH,GAAG,CAAQ;QAJb,cAAS,GAAG,IAAI,GAAG,EAAU,CAAC;QAM7C,IAAI,CAAC,MAAM,CAAC,SAAS,CAAC,KAAK,CAAC,IAAI,CAAC,MAAM,CAAC,SAAS,CAAC,GAAG,CAAC,IAAI,KAAK,IAAI,CAAC,IAAI,GAAG,GAAG,KAAK,EAAE,CAAC;YACpF,MAAM,IAAI,KAAK,CAAC,uBAAuB,KAAK,KAAK,GAAG,EAAE,CAAC,CAAC;QAC1D,CAAC;IACH,CAAC;IAED,QAAQ;QACN,KAAK,IAAI,IAAI,GAAG,IAAI,CAAC,KAAK,EAAE,IAAI,IAAI,IAAI,CAAC,GAAG,EAAE,IAAI,EAAE,EAAE,CAAC;YACrD,IAAI,CAAC,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,IAAI,CAAC,EAAE,CAAC;gBAC9B,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;gBACzB,OAAO,IAAI,CAAC;YACd,CAAC;QACH,CAAC;QACD,MAAM,IAAI,KAAK,CAAC,+BAA+B,IAAI,CAAC,KAAK,KAAK,IAAI,CAAC,GAAG,EAAE,CAAC,CAAC;IAC5E,CAAC;IAED,OAAO,CAAC,IAAY;QAClB,IAAI,CAAC,SAAS,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC;IAC9B,CAAC;IAED,uEAAuE;IACvE,QAAQ,CAAC,IAAY;QACnB,IAAI,IAAI,GAAG,IAAI,CAAC,KAAK,IAAI,IAAI,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC;YACzC,OAAO;QACT,CAAC;QACD,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;IAC3B,CAAC;IAED,GAAG,CAAC,IAAY;QACd,OAAO,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;IAClC,CAAC;CACF"}
@@ -0,0 +1,32 @@
1
+ /**
2
+ * Persisted record for a single managed child. The orchestrator mirrors its
3
+ * in-memory map to disk so that cadre-host can re-attach to surviving
4
+ * children after a restart.
5
+ */
6
+ export interface PersistedHandle {
7
+ containerId: string;
8
+ dockerId: string;
9
+ pid: number;
10
+ startupToken: string;
11
+ workdir: string;
12
+ ports: {
13
+ health: number;
14
+ metrics: number;
15
+ p2p: number;
16
+ };
17
+ spawnedAt: string;
18
+ partyId: string;
19
+ profile: 'storage' | 'transaction';
20
+ }
21
+ export interface PersistedState {
22
+ version: 1;
23
+ handles: PersistedHandle[];
24
+ }
25
+ /** JSON state file at `<rootDir>/state.json`, written atomically. */
26
+ export declare class StateStore {
27
+ private readonly path;
28
+ constructor(rootDir: string);
29
+ load(): PersistedState;
30
+ save(state: PersistedState): void;
31
+ filePath(): string;
32
+ }
@@ -0,0 +1,44 @@
1
+ import { existsSync, readFileSync, renameSync, writeFileSync, mkdirSync } from 'node:fs';
2
+ import { dirname, join } from 'node:path';
3
+ import debug from 'debug';
4
+ const log = debug('cadre:host:state-store');
5
+ const STATE_VERSION = 1;
6
+ /** JSON state file at `<rootDir>/state.json`, written atomically. */
7
+ export class StateStore {
8
+ constructor(rootDir) {
9
+ mkdirSync(rootDir, { recursive: true });
10
+ this.path = join(rootDir, 'state.json');
11
+ }
12
+ load() {
13
+ if (!existsSync(this.path)) {
14
+ return { version: STATE_VERSION, handles: [] };
15
+ }
16
+ try {
17
+ const raw = readFileSync(this.path, 'utf8');
18
+ const parsed = JSON.parse(raw);
19
+ if (!parsed || typeof parsed !== 'object' || !Array.isArray(parsed.handles)) {
20
+ log('state file at %s has unexpected shape; treating as empty', this.path);
21
+ return { version: STATE_VERSION, handles: [] };
22
+ }
23
+ return { version: STATE_VERSION, handles: parsed.handles };
24
+ }
25
+ catch (err) {
26
+ // A corrupt state file is operationally significant — surviving children
27
+ // may now be leaked to the OS without an in-memory handle. Make the
28
+ // failure visible rather than silently returning empty.
29
+ log('failed to load state file at %s: %o', this.path, err);
30
+ return { version: STATE_VERSION, handles: [] };
31
+ }
32
+ }
33
+ save(state) {
34
+ mkdirSync(dirname(this.path), { recursive: true });
35
+ const payload = JSON.stringify({ version: STATE_VERSION, handles: state.handles }, null, 2);
36
+ const tmp = `${this.path}.tmp`;
37
+ writeFileSync(tmp, payload, { encoding: 'utf8' });
38
+ renameSync(tmp, this.path);
39
+ }
40
+ filePath() {
41
+ return this.path;
42
+ }
43
+ }
44
+ //# sourceMappingURL=state-store.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"state-store.js","sourceRoot":"","sources":["../../src/orchestrator/state-store.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,YAAY,EAAE,UAAU,EAAE,aAAa,EAAE,SAAS,EAAE,MAAM,SAAS,CAAC;AACzF,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AAC1C,OAAO,KAAK,MAAM,OAAO,CAAC;AAE1B,MAAM,GAAG,GAAG,KAAK,CAAC,wBAAwB,CAAC,CAAC;AAwB5C,MAAM,aAAa,GAAG,CAAC,CAAC;AAExB,qEAAqE;AACrE,MAAM,OAAO,UAAU;IAGrB,YAAY,OAAe;QACzB,SAAS,CAAC,OAAO,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;QACxC,IAAI,CAAC,IAAI,GAAG,IAAI,CAAC,OAAO,EAAE,YAAY,CAAC,CAAC;IAC1C,CAAC;IAED,IAAI;QACF,IAAI,CAAC,UAAU,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC;YAC3B,OAAO,EAAE,OAAO,EAAE,aAAa,EAAE,OAAO,EAAE,EAAE,EAAE,CAAC;QACjD,CAAC;QACD,IAAI,CAAC;YACH,MAAM,GAAG,GAAG,YAAY,CAAC,IAAI,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC;YAC5C,MAAM,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,CAAmB,CAAC;YACjD,IAAI,CAAC,MAAM,IAAI,OAAO,MAAM,KAAK,QAAQ,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,MAAM,CAAC,OAAO,CAAC,EAAE,CAAC;gBAC5E,GAAG,CAAC,0DAA0D,EAAE,IAAI,CAAC,IAAI,CAAC,CAAC;gBAC3E,OAAO,EAAE,OAAO,EAAE,aAAa,EAAE,OAAO,EAAE,EAAE,EAAE,CAAC;YACjD,CAAC;YACD,OAAO,EAAE,OAAO,EAAE,aAAa,EAAE,OAAO,EAAE,MAAM,CAAC,OAAO,EAAE,CAAC;QAC7D,CAAC;QAAC,OAAO,GAAG,EAAE,CAAC;YACb,yEAAyE;YACzE,oEAAoE;YACpE,wDAAwD;YACxD,GAAG,CAAC,qCAAqC,EAAE,IAAI,CAAC,IAAI,EAAE,GAAG,CAAC,CAAC;YAC3D,OAAO,EAAE,OAAO,EAAE,aAAa,EAAE,OAAO,EAAE,EAAE,EAAE,CAAC;QACjD,CAAC;IACH,CAAC;IAED,IAAI,CAAC,KAAqB;QACxB,SAAS,CAAC,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;QACnD,MAAM,OAAO,GAAG,IAAI,CAAC,SAAS,CAAC,EAAE,OAAO,EAAE,aAAa,EAAE,OAAO,EAAE,KAAK,CAAC,OAAO,EAAE,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC;QAC5F,MAAM,GAAG,GAAG,GAAG,IAAI,CAAC,IAAI,MAAM,CAAC;QAC/B,aAAa,CAAC,GAAG,EAAE,OAAO,EAAE,EAAE,QAAQ,EAAE,MAAM,EAAE,CAAC,CAAC;QAClD,UAAU,CAAC,GAAG,EAAE,IAAI,CAAC,IAAI,CAAC,CAAC;IAC7B,CAAC;IAED,QAAQ;QACN,OAAO,IAAI,CAAC,IAAI,CAAC;IACnB,CAAC;CACF"}
@@ -0,0 +1,85 @@
1
+ /**
2
+ * Public + internal types for the HostProcessOrchestrator.
3
+ */
4
+ import type { ChildProcess } from 'node:child_process';
5
+ /** User-facing configuration for `HostProcessOrchestrator`. */
6
+ export interface HostProcessConfig {
7
+ /** Root directory under which per-container workdirs live. */
8
+ rootDir: string;
9
+ /** Port allocation range (default 10000..20000). */
10
+ portRange?: {
11
+ start: number;
12
+ end: number;
13
+ };
14
+ /** Default resource limits (memory only in v1). */
15
+ defaultResources?: {
16
+ memoryLimit?: string;
17
+ };
18
+ /** SIGTERM → SIGKILL grace period (default 10000 ms). */
19
+ stopTimeoutMs?: number;
20
+ /** Log rotation: bytes per file (default 10 MB). */
21
+ logMaxBytes?: number;
22
+ /** Log rotation: max rotated files (default 5). */
23
+ logMaxFiles?: number;
24
+ /**
25
+ * Test-only hook: override the child entrypoint. When set, the orchestrator
26
+ * spawns `node <entrypoint> ...` instead of resolving `@serfab/cadre-cli`.
27
+ */
28
+ spawn?: {
29
+ entrypoint?: string;
30
+ };
31
+ }
32
+ /** In-memory record per managed child. */
33
+ export interface Handle {
34
+ containerId: string;
35
+ dockerId: string;
36
+ pid: number;
37
+ startupToken: string;
38
+ workdir: string;
39
+ ports: {
40
+ health: number;
41
+ metrics: number;
42
+ p2p: number;
43
+ };
44
+ spawnedAt: string;
45
+ partyId: string;
46
+ profile: 'storage' | 'transaction';
47
+ /** Live ChildProcess reference; absent after re-attach via init(). */
48
+ child?: ChildProcess;
49
+ /** Marked false when init() finds the PID dead or token mismatched. */
50
+ alive: boolean;
51
+ }
52
+ /** Public view of a managed node — surfaced to `/api/nodes` callers. */
53
+ export interface ManagedNodeInfo {
54
+ /** Friendly container id passed at create time. */
55
+ id: string;
56
+ /** Opaque dockerId (`pid:token`) — pass to stop/getStats/getLogs. */
57
+ dockerId: string;
58
+ partyId: string;
59
+ profile: 'storage' | 'transaction';
60
+ /** Runtime state derived from `Handle.alive`. */
61
+ status: 'running' | 'stopped';
62
+ spawnedAt: string;
63
+ workdir: string;
64
+ ports: {
65
+ health: number;
66
+ metrics: number;
67
+ p2p: number;
68
+ };
69
+ }
70
+ /**
71
+ * Listener invoked when a managed node's lifecycle state changes
72
+ * (create, child exit, manual stop). Subscribe via
73
+ * `HostProcessOrchestrator.onStateChange`.
74
+ */
75
+ export type NodeStateListener = (info: ManagedNodeInfo) => void;
76
+ /**
77
+ * Encode an opaque handle that callers treat the same way they treat a
78
+ * Docker container hash. The shape is `<pid>:<token>`; tokens are hex
79
+ * generated from `crypto.randomBytes` so they never contain `:`.
80
+ */
81
+ export declare function encodeDockerId(pid: number, token: string): string;
82
+ export declare function decodeDockerId(id: string): {
83
+ pid: number;
84
+ token: string;
85
+ };
@@ -0,0 +1,31 @@
1
+ /**
2
+ * Public + internal types for the HostProcessOrchestrator.
3
+ */
4
+ const TOKEN_SEPARATOR = ':';
5
+ /**
6
+ * Encode an opaque handle that callers treat the same way they treat a
7
+ * Docker container hash. The shape is `<pid>:<token>`; tokens are hex
8
+ * generated from `crypto.randomBytes` so they never contain `:`.
9
+ */
10
+ export function encodeDockerId(pid, token) {
11
+ if (!Number.isInteger(pid) || pid <= 0) {
12
+ throw new Error(`Invalid pid for docker id: ${pid}`);
13
+ }
14
+ if (token.length === 0 || token.includes(TOKEN_SEPARATOR)) {
15
+ throw new Error(`Invalid token for docker id: ${JSON.stringify(token)}`);
16
+ }
17
+ return `${pid}${TOKEN_SEPARATOR}${token}`;
18
+ }
19
+ export function decodeDockerId(id) {
20
+ const idx = id.indexOf(TOKEN_SEPARATOR);
21
+ if (idx <= 0) {
22
+ throw new Error(`Malformed dockerId: ${id}`);
23
+ }
24
+ const pid = Number.parseInt(id.slice(0, idx), 10);
25
+ const token = id.slice(idx + 1);
26
+ if (!Number.isInteger(pid) || pid <= 0 || token.length === 0) {
27
+ throw new Error(`Malformed dockerId: ${id}`);
28
+ }
29
+ return { pid, token };
30
+ }
31
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../../src/orchestrator/types.ts"],"names":[],"mappings":"AAAA;;GAEG;AAgEH,MAAM,eAAe,GAAG,GAAG,CAAC;AAE5B;;;;GAIG;AACH,MAAM,UAAU,cAAc,CAAC,GAAW,EAAE,KAAa;IACvD,IAAI,CAAC,MAAM,CAAC,SAAS,CAAC,GAAG,CAAC,IAAI,GAAG,IAAI,CAAC,EAAE,CAAC;QACvC,MAAM,IAAI,KAAK,CAAC,8BAA8B,GAAG,EAAE,CAAC,CAAC;IACvD,CAAC;IACD,IAAI,KAAK,CAAC,MAAM,KAAK,CAAC,IAAI,KAAK,CAAC,QAAQ,CAAC,eAAe,CAAC,EAAE,CAAC;QAC1D,MAAM,IAAI,KAAK,CAAC,gCAAgC,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC;IAC3E,CAAC;IACD,OAAO,GAAG,GAAG,GAAG,eAAe,GAAG,KAAK,EAAE,CAAC;AAC5C,CAAC;AAED,MAAM,UAAU,cAAc,CAAC,EAAU;IACvC,MAAM,GAAG,GAAG,EAAE,CAAC,OAAO,CAAC,eAAe,CAAC,CAAC;IACxC,IAAI,GAAG,IAAI,CAAC,EAAE,CAAC;QACb,MAAM,IAAI,KAAK,CAAC,uBAAuB,EAAE,EAAE,CAAC,CAAC;IAC/C,CAAC;IACD,MAAM,GAAG,GAAG,MAAM,CAAC,QAAQ,CAAC,EAAE,CAAC,KAAK,CAAC,CAAC,EAAE,GAAG,CAAC,EAAE,EAAE,CAAC,CAAC;IAClD,MAAM,KAAK,GAAG,EAAE,CAAC,KAAK,CAAC,GAAG,GAAG,CAAC,CAAC,CAAC;IAChC,IAAI,CAAC,MAAM,CAAC,SAAS,CAAC,GAAG,CAAC,IAAI,GAAG,IAAI,CAAC,IAAI,KAAK,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QAC7D,MAAM,IAAI,KAAK,CAAC,uBAAuB,EAAE,EAAE,CAAC,CAAC;IAC/C,CAAC;IACD,OAAO,EAAE,GAAG,EAAE,KAAK,EAAE,CAAC;AACxB,CAAC"}
@@ -0,0 +1,26 @@
1
+ /**
2
+ * Fastify error handler — translates known typed errors to stable HTTP status
3
+ * codes and uniform `{ ok: false, error: { code, message } }` payloads.
4
+ *
5
+ * Codes (cross-referenced with `auth/types.ts`, `nat/types.ts`, `update/types.ts`):
6
+ * - TrustCircleError
7
+ * invalid_label, invalid_token → 400
8
+ * not_found, already_redeemed → 404
9
+ * expired → 410
10
+ * storage_error → 500
11
+ * - NatError
12
+ * invalid_config, ddns_provider_unknown,
13
+ * ddns_credentials_missing → 400
14
+ * secrets_unavailable, storage_error,
15
+ * mapping_failed, router_unreachable,
16
+ * ip_detection_failed, ddns_update_failed → 500
17
+ * - UpdateErrorException
18
+ * manifest_invalid, no_update_available,
19
+ * min_previous_version → 400
20
+ * apply_in_progress → 409
21
+ * signature_invalid, unsupported_state_version,
22
+ * apply_failed, rollback_failed, storage_error → 500
23
+ * - everything else → 500 (code "internal")
24
+ */
25
+ import type { FastifyInstance } from 'fastify';
26
+ export declare function registerErrorHandler(app: FastifyInstance): void;
@@ -0,0 +1,95 @@
1
+ /**
2
+ * Fastify error handler — translates known typed errors to stable HTTP status
3
+ * codes and uniform `{ ok: false, error: { code, message } }` payloads.
4
+ *
5
+ * Codes (cross-referenced with `auth/types.ts`, `nat/types.ts`, `update/types.ts`):
6
+ * - TrustCircleError
7
+ * invalid_label, invalid_token → 400
8
+ * not_found, already_redeemed → 404
9
+ * expired → 410
10
+ * storage_error → 500
11
+ * - NatError
12
+ * invalid_config, ddns_provider_unknown,
13
+ * ddns_credentials_missing → 400
14
+ * secrets_unavailable, storage_error,
15
+ * mapping_failed, router_unreachable,
16
+ * ip_detection_failed, ddns_update_failed → 500
17
+ * - UpdateErrorException
18
+ * manifest_invalid, no_update_available,
19
+ * min_previous_version → 400
20
+ * apply_in_progress → 409
21
+ * signature_invalid, unsupported_state_version,
22
+ * apply_failed, rollback_failed, storage_error → 500
23
+ * - everything else → 500 (code "internal")
24
+ */
25
+ import debug from 'debug';
26
+ import { TrustCircleError } from '../auth/types.js';
27
+ import { NatError } from '../nat/types.js';
28
+ import { UpdateErrorException } from '../update/types.js';
29
+ const log = debug('cadre:host:error-handler');
30
+ const TRUST_STATUS = {
31
+ invalid_label: 400,
32
+ invalid_token: 400,
33
+ not_found: 404,
34
+ already_redeemed: 404,
35
+ expired: 410,
36
+ storage_error: 500,
37
+ };
38
+ const NAT_STATUS = {
39
+ invalid_config: 400,
40
+ ddns_provider_unknown: 400,
41
+ ddns_credentials_missing: 400,
42
+ secrets_unavailable: 500,
43
+ storage_error: 500,
44
+ mapping_failed: 500,
45
+ router_unreachable: 500,
46
+ ip_detection_failed: 500,
47
+ ddns_update_failed: 500,
48
+ };
49
+ const UPDATE_STATUS = {
50
+ manifest_invalid: 400,
51
+ no_update_available: 400,
52
+ min_previous_version: 400,
53
+ apply_in_progress: 409,
54
+ signature_invalid: 500,
55
+ unsupported_state_version: 500,
56
+ apply_failed: 500,
57
+ rollback_failed: 500,
58
+ storage_error: 500,
59
+ };
60
+ export function registerErrorHandler(app) {
61
+ app.setErrorHandler((err, request, reply) => {
62
+ const { status, code, message } = classify(err);
63
+ if (status >= 500) {
64
+ log('5xx %s %s: [%s] %s', request.method, request.url, code, message);
65
+ }
66
+ else {
67
+ log('%d %s %s: [%s] %s', status, request.method, request.url, code, message);
68
+ }
69
+ return reply.code(status).send({
70
+ ok: false,
71
+ error: { code, message },
72
+ });
73
+ });
74
+ }
75
+ function classify(err) {
76
+ if (err instanceof TrustCircleError) {
77
+ return { status: TRUST_STATUS[err.code] ?? 500, code: err.code, message: err.message };
78
+ }
79
+ if (err instanceof NatError) {
80
+ return { status: NAT_STATUS[err.code] ?? 500, code: err.code, message: err.message };
81
+ }
82
+ if (err instanceof UpdateErrorException) {
83
+ return { status: UPDATE_STATUS[err.code] ?? 500, code: err.code, message: err.message };
84
+ }
85
+ // Fastify body-parsing / validation errors have a statusCode we should honour.
86
+ if (typeof err.statusCode === 'number' && err.statusCode >= 400 && err.statusCode < 600) {
87
+ return {
88
+ status: err.statusCode,
89
+ code: err.code ?? (err.statusCode === 404 ? 'not_found' : 'bad_request'),
90
+ message: err.message ?? 'request failed',
91
+ };
92
+ }
93
+ return { status: 500, code: 'internal', message: err.message || 'internal error' };
94
+ }
95
+ //# sourceMappingURL=error-handler.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"error-handler.js","sourceRoot":"","sources":["../../src/server/error-handler.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,17 @@
1
+ /**
2
+ * In-memory event bus for the local UI.
3
+ *
4
+ * Tiny synchronous fan-out: publishers (route adapters, orchestrator hook,
5
+ * update observer) call `publish`; SSE connections subscribe and receive every
6
+ * event until they unsubscribe. Listener errors are caught so one broken
7
+ * client cannot break others.
8
+ */
9
+ import type { LocalUiEvent } from './types.js';
10
+ export type LocalUiEventListener = (event: LocalUiEvent) => void;
11
+ export declare class EventBus {
12
+ private readonly listeners;
13
+ publish(event: LocalUiEvent): void;
14
+ subscribe(listener: LocalUiEventListener): () => void;
15
+ /** Test helper. */
16
+ listenerCount(): number;
17
+ }
@@ -0,0 +1,36 @@
1
+ /**
2
+ * In-memory event bus for the local UI.
3
+ *
4
+ * Tiny synchronous fan-out: publishers (route adapters, orchestrator hook,
5
+ * update observer) call `publish`; SSE connections subscribe and receive every
6
+ * event until they unsubscribe. Listener errors are caught so one broken
7
+ * client cannot break others.
8
+ */
9
+ import debug from 'debug';
10
+ const log = debug('cadre:host:event-bus');
11
+ export class EventBus {
12
+ constructor() {
13
+ this.listeners = new Set();
14
+ }
15
+ publish(event) {
16
+ for (const listener of this.listeners) {
17
+ try {
18
+ listener(event);
19
+ }
20
+ catch (err) {
21
+ log('listener threw for event %s: %s', event.type, err.message);
22
+ }
23
+ }
24
+ }
25
+ subscribe(listener) {
26
+ this.listeners.add(listener);
27
+ return () => {
28
+ this.listeners.delete(listener);
29
+ };
30
+ }
31
+ /** Test helper. */
32
+ listenerCount() {
33
+ return this.listeners.size;
34
+ }
35
+ }
36
+ //# sourceMappingURL=bus.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"bus.js","sourceRoot":"","sources":["../../../src/server/events/bus.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,KAAK,MAAM,OAAO,CAAC;AAI1B,MAAM,GAAG,GAAG,KAAK,CAAC,sBAAsB,CAAC,CAAC;AAI1C,MAAM,OAAO,QAAQ;IAArB;QACmB,cAAS,GAAG,IAAI,GAAG,EAAwB,CAAC;IAuB/D,CAAC;IArBC,OAAO,CAAC,KAAmB;QACzB,KAAK,MAAM,QAAQ,IAAI,IAAI,CAAC,SAAS,EAAE,CAAC;YACtC,IAAI,CAAC;gBACH,QAAQ,CAAC,KAAK,CAAC,CAAC;YAClB,CAAC;YAAC,OAAO,GAAG,EAAE,CAAC;gBACb,GAAG,CAAC,iCAAiC,EAAE,KAAK,CAAC,IAAI,EAAG,GAAa,CAAC,OAAO,CAAC,CAAC;YAC7E,CAAC;QACH,CAAC;IACH,CAAC;IAED,SAAS,CAAC,QAA8B;QACtC,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC;QAC7B,OAAO,GAAG,EAAE;YACV,IAAI,CAAC,SAAS,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC;QAClC,CAAC,CAAC;IACJ,CAAC;IAED,mBAAmB;IACnB,aAAa;QACX,OAAO,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC;IAC7B,CAAC;CACF"}
@@ -0,0 +1,22 @@
1
+ /**
2
+ * Server-Sent Events route — `GET /api/events`.
3
+ *
4
+ * Hand-rolled (~40 lines vs. the 30 KB @fastify/sse-v2 dep). The wire format:
5
+ *
6
+ * retry: 5000\n\n ← reconnect hint
7
+ * : heartbeat\n\n ← comment line, every 15 s
8
+ * event: <type>\n ← event name
9
+ * data: <json>\n\n ← payload
10
+ *
11
+ * The handler subscribes to the bus on connect, unsubscribes on client
12
+ * disconnect, and ticks a heartbeat so corporate proxies don't close the
13
+ * connection at the idle timeout.
14
+ */
15
+ import type { FastifyInstance } from 'fastify';
16
+ import type { EventBus } from './bus.js';
17
+ export interface SseRouteOptions {
18
+ events: EventBus;
19
+ /** Test override: shorter heartbeat. */
20
+ heartbeatIntervalMs?: number;
21
+ }
22
+ export declare function registerSseRoute(app: FastifyInstance, opts: SseRouteOptions): void;
@@ -0,0 +1,65 @@
1
+ /**
2
+ * Server-Sent Events route — `GET /api/events`.
3
+ *
4
+ * Hand-rolled (~40 lines vs. the 30 KB @fastify/sse-v2 dep). The wire format:
5
+ *
6
+ * retry: 5000\n\n ← reconnect hint
7
+ * : heartbeat\n\n ← comment line, every 15 s
8
+ * event: <type>\n ← event name
9
+ * data: <json>\n\n ← payload
10
+ *
11
+ * The handler subscribes to the bus on connect, unsubscribes on client
12
+ * disconnect, and ticks a heartbeat so corporate proxies don't close the
13
+ * connection at the idle timeout.
14
+ */
15
+ import debug from 'debug';
16
+ const log = debug('cadre:host:sse');
17
+ const HEARTBEAT_INTERVAL_MS = 15000;
18
+ const RECONNECT_HINT_MS = 5000;
19
+ export function registerSseRoute(app, opts) {
20
+ const heartbeatMs = opts.heartbeatIntervalMs ?? HEARTBEAT_INTERVAL_MS;
21
+ app.get('/api/events', (request, reply) => {
22
+ // Tell Fastify we're handling the reply ourselves — it won't try to
23
+ // serialise or send a payload.
24
+ reply.hijack();
25
+ reply.raw.writeHead(200, {
26
+ 'Content-Type': 'text/event-stream',
27
+ 'Cache-Control': 'no-cache, no-transform',
28
+ Connection: 'keep-alive',
29
+ 'X-Accel-Buffering': 'no',
30
+ });
31
+ reply.raw.write(`retry: ${RECONNECT_HINT_MS}\n\n`);
32
+ reply.raw.write(`: connected\n\n`);
33
+ const onEvent = (event) => {
34
+ try {
35
+ reply.raw.write(`event: ${event.type}\n`);
36
+ reply.raw.write(`data: ${JSON.stringify(event)}\n\n`);
37
+ }
38
+ catch (err) {
39
+ log('write failed (client likely gone): %s', err.message);
40
+ }
41
+ };
42
+ const unsubscribe = opts.events.subscribe(onEvent);
43
+ const heartbeat = setInterval(() => {
44
+ try {
45
+ reply.raw.write(`: heartbeat\n\n`);
46
+ }
47
+ catch (err) {
48
+ log('heartbeat write failed: %s', err.message);
49
+ }
50
+ }, heartbeatMs);
51
+ // Don't hold the event loop open for the heartbeat alone.
52
+ if (typeof heartbeat === 'object' && 'unref' in heartbeat) {
53
+ heartbeat.unref?.();
54
+ }
55
+ const cleanup = () => {
56
+ clearInterval(heartbeat);
57
+ unsubscribe();
58
+ };
59
+ request.raw.on('close', cleanup);
60
+ request.raw.on('error', cleanup);
61
+ // The reply object is intentionally not returned — Fastify must not
62
+ // attempt to serialise an SSE response.
63
+ });
64
+ }
65
+ //# sourceMappingURL=sse-route.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sse-route.js","sourceRoot":"","sources":["../../../src/server/events/sse-route.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAGH,OAAO,KAAK,MAAM,OAAO,CAAC;AAK1B,MAAM,GAAG,GAAG,KAAK,CAAC,gBAAgB,CAAC,CAAC;AAEpC,MAAM,qBAAqB,GAAG,KAAM,CAAC;AACrC,MAAM,iBAAiB,GAAG,IAAK,CAAC;AAQhC,MAAM,UAAU,gBAAgB,CAAC,GAAoB,EAAE,IAAqB;IAC1E,MAAM,WAAW,GAAG,IAAI,CAAC,mBAAmB,IAAI,qBAAqB,CAAC;IAEtE,GAAG,CAAC,GAAG,CAAC,aAAa,EAAE,CAAC,OAAuB,EAAE,KAAmB,EAAE,EAAE;QACtE,oEAAoE;QACpE,+BAA+B;QAC/B,KAAK,CAAC,MAAM,EAAE,CAAC;QACf,KAAK,CAAC,GAAG,CAAC,SAAS,CAAC,GAAG,EAAE;YACvB,cAAc,EAAE,mBAAmB;YACnC,eAAe,EAAE,wBAAwB;YACzC,UAAU,EAAE,YAAY;YACxB,mBAAmB,EAAE,IAAI;SAC1B,CAAC,CAAC;QACH,KAAK,CAAC,GAAG,CAAC,KAAK,CAAC,UAAU,iBAAiB,MAAM,CAAC,CAAC;QACnD,KAAK,CAAC,GAAG,CAAC,KAAK,CAAC,iBAAiB,CAAC,CAAC;QAEnC,MAAM,OAAO,GAAG,CAAC,KAAmB,EAAQ,EAAE;YAC5C,IAAI,CAAC;gBACH,KAAK,CAAC,GAAG,CAAC,KAAK,CAAC,UAAU,KAAK,CAAC,IAAI,IAAI,CAAC,CAAC;gBAC1C,KAAK,CAAC,GAAG,CAAC,KAAK,CAAC,SAAS,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC;YACxD,CAAC;YAAC,OAAO,GAAG,EAAE,CAAC;gBACb,GAAG,CAAC,uCAAuC,EAAG,GAAa,CAAC,OAAO,CAAC,CAAC;YACvE,CAAC;QACH,CAAC,CAAC;QACF,MAAM,WAAW,GAAG,IAAI,CAAC,MAAM,CAAC,SAAS,CAAC,OAAO,CAAC,CAAC;QAEnD,MAAM,SAAS,GAAG,WAAW,CAAC,GAAG,EAAE;YACjC,IAAI,CAAC;gBACH,KAAK,CAAC,GAAG,CAAC,KAAK,CAAC,iBAAiB,CAAC,CAAC;YACrC,CAAC;YAAC,OAAO,GAAG,EAAE,CAAC;gBACb,GAAG,CAAC,4BAA4B,EAAG,GAAa,CAAC,OAAO,CAAC,CAAC;YAC5D,CAAC;QACH,CAAC,EAAE,WAAW,CAAC,CAAC;QAChB,0DAA0D;QAC1D,IAAI,OAAO,SAAS,KAAK,QAAQ,IAAI,OAAO,IAAI,SAAS,EAAE,CAAC;YACzD,SAAoC,CAAC,KAAK,EAAE,EAAE,CAAC;QAClD,CAAC;QAED,MAAM,OAAO,GAAG,GAAS,EAAE;YACzB,aAAa,CAAC,SAAS,CAAC,CAAC;YACzB,WAAW,EAAE,CAAC;QAChB,CAAC,CAAC;QACF,OAAO,CAAC,GAAG,CAAC,EAAE,CAAC,OAAO,EAAE,OAAO,CAAC,CAAC;QACjC,OAAO,CAAC,GAAG,CAAC,EAAE,CAAC,OAAO,EAAE,OAAO,CAAC,CAAC;QAEjC,oEAAoE;QACpE,wCAAwC;IAC1C,CAAC,CAAC,CAAC;AACL,CAAC"}