@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
@@ -1,38 +1,38 @@
1
- import { describe, it, expect } from 'vitest';
2
-
3
- import { EventBus } from '../events/bus.js';
4
- import type { LocalUiEvent } from '../events/types.js';
5
-
6
- describe('EventBus', () => {
7
- it('delivers events to subscribers in order', () => {
8
- const bus = new EventBus();
9
- const received: LocalUiEvent[] = [];
10
- bus.subscribe((e) => received.push(e));
11
- bus.publish({ type: 'trust-circle-changed', kind: 'invited' });
12
- bus.publish({ type: 'trust-circle-changed', kind: 'revoked' });
13
- expect(received).toEqual([
14
- { type: 'trust-circle-changed', kind: 'invited' },
15
- { type: 'trust-circle-changed', kind: 'revoked' },
16
- ]);
17
- });
18
-
19
- it('unsubscribe stops further deliveries', () => {
20
- const bus = new EventBus();
21
- const received: LocalUiEvent[] = [];
22
- const unsub = bus.subscribe((e) => received.push(e));
23
- bus.publish({ type: 'trust-circle-changed', kind: 'invited' });
24
- unsub();
25
- bus.publish({ type: 'trust-circle-changed', kind: 'redeemed' });
26
- expect(received).toHaveLength(1);
27
- expect(bus.listenerCount()).toBe(0);
28
- });
29
-
30
- it('one listener throwing does not block others', () => {
31
- const bus = new EventBus();
32
- const received: LocalUiEvent[] = [];
33
- bus.subscribe(() => { throw new Error('bad listener'); });
34
- bus.subscribe((e) => received.push(e));
35
- bus.publish({ type: 'trust-circle-changed', kind: 'invited' });
36
- expect(received).toHaveLength(1);
37
- });
38
- });
1
+ import { describe, it, expect } from 'vitest';
2
+
3
+ import { EventBus } from '../events/bus.js';
4
+ import type { LocalUiEvent } from '../events/types.js';
5
+
6
+ describe('EventBus', () => {
7
+ it('delivers events to subscribers in order', () => {
8
+ const bus = new EventBus();
9
+ const received: LocalUiEvent[] = [];
10
+ bus.subscribe((e) => received.push(e));
11
+ bus.publish({ type: 'hosted-nodes-changed', kind: 'added', nodeId: 'hn_a' });
12
+ bus.publish({ type: 'hosted-nodes-changed', kind: 'claimed', nodeId: 'hn_a' });
13
+ expect(received).toEqual([
14
+ { type: 'hosted-nodes-changed', kind: 'added', nodeId: 'hn_a' },
15
+ { type: 'hosted-nodes-changed', kind: 'claimed', nodeId: 'hn_a' },
16
+ ]);
17
+ });
18
+
19
+ it('unsubscribe stops further deliveries', () => {
20
+ const bus = new EventBus();
21
+ const received: LocalUiEvent[] = [];
22
+ const unsub = bus.subscribe((e) => received.push(e));
23
+ bus.publish({ type: 'hosted-nodes-changed', kind: 'added', nodeId: 'hn_a' });
24
+ unsub();
25
+ bus.publish({ type: 'hosted-nodes-changed', kind: 'removed', nodeId: 'hn_a' });
26
+ expect(received).toHaveLength(1);
27
+ expect(bus.listenerCount()).toBe(0);
28
+ });
29
+
30
+ it('one listener throwing does not block others', () => {
31
+ const bus = new EventBus();
32
+ const received: LocalUiEvent[] = [];
33
+ bus.subscribe(() => { throw new Error('bad listener'); });
34
+ bus.subscribe((e) => received.push(e));
35
+ bus.publish({ type: 'hosted-nodes-changed', kind: 'added', nodeId: 'hn_a' });
36
+ expect(received).toHaveLength(1);
37
+ });
38
+ });
@@ -1,118 +1,81 @@
1
- import Fastify, { type FastifyRequest } from 'fastify';
2
- import { describe, it, expect, beforeEach, afterEach } from 'vitest';
3
-
4
- import { TrustCircleError } from '../../auth/types.js';
5
- import { NatError } from '../../nat/types.js';
6
- import { UpdateErrorException } from '../../update/types.js';
7
- import { GrantError } from '../../donation/types.js';
8
- import { registerErrorHandler } from '../error-handler.js';
9
-
10
- describe('error handler', () => {
11
- let app: ReturnType<typeof Fastify>;
12
-
13
- beforeEach(async () => {
14
- app = Fastify();
15
- registerErrorHandler(app);
16
- app.get('/trust/:code', async (req: FastifyRequest) => {
17
- const { code } = req.params as { code: string };
18
- throw new TrustCircleError(code as never, `trust err: ${code}`);
19
- });
20
- app.get('/nat/:code', async (req: FastifyRequest) => {
21
- const { code } = req.params as { code: string };
22
- throw new NatError(code as never, `nat err: ${code}`);
23
- });
24
- app.get('/update/:code', async (req: FastifyRequest) => {
25
- const { code } = req.params as { code: string };
26
- throw new UpdateErrorException(code as never, `update err: ${code}`);
27
- });
28
- app.get('/grant/:code', async (req: FastifyRequest) => {
29
- const { code } = req.params as { code: string };
30
- throw new GrantError(code as never, `grant err: ${code}`);
31
- });
32
- app.get('/unknown', async () => {
33
- throw new Error('mystery');
34
- });
35
- });
36
-
37
- afterEach(async () => {
38
- await app.close();
39
- });
40
-
41
- it('TrustCircleError invalid_label → 400', async () => {
42
- const res = await app.inject({ method: 'GET', url: '/trust/invalid_label' });
43
- expect(res.statusCode).toBe(400);
44
- const body = res.json() as { ok: boolean; error: { code: string; message: string } };
45
- expect(body).toEqual({
46
- ok: false,
47
- error: { code: 'invalid_label', message: 'trust err: invalid_label' },
48
- });
49
- });
50
-
51
- it('TrustCircleError not_found → 404', async () => {
52
- const res = await app.inject({ method: 'GET', url: '/trust/not_found' });
53
- expect(res.statusCode).toBe(404);
54
- });
55
-
56
- it('TrustCircleError already_redeemed → 404', async () => {
57
- const res = await app.inject({ method: 'GET', url: '/trust/already_redeemed' });
58
- expect(res.statusCode).toBe(404);
59
- });
60
-
61
- it('TrustCircleError expired → 410', async () => {
62
- const res = await app.inject({ method: 'GET', url: '/trust/expired' });
63
- expect(res.statusCode).toBe(410);
64
- });
65
-
66
- it('TrustCircleError storage_error → 500', async () => {
67
- const res = await app.inject({ method: 'GET', url: '/trust/storage_error' });
68
- expect(res.statusCode).toBe(500);
69
- });
70
-
71
- it('NatError invalid_config → 400', async () => {
72
- const res = await app.inject({ method: 'GET', url: '/nat/invalid_config' });
73
- expect(res.statusCode).toBe(400);
74
- const body = res.json() as { error: { code: string } };
75
- expect(body.error.code).toBe('invalid_config');
76
- });
77
-
78
- it('NatError secrets_unavailable → 500', async () => {
79
- const res = await app.inject({ method: 'GET', url: '/nat/secrets_unavailable' });
80
- expect(res.statusCode).toBe(500);
81
- });
82
-
83
- it('UpdateErrorException apply_in_progress → 409', async () => {
84
- const res = await app.inject({ method: 'GET', url: '/update/apply_in_progress' });
85
- expect(res.statusCode).toBe(409);
86
- });
87
-
88
- it('UpdateErrorException no_update_available → 400', async () => {
89
- const res = await app.inject({ method: 'GET', url: '/update/no_update_available' });
90
- expect(res.statusCode).toBe(400);
91
- });
92
-
93
- it('GrantError invalid_max_nodes → 400', async () => {
94
- const res = await app.inject({ method: 'GET', url: '/grant/invalid_max_nodes' });
95
- expect(res.statusCode).toBe(400);
96
- const body = res.json() as { error: { code: string } };
97
- expect(body.error.code).toBe('invalid_max_nodes');
98
- });
99
-
100
- it('GrantError not_found → 404', async () => {
101
- const res = await app.inject({ method: 'GET', url: '/grant/not_found' });
102
- expect(res.statusCode).toBe(404);
103
- });
104
-
105
- it('GrantError storage_error → 500', async () => {
106
- const res = await app.inject({ method: 'GET', url: '/grant/storage_error' });
107
- expect(res.statusCode).toBe(500);
108
- });
109
-
110
- it('unknown Error → 500 internal', async () => {
111
- const res = await app.inject({ method: 'GET', url: '/unknown' });
112
- expect(res.statusCode).toBe(500);
113
- const body = res.json() as { ok: boolean; error: { code: string; message: string } };
114
- expect(body.ok).toBe(false);
115
- expect(body.error.code).toBe('internal');
116
- expect(body.error.message).toBe('mystery');
117
- });
118
- });
1
+ import Fastify, { type FastifyRequest } from 'fastify';
2
+ import { describe, it, expect, beforeEach, afterEach } from 'vitest';
3
+
4
+ import { NatError } from '../../nat/types.js';
5
+ import { UpdateErrorException } from '../../update/types.js';
6
+ import { HostedNodeError } from '../../hosted/types.js';
7
+ import { registerErrorHandler } from '../error-handler.js';
8
+
9
+ describe('error handler', () => {
10
+ let app: ReturnType<typeof Fastify>;
11
+
12
+ beforeEach(async () => {
13
+ app = Fastify();
14
+ registerErrorHandler(app);
15
+ app.get('/nat/:code', async (req: FastifyRequest) => {
16
+ const { code } = req.params as { code: string };
17
+ throw new NatError(code as never, `nat err: ${code}`);
18
+ });
19
+ app.get('/update/:code', async (req: FastifyRequest) => {
20
+ const { code } = req.params as { code: string };
21
+ throw new UpdateErrorException(code as never, `update err: ${code}`);
22
+ });
23
+ app.get('/hosted/:code', async (req: FastifyRequest) => {
24
+ const { code } = req.params as { code: string };
25
+ throw new HostedNodeError(code as never, `hosted err: ${code}`);
26
+ });
27
+ app.get('/unknown', async () => {
28
+ throw new Error('mystery');
29
+ });
30
+ });
31
+
32
+ afterEach(async () => {
33
+ await app.close();
34
+ });
35
+
36
+ it('NatError invalid_config → 400', async () => {
37
+ const res = await app.inject({ method: 'GET', url: '/nat/invalid_config' });
38
+ expect(res.statusCode).toBe(400);
39
+ const body = res.json() as { ok: boolean; error: { code: string; message: string } };
40
+ expect(body).toEqual({
41
+ ok: false,
42
+ error: { code: 'invalid_config', message: 'nat err: invalid_config' },
43
+ });
44
+ });
45
+
46
+ it('NatError secrets_unavailable → 500', async () => {
47
+ const res = await app.inject({ method: 'GET', url: '/nat/secrets_unavailable' });
48
+ expect(res.statusCode).toBe(500);
49
+ });
50
+
51
+ it('UpdateErrorException apply_in_progress → 409', async () => {
52
+ const res = await app.inject({ method: 'GET', url: '/update/apply_in_progress' });
53
+ expect(res.statusCode).toBe(409);
54
+ });
55
+
56
+ it('UpdateErrorException no_update_available → 400', async () => {
57
+ const res = await app.inject({ method: 'GET', url: '/update/no_update_available' });
58
+ expect(res.statusCode).toBe(400);
59
+ });
60
+
61
+ it('HostedNodeError maps each code to its status', async () => {
62
+ const statusOf = async (code: string): Promise<number> => (await app.inject({ method: 'GET', url: `/hosted/${code}` })).statusCode;
63
+ expect(await statusOf('invalid_request')).toBe(400);
64
+ expect(await statusOf('not_found')).toBe(404);
65
+ expect(await statusOf('invalid_state')).toBe(409);
66
+ expect(await statusOf('node_unavailable')).toBe(503);
67
+ expect(await statusOf('orchestrator_error')).toBe(500);
68
+ expect(await statusOf('storage_error')).toBe(500);
69
+ const body = (await app.inject({ method: 'GET', url: '/hosted/invalid_state' })).json() as { error: { code: string } };
70
+ expect(body.error.code).toBe('invalid_state');
71
+ });
72
+
73
+ it('unknown Error → 500 internal', async () => {
74
+ const res = await app.inject({ method: 'GET', url: '/unknown' });
75
+ expect(res.statusCode).toBe(500);
76
+ const body = res.json() as { ok: boolean; error: { code: string; message: string } };
77
+ expect(body.ok).toBe(false);
78
+ expect(body.error.code).toBe('internal');
79
+ expect(body.error.message).toBe('mystery');
80
+ });
81
+ });
@@ -2,18 +2,13 @@
2
2
  * Shared fakes for the local-UI server tests. Holds no tests of its own.
