@blamejs/core 0.7.1 → 0.7.18

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.
Files changed (180) hide show
  1. package/CHANGELOG.md +423 -389
  2. package/README.md +150 -149
  3. package/bin/blamejs.js +0 -0
  4. package/index.js +308 -282
  5. package/lib/api-key.js +660 -672
  6. package/lib/api-snapshot.js +338 -338
  7. package/lib/app-shutdown.js +385 -385
  8. package/lib/app.js +365 -365
  9. package/lib/archive.js +250 -250
  10. package/lib/atomic-file.js +544 -544
  11. package/lib/audit-chain.js +177 -177
  12. package/lib/audit-sign.js +344 -344
  13. package/lib/audit-tools.js +677 -677
  14. package/lib/audit.js +766 -766
  15. package/lib/auth/jwt.js +311 -311
  16. package/lib/auth/lockout.js +436 -436
  17. package/lib/auth/oauth.js +721 -721
  18. package/lib/auth/passkey.js +181 -181
  19. package/lib/auth/password.js +594 -594
  20. package/lib/backup/bundle.js +217 -217
  21. package/lib/backup/crypto.js +176 -176
  22. package/lib/backup/index.js +515 -515
  23. package/lib/backup/manifest.js +282 -282
  24. package/lib/break-glass.js +1338 -1338
  25. package/lib/bundler.js +441 -441
  26. package/lib/cache-redis.js +256 -266
  27. package/lib/cache.js +1206 -1211
  28. package/lib/canonical-json.js +115 -115
  29. package/lib/chain-writer.js +234 -234
  30. package/lib/cli-helpers.js +206 -206
  31. package/lib/cli.js +2334 -2334
  32. package/lib/cluster-provider-db.js +317 -317
  33. package/lib/cluster-storage.js +226 -226
  34. package/lib/cluster.js +703 -703
  35. package/lib/codepoint-class.js +196 -0
  36. package/lib/config-drift.js +301 -301
  37. package/lib/consent.js +222 -222
  38. package/lib/constants.js +191 -191
  39. package/lib/cookies.js +315 -315
  40. package/lib/credential-hash.js +322 -322
  41. package/lib/crypto.js +266 -266
  42. package/lib/csv.js +275 -286
  43. package/lib/db-declare-row-policy.js +267 -267
  44. package/lib/db-declare-view.js +420 -421
  45. package/lib/db-query.js +406 -406
  46. package/lib/db-schema.js +319 -319
  47. package/lib/db.js +1288 -1288
  48. package/lib/deprecate.js +222 -222
  49. package/lib/dev.js +335 -335
  50. package/lib/dual-control.js +473 -473
  51. package/lib/error-page.js +420 -420
  52. package/lib/external-db-migrate.js +441 -441
  53. package/lib/external-db.js +1061 -1061
  54. package/lib/file-type.js +273 -273
  55. package/lib/file-upload.js +1136 -0
  56. package/lib/forms.js +422 -422
  57. package/lib/framework-error.js +293 -202
  58. package/lib/framework-schema.js +717 -717
  59. package/lib/gate-contract.js +971 -0
  60. package/lib/guard-all.js +405 -0
  61. package/lib/guard-archive.js +739 -0
  62. package/lib/guard-csv.js +816 -0
  63. package/lib/guard-email.js +744 -0
  64. package/lib/guard-filename.js +724 -0
  65. package/lib/guard-html.js +976 -0
  66. package/lib/guard-json.js +729 -0
  67. package/lib/guard-markdown.js +586 -0
  68. package/lib/guard-svg.js +976 -0
  69. package/lib/guard-xml.js +405 -0
  70. package/lib/guard-yaml.js +529 -0
  71. package/lib/handlers.js +350 -350
  72. package/lib/http-client-cookie-jar.js +508 -508
  73. package/lib/http-client.js +1195 -1195
  74. package/lib/i18n.js +878 -878
  75. package/lib/jobs.js +185 -185
  76. package/lib/log-stream-cloudwatch.js +369 -369
  77. package/lib/log-stream-local.js +146 -146
  78. package/lib/log-stream-otlp-grpc.js +410 -410
  79. package/lib/log-stream-otlp.js +286 -286
  80. package/lib/log-stream-syslog.js +302 -302
  81. package/lib/log-stream-webhook.js +199 -199
  82. package/lib/log-stream.js +330 -330
  83. package/lib/log.js +500 -500
  84. package/lib/mail-bounce.js +528 -528
  85. package/lib/mail-dkim.js +369 -362
  86. package/lib/mail.js +981 -962
  87. package/lib/metrics.js +683 -683
  88. package/lib/middleware/api-encrypt.js +936 -573
  89. package/lib/middleware/attach-user.js +157 -157
  90. package/lib/middleware/body-parser.js +1170 -1091
  91. package/lib/middleware/bot-guard.js +178 -178
  92. package/lib/middleware/compression.js +452 -452
  93. package/lib/middleware/cors.js +314 -314
  94. package/lib/middleware/csp-nonce.js +348 -348
  95. package/lib/middleware/csrf-protect.js +316 -316
  96. package/lib/middleware/db-role-for.js +264 -269
  97. package/lib/middleware/health.js +392 -392
  98. package/lib/middleware/index.js +79 -79
  99. package/lib/middleware/rate-limit.js +358 -358
  100. package/lib/middleware/request-id.js +61 -61
  101. package/lib/middleware/request-log.js +168 -168
  102. package/lib/middleware/require-auth.js +104 -104
  103. package/lib/middleware/security-headers.js +116 -116
  104. package/lib/middleware/sse.js +166 -166
  105. package/lib/migrations.js +383 -383
  106. package/lib/mtls-ca.js +518 -518
  107. package/lib/mtls-engine-default.js +481 -481
  108. package/lib/network-dns.js +632 -632
  109. package/lib/network-heartbeat.js +290 -290
  110. package/lib/network-nts.js +574 -574
  111. package/lib/network-proxy.js +265 -265
  112. package/lib/network-tls.js +328 -328
  113. package/lib/network.js +233 -233
  114. package/lib/notify.js +612 -614
  115. package/lib/ntp-check.js +229 -229
  116. package/lib/numeric-bounds.js +111 -91
  117. package/lib/object-store/azure-blob-bucket-ops.js +349 -349
  118. package/lib/object-store/azure-blob.js +488 -451
  119. package/lib/object-store/gcs-bucket-ops.js +351 -351
  120. package/lib/object-store/gcs.js +519 -479
  121. package/lib/object-store/http-put.js +153 -153
  122. package/lib/object-store/index.js +197 -197
  123. package/lib/object-store/sigv4-bucket-ops.js +1092 -1092
  124. package/lib/object-store/sigv4.js +903 -855
  125. package/lib/observability.js +151 -151
  126. package/lib/otel-export.js +269 -269
  127. package/lib/pagination.js +464 -464
  128. package/lib/parsers/index.js +80 -80
  129. package/lib/parsers/safe-env.js +642 -642
  130. package/lib/parsers/safe-ini.js +292 -292
  131. package/lib/parsers/safe-toml.js +784 -784
  132. package/lib/parsers/safe-xml.js +390 -390
  133. package/lib/parsers/safe-yaml.js +1015 -1015
  134. package/lib/permissions.js +708 -708
  135. package/lib/pqc-agent.js +87 -87
  136. package/lib/pqc-gate.js +279 -279
  137. package/lib/protobuf-encoder.js +190 -190
  138. package/lib/protocol-dispatcher.js +161 -161
  139. package/lib/pubsub-redis.js +167 -177
  140. package/lib/pubsub.js +429 -429
  141. package/lib/queue-local.js +476 -476
  142. package/lib/queue-redis.js +745 -752
  143. package/lib/queue-sqs.js +319 -319
  144. package/lib/queue.js +695 -695
  145. package/lib/redis-client.js +519 -489
  146. package/lib/request-helpers.js +340 -336
  147. package/lib/restore-bundle.js +237 -237
  148. package/lib/restore-rollback.js +259 -259
  149. package/lib/restore.js +409 -409
  150. package/lib/retry.js +376 -376
  151. package/lib/router.js +748 -748
  152. package/lib/safe-async.js +735 -735
  153. package/lib/safe-buffer.js +237 -237
  154. package/lib/safe-json.js +541 -541
  155. package/lib/safe-schema.js +1266 -1266
  156. package/lib/safe-url.js +159 -159
  157. package/lib/scheduler.js +706 -706
  158. package/lib/security-assert.js +373 -373
  159. package/lib/seeders.js +618 -630
  160. package/lib/session.js +478 -478
  161. package/lib/slug.js +269 -269
  162. package/lib/ssrf-guard.js +401 -401
  163. package/lib/static.js +879 -114
  164. package/lib/storage.js +471 -471
  165. package/lib/subject.js +281 -281
  166. package/lib/template.js +791 -791
  167. package/lib/testing.js +798 -798
  168. package/lib/time.js +310 -310
  169. package/lib/totp.js +302 -302
  170. package/lib/tracing.js +494 -494
  171. package/lib/uuid.js +132 -132
  172. package/lib/validate-opts.js +340 -270
  173. package/lib/vault/index.js +308 -308
  174. package/lib/vault/rotate.js +784 -784
  175. package/lib/vault/wrap.js +296 -296
  176. package/lib/vendor/noble-ciphers.cjs +9 -9
  177. package/lib/webhook.js +595 -598
  178. package/lib/websocket.js +1048 -1048
  179. package/package.json +77 -77
  180. package/sbom.cyclonedx.json +7 -7
