@serfab/cadre-host 1.7.0 → 1.9.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 +31 -16
- package/dist/bin/host.js +41 -20
- package/dist/bin/host.js.map +1 -1
- package/dist/donation/donation-service.d.ts +27 -4
- package/dist/donation/donation-service.js +57 -6
- package/dist/donation/donation-service.js.map +1 -1
- package/dist/donation/donation-store.d.ts +3 -1
- package/dist/donation/donation-store.js +5 -1
- package/dist/donation/donation-store.js.map +1 -1
- package/dist/donation/grant-service.d.ts +16 -7
- package/dist/donation/grant-service.js +57 -10
- package/dist/donation/grant-service.js.map +1 -1
- package/dist/donation/index.d.ts +2 -2
- package/dist/donation/index.js +1 -1
- package/dist/donation/index.js.map +1 -1
- package/dist/donation/types.d.ts +29 -7
- package/dist/donation/types.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/nat/address-resolver.d.ts +3 -2
- package/dist/nat/address-resolver.js.map +1 -1
- package/dist/orchestrator/host-process-orchestrator.d.ts +2 -2
- package/dist/orchestrator/host-process-orchestrator.js +22 -12
- package/dist/orchestrator/host-process-orchestrator.js.map +1 -1
- package/dist/orchestrator/node-identity.d.ts +1 -1
- package/dist/orchestrator/node-identity.js +1 -1
- package/dist/orchestrator/port-allocator.d.ts +6 -39
- package/dist/orchestrator/port-allocator.js +9 -89
- package/dist/orchestrator/port-allocator.js.map +1 -1
- package/dist/server/events/types.d.ts +3 -0
- package/dist/server/index.d.ts +19 -18
- package/dist/server/index.js +13 -15
- package/dist/server/index.js.map +1 -1
- package/dist/server/routes/grants-admin.d.ts +15 -3
- package/dist/server/routes/grants-admin.js +33 -7
- package/dist/server/routes/grants-admin.js.map +1 -1
- package/dist/server/routes/nodes.d.ts +13 -5
- package/dist/server/routes/nodes.js +58 -39
- package/dist/server/routes/nodes.js.map +1 -1
- package/dist/server/routes/status.d.ts +13 -0
- package/dist/server/routes/status.js +2 -0
- package/dist/server/routes/status.js.map +1 -1
- package/dist/ui/assets/index-C6nELNfC.js +23 -0
- package/dist/ui/assets/index-C6nELNfC.js.map +1 -0
- package/dist/ui/assets/index-TtIYSeZO.css +1 -0
- package/dist/ui/index.html +2 -2
- package/package.json +4 -4
- package/src/__tests__/global-setup.ts +58 -56
- package/src/__tests__/orchestrator-env-scrub.test.ts +12 -11
- package/src/__tests__/orchestrator-handle-contract.ts +134 -0
- package/src/__tests__/orchestrator-node-identity.test.ts +6 -7
- package/src/__tests__/orchestrator-owner.test.ts +6 -13
- package/src/__tests__/orchestrator-pin-keys.test.ts +6 -7
- package/src/__tests__/orchestrator-ports.test.ts +5 -34
- package/src/__tests__/orchestrator-push.test.ts +6 -5
- package/src/__tests__/orchestrator-teardown.ts +65 -0
- package/src/__tests__/orchestrator.test.ts +23 -22
- package/src/auth/__tests__/trust-circle-integration.test.ts +1 -1
- package/src/bin/host.ts +56 -33
- package/src/donation/__tests__/donation-service.test.ts +38 -0
- package/src/donation/__tests__/fake-orchestrator.test.ts +19 -14
- package/src/donation/__tests__/fake-orchestrator.ts +21 -3
- package/src/donation/__tests__/grant-service.test.ts +2 -2
- package/src/donation/donation-service.ts +58 -6
- package/src/donation/donation-store.ts +176 -171
- package/src/donation/grant-service.ts +252 -200
- package/src/donation/index.ts +65 -64
- package/src/donation/types.ts +260 -244
- package/src/index.ts +2 -0
- package/src/nat/address-resolver.ts +3 -2
- package/src/orchestrator/host-process-orchestrator.ts +1260 -1251
- package/src/orchestrator/node-identity.ts +1 -1
- package/src/orchestrator/port-allocator.ts +16 -91
- package/src/server/__tests__/fakes.ts +44 -0
- package/src/server/__tests__/grants-admin-route.test.ts +171 -73
- package/src/server/__tests__/nodes-route.test.ts +49 -25
- package/src/server/__tests__/publishers.test.ts +5 -41
- package/src/server/__tests__/server.smoke.test.ts +7 -37
- package/src/server/__tests__/status-route.test.ts +8 -21
- package/src/server/__tests__/strands-route.test.ts +3 -2
- package/src/server/events/types.ts +24 -23
- package/src/server/index.ts +267 -267
- package/src/server/routes/grants-admin.ts +74 -41
- package/src/server/routes/nodes.ts +61 -43
- package/src/server/routes/status.ts +16 -0
- package/dist/donation/__tests__/fake-orchestrator.d.ts +0 -103
- package/dist/donation/__tests__/fake-orchestrator.js +0 -180
- package/dist/donation/__tests__/fake-orchestrator.js.map +0 -1
- package/dist/ui/assets/index-BVoqLjyX.css +0 -1
- package/dist/ui/assets/index-CEoNRwM_.js +0 -17
- package/dist/ui/assets/index-CEoNRwM_.js.map +0 -1
|
@@ -5,10 +5,9 @@ import { join } from 'node:path';
|
|
|
5
5
|
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
6
6
|
|
|
7
7
|
import { createLocalUiServer } from '../index.js';
|
|
8
|
+
import { fakeFounder } from './fakes.js';
|
|
8
9
|
import type { HostProcessOrchestrator } from '../../orchestrator/index.js';
|
|
9
10
|
import type { TrustCircleService } from '../../auth/index.js';
|
|
10
|
-
import type { NatService } from '../../nat/index.js';
|
|
11
|
-
import type { NatStatusSnapshot } from '../../nat/types.js';
|
|
12
11
|
import { GrantService, GrantStore } from '../../donation/index.js';
|
|
13
12
|
|
|
14
13
|
function fakeOrchestrator(): HostProcessOrchestrator {
|
|
@@ -43,37 +42,6 @@ function fakeTrustCircle(): TrustCircleService {
|
|
|
43
42
|
} as unknown as TrustCircleService;
|
|
44
43
|
}
|
|
45
44
|
|
|
46
|
-
const SAMPLE_CONNECTIVITY: NatStatusSnapshot = {
|
|
47
|
-
portMode: 'disabled',
|
|
48
|
-
externalPort: 4001,
|
|
49
|
-
internalPort: 4001,
|
|
50
|
-
routerExternalIp: null,
|
|
51
|
-
mappingLeaseExpiresAt: null,
|
|
52
|
-
externalIp: null,
|
|
53
|
-
externalIpDetectedAt: null,
|
|
54
|
-
cgnatDetected: false,
|
|
55
|
-
directReachability: 'unknown',
|
|
56
|
-
lastTestedAt: null,
|
|
57
|
-
ddns: {
|
|
58
|
-
providerId: null,
|
|
59
|
-
hostname: null,
|
|
60
|
-
externallyManaged: false,
|
|
61
|
-
lastUpdateAt: null,
|
|
62
|
-
lastUpdateOk: null,
|
|
63
|
-
lastError: null,
|
|
64
|
-
},
|
|
65
|
-
};
|
|
66
|
-
|
|
67
|
-
function fakeNat(): NatService {
|
|
68
|
-
return {
|
|
69
|
-
getStatus: () => SAMPLE_CONNECTIVITY,
|
|
70
|
-
putSettings: async () => SAMPLE_CONNECTIVITY,
|
|
71
|
-
testReachability: async () => SAMPLE_CONNECTIVITY,
|
|
72
|
-
listDdnsProviders: () => [],
|
|
73
|
-
putDdns: async () => SAMPLE_CONNECTIVITY,
|
|
74
|
-
} as unknown as NatService;
|
|
75
|
-
}
|
|
76
|
-
|
|
77
45
|
function writeConfig(dir: string): void {
|
|
78
46
|
const cfg = {
|
|
79
47
|
version: 2,
|
|
@@ -102,8 +70,7 @@ describe('createLocalUiServer smoke', () => {
|
|
|
102
70
|
uiPort: 8765,
|
|
103
71
|
dataDir,
|
|
104
72
|
orchestrator: fakeOrchestrator(),
|
|
105
|
-
trustCircle: fakeTrustCircle(),
|
|
106
|
-
nat: fakeNat(),
|
|
73
|
+
founder: fakeFounder({ trustCircle: fakeTrustCircle() }),
|
|
107
74
|
forcePort: 0,
|
|
108
75
|
});
|
|
109
76
|
const { url } = await server.start();
|
|
@@ -119,8 +86,9 @@ describe('createLocalUiServer smoke', () => {
|
|
|
119
86
|
// Without host header: undici's fetch sets it; this is the happy path.
|
|
120
87
|
const res = await fetch(`${baseUrl}/api/status`);
|
|
121
88
|
expect(res.status).toBe(200);
|
|
122
|
-
const body = await res.json() as { service: { name: string } };
|
|
89
|
+
const body = await res.json() as { service: { name: string }; role: string };
|
|
123
90
|
expect(body.service.name).toBe('cadre-host');
|
|
91
|
+
expect(body.role).toBe('founder');
|
|
124
92
|
});
|
|
125
93
|
|
|
126
94
|
it('rejects requests with a foreign Host header', async () => {
|
|
@@ -205,16 +173,18 @@ describe('createLocalUiServer smoke — donor-only (no owner node)', () => {
|
|
|
205
173
|
rmSync(dataDir, { recursive: true, force: true });
|
|
206
174
|
});
|
|
207
175
|
|
|
208
|
-
it('serves /api/status with no trustCircle / connectivity and no nodes', async () => {
|
|
176
|
+
it('serves /api/status as role donor, with no trustCircle / connectivity and no nodes', async () => {
|
|
209
177
|
const res = await fetch(`${baseUrl}/api/status`);
|
|
210
178
|
expect(res.status).toBe(200);
|
|
211
179
|
const body = await res.json() as {
|
|
212
180
|
service: { name: string };
|
|
181
|
+
role: string;
|
|
213
182
|
nodes: unknown[];
|
|
214
183
|
trustCircle?: unknown;
|
|
215
184
|
connectivity?: unknown;
|
|
216
185
|
};
|
|
217
186
|
expect(body.service.name).toBe('cadre-host');
|
|
187
|
+
expect(body.role).toBe('donor');
|
|
218
188
|
expect(body.nodes).toEqual([]);
|
|
219
189
|
expect(body.trustCircle).toBeUndefined();
|
|
220
190
|
expect(body.connectivity).toBeUndefined();
|
|
@@ -11,6 +11,7 @@ import type { NatService } from '../../nat/index.js';
|
|
|
11
11
|
import type { NatStatusSnapshot } from '../../nat/types.js';
|
|
12
12
|
import type { UpdateService } from '../../update/index.js';
|
|
13
13
|
import type { UpdateState } from '../../update/types.js';
|
|
14
|
+
import { SAMPLE_CONNECTIVITY } from './fakes.js';
|
|
14
15
|
|
|
15
16
|
function fakeOrchestrator(nodes: ManagedNodeInfo[]): HostProcessOrchestrator {
|
|
16
17
|
return { listNodes: () => nodes } as unknown as HostProcessOrchestrator;
|
|
@@ -25,27 +26,6 @@ function fakeUpdate(state: UpdateState): UpdateService {
|
|
|
25
26
|
return { getState: async () => state } as unknown as UpdateService;
|
|
26
27
|
}
|
|
27
28
|
|
|
28
|
-
const SAMPLE_CONNECTIVITY: NatStatusSnapshot = {
|
|
29
|
-
portMode: 'auto-upnp',
|
|
30
|
-
externalPort: 4001,
|
|
31
|
-
internalPort: 4001,
|
|
32
|
-
routerExternalIp: '203.0.113.5',
|
|
33
|
-
mappingLeaseExpiresAt: null,
|
|
34
|
-
externalIp: '203.0.113.5',
|
|
35
|
-
externalIpDetectedAt: null,
|
|
36
|
-
cgnatDetected: false,
|
|
37
|
-
directReachability: 'reachable',
|
|
38
|
-
lastTestedAt: null,
|
|
39
|
-
ddns: {
|
|
40
|
-
providerId: null,
|
|
41
|
-
hostname: null,
|
|
42
|
-
externallyManaged: false,
|
|
43
|
-
lastUpdateAt: null,
|
|
44
|
-
lastUpdateOk: null,
|
|
45
|
-
lastError: null,
|
|
46
|
-
},
|
|
47
|
-
};
|
|
48
|
-
|
|
49
29
|
describe('GET /api/status', () => {
|
|
50
30
|
let app: ReturnType<typeof Fastify>;
|
|
51
31
|
|
|
@@ -55,6 +35,7 @@ describe('GET /api/status', () => {
|
|
|
55
35
|
app = Fastify();
|
|
56
36
|
registerErrorHandler(app);
|
|
57
37
|
registerStatusRoute(app, {
|
|
38
|
+
role: 'founder',
|
|
58
39
|
orchestrator: fakeOrchestrator([
|
|
59
40
|
{
|
|
60
41
|
id: 'alice',
|
|
@@ -78,12 +59,14 @@ describe('GET /api/status', () => {
|
|
|
78
59
|
expect(res.statusCode).toBe(200);
|
|
79
60
|
const body = res.json() as {
|
|
80
61
|
service: { name: string; version: string; uptimeSeconds: number };
|
|
62
|
+
role: string;
|
|
81
63
|
nodes: Array<{ id: string; status: string }>;
|
|
82
64
|
trustCircle: { members: number; pending: number };
|
|
83
65
|
connectivity: { portMode: string };
|
|
84
66
|
update?: { available?: string };
|
|
85
67
|
};
|
|
86
68
|
expect(body.service.name).toBe('cadre-host');
|
|
69
|
+
expect(body.role).toBe('founder');
|
|
87
70
|
expect(body.nodes).toHaveLength(1);
|
|
88
71
|
expect(body.nodes[0]).toMatchObject({ id: 'alice', status: 'running' });
|
|
89
72
|
expect(body.trustCircle).toEqual({ members: 1, pending: 0 });
|
|
@@ -95,6 +78,7 @@ describe('GET /api/status', () => {
|
|
|
95
78
|
app = Fastify();
|
|
96
79
|
registerErrorHandler(app);
|
|
97
80
|
registerStatusRoute(app, {
|
|
81
|
+
role: 'donor',
|
|
98
82
|
orchestrator: fakeOrchestrator([]),
|
|
99
83
|
});
|
|
100
84
|
|
|
@@ -102,11 +86,13 @@ describe('GET /api/status', () => {
|
|
|
102
86
|
expect(res.statusCode).toBe(200);
|
|
103
87
|
const body = res.json() as {
|
|
104
88
|
service: { name: string };
|
|
89
|
+
role: string;
|
|
105
90
|
nodes: unknown[];
|
|
106
91
|
trustCircle?: unknown;
|
|
107
92
|
connectivity?: unknown;
|
|
108
93
|
};
|
|
109
94
|
expect(body.service.name).toBe('cadre-host');
|
|
95
|
+
expect(body.role).toBe('donor');
|
|
110
96
|
expect(body.nodes).toEqual([]);
|
|
111
97
|
expect(body.trustCircle).toBeUndefined();
|
|
112
98
|
expect(body.connectivity).toBeUndefined();
|
|
@@ -116,6 +102,7 @@ describe('GET /api/status', () => {
|
|
|
116
102
|
app = Fastify();
|
|
117
103
|
registerErrorHandler(app);
|
|
118
104
|
registerStatusRoute(app, {
|
|
105
|
+
role: 'founder',
|
|
119
106
|
orchestrator: fakeOrchestrator([]),
|
|
120
107
|
trustCircle: fakeTrustCircle({ members: [], pending: [] }),
|
|
121
108
|
nat: fakeNat(SAMPLE_CONNECTIVITY),
|
|
@@ -16,6 +16,7 @@ import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
|
16
16
|
|
|
17
17
|
import { createLocalUiServer } from '../index.js';
|
|
18
18
|
import type { LocalUiEvent } from '../events/types.js';
|
|
19
|
+
import { fakeFounder } from './fakes.js';
|
|
19
20
|
import type { HostProcessOrchestrator } from '../../orchestrator/index.js';
|
|
20
21
|
import type { StrandService } from '../../strands/index.js';
|
|
21
22
|
import { StrandError } from '../../strands/index.js';
|
|
@@ -79,7 +80,7 @@ function buildServer(opts: { withStrands: boolean }): void {
|
|
|
79
80
|
uiPort: 8765,
|
|
80
81
|
dataDir,
|
|
81
82
|
orchestrator: fakeOrchestrator(),
|
|
82
|
-
...(opts.withStrands ? { strands: strands.service } : {}),
|
|
83
|
+
...(opts.withStrands ? { founder: fakeFounder({ strands: strands.service }) } : {}),
|
|
83
84
|
forcePort: 0,
|
|
84
85
|
});
|
|
85
86
|
events = [];
|
|
@@ -214,7 +215,7 @@ describe('DELETE /api/strands/:id — error mapping', () => {
|
|
|
214
215
|
});
|
|
215
216
|
});
|
|
216
217
|
|
|
217
|
-
describe('donor-only mode (no
|
|
218
|
+
describe('donor-only mode (no founder services)', () => {
|
|
218
219
|
beforeEach(async () => {
|
|
219
220
|
await server.app.close();
|
|
220
221
|
buildServer({ withStrands: false });
|
|
@@ -1,23 +1,24 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Event types broadcast over the SPA's SSE channel.
|
|
3
|
-
*
|
|
4
|
-
* Publishers (orchestrator hook, route adapters, update observer) call
|
|
5
|
-
* EventBus.publish; the `/api/events` route fan-outs to connected clients.
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
|
-
import type { ContainerStatus } from '@serfab/cadre-provider';
|
|
9
|
-
|
|
10
|
-
import type { DirectReachability, PortForwardMode } from '../../nat/index.js';
|
|
11
|
-
|
|
12
|
-
export type LocalUiEvent =
|
|
13
|
-
| { type: 'node-state-changed'; nodeId: string; status: ContainerStatus }
|
|
14
|
-
| { type: 'trust-circle-changed'; kind: 'invited' | 'redeemed' | 'revoked' }
|
|
15
|
-
| { type: 'strands-changed'; kind: 'removed' }
|
|
16
|
-
| {
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
1
|
+
/**
|
|
2
|
+
* Event types broadcast over the SPA's SSE channel.
|
|
3
|
+
*
|
|
4
|
+
* Publishers (orchestrator hook, route adapters, update observer) call
|
|
5
|
+
* EventBus.publish; the `/api/events` route fan-outs to connected clients.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { ContainerStatus } from '@serfab/cadre-provider';
|
|
9
|
+
|
|
10
|
+
import type { DirectReachability, PortForwardMode } from '../../nat/index.js';
|
|
11
|
+
|
|
12
|
+
export type LocalUiEvent =
|
|
13
|
+
| { type: 'node-state-changed'; nodeId: string; status: ContainerStatus }
|
|
14
|
+
| { type: 'trust-circle-changed'; kind: 'invited' | 'redeemed' | 'revoked' }
|
|
15
|
+
| { type: 'strands-changed'; kind: 'removed' }
|
|
16
|
+
| { type: 'grants-changed'; kind: 'issued' | 'revoked' | 'terminated' }
|
|
17
|
+
| {
|
|
18
|
+
type: 'connectivity-changed';
|
|
19
|
+
portMode: PortForwardMode;
|
|
20
|
+
directReachability: DirectReachability;
|
|
21
|
+
}
|
|
22
|
+
| { type: 'update-available'; version: string; releaseNotesUrl?: string };
|
|
23
|
+
|
|
24
|
+
export type LocalUiEventType = LocalUiEvent['type'];
|