@serfab/cadre-host 1.13.0 → 1.14.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 (337) hide show
  1. package/README.md +332 -423
  2. package/dist/bin/host.d.ts +6 -3
  3. package/dist/bin/host.js +301 -505
  4. package/dist/bin/host.js.map +1 -1
  5. package/dist/bin/join-output.d.ts +56 -0
  6. package/dist/bin/join-output.js +92 -0
  7. package/dist/bin/join-output.js.map +1 -0
  8. package/dist/bin/nat-output.d.ts +49 -0
  9. package/dist/bin/nat-output.js +127 -0
  10. package/dist/bin/nat-output.js.map +1 -0
  11. package/dist/hosted/claim-details.d.ts +44 -0
  12. package/dist/hosted/claim-details.js +66 -0
  13. package/dist/hosted/claim-details.js.map +1 -0
  14. package/dist/hosted/hosted-node-service.d.ts +318 -0
  15. package/dist/hosted/hosted-node-service.js +666 -0
  16. package/dist/hosted/hosted-node-service.js.map +1 -0
  17. package/dist/hosted/hosted-node-store.d.ts +40 -0
  18. package/dist/hosted/hosted-node-store.js +126 -0
  19. package/dist/hosted/hosted-node-store.js.map +1 -0
  20. package/dist/{donation/donation-supervisor.d.ts → hosted/hosted-node-supervisor.d.ts} +70 -53
  21. package/dist/hosted/hosted-node-supervisor.js +396 -0
  22. package/dist/hosted/hosted-node-supervisor.js.map +1 -0
  23. package/dist/hosted/hosted-node-watcher.d.ts +83 -0
  24. package/dist/hosted/hosted-node-watcher.js +224 -0
  25. package/dist/hosted/hosted-node-watcher.js.map +1 -0
  26. package/dist/hosted/index.d.ts +16 -0
  27. package/dist/hosted/index.js +13 -0
  28. package/dist/hosted/index.js.map +1 -0
  29. package/dist/hosted/node-status.d.ts +35 -0
  30. package/dist/hosted/node-status.js +61 -0
  31. package/dist/hosted/node-status.js.map +1 -0
  32. package/dist/hosted/types.d.ts +134 -0
  33. package/dist/hosted/types.js +22 -0
  34. package/dist/hosted/types.js.map +1 -0
  35. package/dist/index.d.ts +12 -24
  36. package/dist/index.js +9 -19
  37. package/dist/index.js.map +1 -1
  38. package/dist/installer/config.d.ts +4 -34
  39. package/dist/installer/config.js +3 -18
  40. package/dist/installer/config.js.map +1 -1
  41. package/dist/installer/index.d.ts +7 -19
  42. package/dist/installer/index.js +14 -69
  43. package/dist/installer/index.js.map +1 -1
  44. package/dist/installer/paths.d.ts +0 -2
  45. package/dist/installer/paths.js +0 -4
  46. package/dist/installer/paths.js.map +1 -1
  47. package/dist/installer/wizard.d.ts +0 -7
  48. package/dist/installer/wizard.js +1 -8
  49. package/dist/installer/wizard.js.map +1 -1
  50. package/dist/nat/address-resolver.d.ts +18 -17
  51. package/dist/nat/address-resolver.js +79 -25
  52. package/dist/nat/address-resolver.js.map +1 -1
  53. package/dist/nat/address-watch.d.ts +37 -0
  54. package/dist/nat/address-watch.js +84 -0
  55. package/dist/nat/address-watch.js.map +1 -0
  56. package/dist/nat/ddns/duckdns.d.ts +2 -1
  57. package/dist/nat/ddns/duckdns.js +5 -1
  58. package/dist/nat/ddns/duckdns.js.map +1 -1
  59. package/dist/nat/ddns/index.d.ts +6 -0
  60. package/dist/nat/ddns/index.js.map +1 -1
  61. package/dist/nat/index.d.ts +12 -9
  62. package/dist/nat/index.js +6 -5
  63. package/dist/nat/index.js.map +1 -1
  64. package/dist/nat/nat-service.d.ts +203 -77
  65. package/dist/nat/nat-service.js +602 -237
  66. package/dist/nat/nat-service.js.map +1 -1
  67. package/dist/nat/nat-store.d.ts +12 -5
  68. package/dist/nat/nat-store.js +114 -30
  69. package/dist/nat/nat-store.js.map +1 -1
  70. package/dist/nat/port-mapper.d.ts +83 -79
  71. package/dist/nat/port-mapper.js +148 -189
  72. package/dist/nat/port-mapper.js.map +1 -1
  73. package/dist/nat/reachability.d.ts +31 -15
  74. package/dist/nat/reachability.js +97 -22
  75. package/dist/nat/reachability.js.map +1 -1
  76. package/dist/nat/types.d.ts +78 -27
  77. package/dist/nat/types.js +8 -5
  78. package/dist/nat/types.js.map +1 -1
  79. package/dist/orchestrator/host-process-orchestrator.d.ts +29 -71
  80. package/dist/orchestrator/host-process-orchestrator.js +78 -218
  81. package/dist/orchestrator/host-process-orchestrator.js.map +1 -1
  82. package/dist/{installer/identity.d.ts → orchestrator/identity-file.d.ts} +5 -5
  83. package/dist/{installer/identity.js → orchestrator/identity-file.js} +6 -6
  84. package/dist/orchestrator/identity-file.js.map +1 -0
  85. package/dist/orchestrator/index.d.ts +2 -3
  86. package/dist/orchestrator/index.js +1 -1
  87. package/dist/orchestrator/index.js.map +1 -1
  88. package/dist/orchestrator/node-identity.d.ts +6 -6
  89. package/dist/orchestrator/node-identity.js +7 -7
  90. package/dist/orchestrator/node-identity.js.map +1 -1
  91. package/dist/orchestrator/port-allocator.d.ts +5 -6
  92. package/dist/orchestrator/port-allocator.js +9 -11
  93. package/dist/orchestrator/port-allocator.js.map +1 -1
  94. package/dist/orchestrator/state-store.d.ts +3 -5
  95. package/dist/orchestrator/state-store.js +2 -10
  96. package/dist/orchestrator/state-store.js.map +1 -1
  97. package/dist/orchestrator/types.d.ts +33 -22
  98. package/dist/orchestrator/types.js.map +1 -1
  99. package/dist/server/error-handler.d.ts +5 -25
  100. package/dist/server/error-handler.js +11 -74
  101. package/dist/server/error-handler.js.map +1 -1
  102. package/dist/server/events/types.d.ts +7 -11
  103. package/dist/server/events/types.js +2 -1
  104. package/dist/server/events/types.js.map +1 -1
  105. package/dist/server/index.d.ts +11 -37
  106. package/dist/server/index.js +30 -48
  107. package/dist/server/index.js.map +1 -1
  108. package/dist/server/routes/hosted-nodes.d.ts +28 -0
  109. package/dist/server/routes/hosted-nodes.js +68 -0
  110. package/dist/server/routes/hosted-nodes.js.map +1 -0
  111. package/dist/server/routes/nat.d.ts +4 -1
  112. package/dist/server/routes/nat.js +13 -2
  113. package/dist/server/routes/nat.js.map +1 -1
  114. package/dist/server/routes/nodes.d.ts +6 -20
  115. package/dist/server/routes/nodes.js +7 -86
  116. package/dist/server/routes/nodes.js.map +1 -1
  117. package/dist/server/routes/settings.d.ts +2 -7
  118. package/dist/server/routes/settings.js +3 -7
  119. package/dist/server/routes/settings.js.map +1 -1
  120. package/dist/server/routes/status.d.ts +2 -25
  121. package/dist/server/routes/status.js +1 -11
  122. package/dist/server/routes/status.js.map +1 -1
  123. package/dist/server/server.js +8 -8
  124. package/dist/server/settings-store.d.ts +2 -2
  125. package/dist/server/settings-store.js +2 -2
  126. package/dist/server/static.js +1 -2
  127. package/dist/server/static.js.map +1 -1
  128. package/dist/ui/assets/index-4FWuU8ye.js +19 -0
  129. package/dist/ui/assets/index-4FWuU8ye.js.map +1 -0
  130. package/dist/ui/assets/index-Di9Q4d01.css +1 -0
  131. package/dist/ui/index.html +2 -2
  132. package/dist/update/index.d.ts +2 -2
  133. package/dist/update/index.js +2 -2
  134. package/dist/update/store.d.ts +1 -1
  135. package/dist/update/store.js +1 -1
  136. package/dist/update/types.d.ts +1 -1
  137. package/dist/update/types.js +1 -1
  138. package/package.json +5 -6
  139. package/src/__tests__/cli-nat.smoke.test.ts +219 -212
  140. package/src/__tests__/cli.smoke.test.ts +1 -1
  141. package/src/__tests__/global-setup.ts +2 -3
  142. package/src/__tests__/orchestrator-claim-secret.test.ts +112 -0
  143. package/src/__tests__/orchestrator-handle-contract.ts +134 -134
  144. package/src/__tests__/orchestrator-node-identity.test.ts +182 -203
  145. package/src/__tests__/orchestrator-ports.test.ts +124 -143
  146. package/src/__tests__/orchestrator-push.test.ts +138 -146
  147. package/src/__tests__/orchestrator.test.ts +1019 -1063
  148. package/src/bin/host.ts +368 -553
  149. package/src/bin/join-output.ts +120 -0
  150. package/src/bin/nat-output.ts +177 -0
  151. package/src/{donation → hosted}/__tests__/fake-orchestrator.test.ts +173 -173
  152. package/src/{donation → hosted}/__tests__/fake-orchestrator.ts +265 -243
  153. package/src/hosted/__tests__/hosted-node-service.test.ts +609 -0
  154. package/src/hosted/__tests__/hosted-node-supervisor.test.ts +474 -0
  155. package/src/hosted/__tests__/test-invitation.ts +33 -0
  156. package/src/hosted/claim-details.ts +96 -0
  157. package/src/hosted/hosted-node-service.ts +795 -0
  158. package/src/hosted/hosted-node-store.ts +144 -0
  159. package/src/hosted/hosted-node-supervisor.ts +439 -0
  160. package/src/hosted/hosted-node-watcher.ts +250 -0
  161. package/src/hosted/index.ts +53 -0
  162. package/src/hosted/node-status.ts +71 -0
  163. package/src/hosted/types.ts +153 -0
  164. package/src/index.ts +72 -91
  165. package/src/installer/__tests__/config.test.ts +15 -40
  166. package/src/installer/__tests__/installer.smoke.test.ts +5 -86
  167. package/src/installer/__tests__/wizard.test.ts +56 -64
  168. package/src/installer/config.ts +135 -175
  169. package/src/installer/index.ts +15 -83
  170. package/src/installer/paths.ts +104 -109
  171. package/src/installer/wizard.ts +87 -103
  172. package/src/nat/__tests__/address-resolver.test.ts +107 -82
  173. package/src/nat/__tests__/nat-service.test.ts +433 -408
  174. package/src/nat/__tests__/nat-store.test.ts +127 -93
  175. package/src/nat/__tests__/port-mapper.test.ts +30 -191
  176. package/src/nat/__tests__/reachability.test.ts +96 -13
  177. package/src/nat/address-resolver.ts +94 -52
  178. package/src/nat/address-watch.ts +92 -0
  179. package/src/nat/ddns/duckdns.ts +5 -1
  180. package/src/nat/ddns/index.ts +6 -0
  181. package/src/nat/index.ts +87 -71
  182. package/src/nat/nat-service.ts +889 -501
  183. package/src/nat/nat-store.ts +237 -154
  184. package/src/nat/port-mapper.ts +241 -329
  185. package/src/nat/reachability.ts +134 -25
  186. package/src/nat/types.ts +209 -159
  187. package/src/{installer/__tests__/identity.test.ts → orchestrator/__tests__/identity-file.test.ts} +34 -34
  188. package/src/orchestrator/__tests__/node-identity.test.ts +62 -62
  189. package/src/orchestrator/host-process-orchestrator.ts +81 -243
  190. package/src/{installer/identity.ts → orchestrator/identity-file.ts} +56 -56
  191. package/src/orchestrator/index.ts +9 -10
  192. package/src/orchestrator/node-identity.ts +41 -41
  193. package/src/orchestrator/port-allocator.ts +71 -73
  194. package/src/orchestrator/state-store.ts +76 -90
  195. package/src/orchestrator/types.ts +34 -23
  196. package/src/server/__tests__/bus.test.ts +38 -38
  197. package/src/server/__tests__/error-handler.test.ts +81 -118
  198. package/src/server/__tests__/fakes.ts +40 -44
  199. package/src/server/__tests__/hosted-nodes-route.test.ts +160 -0
  200. package/src/server/__tests__/nodes-route.test.ts +114 -283
  201. package/src/server/__tests__/publishers.test.ts +193 -180
  202. package/src/server/__tests__/server.smoke.test.ts +134 -223
  203. package/src/server/__tests__/settings-route.test.ts +179 -195
  204. package/src/server/__tests__/sse-route.test.ts +119 -119
  205. package/src/server/__tests__/status-route.test.ts +82 -121
  206. package/src/server/error-handler.ts +112 -178
  207. package/src/server/events/types.ts +20 -24
  208. package/src/server/index.ts +223 -267
  209. package/src/server/routes/hosted-nodes.ts +83 -0
  210. package/src/server/routes/nat.ts +75 -63
  211. package/src/server/routes/nodes.ts +96 -188
  212. package/src/server/routes/settings.ts +115 -123
  213. package/src/server/routes/status.ts +102 -134
  214. package/src/server/server.ts +8 -8
  215. package/src/server/settings-store.ts +2 -2
  216. package/src/server/static.ts +1 -2
  217. package/src/update/index.ts +2 -2
  218. package/src/update/store.ts +1 -1
  219. package/src/update/types.ts +1 -1
  220. package/dist/auth/duration.d.ts +0 -1
  221. package/dist/auth/duration.js +0 -32
  222. package/dist/auth/duration.js.map +0 -1
  223. package/dist/auth/index.d.ts +0 -11
  224. package/dist/auth/index.js +0 -9
  225. package/dist/auth/index.js.map +0 -1
  226. package/dist/auth/self-label.d.ts +0 -36
  227. package/dist/auth/self-label.js +0 -57
  228. package/dist/auth/self-label.js.map +0 -1
  229. package/dist/auth/trust-circle-store.d.ts +0 -26
  230. package/dist/auth/trust-circle-store.js +0 -129
  231. package/dist/auth/trust-circle-store.js.map +0 -1
  232. package/dist/auth/trust-circle.d.ts +0 -156
  233. package/dist/auth/trust-circle.js +0 -307
  234. package/dist/auth/trust-circle.js.map +0 -1
  235. package/dist/auth/types.d.ts +0 -84
  236. package/dist/auth/types.js +0 -24
  237. package/dist/auth/types.js.map +0 -1
  238. package/dist/donation/donation-service.d.ts +0 -389
  239. package/dist/donation/donation-service.js +0 -828
  240. package/dist/donation/donation-service.js.map +0 -1
  241. package/dist/donation/donation-store.d.ts +0 -51
  242. package/dist/donation/donation-store.js +0 -152
  243. package/dist/donation/donation-store.js.map +0 -1
  244. package/dist/donation/donation-supervisor.js +0 -364
  245. package/dist/donation/donation-supervisor.js.map +0 -1
  246. package/dist/donation/grant-service.d.ts +0 -80
  247. package/dist/donation/grant-service.js +0 -210
  248. package/dist/donation/grant-service.js.map +0 -1
  249. package/dist/donation/grant-store.d.ts +0 -38
  250. package/dist/donation/grant-store.js +0 -135
  251. package/dist/donation/grant-store.js.map +0 -1
  252. package/dist/donation/index.d.ts +0 -20
  253. package/dist/donation/index.js +0 -17
  254. package/dist/donation/index.js.map +0 -1
  255. package/dist/donation/types.d.ts +0 -224
  256. package/dist/donation/types.js +0 -40
  257. package/dist/donation/types.js.map +0 -1
  258. package/dist/installer/identity.js.map +0 -1
  259. package/dist/owner/index.d.ts +0 -11
  260. package/dist/owner/index.js +0 -11
  261. package/dist/owner/index.js.map +0 -1
  262. package/dist/owner/owner-node-client.d.ts +0 -110
  263. package/dist/owner/owner-node-client.js +0 -184
  264. package/dist/owner/owner-node-client.js.map +0 -1
  265. package/dist/server/routes/bootstrap-node-validation.d.ts +0 -115
  266. package/dist/server/routes/bootstrap-node-validation.js +0 -197
  267. package/dist/server/routes/bootstrap-node-validation.js.map +0 -1
  268. package/dist/server/routes/grants-admin.d.ts +0 -29
  269. package/dist/server/routes/grants-admin.js +0 -58
  270. package/dist/server/routes/grants-admin.js.map +0 -1
  271. package/dist/server/routes/grants.d.ts +0 -28
  272. package/dist/server/routes/grants.js +0 -165
  273. package/dist/server/routes/grants.js.map +0 -1
  274. package/dist/server/routes/provision-request-validation.d.ts +0 -45
  275. package/dist/server/routes/provision-request-validation.js +0 -97
  276. package/dist/server/routes/provision-request-validation.js.map +0 -1
  277. package/dist/server/routes/strands.d.ts +0 -20
  278. package/dist/server/routes/strands.js +0 -38
  279. package/dist/server/routes/strands.js.map +0 -1
  280. package/dist/server/routes/trust-circle.d.ts +0 -17
  281. package/dist/server/routes/trust-circle.js +0 -35
  282. package/dist/server/routes/trust-circle.js.map +0 -1
  283. package/dist/strands/index.d.ts +0 -7
  284. package/dist/strands/index.js +0 -6
  285. package/dist/strands/index.js.map +0 -1
  286. package/dist/strands/strand-service.d.ts +0 -50
  287. package/dist/strands/strand-service.js +0 -103
  288. package/dist/strands/strand-service.js.map +0 -1
  289. package/dist/strands/types.d.ts +0 -93
  290. package/dist/strands/types.js +0 -23
  291. package/dist/strands/types.js.map +0 -1
  292. package/dist/ui/assets/index-C6nELNfC.js +0 -23
  293. package/dist/ui/assets/index-C6nELNfC.js.map +0 -1
  294. package/dist/ui/assets/index-TtIYSeZO.css +0 -1
  295. package/src/__tests__/cli-invite.smoke.test.ts +0 -192
  296. package/src/__tests__/host-owner.smoke.test.ts +0 -123
  297. package/src/__tests__/orchestrator-owner.test.ts +0 -234
  298. package/src/__tests__/orchestrator-pin-keys.test.ts +0 -152
  299. package/src/auth/__tests__/duration.test.ts +0 -28
  300. package/src/auth/__tests__/self-label.test.ts +0 -85
  301. package/src/auth/__tests__/trust-circle-integration.test.ts +0 -152
  302. package/src/auth/__tests__/trust-circle-store.test.ts +0 -115
  303. package/src/auth/__tests__/trust-circle.test.ts +0 -445
  304. package/src/auth/duration.ts +0 -33
  305. package/src/auth/index.ts +0 -21
  306. package/src/auth/self-label.ts +0 -74
  307. package/src/auth/trust-circle-store.ts +0 -155
  308. package/src/auth/trust-circle.ts +0 -395
  309. package/src/auth/types.ts +0 -99
  310. package/src/donation/__tests__/donation-service.test.ts +0 -1193
  311. package/src/donation/__tests__/donation-supervisor.test.ts +0 -548
  312. package/src/donation/__tests__/grant-service.test.ts +0 -213
  313. package/src/donation/__tests__/grant-store.test.ts +0 -113
  314. package/src/donation/donation-service.ts +0 -1033
  315. package/src/donation/donation-store.ts +0 -176
  316. package/src/donation/donation-supervisor.ts +0 -411
  317. package/src/donation/grant-service.ts +0 -252
  318. package/src/donation/grant-store.ts +0 -152
  319. package/src/donation/index.ts +0 -65
  320. package/src/donation/types.ts +0 -260
  321. package/src/owner/__tests__/owner-node-client.test.ts +0 -194
  322. package/src/owner/index.ts +0 -12
  323. package/src/owner/owner-node-client.ts +0 -259
  324. package/src/server/__tests__/bootstrap-node-validation.test.ts +0 -138
  325. package/src/server/__tests__/grants-admin-route.test.ts +0 -171
  326. package/src/server/__tests__/grants-route.test.ts +0 -394
  327. package/src/server/__tests__/strands-route.test.ts +0 -234
  328. package/src/server/routes/bootstrap-node-validation.ts +0 -206
  329. package/src/server/routes/grants-admin.ts +0 -74
  330. package/src/server/routes/grants.ts +0 -193
  331. package/src/server/routes/provision-request-validation.ts +0 -105
  332. package/src/server/routes/strands.ts +0 -54
  333. package/src/server/routes/trust-circle.ts +0 -49
  334. package/src/strands/__tests__/strand-service.test.ts +0 -165
  335. package/src/strands/index.ts +0 -14
  336. package/src/strands/strand-service.ts +0 -135
  337. package/src/strands/types.ts +0 -102
