@blamejs/core 0.8.43 → 0.8.50

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 (222) hide show
  1. package/CHANGELOG.md +93 -0
  2. package/README.md +10 -10
  3. package/index.js +52 -0
  4. package/lib/a2a.js +159 -34
  5. package/lib/acme.js +762 -0
  6. package/lib/ai-pref.js +166 -43
  7. package/lib/api-key.js +108 -47
  8. package/lib/api-snapshot.js +157 -40
  9. package/lib/app-shutdown.js +113 -77
  10. package/lib/archive.js +337 -40
  11. package/lib/arg-parser.js +697 -0
  12. package/lib/asyncapi.js +99 -55
  13. package/lib/atomic-file.js +465 -104
  14. package/lib/audit-chain.js +123 -34
  15. package/lib/audit-daily-review.js +389 -0
  16. package/lib/audit-sign.js +302 -56
  17. package/lib/audit-tools.js +412 -63
  18. package/lib/audit.js +656 -35
  19. package/lib/auth/jwt-external.js +17 -0
  20. package/lib/auth/oauth.js +7 -0
  21. package/lib/auth-bot-challenge.js +505 -0
  22. package/lib/auth-header.js +92 -25
  23. package/lib/backup/bundle.js +26 -0
  24. package/lib/backup/index.js +512 -89
  25. package/lib/backup/manifest.js +168 -7
  26. package/lib/break-glass.js +415 -39
  27. package/lib/budr.js +103 -30
  28. package/lib/bundler.js +86 -66
  29. package/lib/cache.js +192 -72
  30. package/lib/chain-writer.js +65 -40
  31. package/lib/circuit-breaker.js +56 -33
  32. package/lib/cli-helpers.js +106 -75
  33. package/lib/cli.js +6 -30
  34. package/lib/cloud-events.js +99 -32
  35. package/lib/cluster-storage.js +162 -37
  36. package/lib/cluster.js +340 -49
  37. package/lib/codepoint-class.js +66 -0
  38. package/lib/compliance.js +424 -24
  39. package/lib/config-drift.js +111 -46
  40. package/lib/config.js +94 -40
  41. package/lib/consent.js +165 -18
  42. package/lib/constants.js +1 -0
  43. package/lib/content-credentials.js +153 -48
  44. package/lib/cookies.js +154 -62
  45. package/lib/credential-hash.js +133 -61
  46. package/lib/crypto-field.js +702 -18
  47. package/lib/crypto-hpke.js +256 -0
  48. package/lib/crypto.js +744 -22
  49. package/lib/csv.js +178 -35
  50. package/lib/daemon.js +456 -0
  51. package/lib/dark-patterns.js +186 -55
  52. package/lib/db-query.js +79 -2
  53. package/lib/db.js +1431 -60
  54. package/lib/ddl-change-control.js +523 -0
  55. package/lib/deprecate.js +195 -40
  56. package/lib/dev.js +82 -39
  57. package/lib/dora.js +67 -48
  58. package/lib/dr-runbook.js +368 -0
  59. package/lib/dsr.js +142 -11
  60. package/lib/dual-control.js +91 -56
  61. package/lib/events.js +120 -41
  62. package/lib/external-db-migrate.js +192 -2
  63. package/lib/external-db.js +795 -50
  64. package/lib/fapi2.js +122 -1
  65. package/lib/fda-21cfr11.js +395 -0
  66. package/lib/fdx.js +132 -2
  67. package/lib/file-type.js +87 -0
  68. package/lib/file-upload.js +93 -0
  69. package/lib/flag.js +82 -20
  70. package/lib/forms.js +132 -29
  71. package/lib/framework-error.js +169 -0
  72. package/lib/framework-schema.js +163 -35
  73. package/lib/gate-contract.js +849 -175
  74. package/lib/graphql-federation.js +68 -7
  75. package/lib/guard-all.js +172 -55
  76. package/lib/guard-archive.js +286 -124
  77. package/lib/guard-auth.js +194 -21
  78. package/lib/guard-cidr.js +190 -28
  79. package/lib/guard-csv.js +397 -51
  80. package/lib/guard-domain.js +213 -91
  81. package/lib/guard-email.js +236 -29
  82. package/lib/guard-filename.js +307 -75
  83. package/lib/guard-graphql.js +263 -30
  84. package/lib/guard-html.js +310 -116
  85. package/lib/guard-image.js +243 -30
  86. package/lib/guard-json.js +260 -54
  87. package/lib/guard-jsonpath.js +235 -23
  88. package/lib/guard-jwt.js +284 -30
  89. package/lib/guard-markdown.js +204 -22
  90. package/lib/guard-mime.js +190 -26
  91. package/lib/guard-oauth.js +277 -28
  92. package/lib/guard-pdf.js +251 -27
  93. package/lib/guard-regex.js +226 -18
  94. package/lib/guard-shell.js +229 -26
  95. package/lib/guard-svg.js +177 -10
  96. package/lib/guard-template.js +232 -21
  97. package/lib/guard-time.js +195 -29
  98. package/lib/guard-uuid.js +189 -30
  99. package/lib/guard-xml.js +259 -36
  100. package/lib/guard-yaml.js +241 -44
  101. package/lib/honeytoken.js +63 -27
  102. package/lib/html-balance.js +83 -0
  103. package/lib/http-client.js +486 -59
  104. package/lib/http-message-signature.js +582 -0
  105. package/lib/i18n.js +102 -49
  106. package/lib/iab-mspa.js +112 -32
  107. package/lib/iab-tcf.js +107 -2
  108. package/lib/inbox.js +90 -52
  109. package/lib/keychain.js +865 -0
  110. package/lib/legal-hold.js +374 -0
  111. package/lib/local-db-thin.js +320 -0
  112. package/lib/log-stream.js +281 -51
  113. package/lib/log.js +184 -86
  114. package/lib/mail-bounce.js +107 -62
  115. package/lib/mail.js +295 -58
  116. package/lib/mcp.js +108 -27
  117. package/lib/metrics.js +98 -89
  118. package/lib/middleware/age-gate.js +36 -0
  119. package/lib/middleware/ai-act-disclosure.js +37 -0
  120. package/lib/middleware/api-encrypt.js +45 -0
  121. package/lib/middleware/assetlinks.js +40 -0
  122. package/lib/middleware/asyncapi-serve.js +35 -0
  123. package/lib/middleware/attach-user.js +40 -0
  124. package/lib/middleware/bearer-auth.js +40 -0
  125. package/lib/middleware/body-parser.js +230 -0
  126. package/lib/middleware/bot-disclose.js +34 -0
  127. package/lib/middleware/bot-guard.js +39 -0
  128. package/lib/middleware/compression.js +37 -0
  129. package/lib/middleware/cookies.js +32 -0
  130. package/lib/middleware/cors.js +40 -0
  131. package/lib/middleware/csp-nonce.js +40 -0
  132. package/lib/middleware/csp-report.js +34 -0
  133. package/lib/middleware/csrf-protect.js +43 -0
  134. package/lib/middleware/daily-byte-quota.js +53 -85
  135. package/lib/middleware/db-role-for.js +40 -0
  136. package/lib/middleware/dpop.js +40 -0
  137. package/lib/middleware/error-handler.js +37 -14
  138. package/lib/middleware/fetch-metadata.js +39 -0
  139. package/lib/middleware/flag-context.js +34 -0
  140. package/lib/middleware/gpc.js +33 -0
  141. package/lib/middleware/headers.js +35 -0
  142. package/lib/middleware/health.js +46 -0
  143. package/lib/middleware/host-allowlist.js +30 -0
  144. package/lib/middleware/network-allowlist.js +38 -0
  145. package/lib/middleware/openapi-serve.js +34 -0
  146. package/lib/middleware/rate-limit.js +160 -18
  147. package/lib/middleware/request-id.js +36 -18
  148. package/lib/middleware/request-log.js +37 -0
  149. package/lib/middleware/require-aal.js +29 -0
  150. package/lib/middleware/require-auth.js +32 -0
  151. package/lib/middleware/require-bound-key.js +41 -0
  152. package/lib/middleware/require-content-type.js +32 -0
  153. package/lib/middleware/require-methods.js +27 -0
  154. package/lib/middleware/require-mtls.js +33 -0
  155. package/lib/middleware/require-step-up.js +37 -0
  156. package/lib/middleware/security-headers.js +44 -0
  157. package/lib/middleware/security-txt.js +38 -0
  158. package/lib/middleware/span-http-server.js +37 -0
  159. package/lib/middleware/sse.js +36 -0
  160. package/lib/middleware/trace-log-correlation.js +33 -0
  161. package/lib/middleware/trace-propagate.js +32 -0
  162. package/lib/middleware/tus-upload.js +90 -0
  163. package/lib/middleware/web-app-manifest.js +53 -0
  164. package/lib/mtls-ca.js +100 -70
  165. package/lib/network-byte-quota.js +308 -0
  166. package/lib/network-heartbeat.js +135 -0
  167. package/lib/network-tls.js +534 -4
  168. package/lib/network.js +103 -0
  169. package/lib/notify.js +114 -43
  170. package/lib/ntp-check.js +192 -51
  171. package/lib/observability.js +145 -47
  172. package/lib/openapi.js +90 -44
  173. package/lib/outbox.js +99 -1
  174. package/lib/pagination.js +168 -86
  175. package/lib/parsers/index.js +16 -5
  176. package/lib/permissions.js +93 -40
  177. package/lib/pqc-agent.js +84 -8
  178. package/lib/pqc-software.js +94 -60
  179. package/lib/process-spawn.js +95 -21
  180. package/lib/pubsub.js +96 -66
  181. package/lib/queue.js +375 -54
  182. package/lib/redact.js +793 -21
  183. package/lib/render.js +139 -47
  184. package/lib/request-helpers.js +485 -121
  185. package/lib/restore-bundle.js +142 -39
  186. package/lib/restore-rollback.js +136 -45
  187. package/lib/retention.js +178 -50
  188. package/lib/retry.js +116 -33
  189. package/lib/router.js +475 -23
  190. package/lib/safe-async.js +543 -94
  191. package/lib/safe-buffer.js +337 -41
  192. package/lib/safe-json.js +467 -62
  193. package/lib/safe-jsonpath.js +285 -0
  194. package/lib/safe-schema.js +631 -87
  195. package/lib/safe-sql.js +221 -59
  196. package/lib/safe-url.js +278 -46
  197. package/lib/sandbox-worker.js +135 -0
  198. package/lib/sandbox.js +358 -0
  199. package/lib/scheduler.js +135 -70
  200. package/lib/self-update.js +647 -0
  201. package/lib/session-device-binding.js +431 -0
  202. package/lib/session.js +259 -49
  203. package/lib/slug.js +138 -26
  204. package/lib/ssrf-guard.js +316 -56
  205. package/lib/storage.js +433 -70
  206. package/lib/subject.js +405 -23
  207. package/lib/template.js +148 -8
  208. package/lib/tenant-quota.js +545 -0
  209. package/lib/testing.js +440 -53
  210. package/lib/time.js +291 -23
  211. package/lib/tls-exporter.js +239 -0
  212. package/lib/tracing.js +90 -74
  213. package/lib/uuid.js +97 -22
  214. package/lib/vault/index.js +284 -22
  215. package/lib/vault/seal-pem-file.js +66 -0
  216. package/lib/watcher.js +368 -0
  217. package/lib/webhook.js +196 -63
  218. package/lib/websocket.js +393 -68
  219. package/lib/wiki-concepts.js +338 -0
  220. package/lib/worker-pool.js +464 -0
  221. package/package.json +3 -3
  222. package/sbom.cyclonedx.json +7 -7
