@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,124 +1,124 @@
|
|
|
1
|
-
import { describe, expect, it } from 'vitest';
|
|
2
|
-
|
|
3
|
-
import { ExternalIpDetector, isPlausibleIp } from '../external-ip.js';
|
|
4
|
-
|
|
5
|
-
function makeFetchSequence(responses: Array<{ ok: boolean; body: string }>): typeof fetch {
|
|
6
|
-
let i = 0;
|
|
7
|
-
return (async () => {
|
|
8
|
-
const r = responses[i++] ?? { ok: false, body: '' };
|
|
9
|
-
return {
|
|
10
|
-
ok: r.ok,
|
|
11
|
-
status: r.ok ? 200 : 500,
|
|
12
|
-
async text() { return r.body; },
|
|
13
|
-
} as unknown as Response;
|
|
14
|
-
}) as typeof fetch;
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
describe('isPlausibleIp()', () => {
|
|
18
|
-
it('accepts IPv4', () => {
|
|
19
|
-
expect(isPlausibleIp('1.2.3.4')).toBe(true);
|
|
20
|
-
expect(isPlausibleIp('255.255.255.255')).toBe(true);
|
|
21
|
-
});
|
|
22
|
-
it('rejects bad IPv4', () => {
|
|
23
|
-
expect(isPlausibleIp('256.0.0.1')).toBe(false);
|
|
24
|
-
expect(isPlausibleIp('1.2.3')).toBe(false);
|
|
25
|
-
});
|
|
26
|
-
it('accepts IPv6 liberally', () => {
|
|
27
|
-
expect(isPlausibleIp('::1')).toBe(true);
|
|
28
|
-
expect(isPlausibleIp('2001:db8::1')).toBe(true);
|
|
29
|
-
});
|
|
30
|
-
it('rejects junk', () => {
|
|
31
|
-
expect(isPlausibleIp('not an ip')).toBe(false);
|
|
32
|
-
expect(isPlausibleIp('')).toBe(false);
|
|
33
|
-
});
|
|
34
|
-
});
|
|
35
|
-
|
|
36
|
-
describe('ExternalIpDetector', () => {
|
|
37
|
-
it('returns null routerIp when no probe configured', async () => {
|
|
38
|
-
const det = new ExternalIpDetector({
|
|
39
|
-
fetch: makeFetchSequence([{ ok: true, body: '1.2.3.4' }]),
|
|
40
|
-
publicIpUrls: ['https://example.test'],
|
|
41
|
-
});
|
|
42
|
-
const r = await det.detect();
|
|
43
|
-
expect(r.routerIp).toBeNull();
|
|
44
|
-
expect(r.publicIp).toBe('1.2.3.4');
|
|
45
|
-
expect(r.cgnat).toBe(false);
|
|
46
|
-
});
|
|
47
|
-
|
|
48
|
-
it('detects CGNAT when router and public IP disagree', async () => {
|
|
49
|
-
const det = new ExternalIpDetector({
|
|
50
|
-
routerProbe: async () => '100.64.0.5', // CGNAT space — fine for testing
|
|
51
|
-
fetch: makeFetchSequence([{ ok: true, body: '203.0.113.10' }]),
|
|
52
|
-
publicIpUrls: ['https://example.test'],
|
|
53
|
-
});
|
|
54
|
-
const r = await det.detect();
|
|
55
|
-
expect(r.routerIp).toBe('100.64.0.5');
|
|
56
|
-
expect(r.publicIp).toBe('203.0.113.10');
|
|
57
|
-
expect(r.cgnat).toBe(true);
|
|
58
|
-
});
|
|
59
|
-
|
|
60
|
-
it('does NOT flag CGNAT when IPs agree', async () => {
|
|
61
|
-
const det = new ExternalIpDetector({
|
|
62
|
-
routerProbe: async () => '1.2.3.4',
|
|
63
|
-
fetch: makeFetchSequence([{ ok: true, body: '1.2.3.4' }]),
|
|
64
|
-
publicIpUrls: ['https://example.test'],
|
|
65
|
-
});
|
|
66
|
-
const r = await det.detect();
|
|
67
|
-
expect(r.cgnat).toBe(false);
|
|
68
|
-
});
|
|
69
|
-
|
|
70
|
-
it('does NOT flag CGNAT when one probe fails', async () => {
|
|
71
|
-
const det = new ExternalIpDetector({
|
|
72
|
-
routerProbe: async () => '1.2.3.4',
|
|
73
|
-
fetch: makeFetchSequence([{ ok: false, body: '' }]),
|
|
74
|
-
publicIpUrls: ['https://example.test'],
|
|
75
|
-
});
|
|
76
|
-
const r = await det.detect();
|
|
77
|
-
expect(r.routerIp).toBe('1.2.3.4');
|
|
78
|
-
expect(r.publicIp).toBeNull();
|
|
79
|
-
expect(r.cgnat).toBe(false);
|
|
80
|
-
});
|
|
81
|
-
|
|
82
|
-
it('falls through public probe URLs until one succeeds', async () => {
|
|
83
|
-
const det = new ExternalIpDetector({
|
|
84
|
-
fetch: makeFetchSequence([
|
|
85
|
-
{ ok: false, body: '' },
|
|
86
|
-
{ ok: true, body: '8.8.8.8' },
|
|
87
|
-
]),
|
|
88
|
-
publicIpUrls: ['https://a.test', 'https://b.test'],
|
|
89
|
-
});
|
|
90
|
-
const r = await det.detect();
|
|
91
|
-
expect(r.publicIp).toBe('8.8.8.8');
|
|
92
|
-
});
|
|
93
|
-
|
|
94
|
-
it('treats router probe throw as null', async () => {
|
|
95
|
-
const det = new ExternalIpDetector({
|
|
96
|
-
routerProbe: async () => { throw new Error('upnp not reachable'); },
|
|
97
|
-
fetch: makeFetchSequence([{ ok: true, body: '1.2.3.4' }]),
|
|
98
|
-
publicIpUrls: ['https://a.test'],
|
|
99
|
-
});
|
|
100
|
-
const r = await det.detect();
|
|
101
|
-
expect(r.routerIp).toBeNull();
|
|
102
|
-
expect(r.publicIp).toBe('1.2.3.4');
|
|
103
|
-
expect(r.cgnat).toBe(false);
|
|
104
|
-
});
|
|
105
|
-
|
|
106
|
-
it('rejects implausible router-probe IP', async () => {
|
|
107
|
-
const det = new ExternalIpDetector({
|
|
108
|
-
routerProbe: async () => 'not-an-ip',
|
|
109
|
-
fetch: makeFetchSequence([{ ok: true, body: '1.2.3.4' }]),
|
|
110
|
-
publicIpUrls: ['https://a.test'],
|
|
111
|
-
});
|
|
112
|
-
const r = await det.detect();
|
|
113
|
-
expect(r.routerIp).toBeNull();
|
|
114
|
-
});
|
|
115
|
-
|
|
116
|
-
it('rejects non-IP public-service body', async () => {
|
|
117
|
-
const det = new ExternalIpDetector({
|
|
118
|
-
fetch: makeFetchSequence([{ ok: true, body: '<html>error</html>' }]),
|
|
119
|
-
publicIpUrls: ['https://a.test'],
|
|
120
|
-
});
|
|
121
|
-
const r = await det.detect();
|
|
122
|
-
expect(r.publicIp).toBeNull();
|
|
123
|
-
});
|
|
124
|
-
});
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
|
|
3
|
+
import { ExternalIpDetector, isPlausibleIp } from '../external-ip.js';
|
|
4
|
+
|
|
5
|
+
function makeFetchSequence(responses: Array<{ ok: boolean; body: string }>): typeof fetch {
|
|
6
|
+
let i = 0;
|
|
7
|
+
return (async () => {
|
|
8
|
+
const r = responses[i++] ?? { ok: false, body: '' };
|
|
9
|
+
return {
|
|
10
|
+
ok: r.ok,
|
|
11
|
+
status: r.ok ? 200 : 500,
|
|
12
|
+
async text() { return r.body; },
|
|
13
|
+
} as unknown as Response;
|
|
14
|
+
}) as typeof fetch;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
describe('isPlausibleIp()', () => {
|
|
18
|
+
it('accepts IPv4', () => {
|
|
19
|
+
expect(isPlausibleIp('1.2.3.4')).toBe(true);
|
|
20
|
+
expect(isPlausibleIp('255.255.255.255')).toBe(true);
|
|
21
|
+
});
|
|
22
|
+
it('rejects bad IPv4', () => {
|
|
23
|
+
expect(isPlausibleIp('256.0.0.1')).toBe(false);
|
|
24
|
+
expect(isPlausibleIp('1.2.3')).toBe(false);
|
|
25
|
+
});
|
|
26
|
+
it('accepts IPv6 liberally', () => {
|
|
27
|
+
expect(isPlausibleIp('::1')).toBe(true);
|
|
28
|
+
expect(isPlausibleIp('2001:db8::1')).toBe(true);
|
|
29
|
+
});
|
|
30
|
+
it('rejects junk', () => {
|
|
31
|
+
expect(isPlausibleIp('not an ip')).toBe(false);
|
|
32
|
+
expect(isPlausibleIp('')).toBe(false);
|
|
33
|
+
});
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
describe('ExternalIpDetector', () => {
|
|
37
|
+
it('returns null routerIp when no probe configured', async () => {
|
|
38
|
+
const det = new ExternalIpDetector({
|
|
39
|
+
fetch: makeFetchSequence([{ ok: true, body: '1.2.3.4' }]),
|
|
40
|
+
publicIpUrls: ['https://example.test'],
|
|
41
|
+
});
|
|
42
|
+
const r = await det.detect();
|
|
43
|
+
expect(r.routerIp).toBeNull();
|
|
44
|
+
expect(r.publicIp).toBe('1.2.3.4');
|
|
45
|
+
expect(r.cgnat).toBe(false);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it('detects CGNAT when router and public IP disagree', async () => {
|
|
49
|
+
const det = new ExternalIpDetector({
|
|
50
|
+
routerProbe: async () => '100.64.0.5', // CGNAT space — fine for testing
|
|
51
|
+
fetch: makeFetchSequence([{ ok: true, body: '203.0.113.10' }]),
|
|
52
|
+
publicIpUrls: ['https://example.test'],
|
|
53
|
+
});
|
|
54
|
+
const r = await det.detect();
|
|
55
|
+
expect(r.routerIp).toBe('100.64.0.5');
|
|
56
|
+
expect(r.publicIp).toBe('203.0.113.10');
|
|
57
|
+
expect(r.cgnat).toBe(true);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it('does NOT flag CGNAT when IPs agree', async () => {
|
|
61
|
+
const det = new ExternalIpDetector({
|
|
62
|
+
routerProbe: async () => '1.2.3.4',
|
|
63
|
+
fetch: makeFetchSequence([{ ok: true, body: '1.2.3.4' }]),
|
|
64
|
+
publicIpUrls: ['https://example.test'],
|
|
65
|
+
});
|
|
66
|
+
const r = await det.detect();
|
|
67
|
+
expect(r.cgnat).toBe(false);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it('does NOT flag CGNAT when one probe fails', async () => {
|
|
71
|
+
const det = new ExternalIpDetector({
|
|
72
|
+
routerProbe: async () => '1.2.3.4',
|
|
73
|
+
fetch: makeFetchSequence([{ ok: false, body: '' }]),
|
|
74
|
+
publicIpUrls: ['https://example.test'],
|
|
75
|
+
});
|
|
76
|
+
const r = await det.detect();
|
|
77
|
+
expect(r.routerIp).toBe('1.2.3.4');
|
|
78
|
+
expect(r.publicIp).toBeNull();
|
|
79
|
+
expect(r.cgnat).toBe(false);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it('falls through public probe URLs until one succeeds', async () => {
|
|
83
|
+
const det = new ExternalIpDetector({
|
|
84
|
+
fetch: makeFetchSequence([
|
|
85
|
+
{ ok: false, body: '' },
|
|
86
|
+
{ ok: true, body: '8.8.8.8' },
|
|
87
|
+
]),
|
|
88
|
+
publicIpUrls: ['https://a.test', 'https://b.test'],
|
|
89
|
+
});
|
|
90
|
+
const r = await det.detect();
|
|
91
|
+
expect(r.publicIp).toBe('8.8.8.8');
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it('treats router probe throw as null', async () => {
|
|
95
|
+
const det = new ExternalIpDetector({
|
|
96
|
+
routerProbe: async () => { throw new Error('upnp not reachable'); },
|
|
97
|
+
fetch: makeFetchSequence([{ ok: true, body: '1.2.3.4' }]),
|
|
98
|
+
publicIpUrls: ['https://a.test'],
|
|
99
|
+
});
|
|
100
|
+
const r = await det.detect();
|
|
101
|
+
expect(r.routerIp).toBeNull();
|
|
102
|
+
expect(r.publicIp).toBe('1.2.3.4');
|
|
103
|
+
expect(r.cgnat).toBe(false);
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
it('rejects implausible router-probe IP', async () => {
|
|
107
|
+
const det = new ExternalIpDetector({
|
|
108
|
+
routerProbe: async () => 'not-an-ip',
|
|
109
|
+
fetch: makeFetchSequence([{ ok: true, body: '1.2.3.4' }]),
|
|
110
|
+
publicIpUrls: ['https://a.test'],
|
|
111
|
+
});
|
|
112
|
+
const r = await det.detect();
|
|
113
|
+
expect(r.routerIp).toBeNull();
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
it('rejects non-IP public-service body', async () => {
|
|
117
|
+
const det = new ExternalIpDetector({
|
|
118
|
+
fetch: makeFetchSequence([{ ok: true, body: '<html>error</html>' }]),
|
|
119
|
+
publicIpUrls: ['https://a.test'],
|
|
120
|
+
});
|
|
121
|
+
const r = await det.detect();
|
|
122
|
+
expect(r.publicIp).toBeNull();
|
|
123
|
+
});
|
|
124
|
+
});
|
|
@@ -87,6 +87,21 @@ function makeDetector(opts: { router?: string | null; pub?: string | null }): Ex
|
|
|
87
87
|
});
|
|
88
88
|
}
|
|
89
89
|
|
|
90
|
+
/**
|
|
91
|
+
* The provider call a `putDdns` lands on. Injected wherever a case configures a real
|
|
92
|
+
* provider, so no case here reaches www.duckdns.org — an outbound request in a unit
|
|
93
|
+
* test is a hang waiting for a machine without network. DuckDNS's own contract (the
|
|
94
|
+
* plain-text `OK`) is covered in the ddns specs.
|
|
95
|
+
*/
|
|
96
|
+
function okFetch(): typeof fetch {
|
|
97
|
+
return (async () => ({
|
|
98
|
+
ok: true,
|
|
99
|
+
status: 200,
|
|
100
|
+
statusText: 'OK',
|
|
101
|
+
async text() { return 'OK'; },
|
|
102
|
+
})) as unknown as typeof fetch;
|
|
103
|
+
}
|
|
104
|
+
|
|
90
105
|
let tmpRoot: string;
|
|
91
106
|
beforeEach(() => {
|
|
92
107
|
tmpRoot = mkdtempSync(join(tmpdir(), 'cadre-host-nat-svc-'));
|
|
@@ -169,6 +184,7 @@ describe('NatService', () => {
|
|
|
169
184
|
secretsStore: secrets,
|
|
170
185
|
portMapper: mapper,
|
|
171
186
|
externalIpDetector: makeDetector({ pub: '1.2.3.4' }),
|
|
187
|
+
fetch: okFetch(),
|
|
172
188
|
});
|
|
173
189
|
await svc.start();
|
|
174
190
|
await svc.putDdns({
|
|
@@ -218,6 +234,7 @@ describe('NatService', () => {
|
|
|
218
234
|
secretsStore: secrets,
|
|
219
235
|
portMapper: mapper,
|
|
220
236
|
externalIpDetector: makeDetector({ pub: '1.2.3.4' }),
|
|
237
|
+
fetch: okFetch(),
|
|
221
238
|
});
|
|
222
239
|
await svc.start();
|
|
223
240
|
const status = await svc.putDdns({
|
|
@@ -474,6 +491,7 @@ describe('createNatHandlers', () => {
|
|
|
474
491
|
secretsStore: makeSecrets(),
|
|
475
492
|
portMapper: mapper,
|
|
476
493
|
externalIpDetector: makeDetector({ pub: '1.2.3.4' }),
|
|
494
|
+
fetch: okFetch(),
|
|
477
495
|
});
|
|
478
496
|
await svc.start();
|
|
479
497
|
const h = createNatHandlers(svc);
|
|
@@ -1,93 +1,93 @@
|
|
|
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 { NatStore } from '../nat-store.js';
|
|
7
|
-
import { NatError } from '../types.js';
|
|
8
|
-
|
|
9
|
-
let tmpRoot: string;
|
|
10
|
-
|
|
11
|
-
beforeEach(() => {
|
|
12
|
-
tmpRoot = mkdtempSync(join(tmpdir(), 'cadre-host-nat-store-'));
|
|
13
|
-
});
|
|
14
|
-
|
|
15
|
-
afterEach(() => {
|
|
16
|
-
try { rmSync(tmpRoot, { recursive: true, force: true }); } catch { /* ignore */ }
|
|
17
|
-
});
|
|
18
|
-
|
|
19
|
-
describe('NatStore', () => {
|
|
20
|
-
it('returns defaults when file is missing', () => {
|
|
21
|
-
const store = new NatStore(tmpRoot);
|
|
22
|
-
const s = store.load();
|
|
23
|
-
expect(s.externalPort).toBe(4001);
|
|
24
|
-
expect(s.internalPort).toBe(4001);
|
|
25
|
-
expect(s.upnpEnabled).toBe(true);
|
|
26
|
-
expect(s.ddns.providerId).toBeNull();
|
|
27
|
-
expect(s.ddns.externallyManaged).toBe(false);
|
|
28
|
-
expect(s.ddns.intervalMs).toBe(5 * 60 * 1000);
|
|
29
|
-
});
|
|
30
|
-
|
|
31
|
-
it('persists and reloads settings atomically', () => {
|
|
32
|
-
const store = new NatStore(tmpRoot);
|
|
33
|
-
store.update({ externalPort: 4002, upnpEnabled: false });
|
|
34
|
-
|
|
35
|
-
expect(existsSync(join(tmpRoot, 'nat.json'))).toBe(true);
|
|
36
|
-
expect(existsSync(join(tmpRoot, 'nat.json.tmp'))).toBe(false);
|
|
37
|
-
|
|
38
|
-
const reread = new NatStore(tmpRoot);
|
|
39
|
-
const s = reread.load();
|
|
40
|
-
expect(s.externalPort).toBe(4002);
|
|
41
|
-
expect(s.upnpEnabled).toBe(false);
|
|
42
|
-
});
|
|
43
|
-
|
|
44
|
-
it('merges ddns subtree on update', () => {
|
|
45
|
-
const store = new NatStore(tmpRoot);
|
|
46
|
-
store.update({
|
|
47
|
-
ddns: {
|
|
48
|
-
providerId: 'duckdns',
|
|
49
|
-
hostname: 'foo.duckdns.org',
|
|
50
|
-
externallyManaged: false,
|
|
51
|
-
intervalMs: 60_000,
|
|
52
|
-
},
|
|
53
|
-
});
|
|
54
|
-
|
|
55
|
-
store.update({ ddns: { externallyManaged: true } as never });
|
|
56
|
-
|
|
57
|
-
const s = store.load();
|
|
58
|
-
expect(s.ddns.providerId).toBe('duckdns');
|
|
59
|
-
expect(s.ddns.hostname).toBe('foo.duckdns.org');
|
|
60
|
-
expect(s.ddns.externallyManaged).toBe(true);
|
|
61
|
-
expect(s.ddns.intervalMs).toBe(60_000);
|
|
62
|
-
});
|
|
63
|
-
|
|
64
|
-
it('persists version field', () => {
|
|
65
|
-
const store = new NatStore(tmpRoot);
|
|
66
|
-
store.update({ externalPort: 4001 });
|
|
67
|
-
const raw = JSON.parse(readFileSync(join(tmpRoot, 'nat.json'), 'utf8'));
|
|
68
|
-
expect(raw.version).toBe(1);
|
|
69
|
-
});
|
|
70
|
-
|
|
71
|
-
it('throws on malformed JSON rather than silently wiping', () => {
|
|
72
|
-
writeFileSync(join(tmpRoot, 'nat.json'), 'not json', 'utf8');
|
|
73
|
-
const store = new NatStore(tmpRoot);
|
|
74
|
-
expect(() => store.load()).toThrow(NatError);
|
|
75
|
-
});
|
|
76
|
-
|
|
77
|
-
it('rejects invalid port', () => {
|
|
78
|
-
const store = new NatStore(tmpRoot);
|
|
79
|
-
expect(() => store.update({ externalPort: 0 })).toThrow(NatError);
|
|
80
|
-
expect(() => store.update({ externalPort: 70000 })).toThrow(NatError);
|
|
81
|
-
});
|
|
82
|
-
|
|
83
|
-
it('rejects too-small interval', () => {
|
|
84
|
-
const store = new NatStore(tmpRoot);
|
|
85
|
-
expect(() => store.update({ ddns: { intervalMs: 500 } as never })).toThrow(NatError);
|
|
86
|
-
});
|
|
87
|
-
|
|
88
|
-
it('rejects empty string providerId/hostname', () => {
|
|
89
|
-
const store = new NatStore(tmpRoot);
|
|
90
|
-
expect(() => store.update({ ddns: { providerId: '' } as never })).toThrow(NatError);
|
|
91
|
-
expect(() => store.update({ ddns: { hostname: '' } as never })).toThrow(NatError);
|
|
92
|
-
});
|
|
93
|
-
});
|
|
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 { NatStore } from '../nat-store.js';
|
|
7
|
+
import { NatError } from '../types.js';
|
|
8
|
+
|
|
9
|
+
let tmpRoot: string;
|
|
10
|
+
|
|
11
|
+
beforeEach(() => {
|
|
12
|
+
tmpRoot = mkdtempSync(join(tmpdir(), 'cadre-host-nat-store-'));
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
afterEach(() => {
|
|
16
|
+
try { rmSync(tmpRoot, { recursive: true, force: true }); } catch { /* ignore */ }
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
describe('NatStore', () => {
|
|
20
|
+
it('returns defaults when file is missing', () => {
|
|
21
|
+
const store = new NatStore(tmpRoot);
|
|
22
|
+
const s = store.load();
|
|
23
|
+
expect(s.externalPort).toBe(4001);
|
|
24
|
+
expect(s.internalPort).toBe(4001);
|
|
25
|
+
expect(s.upnpEnabled).toBe(true);
|
|
26
|
+
expect(s.ddns.providerId).toBeNull();
|
|
27
|
+
expect(s.ddns.externallyManaged).toBe(false);
|
|
28
|
+
expect(s.ddns.intervalMs).toBe(5 * 60 * 1000);
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it('persists and reloads settings atomically', () => {
|
|
32
|
+
const store = new NatStore(tmpRoot);
|
|
33
|
+
store.update({ externalPort: 4002, upnpEnabled: false });
|
|
34
|
+
|
|
35
|
+
expect(existsSync(join(tmpRoot, 'nat.json'))).toBe(true);
|
|
36
|
+
expect(existsSync(join(tmpRoot, 'nat.json.tmp'))).toBe(false);
|
|
37
|
+
|
|
38
|
+
const reread = new NatStore(tmpRoot);
|
|
39
|
+
const s = reread.load();
|
|
40
|
+
expect(s.externalPort).toBe(4002);
|
|
41
|
+
expect(s.upnpEnabled).toBe(false);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it('merges ddns subtree on update', () => {
|
|
45
|
+
const store = new NatStore(tmpRoot);
|
|
46
|
+
store.update({
|
|
47
|
+
ddns: {
|
|
48
|
+
providerId: 'duckdns',
|
|
49
|
+
hostname: 'foo.duckdns.org',
|
|
50
|
+
externallyManaged: false,
|
|
51
|
+
intervalMs: 60_000,
|
|
52
|
+
},
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
store.update({ ddns: { externallyManaged: true } as never });
|
|
56
|
+
|
|
57
|
+
const s = store.load();
|
|
58
|
+
expect(s.ddns.providerId).toBe('duckdns');
|
|
59
|
+
expect(s.ddns.hostname).toBe('foo.duckdns.org');
|
|
60
|
+
expect(s.ddns.externallyManaged).toBe(true);
|
|
61
|
+
expect(s.ddns.intervalMs).toBe(60_000);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it('persists version field', () => {
|
|
65
|
+
const store = new NatStore(tmpRoot);
|
|
66
|
+
store.update({ externalPort: 4001 });
|
|
67
|
+
const raw = JSON.parse(readFileSync(join(tmpRoot, 'nat.json'), 'utf8'));
|
|
68
|
+
expect(raw.version).toBe(1);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it('throws on malformed JSON rather than silently wiping', () => {
|
|
72
|
+
writeFileSync(join(tmpRoot, 'nat.json'), 'not json', 'utf8');
|
|
73
|
+
const store = new NatStore(tmpRoot);
|
|
74
|
+
expect(() => store.load()).toThrow(NatError);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it('rejects invalid port', () => {
|
|
78
|
+
const store = new NatStore(tmpRoot);
|
|
79
|
+
expect(() => store.update({ externalPort: 0 })).toThrow(NatError);
|
|
80
|
+
expect(() => store.update({ externalPort: 70000 })).toThrow(NatError);
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it('rejects too-small interval', () => {
|
|
84
|
+
const store = new NatStore(tmpRoot);
|
|
85
|
+
expect(() => store.update({ ddns: { intervalMs: 500 } as never })).toThrow(NatError);
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it('rejects empty string providerId/hostname', () => {
|
|
89
|
+
const store = new NatStore(tmpRoot);
|
|
90
|
+
expect(() => store.update({ ddns: { providerId: '' } as never })).toThrow(NatError);
|
|
91
|
+
expect(() => store.update({ ddns: { hostname: '' } as never })).toThrow(NatError);
|
|
92
|
+
});
|
|
93
|
+
});
|