3
3
  */
4
4
 
5
- import type { FounderServices } from '../index.js';
6
- import type { TrustCircleService } from '../../auth/index.js';
7
- import type { NatService } from '../../nat/index.js';
5
+ import type { NatService, NatChangeListener } from '../../nat/index.js';
8
6
  import type { NatStatusSnapshot } from '../../nat/types.js';
9
- import type { StrandService } from '../../strands/index.js';
10
7
 
11
8
  export const SAMPLE_CONNECTIVITY: NatStatusSnapshot = {
12
- portMode: 'auto-upnp',
13
- externalPort: 4001,
14
- internalPort: 4001,
15
- routerExternalIp: '203.0.113.5',
16
- mappingLeaseExpiresAt: null,
9
+ network: { setting: 'auto', mode: 'lan', publicInterfaceIp: null },
10
+ upnpEnabled: true,
11
+ gateway: { found: true, lanAddress: '192.168.1.20', routerExternalIp: '203.0.113.5', lastError: null },
17
12
  externalIp: '203.0.113.5',
18
13
  externalIpDetectedAt: null,
19
14
  cgnatDetected: false,
@@ -27,18 +22,20 @@ export const SAMPLE_CONNECTIVITY: NatStatusSnapshot = {
27
22
  lastUpdateOk: null,
28
23
  lastError: null,
29
24
  },
25
+ nodes: [],
30
26
  };
