@blamejs/core 0.7.18 → 0.7.19
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/CHANGELOG.md +425 -423
- package/README.md +150 -150
- package/bin/blamejs.js +0 -0
- package/index.js +310 -308
- package/lib/api-key.js +660 -660
- package/lib/api-snapshot.js +338 -338
- package/lib/app-shutdown.js +385 -385
- package/lib/app.js +365 -365
- package/lib/archive.js +250 -250
- package/lib/atomic-file.js +544 -544
- package/lib/audit-chain.js +177 -177
- package/lib/audit-sign.js +344 -344
- package/lib/audit-tools.js +677 -677
- package/lib/audit.js +766 -766
- package/lib/auth/jwt-external.js +365 -0
- package/lib/auth/jwt.js +337 -311
- package/lib/auth/lockout.js +436 -436
- package/lib/auth/oauth.js +721 -721
- package/lib/auth/passkey.js +181 -181
- package/lib/auth/password.js +628 -594
- package/lib/backup/bundle.js +217 -217
- package/lib/backup/crypto.js +176 -176
- package/lib/backup/index.js +515 -515
- package/lib/backup/manifest.js +282 -282
- package/lib/break-glass.js +1338 -1338
- package/lib/bundler.js +441 -441
- package/lib/cache-redis.js +256 -256
- package/lib/cache.js +1206 -1206
- package/lib/canonical-json.js +115 -115
- package/lib/chain-writer.js +234 -234
- package/lib/cli-helpers.js +206 -206
- package/lib/cli.js +2334 -2334
- package/lib/cluster-provider-db.js +317 -317
- package/lib/cluster-storage.js +226 -226
- package/lib/cluster.js +703 -703
- package/lib/config-drift.js +301 -301
- package/lib/consent.js +222 -222
- package/lib/constants.js +191 -191
- package/lib/cookies.js +315 -315
- package/lib/credential-hash.js +322 -322
- package/lib/crypto.js +266 -266
- package/lib/csv.js +275 -275
- package/lib/db-declare-row-policy.js +267 -267
- package/lib/db-declare-view.js +420 -420
- package/lib/db-query.js +406 -406
- package/lib/db-schema.js +319 -319
- package/lib/db.js +1288 -1288
- package/lib/deprecate.js +222 -222
- package/lib/dev.js +335 -335
- package/lib/dual-control.js +473 -473
- package/lib/error-page.js +420 -420
- package/lib/external-db-migrate.js +441 -441
- package/lib/external-db.js +1061 -1061
- package/lib/file-type.js +273 -273
- package/lib/forms.js +422 -422
- package/lib/framework-error.js +293 -293
- package/lib/framework-schema.js +717 -717
- package/lib/handlers.js +350 -350
- package/lib/http-client-cookie-jar.js +508 -508
- package/lib/http-client.js +1195 -1195
- package/lib/i18n.js +878 -878
- package/lib/jobs.js +185 -185
- package/lib/log-stream-cloudwatch.js +369 -369
- package/lib/log-stream-local.js +146 -146
- package/lib/log-stream-otlp-grpc.js +410 -410
- package/lib/log-stream-otlp.js +286 -286
- package/lib/log-stream-syslog.js +302 -302
- package/lib/log-stream-webhook.js +199 -199
- package/lib/log-stream.js +330 -330
- package/lib/log.js +500 -500
- package/lib/mail-bounce.js +528 -528
- package/lib/mail-dkim.js +369 -369
- package/lib/mail.js +981 -981
- package/lib/metrics.js +683 -683
- package/lib/middleware/api-encrypt.js +936 -936
- package/lib/middleware/attach-user.js +157 -157
- package/lib/middleware/bearer-auth.js +152 -0
- package/lib/middleware/body-parser.js +1170 -1170
- package/lib/middleware/bot-guard.js +178 -178
- package/lib/middleware/compression.js +452 -452
- package/lib/middleware/cors.js +314 -314
- package/lib/middleware/csp-nonce.js +348 -348
- package/lib/middleware/csrf-protect.js +316 -316
- package/lib/middleware/db-role-for.js +264 -264
- package/lib/middleware/health.js +392 -392
- package/lib/middleware/index.js +82 -79
- package/lib/middleware/rate-limit.js +358 -358
- package/lib/middleware/request-id.js +61 -61
- package/lib/middleware/request-log.js +168 -168
- package/lib/middleware/require-auth.js +104 -104
- package/lib/middleware/security-headers.js +116 -116
- package/lib/middleware/sse.js +166 -166
- package/lib/migrations.js +383 -383
- package/lib/mtls-ca.js +518 -518
- package/lib/mtls-engine-default.js +481 -481
- package/lib/network-dns.js +632 -632
- package/lib/network-heartbeat.js +290 -290
- package/lib/network-nts.js +574 -574
- package/lib/network-proxy.js +265 -265
- package/lib/network-tls.js +328 -328
- package/lib/network.js +233 -233
- package/lib/notify.js +612 -612
- package/lib/ntp-check.js +229 -229
- package/lib/numeric-bounds.js +111 -111
- package/lib/object-store/azure-blob-bucket-ops.js +349 -349
- package/lib/object-store/azure-blob.js +488 -488
- package/lib/object-store/gcs-bucket-ops.js +351 -351
- package/lib/object-store/gcs.js +519 -519
- package/lib/object-store/http-put.js +153 -153
- package/lib/object-store/index.js +197 -197
- package/lib/object-store/sigv4-bucket-ops.js +1092 -1092
- package/lib/object-store/sigv4.js +903 -903
- package/lib/observability.js +151 -151
- package/lib/otel-export.js +269 -269
- package/lib/pagination.js +464 -464
- package/lib/parsers/index.js +80 -80
- package/lib/parsers/safe-env.js +642 -642
- package/lib/parsers/safe-ini.js +292 -292
- package/lib/parsers/safe-toml.js +784 -784
- package/lib/parsers/safe-xml.js +390 -390
- package/lib/parsers/safe-yaml.js +1015 -1015
- package/lib/permissions.js +708 -708
- package/lib/pqc-agent.js +87 -87
- package/lib/pqc-gate.js +279 -279
- package/lib/protobuf-encoder.js +190 -190
- package/lib/protocol-dispatcher.js +161 -161
- package/lib/pubsub-redis.js +167 -167
- package/lib/pubsub.js +429 -429
- package/lib/queue-local.js +476 -476
- package/lib/queue-redis.js +745 -745
- package/lib/queue-sqs.js +319 -319
- package/lib/queue.js +695 -695
- package/lib/redis-client.js +519 -519
- package/lib/request-helpers.js +340 -340
- package/lib/restore-bundle.js +237 -237
- package/lib/restore-rollback.js +259 -259
- package/lib/restore.js +409 -409
- package/lib/retry.js +376 -376
- package/lib/router.js +748 -748
- package/lib/safe-async.js +735 -735
- package/lib/safe-buffer.js +237 -237
- package/lib/safe-json.js +541 -541
- package/lib/safe-schema.js +1266 -1266
- package/lib/safe-url.js +159 -159
- package/lib/scheduler.js +706 -706
- package/lib/security-assert.js +373 -373
- package/lib/seeders.js +618 -618
- package/lib/session.js +535 -478
- package/lib/slug.js +269 -269
- package/lib/ssrf-guard.js +401 -401
- package/lib/storage.js +471 -471
- package/lib/subject.js +281 -281
- package/lib/template.js +791 -791
- package/lib/testing.js +798 -798
- package/lib/time.js +310 -310
- package/lib/totp.js +302 -302
- package/lib/tracing.js +494 -494
- package/lib/uuid.js +132 -132
- package/lib/validate-opts.js +340 -340
- package/lib/vault/index.js +308 -308
- package/lib/vault/rotate.js +784 -784
- package/lib/vault/wrap.js +296 -296
- package/lib/vendor/noble-ciphers.cjs +9 -9
- package/lib/webhook.js +595 -595
- package/lib/websocket.js +1048 -1048
- package/package.json +77 -77
- package/sbom.cyclonedx.json +7 -7
package/lib/network-tls.js
CHANGED
|
@@ -1,328 +1,328 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
|
|
3
|
-
var tls = require("node:tls");
|
|
4
|
-
var fs = require("node:fs");
|
|
5
|
-
var path = require("node:path");
|
|
6
|
-
var nodeCrypto = require("node:crypto");
|
|
7
|
-
|
|
8
|
-
var C = require("./constants");
|
|
9
|
-
var safeBuffer = require("./safe-buffer");
|
|
10
|
-
var validateOpts = require("./validate-opts");
|
|
11
|
-
var lazyRequire = require("./lazy-require");
|
|
12
|
-
var { defineClass } = require("./framework-error");
|
|
13
|
-
|
|
14
|
-
var TlsTrustError = defineClass("TlsTrustError", { alwaysPermanent: true });
|
|
15
|
-
|
|
16
|
-
var observability = lazyRequire(function () { return require("./observability"); });
|
|
17
|
-
var audit = lazyRequire(function () { return require("./audit"); });
|
|
18
|
-
|
|
19
|
-
var STATE = {
|
|
20
|
-
cas: [],
|
|
21
|
-
systemTrust: false,
|
|
22
|
-
baselineFingerprints: null,
|
|
23
|
-
};
|
|
24
|
-
|
|
25
|
-
function _normalizePem(pem) {
|
|
26
|
-
if (Buffer.isBuffer(pem)) pem = pem.toString("utf8");
|
|
27
|
-
if (typeof pem !== "string") {
|
|
28
|
-
throw new TlsTrustError("tls/bad-ca", "CA must be a PEM string or path, got " + typeof pem);
|
|
29
|
-
}
|
|
30
|
-
return pem.replace(/\r\n/g, "\n").trim();
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
function _splitPemBundle(pem) {
|
|
34
|
-
var blocks = [];
|
|
35
|
-
var lines = pem.split("\n");
|
|
36
|
-
var current = null;
|
|
37
|
-
for (var i = 0; i < lines.length; i++) {
|
|
38
|
-
var line = lines[i];
|
|
39
|
-
if (line.indexOf("-----BEGIN CERTIFICATE-----") === 0) {
|
|
40
|
-
current = [line];
|
|
41
|
-
} else if (current) {
|
|
42
|
-
current.push(line);
|
|
43
|
-
if (line.indexOf("-----END CERTIFICATE-----") === 0) {
|
|
44
|
-
blocks.push(current.join("\n"));
|
|
45
|
-
current = null;
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
|
-
}
|
|
49
|
-
return blocks;
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
function _certMetadata(pem) {
|
|
53
|
-
try {
|
|
54
|
-
var x = new nodeCrypto.X509Certificate(pem);
|
|
55
|
-
return {
|
|
56
|
-
subject: x.subject,
|
|
57
|
-
issuer: x.issuer,
|
|
58
|
-
validFrom: x.validFrom,
|
|
59
|
-
validTo: x.validTo,
|
|
60
|
-
fingerprint256: x.fingerprint256,
|
|
61
|
-
serialNumber: x.serialNumber,
|
|
62
|
-
isSelfSigned: x.subject === x.issuer,
|
|
63
|
-
};
|
|
64
|
-
} catch (e) {
|
|
65
|
-
throw new TlsTrustError("tls/bad-ca-pem", "CA PEM not parseable: " + e.message);
|
|
66
|
-
}
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
function _isPathLike(s) {
|
|
70
|
-
if (s.indexOf("-----BEGIN") !== -1) return false;
|
|
71
|
-
if (s.length > C.BYTES.kib(1)) return false;
|
|
72
|
-
if (safeBuffer.hasCrlf(s)) return false;
|
|
73
|
-
return true;
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
function _readPath(p) {
|
|
77
|
-
var stat = fs.statSync(p);
|
|
78
|
-
if (stat.isDirectory()) {
|
|
79
|
-
var files = fs.readdirSync(p)
|
|
80
|
-
.filter(function (f) { return /\.(pem|crt|cer)$/i.test(f); })
|
|
81
|
-
.sort();
|
|
82
|
-
return files.map(function (f) { return fs.readFileSync(path.join(p, f), "utf8"); }).join("\n");
|
|
83
|
-
}
|
|
84
|
-
return fs.readFileSync(p, "utf8");
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
function addCa(pemOrPath, opts) {
|
|
88
|
-
opts = opts || {};
|
|
89
|
-
validateOpts(opts, ["label", "audit"], "tls.addCa");
|
|
90
|
-
var raw = pemOrPath;
|
|
91
|
-
if (typeof pemOrPath === "string" && _isPathLike(pemOrPath)) {
|
|
92
|
-
var stat;
|
|
93
|
-
try { stat = fs.statSync(pemOrPath); } catch (_e) {
|
|
94
|
-
throw new TlsTrustError("tls/empty-pem", "tls.addCa: input has no PEM marker and is not a readable path: " +
|
|
95
|
-
pemOrPath);
|
|
96
|
-
}
|
|
97
|
-
raw = _readPath(pemOrPath);
|
|
98
|
-
if (!stat) raw = "";
|
|
99
|
-
}
|
|
100
|
-
raw = _normalizePem(raw);
|
|
101
|
-
var blocks = _splitPemBundle(raw);
|
|
102
|
-
if (blocks.length === 0) {
|
|
103
|
-
throw new TlsTrustError("tls/empty-pem", "no CERTIFICATE blocks found in PEM input");
|
|
104
|
-
}
|
|
105
|
-
var added = [];
|
|
106
|
-
for (var i = 0; i < blocks.length; i++) {
|
|
107
|
-
var meta = _certMetadata(blocks[i]);
|
|
108
|
-
STATE.cas.push({ pem: blocks[i], meta: meta, label: opts.label || null, addedAt: Date.now() });
|
|
109
|
-
added.push(meta);
|
|
110
|
-
}
|
|
111
|
-
_emitAuditAdd(added, opts);
|
|
112
|
-
_emitObs("network.tls.ca.added", { count: added.length });
|
|
113
|
-
return added;
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
function addCaBundle(p, opts) {
|
|
117
|
-
return addCa(p, opts);
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
function useSystemTrust(enable) {
|
|
121
|
-
STATE.systemTrust = enable !== false;
|
|
122
|
-
_emitObs("network.tls.system_trust.set", { enabled: STATE.systemTrust });
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
function isSystemTrustEnabled() { return !!STATE.systemTrust; }
|
|
126
|
-
|
|
127
|
-
function getTrustStore() {
|
|
128
|
-
return STATE.cas.map(function (entry) {
|
|
129
|
-
return {
|
|
130
|
-
label: entry.label,
|
|
131
|
-
addedAt: entry.addedAt,
|
|
132
|
-
subject: entry.meta.subject,
|
|
133
|
-
issuer: entry.meta.issuer,
|
|
134
|
-
validFrom: entry.meta.validFrom,
|
|
135
|
-
validTo: entry.meta.validTo,
|
|
136
|
-
fingerprint256: entry.meta.fingerprint256,
|
|
137
|
-
serialNumber: entry.meta.serialNumber,
|
|
138
|
-
isSelfSigned: entry.meta.isSelfSigned,
|
|
139
|
-
};
|
|
140
|
-
});
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
function _emitAuditRemove(metaList, reason) {
|
|
144
|
-
var sink;
|
|
145
|
-
try { sink = audit(); } catch (_e) { sink = null; }
|
|
146
|
-
if (!sink || typeof sink.safeEmit !== "function") return;
|
|
147
|
-
for (var i = 0; i < metaList.length; i++) {
|
|
148
|
-
var m = metaList[i];
|
|
149
|
-
try {
|
|
150
|
-
sink.safeEmit({
|
|
151
|
-
action: "network.tls.ca.removed",
|
|
152
|
-
outcome: "success",
|
|
153
|
-
metadata: {
|
|
154
|
-
subject: m.subject,
|
|
155
|
-
issuer: m.issuer,
|
|
156
|
-
fingerprint256: m.fingerprint256,
|
|
157
|
-
validFrom: m.validFrom,
|
|
158
|
-
validTo: m.validTo,
|
|
159
|
-
isSelfSigned: m.isSelfSigned,
|
|
160
|
-
label: m.label,
|
|
161
|
-
reason: reason || "operator",
|
|
162
|
-
},
|
|
163
|
-
});
|
|
164
|
-
} catch (_e) { /* audit best-effort — never break the caller */ }
|
|
165
|
-
}
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
function removeCa(fingerprint256, opts) {
|
|
169
|
-
if (typeof fingerprint256 !== "string" || fingerprint256.length === 0) {
|
|
170
|
-
throw new TlsTrustError("tls/bad-fingerprint", "tls.removeCa: fingerprint256 must be a non-empty string");
|
|
171
|
-
}
|
|
172
|
-
var fp = fingerprint256.toUpperCase();
|
|
173
|
-
var removed = [];
|
|
174
|
-
STATE.cas = STATE.cas.filter(function (entry) {
|
|
175
|
-
var entryFp = (entry.meta.fingerprint256 || "").toUpperCase();
|
|
176
|
-
if (entryFp === fp) {
|
|
177
|
-
removed.push(Object.assign({ label: entry.label }, entry.meta));
|
|
178
|
-
return false;
|
|
179
|
-
}
|
|
180
|
-
return true;
|
|
181
|
-
});
|
|
182
|
-
if (removed.length === 0) return 0;
|
|
183
|
-
if (!opts || opts.audit !== false) _emitAuditRemove(removed, "operator-remove");
|
|
184
|
-
_emitObs("network.tls.ca.removed", { count: removed.length, reason: "operator" });
|
|
185
|
-
return removed.length;
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
function removeCaByLabel(label, opts) {
|
|
189
|
-
if (typeof label !== "string" || label.length === 0) {
|
|
190
|
-
throw new TlsTrustError("tls/bad-label", "tls.removeCaByLabel: label must be a non-empty string");
|
|
191
|
-
}
|
|
192
|
-
var removed = [];
|
|
193
|
-
STATE.cas = STATE.cas.filter(function (entry) {
|
|
194
|
-
if (entry.label === label) {
|
|
195
|
-
removed.push(Object.assign({ label: entry.label }, entry.meta));
|
|
196
|
-
return false;
|
|
197
|
-
}
|
|
198
|
-
return true;
|
|
199
|
-
});
|
|
200
|
-
if (removed.length === 0) return 0;
|
|
201
|
-
if (!opts || opts.audit !== false) _emitAuditRemove(removed, "operator-remove-by-label");
|
|
202
|
-
_emitObs("network.tls.ca.removed", { count: removed.length, reason: "label" });
|
|
203
|
-
return removed.length;
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
function clearAll(opts) {
|
|
207
|
-
if (STATE.cas.length === 0) return 0;
|
|
208
|
-
var removed = STATE.cas.map(function (e) { return Object.assign({ label: e.label }, e.meta); });
|
|
209
|
-
STATE.cas = [];
|
|
210
|
-
if (!opts || opts.audit !== false) _emitAuditRemove(removed, "operator-clear-all");
|
|
211
|
-
_emitObs("network.tls.ca.cleared", { count: removed.length });
|
|
212
|
-
return removed.length;
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
function purgeExpired(opts) {
|
|
216
|
-
var nowMs = Date.now();
|
|
217
|
-
var removed = [];
|
|
218
|
-
STATE.cas = STATE.cas.filter(function (entry) {
|
|
219
|
-
var validToMs = entry.meta.validTo ? Date.parse(entry.meta.validTo) : NaN;
|
|
220
|
-
if (isFinite(validToMs) && validToMs < nowMs) {
|
|
221
|
-
removed.push(Object.assign({ label: entry.label }, entry.meta));
|
|
222
|
-
return false;
|
|
223
|
-
}
|
|
224
|
-
return true;
|
|
225
|
-
});
|
|
226
|
-
if (removed.length === 0) return 0;
|
|
227
|
-
if (!opts || opts.audit !== false) _emitAuditRemove(removed, "expired");
|
|
228
|
-
_emitObs("network.tls.ca.purged_expired", { count: removed.length });
|
|
229
|
-
return removed.length;
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
function expiringSoon(windowMs) {
|
|
233
|
-
if (typeof windowMs !== "number" || !isFinite(windowMs) || windowMs < 0) {
|
|
234
|
-
throw new TlsTrustError("tls/bad-window", "tls.expiringSoon: windowMs must be a non-negative finite number");
|
|
235
|
-
}
|
|
236
|
-
var threshold = Date.now() + windowMs;
|
|
237
|
-
return STATE.cas.filter(function (entry) {
|
|
238
|
-
var validToMs = entry.meta.validTo ? Date.parse(entry.meta.validTo) : NaN;
|
|
239
|
-
return isFinite(validToMs) && validToMs <= threshold;
|
|
240
|
-
}).map(function (entry) {
|
|
241
|
-
return Object.assign({ label: entry.label }, entry.meta);
|
|
242
|
-
});
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
function captureBaselineFingerprints() {
|
|
246
|
-
STATE.baselineFingerprints = STATE.cas.map(function (e) { return e.meta.fingerprint256; });
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
function detectBaselineDrift() {
|
|
250
|
-
if (!STATE.baselineFingerprints) return null;
|
|
251
|
-
var current = STATE.cas.map(function (e) { return e.meta.fingerprint256; });
|
|
252
|
-
var added = current.filter(function (fp) { return STATE.baselineFingerprints.indexOf(fp) === -1; });
|
|
253
|
-
var removed = STATE.baselineFingerprints.filter(function (fp) { return current.indexOf(fp) === -1; });
|
|
254
|
-
return { added: added, removed: removed, drifted: added.length > 0 || removed.length > 0 };
|
|
255
|
-
}
|
|
256
|
-
|
|
257
|
-
function applyToContext(opts) {
|
|
258
|
-
opts = opts || {};
|
|
259
|
-
validateOpts(opts, ["base"], "tls.applyToContext");
|
|
260
|
-
var base = Object.assign({}, opts.base || {});
|
|
261
|
-
var caStrings = STATE.cas.map(function (e) { return e.pem; });
|
|
262
|
-
if (STATE.systemTrust) {
|
|
263
|
-
var rootCAs = tls.rootCertificates;
|
|
264
|
-
if (Array.isArray(rootCAs)) {
|
|
265
|
-
caStrings = caStrings.concat(rootCAs);
|
|
266
|
-
}
|
|
267
|
-
}
|
|
268
|
-
if (caStrings.length > 0) base.ca = caStrings;
|
|
269
|
-
return base;
|
|
270
|
-
}
|
|
271
|
-
|
|
272
|
-
function getCaPems() {
|
|
273
|
-
return STATE.cas.map(function (e) { return e.pem; });
|
|
274
|
-
}
|
|
275
|
-
|
|
276
|
-
function _emitAuditAdd(metaList, opts) {
|
|
277
|
-
if (opts.audit === false) return;
|
|
278
|
-
var sink;
|
|
279
|
-
try { sink = audit(); } catch (_e) { sink = null; }
|
|
280
|
-
if (!sink || typeof sink.safeEmit !== "function") return;
|
|
281
|
-
for (var i = 0; i < metaList.length; i++) {
|
|
282
|
-
var m = metaList[i];
|
|
283
|
-
try {
|
|
284
|
-
sink.safeEmit({
|
|
285
|
-
action: "network.tls.ca.added",
|
|
286
|
-
outcome: "success",
|
|
287
|
-
metadata: {
|
|
288
|
-
subject: m.subject,
|
|
289
|
-
issuer: m.issuer,
|
|
290
|
-
fingerprint256: m.fingerprint256,
|
|
291
|
-
validFrom: m.validFrom,
|
|
292
|
-
validTo: m.validTo,
|
|
293
|
-
isSelfSigned: m.isSelfSigned,
|
|
294
|
-
label: opts.label || null,
|
|
295
|
-
},
|
|
296
|
-
});
|
|
297
|
-
} catch (_e) { /* audit best-effort — never break the caller */ }
|
|
298
|
-
}
|
|
299
|
-
}
|
|
300
|
-
|
|
301
|
-
function _emitObs(name, fields) {
|
|
302
|
-
try { observability().emit(name, fields || {}); } catch (_e) { /* obs best-effort */ }
|
|
303
|
-
}
|
|
304
|
-
|
|
305
|
-
function _resetForTest() {
|
|
306
|
-
STATE.cas = [];
|
|
307
|
-
STATE.systemTrust = false;
|
|
308
|
-
STATE.baselineFingerprints = null;
|
|
309
|
-
}
|
|
310
|
-
|
|
311
|
-
module.exports = {
|
|
312
|
-
addCa: addCa,
|
|
313
|
-
addCaBundle: addCaBundle,
|
|
314
|
-
removeCa: removeCa,
|
|
315
|
-
removeCaByLabel: removeCaByLabel,
|
|
316
|
-
clearAll: clearAll,
|
|
317
|
-
purgeExpired: purgeExpired,
|
|
318
|
-
expiringSoon: expiringSoon,
|
|
319
|
-
useSystemTrust: useSystemTrust,
|
|
320
|
-
isSystemTrustEnabled: isSystemTrustEnabled,
|
|
321
|
-
getTrustStore: getTrustStore,
|
|
322
|
-
captureBaselineFingerprints: captureBaselineFingerprints,
|
|
323
|
-
detectBaselineDrift: detectBaselineDrift,
|
|
324
|
-
applyToContext: applyToContext,
|
|
325
|
-
getCaPems: getCaPems,
|
|
326
|
-
TlsTrustError: TlsTrustError,
|
|
327
|
-
_resetForTest: _resetForTest,
|
|
328
|
-
};
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
var tls = require("node:tls");
|
|
4
|
+
var fs = require("node:fs");
|
|
5
|
+
var path = require("node:path");
|
|
6
|
+
var nodeCrypto = require("node:crypto");
|
|
7
|
+
|
|
8
|
+
var C = require("./constants");
|
|
9
|
+
var safeBuffer = require("./safe-buffer");
|
|
10
|
+
var validateOpts = require("./validate-opts");
|
|
11
|
+
var lazyRequire = require("./lazy-require");
|
|
12
|
+
var { defineClass } = require("./framework-error");
|
|
13
|
+
|
|
14
|
+
var TlsTrustError = defineClass("TlsTrustError", { alwaysPermanent: true });
|
|
15
|
+
|
|
16
|
+
var observability = lazyRequire(function () { return require("./observability"); });
|
|
17
|
+
var audit = lazyRequire(function () { return require("./audit"); });
|
|
18
|
+
|
|
19
|
+
var STATE = {
|
|
20
|
+
cas: [],
|
|
21
|
+
systemTrust: false,
|
|
22
|
+
baselineFingerprints: null,
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
function _normalizePem(pem) {
|
|
26
|
+
if (Buffer.isBuffer(pem)) pem = pem.toString("utf8");
|
|
27
|
+
if (typeof pem !== "string") {
|
|
28
|
+
throw new TlsTrustError("tls/bad-ca", "CA must be a PEM string or path, got " + typeof pem);
|
|
29
|
+
}
|
|
30
|
+
return pem.replace(/\r\n/g, "\n").trim();
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function _splitPemBundle(pem) {
|
|
34
|
+
var blocks = [];
|
|
35
|
+
var lines = pem.split("\n");
|
|
36
|
+
var current = null;
|
|
37
|
+
for (var i = 0; i < lines.length; i++) {
|
|
38
|
+
var line = lines[i];
|
|
39
|
+
if (line.indexOf("-----BEGIN CERTIFICATE-----") === 0) {
|
|
40
|
+
current = [line];
|
|
41
|
+
} else if (current) {
|
|
42
|
+
current.push(line);
|
|
43
|
+
if (line.indexOf("-----END CERTIFICATE-----") === 0) {
|
|
44
|
+
blocks.push(current.join("\n"));
|
|
45
|
+
current = null;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
return blocks;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function _certMetadata(pem) {
|
|
53
|
+
try {
|
|
54
|
+
var x = new nodeCrypto.X509Certificate(pem);
|
|
55
|
+
return {
|
|
56
|
+
subject: x.subject,
|
|
57
|
+
issuer: x.issuer,
|
|
58
|
+
validFrom: x.validFrom,
|
|
59
|
+
validTo: x.validTo,
|
|
60
|
+
fingerprint256: x.fingerprint256,
|
|
61
|
+
serialNumber: x.serialNumber,
|
|
62
|
+
isSelfSigned: x.subject === x.issuer,
|
|
63
|
+
};
|
|
64
|
+
} catch (e) {
|
|
65
|
+
throw new TlsTrustError("tls/bad-ca-pem", "CA PEM not parseable: " + e.message);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function _isPathLike(s) {
|
|
70
|
+
if (s.indexOf("-----BEGIN") !== -1) return false;
|
|
71
|
+
if (s.length > C.BYTES.kib(1)) return false;
|
|
72
|
+
if (safeBuffer.hasCrlf(s)) return false;
|
|
73
|
+
return true;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function _readPath(p) {
|
|
77
|
+
var stat = fs.statSync(p);
|
|
78
|
+
if (stat.isDirectory()) {
|
|
79
|
+
var files = fs.readdirSync(p)
|
|
80
|
+
.filter(function (f) { return /\.(pem|crt|cer)$/i.test(f); })
|
|
81
|
+
.sort();
|
|
82
|
+
return files.map(function (f) { return fs.readFileSync(path.join(p, f), "utf8"); }).join("\n");
|
|
83
|
+
}
|
|
84
|
+
return fs.readFileSync(p, "utf8");
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function addCa(pemOrPath, opts) {
|
|
88
|
+
opts = opts || {};
|
|
89
|
+
validateOpts(opts, ["label", "audit"], "tls.addCa");
|
|
90
|
+
var raw = pemOrPath;
|
|
91
|
+
if (typeof pemOrPath === "string" && _isPathLike(pemOrPath)) {
|
|
92
|
+
var stat;
|
|
93
|
+
try { stat = fs.statSync(pemOrPath); } catch (_e) {
|
|
94
|
+
throw new TlsTrustError("tls/empty-pem", "tls.addCa: input has no PEM marker and is not a readable path: " +
|
|
95
|
+
pemOrPath);
|
|
96
|
+
}
|
|
97
|
+
raw = _readPath(pemOrPath);
|
|
98
|
+
if (!stat) raw = "";
|
|
99
|
+
}
|
|
100
|
+
raw = _normalizePem(raw);
|
|
101
|
+
var blocks = _splitPemBundle(raw);
|
|
102
|
+
if (blocks.length === 0) {
|
|
103
|
+
throw new TlsTrustError("tls/empty-pem", "no CERTIFICATE blocks found in PEM input");
|
|
104
|
+
}
|
|
105
|
+
var added = [];
|
|
106
|
+
for (var i = 0; i < blocks.length; i++) {
|
|
107
|
+
var meta = _certMetadata(blocks[i]);
|
|
108
|
+
STATE.cas.push({ pem: blocks[i], meta: meta, label: opts.label || null, addedAt: Date.now() });
|
|
109
|
+
added.push(meta);
|
|
110
|
+
}
|
|
111
|
+
_emitAuditAdd(added, opts);
|
|
112
|
+
_emitObs("network.tls.ca.added", { count: added.length });
|
|
113
|
+
return added;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function addCaBundle(p, opts) {
|
|
117
|
+
return addCa(p, opts);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function useSystemTrust(enable) {
|
|
121
|
+
STATE.systemTrust = enable !== false;
|
|
122
|
+
_emitObs("network.tls.system_trust.set", { enabled: STATE.systemTrust });
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function isSystemTrustEnabled() { return !!STATE.systemTrust; }
|
|
126
|
+
|
|
127
|
+
function getTrustStore() {
|
|
128
|
+
return STATE.cas.map(function (entry) {
|
|
129
|
+
return {
|
|
130
|
+
label: entry.label,
|
|
131
|
+
addedAt: entry.addedAt,
|
|
132
|
+
subject: entry.meta.subject,
|
|
133
|
+
issuer: entry.meta.issuer,
|
|
134
|
+
validFrom: entry.meta.validFrom,
|
|
135
|
+
validTo: entry.meta.validTo,
|
|
136
|
+
fingerprint256: entry.meta.fingerprint256,
|
|
137
|
+
serialNumber: entry.meta.serialNumber,
|
|
138
|
+
isSelfSigned: entry.meta.isSelfSigned,
|
|
139
|
+
};
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function _emitAuditRemove(metaList, reason) {
|
|
144
|
+
var sink;
|
|
145
|
+
try { sink = audit(); } catch (_e) { sink = null; }
|
|
146
|
+
if (!sink || typeof sink.safeEmit !== "function") return;
|
|
147
|
+
for (var i = 0; i < metaList.length; i++) {
|
|
148
|
+
var m = metaList[i];
|
|
149
|
+
try {
|
|
150
|
+
sink.safeEmit({
|
|
151
|
+
action: "network.tls.ca.removed",
|
|
152
|
+
outcome: "success",
|
|
153
|
+
metadata: {
|
|
154
|
+
subject: m.subject,
|
|
155
|
+
issuer: m.issuer,
|
|
156
|
+
fingerprint256: m.fingerprint256,
|
|
157
|
+
validFrom: m.validFrom,
|
|
158
|
+
validTo: m.validTo,
|
|
159
|
+
isSelfSigned: m.isSelfSigned,
|
|
160
|
+
label: m.label,
|
|
161
|
+
reason: reason || "operator",
|
|
162
|
+
},
|
|
163
|
+
});
|
|
164
|
+
} catch (_e) { /* audit best-effort — never break the caller */ }
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function removeCa(fingerprint256, opts) {
|
|
169
|
+
if (typeof fingerprint256 !== "string" || fingerprint256.length === 0) {
|
|
170
|
+
throw new TlsTrustError("tls/bad-fingerprint", "tls.removeCa: fingerprint256 must be a non-empty string");
|
|
171
|
+
}
|
|
172
|
+
var fp = fingerprint256.toUpperCase();
|
|
173
|
+
var removed = [];
|
|
174
|
+
STATE.cas = STATE.cas.filter(function (entry) {
|
|
175
|
+
var entryFp = (entry.meta.fingerprint256 || "").toUpperCase();
|
|
176
|
+
if (entryFp === fp) {
|
|
177
|
+
removed.push(Object.assign({ label: entry.label }, entry.meta));
|
|
178
|
+
return false;
|
|
179
|
+
}
|
|
180
|
+
return true;
|
|
181
|
+
});
|
|
182
|
+
if (removed.length === 0) return 0;
|
|
183
|
+
if (!opts || opts.audit !== false) _emitAuditRemove(removed, "operator-remove");
|
|
184
|
+
_emitObs("network.tls.ca.removed", { count: removed.length, reason: "operator" });
|
|
185
|
+
return removed.length;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function removeCaByLabel(label, opts) {
|
|
189
|
+
if (typeof label !== "string" || label.length === 0) {
|
|
190
|
+
throw new TlsTrustError("tls/bad-label", "tls.removeCaByLabel: label must be a non-empty string");
|
|
191
|
+
}
|
|
192
|
+
var removed = [];
|
|
193
|
+
STATE.cas = STATE.cas.filter(function (entry) {
|
|
194
|
+
if (entry.label === label) {
|
|
195
|
+
removed.push(Object.assign({ label: entry.label }, entry.meta));
|
|
196
|
+
return false;
|
|
197
|
+
}
|
|
198
|
+
return true;
|
|
199
|
+
});
|
|
200
|
+
if (removed.length === 0) return 0;
|
|
201
|
+
if (!opts || opts.audit !== false) _emitAuditRemove(removed, "operator-remove-by-label");
|
|
202
|
+
_emitObs("network.tls.ca.removed", { count: removed.length, reason: "label" });
|
|
203
|
+
return removed.length;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function clearAll(opts) {
|
|
207
|
+
if (STATE.cas.length === 0) return 0;
|
|
208
|
+
var removed = STATE.cas.map(function (e) { return Object.assign({ label: e.label }, e.meta); });
|
|
209
|
+
STATE.cas = [];
|
|
210
|
+
if (!opts || opts.audit !== false) _emitAuditRemove(removed, "operator-clear-all");
|
|
211
|
+
_emitObs("network.tls.ca.cleared", { count: removed.length });
|
|
212
|
+
return removed.length;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
function purgeExpired(opts) {
|
|
216
|
+
var nowMs = Date.now();
|
|
217
|
+
var removed = [];
|
|
218
|
+
STATE.cas = STATE.cas.filter(function (entry) {
|
|
219
|
+
var validToMs = entry.meta.validTo ? Date.parse(entry.meta.validTo) : NaN;
|
|
220
|
+
if (isFinite(validToMs) && validToMs < nowMs) {
|
|
221
|
+
removed.push(Object.assign({ label: entry.label }, entry.meta));
|
|
222
|
+
return false;
|
|
223
|
+
}
|
|
224
|
+
return true;
|
|
225
|
+
});
|
|
226
|
+
if (removed.length === 0) return 0;
|
|
227
|
+
if (!opts || opts.audit !== false) _emitAuditRemove(removed, "expired");
|
|
228
|
+
_emitObs("network.tls.ca.purged_expired", { count: removed.length });
|
|
229
|
+
return removed.length;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
function expiringSoon(windowMs) {
|
|
233
|
+
if (typeof windowMs !== "number" || !isFinite(windowMs) || windowMs < 0) {
|
|
234
|
+
throw new TlsTrustError("tls/bad-window", "tls.expiringSoon: windowMs must be a non-negative finite number");
|
|
235
|
+
}
|
|
236
|
+
var threshold = Date.now() + windowMs;
|
|
237
|
+
return STATE.cas.filter(function (entry) {
|
|
238
|
+
var validToMs = entry.meta.validTo ? Date.parse(entry.meta.validTo) : NaN;
|
|
239
|
+
return isFinite(validToMs) && validToMs <= threshold;
|
|
240
|
+
}).map(function (entry) {
|
|
241
|
+
return Object.assign({ label: entry.label }, entry.meta);
|
|
242
|
+
});
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
function captureBaselineFingerprints() {
|
|
246
|
+
STATE.baselineFingerprints = STATE.cas.map(function (e) { return e.meta.fingerprint256; });
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
function detectBaselineDrift() {
|
|
250
|
+
if (!STATE.baselineFingerprints) return null;
|
|
251
|
+
var current = STATE.cas.map(function (e) { return e.meta.fingerprint256; });
|
|
252
|
+
var added = current.filter(function (fp) { return STATE.baselineFingerprints.indexOf(fp) === -1; });
|
|
253
|
+
var removed = STATE.baselineFingerprints.filter(function (fp) { return current.indexOf(fp) === -1; });
|
|
254
|
+
return { added: added, removed: removed, drifted: added.length > 0 || removed.length > 0 };
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
function applyToContext(opts) {
|
|
258
|
+
opts = opts || {};
|
|
259
|
+
validateOpts(opts, ["base"], "tls.applyToContext");
|
|
260
|
+
var base = Object.assign({}, opts.base || {});
|
|
261
|
+
var caStrings = STATE.cas.map(function (e) { return e.pem; });
|
|
262
|
+
if (STATE.systemTrust) {
|
|
263
|
+
var rootCAs = tls.rootCertificates;
|
|
264
|
+
if (Array.isArray(rootCAs)) {
|
|
265
|
+
caStrings = caStrings.concat(rootCAs);
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
if (caStrings.length > 0) base.ca = caStrings;
|
|
269
|
+
return base;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
function getCaPems() {
|
|
273
|
+
return STATE.cas.map(function (e) { return e.pem; });
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
function _emitAuditAdd(metaList, opts) {
|
|
277
|
+
if (opts.audit === false) return;
|
|
278
|
+
var sink;
|
|
279
|
+
try { sink = audit(); } catch (_e) { sink = null; }
|
|
280
|
+
if (!sink || typeof sink.safeEmit !== "function") return;
|
|
281
|
+
for (var i = 0; i < metaList.length; i++) {
|
|
282
|
+
var m = metaList[i];
|
|
283
|
+
try {
|
|
284
|
+
sink.safeEmit({
|
|
285
|
+
action: "network.tls.ca.added",
|
|
286
|
+
outcome: "success",
|
|
287
|
+
metadata: {
|
|
288
|
+
subject: m.subject,
|
|
289
|
+
issuer: m.issuer,
|
|
290
|
+
fingerprint256: m.fingerprint256,
|
|
291
|
+
validFrom: m.validFrom,
|
|
292
|
+
validTo: m.validTo,
|
|
293
|
+
isSelfSigned: m.isSelfSigned,
|
|
294
|
+
label: opts.label || null,
|
|
295
|
+
},
|
|
296
|
+
});
|
|
297
|
+
} catch (_e) { /* audit best-effort — never break the caller */ }
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
function _emitObs(name, fields) {
|
|
302
|
+
try { observability().emit(name, fields || {}); } catch (_e) { /* obs best-effort */ }
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
function _resetForTest() {
|
|
306
|
+
STATE.cas = [];
|
|
307
|
+
STATE.systemTrust = false;
|
|
308
|
+
STATE.baselineFingerprints = null;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
module.exports = {
|
|
312
|
+
addCa: addCa,
|
|
313
|
+
addCaBundle: addCaBundle,
|
|
314
|
+
removeCa: removeCa,
|
|
315
|
+
removeCaByLabel: removeCaByLabel,
|
|
316
|
+
clearAll: clearAll,
|
|
317
|
+
purgeExpired: purgeExpired,
|
|
318
|
+
expiringSoon: expiringSoon,
|
|
319
|
+
useSystemTrust: useSystemTrust,
|
|
320
|
+
isSystemTrustEnabled: isSystemTrustEnabled,
|
|
321
|
+
getTrustStore: getTrustStore,
|
|
322
|
+
captureBaselineFingerprints: captureBaselineFingerprints,
|
|
323
|
+
detectBaselineDrift: detectBaselineDrift,
|
|
324
|
+
applyToContext: applyToContext,
|
|
325
|
+
getCaPems: getCaPems,
|
|
326
|
+
TlsTrustError: TlsTrustError,
|
|
327
|
+
_resetForTest: _resetForTest,
|
|
328
|
+
};
|