@blamejs/core 0.15.12 → 0.15.14

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 (365) hide show
  1. package/CHANGELOG.md +4 -0
  2. package/index.js +2 -0
  3. package/lib/a2a-tasks.js +45 -29
  4. package/lib/acme.js +6 -5
  5. package/lib/agent-event-bus.js +18 -4
  6. package/lib/agent-idempotency.js +7 -6
  7. package/lib/agent-orchestrator.js +2 -5
  8. package/lib/agent-saga.js +3 -5
  9. package/lib/agent-snapshot.js +32 -2
  10. package/lib/agent-tenant.js +2 -5
  11. package/lib/ai-adverse-decision.js +2 -15
  12. package/lib/ai-aedt-bias-audit.js +2 -1
  13. package/lib/ai-capability.js +1 -6
  14. package/lib/ai-content-detect.js +1 -3
  15. package/lib/ai-dp.js +1 -5
  16. package/lib/ai-frontier-protocol.js +1 -1
  17. package/lib/ai-input.js +2 -2
  18. package/lib/ai-model-manifest.js +1 -1
  19. package/lib/ai-output.js +16 -7
  20. package/lib/ai-pref.js +3 -8
  21. package/lib/ai-quota.js +3 -14
  22. package/lib/api-key.js +37 -28
  23. package/lib/api-snapshot.js +4 -1
  24. package/lib/app-shutdown.js +7 -1
  25. package/lib/archive-adapters.js +2 -4
  26. package/lib/archive-entry-policy.js +32 -0
  27. package/lib/archive-gz.js +9 -0
  28. package/lib/archive-read.js +14 -24
  29. package/lib/archive-tar-read.js +56 -24
  30. package/lib/archive.js +6 -12
  31. package/lib/arg-parser.js +7 -6
  32. package/lib/asn1-der.js +70 -22
  33. package/lib/asyncapi-traits.js +2 -6
  34. package/lib/atomic-file.js +312 -33
  35. package/lib/audit-chain.js +183 -54
  36. package/lib/audit-daily-review.js +36 -16
  37. package/lib/audit-emit.js +82 -0
  38. package/lib/audit-sign.js +258 -0
  39. package/lib/audit-tools.js +108 -23
  40. package/lib/audit.js +91 -2
  41. package/lib/auth/access-lock.js +7 -29
  42. package/lib/auth/bot-challenge.js +1 -1
  43. package/lib/auth/ciba.js +43 -4
  44. package/lib/auth/dpop.js +29 -36
  45. package/lib/auth/fido-mds3.js +14 -16
  46. package/lib/auth/jwt-external.js +26 -8
  47. package/lib/auth/jwt.js +15 -17
  48. package/lib/auth/lockout.js +24 -27
  49. package/lib/auth/oauth.js +67 -45
  50. package/lib/auth/oid4vci.js +55 -32
  51. package/lib/auth/oid4vp.js +3 -2
  52. package/lib/auth/openid-federation.js +6 -6
  53. package/lib/auth/passkey.js +4 -4
  54. package/lib/auth/password.js +8 -2
  55. package/lib/auth/saml.js +70 -40
  56. package/lib/auth/sd-jwt-vc-holder.js +2 -14
  57. package/lib/auth/sd-jwt-vc-issuer.js +2 -14
  58. package/lib/auth/sd-jwt-vc.js +25 -5
  59. package/lib/auth/status-list.js +21 -9
  60. package/lib/auth/step-up.js +15 -13
  61. package/lib/auth-bot-challenge.js +27 -39
  62. package/lib/backup/bundle.js +18 -20
  63. package/lib/backup/crypto.js +23 -8
  64. package/lib/backup/index.js +55 -67
  65. package/lib/backup/manifest.js +7 -1
  66. package/lib/bounded-map.js +112 -1
  67. package/lib/breach-deadline.js +1 -11
  68. package/lib/break-glass.js +41 -22
  69. package/lib/cache.js +7 -18
  70. package/lib/cbor.js +36 -12
  71. package/lib/cdn-cache-control.js +15 -12
  72. package/lib/cert.js +8 -13
  73. package/lib/chain-writer.js +162 -47
  74. package/lib/cli.js +10 -2
  75. package/lib/client-hints.js +7 -9
  76. package/lib/cloud-events.js +43 -33
  77. package/lib/cluster-storage.js +9 -41
  78. package/lib/cms-codec.js +2 -1
  79. package/lib/codepoint-class.js +84 -1
  80. package/lib/compliance-ai-act-logging.js +1 -6
  81. package/lib/compliance-ai-act-transparency.js +2 -4
  82. package/lib/compliance-eaa.js +2 -12
  83. package/lib/compliance-sanctions-fetcher.js +21 -28
  84. package/lib/compliance-sanctions.js +11 -21
  85. package/lib/compliance.js +3 -10
  86. package/lib/config-drift.js +24 -20
  87. package/lib/content-credentials.js +11 -8
  88. package/lib/content-digest.js +21 -12
  89. package/lib/cookies.js +15 -13
  90. package/lib/cose.js +39 -11
  91. package/lib/cra-report.js +1 -11
  92. package/lib/crdt.js +2 -1
  93. package/lib/crypto-field.js +34 -33
  94. package/lib/crypto.js +138 -7
  95. package/lib/csp.js +239 -3
  96. package/lib/daemon.js +46 -42
  97. package/lib/data-act.js +46 -13
  98. package/lib/db-file-lifecycle.js +14 -6
  99. package/lib/db-query.js +75 -49
  100. package/lib/db.js +66 -27
  101. package/lib/dbsc.js +5 -6
  102. package/lib/ddl-change-control.js +18 -14
  103. package/lib/deprecate.js +4 -5
  104. package/lib/did.js +6 -2
  105. package/lib/dora.js +43 -13
  106. package/lib/dr-runbook.js +1 -1
  107. package/lib/dsa.js +2 -1
  108. package/lib/dsr.js +30 -20
  109. package/lib/early-hints.js +19 -0
  110. package/lib/eat.js +5 -1
  111. package/lib/external-db-migrate.js +21 -22
  112. package/lib/external-db.js +74 -30
  113. package/lib/fda-21cfr11.js +42 -13
  114. package/lib/fdx.js +22 -18
  115. package/lib/file-upload.js +80 -66
  116. package/lib/flag-evaluation-context.js +5 -8
  117. package/lib/flag-providers.js +6 -2
  118. package/lib/forms.js +1 -1
  119. package/lib/framework-error.js +12 -0
  120. package/lib/framework-schema.js +19 -0
  121. package/lib/fsm.js +7 -12
  122. package/lib/gate-contract.js +911 -39
  123. package/lib/gdpr-ropa.js +22 -22
  124. package/lib/graphql-federation.js +18 -5
  125. package/lib/guard-agent-registry.js +2 -1
  126. package/lib/guard-all.js +3 -2
  127. package/lib/guard-archive.js +9 -30
  128. package/lib/guard-auth.js +23 -80
  129. package/lib/guard-cidr.js +20 -96
  130. package/lib/guard-csv.js +135 -196
  131. package/lib/guard-domain.js +23 -106
  132. package/lib/guard-dsn.js +17 -14
  133. package/lib/guard-email.js +46 -53
  134. package/lib/guard-envelope.js +2 -2
  135. package/lib/guard-event-bus-topic.js +2 -1
  136. package/lib/guard-filename.js +12 -60
  137. package/lib/guard-graphql.js +28 -75
  138. package/lib/guard-html-wcag.js +15 -2
  139. package/lib/guard-html.js +74 -128
  140. package/lib/guard-idempotency-key.js +2 -1
  141. package/lib/guard-image.js +280 -77
  142. package/lib/guard-imap-command.js +9 -10
  143. package/lib/guard-jmap.js +1 -1
  144. package/lib/guard-json.js +101 -109
  145. package/lib/guard-jsonpath.js +20 -88
  146. package/lib/guard-jwt.js +32 -114
  147. package/lib/guard-list-id.js +2 -7
  148. package/lib/guard-list-unsubscribe.js +2 -7
  149. package/lib/guard-mail-compose.js +5 -6
  150. package/lib/guard-mail-move.js +3 -2
  151. package/lib/guard-mail-query.js +5 -3
  152. package/lib/guard-mail-sieve.js +6 -7
  153. package/lib/guard-managesieve-command.js +6 -5
  154. package/lib/guard-markdown.js +76 -110
  155. package/lib/guard-message-id.js +5 -6
  156. package/lib/guard-mime.js +20 -99
  157. package/lib/guard-oauth.js +19 -73
  158. package/lib/guard-pdf.js +65 -72
  159. package/lib/guard-pop3-command.js +13 -14
  160. package/lib/guard-posture-chain.js +2 -1
  161. package/lib/guard-regex.js +24 -99
  162. package/lib/guard-saga-config.js +2 -1
  163. package/lib/guard-shell.js +22 -87
  164. package/lib/guard-smtp-command.js +9 -12
  165. package/lib/guard-sql.js +15 -13
  166. package/lib/guard-stream-args.js +2 -1
  167. package/lib/guard-svg.js +103 -149
  168. package/lib/guard-template.js +23 -80
  169. package/lib/guard-tenant-id.js +2 -1
  170. package/lib/guard-text.js +592 -0
  171. package/lib/guard-time.js +27 -95
  172. package/lib/guard-uuid.js +21 -93
  173. package/lib/guard-xml.js +76 -106
  174. package/lib/guard-yaml.js +24 -60
  175. package/lib/html-balance.js +7 -3
  176. package/lib/http-client-cache.js +5 -12
  177. package/lib/http-client-cookie-jar.js +35 -16
  178. package/lib/http-client.js +233 -74
  179. package/lib/http-message-signature.js +3 -2
  180. package/lib/i18n-messageformat.js +5 -7
  181. package/lib/i18n.js +83 -26
  182. package/lib/iab-tcf.js +3 -2
  183. package/lib/importmap-integrity.js +41 -1
  184. package/lib/inbox.js +8 -8
  185. package/lib/incident-report.js +12 -27
  186. package/lib/ip-utils.js +49 -6
  187. package/lib/jobs.js +3 -2
  188. package/lib/json-patch.js +1 -1
  189. package/lib/json-path.js +24 -3
  190. package/lib/jtd.js +2 -2
  191. package/lib/keychain.js +6 -18
  192. package/lib/legal-hold.js +30 -23
  193. package/lib/log-stream-cloudwatch.js +44 -112
  194. package/lib/log-stream-otlp-grpc.js +17 -14
  195. package/lib/log-stream-otlp.js +16 -80
  196. package/lib/log-stream-webhook.js +20 -92
  197. package/lib/log.js +2 -2
  198. package/lib/mail-agent.js +2 -2
  199. package/lib/mail-arc-sign.js +8 -3
  200. package/lib/mail-arf.js +4 -3
  201. package/lib/mail-auth.js +43 -69
  202. package/lib/mail-bimi.js +35 -55
  203. package/lib/mail-bounce.js +3 -3
  204. package/lib/mail-crypto-pgp.js +3 -2
  205. package/lib/mail-crypto-smime.js +71 -6
  206. package/lib/mail-dav.js +8 -35
  207. package/lib/mail-deploy.js +15 -14
  208. package/lib/mail-dkim.js +19 -38
  209. package/lib/mail-greylist.js +17 -30
  210. package/lib/mail-helo.js +4 -7
  211. package/lib/mail-journal.js +13 -9
  212. package/lib/mail-mdn.js +12 -7
  213. package/lib/mail-rbl.js +7 -12
  214. package/lib/mail-scan.js +5 -7
  215. package/lib/mail-send-deliver.js +33 -20
  216. package/lib/mail-server-imap.js +32 -87
  217. package/lib/mail-server-jmap.js +39 -52
  218. package/lib/mail-server-managesieve.js +29 -83
  219. package/lib/mail-server-mx.js +33 -74
  220. package/lib/mail-server-net.js +177 -0
  221. package/lib/mail-server-pop3.js +30 -83
  222. package/lib/mail-server-rate-limit.js +30 -6
  223. package/lib/mail-server-registry.js +1 -1
  224. package/lib/mail-server-submission.js +34 -73
  225. package/lib/mail-server-tls.js +98 -2
  226. package/lib/mail-spam-score.js +9 -12
  227. package/lib/mail-store-fts.js +1 -1
  228. package/lib/mail-store.js +3 -2
  229. package/lib/mail.js +28 -13
  230. package/lib/markup-escape.js +31 -0
  231. package/lib/markup-tokenizer.js +78 -0
  232. package/lib/mcp-tool-registry.js +26 -23
  233. package/lib/mcp.js +7 -5
  234. package/lib/mdoc.js +36 -14
  235. package/lib/metrics.js +46 -33
  236. package/lib/middleware/age-gate.js +3 -23
  237. package/lib/middleware/api-encrypt.js +13 -24
  238. package/lib/middleware/assetlinks.js +2 -7
  239. package/lib/middleware/bearer-auth.js +2 -1
  240. package/lib/middleware/body-parser.js +41 -52
  241. package/lib/middleware/bot-disclose.js +7 -10
  242. package/lib/middleware/bot-guard.js +28 -28
  243. package/lib/middleware/clear-site-data.js +5 -1
  244. package/lib/middleware/compression.js +9 -0
  245. package/lib/middleware/cors.js +32 -23
  246. package/lib/middleware/csrf-protect.js +73 -23
  247. package/lib/middleware/daily-byte-quota.js +12 -30
  248. package/lib/middleware/deny-response.js +19 -1
  249. package/lib/middleware/fetch-metadata.js +35 -4
  250. package/lib/middleware/idempotency-key.js +4 -20
  251. package/lib/middleware/network-allowlist.js +61 -30
  252. package/lib/middleware/rate-limit.js +45 -44
  253. package/lib/middleware/scim-server.js +5 -3
  254. package/lib/middleware/security-headers.js +33 -7
  255. package/lib/middleware/security-txt.js +2 -6
  256. package/lib/middleware/speculation-rules.js +6 -3
  257. package/lib/middleware/tus-upload.js +14 -29
  258. package/lib/middleware/web-app-manifest.js +2 -7
  259. package/lib/migrations.js +4 -13
  260. package/lib/mime-parse.js +34 -17
  261. package/lib/module-loader.js +44 -0
  262. package/lib/money.js +105 -0
  263. package/lib/mtls-ca.js +125 -41
  264. package/lib/network-byte-quota.js +4 -18
  265. package/lib/network-dane.js +8 -6
  266. package/lib/network-dns-resolver.js +98 -7
  267. package/lib/network-dns.js +9 -2
  268. package/lib/network-dnssec.js +18 -17
  269. package/lib/network-heartbeat.js +3 -2
  270. package/lib/network-smtp-policy.js +52 -46
  271. package/lib/network-tls.js +67 -47
  272. package/lib/network-tsig.js +2 -2
  273. package/lib/nis2-report.js +2 -12
  274. package/lib/nist-crosswalk.js +1 -1
  275. package/lib/nonce-store.js +81 -3
  276. package/lib/ntp-check.js +28 -0
  277. package/lib/numeric-bounds.js +31 -8
  278. package/lib/object-store/azure-blob-bucket-ops.js +2 -6
  279. package/lib/object-store/azure-blob.js +6 -47
  280. package/lib/object-store/gcs-bucket-ops.js +4 -2
  281. package/lib/object-store/gcs.js +14 -75
  282. package/lib/object-store/http-put.js +2 -7
  283. package/lib/object-store/http-request.js +157 -0
  284. package/lib/object-store/local.js +37 -17
  285. package/lib/object-store/sigv4-bucket-ops.js +2 -1
  286. package/lib/object-store/sigv4.js +10 -79
  287. package/lib/observability-otlp-exporter.js +29 -29
  288. package/lib/observability.js +95 -0
  289. package/lib/openapi-paths-builder.js +7 -3
  290. package/lib/otel-export.js +7 -10
  291. package/lib/outbox.js +54 -41
  292. package/lib/pagination.js +11 -15
  293. package/lib/parsers/safe-env.js +5 -4
  294. package/lib/parsers/safe-ini.js +10 -4
  295. package/lib/parsers/safe-toml.js +11 -9
  296. package/lib/parsers/safe-xml.js +3 -3
  297. package/lib/parsers/safe-yaml.js +28 -9
  298. package/lib/pick.js +63 -5
  299. package/lib/pipl-cn.js +69 -59
  300. package/lib/privacy-pass.js +8 -6
  301. package/lib/problem-details.js +2 -5
  302. package/lib/pubsub.js +4 -8
  303. package/lib/queue-local.js +10 -3
  304. package/lib/redact.js +9 -4
  305. package/lib/render.js +4 -2
  306. package/lib/request-helpers.js +334 -29
  307. package/lib/resource-access-lock.js +3 -3
  308. package/lib/restore-bundle.js +46 -18
  309. package/lib/restore-rollback.js +10 -4
  310. package/lib/restore.js +24 -22
  311. package/lib/retention.js +23 -9
  312. package/lib/router.js +17 -4
  313. package/lib/safe-async.js +457 -0
  314. package/lib/safe-buffer.js +137 -6
  315. package/lib/safe-decompress.js +2 -1
  316. package/lib/safe-dns.js +2 -1
  317. package/lib/safe-ical.js +14 -28
  318. package/lib/safe-icap.js +1 -1
  319. package/lib/safe-json.js +51 -8
  320. package/lib/safe-jsonpath.js +6 -5
  321. package/lib/safe-mime.js +25 -29
  322. package/lib/safe-redirect.js +2 -5
  323. package/lib/safe-schema.js +7 -6
  324. package/lib/safe-sieve.js +1 -1
  325. package/lib/safe-sql.js +126 -0
  326. package/lib/safe-vcard.js +13 -27
  327. package/lib/sandbox-worker.js +15 -2
  328. package/lib/sandbox.js +4 -3
  329. package/lib/scheduler.js +22 -13
  330. package/lib/sec-cyber.js +82 -37
  331. package/lib/seeders.js +4 -11
  332. package/lib/self-update-standalone-verifier.js +16 -0
  333. package/lib/self-update.js +92 -69
  334. package/lib/session-device-binding.js +112 -66
  335. package/lib/session.js +219 -7
  336. package/lib/sql.js +228 -81
  337. package/lib/sse.js +6 -10
  338. package/lib/static.js +201 -111
  339. package/lib/storage.js +4 -2
  340. package/lib/structured-fields.js +275 -16
  341. package/lib/template.js +7 -5
  342. package/lib/tenant-quota.js +53 -38
  343. package/lib/tsa.js +16 -17
  344. package/lib/validate-opts.js +154 -8
  345. package/lib/vault/index.js +5 -0
  346. package/lib/vault/passphrase-ops.js +22 -26
  347. package/lib/vault/passphrase-source.js +8 -3
  348. package/lib/vault/rotate.js +13 -18
  349. package/lib/vault/seal-pem-file.js +34 -49
  350. package/lib/vault-aad.js +3 -2
  351. package/lib/vc.js +3 -8
  352. package/lib/vendor/MANIFEST.json +10 -10
  353. package/lib/vendor/public-suffix-list.dat +23 -10
  354. package/lib/vendor/public-suffix-list.data.js +5498 -5494
  355. package/lib/vex.js +35 -13
  356. package/lib/web-push-vapid.js +23 -20
  357. package/lib/webhook-dispatcher.js +706 -0
  358. package/lib/webhook.js +59 -19
  359. package/lib/websocket-channels.js +9 -7
  360. package/lib/websocket.js +8 -4
  361. package/lib/worm.js +3 -4
  362. package/lib/ws-client.js +65 -56
  363. package/lib/x509-chain.js +44 -0
  364. package/package.json +1 -1
  365. package/sbom.cdx.json +6 -6
