@blamejs/core 0.4.28 → 0.5.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/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/db.js CHANGED
@@ -109,6 +109,8 @@ var RESERVED_TABLE_NAMES = new Set([
109
109
  "_blamejs_cache",
110
110
  "_blamejs_seeders",
111
111
  "_blamejs_seeders_lock",
112
+ "_blamejs_break_glass_policies",
113
+ "_blamejs_break_glass_grants",
112
114
  ]);
113
115
 
114
116
  var FRAMEWORK_SCHEMA = [
@@ -423,6 +425,67 @@ var FRAMEWORK_SCHEMA = [
423
425
  },
424
426
  sealedFields: [],
425
427
  },
428
+ {
429
+ // _blamejs_break_glass_policies — column-level break-glass policy
430
+ // registry. One row per (table) declares which columns are
431
+ // glass-locked and what the operator's grant rules are. Sealed
432
+ // columns hold the column-list, factor-list, and bypass config so
433
+ // policy contents aren't browsable in cleartext.
434
+ name: "_blamejs_break_glass_policies",
435
+ columns: {
436
+ tableName: "TEXT PRIMARY KEY",
437
+ columnsJson: "TEXT NOT NULL",
438
+ factorsJson: "TEXT NOT NULL",
439
+ cryptographic: "INTEGER NOT NULL DEFAULT 0",
440
+ grantTtlMs: "INTEGER NOT NULL",
441
+ maxRowsPerGrant: "INTEGER NOT NULL DEFAULT 1",
442
+ reasonRequired: "INTEGER NOT NULL DEFAULT 1",
443
+ reasonMinLength: "INTEGER NOT NULL DEFAULT 12",
444
+ pinIp: "INTEGER NOT NULL DEFAULT 1",
445
+ sessionPin: "INTEGER NOT NULL DEFAULT 1",
446
+ onLockedAccess: "TEXT NOT NULL DEFAULT 'throw'",
447
+ requireScope: "TEXT",
448
+ serviceAccountBypassJson: "TEXT",
449
+ auditReasonStorage: "TEXT NOT NULL DEFAULT 'cleartext'",
450
+ updatedAt: "INTEGER NOT NULL",
451
+ },
452
+ indexes: [],
453
+ sealedFields: ["columnsJson", "factorsJson", "serviceAccountBypassJson"],
454
+ },
455
+ {
456
+ // _blamejs_break_glass_grants — issued grants. Each successful
457
+ // step-up creates one row; each row read decrements rowsRemaining.
458
+ // Default maxRowsPerGrant=1 enforces "row by row" auth per the
459
+ // operator-confirmed shape (each row access = its own grant).
460
+ // Sealed columns hold reason + scopeColumns so audit-readable
461
+ // metadata doesn't leak in cleartext.
462
+ name: "_blamejs_break_glass_grants",
463
+ columns: {
464
+ _id: "TEXT PRIMARY KEY",
465
+ issuedToActorId: "TEXT NOT NULL",
466
+ issuedToActorHash: "TEXT NOT NULL",
467
+ factorType: "TEXT NOT NULL",
468
+ reasonSealed: "TEXT",
469
+ scopeTable: "TEXT NOT NULL",
470
+ scopeColumnsJson: "TEXT NOT NULL",
471
+ issuedAt: "INTEGER NOT NULL",
472
+ expiresAt: "INTEGER NOT NULL",
473
+ maxRowsPerGrant: "INTEGER NOT NULL",
474
+ rowsConsumed: "INTEGER NOT NULL DEFAULT 0",
475
+ revokedAt: "INTEGER",
476
+ sessionId: "TEXT",
477
+ ip: "TEXT",
478
+ kwGrantHalf: "TEXT",
479
+ },
480
+ indexes: [
481
+ { name: "idx_bg_grants_actor", columns: ["issuedToActorHash"] },
482
+ { name: "idx_bg_grants_table", columns: ["scopeTable"] },
483
+ "expiresAt",
484
+ "revokedAt",
485
+ ],
486
+ derivedHashes: { issuedToActorHash: { from: "issuedToActorId" } },
487
+ sealedFields: ["reasonSealed", "scopeColumnsJson"],
488
+ },
426
489
  ];
427
490
 
428
491
  var log = boot("db");
