@blamejs/core 0.6.70 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +4 -0
- package/README.md +11 -1
- package/index.js +2 -5
- package/lib/api-key.js +24 -55
- package/lib/api-snapshot.js +4 -1
- package/lib/app-shutdown.js +20 -16
- package/lib/app.js +6 -5
- package/lib/archive.js +58 -50
- package/lib/atomic-file.js +11 -5
- package/lib/audit-chain.js +12 -4
- package/lib/audit-sign.js +38 -13
- package/lib/audit-tools.js +4 -4
- package/lib/audit.js +8 -4
- package/lib/auth/jwt.js +3 -2
- package/lib/auth/lockout.js +3 -13
- package/lib/auth/oauth.js +27 -17
- package/lib/auth/passkey.js +1 -5
- package/lib/auth/password.js +23 -13
- package/lib/backup/bundle.js +2 -4
- package/lib/backup/crypto.js +8 -6
- package/lib/backup/index.js +18 -14
- package/lib/backup/manifest.js +6 -2
- package/lib/break-glass.js +12 -7
- package/lib/bundler.js +32 -21
- package/lib/cache-redis.js +8 -3
- package/lib/cache.js +39 -59
- package/lib/canonical-json.js +17 -2
- package/lib/chain-writer.js +2 -2
- package/lib/cli-helpers.js +9 -4
- package/lib/cli.js +27 -5
- package/lib/cluster-provider-db.js +2 -1
- package/lib/cluster-storage.js +71 -11
- package/lib/cluster.js +30 -25
- package/lib/config-drift.js +3 -4
- package/lib/constants.js +8 -3
- package/lib/cookies.js +16 -4
- package/lib/credential-hash.js +4 -7
- package/lib/crypto.js +9 -9
- package/lib/csv.js +11 -8
- package/lib/db-declare-row-policy.js +4 -9
- package/lib/db-declare-view.js +3 -6
- package/lib/db-query.js +9 -3
- package/lib/db-schema.js +75 -10
- package/lib/db.js +116 -70
- package/lib/deprecate.js +3 -8
- package/lib/dev.js +30 -19
- package/lib/dual-control.js +2 -4
- package/lib/error-page.js +32 -39
- package/lib/external-db-migrate.js +23 -26
- package/lib/external-db.js +11 -3
- package/lib/file-type.js +16 -8
- package/lib/forms.js +48 -12
- package/lib/framework-error.js +1 -2
- package/lib/{internal-sha1-hibp.js → framework-sha1-hibp.js} +1 -1
- package/lib/handlers.js +1 -1
- package/lib/http-client-cookie-jar.js +25 -18
- package/lib/http-client.js +46 -23
- package/lib/i18n.js +6 -16
- package/lib/jobs.js +5 -1
- package/lib/log-stream-cloudwatch.js +31 -30
- package/lib/log-stream-local.js +12 -3
- package/lib/log-stream-otlp-grpc.js +29 -23
- package/lib/log-stream-otlp.js +13 -19
- package/lib/log-stream-syslog.js +27 -22
- package/lib/log-stream-webhook.js +8 -17
- package/lib/log-stream.js +13 -2
- package/lib/log.js +83 -5
- package/lib/mail-bounce.js +22 -15
- package/lib/mail-dkim.js +3 -6
- package/lib/mail.js +62 -34
- package/lib/metrics.js +61 -25
- package/lib/middleware/api-encrypt.js +15 -13
- package/lib/middleware/body-parser.js +46 -39
- package/lib/middleware/bot-guard.js +25 -4
- package/lib/middleware/compression.js +26 -11
- package/lib/middleware/cors.js +6 -5
- package/lib/middleware/csp-nonce.js +4 -3
- package/lib/middleware/csrf-protect.js +1 -1
- package/lib/middleware/db-role-for.js +7 -30
- package/lib/middleware/health.js +42 -8
- package/lib/middleware/index.js +55 -36
- package/lib/middleware/rate-limit.js +11 -8
- package/lib/middleware/request-id.js +10 -2
- package/lib/middleware/request-log.js +3 -2
- package/lib/middleware/require-auth.js +5 -2
- package/lib/middleware/sse.js +5 -4
- package/lib/migration-files.js +37 -0
- package/lib/migrations.js +26 -15
- package/lib/mtls-ca.js +38 -17
- package/lib/mtls-engine-default.js +15 -7
- package/lib/network-dns.js +49 -20
- package/lib/network-heartbeat.js +7 -7
- package/lib/network-nts.js +70 -48
- package/lib/network-proxy.js +34 -15
- package/lib/network-tls.js +7 -5
- package/lib/network.js +3 -3
- package/lib/notify.js +14 -36
- package/lib/ntp-check.js +7 -6
- package/lib/numeric-bounds.js +35 -3
- package/lib/object-store/azure-blob-bucket-ops.js +73 -15
- package/lib/object-store/azure-blob.js +31 -24
- package/lib/object-store/gcs-bucket-ops.js +41 -17
- package/lib/object-store/gcs.js +41 -34
- package/lib/object-store/http-put.js +2 -11
- package/lib/object-store/http-request.js +38 -0
- package/lib/object-store/index.js +1 -1
- package/lib/object-store/sigv4-bucket-ops.js +33 -37
- package/lib/object-store/sigv4.js +50 -30
- package/lib/observability.js +14 -3
- package/lib/otel-export.js +16 -14
- package/lib/pagination.js +23 -8
- package/lib/parsers/index.js +11 -5
- package/lib/parsers/safe-env.js +43 -14
- package/lib/parsers/safe-ini.js +29 -10
- package/lib/parsers/safe-toml.js +55 -25
- package/lib/parsers/safe-xml.js +22 -11
- package/lib/parsers/safe-yaml.js +55 -17
- package/lib/permissions.js +24 -33
- package/lib/pqc-agent.js +4 -2
- package/lib/pqc-gate.js +28 -15
- package/lib/protobuf-encoder.js +9 -3
- package/lib/protocol-dispatcher.js +29 -12
- package/lib/pubsub-redis.js +25 -8
- package/lib/pubsub.js +84 -12
- package/lib/queue-local.js +13 -4
- package/lib/queue-redis.js +2 -2
- package/lib/queue-sqs.js +21 -16
- package/lib/queue.js +8 -4
- package/lib/redis-client.js +13 -10
- package/lib/request-helpers.js +33 -2
- package/lib/restore-bundle.js +2 -4
- package/lib/restore-rollback.js +4 -1
- package/lib/restore.js +12 -10
- package/lib/retry.js +58 -9
- package/lib/router.js +96 -27
- package/lib/safe-async.js +97 -4
- package/lib/safe-buffer.js +47 -0
- package/lib/safe-json.js +18 -9
- package/lib/safe-schema.js +54 -18
- package/lib/safe-url.js +6 -2
- package/lib/scheduler.js +25 -14
- package/lib/security-assert.js +31 -26
- package/lib/seeders.js +71 -60
- package/lib/session.js +13 -7
- package/lib/slug.js +35 -8
- package/lib/ssrf-guard.js +58 -43
- package/lib/static.js +11 -7
- package/lib/storage.js +2 -1
- package/lib/template.js +17 -7
- package/lib/testing.js +28 -8
- package/lib/time.js +26 -5
- package/lib/totp.js +18 -9
- package/lib/tracing.js +15 -5
- package/lib/uuid.js +47 -20
- package/lib/validate-opts.js +213 -0
- package/lib/vault/index.js +36 -27
- package/lib/vault/rotate.js +41 -22
- package/lib/vault/wrap.js +26 -19
- package/lib/webhook.js +37 -68
- package/lib/websocket.js +60 -46
- package/package.json +1 -1
- package/sbom.cyclonedx.json +6 -6
- package/lib/auth/totp.js +0 -17
- package/lib/object-store/retry.js +0 -15
package/lib/audit-chain.js
CHANGED
|
@@ -19,10 +19,17 @@
|
|
|
19
19
|
* verification is O(n) and walks every row at boot.
|
|
20
20
|
*/
|
|
21
21
|
var canonicalJson = require("./canonical-json");
|
|
22
|
+
var C = require("./constants");
|
|
22
23
|
var { sha3Hash } = require("./crypto");
|
|
23
24
|
|
|
24
|
-
//
|
|
25
|
-
|
|
25
|
+
// SHA3-512 outputs 64 bytes; routed through C.BYTES so the file's byte
|
|
26
|
+
// arithmetic has one source of truth. Hex-encoded width is twice the
|
|
27
|
+
// byte count.
|
|
28
|
+
var SHA3_512_BYTES = C.BYTES.bytes(64);
|
|
29
|
+
var SHA3_512_HEX_LEN = SHA3_512_BYTES * 2;
|
|
30
|
+
|
|
31
|
+
// All-zero SHA3-512 sentinel prevHash for the first row.
|
|
32
|
+
var ZERO_HASH = "0".repeat(SHA3_512_HEX_LEN);
|
|
26
33
|
|
|
27
34
|
// Canonicalize a row for hashing. Excludes the hash/nonce columns themselves
|
|
28
35
|
// and any caller-specified columns. Sorted keys, JSON-encoded values; Buffer
|
|
@@ -44,8 +51,9 @@ function canonicalize(row, excludeKeys) {
|
|
|
44
51
|
// Compute a row's hash given its predecessor's hash, the row's logical fields
|
|
45
52
|
// (already excluding prevHash, rowHash, nonce), and the row's nonce buffer.
|
|
46
53
|
function computeRowHash(prevHash, rowFields, nonce) {
|
|
47
|
-
if (typeof prevHash !== "string" || prevHash.length !==
|
|
48
|
-
throw new Error("prevHash must be a
|
|
54
|
+
if (typeof prevHash !== "string" || prevHash.length !== SHA3_512_HEX_LEN) {
|
|
55
|
+
throw new Error("prevHash must be a " + SHA3_512_HEX_LEN +
|
|
56
|
+
"-char hex string (SHA3-512); got length " +
|
|
49
57
|
(prevHash && prevHash.length));
|
|
50
58
|
}
|
|
51
59
|
if (!Buffer.isBuffer(nonce) || nonce.length === 0) {
|
package/lib/audit-sign.js
CHANGED
|
@@ -61,12 +61,20 @@ var path = require("path");
|
|
|
61
61
|
var nodeCrypto = require("crypto");
|
|
62
62
|
var atomicFile = require("./atomic-file");
|
|
63
63
|
var { sha3Hash } = require("./crypto");
|
|
64
|
+
var { defineClass } = require("./framework-error");
|
|
64
65
|
var { boot } = require("./log");
|
|
65
66
|
var safeBuffer = require("./safe-buffer");
|
|
66
67
|
var safeJson = require("./safe-json");
|
|
67
68
|
var vaultPassphraseSource = require("./vault/passphrase-source");
|
|
68
69
|
var vaultWrap = require("./vault/wrap");
|
|
69
70
|
|
|
71
|
+
// AuditSignError is thrown by init() for fatal boot-time conditions
|
|
72
|
+
// (conflicting on-disk key files, passphrase rejected, schema invalid).
|
|
73
|
+
// The caller — CLI or app boot — catches and translates to an exit code;
|
|
74
|
+
// lib/ never calls process.exit unilaterally.
|
|
75
|
+
var AuditSignError = defineClass("AuditSignError", { alwaysPermanent: true });
|
|
76
|
+
var _err = AuditSignError.factory;
|
|
77
|
+
|
|
70
78
|
// Default for newly-generated keys. Operators can override at init
|
|
71
79
|
// via opts.algorithm — e.g. `auditSigning: { algorithm: "ml-dsa-87" }`
|
|
72
80
|
// for throughput-sensitive deployments. Existing key files determine
|
|
@@ -131,18 +139,23 @@ var pendingNewKeyAlg = null;
|
|
|
131
139
|
|
|
132
140
|
async function init(opts) {
|
|
133
141
|
if (initialized) return;
|
|
134
|
-
if (!opts || !opts.dataDir)
|
|
142
|
+
if (!opts || !opts.dataDir) {
|
|
143
|
+
throw new AuditSignError("auditSign/bad-init",
|
|
144
|
+
"auditSign.init({ dataDir }) is required");
|
|
145
|
+
}
|
|
135
146
|
|
|
136
147
|
var mode = (opts.mode || "wrapped").toLowerCase();
|
|
137
148
|
if (mode !== "wrapped" && mode !== "plaintext") {
|
|
138
|
-
throw new
|
|
149
|
+
throw new AuditSignError("auditSign/bad-mode",
|
|
150
|
+
"auditSign.init: mode must be 'wrapped' or 'plaintext'");
|
|
139
151
|
}
|
|
140
152
|
// Algorithm-on-generate. Validated against the supported list so
|
|
141
153
|
// typos surface here, not as an opaque "key generation failed"
|
|
142
154
|
// deeper in nodeCrypto.
|
|
143
155
|
var alg = (opts.algorithm || DEFAULT_SIGNING_ALG).toLowerCase();
|
|
144
156
|
if (SUPPORTED_SIGNING_ALGS.indexOf(alg) === -1) {
|
|
145
|
-
throw new
|
|
157
|
+
throw new AuditSignError("auditSign/bad-algorithm",
|
|
158
|
+
"auditSign.init: algorithm must be one of " +
|
|
146
159
|
SUPPORTED_SIGNING_ALGS.join(", ") + " (got: " + alg + ")");
|
|
147
160
|
}
|
|
148
161
|
pendingNewKeyAlg = alg;
|
|
@@ -157,16 +170,16 @@ async function init(opts) {
|
|
|
157
170
|
var hasPlaintext = fs.existsSync(paths.plaintext);
|
|
158
171
|
var hasSealed = fs.existsSync(paths.sealed);
|
|
159
172
|
if (hasPlaintext && hasSealed) {
|
|
160
|
-
|
|
161
|
-
|
|
173
|
+
throw _err("KEY_FILE_CONFLICT",
|
|
174
|
+
"both audit-sign.key and audit-sign.key.sealed exist; resolve manually");
|
|
162
175
|
}
|
|
163
176
|
if (hasSealed && mode === "plaintext") {
|
|
164
|
-
|
|
165
|
-
|
|
177
|
+
throw _err("MODE_MISMATCH",
|
|
178
|
+
"audit-sign.key.sealed exists but mode='plaintext' requested");
|
|
166
179
|
}
|
|
167
180
|
if (hasPlaintext && mode === "wrapped") {
|
|
168
|
-
|
|
169
|
-
|
|
181
|
+
throw _err("MODE_MISMATCH",
|
|
182
|
+
"audit-sign.key (plaintext) exists but mode='wrapped' requested");
|
|
170
183
|
}
|
|
171
184
|
|
|
172
185
|
if (mode === "wrapped") {
|
|
@@ -185,7 +198,10 @@ function _initPlaintext() {
|
|
|
185
198
|
if (fs.existsSync(paths.plaintext)) {
|
|
186
199
|
var loaded;
|
|
187
200
|
try { loaded = safeJson.parse(atomicFile.readSync(paths.plaintext), { schema: SIGNING_KEY_SCHEMA }); }
|
|
188
|
-
catch (e) {
|
|
201
|
+
catch (e) {
|
|
202
|
+
throw _err("KEY_FILE_CORRUPT",
|
|
203
|
+
"audit-sign.key corrupted or schema-invalid at " + paths.plaintext + " - " + e.message);
|
|
204
|
+
}
|
|
189
205
|
var loadedAlg = loaded.algorithm || LEGACY_DEFAULT_ALG;
|
|
190
206
|
keys = {
|
|
191
207
|
publicKey: loaded.publicKey,
|
|
@@ -222,10 +238,16 @@ async function _initWrapped() {
|
|
|
222
238
|
var plaintextBuf;
|
|
223
239
|
try {
|
|
224
240
|
try { plaintextBuf = await vaultWrap.unwrap(sealedBytes, passphrase); }
|
|
225
|
-
catch (e) {
|
|
241
|
+
catch (e) {
|
|
242
|
+
throw _err("PASSPHRASE_REJECTED",
|
|
243
|
+
"audit-signing passphrase rejected (" + e.message + ")");
|
|
244
|
+
}
|
|
226
245
|
var loaded;
|
|
227
246
|
try { loaded = safeJson.parse(plaintextBuf, { schema: SIGNING_KEY_SCHEMA }); }
|
|
228
|
-
catch (e) {
|
|
247
|
+
catch (e) {
|
|
248
|
+
throw _err("UNWRAPPED_INVALID",
|
|
249
|
+
"unwrapped audit-sign.key invalid: " + e.message);
|
|
250
|
+
}
|
|
229
251
|
var loadedAlg = loaded.algorithm || LEGACY_DEFAULT_ALG;
|
|
230
252
|
keys = {
|
|
231
253
|
publicKey: loaded.publicKey,
|
|
@@ -272,7 +294,10 @@ async function _initFirstRunWrapped() {
|
|
|
272
294
|
// ---- Public API ----
|
|
273
295
|
|
|
274
296
|
function _requireInit() {
|
|
275
|
-
if (!initialized)
|
|
297
|
+
if (!initialized) {
|
|
298
|
+
throw new AuditSignError("auditSign/not-initialized",
|
|
299
|
+
"auditSign.init() must be awaited before sign/verify");
|
|
300
|
+
}
|
|
276
301
|
}
|
|
277
302
|
|
|
278
303
|
function sign(payload) {
|
package/lib/audit-tools.js
CHANGED
|
@@ -53,6 +53,7 @@
|
|
|
53
53
|
|
|
54
54
|
var fs = require("fs");
|
|
55
55
|
var path = require("path");
|
|
56
|
+
var pkg = require("../package.json");
|
|
56
57
|
var atomicFile = require("./atomic-file");
|
|
57
58
|
var auditChain = require("./audit-chain");
|
|
58
59
|
var canonicalJson = require("./canonical-json");
|
|
@@ -63,6 +64,8 @@ var lazyRequire = require("./lazy-require");
|
|
|
63
64
|
var jsonSafe = require("./safe-json");
|
|
64
65
|
var { defineClass } = require("./framework-error");
|
|
65
66
|
|
|
67
|
+
var FRAMEWORK_VERSION = (pkg && pkg.version) || "unknown";
|
|
68
|
+
|
|
66
69
|
// Lazy `db` — db requires audit at top-of-file, audit transitively
|
|
67
70
|
// reaches into audit-tools via the operator-supplied default fns,
|
|
68
71
|
// so importing db at audit-tools' top would close the cycle. Lazy
|
|
@@ -275,14 +278,11 @@ async function _writeBundle(args) {
|
|
|
275
278
|
}
|
|
276
279
|
|
|
277
280
|
// 3. Build manifest
|
|
278
|
-
var version;
|
|
279
|
-
try { version = require("../package.json").version; }
|
|
280
|
-
catch (_e) { version = "unknown"; }
|
|
281
281
|
var manifest = {
|
|
282
282
|
format: BUNDLE_FORMAT,
|
|
283
283
|
kind: kind,
|
|
284
284
|
createdAt: Date.now(),
|
|
285
|
-
frameworkVersion:
|
|
285
|
+
frameworkVersion: FRAMEWORK_VERSION,
|
|
286
286
|
rowCount: rows.length,
|
|
287
287
|
range: {
|
|
288
288
|
firstCounter: Number(firstRow.monotonicCounter),
|
package/lib/audit.js
CHANGED
|
@@ -425,9 +425,12 @@ function _redactCriteria(c) {
|
|
|
425
425
|
|
|
426
426
|
// Generate a fresh trace id apps can thread through their request handlers
|
|
427
427
|
// and pass into audit.record() / consent.grant() / etc. via the metadata
|
|
428
|
-
// field.
|
|
428
|
+
// field. Width matches the W3C traceparent trace-id format (16 random
|
|
429
|
+
// bytes hex-encoded → 32 chars). Routed through C.BYTES so the byte
|
|
430
|
+
// count has a single source of truth.
|
|
431
|
+
var TRACE_ID_BYTES = C.BYTES.bytes(16);
|
|
429
432
|
function beginTrace() {
|
|
430
|
-
return generateToken(
|
|
433
|
+
return generateToken(TRACE_ID_BYTES);
|
|
431
434
|
}
|
|
432
435
|
|
|
433
436
|
// ---- Checkpoints (tamper-proof external anchor) ----
|
|
@@ -485,7 +488,7 @@ async function checkpoint(opts) {
|
|
|
485
488
|
var signature = auditSign.sign(payload);
|
|
486
489
|
var pubFp = auditSign.getPublicKeyFingerprint();
|
|
487
490
|
|
|
488
|
-
var ckptId = generateToken(
|
|
491
|
+
var ckptId = generateToken(TRACE_ID_BYTES);
|
|
489
492
|
var fencingToken = cluster.fencingToken();
|
|
490
493
|
await _insertCheckpoint(
|
|
491
494
|
[ckptId, createdAt, counter, tip.rowHash, signature, pubFp, fencingToken]
|
|
@@ -648,7 +651,8 @@ function _resetForTest() {
|
|
|
648
651
|
// an in-flight drain that's mid-batch when reset fires bails out
|
|
649
652
|
// instead of writing the rest of the batch to the new database.
|
|
650
653
|
if (_auditHandler) {
|
|
651
|
-
try { _auditHandler.shutdownSync("audit._resetForTest"); }
|
|
654
|
+
try { _auditHandler.shutdownSync("audit._resetForTest"); }
|
|
655
|
+
catch (e) { log.debug("reset-handler-shutdown-failed: " + (e && e.message || e)); }
|
|
652
656
|
_auditHandler = null;
|
|
653
657
|
}
|
|
654
658
|
}
|
package/lib/auth/jwt.js
CHANGED
|
@@ -71,6 +71,7 @@
|
|
|
71
71
|
* vs "not yet valid" UX, audit "bad-signature" attempts separately).
|
|
72
72
|
*/
|
|
73
73
|
var nodeCrypto = require("crypto");
|
|
74
|
+
var C = require("../constants");
|
|
74
75
|
var safeJson = require("../safe-json");
|
|
75
76
|
var { AuthError } = require("../framework-error");
|
|
76
77
|
|
|
@@ -132,7 +133,7 @@ async function sign(claims, opts) {
|
|
|
132
133
|
var key = _toKeyObject(opts.privateKey, "private");
|
|
133
134
|
|
|
134
135
|
var nowMs = opts.now || Date.now();
|
|
135
|
-
var nowSec = Math.floor(nowMs /
|
|
136
|
+
var nowSec = Math.floor(nowMs / C.TIME.seconds(1));
|
|
136
137
|
|
|
137
138
|
// Build the claims object. Operator-provided claim values take
|
|
138
139
|
// precedence over opts shorthand to avoid surprising overrides.
|
|
@@ -242,7 +243,7 @@ async function verify(token, opts) {
|
|
|
242
243
|
}
|
|
243
244
|
|
|
244
245
|
// Time-based claim validation
|
|
245
|
-
var nowSec = Math.floor((opts.now || Date.now()) /
|
|
246
|
+
var nowSec = Math.floor((opts.now || Date.now()) / C.TIME.seconds(1));
|
|
246
247
|
if (opts.clockToleranceSec !== undefined && opts.clockToleranceSec !== null) {
|
|
247
248
|
if (typeof opts.clockToleranceSec !== "number" ||
|
|
248
249
|
!isFinite(opts.clockToleranceSec) ||
|
package/lib/auth/lockout.js
CHANGED
|
@@ -184,19 +184,9 @@ function create(opts) {
|
|
|
184
184
|
}
|
|
185
185
|
}
|
|
186
186
|
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
}
|
|
191
|
-
}
|
|
192
|
-
if (opts.observability !== undefined && opts.observability !== null) {
|
|
193
|
-
if (typeof opts.observability !== "object" || typeof opts.observability.event !== "function") {
|
|
194
|
-
throw _err("BAD_OPT", "auth.lockout.create: observability must be b.observability-shaped (event fn)");
|
|
195
|
-
}
|
|
196
|
-
}
|
|
197
|
-
if (opts.clock !== undefined && typeof opts.clock !== "function") {
|
|
198
|
-
throw _err("BAD_OPT", "auth.lockout.create: clock must be a function or undefined");
|
|
199
|
-
}
|
|
187
|
+
validateOpts.auditShape(opts.audit, "auth.lockout.create", LockoutError);
|
|
188
|
+
validateOpts.observabilityShape(opts.observability, "auth.lockout.create", LockoutError);
|
|
189
|
+
validateOpts.optionalFunction(opts.clock, "auth.lockout.create: clock", LockoutError);
|
|
200
190
|
|
|
201
191
|
var cache = opts.cache;
|
|
202
192
|
var namespace = opts.namespace;
|
package/lib/auth/oauth.js
CHANGED
|
@@ -106,9 +106,11 @@
|
|
|
106
106
|
|
|
107
107
|
var nodeCrypto = require("node:crypto");
|
|
108
108
|
var cache = require("../cache");
|
|
109
|
+
var C = require("../constants");
|
|
110
|
+
var { generateBytes } = require("../crypto");
|
|
111
|
+
var httpClient = require("../http-client");
|
|
109
112
|
var safeJson = require("../safe-json");
|
|
110
113
|
var safeUrl = require("../safe-url");
|
|
111
|
-
var C = require("../constants");
|
|
112
114
|
var { defineClass } = require("../framework-error");
|
|
113
115
|
var { boot } = require("../log");
|
|
114
116
|
|
|
@@ -193,6 +195,18 @@ var DEFAULT_ACCEPTED_ALGS = Object.freeze([
|
|
|
193
195
|
var DEFAULT_DISCOVERY_CACHE_MS = C.TIME.hours(1);
|
|
194
196
|
var DEFAULT_CLOCK_SKEW_MS = C.TIME.minutes(1);
|
|
195
197
|
|
|
198
|
+
// Random material lengths. PKCE verifier per RFC 7636 §4.1 needs >= 256
|
|
199
|
+
// bits of entropy; 32 bytes hits that exactly. State + nonce are 16
|
|
200
|
+
// bytes (128-bit unguessability) which is the minimum recommended by
|
|
201
|
+
// OAuth 2.0 Threat Model §4.4.1.8 / §4.4.1.13.
|
|
202
|
+
var PKCE_VERIFIER_BYTES = C.BYTES.bytes(32);
|
|
203
|
+
var STATE_NONCE_BYTES = C.BYTES.bytes(16);
|
|
204
|
+
// JOSE PSS salt lengths (RFC 7518 §3.5) match the hash-output size:
|
|
205
|
+
// PS256/SHA-256 → 32, PS384/SHA-384 → 48, PS512/SHA-512 → 64.
|
|
206
|
+
var PSS_SALT_BYTES_SHA256 = C.BYTES.bytes(32);
|
|
207
|
+
var PSS_SALT_BYTES_SHA384 = C.BYTES.bytes(48);
|
|
208
|
+
var PSS_SALT_BYTES_SHA512 = C.BYTES.bytes(64);
|
|
209
|
+
|
|
196
210
|
// ---- helpers ----
|
|
197
211
|
|
|
198
212
|
function _b64urlEncode(buf) {
|
|
@@ -208,13 +222,13 @@ function _b64urlDecode(s) {
|
|
|
208
222
|
}
|
|
209
223
|
|
|
210
224
|
function _generateRandomToken(bytes) {
|
|
211
|
-
return _b64urlEncode(
|
|
225
|
+
return _b64urlEncode(generateBytes(bytes));
|
|
212
226
|
}
|
|
213
227
|
|
|
214
228
|
function _generatePkce() {
|
|
215
229
|
// RFC 7636: code_verifier is 43–128 chars [A-Za-z0-9-._~].
|
|
216
230
|
// base64url of 32 random bytes = 43 chars, all valid.
|
|
217
|
-
var verifier = _b64urlEncode(
|
|
231
|
+
var verifier = _b64urlEncode(generateBytes(PKCE_VERIFIER_BYTES));
|
|
218
232
|
var challenge = _b64urlEncode(nodeCrypto.createHash("sha256").update(verifier).digest());
|
|
219
233
|
return { verifier: verifier, challenge: challenge };
|
|
220
234
|
}
|
|
@@ -250,9 +264,9 @@ function _verifyParamsForAlg(alg) {
|
|
|
250
264
|
if (alg === "RS256") return { hash: "sha256", padding: nodeCrypto.constants.RSA_PKCS1_PADDING };
|
|
251
265
|
if (alg === "RS384") return { hash: "sha384", padding: nodeCrypto.constants.RSA_PKCS1_PADDING };
|
|
252
266
|
if (alg === "RS512") return { hash: "sha512", padding: nodeCrypto.constants.RSA_PKCS1_PADDING };
|
|
253
|
-
if (alg === "PS256") return { hash: "sha256", padding: nodeCrypto.constants.RSA_PKCS1_PSS_PADDING, saltLength:
|
|
254
|
-
if (alg === "PS384") return { hash: "sha384", padding: nodeCrypto.constants.RSA_PKCS1_PSS_PADDING, saltLength:
|
|
255
|
-
if (alg === "PS512") return { hash: "sha512", padding: nodeCrypto.constants.RSA_PKCS1_PSS_PADDING, saltLength:
|
|
267
|
+
if (alg === "PS256") return { hash: "sha256", padding: nodeCrypto.constants.RSA_PKCS1_PSS_PADDING, saltLength: PSS_SALT_BYTES_SHA256 };
|
|
268
|
+
if (alg === "PS384") return { hash: "sha384", padding: nodeCrypto.constants.RSA_PKCS1_PSS_PADDING, saltLength: PSS_SALT_BYTES_SHA384 };
|
|
269
|
+
if (alg === "PS512") return { hash: "sha512", padding: nodeCrypto.constants.RSA_PKCS1_PSS_PADDING, saltLength: PSS_SALT_BYTES_SHA512 };
|
|
256
270
|
if (alg === "ES256") return { hash: "sha256", dsaEncoding: "ieee-p1363" };
|
|
257
271
|
if (alg === "ES384") return { hash: "sha384", dsaEncoding: "ieee-p1363" };
|
|
258
272
|
if (alg === "ES512") return { hash: "sha512", dsaEncoding: "ieee-p1363" };
|
|
@@ -346,13 +360,9 @@ function create(opts) {
|
|
|
346
360
|
ttlMs: jwksCacheMs,
|
|
347
361
|
});
|
|
348
362
|
|
|
349
|
-
function _httpClient() {
|
|
350
|
-
return require("../http-client");
|
|
351
|
-
}
|
|
352
|
-
|
|
353
363
|
async function _fetchJson(url, fetchOpts) {
|
|
354
364
|
fetchOpts = fetchOpts || {};
|
|
355
|
-
var hc =
|
|
365
|
+
var hc = httpClient;
|
|
356
366
|
var req = Object.assign({
|
|
357
367
|
url: url,
|
|
358
368
|
method: "GET",
|
|
@@ -417,8 +427,8 @@ function create(opts) {
|
|
|
417
427
|
async function authorizationUrl(uopts) {
|
|
418
428
|
uopts = uopts || {};
|
|
419
429
|
var endpoint = await _resolveEndpoint("authorizationEndpoint");
|
|
420
|
-
var state = uopts.state || _generateRandomToken(
|
|
421
|
-
var nonce = uopts.nonce || (isOidc ? _generateRandomToken(
|
|
430
|
+
var state = uopts.state || _generateRandomToken(STATE_NONCE_BYTES);
|
|
431
|
+
var nonce = uopts.nonce || (isOidc ? _generateRandomToken(STATE_NONCE_BYTES) : null);
|
|
422
432
|
var pkceVals = pkce ? _generatePkce() : null;
|
|
423
433
|
var params = new URLSearchParams();
|
|
424
434
|
params.set("response_type", "code");
|
|
@@ -515,7 +525,7 @@ function create(opts) {
|
|
|
515
525
|
if (ropts.type) body.set("token_type_hint", ropts.type);
|
|
516
526
|
body.set("client_id", clientId);
|
|
517
527
|
if (clientSecret) body.set("client_secret", clientSecret);
|
|
518
|
-
var hc =
|
|
528
|
+
var hc = httpClient;
|
|
519
529
|
var req = {
|
|
520
530
|
url: endpoint,
|
|
521
531
|
method: "POST",
|
|
@@ -534,7 +544,7 @@ function create(opts) {
|
|
|
534
544
|
}
|
|
535
545
|
|
|
536
546
|
async function _postForm(endpoint, body) {
|
|
537
|
-
var hc =
|
|
547
|
+
var hc = httpClient;
|
|
538
548
|
var req = {
|
|
539
549
|
url: endpoint,
|
|
540
550
|
method: "POST",
|
|
@@ -651,8 +661,8 @@ function create(opts) {
|
|
|
651
661
|
}
|
|
652
662
|
|
|
653
663
|
// Claim validation.
|
|
654
|
-
var now = Math.floor(Date.now() /
|
|
655
|
-
var skewSec = Math.floor(clockSkewMs /
|
|
664
|
+
var now = Math.floor(Date.now() / C.TIME.seconds(1));
|
|
665
|
+
var skewSec = Math.floor(clockSkewMs / C.TIME.seconds(1));
|
|
656
666
|
if (typeof payload.exp !== "number" || payload.exp + skewSec < now) {
|
|
657
667
|
throw new OAuthError("auth-oauth/expired", "ID token expired (exp=" + payload.exp + ", now=" + now + ")");
|
|
658
668
|
}
|
package/lib/auth/passkey.js
CHANGED
|
@@ -57,14 +57,10 @@
|
|
|
57
57
|
* sessions, audit, or DB. Routes integrate that themselves; the
|
|
58
58
|
* primitive stays the smallest correct surface.
|
|
59
59
|
*/
|
|
60
|
+
var _wa = require("../vendor/simplewebauthn-server.cjs");
|
|
60
61
|
var { AuthError } = require("../framework-error");
|
|
61
62
|
|
|
62
|
-
// Lazy require: the bundle is ~400 KB and not every blamejs deployment
|
|
63
|
-
// uses passkeys. Loading on first call keeps boot fast for non-passkey
|
|
64
|
-
// apps without forcing operators to gate the import behind a flag.
|
|
65
|
-
var _wa = null;
|
|
66
63
|
function _vendor() {
|
|
67
|
-
if (!_wa) _wa = require("../vendor/simplewebauthn-server.cjs");
|
|
68
64
|
return _wa;
|
|
69
65
|
}
|
|
70
66
|
|
package/lib/auth/password.js
CHANGED
|
@@ -47,21 +47,28 @@
|
|
|
47
47
|
* "the call shape was wrong" (empty plain, oversize plain).
|
|
48
48
|
*/
|
|
49
49
|
var argon2 = require("../vendor/argon2");
|
|
50
|
+
var C = require("../constants");
|
|
50
51
|
var httpClient = require("../http-client");
|
|
51
|
-
var
|
|
52
|
+
var hibpSha1 = require("../framework-sha1-hibp");
|
|
52
53
|
var safeUrl = require("../safe-url");
|
|
53
54
|
var timingSafeEqual = require("../crypto").timingSafeEqual;
|
|
54
55
|
var { AuthError } = require("../framework-error");
|
|
55
56
|
|
|
56
57
|
// Tuning targets ~250–500ms on commodity 2026 hardware. memoryCost
|
|
57
|
-
// is in KiB per Argon2's parameter convention.
|
|
58
|
+
// is in KiB per Argon2's parameter convention. 64 MiB expressed as
|
|
59
|
+
// KiB through C.BYTES.kib so the framework's byte math has a single
|
|
60
|
+
// source of truth.
|
|
58
61
|
var DEFAULT_PARAMS = Object.freeze({
|
|
59
|
-
memoryCost:
|
|
62
|
+
memoryCost: C.BYTES.kib(64), // 64 MiB
|
|
60
63
|
timeCost: 3,
|
|
61
64
|
parallelism: 4,
|
|
62
65
|
});
|
|
63
66
|
|
|
64
|
-
|
|
67
|
+
// Plaintext upper bound. NIST 800-63B requires >= 64 chars; 4 KiB is
|
|
68
|
+
// the framework's defense against amplification attacks (a hostile
|
|
69
|
+
// caller submitting a multi-megabyte "password" would otherwise burn
|
|
70
|
+
// Argon2 cycles for no security gain).
|
|
71
|
+
var MAX_PLAINTEXT_BYTES = C.BYTES.kib(4);
|
|
65
72
|
|
|
66
73
|
// ---- Policy primitive ----------------------------------------------
|
|
67
74
|
//
|
|
@@ -113,14 +120,14 @@ var MAX_PLAINTEXT_BYTES = 4096;
|
|
|
113
120
|
// await policy.reuseProhibited(plain, [oldHash1, oldHash2, oldHash3]);
|
|
114
121
|
//
|
|
115
122
|
// breachCheck:"haveibeenpwned" uses the HIBP k-anonymity API; the
|
|
116
|
-
// SHA-1 hash is computed in-process via lib/
|
|
123
|
+
// SHA-1 hash is computed in-process via lib/framework-sha1-hibp.js
|
|
117
124
|
// (NOT exported on b.crypto — see comment in lib/crypto.js). Only
|
|
118
125
|
// the first 5 hex chars cross the wire. Rate-limit and failure-mode
|
|
119
126
|
// are operator's call: an HIBP outage returns
|
|
120
127
|
// { ok: true, breachCheckSkipped: true } by default; failClosed:true
|
|
121
128
|
// rejects.
|
|
122
129
|
var DEFAULT_POLICY = Object.freeze({
|
|
123
|
-
minLength:
|
|
130
|
+
minLength: 0x08, // NIST floor — hex literal form
|
|
124
131
|
maxLength: MAX_PLAINTEXT_BYTES,
|
|
125
132
|
forbidCommon: [],
|
|
126
133
|
// The bundled top-10000 list ships in lib/vendor/common-passwords-top-10000.txt
|
|
@@ -133,7 +140,7 @@ var DEFAULT_POLICY = Object.freeze({
|
|
|
133
140
|
breachThreshold: 1,
|
|
134
141
|
failClosed: false,
|
|
135
142
|
hibpEndpoint: "https://api.pwnedpasswords.com",
|
|
136
|
-
hibpTimeoutMs:
|
|
143
|
+
hibpTimeoutMs: C.TIME.seconds(1.5),
|
|
137
144
|
// Rotation policy (PCI 8.3.9 / NYDFS / industry-specific). null = no rotation.
|
|
138
145
|
mustRotateAfterMs: null,
|
|
139
146
|
// History reuse (PCI 8.3.7 floor: last-4). 0 = disabled.
|
|
@@ -160,8 +167,11 @@ var COMPLEXITY_DEFAULT = Object.freeze({
|
|
|
160
167
|
// policy opts so the operator can still override individual fields.
|
|
161
168
|
var POLICY_PROFILES = Object.freeze({
|
|
162
169
|
// NIST 800-63B AAL2 baseline — length + breach, no composition.
|
|
170
|
+
// §5.1.1.2 mandates an 8-character floor; routed through C.BYTES.bytes
|
|
171
|
+
// so every length-shaped integer in the framework reads through the
|
|
172
|
+
// same single source of truth.
|
|
163
173
|
"nist-aal2": Object.freeze({
|
|
164
|
-
minLength: 8,
|
|
174
|
+
minLength: C.BYTES.bytes(8),
|
|
165
175
|
breachCheck: "haveibeenpwned",
|
|
166
176
|
}),
|
|
167
177
|
// PCI-DSS 4.0 §8.3 — 12-char min, 90-day rotation, history of 4.
|
|
@@ -170,7 +180,7 @@ var POLICY_PROFILES = Object.freeze({
|
|
|
170
180
|
"pci-4.0": Object.freeze({
|
|
171
181
|
minLength: 12,
|
|
172
182
|
breachCheck: "haveibeenpwned",
|
|
173
|
-
mustRotateAfterMs: 90
|
|
183
|
+
mustRotateAfterMs: C.TIME.days(90),
|
|
174
184
|
historyMinDistance: 4,
|
|
175
185
|
}),
|
|
176
186
|
// HIPAA 164.308 — "procedures for creating, changing, and
|
|
@@ -181,7 +191,7 @@ var POLICY_PROFILES = Object.freeze({
|
|
|
181
191
|
"hipaa-aal2": Object.freeze({
|
|
182
192
|
minLength: 12,
|
|
183
193
|
breachCheck: "haveibeenpwned",
|
|
184
|
-
mustRotateAfterMs: 180
|
|
194
|
+
mustRotateAfterMs: C.TIME.days(180),
|
|
185
195
|
historyMinDistance: 4,
|
|
186
196
|
complexity: {
|
|
187
197
|
minCategories: 3,
|
|
@@ -405,9 +415,9 @@ function policy(opts) {
|
|
|
405
415
|
if (p.breachCheck === "haveibeenpwned") {
|
|
406
416
|
// HIBP k-anonymity: send the first 5 hex chars of the SHA-1
|
|
407
417
|
// hash, scan the returned suffix list. The framework's only
|
|
408
|
-
// SHA-1 usage; HIBP requires it. (See lib/
|
|
418
|
+
// SHA-1 usage; HIBP requires it. (See lib/framework-sha1-hibp.js
|
|
409
419
|
// for the restriction rationale.)
|
|
410
|
-
var sha1Full =
|
|
420
|
+
var sha1Full = hibpSha1.sha1Hex(plaintext).toUpperCase();
|
|
411
421
|
var prefix = sha1Full.slice(0, 5);
|
|
412
422
|
var suffix = sha1Full.slice(5);
|
|
413
423
|
var url = p.hibpEndpoint.replace(/\/+$/, "") + "/range/" + prefix;
|
|
@@ -514,7 +524,7 @@ function _validatePlain(plain) {
|
|
|
514
524
|
|
|
515
525
|
function _resolveParams(opts) {
|
|
516
526
|
var p = Object.assign({}, DEFAULT_PARAMS, opts || {});
|
|
517
|
-
if (typeof p.memoryCost !== "number" || p.memoryCost <
|
|
527
|
+
if (typeof p.memoryCost !== "number" || p.memoryCost < C.BYTES.kib(1)) {
|
|
518
528
|
throw new AuthError("auth-password/bad-params",
|
|
519
529
|
"memoryCost must be >= 1024 KiB (1 MiB)");
|
|
520
530
|
}
|
package/lib/backup/bundle.js
CHANGED
|
@@ -51,6 +51,7 @@ var path = require("path");
|
|
|
51
51
|
var atomicFile = require("../atomic-file");
|
|
52
52
|
var backupCrypto = require("./crypto");
|
|
53
53
|
var backupManifest = require("./manifest");
|
|
54
|
+
var validateOpts = require("../validate-opts");
|
|
54
55
|
var { defineClass } = require("../framework-error");
|
|
55
56
|
|
|
56
57
|
var BackupBundleError = defineClass("BackupBundleError", { alwaysPermanent: true });
|
|
@@ -78,10 +79,7 @@ async function create(opts) {
|
|
|
78
79
|
throw new BackupBundleError("backup-bundle/no-datadir",
|
|
79
80
|
"create: opts.dataDir is required and must exist");
|
|
80
81
|
}
|
|
81
|
-
|
|
82
|
-
throw new BackupBundleError("backup-bundle/no-outdir",
|
|
83
|
-
"create: opts.outDir is required");
|
|
84
|
-
}
|
|
82
|
+
validateOpts.requireNonEmptyString(opts.outDir, "create: opts.outDir", BackupBundleError, "backup-bundle/no-outdir");
|
|
85
83
|
if (fs.existsSync(opts.outDir)) {
|
|
86
84
|
throw new BackupBundleError("backup-bundle/outdir-exists",
|
|
87
85
|
"create: outDir already exists: " + opts.outDir +
|
package/lib/backup/crypto.js
CHANGED
|
@@ -42,6 +42,8 @@
|
|
|
42
42
|
*/
|
|
43
43
|
|
|
44
44
|
var nodeCrypto = require("node:crypto");
|
|
45
|
+
var C = require("../constants");
|
|
46
|
+
var safeBuffer = require("../safe-buffer");
|
|
45
47
|
var { xchacha20poly1305 } = require("../vendor/noble-ciphers.cjs");
|
|
46
48
|
var argon2 = require("../vendor/argon2");
|
|
47
49
|
var { FrameworkError } = require("../framework-error");
|
|
@@ -60,16 +62,16 @@ class BackupCryptoError extends FrameworkError {
|
|
|
60
62
|
// derivation surfaces. Tuned for: ~64 MiB memory, 3 passes, 4 lanes,
|
|
61
63
|
// 32-byte raw output (matches XChaCha20-Poly1305 key length).
|
|
62
64
|
var ARGON2_OPTS = Object.freeze({
|
|
63
|
-
type: 2,
|
|
64
|
-
memoryCost:
|
|
65
|
+
type: 2, // 2 = argon2id
|
|
66
|
+
memoryCost: C.BYTES.kib(64), // 64 MiB expressed in argon2's KiB units
|
|
65
67
|
timeCost: 3,
|
|
66
68
|
parallelism: 4,
|
|
67
|
-
hashLength: 32,
|
|
69
|
+
hashLength: C.BYTES.bytes(32), // matches XChaCha20-Poly1305 key length
|
|
68
70
|
raw: true,
|
|
69
71
|
});
|
|
70
72
|
|
|
71
|
-
var SALT_BYTES = 32; // 256 bits — comfortable margin for global uniqueness
|
|
72
|
-
var NONCE_BYTES = 24; // XChaCha20 nonce length
|
|
73
|
+
var SALT_BYTES = C.BYTES.bytes(32); // 256 bits — comfortable margin for global uniqueness
|
|
74
|
+
var NONCE_BYTES = C.BYTES.bytes(24); // XChaCha20 nonce length
|
|
73
75
|
|
|
74
76
|
function checksum(buf) {
|
|
75
77
|
if (!Buffer.isBuffer(buf) && typeof buf !== "string") {
|
|
@@ -80,7 +82,7 @@ function checksum(buf) {
|
|
|
80
82
|
}
|
|
81
83
|
|
|
82
84
|
function _validateSaltHex(saltHex) {
|
|
83
|
-
if (
|
|
85
|
+
if (!safeBuffer.isHex(saltHex) || saltHex.length % 2 !== 0) {
|
|
84
86
|
throw new BackupCryptoError("backup-crypto/bad-salt",
|
|
85
87
|
"saltHex must be a non-empty hex string with even length");
|
|
86
88
|
}
|
package/lib/backup/index.js
CHANGED
|
@@ -66,7 +66,13 @@ var crypto = require("../crypto");
|
|
|
66
66
|
var atomicFile = require("../atomic-file");
|
|
67
67
|
var backupBundle = require("./bundle");
|
|
68
68
|
var lazyRequire = require("../lazy-require");
|
|
69
|
+
var validateOpts = require("../validate-opts");
|
|
69
70
|
var audit = lazyRequire(function () { return require("../audit"); });
|
|
71
|
+
// lazyRequire ../db so backup stays a leaf module operators can use
|
|
72
|
+
// without the rest of the framework's DB chain loaded in the same
|
|
73
|
+
// module graph (CLI tools, stand-alone backup runners). The db()
|
|
74
|
+
// callable resolves on first access.
|
|
75
|
+
var dbModuleLazy = lazyRequire(function () { return require("../db"); });
|
|
70
76
|
var { defineClass } = require("../framework-error");
|
|
71
77
|
|
|
72
78
|
var BackupError = defineClass("BackupError");
|
|
@@ -75,8 +81,14 @@ var BackupError = defineClass("BackupError");
|
|
|
75
81
|
// (ISO with ':'+'.' replaced by '-') plus a random suffix.
|
|
76
82
|
var BUNDLE_ID_RE = /^\d{4}-\d{2}-\d{2}T\d{2}-\d{2}-\d{2}-\d{3}Z-[0-9a-f]{8}$/;
|
|
77
83
|
|
|
84
|
+
// Cap on bundle-id length BEFORE the regex test — a malicious /
|
|
85
|
+
// pathological input can't make the regex engine spin if we bound
|
|
86
|
+
// length first. Bundle IDs in the wild are 32-ish chars; a 128-cap
|
|
87
|
+
// is comfortable.
|
|
88
|
+
var BUNDLE_ID_MAX_LEN = 0x80;
|
|
78
89
|
function _isValidBundleId(s) {
|
|
79
|
-
return typeof s === "string" &&
|
|
90
|
+
return typeof s === "string" && s.length > 0 &&
|
|
91
|
+
s.length <= BUNDLE_ID_MAX_LEN && BUNDLE_ID_RE.test(s);
|
|
80
92
|
}
|
|
81
93
|
|
|
82
94
|
function _generateBundleId() {
|
|
@@ -98,10 +110,7 @@ function _dirSize(p) {
|
|
|
98
110
|
|
|
99
111
|
function localStorage(opts) {
|
|
100
112
|
opts = opts || {};
|
|
101
|
-
|
|
102
|
-
throw new BackupError("backup/no-storage-root",
|
|
103
|
-
"localStorage: opts.root is required");
|
|
104
|
-
}
|
|
113
|
+
validateOpts.requireNonEmptyString(opts.root, "localStorage: opts.root", BackupError, "backup/no-storage-root");
|
|
105
114
|
var root = opts.root;
|
|
106
115
|
|
|
107
116
|
function _bundlePath(bundleId) {
|
|
@@ -250,16 +259,11 @@ function create(opts) {
|
|
|
250
259
|
// running backup against an external db handle still work).
|
|
251
260
|
if (flushBeforeBackup === null && opts.flushBeforeBackup !== false) {
|
|
252
261
|
try {
|
|
253
|
-
|
|
254
|
-
// without forcing operators to also bring in lib/db. Hoisting to
|
|
255
|
-
// top-of-file would require everyone using `b.backup` from
|
|
256
|
-
// outside the framework to have ./db loadable in the same module
|
|
257
|
-
// graph, which CLI tools and stand-alone backup runners don't.
|
|
258
|
-
var dbModule = require("../db");
|
|
262
|
+
var dbModule = dbModuleLazy();
|
|
259
263
|
if (typeof dbModule.flushToDisk === "function") {
|
|
260
264
|
flushBeforeBackup = function () { dbModule.flushToDisk(); };
|
|
261
265
|
}
|
|
262
|
-
} catch (_e) { /* db not available
|
|
266
|
+
} catch (_e) { /* db not available in this module graph — flush is a no-op */ }
|
|
263
267
|
}
|
|
264
268
|
|
|
265
269
|
function _emitAudit(action, info, outcome) {
|
|
@@ -320,7 +324,7 @@ function create(opts) {
|
|
|
320
324
|
progressCallback: runOpts.progressCallback,
|
|
321
325
|
});
|
|
322
326
|
} catch (e) {
|
|
323
|
-
try { fs.rmSync(stagingDir, { recursive: true, force: true }); } catch (_e) {}
|
|
327
|
+
try { fs.rmSync(stagingDir, { recursive: true, force: true }); } catch (_e) { /* best-effort tmpdir cleanup */ }
|
|
324
328
|
_emitAudit("backup.failure",
|
|
325
329
|
{ bundleId: bundleId, reason: (e && e.message) || String(e) }, "failure");
|
|
326
330
|
throw e;
|
|
@@ -329,7 +333,7 @@ function create(opts) {
|
|
|
329
333
|
try {
|
|
330
334
|
await storage.writeBundle(bundleId, stagingDir);
|
|
331
335
|
} catch (e) {
|
|
332
|
-
try { fs.rmSync(stagingDir, { recursive: true, force: true }); } catch (_e) {}
|
|
336
|
+
try { fs.rmSync(stagingDir, { recursive: true, force: true }); } catch (_e) { /* best-effort tmpdir cleanup */ }
|
|
333
337
|
_emitAudit("backup.failure",
|
|
334
338
|
{ bundleId: bundleId, reason: "storage.writeBundle: " + ((e && e.message) || String(e)) },
|
|
335
339
|
"failure");
|