@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.
Files changed (90) hide show
  1. package/README.md +31 -16
  2. package/dist/bin/host.js +41 -20
  3. package/dist/bin/host.js.map +1 -1
  4. package/dist/donation/donation-service.d.ts +27 -4
  5. package/dist/donation/donation-service.js +57 -6
  6. package/dist/donation/donation-service.js.map +1 -1
  7. package/dist/donation/donation-store.d.ts +3 -1
  8. package/dist/donation/donation-store.js +5 -1
  9. package/dist/donation/donation-store.js.map +1 -1
  10. package/dist/donation/grant-service.d.ts +16 -7
  11. package/dist/donation/grant-service.js +57 -10
  12. package/dist/donation/grant-service.js.map +1 -1
  13. package/dist/donation/index.d.ts +2 -2
  14. package/dist/donation/index.js +1 -1
  15. package/dist/donation/index.js.map +1 -1
  16. package/dist/donation/types.d.ts +29 -7
  17. package/dist/donation/types.js.map +1 -1
  18. package/dist/index.d.ts +1 -1
  19. package/dist/nat/address-resolver.d.ts +3 -2
  20. package/dist/nat/address-resolver.js.map +1 -1
  21. package/dist/orchestrator/host-process-orchestrator.d.ts +2 -2
  22. package/dist/orchestrator/host-process-orchestrator.js +22 -12
  23. package/dist/orchestrator/host-process-orchestrator.js.map +1 -1
  24. package/dist/orchestrator/node-identity.d.ts +1 -1
  25. package/dist/orchestrator/node-identity.js +1 -1
  26. package/dist/orchestrator/port-allocator.d.ts +6 -39
  27. package/dist/orchestrator/port-allocator.js +9 -89
  28. package/dist/orchestrator/port-allocator.js.map +1 -1
  29. package/dist/server/events/types.d.ts +3 -0
  30. package/dist/server/index.d.ts +19 -18
  31. package/dist/server/index.js +13 -15
  32. package/dist/server/index.js.map +1 -1
  33. package/dist/server/routes/grants-admin.d.ts +15 -3
  34. package/dist/server/routes/grants-admin.js +33 -7
  35. package/dist/server/routes/grants-admin.js.map +1 -1
  36. package/dist/server/routes/nodes.d.ts +13 -5
  37. package/dist/server/routes/nodes.js +58 -39
  38. package/dist/server/routes/nodes.js.map +1 -1
  39. package/dist/server/routes/status.d.ts +13 -0
  40. package/dist/server/routes/status.js +2 -0
  41. package/dist/server/routes/status.js.map +1 -1
  42. package/dist/ui/assets/index-C6nELNfC.js +23 -0
  43. package/dist/ui/assets/index-C6nELNfC.js.map +1 -0
  44. package/dist/ui/assets/index-TtIYSeZO.css +1 -0
  45. package/dist/ui/index.html +2 -2
  46. package/package.json +4 -4
  47. package/src/__tests__/global-setup.ts +58 -56
  48. package/src/__tests__/orchestrator-env-scrub.test.ts +12 -11
  49. package/src/__tests__/orchestrator-handle-contract.ts +134 -0
  50. package/src/__tests__/orchestrator-node-identity.test.ts +6 -7
  51. package/src/__tests__/orchestrator-owner.test.ts +6 -13
  52. package/src/__tests__/orchestrator-pin-keys.test.ts +6 -7
  53. package/src/__tests__/orchestrator-ports.test.ts +5 -34
  54. package/src/__tests__/orchestrator-push.test.ts +6 -5
  55. package/src/__tests__/orchestrator-teardown.ts +65 -0
  56. package/src/__tests__/orchestrator.test.ts +23 -22
  57. package/src/auth/__tests__/trust-circle-integration.test.ts +1 -1
  58. package/src/bin/host.ts +56 -33
  59. package/src/donation/__tests__/donation-service.test.ts +38 -0
  60. package/src/donation/__tests__/fake-orchestrator.test.ts +19 -14
  61. package/src/donation/__tests__/fake-orchestrator.ts +21 -3
  62. package/src/donation/__tests__/grant-service.test.ts +2 -2
  63. package/src/donation/donation-service.ts +58 -6
  64. package/src/donation/donation-store.ts +176 -171
  65. package/src/donation/grant-service.ts +252 -200
  66. package/src/donation/index.ts +65 -64
  67. package/src/donation/types.ts +260 -244
  68. package/src/index.ts +2 -0
  69. package/src/nat/address-resolver.ts +3 -2
  70. package/src/orchestrator/host-process-orchestrator.ts +1260 -1251
  71. package/src/orchestrator/node-identity.ts +1 -1
  72. package/src/orchestrator/port-allocator.ts +16 -91
  73. package/src/server/__tests__/fakes.ts +44 -0
  74. package/src/server/__tests__/grants-admin-route.test.ts +171 -73
  75. package/src/server/__tests__/nodes-route.test.ts +49 -25
  76. package/src/server/__tests__/publishers.test.ts +5 -41
  77. package/src/server/__tests__/server.smoke.test.ts +7 -37
  78. package/src/server/__tests__/status-route.test.ts +8 -21
  79. package/src/server/__tests__/strands-route.test.ts +3 -2
  80. package/src/server/events/types.ts +24 -23
  81. package/src/server/index.ts +267 -267
  82. package/src/server/routes/grants-admin.ts +74 -41
  83. package/src/server/routes/nodes.ts +61 -43
  84. package/src/server/routes/status.ts +16 -0
  85. package/dist/donation/__tests__/fake-orchestrator.d.ts +0 -103
  86. package/dist/donation/__tests__/fake-orchestrator.js +0 -180
  87. package/dist/donation/__tests__/fake-orchestrator.js.map +0 -1
  88. package/dist/ui/assets/index-BVoqLjyX.css +0 -1
  89. package/dist/ui/assets/index-CEoNRwM_.js +0 -17
  90. 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 strands option)', () => {
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
- type: 'connectivity-changed';
18
- portMode: PortForwardMode;
19
- directReachability: DirectReachability;
20
- }
21
- | { type: 'update-available'; version: string; releaseNotesUrl?: string };
22
-
23
- export type LocalUiEventType = LocalUiEvent['type'];
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'];