package/lib/safe-json.js CHANGED
@@ -1,63 +1,37 @@
1
1
  "use strict";
2
2
  /**
3
- * Security-focused, fault-tolerant JSON utilities + lightweight schema
4
- * validation with extensible format registry.
5
- *
6
- * Native JSON.parse leaves several footguns to the caller:
7
- * - No size limit — large inputs DoS the parser thread
8
- * - No depth limit — deeply nested input can stack-overflow downstream code
9
- * - __proto__ / constructor / prototype keys land in the result and can be
10
- * turned into prototype pollution by any later object-merge / clone
11
- * - Errors include only a character position, no surrounding context
12
- *
13
- * This module fixes all of the above with conservative defaults and adds a
14
- * lightweight schema validator (a strict subset of JSON Schema) so apps
15
- * can declare what they expect at the trust boundary.
16
- *
17
- * Public API:
18
- *
19
- * json.parse(input, opts?) → value | throws SafeJsonError
20
- * (with opts.collectErrors: { ok, value, errors[] })
21
- * json.parseOrDefault(input, fallback, opts?) value (no throw)
22
- * json.stringify(value, opts?) → string | throws SafeJsonError
23
- * json.canonical(value, opts?) → string (sorted keys)
24
- * json.validate(value, schema, opts?) → value | throws
25
- * (with opts.collectErrors: { ok, value, errors[] })
26
- * json.registerFormat(name, validator) register a custom format
27
- * json.formats → built-in format names
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
- * Schemas are app-developer-supplied (not user-controlled); regex patterns
59
- * are trusted not to be ReDoS-prone. Format validators in the built-in
60
- * registry are anchored / bounded.
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
- _walkAndCheck(value[k], depth + 1, maxDepth, allowProto);
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
- function canonical(value, _opts) {
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
  };