@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.
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/guard-uuid.js CHANGED
@@ -1,38 +1,36 @@
1
1
  "use strict";
2
2
  /**
3
- * guard-uuid — UUID identifier-safety primitive (b.guardUuid).
3
+ * @module b.guardUuid
4
+ * @nav Guards
5
+ * @title Guard Uuid
4
6
  *
5
- * Validates user-supplied UUID strings per RFC 9562 (May 2024,
6
- * obsoletes RFC 4122). KIND="identifier" consumes ctx.identifier
7
- * (or ctx.uuid).
7
+ * @intro
8
+ * UUID identifier-safety guard. Validates user-supplied UUID
9
+ * strings per RFC 9562 (May 2024 — obsoletes RFC 4122) and
10
+ * refuses non-RFC shapes that downstream parsers routinely
11
+ * misinterpret. KIND="identifier" — the gate consumes
12
+ * `ctx.identifier` (or `ctx.uuid`).
8
13
  *
9
- * Threat catalog:
10
- * - Wrong length / shape — UUIDs are 36 chars with hyphens, 32 hex
11
- * without, or 38 with Microsoft GUID braces; anything else is
12
- * malformed and a downstream parser may diverge.
13
- * - Wrong character class non-hex characters anywhere.
14
- * - Invalid version field (RFC 9562 §4.2) — versions 1-8 are
15
- * defined; 0 and 9-F are reserved/unassigned and indicate
16
- * hand-rolled or attacker-shaped IDs.
17
- * - Variant bits (RFC 9562 §4.1) only 10xx (RFC 4122/9562
18
- * variant) is the canonical UUID variant; other variants
19
- * (NCS-reserved 0xxx, Microsoft-reserved 110x, future-reserved
20
- * 111x) often indicate non-UUID payloads coerced into the slot.
21
- * - Nil UUID (RFC 9562 §5.9 — all zeros) — usually represents
22
- * "no UUID set"; passing through can mask a missing-key bug.
23
- * - Max UUID (RFC 9562 §5.10 — all FF) — sentinel value with the
24
- * same semantic risk as nil.
25
- * - urn:uuid: prefix (RFC 4122 §3) — when not requested by the
26
- * caller, can disguise a UUID inside a URN-shape parser.
27
- * - Microsoft GUID braces `{...}` — disguise a UUID inside a
28
- * COM-style serialization parser.
29
- * - BIDI / zero-width / control / null-byte — universal-refuse.
14
+ * Threat catalog: wrong length / shape (canonical 36-char
15
+ * hyphenated, 32-char hyphenless, 38-char braced, or
16
+ * `urn:uuid:` prefixed anything else is malformed); wrong
17
+ * character class (non-hex anywhere); invalid version field
18
+ * (RFC 9562 §4.2 defines 1-8; 0 and 9-F are reserved /
19
+ * unassigned and indicate hand-rolled or attacker-shaped IDs);
20
+ * variant bits (RFC 9562 §4.1 only 10xx is the canonical
21
+ * variant; NCS-reserved 0xxx, Microsoft 110x, future 111x often
22
+ * indicate non-UUID payloads coerced into the slot); nil UUID
23
+ * (§5.9 all zeros usually "no UUID set", masks missing-key
24
+ * bugs when passed through); max UUID (§5.10 all FF sentinel
25
+ * with the same semantic risk as nil); `urn:uuid:` prefix
26
+ * smuggling; Microsoft GUID braces `{...}` smuggling;
27
+ * BIDI / zero-width / C0-control / null-byte universal-refuse.
30
28
  *
31
- * var rv = b.guardUuid.validate("550e8400-e29b-41d4-a716-446655440000",
32
- * { profile: "strict" });
33
- * var safe = b.guardUuid.sanitize("urn:uuid:550E8400-...",
34
- * { profile: "balanced" });
35
- * var g = b.guardUuid.gate({ profile: "strict" });
29
+ * Profiles: `strict` / `balanced` / `permissive`. Compliance
30
+ * postures: `hipaa` / `pci-dss` / `gdpr` / `soc2`.
31
+ *
32
+ * @card
33
+ * UUID identifier-safety guard.
36
34
  */