31
27
 
32
28
  /**
33
- * A founder-role service set whose members answer only the reads the server
34
- * makes on its own (status, boot publish): an empty trust circle, the sample
35
- * connectivity, no strands. Pass the service a test exercises as an override.
29
+ * A NAT service that answers only the reads the server makes on its own
30
+ * (status, boot publish) and the writes the settings route forwards. `emit`
31
+ * fires the change listeners the server registered, for tests of that wiring.
36
32
  */
37
- export function fakeFounder(overrides: Partial<FounderServices> = {}): FounderServices {
33
+ export function fakeNat(snapshot: NatStatusSnapshot = SAMPLE_CONNECTIVITY): NatService & { emit(snap: NatStatusSnapshot): void } {
34
+ const listeners = new Set<NatChangeListener>();
38
35
  return {
39
- trustCircle: { list: async () => ({ members: [], pending: [] }) } as unknown as TrustCircleService,
40
- nat: { getStatus: () => SAMPLE_CONNECTIVITY } as unknown as NatService,
41
- strands: { list: async () => ({ strands: [], controlConnections: 0 }) } as unknown as StrandService,
42
- ...overrides,
43
- };
36
+ getStatus: () => snapshot,
37
+ onChange: (listener: NatChangeListener) => { listeners.add(listener); return () => { listeners.delete(listener); }; },
38
+ putSettings: async () => snapshot,
39
+ emit: (snap: NatStatusSnapshot) => { for (const l of listeners) l(snap); },
40
+ } as unknown as NatService & { emit(snap: NatStatusSnapshot): void };
44
41
  }
