@blamejs/core 0.6.69 → 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.
Files changed (164) hide show
  1. package/CHANGELOG.md +6 -0
  2. package/README.md +11 -1
  3. package/index.js +2 -5
  4. package/lib/api-key.js +24 -55
  5. package/lib/api-snapshot.js +4 -1
  6. package/lib/app-shutdown.js +20 -16
  7. package/lib/app.js +6 -5
  8. package/lib/archive.js +58 -50
  9. package/lib/atomic-file.js +11 -5
  10. package/lib/audit-chain.js +12 -4
  11. package/lib/audit-sign.js +38 -13
  12. package/lib/audit-tools.js +4 -4
  13. package/lib/audit.js +8 -4
  14. package/lib/auth/jwt.js +3 -2
  15. package/lib/auth/lockout.js +3 -13
  16. package/lib/auth/oauth.js +27 -17
  17. package/lib/auth/passkey.js +1 -5
  18. package/lib/auth/password.js +23 -13
  19. package/lib/backup/bundle.js +2 -4
  20. package/lib/backup/crypto.js +8 -6
  21. package/lib/backup/index.js +18 -14
  22. package/lib/backup/manifest.js +6 -2
  23. package/lib/break-glass.js +12 -7
  24. package/lib/bundler.js +32 -21
  25. package/lib/cache-redis.js +8 -3
  26. package/lib/cache.js +39 -59
  27. package/lib/canonical-json.js +17 -2
  28. package/lib/chain-writer.js +2 -2
  29. package/lib/cli-helpers.js +9 -4
  30. package/lib/cli.js +27 -5
  31. package/lib/cluster-provider-db.js +2 -1
  32. package/lib/cluster-storage.js +71 -11
  33. package/lib/cluster.js +30 -25
  34. package/lib/config-drift.js +3 -4
  35. package/lib/constants.js +8 -3
  36. package/lib/cookies.js +16 -4
  37. package/lib/credential-hash.js +4 -7
  38. package/lib/crypto.js +9 -9
  39. package/lib/csv.js +11 -8
  40. package/lib/db-declare-row-policy.js +4 -9
  41. package/lib/db-declare-view.js +3 -6
  42. package/lib/db-query.js +9 -3
  43. package/lib/db-schema.js +75 -10
  44. package/lib/db.js +116 -70
  45. package/lib/deprecate.js +3 -8
  46. package/lib/dev.js +30 -19
  47. package/lib/dual-control.js +2 -4
  48. package/lib/error-page.js +32 -39
  49. package/lib/external-db-migrate.js +27 -20
  50. package/lib/external-db.js +11 -3
  51. package/lib/file-type.js +16 -8
  52. package/lib/forms.js +48 -12
  53. package/lib/framework-error.js +1 -2
  54. package/lib/{internal-sha1-hibp.js → framework-sha1-hibp.js} +1 -1
  55. package/lib/handlers.js +1 -1
  56. package/lib/http-client-cookie-jar.js +25 -18
  57. package/lib/http-client.js +46 -23
  58. package/lib/i18n.js +6 -16
  59. package/lib/jobs.js +5 -1
  60. package/lib/log-stream-cloudwatch.js +31 -30
  61. package/lib/log-stream-local.js +12 -3
  62. package/lib/log-stream-otlp-grpc.js +29 -23
  63. package/lib/log-stream-otlp.js +13 -19
  64. package/lib/log-stream-syslog.js +27 -22
  65. package/lib/log-stream-webhook.js +8 -17
  66. package/lib/log-stream.js +13 -2
  67. package/lib/log.js +83 -5
  68. package/lib/mail-bounce.js +22 -15
  69. package/lib/mail-dkim.js +3 -6
  70. package/lib/mail.js +62 -34
  71. package/lib/metrics.js +61 -25
  72. package/lib/middleware/api-encrypt.js +15 -13
  73. package/lib/middleware/body-parser.js +46 -39
  74. package/lib/middleware/bot-guard.js +25 -4
  75. package/lib/middleware/compression.js +26 -11
  76. package/lib/middleware/cors.js +6 -5
  77. package/lib/middleware/csp-nonce.js +17 -4
  78. package/lib/middleware/csrf-protect.js +1 -1
  79. package/lib/middleware/db-role-for.js +7 -30
  80. package/lib/middleware/health.js +42 -8
  81. package/lib/middleware/index.js +55 -36
  82. package/lib/middleware/rate-limit.js +11 -8
  83. package/lib/middleware/request-id.js +10 -2
  84. package/lib/middleware/request-log.js +3 -2
  85. package/lib/middleware/require-auth.js +5 -2
  86. package/lib/middleware/sse.js +5 -4
  87. package/lib/migration-files.js +37 -0
  88. package/lib/migrations.js +42 -17
  89. package/lib/mtls-ca.js +38 -17
  90. package/lib/mtls-engine-default.js +15 -7
  91. package/lib/network-dns.js +49 -20
  92. package/lib/network-heartbeat.js +7 -7
  93. package/lib/network-nts.js +70 -48
  94. package/lib/network-proxy.js +34 -15
  95. package/lib/network-tls.js +7 -5
  96. package/lib/network.js +3 -3
  97. package/lib/notify.js +14 -36
  98. package/lib/ntp-check.js +7 -6
  99. package/lib/numeric-bounds.js +35 -3
  100. package/lib/object-store/azure-blob-bucket-ops.js +73 -15
  101. package/lib/object-store/azure-blob.js +31 -24
  102. package/lib/object-store/gcs-bucket-ops.js +41 -17
  103. package/lib/object-store/gcs.js +41 -34
  104. package/lib/object-store/http-put.js +2 -11
  105. package/lib/object-store/http-request.js +38 -0
  106. package/lib/object-store/index.js +1 -1
  107. package/lib/object-store/sigv4-bucket-ops.js +33 -37
  108. package/lib/object-store/sigv4.js +50 -30
  109. package/lib/observability.js +14 -3
  110. package/lib/otel-export.js +16 -14
  111. package/lib/pagination.js +23 -8
  112. package/lib/parsers/index.js +11 -5
  113. package/lib/parsers/safe-env.js +43 -14
  114. package/lib/parsers/safe-ini.js +29 -10
  115. package/lib/parsers/safe-toml.js +55 -25
  116. package/lib/parsers/safe-xml.js +22 -11
  117. package/lib/parsers/safe-yaml.js +55 -17
  118. package/lib/permissions.js +24 -33
  119. package/lib/pqc-agent.js +4 -2
  120. package/lib/pqc-gate.js +28 -15
  121. package/lib/protobuf-encoder.js +9 -3
  122. package/lib/protocol-dispatcher.js +29 -12
  123. package/lib/pubsub-redis.js +25 -8
  124. package/lib/pubsub.js +84 -12
  125. package/lib/queue-local.js +13 -4
  126. package/lib/queue-redis.js +2 -2
  127. package/lib/queue-sqs.js +21 -16
  128. package/lib/queue.js +8 -4
  129. package/lib/redis-client.js +13 -10
  130. package/lib/request-helpers.js +33 -2
  131. package/lib/restore-bundle.js +2 -4
  132. package/lib/restore-rollback.js +4 -1
  133. package/lib/restore.js +12 -10
  134. package/lib/retry.js +58 -9
  135. package/lib/router.js +96 -27
  136. package/lib/safe-async.js +97 -4
  137. package/lib/safe-buffer.js +47 -0
  138. package/lib/safe-json.js +18 -9
  139. package/lib/safe-schema.js +54 -18
  140. package/lib/safe-url.js +6 -2
  141. package/lib/scheduler.js +25 -14
  142. package/lib/security-assert.js +31 -26
  143. package/lib/seeders.js +71 -60
  144. package/lib/session.js +13 -7
  145. package/lib/slug.js +35 -8
  146. package/lib/ssrf-guard.js +58 -43
  147. package/lib/static.js +11 -7
  148. package/lib/storage.js +2 -1
  149. package/lib/template.js +17 -7
  150. package/lib/testing.js +28 -8
  151. package/lib/time.js +26 -5
  152. package/lib/totp.js +18 -9
  153. package/lib/tracing.js +15 -5
  154. package/lib/uuid.js +47 -20
  155. package/lib/validate-opts.js +213 -0
  156. package/lib/vault/index.js +36 -27
  157. package/lib/vault/rotate.js +41 -22
  158. package/lib/vault/wrap.js +26 -19
  159. package/lib/webhook.js +37 -68
  160. package/lib/websocket.js +60 -46
  161. package/package.json +1 -1
  162. package/sbom.cyclonedx.json +6 -6
  163. package/lib/auth/totp.js +0 -17
  164. package/lib/object-store/retry.js +0 -15