@@ -50,6 +50,8 @@ var bCrypto = require("./crypto");
50
50
  var lazyRequire = require("./lazy-require");
51
51
  var safeAsync = require("./safe-async");
52
52
  var validateOpts = require("./validate-opts");
53
+ var numericBounds = require("./numeric-bounds");
54
+ var codepointClass = require("./codepoint-class");
53
55
  var { GateContractError, defineClass } = require("./framework-error");
54
56
 
55
57
  var observability = lazyRequire(function () { return require("./observability"); });
@@ -977,10 +979,8 @@ function canaryGate(gate, opts) {
977
979
  function cachingGate(gate, opts) {
978
980
  opts = opts || {};
979
981
  var backend = opts.backend;
980
- if (!backend || typeof backend.get !== "function" || typeof backend.set !== "function") {
981
- throw _err("gate-contract/bad-opt",
982
- "cachingGate: opts.backend must expose { get, set } (b.cache shape)");
983
- }
982
+ validateOpts.requireMethods(backend, ["get", "set"],
983
+ "cachingGate: opts.backend (b.cache shape)", GateContractError, "gate-contract/bad-opt");
984
984
  // ttlMs is read inside check() via closure; keep DEFAULT_CACHE_TTL_MS
985
985
  // referenced even if the host-side cache wrapping path doesn't need
986
986
  // explicit TTL today (the gate's per-instance cache uses cacheTtlMs).
@@ -1095,13 +1095,49 @@ function makeProfileBuilder(profiles) {
1095
1095
  * posture.piiPolicy; // → "redact"
1096
1096
  */
1097
1097
  function lookupCompliancePosture(name, postures, errorFactory, codePrefix) {
1098
- if (!postures || !postures[name]) {
1098
+ // hasOwnProperty: `name` is caller input; a bracket lookup would let a
1099
+ // prototype key like "constructor" pass the presence check and reach
1100
+ // Object.assign (proto shadowing).
1101
+ if (!postures || !Object.prototype.hasOwnProperty.call(postures, name)) {
1099
1102
  throw errorFactory(codePrefix + ".bad-posture",
1100
1103
  "unknown compliancePosture " + JSON.stringify(name));
1101
1104
  }
1102
1105
  return Object.assign({}, postures[name]);
1103
1106
  }
1104
1107
 
1108
+ /**
1109
+ * @primitive b.gateContract.makePostureAccessor
1110
+ * @signature b.gateContract.makePostureAccessor(postures, opts?)
1111
+ * @since 0.15.14
1112
+ * @status stable
1113
+ * @related b.gateContract.lookupCompliancePosture, b.gateContract.resolveProfileName
1114
+ *
1115
+ * Build the public `compliancePosture(name)` accessor a guard exposes — maps a
1116
+ * compliance-posture name through the guard's own `postures` table to the
1117
+ * profile it selects, returning `opts.fallback` (default `null`) for an
1118
+ * unknown name. Folds the one-line lookup the mail-scanner / content-detect
1119
+ * factories each redefined verbatim (`return POSTURES[name] || null`) into one
1120
+ * proto-shadow-safe helper: the membership test is `hasOwnProperty.call` so a
1121
+ * prototype key (constructor / __proto__ / toString) resolves to the fallback
1122
+ * rather than an inherited Function. Unlike `lookupCompliancePosture` it does
1123
+ * NOT throw and returns the raw mapped value (a profile name string), not an
1124
+ * object copy.
1125
+ *
1126
+ * @opts
1127
+ * fallback: any, // value returned for an unknown / proto-key name (default null)
1128
+ *
1129
+ * @example
1130
+ * var compliancePosture = b.gateContract.makePostureAccessor(COMPLIANCE_POSTURES);
1131
+ * compliancePosture("hipaa"); // → "strict"
1132
+ * compliancePosture("constructor"); // → null (proto key, not an own posture)
1133
+ */
1134
+ function makePostureAccessor(postures, opts) {
1135
+ var fallback = (opts && Object.prototype.hasOwnProperty.call(opts, "fallback")) ? opts.fallback : null;
1136
+ return function (name) {
1137
+ return Object.prototype.hasOwnProperty.call(postures, name) ? postures[name] : fallback;
1138
+ };
1139
+ }
1140
+
1105
1141
  // "GuardCidrError" -> "guardCidr" — the guard's audit/message identity, derived
1106
1142
  // once from its error class name. Used for the default gate's audit/metric
1107
1143
  // prefix AND the profile resolver's error message, so neither re-cases the name.
@@ -1166,6 +1202,48 @@ function makeProfileResolver(cfg) {
1166
1202
  };
1167
1203
  }
1168
1204
 
1205
+ /**
1206
+ * @primitive b.gateContract.resolveProfileName
1207
+ * @signature b.gateContract.resolveProfileName(opts, postures, defaultProfile)
1208
+ * @since 0.15.13
1209
+ * @status stable
1210
+ * @related b.gateContract.makeProfileResolver
1211
+ *
1212
+ * Resolve a profile NAME from create-time opts with PROFILE precedence: an
1213
+ * explicit `opts.profile` wins, else `opts.posture` mapped through the
1214
+ * compliance-posture table, else `defaultProfile`. Returns the name WITHOUT
1215
+ * validating it — the caller checks membership in its own `PROFILES` map and
1216
+ * throws its own typed, field-specific error. This is the resolution EXPRESSION
1217
+ * the mail-scanner / envelope factories (mail-greylist / mail-rbl / mail-scan /
1218
+ * mail-spam-score / mail-helo / guard-envelope) each hand-rolled identically.
1219
+ *
1220
+ * It differs from `makeProfileResolver` in two deliberate ways: it does not
1221
+ * throw (so the caller keeps its bespoke bad-profile message) and it gives
1222
+ * `profile` precedence rather than `posture` precedence. The two precedences
1223
+ * coexist in the framework today (the `defineParser`-shaped guards resolve
1224
+ * posture-first); unifying them is a policy decision, and routing every caller
1225
+ * through one of these two helpers is what makes that decision a single edit.
1226
+ *
1227
+ * @opts
1228
+ * profile: string, // explicit profile name — wins when present
1229
+ * posture: string, // compliance posture, mapped through `postures`
1230
+ *
1231
+ * @example
1232
+ * var name = b.gateContract.resolveProfileName(
1233
+ * { profile: "balanced" }, COMPLIANCE_POSTURES, "strict");
1234
+ * // → "balanced"
1235
+ */
1236
+ function resolveProfileName(opts, postures, defaultProfile) {
1237
+ opts = opts || {};
1238
+ // hasOwnProperty: opts.posture is caller/operator input; a bracket lookup
1239
+ // would let a prototype key like "constructor" resolve to an inherited
1240
+ // truthy value and become the profile name (proto shadowing).
1241
+ var postureProfile = (opts.posture && postures &&
1242
+ Object.prototype.hasOwnProperty.call(postures, opts.posture))
1243
+ ? postures[opts.posture] : null;
1244
+ return opts.profile || postureProfile || defaultProfile;
1245
+ }
1246
+
1169
1247
  /**
1170
1248
  * @primitive b.gateContract.throwOnRefusalSeverity
1171
1249
  * @signature b.gateContract.throwOnRefusalSeverity(issues, cfg)
@@ -1249,6 +1327,244 @@ var ALL_STRICT_POSTURES = Object.freeze({
1249
1327
  soc2: "strict",
1250
1328
  });
1251
1329
 
1330
+ /**
1331
+ * @primitive b.gateContract.CHAR_THREATS_REJECT_ALL
1332
+ * @signature b.gateContract.CHAR_THREATS_REJECT_ALL
1333
+ * @since 0.15.13
1334
+ * @status stable
1335
+ * @compliance hipaa, pci-dss, gdpr, soc2
1336
+ * @related b.gateContract.charThreatDisposition, b.gateContract.makeProfileBuilder
1337
+ *
1338
+ * The universal character-safety floor: the four invisible-character
1339
+ * threats — BIDI overrides, C0/C1 control bytes, embedded null bytes,
1340
+ * and zero-width characters — each set to `"reject"`. These four
1341
+ * classes are categorically unsafe in an identifier or structured
1342
+ * value (forgery, log injection, label-segmentation, parser
1343
+ * confusion), so every identifier/protocol guard refuses them in
1344
+ * every profile tier and the content guards refuse them in `strict`.
1345
+ *
1346
+ * Spread this frozen block into a profile tier instead of re-declaring
1347
+ * the four lines: `{ ...gateContract.CHAR_THREATS_REJECT_ALL, ... }`.
1348
+ * A tier that relaxes one class overrides after the spread (e.g.
1349
+ * `{ ...CHAR_THREATS_REJECT_ALL, zeroWidthPolicy: "strip" }`), keeping
1350
+ * the floor for the other three. Frozen and shared by reference; the
1351
+ * spread copies the values into each consumer's own tier object.
1352
+ *
1353
+ * @example
1354
+ * var PROFILES = Object.freeze({
1355
+ * strict: { ...b.gateContract.CHAR_THREATS_REJECT_ALL, maxBytes: 256 },
1356
+ * });
1357
+ * PROFILES.strict.bidiPolicy; // → "reject"
1358
+ * Object.isFrozen(b.gateContract.CHAR_THREATS_REJECT_ALL); // → true
1359
+ */
1360
+ var CHAR_THREATS_REJECT_ALL = Object.freeze({
1361
+ bidiPolicy: "reject",
1362
+ controlPolicy: "reject",
1363
+ nullBytePolicy: "reject",
1364
+ zeroWidthPolicy: "reject",
1365
+ });
1366
+
1367
+ /**
1368
+ * @primitive b.gateContract.DANGEROUS_URL_SCHEMES
1369
+ * @signature b.gateContract.DANGEROUS_URL_SCHEMES
1370
+ * @since 0.15.13
1371
+ * @status stable
1372
+ * @compliance soc2
1373
+ * @related b.gateContract.CHAR_THREATS_REJECT_ALL, b.guardHtml.validate, b.guardSvg.validate
1374
+ *
1375
+ * The frozen denylist of URL schemes that are categorically unsafe inside a
1376
+ * markup attribute value (`href` / `src` / `xlink:href`) — the markup XSS and
1377
+ * dangerous-resource vector set. `javascript` / `vbscript` / `livescript` /
1378
+ * `mocha` / `ecmascript` execute script; `data` / `view-source` / `mhtml` /
1379
+ * `feed` carry or expose renderable content; `file` / `jar` / `intent` reach
1380
+ * local resources or protocol handlers. A markup sanitizer rejects an
1381
+ * attribute whose scheme is in this list.
1382
+ *
1383
+ * Lower-cased, scheme-name only (no trailing colon) so callers compare against
1384
+ * a lower-cased parsed scheme via `indexOf(scheme) !== -1`. This is the
1385
+ * markup-attribute DENYLIST — distinct from `b.safeUrl`'s protocol ALLOWLIST,
1386
+ * which governs full-URL parsing where only an explicit set of protocols is
1387
+ * permitted.
1388
+ *
1389
+ * @example
1390
+ * b.gateContract.DANGEROUS_URL_SCHEMES.indexOf("javascript"); // → 0 (dangerous)
1391
+ * b.gateContract.DANGEROUS_URL_SCHEMES.indexOf("https"); // → -1 (allowed)
1392
+ * Object.isFrozen(b.gateContract.DANGEROUS_URL_SCHEMES); // → true
1393
+ */
1394
+ var DANGEROUS_URL_SCHEMES = Object.freeze([
1395
+ "javascript", "vbscript", "livescript", "mocha", "ecmascript",
1396
+ "file", "mhtml", "jar", "intent", "view-source", "feed", "data",
1397
+ ]);
1398
+
1399
+ /**
1400
+ * @primitive b.gateContract.SAFE_URL_SCHEMES
1401
+ * @signature b.gateContract.SAFE_URL_SCHEMES
1402
+ * @since 0.15.13
1403
+ * @status stable
1404
+ * @compliance soc2
1405
+ * @related b.gateContract.DANGEROUS_URL_SCHEMES
1406
+ *
1407
+ * The frozen base allowlist of URL schemes a markup sanitizer accepts in an
1408
+ * attribute value at the `strict` tier — `http` / `https` / `mailto` / `tel`.
1409
+ * A guard extends it for looser tiers (e.g. `SAFE_URL_SCHEMES.concat(["ftp"])`)
1410
+ * rather than re-declaring the base. Scheme names only, no trailing colon.
1411
+ *
1412
+ * @example
1413
+ * b.gateContract.SAFE_URL_SCHEMES; // → ["http","https","mailto","tel"]
1414
+ * Object.isFrozen(b.gateContract.SAFE_URL_SCHEMES); // → true
1415
+ */
1416
+ var SAFE_URL_SCHEMES = Object.freeze(["http", "https", "mailto", "tel"]);
1417
+
1418
+ /**
1419
+ * @primitive b.gateContract.identifierFixtures
1420
+ * @signature b.gateContract.identifierFixtures(benign, hostile, encoding?)
1421
+ * @since 0.15.13
1422
+ * @status stable
1423
+ * @related b.gateContract.defineGuard
1424
+ *
1425
+ * Build an identifier guard's frozen `INTEGRATION_FIXTURES` from one benign
1426
+ * and one hostile sample string. The layer-5 host harness feeds the string
1427
+ * form to `gate.check({ identifier })` and the byte form to the upload /
1428
+ * digest paths, so the two are the same value in two representations. A guard
1429
+ * that hand-writes both forms repeats its sample literal twice (`benignBytes:
1430
+ * Buffer.from("x"), benignIdentifier: "x"`); declaring the string once and
1431
+ * deriving the buffer removes that per-guard duplication. `encoding` defaults
1432
+ * to `"utf8"` — pass `"ascii"` for line-protocol command samples whose bytes
1433
+ * must stay single-octet.
1434
+ *
1435
+ * @example
1436
+ * var INTEGRATION_FIXTURES =
1437
+ * b.gateContract.identifierFixtures("example.com", "192.168.1.1");
1438
+ * INTEGRATION_FIXTURES.benignIdentifier; // → "example.com"
1439
+ * INTEGRATION_FIXTURES.benignBytes; // → Buffer "example.com"
1440
+ * Object.isFrozen(INTEGRATION_FIXTURES); // → true
1441
+ */
1442
+ function identifierFixtures(benign, hostile, encoding) {
1443
+ validateOpts.requireNonEmptyString(benign,
1444
+ "gateContract.identifierFixtures: benign", GateContractError, "gate-contract/bad-opt");
1445
+ validateOpts.requireNonEmptyString(hostile,
1446
+ "gateContract.identifierFixtures: hostile", GateContractError, "gate-contract/bad-opt");
1447
+ var enc = encoding || "utf8";
1448
+ if (typeof enc !== "string" || !Buffer.isEncoding(enc)) {
1449
+ throw _err("gate-contract/bad-opt",
1450
+ "gateContract.identifierFixtures: encoding must be a valid Buffer encoding, got " +
1451
+ JSON.stringify(encoding));
1452
+ }
1453
+ return Object.freeze({
1454
+ kind: "identifier",
1455
+ benignBytes: Buffer.from(benign, enc),
1456
+ hostileBytes: Buffer.from(hostile, enc),
1457
+ benignIdentifier: benign,
1458
+ hostileIdentifier: hostile,
1459
+ });
1460
+ }
1461
+
1462
+ /**
1463
+ * @primitive b.gateContract.compliancePostures
1464
+ * @signature b.gateContract.compliancePostures(profiles, spec)
1465
+ * @since 0.15.13
1466
+ * @status stable
1467
+ * @compliance hipaa, pci-dss, gdpr, soc2
1468
+ * @related b.gateContract.ALL_STRICT_POSTURES, b.gateContract.resolveProfileAndPosture
1469
+ *
1470
+ * Build a content guard's four-posture `COMPLIANCE_POSTURES` map from its
1471
+ * profile set and a single forensic-snippet budget, encoding the framework's
1472
+ * regulation-disposition policy in one place instead of re-declaring it in
1473
+ * every guard. Each regulation maps to the profile tier whose disposition
1474
+ * matches its intent:
1475
+ *
1476
+ * - `hipaa` / `pci-dss` / `soc2` → the `strict` profile. These regimes
1477
+ * demand forensic integrity — the record must not be silently
1478
+ * altered — so every threat class is rejected, never sanitized.
1479
+ * - `gdpr` → the `balanced` profile. Data-minimization favors removing
1480
+ * the offending bytes over rejecting the whole value, so on free-text
1481
+ * content the balanced tier strips the sanitizable character classes (bidi
1482
+ * / control / zero-width) while still rejecting structural threats. For an
1483
+ * identifier guard the balanced tier rejects those classes too —
1484
+ * stripping bytes from an identifier would change its identity — so
1485
+ * the disposition follows the guard's own content kind automatically.
1486
+ *
1487
+ * The forensic snippet budget scales with each regime's retention posture:
1488
+ * `hipaa` / `pci-dss` keep `base` bytes, `gdpr` keeps `base / 2` (retain less
1489
+ * hostile data under data-minimization), `soc2` keeps `base * 2` (audit
1490
+ * retention). Pass `spec.overlays` to layer a deliberate per-posture delta on
1491
+ * top of the tier — e.g. a filename guard stripping bidi / control under
1492
+ * `gdpr` where its balanced profile would reject them. Each returned posture is
1493
+ * frozen and shared by reference.
1494
+ *
1495
+ * @opts
1496
+ * {
1497
+ * base: number, // required, positive even byte count: the hipaa/pci snippet budget
1498
+ * overlays: { // optional per-posture policy deltas, merged last
1499
+ * hipaa: object,
1500
+ * "pci-dss": object,
1501
+ * gdpr: object,
1502
+ * soc2: object,
1503
+ * },
1504
+ * }
1505
+ *
1506
+ * @example
1507
+ * var COMPLIANCE_POSTURES = b.gateContract.compliancePostures(PROFILES, {
1508
+ * base: 256,
1509
+ * });
1510
+ * COMPLIANCE_POSTURES.gdpr.forensicSnippetBytes; // → 128
1511
+ * Object.isFrozen(COMPLIANCE_POSTURES.hipaa); // → true
1512
+ */
1513
+ function compliancePostures(profiles, spec) {
1514
+ validateOpts.requireObject(profiles, "gateContract.compliancePostures", GateContractError);
1515
+ if (!profiles.strict || !profiles.balanced) {
1516
+ throw _err("gate-contract/bad-profiles",
1517
+ "compliancePostures: profiles must include 'strict' and 'balanced'");
1518
+ }
1519
+ spec = spec || {};
1520
+ var base = spec.base;
1521
+ if (typeof base !== "number" || !isFinite(base) || base <= 0 || base % 2 !== 0) {
1522
+ throw _err("gate-contract/bad-base",
1523
+ "compliancePostures: spec.base must be a positive even byte count");
1524
+ }
1525
+ var overlays = spec.overlays || {};
1526
+ function build(tier, snippetBytes, overlay) {
1527
+ return Object.freeze(Object.assign({}, profiles[tier],
1528
+ { forensicSnippetBytes: C.BYTES.bytes(snippetBytes) }, overlay || {}));
1529
+ }
1530
+ return Object.freeze({
1531
+ hipaa: build("strict", base, overlays.hipaa),
1532
+ "pci-dss": build("strict", base, overlays["pci-dss"]),
1533
+ gdpr: build("balanced", base / 2, overlays.gdpr),
1534
+ soc2: build("strict", base * 2, overlays.soc2),
1535
+ });
1536
+ }
1537
+
1538
+ /**
1539
+ * @primitive b.gateContract.strictDefaults
1540
+ * @signature b.gateContract.strictDefaults(profiles, overlay?)
1541
+ * @since 0.15.13
1542
+ * @status stable
1543
+ * @related b.gateContract.compliancePostures, b.gateContract.defineGuard
1544
+ *
1545
+ * Build a guard's frozen `DEFAULTS` opts: its `strict` profile, in `enforce`
1546
+ * mode, plus any per-guard overlay. Every guard's no-opts call path starts from
1547
+ * the strictest profile with enforcement on (security-on by default); the only
1548
+ * variation is a guard that adds a parse runtime cap (`maxRuntimeMs`) or another
1549
+ * default override. Replaces the hand-rolled `Object.freeze(Object.assign({},
1550
+ * PROFILES["strict"], { mode: "enforce", … }))` every guard repeated.
1551
+ *
1552
+ * @opts
1553
+ * overlay: object // optional per-guard default overrides merged last (e.g. { maxRuntimeMs: C.TIME.seconds(10) }); may override `mode`
1554
+ *
1555
+ * @example
1556
+ * var DEFAULTS = b.gateContract.strictDefaults(PROFILES); // strict + enforce
1557
+ * var DEFAULTS = b.gateContract.strictDefaults(PROFILES, { maxRuntimeMs: 10000 }); // + a parse runtime cap
1558
+ */
1559
+ function strictDefaults(profiles, overlay) {
1560
+ validateOpts.requireObject(profiles, "gateContract.strictDefaults", GateContractError);
1561
+ if (!profiles.strict) {
1562
+ throw _err("gate-contract/bad-profiles",
1563
+ "strictDefaults: profiles must include 'strict'");
1564
+ }
1565
+ return Object.freeze(Object.assign({}, profiles.strict, { mode: "enforce" }, overlay || {}));
1566
+ }
1567
+
1252
1568
  /**
1253
1569
  * @primitive b.gateContract.makeRulePackLoader
1254
1570
  * @signature b.gateContract.makeRulePackLoader(errorClass, codePrefix)
@@ -1386,6 +1702,234 @@ function buildGuardGate(name, opts, check) {
1386
1702
  });
1387
1703
  }
1388
1704
 
1705
+ /**
1706
+ * @primitive b.gateContract.severityDisposition
1707
+ * @signature b.gateContract.severityDisposition(issues)
1708
+ * @since 0.15.13
1709
+ * @status stable
1710
+ * @related b.gateContract.buildGuardGate, b.gateContract.buildContentGate
1711
+ *
1712
+ * The non-sanitizing guard gate's severity action-chain in one place. A guard
1713
+ * that cannot repair its subject (an auth bundle, an OAuth flow, a GraphQL
1714
+ * request, image / PDF metadata, an archive entry list, an email body, a regex
1715
+ * pattern) ends its `gate` check with the identical disposition: `serve` when
1716
+ * there are no findings, `audit-only` when no finding reaches refusal severity,
1717
+ * else `refuse`. This is the sibling of `buildContentGate` (which adds the
1718
+ * sanitize attempt for content that CAN be repaired). Each guard keeps its own
1719
+ * subject extraction + validate call, then returns `severityDisposition(rv.issues)`.
1720
+ *
1721
+ * A finding of `critical` OR `high` severity refuses; anything lower is
1722
+ * `audit-only`. The returned shape matches a gate `check` result:
1723
+ * `{ ok, action }` (no `issues` on a clean serve) or `{ ok, action, issues }`.
1724
+ *
1725
+ * @example
1726
+ * var rv = module.exports.validate(bundle, opts);
1727
+ * return b.gateContract.severityDisposition(rv.issues);
1728
+ * // [] → { ok: true, action: "serve" }
1729
+ * // [{ severity: "low" }] → { ok: true, action: "audit-only", issues: [...] }
1730
+ * // [{ severity: "high" }] → { ok: false, action: "refuse", issues: [...] }
1731
+ */
1732
+ function severityDisposition(issues) {
1733
+ if (issues.length === 0) return { ok: true, action: "serve" };
1734
+ var hasCritical = issues.some(function (i) { return i.severity === "critical"; });
1735
+ var hasHigh = issues.some(function (i) { return i.severity === "high"; });
1736
+ if (!hasCritical && !hasHigh) {
1737
+ return { ok: true, action: "audit-only", issues: issues };
1738
+ }
1739
+ return { ok: false, action: "refuse", issues: issues };
1740
+ }
1741
+
1742
+ /**
1743
+ * @primitive b.gateContract.buildContentGate
1744
+ * @signature b.gateContract.buildContentGate(spec)
1745
+ * @since 0.15.13
1746
+ * @status stable
1747
+ * @related b.gateContract.buildGuardGate, b.gateContract.defineGuard
1748
+ *
1749
+ * The content-guard gate action-chain in one place. Every content guard's
1750
+ * `gate(opts)` ran the identical chain — extract the bytes, `serve` a clean
1751
+ * input, `audit-only` when no issue reaches the refusal severity, attempt
1752
+ * `sanitize` when the input is eligible, else `refuse` — differing only in
1753
+ * declarative axes. Passing those axes to one primitive replaces ~30 lines of
1754
+ * per-guard gate body and makes the chain impossible to drift between guards.
1755
+ *
1756
+ * A CRITICAL finding always `refuse`s — too severe to serve even sanitized (a
1757
+ * stripped script can still carry an mXSS / parser-differential vector). Only
1758
+ * HIGH findings are sanitize-eligible, and even then the action is PROVEN, not
1759
+ * guessed: the gate runs `produceSanitized` then RE-VALIDATES its output,
1760
+ * returning `sanitize` only when the result is verifiably clean — otherwise
1761
+ * `refuse`. An operator's `reject` choice lands as critical (→ refuse) and a
1762
+ * `strip` choice as high (→ sanitize-if-verified), so the severity carries the
1763
+ * disposition with no per-policy bookkeeping — replacing a global "is any
1764
+ * policy set to reject?" guess that wrongly froze sanitize for findings
1765
+ * unrelated to the rejected policy. The sanitizer's own policy-respecting
1766
+ * behaviour does the rest: it throws on / leaves a reject-class finding so the
1767
+ * re-validate still refuses, and refuses anything it cannot actually repair.
1768
+ * `sanitizeBlockingKinds` skips the attempt for inputs a text sanitizer must not
1769
+ * touch (gzipped SVGZ bytes); a thrown producer falls through to `refuse`.
1770
+ *
1771
+ * @opts
1772
+ * name: string, // gate label (audit/metric/cache identity)
1773
+ * opts: object, // already resolved profile/posture opts
1774
+ * validate: function, // (subject, opts) -> { ok, issues }
1775
+ * produceSanitized: function, // (subject, opts) -> Buffer|string
1776
+ * ctxField: "text"|"bytes", // default "text" (extractBytesAsText); "bytes" reads ctx.bytes raw
1777
+ * sanitizeBlockingKinds: string[], // issue kinds that skip the sanitize attempt (e.g. ["svgz-compressed"])
1778
+ *
1779
+ * @example
1780
+ * var g = b.gateContract.buildContentGate({
1781
+ * name: "guardXml:strict", opts: resolved, validate: validate,
1782
+ * produceSanitized: function (t, o) { return sanitize(t, o); },
1783
+ * });
1784
+ * (await g.check({ bytes: Buffer.from("<a>1</a>") })).action; // → "serve"
1785
+ */
1786
+ function buildContentGate(spec) {
1787
+ var opts = spec.opts || {};
1788
+ var ctxField = spec.ctxField === "bytes" ? "bytes" : "text";
1789
+ var blockKind = Array.isArray(spec.sanitizeBlockingKinds) ? spec.sanitizeBlockingKinds : [];
1790
+
1791
+ // A finding's disposition is what the operator's POLICY for that finding
1792
+ // class chose, not the finding's impact severity. The guard declares the
1793
+ // binding via spec.dispositionFor(issue, opts); anything it doesn't classify
1794
+ // (an operator-injected rule, a structural hard-cap, an un-mapped kind) falls
1795
+ // back to severity — and that fallback is CONSERVATIVE (a refusal-severity
1796
+ // finding refuses, because the gate has no proof it can be repaired).
1797
+ function _disposition(issue, fromGuard) {
1798
+ if (fromGuard && typeof spec.dispositionFor === "function") {
1799
+ var d = spec.dispositionFor(issue, opts);
1800
+ if (d === "refuse" || d === "sanitize" || d === "audit") return d;
1801
+ }
1802
+ if (issue.disposition === "refuse" || issue.disposition === "sanitize" ||
1803
+ issue.disposition === "audit") return issue.disposition;
1804
+ return (issue.severity === "critical" || issue.severity === "high") ? "refuse" : "audit";
1805
+ }
1806
+
1807
+ return buildGuardGate(spec.name, opts, async function (ctx) {
1808
+ var subject = ctxField === "bytes" ? (ctx && ctx.bytes) : extractBytesAsText(ctx);
1809
+ if (!subject) return { ok: true, action: "serve" };
1810
+
1811
+ var rv = spec.validate(subject, opts);
1812
+ var entries = rv.issues.map(function (i) { return { issue: i, disp: _disposition(i, true) }; });
1813
+ // Operator-injected detect-only findings (spec.extraIssues): the guard owns
1814
+ // no sanitizer for them, so a refusal-severity hit can only refuse — never
1815
+ // serve a "sanitized" output that still carries the operator's finding.
1816
+ if (typeof spec.extraIssues === "function") {
1817
+ var extra = spec.extraIssues(subject, opts, ctx) || [];
1818
+ for (var k = 0; k < extra.length; k++) {
1819
+ entries.push({ issue: extra[k], disp: _disposition(extra[k], false) });
1820
+ }
1821
+ }
1822
+ if (entries.length === 0) return { ok: true, action: "serve" };
1823
+
1824
+ var issues = entries.map(function (e) { return e.issue; });
1825
+ // refuse wins over sanitize wins over audit. A finding whose policy is
1826
+ // `reject` (→ refuse), an always-dangerous denylist hit, or a structural
1827
+ // hard-cap refuses the whole input regardless of any sanitizable siblings.
1828
+ if (entries.some(function (e) { return e.disp === "refuse"; })) {
1829
+ return { ok: false, action: "refuse", issues: issues };
1830
+ }
1831
+ // Some finding's policy chose to mitigate (strip / prefix / redact / drop):
1832
+ // run the guard's own sanitizer, which performs exactly the policy-selected
1833
+ // transform, and serve its output. The operator picked sanitize-and-serve
1834
+ // over reject for these classes, so the result is trusted — no re-validation
1835
+ // (a mitigation like CSV's prefix-tab is in-place, not removal, so a second
1836
+ // detector pass would wrongly refuse it). sanitizeBlockingKinds skips the
1837
+ // attempt for inputs a text sanitizer must not touch (gzipped SVGZ bytes);
1838
+ // a sanitizer that throws (e.g. a reject-policy parse) falls through.
1839
+ if (entries.some(function (e) { return e.disp === "sanitize"; })) {
1840
+ var blocked = blockKind.some(function (kind) {
1841
+ return issues.some(function (it) { return it.kind === kind; });
1842
+ });
1843
+ if (!blocked && typeof spec.produceSanitized === "function") {
1844
+ try {
1845
+ var clean = spec.produceSanitized(subject, opts);
1846
+ var cleanBuf = Buffer.isBuffer(clean) ? clean : Buffer.from(String(clean), "utf8");
1847
+ return { ok: true, action: "sanitize", sanitized: cleanBuf, issues: issues };
1848
+ } catch (_e) { /* sanitizer could not repair → refuse */ }
1849
+ }
1850
+ return { ok: false, action: "refuse", issues: issues };
1851
+ }
1852
+ // Only audit-disposition (sub-refusal observational) findings remain.
1853
+ return { ok: true, action: "audit-only", issues: issues };
1854
+ });
1855
+ }
1856
+
1857
+ /**
1858
+ * @primitive b.gateContract.policyDisposition
1859
+ * @signature b.gateContract.policyDisposition(policy)
1860
+ * @since 0.15.13
1861
+ * @status stable
1862
+ * @related b.gateContract.buildContentGate
1863
+ *
1864
+ * Map an operator content-policy value to the gate disposition it selects.
1865
+ * A content guard emits a finding only when the governing policy is not
1866
+ * `allow`; this turns that policy into what the gate should DO with the
1867
+ * finding — independent of the finding's impact severity:
1868
+ *
1869
+ * - `reject` → `refuse` (operator chose to reject this class outright)
1870
+ * - `audit` / `audit-only` → `audit` (observe, do not block or alter)
1871
+ * - a known mitigation (`strip`, `prefix-tab`, `prefix-quote`,
1872
+ * `wrap-with-quotes-and-prefix`, `allowlist`, `redact`, `trim`) →
1873
+ * `sanitize` (the guard's sanitizer performs the chosen transform)
1874
+ *
1875
+ * Fails CLOSED: an unrecognized policy value (a typo such as `rejet`, or a
1876
+ * mitigation name not in the known set) maps to `refuse`, never `sanitize` —
1877
+ * a misconfiguration must not silently downgrade a finding to serve-after-
1878
+ * best-effort. Add a new mitigation to MITIGATION_POLICIES when one ships.
1879
+ *
1880
+ * @example
1881
+ * b.gateContract.policyDisposition("reject"); // → "refuse"
1882
+ * b.gateContract.policyDisposition("strip"); // → "sanitize"
1883
+ * b.gateContract.policyDisposition("audit-only"); // → "audit"
1884
+ * b.gateContract.policyDisposition("rejet"); // → "refuse" (fail closed)
1885
+ */
1886
+ var MITIGATION_POLICIES = Object.freeze({
1887
+ strip: true, "prefix-tab": true, "prefix-quote": true,
1888
+ "wrap-with-quotes-and-prefix": true, allowlist: true, redact: true, trim: true,
1889
+ });
1890
+ function policyDisposition(policy) {
1891
+ if (policy === "reject") return "refuse";
1892
+ if (policy === "audit" || policy === "audit-only") return "audit";
1893
+ if (MITIGATION_POLICIES[policy] === true) return "sanitize";
1894
+ return "refuse";
1895
+ }
1896
+
1897
+ /**
1898
+ * @primitive b.gateContract.charThreatDisposition
1899
+ * @signature b.gateContract.charThreatDisposition(issue, opts)
1900
+ * @since 0.15.13
1901
+ * @status stable
1902
+ * @related b.gateContract.policyDisposition, b.codepointClass.detectCharThreats
1903
+ *
1904
+ * Gate disposition for the shared character-threat findings every content
1905
+ * guard collects via `codepointClass.detectCharThreats` — `bidi-override`,
1906
+ * `null-byte`, `control-char` — resolved from the guard's per-class policy
1907
+ * (`bidiPolicy` / `nullBytePolicy` / `controlPolicy`). Returns `null` for any
1908
+ * other kind so a guard's own `dispositionFor` can fall through to it for the
1909
+ * shared kinds and handle its guard-specific findings itself.
1910
+ *
1911
+ * @opts
1912
+ * bidiPolicy: string, // governs the bidi-override finding
1913
+ * nullBytePolicy: string, // governs the null-byte finding
1914
+ * controlPolicy: string, // governs the control-char finding
1915
+ * zeroWidthPolicy: string, // governs the zero-width finding
1916
+ *
1917
+ * @example
1918
+ * function dispositionFor(issue, opts) {
1919
+ * return b.gateContract.charThreatDisposition(issue, opts) ||
1920
+ * mySpecificMapping(issue, opts);
1921
+ * }
1922
+ */
1923
+ function charThreatDisposition(issue, opts) {
1924
+ switch (issue.kind) {
1925
+ case "bidi-override": return policyDisposition(opts.bidiPolicy);
1926
+ case "null-byte": return policyDisposition(opts.nullBytePolicy);
1927
+ case "control-char": return policyDisposition(opts.controlPolicy);
1928
+ case "zero-width": return policyDisposition(opts.zeroWidthPolicy);
1929
+ default: return null;
1930
+ }
1931
+ }
1932
+
1389
1933
  /**
1390
1934
  * @primitive b.gateContract.aggregateIssues
1391
1935
  * @signature b.gateContract.aggregateIssues(issues)
@@ -1439,34 +1983,159 @@ function aggregateIssues(issues) {
1439
1983
  * bad.issues[0].kind; // → "bad-input"
1440
1984
  */
1441
1985
  function badInputResultIfNotStringOrBuffer(input) {
1442
- if (typeof input === "string" || Buffer.isBuffer(input)) return null;
1986
+ // The "bytes" input contract IS this check compose it so there is one
1987
+ // source of truth for "string or Buffer, else bad-input".
1988
+ var extracted = INPUT_CONTRACTS.bytes(input);
1989
+ if (!extracted.badInput) return null;
1443
1990
  return {
1444
1991
  ok: false,
1445
1992
  issues: [{ kind: "bad-input", severity: "high",
1446
- snippet: "input is not string or Buffer" }],
1993
+ snippet: extracted.badInput }],
1447
1994
  };
1448
1995
  }
1449
1996
 
1997
+ /**
1998
+ * @primitive b.gateContract.detectStringInput
1999
+ * @signature b.gateContract.detectStringInput(input, opts, cfg)
2000
+ * @since 0.15.13
2001
+ * @status stable
2002
+ * @related b.gateContract.badInputResultIfNotStringOrBuffer, b.codepointClass.detectCharThreats
2003
+ *
2004
+ * The whole detector preamble every `raw`-contract string guard opens with:
2005
+ * reject a non-string input, then an empty one, then one over the byte cap,
2006
+ * else collect the codepoint-class threats (BIDI / control / null / zero-width)
2007
+ * the guard then appends its own findings to. A guard on the `raw` input
2008
+ * contract owns its own input check (see `INPUT_CONTRACTS`); this builds that
2009
+ * preamble once, guard-named, instead of re-spelling its four steps in every
2010
+ * `_detectIssues`. Returns `{ done, issues }`: when `done` the detector returns
2011
+ * `issues` verbatim (the `<name>.bad-input` / `<name>.empty` / cap issue, or
2012
+ * `[]` for a legal empty); when not `done`, `issues` is the codepoint-threat
2013
+ * list the detector continues from. The byte cap runs before the codepoint
2014
+ * scan so a huge input is rejected before the O(n) scan.
2015
+ *
2016
+ * The cap's divergence is data, not branching: `cap.bytes` is the limit (the
2017
+ * guard's resolved `maxBytes` / `maxPatternBytes` / `maxDomainOctets`),
2018
+ * `cap.kind` the issue kind (default `<name>-cap`), and `cap.snippet` the
2019
+ * message — a string, or a `function(byteLen, bytes)` when it embeds the
2020
+ * measured length. Omit `cap` for a guard with no byte cap.
2021
+ *
2022
+ * @opts
2023
+ * {
2024
+ * name: string, // required: the guard name — ruleId prefix + default noun
2025
+ * noun: string, // default name — the subject word in the bad-input/empty snippet
2026
+ * emptyMode: string, // "issue" (default) → <name>.empty issue · "ok" → [] (empty is legal) · "skip" → no empty check
2027
+ * cap: { // omit when the guard has no byte cap
2028
+ * bytes: number, // required: the byte limit
2029
+ * kind: string, // default "<name>-cap" — the cap issue kind (and ruleId suffix)
2030
+ * snippet: string|function, // default "<noun> input exceeds maxBytes <bytes>"; fn(byteLen, bytes) when it needs the measured length
2031
+ * },
2032
+ * scanCodepoints: boolean, // default true: the not-done result carries the codepoint-class scan. Pass false for a guard that scans codepoints later in its own detection (or parses them via its format, e.g. JSON) — the not-done result is then `[]`.
2033
+ * }
2034
+ *
2035
+ * @example
2036
+ * function _detectIssues(input, opts) {
2037
+ * var pre = b.gateContract.detectStringInput(input, opts, {
2038
+ * name: "cidr", cap: { bytes: opts.maxBytes },
2039
+ * });
2040
+ * if (pre.done) return pre.issues;
2041
+ * var issues = pre.issues; // codepoint-class threats so far
2042
+ * // … guard-specific detection appends to issues …
2043
+ * return issues;
2044
+ * }
2045
+ */
2046
+ function detectStringInput(input, opts, cfg) {
2047
+ validateOpts.requireObject(cfg, "gateContract.detectStringInput", GateContractError);
2048
+ validateOpts.requireNonEmptyString(cfg.name,
2049
+ "gateContract.detectStringInput: cfg.name", GateContractError, "gate-contract/bad-opt");
2050
+ var noun = cfg.noun || cfg.name;
2051
+ if (typeof input !== "string") {
2052
+ return { done: true, issues: [{ kind: "bad-input", severity: "high",
2053
+ ruleId: cfg.name + ".bad-input", snippet: noun + " is not a string" }] };
2054
+ }
2055
+ var emptyMode = cfg.emptyMode || "issue";
2056
+ if (emptyMode !== "skip" && input.length === 0) {
2057
+ return { done: true, issues: emptyMode === "ok" ? [] :
2058
+ [{ kind: "empty", severity: "high", ruleId: cfg.name + ".empty",
2059
+ snippet: noun + " is empty" }] };
2060
+ }
2061
+ if (cfg.cap) {
2062
+ var byteLen = Buffer.byteLength(input, "utf8");
2063
+ if (byteLen > cfg.cap.bytes) {
2064
+ var capKind = cfg.cap.kind || (cfg.name + "-cap");
2065
+ var capSnippet = typeof cfg.cap.snippet === "function"
2066
+ ? cfg.cap.snippet(byteLen, cfg.cap.bytes)
2067
+ : (cfg.cap.snippet || (noun + " input exceeds maxBytes " + cfg.cap.bytes));
2068
+ return { done: true, issues: [{ kind: capKind, severity: "high",
2069
+ ruleId: cfg.name + "." + capKind, snippet: capSnippet }] };
2070
+ }
2071
+ }
2072
+ if (cfg.scanCodepoints === false) return { done: false, issues: [] };
2073
+ return { done: false, issues: codepointClass.detectCharThreats(input, opts, cfg.name) };
2074
+ }
2075
+
2076
+ // Input contracts — the one place that knows how to turn a raw guard input
2077
+ // into the subject its detector expects (or flag bad input). Every guard's
2078
+ // `validate` differs only in its input shape; the contract captures that
2079
+ // difference so one `runIssueValidator` serves them all:
2080
+ //
2081
+ // - "text": string / Buffer → UTF-8 text; anything else is bad input. The
2082
+ // content guards (csv / html / markdown / json / ...) whose detector takes
2083
+ // a string but does NOT type-check it themselves.
2084
+ // - "bytes": string / Buffer is accepted and reaches the detector UNCHANGED
2085
+ // (no utf8 coercion); anything else is bad input. The guards that inspect
2086
+ // raw bytes before decoding (filename overlong-UTF-8, sql encoding gate) —
2087
+ // converting to text first would hide the very bytes they check. This is
2088
+ // exactly `badInputResultIfNotStringOrBuffer(input) ||
2089
+ // aggregateIssues(detect(input))`.
2090
+ // - "raw" (default): identity — the value reaches the detector untouched and
2091
+ // the detector owns its own (often typed) bad-input. The object-bag guards
2092
+ // (image / pdf metadata, archive entries) and the string guards whose
2093
+ // detector type-checks itself (email). This is exactly
2094
+ // `aggregateIssues(detect(input))`.
2095
+ //
2096
+ // A guard with a bespoke shape passes its own `function(input){ return
2097
+ // { subject } | { badInput: msg } }` instead of a name. New input shapes
2098
+ // extend the class by adding a contract, never by branching in defineGuard.
2099
+ var INPUT_CONTRACTS = {
2100
+ text: function (input) {
2101
+ if (typeof input === "string") return { subject: input };
2102
+ if (Buffer.isBuffer(input)) return { subject: input.toString("utf8") };
2103
+ return { badInput: "input is not string or Buffer" };
2104
+ },
2105
+ bytes: function (input) {
2106
+ if (typeof input === "string" || Buffer.isBuffer(input)) return { subject: input };
2107
+ return { badInput: "input is not string or Buffer" };
2108
+ },
2109
+ raw: function (input) { return { subject: input }; },
2110
+ };
2111
+ Object.freeze(INPUT_CONTRACTS);
2112
+
2113
+ function resolveInputContract(contract) {
2114
+ if (typeof contract === "function") return contract;
2115
+ return Object.prototype.hasOwnProperty.call(INPUT_CONTRACTS, contract) ? INPUT_CONTRACTS[contract] : INPUT_CONTRACTS.text;
2116
+ }
2117
+
1450
2118
  /**
1451
2119
  * @primitive b.gateContract.runIssueValidator
1452
- * @signature b.gateContract.runIssueValidator(input, opts, detector)
2120
+ * @signature b.gateContract.runIssueValidator(input, opts, detector, contract?)
1453
2121
  * @since 0.7.5
1454
2122
  * @status stable
1455
2123
  * @related b.gateContract.aggregateIssues, b.gateContract.badInputResultIfNotStringOrBuffer
1456
2124
  *
1457
- * Boilerplate for guard-* `validate(input, opts)` entry points.
1458
- * Normalizes string-or-Buffer input to a UTF-8 string, returns the
1459
- * canonical `{ ok: false, issues: [{ kind: "bad-input", ... }] }` shape
1460
- * on type mismatch, otherwise calls the operator-supplied `detector`
1461
- * and aggregates its issues array. Result `ok` is `true` only when no
1462
- * detected issue is `critical` / `high` severity. Lets every guard's
1463
- * `validate()` body be identical scaffolding around the per-guard
1464
- * detector. The `opts` argument is forwarded verbatim as the second
1465
- * argument to `detector(text, opts)` its shape is detector-defined,
1466
- * not constrained by gate-contract.
2125
+ * The single `validate(input, opts)` engine for the whole guard family. An
2126
+ * input contract normalizes the raw input to the subject the detector expects
2127
+ * (or flags bad input), then the detector runs and its issues aggregate. One
2128
+ * engine spans every input shape in the family: `"text"` (the default) coerces
2129
+ * string / Buffer to UTF-8 and refuses anything else; `"raw"` hands the value
2130
+ * through so an object-bag or byte-level detector owns its own bad-input
2131
+ * (identical to `aggregateIssues(detector(input, opts))`); a guard with a
2132
+ * bespoke shape passes its own extractor `function(input) -> { subject } |
2133
+ * { badInput: message }`. Result `ok` is `true` only when no detected issue is
2134
+ * `critical` / `high` severity. The `opts` argument is forwarded verbatim as
2135
+ * the detector's second argument — its shape is detector-defined.
1467
2136
  *
1468
2137
  * @opts
1469
- * ...: any, // detector-defined; passed through to detector(text, opts)
2138
+ * ...: any, // detector-defined; passed through to detector(subject, opts)
1470
2139
  *
1471
2140
  * @example
1472
2141
  * function detectFormulaTrigger(text) {
@@ -1481,18 +2150,16 @@ function badInputResultIfNotStringOrBuffer(input) {
1481
2150
  * var ok = b.gateContract.runIssueValidator("ada,36", {}, detectFormulaTrigger);
1482
2151
  * ok.ok; // → true
1483
2152
  */
1484
- function runIssueValidator(input, opts, detector) {
1485
- var text = typeof input === "string"
1486
- ? input
1487
- : (Buffer.isBuffer(input) ? input.toString("utf8") : null);
1488
- if (text == null) {
2153
+ function runIssueValidator(input, opts, detector, contract) {
2154
+ var extracted = resolveInputContract(contract)(input);
2155
+ if (extracted && typeof extracted.badInput === "string") {
1489
2156
  return {
1490
2157
  ok: false,
1491
2158
  issues: [{ kind: "bad-input", severity: "high",
1492
- snippet: "input is not string or Buffer" }],
2159
+ snippet: extracted.badInput }],
1493
2160
  };
1494
2161
  }
1495
- return aggregateIssues(detector(text, opts));
2162
+ return aggregateIssues(detector(extracted.subject, opts));
1496
2163
  }
1497
2164
 
1498
2165
  /**
@@ -1910,8 +2577,10 @@ function _ctxValueForKind(kind, ctx, override) {
1910
2577
  * errorName: string, // defineClass name (mutually exclusive with errorClass)
1911
2578
  * errorClass: function, // pre-built FrameworkError subclass
1912
2579
  * profiles: object, // PROFILES (must include strict/balanced/permissive); required
1913
- * defaults: object, // DEFAULTS baseline (default profiles.strict)
1914
- * postures: object, // COMPLIANCE_POSTURES (default ALL_STRICT_POSTURES)
2580
+ * defaults: object, // DEFAULTS baseline (default profiles.strict, or strictDefaults(profiles, defaultsOverlay) when `base` is given)
2581
+ * postures: object, // COMPLIANCE_POSTURES (default ALL_STRICT_POSTURES, or compliancePostures(profiles, { base }) when `base` is given)
2582
+ * base: number, // forensic snippet budget — when given (and defaults/postures omitted), the factory derives both via strictDefaults + compliancePostures
2583
+ * defaultsOverlay: object, // per-guard default overrides merged into the derived strictDefaults (e.g. { maxRuntimeMs: ... }); only used with `base`
1915
2584
  * mimeTypes: string[], // content guards only
1916
2585
  * extensions: string[], // content guards only
1917
2586
  * integrationFixtures: object, // INTEGRATION_FIXTURES (consumed by host harness)
@@ -1953,9 +2622,6 @@ function defineGuard(spec) {
1953
2622
  }
1954
2623
  validateOpts.requireObject(spec.profiles, "gateContract.defineGuard: profiles",
1955
2624
  GateContractError);
1956
- if (typeof spec.validate !== "function") {
1957
- throw _err("gate-contract/bad-opt", "defineGuard: validate must be a function");
1958
- }
1959
2625
  if (spec.errorClass && spec.errorName) {
1960
2626
  throw _err("gate-contract/bad-opt",
1961
2627
  "defineGuard: pass errorClass OR errorName, not both");
@@ -1967,8 +2633,130 @@ function defineGuard(spec) {
1967
2633
  spec.name.charAt(0).toUpperCase() + spec.name.slice(1) + "Error"),
1968
2634
  { alwaysPermanent: true });
1969
2635
  var profiles = spec.profiles;
1970
- var defaults = spec.defaults || profiles.strict || {};
1971
- var postures = spec.postures || ALL_STRICT_POSTURES;
2636
+ // A guard may hand `defaults` / `postures` explicitly, OR pass `base` (the
2637
+ // forensic snippet budget) and let the factory derive the standard config —
2638
+ // `strictDefaults(profiles, defaultsOverlay)` + `compliancePostures(profiles,
2639
+ // { base })` — so the guard file needn't declare the two module-vars itself.
2640
+ var defaults = spec.defaults ||
2641
+ (typeof spec.base === "number" ? strictDefaults(profiles, spec.defaultsOverlay)
2642
+ : (profiles.strict || {}));
2643
+ var postures = spec.postures ||
2644
+ (typeof spec.base === "number" ? compliancePostures(profiles, { base: spec.base })
2645
+ : ALL_STRICT_POSTURES);
2646
+
2647
+ // Dynamic guard assembly — the upstream primitive absorbs the per-guard
2648
+ // binding wrappers. A guard may pass a raw `detect(input, opts) -> issues[]`
2649
+ // (the guard-specific detection logic) plus an optional `sanitizeTransform(
2650
+ // input, resolvedOpts) -> value`, instead of hand-rolling `_resolveOpts`,
2651
+ // `validate`, and the `sanitize` resolve→detect→throw boilerplate that every
2652
+ // guard otherwise duplicates. defineGuard already owns the profile/posture/
2653
+ // defaults/errorClass/prefix, so it binds the resolver here and builds
2654
+ // validate + sanitize. Behaviour matches the hand-written wrappers exactly:
2655
+ // validate runs detect on the RAW opts (detect resolves what it needs);
2656
+ // sanitize resolves first, then detect → throwOnRefusalSeverity → transform.
2657
+ // The bound profile/posture resolver — built once from the spec's binding
2658
+ // config (profiles/postures/defaults/errorClass/prefix) and EXPOSED on the
2659
+ // guard as `resolveOpts` (below), so a bespoke gate calls
2660
+ // `module.exports.resolveOpts(opts)` instead of each guard hand-rolling the
2661
+ // identical `function _resolveOpts(o){ return resolveProfileAndPosture(o,
2662
+ // {...}) }` binding wrapper. The binding config lives in ONE place.
2663
+ var _resolveGuardOpts = function (o) {
2664
+ return resolveProfileAndPosture(o || {}, {
2665
+ profiles: profiles,
2666
+ compliancePostures: postures,
2667
+ defaults: defaults,
2668
+ errorClass: ErrorClass,
2669
+ errCodePrefix: prefix,
2670
+ });
2671
+ };
2672
+ if (typeof spec.detect === "function") {
2673
+ var intOpts = Array.isArray(spec.intOpts) ? spec.intOpts.slice() : null;
2674
+ if (typeof spec.validate !== "function") {
2675
+ spec.validate = function (input, opts) {
2676
+ var resolved = _resolveGuardOpts(opts);
2677
+ if (intOpts) {
2678
+ numericBounds.requireAllPositiveFiniteIntIfPresent(resolved, intOpts,
2679
+ spec.name + ".validate", ErrorClass, prefix + ".bad-opt");
2680
+ }
2681
+ // One engine, the guard's input contract picks the shape. Default
2682
+ // "raw" reproduces the historical aggregateIssues(detect(input)) — the
2683
+ // detector owns its own bad-input (object-bag guards image/pdf, the
2684
+ // byte-level guards). A guard whose detector takes a string but does
2685
+ // not type-check it (e.g. csv returns [] on a non-string) sets
2686
+ // inputContract: "text" so non-text input is refused as bad-input.
2687
+ return runIssueValidator(input, resolved, spec.detect,
2688
+ spec.inputContract || "raw");
2689
+ };
2690
+ }
2691
+ if (typeof spec.sanitizeTransform === "function" && typeof spec.sanitize !== "function") {
2692
+ // spec.sanitizeSeverities narrows which severities REFUSE (throw) vs are
2693
+ // stripped/repaired by sanitizeTransform. Default ['critical','high'];
2694
+ // a guard that repairs high-severity findings and refuses only the
2695
+ // unrepairable critical shapes (markdown / email / xml / yaml) passes
2696
+ // ['critical'] so the generated sanitize matches its hand-written one.
2697
+ // An empty array means "strip unconditionally, never refuse" (csv / text
2698
+ // best-effort scrubbers, whose sanitize never throws on a detected issue).
2699
+ var sanitizeSeverities = Array.isArray(spec.sanitizeSeverities)
2700
+ ? spec.sanitizeSeverities.slice() : null;
2701
+ var refusesOnDetect = sanitizeSeverities === null || sanitizeSeverities.length > 0;
2702
+ // spec.sanitizeAmplificationCap (a string = the resolved-opts field name
2703
+ // carrying the max growth ratio) opts the guard into the "sanitize must
2704
+ // shrink, never grow" post-condition: the transform runs on extracted
2705
+ // text and the output length is capped at ratio×input. Used by the text
2706
+ // scrubbers (csv / text) whose hand-written sanitize threw
2707
+ // `<prefix>.sanitize-amplified`. When unset, sanitize keeps the raw input
2708
+ // (binary passthrough guards image / pdf must not be utf8-decoded).
2709
+ var ampCapField = typeof spec.sanitizeAmplificationCap === "string"
2710
+ ? spec.sanitizeAmplificationCap : null;
2711
+ spec.sanitize = function (input, opts) {
2712
+ var resolved = _resolveGuardOpts(opts);
2713
+ var subject = input;
2714
+ if (ampCapField) {
2715
+ // Same text contract the validate engine uses — string/Buffer→text,
2716
+ // refuse anything else (here as a throw, sanitize's contract).
2717
+ var extracted = INPUT_CONTRACTS.text(input);
2718
+ if (extracted.badInput) {
2719
+ throw ErrorClass.factory(prefix + ".bad-input",
2720
+ "sanitize requires string or Buffer input");
2721
+ }
2722
+ subject = extracted.subject;
2723
+ }
2724
+ if (refusesOnDetect) {
2725
+ var issues = spec.detect(subject, resolved);
2726
+ // A `bad-input` issue means the input is UNPROCESSABLE (wrong type /
2727
+ // shape), not a content finding — a scrubber must never let it slip
2728
+ // into the transform (which would return the garbage verbatim). So it
2729
+ // refuses ALWAYS, independent of which CONTENT severities this guard's
2730
+ // sanitize tolerates via sanitizeSeverities. (csv/text reach the same
2731
+ // refusal earlier through the ampCapField text contract above.)
2732
+ for (var bi = 0; bi < issues.length; bi += 1) {
2733
+ if (issues[bi].kind === "bad-input") {
2734
+ throw ErrorClass.factory(prefix + ".bad-input",
2735
+ issues[bi].snippet || "sanitize: input is not processable");
2736
+ }
2737
+ }
2738
+ var throwOpts = { errorClass: ErrorClass, codePrefix: prefix };
2739
+ if (sanitizeSeverities) throwOpts.severities = sanitizeSeverities;
2740
+ throwOnRefusalSeverity(issues, throwOpts);
2741
+ }
2742
+ var out = spec.sanitizeTransform(subject, resolved);
2743
+ if (ampCapField) {
2744
+ var cap = resolved[ampCapField];
2745
+ if (typeof cap === "number") {
2746
+ var amp = out.length / Math.max(subject.length, 1);
2747
+ if (amp > cap) {
2748
+ throw ErrorClass.factory(prefix + ".sanitize-amplified",
2749
+ "sanitize grew output " + amp.toFixed(2) + "x; cap " + cap);
2750
+ }
2751
+ }
2752
+ }
2753
+ return out;
2754
+ };
2755
+ }
2756
+ }
2757
+ if (typeof spec.validate !== "function") {
2758
+ throw _err("gate-contract/bad-opt", "defineGuard: validate (or detect) must be a function");
2759
+ }
1972
2760
 
1973
2761
  var buildProfileFn = makeProfileBuilder(profiles);
1974
2762
  function compliancePostureFn(name) {
@@ -2012,12 +2800,7 @@ function defineGuard(spec) {
2012
2800
  var value = _ctxValueForKind(spec.kind, ctx, ctxFields);
2013
2801
  if (!value) return { ok: true, action: "serve" };
2014
2802
  var rv = spec.validate(value, opts);
2015
- if (!rv.issues || rv.issues.length === 0) return { ok: true, action: "serve" };
2016
- var hasBlocking = rv.issues.some(function (i) {
2017
- return i.severity === "critical" || i.severity === "high";
2018
- });
2019
- if (!hasBlocking) return { ok: true, action: "audit-only", issues: rv.issues };
2020
- return { ok: false, action: "refuse", issues: rv.issues };
2803
+ return severityDisposition(rv.issues || []);
2021
2804
  };
2022
2805
  return buildGuardGate(
2023
2806
  opts.name || (gateNamePrefix + ":" + (opts.profile || "default")),
@@ -2031,6 +2814,7 @@ function defineGuard(spec) {
2031
2814
  NAME: spec.name,
2032
2815
  KIND: spec.kind,
2033
2816
  validate: spec.validate,
2817
+ resolveOpts: _resolveGuardOpts,
2034
2818
  buildProfile: buildProfileFn,
2035
2819
  compliancePosture: compliancePostureFn,
2036
2820
  loadRulePack: rulePacks.load,
@@ -2259,6 +3043,81 @@ function defineParser(spec) {
2259
3043
  * decision.action; // → "serve" | "refuse" | …
2260
3044
  */
2261
3045
 
3046
+ /**
3047
+ * @abiTemplate defineGuard
3048
+ * @method validate
3049
+ * @signature b.{NS}.validate(input, opts?)
3050
+ * @status stable
3051
+ * @related b.{NS}.gate, b.{NS}.sanitize
3052
+ *
3053
+ * Inspect `input` under a resolved profile + compliance posture and return a
3054
+ * structured result `{ ok, issues }` WITHOUT throwing — `ok` is false when any
3055
+ * `high` / `critical` issue fired, and `issues` lists every finding (kind,
3056
+ * severity, ruleId, snippet). `opts` selects the `profile` /
3057
+ * `compliancePosture`; omitted opts use this guard's default profile. Wired by
3058
+ * `gateContract.defineGuard` from the guard's detection logic through
3059
+ * `gateContract.aggregateIssues`, so the result shape and severity gating are
3060
+ * identical across the guard family.
3061
+ *
3062
+ * @opts
3063
+ * profile: string, // one of PROFILES; default this guard's default
3064
+ * compliancePosture: string, // overlay one of hipaa/pci-dss/gdpr/soc2
3065
+ *
3066
+ * @example
3067
+ * var rv = b.{NS}.validate(input, { profile: "strict" });
3068
+ * rv.ok; // → true | false
3069
+ * rv.issues; // → [ { kind, severity, … }, … ]
3070
+ */
3071
+
3072
+ /**
3073
+ * @abiTemplate defineGuard
3074
+ * @method sanitize
3075
+ * @signature b.{NS}.sanitize(input, opts?)
3076
+ * @status stable
3077
+ * @related b.{NS}.validate, b.{NS}.gate
3078
+ *
3079
+ * Return a normalized form of `input` when no `high` / `critical` issue fires;
3080
+ * throw `{ERR}` on any such refusal (best-effort repair, never a silent pass).
3081
+ * Resolves the profile + posture, runs the guard's detection, throws via
3082
+ * `gateContract.throwOnRefusalSeverity` on a refusal, then applies the guard's
3083
+ * own safe transform. Wired by `gateContract.defineGuard`, so the
3084
+ * resolve → detect → throw → transform order is identical across the family; a
3085
+ * guard with no safe transform ships no `sanitize`.
3086
+ *
3087
+ * @opts
3088
+ * profile: string, // one of PROFILES; default this guard's default
3089
+ * compliancePosture: string, // overlay one of hipaa/pci-dss/gdpr/soc2
3090
+ *
3091
+ * @example
3092
+ * var safe = b.{NS}.sanitize(input, { profile: "permissive" });
3093
+ * safe; // → normalized value
3094
+ */
3095
+
3096
+ /**
3097
+ * @abiTemplate defineGuard
3098
+ * @method resolveOpts
3099
+ * @signature b.{NS}.resolveOpts(opts?)
3100
+ * @status stable
3101
+ * @related b.{NS}.validate, b.{NS}.gate
3102
+ *
3103
+ * Resolve caller `opts` against this guard's `PROFILES` + compliance-posture
3104
+ * overlays into the fully-defaulted option set the guard runs on — the same
3105
+ * resolution `validate` / `sanitize` / `gate` apply internally. Wired by
3106
+ * `gateContract.defineGuard` from the guard's binding config (profiles /
3107
+ * postures / defaults / error class), so a guard's bespoke `gate` calls
3108
+ * `resolveOpts` instead of re-declaring the per-guard resolver wrapper. Throws
3109
+ * `{ERR}` with code `"{CODE}.bad-opt"` / `"{CODE}.bad-posture"` on an unknown
3110
+ * profile or posture name.
3111
+ *
3112
+ * @opts
3113
+ * profile: string, // one of PROFILES; default this guard's default
3114
+ * compliancePosture: string, // overlay one of hipaa/pci-dss/gdpr/soc2
3115
+ *
3116
+ * @example
3117
+ * var resolved = b.{NS}.resolveOpts({ profile: "strict" });
3118
+ * resolved.profile; // → "strict"
3119
+ */
3120
+
2262
3121
  /**
2263
3122
  * @abiTemplate defineParser
2264
3123
  * @method compliancePosture
@@ -2300,12 +3159,25 @@ module.exports = {
2300
3159
  resolveProfileAndPosture: resolveProfileAndPosture,
2301
3160
  runIssueValidator: runIssueValidator,
2302
3161
  buildGuardGate: buildGuardGate,
3162
+ severityDisposition: severityDisposition,
3163
+ buildContentGate: buildContentGate,
3164
+ policyDisposition: policyDisposition,
3165
+ charThreatDisposition: charThreatDisposition,
2303
3166
  extractBytesAsText: extractBytesAsText,
2304
3167
  lookupCompliancePosture: lookupCompliancePosture,
3168
+ makePostureAccessor: makePostureAccessor,
2305
3169
  ALL_STRICT_POSTURES: ALL_STRICT_POSTURES,
3170
+ CHAR_THREATS_REJECT_ALL: CHAR_THREATS_REJECT_ALL,
3171
+ DANGEROUS_URL_SCHEMES: DANGEROUS_URL_SCHEMES,
3172
+ SAFE_URL_SCHEMES: SAFE_URL_SCHEMES,
3173
+ identifierFixtures: identifierFixtures,
3174
+ compliancePostures: compliancePostures,
3175
+ strictDefaults: strictDefaults,
3176
+ detectStringInput: detectStringInput,
2306
3177
  makeRulePackLoader: makeRulePackLoader,
2307
3178
  makeProfileBuilder: makeProfileBuilder,
2308
3179
  makeProfileResolver: makeProfileResolver,
3180
+ resolveProfileName: resolveProfileName,
2309
3181
  throwOnRefusalSeverity: throwOnRefusalSeverity,
2310
3182
  badInputResultIfNotStringOrBuffer: badInputResultIfNotStringOrBuffer,
2311
3183
  aggregateIssues: aggregateIssues,