@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,84 +1,84 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Origin guard — defeats DNS-rebind attacks against the loopback listener.
|
|
3
|
-
*
|
|
4
|
-
* The listener is bound to 127.0.0.1 only, but a malicious page on
|
|
5
|
-
* `evil.example.com` can still resolve its own hostname to 127.0.0.1 and
|
|
6
|
-
* speak HTTP to us. We mitigate by requiring that:
|
|
7
|
-
* - `Host` is `127.0.0.1[:port]` or `localhost[:port]` (case-insensitive),
|
|
8
|
-
* - `Origin` (when present) matches one of those origins.
|
|
9
|
-
*
|
|
10
|
-
* Forbidden requests return 403 with the standard error envelope.
|
|
11
|
-
*
|
|
12
|
-
* The expected port can be supplied as a constant or as a getter — the
|
|
13
|
-
* server's port-fallback loop discovers the bound port only after `listen`,
|
|
14
|
-
* but Fastify hooks must be registered before `ready`, so the guard reads
|
|
15
|
-
* the port at request time via the getter.
|
|
16
|
-
*/
|
|
17
|
-
|
|
18
|
-
import type { FastifyInstance, FastifyReply, FastifyRequest } from 'fastify';
|
|
19
|
-
|
|
20
|
-
export interface OriginGuardOptions {
|
|
21
|
-
/** Bound port (constant) or a getter for the current bound port. */
|
|
22
|
-
port: number | (() => number);
|
|
23
|
-
/** Allow these additional hosts (test harnesses, integrations). */
|
|
24
|
-
extraAllowedHosts?: ReadonlyArray<string>;
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
const ALLOWED_HOSTNAMES = ['127.0.0.1', 'localhost', '[::1]', '::1'] as const;
|
|
28
|
-
|
|
29
|
-
export function registerOriginGuard(app: FastifyInstance, opts: OriginGuardOptions): void {
|
|
30
|
-
const getPort = typeof opts.port === 'function' ? opts.port : () => opts.port as number;
|
|
31
|
-
const extras = (opts.extraAllowedHosts ?? []).map((e) => e.toLowerCase());
|
|
32
|
-
|
|
33
|
-
app.addHook('onRequest', async (request: FastifyRequest, reply: FastifyReply) => {
|
|
34
|
-
const port = getPort();
|
|
35
|
-
const allowedHosts = buildAllowedHostHeaders(port, extras);
|
|
36
|
-
const allowedOrigins = buildAllowedOrigins(port, extras);
|
|
37
|
-
|
|
38
|
-
const hostHeader = (request.headers.host ?? '').toLowerCase();
|
|
39
|
-
if (!hostHeader || !allowedHosts.has(hostHeader)) {
|
|
40
|
-
return reply.code(403).send({
|
|
41
|
-
ok: false,
|
|
42
|
-
error: {
|
|
43
|
-
code: 'forbidden_origin',
|
|
44
|
-
message: `Host header "${request.headers.host ?? ''}" is not allowed`,
|
|
45
|
-
},
|
|
46
|
-
});
|
|
47
|
-
}
|
|
48
|
-
const origin = request.headers.origin;
|
|
49
|
-
if (typeof origin === 'string' && origin.length > 0) {
|
|
50
|
-
if (!allowedOrigins.has(origin.toLowerCase())) {
|
|
51
|
-
return reply.code(403).send({
|
|
52
|
-
ok: false,
|
|
53
|
-
error: {
|
|
54
|
-
code: 'forbidden_origin',
|
|
55
|
-
message: `Origin "${origin}" is not allowed`,
|
|
56
|
-
},
|
|
57
|
-
});
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
|
-
return undefined;
|
|
61
|
-
});
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
function buildAllowedHostHeaders(port: number, extras: ReadonlyArray<string>): Set<string> {
|
|
65
|
-
const set = new Set<string>();
|
|
66
|
-
for (const host of ALLOWED_HOSTNAMES) {
|
|
67
|
-
set.add(host);
|
|
68
|
-
set.add(`${host}:${port}`);
|
|
69
|
-
}
|
|
70
|
-
for (const e of extras) set.add(e);
|
|
71
|
-
return set;
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
function buildAllowedOrigins(port: number, extras: ReadonlyArray<string>): Set<string> {
|
|
75
|
-
const set = new Set<string>();
|
|
76
|
-
for (const host of ALLOWED_HOSTNAMES) {
|
|
77
|
-
set.add(`http://${host}`);
|
|
78
|
-
set.add(`http://${host}:${port}`);
|
|
79
|
-
set.add(`https://${host}`);
|
|
80
|
-
set.add(`https://${host}:${port}`);
|
|
81
|
-
}
|
|
82
|
-
for (const e of extras) set.add(e);
|
|
83
|
-
return set;
|
|
84
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* Origin guard — defeats DNS-rebind attacks against the loopback listener.
|
|
3
|
+
*
|
|
4
|
+
* The listener is bound to 127.0.0.1 only, but a malicious page on
|
|
5
|
+
* `evil.example.com` can still resolve its own hostname to 127.0.0.1 and
|
|
6
|
+
* speak HTTP to us. We mitigate by requiring that:
|
|
7
|
+
* - `Host` is `127.0.0.1[:port]` or `localhost[:port]` (case-insensitive),
|
|
8
|
+
* - `Origin` (when present) matches one of those origins.
|
|
9
|
+
*
|
|
10
|
+
* Forbidden requests return 403 with the standard error envelope.
|
|
11
|
+
*
|
|
12
|
+
* The expected port can be supplied as a constant or as a getter — the
|
|
13
|
+
* server's port-fallback loop discovers the bound port only after `listen`,
|
|
14
|
+
* but Fastify hooks must be registered before `ready`, so the guard reads
|
|
15
|
+
* the port at request time via the getter.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import type { FastifyInstance, FastifyReply, FastifyRequest } from 'fastify';
|
|
19
|
+
|
|
20
|
+
export interface OriginGuardOptions {
|
|
21
|
+
/** Bound port (constant) or a getter for the current bound port. */
|
|
22
|
+
port: number | (() => number);
|
|
23
|
+
/** Allow these additional hosts (test harnesses, integrations). */
|
|
24
|
+
extraAllowedHosts?: ReadonlyArray<string>;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const ALLOWED_HOSTNAMES = ['127.0.0.1', 'localhost', '[::1]', '::1'] as const;
|
|
28
|
+
|
|
29
|
+
export function registerOriginGuard(app: FastifyInstance, opts: OriginGuardOptions): void {
|
|
30
|
+
const getPort = typeof opts.port === 'function' ? opts.port : () => opts.port as number;
|
|
31
|
+
const extras = (opts.extraAllowedHosts ?? []).map((e) => e.toLowerCase());
|
|
32
|
+
|
|
33
|
+
app.addHook('onRequest', async (request: FastifyRequest, reply: FastifyReply) => {
|
|
34
|
+
const port = getPort();
|
|
35
|
+
const allowedHosts = buildAllowedHostHeaders(port, extras);
|
|
36
|
+
const allowedOrigins = buildAllowedOrigins(port, extras);
|
|
37
|
+
|
|
38
|
+
const hostHeader = (request.headers.host ?? '').toLowerCase();
|
|
39
|
+
if (!hostHeader || !allowedHosts.has(hostHeader)) {
|
|
40
|
+
return reply.code(403).send({
|
|
41
|
+
ok: false,
|
|
42
|
+
error: {
|
|
43
|
+
code: 'forbidden_origin',
|
|
44
|
+
message: `Host header "${request.headers.host ?? ''}" is not allowed`,
|
|
45
|
+
},
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
const origin = request.headers.origin;
|
|
49
|
+
if (typeof origin === 'string' && origin.length > 0) {
|
|
50
|
+
if (!allowedOrigins.has(origin.toLowerCase())) {
|
|
51
|
+
return reply.code(403).send({
|
|
52
|
+
ok: false,
|
|
53
|
+
error: {
|
|
54
|
+
code: 'forbidden_origin',
|
|
55
|
+
message: `Origin "${origin}" is not allowed`,
|
|
56
|
+
},
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
return undefined;
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function buildAllowedHostHeaders(port: number, extras: ReadonlyArray<string>): Set<string> {
|
|
65
|
+
const set = new Set<string>();
|
|
66
|
+
for (const host of ALLOWED_HOSTNAMES) {
|
|
67
|
+
set.add(host);
|
|
68
|
+
set.add(`${host}:${port}`);
|
|
69
|
+
}
|
|
70
|
+
for (const e of extras) set.add(e);
|
|
71
|
+
return set;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function buildAllowedOrigins(port: number, extras: ReadonlyArray<string>): Set<string> {
|
|
75
|
+
const set = new Set<string>();
|
|
76
|
+
for (const host of ALLOWED_HOSTNAMES) {
|
|
77
|
+
set.add(`http://${host}`);
|
|
78
|
+
set.add(`http://${host}:${port}`);
|
|
79
|
+
set.add(`https://${host}`);
|
|
80
|
+
set.add(`https://${host}:${port}`);
|
|
81
|
+
}
|
|
82
|
+
for (const e of extras) set.add(e);
|
|
83
|
+
return set;
|
|
84
|
+
}
|
|
@@ -1,41 +1,41 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Donation grant **admin** HTTP routes — thin adapters around
|
|
3
|
-
* `GrantAdminHandlers` (`createGrantAdminHandlers(service)`).
|
|
4
|
-
*
|
|
5
|
-
* Mount path: `/grants-admin` — the admin surface the `cadre-host grant` CLI
|
|
6
|
-
* targets. It is **loopback, no bearer**: same-machine admin, matching
|
|
7
|
-
* cadre-host's local-UI "no login" posture (see docs/cadre-host.md §
|
|
8
|
-
* Security posture). This is distinct from the grantee-facing `/grants`
|
|
9
|
-
* provisioning surface that carries the bearer gate — that arrives in the
|
|
10
|
-
* `2-donation-service` ticket.
|
|
11
|
-
*/
|
|
12
|
-
|
|
13
|
-
import type { FastifyInstance } from 'fastify';
|
|
14
|
-
|
|
15
|
-
import type { GrantAdminHandlers } from '../../donation/types.js';
|
|
16
|
-
|
|
17
|
-
export interface GrantsAdminRoutesOptions {
|
|
18
|
-
handlers: GrantAdminHandlers;
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
export function registerGrantsAdminRoutes(app: FastifyInstance, opts: GrantsAdminRoutesOptions): void {
|
|
22
|
-
const { handlers } = opts;
|
|
23
|
-
|
|
24
|
-
app.get('/grants-admin', async () => {
|
|
25
|
-
return handlers.listGrants();
|
|
26
|
-
});
|
|
27
|
-
|
|
28
|
-
app.post('/grants-admin', async (request) => {
|
|
29
|
-
const body = (request.body ?? {}) as { label?: unknown; maxNodes?: unknown; ttlMs?: unknown };
|
|
30
|
-
const label = typeof body.label === 'string' ? body.label : '';
|
|
31
|
-
const args: { label: string; maxNodes?: number; ttlMs?: number } = { label };
|
|
32
|
-
if (typeof body.maxNodes === 'number') args.maxNodes = body.maxNodes;
|
|
33
|
-
if (typeof body.ttlMs === 'number') args.ttlMs = body.ttlMs;
|
|
34
|
-
return handlers.postGrant(args);
|
|
35
|
-
});
|
|
36
|
-
|
|
37
|
-
app.delete<{ Params: { token: string } }>('/grants-admin/:token', async (request) => {
|
|
38
|
-
await handlers.deleteGrant(request.params.token);
|
|
39
|
-
return { ok: true };
|
|
40
|
-
});
|
|
41
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* Donation grant **admin** HTTP routes — thin adapters around
|
|
3
|
+
* `GrantAdminHandlers` (`createGrantAdminHandlers(service)`).
|
|
4
|
+
*
|
|
5
|
+
* Mount path: `/grants-admin` — the admin surface the `cadre-host grant` CLI
|
|
6
|
+
* targets. It is **loopback, no bearer**: same-machine admin, matching
|
|
7
|
+
* cadre-host's local-UI "no login" posture (see docs/cadre-host.md §
|
|
8
|
+
* Security posture). This is distinct from the grantee-facing `/grants`
|
|
9
|
+
* provisioning surface that carries the bearer gate — that arrives in the
|
|
10
|
+
* `2-donation-service` ticket.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import type { FastifyInstance } from 'fastify';
|
|
14
|
+
|
|
15
|
+
import type { GrantAdminHandlers } from '../../donation/types.js';
|
|
16
|
+
|
|
17
|
+
export interface GrantsAdminRoutesOptions {
|
|
18
|
+
handlers: GrantAdminHandlers;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function registerGrantsAdminRoutes(app: FastifyInstance, opts: GrantsAdminRoutesOptions): void {
|
|
22
|
+
const { handlers } = opts;
|
|
23
|
+
|
|
24
|
+
app.get('/grants-admin', async () => {
|
|
25
|
+
return handlers.listGrants();
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
app.post('/grants-admin', async (request) => {
|
|
29
|
+
const body = (request.body ?? {}) as { label?: unknown; maxNodes?: unknown; ttlMs?: unknown };
|
|
30
|
+
const label = typeof body.label === 'string' ? body.label : '';
|
|
31
|
+
const args: { label: string; maxNodes?: number; ttlMs?: number } = { label };
|
|
32
|
+
if (typeof body.maxNodes === 'number') args.maxNodes = body.maxNodes;
|
|
33
|
+
if (typeof body.ttlMs === 'number') args.ttlMs = body.ttlMs;
|
|
34
|
+
return handlers.postGrant(args);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
app.delete<{ Params: { token: string } }>('/grants-admin/:token', async (request) => {
|
|
38
|
+
await handlers.deleteGrant(request.params.token);
|
|
39
|
+
return { ok: true };
|
|
40
|
+
});
|
|
41
|
+
}
|