@serfab/cadre-host 1.12.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,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
+ });
@@ -1,44 +1,40 @@
1
- /**
2
- * Shared fakes for the local-UI server tests. Holds no tests of its own.
3
- */
4
-
5
- import type { FounderServices } from '../index.js';
6
- import type { TrustCircleService } from '../../auth/index.js';
7
- import type { NatService } from '../../nat/index.js';
8
- import type { NatStatusSnapshot } from '../../nat/types.js';
9
- import type { StrandService } from '../../strands/index.js';
10
-
11
- export const SAMPLE_CONNECTIVITY: NatStatusSnapshot = {
12
- portMode: 'auto-upnp',
13
- externalPort: 4001,
14
- internalPort: 4001,
15
- routerExternalIp: '203.0.113.5',
16
- mappingLeaseExpiresAt: null,
17
- externalIp: '203.0.113.5',
18
- externalIpDetectedAt: null,
19
- cgnatDetected: false,
20
- directReachability: 'reachable',
21
- lastTestedAt: null,
22
- ddns: {
23
- providerId: null,
24
- hostname: null,
25
- externallyManaged: false,
26
- lastUpdateAt: null,
27
- lastUpdateOk: null,
28
- lastError: null,
29
- },
30
- };
31
-
32
- /**
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.
36
- */
37
- export function fakeFounder(overrides: Partial<FounderServices> = {}): FounderServices {
38
- 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
- };
44
- }
1
+ /**
2
+ * Shared fakes for the local-UI server tests. Holds no tests of its own.
3
+ */
4
+
5
+ import type { NatService, NatChangeListener } from '../../nat/index.js';
6
+ import type { NatStatusSnapshot } from '../../nat/types.js';
7
+
8
+ export const SAMPLE_CONNECTIVITY: NatStatusSnapshot = {
9
+ upnpEnabled: true,
10
+ gateway: { found: true, lanAddress: '192.168.1.20', routerExternalIp: '203.0.113.5', lastError: null },
11
+ externalIp: '203.0.113.5',
12
+ externalIpDetectedAt: null,
13
+ cgnatDetected: false,
14
+ directReachability: 'reachable',
15
+ lastTestedAt: null,
16
+ ddns: {
17
+ providerId: null,
18
+ hostname: null,
19
+ externallyManaged: false,
20
+ lastUpdateAt: null,
21
+ lastUpdateOk: null,
22
+ lastError: null,
23
+ },
24
+ nodes: [],
25
+ };
26
+
27
+ /**
28
+ * A NAT service that answers only the reads the server makes on its own
29
+ * (status, boot publish) and the writes the settings route forwards. `emit`
30
+ * fires the change listeners the server registered, for tests of that wiring.
31
+ */
32
+ export function fakeNat(snapshot: NatStatusSnapshot = SAMPLE_CONNECTIVITY): NatService & { emit(snap: NatStatusSnapshot): void } {
33
+ const listeners = new Set<NatChangeListener>();
34
+ return {
35
+ getStatus: () => snapshot,
36
+ onChange: (listener: NatChangeListener) => { listeners.add(listener); return () => { listeners.delete(listener); }; },
37
+ putSettings: async () => snapshot,
38
+ emit: (snap: NatStatusSnapshot) => { for (const l of listeners) l(snap); },
39
+ } as unknown as NatService & { emit(snap: NatStatusSnapshot): void };
40
+ }
@@ -0,0 +1,160 @@
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: [] }) },
116
+ });
117
+ app = Fastify();
118
+ registerErrorHandler(app);
119
+ registerHostedNodesRoutes(app, { hostedNodes });
120
+ });
121
+
122
+ afterEach(async () => {
123
+ await app.close();
124
+ rmSync(tmpRoot, { recursive: true, force: true });
125
+ });
126
+
127
+ it('answers 400 for an invitation that does not decode, before anything is written or spawned', async () => {
128
+ for (const invitation of ['not an invitation', 42]) {
129
+ const res = await app.inject({ method: 'POST', url: '/api/hosted-nodes', payload: { invitation } });
130
+ expect(res.statusCode).toBe(400);
131
+ expect(res.json()).toMatchObject({ ok: false, error: { code: 'invalid_request' } });
132
+ }
133
+ expect(store.list()).toEqual([]);
134
+ expect(orch.createCalls).toEqual([]);
135
+ });
136
+
137
+ it('answers 409 to Retry unless no member of the cadre could be reached', async () => {
138
+ const created = await app.inject({ method: 'POST', url: '/api/hosted-nodes', payload: { invitation: encodedTestInvitation('party-P') } });
139
+ expect(created.statusCode).toBe(201);
140
+ const { id } = (created.json() as { data: { node: HostedNodeView } }).data.node;
141
+
142
+ // Still joining: nothing to retry.
143
+ expect((await app.inject({ method: 'POST', url: `/api/hosted-nodes/${id}/retry` })).statusCode).toBe(409);
144
+
145
+ // Refused by a member (an expired invitation): final.
146
+ const node = store.get(id)!;
147
+ store.put({ ...node, status: 'error', error: 'Cadre invitation refused: expired (invite-spent)', retryable: false });
148
+ const refused = await app.inject({ method: 'POST', url: `/api/hosted-nodes/${id}/retry` });
149
+ expect(refused.statusCode).toBe(409);
150
+ expect(refused.json()).toMatchObject({ ok: false, error: { code: 'invalid_state' } });
151
+ expect(orch.createCalls).toHaveLength(1);
152
+
153
+ // No member reachable: Retry starts it again.
154
+ store.put({ ...node, status: 'error', error: 'No member named in the invitation could be reached.', retryable: true });
155
+ orch.crash(node.dockerId!);
156
+ const retried = await app.inject({ method: 'POST', url: `/api/hosted-nodes/${id}/retry` });
157
+ expect(retried.statusCode).toBe(200);
158
+ expect(retried.json()).toMatchObject({ ok: true, data: { node: { id, status: 'joining' } } });
159
+ });
160
+ });