@camstack/system 1.2.428 → 1.2.429

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 (100) hide show
  1. package/dist/agent-https-names-CpUSuP4w.js +120 -0
  2. package/dist/agent-https-names-uetr4OQl.mjs +91 -0
  3. package/dist/auth/auth-audit-log.d.ts +1 -1
  4. package/dist/auth/device-ca-store.d.ts +57 -0
  5. package/dist/auth/device-ca.d.ts +75 -0
  6. package/dist/auth/device-credential-manager.d.ts +154 -0
  7. package/dist/auth/login-session-manager.d.ts +14 -2
  8. package/dist/auth/user-management-internal.d.ts +10 -1
  9. package/dist/builtins/addon-pages-aggregator/addon-pages-aggregator.addon.js +1 -1
  10. package/dist/builtins/addon-pages-aggregator/addon-pages-aggregator.addon.mjs +1 -1
  11. package/dist/builtins/addon-widgets-aggregator/addon-widgets-aggregator.addon.js +1 -1
  12. package/dist/builtins/addon-widgets-aggregator/addon-widgets-aggregator.addon.mjs +1 -1
  13. package/dist/builtins/alerts/alerts.addon.js +1 -1
  14. package/dist/builtins/alerts/alerts.addon.mjs +1 -1
  15. package/dist/builtins/autotrack/index.js +1 -1
  16. package/dist/builtins/autotrack/index.mjs +1 -1
  17. package/dist/builtins/backup-orchestrator/backup-orchestrator.addon.js +2 -2
  18. package/dist/builtins/backup-orchestrator/backup-orchestrator.addon.mjs +2 -2
  19. package/dist/builtins/calendars/calendars.addon.js +1 -1
  20. package/dist/builtins/calendars/calendars.addon.mjs +1 -1
  21. package/dist/builtins/camera-grid/index.js +1 -1
  22. package/dist/builtins/camera-grid/index.mjs +1 -1
  23. package/dist/builtins/composer/composer.addon.js +2 -2
  24. package/dist/builtins/composer/composer.addon.mjs +2 -2
  25. package/dist/builtins/console-logging/index.js +1 -1
  26. package/dist/builtins/console-logging/index.mjs +1 -1
  27. package/dist/builtins/core-blocks/core-blocks.addon.js +2 -2
  28. package/dist/builtins/core-blocks/core-blocks.addon.mjs +2 -2
  29. package/dist/builtins/device-manager/device-manager.addon.js +2 -2
  30. package/dist/builtins/device-manager/device-manager.addon.mjs +2 -2
  31. package/dist/builtins/hub-forwarder/index.js +1 -1
  32. package/dist/builtins/hub-forwarder/index.mjs +1 -1
  33. package/dist/builtins/keychain/index.js +10 -6
  34. package/dist/builtins/keychain/index.mjs +8 -4
  35. package/dist/builtins/liveness-monitor/liveness-monitor.addon.js +1 -1
  36. package/dist/builtins/liveness-monitor/liveness-monitor.addon.mjs +1 -1
  37. package/dist/builtins/local-auth/local-auth.addon.d.ts +6 -0
  38. package/dist/builtins/local-auth/local-auth.addon.js +955 -47
  39. package/dist/builtins/local-auth/local-auth.addon.mjs +946 -41
  40. package/dist/builtins/local-network/local-network.addon.js +2 -2
  41. package/dist/builtins/local-network/local-network.addon.mjs +2 -2
  42. package/dist/builtins/loki-logging/index.js +1 -1
  43. package/dist/builtins/loki-logging/index.mjs +1 -1
  44. package/dist/builtins/native-metrics/native-metrics.addon.js +1 -1
  45. package/dist/builtins/native-metrics/native-metrics.addon.mjs +1 -1
  46. package/dist/builtins/platform-probe/index.js +1 -1
  47. package/dist/builtins/platform-probe/index.mjs +1 -1
  48. package/dist/builtins/remote-access-orchestrator/remote-access-orchestrator.addon.js +1 -1
  49. package/dist/builtins/remote-access-orchestrator/remote-access-orchestrator.addon.mjs +1 -1
  50. package/dist/builtins/snapshot/index.js +1 -1
  51. package/dist/builtins/snapshot/index.mjs +1 -1
  52. package/dist/builtins/sqlite-storage/filesystem-storage.addon.js +1 -1
  53. package/dist/builtins/sqlite-storage/filesystem-storage.addon.mjs +1 -1
  54. package/dist/builtins/sqlite-storage/index.js +1 -1
  55. package/dist/builtins/sqlite-storage/index.mjs +1 -1
  56. package/dist/builtins/sqlite-storage/sqlite-settings.addon.js +3 -3
  57. package/dist/builtins/sqlite-storage/sqlite-settings.addon.mjs +3 -3
  58. package/dist/builtins/storage-orchestrator/storage-orchestrator.addon.js +1 -1
  59. package/dist/builtins/storage-orchestrator/storage-orchestrator.addon.mjs +1 -1
  60. package/dist/builtins/system-config/system-config.addon.d.ts +18 -0
  61. package/dist/builtins/system-config/system-config.addon.js +834 -18
  62. package/dist/builtins/system-config/system-config.addon.mjs +819 -19
  63. package/dist/builtins/winston-logging/index.js +1 -1
  64. package/dist/builtins/winston-logging/index.mjs +1 -1
  65. package/dist/{composition-sources-DvCGZEUI.js → composition-sources-C2VNX6tF.js} +1 -1
  66. package/dist/{composition-sources-BQNxXQZG.mjs → composition-sources-SYtYuAgh.mjs} +1 -1
  67. package/dist/{constants-CR4If_lP.mjs → constants-CKuWoWvc.mjs} +1 -1
  68. package/dist/{constants-CCSJGnjs.js → constants-RI-hGP5S.js} +1 -1
  69. package/dist/device-ca-BWfdDB3k.js +462 -0
  70. package/dist/device-ca-R7DKKZL6.mjs +355 -0
  71. package/dist/{dist-BQG3QFg7.js → dist-Bgy_AmwX.js} +554 -0
  72. package/dist/{dist-DHSJV4Qc.mjs → dist-CSSfTM18.mjs} +507 -1
  73. package/dist/file-host-worker.js +1 -1
  74. package/dist/file-host-worker.mjs +1 -1
  75. package/dist/index.d.ts +4 -1
  76. package/dist/index.js +59 -12
  77. package/dist/index.mjs +10 -9
  78. package/dist/kernel/index.d.ts +2 -2
  79. package/dist/kernel/moleculer/mesh-mtls/index.d.ts +4 -1
  80. package/dist/kernel/moleculer/mesh-mtls/mesh-certificate-registry.d.ts +91 -0
  81. package/dist/kernel/moleculer/mesh-mtls/mesh-enroll.d.ts +4 -0
  82. package/dist/kernel/moleculer/mesh-mtls/mesh-peer-transport.d.ts +23 -0
  83. package/dist/kernel/moleculer/mesh-mtls/mesh-renewal.d.ts +126 -0
  84. package/dist/kernel/moleculer/mesh-mtls/mesh-tls-gate.d.ts +40 -0
  85. package/dist/kernel/moleculer/mesh-mtls/tls-tcp-transporter.d.ts +15 -0
  86. package/dist/{restore-archive-DH1Pxq_1.mjs → restore-archive-AIwMHO5_.mjs} +1 -1
  87. package/dist/{restore-archive-Dccm8lkA.js → restore-archive-BYUEMsog.js} +1 -1
  88. package/dist/{retired-settings-keys-BfGaSeQI.js → retired-settings-keys-BTPkRLTS.js} +1 -1
  89. package/dist/{retired-settings-keys-CPz7zpza.mjs → retired-settings-keys-H0rYXT27.mjs} +1 -1
  90. package/dist/tls/agent-https-names.d.ts +27 -0
  91. package/dist/tls/agent-https.d.ts +111 -0
  92. package/dist/tls/cert-issue.d.ts +7 -0
  93. package/dist/tls/index.d.ts +3 -0
  94. package/dist/{tls-BgImdxCO.js → tls-C5IYg3V5.js} +406 -0
  95. package/dist/{tls-CaOLZ09W.mjs → tls-CpcGkeJV.mjs} +314 -4
  96. package/dist/{wal-maintenance-DItkuOvu.js → wal-maintenance-BSGsxKW7.js} +0 -0
  97. package/dist/{wal-maintenance-Drqhcjit.mjs → wal-maintenance-DW0FfGV7.mjs} +0 -0
  98. package/package.json +4 -1
  99. package/dist/jwt-signing-key-BZm055IP.js +0 -174
  100. package/dist/jwt-signing-key-DL-qk1SM.mjs +0 -127
