@blamejs/core 0.4.27 → 0.4.29

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 CHANGED
@@ -8,6 +8,8 @@ upgrading across more than a few patches at a time.
8
8
 
9
9
  ## v0.4.x
10
10
 
11
+ - **0.4.28** (2026-04-30) — primitive-drift sweep: inline-require hoisting + safeAsync.sleep + time-math
12
+ - **0.4.27** (2026-04-30) — primitive-drift sweep: regex + escape consolidation, IPv6 completion
11
13
  - **0.4.26** (2026-04-30) — primitive-drift sweep: middleware audit context + safeUrl
12
14
  - **0.4.25** (2026-04-30) — b.objectStore.bucketOps: bucket-level operations (SigV4)
13
15
  - **0.4.24** (2026-04-30) — b.objectStore: multipart upload + server-side encryption
package/lib/api-key.js CHANGED
@@ -13,7 +13,7 @@
13
13
  * ownerId: "user-42",
14
14
  * scopes: ["read:users", "write:posts"],
15
15
  * metadata: { name: "Mobile app v3" },
16
- * expiresAt: Date.now() + 90 * 86400 * 1000,
16
+ * expiresAt: Date.now() + b.constants.TIME.days(90),
17
17
  * });
18
18
  * // issued.key — "bk_live_<idHex>_<secretHex>" (returned ONCE)
19
19
  * // issued.id — "<idHex>"