@@ -0,0 +1,729 @@
1
+ "use strict";
2
+ /**
3
+ * guard-json — JSON content-safety primitive (b.guardJson).
4
+ *
5
+ * Threat catalog grounded in current research (multiple 2025–2026 CVEs
6
+ * + ongoing prototype-pollution vulnerability series):
7
+ * - CVE-2025-55182 React/Next.js Server Functions deserialization → RCE
8
+ * - CVE-2025-57820 / CVE-2026-30226 Svelte devalue prototype pollution
9
+ * - CVE-2026-35209 defu prototype pollution
10
+ * - CVE-2026-28794 @orpc/client prototype pollution via deserialization
11
+ * - CVE-2025-13465 Lodash prototype-chain path traversal
12
+ * - CVE-2025-25014 Kibana prototype pollution → RCE
13
+ * - CVE-2024-38984 json-override prototype pollution
14
+ * - CVE-2022-42743 deep-parse-json prototype pollution
15
+ * - GHSA-9c47-m6qq-7p4h JSON5 prototype pollution via parse method
16
+ *
17
+ * var rv = b.guardJson.validate(input, { profile: "strict" });
18
+ * var safe = b.guardJson.parse(input, { profile: "strict" });
19
+ * var g = b.guardJson.gate({ profile: "strict" });
20
+ *
21
+ * Threat catalog covered:
22
+ *
23
+ * 1. Prototype pollution — `__proto__` / `constructor` / `prototype`
24
+ * keys anywhere in the tree. Refused regardless of profile under
25
+ * strict; balanced strips; permissive audits.
26
+ *
27
+ * 2. Array-index pollution — non-numeric keys ON array values
28
+ * (Svelte devalue CVE-2025-57820 class). When operator JSON
29
+ * structurally has arrays, keys like `push` / `toString` injected
30
+ * via reviver/parser quirks pollute Array.prototype.
31
+ *
32
+ * 3. Depth bombs — nested arrays/objects past safe stack. Refused
33
+ * at the configured cap.
34
+ *
35
+ * 4. Breadth / key-count bombs — single object with massive key count
36
+ * (10⁶ keys → CPU + memory blow-up).
37
+ *
38
+ * 5. Duplicate keys — RFC 8259 says keys SHOULD be unique; in
39
+ * practice JSON.parse silently last-wins, which lets attackers
40
+ * smuggle duplicate-key payloads past validation that ran on the
41
+ * first occurrence (Bishop Fox JSON-interoperability research).
42
+ *
43
+ * 6. NaN / Infinity / -Infinity — RFC 8259 forbids; JSON5 / lenient
44
+ * parsers accept. Refused under strict.
45
+ *
46
+ * 7. Comments (single-line // and block-style) — RFC 8259 forbids;
47
+ * JSON5 / JSONC accept. Refused under strict.
48
+ *
49
+ * 8. Trailing commas — JSON5 accepts; refused under strict.
50
+ *
51
+ * 9. BOM injection — leading or mid-stream U+FEFF.
52
+ *
53
+ * 10. Bidi / null / control chars in string values — same codepoint
54
+ * catalog as guard-csv/html/svg via lib/codepoint-class.
55
+ *
56
+ * 11. Numeric precision loss — values above
57
+ * `Number.MAX_SAFE_INTEGER` silently lose precision when round-
58
+ * tripped through Number. Detected via raw-text scan for digit
59
+ * runs longer than safe-int width.
60
+ *
61
+ * 12. Total size cap (anti-DoS).
62
+ *
63
+ * 13. Top-level-key allowlist — strict profile requires the operator
64
+ * to declare allowed top-level keys; balanced/permissive skip.
65
+ *
66
+ * Source files MUST be pure ASCII per the codepoint-table programmatic
67
+ * regex pattern; threat-detection regexes use lib/codepoint-class.
68
+ */
69
+
70
+ var codepointClass = require("./codepoint-class");
71
+ var lazyRequire = require("./lazy-require");
72
+ var gateContract = require("./gate-contract");
73
+ var C = require("./constants");
74
+ var numericBounds = require("./numeric-bounds");
75
+ var safeJson = require("./safe-json");
76
+ var { GuardJsonError } = require("./framework-error");
77
+
78
+ var observability = lazyRequire(function () { return require("./observability"); });
79
+ void observability;
80
+
81
+ var _err = GuardJsonError.factory;
82
+
83
+ // ---- Compiled detectors ----
84
+
85
+ var BIDI_RE = codepointClass.BIDI_RE;
86
+ var C0_CTRL_RE = codepointClass.C0_CTRL_RE;
87
+ var ZW_RE = codepointClass.ZERO_WIDTH_RE;
88
+ var NULL_BYTE = codepointClass.NULL_BYTE;
89
+ var BOM_CHAR = codepointClass.BOM_CHAR;
90
+
91
+ // Prototype-pollution key denylist. Operator-supplied JSON containing
92
+ // any of these keys at any depth is refused under strict.
93
+ var POLLUTION_KEYS = Object.freeze(["__proto__", "constructor", "prototype"]);
94
+
95
+ // Comment / NaN / Infinity / hex / single-quote markers — pre-parse
96
+ // scan on the raw source. JSON.parse rejects most of these, but JSON5
97
+ // / JSONC parsers accept and silently coerce; we surface the source
98
+ // shape so operators can refuse the whole input regardless of which
99
+ // parser their downstream code uses.
100
+ var COMMENT_LINE_RE = /(^|[^"\\])\/\/[^\r\n]*/m;
101
+ var COMMENT_BLOCK_RE = /\/\*[\s\S]*?\*\//;
102
+ var BARE_NAN_RE = /(^|[\s,:[{(])(NaN|Infinity|-Infinity|undefined)\b/;
103
+ var TRAILING_COMMA_RE = /,(\s*[\]}])/;
104
+ var SINGLE_QUOTED_KEY_RE = /(^|[\s,{])'[^']*'\s*:/;
105
+ var HEX_LITERAL_RE = /(^|[\s,:[{(])-?0[xX][0-9a-fA-F]+\b/;
106
+
107
+ // Numeric precision — runs of 16+ digits in a number context (above
108
+ // Number.MAX_SAFE_INTEGER ≈ 9.007e15 ≈ 16 digits).
109
+ var BIG_INTEGER_RE = /(^|[\s,:[{(])(-?\d{17,})(?:[\s,\]}]|$)/;
110
+
111
+ // Prototype-pollution key detector — SOURCE-level. After JSON.parse,
112
+ // __proto__ disappears from Object.keys() (it routes through the
113
+ // prototype setter), so a tree-walk after parse misses it. Scan the
114
+ // raw source for `"__proto__":`, `"constructor":`, `"prototype":`
115
+ // patterns to catch hostile sources before any parser sees them. The
116
+ // detection is conservative (matches inside strings too — operator's
117
+ // downstream code may run a JSON.parse without the safeJson reviver
118
+ // and the pollution still lands).
119
+ var POLLUTION_KEY_SOURCE_RE = /"(__proto__|constructor|prototype)"\s*:/;
120
+
121
+ // ---- Profile presets ----
122
+
123
+ var PROFILES = Object.freeze({
124
+ "strict": {
125
+ pollutionPolicy: "reject",
126
+ duplicateKeyPolicy: "reject",
127
+ nanInfinityPolicy: "reject",
128
+ commentPolicy: "reject",
129
+ trailingCommaPolicy: "reject",
130
+ json5SyntaxPolicy: "reject", // single-quoted / hex / unquoted-key
131
+ bomPolicy: "reject",
132
+ bidiPolicy: "reject",
133
+ controlPolicy: "reject",
134
+ nullBytePolicy: "reject",
135
+ zeroWidthPolicy: "reject",
136
+ numericPrecisionPolicy: "reject",
137
+ requireTopLevelKeyAllowlist: false, // operator opts in via topLevelKeyAllowlist
138
+ topLevelKeyAllowlist: null,
139
+ maxBytes: C.BYTES.mib(2),
140
+ maxDepth: 8, // allow:raw-byte-literal — recursion depth, not byte size
141
+ maxKeysPerObject: 256, // allow:raw-byte-literal — key count cap, not byte size
142
+ maxArrayLength: 1024, // allow:raw-byte-literal — array length cap, not byte size
143
+ maxStringLength: C.BYTES.kib(8),
144
+ maxTotalNodes: 0x2000, // allow:raw-byte-literal — node count cap, not byte size
145
+ },
146
+ "balanced": {
147
+ pollutionPolicy: "strip", // remove __proto__ keys silently
148
+ duplicateKeyPolicy: "audit",
149
+ nanInfinityPolicy: "reject",
150
+ commentPolicy: "audit",
151
+ trailingCommaPolicy: "audit",
152
+ json5SyntaxPolicy: "audit",
153
+ bomPolicy: "strip",
154
+ bidiPolicy: "strip",
155
+ controlPolicy: "strip",
156
+ nullBytePolicy: "strip",
157
+ zeroWidthPolicy: "strip",
158
+ numericPrecisionPolicy: "audit",
159
+ requireTopLevelKeyAllowlist: false,
160
+ topLevelKeyAllowlist: null,
161
+ maxBytes: C.BYTES.mib(8),
162
+ maxDepth: 32, // allow:raw-byte-literal — recursion depth, not byte size
163
+ maxKeysPerObject: 4096, // allow:raw-byte-literal — key count cap, not byte size
164
+ maxArrayLength: 65536, // allow:raw-byte-literal — array length cap, not byte size
165
+ maxStringLength: C.BYTES.kib(64),
166
+ maxTotalNodes: 0x10000, // allow:raw-byte-literal — node count cap, not byte size
167
+ },
168
+ "permissive": {
169
+ pollutionPolicy: "audit",
170
+ duplicateKeyPolicy: "audit",
171
+ nanInfinityPolicy: "audit",
172
+ commentPolicy: "audit",
173
+ trailingCommaPolicy: "audit",
174
+ json5SyntaxPolicy: "audit",
175
+ bomPolicy: "strip",
176
+ bidiPolicy: "audit",
177
+ controlPolicy: "strip",
178
+ nullBytePolicy: "reject",
179
+ zeroWidthPolicy: "strip",
180
+ numericPrecisionPolicy: "audit",
181
+ requireTopLevelKeyAllowlist: false,
182
+ topLevelKeyAllowlist: null,
183
+ maxBytes: C.BYTES.mib(64),
184
+ maxDepth: 64, // allow:raw-byte-literal — recursion depth, not byte size
185
+ maxKeysPerObject: 65536, // allow:raw-byte-literal — key count cap, not byte size
186
+ maxArrayLength: 1048576, // allow:raw-byte-literal — array length cap, not byte size
187
+ maxStringLength: C.BYTES.kib(256),
188
+ maxTotalNodes: 0x40000, // allow:raw-byte-literal — node count cap, not byte size
189
+ },
190
+ });
191
+
192
+ var DEFAULTS = Object.freeze(Object.assign({}, PROFILES["strict"], {
193
+ mode: "enforce",
194
+ maxRuntimeMs: C.TIME.seconds(10),
195
+ }));
196
+
197
+ var COMPLIANCE_POSTURES = Object.freeze({
198
+ "hipaa": Object.assign({}, PROFILES["strict"], {
199
+ forensicSnippetBytes: C.BYTES.bytes(256),
200
+ }),
201
+ "pci-dss": Object.assign({}, PROFILES["strict"], {
202
+ forensicSnippetBytes: C.BYTES.bytes(256),
203
+ }),
204
+ "gdpr": Object.assign({}, PROFILES["balanced"], {
205
+ forensicSnippetBytes: C.BYTES.bytes(128),
206
+ }),
207
+ "soc2-cc7": Object.assign({}, PROFILES["strict"], {
208
+ forensicSnippetBytes: C.BYTES.bytes(512),
209
+ }),
210
+ });
211
+
212
+ // ---- Helpers ----
213
+
214
+ function _resolveOpts(opts) {
215
+ return gateContract.resolveProfileAndPosture(opts, {
216
+ profiles: PROFILES,
217
+ compliancePostures: COMPLIANCE_POSTURES,
218
+ defaults: DEFAULTS,
219
+ errorClass: GuardJsonError,
220
+ errCodePrefix: "json",
221
+ });
222
+ }
223
+
224
+ function _isPollutionKey(key) {
225
+ return POLLUTION_KEYS.indexOf(key) !== -1;
226
+ }
227
+
228
+ // _scanPollutionKeys — walks parsed JSON tree counting prototype-
229
+ // pollution keys and depth + breadth + node-count exhaustion. Operator
230
+ // can either reject the whole tree or strip pollution keys.
231
+ function _scanTree(value, opts, ctx) {
232
+ if (!ctx) ctx = { depth: 0, totalNodes: 0, pollutionHits: [],
233
+ duplicateKeyHits: [], breadthCapHits: [],
234
+ arrayLenCapHits: [], depthCapHits: [],
235
+ stringTooLongHits: [] };
236
+ ctx.totalNodes += 1;
237
+ if (ctx.totalNodes > opts.maxTotalNodes) {
238
+ ctx.depthCapHits.push({ kind: "node-count-cap",
239
+ snippet: "node count exceeds maxTotalNodes " + opts.maxTotalNodes });
240
+ return ctx;
241
+ }
242
+ if (ctx.depth > opts.maxDepth) {
243
+ ctx.depthCapHits.push({ kind: "depth-cap",
244
+ snippet: "depth " + ctx.depth + " exceeds maxDepth " + opts.maxDepth });
245
+ return ctx;
246
+ }
247
+ if (value === null || typeof value !== "object") {
248
+ if (typeof value === "string" && value.length > opts.maxStringLength) {
249
+ ctx.stringTooLongHits.push({
250
+ kind: "string-too-long",
251
+ snippet: "string length " + value.length +
252
+ " exceeds maxStringLength " + opts.maxStringLength,
253
+ });
254
+ }
255
+ return ctx;
256
+ }
257
+ if (Array.isArray(value)) {
258
+ if (value.length > opts.maxArrayLength) {
259
+ ctx.arrayLenCapHits.push({
260
+ kind: "array-length-cap",
261
+ snippet: "array length " + value.length +
262
+ " exceeds maxArrayLength " + opts.maxArrayLength,
263
+ });
264
+ }
265
+ for (var i = 0; i < value.length; i += 1) {
266
+ ctx.depth += 1;
267
+ _scanTree(value[i], opts, ctx);
268
+ ctx.depth -= 1;
269
+ }
270
+ return ctx;
271
+ }
272
+ // Plain object.
273
+ var keys = Object.keys(value);
274
+ if (keys.length > opts.maxKeysPerObject) {
275
+ ctx.breadthCapHits.push({
276
+ kind: "key-count-cap",
277
+ snippet: "object key count " + keys.length +
278
+ " exceeds maxKeysPerObject " + opts.maxKeysPerObject,
279
+ });
280
+ }
281
+ for (var ki = 0; ki < keys.length; ki += 1) {
282
+ var k = keys[ki];
283
+ if (_isPollutionKey(k)) {
284
+ ctx.pollutionHits.push({
285
+ kind: "prototype-pollution-key",
286
+ snippet: "prototype-pollution key " + JSON.stringify(k) +
287
+ " at depth " + ctx.depth,
288
+ });
289
+ }
290
+ ctx.depth += 1;
291
+ _scanTree(value[k], opts, ctx);
292
+ ctx.depth -= 1;
293
+ }
294
+ return ctx;
295
+ }
296
+
297
+ // _scanRawSource — pre-parse text scan for syntax-level threats that
298
+ // vanish after JSON.parse normalizes them: comments, trailing commas,
299
+ // NaN/Infinity, hex literals, single-quoted keys, BOM, big-integer
300
+ // precision-loss candidates.
301
+ function _scanRawSource(text, opts) {
302
+ var issues = [];
303
+ if (text.indexOf(BOM_CHAR) === 0 && opts.bomPolicy !== "allow") {
304
+ issues.push({
305
+ kind: "bom-leading", severity: "high", ruleId: "json.bom",
306
+ snippet: "leading BOM (U+FEFF)",
307
+ });
308
+ }
309
+ if (text.indexOf(BOM_CHAR) > 0 && opts.bomPolicy !== "allow") {
310
+ issues.push({
311
+ kind: "bom-mid-stream", severity: "high", ruleId: "json.bom",
312
+ snippet: "BOM mid-stream",
313
+ });
314
+ }
315
+ // Commented forms.
316
+ if (opts.commentPolicy !== "allow") {
317
+ if (COMMENT_BLOCK_RE.test(text)) { // allow:regex-no-length-cap — text bounded by maxBytes above
318
+ issues.push({
319
+ kind: "comment-block", severity: "high", ruleId: "json.comment",
320
+ snippet: "block comment /* ... */ (RFC 8259 forbids; JSON5/JSONC accept)",
321
+ });
322
+ }
323
+ if (COMMENT_LINE_RE.test(text)) { // allow:regex-no-length-cap — text bounded by maxBytes above
324
+ issues.push({
325
+ kind: "comment-line", severity: "high", ruleId: "json.comment",
326
+ snippet: "line comment // (RFC 8259 forbids; JSON5/JSONC accept)",
327
+ });
328
+ }
329
+ }
330
+ if (opts.nanInfinityPolicy !== "allow" && BARE_NAN_RE.test(text)) { // allow:regex-no-length-cap — text bounded by maxBytes above
331
+ issues.push({
332
+ kind: "nan-infinity", severity: "high", ruleId: "json.nan-infinity",
333
+ snippet: "bare NaN / Infinity / undefined token (RFC 8259 forbids)",
334
+ });
335
+ }
336
+ if (opts.trailingCommaPolicy !== "allow" && TRAILING_COMMA_RE.test(text)) { // allow:regex-no-length-cap — text bounded by maxBytes above
337
+ issues.push({
338
+ kind: "trailing-comma", severity: "high", ruleId: "json.trailing-comma",
339
+ snippet: "trailing comma (RFC 8259 forbids)",
340
+ });
341
+ }
342
+ if (opts.json5SyntaxPolicy !== "allow") {
343
+ if (SINGLE_QUOTED_KEY_RE.test(text)) { // allow:regex-no-length-cap — text bounded by maxBytes above
344
+ issues.push({
345
+ kind: "single-quoted-key", severity: "high", ruleId: "json.json5-syntax",
346
+ snippet: "single-quoted key (JSON5 only; not RFC 8259)",
347
+ });
348
+ }
349
+ if (HEX_LITERAL_RE.test(text)) { // allow:regex-no-length-cap — text bounded by maxBytes above
350
+ issues.push({
351
+ kind: "hex-literal", severity: "high", ruleId: "json.json5-syntax",
352
+ snippet: "hex numeric literal (JSON5 only; not RFC 8259)",
353
+ });
354
+ }
355
+ }
356
+ if (opts.numericPrecisionPolicy !== "allow" && BIG_INTEGER_RE.test(text)) { // allow:regex-no-length-cap — text bounded by maxBytes above
357
+ issues.push({
358
+ kind: "numeric-precision-loss", severity: "warn",
359
+ ruleId: "json.numeric-precision",
360
+ snippet: "integer above Number.MAX_SAFE_INTEGER (precision loss)",
361
+ });
362
+ }
363
+ // Prototype-pollution source scan — catches __proto__/constructor/
364
+ // prototype keys before any downstream parser sees them. Critical
365
+ // when the operator's downstream code uses raw JSON.parse without
366
+ // safeJson's reviver.
367
+ if (opts.pollutionPolicy !== "allow") {
368
+ var protoIter = text.matchAll(/"(__proto__|constructor|prototype)"\s*:/g);
369
+ var protoMatch;
370
+ for (protoMatch of protoIter) {
371
+ issues.push({
372
+ kind: "prototype-pollution-key",
373
+ severity: opts.pollutionPolicy === "reject" ? "critical" : "high",
374
+ ruleId: "json.prototype-pollution",
375
+ location: protoMatch.index,
376
+ snippet: "prototype-pollution key " + JSON.stringify(protoMatch[1]) +
377
+ " at byte " + protoMatch.index +
378
+ " (CVE-2025-55182 / CVE-2025-57820 class)",
379
+ });
380
+ }
381
+ }
382
+ // Bidi / null / control via shared codepoint class.
383
+ issues.push.apply(issues, codepointClass.detectCharThreats(text, opts, "json"));
384
+ if (opts.zeroWidthPolicy !== "allow" && opts.zeroWidthPolicy !== "strip" &&
385
+ ZW_RE.test(text)) { // allow:regex-no-length-cap — text bounded by maxBytes above
386
+ issues.push({
387
+ kind: "zero-width", severity: "warn", ruleId: "json.zero-width",
388
+ snippet: "zero-width / invisible-formatting char in JSON source",
389
+ });
390
+ }
391
+ return issues;
392
+ }
393
+
394
+ // _detectIssues — full validate path: raw-source pre-scan + parse +
395
+ // tree walk.
396
+ function _detectIssues(input, opts) {
397
+ var issues = [];
398
+ if (typeof input !== "string") {
399
+ return [{ kind: "bad-input", severity: "high",
400
+ snippet: "input is not a string" }];
401
+ }
402
+ if (input.length > opts.maxBytes) {
403
+ return [{ kind: "too-large", severity: "high", ruleId: "json.too-large",
404
+ snippet: "input " + input.length +
405
+ " bytes exceeds maxBytes " + opts.maxBytes }];
406
+ }
407
+
408
+ // Raw-source pre-scan.
409
+ issues = issues.concat(_scanRawSource(input, opts));
410
+
411
+ // Try parsing — bail early on syntax errors so the operator gets a
412
+ // usable error rather than crashing the validator. safeJson.parse
413
+ // already enforces top-level depth/size caps and returns an Error
414
+ // on malformed input.
415
+ var parsed;
416
+ try {
417
+ parsed = safeJson.parse(input, {
418
+ maxBytes: opts.maxBytes,
419
+ maxDepth: opts.maxDepth,
420
+ });
421
+ } catch (e) {
422
+ issues.push({
423
+ kind: "parse-failed", severity: "critical", ruleId: "json.parse",
424
+ snippet: "JSON parse failed: " + (e && e.message),
425
+ });
426
+ return issues;
427
+ }
428
+
429
+ // Top-level-key allowlist check.
430
+ if (opts.requireTopLevelKeyAllowlist || Array.isArray(opts.topLevelKeyAllowlist)) {
431
+ if (!Array.isArray(opts.topLevelKeyAllowlist)) {
432
+ issues.push({
433
+ kind: "missing-allowlist", severity: "high",
434
+ ruleId: "json.top-level-allowlist",
435
+ snippet: "requireTopLevelKeyAllowlist set but topLevelKeyAllowlist is null",
436
+ });
437
+ } else if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
438
+ var topKeys = Object.keys(parsed);
439
+ var allow = opts.topLevelKeyAllowlist;
440
+ for (var tki = 0; tki < topKeys.length; tki += 1) {
441
+ if (allow.indexOf(topKeys[tki]) === -1) {
442
+ issues.push({
443
+ kind: "top-level-key-not-allowlisted", severity: "high",
444
+ ruleId: "json.top-level-allowlist",
445
+ snippet: "top-level key " + JSON.stringify(topKeys[tki]) +
446
+ " not in topLevelKeyAllowlist",
447
+ });
448
+ }
449
+ }
450
+ }
451
+ }
452
+
453
+ // Tree walk for depth / breadth / string-length / array-length /
454
+ // node-count caps. Pollution-key detection happens at the source
455
+ // level (above) — after JSON.parse, __proto__ is invisible to
456
+ // Object.keys() (routes through the prototype setter), so a post-
457
+ // parse walk misses the pollution-source case.
458
+ var ctx = _scanTree(parsed, opts);
459
+ for (var bi = 0; bi < ctx.breadthCapHits.length; bi += 1) {
460
+ issues.push(Object.assign({ severity: "high",
461
+ ruleId: "json.breadth-cap" }, ctx.breadthCapHits[bi]));
462
+ }
463
+ for (var ai = 0; ai < ctx.arrayLenCapHits.length; ai += 1) {
464
+ issues.push(Object.assign({ severity: "high",
465
+ ruleId: "json.array-length-cap" }, ctx.arrayLenCapHits[ai]));
466
+ }
467
+ for (var di = 0; di < ctx.depthCapHits.length; di += 1) {
468
+ issues.push(Object.assign({ severity: "high",
469
+ ruleId: "json.depth-cap" }, ctx.depthCapHits[di]));
470
+ }
471
+ for (var si = 0; si < ctx.stringTooLongHits.length; si += 1) {
472
+ issues.push(Object.assign({ severity: "high",
473
+ ruleId: "json.string-too-long" }, ctx.stringTooLongHits[si]));
474
+ }
475
+
476
+ // Duplicate-key detection — JSON.parse silently last-wins. We re-
477
+ // scan the source for duplicate keys at the same nesting depth.
478
+ if (opts.duplicateKeyPolicy !== "allow") {
479
+ var dups = _detectDuplicateKeys(input);
480
+ for (var dki = 0; dki < dups.length; dki += 1) {
481
+ issues.push({
482
+ kind: "duplicate-key",
483
+ severity: opts.duplicateKeyPolicy === "reject" ? "critical" : "warn",
484
+ ruleId: "json.duplicate-key",
485
+ snippet: "duplicate key " + JSON.stringify(dups[dki]) +
486
+ " (RFC 8259 SHOULD-unique; last-wins silently)",
487
+ });
488
+ }
489
+ }
490
+
491
+ return issues;
492
+ }
493
+
494
+ // _detectDuplicateKeys — minimal source scan that counts identical
495
+ // quoted keys at the same `{ ... }` nesting level. Not a full parser;
496
+ // catches the common `{"a":1,"a":2}` shape and similar.
497
+ function _detectDuplicateKeys(text) {
498
+ var seen = [Object.create(null)]; // stack of scopes, top = current
499
+ var dups = Object.create(null);
500
+ var len = text.length;
501
+ var i = 0;
502
+ while (i < len) {
503
+ var c = text.charAt(i);
504
+ if (c === "{") { seen.push(Object.create(null)); i += 1; continue; }
505
+ if (c === "}") { if (seen.length > 1) seen.pop(); i += 1; continue; }
506
+ if (c === '"') {
507
+ // Read the string up to its closing quote.
508
+ var start = i + 1;
509
+ var p = start;
510
+ while (p < len) {
511
+ var cp = text.charAt(p);
512
+ if (cp === "\\") { p += 2; continue; }
513
+ if (cp === '"') break;
514
+ p += 1;
515
+ }
516
+ var keyText = text.slice(start, p);
517
+ i = p + 1;
518
+ // Skip whitespace; if next non-whitespace is `:`, this string is
519
+ // an object key.
520
+ while (i < len && /\s/.test(text.charAt(i))) i += 1;
521
+ if (i < len && text.charAt(i) === ":") {
522
+ var scope = seen[seen.length - 1];
523
+ if (scope[keyText] === true) dups[keyText] = true;
524
+ else scope[keyText] = true;
525
+ i += 1;
526
+ }
527
+ continue;
528
+ }
529
+ i += 1;
530
+ }
531
+ return Object.keys(dups);
532
+ }
533
+
534
+ // _stripPollutionTree — recursively delete __proto__/constructor/
535
+ // prototype keys from the parsed tree. Used by sanitize when policy is
536
+ // "strip". Walks Object.create(null)-shaped clones so the cleaned tree
537
+ // has no prototype pollution.
538
+ function _stripPollutionTree(value, opts, depth) {
539
+ depth = depth || 0;
540
+ if (depth > opts.maxDepth) return value;
541
+ if (value === null || typeof value !== "object") return value;
542
+ if (Array.isArray(value)) {
543
+ var out = [];
544
+ for (var i = 0; i < value.length; i += 1) {
545
+ out.push(_stripPollutionTree(value[i], opts, depth + 1));
546
+ }
547
+ return out;
548
+ }
549
+ var keys = Object.keys(value);
550
+ var clean = Object.create(null);
551
+ for (var ki = 0; ki < keys.length; ki += 1) {
552
+ var k = keys[ki];
553
+ if (_isPollutionKey(k)) continue;
554
+ clean[k] = _stripPollutionTree(value[k], opts, depth + 1);
555
+ }
556
+ return clean;
557
+ }
558
+
559
+ // ---- Public surface ----
560
+
561
+ function validate(input, opts) {
562
+ opts = _resolveOpts(opts);
563
+ numericBounds.requireAllPositiveFiniteIntIfPresent(opts,
564
+ ["maxBytes", "maxDepth", "maxKeysPerObject", "maxArrayLength",
565
+ "maxStringLength", "maxTotalNodes"],
566
+ "guardJson.validate", GuardJsonError, "json.bad-opt");
567
+ if (typeof input !== "string") {
568
+ return {
569
+ ok: false,
570
+ issues: [{ kind: "bad-input", severity: "high",
571
+ snippet: "input is not a string" }],
572
+ };
573
+ }
574
+ return gateContract.aggregateIssues(_detectIssues(input, opts));
575
+ }
576
+
577
+ function parse(input, opts) {
578
+ opts = _resolveOpts(opts);
579
+ if (typeof input !== "string") {
580
+ throw _err("json.bad-input", "parse requires string input");
581
+ }
582
+ // Strip BOM if policy says strip.
583
+ if (opts.bomPolicy === "strip" && input.indexOf(BOM_CHAR) === 0) {
584
+ input = input.slice(1);
585
+ }
586
+ // Strip control chars from the source (rare in practice; refused
587
+ // under strict; balanced/permissive allow strip).
588
+ if (opts.controlPolicy === "strip") {
589
+ input = input.replace(codepointClass.C0_CTRL_RE_G, "");
590
+ }
591
+ if (opts.zeroWidthPolicy === "strip") {
592
+ input = input.replace(codepointClass.ZW_RE_G, "");
593
+ }
594
+ // Source-level pollution check — refuse early when policy is reject.
595
+ if (opts.pollutionPolicy === "reject" && POLLUTION_KEY_SOURCE_RE.test(input)) { // allow:regex-no-length-cap — input bounded by maxBytes above
596
+ throw _err("json.prototype-pollution",
597
+ "guardJson.parse: source contains prototype-pollution key " +
598
+ "(__proto__ / constructor / prototype)");
599
+ }
600
+ // Refuse on other critical pre-parse threats per policy.
601
+ var preIssues = _scanRawSource(input, opts);
602
+ for (var pi = 0; pi < preIssues.length; pi += 1) {
603
+ var issue = preIssues[pi];
604
+ if (issue.kind === "prototype-pollution-key") continue; // handled above
605
+ if (issue.severity === "critical" ||
606
+ (issue.severity === "high" &&
607
+ opts[_policyKeyForRuleId(issue.ruleId)] === "reject")) {
608
+ throw _err(issue.ruleId, "guardJson.parse: " + issue.snippet);
609
+ }
610
+ }
611
+ // safeJson.parse strips POISONED_KEYS via the reviver pass; this is
612
+ // the canonical strip path. allowProto=true preserves them for the
613
+ // permissive/audit path.
614
+ var allowProto = opts.pollutionPolicy === "allow" ||
615
+ opts.pollutionPolicy === "audit";
616
+ var parsed;
617
+ try {
618
+ parsed = safeJson.parse(input, {
619
+ maxBytes: opts.maxBytes,
620
+ maxDepth: opts.maxDepth,
621
+ allowProto: allowProto,
622
+ });
623
+ } catch (e) {
624
+ throw _err("json.parse", "guardJson.parse: " + (e && e.message));
625
+ }
626
+ return parsed;
627
+ }
628
+
629
+ function _policyKeyForRuleId(ruleId) {
630
+ // Map issue ruleId → opts policy key for the reject-decision lookup.
631
+ var map = {
632
+ "json.bom": "bomPolicy",
633
+ "json.comment": "commentPolicy",
634
+ "json.nan-infinity": "nanInfinityPolicy",
635
+ "json.trailing-comma": "trailingCommaPolicy",
636
+ "json.json5-syntax": "json5SyntaxPolicy",
637
+ "json.numeric-precision": "numericPrecisionPolicy",
638
+ "json.bidi": "bidiPolicy",
639
+ "json.control": "controlPolicy",
640
+ "json.null-byte": "nullBytePolicy",
641
+ "json.zero-width": "zeroWidthPolicy",
642
+ };
643
+ return map[ruleId] || null;
644
+ }
645
+
646
+ function gate(opts) {
647
+ opts = _resolveOpts(opts);
648
+ return gateContract.buildGuardGate(
649
+ opts.name || "guardJson:" + (opts.profile || "default"),
650
+ opts,
651
+ async function (ctx) {
652
+ var text = gateContract.extractBytesAsText(ctx);
653
+ if (!text) return { ok: true, action: "serve" };
654
+ var rv = validate(text, opts);
655
+ if (rv.issues.length === 0) return { ok: true, action: "serve" };
656
+ var hasCritical = rv.issues.some(function (i) {
657
+ return i.severity === "critical" || i.severity === "high";
658
+ });
659
+ if (!hasCritical) return { ok: true, action: "audit-only", issues: rv.issues };
660
+
661
+ // Sanitize-eligibility: every reject-policy must be off.
662
+ var canSanitize = opts.pollutionPolicy !== "reject" &&
663
+ opts.duplicateKeyPolicy !== "reject" &&
664
+ opts.nanInfinityPolicy !== "reject" &&
665
+ opts.commentPolicy !== "reject" &&
666
+ opts.trailingCommaPolicy !== "reject" &&
667
+ opts.json5SyntaxPolicy !== "reject" &&
668
+ opts.bomPolicy !== "reject" &&
669
+ opts.bidiPolicy !== "reject" &&
670
+ opts.controlPolicy !== "reject" &&
671
+ opts.nullBytePolicy !== "reject";
672
+ if (canSanitize) {
673
+ try {
674
+ var clean = parse(text, opts);
675
+ var emitted = JSON.stringify(clean);
676
+ return {
677
+ ok: true, action: "sanitize",
678
+ sanitized: Buffer.from(emitted, "utf8"),
679
+ issues: rv.issues,
680
+ };
681
+ } catch (_e) { /* fall through */ }
682
+ }
683
+ return { ok: false, action: "refuse", issues: rv.issues };
684
+ });
685
+ }
686
+
687
+ var buildProfile = gateContract.makeProfileBuilder(PROFILES);
688
+
689
+ function compliancePosture(name) {
690
+ return gateContract.lookupCompliancePosture(name, COMPLIANCE_POSTURES, _err, "json");
691
+ }
692
+
693
+ var _jsonRulePacks = gateContract.makeRulePackLoader(GuardJsonError, "json");
694
+ var loadRulePack = _jsonRulePacks.load;
695
+
696
+ module.exports = {
697
+ // ---- guard-* family registry exports ----
698
+ NAME: "json",
699
+ KIND: "content",
700
+ MIME_TYPES: Object.freeze([
701
+ "application/json", "application/ld+json", "application/vnd.api+json",
702
+ ]),
703
+ EXTENSIONS: Object.freeze([".json", ".jsonld"]),
704
+ INTEGRATION_FIXTURES: Object.freeze({
705
+ kind: "content",
706
+ contentType: "application/json",
707
+ extension: ".json",
708
+ benignBytes: Buffer.from('{"name":"alice","age":30}', "utf8"),
709
+ // Hostile: prototype-pollution payload (CVE-2025-55182 React Server
710
+ // Functions class; CVE-2025-57820 Svelte devalue class).
711
+ hostileBytes: Buffer.from('{"__proto__":{"polluted":true}}', "utf8"),
712
+ }),
713
+ // ---- primitive surface ----
714
+ validate: validate,
715
+ parse: parse,
716
+ gate: gate,
717
+ buildProfile: buildProfile,
718
+ compliancePosture: compliancePosture,
719
+ loadRulePack: loadRulePack,
720
+ PROFILES: PROFILES,
721
+ DEFAULTS: DEFAULTS,
722
+ COMPLIANCE_POSTURES: COMPLIANCE_POSTURES,
723
+ POLLUTION_KEYS: POLLUTION_KEYS,
724
+ GuardJsonError: GuardJsonError,
725
+ };
726
+
727
+ void BIDI_RE; // referenced via codepointClass.detectCharThreats; binding kept for clarity
728
+ void C0_CTRL_RE;
729
+ void NULL_BYTE;