@serfab/cadre-host 0.8.1 → 0.10.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 (198) hide show
  1. package/dist/auth/index.d.ts +2 -0
  2. package/dist/auth/index.js +1 -0
  3. package/dist/auth/index.js.map +1 -1
  4. package/dist/auth/self-label.d.ts +36 -0
  5. package/dist/auth/self-label.js +57 -0
  6. package/dist/auth/self-label.js.map +1 -0
  7. package/dist/auth/trust-circle.d.ts +36 -9
  8. package/dist/auth/trust-circle.js +40 -20
  9. package/dist/auth/trust-circle.js.map +1 -1
  10. package/dist/auth/types.d.ts +2 -2
  11. package/dist/bin/host.js +291 -54
  12. package/dist/bin/host.js.map +1 -1
  13. package/dist/donation/__tests__/fake-orchestrator.d.ts +103 -0
  14. package/dist/donation/__tests__/fake-orchestrator.js +180 -0
  15. package/dist/donation/__tests__/fake-orchestrator.js.map +1 -0
  16. package/dist/donation/donation-service.d.ts +361 -0
  17. package/dist/donation/donation-service.js +763 -0
  18. package/dist/donation/donation-service.js.map +1 -0
  19. package/dist/donation/donation-store.d.ts +49 -0
  20. package/dist/donation/donation-store.js +148 -0
  21. package/dist/donation/donation-store.js.map +1 -0
  22. package/dist/donation/donation-supervisor.d.ts +167 -0
  23. package/dist/donation/donation-supervisor.js +341 -0
  24. package/dist/donation/donation-supervisor.js.map +1 -0
  25. package/dist/donation/grant-service.d.ts +71 -0
  26. package/dist/donation/grant-service.js +163 -0
  27. package/dist/donation/grant-service.js.map +1 -0
  28. package/dist/donation/grant-store.d.ts +38 -0
  29. package/dist/donation/grant-store.js +135 -0
  30. package/dist/donation/grant-store.js.map +1 -0
  31. package/dist/donation/index.d.ts +20 -0
  32. package/dist/donation/index.js +17 -0
  33. package/dist/donation/index.js.map +1 -0
  34. package/dist/donation/types.d.ts +201 -0
  35. package/dist/donation/types.js +40 -0
  36. package/dist/donation/types.js.map +1 -0
  37. package/dist/index.d.ts +9 -5
  38. package/dist/index.js +7 -3
  39. package/dist/index.js.map +1 -1
  40. package/dist/installer/config.d.ts +26 -0
  41. package/dist/installer/config.js +13 -0
  42. package/dist/installer/config.js.map +1 -1
  43. package/dist/installer/index.d.ts +2 -0
  44. package/dist/installer/index.js +3 -0
  45. package/dist/installer/index.js.map +1 -1
  46. package/dist/installer/wizard.d.ts +4 -0
  47. package/dist/installer/wizard.js +5 -1
  48. package/dist/installer/wizard.js.map +1 -1
  49. package/dist/nat/nat-service.d.ts +5 -5
  50. package/dist/nat/nat-service.js +9 -9
  51. package/dist/nat/nat-service.js.map +1 -1
  52. package/dist/nat/types.d.ts +1 -1
  53. package/dist/orchestrator/host-process-orchestrator.d.ts +144 -32
  54. package/dist/orchestrator/host-process-orchestrator.js +397 -128
  55. package/dist/orchestrator/host-process-orchestrator.js.map +1 -1
  56. package/dist/orchestrator/index.d.ts +3 -3
  57. package/dist/orchestrator/index.js +1 -1
  58. package/dist/orchestrator/index.js.map +1 -1
  59. package/dist/orchestrator/node-identity.d.ts +26 -0
  60. package/dist/orchestrator/node-identity.js +36 -0
  61. package/dist/orchestrator/node-identity.js.map +1 -0
  62. package/dist/orchestrator/port-allocator.d.ts +14 -0
  63. package/dist/orchestrator/port-allocator.js +45 -0
  64. package/dist/orchestrator/port-allocator.js.map +1 -1
  65. package/dist/orchestrator/state-store.d.ts +5 -5
  66. package/dist/orchestrator/state-store.js +2 -2
  67. package/dist/orchestrator/state-store.js.map +1 -1
  68. package/dist/orchestrator/types.d.ts +8 -8
  69. package/dist/owner/index.d.ts +11 -0
  70. package/dist/owner/index.js +11 -0
  71. package/dist/owner/index.js.map +1 -0
  72. package/dist/owner/owner-node-client.d.ts +110 -0
  73. package/dist/{authority/authority-node-client.js → owner/owner-node-client.js} +58 -17
  74. package/dist/owner/owner-node-client.js.map +1 -0
  75. package/dist/server/error-handler.d.ts +22 -1
  76. package/dist/server/error-handler.js +64 -1
  77. package/dist/server/error-handler.js.map +1 -1
  78. package/dist/server/events/types.d.ts +3 -0
  79. package/dist/server/index.d.ts +33 -2
  80. package/dist/server/index.js +43 -16
  81. package/dist/server/index.js.map +1 -1
  82. package/dist/server/routes/grants-admin.d.ts +17 -0
  83. package/dist/server/routes/grants-admin.js +32 -0
  84. package/dist/server/routes/grants-admin.js.map +1 -0
  85. package/dist/server/routes/grants.d.ts +28 -0
  86. package/dist/server/routes/grants.js +182 -0
  87. package/dist/server/routes/grants.js.map +1 -0
  88. package/dist/server/routes/nodes.d.ts +5 -4
  89. package/dist/server/routes/nodes.js +19 -18
  90. package/dist/server/routes/nodes.js.map +1 -1
  91. package/dist/server/routes/settings.d.ts +6 -1
  92. package/dist/server/routes/settings.js +5 -2
  93. package/dist/server/routes/settings.js.map +1 -1
  94. package/dist/server/routes/status.d.ts +8 -4
  95. package/dist/server/routes/status.js +9 -4
  96. package/dist/server/routes/status.js.map +1 -1
  97. package/dist/server/routes/strands.d.ts +20 -0
  98. package/dist/server/routes/strands.js +38 -0
  99. package/dist/server/routes/strands.js.map +1 -0
  100. package/dist/strands/index.d.ts +7 -0
  101. package/dist/strands/index.js +6 -0
  102. package/dist/strands/index.js.map +1 -0
  103. package/dist/strands/strand-service.d.ts +50 -0
  104. package/dist/strands/strand-service.js +103 -0
  105. package/dist/strands/strand-service.js.map +1 -0
  106. package/dist/strands/types.d.ts +93 -0
  107. package/dist/strands/types.js +23 -0
  108. package/dist/strands/types.js.map +1 -0
  109. package/dist/ui/assets/index-BVoqLjyX.css +1 -0
  110. package/dist/ui/assets/index-C-iks41M.js +17 -0
  111. package/dist/ui/assets/index-C-iks41M.js.map +1 -0
  112. package/dist/ui/index.html +2 -2
  113. package/dist/update/release-key.d.ts +1 -1
  114. package/dist/update/release-key.js +8 -6
  115. package/dist/update/release-key.js.map +1 -1
  116. package/package.json +5 -5
  117. package/src/__tests__/build-targets.test.ts +26 -0
  118. package/src/__tests__/global-setup.ts +51 -0
  119. package/src/__tests__/{host-authority.smoke.test.ts → host-owner.smoke.test.ts} +7 -7
  120. package/src/__tests__/orchestrator-env-scrub.test.ts +140 -0
  121. package/src/__tests__/orchestrator-node-identity.test.ts +204 -0
  122. package/src/__tests__/{orchestrator-authority.test.ts → orchestrator-owner.test.ts} +33 -33
  123. package/src/__tests__/orchestrator-pin-keys.test.ts +153 -0
  124. package/src/__tests__/orchestrator-push.test.ts +10 -10
  125. package/src/__tests__/orchestrator.test.ts +387 -6
  126. package/src/auth/__tests__/self-label.test.ts +85 -0
  127. package/src/auth/__tests__/trust-circle-integration.test.ts +152 -147
  128. package/src/auth/__tests__/trust-circle.test.ts +55 -11
  129. package/src/auth/index.ts +2 -0
  130. package/src/auth/self-label.ts +74 -0
  131. package/src/auth/trust-circle.ts +54 -22
  132. package/src/auth/types.ts +2 -2
  133. package/src/bin/host.ts +1299 -1038
  134. package/src/donation/__tests__/donation-service.test.ts +1068 -0
  135. package/src/donation/__tests__/donation-supervisor.test.ts +510 -0
  136. package/src/donation/__tests__/fake-orchestrator.test.ts +168 -0
  137. package/src/donation/__tests__/fake-orchestrator.ts +225 -0
  138. package/src/donation/__tests__/grant-service.test.ts +213 -0
  139. package/src/donation/__tests__/grant-store.test.ts +113 -0
  140. package/src/donation/donation-service.ts +962 -0
  141. package/src/donation/donation-store.ts +171 -0
  142. package/src/donation/donation-supervisor.ts +388 -0
  143. package/src/donation/grant-service.ts +200 -0
  144. package/src/donation/grant-store.ts +152 -0
  145. package/src/donation/index.ts +64 -0
  146. package/src/donation/types.ts +243 -0
  147. package/src/index.ts +51 -5
  148. package/src/installer/__tests__/config.test.ts +27 -1
  149. package/src/installer/__tests__/installer.smoke.test.ts +31 -1
  150. package/src/installer/__tests__/wizard.test.ts +5 -1
  151. package/src/installer/config.ts +34 -0
  152. package/src/installer/index.ts +5 -0
  153. package/src/installer/wizard.ts +12 -1
  154. package/src/nat/__tests__/nat-service.test.ts +5 -5
  155. package/src/nat/nat-service.ts +10 -10
  156. package/src/nat/types.ts +1 -1
  157. package/src/orchestrator/__tests__/node-identity.test.ts +62 -0
  158. package/src/orchestrator/host-process-orchestrator.ts +401 -134
  159. package/src/orchestrator/index.ts +3 -3
  160. package/src/orchestrator/node-identity.ts +40 -0
  161. package/src/orchestrator/port-allocator.ts +48 -0
  162. package/src/orchestrator/state-store.ts +7 -7
  163. package/src/orchestrator/types.ts +8 -8
  164. package/src/{authority/__tests__/authority-node-client.test.ts → owner/__tests__/owner-node-client.test.ts} +45 -17
  165. package/src/owner/index.ts +12 -0
  166. package/src/{authority/authority-node-client.ts → owner/owner-node-client.ts} +83 -23
  167. package/src/server/__tests__/error-handler.test.ts +22 -0
  168. package/src/server/__tests__/grants-admin-route.test.ts +73 -0
  169. package/src/server/__tests__/grants-route.test.ts +263 -0
  170. package/src/server/__tests__/nodes-route.test.ts +25 -25
  171. package/src/server/__tests__/server.smoke.test.ts +75 -0
  172. package/src/server/__tests__/settings-route.test.ts +17 -0
  173. package/src/server/__tests__/status-route.test.ts +21 -0
  174. package/src/server/__tests__/strands-route.test.ts +233 -0
  175. package/src/server/error-handler.ts +67 -1
  176. package/src/server/events/types.ts +1 -0
  177. package/src/server/index.ts +75 -17
  178. package/src/server/routes/grants-admin.ts +41 -0
  179. package/src/server/routes/grants.ts +210 -0
  180. package/src/server/routes/nodes.ts +19 -18
  181. package/src/server/routes/settings.ts +11 -3
  182. package/src/server/routes/status.ts +18 -8
  183. package/src/server/routes/strands.ts +54 -0
  184. package/src/strands/__tests__/strand-service.test.ts +165 -0
  185. package/src/strands/index.ts +14 -0
  186. package/src/strands/strand-service.ts +135 -0
  187. package/src/strands/types.ts +102 -0
  188. package/src/update/__tests__/release-key.test.ts +16 -3
  189. package/src/update/release-key.ts +8 -6
  190. package/dist/authority/authority-node-client.d.ts +0 -77
  191. package/dist/authority/authority-node-client.js.map +0 -1
  192. package/dist/authority/index.d.ts +0 -11
  193. package/dist/authority/index.js +0 -11
  194. package/dist/authority/index.js.map +0 -1
  195. package/dist/ui/assets/index-BlMXQPSw.js +0 -14
  196. package/dist/ui/assets/index-BlMXQPSw.js.map +0 -1
  197. package/dist/ui/assets/index-BwU5mJGF.css +0 -1
  198. package/src/authority/index.ts +0 -12