@@ -1,11 +1,11 @@
1
- import { a as KeyUsagesExtension, d as X509Certificate$1, f as X509CertificateGenerator, i as KeyUsageFlags, l as SubjectAlternativeNameExtension, n as BasicConstraintsExtension, o as PemConverter, p as cryptoProvider, r as ExtendedKeyUsageExtension, t as AuthorityKeyIdentifierExtension, u as SubjectKeyIdentifierExtension } from "./x509.es-CAjSsrkk.mjs";
2
- import { X509Certificate, createPrivateKey, randomBytes, webcrypto } from "node:crypto";
1
+ import { a as KeyUsagesExtension, c as Pkcs10CertificateRequestGenerator, d as X509Certificate$1, f as X509CertificateGenerator, i as KeyUsageFlags, l as SubjectAlternativeNameExtension, n as BasicConstraintsExtension, o as PemConverter, p as cryptoProvider, r as ExtendedKeyUsageExtension, s as Pkcs10CertificateRequest, t as AuthorityKeyIdentifierExtension, u as SubjectKeyIdentifierExtension } from "./x509.es-CAjSsrkk.mjs";
2
+ import { X509Certificate, createHash, createPrivateKey, randomBytes, webcrypto } from "node:crypto";
3
3
  import { writeOwnerOnlyFileSync } from "@camstack/types/node";
4
4
  import { isIP } from "node:net";
5
5
  import { mkdir, writeFile } from "node:fs/promises";
6
6
  import { join } from "node:path";
7
7
  import * as os from "node:os";
8
- import { existsSync, mkdirSync, readFileSync, renameSync, rmSync, writeFileSync } from "node:fs";
8
+ import { chmodSync, existsSync, mkdirSync, readFileSync, renameSync, rmSync, writeFileSync } from "node:fs";
9
9
  import { createServer as createServer$1 } from "node:http";
10
10
  //#region src/http/lan-http-bind.ts
