@blamejs/core 0.17.23 → 0.18.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/CHANGELOG.md +11 -5
- package/NOTICE +12 -13
- package/README.md +3 -3
- package/lib/audit.js +69 -1
- package/lib/auth/oauth.js +57 -0
- package/lib/cli.js +15 -5
- package/lib/daemon.js +192 -2
- package/lib/guard-tenant-id.js +12 -1
- package/lib/mtls-ca.js +173 -18
- package/lib/mtls-engine-default.js +339 -314
- package/lib/outbox.js +43 -13
- package/lib/redact.js +39 -6
- package/lib/safe-json.js +9 -1
- package/lib/self-update.js +215 -24
- package/lib/vendor/MANIFEST.json +27 -30
- package/lib/vendor/blamejs-pki.cjs +28419 -0
- package/lib/webhook-dispatcher.js +55 -24
- package/package.json +1 -1
- package/sbom.cdx.json +6 -6
- package/lib/vendor/pki.cjs +0 -39718
package/lib/outbox.js
CHANGED
|
@@ -102,6 +102,28 @@ function _validateTableName(name) {
|
|
|
102
102
|
return safeSql.quoteIdentifier(name, undefined, { allowReserved: true }); // parity with b.db.from()
|
|
103
103
|
}
|
|
104
104
|
|
|
105
|
+
// The outbox is always operator-supplied `table` executed against a concrete
|
|
106
|
+
// externalDb handle (never b.clusterStorage — nothing here rewrites bare table
|
|
107
|
+
// names), so every b.sql builder quotes the table name by construction
|
|
108
|
+
// (quoteName: true). A quoted identifier is what makes a reserved-word or
|
|
109
|
+
// case-sensitive operator table (a table literally named "from") emit valid
|
|
110
|
+
// SQL — parity with b.db.from()'s allowReserved. Same posture as b.mailStore
|
|
111
|
+
// against its concrete sqlite handle.
|
|
112
|
+
function _tableOpts(dialect) { return { dialect: dialect, quoteName: true }; }
|
|
113
|
+
|
|
114
|
+
// PostgreSQL folds UNQUOTED identifiers to lowercase, so every pre-0.18 deployment
|
|
115
|
+
// (whose DDL and queries emitted the table name unquoted) already has a
|
|
116
|
+
// lowercase-folded table. Now that the name is always quoted (quoteName above), a
|
|
117
|
+
// mixed-case config like table: "MyOutbox" would target a NEW case-sensitive
|
|
118
|
+
// "MyOutbox" table and strand the rows in the original folded "myoutbox". Fold to
|
|
119
|
+
// lowercase on postgres BEFORE quoting so a legacy config keeps resolving to its
|
|
120
|
+
// existing table; reserved words (already lowercase) still quote correctly. sqlite
|
|
121
|
+
// is case-insensitive (no fold); MySQL's folding is server-configured
|
|
122
|
+
// (lower_case_table_names) and applied equally to the pre-quote name, so leave it.
|
|
123
|
+
function _foldTableForDialect(name, dialect) {
|
|
124
|
+
return dialect === "postgres" ? String(name).toLowerCase() : name;
|
|
125
|
+
}
|
|
126
|
+
|
|
105
127
|
// Map the operator backend's dialect tag to the b.sql dialect vocabulary.
|
|
106
128
|
// b.sql's terminal toExternalSql() then emits $1..$N for postgres and `?`
|
|
107
129
|
// for sqlite / mysql, matching what the operator-supplied driver expects.
|
|
@@ -182,6 +204,8 @@ function _toDebeziumEnvelope(rawEvent, opts) {
|
|
|
182
204
|
before: (payload && payload.before) || null,
|
|
183
205
|
after: (payload && payload.after !== undefined) ? payload.after : payload,
|
|
184
206
|
source: {
|
|
207
|
+
// connectorName/connectorVersion are defaulted to non-empty strings in create(), so these || fallbacks are unreachable.
|
|
208
|
+
/* c8 ignore next 2 */
|
|
185
209
|
connector: opts.connectorName || "blamejs",
|
|
186
210
|
version: opts.connectorVersion || DEFAULT_DEBEZIUM_CONNECTOR_VERSION,
|
|
187
211
|
db: opts.dbName || null,
|
|
@@ -300,6 +324,10 @@ function create(opts) {
|
|
|
300
324
|
|
|
301
325
|
var auditOn = opts.audit !== false;
|
|
302
326
|
var externalDb = opts.externalDb;
|
|
327
|
+
// Fold the operator table name for the backend's identifier rules BEFORE it is
|
|
328
|
+
// quoted downstream, so a legacy mixed-case config still targets its existing
|
|
329
|
+
// (folded) table on postgres. Reassign a clone so the caller's opts is untouched.
|
|
330
|
+
opts = Object.assign({}, opts, { table: _foldTableForDialect(opts.table, _sqlDialect(externalDb)) });
|
|
303
331
|
var publisher = opts.publisher;
|
|
304
332
|
var envelope = opts.envelope || "raw";
|
|
305
333
|
if (envelope !== "raw" && envelope !== "debezium") {
|
|
@@ -365,7 +393,7 @@ function create(opts) {
|
|
|
365
393
|
// enqueued_at and next_attempt_at both take the same publisher-clock
|
|
366
394
|
// moment; b.sql binds it as two separate `?` so the placeholder/param
|
|
367
395
|
// parity gate holds (no $5-reused-twice shorthand).
|
|
368
|
-
var stmt = sql.insert(opts.table,
|
|
396
|
+
var stmt = sql.insert(opts.table, _tableOpts(_sqlDialect(externalDb)))
|
|
369
397
|
.values({
|
|
370
398
|
topic: event.topic,
|
|
371
399
|
payload: payloadJson,
|
|
@@ -405,7 +433,7 @@ function create(opts) {
|
|
|
405
433
|
{ name: "attempts", type: "INTEGER", notNull: true, default: 0 },
|
|
406
434
|
{ name: "last_error", type: "TEXT" },
|
|
407
435
|
{ name: "status", type: "VARCHAR(16)", notNull: true, default: "pending" },
|
|
408
|
-
],
|
|
436
|
+
], _tableOpts(dialect)), dialect);
|
|
409
437
|
// Index for the publisher's claim path (scans status='pending' ORDER BY
|
|
410
438
|
// next_attempt_at). sqlite/postgres support a partial index (WHERE on
|
|
411
439
|
// CREATE INDEX) on next_attempt_at; MySQL does NOT — a WHERE there is a
|
|
@@ -414,7 +442,7 @@ function create(opts) {
|
|
|
414
442
|
// equality+range scan. The 'pending' literal is a builder-emitted static
|
|
415
443
|
// predicate, opted in via allowLiterals.
|
|
416
444
|
var idxCols = dialect === "mysql" ? ["status", "next_attempt_at"] : ["next_attempt_at"];
|
|
417
|
-
var idxOpts =
|
|
445
|
+
var idxOpts = _tableOpts(dialect);
|
|
418
446
|
if (dialect !== "mysql") idxOpts.where = "status = 'pending'";
|
|
419
447
|
var idx = sql.toExternalSql(sql.createIndex(opts.table + "_pending_idx", opts.table,
|
|
420
448
|
idxCols, idxOpts), dialect);
|
|
@@ -428,7 +456,7 @@ function create(opts) {
|
|
|
428
456
|
// claimed_at the reaper can't tell a stranded claim from a live one.
|
|
429
457
|
try {
|
|
430
458
|
var alter = sql.toExternalSql(sql.alterTable(opts.table,
|
|
431
|
-
{ addColumn: { name: "claimed_at", type: tsType } },
|
|
459
|
+
{ addColumn: { name: "claimed_at", type: tsType } }, _tableOpts(dialect)), dialect);
|
|
432
460
|
await target.query(alter.sql, alter.params);
|
|
433
461
|
} catch (_e) { /* column already present — idempotent add */ }
|
|
434
462
|
}
|
|
@@ -466,7 +494,7 @@ function create(opts) {
|
|
|
466
494
|
var nowExpr = _utcNowExpr(externalDb);
|
|
467
495
|
// status='pending' is a builder-emitted static predicate (opted in
|
|
468
496
|
// via allowLiterals); next_attempt_at <= ? + the LIMIT both bind.
|
|
469
|
-
var selectBuilder = sql.select(opts.table,
|
|
497
|
+
var selectBuilder = sql.select(opts.table, _tableOpts(dialect))
|
|
470
498
|
.columns(CLAIM_COLS)
|
|
471
499
|
.whereRaw("status = 'pending'", [], { allowLiterals: true })
|
|
472
500
|
.whereRaw("next_attempt_at <= ?", [nowExpr])
|
|
@@ -492,7 +520,7 @@ function create(opts) {
|
|
|
492
520
|
// Postgres/MySQL: row lock held; whereInArray emits `id = ANY(?)`
|
|
493
521
|
// on postgres (the whole id set as one bound array) / expanded
|
|
494
522
|
// `IN (?, ?, ...)` on mysql.
|
|
495
|
-
var claimUpdate = sql.update(opts.table,
|
|
523
|
+
var claimUpdate = sql.update(opts.table, _tableOpts(dialect))
|
|
496
524
|
.set({ status: "in-flight", claimed_at: _utcNowExpr(externalDb) })
|
|
497
525
|
.whereInArray("id", ids)
|
|
498
526
|
.toExternalSql(dialect);
|
|
@@ -504,13 +532,13 @@ function create(opts) {
|
|
|
504
532
|
// update we re-read the in-flight rows we own; rows that
|
|
505
533
|
// another publisher beat us to are skipped. whereInArray expands
|
|
506
534
|
// to an `IN (?, ?, ...)` placeholder list on sqlite.
|
|
507
|
-
var markUpdate = sql.update(opts.table,
|
|
535
|
+
var markUpdate = sql.update(opts.table, _tableOpts(dialect))
|
|
508
536
|
.set({ status: "in-flight", claimed_at: _utcNowExpr(externalDb) })
|
|
509
537
|
.whereRaw("status = 'pending'", [], { allowLiterals: true })
|
|
510
538
|
.whereInArray("id", ids)
|
|
511
539
|
.toExternalSql(dialect);
|
|
512
540
|
await xdb.query(markUpdate.sql, markUpdate.params);
|
|
513
|
-
var afterSelect = sql.select(opts.table,
|
|
541
|
+
var afterSelect = sql.select(opts.table, _tableOpts(dialect))
|
|
514
542
|
.columns(CLAIM_COLS)
|
|
515
543
|
.whereRaw("status = 'in-flight'", [], { allowLiterals: true })
|
|
516
544
|
.whereInArray("id", ids)
|
|
@@ -544,7 +572,7 @@ function create(opts) {
|
|
|
544
572
|
async function _reapStaleInflight() {
|
|
545
573
|
var dialect = _sqlDialect(externalDb);
|
|
546
574
|
var cutoff = new Date(Date.now() - claimReclaimMs);
|
|
547
|
-
var stmt = sql.update(opts.table,
|
|
575
|
+
var stmt = sql.update(opts.table, _tableOpts(dialect))
|
|
548
576
|
.set({ status: "pending", claimed_at: null })
|
|
549
577
|
.whereRaw("status = 'in-flight'", [], { allowLiterals: true })
|
|
550
578
|
.whereRaw("(claimed_at IS NULL OR claimed_at <= ?)", [cutoff])
|
|
@@ -555,7 +583,7 @@ function create(opts) {
|
|
|
555
583
|
|
|
556
584
|
async function _markPublished(id) {
|
|
557
585
|
var dialect = _sqlDialect(externalDb);
|
|
558
|
-
var stmt = sql.update(opts.table,
|
|
586
|
+
var stmt = sql.update(opts.table, _tableOpts(dialect))
|
|
559
587
|
.set({ status: "published", published_at: _utcNowExpr(externalDb) })
|
|
560
588
|
.where("id", id)
|
|
561
589
|
.toExternalSql(dialect);
|
|
@@ -565,7 +593,7 @@ function create(opts) {
|
|
|
565
593
|
async function _markRetry(id, attempts, errMsg) {
|
|
566
594
|
var dialect = _sqlDialect(externalDb);
|
|
567
595
|
var nextAt = new Date(Date.now() + _backoffMs(attempts + 1));
|
|
568
|
-
var stmt = sql.update(opts.table,
|
|
596
|
+
var stmt = sql.update(opts.table, _tableOpts(dialect))
|
|
569
597
|
.set({
|
|
570
598
|
status: "pending",
|
|
571
599
|
attempts: attempts + 1,
|
|
@@ -579,7 +607,7 @@ function create(opts) {
|
|
|
579
607
|
|
|
580
608
|
async function _markDead(id, attempts, errMsg) {
|
|
581
609
|
var dialect = _sqlDialect(externalDb);
|
|
582
|
-
var stmt = sql.update(opts.table,
|
|
610
|
+
var stmt = sql.update(opts.table, _tableOpts(dialect))
|
|
583
611
|
.set({
|
|
584
612
|
status: "dead",
|
|
585
613
|
attempts: attempts + 1,
|
|
@@ -654,6 +682,8 @@ function create(opts) {
|
|
|
654
682
|
workerHandle = null;
|
|
655
683
|
}
|
|
656
684
|
if (inFlight) {
|
|
685
|
+
// inFlight is _processOnce().catch(...).finally(...), which never rejects, so this drop-silent catch cannot fire.
|
|
686
|
+
/* c8 ignore next */
|
|
657
687
|
try { await inFlight; } catch (_e) { /* drop-silent */ }
|
|
658
688
|
}
|
|
659
689
|
_emitAudit("system.outbox.stopped", "success", { name: name });
|
|
@@ -664,7 +694,7 @@ function create(opts) {
|
|
|
664
694
|
// status is a fixed builder-internal literal ('pending' / 'dead'),
|
|
665
695
|
// never operator input; opted in via allowLiterals. COUNT(*) AS n is
|
|
666
696
|
// the count aggregate with an alias.
|
|
667
|
-
var stmt = sql.select(opts.table,
|
|
697
|
+
var stmt = sql.select(opts.table, _tableOpts(dialect))
|
|
668
698
|
.count("*", "n")
|
|
669
699
|
.whereRaw("status = '" + status + "'", [], { allowLiterals: true })
|
|
670
700
|
.toExternalSql(dialect);
|
package/lib/redact.js
CHANGED
|
@@ -74,6 +74,8 @@ var SENSITIVE_FIELDS = [
|
|
|
74
74
|
// matches the pattern. Used as a fallback when field-name redaction misses.
|
|
75
75
|
function _luhnCheck(num) {
|
|
76
76
|
var digits = num.replace(/\D/g, "");
|
|
77
|
+
// unreachable: every _luhnCheck caller pre-gates digit length to 13-19
|
|
78
|
+
/* c8 ignore next */
|
|
77
79
|
if (digits.length < 13 || digits.length > 19) return false;
|
|
78
80
|
var sum = 0;
|
|
79
81
|
var alt = false;
|
|
@@ -90,6 +92,8 @@ var VALUE_DETECTORS = [
|
|
|
90
92
|
{
|
|
91
93
|
name: "credit-card",
|
|
92
94
|
test: function (v) {
|
|
95
|
+
// unreachable: _redactValue only invokes value detectors on strings
|
|
96
|
+
/* c8 ignore next */
|
|
93
97
|
if (typeof v !== "string") return false;
|
|
94
98
|
var digits = v.replace(/\s|-/g, "");
|
|
95
99
|
if (!/^\d{13,19}$/.test(digits)) return false;
|
|
@@ -231,6 +235,8 @@ function _isSensitiveFieldName(key) {
|
|
|
231
235
|
}
|
|
232
236
|
|
|
233
237
|
function _redactValue(value) {
|
|
238
|
+
// unreachable: _redact only calls _redactValue inside its string branch
|
|
239
|
+
/* c8 ignore next */
|
|
234
240
|
if (typeof value !== "string") return value;
|
|
235
241
|
var allDetectors = VALUE_DETECTORS.concat(customDetectors);
|
|
236
242
|
for (var i = 0; i < allDetectors.length; i++) {
|
|
@@ -396,7 +402,9 @@ var CLASSIFIER_PATTERNS = Object.freeze({
|
|
|
396
402
|
var c = rearranged.charCodeAt(i);
|
|
397
403
|
if (c >= 48 && c <= 57) num += rearranged.charAt(i); // ASCII '0'..'9' codepoint range
|
|
398
404
|
else if (c >= 65 && c <= 90) num += String(c - 55);
|
|
405
|
+
/* c8 ignore start */ // unreachable: the regex gate above admits only [A-Z0-9]
|
|
399
406
|
else return false;
|
|
407
|
+
/* c8 ignore stop */
|
|
400
408
|
}
|
|
401
409
|
// Long-integer mod 97 in chunks
|
|
402
410
|
var rem = 0;
|
|
@@ -600,6 +608,8 @@ function classifyDefaults(opts) {
|
|
|
600
608
|
if (Buffer.isBuffer(bodyVal) || bodyVal instanceof Uint8Array) {
|
|
601
609
|
var asText;
|
|
602
610
|
try { asText = Buffer.from(bodyVal).toString("utf8"); }
|
|
611
|
+
// unreachable drop-silent: Buffer.toString only throws on >512MiB payloads
|
|
612
|
+
/* c8 ignore next */
|
|
603
613
|
catch (_e) { asText = ""; }
|
|
604
614
|
var scannedText = _scanString(asText, "body");
|
|
605
615
|
redactedBody = scannedText === asText ? bodyVal : Buffer.from(scannedText, "utf8");
|
|
@@ -646,6 +656,8 @@ function _emitDlp(action, outcome, metadata) {
|
|
|
646
656
|
audit().safeEmit({
|
|
647
657
|
action: action,
|
|
648
658
|
outcome: outcome,
|
|
659
|
+
// unreachable: every _emitDlp caller passes a metadata object literal
|
|
660
|
+
/* c8 ignore next */
|
|
649
661
|
metadata: metadata || {},
|
|
650
662
|
});
|
|
651
663
|
} catch (_e) { /* drop-silent */ }
|
|
@@ -658,6 +670,8 @@ function _wrapClassifier(fn, where) {
|
|
|
658
670
|
}
|
|
659
671
|
return function safeClassify(input) {
|
|
660
672
|
var v;
|
|
673
|
+
// unreachable: the wrapped classifier is only invoked internally with an object
|
|
674
|
+
/* c8 ignore next */
|
|
661
675
|
try { v = fn(input || {}); }
|
|
662
676
|
catch (e) {
|
|
663
677
|
// Classifier threw — treat as refuse (fail-closed, since the
|
|
@@ -1069,8 +1083,9 @@ var TEXT_REDACT_MARKER = "[redacted]";
|
|
|
1069
1083
|
* secret may be interpolated mid-sentence. Unlike `redact` (structured,
|
|
1070
1084
|
* whole-value, anchored), this uses word-boundary fragment replacement so
|
|
1071
1085
|
* "login failed: <jwt> for bob" keeps everything but the jwt. Detects PEM
|
|
1072
|
-
* blocks, JWTs, AWS access keys, URL-userinfo passwords
|
|
1073
|
-
* `key=secret` assignments,
|
|
1086
|
+
* blocks, JWTs, AWS access keys, vault-sealed ciphertext, URL-userinfo passwords
|
|
1087
|
+
* (including empty-username forms), bearer tokens, `key=secret` assignments,
|
|
1088
|
+
* SSN/EIN, and Luhn-valid PANs. The high-entropy
|
|
1074
1089
|
* api-key-shape detector is deliberately excluded (on free text it eats
|
|
1075
1090
|
* ordinary IDs / hashes / base64). Drop-safe: never throws (it runs on the
|
|
1076
1091
|
* hot-path log-emit sink); on any error it returns a fully-masked marker rather
|
|
@@ -1089,12 +1104,28 @@ function redactText(str) {
|
|
|
1089
1104
|
.replace(/\beyJ[A-Za-z0-9_-]{1,4096}\.[A-Za-z0-9_-]{1,8192}\.[A-Za-z0-9_-]{1,4096}\b/g, TEXT_REDACT_MARKER)
|
|
1090
1105
|
// AWS access-key IDs.
|
|
1091
1106
|
.replace(/\b(?:AKIA|ASIA|AGPA|AIDA|AROA|AIPA|ANPA|ANVA|ASCA)[A-Z0-9]{16}\b/g, TEXT_REDACT_MARKER)
|
|
1092
|
-
//
|
|
1093
|
-
|
|
1107
|
+
// Vault-sealed ciphertext (vault:v1:…) — the structured meta path strips
|
|
1108
|
+
// these via the vault-sealed value detector (anchored to startsWith), so
|
|
1109
|
+
// an embedded one in a free-text message must not survive either.
|
|
1110
|
+
.replace(/\bvault:[A-Za-z0-9+/=_.:-]{3,8192}/g, TEXT_REDACT_MARKER)
|
|
1111
|
+
// Credentials in a URL userinfo: scheme://user:secret@host. The username
|
|
1112
|
+
// segment is optional ({0,…}) so an empty-username form (redis://:pw@host)
|
|
1113
|
+
// — which the meta path's connection-string detector catches — is scrubbed
|
|
1114
|
+
// too; a password segment before the '@' is what flags the credential.
|
|
1115
|
+
.replace(/([a-z][a-z0-9+.-]{0,32}:\/\/[^\s:@/]{0,256}:)[^\s@/]{1,256}(@)/gi, "$1" + TEXT_REDACT_MARKER + "$2")
|
|
1094
1116
|
// Bearer tokens.
|
|
1095
1117
|
.replace(/\b([Bb]earer\s{1,4})[A-Za-z0-9._~+/-]{8,4096}=*/g, "$1" + TEXT_REDACT_MARKER)
|
|
1096
|
-
// key=secret / password: secret style assignments.
|
|
1097
|
-
|
|
1118
|
+
// key=secret / password: secret style assignments. The specific
|
|
1119
|
+
// *_token forms precede bare `token` so an underscore-joined name
|
|
1120
|
+
// (id_token, refresh_token — where \btoken cannot match across the
|
|
1121
|
+
// underscore) is still redacted, matching the meta path's coverage.
|
|
1122
|
+
// The optional ["'] on BOTH sides of the delimiter lets the JSON / quoted
|
|
1123
|
+
// form ({"refresh_token":"opaque…"}) match: the key's closing quote sits
|
|
1124
|
+
// before the delimiter and the value's opening quote after it, and the value
|
|
1125
|
+
// class excludes quotes so it could otherwise neither reach the delimiter nor
|
|
1126
|
+
// begin at the value. The value still stops at the closing quote, leaving the
|
|
1127
|
+
// surrounding structure intact.
|
|
1128
|
+
.replace(/\b((?:api[_-]?key|access[_-]?token|refresh[_-]?token|id[_-]?token|secret|password|passwd|pwd|token)["']?\s{0,4}[=:]\s{0,4}["']?)[^\s&;"']{6,4096}/gi, "$1" + TEXT_REDACT_MARKER)
|
|
1098
1129
|
// SSN / EIN.
|
|
1099
1130
|
.replace(/\b\d{3}-\d{2}-\d{4}\b/g, TEXT_REDACT_MARKER)
|
|
1100
1131
|
.replace(/\b\d{2}-\d{7}\b/g, TEXT_REDACT_MARKER)
|
|
@@ -1103,6 +1134,8 @@ function redactText(str) {
|
|
|
1103
1134
|
var inner = m.replace(/[\s-]/g, "");
|
|
1104
1135
|
return (inner.length >= 13 && inner.length <= 19 && _luhnCheck(inner)) ? TEXT_REDACT_MARKER : m;
|
|
1105
1136
|
});
|
|
1137
|
+
// unreachable drop-silent: the replace chain runs on a guaranteed string with static regexes
|
|
1138
|
+
/* c8 ignore next 3 */
|
|
1106
1139
|
} catch (_e) {
|
|
1107
1140
|
return TEXT_REDACT_MARKER;
|
|
1108
1141
|
}
|
package/lib/safe-json.js
CHANGED
|
@@ -170,7 +170,14 @@ function parse(input, opts) {
|
|
|
170
170
|
try {
|
|
171
171
|
parsed = JSON.parse(input, allowProto ? undefined : _stripProtoKeys);
|
|
172
172
|
} catch (e) {
|
|
173
|
-
|
|
173
|
+
// V8's SyntaxError.message echoes a window of the offending input
|
|
174
|
+
// ("Unexpected token 'M', \"{...secret...\"... is not valid JSON").
|
|
175
|
+
// At a trust boundary that snippet can carry secret-bearing bytes
|
|
176
|
+
// (a decrypted key, a token) straight into any log that records the
|
|
177
|
+
// thrown error (CWE-532). Keep only the non-secret facts: the stable
|
|
178
|
+
// code and, when V8 provides it, the numeric character offset.
|
|
179
|
+
var pos = (/position (\d+)/.exec(e && e.message) || [])[1];
|
|
180
|
+
throw new SafeJsonError("invalid JSON syntax" + (pos ? " at position " + pos : ""), "json/syntax");
|
|
174
181
|
}
|
|
175
182
|
|
|
176
183
|
_walkAndCheck(parsed, 0, maxDepth, allowProto, maxKeys);
|
|
@@ -313,6 +320,7 @@ function _walkAndCheck(value, depth, maxDepth, allowProto, maxKeys) {
|
|
|
313
320
|
}
|
|
314
321
|
if (!allowProto) {
|
|
315
322
|
pick.POISONED_KEYS.forEach(function (k) {
|
|
323
|
+
/* c8 ignore next -- defensive second layer: when !allowProto the parse reviver already stripped every poisoned key before this walk, so the own-key delete is never reached from the public API */
|
|
316
324
|
if (Object.prototype.hasOwnProperty.call(value, k)) delete value[k];
|
|
317
325
|
});
|
|
318
326
|
}
|