@serfab/cadre-host 1.13.0 → 1.15.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 (352) hide show
  1. package/README.md +347 -423
  2. package/dist/bin/host.d.ts +6 -3
  3. package/dist/bin/host.js +332 -524
  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 +54 -0
  9. package/dist/bin/nat-output.js +138 -0
  10. package/dist/bin/nat-output.js.map +1 -0
  11. package/dist/bin/ui-access.d.ts +22 -0
  12. package/dist/bin/ui-access.js +44 -0
  13. package/dist/bin/ui-access.js.map +1 -0
  14. package/dist/hosted/claim-details.d.ts +66 -0
  15. package/dist/hosted/claim-details.js +60 -0
  16. package/dist/hosted/claim-details.js.map +1 -0
  17. package/dist/hosted/hosted-node-service.d.ts +325 -0
  18. package/dist/hosted/hosted-node-service.js +678 -0
  19. package/dist/hosted/hosted-node-service.js.map +1 -0
  20. package/dist/hosted/hosted-node-store.d.ts +40 -0
  21. package/dist/hosted/hosted-node-store.js +126 -0
  22. package/dist/hosted/hosted-node-store.js.map +1 -0
  23. package/dist/{donation/donation-supervisor.d.ts → hosted/hosted-node-supervisor.d.ts} +70 -53
  24. package/dist/hosted/hosted-node-supervisor.js +396 -0
  25. package/dist/hosted/hosted-node-supervisor.js.map +1 -0
  26. package/dist/hosted/hosted-node-watcher.d.ts +95 -0
  27. package/dist/hosted/hosted-node-watcher.js +260 -0
  28. package/dist/hosted/hosted-node-watcher.js.map +1 -0
  29. package/dist/hosted/index.d.ts +16 -0
  30. package/dist/hosted/index.js +13 -0
  31. package/dist/hosted/index.js.map +1 -0
  32. package/dist/hosted/node-status.d.ts +51 -0
  33. package/dist/hosted/node-status.js +77 -0
  34. package/dist/hosted/node-status.js.map +1 -0
  35. package/dist/hosted/types.d.ts +141 -0
  36. package/dist/hosted/types.js +22 -0
  37. package/dist/hosted/types.js.map +1 -0
  38. package/dist/index.d.ts +12 -24
  39. package/dist/index.js +9 -19
  40. package/dist/index.js.map +1 -1
  41. package/dist/installer/config.d.ts +27 -34
  42. package/dist/installer/config.js +46 -15
  43. package/dist/installer/config.js.map +1 -1
  44. package/dist/installer/index.d.ts +9 -19
  45. package/dist/installer/index.js +21 -85
  46. package/dist/installer/index.js.map +1 -1
  47. package/dist/installer/paths.d.ts +0 -2
  48. package/dist/installer/paths.js +0 -4
  49. package/dist/installer/paths.js.map +1 -1
  50. package/dist/installer/wizard.d.ts +12 -9
  51. package/dist/installer/wizard.js +15 -14
  52. package/dist/installer/wizard.js.map +1 -1
  53. package/dist/nat/address-resolver.d.ts +18 -17
  54. package/dist/nat/address-resolver.js +79 -25
  55. package/dist/nat/address-resolver.js.map +1 -1
  56. package/dist/nat/address-watch.d.ts +37 -0
  57. package/dist/nat/address-watch.js +84 -0
  58. package/dist/nat/address-watch.js.map +1 -0
  59. package/dist/nat/ddns/duckdns.d.ts +2 -1
  60. package/dist/nat/ddns/duckdns.js +5 -1
  61. package/dist/nat/ddns/duckdns.js.map +1 -1
  62. package/dist/nat/ddns/index.d.ts +6 -0
  63. package/dist/nat/ddns/index.js.map +1 -1
  64. package/dist/nat/index.d.ts +13 -9
  65. package/dist/nat/index.js +7 -5
  66. package/dist/nat/index.js.map +1 -1
  67. package/dist/nat/nat-service.d.ts +220 -77
  68. package/dist/nat/nat-service.js +653 -239
  69. package/dist/nat/nat-service.js.map +1 -1
  70. package/dist/nat/nat-store.d.ts +12 -5
  71. package/dist/nat/nat-store.js +124 -29
  72. package/dist/nat/nat-store.js.map +1 -1
  73. package/dist/nat/network-mode.d.ts +19 -0
  74. package/dist/nat/network-mode.js +32 -0
  75. package/dist/nat/network-mode.js.map +1 -0
  76. package/dist/nat/port-mapper.d.ts +83 -79
  77. package/dist/nat/port-mapper.js +148 -189
  78. package/dist/nat/port-mapper.js.map +1 -1
  79. package/dist/nat/reachability.d.ts +32 -15
  80. package/dist/nat/reachability.js +110 -22
  81. package/dist/nat/reachability.js.map +1 -1
  82. package/dist/nat/types.d.ts +98 -27
  83. package/dist/nat/types.js +8 -5
  84. package/dist/nat/types.js.map +1 -1
  85. package/dist/orchestrator/host-process-orchestrator.d.ts +32 -74
  86. package/dist/orchestrator/host-process-orchestrator.js +84 -233
  87. package/dist/orchestrator/host-process-orchestrator.js.map +1 -1
  88. package/dist/{installer/identity.d.ts → orchestrator/identity-file.d.ts} +5 -5
  89. package/dist/{installer/identity.js → orchestrator/identity-file.js} +6 -6
  90. package/dist/orchestrator/identity-file.js.map +1 -0
  91. package/dist/orchestrator/index.d.ts +2 -3
  92. package/dist/orchestrator/index.js +1 -1
  93. package/dist/orchestrator/index.js.map +1 -1
  94. package/dist/orchestrator/node-identity.d.ts +6 -6
  95. package/dist/orchestrator/node-identity.js +7 -7
  96. package/dist/orchestrator/node-identity.js.map +1 -1
  97. package/dist/orchestrator/port-allocator.d.ts +5 -6
  98. package/dist/orchestrator/port-allocator.js +9 -11
  99. package/dist/orchestrator/port-allocator.js.map +1 -1
  100. package/dist/orchestrator/state-store.d.ts +3 -5
  101. package/dist/orchestrator/state-store.js +2 -10
  102. package/dist/orchestrator/state-store.js.map +1 -1
  103. package/dist/orchestrator/types.d.ts +33 -22
  104. package/dist/orchestrator/types.js.map +1 -1
  105. package/dist/package-version.d.ts +1 -0
  106. package/dist/package-version.js +4 -0
  107. package/dist/package-version.js.map +1 -0
  108. package/dist/server/error-handler.d.ts +5 -25
  109. package/dist/server/error-handler.js +11 -74
  110. package/dist/server/error-handler.js.map +1 -1
  111. package/dist/server/events/types.d.ts +7 -11
  112. package/dist/server/events/types.js +2 -1
  113. package/dist/server/events/types.js.map +1 -1
  114. package/dist/server/index.d.ts +11 -37
  115. package/dist/server/index.js +30 -48
  116. package/dist/server/index.js.map +1 -1
  117. package/dist/server/routes/hosted-nodes.d.ts +28 -0
  118. package/dist/server/routes/hosted-nodes.js +68 -0
  119. package/dist/server/routes/hosted-nodes.js.map +1 -0
  120. package/dist/server/routes/nat.d.ts +4 -1
  121. package/dist/server/routes/nat.js +13 -2
  122. package/dist/server/routes/nat.js.map +1 -1
  123. package/dist/server/routes/nodes.d.ts +6 -20
  124. package/dist/server/routes/nodes.js +7 -86
  125. package/dist/server/routes/nodes.js.map +1 -1
  126. package/dist/server/routes/settings.d.ts +3 -7
  127. package/dist/server/routes/settings.js +18 -8
  128. package/dist/server/routes/settings.js.map +1 -1
  129. package/dist/server/routes/status.d.ts +2 -25
  130. package/dist/server/routes/status.js +3 -43
  131. package/dist/server/routes/status.js.map +1 -1
  132. package/dist/server/server.js +8 -8
  133. package/dist/server/settings-store.d.ts +2 -2
  134. package/dist/server/settings-store.js +2 -2
  135. package/dist/server/static.js +1 -2
  136. package/dist/server/static.js.map +1 -1
  137. package/dist/ui/assets/index-BHd8CWZA.css +1 -0
  138. package/dist/ui/assets/index-Ccr2GjPT.js +23 -0
  139. package/dist/ui/assets/index-Ccr2GjPT.js.map +1 -0
  140. package/dist/ui/index.html +2 -2
  141. package/dist/update/index.d.ts +2 -2
  142. package/dist/update/index.js +2 -2
  143. package/dist/update/store.d.ts +1 -1
  144. package/dist/update/store.js +1 -1
  145. package/dist/update/types.d.ts +1 -1
  146. package/dist/update/types.js +1 -1
  147. package/package.json +5 -7
  148. package/src/__tests__/cli-nat.smoke.test.ts +219 -212
  149. package/src/__tests__/cli.smoke.test.ts +2 -2
  150. package/src/__tests__/global-setup.ts +2 -3
  151. package/src/__tests__/orchestrator-claim-secret.test.ts +111 -0
  152. package/src/__tests__/orchestrator-env-scrub.test.ts +160 -163
  153. package/src/__tests__/orchestrator-handle-contract.ts +134 -134
  154. package/src/__tests__/orchestrator-node-identity.test.ts +177 -203
  155. package/src/__tests__/orchestrator-ports.test.ts +124 -143
  156. package/src/__tests__/orchestrator-push.test.ts +138 -146
  157. package/src/__tests__/orchestrator.test.ts +1017 -1063
  158. package/src/__tests__/ui-access.test.ts +30 -0
  159. package/src/bin/host.ts +397 -572
  160. package/src/bin/join-output.ts +120 -0
  161. package/src/bin/nat-output.ts +188 -0
  162. package/src/bin/ui-access.ts +50 -0
  163. package/src/{donation → hosted}/__tests__/fake-orchestrator.test.ts +172 -173
  164. package/src/{donation → hosted}/__tests__/fake-orchestrator.ts +43 -23
  165. package/src/hosted/__tests__/hosted-node-service.test.ts +708 -0
  166. package/src/hosted/__tests__/hosted-node-supervisor.test.ts +475 -0
  167. package/src/hosted/__tests__/test-invitation.ts +33 -0
  168. package/src/hosted/claim-details.ts +103 -0
  169. package/src/hosted/hosted-node-service.ts +814 -0
  170. package/src/hosted/hosted-node-store.ts +144 -0
  171. package/src/hosted/hosted-node-supervisor.ts +439 -0
  172. package/src/hosted/hosted-node-watcher.ts +288 -0
  173. package/src/hosted/index.ts +53 -0
  174. package/src/hosted/node-status.ts +97 -0
  175. package/src/hosted/types.ts +160 -0
  176. package/src/index.ts +72 -91
  177. package/src/installer/__tests__/config.test.ts +15 -40
  178. package/src/installer/__tests__/installer.smoke.test.ts +184 -263
  179. package/src/installer/__tests__/wizard.test.ts +25 -13
  180. package/src/installer/config.ts +201 -175
  181. package/src/installer/index.ts +252 -327
  182. package/src/installer/paths.ts +104 -109
  183. package/src/installer/wizard.ts +24 -26
  184. package/src/nat/__tests__/address-resolver.test.ts +107 -82
  185. package/src/nat/__tests__/nat-service.test.ts +608 -515
  186. package/src/nat/__tests__/nat-store.test.ts +49 -12
  187. package/src/nat/__tests__/network-mode.test.ts +38 -0
  188. package/src/nat/__tests__/port-mapper.test.ts +30 -191
  189. package/src/nat/__tests__/reachability.test.ts +96 -13
  190. package/src/nat/address-resolver.ts +94 -52
  191. package/src/nat/address-watch.ts +92 -0
  192. package/src/nat/ddns/duckdns.ts +5 -1
  193. package/src/nat/ddns/index.ts +6 -0
  194. package/src/nat/index.ts +33 -13
  195. package/src/nat/nat-service.ts +942 -501
  196. package/src/nat/nat-store.ts +126 -31
  197. package/src/nat/network-mode.ts +36 -0
  198. package/src/nat/port-mapper.ts +241 -329
  199. package/src/nat/reachability.ts +155 -34
  200. package/src/nat/types.ts +110 -37
  201. package/src/{installer/__tests__/identity.test.ts → orchestrator/__tests__/identity-file.test.ts} +34 -34
  202. package/src/orchestrator/__tests__/node-identity.test.ts +62 -62
  203. package/src/orchestrator/host-process-orchestrator.ts +1089 -1260
  204. package/src/{installer/identity.ts → orchestrator/identity-file.ts} +56 -56
  205. package/src/orchestrator/index.ts +9 -10
  206. package/src/orchestrator/node-identity.ts +41 -41
  207. package/src/orchestrator/port-allocator.ts +71 -73
  208. package/src/orchestrator/state-store.ts +76 -90
  209. package/src/orchestrator/types.ts +34 -23
  210. package/src/package-version.ts +4 -0
  211. package/src/server/__tests__/bus.test.ts +38 -38
  212. package/src/server/__tests__/error-handler.test.ts +81 -118
  213. package/src/server/__tests__/fakes.ts +15 -18
  214. package/src/server/__tests__/hosted-nodes-route.test.ts +161 -0
  215. package/src/server/__tests__/nodes-route.test.ts +114 -283
  216. package/src/server/__tests__/publishers.test.ts +193 -180
  217. package/src/server/__tests__/server.smoke.test.ts +134 -223
  218. package/src/server/__tests__/settings-route.test.ts +210 -195
  219. package/src/server/__tests__/sse-route.test.ts +119 -119
  220. package/src/server/__tests__/status-route.test.ts +82 -121
  221. package/src/server/error-handler.ts +112 -178
  222. package/src/server/events/types.ts +20 -24
  223. package/src/server/index.ts +223 -267
  224. package/src/server/routes/hosted-nodes.ts +83 -0
  225. package/src/server/routes/nat.ts +75 -63
  226. package/src/server/routes/nodes.ts +96 -188
  227. package/src/server/routes/settings.ts +20 -15
  228. package/src/server/routes/status.ts +5 -67
  229. package/src/server/server.ts +8 -8
  230. package/src/server/settings-store.ts +2 -2
  231. package/src/server/static.ts +1 -2
  232. package/src/update/index.ts +2 -2
  233. package/src/update/store.ts +1 -1
  234. package/src/update/types.ts +1 -1
  235. package/dist/auth/duration.d.ts +0 -1
  236. package/dist/auth/duration.js +0 -32
  237. package/dist/auth/duration.js.map +0 -1
  238. package/dist/auth/index.d.ts +0 -11
  239. package/dist/auth/index.js +0 -9
  240. package/dist/auth/index.js.map +0 -1
  241. package/dist/auth/self-label.d.ts +0 -36
  242. package/dist/auth/self-label.js +0 -57
  243. package/dist/auth/self-label.js.map +0 -1
  244. package/dist/auth/trust-circle-store.d.ts +0 -26
  245. package/dist/auth/trust-circle-store.js +0 -129
  246. package/dist/auth/trust-circle-store.js.map +0 -1
  247. package/dist/auth/trust-circle.d.ts +0 -156
  248. package/dist/auth/trust-circle.js +0 -307
  249. package/dist/auth/trust-circle.js.map +0 -1
  250. package/dist/auth/types.d.ts +0 -84
  251. package/dist/auth/types.js +0 -24
  252. package/dist/auth/types.js.map +0 -1
  253. package/dist/donation/donation-service.d.ts +0 -389
  254. package/dist/donation/donation-service.js +0 -828
  255. package/dist/donation/donation-service.js.map +0 -1
  256. package/dist/donation/donation-store.d.ts +0 -51
  257. package/dist/donation/donation-store.js +0 -152
  258. package/dist/donation/donation-store.js.map +0 -1
  259. package/dist/donation/donation-supervisor.js +0 -364
  260. package/dist/donation/donation-supervisor.js.map +0 -1
  261. package/dist/donation/grant-service.d.ts +0 -80
  262. package/dist/donation/grant-service.js +0 -210
  263. package/dist/donation/grant-service.js.map +0 -1
  264. package/dist/donation/grant-store.d.ts +0 -38
  265. package/dist/donation/grant-store.js +0 -135
  266. package/dist/donation/grant-store.js.map +0 -1
  267. package/dist/donation/index.d.ts +0 -20
  268. package/dist/donation/index.js +0 -17
  269. package/dist/donation/index.js.map +0 -1
  270. package/dist/donation/types.d.ts +0 -224
  271. package/dist/donation/types.js +0 -40
  272. package/dist/donation/types.js.map +0 -1
  273. package/dist/installer/identity.js.map +0 -1
  274. package/dist/owner/index.d.ts +0 -11
  275. package/dist/owner/index.js +0 -11
  276. package/dist/owner/index.js.map +0 -1
  277. package/dist/owner/owner-node-client.d.ts +0 -110
  278. package/dist/owner/owner-node-client.js +0 -184
  279. package/dist/owner/owner-node-client.js.map +0 -1
  280. package/dist/server/routes/bootstrap-node-validation.d.ts +0 -115
  281. package/dist/server/routes/bootstrap-node-validation.js +0 -197
  282. package/dist/server/routes/bootstrap-node-validation.js.map +0 -1
  283. package/dist/server/routes/grants-admin.d.ts +0 -29
  284. package/dist/server/routes/grants-admin.js +0 -58
  285. package/dist/server/routes/grants-admin.js.map +0 -1
  286. package/dist/server/routes/grants.d.ts +0 -28
  287. package/dist/server/routes/grants.js +0 -165
  288. package/dist/server/routes/grants.js.map +0 -1
  289. package/dist/server/routes/provision-request-validation.d.ts +0 -45
  290. package/dist/server/routes/provision-request-validation.js +0 -97
  291. package/dist/server/routes/provision-request-validation.js.map +0 -1
  292. package/dist/server/routes/strands.d.ts +0 -20
  293. package/dist/server/routes/strands.js +0 -38
  294. package/dist/server/routes/strands.js.map +0 -1
  295. package/dist/server/routes/trust-circle.d.ts +0 -17
  296. package/dist/server/routes/trust-circle.js +0 -35
  297. package/dist/server/routes/trust-circle.js.map +0 -1
  298. package/dist/strands/index.d.ts +0 -7
  299. package/dist/strands/index.js +0 -6
  300. package/dist/strands/index.js.map +0 -1
  301. package/dist/strands/strand-service.d.ts +0 -50
  302. package/dist/strands/strand-service.js +0 -103
  303. package/dist/strands/strand-service.js.map +0 -1
  304. package/dist/strands/types.d.ts +0 -93
  305. package/dist/strands/types.js +0 -23
  306. package/dist/strands/types.js.map +0 -1
  307. package/dist/ui/assets/index-C6nELNfC.js +0 -23
  308. package/dist/ui/assets/index-C6nELNfC.js.map +0 -1
  309. package/dist/ui/assets/index-TtIYSeZO.css +0 -1
  310. package/src/__tests__/cli-invite.smoke.test.ts +0 -192
  311. package/src/__tests__/host-owner.smoke.test.ts +0 -123
  312. package/src/__tests__/orchestrator-owner.test.ts +0 -234
  313. package/src/__tests__/orchestrator-pin-keys.test.ts +0 -152
  314. package/src/auth/__tests__/duration.test.ts +0 -28
  315. package/src/auth/__tests__/self-label.test.ts +0 -85
  316. package/src/auth/__tests__/trust-circle-integration.test.ts +0 -152
  317. package/src/auth/__tests__/trust-circle-store.test.ts +0 -115
  318. package/src/auth/__tests__/trust-circle.test.ts +0 -445
  319. package/src/auth/duration.ts +0 -33
  320. package/src/auth/index.ts +0 -21
  321. package/src/auth/self-label.ts +0 -74
  322. package/src/auth/trust-circle-store.ts +0 -155
  323. package/src/auth/trust-circle.ts +0 -395
  324. package/src/auth/types.ts +0 -99
  325. package/src/donation/__tests__/donation-service.test.ts +0 -1193
  326. package/src/donation/__tests__/donation-supervisor.test.ts +0 -548
  327. package/src/donation/__tests__/grant-service.test.ts +0 -213
  328. package/src/donation/__tests__/grant-store.test.ts +0 -113
  329. package/src/donation/donation-service.ts +0 -1033
  330. package/src/donation/donation-store.ts +0 -176
  331. package/src/donation/donation-supervisor.ts +0 -411
  332. package/src/donation/grant-service.ts +0 -252
  333. package/src/donation/grant-store.ts +0 -152
  334. package/src/donation/index.ts +0 -65
  335. package/src/donation/types.ts +0 -260
  336. package/src/owner/__tests__/owner-node-client.test.ts +0 -194
  337. package/src/owner/index.ts +0 -12
  338. package/src/owner/owner-node-client.ts +0 -259
  339. package/src/server/__tests__/bootstrap-node-validation.test.ts +0 -138
  340. package/src/server/__tests__/grants-admin-route.test.ts +0 -171
  341. package/src/server/__tests__/grants-route.test.ts +0 -394
  342. package/src/server/__tests__/strands-route.test.ts +0 -234
  343. package/src/server/routes/bootstrap-node-validation.ts +0 -206
  344. package/src/server/routes/grants-admin.ts +0 -74
  345. package/src/server/routes/grants.ts +0 -193
  346. package/src/server/routes/provision-request-validation.ts +0 -105
  347. package/src/server/routes/strands.ts +0 -54
  348. package/src/server/routes/trust-circle.ts +0 -49
  349. package/src/strands/__tests__/strand-service.test.ts +0 -165
  350. package/src/strands/index.ts +0 -14
  351. package/src/strands/strand-service.ts +0 -135
  352. package/src/strands/types.ts +0 -102
