@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.
- package/README.md +332 -423
- package/dist/bin/host.d.ts +6 -3
- package/dist/bin/host.js +301 -505
- package/dist/bin/host.js.map +1 -1
- package/dist/bin/join-output.d.ts +56 -0
- package/dist/bin/join-output.js +92 -0
- package/dist/bin/join-output.js.map +1 -0
- package/dist/bin/nat-output.d.ts +49 -0
- package/dist/bin/nat-output.js +127 -0
- package/dist/bin/nat-output.js.map +1 -0
- package/dist/hosted/claim-details.d.ts +44 -0
- package/dist/hosted/claim-details.js +66 -0
- package/dist/hosted/claim-details.js.map +1 -0
- package/dist/hosted/hosted-node-service.d.ts +318 -0
- package/dist/hosted/hosted-node-service.js +666 -0
- package/dist/hosted/hosted-node-service.js.map +1 -0
- package/dist/hosted/hosted-node-store.d.ts +40 -0
- package/dist/hosted/hosted-node-store.js +126 -0
- package/dist/hosted/hosted-node-store.js.map +1 -0
- package/dist/{donation/donation-supervisor.d.ts → hosted/hosted-node-supervisor.d.ts} +70 -53
- package/dist/hosted/hosted-node-supervisor.js +396 -0
- package/dist/hosted/hosted-node-supervisor.js.map +1 -0
- package/dist/hosted/hosted-node-watcher.d.ts +83 -0
- package/dist/hosted/hosted-node-watcher.js +224 -0
- package/dist/hosted/hosted-node-watcher.js.map +1 -0
- package/dist/hosted/index.d.ts +16 -0
- package/dist/hosted/index.js +13 -0
- package/dist/hosted/index.js.map +1 -0
- package/dist/hosted/node-status.d.ts +35 -0
- package/dist/hosted/node-status.js +61 -0
- package/dist/hosted/node-status.js.map +1 -0
- package/dist/hosted/types.d.ts +134 -0
- package/dist/hosted/types.js +22 -0
- package/dist/hosted/types.js.map +1 -0
- package/dist/index.d.ts +12 -24
- package/dist/index.js +9 -19
- package/dist/index.js.map +1 -1
- package/dist/installer/config.d.ts +4 -34
- package/dist/installer/config.js +3 -18
- package/dist/installer/config.js.map +1 -1
- package/dist/installer/index.d.ts +7 -19
- package/dist/installer/index.js +14 -69
- package/dist/installer/index.js.map +1 -1
- package/dist/installer/paths.d.ts +0 -2
- package/dist/installer/paths.js +0 -4
- package/dist/installer/paths.js.map +1 -1
- package/dist/installer/wizard.d.ts +0 -7
- package/dist/installer/wizard.js +1 -8
- package/dist/installer/wizard.js.map +1 -1
- package/dist/nat/address-resolver.d.ts +18 -17
- package/dist/nat/address-resolver.js +79 -25
- package/dist/nat/address-resolver.js.map +1 -1
- package/dist/nat/address-watch.d.ts +37 -0
- package/dist/nat/address-watch.js +84 -0
- package/dist/nat/address-watch.js.map +1 -0
- package/dist/nat/ddns/duckdns.d.ts +2 -1
- package/dist/nat/ddns/duckdns.js +5 -1
- package/dist/nat/ddns/duckdns.js.map +1 -1
- package/dist/nat/ddns/index.d.ts +6 -0
- package/dist/nat/ddns/index.js.map +1 -1
- package/dist/nat/index.d.ts +12 -9
- package/dist/nat/index.js +6 -5
- package/dist/nat/index.js.map +1 -1
- package/dist/nat/nat-service.d.ts +203 -77
- package/dist/nat/nat-service.js +602 -237
- package/dist/nat/nat-service.js.map +1 -1
- package/dist/nat/nat-store.d.ts +12 -5
- package/dist/nat/nat-store.js +114 -30
- package/dist/nat/nat-store.js.map +1 -1
- package/dist/nat/port-mapper.d.ts +83 -79
- package/dist/nat/port-mapper.js +148 -189
- package/dist/nat/port-mapper.js.map +1 -1
- package/dist/nat/reachability.d.ts +31 -15
- package/dist/nat/reachability.js +97 -22
- package/dist/nat/reachability.js.map +1 -1
- package/dist/nat/types.d.ts +78 -27
- package/dist/nat/types.js +8 -5
- package/dist/nat/types.js.map +1 -1
- package/dist/orchestrator/host-process-orchestrator.d.ts +29 -71
- package/dist/orchestrator/host-process-orchestrator.js +78 -218
- package/dist/orchestrator/host-process-orchestrator.js.map +1 -1
- package/dist/{installer/identity.d.ts → orchestrator/identity-file.d.ts} +5 -5
- package/dist/{installer/identity.js → orchestrator/identity-file.js} +6 -6
- package/dist/orchestrator/identity-file.js.map +1 -0
- package/dist/orchestrator/index.d.ts +2 -3
- package/dist/orchestrator/index.js +1 -1
- package/dist/orchestrator/index.js.map +1 -1
- package/dist/orchestrator/node-identity.d.ts +6 -6
- package/dist/orchestrator/node-identity.js +7 -7
- package/dist/orchestrator/node-identity.js.map +1 -1
- package/dist/orchestrator/port-allocator.d.ts +5 -6
- package/dist/orchestrator/port-allocator.js +9 -11
- package/dist/orchestrator/port-allocator.js.map +1 -1
- package/dist/orchestrator/state-store.d.ts +3 -5
- package/dist/orchestrator/state-store.js +2 -10
- package/dist/orchestrator/state-store.js.map +1 -1
- package/dist/orchestrator/types.d.ts +33 -22
- package/dist/orchestrator/types.js.map +1 -1
- package/dist/server/error-handler.d.ts +5 -25
- package/dist/server/error-handler.js +11 -74
- package/dist/server/error-handler.js.map +1 -1
- package/dist/server/events/types.d.ts +7 -11
- package/dist/server/events/types.js +2 -1
- package/dist/server/events/types.js.map +1 -1
- package/dist/server/index.d.ts +11 -37
- package/dist/server/index.js +30 -48
- package/dist/server/index.js.map +1 -1
- package/dist/server/routes/hosted-nodes.d.ts +28 -0
- package/dist/server/routes/hosted-nodes.js +68 -0
- package/dist/server/routes/hosted-nodes.js.map +1 -0
- package/dist/server/routes/nat.d.ts +4 -1
- package/dist/server/routes/nat.js +13 -2
- package/dist/server/routes/nat.js.map +1 -1
- package/dist/server/routes/nodes.d.ts +6 -20
- package/dist/server/routes/nodes.js +7 -86
- package/dist/server/routes/nodes.js.map +1 -1
- package/dist/server/routes/settings.d.ts +2 -7
- package/dist/server/routes/settings.js +3 -7
- package/dist/server/routes/settings.js.map +1 -1
- package/dist/server/routes/status.d.ts +2 -25
- package/dist/server/routes/status.js +1 -11
- package/dist/server/routes/status.js.map +1 -1
- package/dist/server/server.js +8 -8
- package/dist/server/settings-store.d.ts +2 -2
- package/dist/server/settings-store.js +2 -2
- package/dist/server/static.js +1 -2
- package/dist/server/static.js.map +1 -1
- package/dist/ui/assets/index-4FWuU8ye.js +19 -0
- package/dist/ui/assets/index-4FWuU8ye.js.map +1 -0
- package/dist/ui/assets/index-Di9Q4d01.css +1 -0
- package/dist/ui/index.html +2 -2
- package/dist/update/index.d.ts +2 -2
- package/dist/update/index.js +2 -2
- package/dist/update/store.d.ts +1 -1
- package/dist/update/store.js +1 -1
- package/dist/update/types.d.ts +1 -1
- package/dist/update/types.js +1 -1
- package/package.json +5 -6
- package/src/__tests__/cli-nat.smoke.test.ts +219 -212
- package/src/__tests__/cli.smoke.test.ts +1 -1
- package/src/__tests__/global-setup.ts +2 -3
- package/src/__tests__/orchestrator-claim-secret.test.ts +112 -0
- package/src/__tests__/orchestrator-handle-contract.ts +134 -134
- package/src/__tests__/orchestrator-node-identity.test.ts +182 -203
- package/src/__tests__/orchestrator-ports.test.ts +124 -143
- package/src/__tests__/orchestrator-push.test.ts +138 -146
- package/src/__tests__/orchestrator.test.ts +1019 -1063
- package/src/bin/host.ts +368 -553
- package/src/bin/join-output.ts +120 -0
- package/src/bin/nat-output.ts +177 -0
- package/src/{donation → hosted}/__tests__/fake-orchestrator.test.ts +173 -173
- package/src/{donation → hosted}/__tests__/fake-orchestrator.ts +265 -243
- package/src/hosted/__tests__/hosted-node-service.test.ts +609 -0
- package/src/hosted/__tests__/hosted-node-supervisor.test.ts +474 -0
- package/src/hosted/__tests__/test-invitation.ts +33 -0
- package/src/hosted/claim-details.ts +96 -0
- package/src/hosted/hosted-node-service.ts +795 -0
- package/src/hosted/hosted-node-store.ts +144 -0
- package/src/hosted/hosted-node-supervisor.ts +439 -0
- package/src/hosted/hosted-node-watcher.ts +250 -0
- package/src/hosted/index.ts +53 -0
- package/src/hosted/node-status.ts +71 -0
- package/src/hosted/types.ts +153 -0
- package/src/index.ts +72 -91
- package/src/installer/__tests__/config.test.ts +15 -40
- package/src/installer/__tests__/installer.smoke.test.ts +5 -86
- package/src/installer/__tests__/wizard.test.ts +56 -64
- package/src/installer/config.ts +135 -175
- package/src/installer/index.ts +15 -83
- package/src/installer/paths.ts +104 -109
- package/src/installer/wizard.ts +87 -103
- package/src/nat/__tests__/address-resolver.test.ts +107 -82
- package/src/nat/__tests__/nat-service.test.ts +433 -408
- package/src/nat/__tests__/nat-store.test.ts +127 -93
- package/src/nat/__tests__/port-mapper.test.ts +30 -191
- package/src/nat/__tests__/reachability.test.ts +96 -13
- package/src/nat/address-resolver.ts +94 -52
- package/src/nat/address-watch.ts +92 -0
- package/src/nat/ddns/duckdns.ts +5 -1
- package/src/nat/ddns/index.ts +6 -0
- package/src/nat/index.ts +87 -71
- package/src/nat/nat-service.ts +889 -501
- package/src/nat/nat-store.ts +237 -154
- package/src/nat/port-mapper.ts +241 -329
- package/src/nat/reachability.ts +134 -25
- package/src/nat/types.ts +209 -159
- package/src/{installer/__tests__/identity.test.ts → orchestrator/__tests__/identity-file.test.ts} +34 -34
- package/src/orchestrator/__tests__/node-identity.test.ts +62 -62
- package/src/orchestrator/host-process-orchestrator.ts +81 -243
- package/src/{installer/identity.ts → orchestrator/identity-file.ts} +56 -56
- package/src/orchestrator/index.ts +9 -10
- package/src/orchestrator/node-identity.ts +41 -41
- package/src/orchestrator/port-allocator.ts +71 -73
- package/src/orchestrator/state-store.ts +76 -90
- package/src/orchestrator/types.ts +34 -23
- package/src/server/__tests__/bus.test.ts +38 -38
- package/src/server/__tests__/error-handler.test.ts +81 -118
- package/src/server/__tests__/fakes.ts +40 -44
- package/src/server/__tests__/hosted-nodes-route.test.ts +160 -0
- package/src/server/__tests__/nodes-route.test.ts +114 -283
- package/src/server/__tests__/publishers.test.ts +193 -180
- package/src/server/__tests__/server.smoke.test.ts +134 -223
- package/src/server/__tests__/settings-route.test.ts +179 -195
- package/src/server/__tests__/sse-route.test.ts +119 -119
- package/src/server/__tests__/status-route.test.ts +82 -121
- package/src/server/error-handler.ts +112 -178
- package/src/server/events/types.ts +20 -24
- package/src/server/index.ts +223 -267
- package/src/server/routes/hosted-nodes.ts +83 -0
- package/src/server/routes/nat.ts +75 -63
- package/src/server/routes/nodes.ts +96 -188
- package/src/server/routes/settings.ts +115 -123
- package/src/server/routes/status.ts +102 -134
- package/src/server/server.ts +8 -8
- package/src/server/settings-store.ts +2 -2
- package/src/server/static.ts +1 -2
- package/src/update/index.ts +2 -2
- package/src/update/store.ts +1 -1
- package/src/update/types.ts +1 -1
- package/dist/auth/duration.d.ts +0 -1
- package/dist/auth/duration.js +0 -32
- package/dist/auth/duration.js.map +0 -1
- package/dist/auth/index.d.ts +0 -11
- package/dist/auth/index.js +0 -9
- package/dist/auth/index.js.map +0 -1
- package/dist/auth/self-label.d.ts +0 -36
- package/dist/auth/self-label.js +0 -57
- package/dist/auth/self-label.js.map +0 -1
- package/dist/auth/trust-circle-store.d.ts +0 -26
- package/dist/auth/trust-circle-store.js +0 -129
- package/dist/auth/trust-circle-store.js.map +0 -1
- package/dist/auth/trust-circle.d.ts +0 -156
- package/dist/auth/trust-circle.js +0 -307
- package/dist/auth/trust-circle.js.map +0 -1
- package/dist/auth/types.d.ts +0 -84
- package/dist/auth/types.js +0 -24
- package/dist/auth/types.js.map +0 -1
- package/dist/donation/donation-service.d.ts +0 -389
- package/dist/donation/donation-service.js +0 -828
- package/dist/donation/donation-service.js.map +0 -1
- package/dist/donation/donation-store.d.ts +0 -51
- package/dist/donation/donation-store.js +0 -152
- package/dist/donation/donation-store.js.map +0 -1
- package/dist/donation/donation-supervisor.js +0 -364
- package/dist/donation/donation-supervisor.js.map +0 -1
- package/dist/donation/grant-service.d.ts +0 -80
- package/dist/donation/grant-service.js +0 -210
- package/dist/donation/grant-service.js.map +0 -1
- package/dist/donation/grant-store.d.ts +0 -38
- package/dist/donation/grant-store.js +0 -135
- package/dist/donation/grant-store.js.map +0 -1
- package/dist/donation/index.d.ts +0 -20
- package/dist/donation/index.js +0 -17
- package/dist/donation/index.js.map +0 -1
- package/dist/donation/types.d.ts +0 -224
- package/dist/donation/types.js +0 -40
- package/dist/donation/types.js.map +0 -1
- package/dist/installer/identity.js.map +0 -1
- package/dist/owner/index.d.ts +0 -11
- package/dist/owner/index.js +0 -11
- package/dist/owner/index.js.map +0 -1
- package/dist/owner/owner-node-client.d.ts +0 -110
- package/dist/owner/owner-node-client.js +0 -184
- package/dist/owner/owner-node-client.js.map +0 -1
- package/dist/server/routes/bootstrap-node-validation.d.ts +0 -115
- package/dist/server/routes/bootstrap-node-validation.js +0 -197
- package/dist/server/routes/bootstrap-node-validation.js.map +0 -1
- package/dist/server/routes/grants-admin.d.ts +0 -29
- package/dist/server/routes/grants-admin.js +0 -58
- package/dist/server/routes/grants-admin.js.map +0 -1
- package/dist/server/routes/grants.d.ts +0 -28
- package/dist/server/routes/grants.js +0 -165
- package/dist/server/routes/grants.js.map +0 -1
- package/dist/server/routes/provision-request-validation.d.ts +0 -45
- package/dist/server/routes/provision-request-validation.js +0 -97
- package/dist/server/routes/provision-request-validation.js.map +0 -1
- package/dist/server/routes/strands.d.ts +0 -20
- package/dist/server/routes/strands.js +0 -38
- package/dist/server/routes/strands.js.map +0 -1
- package/dist/server/routes/trust-circle.d.ts +0 -17
- package/dist/server/routes/trust-circle.js +0 -35
- package/dist/server/routes/trust-circle.js.map +0 -1
- package/dist/strands/index.d.ts +0 -7
- package/dist/strands/index.js +0 -6
- package/dist/strands/index.js.map +0 -1
- package/dist/strands/strand-service.d.ts +0 -50
- package/dist/strands/strand-service.js +0 -103
- package/dist/strands/strand-service.js.map +0 -1
- package/dist/strands/types.d.ts +0 -93
- package/dist/strands/types.js +0 -23
- package/dist/strands/types.js.map +0 -1
- package/dist/ui/assets/index-C6nELNfC.js +0 -23
- package/dist/ui/assets/index-C6nELNfC.js.map +0 -1
- package/dist/ui/assets/index-TtIYSeZO.css +0 -1
- package/src/__tests__/cli-invite.smoke.test.ts +0 -192
- package/src/__tests__/host-owner.smoke.test.ts +0 -123
- package/src/__tests__/orchestrator-owner.test.ts +0 -234
- package/src/__tests__/orchestrator-pin-keys.test.ts +0 -152
- package/src/auth/__tests__/duration.test.ts +0 -28
- package/src/auth/__tests__/self-label.test.ts +0 -85
- package/src/auth/__tests__/trust-circle-integration.test.ts +0 -152
- package/src/auth/__tests__/trust-circle-store.test.ts +0 -115
- package/src/auth/__tests__/trust-circle.test.ts +0 -445
- package/src/auth/duration.ts +0 -33
- package/src/auth/index.ts +0 -21
- package/src/auth/self-label.ts +0 -74
- package/src/auth/trust-circle-store.ts +0 -155
- package/src/auth/trust-circle.ts +0 -395
- package/src/auth/types.ts +0 -99
- package/src/donation/__tests__/donation-service.test.ts +0 -1193
- package/src/donation/__tests__/donation-supervisor.test.ts +0 -548
- package/src/donation/__tests__/grant-service.test.ts +0 -213
- package/src/donation/__tests__/grant-store.test.ts +0 -113
- package/src/donation/donation-service.ts +0 -1033
- package/src/donation/donation-store.ts +0 -176
- package/src/donation/donation-supervisor.ts +0 -411
- package/src/donation/grant-service.ts +0 -252
- package/src/donation/grant-store.ts +0 -152
- package/src/donation/index.ts +0 -65
- package/src/donation/types.ts +0 -260
- package/src/owner/__tests__/owner-node-client.test.ts +0 -194
- package/src/owner/index.ts +0 -12
- package/src/owner/owner-node-client.ts +0 -259
- package/src/server/__tests__/bootstrap-node-validation.test.ts +0 -138
- package/src/server/__tests__/grants-admin-route.test.ts +0 -171
- package/src/server/__tests__/grants-route.test.ts +0 -394
- package/src/server/__tests__/strands-route.test.ts +0 -234
- package/src/server/routes/bootstrap-node-validation.ts +0 -206
- package/src/server/routes/grants-admin.ts +0 -74
- package/src/server/routes/grants.ts +0 -193
- package/src/server/routes/provision-request-validation.ts +0 -105
- package/src/server/routes/strands.ts +0 -54
- package/src/server/routes/trust-circle.ts +0 -49
- package/src/strands/__tests__/strand-service.test.ts +0 -165
- package/src/strands/index.ts +0 -14
- package/src/strands/strand-service.ts +0 -135
- package/src/strands/types.ts +0 -102
|
@@ -1,38 +1,38 @@
|
|
|
1
|
-
import { describe, it, expect } from 'vitest';
|
|
2
|
-
|
|
3
|
-
import { EventBus } from '../events/bus.js';
|
|
4
|
-
import type { LocalUiEvent } from '../events/types.js';
|
|
5
|
-
|
|
6
|
-
describe('EventBus', () => {
|
|
7
|
-
it('delivers events to subscribers in order', () => {
|
|
8
|
-
const bus = new EventBus();
|
|
9
|
-
const received: LocalUiEvent[] = [];
|
|
10
|
-
bus.subscribe((e) => received.push(e));
|
|
11
|
-
bus.publish({ type: '
|
|
12
|
-
bus.publish({ type: '
|
|
13
|
-
expect(received).toEqual([
|
|
14
|
-
{ type: '
|
|
15
|
-
{ type: '
|
|
16
|
-
]);
|
|
17
|
-
});
|
|
18
|
-
|
|
19
|
-
it('unsubscribe stops further deliveries', () => {
|
|
20
|
-
const bus = new EventBus();
|
|
21
|
-
const received: LocalUiEvent[] = [];
|
|
22
|
-
const unsub = bus.subscribe((e) => received.push(e));
|
|
23
|
-
bus.publish({ type: '
|
|
24
|
-
unsub();
|
|
25
|
-
bus.publish({ type: '
|
|
26
|
-
expect(received).toHaveLength(1);
|
|
27
|
-
expect(bus.listenerCount()).toBe(0);
|
|
28
|
-
});
|
|
29
|
-
|
|
30
|
-
it('one listener throwing does not block others', () => {
|
|
31
|
-
const bus = new EventBus();
|
|
32
|
-
const received: LocalUiEvent[] = [];
|
|
33
|
-
bus.subscribe(() => { throw new Error('bad listener'); });
|
|
34
|
-
bus.subscribe((e) => received.push(e));
|
|
35
|
-
bus.publish({ type: '
|
|
36
|
-
expect(received).toHaveLength(1);
|
|
37
|
-
});
|
|
38
|
-
});
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
|
|
3
|
+
import { EventBus } from '../events/bus.js';
|
|
4
|
+
import type { LocalUiEvent } from '../events/types.js';
|
|
5
|
+
|
|
6
|
+
describe('EventBus', () => {
|
|
7
|
+
it('delivers events to subscribers in order', () => {
|
|
8
|
+
const bus = new EventBus();
|
|
9
|
+
const received: LocalUiEvent[] = [];
|
|
10
|
+
bus.subscribe((e) => received.push(e));
|
|
11
|
+
bus.publish({ type: 'hosted-nodes-changed', kind: 'added', nodeId: 'hn_a' });
|
|
12
|
+
bus.publish({ type: 'hosted-nodes-changed', kind: 'claimed', nodeId: 'hn_a' });
|
|
13
|
+
expect(received).toEqual([
|
|
14
|
+
{ type: 'hosted-nodes-changed', kind: 'added', nodeId: 'hn_a' },
|
|
15
|
+
{ type: 'hosted-nodes-changed', kind: 'claimed', nodeId: 'hn_a' },
|
|
16
|
+
]);
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
it('unsubscribe stops further deliveries', () => {
|
|
20
|
+
const bus = new EventBus();
|
|
21
|
+
const received: LocalUiEvent[] = [];
|
|
22
|
+
const unsub = bus.subscribe((e) => received.push(e));
|
|
23
|
+
bus.publish({ type: 'hosted-nodes-changed', kind: 'added', nodeId: 'hn_a' });
|
|
24
|
+
unsub();
|
|
25
|
+
bus.publish({ type: 'hosted-nodes-changed', kind: 'removed', nodeId: 'hn_a' });
|
|
26
|
+
expect(received).toHaveLength(1);
|
|
27
|
+
expect(bus.listenerCount()).toBe(0);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it('one listener throwing does not block others', () => {
|
|
31
|
+
const bus = new EventBus();
|
|
32
|
+
const received: LocalUiEvent[] = [];
|
|
33
|
+
bus.subscribe(() => { throw new Error('bad listener'); });
|
|
34
|
+
bus.subscribe((e) => received.push(e));
|
|
35
|
+
bus.publish({ type: 'hosted-nodes-changed', kind: 'added', nodeId: 'hn_a' });
|
|
36
|
+
expect(received).toHaveLength(1);
|
|
37
|
+
});
|
|
38
|
+
});
|
|
@@ -1,118 +1,81 @@
|
|
|
1
|
-
import Fastify, { type FastifyRequest } from 'fastify';
|
|
2
|
-
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
3
|
-
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
app
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
});
|
|
50
|
-
|
|
51
|
-
it('
|
|
52
|
-
const res = await app.inject({ method: 'GET', url: '/
|
|
53
|
-
expect(res.statusCode).toBe(
|
|
54
|
-
});
|
|
55
|
-
|
|
56
|
-
it('
|
|
57
|
-
const res = await app.inject({ method: 'GET', url: '/
|
|
58
|
-
expect(res.statusCode).toBe(
|
|
59
|
-
});
|
|
60
|
-
|
|
61
|
-
it('
|
|
62
|
-
const
|
|
63
|
-
expect(
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
expect(
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
const
|
|
75
|
-
expect(
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
it('UpdateErrorException apply_in_progress → 409', async () => {
|
|
84
|
-
const res = await app.inject({ method: 'GET', url: '/update/apply_in_progress' });
|
|
85
|
-
expect(res.statusCode).toBe(409);
|
|
86
|
-
});
|
|
87
|
-
|
|
88
|
-
it('UpdateErrorException no_update_available → 400', async () => {
|
|
89
|
-
const res = await app.inject({ method: 'GET', url: '/update/no_update_available' });
|
|
90
|
-
expect(res.statusCode).toBe(400);
|
|
91
|
-
});
|
|
92
|
-
|
|
93
|
-
it('GrantError invalid_max_nodes → 400', async () => {
|
|
94
|
-
const res = await app.inject({ method: 'GET', url: '/grant/invalid_max_nodes' });
|
|
95
|
-
expect(res.statusCode).toBe(400);
|
|
96
|
-
const body = res.json() as { error: { code: string } };
|
|
97
|
-
expect(body.error.code).toBe('invalid_max_nodes');
|
|
98
|
-
});
|
|
99
|
-
|
|
100
|
-
it('GrantError not_found → 404', async () => {
|
|
101
|
-
const res = await app.inject({ method: 'GET', url: '/grant/not_found' });
|
|
102
|
-
expect(res.statusCode).toBe(404);
|
|
103
|
-
});
|
|
104
|
-
|
|
105
|
-
it('GrantError storage_error → 500', async () => {
|
|
106
|
-
const res = await app.inject({ method: 'GET', url: '/grant/storage_error' });
|
|
107
|
-
expect(res.statusCode).toBe(500);
|
|
108
|
-
});
|
|
109
|
-
|
|
110
|
-
it('unknown Error → 500 internal', async () => {
|
|
111
|
-
const res = await app.inject({ method: 'GET', url: '/unknown' });
|
|
112
|
-
expect(res.statusCode).toBe(500);
|
|
113
|
-
const body = res.json() as { ok: boolean; error: { code: string; message: string } };
|
|
114
|
-
expect(body.ok).toBe(false);
|
|
115
|
-
expect(body.error.code).toBe('internal');
|
|
116
|
-
expect(body.error.message).toBe('mystery');
|
|
117
|
-
});
|
|
118
|
-
});
|
|
1
|
+
import Fastify, { type FastifyRequest } from 'fastify';
|
|
2
|
+
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
3
|
+
|
|
4
|
+
import { NatError } from '../../nat/types.js';
|
|
5
|
+
import { UpdateErrorException } from '../../update/types.js';
|
|
6
|
+
import { HostedNodeError } from '../../hosted/types.js';
|
|
7
|
+
import { registerErrorHandler } from '../error-handler.js';
|
|
8
|
+
|
|
9
|
+
describe('error handler', () => {
|
|
10
|
+
let app: ReturnType<typeof Fastify>;
|
|
11
|
+
|
|
12
|
+
beforeEach(async () => {
|
|
13
|
+
app = Fastify();
|
|
14
|
+
registerErrorHandler(app);
|
|
15
|
+
app.get('/nat/:code', async (req: FastifyRequest) => {
|
|
16
|
+
const { code } = req.params as { code: string };
|
|
17
|
+
throw new NatError(code as never, `nat err: ${code}`);
|
|
18
|
+
});
|
|
19
|
+
app.get('/update/:code', async (req: FastifyRequest) => {
|
|
20
|
+
const { code } = req.params as { code: string };
|
|
21
|
+
throw new UpdateErrorException(code as never, `update err: ${code}`);
|
|
22
|
+
});
|
|
23
|
+
app.get('/hosted/:code', async (req: FastifyRequest) => {
|
|
24
|
+
const { code } = req.params as { code: string };
|
|
25
|
+
throw new HostedNodeError(code as never, `hosted err: ${code}`);
|
|
26
|
+
});
|
|
27
|
+
app.get('/unknown', async () => {
|
|
28
|
+
throw new Error('mystery');
|
|
29
|
+
});
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
afterEach(async () => {
|
|
33
|
+
await app.close();
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it('NatError invalid_config → 400', async () => {
|
|
37
|
+
const res = await app.inject({ method: 'GET', url: '/nat/invalid_config' });
|
|
38
|
+
expect(res.statusCode).toBe(400);
|
|
39
|
+
const body = res.json() as { ok: boolean; error: { code: string; message: string } };
|
|
40
|
+
expect(body).toEqual({
|
|
41
|
+
ok: false,
|
|
42
|
+
error: { code: 'invalid_config', message: 'nat err: invalid_config' },
|
|
43
|
+
});
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it('NatError secrets_unavailable → 500', async () => {
|
|
47
|
+
const res = await app.inject({ method: 'GET', url: '/nat/secrets_unavailable' });
|
|
48
|
+
expect(res.statusCode).toBe(500);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it('UpdateErrorException apply_in_progress → 409', async () => {
|
|
52
|
+
const res = await app.inject({ method: 'GET', url: '/update/apply_in_progress' });
|
|
53
|
+
expect(res.statusCode).toBe(409);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it('UpdateErrorException no_update_available → 400', async () => {
|
|
57
|
+
const res = await app.inject({ method: 'GET', url: '/update/no_update_available' });
|
|
58
|
+
expect(res.statusCode).toBe(400);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it('HostedNodeError maps each code to its status', async () => {
|
|
62
|
+
const statusOf = async (code: string): Promise<number> => (await app.inject({ method: 'GET', url: `/hosted/${code}` })).statusCode;
|
|
63
|
+
expect(await statusOf('invalid_request')).toBe(400);
|
|
64
|
+
expect(await statusOf('not_found')).toBe(404);
|
|
65
|
+
expect(await statusOf('invalid_state')).toBe(409);
|
|
66
|
+
expect(await statusOf('node_unavailable')).toBe(503);
|
|
67
|
+
expect(await statusOf('orchestrator_error')).toBe(500);
|
|
68
|
+
expect(await statusOf('storage_error')).toBe(500);
|
|
69
|
+
const body = (await app.inject({ method: 'GET', url: '/hosted/invalid_state' })).json() as { error: { code: string } };
|
|
70
|
+
expect(body.error.code).toBe('invalid_state');
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it('unknown Error → 500 internal', async () => {
|
|
74
|
+
const res = await app.inject({ method: 'GET', url: '/unknown' });
|
|
75
|
+
expect(res.statusCode).toBe(500);
|
|
76
|
+
const body = res.json() as { ok: boolean; error: { code: string; message: string } };
|
|
77
|
+
expect(body.ok).toBe(false);
|
|
78
|
+
expect(body.error.code).toBe('internal');
|
|
79
|
+
expect(body.error.message).toBe('mystery');
|
|
80
|
+
});
|
|
81
|
+
});
|
|
@@ -1,44 +1,40 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Shared fakes for the local-UI server tests. Holds no tests of its own.
|
|
3
|
-
*/
|
|
4
|
-
|
|
5
|
-
import type {
|
|
6
|
-
import type {
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
strands: { list: async () => ({ strands: [], controlConnections: 0 }) } as unknown as StrandService,
|
|
42
|
-
...overrides,
|
|
43
|
-
};
|
|
44
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* Shared fakes for the local-UI server tests. Holds no tests of its own.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import type { NatService, NatChangeListener } from '../../nat/index.js';
|
|
6
|
+
import type { NatStatusSnapshot } from '../../nat/types.js';
|
|
7
|
+
|
|
8
|
+
export const SAMPLE_CONNECTIVITY: NatStatusSnapshot = {
|
|
9
|
+
upnpEnabled: true,
|
|
10
|
+
gateway: { found: true, lanAddress: '192.168.1.20', routerExternalIp: '203.0.113.5', lastError: null },
|
|
11
|
+
externalIp: '203.0.113.5',
|
|
12
|
+
externalIpDetectedAt: null,
|
|
13
|
+
cgnatDetected: false,
|
|
14
|
+
directReachability: 'reachable',
|
|
15
|
+
lastTestedAt: null,
|
|
16
|
+
ddns: {
|
|
17
|
+
providerId: null,
|
|
18
|
+
hostname: null,
|
|
19
|
+
externallyManaged: false,
|
|
20
|
+
lastUpdateAt: null,
|
|
21
|
+
lastUpdateOk: null,
|
|
22
|
+
lastError: null,
|
|
23
|
+
},
|
|
24
|
+
nodes: [],
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* A NAT service that answers only the reads the server makes on its own
|
|
29
|
+
* (status, boot publish) and the writes the settings route forwards. `emit`
|
|
30
|
+
* fires the change listeners the server registered, for tests of that wiring.
|
|
31
|
+
*/
|
|
32
|
+
export function fakeNat(snapshot: NatStatusSnapshot = SAMPLE_CONNECTIVITY): NatService & { emit(snap: NatStatusSnapshot): void } {
|
|
33
|
+
const listeners = new Set<NatChangeListener>();
|
|
34
|
+
return {
|
|
35
|
+
getStatus: () => snapshot,
|
|
36
|
+
onChange: (listener: NatChangeListener) => { listeners.add(listener); return () => { listeners.delete(listener); }; },
|
|
37
|
+
putSettings: async () => snapshot,
|
|
38
|
+
emit: (snap: NatStatusSnapshot) => { for (const l of listeners) l(snap); },
|
|
39
|
+
} as unknown as NatService & { emit(snap: NatStatusSnapshot): void };
|
|
40
|
+
}
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `/api/hosted-nodes` route tests: the status code each outcome maps to, driven
|
|
3
|
+
* against a stub service so no child spawns, and the join-by-invitation refusals
|
|
4
|
+
* against the real service over the fake orchestrator, where "before anything is
|
|
5
|
+
* spawned" can be checked. The service's own behaviour is
|
|
6
|
+
* `hosted/__tests__/hosted-node-service.test.ts`.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import Fastify from 'fastify';
|
|
10
|
+
import { mkdtempSync, rmSync } from 'node:fs';
|
|
11
|
+
import { tmpdir } from 'node:os';
|
|
12
|
+
import { join } from 'node:path';
|
|
13
|
+
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
|
|
14
|
+
|
|
15
|
+
import { HostedNodeService } from '../../hosted/hosted-node-service.js';
|
|
16
|
+
import { HostedNodeStore } from '../../hosted/hosted-node-store.js';
|
|
17
|
+
import { HostedNodeError, type HostedNodeView } from '../../hosted/types.js';
|
|
18
|
+
import { FakeOrchestrator } from '../../hosted/__tests__/fake-orchestrator.js';
|
|
19
|
+
import { encodedTestInvitation } from '../../hosted/__tests__/test-invitation.js';
|
|
20
|
+
import { registerErrorHandler } from '../error-handler.js';
|
|
21
|
+
import { registerHostedNodesRoutes } from '../routes/hosted-nodes.js';
|
|
22
|
+
|
|
23
|
+
const UNCLAIMED: HostedNodeView = {
|
|
24
|
+
id: 'hn_a', join: { kind: 'claim' }, partyId: 'unclaimed', profile: 'storage', status: 'unclaimed',
|
|
25
|
+
dockerId: '1:a', createdAt: '2026-01-01T00:00:00.000Z', updatedAt: '2026-01-01T00:00:00.000Z',
|
|
26
|
+
};
|
|
27
|
+
const JOINED: HostedNodeView = { ...UNCLAIMED, id: 'hn_b', status: 'joined', partyId: 'party-P', ownerKey: 'owner' };
|
|
28
|
+
|
|
29
|
+
/** A service with two nodes, answering as the real one would for each. */
|
|
30
|
+
function stubService(): HostedNodeService & { removed: string[] } {
|
|
31
|
+
const nodes = new Map([[UNCLAIMED.id, UNCLAIMED], [JOINED.id, JOINED]]);
|
|
32
|
+
const removed: string[] = [];
|
|
33
|
+
const require = (id: string): HostedNodeView => {
|
|
34
|
+
const node = nodes.get(id);
|
|
35
|
+
if (!node) throw new HostedNodeError('not_found', `No such hosted node: ${id}`);
|
|
36
|
+
return node;
|
|
37
|
+
};
|
|
38
|
+
return {
|
|
39
|
+
removed,
|
|
40
|
+
list: () => [...nodes.values()],
|
|
41
|
+
get: (id: string) => nodes.get(id),
|
|
42
|
+
join: async () => ({ ...UNCLAIMED, id: 'hn_new' }),
|
|
43
|
+
claimDetails: async (id: string) => {
|
|
44
|
+
const node = require(id);
|
|
45
|
+
if (node.status !== 'unclaimed') throw new HostedNodeError('invalid_state', `Hosted node ${id} is ${node.status}`);
|
|
46
|
+
if (id === 'hn_starting') throw new HostedNodeError('node_unavailable', 'not yet');
|
|
47
|
+
return { payload: 'sereus-join:1.x', peerId: '12D3KooW', multiaddrs: ['/ip4/192.168.1.20/tcp/1/ws/p2p/12D3KooW'], reachability: null };
|
|
48
|
+
},
|
|
49
|
+
remove: async (id: string) => { require(id); removed.push(id); },
|
|
50
|
+
reset: async (id: string) => { require(id); return { ...UNCLAIMED, id: 'hn_reset' }; },
|
|
51
|
+
} as unknown as HostedNodeService & { removed: string[] };
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
describe('/api/hosted-nodes routes', () => {
|
|
55
|
+
let app: ReturnType<typeof Fastify>;
|
|
56
|
+
let service: ReturnType<typeof stubService>;
|
|
57
|
+
|
|
58
|
+
beforeEach(() => {
|
|
59
|
+
app = Fastify();
|
|
60
|
+
registerErrorHandler(app);
|
|
61
|
+
service = stubService();
|
|
62
|
+
registerHostedNodesRoutes(app, { hostedNodes: service });
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
afterEach(async () => {
|
|
66
|
+
await app.close();
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it('answers each outcome with its status code', async () => {
|
|
70
|
+
expect((await app.inject({ method: 'GET', url: '/api/hosted-nodes' })).json()).toEqual({ ok: true, data: { nodes: [UNCLAIMED, JOINED] } });
|
|
71
|
+
|
|
72
|
+
const created = await app.inject({ method: 'POST', url: '/api/hosted-nodes' });
|
|
73
|
+
expect(created.statusCode).toBe(201);
|
|
74
|
+
expect(created.json()).toMatchObject({ ok: true, data: { node: { id: 'hn_new', status: 'unclaimed' } } });
|
|
75
|
+
|
|
76
|
+
expect((await app.inject({ method: 'GET', url: '/api/hosted-nodes/hn_a' })).json()).toEqual({ ok: true, data: { node: UNCLAIMED } });
|
|
77
|
+
const missing = await app.inject({ method: 'GET', url: '/api/hosted-nodes/hn_nobody' });
|
|
78
|
+
expect(missing.statusCode).toBe(404);
|
|
79
|
+
expect(missing.json()).toMatchObject({ ok: false, error: { code: 'not_found' } });
|
|
80
|
+
|
|
81
|
+
const claim = await app.inject({ method: 'GET', url: '/api/hosted-nodes/hn_a/claim' });
|
|
82
|
+
expect(claim.statusCode).toBe(200);
|
|
83
|
+
expect(claim.json()).toMatchObject({ ok: true, data: { payload: 'sereus-join:1.x', peerId: '12D3KooW' } });
|
|
84
|
+
|
|
85
|
+
const removed = await app.inject({ method: 'DELETE', url: '/api/hosted-nodes/hn_b' });
|
|
86
|
+
expect(removed.statusCode).toBe(204);
|
|
87
|
+
expect(service.removed).toEqual(['hn_b']);
|
|
88
|
+
expect((await app.inject({ method: 'DELETE', url: '/api/hosted-nodes/hn_nobody' })).statusCode).toBe(404);
|
|
89
|
+
|
|
90
|
+
const reset = await app.inject({ method: 'POST', url: '/api/hosted-nodes/hn_a/reset' });
|
|
91
|
+
expect(reset.statusCode).toBe(201);
|
|
92
|
+
expect(reset.json()).toMatchObject({ ok: true, data: { node: { id: 'hn_reset' } } });
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it('answers 409 for the claim details of a joined node', async () => {
|
|
96
|
+
const res = await app.inject({ method: 'GET', url: '/api/hosted-nodes/hn_b/claim' });
|
|
97
|
+
expect(res.statusCode).toBe(409);
|
|
98
|
+
expect(res.json()).toMatchObject({ ok: false, error: { code: 'invalid_state' } });
|
|
99
|
+
});
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
describe('/api/hosted-nodes join by invitation, over the real service', () => {
|
|
103
|
+
let app: ReturnType<typeof Fastify>;
|
|
104
|
+
let orch: FakeOrchestrator;
|
|
105
|
+
let store: HostedNodeStore;
|
|
106
|
+
let tmpRoot: string;
|
|
107
|
+
|
|
108
|
+
beforeEach(() => {
|
|
109
|
+
tmpRoot = mkdtempSync(join(tmpdir(), 'cadre-host-hosted-route-'));
|
|
110
|
+
orch = new FakeOrchestrator();
|
|
111
|
+
store = new HostedNodeStore(tmpRoot);
|
|
112
|
+
const hostedNodes = new HostedNodeService({
|
|
113
|
+
orchestrator: orch,
|
|
114
|
+
store,
|
|
115
|
+
addresses: { publicAddressesFor: () => [], getStatus: () => ({ nodes: [] }) },
|
|
116
|
+
});
|
|
117
|
+
app = Fastify();
|
|
118
|
+
registerErrorHandler(app);
|
|
119
|
+
registerHostedNodesRoutes(app, { hostedNodes });
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
afterEach(async () => {
|
|
123
|
+
await app.close();
|
|
124
|
+
rmSync(tmpRoot, { recursive: true, force: true });
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
it('answers 400 for an invitation that does not decode, before anything is written or spawned', async () => {
|
|
128
|
+
for (const invitation of ['not an invitation', 42]) {
|
|
129
|
+
const res = await app.inject({ method: 'POST', url: '/api/hosted-nodes', payload: { invitation } });
|
|
130
|
+
expect(res.statusCode).toBe(400);
|
|
131
|
+
expect(res.json()).toMatchObject({ ok: false, error: { code: 'invalid_request' } });
|
|
132
|
+
}
|
|
133
|
+
expect(store.list()).toEqual([]);
|
|
134
|
+
expect(orch.createCalls).toEqual([]);
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
it('answers 409 to Retry unless no member of the cadre could be reached', async () => {
|
|
138
|
+
const created = await app.inject({ method: 'POST', url: '/api/hosted-nodes', payload: { invitation: encodedTestInvitation('party-P') } });
|
|
139
|
+
expect(created.statusCode).toBe(201);
|
|
140
|
+
const { id } = (created.json() as { data: { node: HostedNodeView } }).data.node;
|
|
141
|
+
|
|
142
|
+
// Still joining: nothing to retry.
|
|
143
|
+
expect((await app.inject({ method: 'POST', url: `/api/hosted-nodes/${id}/retry` })).statusCode).toBe(409);
|
|
144
|
+
|
|
145
|
+
// Refused by a member (an expired invitation): final.
|
|
146
|
+
const node = store.get(id)!;
|
|
147
|
+
store.put({ ...node, status: 'error', error: 'Cadre invitation refused: expired (invite-spent)', retryable: false });
|
|
148
|
+
const refused = await app.inject({ method: 'POST', url: `/api/hosted-nodes/${id}/retry` });
|
|
149
|
+
expect(refused.statusCode).toBe(409);
|
|
150
|
+
expect(refused.json()).toMatchObject({ ok: false, error: { code: 'invalid_state' } });
|
|
151
|
+
expect(orch.createCalls).toHaveLength(1);
|
|
152
|
+
|
|
153
|
+
// No member reachable: Retry starts it again.
|
|
154
|
+
store.put({ ...node, status: 'error', error: 'No member named in the invitation could be reached.', retryable: true });
|
|
155
|
+
orch.crash(node.dockerId!);
|
|
156
|
+
const retried = await app.inject({ method: 'POST', url: `/api/hosted-nodes/${id}/retry` });
|
|
157
|
+
expect(retried.statusCode).toBe(200);
|
|
158
|
+
expect(retried.json()).toMatchObject({ ok: true, data: { node: { id, status: 'joining' } } });
|
|
159
|
+
});
|
|
160
|
+
});
|