@@ -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
- // All-zero SHA3-512 (128 hex chars) sentinel prevHash for the first row.
25
- var ZERO_HASH = "0".repeat(128);
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 !== 128) {
48
- throw new Error("prevHash must be a 128-char hex string (SHA3-512); got length " +
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) throw new Error("auditSign.init({ dataDir }) is required");
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 Error("auditSign.init: mode must be 'wrapped' or 'plaintext'");
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 Error("auditSign.init: algorithm must be one of " +
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
- log.error("FATAL: both audit-sign.key and audit-sign.key.sealed exist; resolve manually.");
161
- process.exit(1);
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
- log.error("FATAL: audit-sign.key.sealed exists but mode='plaintext' requested.");
165
- process.exit(1);
177
+ throw _err("MODE_MISMATCH",
178
+ "audit-sign.key.sealed exists but mode='plaintext' requested");
166
179
  }
167
180
  if (hasPlaintext && mode === "wrapped") {
168
- log.error("FATAL: audit-sign.key (plaintext) exists but mode='wrapped' requested.");
169
- process.exit(1);
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) { log.error("FATAL: audit-sign.key corrupted or schema-invalid at " + paths.plaintext + " — " + e.message); process.exit(1); }
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) { log.error("FATAL: audit-signing passphrase rejected (" + e.message + ")"); process.exit(1); }
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) { log.error("FATAL: unwrapped audit-sign.key invalid: " + e.message); process.exit(1); }
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) throw new Error("auditSign.init() must be awaited before sign/verify");
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) {
@@ -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: version,
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. 32 hex chars matches the W3C traceparent trace-id format width.
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(16);
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(16);
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"); } catch (_e) {}
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 / 1000);
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()) / 1000);
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) ||
@@ -184,19 +184,9 @@ function create(opts) {
184
184
  }
