@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 +0,0 @@
1
- .dot.svelte-1qa2kn2{display:inline-flex;align-items:center;gap:.5rem;font-weight:500}.bulb.svelte-1qa2kn2{display:inline-block;border-radius:999px;flex:0 0 auto;background:var(--color-text-soft);box-shadow:0 0 0 3px #0000000f}.dot-ok.svelte-1qa2kn2 .bulb:where(.svelte-1qa2kn2){background:var(--color-success);box-shadow:0 0 0 3px var(--color-success-bg)}.dot-warn.svelte-1qa2kn2 .bulb:where(.svelte-1qa2kn2){background:var(--color-warn);box-shadow:0 0 0 3px var(--color-warn-bg)}.dot-err.svelte-1qa2kn2 .bulb:where(.svelte-1qa2kn2){background:var(--color-danger);box-shadow:0 0 0 3px var(--color-danger-bg)}.dot-muted.svelte-1qa2kn2 .bulb:where(.svelte-1qa2kn2){background:var(--color-text-soft)}.dot-ok.svelte-1qa2kn2{color:var(--color-success)}.dot-warn.svelte-1qa2kn2{color:var(--color-warn)}.dot-err.svelte-1qa2kn2{color:var(--color-danger)}.row.svelte-lwwmgz{display:inline-flex;gap:.4rem;flex-wrap:wrap}.banner.svelte-15mtls8{display:flex;align-items:center;justify-content:space-between;gap:var(--space-3);padding:var(--space-3) var(--space-4);border-radius:var(--radius);background:var(--color-info-bg);color:var(--color-info);border:1px solid var(--color-info)}.summary.svelte-15mtls8{grid-template-columns:repeat(auto-fit,minmax(15rem,1fr))}.kv.svelte-15mtls8{margin:var(--space-3) 0 0 0;display:grid;grid-template-columns:max-content 1fr;gap:var(--space-1) var(--space-3);font-size:.9rem}.kv.svelte-15mtls8>div:where(.svelte-15mtls8){display:contents}.kv.svelte-15mtls8 dt:where(.svelte-15mtls8){color:var(--color-text-muted)}.kv.svelte-15mtls8 dd:where(.svelte-15mtls8){margin:0}.big.svelte-15mtls8{font-size:1.75rem;font-weight:600;margin:var(--space-2) 0}.big.svelte-15mtls8 .muted:where(.svelte-15mtls8){font-size:.95rem;font-weight:400}.link.svelte-15mtls8{display:inline-block;margin-top:var(--space-2);font-weight:500}.backdrop.svelte-80vbit{position:fixed;inset:0;background:#0f111673;z-index:1000}.dialog.svelte-80vbit{position:fixed;top:50%;left:50%;transform:translate(-50%,-50%);background:var(--color-surface);border:1px solid var(--color-border);border-radius:var(--radius-lg);box-shadow:var(--shadow-lg);padding:var(--space-5);min-width:min(24rem,calc(100vw - 2rem));max-width:min(32rem,calc(100vw - 2rem));overflow-wrap:anywhere;z-index:1010}.note.svelte-80vbit{background:var(--color-warn-bg);color:var(--color-warn);border-radius:var(--radius);padding:var(--space-2) var(--space-3);font-size:.9rem}.gate.svelte-80vbit{margin-top:var(--space-3);overflow-wrap:anywhere}.actions.svelte-80vbit{display:flex;justify-content:flex-end;gap:.5rem;margin-top:var(--space-4)}.qr.svelte-1uqetyj{background:#fff;border-radius:var(--radius);padding:.5rem;display:inline-block;line-height:0}.qr.svelte-1uqetyj svg{width:100%;height:100%;display:block}.placeholder.svelte-1uqetyj{background:var(--color-surface-alt)}.qr-wrap.svelte-sddtr9{display:flex;justify-content:center;margin:var(--space-3) 0}.token.svelte-sddtr9{display:flex;flex-direction:column;gap:.5rem}.token.svelte-sddtr9 textarea:where(.svelte-sddtr9){font-family:var(--font-mono);font-size:.78rem;word-break:break-all;resize:vertical}.token.svelte-sddtr9 button:where(.svelte-sddtr9){align-self:flex-end;display:inline-flex;align-items:center;gap:.375rem}.backdrop.svelte-1d46kin{position:fixed;inset:0;background:#0f111673;z-index:1000}.dialog.svelte-1d46kin{position:fixed;top:50%;left:50%;transform:translate(-50%,-50%);background:var(--color-surface);border:1px solid var(--color-border);border-radius:var(--radius-lg);box-shadow:var(--shadow-lg);padding:var(--space-5);width:min(28rem,calc(100vw - 2rem));max-height:calc(100vh - 2rem);overflow:auto;z-index:1010}.actions.svelte-1d46kin{display:flex;justify-content:flex-end;gap:.5rem;margin-top:var(--space-4)}.small.svelte-1d46kin{font-size:.85rem}.page-header.svelte-3alf51{display:flex;align-items:flex-end;justify-content:space-between;flex-wrap:wrap;gap:var(--space-3)}.list.svelte-3alf51{list-style:none;padding:0;margin:var(--space-3) 0 0 0;display:flex;flex-direction:column;gap:.5rem}.list.svelte-3alf51 li:where(.svelte-3alf51){display:flex;align-items:center;justify-content:space-between;gap:var(--space-3);padding:var(--space-3);border:1px solid var(--color-border);border-radius:var(--radius);background:var(--color-surface-alt)}.member-main.svelte-3alf51{display:flex;flex-direction:column;gap:.125rem;min-width:0}.member-label.svelte-3alf51{font-weight:500}.small.svelte-3alf51{font-size:.85rem}.grid.two.svelte-hh3bto{grid-template-columns:repeat(auto-fit,minmax(20rem,1fr))}.kv.svelte-hh3bto{margin:var(--space-3) 0;display:grid;grid-template-columns:max-content 1fr;gap:var(--space-1) var(--space-3);font-size:.9rem}.kv.svelte-hh3bto>div:where(.svelte-hh3bto){display:contents}.kv.svelte-hh3bto dt:where(.svelte-hh3bto){color:var(--color-text-muted)}.kv.svelte-hh3bto dd:where(.svelte-hh3bto){margin:0}.row.inline.svelte-hh3bto{gap:.5rem}.row.inline.svelte-hh3bto input[type=checkbox]:where(.svelte-hh3bto){width:auto}.warning.svelte-hh3bto{background:var(--color-warn-bg);border-color:var(--color-warn)}.warning.svelte-hh3bto ul:where(.svelte-hh3bto){margin:0;padding-left:1.25rem}.actions.svelte-hh3bto{display:flex;justify-content:flex-end;gap:.5rem}.small.svelte-hh3bto{font-size:.85rem}table.svelte-7tcksg{width:100%;border-collapse:collapse;font-size:.92rem}th.svelte-7tcksg,td.svelte-7tcksg{padding:.5rem .75rem;text-align:left;border-bottom:1px solid var(--color-border)}th.svelte-7tcksg{font-weight:600;font-size:.78rem;text-transform:uppercase;letter-spacing:.05em;color:var(--color-text-muted);border-bottom-color:var(--color-border-strong)}tr.svelte-7tcksg:last-child td:where(.svelte-7tcksg){border-bottom:none}.log-card.svelte-15ufwrh{display:flex;flex-direction:column;gap:var(--space-3)}.header.svelte-15ufwrh{display:flex;align-items:center;justify-content:space-between;gap:var(--space-3)}.logs.svelte-15ufwrh{max-height:24rem;font-size:.78rem;line-height:1.45;white-space:pre}.small.svelte-15ufwrh{font-size:.85rem}button.svelte-15ufwrh{display:inline-flex;align-items:center;gap:.375rem}.back.svelte-1eq4g96{font-size:.92rem}.page-header.svelte-1eq4g96{display:flex;align-items:flex-start;justify-content:space-between;flex-wrap:wrap;gap:var(--space-3)}.kv.svelte-1eq4g96{margin:var(--space-3) 0;display:grid;grid-template-columns:max-content 1fr;gap:var(--space-1) var(--space-3);font-size:.92rem}.kv.svelte-1eq4g96>div:where(.svelte-1eq4g96){display:contents}.kv.svelte-1eq4g96 dt:where(.svelte-1eq4g96){color:var(--color-text-muted)}.kv.svelte-1eq4g96 dd:where(.svelte-1eq4g96){margin:0}.actions.svelte-1eq4g96{display:flex;gap:.5rem;flex-wrap:wrap}.row.inline.svelte-1cl3wo6{gap:.5rem}.row.inline.svelte-1cl3wo6 input[type=checkbox]:where(.svelte-1cl3wo6){width:auto}.banner.svelte-1cl3wo6{display:flex;align-items:center;justify-content:space-between;gap:var(--space-3);padding:var(--space-3);border-radius:var(--radius);background:var(--color-info-bg);color:var(--color-info);border:1px solid var(--color-info);flex-wrap:wrap}.kv.svelte-1cl3wo6{margin:var(--space-3) 0;display:grid;grid-template-columns:max-content 1fr;gap:var(--space-1) var(--space-3);font-size:.92rem}.kv.svelte-1cl3wo6>div:where(.svelte-1cl3wo6){display:contents}.kv.svelte-1cl3wo6 dt:where(.svelte-1cl3wo6){color:var(--color-text-muted)}.kv.svelte-1cl3wo6 dd:where(.svelte-1cl3wo6){margin:0}.copy-row.svelte-1cl3wo6{display:flex;align-items:center;gap:.375rem}.actions.svelte-1cl3wo6{display:flex;justify-content:flex-end}.small.svelte-1cl3wo6{font-size:.85rem}.error.svelte-1cl3wo6{color:var(--color-danger)}.list.svelte-ngb3vw{list-style:none;padding:0;margin:0;display:flex;flex-direction:column;gap:.5rem}.list.svelte-ngb3vw li:where(.svelte-ngb3vw){display:flex;align-items:center;justify-content:space-between;gap:var(--space-3);padding:var(--space-3);border:1px solid var(--color-border);border-radius:var(--radius);background:var(--color-surface-alt)}.strand-main.svelte-ngb3vw{display:flex;flex-direction:column;gap:.25rem;min-width:0}.strand-id.svelte-ngb3vw{overflow-wrap:anywhere;user-select:text}.error.svelte-ngb3vw{color:var(--color-danger)}.meta.svelte-ngb3vw{gap:.5rem}.small.svelte-ngb3vw{font-size:.85rem}.warning.svelte-ngb3vw{background:var(--color-warn-bg);border-color:var(--color-warn)}.warning.svelte-ngb3vw code:where(.svelte-ngb3vw){overflow-wrap:anywhere}.actions.svelte-ngb3vw{display:flex;justify-content:flex-end;gap:.5rem}.backdrop.svelte-1gylbi3{position:fixed;inset:0;background:#0f111673;z-index:1000}.dialog.svelte-1gylbi3{position:fixed;top:50%;left:50%;transform:translate(-50%,-50%);background:var(--color-surface);border:1px solid var(--color-border);border-radius:var(--radius-lg);box-shadow:var(--shadow-lg);padding:var(--space-5);width:min(28rem,calc(100vw - 2rem));max-height:calc(100vh - 2rem);overflow:auto;overflow-wrap:anywhere;z-index:1010}.actions.svelte-1gylbi3{display:flex;justify-content:flex-end;gap:.5rem;margin-top:var(--space-4)}.small.svelte-1gylbi3{font-size:.85rem}.page-header.svelte-1jncsno{display:flex;align-items:flex-end;justify-content:space-between;flex-wrap:wrap;gap:var(--space-3)}.list.svelte-1jncsno{list-style:none;padding:0;margin:0;display:flex;flex-direction:column;gap:.5rem}.list.svelte-1jncsno>li:where(.svelte-1jncsno){display:flex;align-items:center;justify-content:space-between;flex-wrap:wrap;gap:var(--space-3);padding:var(--space-3);border:1px solid var(--color-border);border-radius:var(--radius);background:var(--color-surface-alt)}.grant-main.svelte-1jncsno{display:flex;flex-direction:column;gap:.25rem;min-width:0;overflow-wrap:anywhere}.grant-label.svelte-1jncsno{font-weight:500}.meta.svelte-1jncsno{gap:.5rem}.donations.svelte-1jncsno{list-style:none;padding:0;margin:.25rem 0 0;display:flex;flex-direction:column;gap:.125rem}.donations.svelte-1jncsno li:where(.svelte-1jncsno){display:flex;flex-wrap:wrap;gap:.5rem}.actions.svelte-1jncsno{display:flex;gap:.5rem}.keep.svelte-1jncsno{display:flex;align-items:center;gap:.5rem;margin-top:var(--space-3);font-size:.9rem;font-weight:400;color:var(--color-text)}.error.svelte-1jncsno{color:var(--color-danger)}.small.svelte-1jncsno{font-size:.85rem}.toast-stack.svelte-g7yjw8{position:fixed;right:1rem;bottom:1rem;display:flex;flex-direction:column;gap:.5rem;max-width:min(30rem,calc(100vw - 2rem));z-index:1100}.toast.svelte-g7yjw8{background:var(--color-surface);color:var(--color-text);border:1px solid var(--color-border-strong);border-radius:var(--radius);padding:.625rem .75rem;display:flex;align-items:center;gap:.5rem;box-shadow:var(--shadow);font-size:.92rem}.toast-error.svelte-g7yjw8{border-color:var(--color-danger);background:var(--color-danger-bg);color:var(--color-danger)}.toast-success.svelte-g7yjw8{border-color:var(--color-success);background:var(--color-success-bg);color:var(--color-success)}.text.svelte-g7yjw8{flex:1 1 auto}button.svelte-g7yjw8{font-size:1.1rem;line-height:1;padding:.125rem .5rem;color:currentColor}.topbar.svelte-1r8vfx3{position:sticky;top:0;z-index:10;display:flex;align-items:center;justify-content:space-between;gap:var(--space-4);padding:var(--space-3) var(--space-5);background:var(--color-surface);border-bottom:1px solid var(--color-border);backdrop-filter:saturate(180%) blur(8px)}.brand.svelte-1r8vfx3{display:flex;align-items:center;gap:.6rem;font-weight:600}.logo.svelte-1r8vfx3{color:var(--color-accent);font-size:1.1rem}nav.svelte-1r8vfx3{display:flex;gap:.125rem}nav.svelte-1r8vfx3 a:where(.svelte-1r8vfx3){font-size:.92rem;color:var(--color-text-muted);padding:.375rem .75rem;border-radius:var(--radius);text-decoration:none}nav.svelte-1r8vfx3 a:where(.svelte-1r8vfx3):hover{background:var(--color-surface-alt);color:var(--color-text)}nav.svelte-1r8vfx3 a.active:where(.svelte-1r8vfx3){color:var(--color-accent-on);background:var(--color-accent)}main.svelte-1r8vfx3{flex:1 1 auto;width:min(64rem,100%);margin:0 auto;padding:var(--space-5)}@media(max-width:640px){.topbar.svelte-1r8vfx3{flex-direction:column;align-items:flex-start;gap:var(--space-2);padding:var(--space-2) var(--space-3)}nav.svelte-1r8vfx3{width:100%;overflow-x:auto}main.svelte-1r8vfx3{padding:var(--space-3)}}:root{color-scheme:light dark;--font-sans: ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;--font-mono: ui-monospace, "SF Mono", "Cascadia Code", "Roboto Mono", Menlo, monospace;--radius-sm: .25rem;--radius: .5rem;--radius-lg: .75rem;--space-1: .25rem;--space-2: .5rem;--space-3: .75rem;--space-4: 1rem;--space-5: 1.5rem;--space-6: 2rem;--space-7: 3rem;--color-bg: #f7f8fa;--color-surface: #ffffff;--color-surface-alt: #f0f2f6;--color-border: #d9dde3;--color-border-strong: #b8bec7;--color-text: #1d1f24;--color-text-muted: #5b626d;--color-text-soft: #8a8f99;--color-accent: #1d4ed8;--color-accent-strong: #1e3a8a;--color-accent-on: #ffffff;--color-accent-soft: #e0e7ff;--color-success: #15803d;--color-success-bg: #dcfce7;--color-warn: #b45309;--color-warn-bg: #fef3c7;--color-danger: #b91c1c;--color-danger-bg: #fee2e2;--color-info: #1d4ed8;--color-info-bg: #dbeafe;--shadow-sm: 0 1px 2px rgba(15, 23, 42, .05);--shadow: 0 4px 12px rgba(15, 23, 42, .08);--shadow-lg: 0 12px 32px rgba(15, 23, 42, .16)}@media(prefers-color-scheme:dark){:root{--color-bg: #0f1116;--color-surface: #181b22;--color-surface-alt: #20242d;--color-border: #2c313c;--color-border-strong: #3b424f;--color-text: #e8eaf0;--color-text-muted: #a4abb8;--color-text-soft: #777e8b;--color-accent: #93c5fd;--color-accent-strong: #bfdbfe;--color-accent-on: #0f1116;--color-accent-soft: #1e293b;--color-success: #4ade80;--color-success-bg: #14532d;--color-warn: #fbbf24;--color-warn-bg: #78350f;--color-danger: #f87171;--color-danger-bg: #7f1d1d;--color-info: #93c5fd;--color-info-bg: #1e3a8a;--shadow-sm: 0 1px 2px rgba(0, 0, 0, .4);--shadow: 0 4px 12px rgba(0, 0, 0, .45);--shadow-lg: 0 12px 32px rgba(0, 0, 0, .6)}}*,*:before,*:after{box-sizing:border-box}html,body{margin:0;padding:0;background:var(--color-bg);color:var(--color-text);font-family:var(--font-sans);font-size:15px;line-height:1.5;-webkit-font-smoothing:antialiased;min-height:100vh}#app{min-height:100vh;display:flex;flex-direction:column}h1,h2,h3,h4,h5,h6{margin:0 0 var(--space-2) 0;font-weight:600;line-height:1.25;letter-spacing:-.01em}h1{font-size:1.5rem}h2{font-size:1.25rem}h3{font-size:1.05rem}p{margin:0 0 var(--space-3) 0}a{color:var(--color-accent);text-decoration:none}a:hover{text-decoration:underline}button{font:inherit;cursor:pointer;border:1px solid var(--color-border-strong);background:var(--color-surface);color:var(--color-text);padding:var(--space-2) var(--space-3);border-radius:var(--radius);transition:background-color .12s ease,border-color .12s ease}button:hover:not(:disabled){background:var(--color-surface-alt);border-color:var(--color-text-muted)}button:focus-visible,a:focus-visible,input:focus-visible,select:focus-visible,textarea:focus-visible{outline:2px solid var(--color-accent);outline-offset:2px}button:disabled{opacity:.55;cursor:not-allowed}button.primary{background:var(--color-accent);border-color:var(--color-accent);color:var(--color-accent-on)}button.primary:hover:not(:disabled){background:var(--color-accent-strong);border-color:var(--color-accent-strong)}button.danger{background:var(--color-danger);border-color:var(--color-danger);color:#fff}button.ghost{background:transparent;border-color:transparent;color:var(--color-text-muted);padding:var(--space-1) var(--space-2)}button.ghost:hover:not(:disabled){color:var(--color-text);background:var(--color-surface-alt)}input[type=text],input[type=url],input[type=number],input[type=password],select,textarea{font:inherit;color:var(--color-text);background:var(--color-surface);border:1px solid var(--color-border-strong);border-radius:var(--radius);padding:var(--space-2) var(--space-3);width:100%}label{display:block;font-size:.85rem;color:var(--color-text-muted);font-weight:500;margin-bottom:var(--space-1)}code,pre{font-family:var(--font-mono);font-size:.85rem}pre{background:var(--color-surface-alt);padding:var(--space-3);border-radius:var(--radius);overflow:auto;margin:0}hr{border:none;border-top:1px solid var(--color-border);margin:var(--space-5) 0}.card{background:var(--color-surface);border:1px solid var(--color-border);border-radius:var(--radius-lg);padding:var(--space-5);box-shadow:var(--shadow-sm)}.muted{color:var(--color-text-muted)}.row{display:flex;gap:var(--space-3);align-items:center;flex-wrap:wrap}.stack{display:flex;flex-direction:column;gap:var(--space-3)}.grid{display:grid;gap:var(--space-4)}.badge{display:inline-flex;align-items:center;gap:var(--space-1);font-size:.78rem;font-weight:600;letter-spacing:.02em;text-transform:uppercase;padding:.125rem .5rem;border-radius:999px;background:var(--color-surface-alt);color:var(--color-text-muted)}.badge.ok{background:var(--color-success-bg);color:var(--color-success)}.badge.warn{background:var(--color-warn-bg);color:var(--color-warn)}.badge.err{background:var(--color-danger-bg);color:var(--color-danger)}.badge.info{background:var(--color-info-bg);color:var(--color-info)}.visually-hidden{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0}
@@ -1,192 +0,0 @@
1
- /**
2
- * CLI smoke test for the `invite` subcommand. Spawns a stub HTTP server
3
- * playing the role of cadre-host's management API and asserts the CLI:
4
- * - POSTs the expected body to /auth/invites
5
- * - Prints the encodedInvite on stdout
6
- * - Exits 0 on success / non-zero on connect failure
7
- *
8
- * Relies on `yarn build` having produced dist/bin/host.js.
9
- */
10
-
11
- import { spawn } from 'node:child_process';
12
- import { createServer, type IncomingMessage, type Server, type ServerResponse } from 'node:http';
13
- import { existsSync } from 'node:fs';
14
- import { fileURLToPath } from 'node:url';
15
- import { dirname, resolve } from 'node:path';
16
- import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
17
-
18
- const here = dirname(fileURLToPath(import.meta.url));
19
- const binPath = resolve(here, '../../dist/bin/host.js');
20
-
21
- // These smoke tests cold-start the built CLI as a child process (Node boot +,
22
- // for some, an HTTP server). That comfortably exceeds vitest's 5s default under
23
- // heavy parallel load, so give the subprocess generous headroom.
24
- vi.setConfig({ testTimeout: 30000 });
25
-
26
- interface StubServer {
27
- server: Server;
28
- port: number;
29
- requests: Array<{ method: string; url: string; body: string }>;
30
- setResponse(handler: (req: IncomingMessage, res: ServerResponse) => void): void;
31
- }
32
-
33
- async function startStub(): Promise<StubServer> {
34
- const requests: StubServer['requests'] = [];
35
- let handler: (req: IncomingMessage, res: ServerResponse) => void = (_req, res) => {
36
- res.statusCode = 200;
37
- res.setHeader('content-type', 'application/json');
38
- res.end(JSON.stringify({
39
- encodedInvite: 'fake-encoded-invite',
40
- token: 'fake-token',
41
- expiresAt: new Date(Date.now() + 60_000).toISOString(),
42
- }));
43
- };
44
-
45
- const server = createServer((req, res) => {
46
- const chunks: Buffer[] = [];
47
- req.on('data', (chunk: Buffer) => chunks.push(chunk));
48
- req.on('end', () => {
49
- requests.push({ method: req.method ?? '', url: req.url ?? '', body: Buffer.concat(chunks).toString('utf8') });
50
- handler(req, res);
51
- });
52
- });
53
-
54
- await new Promise<void>((resolveStart) => server.listen(0, '127.0.0.1', () => resolveStart()));
55
- const addr = server.address();
56
- const port = typeof addr === 'object' && addr ? addr.port : 0;
57
- return {
58
- server,
59
- port,
60
- requests,
61
- setResponse: (h) => { handler = h; },
62
- };
63
- }
64
-
65
- function runCli(args: string[]): Promise<{ code: number; stdout: string; stderr: string }> {
66
- return new Promise((resolveRun) => {
67
- const child = spawn(process.execPath, [binPath, ...args], { stdio: ['ignore', 'pipe', 'pipe'] });
68
- let stdout = '';
69
- let stderr = '';
70
- child.stdout.on('data', (chunk: Buffer) => { stdout += chunk.toString('utf8'); });
71
- child.stderr.on('data', (chunk: Buffer) => { stderr += chunk.toString('utf8'); });
72
- child.on('close', (code) => resolveRun({ code: code ?? -1, stdout, stderr }));
73
- });
74
- }
75
-
76
- let stub: StubServer;
77
-
78
- beforeEach(async () => {
79
- if (!existsSync(binPath)) {
80
- throw new Error(`cadre-host bin not built at ${binPath}; run \`yarn build\` first`);
81
- }
82
- stub = await startStub();
83
- });
84
-
85
- afterEach(async () => {
86
- await new Promise<void>((resolveClose) => stub.server.close(() => resolveClose()));
87
- });
88
-
89
- describe('cadre-host invite CLI', () => {
90
- it('POSTs to /auth/invites with label + ttlMs and prints encoded invite', async () => {
91
- const result = await runCli(['invite', "Mom's phone", '--ttl', '12h', '--port', String(stub.port)]);
92
-
93
- expect(result.code).toBe(0);
94
- expect(result.stdout.trim()).toBe('fake-encoded-invite');
95
- expect(stub.requests).toHaveLength(1);
96
- expect(stub.requests[0]!.method).toBe('POST');
97
- expect(stub.requests[0]!.url).toBe('/auth/invites');
98
- const body = JSON.parse(stub.requests[0]!.body);
99
- expect(body.label).toBe("Mom's phone");
100
- expect(body.ttlMs).toBe(12 * 60 * 60 * 1000);
101
- });
102
-
103
- it('exits non-zero when cadre-host returns an error', async () => {
104
- stub.setResponse((_req, res) => {
105
- res.statusCode = 500;
106
- res.end('boom');
107
- });
108
- const result = await runCli(['invite', 'Test', '--port', String(stub.port)]);
109
- expect(result.code).not.toBe(0);
110
- expect(result.stderr).toContain('500');
111
- });
112
-
113
- it('exits 2 (no service) when cadre-host is not reachable', async () => {
114
- // Use a port the stub server occupied; close it first so connections fail.
115
- const port = stub.port;
116
- await new Promise<void>((resolveClose) => stub.server.close(() => resolveClose()));
117
- const result = await runCli(['invite', 'Test', '--port', String(port)]);
118
- expect(result.code).toBe(2);
119
- expect(result.stderr).toMatch(/cadre-host start|Failed to reach/);
120
- });
121
-
122
- it('rejects invalid --ttl', async () => {
123
- const result = await runCli(['invite', 'Test', '--ttl', 'eternity', '--port', String(stub.port)]);
124
- expect(result.code).toBe(1);
125
- expect(result.stderr).toMatch(/Invalid --ttl/);
126
- });
127
- });
128
-
129
- describe('cadre-host trust list CLI', () => {
130
- it('renders members and pending invites', async () => {
131
- stub.setResponse((_req, res) => {
132
- res.statusCode = 200;
133
- res.setHeader('content-type', 'application/json');
134
- res.end(JSON.stringify({
135
- members: [
136
- { peerId: '12D3KooWAlice', label: 'Alice', addedAt: 't1' },
137
- { peerId: '12D3KooWHost', label: 'Host', addedAt: 't2', self: true },
138
- ],
139
- pending: [
140
- { token: 'tok1', label: 'Mom', createdAt: 't3', expiresAt: 't4' },
141
- ],
142
- }));
143
- });
144
-
145
- const result = await runCli(['trust', 'list', '--port', String(stub.port)]);
146
- expect(result.code).toBe(0);
147
- expect(result.stdout).toContain('12D3KooWAlice');
148
- expect(result.stdout).toContain('Alice');
149
- expect(result.stdout).toContain('[self]');
150
- expect(result.stdout).toContain('tok1');
151
- expect(result.stdout).toContain('Mom');
152
- });
153
- });
154
-
155
- describe('cadre-host trust revoke CLI', () => {
156
- it('DELETEs /auth/invites/<token> by default', async () => {
157
- stub.setResponse((_req, res) => {
158
- res.statusCode = 204;
159
- res.end();
160
- });
161
- const result = await runCli(['trust', 'revoke', 'some-token', '--port', String(stub.port)]);
162
- expect(result.code).toBe(0);
163
- expect(stub.requests[0]!.method).toBe('DELETE');
164
- expect(stub.requests[0]!.url).toBe('/auth/invites/some-token');
165
- });
166
-
167
- it('DELETEs /auth/members/<peerId> when id looks like a peerId', async () => {
168
- stub.setResponse((_req, res) => {
169
- res.statusCode = 204;
170
- res.end();
171
- });
172
- const result = await runCli(['trust', 'revoke', '12D3KooWAlice', '--port', String(stub.port)]);
173
- expect(result.code).toBe(0);
174
- expect(stub.requests[0]!.url).toBe('/auth/members/12D3KooWAlice');
175
- });
176
-
177
- it('treats secp256k1 peer IDs (16Uiu2HAm…) as members', async () => {
178
- stub.setResponse((_req, res) => {
179
- res.statusCode = 204;
180
- res.end();
181
- });
182
- const result = await runCli(['trust', 'revoke', '16Uiu2HAmExample', '--port', String(stub.port)]);
183
- expect(result.code).toBe(0);
184
- expect(stub.requests[0]!.url).toBe('/auth/members/16Uiu2HAmExample');
185
- });
186
-
187
- it('rejects invalid --port', async () => {
188
- const result = await runCli(['trust', 'revoke', 'some-token', '--port', 'garbage']);
189
- expect(result.code).toBe(1);
190
- expect(result.stderr).toMatch(/Invalid --port/);
191
- });
192
- });
@@ -1,123 +0,0 @@
1
- /**
2
- * Server-level smoke: the /auth/* HTTP contract over the owner-node
3
- * channel adapter. Validates the two behaviours the realignment must
4
- * preserve:
5
- * 1. With the node up, POST /auth/invites returns an encodedInvite (no 500).
6
- * 2. With the node refusing connections, GET /auth/trust-circle still lists
7
- * local labels, and POST /auth/invites returns 503 (not a raw 500).
8
- *
9
- * The owner node is represented by a stub HTTP server matching the 6.6
10
- * admin contract (the spawn path itself is covered by
11
- * orchestrator-owner.test.ts).
12
- */
13
-
14
- import { afterEach, beforeEach, describe, expect, it } from 'vitest';
15
- import Fastify from 'fastify';
16
- import http from 'node:http';
17
- import { mkdtempSync, rmSync } from 'node:fs';
18
- import { tmpdir } from 'node:os';
19
- import { join } from 'node:path';
20
-
21
- import { TrustCircleService, TrustCircleStore, createTrustCircleHandlers } from '../auth/index.js';
22
- import { OwnerNodeClient } from '../owner/index.js';
23
- import { registerErrorHandler } from '../server/error-handler.js';
24
- import { registerTrustCircleRoutes } from '../server/routes/trust-circle.js';
25
- import { EventBus } from '../server/events/bus.js';
26
-
27
- const TOKEN = 'tok';
28
-
29
- let tmpRoot: string;
30
- beforeEach(() => { tmpRoot = mkdtempSync(join(tmpdir(), 'cadre-host-smoke-')); });
31
- afterEach(() => { try { rmSync(tmpRoot, { recursive: true, force: true }); } catch { /* ignore */ } });
32
-
33
- function buildApp(service: TrustCircleService) {
34
- const app = Fastify();
35
- registerErrorHandler(app);
36
- registerTrustCircleRoutes(app, { handlers: createTrustCircleHandlers(service), events: new EventBus() });
37
- return app;
38
- }
39
-
40
- describe('host /auth delegation — node up', () => {
41
- let server: http.Server;
42
- let baseUrl: string;
43
-
44
- beforeEach(async () => {
45
- server = http.createServer((req, res) => {
46
- const chunks: Buffer[] = [];
47
- req.on('data', (c) => chunks.push(c as Buffer));
48
- req.on('end', () => {
49
- if (req.headers['authorization'] !== `Bearer ${TOKEN}`) {
50
- res.writeHead(401); res.end(JSON.stringify({ ok: false, error: { code: 'not_authorized', message: 'x' } })); return;
51
- }
52
- if (req.url === '/admin/invites' && req.method === 'POST') {
53
- res.writeHead(200, { 'content-type': 'application/json' });
54
- res.end(JSON.stringify({ ok: true, data: { invite: { partyId: 'p', ownerAddrs: [], token: 't', createdAt: 1 }, encodedInvite: 'ENC' } }));
55
- return;
56
- }
57
- if (req.url === '/admin/members' && req.method === 'GET') {
58
- res.writeHead(200, { 'content-type': 'application/json' });
59
- res.end(JSON.stringify({ ok: true, data: { members: [] } }));
60
- return;
61
- }
62
- res.writeHead(400); res.end(JSON.stringify({ ok: false, error: { code: 'bad_request', message: req.url } }));
63
- });
64
- });
65
- await new Promise<void>((resolve) => server.listen(0, '127.0.0.1', () => resolve()));
66
- const a = server.address();
67
- baseUrl = `http://127.0.0.1:${typeof a === 'object' && a ? a.port : 0}`;
68
- });
69
-
70
- afterEach(async () => { await new Promise<void>((resolve) => server.close(() => resolve())); });
71
-
72
- it('POST /auth/invites returns an encodedInvite (no 500)', async () => {
73
- const client = new OwnerNodeClient({ baseUrl, token: TOKEN });
74
- const service = new TrustCircleService({ cadreNode: client, store: new TrustCircleStore(tmpRoot) });
75
- const app = buildApp(service);
76
- try {
77
- const res = await app.inject({ method: 'POST', url: '/auth/invites', payload: { label: 'Mom' } });
78
- expect(res.statusCode).toBe(200);
79
- const body = res.json() as { encodedInvite: string };
80
- expect(body.encodedInvite).toBe('ENC');
81
- } finally {
82
- await app.close();
83
- }
84
- });
85
- });
86
-
87
- describe('host /auth delegation — node refusing connections', () => {
88
- let closedBaseUrl: string;
89
-
90
- beforeEach(async () => {
91
- const tmp = http.createServer();
92
- const port = await new Promise<number>((resolve) => {
93
- tmp.listen(0, '127.0.0.1', () => {
94
- const a = tmp.address();
95
- resolve(typeof a === 'object' && a ? a.port : 0);
96
- });
97
- });
98
- await new Promise<void>((resolve) => tmp.close(() => resolve()));
99
- closedBaseUrl = `http://127.0.0.1:${port}`;
100
- });
101
-
102
- it('GET /auth/trust-circle lists local labels and POST /auth/invites returns 503', async () => {
103
- const store = new TrustCircleStore(tmpRoot);
104
- store.addMember({ peerId: '12D3KooWLocal', label: 'My laptop', addedAt: '2025-01-01T00:00:00Z' });
105
-
106
- const client = new OwnerNodeClient({ baseUrl: closedBaseUrl, token: TOKEN });
107
- const service = new TrustCircleService({ cadreNode: client, store });
108
- const app = buildApp(service);
109
- try {
110
- const list = await app.inject({ method: 'GET', url: '/auth/trust-circle' });
111
- expect(list.statusCode).toBe(200);
112
- const snap = list.json() as { members: Array<{ peerId: string; label: string }> };
113
- expect(snap.members.some((m) => m.peerId === '12D3KooWLocal' && m.label === 'My laptop')).toBe(true);
114
-
115
- const invite = await app.inject({ method: 'POST', url: '/auth/invites', payload: { label: 'Dad' } });
116
- expect(invite.statusCode).toBe(503);
117
- const body = invite.json() as { error: { code: string } };
118
- expect(body.error.code).toBe('node_unavailable');
119
- } finally {
120
- await app.close();
121
- }
122
- });
123
- });
@@ -1,234 +0,0 @@
1
- /**
2
- * HostProcessOrchestrator owner-node lifecycle tests.
3
- *
4
- * Uses the `spawn.entrypoint` hook with a fake CLI that writes its startup
5
- * token and binds a minimal `/admin/identity` endpoint on the admin port —
6
- * enough to validate spawn, idempotency, admin-port allocation/persistence,
7
- * the end-to-end OwnerNodeClient round-trip, and init() re-attach.
8
- */
9
-
10
- import { afterEach, beforeEach, describe, expect, it } from 'vitest';
11
- import { existsSync, mkdtempSync, mkdirSync, rmSync, writeFileSync } from 'node:fs';
12
- import { tmpdir } from 'node:os';
13
- import { join } from 'node:path';
14
-
15
- import { HostProcessOrchestrator, OWNER_CONTAINER_ID } from '../orchestrator/host-process-orchestrator.js';
16
- import { StateStore } from '../orchestrator/state-store.js';
17
- import { isPidAlive } from '../orchestrator/pid-liveness.js';
18
- import { decodeDockerId } from '../orchestrator/types.js';
19
- import { OwnerNodeClient } from '../owner/owner-node-client.js';
20
- import { removeAllNodes } from './orchestrator-teardown.js';
21
-
22
- const FAKE_OWNER_CLI = `
23
- import fs from 'node:fs';
24
- import http from 'node:http';
25
-
26
- const args = process.argv.slice(2);
27
- const get = (flag) => { const i = args.indexOf(flag); return i >= 0 ? args[i + 1] : undefined; };
28
-
29
- const tokenPath = get('--startup-token-file');
30
- const adminPort = get('--admin-port');
31
- const token = process.env.CADRE_STARTUP_TOKEN ?? '';
32
-
33
- if (tokenPath && token) {
34
- try { fs.writeFileSync(tokenPath, token, 'utf8'); } catch (err) { console.error('token write failed', err); }
35
- }
36
-
37
- if (adminPort && token) {
38
- const server = http.createServer((req, res) => {
39
- const auth = req.headers['authorization'];
40
- if (auth !== 'Bearer ' + token) {
41
- res.writeHead(401, { 'content-type': 'application/json' });
42
- res.end(JSON.stringify({ ok: false, error: { code: 'not_authorized', message: 'bad token' } }));
43
- return;
44
- }
45
- if (req.url === '/admin/identity' && req.method === 'GET') {
46
- res.writeHead(200, { 'content-type': 'application/json' });
47
- res.end(JSON.stringify({ ok: true, data: { peerId: 'FAKEPEER', partyId: 'fake-party' } }));
48
- return;
49
- }
50
- res.writeHead(400, { 'content-type': 'application/json' });
51
- res.end(JSON.stringify({ ok: false, error: { code: 'bad_request', message: req.url } }));
52
- });
53
- server.listen(parseInt(adminPort, 10), '127.0.0.1', () => {
54
- console.log('admin listening on ' + adminPort);
55
- });
56
- }
57
-
58
- process.on('SIGTERM', () => process.exit(0));
59
- console.log('fake owner up');
60
- setInterval(() => {}, 1 << 30);
61
- `;
62
-
63
- let tmpRoot: string;
64
- let scriptPath: string;
65
- const orchestrators: HostProcessOrchestrator[] = [];
66
-
67
- beforeEach(() => {
68
- tmpRoot = mkdtempSync(join(tmpdir(), 'cadre-host-auth-'));
69
- scriptPath = join(tmpRoot, 'fake-owner.mjs');
70
- writeFileSync(scriptPath, FAKE_OWNER_CLI, 'utf8');
71
- });
72
-
73
- afterEach(async () => {
74
- try {
75
- await removeAllNodes(orchestrators);
76
- } finally {
77
- await sleep(50);
78
- try { rmSync(tmpRoot, { recursive: true, force: true }); } catch { /* ignore */ }
79
- }
80
- });
81
-
82
- function makeOrchestrator(rootDir: string): HostProcessOrchestrator {
83
- mkdirSync(rootDir, { recursive: true });
84
- const orch = new HostProcessOrchestrator({
85
- rootDir,
86
- portRange: { start: 18500, end: 18999 },
87
- stopTimeoutMs: 1500,
88
- spawn: { entrypoint: scriptPath },
89
- });
90
- orchestrators.push(orch);
91
- return orch;
92
- }
93
-
94
- async function waitFor(predicate: () => boolean | Promise<boolean>, timeoutMs = 4000, intervalMs = 50): Promise<void> {
95
- const deadline = Date.now() + timeoutMs;
96
- while (Date.now() < deadline) {
97
- if (await predicate()) return;
98
- await sleep(intervalMs);
99
- }
100
- throw new Error('waitFor: timed out');
101
- }
102
-
103
- function sleep(ms: number): Promise<void> {
104
- return new Promise((res) => setTimeout(res, ms));
105
- }
106
-
107
- const CFG = { identityPath: 'C:/fake/identity.key', partyId: 'install-id-123', libp2pPort: 4555 };
108
-
109
- describe('HostProcessOrchestrator.ensureOwnerNode', () => {
110
- it('spawns the owner node, allocates+persists an admin port', async () => {
111
- const orch = makeOrchestrator(join(tmpRoot, 'a'));
112
- await orch.init();
113
- const node = await orch.ensureOwnerNode(CFG);
114
-
115
- expect(node.id).toBe(OWNER_CONTAINER_ID);
116
- expect(node.owner).toBe(true);
117
- expect(node.ports.admin).toBeGreaterThanOrEqual(18500);
118
- expect(node.ports.p2p).toBe(4555);
119
-
120
- const endpoint = orch.getOwnerAdminEndpoint();
121
- expect(endpoint).toBeDefined();
122
- expect(endpoint!.baseUrl).toBe(`http://127.0.0.1:${node.ports.admin}`);
123
-
124
- // Persisted with admin port + owner flag + ownerConfig.
125
- const persisted = new StateStore(join(tmpRoot, 'a')).load();
126
- expect(persisted.ownerConfig).toMatchObject({ partyId: 'install-id-123', libp2pPort: 4555 });
127
- const ph = persisted.handles.find((h) => h.containerId === OWNER_CONTAINER_ID);
128
- expect(ph?.owner).toBe(true);
129
- expect(typeof ph?.ports.admin).toBe('number');
130
- });
131
-
132
- it('is idempotent — a second call does not spawn a second child', async () => {
133
- const orch = makeOrchestrator(join(tmpRoot, 'b'));
134
- await orch.init();
135
- const first = await orch.ensureOwnerNode(CFG);
136
- await waitFor(() => orch.isRunning(first.dockerId));
137
-
138
- const second = await orch.ensureOwnerNode(CFG);
139
- expect(second.dockerId).toBe(first.dockerId);
140
- expect(orch.listNodes()).toHaveLength(1);
141
- });
142
-
143
- it('the OwnerNodeClient reaches the spawned admin channel', async () => {
144
- const orch = makeOrchestrator(join(tmpRoot, 'c'));
145
- await orch.init();
146
- const node = await orch.ensureOwnerNode(CFG);
147
- await waitFor(() => orch.isRunning(node.dockerId));
148
-
149
- const client = new OwnerNodeClient(() => orch.getOwnerAdminEndpoint());
150
- await waitFor(async () => {
151
- try { return (await client.getPeerId()) === 'FAKEPEER'; } catch { return false; }
152
- });
153
- expect(await client.getPeerId()).toBe('FAKEPEER');
154
- });
155
-
156
- it('getOwnerAdminEndpoint() survives an init() re-attach', async () => {
157
- const rootDir = join(tmpRoot, 'd');
158
- const a = makeOrchestrator(rootDir);
159
- await a.init();
160
- const node = await a.ensureOwnerNode(CFG);
161
- // Not `a.isRunning`: that answers from the ChildProcess at once, but `b`
162
- // below can only recognise the node once it has written its startup token.
163
- await waitFor(() => existsSync(join(rootDir, OWNER_CONTAINER_ID, '.startup-token')));
164
- const before = a.getOwnerAdminEndpoint();
165
-
166
- const b = makeOrchestrator(rootDir);
167
- await b.init();
168
- expect(b.isOwnerNode(OWNER_CONTAINER_ID)).toBe(true);
169
- expect(b.hasOwnerConfig()).toBe(true);
170
- const after = b.getOwnerAdminEndpoint();
171
- expect(after).toEqual(before);
172
- expect(await b.isRunning(node.dockerId)).toBe(true);
173
-
174
- const { pid } = decodeDockerId(node.dockerId);
175
- expect(isPidAlive(pid)).toBe(true);
176
- });
177
-
178
- // The owner's own guard arm: its admin channel binds on loopback, so the probe
179
- // must check 127.0.0.1 too — the fake owner binds nothing else.
180
- it('refuses to re-spawn over a re-attached owner whose admin port is still bound', async () => {
181
- const rootDir = join(tmpRoot, 'g');
182
- const a = makeOrchestrator(rootDir);
183
- await a.init();
184
- const node = await a.ensureOwnerNode(CFG);
185
- const tokenPath = join(rootDir, OWNER_CONTAINER_ID, '.startup-token');
186
- await waitFor(async () => (await a.getLogs(node.dockerId)).includes('admin listening'));
187
-
188
- // Unverified as far as `b` can tell: live pid, token not (yet) matching.
189
- writeFileSync(tokenPath, 'not-yet', 'utf8');
190
- const b = makeOrchestrator(rootDir);
191
- await b.init();
192
- expect(await b.isRunning(node.dockerId)).toBe(false);
193
-
194
- await expect(b.ensureOwnerNode(CFG)).rejects.toThrow(
195
- new RegExp(`owner may still be running: port ${node.ports.admin} on 127\\.0\\.0\\.1 is unavailable`),
196
- );
197
- expect(b.listNodes().map((n) => n.dockerId)).toEqual([node.dockerId]);
198
- expect(isPidAlive(decodeDockerId(node.dockerId).pid)).toBe(true);
199
- });
200
-
201
- it('restartOwnerNode preserves the workdir and re-spawns', async () => {
202
- const orch = makeOrchestrator(join(tmpRoot, 'e'));
203
- await orch.init();
204
- const first = await orch.ensureOwnerNode(CFG);
205
- await waitFor(() => orch.isRunning(first.dockerId));
206
- const { pid: firstPid } = decodeDockerId(first.dockerId);
207
-
208
- const restarted = await orch.restartOwnerNode();
209
- await waitFor(() => orch.isRunning(restarted.dockerId));
210
-
211
- // New child (different pid/token), still exactly one node, workdir reused.
212
- expect(restarted.dockerId).not.toBe(first.dockerId);
213
- expect(orch.listNodes()).toHaveLength(1);
214
- await waitFor(() => !isPidAlive(firstPid));
215
- });
216
-
217
- // Like a lent node, the owner node comes back on its previous ports even when lower
218
- // ones are free. Only its TCP port differs: the configured libp2pPort wins over the
219
- // previous handle's, because the NAT mapping points at the configured one.
220
- it('re-spawns on its previous ports, taking p2p from the current config', async () => {
221
- const orch = makeOrchestrator(join(tmpRoot, 'f'));
222
- await orch.init();
223
- const lower = await orch.createContainer({ containerId: 'c0', partyId: 'party-c0', bootstrapNodes: [], profile: 'transaction' });
224
- const first = await orch.ensureOwnerNode(CFG);
225
- await waitFor(() => orch.isRunning(first.dockerId));
226
-
227
- await orch.removeContainer(lower.dockerId);
228
- await orch.stopOwnerNode();
229
- const respawned = await orch.ensureOwnerNode({ ...CFG, libp2pPort: 4556 });
230
-
231
- expect(respawned.dockerId).not.toBe(first.dockerId);
232
- expect(respawned.ports).toEqual({ ...first.ports, p2p: 4556 });
233
- });
234
- });