@@ -5,8 +5,8 @@
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
6
  <meta name="color-scheme" content="light dark" />
7
7
  <title>cadre-host</title>
8
- <script type="module" crossorigin src="./assets/index-BlMXQPSw.js"></script>
9
- <link rel="stylesheet" crossorigin href="./assets/index-BwU5mJGF.css">
8
+ <script type="module" crossorigin src="./assets/index-C-iks41M.js"></script>
9
+ <link rel="stylesheet" crossorigin href="./assets/index-BVoqLjyX.css">
10
10
  </head>
11
11
  <body>
12
12
  <div id="app"></div>
@@ -21,7 +21,7 @@ export declare function getReleasePublicKey(env?: NodeJS.ProcessEnv): KeyObject;
21
21
  * ships in source until the release operator embeds a real public key.
22
22
  *
23
23
  * Consumers:
24
- * - the publish guard (`scripts/publish-package.js`) refuses to ship a build
24
+ * - the publish guard (`scripts/publish-package.mjs`) refuses to ship a build
25
25
  * whose embedded key is still the placeholder, so a real release can never
26
26
  * silently ship the dead key;
27
27
  * - `UpdateService.check()` logs a diagnostic breadcrumb so a misbuilt binary
@@ -9,11 +9,13 @@
9
9
  */