37
35
 
38
36
  var codepointClass = require("./codepoint-class");
@@ -292,6 +290,46 @@ function _detectIssues(input, opts) {
292
290
  return issues;
293
291
  }
294
292
 
293
+ /**
294
+ * @primitive b.guardUuid.validate
295
+ * @signature b.guardUuid.validate(input, opts?)
296
+ * @since 0.7.44
297
+ * @status stable
298
+ * @compliance hipaa, pci-dss, gdpr, soc2
299
+ * @related b.guardUuid.sanitize, b.guardUuid.gate, b.uuid.v4, b.uuid.v7
300
+ *
301
+ * Inspect a UUID string against the resolved profile and return
302
+ * `{ ok, issues }`. Each issue carries `kind` / `severity`
303
+ * (`critical` | `high` | `medium` | `low`) / `ruleId` / `snippet`.
304
+ * Non-string input returns a single `uuid.bad-input` issue rather
305
+ * than throwing — callers that prefer an exception use
306
+ * `b.guardUuid.sanitize`.
307
+ *
308
+ * @opts
309
+ * profile: "strict"|"balanced"|"permissive",
310
+ * compliance: "hipaa"|"pci-dss"|"gdpr"|"soc2",
311
+ * bidiPolicy: "reject"|"strip"|"audit"|"allow",
312
+ * controlPolicy: "reject"|"strip"|"allow",
313
+ * nullBytePolicy: "reject"|"strip"|"allow",
314
+ * zeroWidthPolicy: "reject"|"strip"|"allow",
315
+ * formatPolicy: "hyphenated"|"hyphenless"|"braced"|"urn"|"hyphenated-only"|"any",
316
+ * versionPolicy: "reject-unassigned"|"audit"|"allow",
317
+ * variantPolicy: "reject-non-rfc"|"audit"|"allow",
318
+ * nilPolicy: "reject"|"audit"|"allow",
319
+ * maxPolicy: "reject"|"audit"|"allow",
320
+ * urnPolicy: "reject"|"audit"|"allow",
321
+ * maxBytes: number,
322
+ *
323
+ * @example
324
+ * var rv = b.guardUuid.validate("550e8400-e29b-41d4-a716-446655440000",
325
+ * { profile: "strict" });
326
+ * rv.ok; // → true
327
+ *
328
+ * var bad = b.guardUuid.validate("00000000-0000-0000-0000-000000000000",
329
+ * { profile: "strict" });
330
+ * bad.ok; // → false
331
+ * bad.issues[0].ruleId; // → "uuid.nil"
332
+ */
295
333
  function validate(input, opts) {
296
334
  opts = _resolveOpts(opts);
297
335
  numericBounds.requireAllPositiveFiniteIntIfPresent(opts,
@@ -308,6 +346,36 @@ function validate(input, opts) {
308
346
  return gateContract.aggregateIssues(_detectIssues(input, opts));
309
347
  }
310
348
 
349
+ /**
350
+ * @primitive b.guardUuid.sanitize
351
+ * @signature b.guardUuid.sanitize(input, opts?)
352
+ * @since 0.7.44
353
+ * @status stable
354
+ * @related b.guardUuid.validate, b.guardUuid.gate
355
+ *
356
+ * Normalize a UUID to canonical hyphenated lowercase form. Strips
357
+ * Microsoft GUID braces `{...}` and the `urn:uuid:` prefix. Throws
358
+ * `GuardUuidError` when any `critical` or `high` issue fires
359
+ * (nil / max sentinel under reject, unassigned version, non-RFC
360
+ * variant). Use `validate` to inspect issues without throwing.
361
+ *
362
+ * @opts
363
+ * profile: "strict"|"balanced"|"permissive",
364
+ * compliance: "hipaa"|"pci-dss"|"gdpr"|"soc2",
365
+ * ...: same shape as b.guardUuid.validate opts,
366
+ *
367
+ * @example
368
+ * var safe = b.guardUuid.sanitize("urn:uuid:550E8400-E29B-41D4-A716-446655440000",
369
+ * { profile: "balanced" });
370
+ * safe; // → "550e8400-e29b-41d4-a716-446655440000"
371
+ *
372
+ * try {
373
+ * b.guardUuid.sanitize("ffffffff-ffff-ffff-ffff-ffffffffffff",
374
+ * { profile: "strict" });
375
+ * } catch (e) {
376
+ * e.code; // → "uuid.max"
377
+ * }
378
+ */
311
379
  function sanitize(input, opts) {
312
380
  opts = _resolveOpts(opts);
313
381
  if (typeof input !== "string") {
@@ -330,6 +398,35 @@ function sanitize(input, opts) {
330
398
  hex.slice(20); // allow:raw-byte-literal — UUID hex slice positions
331
399
  }
332
400
 
401
+ /**
402
+ * @primitive b.guardUuid.gate
403
+ * @signature b.guardUuid.gate(opts?)
404
+ * @since 0.7.44
405
+ * @status stable
406
+ * @compliance hipaa, pci-dss, gdpr, soc2
407
+ * @related b.guardUuid.validate, b.guardUuid.sanitize, b.guardAll.gate
408
+ *
409
+ * Build an async gate `(ctx) -> { ok, action, issues }` consumable
410
+ * by `b.guardAll`, ID validators, and any host that handles
411
+ * UUID-shaped tokens. The gate reads `ctx.identifier` (or
412
+ * `ctx.uuid`), runs `validate`, and maps severity to action: zero
413
+ * issues `serve`; only low/medium `audit-only`; any high/critical
414
+ * `refuse`.
415
+ *
416
+ * @opts
417
+ * name: string, // gate label for audit / observability
418
+ * profile: "strict"|"balanced"|"permissive",
419
+ * compliance: "hipaa"|"pci-dss"|"gdpr"|"soc2",
420
+ * ...: same shape as b.guardUuid.validate opts,
421
+ *
422
+ * @example
423
+ * var g = b.guardUuid.gate({ profile: "strict" });
424
+ * var rv = await g({ identifier: "550e8400-e29b-41d4-a716-446655440000" });
425
+ * rv.action; // → "serve"
426
+ *
427
+ * var bad = await g({ identifier: "{550e8400-e29b-41d4-a716-446655440000}" });
428
+ * bad.action; // → "refuse"
429
+ */
333
430
  function gate(opts) {
334
431
  opts = _resolveOpts(opts);
335
432
  return gateContract.buildGuardGate(
@@ -353,14 +450,76 @@ function gate(opts) {
353
450
  });
354
451
  }
355
452
 
453
+ /**
454
+ * @primitive b.guardUuid.buildProfile
455
+ * @signature b.guardUuid.buildProfile(opts)
456
+ * @since 0.7.44
457
+ * @status stable
458
+ * @related b.guardUuid.gate, b.guardUuid.compliancePosture
459
+ *
460
+ * Compose a derived profile from one or more named bases plus
461
+ * inline overrides. `opts.extends` is a profile name or array of
462
+ * names (later entries shadow earlier ones); inline keys win last.
463
+ *
464
+ * @opts
465
+ * extends: string|string[], // base profile name(s) to compose
466
+ * ...: any guard-uuid key, // inline override of resolved keys
467
+ *
468
+ * @example
469
+ * var custom = b.guardUuid.buildProfile({
470
+ * extends: "balanced",
471
+ * formatPolicy: "hyphenated-only",
472
+ * nilPolicy: "audit",
473
+ * });
474
+ * custom.formatPolicy; // → "hyphenated-only"
475
+ * custom.nilPolicy; // → "audit"
476
+ */
356
477
  var buildProfile = gateContract.makeProfileBuilder(PROFILES);
357
478
 
479
+ /**
480
+ * @primitive b.guardUuid.compliancePosture
481
+ * @signature b.guardUuid.compliancePosture(name)
482
+ * @since 0.7.44
483
+ * @status stable
484
+ * @compliance hipaa, pci-dss, gdpr, soc2
485
+ * @related b.guardUuid.gate, b.guardUuid.buildProfile
486
+ *
487
+ * Look up a compliance-posture overlay by name (`"hipaa"` /
488
+ * `"pci-dss"` / `"gdpr"` / `"soc2"`). Returns a shallow clone of
489
+ * the posture object — the caller may mutate freely. Throws
490
+ * `GuardUuidError("uuid.bad-posture")` on unknown name.
491
+ *
492
+ * @example
493
+ * var posture = b.guardUuid.compliancePosture("hipaa");
494
+ * posture.nilPolicy; // → "reject"
495
+ */
358
496
  function compliancePosture(name) {
359
497
  return gateContract.lookupCompliancePosture(name, COMPLIANCE_POSTURES,
360
498
  _err, "uuid");
361
499
  }
362
500
 
363
501
  var _uuidRulePacks = gateContract.makeRulePackLoader(GuardUuidError, "uuid");
502
+ /**
503
+ * @primitive b.guardUuid.loadRulePack
504
+ * @signature b.guardUuid.loadRulePack(pack)
505
+ * @since 0.7.44
506
+ * @status stable
507
+ * @related b.guardUuid.gate
508
+ *
509
+ * Register an operator-supplied rule pack with the guard-uuid
510
+ * registry. The pack is identified by `pack.id` (non-empty
511
+ * string) and stored for later inspection / dispatch by gates
512
+ * that opt in via `opts.rulePackId`. Throws
513
+ * `GuardUuidError("uuid.bad-opt")` when `pack` is missing or
514
+ * `pack.id` is not a non-empty string.
515
+ *
516
+ * @example
517
+ * var pack = b.guardUuid.loadRulePack({
518
+ * id: "v7-only",
519
+ * allowedVersions: [7],
520
+ * });
521
+ * pack.id; // → "v7-only"
522
+ */
364
523
  var loadRulePack = _uuidRulePacks.load;
365
524
 
366
525
  module.exports = {
package/lib/guard-xml.js CHANGED
@@ -1,45 +1,70 @@
1
1
  "use strict";
2
2
  /**
3
- * guard-xml — XML content-safety primitive (b.guardXml).
3
+ * @module b.guardXml
4
+ * @nav Guards
5
+ * @title Guard Xml
4
6
  *
5
- * Threat catalog grounded in current research (XXE remains active in
6
- * 2025-2026 despite 20+ years of awareness):
7
- * - CVE-2026-24400 AssertJ XXE via toXmlDocument default parser
8
- * - CVE-2025-3225 sitemap parser XXE
9
- * - CVE-2024-1455 LangChain XXE
10
- * - CVE-2024-25062 libxml2 use-after-free with DTD + XInclude
11
- * - CVE-2024-56171 libxml2 schema use-after-free
12
- * - CVE-2025-24928 libxml2 stack overflow on DTD validation
13
- * - CVE-2025-32415 libxml2 schema heap under-read
14
- * - CVE-2025-27113 libxml2 NULL deref in pattern.c
15
- * - CVE-2024-8176 libexpat stack overflow (recursive entity expansion)
7
+ * @intro
8
+ * XML content-safety guard defends against the XXE / billion-
9
+ * laughs / external-entity / XSLT-exec catalog that has remained
10
+ * active for 20+ years and continues to ship CVEs through 2025-
11
+ * 2026. XML attack surface centers on the DOCTYPE subset, where
12
+ * entity declarations and external references convert a benign-
13
+ * looking XML document into a file-disclosure / SSRF / RCE / DoS
14
+ * primitive depending on the parser.
16
15
  *
17
- * var rv = b.guardXml.validate(input, { profile: "strict" });
18
- * var safe = b.guardXml.sanitize(input, { profile: "balanced" });
19
- * var g = b.guardXml.gate({ profile: "strict" });
16
+ * XXE / external entity (XML External Entity) defense:
17
+ * `<!ENTITY xxe SYSTEM "file:///etc/passwd">` and `SYSTEM` /
18
+ * `PUBLIC` identifiers pointing at `file://` / `http://` /
19
+ * `https://` / `ftp://` / `gopher://` / `jar://` / `netdoc://`
20
+ * are refused regardless of profile. CVE-2026-24400 AssertJ
21
+ * `toXmlDocument` default parser, CVE-2025-3225 sitemap parser,
22
+ * CVE-2024-1455 LangChain XXE, and CVE-2024-25062 libxml2 UAF
23
+ * with DTD + XInclude all fit this shape.
20
24
  *
21
- * Threat catalog covered:
25
+ * Billion-laughs / entity-expansion DoS: `<!ENTITY lol "lol">` +
26
+ * `<!ENTITY lol2 "&lol;&lol;...">` recursive declarations expand
27
+ * exponentially when the parser dereferences. Refused via the
28
+ * blanket `<!ENTITY>` rule; parameter entities (`<!ENTITY %>`
29
+ * prefix) get an additional out-of-band exfil tag. CVE-2024-8176
30
+ * libexpat stack overflow on recursive entity expansion +
31
+ * CVE-2025-24928 libxml2 stack overflow on DTD validation track
32
+ * the family.
22
33
  *
23
- * 1. DOCTYPE declarations refuse unconditionally regardless of
24
- * profile. Catches billion-laughs entity expansion + external
25
- * entity loading (XXE) + SYSTEM identifier exfil.
26
- * 2. <!ENTITY> declarations including parameter entities (% prefix).
27
- * 3. External entities — file:// / http:// / SYSTEM identifiers in
28
- * DOCTYPE subset.
29
- * 4. XInclude — <xi:include href="..."/> remote inclusion.
30
- * 5. xsi:schemaLocation / xsi:noNamespaceSchemaLocation — operator-
31
- * controlled schema fetch.
32
- * 6. Processing instructions <?xml-stylesheet ...?> CSS injection
33
- * vector.
34
- * 7. CDATA sections — often used to hide payloads from naive
35
- * scanners.
36
- * 8. XML signature wrapping (xmldsig) — surface that requires
37
- * careful operator handling; flagged as audit.
38
- * 9. Bidi / null / control / zero-width chars in element text +
39
- * attribute values.
40
- * 10. Anti-DoS caps — total document size, max element count, max
41
- * attribute count per element, max depth, max attribute value
42
- * length.
34
+ * DTD external-entity refusal: every `<!DOCTYPE>` declaration is
35
+ * refused unconditionally there is no safe DTD subset that
36
+ * defenders can enumerate against the parser-quirk landscape, so
37
+ * the only stable posture is to reject the surface entirely.
38
+ *
39
+ * XSLT / processing-instruction exec defense: `<?xml-stylesheet
40
+ * href="...">` and other `<?PI ?>` shapes can route the document
41
+ * through an XSLT processor with `document()` / `xsl:include` /
42
+ * `xsl:import` full file-disclosure + SSRF surface. Flagged
43
+ * under balanced; refused under strict (after the standard
44
+ * `<?xml ... ?>` declaration is stripped).
45
+ *
46
+ * XInclude (`<xi:include href="...">`) and `xsi:schemaLocation` /
47
+ * `xsi:noNamespaceSchemaLocation` are operator-controlled fetch
48
+ * surfaces; XML signature elements (`xmldsig`) require operator
49
+ * defense against signature-wrapping attacks. CDATA sections
50
+ * often hide payloads from naive scanners.
51
+ *
52
+ * Anti-DoS caps: total document size (`maxBytes`), nesting depth
53
+ * (`maxDepth`), element count (`maxElements`), attribute count per
54
+ * element (`maxAttrsPerElement`), and attribute value length
55
+ * (`maxAttrValueBytes`).
56
+ *
57
+ * Bidi / null / control / zero-width character threats route
58
+ * through the shared lib/codepoint-class detector.
59
+ *
60
+ * Profiles: `strict` / `balanced` / `permissive`. Compliance
61
+ * postures: `hipaa` / `pci-dss` / `gdpr` / `soc2`. Even under
62
+ * `permissive`, DOCTYPE / ENTITY / external-entity refusal stays
63
+ * on — the billion-laughs and XXE classes have no safe permissive
64
+ * posture.
65
+ *
66
+ * @card
67
+ * XML content-safety guard — defends against the XXE / billion- laughs / external-entity / XSLT-exec catalog that has remained active for 20+ years and continues to ship CVEs through 2025- 2026.
43
68
  */
44
69
 
45
70
  var codepointClass = require("./codepoint-class");
@@ -299,6 +324,59 @@ function _detectIssues(input, opts) {
299
324
 
300
325
  // ---- Public surface ----
301
326
 
327
+ /**
328
+ * @primitive b.guardXml.validate
329
+ * @signature b.guardXml.validate(input, opts?)
330
+ * @since 0.7.15
331
+ * @status stable
332
+ * @compliance hipaa, pci-dss, gdpr, soc2
333
+ * @related b.guardXml.sanitize, b.guardXml.gate
334
+ *
335
+ * Inspect `input` (string of XML source) for the full guard-xml
336
+ * threat catalog without invoking a parser. Returns
337
+ * `{ ok, issues, severities }` where `issues` enumerates every
338
+ * DOCTYPE declaration, `<!ENTITY>` definition (including parameter
339
+ * entities), SYSTEM/PUBLIC external-entity reference, XInclude
340
+ * directive, xsi:schemaLocation hint, processing instruction (after
341
+ * the standard `<?xml ?>` declaration), CDATA section, XML signature
342
+ * element, and codepoint-class threat. Element / depth caps are
343
+ * estimated via tag-count + nesting heuristics — strict-mode rejects
344
+ * exceeding the configured caps without requiring a full parse.
345
+ *
346
+ * Profile-driven (`strict` / `balanced` / `permissive`) and posture-
347
+ * driven (`hipaa` / `pci-dss` / `gdpr` / `soc2`). Note that
348
+ * DOCTYPE / `<!ENTITY>` / external-entity refusal stays on under
349
+ * every profile — there is no safe permissive posture for the XXE
350
+ * + billion-laughs class.
351
+ *
352
+ * @opts
353
+ * profile: "strict"|"balanced"|"permissive",
354
+ * compliance: "hipaa"|"pci-dss"|"gdpr"|"soc2",
355
+ * doctypePolicy: "reject"|"audit"|"allow",
356
+ * entityPolicy: "reject"|"audit"|"allow",
357
+ * externalEntityPolicy: "reject"|"audit"|"allow",
358
+ * xincludePolicy: "reject"|"audit"|"allow",
359
+ * schemaLocationPolicy: "reject"|"audit"|"allow",
360
+ * processingInstrPolicy: "reject"|"audit"|"allow",
361
+ * cdataPolicy: "reject"|"audit"|"allow",
362
+ * xmlDsigPolicy: "audit"|"allow",
363
+ * bidiPolicy: "reject"|"strip"|"audit"|"allow",
364
+ * controlPolicy: "reject"|"strip"|"allow",
365
+ * nullBytePolicy: "reject"|"strip"|"allow",
366
+ * zeroWidthPolicy: "reject"|"strip"|"audit"|"allow",
367
+ * maxBytes: number, // total source byte cap
368
+ * maxDepth: number, // estimated nesting depth cap
369
+ * maxElements: number, // total open-tag count cap
370
+ * maxAttrsPerElement: number, // attribute count cap per element
371
+ * maxAttrValueBytes: number, // per-attr-value length cap
372
+ *
373
+ * @example
374
+ * var hostile = '<?xml version="1.0"?>\n' +
375
+ * '<!DOCTYPE r [<!ENTITY xx "yy">]>\n<r/>';
376
+ * var rv = b.guardXml.validate(hostile, { profile: "strict" });
377
+ * rv.ok; // → false
378
+ * rv.issues.some(function (i) { return i.kind === "doctype"; }); // → true
379
+ */
302
380
  function validate(input, opts) {
303
381
  opts = _resolveOpts(opts);
304
382
  numericBounds.requireAllPositiveFiniteIntIfPresent(opts,
@@ -315,6 +393,44 @@ function validate(input, opts) {
315
393
  return gateContract.aggregateIssues(_detectIssues(input, opts));
316
394
  }
317
395
 
396
+ /**
397
+ * @primitive b.guardXml.sanitize
398
+ * @signature b.guardXml.sanitize(input, opts?)
399
+ * @since 0.7.15
400
+ * @status stable
401
+ * @related b.guardXml.validate, b.guardXml.gate
402
+ *
403
+ * Best-effort cleanup of `input` (string of XML source): strips
404
+ * codepoint-class threats per policy (BOM, bidi when
405
+ * `bidiPolicy: "strip"`, C0 controls when `controlPolicy: "strip"`,
406
+ * null bytes when `nullBytePolicy: "strip"`, zero-width characters
407
+ * when `zeroWidthPolicy: "strip"`). Throws `GuardXmlError` on any
408
+ * critical issue — DOCTYPE / `<!ENTITY>` / external-entity / param-
409
+ * entity shapes have no safe sanitization (the only correct response
410
+ * is refusal). The error code matches the triggering rule
411
+ * (`xml.doctype`, `xml.entity`, `xml.external-entity`, etc.).
412
+ *
413
+ * Sanitize is intentionally narrow: it cleans the character-class
414
+ * surface but never rewrites structural XML. Use `b.guardXml.gate`
415
+ * for the full sanitize-or-refuse action chain inside a request
416
+ * pipeline.
417
+ *
418
+ * @opts
419
+ * profile: "strict"|"balanced"|"permissive",
420
+ * compliance: "hipaa"|"pci-dss"|"gdpr"|"soc2",
421
+ * bidiPolicy: "reject"|"strip"|"audit"|"allow",
422
+ * controlPolicy: "reject"|"strip"|"allow",
423
+ * nullBytePolicy: "reject"|"strip"|"allow",
424
+ * zeroWidthPolicy: "reject"|"strip"|"audit"|"allow",
425
+ *
426
+ * @example
427
+ * // Build hostile input programmatically so the source stays ASCII.
428
+ * var ZWSP = String.fromCharCode(0x200B);
429
+ * var clean = b.guardXml.sanitize("<root>hello" + ZWSP + "</root>", {
430
+ * profile: "balanced",
431
+ * });
432
+ * clean.indexOf(ZWSP) === -1; // → true
433
+ */
318
434
  function sanitize(input, opts) {
319
435
  opts = _resolveOpts(opts);
320
436
  if (typeof input !== "string") {
@@ -334,6 +450,43 @@ function sanitize(input, opts) {
334
450
  return codepointClass.applyCharStripPolicies(input, opts);
335
451
  }
336
452
 
453
+ /**
454
+ * @primitive b.guardXml.gate
455
+ * @signature b.guardXml.gate(opts?)
456
+ * @since 0.7.15
457
+ * @status stable
458
+ * @compliance hipaa, pci-dss, gdpr, soc2
459
+ * @related b.guardXml.validate, b.guardXml.sanitize, b.staticServe.create, b.fileUpload.create
460
+ *
461
+ * Build a `b.gateContract` gate suitable for plugging into
462
+ * `b.staticServe({ contentSafety: { ".xml": gate } })`,
463
+ * `b.fileUpload({ contentSafety: { "application/xml": gate } })`,
464
+ * or any host primitive that consumes the gate-contract shape.
465
+ * Action chain on validation: `serve` (no issues) → `audit-only`
466
+ * (warn-only issues) → `sanitize` (high/critical when DOCTYPE /
467
+ * ENTITY / external-entity policies are not `reject`, which strips
468
+ * codepoint-class threats only) → `refuse` (any of those structural
469
+ * policies is reject and a critical issue fired, or sanitize threw).
470
+ *
471
+ * Under strict and balanced both, DOCTYPE / ENTITY / external-entity
472
+ * are reject — so the gate jumps from `audit-only` straight to
473
+ * `refuse` for the XXE / billion-laughs class. Permissive allows
474
+ * downgrading XInclude / schemaLocation / PI / CDATA to `audit`,
475
+ * but never DOCTYPE / ENTITY / external-entity.
476
+ *
477
+ * @opts
478
+ * profile: "strict"|"balanced"|"permissive",
479
+ * compliance: "hipaa"|"pci-dss"|"gdpr"|"soc2",
480
+ * name: string, // gate identity for audit / observability
481
+ *
482
+ * @example
483
+ * var xmlGate = b.guardXml.gate({ profile: "strict" });
484
+ * var hostile = Buffer.from(
485
+ * '<?xml version="1.0"?>\n<!DOCTYPE r [<!ENTITY a "b">]>\n<r/>',
486
+ * "utf8");
487
+ * var verdict = await xmlGate.check({ bytes: hostile });
488
+ * verdict.action; // → "refuse"
489
+ */
337
490
  function gate(opts) {
338
491
  opts = _resolveOpts(opts);
339
492
  return gateContract.buildGuardGate(
@@ -365,13 +518,83 @@ function gate(opts) {
365
518
  });
366
519
  }
367
520
 
521
+ /**
522
+ * @primitive b.guardXml.buildProfile
523
+ * @signature b.guardXml.buildProfile(opts)
524
+ * @since 0.7.15
525
+ * @status stable
526
+ * @related b.guardXml.gate, b.guardXml.compliancePosture
527
+ *
528
+ * Compose a derived profile from one or more named bases plus
529
+ * inline overrides. `opts.extends` is a profile name (`"strict"` /
530
+ * `"balanced"` / `"permissive"`) or an array of names; later entries
531
+ * shadow earlier ones. Inline `opts` keys win last. Used to keep
532
+ * operator-defined profiles traceable to a baseline rather than re-
533
+ * typing every key.
534
+ *
535
+ * @opts
536
+ * extends: string|string[], // base profile name(s) to compose
537
+ * ...: any guard-xml key, // inline override of resolved keys
538
+ *
539
+ * @example
540
+ * var custom = b.guardXml.buildProfile({
541
+ * extends: "balanced",
542
+ * cdataPolicy: "reject",
543
+ * maxElements: 4096,
544
+ * });
545
+ * custom.cdataPolicy; // → "reject"
546
+ * custom.maxElements; // → 4096
547
+ */
368
548
  var buildProfile = gateContract.makeProfileBuilder(PROFILES);
369
549
 
550
+ /**
551
+ * @primitive b.guardXml.compliancePosture
552
+ * @signature b.guardXml.compliancePosture(name)
553
+ * @since 0.7.15
554
+ * @status stable
555
+ * @compliance hipaa, pci-dss, gdpr, soc2
556
+ * @related b.guardXml.gate, b.guardXml.buildProfile
557
+ *
558
+ * Look up a compliance-posture overlay by name (`"hipaa"` /
559
+ * `"pci-dss"` / `"gdpr"` / `"soc2"`). Returns a shallow clone of the
560
+ * posture object — the caller may mutate freely. Throws
561
+ * `GuardXmlError("xml.bad-posture")` on unknown name.
562
+ *
563
+ * @example
564
+ * var posture = b.guardXml.compliancePosture("hipaa");
565
+ * posture.doctypePolicy; // → "reject"
566
+ * posture.forensicSnippetBytes; // → 256
567
+ */
370
568
  function compliancePosture(name) {
371
569
  return gateContract.lookupCompliancePosture(name, COMPLIANCE_POSTURES, _err, "xml");
372
570
  }
373
571
 
374
572
  var _xmlRulePacks = gateContract.makeRulePackLoader(GuardXmlError, "xml");
573
+ /**
574
+ * @primitive b.guardXml.loadRulePack
575
+ * @signature b.guardXml.loadRulePack(pack)
576
+ * @since 0.7.15
577
+ * @status stable
578
+ * @related b.guardXml.gate
579
+ *
580
+ * Register an operator-supplied rule pack with the guard-xml
581
+ * registry. The pack is identified by `pack.id` (non-empty string)
582
+ * and stored for later inspection / dispatch by gates that opt in
583
+ * via `opts.rulePackId`. Returns the pack object unchanged on
584
+ * success; throws `GuardXmlError("xml.bad-opt")` when `pack` is
585
+ * missing or `pack.id` is not a non-empty string.
586
+ *
587
+ * @example
588
+ * var pack = b.guardXml.loadRulePack({
589
+ * id: "soap-envelope",
590
+ * rules: [
591
+ * { id: "must-have-envelope", severity: "high",
592
+ * detect: function (text) { return text.indexOf("<soap:Envelope") === -1; },
593
+ * reason: "SOAP request missing soap:Envelope root" },
594
+ * ],
595
+ * });
596
+ * pack.id; // → "soap-envelope"
597
+ */
375
598
  var loadRulePack = _xmlRulePacks.load;
376
599
 
377
600
  module.exports = {