11
11
  /**
@@ -457,6 +457,35 @@ async function issueLeaf(ca, identity, commonName, validDays, now) {
457
457
  keyPem: await exportKeyPem(keys.privateKey)
458
458
  };
459
459
  }
460
+ /**
461
+ * A server leaf for a key pair the CALLER holds (D856): an agent sends a CSR, the
462
+ * hub signs its public key under the local root. Same extensions as
463
+ * {@link issueLeaf} - `CA:FALSE`, serverAuth only - so a device that trusts the
464
+ * hub root trusts the agent. The private key never exists on the hub.
465
+ */
466
+ async function issueLeafFromCsr(ca, csrPem, identity, commonName, validDays, now) {
467
+ const csr = new Pkcs10CertificateRequest(csrPem);
468
+ if (!await csr.verify(webcrypto)) throw new Error("CSR signature is invalid");
469
+ const extensions = [
470
+ new BasicConstraintsExtension(false, void 0, true),
471
+ new KeyUsagesExtension(KeyUsageFlags.digitalSignature | KeyUsageFlags.keyEncipherment, true),
472
+ new ExtendedKeyUsageExtension([SERVER_AUTH_OID]),
473
+ await SubjectKeyIdentifierExtension.create(csr.publicKey),
474
+ await AuthorityKeyIdentifierExtension.create(ca.cert),
475
+ new SubjectAlternativeNameExtension(subjectAltNames(identity))
476
+ ];
477
+ return toCertPem(await X509CertificateGenerator.create({
478
+ serialNumber: freshSerial(),
479
+ subject: `CN=${commonName}`,
480
+ issuer: ca.cert.subject,
481
+ notBefore: now,
482
+ notAfter: new Date(now.getTime() + validDays * MS_PER_DAY),
483
+ publicKey: csr.publicKey,
484
+ signingKey: ca.privateKey,
485
+ signingAlgorithm: SIGNING_ALGORITHM,
486
+ extensions
487
+ }));
488
+ }
460
489
  //#endregion
461
490
  //#region src/tls/cert-manager.ts
462
491
  var DEFAULT_COMMON_NAME = "camstack.local";
@@ -939,4 +968,285 @@ function applyCertificateToListener(dataDir) {
939
968
  return reloadHttpsListener(loadTlsCert(certPath, keyPath));
940
969
  }
941
970
  //#endregion