10
10
  import { createPublicKey } from 'node:crypto';
11
11
  /**
12
- * Raw 32-byte Ed25519 public key, base64-encoded. The all-zeros value below is
13
- * the placeholder that ships in source until the release operator embeds a real
14
- * key (`isPlaceholderReleaseKey` reports it; the publish guard refuses to ship
15
- * it). Mismatch with the real signer surfaces as `signature_invalid`, which the
16
- * UI treats as "update available but unverifiable" rather than auto-applying.
12
+ * Raw 32-byte Ed25519 public key, base64-encoded. This is the real production
13
+ * public key embedded by the release operator (see the "Release signing"
14
+ * runbook in `docs/cadre-host.md`). Before a real key was embedded the source
15
+ * shipped an all-zeros placeholder; `isPlaceholderReleaseKey` still detects that
16
+ * state and the publish guard refuses to ship it. Mismatch with the real signer
17
+ * surfaces as `signature_invalid`, which the UI treats as "update available but
18
+ * unverifiable" rather than auto-applying.
17
19
  */
18
20
  const PROD_KEY_BASE64 = 'XyRVnOY9DVgU6xdMgJIguOsc9B2L1o2KoU9626Nk+OE=';
19
21
  /**
@@ -36,7 +38,7 @@ export function getReleasePublicKey(env = process.env) {
36
38
  * ships in source until the release operator embeds a real public key.
37
39
  *
38
40
  * Consumers:
39
- * - the publish guard (`scripts/publish-package.js`) refuses to ship a build
41
+ * - the publish guard (`scripts/publish-package.mjs`) refuses to ship a build
40
42
  * whose embedded key is still the placeholder, so a real release can never
41
43
  * silently ship the dead key;
42
44
  * - `UpdateService.check()` logs a diagnostic breadcrumb so a misbuilt binary
@@ -1 +1 @@
1
- {"version":3,"file":"release-key.js","sourceRoot":"","sources":["../../src/update/release-key.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAGH,OAAO,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AAE9C;;;;;;GAMG;AACH,MAAM,eAAe,GAAG,8CAA8C,CAAC;AAEvE;;;GAGG;AACH,MAAM,UAAU,yBAAyB,CAAC,MAAyB,OAAO,CAAC,GAAG;IAC5E,MAAM,QAAQ,GAAG,GAAG,CAAC,yBAAyB,EAAE,IAAI,EAAE,CAAC;IACvD,IAAI,QAAQ,IAAI,QAAQ,CAAC,MAAM,GAAG,CAAC;QAAE,OAAO,QAAQ,CAAC;IACrD,OAAO,eAAe,CAAC;AACzB,CAAC;AAED,0DAA0D;AAC1D,MAAM,UAAU,mBAAmB,CAAC,MAAyB,OAAO,CAAC,GAAG;IACtE,OAAO,cAAc,CAAC,yBAAyB,CAAC,GAAG,CAAC,CAAC,CAAC;AACxD,CAAC;AAED;;;;;;;;;;;GAWG;AACH,MAAM,UAAU,uBAAuB,CAAC,MAAyB,OAAO,CAAC,GAAG;IAC1E,MAAM,GAAG,GAAG,MAAM,CAAC,IAAI,CAAC,yBAAyB,CAAC,GAAG,CAAC,EAAE,QAAQ,CAAC,CAAC;IAClE,IAAI,GAAG,CAAC,MAAM,KAAK,EAAE;QAAE,OAAO,KAAK,CAAC;IACpC,OAAO,GAAG,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC;AACnC,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,cAAc,CAAC,SAAiB;IAC9C,MAAM,GAAG,GAAG,MAAM,CAAC,IAAI,CAAC,SAAS,EAAE,QAAQ,CAAC,CAAC;IAC7C,IAAI,GAAG,CAAC,MAAM,KAAK,EAAE,EAAE,CAAC;QACtB,MAAM,IAAI,KAAK,CAAC,4CAA4C,GAAG,CAAC,MAAM,EAAE,CAAC,CAAC;IAC5E,CAAC;IACD,0DAA0D;IAC1D,MAAM,MAAM,GAAG,MAAM,CAAC,IAAI,CAAC,0BAA0B,EAAE,KAAK,CAAC,CAAC;IAC9D,MAAM,IAAI,GAAG,MAAM,CAAC,MAAM,CAAC,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC,CAAC;IAC1C,OAAO,eAAe,CAAC,EAAE,GAAG,EAAE,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,IAAI,EAAE,MAAM,EAAE,CAAC,CAAC;AACrE,CAAC"}
1
+ {"version":3,"file":"release-key.js","sourceRoot":"","sources":["../../src/update/release-key.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAGH,OAAO,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AAE9C;;;;;;;;GAQG;AACH,MAAM,eAAe,GAAG,8CAA8C,CAAC;AAEvE;;;GAGG;AACH,MAAM,UAAU,yBAAyB,CAAC,MAAyB,OAAO,CAAC,GAAG;IAC5E,MAAM,QAAQ,GAAG,GAAG,CAAC,yBAAyB,EAAE,IAAI,EAAE,CAAC;IACvD,IAAI,QAAQ,IAAI,QAAQ,CAAC,MAAM,GAAG,CAAC;QAAE,OAAO,QAAQ,CAAC;IACrD,OAAO,eAAe,CAAC;AACzB,CAAC;AAED,0DAA0D;AAC1D,MAAM,UAAU,mBAAmB,CAAC,MAAyB,OAAO,CAAC,GAAG;IACtE,OAAO,cAAc,CAAC,yBAAyB,CAAC,GAAG,CAAC,CAAC,CAAC;AACxD,CAAC;AAED;;;;;;;;;;;GAWG;AACH,MAAM,UAAU,uBAAuB,CAAC,MAAyB,OAAO,CAAC,GAAG;IAC1E,MAAM,GAAG,GAAG,MAAM,CAAC,IAAI,CAAC,yBAAyB,CAAC,GAAG,CAAC,EAAE,QAAQ,CAAC,CAAC;IAClE,IAAI,GAAG,CAAC,MAAM,KAAK,EAAE;QAAE,OAAO,KAAK,CAAC;IACpC,OAAO,GAAG,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC;AACnC,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,cAAc,CAAC,SAAiB;IAC9C,MAAM,GAAG,GAAG,MAAM,CAAC,IAAI,CAAC,SAAS,EAAE,QAAQ,CAAC,CAAC;IAC7C,IAAI,GAAG,CAAC,MAAM,KAAK,EAAE,EAAE,CAAC;QACtB,MAAM,IAAI,KAAK,CAAC,4CAA4C,GAAG,CAAC,MAAM,EAAE,CAAC,CAAC;IAC5E,CAAC;IACD,0DAA0D;IAC1D,MAAM,MAAM,GAAG,MAAM,CAAC,IAAI,CAAC,0BAA0B,EAAE,KAAK,CAAC,CAAC;IAC9D,MAAM,IAAI,GAAG,MAAM,CAAC,MAAM,CAAC,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC,CAAC;IAC1C,OAAO,eAAe,CAAC,EAAE,GAAG,EAAE,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,IAAI,EAAE,MAAM,EAAE,CAAC,CAAC;AACrE,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@serfab/cadre-host",
3
- "version": "0.8.1",
3
+ "version": "0.10.0",
4
4
  "type": "module",
5
5
  "description": "Self-hosted cadre node manager for basement-PC deployments",
6
6
  "main": "dist/index.js",
@@ -38,7 +38,7 @@
38
38
  "build:server": "tsc -p tsconfig.build.json",
39
39
  "build:ui": "vite build --config ui/vite.config.ts",
40
40
  "dev:ui": "vite --config ui/vite.config.ts",
41
- "typecheck": "tsc -p tsconfig.typecheck.json --noEmit",
41
+ "typecheck": "tsc -p tsconfig.typecheck.json --noEmit && tsc -p ui/tsconfig.json --noEmit",
42
42
  "check:svelte": "svelte-check --tsconfig tsconfig.json --workspace ui",
43
43
  "test": "vitest run --passWithNoTests",
44
44
  "dev:test": "vitest",
@@ -64,9 +64,9 @@
64
64
  "@libp2p/crypto": "^5.1.13",
65
65
  "@libp2p/interface": "^3.1.0",
66
66
  "@libp2p/peer-id": "^6.0.4",
67
- "@serfab/cadre-cli": "^0.8.1",
68
- "@serfab/cadre-core": "^0.8.1",
69
- "@serfab/cadre-provider": "^0.8.1",
67
+ "@serfab/cadre-cli": "^0.10.0",
68
+ "@serfab/cadre-core": "^0.10.0",
69
+ "@serfab/cadre-provider": "^0.10.0",
70
70
  "commander": "^14.0.2",
71
71
  "debug": "^4.4.3",
72
72
  "fastify": "^5.7.1",
@@ -0,0 +1,26 @@
1
+ /**
2
+ * Holds this suite's stale-build target list against the package it guards.
3
+ *
4
+ * `global-setup.ts`'s `TARGETS` is hand-written. Without this, adding a workspace
5
+ * or linked dependency to `package.json` leaves it unguarded and says nothing —
6
+ * the suite goes on passing while it runs that dependency's stale `dist`.
7
+ *
8
+ * The list is deliberately wider than `dependencies` (it also covers
9
+ * `quereus-plugin-sereus` and the packages beneath it, reached transitively
10
+ * through `cadre-core`), so only missing entries fail — extra ones are the point.
11
+ *
12
+ * Both pinned names are `workspace:` deps: cadre-host declares no `link:` entry of
13
+ * its own — those arrive transitively through `@serfab/cadre-core`.
14
+ */
15
+
16
+ import { describeBuildTargets, packageRootFrom } from '../../../../test-harness/build-targets-spec.js';
17
+ import { TARGETS } from './global-setup.js';
18
+
19
+ describeBuildTargets('cadre-host', {
20
+ packageDir: packageRootFrom(import.meta.url, '..', '..'),
21
+ targets: TARGETS,
22
+ expectFound: {
23
+ '@serfab/cadre-core': 'workspace',
24
+ '@serfab/cadre-cli': 'workspace',
25
+ },
26
+ });
@@ -0,0 +1,51 @@
1
+ /**
2
+ * Vitest global setup — runs once before the whole cadre-host suite.
3
+ *
4
+ * `src/update/manifest.ts` and `src/push/index.ts` import real symbols from
5
+ * `@serfab/cadre-core` (`canonicalJson`, `validatePushCredentials`), and
6
+ * `trust-circle-integration.test.ts` imports and drives a real `CadreNode` from
7
+ * it too. `@serfab/cadre-core`'s compiled entry point in turn imports
8
+ * `@serfab/quereus-plugin-sereus`, which imports the `@optimystic/*` and
9
+ * `@quereus/quereus` packages linked in from their sibling checkouts.
10
+ * `host-process-orchestrator.ts` also resolves `@serfab/cadre-cli`'s bin path at
11
+ * runtime, and `@serfab/cadre-provider` is a declared dependency in its own
12
+ * right. Every one of those resolves through a `node_modules` symlink whose
13
+ * manifest points at `dist`, so an edit to any of their `src` with no following
14
+ * build is invisible here: the suite runs the previous build and reports green
15
+ * about code it never executed. Fail the run up front instead.
16
+ *
17
+ * This does not cover cadre-host's *own* `src` going stale relative to its own
18
+ * `dist` — the CLI smoke tests spawn `dist/bin/host.js` as a child process, which
19
+ * is a same-package staleness concern, not a cross-package one `BuildTarget` is
20
+ * meant to check (see `test-harness/build-freshness.ts`'s module comment).
21
+ *
22
+ * The guard itself lives at the repo root (`test-harness/build-freshness.ts`),
23
+ * shared with the other suites that call it; the list of packages below is this
24
+ * suite's own concern.
25
+ */
26
+
27
+ import { assertBuildFresh, type BuildTarget } from '../../../../test-harness/build-freshness.js';
28
+
29
+ /**
30
+ * Every package this suite runs compiled code from — the `workspace:` entries of
31
+ * `package.json`'s `dependencies`, plus the `link:` packages reached
32
+ * transitively through them.
33
+ *
34
+ * Exported so `build-targets.test.ts` can hold it against this package's actual
35
+ * `dependencies` — a hand-written list rots silently otherwise.
36
+ */
37
+ export const TARGETS: BuildTarget[] = [
38
+ { packageName: '@serfab/cadre-cli', distEntry: 'dist/bin/cadre.js', location: 'workspace' },
39
+ { packageName: '@serfab/cadre-core', distEntry: 'dist/index.js', location: 'workspace' },
40
+ { packageName: '@serfab/cadre-provider', distEntry: 'dist/index.js', location: 'workspace' },
41
+ { packageName: '@serfab/quereus-plugin-sereus', distEntry: 'dist/index.js', location: 'workspace' },
42
+ { packageName: '@optimystic/db-core', distEntry: 'dist/src/index.js', location: 'linked' },
43
+ { packageName: '@optimystic/db-p2p', distEntry: 'dist/src/index.js', location: 'linked' },
44
+ { packageName: '@optimystic/quereus-plugin-crypto', distEntry: 'dist/index.js', location: 'linked' },
45
+ { packageName: '@optimystic/quereus-plugin-optimystic', distEntry: 'dist/index.js', location: 'linked' },
46
+ { packageName: '@quereus/quereus', distEntry: 'dist/src/index.js', location: 'linked' },
47
+ ];
48
+
49
+ export default function setup(): void {
50
+ assertBuildFresh(TARGETS, import.meta.url);
51
+ }
@@ -1,14 +1,14 @@
1
1
  /**
2
- * Server-level smoke: the /auth/* HTTP contract over the authority-node
2
+ * Server-level smoke: the /auth/* HTTP contract over the owner-node
3
3
  * channel adapter. Validates the two behaviours the realignment must
4
4
  * preserve:
5
5
  * 1. With the node up, POST /auth/invites returns an encodedInvite (no 500).
6
6
  * 2. With the node refusing connections, GET /auth/trust-circle still lists
7
7
  * local labels, and POST /auth/invites returns 503 (not a raw 500).
8
8
  *
9
- * The authority node is represented by a stub HTTP server matching the 6.6
9
+ * The owner node is represented by a stub HTTP server matching the 6.6
10
10
  * admin contract (the spawn path itself is covered by
11
- * orchestrator-authority.test.ts).
11
+ * orchestrator-owner.test.ts).
12
12
  */