185
185
  }
186
186
 
187
- if (opts.audit !== undefined && opts.audit !== null) {
188
- if (typeof opts.audit !== "object" || typeof opts.audit.safeEmit !== "function") {
189
- throw _err("BAD_OPT", "auth.lockout.create: audit must be a b.audit-shaped object (safeEmit fn)");
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(nodeCrypto.randomBytes(bytes));
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(nodeCrypto.randomBytes(32));
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: 32 };
254
- if (alg === "PS384") return { hash: "sha384", padding: nodeCrypto.constants.RSA_PKCS1_PSS_PADDING, saltLength: 48 };
255
- if (alg === "PS512") return { hash: "sha512", padding: nodeCrypto.constants.RSA_PKCS1_PSS_PADDING, saltLength: 64 };
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 = _httpClient();
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(16);
421
- var nonce = uopts.nonce || (isOidc ? _generateRandomToken(16) : null);
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 = _httpClient();
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 = _httpClient();
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() / 1000);
655
- var skewSec = Math.floor(clockSkewMs / 1000);
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
  }
@@ -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
 
@@ -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 internalSha1Hibp = require("../internal-sha1-hibp");
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: 65536, // 64 MiB
62
+ memoryCost: C.BYTES.kib(64), // 64 MiB
60
63
  timeCost: 3,
61
64
  parallelism: 4,
62
65
  });
63
66
 
64
- var MAX_PLAINTEXT_BYTES = 4096;
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/internal-sha1-hibp.js
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: 8, // NIST floor
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: 1500,
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 * 24 * 60 * 60 * 1000,
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 * 24 * 60 * 60 * 1000,
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/internal-sha1-hibp.js
418
+ // SHA-1 usage; HIBP requires it. (See lib/framework-sha1-hibp.js
409
419
  // for the restriction rationale.)
410
- var sha1Full = internalSha1Hibp.sha1Hex(plaintext).toUpperCase();
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 < 1024) {
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
  }
@@ -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
- if (typeof opts.outDir !== "string" || opts.outDir.length === 0) {
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 +
@@ -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, // 2 = argon2id
64
- memoryCost: 65536, // 64 MiB
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 (typeof saltHex !== "string" || !/^[0-9a-fA-F]+$/.test(saltHex) || saltHex.length % 2 !== 0) {
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
  }
@@ -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" && BUNDLE_ID_RE.test(s);
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
- if (typeof opts.root !== "string" || opts.root.length === 0) {
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
- // Inline require: backup is a leaf module the framework re-exports
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; flush is a no-op */ }
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");