@@ -1,143 +1,124 @@
1
- /**
2
- * The pure port-set logic behind `HostProcessOrchestrator`: the helpers that
3
- * allocate, hold, release and reuse one node's set of ports, and the listen
4
- * addresses a child is started with. No child processes — the behaviour these
5
- * feed is exercised against a stub child in `orchestrator.test.ts`.
6
- */
7
-
8
- import { describe, expect, it } from 'vitest';
9
-
10
- import { PortAllocator } from '@serfab/cadre-provider';
11
-
12
- import { childListenAddrs } from '../orchestrator/host-process-orchestrator.js';
13
- import {
14
- allocateNodePorts,
15
- releaseNodePorts,
16
- reserveNodePorts,
17
- reusedNodePorts,
18
- } from '../orchestrator/port-allocator.js';
19
- import type { NodePorts } from '../orchestrator/types.js';
20
-
21
- /**
22
- * A port set as `state.json` hands it back from a build that predates the `ws` key:
23
- * the type says every key is present, the data does not.
24
- */
25
- function portsWithoutWs(ports: Omit<NodePorts, 'ws'>): NodePorts {
26
- return ports as NodePorts;
27
- }
28
-
29
- describe('allocateNodePorts', () => {
30
- it('allocates the five ports in a fixed order, the newest key last', () => {
31
- const a = new PortAllocator(10000, 10010);
32
- expect(allocateNodePorts(a)).toEqual({ health: 10000, metrics: 10001, p2p: 10002, admin: 10003, ws: 10004 });
33
- });
34
-
35
- // A partial set left reserved would leak from a bounded range on every failed
36
- // provision — the whole reason this helper exists rather than five allocates.
37
- it('is all-or-nothing: an exhausted range releases everything it took', () => {
38
- const a = new PortAllocator(13000, 13002);
39
- expect(() => allocateNodePorts(a)).toThrow(/No available ports/);
40
- expect(a.has(13000)).toBe(false);
41
- expect(a.has(13001)).toBe(false);
42
- expect(a.has(13002)).toBe(false);
43
- });
44
-
45
- // Four ports used to be one node's worth. A range sized for that now fails on the
46
- // fifth, and must neither hand back a set missing `ws` nor keep the four it took.
47
- it('fails cleanly on a range with room for four ports but not five', () => {
48
- const a = new PortAllocator(13000, 13003);
49
- expect(() => allocateNodePorts(a)).toThrow(/No available ports/);
50
- for (let port = 13000; port <= 13003; port++) expect(a.has(port)).toBe(false);
51
- });
52
-
53
- // The owner node's p2p port is pinned by the NAT mapping, not allocated.
54
- it('honours an override and reserves it', () => {
55
- const a = new PortAllocator(10000, 10010);
56
- expect(allocateNodePorts(a, { p2p: 10005 })).toEqual({
57
- health: 10000,
58
- metrics: 10001,
59
- p2p: 10005,
60
- admin: 10002,
61
- ws: 10003,
62
- });
63
- // Reserved, so no later allocation can hand it out again.
64
- expect(a.has(10005)).toBe(true);
65
- });
66
-
67
- // An out-of-range override is the production case for the owner node: its
68
- // libp2p port is chosen by NAT config, not by the allocator, so reserving it
69
- // is a documented no-op rather than an error.
70
- it('accepts an override outside the managed range without reserving it', () => {
71
- const a = new PortAllocator(10000, 10010);
72
- expect(allocateNodePorts(a, { p2p: 40000 })).toMatchObject({ p2p: 40000, health: 10000 });
73
- expect(a.has(40000)).toBe(false);
74
- });
75
-
76
- // A re-spawn passes its previous set as overrides; every one must come back exactly.
77
- it('returns a full set of overrides exactly, allocating nothing', () => {
78
- const a = new PortAllocator(10000, 10010);
79
- const previous = { health: 10006, metrics: 10007, p2p: 10008, admin: 10009, ws: 10010 };
80
- expect(allocateNodePorts(a, previous)).toEqual(previous);
81
- expect(a.allocate()).toBe(10000);
82
- });
83
- });
84
-
85
- describe('reserveNodePorts / releaseNodePorts', () => {
86
- it('hold and then release every key of the set', () => {
87
- const a = new PortAllocator(10000, 10010);
88
- const ports = { health: 10000, metrics: 10001, p2p: 10002, admin: 10003, ws: 10004 };
89
- reserveNodePorts(a, ports);
90
- for (const port of Object.values(ports)) expect(a.has(port)).toBe(true);
91
- releaseNodePorts(a, ports);
92
- for (const port of Object.values(ports)) expect(a.has(port)).toBe(false);
93
- });
94
-
95
- // Rehydrating, or dropping, a `state.json` handle written before `ws` existed.
96
- it('skip a key the set lacks', () => {
97
- const a = new PortAllocator(10000, 10010);
98
- const ports = portsWithoutWs({ health: 10000, metrics: 10001, p2p: 10002, admin: 10003 });
99
- reserveNodePorts(a, ports);
100
- expect(a.has(undefined as unknown as number)).toBe(false);
101
- expect(a.allocate()).toBe(10004);
102
- releaseNodePorts(a, ports);
103
- expect(a.has(10000)).toBe(false);
104
- expect(a.has(10004)).toBe(true);
105
- });
106
- });
107
-
108
- describe('reusedNodePorts', () => {
109
- const previous = { health: 10005, metrics: 10006, p2p: 10007, admin: 10008, ws: 10009 };
110
-
111
- it("returns the dropped handle's whole port set", () => {
112
- expect(reusedNodePorts([{ ports: previous }])).toEqual(previous);
113
- });
114
-
115
- it('returns nothing when nothing was dropped, so every port is allocated fresh', () => {
116
- expect(reusedNodePorts([])).toEqual({});
117
- });
118
-
119
- // A handle persisted before `ws` existed: its other ports come back, `ws` is new.
120
- it('omits a key the dropped handle lacks', () => {
121
- const legacy = portsWithoutWs({ health: 10005, metrics: 10006, p2p: 10007, admin: 10008 });
122
- const reused = reusedNodePorts([{ ports: legacy }]);
123
- expect(reused).toEqual({ health: 10005, metrics: 10006, p2p: 10007, admin: 10008 });
124
- expect('ws' in reused).toBe(false);
125
-
126
- const a = new PortAllocator(10000, 10010);
127
- expect(allocateNodePorts(a, reused)).toEqual({ health: 10005, metrics: 10006, p2p: 10007, admin: 10008, ws: 10000 });
128
- });
129
-
130
- it('takes the last handle when a state holding duplicates dropped several', () => {
131
- const older = { health: 10000, metrics: 10001, p2p: 10002, admin: 10003, ws: 10004 };
132
- expect(reusedNodePorts([{ ports: older }, { ports: previous }])).toEqual(previous);
133
- });
134
- });
135
-
136
- describe('childListenAddrs', () => {
137
- it('names a TCP listener on the p2p port and a WebSocket listener on the ws port, on every interface', () => {
138
- expect(childListenAddrs({ p2p: 10002, ws: 10004 })).toEqual([
139
- '/ip4/0.0.0.0/tcp/10002',
140
- '/ip4/0.0.0.0/tcp/10004/ws',
141
- ]);
142
- });
143
- });
1
+ /**
2
+ * The pure port-set logic behind `HostProcessOrchestrator`: the helpers that
3
+ * allocate, hold, release and reuse one node's set of ports, and the listen
4
+ * addresses a child is started with. No child processes — the behaviour these
5
+ * feed is exercised against a stub child in `orchestrator.test.ts`.
6
+ */
7
+
8
+ import { describe, expect, it } from 'vitest';
9
+
10
+ import { PortAllocator } from '@serfab/cadre-provider';
11
+
12
+ import { childListenAddrs } from '../orchestrator/host-process-orchestrator.js';
13
+ import {
14
+ allocateNodePorts,
15
+ releaseNodePorts,
16
+ reserveNodePorts,
17
+ reusedNodePorts,
18
+ } from '../orchestrator/port-allocator.js';
19
+ import type { NodePorts } from '../orchestrator/types.js';
20
+
21
+ /**
22
+ * A port set as `state.json` hands it back from a build that predates the `ws` key:
23
+ * the type says every key is present, the data does not.
24
+ */
25
+ function portsWithoutWs(ports: Omit<NodePorts, 'ws'>): NodePorts {
26
+ return ports as NodePorts;
27
+ }
28
+
29
+ describe('allocateNodePorts', () => {
30
+ it('allocates the four ports in a fixed order', () => {
31
+ const a = new PortAllocator(10000, 10010);
32
+ expect(allocateNodePorts(a)).toEqual({ health: 10000, metrics: 10001, p2p: 10002, ws: 10003 });
33
+ });
34
+
35
+ // A partial set left reserved would leak from a bounded range on every failed
36
+ // provision — the whole reason this helper exists rather than four allocates.
37
+ it('is all-or-nothing: an exhausted range releases everything it took', () => {
38
+ const a = new PortAllocator(13000, 13002);
39
+ expect(() => allocateNodePorts(a)).toThrow(/No available ports/);
40
+ expect(a.has(13000)).toBe(false);
41
+ expect(a.has(13001)).toBe(false);
42
+ expect(a.has(13002)).toBe(false);
43
+ });
44
+
45
+ it('honours an override and reserves it', () => {
46
+ const a = new PortAllocator(10000, 10010);
47
+ expect(allocateNodePorts(a, { p2p: 10005 })).toEqual({
48
+ health: 10000,
49
+ metrics: 10001,
50
+ p2p: 10005,
51
+ ws: 10002,
52
+ });
53
+ // Reserved, so no later allocation can hand it out again.
54
+ expect(a.has(10005)).toBe(true);
55
+ });
56
+
57
+ // A re-spawn passes its previous set as overrides; every one must come back exactly.
58
+ it('returns a full set of overrides exactly, allocating nothing', () => {
59
+ const a = new PortAllocator(10000, 10010);
60
+ const previous = { health: 10006, metrics: 10007, p2p: 10008, ws: 10009 };
61
+ expect(allocateNodePorts(a, previous)).toEqual(previous);
62
+ expect(a.allocate()).toBe(10000);
63
+ });
64
+ });
65
+
66
+ describe('reserveNodePorts / releaseNodePorts', () => {
67
+ it('hold and then release every key of the set', () => {
68
+ const a = new PortAllocator(10000, 10010);
69
+ const ports = { health: 10000, metrics: 10001, p2p: 10002, ws: 10003 };
70
+ reserveNodePorts(a, ports);
71
+ for (const port of Object.values(ports)) expect(a.has(port)).toBe(true);
72
+ releaseNodePorts(a, ports);
73
+ for (const port of Object.values(ports)) expect(a.has(port)).toBe(false);
74
+ });
75
+
76
+ // Rehydrating, or dropping, a `state.json` handle written before `ws` existed.
77
+ it('skip a key the set lacks', () => {
78
+ const a = new PortAllocator(10000, 10010);
79
+ const ports = portsWithoutWs({ health: 10000, metrics: 10001, p2p: 10002 });
80
+ reserveNodePorts(a, ports);
81
+ expect(a.has(undefined as unknown as number)).toBe(false);
82
+ expect(a.allocate()).toBe(10003);
83
+ releaseNodePorts(a, ports);
84
+ expect(a.has(10000)).toBe(false);
85
+ expect(a.has(10003)).toBe(true);
86
+ });
87
+ });
88
+
89
+ describe('reusedNodePorts', () => {
90
+ const previous = { health: 10005, metrics: 10006, p2p: 10007, ws: 10008 };
91
+
92
+ it("returns the dropped handle's whole port set", () => {
93
+ expect(reusedNodePorts([{ ports: previous }])).toEqual(previous);
94
+ });
95
+
96
+ it('returns nothing when nothing was dropped, so every port is allocated fresh', () => {
97
+ expect(reusedNodePorts([])).toEqual({});
98
+ });
99
+
100
+ // A handle persisted before `ws` existed: its other ports come back, `ws` is new.
101
+ it('omits a key the dropped handle lacks', () => {
102
+ const legacy = portsWithoutWs({ health: 10005, metrics: 10006, p2p: 10007 });
103
+ const reused = reusedNodePorts([{ ports: legacy }]);
104
+ expect(reused).toEqual({ health: 10005, metrics: 10006, p2p: 10007 });
105
+ expect('ws' in reused).toBe(false);
106
+
107
+ const a = new PortAllocator(10000, 10010);
108
+ expect(allocateNodePorts(a, reused)).toEqual({ health: 10005, metrics: 10006, p2p: 10007, ws: 10000 });
109
+ });
110
+
111
+ it('takes the last handle when a state holding duplicates dropped several', () => {
112
+ const older = { health: 10000, metrics: 10001, p2p: 10002, ws: 10003 };
113
+ expect(reusedNodePorts([{ ports: older }, { ports: previous }])).toEqual(previous);
114
+ });
115
+ });
116
+
117
+ describe('childListenAddrs', () => {
118
+ it('names a TCP listener on the p2p port and a WebSocket listener on the ws port, on every interface', () => {
119
+ expect(childListenAddrs({ p2p: 10002, ws: 10004 })).toEqual([
120
+ '/ip4/0.0.0.0/tcp/10002',
121
+ '/ip4/0.0.0.0/tcp/10004/ws',
122
+ ]);
123
+ });
124
+ });
@@ -1,146 +1,138 @@
1
- /**
2
- * HostProcessOrchestrator push-credential injection tests.
3
- *
4
- * Verifies the orchestrator writes `config.push` into the spawned node's
5
- * cadre.json when the pushResolver yields credentials, omits it otherwise,
6
- * re-resolves on every (re-)spawn, and never persists a raw private key in
7
- * state.json. Uses a fake CLI entrypoint that just writes its startup token.
8
- */
9
-
10
- import { afterEach, beforeEach, describe, expect, it } from 'vitest';
11
- import { mkdtempSync, mkdirSync, rmSync, readFileSync, writeFileSync } from 'node:fs';
12
- import { tmpdir } from 'node:os';
13
- import { join } from 'node:path';
14
-
15
- import type { PushCredentials } from '@serfab/cadre-core';
16
- import { HostProcessOrchestrator, OWNER_CONTAINER_ID } from '../orchestrator/host-process-orchestrator.js';
17
- import { removeAllNodes } from './orchestrator-teardown.js';
18
-
19
- const FAKE_CLI = `
20
- import fs from 'node:fs';
21
- const args = process.argv.slice(2);
22
- const get = (flag) => { const i = args.indexOf(flag); return i >= 0 ? args[i + 1] : undefined; };
23
- const tokenPath = get('--startup-token-file');
24
- const token = process.env.CADRE_STARTUP_TOKEN ?? '';
25
- if (tokenPath && token) { try { fs.writeFileSync(tokenPath, token, 'utf8'); } catch (e) { console.error(e); } }
26
- process.on('SIGTERM', () => process.exit(0));
27
- setInterval(() => {}, 1 << 30);
28
- `;
29
-
30
- const FCM = { projectId: 'proj', clientEmail: 'svc@proj.iam', privateKey: 'FCM-SECRET-PEM' };
31
- const APNS = { keyId: 'KID', teamId: 'TEAM', bundleId: 'com.example.app', privateKey: 'APNS-SECRET-P8', production: false };
32
- const CFG = { identityPath: 'C:/fake/identity.key', partyId: 'install-id-123', libp2pPort: 4655 };
33
-
34
- let tmpRoot: string;
35
- let scriptPath: string;
36
- const orchestrators: HostProcessOrchestrator[] = [];
37
-
38
- beforeEach(() => {
39
- tmpRoot = mkdtempSync(join(tmpdir(), 'cadre-host-push-'));
40
- scriptPath = join(tmpRoot, 'fake-cli.mjs');
41
- writeFileSync(scriptPath, FAKE_CLI, 'utf8');
42
- });
43
-
44
- afterEach(async () => {
45
- try {
46
- await removeAllNodes(orchestrators);
47
- } finally {
48
- await sleep(50);
49
- try { rmSync(tmpRoot, { recursive: true, force: true }); } catch { /* ignore */ }
50
- }
51
- });
52
-
53
- function makeOrchestrator(rootDir: string, pushResolver?: () => Promise<PushCredentials | undefined>): HostProcessOrchestrator {
54
- mkdirSync(rootDir, { recursive: true });
55
- const orch = new HostProcessOrchestrator({
56
- rootDir,
57
- portRange: { start: 18000, end: 18499 },
58
- stopTimeoutMs: 1500,
59
- spawn: { entrypoint: scriptPath },
60
- ...(pushResolver ? { pushResolver } : {}),
61
- });
62
- orchestrators.push(orch);
63
- return orch;
64
- }
65
-
66
- function readChildConfig(rootDir: string, containerId = OWNER_CONTAINER_ID): Record<string, unknown> {
67
- const raw = readFileSync(join(rootDir, containerId, 'cadre.json'), 'utf8');
68
- return JSON.parse(raw) as Record<string, unknown>;
69
- }
70
-
71
- function sleep(ms: number): Promise<void> {
72
- return new Promise((res) => setTimeout(res, ms));
73
- }
74
-
75
- describe('HostProcessOrchestrator push injection', () => {
76
- it('writes config.push into cadre.json when the resolver yields credentials', async () => {
77
- const rootDir = join(tmpRoot, 'a');
78
- const orch = makeOrchestrator(rootDir, async () => ({ fcm: FCM, apns: APNS, cooldownMs: 1000 }));
79
- await orch.init();
80
- await orch.ensureOwnerNode(CFG);
81
-
82
- const cfg = readChildConfig(rootDir);
83
- expect(cfg.push).toEqual({ fcm: FCM, apns: APNS, cooldownMs: 1000 });
84
- });
85
-
86
- it('omits config.push when no resolver is configured', async () => {
87
- const rootDir = join(tmpRoot, 'b');
88
- const orch = makeOrchestrator(rootDir);
89
- await orch.init();
90
- await orch.ensureOwnerNode(CFG);
91
-
92
- const cfg = readChildConfig(rootDir);
93
- expect(cfg.push).toBeUndefined();
94
- });
95
-
96
- it('omits config.push when the resolver returns undefined (push opt-in)', async () => {
97
- const rootDir = join(tmpRoot, 'c');
98
- const orch = makeOrchestrator(rootDir, async () => undefined);
99
- await orch.init();
100
- await orch.ensureOwnerNode(CFG);
101
-
102
- expect(readChildConfig(rootDir).push).toBeUndefined();
103
- });
104
-
105
- it('degrades to no push (does not fail the spawn) when the resolver throws', async () => {
106
- const rootDir = join(tmpRoot, 'd');
107
- const orch = makeOrchestrator(rootDir, async () => { throw new Error('partial creds'); });
108
- await orch.init();
109
- // Spawn still succeeds.
110
- const node = await orch.ensureOwnerNode(CFG);
111
- expect(node.id).toBe(OWNER_CONTAINER_ID);
112
- expect(readChildConfig(rootDir).push).toBeUndefined();
113
- });
114
-
115
- it('re-resolves on re-spawn and never persists a raw private key in state.json', async () => {
116
- const rootDir = join(tmpRoot, 'e');
117
- let current: PushCredentials | undefined = { fcm: FCM };
118
- const orch = makeOrchestrator(rootDir, async () => current);
119
- await orch.init();
120
- await orch.ensureOwnerNode(CFG);
121
- expect(readChildConfig(rootDir).push).toEqual({ fcm: FCM });
122
-
123
- // state.json must not carry the raw key — only re-resolvable references.
124
- const stateRaw = readFileSync(join(rootDir, 'state.json'), 'utf8');
125
- expect(stateRaw).not.toContain('FCM-SECRET-PEM');
126
-
127
- // Rotate the resolver's answer, then restart — the new spawn must pick it up.
128
- current = { apns: APNS };
129
- await orch.restartOwnerNode();
130
- const cfg2 = readChildConfig(rootDir);
131
- expect(cfg2.push).toEqual({ apns: APNS });
132
- expect((cfg2.push as PushCredentials).fcm).toBeUndefined();
133
- });
134
-
135
- it('injects push for a storage-profile managed node but not a transaction node', async () => {
136
- const rootDir = join(tmpRoot, 'f');
137
- const orch = makeOrchestrator(rootDir, async () => ({ fcm: FCM }));
138
- await orch.init();
139
-
140
- await orch.createContainer({ containerId: 'storage-1', partyId: 'p', bootstrapNodes: [], profile: 'storage' });
141
- expect(readChildConfig(rootDir, 'storage-1').push).toEqual({ fcm: FCM });
142
-
143
- await orch.createContainer({ containerId: 'txn-1', partyId: 'p', bootstrapNodes: [], profile: 'transaction' });
144
- expect(readChildConfig(rootDir, 'txn-1').push).toBeUndefined();
145
- });
146
- });
1
+ /**
2
+ * HostProcessOrchestrator push-credential injection tests.
3
+ *
4
+ * Verifies the orchestrator writes `config.push` into the spawned node's
5
+ * cadre.json when the pushResolver yields credentials, omits it otherwise,
6
+ * re-resolves on every (re-)spawn, and never persists a raw private key in
7
+ * state.json. Uses a fake CLI entrypoint that just writes its startup token.
8
+ */
9
+
10
+ import { afterEach, beforeEach, describe, expect, it } from 'vitest';
11
+ import { mkdtempSync, mkdirSync, rmSync, readFileSync, writeFileSync } from 'node:fs';
12
+ import { tmpdir } from 'node:os';
13
+ import { join } from 'node:path';
14
+
15
+ import type { PushCredentials } from '@serfab/cadre-core';
16
+ import { HostProcessOrchestrator } from '../orchestrator/host-process-orchestrator.js';
17
+ import { removeAllNodes } from './orchestrator-teardown.js';
18
+
19
+ const FAKE_CLI = `
20
+ import fs from 'node:fs';
21
+ const args = process.argv.slice(2);
22
+ const get = (flag) => { const i = args.indexOf(flag); return i >= 0 ? args[i + 1] : undefined; };
23
+ const tokenPath = get('--startup-token-file');
24
+ const token = process.env.CADRE_STARTUP_TOKEN ?? '';
25
+ if (tokenPath && token) { try { fs.writeFileSync(tokenPath, token, 'utf8'); } catch (e) { console.error(e); } }
26
+ process.on('SIGTERM', () => process.exit(0));
27
+ setInterval(() => {}, 1 << 30);
28
+ `;
29
+
30
+ const FCM = { projectId: 'proj', clientEmail: 'svc@proj.iam', privateKey: 'FCM-SECRET-PEM' };
31
+ const APNS = { keyId: 'KID', teamId: 'TEAM', bundleId: 'com.example.app', privateKey: 'APNS-SECRET-P8', production: false };
32
+ /** A storage node — the one profile whose spawn carries push. */
33
+ const NODE = { containerId: 'n1', partyId: 'p', bootstrapNodes: [], profile: 'storage' as const };
34
+
35
+ let tmpRoot: string;
36
+ let scriptPath: string;
37
+ const orchestrators: HostProcessOrchestrator[] = [];
38
+
39
+ beforeEach(() => {
40
+ tmpRoot = mkdtempSync(join(tmpdir(), 'cadre-host-push-'));
41
+ scriptPath = join(tmpRoot, 'fake-cli.mjs');
42
+ writeFileSync(scriptPath, FAKE_CLI, 'utf8');
43
+ });
44
+
45
+ afterEach(async () => {
46
+ try {
47
+ await removeAllNodes(orchestrators);
48
+ } finally {
49
+ await sleep(50);
50
+ try { rmSync(tmpRoot, { recursive: true, force: true }); } catch { /* ignore */ }
51
+ }
52
+ });
53
+
54
+ function makeOrchestrator(rootDir: string, pushResolver?: () => Promise<PushCredentials | undefined>): HostProcessOrchestrator {
55
+ mkdirSync(rootDir, { recursive: true });
56
+ const orch = new HostProcessOrchestrator({
57
+ rootDir,
58
+ portRange: { start: 18000, end: 18499 },
59
+ stopTimeoutMs: 1500,
60
+ spawn: { entrypoint: scriptPath },
61
+ ...(pushResolver ? { pushResolver } : {}),
62
+ });
63
+ orchestrators.push(orch);
64
+ return orch;
65
+ }
66
+
67
+ function readChildConfig(rootDir: string, containerId = NODE.containerId): Record<string, unknown> {
68
+ const raw = readFileSync(join(rootDir, containerId, 'cadre.json'), 'utf8');
69
+ return JSON.parse(raw) as Record<string, unknown>;
70
+ }
71
+
72
+ function sleep(ms: number): Promise<void> {
73
+ return new Promise((res) => setTimeout(res, ms));
74
+ }
75
+
76
+ describe('HostProcessOrchestrator push injection', () => {
77
+ it('omits config.push when no resolver is configured', async () => {
78
+ const rootDir = join(tmpRoot, 'b');
79
+ const orch = makeOrchestrator(rootDir);
80
+ await orch.init();
81
+ await orch.createContainer(NODE);
82
+
83
+ const cfg = readChildConfig(rootDir);
84
+ expect(cfg.push).toBeUndefined();
85
+ });
86
+
87
+ it('omits config.push when the resolver returns undefined (push opt-in)', async () => {
88
+ const rootDir = join(tmpRoot, 'c');
89
+ const orch = makeOrchestrator(rootDir, async () => undefined);
90
+ await orch.init();
91
+ await orch.createContainer(NODE);
92
+
93
+ expect(readChildConfig(rootDir).push).toBeUndefined();
94
+ });
95
+
96
+ it('degrades to no push (does not fail the spawn) when the resolver throws', async () => {
97
+ const rootDir = join(tmpRoot, 'd');
98
+ const orch = makeOrchestrator(rootDir, async () => { throw new Error('partial creds'); });
99
+ await orch.init();
100
+ // Spawn still succeeds.
101
+ const created = await orch.createContainer(NODE);
102
+ expect(orch.getNode(created.dockerId)?.status).toBe('running');
103
+ expect(readChildConfig(rootDir).push).toBeUndefined();
104
+ });
105
+
106
+ it('re-resolves on re-spawn and never persists a raw private key in state.json', async () => {
107
+ const rootDir = join(tmpRoot, 'e');
108
+ let current: PushCredentials | undefined = { fcm: FCM };
109
+ const orch = makeOrchestrator(rootDir, async () => current);
110
+ await orch.init();
111
+ const first = await orch.createContainer(NODE);
112
+ expect(readChildConfig(rootDir).push).toEqual({ fcm: FCM });
113
+
114
+ // state.json must not carry the raw key — only re-resolvable references.
115
+ const stateRaw = readFileSync(join(rootDir, 'state.json'), 'utf8');
116
+ expect(stateRaw).not.toContain('FCM-SECRET-PEM');
117
+
118
+ // Rotate the resolver's answer, then re-spawn — the new spawn must pick it up.
119
+ current = { apns: APNS };
120
+ await orch.stopContainer(first.dockerId);
121
+ await orch.createContainer(NODE);
122
+ const cfg2 = readChildConfig(rootDir);
123
+ expect(cfg2.push).toEqual({ apns: APNS });
124
+ expect((cfg2.push as PushCredentials).fcm).toBeUndefined();
125
+ });
126
+
127
+ it('injects push for a storage-profile managed node but not a transaction node', async () => {
128
+ const rootDir = join(tmpRoot, 'f');
129
+ const orch = makeOrchestrator(rootDir, async () => ({ fcm: FCM, apns: APNS, cooldownMs: 1000 }));
130
+ await orch.init();
131
+
132
+ await orch.createContainer({ containerId: 'storage-1', partyId: 'p', bootstrapNodes: [], profile: 'storage' });
133
+ expect(readChildConfig(rootDir, 'storage-1').push).toEqual({ fcm: FCM, apns: APNS, cooldownMs: 1000 });
134
+
135
+ await orch.createContainer({ containerId: 'txn-1', partyId: 'p', bootstrapNodes: [], profile: 'transaction' });
136
+ expect(readChildConfig(rootDir, 'txn-1').push).toBeUndefined();
137
+ });
138
+ });