@camstack/system 1.2.274 → 1.2.275

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 (65) hide show
  1. package/dist/builtins/addon-pages-aggregator/addon-pages-aggregator.addon.js +1 -1
  2. package/dist/builtins/addon-pages-aggregator/addon-pages-aggregator.addon.mjs +1 -1
  3. package/dist/builtins/addon-widgets-aggregator/addon-widgets-aggregator.addon.js +1 -1
  4. package/dist/builtins/addon-widgets-aggregator/addon-widgets-aggregator.addon.mjs +1 -1
  5. package/dist/builtins/alerts/alerts.addon.js +1 -1
  6. package/dist/builtins/alerts/alerts.addon.mjs +1 -1
  7. package/dist/builtins/autotrack/index.js +1 -1
  8. package/dist/builtins/autotrack/index.mjs +1 -1
  9. package/dist/builtins/backup-orchestrator/backup-orchestrator.addon.js +1 -1
  10. package/dist/builtins/backup-orchestrator/backup-orchestrator.addon.mjs +1 -1
  11. package/dist/builtins/camera-grid/index.js +1 -1
  12. package/dist/builtins/camera-grid/index.mjs +1 -1
  13. package/dist/builtins/console-logging/index.js +1 -1
  14. package/dist/builtins/console-logging/index.mjs +1 -1
  15. package/dist/builtins/core-blocks/core-blocks.addon.js +1 -1
  16. package/dist/builtins/core-blocks/core-blocks.addon.mjs +1 -1
  17. package/dist/builtins/device-manager/device-manager.addon.js +2 -2
  18. package/dist/builtins/device-manager/device-manager.addon.mjs +2 -2
  19. package/dist/builtins/doorbell/virtual-doorbell.addon.js +1 -1
  20. package/dist/builtins/doorbell/virtual-doorbell.addon.mjs +1 -1
  21. package/dist/builtins/hub-forwarder/index.js +1 -1
  22. package/dist/builtins/hub-forwarder/index.mjs +1 -1
  23. package/dist/builtins/liveness-monitor/liveness-monitor.addon.js +1 -1
  24. package/dist/builtins/liveness-monitor/liveness-monitor.addon.mjs +1 -1
  25. package/dist/builtins/local-auth/local-auth.addon.js +1 -1
  26. package/dist/builtins/local-auth/local-auth.addon.mjs +1 -1
  27. package/dist/builtins/local-network/local-network.addon.js +2 -2
  28. package/dist/builtins/local-network/local-network.addon.mjs +2 -2
  29. package/dist/builtins/loki-logging/index.js +1 -1
  30. package/dist/builtins/loki-logging/index.mjs +1 -1
  31. package/dist/builtins/native-metrics/native-metrics.addon.js +1 -1
  32. package/dist/builtins/native-metrics/native-metrics.addon.mjs +1 -1
  33. package/dist/builtins/platform-probe/index.js +1 -1
  34. package/dist/builtins/platform-probe/index.mjs +1 -1
  35. package/dist/builtins/remote-access-orchestrator/remote-access-orchestrator.addon.js +1 -1
  36. package/dist/builtins/remote-access-orchestrator/remote-access-orchestrator.addon.mjs +1 -1
  37. package/dist/builtins/snapshot/index.js +26 -6
  38. package/dist/builtins/snapshot/index.mjs +26 -6
  39. package/dist/builtins/snapshot/snapshot-durable-store.d.ts +4 -0
  40. package/dist/builtins/sqlite-storage/filesystem-storage.addon.js +1 -1
  41. package/dist/builtins/sqlite-storage/filesystem-storage.addon.mjs +1 -1
  42. package/dist/builtins/sqlite-storage/sqlite-settings.addon.js +0 -0
  43. package/dist/builtins/sqlite-storage/sqlite-settings.addon.mjs +0 -0
  44. package/dist/builtins/storage-orchestrator/storage-orchestrator.addon.js +1 -1
  45. package/dist/builtins/storage-orchestrator/storage-orchestrator.addon.mjs +1 -1
  46. package/dist/builtins/system-config/system-config.addon.js +1 -1
  47. package/dist/builtins/system-config/system-config.addon.mjs +1 -1
  48. package/dist/builtins/winston-logging/index.js +1 -1
  49. package/dist/builtins/winston-logging/index.mjs +1 -1
  50. package/dist/{dist-C_4Xt0Ya.js → dist-CYXhwfDf.js} +81 -7
  51. package/dist/{dist-DchKwh_m.mjs → dist-DWSJVcZe.mjs} +81 -7
  52. package/dist/index.js +2 -2
  53. package/dist/index.mjs +2 -2
  54. package/dist/{retired-settings-keys-DPSHUHMu.mjs → retired-settings-keys-BHGnCLW0.mjs} +1 -1
  55. package/dist/{retired-settings-keys-6wZNp7lr.js → retired-settings-keys-DEFtUH8o.js} +1 -1
  56. package/dist/tls/cert-identity.d.ts +22 -0
  57. package/dist/tls/cert-issue.d.ts +31 -0
  58. package/dist/tls/cert-manager.d.ts +8 -2
  59. package/dist/tls/index.d.ts +2 -2
  60. package/dist/tls-D_wUeOTD.js +12122 -0
  61. package/dist/tls-IM-v7JV4.mjs +11984 -0
  62. package/package.json +2 -1
  63. package/dist/tls/cert-openssl.d.ts +0 -23
  64. package/dist/tls-2xbgg78V.mjs +0 -752
  65. package/dist/tls-BFs4PzoW.js +0 -890
