@blamejs/core 0.4.28 → 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 +1 -0
- package/lib/api-key.js +4 -10
- 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/mail.js +13 -2
- package/lib/middleware/api-encrypt.js +24 -7
- package/lib/middleware/attach-user.js +3 -2
- package/lib/middleware/cors.js +11 -1
- package/lib/notify.js +1 -9
- package/lib/request-helpers.js +30 -4
- package/lib/restore-rollback.js +3 -1
- package/lib/seeders.js +1 -9
- package/lib/vault/rotate.js +5 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -8,6 +8,7 @@ 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
|
|
11
12
|
- **0.4.27** (2026-04-30) — primitive-drift sweep: regex + escape consolidation, IPv6 completion
|
|
12
13
|
- **0.4.26** (2026-04-30) — primitive-drift sweep: middleware audit context + safeUrl
|
|
13
14
|
- **0.4.25** (2026-04-30) — b.objectStore.bucketOps: bucket-level operations (SigV4)
|
package/lib/api-key.js
CHANGED
|
@@ -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/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
|
}
|
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/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/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/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;
|