@@ -47,6 +47,18 @@ function scrubbedParentEnv() {
47
47
  }
48
48
  return env;
49
49
  }
50
+ /**
51
+ * The env carrying how the child gets into its cadre, and nowhere else (see
52
+ * `HostedSpawnRequest`): `CADRE_CLAIM_SECRET` makes `cadre-cli start` wait to be
53
+ * claimed, `CADRE_INVITATION` makes it redeem the invitation once it is up.
54
+ */
55
+ function joinEnv(request) {
56
+ if (request.claimSecret)
57
+ return { CADRE_CLAIM_SECRET: request.claimSecret };
58
+ if (request.invitation)
59
+ return { CADRE_INVITATION: request.invitation };
60
+ return undefined;
61
+ }
50
62
  /**
51
63
  * The libp2p listen addresses a managed child binds — its `CADRE_LISTEN_ADDRS`, one
52
64
  * entry per element: TCP on the `p2p` port and WebSocket on the `ws` port, both on
@@ -65,22 +77,17 @@ export function childListenAddrs(ports) {
65
77
  /**
66
78
  * Every TCP port a managed child binds, on the address it binds it to: health and
67
79
  * metrics (`cadre-cli`'s `HealthServer`) and the libp2p TCP and WebSocket listeners
68
- * ({@link childListenAddrs}) on every interface, plus the loopback admin channel —
69
- * for the owner node only, the one child started with `--admin-port`. A key the
70
- * set lacks (a handle persisted by an older build) is skipped.
80
+ * ({@link childListenAddrs}), all on every interface. A key the set lacks (a handle
81
+ * persisted by an older build) is skipped.
71
82
  */