13
13
 
14
14
  import { afterEach, beforeEach, describe, expect, it } from 'vitest';
@@ -19,7 +19,7 @@ import { tmpdir } from 'node:os';
19
19
  import { join } from 'node:path';
20
20
 
21
21
  import { TrustCircleService, TrustCircleStore, createTrustCircleHandlers } from '../auth/index.js';
22
- import { AuthorityNodeClient } from '../authority/index.js';
22
+ import { OwnerNodeClient } from '../owner/index.js';
23
23
  import { registerErrorHandler } from '../server/error-handler.js';
24
24
  import { registerTrustCircleRoutes } from '../server/routes/trust-circle.js';
25
25
  import { EventBus } from '../server/events/bus.js';
@@ -51,7 +51,7 @@ describe('host /auth delegation — node up', () => {
51
51
  }
52
52
  if (req.url === '/admin/invites' && req.method === 'POST') {
53
53
  res.writeHead(200, { 'content-type': 'application/json' });
54
- res.end(JSON.stringify({ ok: true, data: { invite: { partyId: 'p', authorityAddrs: [], token: 't', createdAt: 1 }, encodedInvite: 'ENC' } }));
54
+ res.end(JSON.stringify({ ok: true, data: { invite: { partyId: 'p', ownerAddrs: [], token: 't', createdAt: 1 }, encodedInvite: 'ENC' } }));
55
55
  return;
56
56
  }
57
57
  if (req.url === '/admin/members' && req.method === 'GET') {
@@ -70,7 +70,7 @@ describe('host /auth delegation — node up', () => {
70
70
  afterEach(async () => { await new Promise<void>((resolve) => server.close(() => resolve())); });
71
71
 
72
72
  it('POST /auth/invites returns an encodedInvite (no 500)', async () => {
73
- const client = new AuthorityNodeClient({ baseUrl, token: TOKEN });
73
+ const client = new OwnerNodeClient({ baseUrl, token: TOKEN });
74
74
  const service = new TrustCircleService({ cadreNode: client, store: new TrustCircleStore(tmpRoot) });
75
75
  const app = buildApp(service);
76
76
  try {
@@ -103,7 +103,7 @@ describe('host /auth delegation — node refusing connections', () => {
103
103
  const store = new TrustCircleStore(tmpRoot);
104
104
  store.addMember({ peerId: '12D3KooWLocal', label: 'My laptop', addedAt: '2025-01-01T00:00:00Z' });
105
105
 
106
- const client = new AuthorityNodeClient({ baseUrl: closedBaseUrl, token: TOKEN });
106
+ const client = new OwnerNodeClient({ baseUrl: closedBaseUrl, token: TOKEN });
107
107
  const service = new TrustCircleService({ cadreNode: client, store });
108
108
  const app = buildApp(service);
109
109
  try {
@@ -0,0 +1,140 @@
1
+ /**
2
+ * HostProcessOrchestrator env-scrub test — the manager's own `process.env` must
3
+ * not leak `CADRE_*` config-override vars into spawned children.
4
+ *
5
+ * `launchChild` used to build each child's environment as
6
+ * `{ ...process.env, ...extraEnv, <fixed per-child vars> }`, so ANY `CADRE_*`
7
+ * var set on the manager (not just the ones the orchestrator deliberately
8
+ * sets) reached every child. Because `@serfab/cadre-cli` treats an inherited
9
+ * `CADRE_*` var as a config override that beats the per-child config file, a
10
+ * `CADRE_PARTY_ID` set on the manager could silently pull every child into
11
+ * that party.
12
+ *
13
+ * A fake CLI records the vars under test from its environment to a file so
14
+ * the scrub is observable without a real cadre node.
15
+ */
16
+
17
+ import { afterEach, beforeEach, describe, expect, it } from 'vitest';
18
+ import { existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
19
+ import { tmpdir } from 'node:os';
20
+ import { join } from 'node:path';
21
+
22
+ import { HostProcessOrchestrator } from '../orchestrator/host-process-orchestrator.js';
23
+
24
+ // Writes the vars under test (as seen in the child env) next to the startup token.
25
+ const FAKE_CLI = `
26
+ import fs from 'node:fs';
27
+ import path from 'node:path';
28
+ const args = process.argv.slice(2);
29
+ const get = (flag) => { const i = args.indexOf(flag); return i >= 0 ? args[i + 1] : undefined; };
30
+ const tokenPath = get('--startup-token-file');
31
+ const token = process.env.CADRE_STARTUP_TOKEN ?? '';
32
+ if (tokenPath) {
33
+ const dir = path.dirname(tokenPath);
34
+ const record = {
35
+ CADRE_PARTY_ID: process.env.CADRE_PARTY_ID ?? null,
36
+ CADRE_STORAGE_PATH: process.env.CADRE_STORAGE_PATH ?? null,
37
+ CADRE_ADMIN_PORT: process.env.CADRE_ADMIN_PORT ?? null,
38
+ DEBUG: process.env.DEBUG ?? null,
39
+ };
40
+ try { fs.writeFileSync(path.join(dir, 'env-seen.json'), JSON.stringify(record), 'utf8'); } catch (e) { console.error(e); }
41
+ if (token) { try { fs.writeFileSync(tokenPath, token, 'utf8'); } catch (e) { console.error(e); } }
42
+ }
43
+ process.on('SIGTERM', () => process.exit(0));
44
+ setInterval(() => {}, 1 << 30);
45
+ `;
46
+
47
+ let tmpRoot: string;
48
+ let scriptPath: string;
49
+ const orchestrators: HostProcessOrchestrator[] = [];
50
+ let savedEnv: Record<string, string | undefined>;
51
+
52
+ beforeEach(() => {
53
+ tmpRoot = mkdtempSync(join(tmpdir(), 'cadre-host-env-scrub-'));
54
+ scriptPath = join(tmpRoot, 'fake-cli.mjs');
55
+ writeFileSync(scriptPath, FAKE_CLI, 'utf8');
56
+ savedEnv = {
57
+ CADRE_PARTY_ID: process.env.CADRE_PARTY_ID,
58
+ CADRE_STORAGE_PATH: process.env.CADRE_STORAGE_PATH,
59
+ CADRE_ADMIN_PORT: process.env.CADRE_ADMIN_PORT,
60
+ DEBUG: process.env.DEBUG,
61
+ };
62
+ });
63
+
64
+ afterEach(async () => {
65
+ for (const orch of orchestrators) {
66
+ for (const n of orch.listNodes()) {
67
+ try { await orch.removeContainer(n.dockerId); } catch { /* ignore */ }
68
+ }
69
+ }
70
+ orchestrators.length = 0;
71
+ await sleep(50);
72
+ try { rmSync(tmpRoot, { recursive: true, force: true }); } catch { /* ignore */ }
73
+ for (const [key, value] of Object.entries(savedEnv)) {
74
+ if (value === undefined) delete process.env[key];
75
+ else process.env[key] = value;
76
+ }
77
+ });
78
+
79
+ function makeOrchestrator(rootDir: string): HostProcessOrchestrator {
80
+ mkdirSync(rootDir, { recursive: true });
81
+ const orch = new HostProcessOrchestrator({
82
+ rootDir,
83
+ portRange: { start: 18000, end: 18499 },
84
+ stopTimeoutMs: 1500,
85
+ spawn: { entrypoint: scriptPath },
86
+ });
87
+ orchestrators.push(orch);
88
+ return orch;
89
+ }
90
+
91
+ async function waitForFile(path: string, timeoutMs = 5000): Promise<string> {
92
+ const deadline = Date.now() + timeoutMs;
93
+ while (Date.now() < deadline) {
94
+ if (existsSync(path)) return readFileSync(path, 'utf8');
95
+ await sleep(50);
96
+ }
97
+ throw new Error(`file never appeared: ${path}`);
98
+ }
99
+
100
+ function sleep(ms: number): Promise<void> {
101
+ return new Promise((res) => setTimeout(res, ms));
102
+ }
103
+
104
+ describe('HostProcessOrchestrator child env scrub', () => {
105
+ it('does not leak manager CADRE_* config-override vars into children, but passes through non-CADRE vars', async () => {
106
+ process.env.CADRE_PARTY_ID = 'conflicting-party';
107
+ process.env.CADRE_STORAGE_PATH = '/tmp/should-not-leak';
108
+ // Not an ENV_MAPPINGS config override — read directly by the cli's `start`
109
+ // command — so it only stays scrubbed if the scrub is prefix-wide.
110
+ process.env.CADRE_ADMIN_PORT = '19999';
111
+ process.env.DEBUG = 'some:value';
112
+
113
+ const rootDir = join(tmpRoot, 'a');
114
+ const orch = makeOrchestrator(rootDir);
115
+ await orch.init();
116
+
117
+ await orch.createContainer({
118
+ containerId: 'child-1',
119
+ partyId: 'party-1',
120
+ bootstrapNodes: [],
121
+ profile: 'storage',
122
+ });
123
+ await orch.createContainer({
124
+ containerId: 'child-2',
125
+ partyId: 'party-2',
126
+ bootstrapNodes: [],
127
+ profile: 'storage',
128
+ });
129
+
130
+ const seen1 = JSON.parse(await waitForFile(join(rootDir, 'child-1', 'env-seen.json')));
131
+ const seen2 = JSON.parse(await waitForFile(join(rootDir, 'child-2', 'env-seen.json')));
132
+
133
+ for (const seen of [seen1, seen2]) {
134
+ expect(seen.CADRE_PARTY_ID).toBeNull();
135
+ expect(seen.CADRE_STORAGE_PATH).toBeNull();
136
+ expect(seen.CADRE_ADMIN_PORT).toBeNull();
137
+ expect(seen.DEBUG).toBe('some:value');
138
+ }
139
+ });
140
+ });
@@ -0,0 +1,204 @@
1
+ /**
2
+ * HostProcessOrchestrator per-node identity tests.
3
+ *
4
+ * A donated node must keep the peer id its host cadre approved across restarts,
5
+ * and needs a durable on-disk home for its node-local stores (`cadre-cli start`
6
+ * opens the file-backed bootstrap-peer / trusted-owner stores only when a
7
+ * protobuf identity key file is configured). The orchestrator therefore writes
8
+ * `<workdir>/identity.key` once per container and passes it as
9
+ * `--identity-protobuf`.
10
+ *
11
+ * A fake CLI records the `--identity-protobuf` argument it was launched with, so
12
+ * the spawn wiring is observable without a real cadre node.
13
+ */
14
+
15
+ import { afterEach, beforeEach, describe, expect, it } from 'vitest';
16
+ import { existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
17
+ import { tmpdir } from 'node:os';
18
+ import { join } from 'node:path';
19
+
20
+ import { HostProcessOrchestrator, OWNER_CONTAINER_ID } from '../orchestrator/host-process-orchestrator.js';
21
+ import { loadIdentity } from '../installer/identity.js';
22
+
23
+ // Writes the --identity-protobuf argument (as seen on its own command line) next
24
+ // to the startup token, then behaves like a minimal long-lived node.
25
+ const FAKE_CLI = `
26
+ import fs from 'node:fs';
27
+ import path from 'node:path';
28
+ const args = process.argv.slice(2);
29
+ const get = (flag) => { const i = args.indexOf(flag); return i >= 0 ? args[i + 1] : undefined; };
30
+ const tokenPath = get('--startup-token-file');
31
+ const token = process.env.CADRE_STARTUP_TOKEN ?? '';
32
+ if (tokenPath) {
33
+ const dir = path.dirname(tokenPath);
34
+ try { fs.writeFileSync(path.join(dir, 'identity-arg.txt'), get('--identity-protobuf') ?? '', 'utf8'); } catch (e) { console.error(e); }
35
+ if (token) { try { fs.writeFileSync(tokenPath, token, 'utf8'); } catch (e) { console.error(e); } }
36
+ }
37
+ process.on('SIGTERM', () => process.exit(0));
38
+ setInterval(() => {}, 1 << 30);
39
+ `;
40
+
41
+ let tmpRoot: string;
42
+ let scriptPath: string;
43
+ const orchestrators: HostProcessOrchestrator[] = [];
44
+
45
+ beforeEach(() => {
46
+ tmpRoot = mkdtempSync(join(tmpdir(), 'cadre-host-nodeid-'));
47
+ scriptPath = join(tmpRoot, 'fake-cli.mjs');
48
+ writeFileSync(scriptPath, FAKE_CLI, 'utf8');
49
+ });
50
+
51
+ afterEach(async () => {
52
+ for (const orch of orchestrators) {
53
+ for (const n of orch.listNodes()) {
54
+ try { await orch.removeContainer(n.dockerId); } catch { /* ignore */ }
55
+ }
56
+ }
57
+ orchestrators.length = 0;
58
+ await sleep(50);
59
+ try { rmSync(tmpRoot, { recursive: true, force: true }); } catch { /* ignore */ }
60
+ });
61
+
62
+ function makeOrchestrator(
63
+ rootDir: string,
64
+ portRange: { start: number; end: number } = { start: 17500, end: 17999 },
65
+ ): HostProcessOrchestrator {
66
+ mkdirSync(rootDir, { recursive: true });
67
+ const orch = new HostProcessOrchestrator({
68
+ rootDir,
69
+ portRange,
70
+ stopTimeoutMs: 1500,
71
+ spawn: { entrypoint: scriptPath },
72
+ });
73
+ orchestrators.push(orch);
74
+ return orch;
75
+ }
76
+
77
+ async function waitForFile(path: string, timeoutMs = 5000): Promise<string> {
78
+ const deadline = Date.now() + timeoutMs;
79
+ while (Date.now() < deadline) {
80
+ if (existsSync(path)) return readFileSync(path, 'utf8');
81
+ await sleep(50);
82
+ }
83
+ throw new Error(`file never appeared: ${path}`);
84
+ }
85
+
86
+ function sleep(ms: number): Promise<void> {
87
+ return new Promise((res) => setTimeout(res, ms));
88
+ }
89
+
90
+ describe('HostProcessOrchestrator node identity', () => {
91
+ it('passes --identity-protobuf pointing at a real key inside the node workdir', async () => {
92
+ const rootDir = join(tmpRoot, 'a');
93
+ const orch = makeOrchestrator(rootDir);
94
+ await orch.init();
95
+
96
+ await orch.createContainer({
97
+ containerId: 'donated-1',
98
+ partyId: 'foreign-P',
99
+ bootstrapNodes: [],
100
+ profile: 'storage',
101
+ pinnedOwnerKeys: ['key-a'],
102
+ });
103
+
104
+ const workdir = join(rootDir, 'donated-1');
105
+ const arg = await waitForFile(join(workdir, 'identity-arg.txt'));
106
+ expect(arg).toBe(join(workdir, 'identity.key'));
107
+ // Decodes as a libp2p protobuf private key → the child gets a usable identity.
108
+ expect(loadIdentity(arg).peerId).toMatch(/^12D3Koo/);
109
+ });
110
+
111
+ it('reuses the same identity when the same containerId is spawned again', async () => {
112
+ const rootDir = join(tmpRoot, 'b');
113
+ const orch = makeOrchestrator(rootDir);
114
+ await orch.init();
115
+
116
+ const first = await orch.createContainer({
117
+ containerId: 'donated-2',
118
+ partyId: 'foreign-P',
119
+ bootstrapNodes: [],
120
+ profile: 'storage',
121
+ pinnedOwnerKeys: ['key-a'],
122
+ });
123
+ const workdir = join(rootDir, 'donated-2');
124
+ await waitForFile(join(workdir, 'identity-arg.txt'));
125
+ const before = loadIdentity(join(workdir, 'identity.key')).peerId;
126
+
127
+ // Stop (not remove — removal deletes the workdir) and re-spawn, as a restart
128
+ // of a live loan would.
129
+ await orch.stopContainer(first.dockerId);
130
+ await orch.createContainer({
131
+ containerId: 'donated-2',
132
+ partyId: 'foreign-P',
133
+ bootstrapNodes: [],
134
+ profile: 'storage',
135
+ pinnedOwnerKeys: ['key-a'],
136
+ });
137
+
138
+ const after = loadIdentity(join(workdir, 'identity.key')).peerId;
139
+ expect(after).toBe(before);
140
+ });
141
+
142
+ it('destroys the identity with the workdir when the loan is terminated', async () => {
143
+ const rootDir = join(tmpRoot, 'c');
144
+ const orch = makeOrchestrator(rootDir);
145
+ await orch.init();
146
+
147
+ const created = await orch.createContainer({
148
+ containerId: 'donated-3',
149
+ partyId: 'foreign-P',
150
+ bootstrapNodes: [],
151
+ profile: 'storage',
152
+ pinnedOwnerKeys: ['key-a'],
153
+ });
154
+ const identityPath = join(rootDir, 'donated-3', 'identity.key');
155
+ await waitForFile(identityPath);
156
+
157
+ await orch.removeContainer(created.dockerId);
158
+ expect(existsSync(identityPath)).toBe(false);
159
+ expect(existsSync(join(rootDir, 'donated-3'))).toBe(false);
160
+ });
161
+
162
+ // The failure the identity step can actually produce is a damaged key file,
163
+ // and `DonationService.provision` turns every such throw into an `error`
164
+ // record and lets the grantee retry. If the spawn path reserved its ports
165
+ // before the step that throws, each retry would burn four more ports out of a
166
+ // bounded range until provisioning stopped working altogether.
167
+ it('reserves no ports when the identity step fails', async () => {
168
+ const rootDir = join(tmpRoot, 'd');
169
+ // Exactly one node's worth of ports (health, metrics, p2p, admin).
170
+ const orch = makeOrchestrator(rootDir, { start: 18100, end: 18103 });
171
+ await orch.init();
172
+
173
+ mkdirSync(join(rootDir, 'donated-bad'), { recursive: true });
174
+ writeFileSync(join(rootDir, 'donated-bad', 'identity.key'), 'not a protobuf private key', 'utf8');
175
+
176
+ const request = {
177
+ partyId: 'foreign-P',
178
+ bootstrapNodes: [],
179
+ profile: 'storage' as const,
180
+ pinnedOwnerKeys: ['key-a'],
181
+ };
182
+ await expect(orch.createContainer({ ...request, containerId: 'donated-bad' })).rejects.toThrow();
183
+
184
+ // The whole range is still free, so a healthy container still gets its four.
185
+ const ok = await orch.createContainer({ ...request, containerId: 'donated-good' });
186
+ expect(ok.p2pPort).toBeGreaterThanOrEqual(18100);
187
+ });
188
+
189
+ // The owner node carries the HOST's installer identity from <dataDir>, not a
190
+ // per-workdir key — a second key in its workdir would mean the host's own
191
+ // cadre node quietly changed peer id.
192
+ it('leaves the owner node on its own installer identity', async () => {
193
+ const rootDir = join(tmpRoot, 'e');
194
+ const orch = makeOrchestrator(rootDir);
195
+ await orch.init();
196
+
197
+ const hostIdentity = join(tmpRoot, 'host-identity.key');
198
+ await orch.ensureOwnerNode({ identityPath: hostIdentity, partyId: 'host-P', libp2pPort: 4101 });
199
+
200
+ const ownerWorkdir = join(rootDir, OWNER_CONTAINER_ID);
201
+ expect(await waitForFile(join(ownerWorkdir, 'identity-arg.txt'))).toBe(hostIdentity);
202
+ expect(existsSync(join(ownerWorkdir, 'identity.key'))).toBe(false);
203
+ });
204
+ });