@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 +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
- });