72
- function childPortBindings(ports, owner) {
83
+ function childPortBindings(ports) {
73
84
  const bindings = [];
74
85
  for (const port of [ports.health, ports.metrics, ports.p2p, ports.ws]) {
75
86
  if (port !== undefined)
76
87
  bindings.push({ port, host: '0.0.0.0' });
77
88
  }
78
- if (owner && ports.admin !== undefined)
79
- bindings.push({ port: ports.admin, host: '127.0.0.1' });
80
89
  return bindings;
81
90
  }
82
- /** Fixed friendly id for the admin's owner node. */
83
- export const OWNER_CONTAINER_ID = 'owner';
84
91
  /**
85
92
  * Runs cadre nodes as plain Node child processes on the host.
86
93
  *
@@ -127,6 +134,7 @@ export class HostProcessOrchestrator {
127
134
  this.defaultMemoryLimit = cfg.defaultResources?.memoryLimit;
128
135
  this.cliEntrypoint = cfg.spawn?.entrypoint ?? resolveCadreCliBin();
129
136
  this.pushResolver = cfg.pushResolver;
137
+ this.announceAddrs = cfg.announceAddrs;
130
138
  log('HostProcessOrchestrator rootDir=%s cli=%s', this.rootDir, this.cliEntrypoint);
131
139
  }
132
140
  /**
@@ -136,7 +144,6 @@ export class HostProcessOrchestrator {
136
144
  */
137
145
  async init() {
138
146
  const state = this.stateStore.load();
139
- this.ownerConfig = state.ownerConfig;
140
147
  for (const persisted of state.handles) {
141
148
  const handle = {
142
149
  containerId: persisted.containerId,
@@ -148,7 +155,7 @@ export class HostProcessOrchestrator {
148
155
  spawnedAt: persisted.spawnedAt,
149
156
  partyId: persisted.partyId,
150
157
  profile: persisted.profile,
151
- ...(persisted.owner ? { owner: true } : {}),
158
+ announcedAddrs: persisted.announcedAddrs ?? [],
152
159
  alive: false,
153
160
  };
154
161
  handle.alive = isHandleLive(handle);
@@ -194,25 +201,32 @@ export class HostProcessOrchestrator {
194
201
  }
195
202
  /**
196
203
  * Register a state-change listener. Invoked when a handle's `alive`
197
- * transitions (child exit, manual stop). Returns an unsubscribe fn.
204
+ * transitions (create, child exit, manual stop) and once more, as `stopped`,
205
+ * when a handle is removed. Returns an unsubscribe fn.
198
206
  */
199
207
  onStateChange(listener) {
200
208
  this.stateListeners.add(listener);
201
209
  return () => { this.stateListeners.delete(listener); };
202
210
  }
203
211
  /**
204
- * Spawn a generic managed node — the donated-node path (the requester's cadre,
205
- * pinned to the *requester's* owner key). The node is given its own protobuf
206
- * identity key inside its workdir (`ensureNodeIdentity`, reused across
212
+ * Spawn a managed node — a hosted node for some cadre, started waiting to be
213
+ * claimed when the request carries a `claimSecret`, or redeeming the cadre
214
+ * invitation it carries as `invitation`. The node is given its own
215
+ * protobuf identity key inside its workdir (`ensureNodeIdentity`, reused across
207
216
  * re-spawns of the same containerId), which is what makes its peer id stable
208
217
  * across restarts AND what makes its node-local stores durable: `cadre-cli
209
218
  * start` opens the file-backed bootstrap-peer, trusted-owner and strand
210
219
  * network-state stores in the node's state directory, which defaults to the
211
220
  * directory holding the `cadre.json` written here — the same workdir.
212
- * Terminating the loan (`removeContainer`) deletes the workdir, so the key and
221
+ * Removing the node (`removeContainer`) deletes the workdir, so the key and
213
222
  * every store go with it.
214
223
  */
215
224
  async createContainer(request) {
225
+ // Before anything is allocated: `cadre-cli start` refuses the pair anyway, and a child
226
+ // that exits at once would only surface as a crash loop.
227
+ if (request.claimSecret && request.invitation) {
228
+ throw new Error(`container ${request.containerId}: a spawn carries a claim secret or an invitation, not both`);
229
+ }
216
230
  const workdir = this.workdirFor(request.containerId);
217
231
  // Sampled BEFORE `ensureNodeIdentity`, which brings the directory into
218
232
  // existence as a side effect of writing the key. Only a spawn that found
@@ -233,25 +247,14 @@ export class HostProcessOrchestrator {
233
247
  // node's worth of ports from a bounded range on every failed provision attempt.
234
248
  const identity = await ensureNodeIdentity(workdir);
235
249
  log('container %s identity peerId=%s', request.containerId, identity.peerId);
236
- // A node started with pinned owner keys belongs to a FOREIGN cadre (the
237
- // node-donation flow: it trusts the requester's owner key, not the host's).
238
- // The host's FCM/APNs credentials are minted for the host owner's own app
239
- // and are meaningless to a foreign cadre — so donated nodes get NO push
240
- // block. (Per-grantee push creds would be a future ticket; none in v1.)
241
- const foreignParty = (request.pinnedOwnerKeys?.length ?? 0) > 0;
242
250
  // Only storage-profile nodes participate in strands and thus fan out push
243
251
  // wakes — a transaction-only node need not carry credentials. Resolved
244
252
  // BEFORE the drop below, which keeps the drop → launch window synchronous
245
253
  // (see restoreDroppedHandles).
246
- const push = request.profile === 'storage' && !foreignParty ? await this.resolvePush() : undefined;
247
- // Pinned owner key(s) reach the child via CADRE_OWNER_KEYS (comma-separated);
248
- // `cadre-cli start` unions it into its cold-start pinnedKeyTrustPolicy so the
249
- // node will accept the foreign-authority-signed seed presented via POST /seed.
250
- const extraEnv = request.pinnedOwnerKeys?.length
251
- ? { CADRE_OWNER_KEYS: request.pinnedOwnerKeys.join(',') }
252
- : undefined;
254
+ const push = request.profile === 'storage' ? await this.resolvePush() : undefined;
255
+ const extraEnv = joinEnv(request);
253
256
  // The last `await` on this path: checked as close to the drop as it can be.
254
- await this.refuseRespawnOverLiveChild(request.containerId, {}, false);
257
+ await this.refuseRespawnOverLiveChild(request.containerId);
255
258
  // Nothing from here to the launch is `await`ed — which is what keeps
256
259
  // `restoreDroppedHandles`'s documented precondition true even though the
257
260
  // `try` now opens further up. Do not add an `await` past this line.
@@ -266,7 +269,6 @@ export class HostProcessOrchestrator {
266
269
  partyId: request.partyId,
267
270
  profile: request.profile,
268
271
  ports,
269
- owner: false,
270
272
  buildConfig: () => this.buildChildConfig(request, workdir, push),
271
273
  extraArgs: ['--identity-file', identity.path],
272
274
  ...(extraEnv ? { extraEnv } : {}),
@@ -294,7 +296,7 @@ export class HostProcessOrchestrator {
294
296
  }
295
297
  /**
296
298
  * Remove the working directory a spawn created for `containerId` when no
297
- * handle owns it — the cleanup of last resort for a donation record that
299
+ * handle owns it — the cleanup of last resort for a hosted-node record that
298
300
  * never got a `dockerId` (the host died between `ensureNodeIdentity` and the
299
301
  * handle write, so `createContainer`'s own unwind never ran). Returns whether
300
302
  * anything was removed.
@@ -303,17 +305,12 @@ export class HostProcessOrchestrator {
303
305
  * to await.
304
306
  */
305
307
  reclaimWorkdir(containerId) {
306
- // The admin's own node is never reclaimed by this route: its workdir is a
307
- // single fixed path reused by every restart and it holds the host's own
308
- // control-DB storage.
309
- if (containerId === OWNER_CONTAINER_ID)
310
- return false;
311
308
  // A live handle owns that directory, and `removeContainer` — which stops
312
309
  // the child first — is the only thing allowed to delete it.
313
310
  if (this.resolveDockerId(containerId))
314
311
  return false;
315
312
  const workdir = this.workdirFor(containerId);
316
- // Defence, not a live case: donation ids are `grn_<base64url>` and so can
313
+ // Defence, not a live case: hosted-node ids are `hn_<base64url>` and so can
317
314
  // hold neither a path separator nor a dot. A caller that ever passes
318
315
  // something else must not be able to walk out of `rootDir`.
319
316
  if (!workdir.startsWith(this.rootDir + sep)) {
@@ -376,120 +373,27 @@ export class HostProcessOrchestrator {
376
373
  }
377
374
  }
378
375
  /**
379
- * Ensure the admin's owner node is running, spawning it if absent or
380
- * dead. Idempotent: a second call with the node already alive returns the
381
- * existing handle without launching a second child. The spawn parameters
382
- * are persisted so a later `/api/nodes/:id/{start,restart}` (or an
383
- * orchestrator restart) can re-spawn it without re-supplying them.
384
- *
385
- * The owner node is spawned as `cadre-cli start --owner
386
- * --admin-port <p> --identity-file <identityPath>` so it carries the
387
- * host's libp2p identity, founds/joins the control network, and exposes the
388
- * loopback admin channel the manager delegates to.
376
+ * The public addresses the node about to spawn announces. A hook that throws costs
377
+ * the node its public addresses, never its start: it still binds and is reachable
378
+ * on its LAN addresses, and `NatService` restarts it once the addresses it should
379
+ * announce differ from these.
389
380
  */
390
- async ensureOwnerNode(cfg) {
391
- const config = cfg ?? this.ownerConfig;
392
- if (!config) {
393
- throw new Error('ensureOwnerNode: no owner config supplied and none persisted');
394
- }
395
- this.ownerConfig = config;
396
- const existing = this.findOwnerHandle();
397
- if (existing && isHandleLive(existing)) {
398
- this.persist();
399
- return toNodeInfo(existing);
400
- }
401
- const profile = config.profile ?? 'storage';
402
- // Re-resolve push credentials from the secret store on every (re-)spawn so a
403
- // restart picks up rotated keys and nothing raw is replayed from state.json.
404
- // Resolved BEFORE the drop below so the drop → launch window stays
405
- // synchronous (see restoreDroppedHandles).
406
- const push = await this.resolvePush();
407
- // The last `await` before the drop — same guard as in createContainer.
408
- await this.refuseRespawnOverLiveChild(OWNER_CONTAINER_ID, { p2p: config.libp2pPort }, true);
409
- // NOTE: the short-circuit above matches via `findOwnerHandle` (owner flag OR
410
- // containerId), this drop only by containerId. Equivalent today — every
411
- // owner handle is launched with OWNER_CONTAINER_ID. If an owner spawn ever
412
- // takes a different containerId, the two would disagree and the handle the
413
- // short-circuit found would survive here, leaking it and its ports.
414
- const dropped = this.dropStaleHandle(OWNER_CONTAINER_ID);
415
- let ports;
381
+ announceAddrsFor(containerId, ports) {
382
+ if (!this.announceAddrs)
383
+ return [];
416
384
  try {
417
- // The owner node must listen on the configured libp2p port so the NAT
418
- // mapping (external → internal) lands on it — hence the p2p override, which
419
- // wins over the dropped handle's. Every other port comes back as it was, for
420
- // the same reason as in createContainer.
421
- ports = allocateNodePorts(this.portAllocator, { ...reusedNodePorts(dropped), p2p: config.libp2pPort });
422
- const result = this.launchChild({
423
- containerId: OWNER_CONTAINER_ID,
424
- partyId: config.partyId,
425
- profile,
426
- ports,
427
- owner: true,
428
- buildConfig: (workdir) => this.buildOwnerChildConfig(config, profile, workdir, push),
429
- extraArgs: ['--owner', '--admin-port', String(ports.admin), '--identity-file', config.identityPath],
430
- });
431
- const handle = this.handles.get(result.dockerId);
432
- return toNodeInfo(handle);
385
+ return this.announceAddrs(containerId, ports);
433
386
  }
434
387
  catch (err) {
435
- // Release before restoring — see the same note in createContainer.
436
- if (ports)
437
- this.releasePorts(ports);
438
- this.restoreDroppedHandles(dropped);
439
- // Deliberately NOT `discardWorkdir` — the asymmetry with createContainer
440
- // is the point. `<rootDir>/owner` is a single fixed path reused by every
441
- // restart and holds the host's own control-DB storage, so it neither
442
- // grows without bound nor is safe to delete on a failed spawn.
443
- throw err;
444
- }
445
- }
446
- /** Endpoint + bearer for the owner node's admin channel, if spawned. */
447
- getOwnerAdminEndpoint() {
448
- const handle = this.findOwnerHandle();
449
- if (!handle)
450
- return undefined;
451
- return {
452
- baseUrl: `http://127.0.0.1:${handle.ports.admin}`,
453
- token: handle.startupToken,
454
- };
455
- }
456
- /** Whether spawn parameters for the owner node are known (persisted). */
457
- hasOwnerConfig() {
458
- return this.ownerConfig !== undefined;
459
- }
460
- /** Whether the given friendly/docker id refers to the owner node. */
461
- isOwnerNode(idOrDockerId) {
462
- if (idOrDockerId === OWNER_CONTAINER_ID)
463
- return true;
464
- const direct = this.handles.get(idOrDockerId);
465
- if (direct?.owner)
466
- return true;
467
- for (const h of this.handles.values()) {
468
- if (h.owner && h.containerId === idOrDockerId)
469
- return true;
388
+ log('announce hook failed for %s; it starts with no public addresses: %s', containerId, err.message);
389
+ return [];
470
390
  }
471
- return false;
472
- }
473
- /** Stop the owner node (if running). Used on graceful shutdown. */
474
- async stopOwnerNode() {
475
- const handle = this.findOwnerHandle();
476
- if (handle && isPidAlive(handle.pid)) {
477
- await this.stopContainer(handle.dockerId);
478
- }
479
- }
480
- /** Stop the owner node (if running), then re-spawn it from saved config. */
481
- async restartOwnerNode() {
482
- const handle = this.findOwnerHandle();
483
- if (handle && isPidAlive(handle.pid)) {
484
- await this.stopContainer(handle.dockerId);
485
- }
486
- return this.ensureOwnerNode();
487
391
  }
488
392
  /**
489
- * Core spawn mechanics shared by `createContainer` and `ensureOwnerNode`.
393
+ * Core spawn mechanics behind `createContainer`.
490
394
  *
491
395
  * The caller has already allocated `ports` and **owns their lifetime**: this
492
- * method never releases them, so every caller must release on any throw out
396
+ * method never releases them, so the caller must release on any throw out
493
397
  * of here (it has to unwind its handle drop on the same path anyway — see
494
398
  * `restoreDroppedHandles`).
495
399
  *
@@ -506,12 +410,6 @@ export class HostProcessOrchestrator {
506
410
  mkdirSync(workdir, { recursive: true });
507
411
  mkdirSync(join(workdir, 'storage'), { recursive: true });
508
412
  const token = randomBytes(16).toString('hex');
509
- // Per-container secret gating the node's `POST /seed` route. The node
510
- // refuses seed delivery unless the caller presents this as a bearer token
511
- // (and unregisters the route entirely when the env var is empty), so we
512
- // mint a fresh high-entropy value per child and hand it back on the result
513
- // for `ContainerService.applySeed` to send.
514
- const seedToken = randomBytes(32).toString('base64url');
515
413
  const tokenPath = join(workdir, STARTUP_TOKEN_FILE);
516
414
  // Ensure stale token from a previous container with the same id is cleared.
517
415
  if (existsSync(tokenPath)) {
@@ -525,32 +423,29 @@ export class HostProcessOrchestrator {
525
423
  const memoryLimit = opts.memoryLimit ?? this.defaultMemoryLimit;
526
424
  const heapBytes = parseMemoryLimit(memoryLimit);
527
425
  const heapMB = heapBytes !== undefined ? Math.max(64, Math.floor(heapBytes / (1024 * 1024))) : undefined;
426
+ const announcedAddrs = this.announceAddrsFor(containerId, ports);
528
427
  const env = {
529
428
  // Scrubbed, not raw process.env: every CADRE_* config-override key is
530
429
  // stripped so a var set on the manager can't silently reconfigure every
531
430
  // child (see scrubbedParentEnv). Caller-supplied extras come next — the
532
431
  // fixed per-child vars below always win so extraEnv can never clobber
533
- // the seed/startup tokens or port wiring.
432
+ // the startup token or port wiring.
534
433
  ...scrubbedParentEnv(),
535
434
  ...opts.extraEnv,
536
435
  CADRE_STARTUP_TOKEN: token,
537
436
  CADRE_HEALTH_PORT: String(ports.health),
538
437
  CADRE_METRICS_PORT: String(ports.metrics),
539
438
  CADRE_LISTEN_ADDRS: childListenAddrs(ports).join(','),
540
- // NOTE: the scrub above drops every CADRE_* var and `extraEnv` is built only from
541
- // pinnedOwnerKeys, so a managed child advertises only the ports assigned to it here
542
- // — `CADRE_ANNOUNCE_ADDRS`/`CADRE_APPEND_ANNOUNCE_ADDRS` cannot reach it. Fine while
543
- // children are reached at those ports; if a host is ever fronted by a proxy or DNS
544
- // name, plumb an announce var through from host config. The TCP
545
- // and WebSocket ports are the CHILD'S CONTROL NODE's alone: each strand node the
546
- // child runs binds the same two entries with OS-assigned ports instead, since one
547
- // port cannot be held twice (`cadre-core/src/strand-network-config.ts`). A NAT
548
- // forward (the owner node's, via `NatService`) covers the TCP port only, so it
549
- // reaches the control node over TCP alone; a phone outside the LAN, which needs the
550
- // WebSocket port, and every strand node are reached through observed addresses only,
551
- // since no `CADRE_RELAY_ADDRS` is set here and a child therefore holds no relay
552
- // reservation.
553
- CADRE_SEED_TOKEN: seedToken,
439
+ // The append form, not `CADRE_ANNOUNCE_ADDRS`: that one replaces the advertised
440
+ // set and would drop the LAN addresses a phone at home dials. Every entry must be
441
+ // a valid multiaddr: cadre-core refuses to start on one that is not.
442
+ // The TCP and WebSocket ports are the CHILD'S CONTROL NODE's alone: each strand
443
+ // node the child runs binds the same two entries with OS-assigned ports instead,
444
+ // since one port cannot be held twice, and inherits neither announce field
445
+ // (`cadre-core/src/strand-network-config.ts`). Those ports are not mapped, and
446
+ // strand nodes are reached through observed addresses only, since no
447
+ // `CADRE_RELAY_ADDRS` is set here and a child therefore holds no relay reservation.
448
+ ...(announcedAddrs.length > 0 ? { CADRE_APPEND_ANNOUNCE_ADDRS: announcedAddrs.join(',') } : {}),
554
449
  // Pin each child's node-local state (trusted-owner anchor, retained
555
450
  // cold-start dial targets) to its OWN workdir. This is the same value the
556
451
  // cli would derive by default (the config file lives here too), but stated
@@ -612,7 +507,7 @@ export class HostProcessOrchestrator {
612
507
  spawnedAt: new Date().toISOString(),
613
508
  partyId: opts.partyId,
614
509
  profile: opts.profile,
615
- ...(opts.owner ? { owner: true } : {}),
510
+ announcedAddrs,
616
511
  child,
617
512
  alive: true,
618
513
  };
@@ -644,23 +539,20 @@ export class HostProcessOrchestrator {
644
539
  dockerId,
645
540
  healthEndpoint: `http://localhost:${ports.health}/health`,
646
541
  metricsEndpoint: `http://localhost:${ports.metrics}/metrics`,
647
- // The node's seed API (`POST /seed`) is bound to the same server/port as `/health`.
648
- seedEndpoint: `http://localhost:${ports.health}/seed`,
649
- seedToken,
650
542
  p2pPort: ports.p2p,
651
543
  };
652
544
  }
653
545
  /**
654
546
  * Refuse to re-spawn `containerId` while a previous child of it may still be
655
- * running. Both spawn paths call this as their last `await` before the handle
547
+ * running. `createContainer` calls this as its last `await` before the handle
656
548
  * drop, so a refusal happens while nothing has been released, no token rotated
657
549
  * and no child launched — the caller sees an ordinary failed spawn.
658
550
  *
659
551
  * Why it exists: the drop hands the old handle's ports back and the launch takes
660
552
  * those same ports (`reusedNodePorts`), so a child started over a live one dies
661
- * on a port clash — but only after `launchChild` has returned success with a
662
- * fresh seed token. The caller then records a dead child, and the live one
663
- * refuses the new token.
553
+ * on a port clash — but only after `launchChild` has returned success. The
554
+ * caller then records a dead child, and the live one keeps running with no
555
+ * handle naming it.
664
556
  *
665
557
  * - A handle this process spawned is checked exactly ({@link isHandleLive}); a
666
558
  * live one refuses outright.
@@ -670,9 +562,6 @@ export class HostProcessOrchestrator {
670
562
  * is already bound, refuse. The pid is never killed on that evidence — it may
671
563
  * not be ours.
672
564
  *
673
- * `overrides` are the ports the caller will override on top of the reused ones
674
- * (the owner node's configured p2p port); `owner` adds the admin channel.
675
- *
676
565
  * NOTE: the probe cannot see a re-attached child still loading its modules —
677
566
  * `cadre-cli start` writes its token and only then binds, so a child in that
678
567
  * window shows neither, and a re-spawn launched then races it for the ports.
@@ -687,14 +576,14 @@ export class HostProcessOrchestrator {
687
576
  * child is ever seen dying on its own port right after a host restart, probe by
688
577
  * connecting instead of binding.
689
578
  */
690
- async refuseRespawnOverLiveChild(containerId, overrides, owner) {
579
+ async refuseRespawnOverLiveChild(containerId) {
691
580
  const previous = this.handlesFor(containerId);
692
581
  if (previous.some(isHandleLive)) {
693
582
  throw new Error(`container ${containerId} is still running`);
694
583
  }
695
584
  if (!previous.some((h) => isPidAlive(h.pid)))
696
585
  return;
697
- const bindings = childPortBindings({ ...reusedNodePorts(previous), ...overrides }, owner);
586
+ const bindings = childPortBindings(reusedNodePorts(previous));
698
587
  try {
699
588
  await Promise.all(bindings.map(assertPortFree));
700
589
  }
@@ -709,9 +598,9 @@ export class HostProcessOrchestrator {
709
598
  /**
710
599
  * Drop any handle left over from a previous spawn of the same `containerId`
711
600
  * and release its ports. Handles are keyed by the per-spawn `dockerId`, so
712
- * without this a re-spawn (donated-node respawn) would strand the prior
601
+ * without this a re-spawn (a hosted node's respawn) would strand the prior
713
602
  * handle in the map forever, leaking its ports from a bounded range each
714
- * time. Mirrors the same cleanup in `ensureOwnerNode`.
603
+ * time.
715
604
  *
716
605
  * DO NOT delete the workdir: the identity key and node-local stores
717
606
  * (trusted-owner anchor, retained cold-start dial targets) live there and are
@@ -721,16 +610,16 @@ export class HostProcessOrchestrator {
721
610
  * Releasing the ports hands them straight back to the allocator, and the
722
611
  * re-spawn then takes those same ports as overrides (`reusedNodePorts`), so a
723
612
  * re-spawn while the *previous* child is still listening would bind-clash.
724
- * Callers must therefore never drop a live child, and both do so only after
613
+ * The caller must therefore never drop a live child, and does so only after
725
614
  * {@link refuseRespawnOverLiveChild} has passed. A caller that needs to replace
726
615
  * a live child must stop it first.
727
616
  *
728
617
  * **The drop is reversible.** It happens before the launch can fail, so the
729
- * dropped handles are returned and every caller restores them via
618
+ * dropped handles are returned and the caller restores them via
730
619
  * {@link restoreDroppedHandles} when the launch throws — otherwise the caller
731
620
  * would be left holding a dockerId this orchestrator no longer knows, and the
732
621
  * node's workdir could never be reclaimed. On a *successful* spawn the drop
733
- * stands: `DonationService.abandonRespawn` depends on the old handle being
622
+ * stands: `HostedNodeService.abandonRespawn` depends on the old handle being
734
623
  * gone by then.
735
624
  */
736
625
  dropStaleHandle(containerId) {
@@ -749,10 +638,10 @@ export class HostProcessOrchestrator {
749
638
  *
750
639
  * **Precondition: the drop → launch window is synchronous.** Restoring is
751
640
  * sound only because nothing can have taken those ports or run cleanup
752
- * against the handle in between — `launchChild` contains no `await` and both
753
- * callers resolve push credentials *before* their drop. `launchChild`'s half
641
+ * against the handle in between — `launchChild` contains no `await` and the
642
+ * caller resolves push credentials *before* its drop. `launchChild`'s half
754
643
  * is enforced: its declared return type is not a Promise, so adding an
755
- * `await` there is a compile error. The callers' half is not — an `await`
644
+ * `await` there is a compile error. The caller's half is not — an `await`
756
645
  * slipped between a drop and its launch silently breaks this.
757
646
  *
758
647
  * No `persist()` here: `state.json` is rewritten only by `launchChild` and
@@ -778,14 +667,6 @@ export class HostProcessOrchestrator {
778
667
  workdirFor(containerId) {
779
668
  return join(this.rootDir, containerId);
780
669
  }
781
- /** Locate the owner node's handle, if one has been spawned. */
782
- findOwnerHandle() {
783
- for (const h of this.handles.values()) {
784
- if (h.owner || h.containerId === OWNER_CONTAINER_ID)
785
- return h;
786
- }
787
- return undefined;
788
- }
789
670
  /**
790
671
  * Inverse of {@link releasePorts} — hold a handle's whole port set against
791
672
  * the allocator. Used both to rehydrate on `init` and to undo a drop whose
@@ -861,6 +742,11 @@ export class HostProcessOrchestrator {
861
742
  this.discardWorkdir(handle.workdir, { maxRetries: 20, retryDelay: 200 });
862
743
  this.handles.delete(dockerId);
863
744
  this.persist();
745
+ // Removal is a state change too: `NatService` releases the node's port
746
+ // mappings on it at once rather than at its next timer pass. A stopped
747
+ // event for a node no longer in `listNodes()` is how listeners tell a
748
+ // removal from a stop.
749
+ this.emitStateChange(handle);
864
750
  }
865
751
  /**
866
752
  * Whether the child `dockerId` names is running — see {@link isHandleLive}. An
@@ -912,7 +798,8 @@ export class HostProcessOrchestrator {
912
798
  const cfg = {
913
799
  controlNetwork: {
914
800
  partyId: req.partyId,
915
- bootstrapNodes: req.bootstrapNodes,
801
+ // A hosted node is dialed (a claim) or dials the members its invitation names.
802
+ bootstrapNodes: [],
916
803
  },
917
804
  profile: req.profile,
918
805
  // The request carries the filter as text (the provider hands the same text to
@@ -939,46 +826,10 @@ export class HostProcessOrchestrator {
939
826
  cfg.push = push;
940
827
  return cfg;
941
828
  }
942
- /**
943
- * Child config for the **host's own personal cadre** owner node — the opt-in
944
- * "founder" persona spawned only when `ownCadre.enabled` (see
945
- * docs/cadre-host.md § Two roles: donor and founder). It founds/joins the
946
- * control network for the host's own `partyId` (no bootstrap peers — it is the
947
- * founding node of the host's *own* cadre) and carries the host identity via
948
- * `--identity-file` (passed as a spawn arg).
949
- *
950
- * This is NOT the node cadre-host donates to a requester. Donated nodes are
951
- * generic (`createContainer` / `buildChildConfig`): they join the
952
- * **requester's** cadre via bootstrap peers and pin the *requester's* owner
953
- * key — they never run a host genesis.
954
- */
955
- buildOwnerChildConfig(cfg, profile, workdir, push) {
956
- const config = {
957
- controlNetwork: {
958
- partyId: cfg.partyId,
959
- bootstrapNodes: [],
960
- },
961
- profile,
962
- strandFilter: 'all',
963
- };
964
- if (profile === 'storage') {
965
- config.storage = { type: 'file', path: join(workdir, 'storage') };
966
- }
967
- // The host-own-cadre owner/storage node participates in strands, so it owns
968
- // the push-wake fan-out when credentials are configured. (Donated nodes
969
- // belong to foreign cadres and get NO host push block — see the donor path.)
970
- // Written into cadre.json (same host trust boundary as the workdir's
971
- // control-DB); keys are never logged — only the redacted presence line in
972
- // resolvePush.
973
- if (push)
974
- config.push = push;
975
- return config;
976
- }
977
829
  persist() {
978
830
  const state = {
979
831
  version: 1,
980
832
  handles: [...this.handles.values()].map((h) => this.toPersisted(h)),
981
- ...(this.ownerConfig ? { ownerConfig: this.ownerConfig } : {}),
982
833
  };
983
834
  this.stateStore.save(state);
984
835
  }
@@ -1006,7 +857,7 @@ export class HostProcessOrchestrator {
1006
857
  spawnedAt: h.spawnedAt,
1007
858
  partyId: h.partyId,
1008
859
  profile: h.profile,
1009
- ...(h.owner ? { owner: true } : {}),
860
+ announcedAddrs: h.announcedAddrs,
1010
861
  };
1011
862
  }
1012
863
  }
@@ -1020,7 +871,7 @@ function toNodeInfo(h) {
1020
871
  spawnedAt: h.spawnedAt,
1021
872
  workdir: h.workdir,
1022
873
  ports: { ...h.ports },
1023
- ...(h.owner ? { owner: true } : {}),
874
+ announcedAddrs: [...h.announcedAddrs],
1024
875
  };
1025
876
  }
1026
877
  /**
@@ -1040,8 +891,8 @@ function resolveCadreCliBin() {
1040
891
  }
1041
892
  /**
1042
893
  * Whether the process a handle names is still the child it was launched as — the
1043
- * one liveness rule behind `init`, `isRunning`, `ensureOwnerNode`'s short-circuit
1044
- * and the pre-spawn guard, so they cannot drift apart.
894
+ * one liveness rule behind `init`, `isRunning` and the pre-spawn guard, so they
895
+ * cannot drift apart.
1045
896
  *
1046
897
  * A handle this process spawned carries its `ChildProcess`, and that answer is
1047
898
  * exact from the moment of spawn: no exit seen, and the pid still answering. (The