@camstack/system 1.2.428 → 1.2.430
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/dist/agent-https-names-CpUSuP4w.js +120 -0
- package/dist/agent-https-names-uetr4OQl.mjs +91 -0
- package/dist/auth/auth-audit-log.d.ts +1 -1
- package/dist/auth/device-ca-store.d.ts +57 -0
- package/dist/auth/device-ca.d.ts +75 -0
- package/dist/auth/device-credential-manager.d.ts +154 -0
- package/dist/auth/login-session-manager.d.ts +14 -2
- package/dist/auth/user-management-internal.d.ts +10 -1
- package/dist/builtins/addon-pages-aggregator/addon-pages-aggregator.addon.js +1 -1
- package/dist/builtins/addon-pages-aggregator/addon-pages-aggregator.addon.mjs +1 -1
- package/dist/builtins/addon-widgets-aggregator/addon-widgets-aggregator.addon.js +1 -1
- package/dist/builtins/addon-widgets-aggregator/addon-widgets-aggregator.addon.mjs +1 -1
- package/dist/builtins/alerts/alerts.addon.js +1 -1
- package/dist/builtins/alerts/alerts.addon.mjs +1 -1
- package/dist/builtins/autotrack/index.js +1 -1
- package/dist/builtins/autotrack/index.mjs +1 -1
- package/dist/builtins/backup-orchestrator/backup-orchestrator.addon.js +2 -2
- package/dist/builtins/backup-orchestrator/backup-orchestrator.addon.mjs +2 -2
- package/dist/builtins/calendars/calendars.addon.js +1 -1
- package/dist/builtins/calendars/calendars.addon.mjs +1 -1
- package/dist/builtins/camera-grid/index.js +1 -1
- package/dist/builtins/camera-grid/index.mjs +1 -1
- package/dist/builtins/composer/composer.addon.js +2 -2
- package/dist/builtins/composer/composer.addon.mjs +2 -2
- package/dist/builtins/console-logging/index.js +1 -1
- package/dist/builtins/console-logging/index.mjs +1 -1
- package/dist/builtins/core-blocks/core-blocks.addon.js +2 -2
- package/dist/builtins/core-blocks/core-blocks.addon.mjs +2 -2
- package/dist/builtins/device-manager/device-manager.addon.js +2 -2
- package/dist/builtins/device-manager/device-manager.addon.mjs +2 -2
- package/dist/builtins/hub-forwarder/index.js +1 -1
- package/dist/builtins/hub-forwarder/index.mjs +1 -1
- package/dist/builtins/keychain/index.js +10 -6
- package/dist/builtins/keychain/index.mjs +8 -4
- package/dist/builtins/liveness-monitor/liveness-monitor.addon.js +1 -1
- package/dist/builtins/liveness-monitor/liveness-monitor.addon.mjs +1 -1
- package/dist/builtins/local-auth/local-auth.addon.d.ts +6 -0
- package/dist/builtins/local-auth/local-auth.addon.js +955 -47
- package/dist/builtins/local-auth/local-auth.addon.mjs +946 -41
- package/dist/builtins/local-network/local-network.addon.js +2 -2
- package/dist/builtins/local-network/local-network.addon.mjs +2 -2
- package/dist/builtins/loki-logging/index.js +1 -1
- package/dist/builtins/loki-logging/index.mjs +1 -1
- package/dist/builtins/native-metrics/native-metrics.addon.js +1 -1
- package/dist/builtins/native-metrics/native-metrics.addon.mjs +1 -1
- package/dist/builtins/platform-probe/index.js +1 -1
- package/dist/builtins/platform-probe/index.mjs +1 -1
- package/dist/builtins/remote-access-orchestrator/remote-access-orchestrator.addon.js +1 -1
- package/dist/builtins/remote-access-orchestrator/remote-access-orchestrator.addon.mjs +1 -1
- package/dist/builtins/snapshot/index.js +1 -1
- package/dist/builtins/snapshot/index.mjs +1 -1
- package/dist/builtins/sqlite-storage/file-host-client.d.ts +11 -0
- package/dist/builtins/sqlite-storage/filesystem-storage.addon.js +1 -1
- package/dist/builtins/sqlite-storage/filesystem-storage.addon.mjs +1 -1
- package/dist/builtins/sqlite-storage/index.js +1 -1
- package/dist/builtins/sqlite-storage/index.mjs +1 -1
- package/dist/builtins/sqlite-storage/sqlite-settings.addon.js +34 -4
- package/dist/builtins/sqlite-storage/sqlite-settings.addon.mjs +34 -4
- package/dist/builtins/storage-orchestrator/storage-orchestrator.addon.js +1 -1
- package/dist/builtins/storage-orchestrator/storage-orchestrator.addon.mjs +1 -1
- package/dist/builtins/system-config/system-config.addon.d.ts +18 -0
- package/dist/builtins/system-config/system-config.addon.js +834 -18
- package/dist/builtins/system-config/system-config.addon.mjs +819 -19
- package/dist/builtins/winston-logging/index.js +1 -1
- package/dist/builtins/winston-logging/index.mjs +1 -1
- package/dist/{composition-sources-BQNxXQZG.mjs → composition-sources-Cw_TXEq9.mjs} +1 -1
- package/dist/{composition-sources-DvCGZEUI.js → composition-sources-o5MPSlk6.js} +1 -1
- package/dist/{constants-CCSJGnjs.js → constants-BcNaVkvI.js} +1 -1
- package/dist/{constants-CR4If_lP.mjs → constants-CttE77VF.mjs} +1 -1
- package/dist/device-ca-B-BTxeCj.mjs +355 -0
- package/dist/device-ca-CKiqh2Iu.js +462 -0
- package/dist/{dist-DHSJV4Qc.mjs → dist-CXG6muCR.mjs} +1052 -35
- package/dist/{dist-BQG3QFg7.js → dist-DxJdci75.js} +1099 -34
- package/dist/file-host-worker.js +1 -1
- package/dist/file-host-worker.mjs +1 -1
- package/dist/index.d.ts +4 -1
- package/dist/index.js +59 -12
- package/dist/index.mjs +10 -9
- package/dist/kernel/index.d.ts +2 -2
- package/dist/kernel/moleculer/mesh-mtls/index.d.ts +4 -1
- package/dist/kernel/moleculer/mesh-mtls/mesh-certificate-registry.d.ts +91 -0
- package/dist/kernel/moleculer/mesh-mtls/mesh-enroll.d.ts +4 -0
- package/dist/kernel/moleculer/mesh-mtls/mesh-peer-transport.d.ts +23 -0
- package/dist/kernel/moleculer/mesh-mtls/mesh-renewal.d.ts +126 -0
- package/dist/kernel/moleculer/mesh-mtls/mesh-tls-gate.d.ts +40 -0
- package/dist/kernel/moleculer/mesh-mtls/tls-tcp-transporter.d.ts +15 -0
- package/dist/{restore-archive-Dccm8lkA.js → restore-archive-CQJNoqRK.js} +1 -1
- package/dist/{restore-archive-DH1Pxq_1.mjs → restore-archive-D_wBL1OO.mjs} +1 -1
- package/dist/{retired-settings-keys-CPz7zpza.mjs → retired-settings-keys-EmDe11cP.mjs} +1 -1
- package/dist/{retired-settings-keys-BfGaSeQI.js → retired-settings-keys-g1oifXxR.js} +1 -1
- package/dist/tls/agent-https-names.d.ts +27 -0
- package/dist/tls/agent-https.d.ts +111 -0
- package/dist/tls/cert-issue.d.ts +7 -0
- package/dist/tls/index.d.ts +3 -0
- package/dist/{tls-BgImdxCO.js → tls-C5IYg3V5.js} +406 -0
- package/dist/{tls-CaOLZ09W.mjs → tls-CpcGkeJV.mjs} +314 -4
- package/dist/{wal-maintenance-DItkuOvu.js → wal-maintenance-BW1LIHi0.js} +0 -0
- package/dist/{wal-maintenance-Drqhcjit.mjs → wal-maintenance-vhPZLYuA.mjs} +0 -0
- package/package.json +4 -1
- package/dist/jwt-signing-key-BZm055IP.js +0 -174
- 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
|
-
|
|
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 };
|
|
Binary file
|
|
Binary file
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@camstack/system",
|
|
3
|
-
"version": "1.2.
|
|
3
|
+
"version": "1.2.430",
|
|
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 };
|