@@ -0,0 +1,161 @@
1
+ /**
2
+ * `/api/hosted-nodes` route tests: the status code each outcome maps to, driven
3
+ * against a stub service so no child spawns, and the join-by-invitation refusals
4
+ * against the real service over the fake orchestrator, where "before anything is
5
+ * spawned" can be checked. The service's own behaviour is
6
+ * `hosted/__tests__/hosted-node-service.test.ts`.
7
+ */
8
+
9
+ import Fastify from 'fastify';
10
+ import { mkdtempSync, rmSync } from 'node:fs';
11
+ import { tmpdir } from 'node:os';
12
+ import { join } from 'node:path';
13
+ import { afterEach, beforeEach, describe, expect, it } from 'vitest';
14
+
15
+ import { HostedNodeService } from '../../hosted/hosted-node-service.js';
16
+ import { HostedNodeStore } from '../../hosted/hosted-node-store.js';
17
+ import { HostedNodeError, type HostedNodeView } from '../../hosted/types.js';
18
+ import { FakeOrchestrator } from '../../hosted/__tests__/fake-orchestrator.js';
19
+ import { encodedTestInvitation } from '../../hosted/__tests__/test-invitation.js';
20
+ import { registerErrorHandler } from '../error-handler.js';
21
+ import { registerHostedNodesRoutes } from '../routes/hosted-nodes.js';
22
+
23
+ const UNCLAIMED: HostedNodeView = {
24
+ id: 'hn_a', join: { kind: 'claim' }, partyId: 'unclaimed', profile: 'storage', status: 'unclaimed',
25
+ dockerId: '1:a', createdAt: '2026-01-01T00:00:00.000Z', updatedAt: '2026-01-01T00:00:00.000Z',
26
+ };
27
+ const JOINED: HostedNodeView = { ...UNCLAIMED, id: 'hn_b', status: 'joined', partyId: 'party-P', ownerKey: 'owner' };
28
+
29
+ /** A service with two nodes, answering as the real one would for each. */
30
+ function stubService(): HostedNodeService & { removed: string[] } {
31
+ const nodes = new Map([[UNCLAIMED.id, UNCLAIMED], [JOINED.id, JOINED]]);
32
+ const removed: string[] = [];
33
+ const require = (id: string): HostedNodeView => {
34
+ const node = nodes.get(id);
35
+ if (!node) throw new HostedNodeError('not_found', `No such hosted node: ${id}`);
36
+ return node;
37
+ };
38
+ return {
39
+ removed,
40
+ list: () => [...nodes.values()],
41
+ get: (id: string) => nodes.get(id),
42
+ join: async () => ({ ...UNCLAIMED, id: 'hn_new' }),
43
+ claimDetails: async (id: string) => {
44
+ const node = require(id);
45
+ if (node.status !== 'unclaimed') throw new HostedNodeError('invalid_state', `Hosted node ${id} is ${node.status}`);
46
+ if (id === 'hn_starting') throw new HostedNodeError('node_unavailable', 'not yet');
47
+ return { payload: 'sereus-join:1.x', peerId: '12D3KooW', multiaddrs: ['/ip4/192.168.1.20/tcp/1/ws/p2p/12D3KooW'], reachability: null };
48
+ },
49
+ remove: async (id: string) => { require(id); removed.push(id); },
50
+ reset: async (id: string) => { require(id); return { ...UNCLAIMED, id: 'hn_reset' }; },
51
+ } as unknown as HostedNodeService & { removed: string[] };
52
+ }
53
+
54
+ describe('/api/hosted-nodes routes', () => {
55
+ let app: ReturnType<typeof Fastify>;
56
+ let service: ReturnType<typeof stubService>;
57
+
58
+ beforeEach(() => {
59
+ app = Fastify();
60
+ registerErrorHandler(app);
61
+ service = stubService();
62
+ registerHostedNodesRoutes(app, { hostedNodes: service });
63
+ });
64
+
65
+ afterEach(async () => {
66
+ await app.close();
67
+ });
68
+
69
+ it('answers each outcome with its status code', async () => {
70
+ expect((await app.inject({ method: 'GET', url: '/api/hosted-nodes' })).json()).toEqual({ ok: true, data: { nodes: [UNCLAIMED, JOINED] } });
71
+
72
+ const created = await app.inject({ method: 'POST', url: '/api/hosted-nodes' });
73
+ expect(created.statusCode).toBe(201);
74
+ expect(created.json()).toMatchObject({ ok: true, data: { node: { id: 'hn_new', status: 'unclaimed' } } });
75
+
76
+ expect((await app.inject({ method: 'GET', url: '/api/hosted-nodes/hn_a' })).json()).toEqual({ ok: true, data: { node: UNCLAIMED } });
77
+ const missing = await app.inject({ method: 'GET', url: '/api/hosted-nodes/hn_nobody' });
78
+ expect(missing.statusCode).toBe(404);
79
+ expect(missing.json()).toMatchObject({ ok: false, error: { code: 'not_found' } });
80
+
81
+ const claim = await app.inject({ method: 'GET', url: '/api/hosted-nodes/hn_a/claim' });
82
+ expect(claim.statusCode).toBe(200);
83
+ expect(claim.json()).toMatchObject({ ok: true, data: { payload: 'sereus-join:1.x', peerId: '12D3KooW' } });
84
+
85
+ const removed = await app.inject({ method: 'DELETE', url: '/api/hosted-nodes/hn_b' });
86
+ expect(removed.statusCode).toBe(204);
87
+ expect(service.removed).toEqual(['hn_b']);
88
+ expect((await app.inject({ method: 'DELETE', url: '/api/hosted-nodes/hn_nobody' })).statusCode).toBe(404);
89
+
90
+ const reset = await app.inject({ method: 'POST', url: '/api/hosted-nodes/hn_a/reset' });
91
+ expect(reset.statusCode).toBe(201);
92
+ expect(reset.json()).toMatchObject({ ok: true, data: { node: { id: 'hn_reset' } } });
93
+ });
94
+
95
+ it('answers 409 for the claim details of a joined node', async () => {
96
+ const res = await app.inject({ method: 'GET', url: '/api/hosted-nodes/hn_b/claim' });
97
+ expect(res.statusCode).toBe(409);
98
+ expect(res.json()).toMatchObject({ ok: false, error: { code: 'invalid_state' } });
99
+ });
100
+ });
101
+
102
+ describe('/api/hosted-nodes join by invitation, over the real service', () => {
103
+ let app: ReturnType<typeof Fastify>;
104
+ let orch: FakeOrchestrator;
105
+ let store: HostedNodeStore;
106
+ let tmpRoot: string;
107
+
108
+ beforeEach(() => {
109
+ tmpRoot = mkdtempSync(join(tmpdir(), 'cadre-host-hosted-route-'));
110
+ orch = new FakeOrchestrator();
111
+ store = new HostedNodeStore(tmpRoot);
112
+ const hostedNodes = new HostedNodeService({
113
+ orchestrator: orch,
114
+ store,
115
+ addresses: { publicAddressesFor: () => [], getStatus: () => ({ nodes: [], gateway: { lanAddress: null }, network: { mode: 'lan' } }) },
116
+ primaryLan: async () => undefined,
117
+ });
118
+ app = Fastify();
119
+ registerErrorHandler(app);
120
+ registerHostedNodesRoutes(app, { hostedNodes });
121
+ });
122
+
123
+ afterEach(async () => {
124
+ await app.close();
125
+ rmSync(tmpRoot, { recursive: true, force: true });
126
+ });
127
+
128
+ it('answers 400 for an invitation that does not decode, before anything is written or spawned', async () => {
129
+ for (const invitation of ['not an invitation', 42]) {
130
+ const res = await app.inject({ method: 'POST', url: '/api/hosted-nodes', payload: { invitation } });
131
+ expect(res.statusCode).toBe(400);
132
+ expect(res.json()).toMatchObject({ ok: false, error: { code: 'invalid_request' } });
133
+ }
134
+ expect(store.list()).toEqual([]);
135
+ expect(orch.createCalls).toEqual([]);
136
+ });
137
+
138
+ it('answers 409 to Retry unless no member of the cadre could be reached', async () => {
139
+ const created = await app.inject({ method: 'POST', url: '/api/hosted-nodes', payload: { invitation: encodedTestInvitation('party-P') } });
140
+ expect(created.statusCode).toBe(201);
141
+ const { id } = (created.json() as { data: { node: HostedNodeView } }).data.node;
142
+
143
+ // Still joining: nothing to retry.
144
+ expect((await app.inject({ method: 'POST', url: `/api/hosted-nodes/${id}/retry` })).statusCode).toBe(409);
145
+
146
+ // Refused by a member (an expired invitation): final.
147
+ const node = store.get(id)!;
148
+ store.put({ ...node, status: 'error', error: 'Cadre invitation refused: expired (invite-spent)', retryable: false });
149
+ const refused = await app.inject({ method: 'POST', url: `/api/hosted-nodes/${id}/retry` });
150
+ expect(refused.statusCode).toBe(409);
151
+ expect(refused.json()).toMatchObject({ ok: false, error: { code: 'invalid_state' } });
152
+ expect(orch.createCalls).toHaveLength(1);
153
+
154
+ // No member reachable: Retry starts it again.
155
+ store.put({ ...node, status: 'error', error: 'No member named in the invitation could be reached.', retryable: true });
156
+ orch.crash(node.dockerId!);
157
+ const retried = await app.inject({ method: 'POST', url: `/api/hosted-nodes/${id}/retry` });
158
+ expect(retried.statusCode).toBe(200);
159
+ expect(retried.json()).toMatchObject({ ok: true, data: { node: { id, status: 'joining' } } });
160
+ });
161
+ });