@@ -1118,6 +1181,7 @@ module.exports = {
1118
1181
  try { require("./storage")._resetForTest(); } catch (_e) {}
1119
1182
  try { require("./audit-sign")._resetForTest(); } catch (_e) {}
1120
1183
  try { require("./queue")._resetForTest(); } catch (_e) {}
1184
+ try { require("./break-glass")._resetForTest(); } catch (_e) {}
1121
1185
  try { require("./log-stream")._resetForTest(); } catch (_e) {}
1122
1186
  try { require("./redact")._resetForTest(); } catch (_e) {}
1123
1187
  try { require("./external-db")._resetForTest(); } catch (_e) {}
@@ -120,6 +120,11 @@ var LOCAL_TO_EXTERNAL = Object.freeze({
120
120
  // FRAMEWORK_SCHEMA so cluster-storage.execute routes to either side.
121
121
  _blamejs_seeders: "_blamejs_seeders",
122
122
  _blamejs_seeders_lock: "_blamejs_seeders_lock",
123
+ // Break-glass policy + grant tables. Cluster-shared so a grant
124
+ // issued on node A is honored on node B; policies updated on the
125
+ // leader propagate to all followers via the shared table.
126
+ _blamejs_break_glass_policies: "_blamejs_break_glass_policies",
127
+ _blamejs_break_glass_grants: "_blamejs_break_glass_grants",
123
128
  });
124
129
 
125
130
  function tableName(localName) {
@@ -556,6 +561,70 @@ function _cacheDDL(dialect) {
556
561
  };
557
562
  }
558
563
 
564
+ // _blamejs_break_glass_policies — column-level break-glass policy
565
+ // registry. One row per (table) declares which columns are
566
+ // glass-locked + the operator's grant rules. Sealed columns hide
567
+ // column-list / factor-list / bypass config from cleartext browsing.
568
+ function _breakGlassPoliciesDDL(dialect) {
569
+ var t = _types(dialect);
570
+ var name = LOCAL_TO_EXTERNAL._blamejs_break_glass_policies;
571
+ return {
572
+ create:
573
+ "CREATE TABLE IF NOT EXISTS " + name + " (" +
574
+ " tableName TEXT PRIMARY KEY," +
575
+ " columnsJson TEXT NOT NULL," +
576
+ " factorsJson TEXT NOT NULL," +
577
+ " cryptographic " + t.INT + " NOT NULL DEFAULT 0," +
578
+ " grantTtlMs " + t.INT + " NOT NULL," +
579
+ " maxRowsPerGrant " + t.INT + " NOT NULL DEFAULT 1," +
580
+ " reasonRequired " + t.INT + " NOT NULL DEFAULT 1," +
581
+ " reasonMinLength " + t.INT + " NOT NULL DEFAULT 12," +
582
+ " pinIp " + t.INT + " NOT NULL DEFAULT 1," +
583
+ " sessionPin " + t.INT + " NOT NULL DEFAULT 1," +
584
+ " onLockedAccess TEXT NOT NULL DEFAULT 'throw'," +
585
+ " requireScope TEXT," +
586
+ " serviceAccountBypassJson TEXT," +
587
+ " auditReasonStorage TEXT NOT NULL DEFAULT 'cleartext'," +
588
+ " updatedAt " + t.INT + " NOT NULL" +
589
+ ")",
590
+ indexes: [],
591
+ };
592
+ }
593
+
594
+ // _blamejs_break_glass_grants — issued grants. One row per successful
595
+ // step-up. Default maxRowsPerGrant=1 enforces row-by-row auth per the
596
+ // operator-confirmed shape ("each row access = its own grant").
597
+ function _breakGlassGrantsDDL(dialect) {
598
+ var t = _types(dialect);
599
+ var name = LOCAL_TO_EXTERNAL._blamejs_break_glass_grants;
600
+ return {
601
+ create:
602
+ "CREATE TABLE IF NOT EXISTS " + name + " (" +
603
+ " _id TEXT PRIMARY KEY," +
604
+ " issuedToActorId TEXT NOT NULL," +
605
+ " issuedToActorHash TEXT NOT NULL," +
606
+ " factorType TEXT NOT NULL," +
607
+ " reasonSealed TEXT," +
608
+ " scopeTable TEXT NOT NULL," +
609
+ " scopeColumnsJson TEXT NOT NULL," +
610
+ " issuedAt " + t.INT + " NOT NULL," +
611
+ " expiresAt " + t.INT + " NOT NULL," +
612
+ " maxRowsPerGrant " + t.INT + " NOT NULL," +
613
+ " rowsConsumed " + t.INT + " NOT NULL DEFAULT 0," +
614
+ " revokedAt " + t.INT + "," +
615
+ " sessionId TEXT," +
616
+ " ip TEXT," +
617
+ " kwGrantHalf TEXT" +
618
+ ")",
619
+ indexes: [
620
+ "CREATE INDEX IF NOT EXISTS idx_" + name + "_actor ON " + name + " (issuedToActorHash)",
621
+ "CREATE INDEX IF NOT EXISTS idx_" + name + "_table ON " + name + " (scopeTable)",
622
+ "CREATE INDEX IF NOT EXISTS idx_" + name + "_expires ON " + name + " (expiresAt)",
623
+ "CREATE INDEX IF NOT EXISTS idx_" + name + "_revoked ON " + name + " (revokedAt)",
624
+ ],
625
+ };
626
+ }
627
+
559
628
  // ---- ensureSchema ----
560
629
 
561
630
  async function ensureSchema(opts) {
@@ -590,6 +659,8 @@ async function ensureSchema(opts) {
590
659
  _cacheDDL(dialect),
591
660
  _seedersDDL(dialect),
592
661
  _seedersLockDDL(dialect),
662
+ _breakGlassPoliciesDDL(dialect),
663
+ _breakGlassGrantsDDL(dialect),
593
664
  ];
594
665
 
595
666
  var created = [];
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
  }
@@ -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) {
@@ -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/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) {
@@ -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.28",
3
+ "version": "0.5.0",
4
4
  "description": "The Node framework that owns its stack.",
5
5
  "license": "Apache-2.0",
6
6
  "author": "blamejs contributors",