942
- export { allFamiliesListenHost as A, collectCertIdentity as C, SERVER_AUTH_OID as D, MAX_LEAF_VALIDITY_DAYS as E, registerLanHttpHandler as F, bindPendingLanHttp as M, closeLanHttp as N, evaluateExistingCert as O, readLanHttpState as P, CA_COMMON_NAME as S, LEAF_RENEWAL_WINDOW_DAYS as T, writeExtraSans as _, validateUploadedTls as a, loadTlsCert as b, registerHttpsListener as c, prepareBootTls as d, UPLOADED_CA_FILE as f, splitPemCertificates as g, readTlsMode as h, storeUploadedCertificate as i, applyLanHttp as j, DEFAULT_LAN_HTTP_PORT as k, reloadHttpsListener as l, readTlsAccessStatus as m, regenerateCertificate as n, clearHttpsListener as o, readExtraSans as p, revertToGeneratedCertificate as r, leafFingerprintOf as s, applyCertificateToListener as t, servedHttpsLeafFingerprint as u, writeTlsMode as v, CA_VALIDITY_DAYS as w, reissueTlsLeaf as x, ensureTlsCert as y };
971
+ //#region src/tls/agent-https.ts
972
+ /**
973
+ * HTTPS for agents (D856): a leaf from the hub's LOCAL CA, so a device that
974
+ * trusts the hub root trusts the agents too.
975
+ *
976
+ * The agent generates its key and a CSR; the hub signs only the PUBLIC key under
977
+ * the local CA and returns the certificate - no private key ever crosses the mesh.
978
+ * The agent keeps ONE file, `tls/agent-https.json` (0600: key + leaf + the pinned CA),
979
+ * written atomically (temp file + rename) so a crash can never leave a key that does
980
+ * not match its certificate. It persists no database.
981
+ *
982
+ * What the hub is willing to certify (the smaller safe option). The local CA is trusted
983
+ * by every device that trusts the hub, so a compromised agent must not obtain a name it
984
+ * does not own. The hub signs ONLY what it knows by itself:
985
+ * - the agent's node id (unique in the cluster), when it is a local-only name;
986
+ * - the remote address of the agent's LIVE mesh socket as the hub sees it, when it is
987
+ * a private address - never an address the agent reports.
988
+ * Anything the agent reports (host names, interface addresses) is DROPPED and logged:
989
+ * extra self-reported names would need an explicit operator approval flow, which does
990
+ * not exist, so they are not certified. A name the hub's own certificate holds, or one
991
+ * already certified to another node, is never signed (a NAT'd pair of agents would
992
+ * otherwise share an address).
993
+ */
994
+ cryptoProvider.set(webcrypto);
995
+ function agentHttpsPaths(dataDir) {
996
+ const dir = join(dataDir, "tls");
997
+ return {
998
+ dir,
999
+ bundlePath: join(dir, "agent-https.json"),
1000
+ certPath: join(dir, "agent.crt"),
1001
+ pendingKeyPath: join(dir, "agent.key.pending")
1002
+ };
1003
+ }
1004
+ /**
1005
+ * The material on disk, or `null` while no hub-issued leaf is installed. A bundle that
1006
+ * cannot be parsed is reported through `onCorrupt` and treated as absent: a bad file must
1007
+ * never take the agent API down.
1008
+ */
1009
+ function loadAgentHttps(dataDir, onCorrupt) {
1010
+ const { bundlePath } = agentHttpsPaths(dataDir);
1011
+ if (!existsSync(bundlePath)) return null;
1012
+ try {
1013
+ const parsed = JSON.parse(readFileSync(bundlePath, "utf8"));
1014
+ const field = (name) => {
1015
+ const v = parsed !== null && typeof parsed === "object" ? Reflect.get(parsed, name) : null;
1016
+ return typeof v === "string" && v.length > 0 ? v : null;
1017
+ };
1018
+ const keyPem = field("keyPem");
1019
+ const certPem = field("certPem");
1020
+ const caCertPem = field("caCertPem");
1021
+ if (keyPem === null || certPem === null || caCertPem === null) {
1022
+ onCorrupt?.("the bundle is missing a field");
1023
+ return null;
1024
+ }
1025
+ if (!new X509Certificate(certPem).checkPrivateKey(createPrivateKey(keyPem))) {
1026
+ onCorrupt?.("the certificate does not match the key");
1027
+ return null;
1028
+ }
1029
+ return {
1030
+ keyPem,
1031
+ certPem,
1032
+ caCertPem
1033
+ };
1034
+ } catch (error) {
1035
+ onCorrupt?.(error instanceof Error ? error.message : String(error));
1036
+ return null;
1037
+ }
1038
+ }
1039
+ function collectAgentNames(nodeId, hostFacts) {
1040
+ const identity = collectCertIdentity({
1041
+ commonName: nodeId,
1042
+ extraSans: [],
1043
+ hostFacts
1044
+ });
1045
+ return {
1046
+ hostnames: identity.dnsNames,
1047
+ addresses: identity.ipAddresses
1048
+ };
1049
+ }
1050
+ /** SHA-256 of the hub local CA this agent pinned, or `null` while none is. */
1051
+ function pinnedHttpsCaFingerprint(dataDir) {
1052
+ const material = loadAgentHttps(dataDir);
1053
+ return material === null ? null : new X509Certificate(material.caCertPem).fingerprint256;
1054
+ }
1055
+ /**
1056
+ * A fresh key pair (EC P-256, kept in `agent.key.pending`, 0600) and its CSR. The
1057
+ * key becomes `agent.key` only when the matching certificate is installed, so a
1058
+ * request that is never answered changes nothing that is being served.
1059
+ */
1060
+ async function createAgentHttpsRequest(dataDir, nodeId, names) {
1061
+ const paths = agentHttpsPaths(dataDir);
1062
+ mkdirSync(paths.dir, { recursive: true });
1063
+ const keys = await webcrypto.subtle.generateKey({
1064
+ name: "ECDSA",
1065
+ namedCurve: "P-256"
1066
+ }, true, ["sign", "verify"]);
1067
+ const csr = await Pkcs10CertificateRequestGenerator.create({
1068
+ name: `CN=${nodeId}`,
1069
+ keys,
1070
+ signingAlgorithm: {
1071
+ name: "ECDSA",
1072
+ hash: "SHA-256"
1073
+ }
1074
+ });
1075
+ const pkcs8 = await webcrypto.subtle.exportKey("pkcs8", keys.privateKey);
1076
+ writeOwnerOnlyFileSync(paths.pendingKeyPath, `${PemConverter.encode(pkcs8, "PRIVATE KEY")}\n`);
1077
+ return {
1078
+ csrPem: `${csr.toString("pem")}\n`,
1079
+ ...names
1080
+ };
1081
+ }
1082
+ var AgentHttpsInstallError = class extends Error {
1083
+ constructor(message) {
1084
+ super(message);
1085
+ this.name = "AgentHttpsInstallError";
1086
+ }
1087
+ };
1088
+ /**
1089
+ * Install the hub-signed leaf for the pending key. Refused when it is not for that key, when
1090
+ * it is not signed by the CA it came with, or - once a CA is pinned - when that CA is not the
1091
+ * one this agent already pinned (the CA that arrives with a later leaf is never trusted on its
1092
+ * own say: the first one came over the authenticated mesh, every later leaf must chain to it).
1093
+ * Key, leaf and CA are written as ONE file, atomically.
1094
+ */
1095
+ function installAgentHttps(dataDir, input) {
1096
+ const paths = agentHttpsPaths(dataDir);
1097
+ if (!existsSync(paths.pendingKeyPath)) throw new AgentHttpsInstallError("no pending key: prepareHttpsCertificate was not called");
1098
+ const keyPem = readFileSync(paths.pendingKeyPath, "utf8");
1099
+ let leaf;
1100
+ let ca;
1101
+ try {
1102
+ leaf = new X509Certificate(input.certPem);
1103
+ ca = new X509Certificate(input.caCertPem);
1104
+ } catch {
1105
+ throw new AgentHttpsInstallError("the certificate or the CA is not a valid PEM certificate");
1106
+ }
1107
+ if (!leaf.checkPrivateKey(createPrivateKey(keyPem))) throw new AgentHttpsInstallError("the certificate is not for this node's pending key");
1108
+ const pinned = loadAgentHttps(dataDir);
1109
+ if (pinned !== null && input.caRotation !== true) {
1110
+ const pinnedCa = new X509Certificate(pinned.caCertPem);
1111
+ if (pinnedCa.fingerprint256 !== ca.fingerprint256 || !leaf.verify(pinnedCa.publicKey)) throw new AgentHttpsInstallError("the leaf does not chain to the hub local CA this agent pinned at its first install (if the hub rotated its local CA, renew HTTPS from the hub - it signals the rotation)");
1112
+ }
1113
+ if (!leaf.verify(ca.publicKey)) throw new AgentHttpsInstallError("the certificate is not signed by the CA that came with it");
1114
+ if (new Date(leaf.validTo).getTime() <= Date.now()) throw new AgentHttpsInstallError("the certificate is already expired");
1115
+ mkdirSync(paths.dir, { recursive: true });
1116
+ const tmp = `${paths.bundlePath}.tmp`;
1117
+ writeOwnerOnlyFileSync(tmp, JSON.stringify({
1118
+ keyPem,
1119
+ certPem: input.certPem,
1120
+ caCertPem: input.caCertPem
1121
+ }));
1122
+ renameSync(tmp, paths.bundlePath);
1123
+ chmodSync(paths.bundlePath, 384);
1124
+ writeFileAtomic(paths.certPath, input.certPem.endsWith("\n") ? input.certPem : `${input.certPem}\n`);
1125
+ return {
1126
+ keyPem,
1127
+ certPem: input.certPem,
1128
+ caCertPem: input.caCertPem
1129
+ };
1130
+ }
1131
+ function writeFileAtomic(path, content) {
1132
+ const tmp = `${path}.tmp`;
1133
+ writeFileSync(tmp, content);
1134
+ renameSync(tmp, path);
1135
+ }
1136
+ /** Fingerprint, issuer, SANs and validity of a served leaf. */
1137
+ function describeHttpsLeaf(certPem) {
1138
+ const x509 = new X509Certificate(certPem);
1139
+ return {
1140
+ fingerprintSha256: x509.fingerprint256,
1141
+ issuer: x509.issuer.replaceAll("\n", ", "),
1142
+ sans: (x509.subjectAltName ?? "").split(",").map((s) => s.trim()).filter((s) => s.length > 0),
1143
+ notBefore: Date.parse(x509.validFrom),
1144
+ notAfter: Date.parse(x509.validTo)
1145
+ };
1146
+ }
1147
+ var LOCAL_SUFFIXES = [
1148
+ ".local",
1149
+ ".lan",
1150
+ ".home.arpa",
1151
+ ".internal",
1152
+ ".localdomain"
1153
+ ];
1154
+ var LABEL_RE = /^(?!-)[a-z0-9-]{1,63}(?<!-)$/i;
1155
+ /** Private-network IPv4/IPv6: RFC 1918, CGNAT (Tailscale), link-local, ULA. */
1156
+ function isPrivateAddress(address) {
1157
+ const family = isIP(address);
1158
+ if (family === 4) {
1159
+ const [a, b] = address.split(".").map(Number);
1160
+ return a === 10 || a === 172 && b >= 16 && b <= 31 || a === 192 && b === 168 || a === 100 && b >= 64 && b <= 127 || a === 169 && b === 254;
1161
+ }
1162
+ if (family === 6) {
1163
+ const lower = address.toLowerCase();
1164
+ return lower.startsWith("fc") || lower.startsWith("fd") || lower.startsWith("fe80:");
1165
+ }
1166
+ return false;
1167
+ }
1168
+ /** The default docker bridge network (172.17.0.0/16): identical on every host, so it names nothing. */
1169
+ function isContainerBridgeAddress(address) {
1170
+ return /^172\.17\.\d{1,3}\.\d{1,3}$/.test(address);
1171
+ }
1172
+ /** A name that cannot collide with public DNS. */
1173
+ function isLocalOnlyHostname(name) {
1174
+ const lower = name.toLowerCase();
1175
+ if (lower.length === 0 || lower.length > 253) return false;
1176
+ if (!lower.includes(".")) return LABEL_RE.test(lower);
1177
+ if (!LOCAL_SUFFIXES.some((suffix) => lower.endsWith(suffix))) return false;
1178
+ return lower.split(".").every((label) => LABEL_RE.test(label));
1179
+ }
1180
+ /**
1181
+ * The names the hub will sign for `nodeId`: its node id and the address of its live mesh
1182
+ * socket - nothing the agent says about itself. `excluded` holds every name already taken
1183
+ * (the hub's own certificate, other nodes' leaves); `reported` is what the agent claimed,
1184
+ * listed only so it can be logged as dropped.
1185
+ */
1186
+ function acceptAgentSans(input) {
1187
+ const excluded = input.excluded ?? /* @__PURE__ */ new Set();
1188
+ const taken = (name) => excluded.has(name.toLowerCase());
1189
+ const rejected = (input.reported ?? []).map((n) => `${n} (self-reported, not certified)`);
1190
+ const dns = [];
1191
+ const ips = [];
1192
+ const id = input.nodeId.toLowerCase();
1193
+ if (id !== "hub" && isLocalOnlyHostname(id) && !taken(id)) dns.push(id);
1194
+ else rejected.push(`${input.nodeId} (node id: not a usable name)`);
1195
+ const address = input.observedAddress?.replace(/^::ffff:/, "");
1196
+ if (address !== void 0) if (address.includes("%")) rejected.push(`${address} (observed address: zone-scoped, only the node id is certified)`);
1197
+ else if (isContainerBridgeAddress(address)) rejected.push(`${address} (observed address: a container bridge address, only the node id is certified)`);
1198
+ else if (isIP(address) !== 0 && isPrivateAddress(address) && !taken(address)) ips.push(address);
1199
+ else rejected.push(`${address} (observed address: not private or already certified)`);
1200
+ return {
1201
+ identity: {
1202
+ dnsNames: dns,
1203
+ ipAddresses: ips,
1204
+ requiredDnsNames: dns,
1205
+ requiredIpAddresses: ips
1206
+ },
1207
+ accepted: [...dns, ...ips],
1208
+ rejected
1209
+ };
1210
+ }
1211
+ /** The SAN values (lower-cased) of a certificate PEM: DNS names and IP addresses. */
1212
+ function certificateNames(certPem) {
1213
+ const names = /* @__PURE__ */ new Set();
1214
+ const san = new X509Certificate(certPem).subjectAltName ?? "";
1215
+ for (const part of san.split(",")) {
1216
+ const [, value] = part.trim().split(/:(.*)/s);
1217
+ if (value !== void 0 && value.length > 0) names.add(value.toLowerCase());
1218
+ }
1219
+ return names;
1220
+ }
1221
+ /**
1222
+ * Hub side: sign an agent's CSR under the local CA in `<hubDataDir>/tls`. Throws when
1223
+ * there is no local CA to sign with or no name survives the policy.
1224
+ */
1225
+ async function issueAgentHttpsLeaf(input) {
1226
+ const caCertPath = join(input.hubDataDir, "tls", "camstack-ca.crt");
1227
+ const caKeyPath = join(input.hubDataDir, "tls", "camstack-ca.key");
1228
+ const hubLeafPath = join(input.hubDataDir, "tls", "camstack.crt");
1229
+ if (!existsSync(caCertPath) || !existsSync(caKeyPath)) throw new Error("the hub has no local CA to sign with (no tls/camstack-ca.crt + key)");
1230
+ const excluded = new Set(input.takenByOthers ?? []);
1231
+ if (existsSync(hubLeafPath)) for (const name of certificateNames(readFileSync(hubLeafPath, "utf8"))) excluded.add(name);
1232
+ const sans = acceptAgentSans({
1233
+ nodeId: input.nodeId,
1234
+ observedAddress: input.observedAddress,
1235
+ excluded,
1236
+ ...input.reportedNames !== void 0 ? { reported: input.reportedNames } : {}
1237
+ });
1238
+ if (sans.accepted.length === 0) throw new Error(`no name of node '${input.nodeId}' can be certified (${sans.rejected.join("; ") || "nothing offered"})`);
1239
+ const ca = await loadCa(readFileSync(caCertPath, "utf8"), readFileSync(caKeyPath, "utf8"));
1240
+ return {
1241
+ certPem: await issueLeafFromCsr(ca, input.csrPem, sans.identity, input.nodeId, 397, input.now ?? /* @__PURE__ */ new Date()),
1242
+ caCertPem: ca.certPem,
1243
+ accepted: sans.accepted,
1244
+ rejected: sans.rejected
1245
+ };
1246
+ }
1247
+ /** SHA-256 of a PEM (for logs that must not print a certificate). */
1248
+ function pemDigest(pem) {
1249
+ return createHash("sha256").update(pem).digest("hex").slice(0, 16);
1250
+ }
1251
+ //#endregion
1252
+ export { readTlsMode as A, CA_VALIDITY_DAYS as B, registerHttpsListener as C, UPLOADED_CA_FILE as D, prepareBootTls as E, loadTlsCert as F, DEFAULT_LAN_HTTP_PORT as G, MAX_LEAF_VALIDITY_DAYS as H, reissueTlsLeaf as I, bindPendingLanHttp as J, allFamiliesListenHost as K, CA_COMMON_NAME as L, writeExtraSans as M, writeTlsMode as N, readExtraSans as O, ensureTlsCert as P, collectCertIdentity as R, leafFingerprintOf as S, servedHttpsLeafFingerprint as T, SERVER_AUTH_OID as U, LEAF_RENEWAL_WINDOW_DAYS as V, evaluateExistingCert as W, readLanHttpState as X, closeLanHttp as Y, registerLanHttpHandler as Z, regenerateCertificate as _, collectAgentNames as a, validateUploadedTls as b, installAgentHttps as c, isPrivateAddress as d, issueAgentHttpsLeaf as f, applyCertificateToListener as g, pinnedHttpsCaFingerprint as h, certificateNames as i, splitPemCertificates as j, readTlsAccessStatus as k, isContainerBridgeAddress as l, pemDigest as m, acceptAgentSans as n, createAgentHttpsRequest as o, loadAgentHttps as p, applyLanHttp as q, agentHttpsPaths as r, describeHttpsLeaf as s, AgentHttpsInstallError as t, isLocalOnlyHostname as u, revertToGeneratedCertificate as v, reloadHttpsListener as w, clearHttpsListener as x, storeUploadedCertificate as y, systemHostFacts as z };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@camstack/system",
3
- "version": "1.2.428",
3
+ "version": "1.2.429",
4
4
  "description": "Core addon for CamStack — builtins, pipeline, process management, auth, logging, events",