@@ -277,16 +277,10 @@ function create(opts) {
277
277
  // The audit chain treats null fields as "unknown", so partial
278
278
  // context is always safe.
279
279
  function _actor(callerOpts, userId) {
280
- var override = {};
281
- if (userId) override.userId = userId;
282
- if (callerOpts && callerOpts.context && typeof callerOpts.context === "object") {
283
- for (var k in callerOpts.context) {
284
- if (Object.prototype.hasOwnProperty.call(callerOpts.context, k)) {
285
- override[k] = callerOpts.context[k];
286
- }
287
- }
288
- }
289
- return requestHelpers.extractActorContext(callerOpts && callerOpts.req, override);
280
+ return requestHelpers.resolveActorWithOverride(
281
+ callerOpts,
282
+ userId ? { userId: userId } : null
283
+ );
290
284
  }
291
285
 
292
286
  function _selectAll() {
@@ -57,9 +57,17 @@ var atomicFile = require("./atomic-file");
57
57
  var auditChain = require("./audit-chain");
58
58
  var auditSign = require("./audit-sign");
59
59
  var backupCrypto = require("./backup/crypto");
60
+ var clusterStorage = require("./cluster-storage");
61
+ var lazyRequire = require("./lazy-require");
60
62
  var jsonSafe = require("./safe-json");
61
63
  var { defineClass } = require("./framework-error");
62
64
 
65
+ // Lazy `db` — db requires audit at top-of-file, audit transitively
66
+ // reaches into audit-tools via the operator-supplied default fns,
67
+ // so importing db at audit-tools' top would close the cycle. Lazy
68
+ // keeps the load order one-way.
69
+ var db = lazyRequire(function () { return require("./db"); });
70
+
63
71
  var AuditToolsError = defineClass("AuditToolsError", { alwaysPermanent: true });
64
72
 
65
73
  var BUNDLE_FORMAT = "blamejs-audit-bundle-v1";
@@ -197,7 +205,6 @@ function _verifyChainSlice(rows, startPrevHash) {
197
205
  // cluster-storage reader so the tooling works in both single-node and
198
206
  // cluster deployments without the caller knowing which mode is active.
199
207
  async function _defaultReadRows(criteria) {
200
- var clusterStorage = require("./cluster-storage");
201
208
  var sql = 'SELECT * FROM "audit_log"';
202
209
  var conds = [];
203
210
  var params = [];
@@ -213,7 +220,6 @@ async function _defaultReadRows(criteria) {
213
220
  }
214
221
 
215
222
  async function _defaultReadCoveringCheckpoint(lastCounter) {
216
- var clusterStorage = require("./cluster-storage");
217
223
  return clusterStorage.executeOne(
218
224
  "SELECT * FROM audit_checkpoints " +
219
225
  "WHERE atMonotonicCounter >= ? " +
@@ -224,7 +230,6 @@ async function _defaultReadCoveringCheckpoint(lastCounter) {
224
230
 
225
231
  async function _defaultReadPredecessorRowHash(firstCounter) {
226
232
  if (firstCounter <= 1) return auditChain.ZERO_HASH;
227
- var clusterStorage = require("./cluster-storage");
228
233
  var row = await clusterStorage.executeOne(
229
234
  "SELECT rowHash FROM audit_log WHERE monotonicCounter = ?",
230
235
  [firstCounter - 1]
@@ -640,16 +645,13 @@ async function purge(opts) {
640
645
  }
641
646
 
642
647
  async function _defaultReadPurgeAnchor() {
643
- var clusterStorage = require("./cluster-storage");
644
648
  return clusterStorage.executeOne(
645
649
  "SELECT * FROM _blamejs_audit_purge_anchor WHERE scope = 'audit'"
646
650
  );
647
651
  }
648
652
 
649
653
  async function _defaultApplyPurge(args) {
650
- var db = require("./db");
651
- var clusterStorage = require("./cluster-storage");
652
- var del = await db.purgeAuditChain({ lastPurgedCounter: args.lastPurgedCounter });
654
+ var del = await db().purgeAuditChain({ lastPurgedCounter: args.lastPurgedCounter });
653
655
  // UPSERT the single-row anchor. SQLite + Postgres both support
654
656
  // INSERT ... ON CONFLICT(scope) DO UPDATE.
655
657
  await clusterStorage.execute(
package/lib/auth/oauth.js CHANGED
@@ -106,11 +106,18 @@
106
106
 
107
107
  var nodeCrypto = require("node:crypto");
108
108
  var cache = require("../cache");
109
+ var safeJson = require("../safe-json");
109
110
  var safeUrl = require("../safe-url");
110
111
  var C = require("../constants");
111
112
  var { defineClass } = require("../framework-error");
112
113
  var { boot } = require("../log");
113
114
 
115
+ // Cap on responses parsed from upstream OAuth providers. Token /
116
+ // userinfo / discovery responses are tiny in spec; 256 KiB leaves
117
+ // huge headroom and prevents a hostile (or compromised) upstream
118
+ // from staging a parse-bomb against the framework.
119
+ var OAUTH_MAX_RESPONSE_BYTES = C.BYTES.kib(256);
120
+
114
121
  var OAuthError = defineClass("OAuthError", { alwaysPermanent: true });
115
122
  var log = boot("auth.oauth");
116
123
 
@@ -360,7 +367,7 @@ function create(opts) {
360
367
  url + " returned " + res.statusCode + ": " + bodyText.slice(0, 500));
361
368
  }
362
369
  if (!res.body) return null;
363
- try { return JSON.parse(res.body.toString("utf8")); }
370
+ try { return safeJson.parse(res.body.toString("utf8"), { maxBytes: OAUTH_MAX_RESPONSE_BYTES }); }
364
371
  catch (e) {
365
372
  throw new OAuthError("auth-oauth/bad-json",
366
373
  url + " response not JSON: " + ((e && e.message) || String(e)));
@@ -547,7 +554,7 @@ function create(opts) {
547
554
  endpoint + " returned " + res.statusCode + ": " + text.slice(0, 500));
548
555
  }
549
556
  var parsed;
550
- try { parsed = JSON.parse(text); }
557
+ try { parsed = safeJson.parse(text, { maxBytes: OAUTH_MAX_RESPONSE_BYTES }); }
551
558
  catch (e) {
552
559
  throw new OAuthError("auth-oauth/bad-token-json",
553
560
  "token endpoint response not JSON: " + ((e && e.message) || String(e)));
@@ -604,8 +611,8 @@ function create(opts) {
604
611
  }
605
612
  var header, payload;
606
613
  try {
607
- header = JSON.parse(_b64urlDecode(parts[0]).toString("utf8"));
608
- payload = JSON.parse(_b64urlDecode(parts[1]).toString("utf8"));
614
+ header = safeJson.parse(_b64urlDecode(parts[0]).toString("utf8"), { maxBytes: OAUTH_MAX_RESPONSE_BYTES });
615
+ payload = safeJson.parse(_b64urlDecode(parts[1]).toString("utf8"), { maxBytes: OAUTH_MAX_RESPONSE_BYTES });
609
616
  } catch (e) {
610
617
  throw new OAuthError("auth-oauth/malformed-jwt",
611
618
  "ID token header/payload base64 decode failed: " + ((e && e.message) || String(e)));
@@ -69,6 +69,7 @@
69
69
  */
70
70
 
71
71
  var C = require("../constants");
72
+ var safeJson = require("../safe-json");
72
73
  var { FrameworkError } = require("../framework-error");
73
74
 
74
75
  class BackupManifestError extends FrameworkError {
@@ -250,7 +251,9 @@ function parse(jsonStr) {
250
251
  }
251
252
  var s = Buffer.isBuffer(jsonStr) ? jsonStr.toString("utf8") : jsonStr;
252
253
  var obj;
253
- try { obj = JSON.parse(s); }
254
+ // Backup manifests are file-bounded operator-supplied input. 16 MiB
255
+ // is generous; real manifests are kilobytes.
256
+ try { obj = safeJson.parse(s, { maxBytes: C.BYTES.mib(16) }); }
254
257
  catch (e) {
255
258
  throw new BackupManifestError("backup-manifest/bad-json",
256
259
  "parse: not valid JSON: " + ((e && e.message) || String(e)));
package/lib/cache.js CHANGED
@@ -683,15 +683,7 @@ function create(opts) {
683
683
  }
684
684
 
685
685
  function _actor(callerOpts) {
686
- var override = {};
687
- if (callerOpts && callerOpts.context && typeof callerOpts.context === "object") {
688
- for (var k in callerOpts.context) {
689
- if (Object.prototype.hasOwnProperty.call(callerOpts.context, k)) {
690
- override[k] = callerOpts.context[k];
691
- }
692
- }
693
- }
694
- return requestHelpers.extractActorContext(callerOpts && callerOpts.req, override);
686
+ return requestHelpers.resolveActorWithOverride(callerOpts);
695
687
  }
696
688
 
697
689
  function _backendFailedAudit(op, err) {
package/lib/cli.js CHANGED
@@ -34,6 +34,9 @@
34
34
  * before encryption or temporarily switch the file to plaintext.
35
35
  */
36
36
 
37
+ var fs = require("node:fs");
38
+ var nodeCrypto = require("node:crypto");
39
+ var os = require("node:os");
37
40
  var path = require("path");
38
41
  var apiSnapshot = require("./api-snapshot");
39
42
  var auditTools = require("./audit-tools");
@@ -898,11 +901,7 @@ async function _runBackup(args, ctx) {
898
901
  }
899
902
 
900
903
  if (sub === "verify") {
901
- var fs2 = require("node:fs");
902
- var os2 = require("node:os");
903
- var nodePath = require("node:path");
904
- var nodeCrypto = require("node:crypto");
905
- var stagingDir = nodePath.join(os2.tmpdir(),
904
+ var stagingDir = path.join(os.tmpdir(),
906
905
  "blamejs-backup-verify-" + nodeCrypto.randomBytes(8).toString("hex"));
907
906
  try {
908
907
  var r = await restoreBundle.extract({
@@ -917,7 +916,7 @@ async function _runBackup(args, ctx) {
917
916
  } catch (e) {
918
917
  return report.error((e && e.message) || String(e));
919
918
  } finally {
920
- try { fs2.rmSync(stagingDir, { recursive: true, force: true }); } catch (_e) { /* best-effort */ }
919
+ try { fs.rmSync(stagingDir, { recursive: true, force: true }); } catch (_e) { /* best-effort */ }
921
920
  }
922
921
  }
923
922
 
@@ -1118,7 +1117,7 @@ async function _runMtls(args, ctx) {
1118
1117
  validityDays: daysP,
1119
1118
  });
1120
1119
  if (outPath) {
1121
- require("node:fs").writeFileSync(outPath, p12.p12, { mode: 0o600 });
1120
+ fs.writeFileSync(outPath, p12.p12, { mode: 0o600 });
1122
1121
  report.write("p12 written: " + outPath);
1123
1122
  } else {
1124
1123
  // No --out: stream the bytes to stdout for piping. Operators
package/lib/db-query.js CHANGED
@@ -26,6 +26,7 @@
26
26
  * a derived hash throws (every encryption uses a fresh nonce — the lookup
27
27
  * would always return zero rows; failing loudly is safer).
28
28
  */
29
+ var { Readable } = require("node:stream");
29
30
  var cryptoField = require("./crypto-field");
30
31
  var { generateToken } = require("./crypto");
31
32
 
@@ -194,7 +195,6 @@ class Query {
194
195
  var stmt = this._db.prepare(sql);
195
196
  var table = this._table;
196
197
  var iter;
197
- var Readable = require("node:stream").Readable;
198
198
  try { iter = stmt.iterate.apply(stmt, this._whereParams); }
199
199
  catch (e) {
200
200
  var r = new Readable({ objectMode: true, read: function () {} });
package/lib/db.js CHANGED
@@ -42,6 +42,7 @@
42
42
  var fs = require("fs");
43
43
  var path = require("path");
44
44
  var { DatabaseSync } = require("node:sqlite");
45
+ var { Readable } = require("node:stream");
45
46
  var atomicFile = require("./atomic-file");
46
47
  var audit = require("./audit");
47
48
  var auditSign = require("./audit-sign");
@@ -796,7 +797,6 @@ function stream(sql) {
796
797
  var table = opts && typeof opts.table === "string" ? opts.table : null;
797
798
  var unseal = table ? cryptoField : null;
798
799
 
799
- var Readable = require("node:stream").Readable;
800
800
  var stmt;
801
801
  var iter;
802
802
  try {
@@ -64,6 +64,8 @@
64
64
  var http = require("http");
65
65
  var https = require("https");
66
66
  var http2 = require("http2");
67
+ var nodeCrypto = require("node:crypto");
68
+ var nodeStream = require("node:stream");
67
69
  var { URL } = require("url");
68
70
  var C = require("./constants");
69
71
  var pqcAgent = require("./pqc-agent");
@@ -321,7 +323,6 @@ function _attachJarCookie(headers, jar, url) {
321
323
  // parser accepts so round-trip from one blamejs app's outbound to
322
324
  // another's inbound is exact.
323
325
  function _buildMultipartBody(spec) {
324
- var nodeCrypto = require("node:crypto");
325
326
  var boundary = "----blamejs-mp-" + nodeCrypto.randomBytes(16).toString("hex");
326
327
  var CRLF = "\r\n";
327
328
  var parts = [];
@@ -701,7 +702,7 @@ function _requestH1(transport, u, opts) {
701
702
  // The framework's contract is to hand back the response stream
702
703
  // unmodified; fix-up via a passthrough keeps that contract while
703
704
  // observing the chunk sizes.
704
- var passthrough = new (require("node:stream").PassThrough)();
705
+ var passthrough = new nodeStream.PassThrough();
705
706
  res.on("data", function (chunk) { _emitDownload(chunk.length); passthrough.write(chunk); });
706
707
  res.on("end", function () { passthrough.end(); });
707
708
  res.on("error", function (e) { passthrough.destroy(e); });
package/lib/mail.js CHANGED
@@ -62,15 +62,22 @@
62
62
  * resend-*. Vendor-specific presets carry their own code prefix so
63
63
  * diagnostic logs identify the provider that rejected the message.
64
64
  */
65
+ var C = require("./constants");
65
66
  var lazyRequire = require("./lazy-require");
67
+ var nodeCrypto = require("node:crypto");
66
68
  var audit = lazyRequire(function () { return require("./audit"); });
67
69
  var httpClient = lazyRequire(function () { return require("./http-client"); });
68
70
  var net = lazyRequire(function () { return require("net"); });
69
71
  var tls = lazyRequire(function () { return require("tls"); });
72
+ var safeJson = require("./safe-json");
70
73
  var safeSchema = require("./safe-schema");
71
74
  var validateOpts = require("./validate-opts");
72
75
  var { FrameworkError } = require("./framework-error");
73
76
 
77
+ // Cap on responses parsed from upstream mail providers (Resend, etc.).
78
+ // Vendor responses are tiny in spec; 256 KiB is generous headroom.
79
+ var MAIL_RESPONSE_MAX_BYTES = C.BYTES.kib(256);
80
+
74
81
  class MailError extends FrameworkError {
75
82
  constructor(code, message, permanent, statusCode) {
76
83
  super(message, code);
@@ -286,7 +293,11 @@ function memoryTransport() {
286
293
  // to send AUTH or DATA in cleartext if the upgrade is rejected.
287
294
 
288
295
  function _newBoundary(label) {
289
- return "blamejs-" + label + "-" + Date.now() + "-" + Math.floor(Math.random() * 1e9);
296
+ // crypto.randomBytes for the boundary suffix matches the framework
297
+ // convention. RFC 5322 only requires uniqueness within a message,
298
+ // but consistency with how every other identifier in lib/ is built
299
+ // wins over premature differentiation.
300
+ return "blamejs-" + label + "-" + Date.now() + "-" + nodeCrypto.randomBytes(8).toString("hex");
290
301
  }
291
302
 
292
303
  // base64-encode the buffer with line wrapping at 76 chars (RFC 2045
@@ -773,7 +784,7 @@ function resendTransport(opts) {
773
784
  interpret: function (res) {
774
785
  var text = res.body ? res.body.toString("utf8") : "";
775
786
  var data;
776
- try { data = JSON.parse(text); }
787
+ try { data = safeJson.parse(text, { maxBytes: MAIL_RESPONSE_MAX_BYTES }); }
777
788
  catch (_e) {
778
789
  throw new MailError("mail/resend-bad-response",
779
790
  "resend response was not JSON: " + text.slice(0, 200), false);
@@ -100,6 +100,8 @@ var crypto = require("../crypto");
100
100
  var C = require("../constants");
101
101
  var lazyRequire = require("../lazy-require");
102
102
  var nonceStoreLib = require("../nonce-store");
103
+ var requestHelpers = require("../request-helpers");
104
+ var safeJson = require("../safe-json");
103
105
  var validateOpts = require("../validate-opts");
104
106
  var { defineClass } = require("../framework-error");
105
107
 
@@ -172,10 +174,19 @@ function create(opts) {
172
174
  validateOpts(opts, [
173
175
  "keypair", "keypairs", "replayWindowMs", "pruneIntervalMs",
174
176
  "nonceStore", "exemptPaths", "contentTypes", "audit",
177
+ "maxDecryptedBytes",
175
178
  ], "middleware.apiEncrypt");
176
179
  var keypairs = _resolveKeypairs(opts);
177
180
  var activeKeypair = keypairs[0];
178
181
  var replayWindowMs = opts.replayWindowMs || DEFAULT_REPLAY_WINDOW_MS;
182
+ // Cap on decrypted-payload size handed to safeJson.parse. Defaults
183
+ // to 4 MiB (bodyParser's default 1 MiB plus headroom for crypto +
184
+ // base64 round-trip). Operators with chunkier inbound payloads
185
+ // raise this; the framework refuses to parse anything larger as a
186
+ // parse-bomb defense.
187
+ var maxDecryptedBytes = opts.maxDecryptedBytes != null
188
+ ? opts.maxDecryptedBytes
189
+ : C.BYTES.mib(4);
179
190
  // The spec calls for a sweep cadence of replayWindowMs/2 — short
180
191
  // enough that expired nonces don't pile up but not so frequent the
181
192
  // sweep query becomes a hot path. Operators can override.
@@ -230,7 +241,7 @@ function create(opts) {
230
241
  };
231
242
  if (auditOn) {
232
243
  audit().safeEmit({
233
- actor: { ip: info.ip },
244
+ actor: requestHelpers.extractActorContext(req),
234
245
  action: "system.api_encrypt.failure",
235
246
  outcome: "denied",
236
247
  reason: reason,
@@ -348,7 +359,7 @@ function create(opts) {
348
359
  try {
349
360
  var ctBuf = Buffer.from(ct, "base64");
350
361
  var ptBuf = crypto.decryptPacked(ctBuf, sessionKey);
351
- clearObj = JSON.parse(ptBuf.toString("utf8"));
362
+ clearObj = safeJson.parse(ptBuf.toString("utf8"), { maxBytes: maxDecryptedBytes });
352
363
  } catch (_e) {
353
364
  _emitFailure(req, "tag");
354
365
  return _writeRejection(res, 400, { error: "encrypted-payload-rejected" });
@@ -403,7 +414,7 @@ function create(opts) {
403
414
 
404
415
  function client(opts) {
405
416
  opts = opts || {};
406
- validateOpts(opts, ["pubkey"], "middleware.apiEncrypt.client");
417
+ validateOpts(opts, ["pubkey", "maxDecryptedBytes"], "middleware.apiEncrypt.client");
407
418
  if (!opts.pubkey || typeof opts.pubkey !== "object") {
408
419
  throw _err("CLIENT_INVALID_PUBKEY",
409
420
  "apiEncrypt.client: opts.pubkey is required ({ publicKey, ecPublicKey })", 500);
@@ -414,6 +425,9 @@ function client(opts) {
414
425
  "apiEncrypt.client: pubkey.publicKey + ecPublicKey must be PEM strings", 500);
415
426
  }
416
427
  var pubkey = opts.pubkey;
428
+ var maxDecryptedBytes = opts.maxDecryptedBytes != null
429
+ ? opts.maxDecryptedBytes
430
+ : C.BYTES.mib(4);
417
431
 
418
432
  function encryptRequest(payload) {
419
433
  if (payload === undefined) payload = null;
@@ -441,7 +455,7 @@ function client(opts) {
441
455
  }
442
456
  var resCtBuf = Buffer.from(responseBody._ct, "base64");
443
457
  var resPtBuf = crypto.decryptPacked(resCtBuf, sessionKey);
444
- return JSON.parse(resPtBuf.toString("utf8"));
458
+ return safeJson.parse(resPtBuf.toString("utf8"), { maxBytes: maxDecryptedBytes });
445
459
  },
446
460
  };
447
461
  }
@@ -474,13 +488,16 @@ function client(opts) {
474
488
  function httpClientEncrypted(opts) {
475
489
  opts = opts || {};
476
490
  validateOpts(opts, [
477
- "pubkey", "baseUrl", "headers", "method",
491
+ "pubkey", "baseUrl", "headers", "method", "maxDecryptedBytes",
478
492
  ], "middleware.apiEncrypt.httpClient");
479
493
  if (!opts.pubkey) {
480
494
  throw _err("CLIENT_INVALID_PUBKEY",
481
495
  "httpClient.encrypted: opts.pubkey is required (the callee's bootstrap doc)", 500);
482
496
  }
483
- var clientCtx = client({ pubkey: opts.pubkey });
497
+ var maxDecryptedBytes = opts.maxDecryptedBytes != null
498
+ ? opts.maxDecryptedBytes
499
+ : C.BYTES.mib(4);
500
+ var clientCtx = client({ pubkey: opts.pubkey, maxDecryptedBytes: maxDecryptedBytes });
484
501
  var baseUrl = opts.baseUrl ? String(opts.baseUrl).replace(/\/$/, "") : "";
485
502
  var defaultHdrs = opts.headers || {};
486
503
  var defaultMethod = opts.method || "POST";
@@ -531,7 +548,7 @@ function httpClientEncrypted(opts) {
531
548
  return { statusCode: resp.statusCode, headers: resp.headers, body: null };
532
549
  }
533
550
  var parsed;
534
- try { parsed = JSON.parse(resp.body.toString("utf8")); }
551
+ try { parsed = safeJson.parse(resp.body.toString("utf8"), { maxBytes: maxDecryptedBytes }); }
535
552
  catch (e) {
536
553
  throw _err("CLIENT_RESPONSE_NOT_JSON",
537
554
  "httpClient.encrypted: response body is not valid JSON: " + e.message);
@@ -43,6 +43,7 @@
43
43
  */
44
44
  var lazyRequire = require("../lazy-require");
45
45
  var cookies = require("../cookies");
46
+ var requestHelpers = require("../request-helpers");
46
47
  var validateOpts = require("../validate-opts");
47
48
  var session = lazyRequire(function () { return require("../session"); });
48
49
  var audit = lazyRequire(function () { return require("../audit"); });
@@ -120,7 +121,7 @@ function create(opts) {
120
121
  audit().emit({
121
122
  action: "auth.session.user_loader_threw",
122
123
  outcome: "failure",
123
- actor: { userId: verified.userId, ip: req.socket && req.socket.remoteAddress },
124
+ actor: requestHelpers.extractActorContext(req, { userId: verified.userId }),
124
125
  reason: (e && e.message) || String(e),
125
126
  });
126
127
  } catch (_ignored) { /* audit best-effort */ }
@@ -135,7 +136,7 @@ function create(opts) {
135
136
  audit().emit({
136
137
  action: "auth.session.user_unloadable",
137
138
  outcome: "failure",
138
- actor: { userId: verified.userId, ip: req.socket && req.socket.remoteAddress },
139
+ actor: requestHelpers.extractActorContext(req, { userId: verified.userId }),
139
140
  });
140
141
  } catch (_ignored) { /* audit best-effort */ }
141
142
  }
@@ -26,23 +26,23 @@
26
26
  *
27
27
  * var bp = b.middleware.bodyParser({
28
28
  * json: {
29
- * limit: 1024 * 1024,
29
+ * limit: b.constants.BYTES.mib(1),
30
30
  * strict: true, // require the body to start with {/[
31
31
  * parseHook: function (parsed) { ... return validatedShape; },
32
32
  * },
33
33
  * urlencoded: {
34
- * limit: 1024 * 1024,
34
+ * limit: b.constants.BYTES.mib(1),
35
35
  * arrayLimit: 100, // for ?tag=a&tag=b → tag: ["a","b"]
36
36
  * },
37
- * text: { limit: 1024 * 1024, charset: "utf-8" },
38
- * raw: { limit: 10 * 1024 * 1024, contentTypes: ["application/octet-stream"] },
37
+ * text: { limit: b.constants.BYTES.mib(1), charset: "utf-8" },
38
+ * raw: { limit: b.constants.BYTES.mib(10), contentTypes: ["application/octet-stream"] },
39
39
  * multipart: {
40
40
  * tmpDir: os.tmpdir(),
41
- * fileSize: 10 * 1024 * 1024,
42
- * totalSize: 50 * 1024 * 1024,
41
+ * fileSize: b.constants.BYTES.mib(10),
42
+ * totalSize: b.constants.BYTES.mib(50),
43
43
  * fileCount: 20,
44
44
  * fieldCount: 100,
45
- * fieldSize: 1024 * 1024,
45
+ * fieldSize: b.constants.BYTES.mib(1),
46
46
  * mimeAllowlist: ["image/jpeg", "image/png", "application/pdf"], // null = any
47
47
  *
48
48
  * // Per-part predicate. Runs after sanitization + MIME checks but
@@ -64,8 +64,8 @@
64
64
  * // global mimeAllowlist for the named field; other fields still
65
65
  * // use the global list.
66
66
  * fields: {
67
- * avatar: { maxBytes: 2 * 1024 * 1024, mimeTypes: ["image/jpeg", "image/png"] },
68
- * document: { maxBytes: 25 * 1024 * 1024 },
67
+ * avatar: { maxBytes: b.constants.BYTES.mib(2), mimeTypes: ["image/jpeg", "image/png"] },
68
+ * document: { maxBytes: b.constants.BYTES.mib(25) },
69
69
  * },
70
70
  *
71
71
  * // When wired, fileFilter rejections emit body-parser.multipart.file_rejected
@@ -44,10 +44,20 @@
44
44
  */
45
45
  var lazyRequire = require("../lazy-require");
46
46
  var audit = lazyRequire(function () { return require("../audit"); });
47
+ var requestHelpers = require("../request-helpers");
47
48
  var safeUrl = require("../safe-url");
48
49
  var validateOpts = require("../validate-opts");
49
50
  var { defineClass } = require("../framework-error");
50
51
 
52
+ // CORS audit events trust the proxy header for actor.ip — same
53
+ // semantics as bot-guard (the apparent origin is what matters when
54
+ // blocking cross-origin attempts behind a CDN).
55
+ function _xffIp(req) {
56
+ var xff = req.headers && req.headers["x-forwarded-for"];
57
+ if (xff) return String(xff).split(",")[0].trim();
58
+ return (req.socket && req.socket.remoteAddress) || null;
59
+ }
60
+
51
61
  var CorsError = defineClass("CorsError", { alwaysPermanent: true });
52
62
 
53
63
  function _matchOrigin(origin, allowList) {
@@ -188,7 +198,7 @@ function create(opts) {
188
198
  if (refuseUnknown) {
189
199
  try {
190
200
  audit().emit({
191
- actor: { ip: (req.headers && req.headers["x-forwarded-for"]) || (req.socket && req.socket.remoteAddress) },
201
+ actor: requestHelpers.extractActorContext(req, { ip: _xffIp(req) }),
192
202
  action: "system.cors.block",
193
203
  outcome: "denied",
194
204
  reason: "origin not in allow-list",
package/lib/notify.js CHANGED
@@ -370,15 +370,7 @@ function create(opts) {
370
370
  }
371
371
 
372
372
  function _actor(callerOpts) {
373
- var override = {};
374
- if (callerOpts && callerOpts.context && typeof callerOpts.context === "object") {
375
- for (var k in callerOpts.context) {
376
- if (Object.prototype.hasOwnProperty.call(callerOpts.context, k)) {
377
- override[k] = callerOpts.context[k];
378
- }
379
- }
380
- }
381
- return requestHelpers.extractActorContext(callerOpts && callerOpts.req, override);
373
+ return requestHelpers.resolveActorWithOverride(callerOpts);
382
374
  }
383
375
 
384
376
  function _redactedMetadata(channel, message, extra) {
@@ -175,7 +175,7 @@ function create(config) {
175
175
  if (!tokenResp.access_token) {
176
176
  throw _err("AUTH_FAILED", "GCS token endpoint returned no access_token: " + res.body.toString("utf8"), true);
177
177
  }
178
- var expiresInMs = (tokenResp.expires_in || 3600) * 1000;
178
+ var expiresInMs = C.TIME.seconds(tokenResp.expires_in || 3600);
179
179
  cachedToken = {
180
180
  accessToken: tokenResp.access_token,
181
181
  expiresAt: Date.now() + expiresInMs,
@@ -33,8 +33,9 @@ var cluster = require("./cluster");
33
33
  var clusterStorage = require("./cluster-storage");
34
34
  var { generateToken } = require("./crypto");
35
35
  var cryptoField = require("./crypto-field");
36
- var safeJson = require("./safe-json");
37
36
  var lazyRequire = require("./lazy-require");
37
+ var safeJson = require("./safe-json");
38
+ var scheduler = require("./scheduler");
38
39
  var { QueueError } = require("./framework-error");
39
40
 
40
41
  var _err = QueueError.factory;
@@ -241,7 +242,6 @@ function create(_config) {
241
242
  if (row && row.repeatCron) {
242
243
  try {
243
244
  var unsealedRow = cryptoField.unsealRow("_blamejs_jobs", row);
244
- var scheduler = require("./scheduler");
245
245
  var cron = scheduler.parseCron(unsealedRow.repeatCron);
246
246
  var nextMs = scheduler.nextCronFire(cron, new Date(nowMs), unsealedRow.repeatTimezone || null);
247
247
  await enqueue(unsealedRow.queueName,
package/lib/queue.js CHANGED
@@ -31,12 +31,13 @@
31
31
  * failed (status='failed')
32
32
  */
33
33
  var C = require("./constants");
34
- var localProto = require("./queue-local");
35
- var retryHelper = require("./retry");
36
- var safeAsync = require("./safe-async");
37
34
  var lazyRequire = require("./lazy-require");
35
+ var nodeCrypto = require("node:crypto");
38
36
  var observability = require("./observability");
39
37
  var protocolDispatcher = require("./protocol-dispatcher");
38
+ var localProto = require("./queue-local");
39
+ var retryHelper = require("./retry");
40
+ var safeAsync = require("./safe-async");
40
41
  var { QueueError } = require("./framework-error");
41
42
 
42
43
  var dispatcher = protocolDispatcher.create({
@@ -565,7 +566,6 @@ function enqueueFlow(spec) {
565
566
  return Promise.reject(e);
566
567
  }
567
568
 
568
- var nodeCrypto = require("node:crypto");
569
569
  var flowId = "flow-" + nodeCrypto.randomBytes(8).toString("hex");
570
570
 
571
571
  return observability.tap("queue.enqueueFlow",
@@ -87,6 +87,31 @@ function extractActorContext(req, override) {
87
87
  return ctx;
88
88
  }
89
89
 
90
+ // Convenience wrapper for primitives that accept an optional
91
+ // `{ req, context }` shape and want to thread it into an audit-emit
92
+ // `actor` field. Replaces the four near-identical `_actor()` helpers
93
+ // that lived in api-key, cache, seeders, and notify before v0.4.29.
94
+ //
95
+ // callerOpts: operator-supplied `{ req?, context? }` (e.g. the
96
+ // primitive's call-site opts bag)
97
+ // baseOverride: optional seed values applied BEFORE callerOpts.context
98
+ // so `context` wins. api-key seeds `{ userId }` here so
99
+ // the resolved key's owner becomes the default actor
100
+ // unless the operator passes their own context.userId.
101
+ //
102
+ // Returns the same shape as extractActorContext.
103
+ function resolveActorWithOverride(callerOpts, baseOverride) {
104
+ var override = baseOverride ? Object.assign({}, baseOverride) : {};
105
+ if (callerOpts && callerOpts.context && typeof callerOpts.context === "object") {
106
+ for (var k in callerOpts.context) {
107
+ if (Object.prototype.hasOwnProperty.call(callerOpts.context, k)) {
108
+ override[k] = callerOpts.context[k];
109
+ }
110
+ }
111
+ }
112
+ return extractActorContext(callerOpts && callerOpts.req, override);
113
+ }
114
+
90
115
  function resolveRoute(req) {
91
116
  if (req && typeof req.routePattern === "string" && req.routePattern.length > 0) {
92
117
  return req.routePattern;
@@ -171,8 +196,9 @@ function parseQualityList(headerValue, opts) {
171
196
  }
172
197
 
173
198
  module.exports = {
174
- resolveRoute: resolveRoute,
175
- captureResponseStatus: captureResponseStatus,
176
- extractActorContext: extractActorContext,
177
- parseQualityList: parseQualityList,
199
+ resolveRoute: resolveRoute,
200
+ captureResponseStatus: captureResponseStatus,
201
+ extractActorContext: extractActorContext,
202
+ resolveActorWithOverride: resolveActorWithOverride,
203
+ parseQualityList: parseQualityList,
178
204
  };
@@ -56,6 +56,8 @@
56
56
  var fs = require("fs");
57
57
  var path = require("path");
58
58
  var atomicFile = require("./atomic-file");
59
+ var C = require("./constants");
60
+ var safeJson = require("./safe-json");
59
61
  var { defineClass } = require("./framework-error");
60
62
 
61
63
  var RestoreRollbackError = defineClass("RestoreRollbackError", { alwaysPermanent: true });
@@ -199,7 +201,7 @@ function list(opts) {
199
201
  var markerPath = p + ".marker.json";
200
202
  var marker = null;
201
203
  if (fs.existsSync(markerPath)) {
202
- try { marker = JSON.parse(fs.readFileSync(markerPath, "utf8")); }
204
+ try { marker = safeJson.parse(fs.readFileSync(markerPath, "utf8"), { maxBytes: C.BYTES.kib(64) }); }
203
205
  catch (_e) { marker = null; }
204
206
  }
205
207
  var stat;
package/lib/router.js CHANGED
@@ -25,6 +25,7 @@ var fs = require("fs");
25
25
  var path = require("path");
26
26
  var { URL } = require("url");
27
27
  var C = require("./constants");
28
+ var safeAsync = require("./safe-async");
28
29
  var websocket = require("./websocket");
29
30
  var { boot } = require("./log");
30
31
 
@@ -217,12 +218,13 @@ class Router {
217
218
  });
218
219
  });
219
220
 
220
- var allClosed = Promise.all(closes);
221
- var timeout = new Promise(function (resolve) {
222
- var t = setTimeout(resolve, timeoutMs);
223
- t.unref();
224
- });
225
- await Promise.race([allClosed, timeout]);
221
+ // Wait up to `timeoutMs` for graceful WS closes, then force-destroy
222
+ // any laggards. `safeAsync.sleep({ unref: true })` matches the
223
+ // framework's outbound-timeout convention.
224
+ await Promise.race([
225
+ Promise.all(closes),
226
+ safeAsync.sleep(timeoutMs, { unref: true }),
227
+ ]);
226
228
  // Force-destroy any laggards — at this point we've waited the full
227
229
  // timeout and they didn't ack. The operator chose timeoutMs; honor it.
228
230
  this._activeWsConns.forEach(function (conn) {
package/lib/seeders.js CHANGED
@@ -404,15 +404,7 @@ function create(opts) {
404
404
  }
405
405
 
406
406
  function _actor(callerOpts) {
407
- var override = {};
408
- if (callerOpts && callerOpts.context && typeof callerOpts.context === "object") {
409
- for (var k in callerOpts.context) {
410
- if (Object.prototype.hasOwnProperty.call(callerOpts.context, k)) {
411
- override[k] = callerOpts.context[k];
412
- }
413
- }
414
- }
415
- return requestHelpers.extractActorContext(callerOpts && callerOpts.req, override);
407
+ return requestHelpers.resolveActorWithOverride(callerOpts);
416
408
  }
417
409
 
418
410
  function _appliedRows(db, env) {
package/lib/session.js CHANGED
@@ -35,12 +35,13 @@
35
35
  * verify / count
36
36
  * — anywhere (any node can read shared session state)
37
37
  */
38
- var { generateToken, sha3Hash } = require("./crypto");
39
- var safeJson = require("./safe-json");
40
- var cryptoField = require("./crypto-field");
41
- var clusterStorage = require("./cluster-storage");
38
+ var audit = require("./audit");
42
39
  var cluster = require("./cluster");
40
+ var clusterStorage = require("./cluster-storage");
43
41
  var C = require("./constants");
42
+ var { generateToken, sha3Hash } = require("./crypto");
43
+ var cryptoField = require("./crypto-field");
44
+ var safeJson = require("./safe-json");
44
45
  var { SessionError } = require("./framework-error");
45
46
 
46
47
  var _err = SessionError.factory;
@@ -260,13 +261,12 @@ async function rotate(oldToken, opts) {
260
261
  // privilege transition so post-incident review can trace which
261
262
  // session id covered which privilege state.
262
263
  try {
263
- var audit = require("./audit");
264
264
  audit.emit({
265
265
  action: "auth.session.rotate",
266
266
  outcome: "success",
267
267
  metadata: { reason: opts.reason || "explicit" },
268
268
  });
269
- } catch (_e) { /* boot-order tolerance */ }
269
+ } catch (_e) { /* audit emit best-effort never block rotate() */ }
270
270
 
271
271
  return { token: newSid, expiresAt: expiresAt };
272
272
  }
@@ -56,6 +56,7 @@ var C = require("../constants");
56
56
  var cryptoField = require("../crypto-field");
57
57
  var cryptoLib = require("../crypto");
58
58
  var lazyRequire = require("../lazy-require");
59
+ var safeJson = require("../safe-json");
59
60
  var vaultWrap = lazyRequire(function () { return require("./wrap"); });
60
61
  var { defineClass } = require("../framework-error");
61
62
 
@@ -479,7 +480,10 @@ function _rotateOverflow(db, table, oldKeys, newKeys, batchSize, progress, warni
479
480
  for (var i = 0; i < rows.length; i++) {
480
481
  var row = rows[i];
481
482
  var doc;
482
- try { doc = JSON.parse(row.data); }
483
+ // Vault overflow rows are framework-written but read at rotate
484
+ // time — gate the parse via safeJson so a corrupted row can't
485
+ // stage a parse-bomb against the rotation worker.
486
+ try { doc = safeJson.parse(row.data, { maxBytes: C.BYTES.mib(16) }); }
483
487
  catch (_e) {
484
488
  warnings.push("malformed overflow JSON at " + table + "._id=" + row._id + " — left unrotated");
485
489
  continue;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blamejs/core",
3
- "version": "0.4.27",
3
+ "version": "0.4.29",
4
4
  "description": "The Node framework that owns its stack.",
5
5
  "license": "Apache-2.0",
6
6
  "author": "blamejs contributors",