@blamejs/core 0.8.42 → 0.8.49
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 +93 -0
- package/README.md +10 -10
- package/index.js +52 -0
- package/lib/a2a.js +159 -34
- package/lib/acme.js +762 -0
- package/lib/ai-pref.js +166 -43
- package/lib/api-key.js +108 -47
- package/lib/api-snapshot.js +157 -40
- package/lib/app-shutdown.js +113 -77
- package/lib/archive.js +337 -40
- package/lib/arg-parser.js +697 -0
- package/lib/asyncapi.js +99 -55
- package/lib/atomic-file.js +465 -104
- package/lib/audit-chain.js +123 -34
- package/lib/audit-daily-review.js +389 -0
- package/lib/audit-sign.js +302 -56
- package/lib/audit-tools.js +412 -63
- package/lib/audit.js +656 -35
- package/lib/auth/jwt-external.js +17 -0
- package/lib/auth/oauth.js +7 -0
- package/lib/auth-bot-challenge.js +505 -0
- package/lib/auth-header.js +92 -25
- package/lib/backup/bundle.js +26 -0
- package/lib/backup/index.js +512 -89
- package/lib/backup/manifest.js +168 -7
- package/lib/break-glass.js +415 -39
- package/lib/budr.js +103 -30
- package/lib/bundler.js +86 -66
- package/lib/cache.js +192 -72
- package/lib/chain-writer.js +65 -40
- package/lib/circuit-breaker.js +56 -33
- package/lib/cli-helpers.js +106 -75
- package/lib/cli.js +6 -30
- package/lib/cloud-events.js +99 -32
- package/lib/cluster-storage.js +162 -37
- package/lib/cluster.js +340 -49
- package/lib/codepoint-class.js +66 -0
- package/lib/compliance.js +424 -24
- package/lib/config-drift.js +111 -46
- package/lib/config.js +94 -40
- package/lib/consent.js +165 -18
- package/lib/constants.js +1 -0
- package/lib/content-credentials.js +153 -48
- package/lib/cookies.js +154 -62
- package/lib/credential-hash.js +133 -61
- package/lib/crypto-field.js +702 -18
- package/lib/crypto-hpke.js +256 -0
- package/lib/crypto.js +744 -22
- package/lib/csv.js +178 -35
- package/lib/daemon.js +456 -0
- package/lib/dark-patterns.js +186 -55
- package/lib/db-query.js +79 -2
- package/lib/db.js +1431 -60
- package/lib/ddl-change-control.js +523 -0
- package/lib/deprecate.js +195 -40
- package/lib/dev.js +82 -39
- package/lib/dora.js +67 -48
- package/lib/dr-runbook.js +368 -0
- package/lib/dsr.js +142 -11
- package/lib/dual-control.js +91 -56
- package/lib/events.js +120 -41
- package/lib/external-db-migrate.js +192 -2
- package/lib/external-db.js +795 -50
- package/lib/fapi2.js +122 -1
- package/lib/fda-21cfr11.js +395 -0
- package/lib/fdx.js +132 -2
- package/lib/file-type.js +87 -0
- package/lib/file-upload.js +93 -0
- package/lib/flag.js +82 -20
- package/lib/forms.js +132 -29
- package/lib/framework-error.js +169 -0
- package/lib/framework-schema.js +163 -35
- package/lib/gate-contract.js +849 -175
- package/lib/graphql-federation.js +68 -7
- package/lib/guard-all.js +172 -55
- package/lib/guard-archive.js +286 -124
- package/lib/guard-auth.js +194 -21
- package/lib/guard-cidr.js +190 -28
- package/lib/guard-csv.js +397 -51
- package/lib/guard-domain.js +213 -91
- package/lib/guard-email.js +236 -29
- package/lib/guard-filename.js +307 -75
- package/lib/guard-graphql.js +263 -30
- package/lib/guard-html.js +310 -116
- package/lib/guard-image.js +243 -30
- package/lib/guard-json.js +260 -54
- package/lib/guard-jsonpath.js +235 -23
- package/lib/guard-jwt.js +284 -30
- package/lib/guard-markdown.js +204 -22
- package/lib/guard-mime.js +190 -26
- package/lib/guard-oauth.js +277 -28
- package/lib/guard-pdf.js +251 -27
- package/lib/guard-regex.js +226 -18
- package/lib/guard-shell.js +229 -26
- package/lib/guard-svg.js +177 -10
- package/lib/guard-template.js +232 -21
- package/lib/guard-time.js +195 -29
- package/lib/guard-uuid.js +189 -30
- package/lib/guard-xml.js +259 -36
- package/lib/guard-yaml.js +241 -44
- package/lib/honeytoken.js +63 -27
- package/lib/html-balance.js +83 -0
- package/lib/http-client.js +486 -59
- package/lib/http-message-signature.js +582 -0
- package/lib/i18n.js +102 -49
- package/lib/iab-mspa.js +112 -32
- package/lib/iab-tcf.js +107 -2
- package/lib/inbox.js +90 -52
- package/lib/keychain.js +865 -0
- package/lib/legal-hold.js +374 -0
- package/lib/local-db-thin.js +320 -0
- package/lib/log-stream.js +281 -51
- package/lib/log.js +184 -86
- package/lib/mail-bounce.js +107 -62
- package/lib/mail.js +295 -58
- package/lib/mcp.js +108 -27
- package/lib/metrics.js +98 -89
- package/lib/middleware/age-gate.js +36 -0
- package/lib/middleware/ai-act-disclosure.js +37 -0
- package/lib/middleware/api-encrypt.js +45 -0
- package/lib/middleware/assetlinks.js +40 -0
- package/lib/middleware/asyncapi-serve.js +35 -0
- package/lib/middleware/attach-user.js +40 -0
- package/lib/middleware/bearer-auth.js +40 -0
- package/lib/middleware/body-parser.js +230 -0
- package/lib/middleware/bot-disclose.js +34 -0
- package/lib/middleware/bot-guard.js +39 -0
- package/lib/middleware/compression.js +37 -0
- package/lib/middleware/cookies.js +32 -0
- package/lib/middleware/cors.js +40 -0
- package/lib/middleware/csp-nonce.js +40 -0
- package/lib/middleware/csp-report.js +34 -0
- package/lib/middleware/csrf-protect.js +43 -0
- package/lib/middleware/daily-byte-quota.js +53 -85
- package/lib/middleware/db-role-for.js +40 -0
- package/lib/middleware/dpop.js +40 -0
- package/lib/middleware/error-handler.js +37 -14
- package/lib/middleware/fetch-metadata.js +39 -0
- package/lib/middleware/flag-context.js +34 -0
- package/lib/middleware/gpc.js +33 -0
- package/lib/middleware/headers.js +35 -0
- package/lib/middleware/health.js +46 -0
- package/lib/middleware/host-allowlist.js +30 -0
- package/lib/middleware/network-allowlist.js +38 -0
- package/lib/middleware/openapi-serve.js +34 -0
- package/lib/middleware/rate-limit.js +160 -18
- package/lib/middleware/request-id.js +36 -18
- package/lib/middleware/request-log.js +37 -0
- package/lib/middleware/require-aal.js +29 -0
- package/lib/middleware/require-auth.js +32 -0
- package/lib/middleware/require-bound-key.js +41 -0
- package/lib/middleware/require-content-type.js +32 -0
- package/lib/middleware/require-methods.js +27 -0
- package/lib/middleware/require-mtls.js +33 -0
- package/lib/middleware/require-step-up.js +37 -0
- package/lib/middleware/security-headers.js +44 -0
- package/lib/middleware/security-txt.js +38 -0
- package/lib/middleware/span-http-server.js +37 -0
- package/lib/middleware/sse.js +36 -0
- package/lib/middleware/trace-log-correlation.js +33 -0
- package/lib/middleware/trace-propagate.js +32 -0
- package/lib/middleware/tus-upload.js +90 -0
- package/lib/middleware/web-app-manifest.js +53 -0
- package/lib/mtls-ca.js +100 -70
- package/lib/network-byte-quota.js +308 -0
- package/lib/network-heartbeat.js +135 -0
- package/lib/network-tls.js +534 -4
- package/lib/network.js +103 -0
- package/lib/notify.js +114 -43
- package/lib/ntp-check.js +192 -51
- package/lib/observability.js +145 -47
- package/lib/openapi.js +90 -44
- package/lib/outbox.js +99 -1
- package/lib/pagination.js +168 -86
- package/lib/parsers/index.js +16 -5
- package/lib/permissions.js +93 -40
- package/lib/pqc-agent.js +84 -8
- package/lib/pqc-software.js +94 -60
- package/lib/process-spawn.js +95 -21
- package/lib/pubsub.js +96 -66
- package/lib/queue.js +375 -54
- package/lib/redact.js +793 -21
- package/lib/render.js +139 -47
- package/lib/request-helpers.js +485 -121
- package/lib/restore-bundle.js +142 -39
- package/lib/restore-rollback.js +136 -45
- package/lib/retention.js +178 -50
- package/lib/retry.js +116 -33
- package/lib/router.js +475 -23
- package/lib/safe-async.js +543 -94
- package/lib/safe-buffer.js +337 -41
- package/lib/safe-json.js +467 -62
- package/lib/safe-jsonpath.js +285 -0
- package/lib/safe-schema.js +631 -87
- package/lib/safe-sql.js +221 -59
- package/lib/safe-url.js +278 -46
- package/lib/sandbox-worker.js +135 -0
- package/lib/sandbox.js +358 -0
- package/lib/scheduler.js +135 -70
- package/lib/self-update.js +647 -0
- package/lib/session-device-binding.js +431 -0
- package/lib/session.js +259 -49
- package/lib/slug.js +138 -26
- package/lib/ssrf-guard.js +316 -56
- package/lib/storage.js +433 -70
- package/lib/subject.js +405 -23
- package/lib/template.js +148 -8
- package/lib/tenant-quota.js +545 -0
- package/lib/testing.js +440 -53
- package/lib/time.js +291 -23
- package/lib/tls-exporter.js +239 -0
- package/lib/tracing.js +90 -74
- package/lib/uuid.js +97 -22
- package/lib/vault/index.js +284 -22
- package/lib/vault/seal-pem-file.js +66 -0
- package/lib/watcher.js +368 -0
- package/lib/webhook.js +196 -63
- package/lib/websocket.js +393 -68
- package/lib/wiki-concepts.js +338 -0
- package/lib/worker-pool.js +464 -0
- package/package.json +3 -3
- package/sbom.cyclonedx.json +7 -7
package/lib/safe-json.js
CHANGED
|
@@ -1,63 +1,37 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
/**
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
* json.SafeJsonError → error class
|
|
29
|
-
*
|
|
30
|
-
* Validation modes (opts.collectErrors):
|
|
31
|
-
* - default (throw): fails loudly on first error — right for trust boundaries
|
|
32
|
-
* (HTTP body parse, sealed payload deserialize, config load).
|
|
33
|
-
* The throw IS the security signal; HTTP middleware catches
|
|
34
|
-
* it and emits a 400 with .path / .code.
|
|
35
|
-
* - collectErrors:true: returns { ok, value, errors[] } — right for form-style
|
|
36
|
-
* bulk validation where the user needs to see every field
|
|
37
|
-
* that failed in one round-trip.
|
|
38
|
-
*
|
|
39
|
-
* Defaults:
|
|
40
|
-
* maxBytes: 1 MiB
|
|
41
|
-
* maxDepth: 100
|
|
42
|
-
* allowProto: false
|
|
43
|
-
* onCircular: "throw"
|
|
44
|
-
*
|
|
45
|
-
* Schema dialect (JSON Schema subset):
|
|
46
|
-
* { type: 'string'|'number'|'integer'|'boolean'|'null'|'array'|'object',
|
|
47
|
-
* enum: [...],
|
|
48
|
-
* // string
|
|
49
|
-
* minLength, maxLength, pattern, format,
|
|
50
|
-
* // number
|
|
51
|
-
* minimum, maximum, exclusiveMinimum, exclusiveMaximum,
|
|
52
|
-
* // array
|
|
53
|
-
* minItems, maxItems, items: <schema>,
|
|
54
|
-
* // object
|
|
55
|
-
* required: [...], properties: { key: <schema>, ... }, additionalProperties: bool
|
|
56
|
-
* }
|
|
3
|
+
* @module b.safeJson
|
|
4
|
+
* @featured true
|
|
5
|
+
* @nav Validation
|
|
6
|
+
* @title Safe Json
|
|
7
|
+
*
|
|
8
|
+
* @intro
|
|
9
|
+
* Hardened JSON parse + stringify + schema validation. Native
|
|
10
|
+
* `JSON.parse` leaves four footguns to the caller — no size cap (DoS
|
|
11
|
+
* the parser thread), no depth cap (stack-overflow downstream), no
|
|
12
|
+
* guard on `__proto__` / `constructor` / `prototype` keys (prototype
|
|
13
|
+
* pollution after any later merge/clone), and errors that report
|
|
14
|
+
* only a character offset with no surrounding context. `b.safeJson`
|
|
15
|
+
* closes all four with conservative defaults.
|
|
16
|
+
*
|
|
17
|
+
* Defaults: 1 MiB body cap, depth 100, 10 000 keys per object
|
|
18
|
+
* (CVE-2026-21717 V8 HashDoS guard), poisoned keys stripped.
|
|
19
|
+
* Stringify refuses circular references unless the caller asks for
|
|
20
|
+
* the `[Circular]` placeholder. `canonical` produces RFC 8785 JCS
|
|
21
|
+
* key-sorted output for signature inputs.
|
|
22
|
+
*
|
|
23
|
+
* The validator is a strict subset of JSON Schema (`type` / `enum`
|
|
24
|
+
* / `minLength` etc. / `required` / `properties` / `additionalProperties`),
|
|
25
|
+
* pluggable formats via `b.safeJson.registerFormat`, two modes:
|
|
26
|
+
* throw on first error (trust-boundary parse) or collect every
|
|
27
|
+
* error (form-style bulk validation).
|
|
57
28
|
*
|
|
58
|
-
*
|
|
59
|
-
*
|
|
60
|
-
*
|
|
29
|
+
* Validation policy: opts and inputs are validated at the call site
|
|
30
|
+
* and throw `SafeJsonError`. The throw IS the security signal; HTTP
|
|
31
|
+
* middleware catches it and emits 400 with `.code` / `.path`.
|
|
32
|
+
*
|
|
33
|
+
* @card
|
|
34
|
+
* Hardened JSON parse + stringify + schema validation.
|
|
61
35
|
*/
|
|
62
36
|
|
|
63
37
|
// ---- Error class ----
|
|
@@ -68,6 +42,30 @@ var safeUrl = require("./safe-url");
|
|
|
68
42
|
var time = require("./time");
|
|
69
43
|
var { FrameworkError } = require("./framework-error");
|
|
70
44
|
|
|
45
|
+
/**
|
|
46
|
+
* @primitive b.safeJson.SafeJsonError
|
|
47
|
+
* @signature b.safeJson.SafeJsonError
|
|
48
|
+
* @since 0.1.0
|
|
49
|
+
* @status stable
|
|
50
|
+
* @related b.safeJson.parse, b.safeJson.validate
|
|
51
|
+
*
|
|
52
|
+
* Error class thrown by every `b.safeJson` primitive on bad input,
|
|
53
|
+
* cap exceedance, or schema-validation failure. Extends
|
|
54
|
+
* `FrameworkError`. Carries a stable `.code` (e.g. `json/too-large`,
|
|
55
|
+
* `json/syntax`, `json/validation`, `json/circular`) plus an
|
|
56
|
+
* optional JSON-pointer-shaped `.path` (e.g. `$.user.email`) for
|
|
57
|
+
* schema-validation errors. HTTP middleware translates these into
|
|
58
|
+
* 400 responses without leaking parser internals.
|
|
59
|
+
*
|
|
60
|
+
* @example
|
|
61
|
+
* var b = require("blamejs");
|
|
62
|
+
* try {
|
|
63
|
+
* b.safeJson.parse("{not json");
|
|
64
|
+
* } catch (e) {
|
|
65
|
+
* e instanceof b.safeJson.SafeJsonError; // → true
|
|
66
|
+
* e.code; // → "json/syntax"
|
|
67
|
+
* }
|
|
68
|
+
*/
|
|
71
69
|
class SafeJsonError extends FrameworkError {
|
|
72
70
|
constructor(message, code, path) {
|
|
73
71
|
super(message);
|
|
@@ -86,11 +84,71 @@ var ABSOLUTE_MAX_DEPTH = 1_000;
|
|
|
86
84
|
var IPV6_HEXTET_COUNT = 0x8;
|
|
87
85
|
var DEFAULT_MAX_BYTES = C.BYTES.mib(1);
|
|
88
86
|
var DEFAULT_MAX_DEPTH = 100;
|
|
87
|
+
// CVE-2026-21717 — V8 HashDoS via integer-like keys. V8's object-shape
|
|
88
|
+
// transition cache degrades to O(n^2) when an object accumulates many
|
|
89
|
+
// distinct integer-string-shaped keys; a JSON body with thousands of
|
|
90
|
+
// `"0"`, `"1"`, ... keys spends O(n^2) CPU on the parse path itself.
|
|
91
|
+
// Cap object-literal-key count per node so a hostile payload cannot
|
|
92
|
+
// reach the degenerate shape.
|
|
93
|
+
var DEFAULT_MAX_KEYS = 10_000;
|
|
94
|
+
var ABSOLUTE_MAX_KEYS = 1_000_000;
|
|
89
95
|
|
|
90
96
|
var POISONED_KEYS = new Set(["__proto__", "constructor", "prototype"]);
|
|
91
97
|
|
|
92
98
|
// ---- parse ----
|
|
93
99
|
|
|
100
|
+
/**
|
|
101
|
+
* @primitive b.safeJson.parse
|
|
102
|
+
* @signature b.safeJson.parse(input, opts?)
|
|
103
|
+
* @since 0.1.0
|
|
104
|
+
* @status stable
|
|
105
|
+
* @related b.safeJson.parseOrDefault, b.safeJson.stringify, b.safeJson.validate
|
|
106
|
+
*
|
|
107
|
+
* Hardened JSON parse. Accepts string / Buffer / Uint8Array,
|
|
108
|
+
* normalizes to UTF-8 text, enforces the byte cap BEFORE the parser
|
|
109
|
+
* sees the input, then bounds nesting depth and per-object key count
|
|
110
|
+
* so a hostile body can't DoS the parse thread or trip V8's HashDoS
|
|
111
|
+
* shape-cache degeneracy (CVE-2026-21717). Strips `__proto__` /
|
|
112
|
+
* `constructor` / `prototype` keys via the `JSON.parse` reviver so a
|
|
113
|
+
* later spread / merge / clone can't pivot into prototype pollution.
|
|
114
|
+
*
|
|
115
|
+
* Throws `SafeJsonError` with a documented `.code`:
|
|
116
|
+
* `json/too-large` / `json/syntax` / `json/too-deep` /
|
|
117
|
+
* `json/too-many-keys` / `json/wrong-input-type` /
|
|
118
|
+
* `json/type-mismatch` / `json/missing-key` / `json/validation`.
|
|
119
|
+
*
|
|
120
|
+
* @opts
|
|
121
|
+
* maxBytes: number, // default 1 MiB; capped at 64 MiB
|
|
122
|
+
* maxDepth: number, // default 100; capped at 1000
|
|
123
|
+
* maxKeys: number, // default 10 000; capped at 1 000 000
|
|
124
|
+
* allowProto: boolean, // default false; keep __proto__/constructor/prototype keys
|
|
125
|
+
* schema: object, // optional JSON-Schema subset; runs b.safeJson.validate
|
|
126
|
+
* collectErrors: boolean, // pair with `schema`: return { ok, value, errors[] } instead of throwing
|
|
127
|
+
* expectType: string, // legacy: "string"|"number"|"boolean"|"null"|"array"|"object"
|
|
128
|
+
* requiredKeys: string[],// legacy: required top-level keys (prefer `schema.required`)
|
|
129
|
+
*
|
|
130
|
+
* @example
|
|
131
|
+
* var b = require("blamejs");
|
|
132
|
+
* var obj = b.safeJson.parse('{"name":"alice","age":30}');
|
|
133
|
+
* obj.name;
|
|
134
|
+
* // → "alice"
|
|
135
|
+
*
|
|
136
|
+
* // Prototype-pollution payload: poisoned keys stripped silently.
|
|
137
|
+
* var clean = b.safeJson.parse('{"__proto__":{"isAdmin":true},"id":1}');
|
|
138
|
+
* Object.prototype.hasOwnProperty.call(clean, "__proto__");
|
|
139
|
+
* // → false
|
|
140
|
+
*
|
|
141
|
+
* // Size cap rejects oversized input before parsing.
|
|
142
|
+
* var big = '"' + "x".repeat(2000) + '"';
|
|
143
|
+
* try { b.safeJson.parse(big, { maxBytes: 1024 }); }
|
|
144
|
+
* catch (e) { e.code; }
|
|
145
|
+
* // → "json/too-large"
|
|
146
|
+
*
|
|
147
|
+
* // Depth cap bounds nesting.
|
|
148
|
+
* try { b.safeJson.parse('[[[[[[1]]]]]]', { maxDepth: 3 }); }
|
|
149
|
+
* catch (e) { e.code; }
|
|
150
|
+
* // → "json/too-deep"
|
|
151
|
+
*/
|
|
94
152
|
function parse(input, opts) {
|
|
95
153
|
opts = opts || {};
|
|
96
154
|
|
|
@@ -104,6 +162,7 @@ function parse(input, opts) {
|
|
|
104
162
|
});
|
|
105
163
|
|
|
106
164
|
var maxDepth = _capInt(opts.maxDepth, DEFAULT_MAX_DEPTH, ABSOLUTE_MAX_DEPTH);
|
|
165
|
+
var maxKeys = _capInt(opts.maxKeys, DEFAULT_MAX_KEYS, ABSOLUTE_MAX_KEYS);
|
|
107
166
|
var allowProto = !!opts.allowProto;
|
|
108
167
|
|
|
109
168
|
var parsed;
|
|
@@ -113,7 +172,7 @@ function parse(input, opts) {
|
|
|
113
172
|
throw new SafeJsonError("invalid JSON: " + e.message, "json/syntax");
|
|
114
173
|
}
|
|
115
174
|
|
|
116
|
-
_walkAndCheck(parsed, 0, maxDepth, allowProto);
|
|
175
|
+
_walkAndCheck(parsed, 0, maxDepth, allowProto, maxKeys);
|
|
117
176
|
|
|
118
177
|
// Optional schema validation (preferred over expectType / requiredKeys)
|
|
119
178
|
if (opts.schema) {
|
|
@@ -146,6 +205,37 @@ function parse(input, opts) {
|
|
|
146
205
|
return parsed;
|
|
147
206
|
}
|
|
148
207
|
|
|
208
|
+
/**
|
|
209
|
+
* @primitive b.safeJson.parseOrDefault
|
|
210
|
+
* @signature b.safeJson.parseOrDefault(input, fallback, opts?)
|
|
211
|
+
* @since 0.1.0
|
|
212
|
+
* @status stable
|
|
213
|
+
* @related b.safeJson.parse
|
|
214
|
+
*
|
|
215
|
+
* Best-effort parse: returns `fallback` on any failure (size cap,
|
|
216
|
+
* syntax error, depth/key cap, schema mismatch). Useful for cache
|
|
217
|
+
* thaw / config files / optional metadata where a malformed payload
|
|
218
|
+
* shouldn't crash the caller. Same caps and prototype-pollution
|
|
219
|
+
* defense as `parse`.
|
|
220
|
+
*
|
|
221
|
+
* @opts
|
|
222
|
+
* maxBytes: number, // default 1 MiB; capped at 64 MiB
|
|
223
|
+
* maxDepth: number, // default 100; capped at 1000
|
|
224
|
+
* maxKeys: number, // default 10 000; capped at 1 000 000
|
|
225
|
+
* allowProto: boolean, // default false; keep __proto__/constructor/prototype keys
|
|
226
|
+
* schema: object, // optional JSON-Schema subset (see b.safeJson.validate)
|
|
227
|
+
*
|
|
228
|
+
* @example
|
|
229
|
+
* var b = require("blamejs");
|
|
230
|
+
* b.safeJson.parseOrDefault('{"x":1}', {});
|
|
231
|
+
* // → { x: 1 }
|
|
232
|
+
*
|
|
233
|
+
* b.safeJson.parseOrDefault("{not json", { x: 0 });
|
|
234
|
+
* // → { x: 0 }
|
|
235
|
+
*
|
|
236
|
+
* b.safeJson.parseOrDefault(null, []);
|
|
237
|
+
* // → []
|
|
238
|
+
*/
|
|
149
239
|
function parseOrDefault(input, fallback, opts) {
|
|
150
240
|
try { return parse(input, opts); }
|
|
151
241
|
catch (_e) { return fallback; }
|
|
@@ -156,13 +246,13 @@ function _stripProtoKeys(key, value) {
|
|
|
156
246
|
return value;
|
|
157
247
|
}
|
|
158
248
|
|
|
159
|
-
function _walkAndCheck(value, depth, maxDepth, allowProto) {
|
|
249
|
+
function _walkAndCheck(value, depth, maxDepth, allowProto, maxKeys) {
|
|
160
250
|
if (depth > maxDepth) {
|
|
161
251
|
throw new SafeJsonError("nesting exceeds maxDepth (" + maxDepth + ")", "json/too-deep");
|
|
162
252
|
}
|
|
163
253
|
if (value === null || typeof value !== "object") return;
|
|
164
254
|
if (Array.isArray(value)) {
|
|
165
|
-
for (var i = 0; i < value.length; i++) _walkAndCheck(value[i], depth + 1, maxDepth, allowProto);
|
|
255
|
+
for (var i = 0; i < value.length; i++) _walkAndCheck(value[i], depth + 1, maxDepth, allowProto, maxKeys);
|
|
166
256
|
return;
|
|
167
257
|
}
|
|
168
258
|
if (!allowProto) {
|
|
@@ -170,9 +260,17 @@ function _walkAndCheck(value, depth, maxDepth, allowProto) {
|
|
|
170
260
|
if (Object.prototype.hasOwnProperty.call(value, k)) delete value[k];
|
|
171
261
|
});
|
|
172
262
|
}
|
|
263
|
+
// CVE-2026-21717 — refuse object literals beyond maxKeys before V8's
|
|
264
|
+
// hidden-class transition cache degrades to O(n^2) on integer-shaped
|
|
265
|
+
// keys.
|
|
266
|
+
var keyCount = 0;
|
|
173
267
|
for (var k in value) {
|
|
174
268
|
if (Object.prototype.hasOwnProperty.call(value, k)) {
|
|
175
|
-
|
|
269
|
+
keyCount += 1;
|
|
270
|
+
if (keyCount > maxKeys) {
|
|
271
|
+
throw new SafeJsonError("object exceeds maxKeys (" + maxKeys + ")", "json/too-many-keys");
|
|
272
|
+
}
|
|
273
|
+
_walkAndCheck(value[k], depth + 1, maxDepth, allowProto, maxKeys);
|
|
176
274
|
}
|
|
177
275
|
}
|
|
178
276
|
}
|
|
@@ -185,6 +283,44 @@ function _typeName(v) {
|
|
|
185
283
|
|
|
186
284
|
// ---- stringify ----
|
|
187
285
|
|
|
286
|
+
/**
|
|
287
|
+
* @primitive b.safeJson.stringify
|
|
288
|
+
* @signature b.safeJson.stringify(value, opts?)
|
|
289
|
+
* @since 0.1.0
|
|
290
|
+
* @status stable
|
|
291
|
+
* @related b.safeJson.parse, b.safeJson.canonical
|
|
292
|
+
*
|
|
293
|
+
* JSON-encode a value with two safeguards `JSON.stringify` doesn't
|
|
294
|
+
* provide: a documented circular-reference policy (throw, or
|
|
295
|
+
* substitute every cycle with a placeholder string) and prototype-
|
|
296
|
+
* key suppression so an object built from a tainted parse can't leak
|
|
297
|
+
* `__proto__` / `constructor` / `prototype` keys back out.
|
|
298
|
+
*
|
|
299
|
+
* Throws `SafeJsonError` with `.code = "json/circular"` when
|
|
300
|
+
* `onCircular: "throw"` (default) hits a cycle.
|
|
301
|
+
*
|
|
302
|
+
* @opts
|
|
303
|
+
* onCircular: "throw" | "replace", // default "throw"
|
|
304
|
+
* circularReplacement: any, // default "[Circular]" (used when onCircular === "replace")
|
|
305
|
+
* allowProto: boolean, // default false; keep __proto__/constructor/prototype keys
|
|
306
|
+
* indent: number | string, // forwarded to JSON.stringify
|
|
307
|
+
*
|
|
308
|
+
* @example
|
|
309
|
+
* var b = require("blamejs");
|
|
310
|
+
* b.safeJson.stringify({ a: 1, b: 2 });
|
|
311
|
+
* // → '{"a":1,"b":2}'
|
|
312
|
+
*
|
|
313
|
+
* // Cycles throw by default.
|
|
314
|
+
* var cyclic = { name: "root" };
|
|
315
|
+
* cyclic.self = cyclic;
|
|
316
|
+
* try { b.safeJson.stringify(cyclic); }
|
|
317
|
+
* catch (e) { e.code; }
|
|
318
|
+
* // → "json/circular"
|
|
319
|
+
*
|
|
320
|
+
* // Opt into placeholder-substitution.
|
|
321
|
+
* var out = b.safeJson.stringify(cyclic, { onCircular: "replace" });
|
|
322
|
+
* // → '{"name":"root","self":"[Circular]"}'
|
|
323
|
+
*/
|
|
188
324
|
function stringify(value, opts) {
|
|
189
325
|
opts = opts || {};
|
|
190
326
|
var onCircular = opts.onCircular || "throw";
|
|
@@ -250,7 +386,39 @@ function _cleanCycles(value, replacement, allowProto) {
|
|
|
250
386
|
|
|
251
387
|
// ---- canonical ----
|
|
252
388
|
|
|
253
|
-
|
|
389
|
+
/**
|
|
390
|
+
* @primitive b.safeJson.canonical
|
|
391
|
+
* @signature b.safeJson.canonical(value)
|
|
392
|
+
* @since 0.1.0
|
|
393
|
+
* @status stable
|
|
394
|
+
* @related b.safeJson.stringify, b.crypto.sign
|
|
395
|
+
*
|
|
396
|
+
* RFC 8785 (JSON Canonicalization Scheme) serialization — produces
|
|
397
|
+
* deterministic output suitable as a hash / signature input. Object
|
|
398
|
+
* keys are lexicographically sorted at every depth, no whitespace is
|
|
399
|
+
* emitted, poisoned keys are stripped, and non-finite numbers
|
|
400
|
+
* (`NaN` / `Infinity`) throw `SafeJsonError` with
|
|
401
|
+
* `.code = "json/non-finite"` instead of silently round-tripping
|
|
402
|
+
* through `null`. Two semantically-equal values produce byte-
|
|
403
|
+
* identical output, which is what signature inputs require.
|
|
404
|
+
*
|
|
405
|
+
* @example
|
|
406
|
+
* var b = require("blamejs");
|
|
407
|
+
* b.safeJson.canonical({ b: 2, a: 1 });
|
|
408
|
+
* // → '{"a":1,"b":2}'
|
|
409
|
+
*
|
|
410
|
+
* // Two equivalent objects produce identical bytes.
|
|
411
|
+
* var x = b.safeJson.canonical({ name: "alice", age: 30 });
|
|
412
|
+
* var y = b.safeJson.canonical({ age: 30, name: "alice" });
|
|
413
|
+
* x === y;
|
|
414
|
+
* // → true
|
|
415
|
+
*
|
|
416
|
+
* // Non-finite numbers refuse to canonicalize.
|
|
417
|
+
* try { b.safeJson.canonical({ ratio: Infinity }); }
|
|
418
|
+
* catch (e) { e.code; }
|
|
419
|
+
* // → "json/non-finite"
|
|
420
|
+
*/
|
|
421
|
+
function canonical(value) {
|
|
254
422
|
if (typeof value === "undefined") return "null";
|
|
255
423
|
|
|
256
424
|
function ser(v) {
|
|
@@ -277,6 +445,32 @@ function canonical(value, _opts) {
|
|
|
277
445
|
// ---- format registry ----
|
|
278
446
|
|
|
279
447
|
// Anchored and bounded — nothing here is ReDoS-prone.
|
|
448
|
+
/**
|
|
449
|
+
* @primitive b.safeJson.formats
|
|
450
|
+
* @signature b.safeJson.formats
|
|
451
|
+
* @since 0.1.0
|
|
452
|
+
* @status stable
|
|
453
|
+
* @related b.safeJson.registerFormat, b.safeJson.validate
|
|
454
|
+
*
|
|
455
|
+
* The built-in format-validator registry consulted by `validate`
|
|
456
|
+
* when a schema declares `{ format: "<name>" }` on a string field.
|
|
457
|
+
* Every entry is anchored, length-bounded, and non-backtracking —
|
|
458
|
+
* safe against ReDoS. Built-ins: `email` / `url` / `uuid` / `ulid`
|
|
459
|
+
* / `iso8601-date` / `iso8601-datetime` / `ipv4` / `ipv6` / `ip`
|
|
460
|
+
* / `hex` / `slug`. Add operator-specific formats with
|
|
461
|
+
* `b.safeJson.registerFormat`.
|
|
462
|
+
*
|
|
463
|
+
* @example
|
|
464
|
+
* var b = require("blamejs");
|
|
465
|
+
* b.safeJson.formats.uuid("f47ac10b-58cc-4372-a567-0e02b2c3d479");
|
|
466
|
+
* // → true
|
|
467
|
+
*
|
|
468
|
+
* b.safeJson.formats.email("alice@example.com");
|
|
469
|
+
* // → true
|
|
470
|
+
*
|
|
471
|
+
* b.safeJson.formats.ipv4("256.0.0.1");
|
|
472
|
+
* // → false
|
|
473
|
+
*/
|
|
280
474
|
var formats = {
|
|
281
475
|
// Structural-only email check (no RFC 5322 attempt). Keeps complexity O(n).
|
|
282
476
|
// Length cap prevents pathological backtracking against long inputs.
|
|
@@ -380,6 +574,32 @@ var formats = {
|
|
|
380
574
|
slug: function (v) { return typeof v === "string" && /^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(v); },
|
|
381
575
|
};
|
|
382
576
|
|
|
577
|
+
/**
|
|
578
|
+
* @primitive b.safeJson.registerFormat
|
|
579
|
+
* @signature b.safeJson.registerFormat(name, validator)
|
|
580
|
+
* @since 0.1.0
|
|
581
|
+
* @status stable
|
|
582
|
+
* @related b.safeJson.formats, b.safeJson.validate
|
|
583
|
+
*
|
|
584
|
+
* Register an operator-supplied format validator. `name` must be
|
|
585
|
+
* lowercase-kebab `[a-z][a-z0-9-]*`; `validator` is `(value) => boolean`.
|
|
586
|
+
* Once registered, schemas can declare `{ type: "string", format:
|
|
587
|
+
* "<name>" }` and the validator runs at every matching node.
|
|
588
|
+
* Throws `SafeJsonError` (`json/bad-format-name` /
|
|
589
|
+
* `json/bad-format-validator`) on invalid arguments.
|
|
590
|
+
*
|
|
591
|
+
* @example
|
|
592
|
+
* var b = require("blamejs");
|
|
593
|
+
* b.safeJson.registerFormat("aws-region", function (v) {
|
|
594
|
+
* return typeof v === "string" && /^[a-z]{2}-[a-z]+-\d$/.test(v);
|
|
595
|
+
* });
|
|
596
|
+
*
|
|
597
|
+
* b.safeJson.formats["aws-region"]("us-east-1");
|
|
598
|
+
* // → true
|
|
599
|
+
*
|
|
600
|
+
* b.safeJson.formats["aws-region"]("invalid");
|
|
601
|
+
* // → false
|
|
602
|
+
*/
|
|
383
603
|
function registerFormat(name, validator) {
|
|
384
604
|
if (typeof name !== "string" || !/^[a-z][a-z0-9-]*$/.test(name)) {
|
|
385
605
|
throw new SafeJsonError("format name must match [a-z][a-z0-9-]*: " + name, "json/bad-format-name");
|
|
@@ -392,6 +612,61 @@ function registerFormat(name, validator) {
|
|
|
392
612
|
|
|
393
613
|
// ---- validate ----
|
|
394
614
|
|
|
615
|
+
/**
|
|
616
|
+
* @primitive b.safeJson.validate
|
|
617
|
+
* @signature b.safeJson.validate(value, schema, opts?)
|
|
618
|
+
* @since 0.1.0
|
|
619
|
+
* @status stable
|
|
620
|
+
* @related b.safeJson.parse, b.safeJson.registerFormat
|
|
621
|
+
*
|
|
622
|
+
* Strict-subset JSON Schema validator. Supported keywords: `type`
|
|
623
|
+
* (`string` / `number` / `integer` / `boolean` / `null` / `array` /
|
|
624
|
+
* `object`), `enum`, `minLength` / `maxLength` / `pattern` /
|
|
625
|
+
* `format` (string), `minimum` / `maximum` / `exclusiveMinimum` /
|
|
626
|
+
* `exclusiveMaximum` (number), `minItems` / `maxItems` / `items`
|
|
627
|
+
* (array), `required` / `properties` / `additionalProperties`
|
|
628
|
+
* (object).
|
|
629
|
+
*
|
|
630
|
+
* Two modes — throw on the first failure (default; ideal for trust-
|
|
631
|
+
* boundary parses) or collect every error with
|
|
632
|
+
* `{ collectErrors: true }` (returns `{ ok, value, errors[] }` for
|
|
633
|
+
* form-style bulk validation). Errors carry a JSON-pointer-shaped
|
|
634
|
+
* `.path` (e.g. `$.user.email`).
|
|
635
|
+
*
|
|
636
|
+
* @opts
|
|
637
|
+
* collectErrors: boolean, // default false; collect every error instead of throwing on first
|
|
638
|
+
*
|
|
639
|
+
* @example
|
|
640
|
+
* var b = require("blamejs");
|
|
641
|
+
* var schema = {
|
|
642
|
+
* type: "object",
|
|
643
|
+
* required: ["email", "age"],
|
|
644
|
+
* properties: {
|
|
645
|
+
* email: { type: "string", format: "email", maxLength: 254 },
|
|
646
|
+
* age: { type: "integer", minimum: 0, maximum: 150 },
|
|
647
|
+
* },
|
|
648
|
+
* additionalProperties: false,
|
|
649
|
+
* };
|
|
650
|
+
*
|
|
651
|
+
* b.safeJson.validate({ email: "a@b.com", age: 30 }, schema);
|
|
652
|
+
* // → { email: "a@b.com", age: 30 }
|
|
653
|
+
*
|
|
654
|
+
* // Throw mode: first failure throws SafeJsonError.
|
|
655
|
+
* try { b.safeJson.validate({ email: "nope", age: -1 }, schema); }
|
|
656
|
+
* catch (e) { e.code; }
|
|
657
|
+
* // → "json/validation"
|
|
658
|
+
*
|
|
659
|
+
* // Collect mode: every failure surfaced.
|
|
660
|
+
* var report = b.safeJson.validate(
|
|
661
|
+
* { email: "nope", age: -1 },
|
|
662
|
+
* schema,
|
|
663
|
+
* { collectErrors: true }
|
|
664
|
+
* );
|
|
665
|
+
* report.ok;
|
|
666
|
+
* // → false
|
|
667
|
+
* report.errors.length >= 2;
|
|
668
|
+
* // → true
|
|
669
|
+
*/
|
|
395
670
|
function validate(value, schema, opts) {
|
|
396
671
|
opts = opts || {};
|
|
397
672
|
if (!schema || typeof schema !== "object") {
|
|
@@ -524,6 +799,134 @@ function _capInt(value, defaultValue, ceiling) {
|
|
|
524
799
|
return Math.min(Math.floor(value), ceiling);
|
|
525
800
|
}
|
|
526
801
|
|
|
802
|
+
/**
|
|
803
|
+
* @primitive b.safeJson.DEFAULT_MAX_BYTES
|
|
804
|
+
* @signature b.safeJson.DEFAULT_MAX_BYTES
|
|
805
|
+
* @since 0.1.0
|
|
806
|
+
* @status stable
|
|
807
|
+
* @related b.safeJson.parse, b.safeJson.ABSOLUTE_MAX_BYTES
|
|
808
|
+
*
|
|
809
|
+
* Default body cap applied by `parse` when the caller doesn't pass
|
|
810
|
+
* `opts.maxBytes` — 1 MiB. Keeps a hostile request from spending
|
|
811
|
+
* arbitrary CPU on the parse thread before the cap kicks in.
|
|
812
|
+
*
|
|
813
|
+
* @example
|
|
814
|
+
* var b = require("blamejs");
|
|
815
|
+
* b.safeJson.DEFAULT_MAX_BYTES;
|
|
816
|
+
* // → 1048576
|
|
817
|
+
*/
|
|
818
|
+
|
|
819
|
+
/**
|
|
820
|
+
* @primitive b.safeJson.DEFAULT_MAX_DEPTH
|
|
821
|
+
* @signature b.safeJson.DEFAULT_MAX_DEPTH
|
|
822
|
+
* @since 0.1.0
|
|
823
|
+
* @status stable
|
|
824
|
+
* @related b.safeJson.parse, b.safeJson.ABSOLUTE_MAX_DEPTH
|
|
825
|
+
*
|
|
826
|
+
* Default nesting-depth cap applied by `parse` when the caller
|
|
827
|
+
* doesn't pass `opts.maxDepth` — 100 levels. Bounds stack-overflow
|
|
828
|
+
* risk for downstream walkers (clone / merge / serializers).
|
|
829
|
+
*
|
|
830
|
+
* @example
|
|
831
|
+
* var b = require("blamejs");
|
|
832
|
+
* b.safeJson.DEFAULT_MAX_DEPTH;
|
|
833
|
+
* // → 100
|
|
834
|
+
*/
|
|
835
|
+
|
|
836
|
+
/**
|
|
837
|
+
* @primitive b.safeJson.DEFAULT_MAX_KEYS
|
|
838
|
+
* @signature b.safeJson.DEFAULT_MAX_KEYS
|
|
839
|
+
* @since 0.1.0
|
|
840
|
+
* @status stable
|
|
841
|
+
* @related b.safeJson.parse, b.safeJson.ABSOLUTE_MAX_KEYS
|
|
842
|
+
*
|
|
843
|
+
* Default per-object key cap applied by `parse` when the caller
|
|
844
|
+
* doesn't pass `opts.maxKeys` — 10 000 keys. Defends against
|
|
845
|
+
* CVE-2026-21717 V8 HashDoS (integer-shaped keys degrading the
|
|
846
|
+
* shape-transition cache to O(n^2)).
|
|
847
|
+
*
|
|
848
|
+
* @example
|
|
849
|
+
* var b = require("blamejs");
|
|
850
|
+
* b.safeJson.DEFAULT_MAX_KEYS;
|
|
851
|
+
* // → 10000
|
|
852
|
+
*/
|
|
853
|
+
|
|
854
|
+
/**
|
|
855
|
+
* @primitive b.safeJson.ABSOLUTE_MAX_BYTES
|
|
856
|
+
* @signature b.safeJson.ABSOLUTE_MAX_BYTES
|
|
857
|
+
* @since 0.1.0
|
|
858
|
+
* @status stable
|
|
859
|
+
* @related b.safeJson.parse, b.safeJson.DEFAULT_MAX_BYTES
|
|
860
|
+
*
|
|
861
|
+
* Hard ceiling for `opts.maxBytes` — 64 MiB. Operator-supplied caps
|
|
862
|
+
* above this clamp down silently so a typo can't disable the
|
|
863
|
+
* defense entirely.
|
|
864
|
+
*
|
|
865
|
+
* @example
|
|
866
|
+
* var b = require("blamejs");
|
|
867
|
+
* b.safeJson.ABSOLUTE_MAX_BYTES;
|
|
868
|
+
* // → 67108864
|
|
869
|
+
*/
|
|
870
|
+
|
|
871
|
+
/**
|
|
872
|
+
* @primitive b.safeJson.ABSOLUTE_MAX_DEPTH
|
|
873
|
+
* @signature b.safeJson.ABSOLUTE_MAX_DEPTH
|
|
874
|
+
* @since 0.1.0
|
|
875
|
+
* @status stable
|
|
876
|
+
* @related b.safeJson.parse, b.safeJson.DEFAULT_MAX_DEPTH
|
|
877
|
+
*
|
|
878
|
+
* Hard ceiling for `opts.maxDepth` — 1000 levels. Caller requests
|
|
879
|
+
* above this clamp down silently.
|
|
880
|
+
*
|
|
881
|
+
* @example
|
|
882
|
+
* var b = require("blamejs");
|
|
883
|
+
* b.safeJson.ABSOLUTE_MAX_DEPTH;
|
|
884
|
+
* // → 1000
|
|
885
|
+
*/
|
|
886
|
+
|
|
887
|
+
/**
|
|
888
|
+
* @primitive b.safeJson.ABSOLUTE_MAX_KEYS
|
|
889
|
+
* @signature b.safeJson.ABSOLUTE_MAX_KEYS
|
|
890
|
+
* @since 0.1.0
|
|
891
|
+
* @status stable
|
|
892
|
+
* @related b.safeJson.parse, b.safeJson.DEFAULT_MAX_KEYS
|
|
893
|
+
*
|
|
894
|
+
* Hard ceiling for `opts.maxKeys` — 1 000 000 keys per object.
|
|
895
|
+
* Clamps caller-supplied caps so the HashDoS guard cannot be
|
|
896
|
+
* accidentally disabled by a too-large value.
|
|
897
|
+
*
|
|
898
|
+
* @example
|
|
899
|
+
* var b = require("blamejs");
|
|
900
|
+
* b.safeJson.ABSOLUTE_MAX_KEYS;
|
|
901
|
+
* // → 1000000
|
|
902
|
+
*/
|
|
903
|
+
|
|
904
|
+
/**
|
|
905
|
+
* @primitive b.safeJson.POISONED_KEYS
|
|
906
|
+
* @signature b.safeJson.POISONED_KEYS
|
|
907
|
+
* @since 0.1.0
|
|
908
|
+
* @status stable
|
|
909
|
+
* @related b.safeJson.parse, b.safeJson.stringify
|
|
910
|
+
*
|
|
911
|
+
* The list of object keys treated as prototype-pollution vectors —
|
|
912
|
+
* `__proto__`, `constructor`, `prototype`. `parse` strips them on
|
|
913
|
+
* the way in (unless `opts.allowProto: true`); `stringify` and
|
|
914
|
+
* `canonical` strip them on the way out. Exposed as an array so
|
|
915
|
+
* operator code that does its own object hygiene can reuse the
|
|
916
|
+
* same canonical list.
|
|
917
|
+
*
|
|
918
|
+
* @example
|
|
919
|
+
* var b = require("blamejs");
|
|
920
|
+
* b.safeJson.POISONED_KEYS;
|
|
921
|
+
* // → ["__proto__", "constructor", "prototype"]
|
|
922
|
+
*
|
|
923
|
+
* // Reuse for operator-side sanitization.
|
|
924
|
+
* var clean = {};
|
|
925
|
+
* Object.keys(input).forEach(function (k) {
|
|
926
|
+
* if (b.safeJson.POISONED_KEYS.indexOf(k) === -1) clean[k] = input[k];
|
|
927
|
+
* });
|
|
928
|
+
*/
|
|
929
|
+
|
|
527
930
|
module.exports = {
|
|
528
931
|
parse: parse,
|
|
529
932
|
parseOrDefault: parseOrDefault,
|
|
@@ -535,7 +938,9 @@ module.exports = {
|
|
|
535
938
|
SafeJsonError: SafeJsonError,
|
|
536
939
|
DEFAULT_MAX_BYTES: DEFAULT_MAX_BYTES,
|
|
537
940
|
DEFAULT_MAX_DEPTH: DEFAULT_MAX_DEPTH,
|
|
941
|
+
DEFAULT_MAX_KEYS: DEFAULT_MAX_KEYS,
|
|
538
942
|
ABSOLUTE_MAX_BYTES: ABSOLUTE_MAX_BYTES,
|
|
539
943
|
ABSOLUTE_MAX_DEPTH: ABSOLUTE_MAX_DEPTH,
|
|
944
|
+
ABSOLUTE_MAX_KEYS: ABSOLUTE_MAX_KEYS,
|
|
540
945
|
POISONED_KEYS: Array.from(POISONED_KEYS),
|
|
541
946
|
};
|