5
5
  "keywords": [
6
6
  "camstack",
@@ -357,6 +357,9 @@
357
357
  },
358
358
  {
359
359
  "name": "user-management"
360
+ },
361
+ {
362
+ "name": "device-credentials"
360
363
  }
361
364
  ]
362
365
  },
@@ -1,174 +0,0 @@
1
- const require_dist = require("./dist-BQG3QFg7.js");
2
- let node_crypto = require("node:crypto");
3
- //#region src/auth/jwt-signing-key.ts
4
- /**
5
- * The hub's session signing key — the HMAC key every JWT this hub mints
6
- * (sessions, SSO bridge, TOTP challenges, service tokens) is signed with.
7
- *
8
- * It lives in the keychain, owner `system` ({@link JWT_SIGNING_KEY_REF}), and
9
- * nowhere else: no environment variable, no `config.yaml` key (D797). The
10
- * hub's `local-auth` builtin loads it after the keychain booted and registers
11
- * it here; every `AuthManager` in the process asks this slot on every mint and
12
- * every verify, so a key that arrives (or changes) after construction is used
13
- * from the next call on.
14
- *
15
- * ## A locked keychain fails closed, visibly
16
- *
17
- * While the key cannot be read, sessions can be neither minted nor verified:
18
- * {@link JwtSigningKeySource.current} throws {@link JwtSigningKeyUnavailableError}
19
- * (a login answers that error, a bearer check reads it as "not authenticated").
20
- * There is deliberately NO fallback — not an env var, and not a fresh random
21
- * key, which would invalidate every session and every app token without
22
- * anyone having asked for it. A key is generated ONLY when the keychain
23
- * answers that none is stored (`missing`), i.e. on a fresh hub.
24
- *
25
- * Process-global through a registered symbol, like the desktop platform key,
26
- * so a second copy of this module (an addon bundle) sees the same key.
27
- */
28
- /** The keychain owner of every system-held secret. */
29
- var SYSTEM_SECRET_OWNER = "system";
30
- /** `secret://system/global/jwtSigningKey`. */
31
- var JWT_SIGNING_KEY_REF = require_dist.buildSecretRef({
32
- owner: SYSTEM_SECRET_OWNER,
33
- scope: { kind: "global" },
34
- fieldPath: "jwtSigningKey"
35
- });
36
- /** Sessions cannot be minted or verified: the signing key is not loaded. */
37
- var JwtSigningKeyUnavailableError = class extends Error {
38
- reason;
39
- constructor(reason) {
40
- super(`sessions are unavailable: the session signing key could not be read from the keychain (${reason}). Unlock the keychain (System → Secrets, or fix keychain.keyFile and restart).`);
41
- this.reason = reason;
42
- this.name = "JwtSigningKeyUnavailableError";
43
- }
44
- };
45
- var SLOT = Symbol.for("camstack.auth.jwt-signing-key");
46
- var NOT_LOADED = {
47
- state: "unavailable",
48
- reason: "not-loaded"
49
- };
50
- function isState(value) {
51
- if (value === null || typeof value !== "object") return false;
52
- const state = Reflect.get(value, "state");
53
- if (state === "ready") return typeof Reflect.get(value, "key") === "string";
54
- return state === "unavailable" && typeof Reflect.get(value, "reason") === "string";
55
- }
56
- /** Register the loaded (or unloadable) key. Called by the hub's `local-auth`. */
57
- function setProcessJwtSigningKey(state) {
58
- Reflect.set(globalThis, SLOT, state);
59
- }
60
- function getProcessJwtSigningKeyState() {
61
- const value = Reflect.get(globalThis, SLOT);
62
- return isState(value) ? value : NOT_LOADED;
63
- }
64
- /** The process slot — the default source of every `AuthManager`. */
65
- var processJwtSigningKey = { current() {
66
- const state = getProcessJwtSigningKeyState();
67
- if (state.state === "ready") return state.key;
68
- throw new JwtSigningKeyUnavailableError(state.reason);
69
- } };
70
- /** A fixed key — tests, and nothing else. */
71
- function staticJwtSigningKey(key) {
72
- return { current: () => key };
73
- }
74
- /** 32 random bytes, hex. */
75
- function generateJwtSigningKey() {
76
- return (0, node_crypto.randomBytes)(32).toString("hex");
77
- }
78
- /**
79
- * Read the key from the keychain; generate and store one ONLY when the
80
- * keychain answers that none exists. Never throws.
81
- */
82
- async function loadJwtSigningKey(deps) {
83
- let entry;
84
- try {
85
- entry = (await deps.secrets.resolve([JWT_SIGNING_KEY_REF]))[JWT_SIGNING_KEY_REF];
86
- } catch (err) {
87
- return {
88
- state: "unavailable",
89
- reason: err instanceof Error ? err.message : String(err)
90
- };
91
- }
92
- if (entry === void 0) return {
93
- state: "unavailable",
94
- reason: "no-answer"
95
- };
96
- switch (entry.kind) {
97
- case "value": return {
98
- state: "ready",
99
- key: entry.value
100
- };
101
- case "unavailable": return {
102
- state: "unavailable",
103
- reason: entry.reason
104
- };
105
- case "forbidden": return {
106
- state: "unavailable",
107
- reason: "forbidden"
108
- };
109
- case "missing": {
110
- const key = (deps.generate ?? generateJwtSigningKey)();
111
- try {
112
- await deps.secrets.put(JWT_SIGNING_KEY_REF, key);
113
- } catch (err) {
114
- return {
115
- state: "unavailable",
116
- reason: `could not store a new key: ${err instanceof Error ? err.message : String(err)}`
117
- };
118
- }
119
- return {
120
- state: "ready",
121
- key
122
- };
123
- }
124
- }
125
- }
126
- //#endregion
127
- Object.defineProperty(exports, "JWT_SIGNING_KEY_REF", {
128
- enumerable: true,
129
- get: function() {
130
- return JWT_SIGNING_KEY_REF;
131
- }
132
- });
133
- Object.defineProperty(exports, "JwtSigningKeyUnavailableError", {
134
- enumerable: true,
135
- get: function() {
136
- return JwtSigningKeyUnavailableError;
137
- }
138
- });
139
- Object.defineProperty(exports, "SYSTEM_SECRET_OWNER", {
140
- enumerable: true,
141
- get: function() {
142
- return SYSTEM_SECRET_OWNER;
143
- }
144
- });
145
- Object.defineProperty(exports, "getProcessJwtSigningKeyState", {
146
- enumerable: true,
147
- get: function() {
148
- return getProcessJwtSigningKeyState;
149
- }
150
- });
151
- Object.defineProperty(exports, "loadJwtSigningKey", {
152
- enumerable: true,
153
- get: function() {
154
- return loadJwtSigningKey;
155
- }
156
- });
157
- Object.defineProperty(exports, "processJwtSigningKey", {
158
- enumerable: true,
159
- get: function() {
160
- return processJwtSigningKey;
161
- }
162
- });
163
- Object.defineProperty(exports, "setProcessJwtSigningKey", {
164
- enumerable: true,
165
- get: function() {
166
- return setProcessJwtSigningKey;
167
- }
168
- });
169
- Object.defineProperty(exports, "staticJwtSigningKey", {
170
- enumerable: true,
171
- get: function() {
172
- return staticJwtSigningKey;
173
- }
174
- });
@@ -1,127 +0,0 @@
1
- import { Sr as buildSecretRef } from "./dist-DHSJV4Qc.mjs";
2
- import { randomBytes } from "node:crypto";
3
- //#region src/auth/jwt-signing-key.ts
4
- /**
5
- * The hub's session signing key — the HMAC key every JWT this hub mints
6
- * (sessions, SSO bridge, TOTP challenges, service tokens) is signed with.
7
- *
8
- * It lives in the keychain, owner `system` ({@link JWT_SIGNING_KEY_REF}), and
9
- * nowhere else: no environment variable, no `config.yaml` key (D797). The
10
- * hub's `local-auth` builtin loads it after the keychain booted and registers
11
- * it here; every `AuthManager` in the process asks this slot on every mint and
12
- * every verify, so a key that arrives (or changes) after construction is used
13
- * from the next call on.
14
- *
15
- * ## A locked keychain fails closed, visibly
16
- *
17
- * While the key cannot be read, sessions can be neither minted nor verified:
18
- * {@link JwtSigningKeySource.current} throws {@link JwtSigningKeyUnavailableError}
19
- * (a login answers that error, a bearer check reads it as "not authenticated").
20
- * There is deliberately NO fallback — not an env var, and not a fresh random
21
- * key, which would invalidate every session and every app token without
22
- * anyone having asked for it. A key is generated ONLY when the keychain
23
- * answers that none is stored (`missing`), i.e. on a fresh hub.
24
- *
25
- * Process-global through a registered symbol, like the desktop platform key,
26
- * so a second copy of this module (an addon bundle) sees the same key.
27
- */
28
- /** The keychain owner of every system-held secret. */
29
- var SYSTEM_SECRET_OWNER = "system";
30
- /** `secret://system/global/jwtSigningKey`. */
31
- var JWT_SIGNING_KEY_REF = buildSecretRef({
32
- owner: SYSTEM_SECRET_OWNER,
33
- scope: { kind: "global" },
34
- fieldPath: "jwtSigningKey"
35
- });
36
- /** Sessions cannot be minted or verified: the signing key is not loaded. */
37
- var JwtSigningKeyUnavailableError = class extends Error {
38
- reason;
39
- constructor(reason) {
40
- super(`sessions are unavailable: the session signing key could not be read from the keychain (${reason}). Unlock the keychain (System → Secrets, or fix keychain.keyFile and restart).`);
41
- this.reason = reason;
42
- this.name = "JwtSigningKeyUnavailableError";
43
- }
44
- };
45
- var SLOT = Symbol.for("camstack.auth.jwt-signing-key");
46
- var NOT_LOADED = {
47
- state: "unavailable",
48
- reason: "not-loaded"
49
- };
50
- function isState(value) {
51
- if (value === null || typeof value !== "object") return false;
52
- const state = Reflect.get(value, "state");
53
- if (state === "ready") return typeof Reflect.get(value, "key") === "string";
54
- return state === "unavailable" && typeof Reflect.get(value, "reason") === "string";
55
- }
56
- /** Register the loaded (or unloadable) key. Called by the hub's `local-auth`. */
57
- function setProcessJwtSigningKey(state) {
58
- Reflect.set(globalThis, SLOT, state);
59
- }
60
- function getProcessJwtSigningKeyState() {
61
- const value = Reflect.get(globalThis, SLOT);
62
- return isState(value) ? value : NOT_LOADED;
63
- }
64
- /** The process slot — the default source of every `AuthManager`. */
65
- var processJwtSigningKey = { current() {
66
- const state = getProcessJwtSigningKeyState();
67
- if (state.state === "ready") return state.key;
68
- throw new JwtSigningKeyUnavailableError(state.reason);
69
- } };
70
- /** A fixed key — tests, and nothing else. */
71
- function staticJwtSigningKey(key) {
72
- return { current: () => key };
73
- }
74
- /** 32 random bytes, hex. */
75
- function generateJwtSigningKey() {
76
- return randomBytes(32).toString("hex");
77
- }
78
- /**
79
- * Read the key from the keychain; generate and store one ONLY when the
80
- * keychain answers that none exists. Never throws.
81
- */
82
- async function loadJwtSigningKey(deps) {
83
- let entry;
84
- try {
85
- entry = (await deps.secrets.resolve([JWT_SIGNING_KEY_REF]))[JWT_SIGNING_KEY_REF];
86
- } catch (err) {
87
- return {
88
- state: "unavailable",
89
- reason: err instanceof Error ? err.message : String(err)
90
- };
91
- }
92
- if (entry === void 0) return {
93
- state: "unavailable",
94
- reason: "no-answer"
95
- };
96
- switch (entry.kind) {
97
- case "value": return {
98
- state: "ready",
99
- key: entry.value
100
- };
101
- case "unavailable": return {
102
- state: "unavailable",
103
- reason: entry.reason
104
- };
105
- case "forbidden": return {
106
- state: "unavailable",
107
- reason: "forbidden"
108
- };
109
- case "missing": {
110
- const key = (deps.generate ?? generateJwtSigningKey)();
111
- try {
112
- await deps.secrets.put(JWT_SIGNING_KEY_REF, key);
113
- } catch (err) {
114
- return {
115
- state: "unavailable",
116
- reason: `could not store a new key: ${err instanceof Error ? err.message : String(err)}`
117
- };
118
- }
119
- return {
120
- state: "ready",
121
- key
122
- };
123
- }
124
- }
125
- }
126
- //#endregion
127
- export { loadJwtSigningKey as a, staticJwtSigningKey as c, getProcessJwtSigningKeyState as i, JwtSigningKeyUnavailableError as n, processJwtSigningKey as o, SYSTEM_SECRET_OWNER as r, setProcessJwtSigningKey as s, JWT_SIGNING_KEY_REF as t };