@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.
- package/dist/auth/index.d.ts +2 -0
- package/dist/auth/index.js +1 -0
- package/dist/auth/index.js.map +1 -1
- package/dist/auth/self-label.d.ts +36 -0
- package/dist/auth/self-label.js +57 -0
- package/dist/auth/self-label.js.map +1 -0
- package/dist/auth/trust-circle.d.ts +36 -9
- package/dist/auth/trust-circle.js +40 -20
- package/dist/auth/trust-circle.js.map +1 -1
- package/dist/auth/types.d.ts +2 -2
- package/dist/bin/host.js +291 -54
- package/dist/bin/host.js.map +1 -1
- package/dist/donation/__tests__/fake-orchestrator.d.ts +103 -0
- package/dist/donation/__tests__/fake-orchestrator.js +180 -0
- package/dist/donation/__tests__/fake-orchestrator.js.map +1 -0
- package/dist/donation/donation-service.d.ts +361 -0
- package/dist/donation/donation-service.js +763 -0
- package/dist/donation/donation-service.js.map +1 -0
- package/dist/donation/donation-store.d.ts +49 -0
- package/dist/donation/donation-store.js +148 -0
- package/dist/donation/donation-store.js.map +1 -0
- package/dist/donation/donation-supervisor.d.ts +167 -0
- package/dist/donation/donation-supervisor.js +341 -0
- package/dist/donation/donation-supervisor.js.map +1 -0
- package/dist/donation/grant-service.d.ts +71 -0
- package/dist/donation/grant-service.js +163 -0
- package/dist/donation/grant-service.js.map +1 -0
- package/dist/donation/grant-store.d.ts +38 -0
- package/dist/donation/grant-store.js +135 -0
- package/dist/donation/grant-store.js.map +1 -0
- package/dist/donation/index.d.ts +20 -0
- package/dist/donation/index.js +17 -0
- package/dist/donation/index.js.map +1 -0
- package/dist/donation/types.d.ts +201 -0
- package/dist/donation/types.js +40 -0
- package/dist/donation/types.js.map +1 -0
- package/dist/index.d.ts +9 -5
- package/dist/index.js +7 -3
- package/dist/index.js.map +1 -1
- package/dist/installer/config.d.ts +26 -0
- package/dist/installer/config.js +13 -0
- package/dist/installer/config.js.map +1 -1
- package/dist/installer/index.d.ts +2 -0
- package/dist/installer/index.js +3 -0
- package/dist/installer/index.js.map +1 -1
- package/dist/installer/wizard.d.ts +4 -0
- package/dist/installer/wizard.js +5 -1
- package/dist/installer/wizard.js.map +1 -1
- package/dist/nat/nat-service.d.ts +5 -5
- package/dist/nat/nat-service.js +9 -9
- package/dist/nat/nat-service.js.map +1 -1
- package/dist/nat/types.d.ts +1 -1
- package/dist/orchestrator/host-process-orchestrator.d.ts +144 -32
- package/dist/orchestrator/host-process-orchestrator.js +397 -128
- package/dist/orchestrator/host-process-orchestrator.js.map +1 -1
- package/dist/orchestrator/index.d.ts +3 -3
- package/dist/orchestrator/index.js +1 -1
- package/dist/orchestrator/index.js.map +1 -1
- package/dist/orchestrator/node-identity.d.ts +26 -0
- package/dist/orchestrator/node-identity.js +36 -0
- package/dist/orchestrator/node-identity.js.map +1 -0
- package/dist/orchestrator/port-allocator.d.ts +14 -0
- package/dist/orchestrator/port-allocator.js +45 -0
- package/dist/orchestrator/port-allocator.js.map +1 -1
- package/dist/orchestrator/state-store.d.ts +5 -5
- package/dist/orchestrator/state-store.js +2 -2
- package/dist/orchestrator/state-store.js.map +1 -1
- package/dist/orchestrator/types.d.ts +8 -8
- package/dist/owner/index.d.ts +11 -0
- package/dist/owner/index.js +11 -0
- package/dist/owner/index.js.map +1 -0
- package/dist/owner/owner-node-client.d.ts +110 -0
- package/dist/{authority/authority-node-client.js → owner/owner-node-client.js} +58 -17
- package/dist/owner/owner-node-client.js.map +1 -0
- package/dist/server/error-handler.d.ts +22 -1
- package/dist/server/error-handler.js +64 -1
- package/dist/server/error-handler.js.map +1 -1
- package/dist/server/events/types.d.ts +3 -0
- package/dist/server/index.d.ts +33 -2
- package/dist/server/index.js +43 -16
- package/dist/server/index.js.map +1 -1
- package/dist/server/routes/grants-admin.d.ts +17 -0
- package/dist/server/routes/grants-admin.js +32 -0
- package/dist/server/routes/grants-admin.js.map +1 -0
- package/dist/server/routes/grants.d.ts +28 -0
- package/dist/server/routes/grants.js +182 -0
- package/dist/server/routes/grants.js.map +1 -0
- package/dist/server/routes/nodes.d.ts +5 -4
- package/dist/server/routes/nodes.js +19 -18
- package/dist/server/routes/nodes.js.map +1 -1
- package/dist/server/routes/settings.d.ts +6 -1
- package/dist/server/routes/settings.js +5 -2
- package/dist/server/routes/settings.js.map +1 -1
- package/dist/server/routes/status.d.ts +8 -4
- package/dist/server/routes/status.js +9 -4
- package/dist/server/routes/status.js.map +1 -1
- package/dist/server/routes/strands.d.ts +20 -0
- package/dist/server/routes/strands.js +38 -0
- package/dist/server/routes/strands.js.map +1 -0
- package/dist/strands/index.d.ts +7 -0
- package/dist/strands/index.js +6 -0
- package/dist/strands/index.js.map +1 -0
- package/dist/strands/strand-service.d.ts +50 -0
- package/dist/strands/strand-service.js +103 -0
- package/dist/strands/strand-service.js.map +1 -0
- package/dist/strands/types.d.ts +93 -0
- package/dist/strands/types.js +23 -0
- package/dist/strands/types.js.map +1 -0
- package/dist/ui/assets/index-BVoqLjyX.css +1 -0
- package/dist/ui/assets/index-C-iks41M.js +17 -0
- package/dist/ui/assets/index-C-iks41M.js.map +1 -0
- package/dist/ui/index.html +2 -2
- package/dist/update/release-key.d.ts +1 -1
- package/dist/update/release-key.js +8 -6
- package/dist/update/release-key.js.map +1 -1
- package/package.json +5 -5
- package/src/__tests__/build-targets.test.ts +26 -0
- package/src/__tests__/global-setup.ts +51 -0
- package/src/__tests__/{host-authority.smoke.test.ts → host-owner.smoke.test.ts} +7 -7
- package/src/__tests__/orchestrator-env-scrub.test.ts +140 -0
- package/src/__tests__/orchestrator-node-identity.test.ts +204 -0
- package/src/__tests__/{orchestrator-authority.test.ts → orchestrator-owner.test.ts} +33 -33
- package/src/__tests__/orchestrator-pin-keys.test.ts +153 -0
- package/src/__tests__/orchestrator-push.test.ts +10 -10
- package/src/__tests__/orchestrator.test.ts +387 -6
- package/src/auth/__tests__/self-label.test.ts +85 -0
- package/src/auth/__tests__/trust-circle-integration.test.ts +152 -147
- package/src/auth/__tests__/trust-circle.test.ts +55 -11
- package/src/auth/index.ts +2 -0
- package/src/auth/self-label.ts +74 -0
- package/src/auth/trust-circle.ts +54 -22
- package/src/auth/types.ts +2 -2
- package/src/bin/host.ts +1299 -1038
- package/src/donation/__tests__/donation-service.test.ts +1068 -0
- package/src/donation/__tests__/donation-supervisor.test.ts +510 -0
- package/src/donation/__tests__/fake-orchestrator.test.ts +168 -0
- package/src/donation/__tests__/fake-orchestrator.ts +225 -0
- package/src/donation/__tests__/grant-service.test.ts +213 -0
- package/src/donation/__tests__/grant-store.test.ts +113 -0
- package/src/donation/donation-service.ts +962 -0
- package/src/donation/donation-store.ts +171 -0
- package/src/donation/donation-supervisor.ts +388 -0
- package/src/donation/grant-service.ts +200 -0
- package/src/donation/grant-store.ts +152 -0
- package/src/donation/index.ts +64 -0
- package/src/donation/types.ts +243 -0
- package/src/index.ts +51 -5
- package/src/installer/__tests__/config.test.ts +27 -1
- package/src/installer/__tests__/installer.smoke.test.ts +31 -1
- package/src/installer/__tests__/wizard.test.ts +5 -1
- package/src/installer/config.ts +34 -0
- package/src/installer/index.ts +5 -0
- package/src/installer/wizard.ts +12 -1
- package/src/nat/__tests__/nat-service.test.ts +5 -5
- package/src/nat/nat-service.ts +10 -10
- package/src/nat/types.ts +1 -1
- package/src/orchestrator/__tests__/node-identity.test.ts +62 -0
- package/src/orchestrator/host-process-orchestrator.ts +401 -134
- package/src/orchestrator/index.ts +3 -3
- package/src/orchestrator/node-identity.ts +40 -0
- package/src/orchestrator/port-allocator.ts +48 -0
- package/src/orchestrator/state-store.ts +7 -7
- package/src/orchestrator/types.ts +8 -8
- package/src/{authority/__tests__/authority-node-client.test.ts → owner/__tests__/owner-node-client.test.ts} +45 -17
- package/src/owner/index.ts +12 -0
- package/src/{authority/authority-node-client.ts → owner/owner-node-client.ts} +83 -23
- package/src/server/__tests__/error-handler.test.ts +22 -0
- package/src/server/__tests__/grants-admin-route.test.ts +73 -0
- package/src/server/__tests__/grants-route.test.ts +263 -0
- package/src/server/__tests__/nodes-route.test.ts +25 -25
- package/src/server/__tests__/server.smoke.test.ts +75 -0
- package/src/server/__tests__/settings-route.test.ts +17 -0
- package/src/server/__tests__/status-route.test.ts +21 -0
- package/src/server/__tests__/strands-route.test.ts +233 -0
- package/src/server/error-handler.ts +67 -1
- package/src/server/events/types.ts +1 -0
- package/src/server/index.ts +75 -17
- package/src/server/routes/grants-admin.ts +41 -0
- package/src/server/routes/grants.ts +210 -0
- package/src/server/routes/nodes.ts +19 -18
- package/src/server/routes/settings.ts +11 -3
- package/src/server/routes/status.ts +18 -8
- package/src/server/routes/strands.ts +54 -0
- package/src/strands/__tests__/strand-service.test.ts +165 -0
- package/src/strands/index.ts +14 -0
- package/src/strands/strand-service.ts +135 -0
- package/src/strands/types.ts +102 -0
- package/src/update/__tests__/release-key.test.ts +16 -3
- package/src/update/release-key.ts +8 -6
- package/dist/authority/authority-node-client.d.ts +0 -77
- package/dist/authority/authority-node-client.js.map +0 -1
- package/dist/authority/index.d.ts +0 -11
- package/dist/authority/index.js +0 -11
- package/dist/authority/index.js.map +0 -1
- package/dist/ui/assets/index-BlMXQPSw.js +0 -14
- package/dist/ui/assets/index-BlMXQPSw.js.map +0 -1
- package/dist/ui/assets/index-BwU5mJGF.css +0 -1
- package/src/authority/index.ts +0 -12
package/dist/ui/index.html
CHANGED
|
@@ -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-
|
|
9
|
-
<link rel="stylesheet" crossorigin href="./assets/index-
|
|
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.
|
|
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.
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
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.
|
|
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
|
|
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.
|
|
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.
|
|
68
|
-
"@serfab/cadre-core": "^0.
|
|
69
|
-
"@serfab/cadre-provider": "^0.
|
|
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
|
|
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
|
|
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-
|
|
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 {
|
|
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',
|
|
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
|
|
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
|
|
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
|
+
});
|