@@ -1,890 +0,0 @@
1
- const require_chunk = require("./chunk-Cek0wNdY.js");
2
- let node_crypto = require("node:crypto");
3
- let node_fs_promises = require("node:fs/promises");
4
- let node_path = require("node:path");
5
- let node_fs = require("node:fs");
6
- let node_child_process = require("node:child_process");
7
- let node_os = require("node:os");
8
- node_os = require_chunk.__toESM(node_os);
9
- let node_util = require("node:util");
10
- let node_http = require("node:http");
11
- let node_net = require("node:net");
12
- //#region src/http/lan-http-bind.ts
13
- /**
14
- * Second HTTP listener that shares the hub's existing request handler.
15
- *
16
- * Fastify is one protocol per instance; HTTPS stays on :4443 and this
17
- * binds a plain `http.Server` that emits `request` AND `upgrade` onto the
18
- * same handler graph (tRPC HTTP, cookies, data-plane, static, tRPC WS).
19
- *
20
- * Forwarding only `request` is not enough: the viewer speaks tRPC exclusively
21
- * over WebSocket (`wsLink`). `/health` wins the LAN race, then `ws://…/trpc`
22
- * hangs because nobody handles the upgrade on this socket.
23
- */
24
- var DEFAULT_HTTP_PORT = 4480;
25
- var handlers = null;
26
- var server = null;
27
- var pending = null;
28
- var state = {
29
- listening: false,
30
- error: null,
31
- port: DEFAULT_HTTP_PORT
32
- };
33
- var boundRequestedHost = null;
34
- var DEFAULT_LAN_HTTP_PORT = DEFAULT_HTTP_PORT;
35
- /** Map "all IPv4" to dual-stack IPv6 (`::`, ipv6Only false) so IPv4 still works. */
36
- function allFamiliesListenHost(host) {
37
- if (host === "0.0.0.0" || host === "*" || host === "") return {
38
- host: "::",
39
- ipv6Only: false
40
- };
41
- return {
42
- host,
43
- ipv6Only: false
44
- };
45
- }
46
- function registerLanHttpHandler(next) {
47
- handlers = next;
48
- }
49
- function readLanHttpState() {
50
- return state;
51
- }
52
- async function applyLanHttp(options) {
53
- pending = options;
54
- if (!options.enabled) {
55
- await closeLanHttp();
56
- state = {
57
- listening: false,
58
- error: null,
59
- port: options.port
60
- };
61
- return state;
62
- }
63
- if (handlers === null) {
64
- state = {
65
- listening: false,
66
- error: "HTTP listener is not registered yet",
67
- port: options.port
68
- };
69
- return state;
70
- }
71
- if (server !== null && state.listening && (options.port === 0 || state.port === options.port) && boundRequestedHost === options.host) return state;
72
- await closeLanHttp();
73
- try {
74
- const bound = await listenHttp(options.port, options.host, handlers);
75
- server = bound;
76
- boundRequestedHost = options.host;
77
- const addr = bound.address();
78
- state = {
79
- listening: true,
80
- error: null,
81
- port: typeof addr === "object" && addr !== null ? addr.port : options.port
82
- };
83
- return state;
84
- } catch (err) {
85
- server = null;
86
- state = {
87
- listening: false,
88
- error: err instanceof Error ? err.message : String(err),
89
- port: options.port
90
- };
91
- return state;
92
- }
93
- }
94
- /** After the hub registers the Fastify request handler, bind whatever was pending. */
95
- async function bindPendingLanHttp() {
96
- if (pending === null) return applyLanHttp({
97
- enabled: true,
98
- port: DEFAULT_HTTP_PORT,
99
- host: "0.0.0.0"
100
- });
101
- return applyLanHttp(pending);
102
- }
103
- async function closeLanHttp() {
104
- const current = server;
105
- server = null;
106
- boundRequestedHost = null;
107
- if (current === null) return;
108
- await new Promise((resolve) => {
109
- current.close(() => resolve());
110
- });
111
- }
112
- function listenHttp(port, host, next) {
113
- const resolved = allFamiliesListenHost(host);
114
- return listenOn(port, resolved, next).catch((err) => {
115
- if (resolved.host === "::" && host !== "::") return listenOn(port, {
116
- host: "0.0.0.0",
117
- ipv6Only: false
118
- }, next);
119
- throw err;
120
- });
121
- }
122
- function listenOn(port, bind, next) {
123
- return new Promise((resolve, reject) => {
124
- const created = (0, node_http.createServer)(next.onRequest);
125
- created.on("upgrade", next.onUpgrade);
126
- const onError = (err) => {
127
- created.off("listening", onListening);
128
- created.close();
129
- reject(err);
130
- };
131
- const onListening = () => {
132
- created.off("error", onError);
133
- resolve(created);
134
- };
135
- created.once("error", onError);
136
- created.once("listening", onListening);
137
- created.listen({
138
- port,
139
- host: bind.host,
140
- ipv6Only: bind.ipv6Only
141
- });
142
- });
143
- }
144
- //#endregion
145
- //#region src/tls/cert-evaluation.ts
146
- /**
147
- * The regeneration rule (D227), as a pure function.
148
- *
149
- * `ensureTlsCert` reissues **only** when the material on disk cannot do
150
- * its job. Never because it is old, never because the machine picked up
151
- * an address the certificate does not need, never because a new build
152
- * shipped. The decision is a function of (files on disk, machine
153
- * identity, clock) alone, so the same inputs always produce the same
154
- * answer — which is what makes a reissue happen once and then stop.
155
- */
156
- /** OID `id-kp-serverAuth`. Apple's trustd requires it on the leaf. */
157
- var SERVER_AUTH_OID = "1.3.6.1.5.5.7.3.1";
158
- /**
159
- * Apple's `SecPolicyCheckOtherTrustValidityPeriod` ceiling is 825 days;
160
- * the CA/Browser Forum's recommended maximum is 397. We sit at 397.
161
- */
162
- var MAX_LEAF_VALIDITY_DAYS = 397;
163
- /** Reissue this long before the leaf dies, so nobody serves an expired one. */
164
- var LEAF_RENEWAL_WINDOW_DAYS = 30;
165
- /** A local root outlives many leaves — it is the anchor an operator trusts. */
166
- var CA_VALIDITY_DAYS = 3650;
167
- var MS_PER_DAY = 864e5;
168
- /** Reasons that invalidate the root as well as the leaf. */
169
- var REASONS_REQUIRING_NEW_CA = [
170
- "missing",
171
- "unreadable",
172
- "no-local-ca",
173
- "ca-expiring"
174
- ];
175
- function reasonRequiresNewCa(reason) {
176
- return REASONS_REQUIRING_NEW_CA.includes(reason);
177
- }
178
- function parse(pem) {
179
- try {
180
- return new node_crypto.X509Certificate(pem);
181
- } catch {
182
- return null;
183
- }
184
- }
185
- function daysBetween(from, to) {
186
- return (to.getTime() - from.getTime()) / MS_PER_DAY;
187
- }
188
- function hasServerAuthEku(leaf) {
189
- const eku = leaf.keyUsage;
190
- return eku !== void 0 && eku.length === 1 && eku[0] === "1.3.6.1.5.5.7.3.1";
191
- }
192
- function keyMatches(leaf, keyPem) {
193
- try {
194
- return leaf.checkPrivateKey((0, node_crypto.createPrivateKey)(keyPem));
195
- } catch {
196
- return false;
197
- }
198
- }
199
- function coversIdentity(leaf, identity) {
200
- for (const name of identity.requiredDnsNames) if (leaf.checkHost(name) === void 0) return false;
201
- for (const ip of identity.requiredIpAddresses) if (leaf.checkIP(ip) === void 0) return false;
202
- return true;
203
- }
204
- /**
205
- * `null` when the material on disk is fit to keep serving.
206
- *
207
- * Order matters only for the quality of the reported reason; any
208
- * non-null answer produces the same action.
209
- */
210
- function evaluateExistingCert(input) {
211
- const leaf = parse(input.chainPem);
212
- const ca = parse(input.caPem);
213
- if (leaf === null || ca === null) return "unreadable";
214
- if (daysBetween(input.now, new Date(ca.validTo)) < 427) return "ca-expiring";
215
- if (!keyMatches(leaf, input.keyPem)) return "key-mismatch";
216
- if (daysBetween(input.now, new Date(leaf.validTo)) < 30) return "expiring";
217
- if (!hasServerAuthEku(leaf)) return "missing-server-auth-eku";
218
- if (leaf.ca || !leaf.checkIssued(ca) || !leaf.verify(ca.publicKey)) return "not-issued-by-local-ca";
219
- if (daysBetween(new Date(leaf.validFrom), new Date(leaf.validTo)) > 398) return "validity-too-long";
220
- if (!coversIdentity(leaf, input.identity)) return "san-coverage-gap";
221
- return null;
222
- }
223
- //#endregion
224
- //#region src/tls/cert-identity.ts
225
- /**
226
- * What names and addresses the hub's certificate has to answer for.
227
- *
228
- * Split out of `cert-manager.ts` because it is pure and because the
229
- * regeneration rule (D227) is decided entirely by the difference between
230
- * *this* and what the certificate on disk already carries.
231
- */
232
- /**
233
- * Interfaces whose addresses are certified but never gate a reissue:
234
- * container bridges, VPN tunnels and mesh overlays hand out addresses
235
- * that change without the hub moving.
236
- */
237
- var VOLATILE_INTERFACE_PREFIXES = [
238
- "docker",
239
- "br-",
240
- "veth",
241
- "virbr",
242
- "cni",
243
- "flannel",
244
- "tun",
245
- "utun",
246
- "tap",
247
- "wg",
248
- "zt",
249
- "tailscale",
250
- "ppp",
251
- "awdl",
252
- "llw"
253
- ];
254
- /** A DNS name we are willing to hand to openssl. */
255
- var DNS_NAME_PATTERN = /^[A-Za-z0-9]([A-Za-z0-9._-]*[A-Za-z0-9])?$/;
256
- function isIpv4(value) {
257
- return (0, node_net.isIP)(value) === 4;
258
- }
259
- function isIpv6(value) {
260
- return (0, node_net.isIP)(value) === 6;
261
- }
262
- /**
263
- * `fe80::/10` and `169.254.0.0/16`. An IPv6 link-local address needs a
264
- * zone index to be usable and a SAN cannot carry one, so such an entry
265
- * can never match anything — it is noise that makes the certificate look
266
- * broader than it is.
267
- */
268
- function isLinkLocal(value) {
269
- if (isIpv4(value)) return value.startsWith("169.254.");
270
- if (!isIpv6(value)) return false;
271
- const firstGroup = value.split(":")[0] ?? "";
272
- if (firstGroup === "") return false;
273
- const parsed = Number.parseInt(firstGroup, 16);
274
- return Number.isFinite(parsed) && (parsed & 65472) === 65152;
275
- }
276
- /** `fc00::/7` — the IPv6 range that behaves like RFC 1918 does for v4. */
277
- function isUniqueLocalIpv6(value) {
278
- if (!isIpv6(value)) return false;
279
- const firstGroup = value.split(":")[0] ?? "";
280
- const parsed = Number.parseInt(firstGroup, 16);
281
- return Number.isFinite(parsed) && (parsed & 65024) === 64512;
282
- }
283
- function isVolatileInterface(name) {
284
- const lower = name.toLowerCase();
285
- return VOLATILE_INTERFACE_PREFIXES.some((prefix) => lower.startsWith(prefix));
286
- }
287
- /**
288
- * A global-scope IPv6 address is NOT a stable identity: SLAAC privacy
289
- * extensions rotate temporary addresses roughly daily, and `os` gives us
290
- * no way to tell a temporary address from a permanent one. Gating on
291
- * one would reissue the certificate every day.
292
- */
293
- function isStableGateAddress(address) {
294
- if (isLinkLocal(address)) return false;
295
- if (isIpv4(address)) return true;
296
- return isUniqueLocalIpv6(address);
297
- }
298
- function addSan(dns, ips, value) {
299
- const trimmed = value.trim();
300
- if (trimmed === "") return;
301
- if ((0, node_net.isIP)(trimmed) !== 0) {
302
- if (!isLinkLocal(trimmed)) ips.add(trimmed);
303
- return;
304
- }
305
- if (DNS_NAME_PATTERN.test(trimmed)) dns.add(trimmed);
306
- }
307
- /**
308
- * Snapshot the machine's identity. Called once per `ensureTlsCert`; the
309
- * result is both the SAN list for a new leaf and the yardstick the
310
- * existing leaf is measured against.
311
- */
312
- function collectCertIdentity(options) {
313
- const dnsNames = /* @__PURE__ */ new Set();
314
- const ipAddresses = new Set(["127.0.0.1", "::1"]);
315
- const requiredDnsNames = /* @__PURE__ */ new Set();
316
- const requiredIpAddresses = new Set(["127.0.0.1"]);
317
- for (const name of [
318
- "localhost",
319
- options.commonName,
320
- node_os.hostname()
321
- ]) {
322
- addSan(dnsNames, ipAddresses, name);
323
- addSan(requiredDnsNames, requiredIpAddresses, name);
324
- }
325
- for (const [ifaceName, addrs] of Object.entries(node_os.networkInterfaces())) for (const addr of addrs ?? []) {
326
- if (addr.internal) continue;
327
- if (isLinkLocal(addr.address)) continue;
328
- ipAddresses.add(addr.address);
329
- if (isVolatileInterface(ifaceName)) continue;
330
- if (isStableGateAddress(addr.address)) requiredIpAddresses.add(addr.address);
331
- }
332
- for (const san of options.extraSans) {
333
- addSan(dnsNames, ipAddresses, san);
334
- addSan(requiredDnsNames, requiredIpAddresses, san);
335
- }
336
- return {
337
- dnsNames: [...dnsNames],
338
- ipAddresses: [...ipAddresses],
339
- requiredDnsNames: [...requiredDnsNames],
340
- requiredIpAddresses: [...requiredIpAddresses]
341
- };
342
- }
343
- //#endregion
344
- //#region src/tls/cert-openssl.ts
345
- /**
346
- * The openssl invocations. Isolated so `cert-manager.ts` reads as policy
347
- * and this file reads as mechanics.
348
- */
349
- var execFileAsync = (0, node_util.promisify)(node_child_process.execFile);
350
- /** Subject of the local root. Distinct from the leaf's CN by construction. */
351
- var CA_COMMON_NAME = "CamStack Local CA";
352
- function sanValue(identity) {
353
- const parts = [];
354
- for (const dns of identity.dnsNames) parts.push(`DNS:${dns}`);
355
- for (const ip of identity.ipAddresses) if ((0, node_net.isIP)(ip) !== 0) parts.push(`IP:${ip}`);
356
- return parts.join(",");
357
- }
358
- /**
359
- * A local root: 10 years, `pathlen:0` so it can sign leaves and nothing
360
- * else, and **no EKU** — Apple rejects an anchor asserting
361
- * `anyExtendedKeyUsage`, and a root with no EKU constrains nothing.
362
- */
363
- async function generateCa(tmpDir) {
364
- const caCertPath = (0, node_path.join)(tmpDir, "ca.crt");
365
- const caKeyPath = (0, node_path.join)(tmpDir, "ca.key");
366
- await execFileAsync("openssl", [
367
- "req",
368
- "-x509",
369
- "-newkey",
370
- "rsa:2048",
371
- "-nodes",
372
- "-sha256",
373
- "-days",
374
- String(CA_VALIDITY_DAYS),
375
- "-keyout",
376
- caKeyPath,
377
- "-out",
378
- caCertPath,
379
- "-subj",
380
- `/CN=${CA_COMMON_NAME}`,
381
- "-addext",
382
- "basicConstraints=critical,CA:TRUE,pathlen:0",
383
- "-addext",
384
- "keyUsage=critical,keyCertSign,cRLSign",
385
- "-addext",
386
- "subjectKeyIdentifier=hash"
387
- ]);
388
- await (0, node_fs_promises.chmod)(caKeyPath, 384);
389
- return {
390
- caCertPath,
391
- caKeyPath
392
- };
393
- }
394
- /**
395
- * A server leaf signed by the local root: `CA:FALSE`, and
396
- * `extendedKeyUsage=serverAuth` — not `anyExtendedKeyUsage`, which
397
- * Apple's `testAppTrustRoot_AnyEKU_AfterJul2019` asserts must fail.
398
- */
399
- async function issueLeaf(tmpDir, ca, identity, commonName, validDays) {
400
- const keyPath = (0, node_path.join)(tmpDir, "leaf.key");
401
- const csrPath = (0, node_path.join)(tmpDir, "leaf.csr");
402
- const certPath = (0, node_path.join)(tmpDir, "leaf.crt");
403
- const extPath = (0, node_path.join)(tmpDir, "leaf.ext");
404
- await (0, node_fs_promises.writeFile)(extPath, [
405
- "basicConstraints=critical,CA:FALSE",
406
- "keyUsage=critical,digitalSignature,keyEncipherment",
407
- "extendedKeyUsage=serverAuth",
408
- "subjectKeyIdentifier=hash",
409
- "authorityKeyIdentifier=keyid,issuer",
410
- `subjectAltName=${sanValue(identity)}`,
411
- ""
412
- ].join("\n"), "utf-8");
413
- await execFileAsync("openssl", [
414
- "req",
415
- "-new",
416
- "-newkey",
417
- "rsa:2048",
418
- "-nodes",
419
- "-sha256",
420
- "-keyout",
421
- keyPath,
422
- "-out",
423
- csrPath,
424
- "-subj",
425
- `/CN=${commonName}`
426
- ]);
427
- await execFileAsync("openssl", [
428
- "x509",
429
- "-req",
430
- "-in",
431
- csrPath,
432
- "-CA",
433
- ca.caCertPath,
434
- "-CAkey",
435
- ca.caKeyPath,
436
- "-set_serial",
437
- `0x00${(0, node_crypto.randomBytes)(16).toString("hex")}`,
438
- "-days",
439
- String(validDays),
440
- "-sha256",
441
- "-extfile",
442
- extPath,
443
- "-out",
444
- certPath
445
- ]);
446
- await (0, node_fs_promises.chmod)(keyPath, 384);
447
- await (0, node_fs_promises.rm)(csrPath, { force: true });
448
- await (0, node_fs_promises.rm)(extPath, { force: true });
449
- return {
450
- certPath,
451
- keyPath
452
- };
453
- }
454
- //#endregion
455
- //#region src/tls/cert-manager.ts
456
- var DEFAULT_COMMON_NAME = "camstack.local";
457
- /**
458
- * Ensure the hub has a usable TLS identity under `<dataDir>/tls`, made of
459
- * a long-lived local **root** (`camstack-ca.crt`) and a short **leaf**
460
- * chained to it (`camstack.crt` = leaf + root, `camstack.key`).
461
- *
462
- * ## The regeneration rule (D227)
463
- *
464
- * Reissue happens when, and only when, the material on disk **cannot do
465
- * its job**:
466
- *
467
- * - it is absent, unreadable, or the key does not match the certificate;
468
- * - the root is absent (every pre-2026-08 install: one self-signed
469
- * `CA:TRUE` leaf and no root) or is inside its renewal window;
470
- * - the leaf is inside its 30-day renewal window;
471
- * - the leaf lacks `extendedKeyUsage = serverAuth` — Apple's trustd
472
- * rejects an app-supplied anchor without it;
473
- * - the leaf is not actually issued by the root on disk;
474
- * - the leaf's validity span exceeds {@link MAX_LEAF_VALIDITY_DAYS};
475
- * - **a name or address the hub must answer for is missing from the SAN
476
- * list** — a DHCP move, a new interface or a new hostname.
477
- *
478
- * And explicitly NOT when: the certificate names an address the machine
479
- * no longer has (a superset still works — the check is one-directional),
480
- * a volatile interface (docker/VPN/mesh) or a global-scope IPv6 address
481
- * changed (SLAAC privacy addresses rotate daily), or a new build shipped.
482
- *
483
- * Every trigger is derived from the files plus the machine, so once a
484
- * reissue clears them it clears them for good: the next boot returns
485
- * `generated: false`. A certificate that moved on every boot would
486
- * invalidate every trust an operator granted by hand — worse than the
487
- * bugs this rule fixes.
488
- *
489
- * **The root is rotated only for root-level reasons.** SAN drift rotates
490
- * the leaf alone, so trust granted to `camstack-ca.crt` survives every
491
- * address change the hub will ever see.
492
- */
493
- async function ensureTlsCert(dataDir, options) {
494
- const tlsDir = (0, node_path.join)(dataDir, "tls");
495
- const paths = {
496
- certPath: (0, node_path.join)(tlsDir, "camstack.crt"),
497
- keyPath: (0, node_path.join)(tlsDir, "camstack.key"),
498
- caCertPath: (0, node_path.join)(tlsDir, "camstack-ca.crt"),
499
- caKeyPath: (0, node_path.join)(tlsDir, "camstack-ca.key")
500
- };
501
- const commonName = options?.commonName ?? DEFAULT_COMMON_NAME;
502
- const validDays = Math.min(options?.validDays ?? 397, 397);
503
- const identity = collectCertIdentity({
504
- commonName,
505
- extraSans: options?.extraSans ?? []
506
- });
507
- const reason = decideRegeneration(paths, identity);
508
- if (reason === null) return describe(paths, false, null, false, null, null);
509
- const previousFingerprint = readFingerprint(paths.certPath);
510
- const caRotated = reasonRequiresNewCa(reason);
511
- (0, node_fs.mkdirSync)(tlsDir, { recursive: true });
512
- try {
513
- await regenerate(paths, identity, commonName, validDays, caRotated);
514
- } catch (err) {
515
- const message = err instanceof Error ? err.message : String(err);
516
- if (!canKeepServing(paths)) throw err;
517
- return describe(paths, false, null, false, null, message);
518
- }
519
- return describe(paths, true, reason, caRotated, previousFingerprint, null);
520
- }
521
- /**
522
- * Operator "Regenerate": issue a new leaf under the existing local CA.
523
- * Never rotates the root — trust granted to `camstack-ca.crt` survives.
524
- */
525
- async function reissueTlsLeaf(dataDir, options) {
526
- const tlsDir = (0, node_path.join)(dataDir, "tls");
527
- const paths = {
528
- certPath: (0, node_path.join)(tlsDir, "camstack.crt"),
529
- keyPath: (0, node_path.join)(tlsDir, "camstack.key"),
530
- caCertPath: (0, node_path.join)(tlsDir, "camstack-ca.crt"),
531
- caKeyPath: (0, node_path.join)(tlsDir, "camstack-ca.key")
532
- };
533
- if (!(0, node_fs.existsSync)(paths.caCertPath) || !(0, node_fs.existsSync)(paths.caKeyPath)) return ensureTlsCert(dataDir, options);
534
- const commonName = options?.commonName ?? DEFAULT_COMMON_NAME;
535
- const validDays = Math.min(options?.validDays ?? 397, 397);
536
- const identity = collectCertIdentity({
537
- commonName,
538
- extraSans: options?.extraSans ?? []
539
- });
540
- const previousFingerprint = readFingerprint(paths.certPath);
541
- (0, node_fs.mkdirSync)(tlsDir, { recursive: true });
542
- await regenerate(paths, identity, commonName, validDays, false);
543
- return describe(paths, true, "san-coverage-gap", false, previousFingerprint, null);
544
- }
545
- /** True when the material already on disk can still terminate TLS. */
546
- function canKeepServing(paths) {
547
- if (!(0, node_fs.existsSync)(paths.certPath) || !(0, node_fs.existsSync)(paths.keyPath)) return false;
548
- if (!(0, node_fs.existsSync)(paths.caCertPath)) return false;
549
- return readFingerprint(paths.certPath) !== null;
550
- }
551
- /** Fingerprint of whatever is on disk, or `null` if there is nothing readable. */
552
- function readFingerprint(certPath) {
553
- try {
554
- return new node_crypto.X509Certificate((0, node_fs.readFileSync)(certPath)).fingerprint256;
555
- } catch {
556
- return null;
557
- }
558
- }
559
- function decideRegeneration(paths, identity) {
560
- if (!(0, node_fs.existsSync)(paths.certPath) || !(0, node_fs.existsSync)(paths.keyPath)) return "missing";
561
- if (!(0, node_fs.existsSync)(paths.caCertPath) || !(0, node_fs.existsSync)(paths.caKeyPath)) return "no-local-ca";
562
- try {
563
- return evaluateExistingCert({
564
- chainPem: (0, node_fs.readFileSync)(paths.certPath, "utf-8"),
565
- keyPem: (0, node_fs.readFileSync)(paths.keyPath, "utf-8"),
566
- caPem: (0, node_fs.readFileSync)(paths.caCertPath, "utf-8"),
567
- identity,
568
- now: /* @__PURE__ */ new Date()
569
- });
570
- } catch {
571
- return "unreadable";
572
- }
573
- }
574
- /**
575
- * Build every file in a scratch directory, then move them into place.
576
- * A crash mid-way leaves the previous material untouched; a crash
577
- * between the moves leaves a key/certificate mismatch, which the
578
- * evaluator reports as `key-mismatch` and heals on the next boot.
579
- */
580
- async function regenerate(paths, identity, commonName, validDays, newCa) {
581
- const scratch = await (0, node_fs_promises.mkdtemp)((0, node_path.join)((0, node_os.tmpdir)(), "camstack-tls-"));
582
- try {
583
- const ca = newCa ? await generateCa(scratch) : {
584
- caCertPath: paths.caCertPath,
585
- caKeyPath: paths.caKeyPath
586
- };
587
- const leaf = await issueLeaf(scratch, ca, identity, commonName, validDays);
588
- const chainPath = (0, node_path.join)(scratch, "chain.crt");
589
- await (0, node_fs_promises.writeFile)(chainPath, `${(0, node_fs.readFileSync)(leaf.certPath, "utf-8").trimEnd()}\n${(0, node_fs.readFileSync)(ca.caCertPath, "utf-8").trimEnd()}\n`, "utf-8");
590
- if (newCa) {
591
- await (0, node_fs_promises.copyFile)(ca.caCertPath, `${paths.caCertPath}.new`);
592
- await (0, node_fs_promises.copyFile)(ca.caKeyPath, `${paths.caKeyPath}.new`);
593
- (0, node_fs.renameSync)(`${paths.caCertPath}.new`, paths.caCertPath);
594
- (0, node_fs.renameSync)(`${paths.caKeyPath}.new`, paths.caKeyPath);
595
- await (0, node_fs_promises.chmod)(paths.caKeyPath, 384);
596
- }
597
- await (0, node_fs_promises.copyFile)(leaf.keyPath, `${paths.keyPath}.new`);
598
- await (0, node_fs_promises.copyFile)(chainPath, `${paths.certPath}.new`);
599
- (0, node_fs.renameSync)(`${paths.keyPath}.new`, paths.keyPath);
600
- (0, node_fs.renameSync)(`${paths.certPath}.new`, paths.certPath);
601
- await (0, node_fs_promises.chmod)(paths.keyPath, 384);
602
- } finally {
603
- await (0, node_fs_promises.rm)(scratch, {
604
- recursive: true,
605
- force: true
606
- });
607
- }
608
- }
609
- function describe(paths, generated, reason, caRotated, previousFingerprintSha256, reissueError) {
610
- const leaf = new node_crypto.X509Certificate((0, node_fs.readFileSync)(paths.certPath));
611
- const ca = new node_crypto.X509Certificate((0, node_fs.readFileSync)(paths.caCertPath));
612
- const san = leaf.subjectAltName ?? "";
613
- return {
614
- ...paths,
615
- generated,
616
- reason,
617
- caRotated,
618
- fingerprintSha256: leaf.fingerprint256,
619
- previousFingerprintSha256,
620
- caFingerprintSha256: ca.fingerprint256,
621
- validTo: new Date(leaf.validTo).toISOString(),
622
- sans: san === "" ? [] : san.split(", "),
623
- reissueError
624
- };
625
- }
626
- /**
627
- * Load TLS cert+key from files. Returns Buffers suitable for Node.js TLS
628
- * options. `certPath` is a full chain, so the listener presents the local
629
- * root alongside the leaf.
630
- */
631
- function loadTlsCert(certPath, keyPath) {
632
- return {
633
- cert: (0, node_fs.readFileSync)(certPath),
634
- key: (0, node_fs.readFileSync)(keyPath)
635
- };
636
- }
637
- //#endregion
638
- //#region src/tls/uploaded-cert.ts
639
- /**
640
- * Operator-supplied material must parse, match its key, and carry
641
- * `extendedKeyUsage = serverAuth` — the same leaf defects D227 refused
642
- * to keep generating.
643
- */
644
- function validateUploadedTls(certPem, keyPem) {
645
- let leaf;
646
- try {
647
- leaf = new node_crypto.X509Certificate(certPem);
648
- } catch {
649
- return {
650
- ok: false,
651
- error: "Certificate PEM could not be parsed."
652
- };
653
- }
654
- try {
655
- if (!leaf.checkPrivateKey((0, node_crypto.createPrivateKey)(keyPem))) return {
656
- ok: false,
657
- error: "Private key does not match the certificate."
658
- };
659
- } catch {
660
- return {
661
- ok: false,
662
- error: "Private key PEM could not be parsed."
663
- };
664
- }
665
- const eku = leaf.keyUsage;
666
- if (eku === void 0 || !eku.includes("1.3.6.1.5.5.7.3.1")) return {
667
- ok: false,
668
- error: "Certificate is missing extendedKeyUsage = serverAuth."
669
- };
670
- return {
671
- ok: true,
672
- fingerprintSha256: leaf.fingerprint256,
673
- validTo: new Date(leaf.validTo).toISOString()
674
- };
675
- }
676
- //#endregion
677
- //#region src/tls/tls-status.ts
678
- function tlsDir(dataDir) {
679
- return (0, node_path.join)(dataDir, "tls");
680
- }
681
- function readTlsMode(dataDir) {
682
- const marker = (0, node_path.join)(tlsDir(dataDir), "mode");
683
- if ((0, node_fs.existsSync)(marker)) {
684
- const raw = (0, node_fs.readFileSync)(marker, "utf-8").trim();
685
- if (raw === "uploaded") return "uploaded";
686
- if (raw === "disabled") return "disabled";
687
- }
688
- return (0, node_fs.existsSync)((0, node_path.join)(tlsDir(dataDir), "camstack.crt")) ? "generated" : "disabled";
689
- }
690
- function writeTlsMode(dataDir, mode) {
691
- (0, node_fs.mkdirSync)(tlsDir(dataDir), { recursive: true });
692
- (0, node_fs.writeFileSync)((0, node_path.join)(tlsDir(dataDir), "mode"), `${mode}\n`, "utf-8");
693
- }
694
- function readExtraSans(dataDir) {
695
- const path = (0, node_path.join)(tlsDir(dataDir), "extra-sans.json");
696
- if (!(0, node_fs.existsSync)(path)) return [];
697
- try {
698
- const parsed = JSON.parse((0, node_fs.readFileSync)(path, "utf-8"));
699
- if (!Array.isArray(parsed)) return [];
700
- return parsed.filter((entry) => typeof entry === "string" && entry.length > 0);
701
- } catch {
702
- return [];
703
- }
704
- }
705
- function writeExtraSans(dataDir, sans) {
706
- (0, node_fs.mkdirSync)(tlsDir(dataDir), { recursive: true });
707
- (0, node_fs.writeFileSync)((0, node_path.join)(tlsDir(dataDir), "extra-sans.json"), `${JSON.stringify(sans)}\n`, "utf-8");
708
- }
709
- function readTlsAccessStatus(dataDir, restartRequired = false) {
710
- const dir = tlsDir(dataDir);
711
- const certPath = (0, node_path.join)(dir, "camstack.crt");
712
- const caPath = (0, node_path.join)(dir, "camstack-ca.crt");
713
- const mode = readTlsMode(dataDir);
714
- if (!(0, node_fs.existsSync)(certPath)) return {
715
- mode: "disabled",
716
- leafFingerprintSha256: null,
717
- caFingerprintSha256: null,
718
- validTo: null,
719
- sans: [],
720
- caCertPem: (0, node_fs.existsSync)(caPath) ? (0, node_fs.readFileSync)(caPath, "utf-8") : null,
721
- reissueError: null,
722
- restartRequired
723
- };
724
- try {
725
- const leaf = new node_crypto.X509Certificate((0, node_fs.readFileSync)(certPath));
726
- const caPem = (0, node_fs.existsSync)(caPath) ? (0, node_fs.readFileSync)(caPath, "utf-8") : null;
727
- const ca = caPem !== null ? new node_crypto.X509Certificate(caPem) : null;
728
- const san = leaf.subjectAltName ?? "";
729
- return {
730
- mode,
731
- leafFingerprintSha256: leaf.fingerprint256,
732
- caFingerprintSha256: ca?.fingerprint256 ?? null,
733
- validTo: new Date(leaf.validTo).toISOString(),
734
- sans: san === "" ? [] : san.split(", "),
735
- caCertPem: caPem,
736
- reissueError: null,
737
- restartRequired
738
- };
739
- } catch {
740
- return {
741
- mode,
742
- leafFingerprintSha256: null,
743
- caFingerprintSha256: null,
744
- validTo: null,
745
- sans: [],
746
- caCertPem: null,
747
- reissueError: "Certificate on disk could not be parsed.",
748
- restartRequired
749
- };
750
- }
751
- }
752
- //#endregion
753
- Object.defineProperty(exports, "CA_COMMON_NAME", {
754
- enumerable: true,
755
- get: function() {
756
- return CA_COMMON_NAME;
757
- }
758
- });
759
- Object.defineProperty(exports, "CA_VALIDITY_DAYS", {
760
- enumerable: true,
761
- get: function() {
762
- return CA_VALIDITY_DAYS;
763
- }
764
- });
765
- Object.defineProperty(exports, "DEFAULT_LAN_HTTP_PORT", {
766
- enumerable: true,
767
- get: function() {
768
- return DEFAULT_LAN_HTTP_PORT;
769
- }
770
- });
771
- Object.defineProperty(exports, "LEAF_RENEWAL_WINDOW_DAYS", {
772
- enumerable: true,
773
- get: function() {
774
- return LEAF_RENEWAL_WINDOW_DAYS;
775
- }
776
- });
777
- Object.defineProperty(exports, "MAX_LEAF_VALIDITY_DAYS", {
778
- enumerable: true,
779
- get: function() {
780
- return MAX_LEAF_VALIDITY_DAYS;
781
- }
782
- });
783
- Object.defineProperty(exports, "SERVER_AUTH_OID", {
784
- enumerable: true,
785
- get: function() {
786
- return SERVER_AUTH_OID;
787
- }
788
- });
789
- Object.defineProperty(exports, "allFamiliesListenHost", {
790
- enumerable: true,
791
- get: function() {
792
- return allFamiliesListenHost;
793
- }
794
- });
795
- Object.defineProperty(exports, "applyLanHttp", {
796
- enumerable: true,
797
- get: function() {
798
- return applyLanHttp;
799
- }
800
- });
801
- Object.defineProperty(exports, "bindPendingLanHttp", {
802
- enumerable: true,
803
- get: function() {
804
- return bindPendingLanHttp;
805
- }
806
- });
807
- Object.defineProperty(exports, "closeLanHttp", {
808
- enumerable: true,
809
- get: function() {
810
- return closeLanHttp;
811
- }
812
- });
813
- Object.defineProperty(exports, "collectCertIdentity", {
814
- enumerable: true,
815
- get: function() {
816
- return collectCertIdentity;
817
- }
818
- });
819
- Object.defineProperty(exports, "ensureTlsCert", {
820
- enumerable: true,
821
- get: function() {
822
- return ensureTlsCert;
823
- }
824
- });
825
- Object.defineProperty(exports, "evaluateExistingCert", {
826
- enumerable: true,
827
- get: function() {
828
- return evaluateExistingCert;
829
- }
830
- });
831
- Object.defineProperty(exports, "loadTlsCert", {
832
- enumerable: true,
833
- get: function() {
834
- return loadTlsCert;
835
- }
836
- });
837
- Object.defineProperty(exports, "readExtraSans", {
838
- enumerable: true,
839
- get: function() {
840
- return readExtraSans;
841
- }
842
- });
843
- Object.defineProperty(exports, "readLanHttpState", {
844
- enumerable: true,
845
- get: function() {
846
- return readLanHttpState;
847
- }
848
- });
849
- Object.defineProperty(exports, "readTlsAccessStatus", {
850
- enumerable: true,
851
- get: function() {
852
- return readTlsAccessStatus;
853
- }
854
- });
855
- Object.defineProperty(exports, "readTlsMode", {
856
- enumerable: true,
857
- get: function() {
858
- return readTlsMode;
859
- }
860
- });
861
- Object.defineProperty(exports, "registerLanHttpHandler", {
862
- enumerable: true,
863
- get: function() {
864
- return registerLanHttpHandler;
865
- }
866
- });
867
- Object.defineProperty(exports, "reissueTlsLeaf", {
868
- enumerable: true,
869
- get: function() {
870
- return reissueTlsLeaf;
871
- }
872
- });
873
- Object.defineProperty(exports, "validateUploadedTls", {
874
- enumerable: true,
875
- get: function() {
876
- return validateUploadedTls;
877
- }
878
- });
879
- Object.defineProperty(exports, "writeExtraSans", {
880
- enumerable: true,
881
- get: function() {
882
- return writeExtraSans;
883
- }
884
- });
885
- Object.defineProperty(exports, "writeTlsMode", {
886
- enumerable: true,
887
- get: function() {
888
- return writeTlsMode;
889
- }
890
- });