@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 +2 -0
- package/lib/api-key.js +5 -11
- package/lib/audit-tools.js +9 -7
- package/lib/auth/oauth.js +11 -4
- package/lib/backup/manifest.js +4 -1
- package/lib/cache.js +1 -9
- package/lib/cli.js +6 -7
- package/lib/db-query.js +1 -1
- package/lib/db.js +1 -1
- package/lib/http-client.js +3 -2
- package/lib/mail.js +13 -2
- package/lib/middleware/api-encrypt.js +24 -7
- package/lib/middleware/attach-user.js +3 -2
- package/lib/middleware/body-parser.js +9 -9
- package/lib/middleware/cors.js +11 -1
- package/lib/notify.js +1 -9
- package/lib/object-store/gcs.js +1 -1
- package/lib/queue-local.js +2 -2
- package/lib/queue.js +4 -4
- package/lib/request-helpers.js +30 -4
- package/lib/restore-rollback.js +3 -1
- package/lib/router.js +8 -6
- package/lib/seeders.js +1 -9
- package/lib/session.js +6 -6
- package/lib/vault/rotate.js +5 -1
- package/package.json +1 -1
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
|
|
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
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
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() {
|
package/lib/audit-tools.js
CHANGED
|
@@ -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
|
|
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
|
|
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 =
|
|
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 =
|
|
608
|
-
payload =
|
|
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)));
|
package/lib/backup/manifest.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
|
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 {
|
|
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
|
-
|
|
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 {
|
package/lib/http-client.js
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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 =
|
|
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:
|
|
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 =
|
|
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
|
|
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
|
|
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 =
|
|
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
|
|
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
|
|
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:
|
|
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:
|
|
34
|
+
* limit: b.constants.BYTES.mib(1),
|
|
35
35
|
* arrayLimit: 100, // for ?tag=a&tag=b → tag: ["a","b"]
|
|
36
36
|
* },
|
|
37
|
-
* text: { limit:
|
|
38
|
-
* raw: { limit: 10
|
|
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
|
|
42
|
-
* totalSize: 50
|
|
41
|
+
* fileSize: b.constants.BYTES.mib(10),
|
|
42
|
+
* totalSize: b.constants.BYTES.mib(50),
|
|
43
43
|
* fileCount: 20,
|
|
44
44
|
* fieldCount: 100,
|
|
45
|
-
* fieldSize:
|
|
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
|
|
68
|
-
* document: { maxBytes: 25
|
|
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
|
package/lib/middleware/cors.js
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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) {
|
package/lib/object-store/gcs.js
CHANGED
|
@@ -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)
|
|
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,
|
package/lib/queue-local.js
CHANGED
|
@@ -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",
|
package/lib/request-helpers.js
CHANGED
|
@@ -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:
|
|
175
|
-
captureResponseStatus:
|
|
176
|
-
extractActorContext:
|
|
177
|
-
|
|
199
|
+
resolveRoute: resolveRoute,
|
|
200
|
+
captureResponseStatus: captureResponseStatus,
|
|
201
|
+
extractActorContext: extractActorContext,
|
|
202
|
+
resolveActorWithOverride: resolveActorWithOverride,
|
|
203
|
+
parseQualityList: parseQualityList,
|
|
178
204
|
};
|
package/lib/restore-rollback.js
CHANGED
|
@@ -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 =
|
|
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
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
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
|
-
|
|
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
|
|
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) { /*
|
|
269
|
+
} catch (_e) { /* audit emit best-effort — never block rotate() */ }
|
|
270
270
|
|
|
271
271
|
return { token: newSid, expiresAt: expiresAt };
|
|
272
272
|
}
|
package/lib/vault/rotate.js
CHANGED
|
@@ -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
|
-
|
|
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;
|