@serfab/cadre-host 0.10.0 → 0.12.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 +406 -296
- package/dist/bin/host.d.ts +11 -7
- package/dist/bin/host.js +40 -12
- package/dist/bin/host.js.map +1 -1
- package/dist/installer/config.d.ts +4 -4
- package/dist/installer/config.js +5 -39
- package/dist/installer/config.js.map +1 -1
- package/dist/installer/identity.d.ts +8 -1
- package/dist/installer/identity.js +8 -1
- package/dist/installer/identity.js.map +1 -1
- package/dist/orchestrator/host-process-orchestrator.d.ts +2 -2
- package/dist/orchestrator/host-process-orchestrator.js +9 -4
- package/dist/orchestrator/host-process-orchestrator.js.map +1 -1
- package/dist/server/settings-store.d.ts +1 -1
- package/dist/server/settings-store.js +5 -1
- package/dist/server/settings-store.js.map +1 -1
- package/dist/server/static.js +38 -38
- package/dist/ui/assets/index-C-iks41M.js.map +1 -1
- package/dist/ui/index.html +12 -12
- package/package.json +4 -4
- package/service/cadre-host.service.tmpl +16 -16
- package/service/com.serfab.cadre-host.plist.tmpl +35 -35
- package/service/install-service.ps1 +98 -98
- package/service/uninstall-service.ps1 +19 -19
- package/src/__tests__/build-targets.test.ts +26 -26
- package/src/__tests__/cli-invite.smoke.test.ts +192 -192
- package/src/__tests__/cli-nat.smoke.test.ts +212 -212
- package/src/__tests__/cli.smoke.test.ts +51 -46
- package/src/__tests__/global-setup.ts +56 -51
- package/src/__tests__/host-owner.smoke.test.ts +123 -123
- package/src/__tests__/orchestrator-env-scrub.test.ts +140 -140
- package/src/__tests__/orchestrator-node-identity.test.ts +5 -5
- package/src/__tests__/orchestrator-owner.test.ts +198 -198
- package/src/__tests__/orchestrator-pin-keys.test.ts +153 -153
- package/src/__tests__/orchestrator-push.test.ts +145 -145
- package/src/__tests__/orchestrator.test.ts +951 -951
- package/src/auth/__tests__/duration.test.ts +28 -28
- package/src/auth/__tests__/self-label.test.ts +85 -85
- package/src/auth/__tests__/trust-circle-store.test.ts +115 -115
- package/src/auth/__tests__/trust-circle.test.ts +445 -445
- package/src/auth/duration.ts +33 -33
- package/src/auth/index.ts +21 -21
- package/src/auth/self-label.ts +74 -74
- package/src/auth/trust-circle-store.ts +155 -155
- package/src/auth/types.ts +99 -99
- package/src/bin/__tests__/cli-reference.test.ts +167 -0
- package/src/bin/host.ts +1326 -1299
- package/src/donation/__tests__/donation-service.test.ts +1068 -1068
- package/src/donation/__tests__/donation-supervisor.test.ts +510 -510
- package/src/donation/__tests__/fake-orchestrator.test.ts +168 -168
- package/src/donation/__tests__/fake-orchestrator.ts +225 -225
- package/src/donation/__tests__/grant-service.test.ts +213 -213
- package/src/donation/__tests__/grant-store.test.ts +113 -113
- package/src/donation/donation-service.ts +962 -962
- package/src/donation/donation-store.ts +171 -171
- package/src/donation/donation-supervisor.ts +388 -388
- package/src/donation/grant-service.ts +200 -200
- package/src/donation/grant-store.ts +152 -152
- package/src/donation/index.ts +64 -64
- package/src/donation/types.ts +243 -243
- package/src/installer/__tests__/config.test.ts +134 -112
- package/src/installer/__tests__/identity.test.ts +34 -34
- package/src/installer/__tests__/paths.test.ts +47 -47
- package/src/installer/__tests__/service-host.test.ts +62 -62
- package/src/installer/__tests__/wizard.test.ts +64 -64
- package/src/installer/config.ts +5 -43
- package/src/installer/identity.ts +56 -49
- package/src/installer/paths.ts +109 -109
- package/src/installer/platform.ts +27 -27
- package/src/installer/service-host/index.ts +21 -21
- package/src/installer/service-host/template.ts +21 -21
- package/src/installer/wizard.ts +103 -103
- package/src/nat/__tests__/address-resolver.test.ts +82 -82
- package/src/nat/__tests__/ddns-duckdns.test.ts +83 -83
- package/src/nat/__tests__/ddns-updater.test.ts +253 -253
- package/src/nat/__tests__/external-ip.test.ts +124 -124
- package/src/nat/__tests__/nat-service.test.ts +18 -0
- package/src/nat/__tests__/nat-store.test.ts +93 -93
- package/src/nat/__tests__/port-mapper.test.ts +191 -191
- package/src/nat/__tests__/reachability.test.ts +24 -24
- package/src/nat/__tests__/secrets.test.ts +178 -178
- package/src/nat/address-resolver.ts +51 -51
- package/src/nat/ddns/duckdns.ts +76 -76
- package/src/nat/ddns/index.ts +55 -55
- package/src/nat/ddns/updater.ts +243 -243
- package/src/nat/external-ip.ts +154 -154
- package/src/nat/index.ts +71 -71
- package/src/nat/nat-service.ts +501 -501
- package/src/nat/nat-store.ts +154 -154
- package/src/nat/port-mapper.ts +329 -329
- package/src/nat/reachability.ts +34 -34
- package/src/nat/secrets/file-store.ts +130 -130
- package/src/nat/secrets/index.ts +112 -112
- package/src/nat/secrets/keytar-store.ts +67 -67
- package/src/nat/types.ts +159 -159
- package/src/orchestrator/__tests__/node-identity.test.ts +62 -62
- package/src/orchestrator/host-process-orchestrator.ts +9 -4
- package/src/orchestrator/index.ts +10 -10
- package/src/orchestrator/log-rotator.ts +53 -53
- package/src/orchestrator/node-identity.ts +40 -40
- package/src/orchestrator/pid-liveness.ts +24 -24
- package/src/orchestrator/port-allocator.ts +91 -91
- package/src/orchestrator/state-store.ts +90 -90
- package/src/orchestrator/types.ts +141 -141
- package/src/owner/__tests__/owner-node-client.test.ts +194 -194
- package/src/owner/index.ts +12 -12
- package/src/owner/owner-node-client.ts +259 -259
- package/src/push/__tests__/push.test.ts +90 -90
- package/src/push/index.ts +146 -146
- package/src/server/__tests__/bus.test.ts +38 -38
- package/src/server/__tests__/error-handler.test.ts +118 -118
- package/src/server/__tests__/grants-admin-route.test.ts +73 -73
- package/src/server/__tests__/grants-route.test.ts +263 -263
- package/src/server/__tests__/nodes-route.test.ts +259 -259
- package/src/server/__tests__/origin-guard.test.ts +72 -72
- package/src/server/__tests__/publishers.test.ts +216 -216
- package/src/server/__tests__/server.smoke.test.ts +253 -253
- package/src/server/__tests__/settings-route.test.ts +195 -194
- package/src/server/__tests__/sse-route.test.ts +119 -119
- package/src/server/__tests__/static.test.ts +145 -145
- package/src/server/__tests__/status-route.test.ts +134 -134
- package/src/server/__tests__/strands-route.test.ts +233 -233
- package/src/server/error-handler.ts +178 -178
- package/src/server/events/bus.ts +42 -42
- package/src/server/events/sse-route.ts +81 -81
- package/src/server/events/types.ts +23 -23
- package/src/server/index.ts +267 -267
- package/src/server/origin-guard.ts +84 -84
- package/src/server/routes/grants-admin.ts +41 -41
- package/src/server/routes/grants.ts +210 -210
- package/src/server/routes/nat.ts +63 -63
- package/src/server/routes/nodes.ts +170 -170
- package/src/server/routes/settings.ts +123 -123
- package/src/server/routes/status.ts +118 -118
- package/src/server/routes/strands.ts +54 -54
- package/src/server/routes/trust-circle.ts +49 -49
- package/src/server/routes/update.ts +43 -43
- package/src/server/server.ts +122 -122
- package/src/server/settings-store.ts +5 -1
- package/src/server/static.ts +207 -207
- package/src/strands/__tests__/strand-service.test.ts +165 -165
- package/src/strands/index.ts +14 -14
- package/src/strands/strand-service.ts +135 -135
- package/src/strands/types.ts +102 -102
- package/src/update/__tests__/apply.test.ts +158 -158
- package/src/update/__tests__/manifest.test.ts +128 -128
- package/src/update/__tests__/release-key.test.ts +42 -42
- package/src/update/__tests__/sign.test.ts +78 -78
- package/src/update/__tests__/store.test.ts +81 -81
- package/src/update/__tests__/update-service.integration.test.ts +256 -256
- package/src/update/__tests__/version.test.ts +31 -31
- package/src/update/apply.ts +190 -190
- package/src/update/index.ts +341 -341
- package/src/update/release-key.ts +71 -71
- package/src/update/sign.ts +74 -74
- package/src/update/store.ts +101 -101
- package/src/update/types.ts +109 -109
- package/src/update/version.ts +80 -80
|
@@ -1,28 +1,28 @@
|
|
|
1
|
-
import { describe, expect, it } from 'vitest';
|
|
2
|
-
|
|
3
|
-
import { parseDuration } from '../duration.js';
|
|
4
|
-
|
|
5
|
-
describe('parseDuration', () => {
|
|
6
|
-
it('parses common units', () => {
|
|
7
|
-
expect(parseDuration('500ms')).toBe(500);
|
|
8
|
-
expect(parseDuration('30s')).toBe(30_000);
|
|
9
|
-
expect(parseDuration('5m')).toBe(5 * 60 * 1000);
|
|
10
|
-
expect(parseDuration('24h')).toBe(24 * 60 * 60 * 1000);
|
|
11
|
-
expect(parseDuration('7d')).toBe(7 * 24 * 60 * 60 * 1000);
|
|
12
|
-
expect(parseDuration('2w')).toBe(2 * 7 * 24 * 60 * 60 * 1000);
|
|
13
|
-
});
|
|
14
|
-
|
|
15
|
-
it('treats bare numbers as milliseconds', () => {
|
|
16
|
-
expect(parseDuration('1234')).toBe(1234);
|
|
17
|
-
});
|
|
18
|
-
|
|
19
|
-
it('accepts whitespace and decimals', () => {
|
|
20
|
-
expect(parseDuration(' 1.5h ')).toBe(Math.round(1.5 * 60 * 60 * 1000));
|
|
21
|
-
});
|
|
22
|
-
|
|
23
|
-
it('throws on garbage input', () => {
|
|
24
|
-
expect(() => parseDuration('abc')).toThrow();
|
|
25
|
-
expect(() => parseDuration('1y')).toThrow();
|
|
26
|
-
expect(() => parseDuration('')).toThrow();
|
|
27
|
-
});
|
|
28
|
-
});
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
|
|
3
|
+
import { parseDuration } from '../duration.js';
|
|
4
|
+
|
|
5
|
+
describe('parseDuration', () => {
|
|
6
|
+
it('parses common units', () => {
|
|
7
|
+
expect(parseDuration('500ms')).toBe(500);
|
|
8
|
+
expect(parseDuration('30s')).toBe(30_000);
|
|
9
|
+
expect(parseDuration('5m')).toBe(5 * 60 * 1000);
|
|
10
|
+
expect(parseDuration('24h')).toBe(24 * 60 * 60 * 1000);
|
|
11
|
+
expect(parseDuration('7d')).toBe(7 * 24 * 60 * 60 * 1000);
|
|
12
|
+
expect(parseDuration('2w')).toBe(2 * 7 * 24 * 60 * 60 * 1000);
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
it('treats bare numbers as milliseconds', () => {
|
|
16
|
+
expect(parseDuration('1234')).toBe(1234);
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
it('accepts whitespace and decimals', () => {
|
|
20
|
+
expect(parseDuration(' 1.5h ')).toBe(Math.round(1.5 * 60 * 60 * 1000));
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it('throws on garbage input', () => {
|
|
24
|
+
expect(() => parseDuration('abc')).toThrow();
|
|
25
|
+
expect(() => parseDuration('1y')).toThrow();
|
|
26
|
+
expect(() => parseDuration('')).toThrow();
|
|
27
|
+
});
|
|
28
|
+
});
|
|
@@ -1,85 +1,85 @@
|
|
|
1
|
-
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
|
|
2
|
-
import { mkdtempSync, rmSync } from 'node:fs';
|
|
3
|
-
import { tmpdir } from 'node:os';
|
|
4
|
-
import { join } from 'node:path';
|
|
5
|
-
|
|
6
|
-
import { SELF_LABEL, ensureSelfLabel } from '../self-label.js';
|
|
7
|
-
import { TrustCircleStore } from '../trust-circle-store.js';
|
|
8
|
-
|
|
9
|
-
let tmpRoot: string;
|
|
10
|
-
let store: TrustCircleStore;
|
|
11
|
-
|
|
12
|
-
const NOW = new Date('2025-06-01T12:00:00.000Z');
|
|
13
|
-
const now = (): Date => NOW;
|
|
14
|
-
|
|
15
|
-
beforeEach(() => {
|
|
16
|
-
tmpRoot = mkdtempSync(join(tmpdir(), 'cadre-host-self-label-'));
|
|
17
|
-
store = new TrustCircleStore(tmpRoot);
|
|
18
|
-
});
|
|
19
|
-
|
|
20
|
-
afterEach(() => {
|
|
21
|
-
try { rmSync(tmpRoot, { recursive: true, force: true }); } catch { /* ignore */ }
|
|
22
|
-
});
|
|
23
|
-
|
|
24
|
-
describe('ensureSelfLabel', () => {
|
|
25
|
-
it('writes a self-flagged label for the owner peer ID', async () => {
|
|
26
|
-
const peerId = await ensureSelfLabel({ store, getPeerId: async () => '12D3KooWOwner', now });
|
|
27
|
-
|
|
28
|
-
expect(peerId).toBe('12D3KooWOwner');
|
|
29
|
-
expect(store.getMember('12D3KooWOwner')).toEqual({
|
|
30
|
-
peerId: '12D3KooWOwner',
|
|
31
|
-
label: SELF_LABEL,
|
|
32
|
-
addedAt: NOW.toISOString(),
|
|
33
|
-
self: true,
|
|
34
|
-
});
|
|
35
|
-
});
|
|
36
|
-
|
|
37
|
-
it('keeps an admin-renamed label on a second run', async () => {
|
|
38
|
-
store.addMember({ peerId: '12D3KooWOwner', label: 'Basement PC', addedAt: 't0', self: true });
|
|
39
|
-
|
|
40
|
-
await ensureSelfLabel({ store, getPeerId: async () => '12D3KooWOwner', now });
|
|
41
|
-
|
|
42
|
-
expect(store.getMember('12D3KooWOwner')).toMatchObject({ label: 'Basement PC', addedAt: 't0', self: true });
|
|
43
|
-
});
|
|
44
|
-
|
|
45
|
-
it('stamps the self flag onto a pre-existing unflagged row', async () => {
|
|
46
|
-
store.addMember({ peerId: '12D3KooWOwner', label: 'Basement PC', addedAt: 't0' });
|
|
47
|
-
|
|
48
|
-
await ensureSelfLabel({ store, getPeerId: async () => '12D3KooWOwner', now });
|
|
49
|
-
|
|
50
|
-
expect(store.getMember('12D3KooWOwner')).toEqual({
|
|
51
|
-
peerId: '12D3KooWOwner',
|
|
52
|
-
label: 'Basement PC',
|
|
53
|
-
addedAt: 't0',
|
|
54
|
-
self: true,
|
|
55
|
-
});
|
|
56
|
-
});
|
|
57
|
-
|
|
58
|
-
it('drops a stale self label after the node identity changes', async () => {
|
|
59
|
-
store.addMember({ peerId: '12D3KooWOld', label: SELF_LABEL, addedAt: 't0', self: true });
|
|
60
|
-
store.addMember({ peerId: '12D3KooWPhone', label: "Mom's phone", addedAt: 't1' });
|
|
61
|
-
|
|
62
|
-
await ensureSelfLabel({ store, getPeerId: async () => '12D3KooWNew', now });
|
|
63
|
-
|
|
64
|
-
expect(store.getMember('12D3KooWOld')).toBeUndefined();
|
|
65
|
-
expect(store.getMember('12D3KooWNew')).toMatchObject({ label: SELF_LABEL, self: true });
|
|
66
|
-
// Non-self labels are untouched.
|
|
67
|
-
expect(store.getMember('12D3KooWPhone')).toMatchObject({ label: "Mom's phone" });
|
|
68
|
-
});
|
|
69
|
-
|
|
70
|
-
it('writes nothing when the node is not ready yet (empty peer ID)', async () => {
|
|
71
|
-
const peerId = await ensureSelfLabel({ store, getPeerId: async () => '', now });
|
|
72
|
-
|
|
73
|
-
expect(peerId).toBeUndefined();
|
|
74
|
-
expect(store.listMembers()).toEqual([]);
|
|
75
|
-
});
|
|
76
|
-
|
|
77
|
-
it('propagates a getPeerId failure to the caller', async () => {
|
|
78
|
-
await expect(ensureSelfLabel({
|
|
79
|
-
store,
|
|
80
|
-
getPeerId: async () => { throw new Error('owner node unavailable'); },
|
|
81
|
-
now,
|
|
82
|
-
})).rejects.toThrow('owner node unavailable');
|
|
83
|
-
expect(store.listMembers()).toEqual([]);
|
|
84
|
-
});
|
|
85
|
-
});
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
|
|
2
|
+
import { mkdtempSync, rmSync } from 'node:fs';
|
|
3
|
+
import { tmpdir } from 'node:os';
|
|
4
|
+
import { join } from 'node:path';
|
|
5
|
+
|
|
6
|
+
import { SELF_LABEL, ensureSelfLabel } from '../self-label.js';
|
|
7
|
+
import { TrustCircleStore } from '../trust-circle-store.js';
|
|
8
|
+
|
|
9
|
+
let tmpRoot: string;
|
|
10
|
+
let store: TrustCircleStore;
|
|
11
|
+
|
|
12
|
+
const NOW = new Date('2025-06-01T12:00:00.000Z');
|
|
13
|
+
const now = (): Date => NOW;
|
|
14
|
+
|
|
15
|
+
beforeEach(() => {
|
|
16
|
+
tmpRoot = mkdtempSync(join(tmpdir(), 'cadre-host-self-label-'));
|
|
17
|
+
store = new TrustCircleStore(tmpRoot);
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
afterEach(() => {
|
|
21
|
+
try { rmSync(tmpRoot, { recursive: true, force: true }); } catch { /* ignore */ }
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
describe('ensureSelfLabel', () => {
|
|
25
|
+
it('writes a self-flagged label for the owner peer ID', async () => {
|
|
26
|
+
const peerId = await ensureSelfLabel({ store, getPeerId: async () => '12D3KooWOwner', now });
|
|
27
|
+
|
|
28
|
+
expect(peerId).toBe('12D3KooWOwner');
|
|
29
|
+
expect(store.getMember('12D3KooWOwner')).toEqual({
|
|
30
|
+
peerId: '12D3KooWOwner',
|
|
31
|
+
label: SELF_LABEL,
|
|
32
|
+
addedAt: NOW.toISOString(),
|
|
33
|
+
self: true,
|
|
34
|
+
});
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it('keeps an admin-renamed label on a second run', async () => {
|
|
38
|
+
store.addMember({ peerId: '12D3KooWOwner', label: 'Basement PC', addedAt: 't0', self: true });
|
|
39
|
+
|
|
40
|
+
await ensureSelfLabel({ store, getPeerId: async () => '12D3KooWOwner', now });
|
|
41
|
+
|
|
42
|
+
expect(store.getMember('12D3KooWOwner')).toMatchObject({ label: 'Basement PC', addedAt: 't0', self: true });
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it('stamps the self flag onto a pre-existing unflagged row', async () => {
|
|
46
|
+
store.addMember({ peerId: '12D3KooWOwner', label: 'Basement PC', addedAt: 't0' });
|
|
47
|
+
|
|
48
|
+
await ensureSelfLabel({ store, getPeerId: async () => '12D3KooWOwner', now });
|
|
49
|
+
|
|
50
|
+
expect(store.getMember('12D3KooWOwner')).toEqual({
|
|
51
|
+
peerId: '12D3KooWOwner',
|
|
52
|
+
label: 'Basement PC',
|
|
53
|
+
addedAt: 't0',
|
|
54
|
+
self: true,
|
|
55
|
+
});
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it('drops a stale self label after the node identity changes', async () => {
|
|
59
|
+
store.addMember({ peerId: '12D3KooWOld', label: SELF_LABEL, addedAt: 't0', self: true });
|
|
60
|
+
store.addMember({ peerId: '12D3KooWPhone', label: "Mom's phone", addedAt: 't1' });
|
|
61
|
+
|
|
62
|
+
await ensureSelfLabel({ store, getPeerId: async () => '12D3KooWNew', now });
|
|
63
|
+
|
|
64
|
+
expect(store.getMember('12D3KooWOld')).toBeUndefined();
|
|
65
|
+
expect(store.getMember('12D3KooWNew')).toMatchObject({ label: SELF_LABEL, self: true });
|
|
66
|
+
// Non-self labels are untouched.
|
|
67
|
+
expect(store.getMember('12D3KooWPhone')).toMatchObject({ label: "Mom's phone" });
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it('writes nothing when the node is not ready yet (empty peer ID)', async () => {
|
|
71
|
+
const peerId = await ensureSelfLabel({ store, getPeerId: async () => '', now });
|
|
72
|
+
|
|
73
|
+
expect(peerId).toBeUndefined();
|
|
74
|
+
expect(store.listMembers()).toEqual([]);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it('propagates a getPeerId failure to the caller', async () => {
|
|
78
|
+
await expect(ensureSelfLabel({
|
|
79
|
+
store,
|
|
80
|
+
getPeerId: async () => { throw new Error('owner node unavailable'); },
|
|
81
|
+
now,
|
|
82
|
+
})).rejects.toThrow('owner node unavailable');
|
|
83
|
+
expect(store.listMembers()).toEqual([]);
|
|
84
|
+
});
|
|
85
|
+
});
|
|
@@ -1,115 +1,115 @@
|
|
|
1
|
-
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
|
|
2
|
-
import { existsSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
|
|
3
|
-
import { tmpdir } from 'node:os';
|
|
4
|
-
import { join } from 'node:path';
|
|
5
|
-
|
|
6
|
-
import { TrustCircleStore } from '../trust-circle-store.js';
|
|
7
|
-
import { TrustCircleError } from '../types.js';
|
|
8
|
-
|
|
9
|
-
let tmpRoot: string;
|
|
10
|
-
|
|
11
|
-
beforeEach(() => {
|
|
12
|
-
tmpRoot = mkdtempSync(join(tmpdir(), 'cadre-host-tc-store-'));
|
|
13
|
-
});
|
|
14
|
-
|
|
15
|
-
afterEach(() => {
|
|
16
|
-
try { rmSync(tmpRoot, { recursive: true, force: true }); } catch { /* ignore */ }
|
|
17
|
-
});
|
|
18
|
-
|
|
19
|
-
describe('TrustCircleStore', () => {
|
|
20
|
-
it('returns empty state when file is missing', () => {
|
|
21
|
-
const store = new TrustCircleStore(tmpRoot);
|
|
22
|
-
expect(store.listMembers()).toEqual([]);
|
|
23
|
-
expect(store.listPending()).toEqual([]);
|
|
24
|
-
});
|
|
25
|
-
|
|
26
|
-
it('round-trips members through save/load', () => {
|
|
27
|
-
const store = new TrustCircleStore(tmpRoot);
|
|
28
|
-
store.addMember({
|
|
29
|
-
peerId: '12D3KooWAlice',
|
|
30
|
-
label: "Alice's phone",
|
|
31
|
-
addedAt: '2025-01-01T00:00:00.000Z',
|
|
32
|
-
});
|
|
33
|
-
store.addMember({
|
|
34
|
-
peerId: '12D3KooWBob',
|
|
35
|
-
label: 'Bob laptop',
|
|
36
|
-
addedAt: '2025-01-02T00:00:00.000Z',
|
|
37
|
-
self: true,
|
|
38
|
-
});
|
|
39
|
-
|
|
40
|
-
// Fresh instance reads from disk.
|
|
41
|
-
const reread = new TrustCircleStore(tmpRoot);
|
|
42
|
-
const members = reread.listMembers();
|
|
43
|
-
expect(members).toHaveLength(2);
|
|
44
|
-
expect(reread.getMember('12D3KooWAlice')).toMatchObject({ label: "Alice's phone" });
|
|
45
|
-
expect(reread.getMember('12D3KooWBob')?.self).toBe(true);
|
|
46
|
-
});
|
|
47
|
-
|
|
48
|
-
it('round-trips pending invites', () => {
|
|
49
|
-
const store = new TrustCircleStore(tmpRoot);
|
|
50
|
-
store.addPending({
|
|
51
|
-
token: 'token-A',
|
|
52
|
-
label: 'Mom',
|
|
53
|
-
createdAt: '2025-01-01T00:00:00.000Z',
|
|
54
|
-
expiresAt: '2025-01-02T00:00:00.000Z',
|
|
55
|
-
});
|
|
56
|
-
store.addPending({
|
|
57
|
-
token: 'token-B',
|
|
58
|
-
label: 'Dad',
|
|
59
|
-
createdAt: '2025-01-02T00:00:00.000Z',
|
|
60
|
-
});
|
|
61
|
-
|
|
62
|
-
const reread = new TrustCircleStore(tmpRoot);
|
|
63
|
-
expect(reread.listPending()).toHaveLength(2);
|
|
64
|
-
expect(reread.getPending('token-A')?.label).toBe('Mom');
|
|
65
|
-
expect(reread.getPending('token-B')?.expiresAt).toBeUndefined();
|
|
66
|
-
});
|
|
67
|
-
|
|
68
|
-
it('removes members and pending invites', () => {
|
|
69
|
-
const store = new TrustCircleStore(tmpRoot);
|
|
70
|
-
store.addMember({ peerId: 'p1', label: 'one', addedAt: 't' });
|
|
71
|
-
store.addPending({ token: 'tok', label: 'two', createdAt: 't' });
|
|
72
|
-
|
|
73
|
-
expect(store.removeMember('p1')).toBe(true);
|
|
74
|
-
expect(store.removeMember('p1')).toBe(false);
|
|
75
|
-
expect(store.removePending('tok')).toBe(true);
|
|
76
|
-
expect(store.removePending('tok')).toBe(false);
|
|
77
|
-
|
|
78
|
-
const reread = new TrustCircleStore(tmpRoot);
|
|
79
|
-
expect(reread.listMembers()).toEqual([]);
|
|
80
|
-
expect(reread.listPending()).toEqual([]);
|
|
81
|
-
});
|
|
82
|
-
|
|
83
|
-
it('writes through a temp file (atomic rename)', () => {
|
|
84
|
-
const store = new TrustCircleStore(tmpRoot);
|
|
85
|
-
store.addMember({ peerId: 'p1', label: 'one', addedAt: 't' });
|
|
86
|
-
|
|
87
|
-
// After save, no .tmp residue should remain.
|
|
88
|
-
expect(existsSync(join(tmpRoot, 'trust-circle.json'))).toBe(true);
|
|
89
|
-
expect(existsSync(join(tmpRoot, 'trust-circle.json.tmp'))).toBe(false);
|
|
90
|
-
});
|
|
91
|
-
|
|
92
|
-
it('throws on malformed file rather than silently wiping', () => {
|
|
93
|
-
writeFileSync(join(tmpRoot, 'trust-circle.json'), 'not json', 'utf8');
|
|
94
|
-
const store = new TrustCircleStore(tmpRoot);
|
|
95
|
-
expect(() => store.load()).toThrow(TrustCircleError);
|
|
96
|
-
});
|
|
97
|
-
|
|
98
|
-
it('throws on file with wrong shape', () => {
|
|
99
|
-
writeFileSync(
|
|
100
|
-
join(tmpRoot, 'trust-circle.json'),
|
|
101
|
-
JSON.stringify({ version: 99, foo: 'bar' }),
|
|
102
|
-
'utf8',
|
|
103
|
-
);
|
|
104
|
-
const store = new TrustCircleStore(tmpRoot);
|
|
105
|
-
expect(() => store.load()).toThrow(TrustCircleError);
|
|
106
|
-
});
|
|
107
|
-
|
|
108
|
-
it('persists version field in saved JSON', () => {
|
|
109
|
-
const store = new TrustCircleStore(tmpRoot);
|
|
110
|
-
store.addMember({ peerId: 'p1', label: 'one', addedAt: 't' });
|
|
111
|
-
const raw = JSON.parse(readFileSync(join(tmpRoot, 'trust-circle.json'), 'utf8'));
|
|
112
|
-
expect(raw.version).toBe(1);
|
|
113
|
-
expect(raw.members.p1.label).toBe('one');
|
|
114
|
-
});
|
|
115
|
-
});
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
|
|
2
|
+
import { existsSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
|
|
3
|
+
import { tmpdir } from 'node:os';
|
|
4
|
+
import { join } from 'node:path';
|
|
5
|
+
|
|
6
|
+
import { TrustCircleStore } from '../trust-circle-store.js';
|
|
7
|
+
import { TrustCircleError } from '../types.js';
|
|
8
|
+
|
|
9
|
+
let tmpRoot: string;
|
|
10
|
+
|
|
11
|
+
beforeEach(() => {
|
|
12
|
+
tmpRoot = mkdtempSync(join(tmpdir(), 'cadre-host-tc-store-'));
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
afterEach(() => {
|
|
16
|
+
try { rmSync(tmpRoot, { recursive: true, force: true }); } catch { /* ignore */ }
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
describe('TrustCircleStore', () => {
|
|
20
|
+
it('returns empty state when file is missing', () => {
|
|
21
|
+
const store = new TrustCircleStore(tmpRoot);
|
|
22
|
+
expect(store.listMembers()).toEqual([]);
|
|
23
|
+
expect(store.listPending()).toEqual([]);
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it('round-trips members through save/load', () => {
|
|
27
|
+
const store = new TrustCircleStore(tmpRoot);
|
|
28
|
+
store.addMember({
|
|
29
|
+
peerId: '12D3KooWAlice',
|
|
30
|
+
label: "Alice's phone",
|
|
31
|
+
addedAt: '2025-01-01T00:00:00.000Z',
|
|
32
|
+
});
|
|
33
|
+
store.addMember({
|
|
34
|
+
peerId: '12D3KooWBob',
|
|
35
|
+
label: 'Bob laptop',
|
|
36
|
+
addedAt: '2025-01-02T00:00:00.000Z',
|
|
37
|
+
self: true,
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
// Fresh instance reads from disk.
|
|
41
|
+
const reread = new TrustCircleStore(tmpRoot);
|
|
42
|
+
const members = reread.listMembers();
|
|
43
|
+
expect(members).toHaveLength(2);
|
|
44
|
+
expect(reread.getMember('12D3KooWAlice')).toMatchObject({ label: "Alice's phone" });
|
|
45
|
+
expect(reread.getMember('12D3KooWBob')?.self).toBe(true);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it('round-trips pending invites', () => {
|
|
49
|
+
const store = new TrustCircleStore(tmpRoot);
|
|
50
|
+
store.addPending({
|
|
51
|
+
token: 'token-A',
|
|
52
|
+
label: 'Mom',
|
|
53
|
+
createdAt: '2025-01-01T00:00:00.000Z',
|
|
54
|
+
expiresAt: '2025-01-02T00:00:00.000Z',
|
|
55
|
+
});
|
|
56
|
+
store.addPending({
|
|
57
|
+
token: 'token-B',
|
|
58
|
+
label: 'Dad',
|
|
59
|
+
createdAt: '2025-01-02T00:00:00.000Z',
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
const reread = new TrustCircleStore(tmpRoot);
|
|
63
|
+
expect(reread.listPending()).toHaveLength(2);
|
|
64
|
+
expect(reread.getPending('token-A')?.label).toBe('Mom');
|
|
65
|
+
expect(reread.getPending('token-B')?.expiresAt).toBeUndefined();
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it('removes members and pending invites', () => {
|
|
69
|
+
const store = new TrustCircleStore(tmpRoot);
|
|
70
|
+
store.addMember({ peerId: 'p1', label: 'one', addedAt: 't' });
|
|
71
|
+
store.addPending({ token: 'tok', label: 'two', createdAt: 't' });
|
|
72
|
+
|
|
73
|
+
expect(store.removeMember('p1')).toBe(true);
|
|
74
|
+
expect(store.removeMember('p1')).toBe(false);
|
|
75
|
+
expect(store.removePending('tok')).toBe(true);
|
|
76
|
+
expect(store.removePending('tok')).toBe(false);
|
|
77
|
+
|
|
78
|
+
const reread = new TrustCircleStore(tmpRoot);
|
|
79
|
+
expect(reread.listMembers()).toEqual([]);
|
|
80
|
+
expect(reread.listPending()).toEqual([]);
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it('writes through a temp file (atomic rename)', () => {
|
|
84
|
+
const store = new TrustCircleStore(tmpRoot);
|
|
85
|
+
store.addMember({ peerId: 'p1', label: 'one', addedAt: 't' });
|
|
86
|
+
|
|
87
|
+
// After save, no .tmp residue should remain.
|
|
88
|
+
expect(existsSync(join(tmpRoot, 'trust-circle.json'))).toBe(true);
|
|
89
|
+
expect(existsSync(join(tmpRoot, 'trust-circle.json.tmp'))).toBe(false);
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it('throws on malformed file rather than silently wiping', () => {
|
|
93
|
+
writeFileSync(join(tmpRoot, 'trust-circle.json'), 'not json', 'utf8');
|
|
94
|
+
const store = new TrustCircleStore(tmpRoot);
|
|
95
|
+
expect(() => store.load()).toThrow(TrustCircleError);
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
it('throws on file with wrong shape', () => {
|
|
99
|
+
writeFileSync(
|
|
100
|
+
join(tmpRoot, 'trust-circle.json'),
|
|
101
|
+
JSON.stringify({ version: 99, foo: 'bar' }),
|
|
102
|
+
'utf8',
|
|
103
|
+
);
|
|
104
|
+
const store = new TrustCircleStore(tmpRoot);
|
|
105
|
+
expect(() => store.load()).toThrow(TrustCircleError);
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it('persists version field in saved JSON', () => {
|
|
109
|
+
const store = new TrustCircleStore(tmpRoot);
|
|
110
|
+
store.addMember({ peerId: 'p1', label: 'one', addedAt: 't' });
|
|
111
|
+
const raw = JSON.parse(readFileSync(join(tmpRoot, 'trust-circle.json'), 'utf8'));
|
|
112
|
+
expect(raw.version).toBe(1);
|
|
113
|
+
expect(raw.members.p1.label).toBe('one');
|
|
114
|
+
});
|
|
115
|
+
});
|