@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
@@ -1,61 +1,48 @@
1
1
  "use strict";
2
2
  /**
3
- * gate-contract — uniform composition contract for the safe-* primitive
4
- * family.
5
- *
6
- * Every safe-* primitive (b.safeCsv / b.safeHtml / b.safeLink / b.safeMime
7
- * / b.safeFilename / etc.) ships a `.gate(opts)` factory that returns the
8
- * shape defined here. Host primitives (b.staticServe / b.fileUpload /
9
- * b.mail / b.objectStore / b.notify / b.audit / etc.) call gate.check()
10
- * at their byte-boundary moment with a uniform context.
11
- *
12
- * The gate decision is captured as:
13
- *
14
- * { ok, action, sanitized?, issues, contentTypeOverride?, headers?,
15
- * forensicHash, forensicSnapshot?, runtimeMs, cacheKey? }
16
- *
17
- * Operator extension surface every primitive inherits these patterns
18
- * from this module (configuration uniform across the family):
19
- *
20
- * - Profile composition (extends + overrides + removes; cycle detection)
21
- * - Hook system (beforeCheck / afterCheck / onIssue / onSanitize / onRefuse / onAudit)
22
- * - Mode posture (enforce / warn-only / shadow / audit-only / log-only / canary)
23
- * - Versioned policies (version + ruleHash) with policyDiff helper
24
- * - Forensic snapshot store (operator-supplied evidence vault)
25
- * - Decision cache (per-forensicHash memoization)
26
- * - Runtime cap with timeout
27
- * - Sandbox isolation (in-process / worker-thread / child-process)
28
- * - Threat-intelligence feed integration
29
- * - Compliance posture pre-sets
30
- *
31
- * Host-side helpers exported here:
32
- *
33
- * runGate(gate, ctx, opts?) — execute single gate with timeout
34
- * composeGates([g1, g2, ...], opts?) — chain; first refusal wins
35
- * multiplexGates({ ext: gate, ... }) — file-extension dispatch
36
- * contentTypeMux({ mime: gate, ... }) — Content-Type dispatch
37
- * byActorTier({ tier: gate, ... }) — actor-tier dispatch
38
- * byRoute({ pattern: gate, ... }) — route-pattern dispatch
39
- * byDirection({ inbound, outbound }) — direction-aware dispatch
40
- * shadowMode(primary, candidate) — A/B compare; emit divergence
41
- * canaryGate(gate, { rate }) — N% rollout
42
- * cachingGate(gate, { backend, ttlMs }) — memoize per-forensicHash
43
- * workerThreadGate(gate, { worker }) — offload to worker
44
- * validateGateShape(g, label, errClass) — schema check at wire-up
45
- * buildProfile({ baseProfile, extends, overrides, removes })
46
- * composeHooks(hooks) — chain operator hooks
47
- *
48
- * Module-level constants:
49
- *
50
- * ACTIONS — allowed action enum
51
- * MODES — allowed mode enum
52
- * ISSUE_SEVERITIES — allowed severity enum
53
- *
54
- * The gate contract is the foundation of the guard-* family. Every
55
- * content-safety primitive that ships in the family composes through it
56
- * (b.guardCsv, future b.guardHtml / b.guardSvg / etc.). b.guardAll then
57
- * aggregates the registered guards into a single security-on-by-default
58
- * gate with operator opt-out via exceptFor.
3
+ * @module b.gateContract
4
+ * @nav Guards
5
+ * @title Gate Contract
6
+ *
7
+ * @intro
8
+ * Shared substrate every `b.guard*` primitive composes against
9
+ * `resolveProfileAndPosture`, `makeProfileBuilder`,
10
+ * `makeRulePackLoader`, `lookupCompliancePosture`, `buildGuardGate`,
11
+ * `aggregateIssues`, `extractBytesAsText`. The contract every guard
12
+ * implements; ensures the action vocabulary
13
+ * (`serve` / `sanitize` / `refuse` / `audit-only`) and the
14
+ * profile-and-posture resolution shape stay identical across the
15
+ * family.
16
+ *
17
+ * Every guard ships a `.gate(opts)` factory returning the shape
18
+ * defined here. Host primitives (`b.staticServe` / `b.fileUpload` /
19
+ * `b.mail` / `b.objectStore`) call `gate.check(ctx)` at their
20
+ * byte-boundary moment with a uniform context. The decision shape
21
+ * is `{ ok, action, sanitized?, issues, contentTypeOverride?,
22
+ * headers?, forensicHash, forensicSnapshot?, runtimeMs, cacheKey? }`.
23
+ *
24
+ * Operator extension surface inherited by every member:
25
+ *
26
+ * - Profile composition (extends + overrides + removes; cycle detection)
27
+ * - Hook system (beforeCheck / afterCheck / onIssue / onSanitize / onRefuse / onAudit)
28
+ * - Mode posture (enforce / warn-only / shadow / audit-only / log-only / canary)
29
+ * - Versioned policies (version + ruleHash) with policyDiff helper
30
+ * - Forensic snapshot store (operator-supplied evidence vault)
31
+ * - Decision cache (per-forensicHash memoization)
32
+ * - Runtime cap with timeout
33
+ * - Compliance-posture pre-sets (hipaa / pci-dss / gdpr / soc2)
34
+ *
35
+ * Module-level constants `ACTIONS` / `MODES` / `ISSUE_SEVERITIES`
36
+ * carry the frozen enums every guard validates against.
37
+ *
38
+ * Foundation for the guard-* family. Every content-safety primitive
39
+ * shipped under `b.guard*` composes through `buildGuardGate`,
40
+ * `makeProfileBuilder`, `makeRulePackLoader`, and
41
+ * `lookupCompliancePosture`; `b.guardAll` aggregates the registered
42
+ * guards into a single security-on-by-default gate.
43
+ *
44
+ * @card
45
+ * Shared substrate every `b.guard*` primitive composes against — `resolveProfileAndPosture`, `makeProfileBuilder`, `makeRulePackLoader`, `lookupCompliancePosture`, `buildGuardGate`, `aggregateIssues`, `extractBytesAsText`.
59
46
  */
60
47
 
61
48
  var C = require("./constants");
@@ -77,10 +64,53 @@ var FINGERPRINT_HEX_LENGTH = C.BYTES.bytes(16);
77
64
  // Default cachingGate TTL when operator doesn't supply one.
78
65
  var DEFAULT_CACHE_TTL_MS = C.TIME.minutes(5);
79
66
 
67
+ /**
68
+ * @primitive b.gateContract.GateContractError
69
+ * @signature b.gateContract.GateContractError
70
+ * @since 0.7.5
71
+ * @status stable
72
+ * @related b.gateContract.defineGate, b.gateContract.validateGateShape
73
+ *
74
+ * FrameworkError subclass thrown by gate-contract entry points on
75
+ * shape violations: `gate-contract/bad-shape` from
76
+ * `validateGateShape`, `gate-contract/bad-opt` from `defineGate` /
77
+ * `cachingGate` / `workerThreadGate`, `gate-contract/profile-cycle`
78
+ * and `gate-contract/unknown-profile` from `buildProfile`.
79
+ * `alwaysPermanent` — never retried by `b.retry`.
80
+ *
81
+ * @example
82
+ * try {
83
+ * b.gateContract.validateGateShape({}, "broken");
84
+ * } catch (e) {
85
+ * e instanceof b.gateContract.GateContractError; // → true
86
+ * e.code; // → "gate-contract/bad-shape"
87
+ * }
88
+ */
80
89
  var _err = GateContractError.factory;
81
90
 
82
91
  // ---- Enumerations (module-level constants) ----
83
92
 
93
+ /**
94
+ * @primitive b.gateContract.ACTIONS
95
+ * @signature b.gateContract.ACTIONS
96
+ * @since 0.7.5
97
+ * @status stable
98
+ * @related b.gateContract.MODES, b.gateContract.defineGate
99
+ *
100
+ * Frozen list of every action a gate decision is allowed to set.
101
+ * `serve` emits the bytes unchanged; `refuse` rejects with an
102
+ * operator-meaningful error; `sanitize` substitutes `decision.sanitized`
103
+ * for the original bytes; `strip` removes the offending content;
104
+ * `audit-only` serves but emits an audit entry; `warn` serves and
105
+ * emits a warning counter; `challenge-mfa` triggers step-up auth
106
+ * before serving; `deny-and-revoke` rejects and invalidates the
107
+ * actor's session.
108
+ *
109
+ * @example
110
+ * b.gateContract.ACTIONS.indexOf("serve"); // → 0
111
+ * b.gateContract.ACTIONS.indexOf("warp-speed"); // → -1
112
+ * Object.isFrozen(b.gateContract.ACTIONS); // → true
113
+ */
84
114
  var ACTIONS = Object.freeze([
85
115
  "serve", // host emits the bytes as-is
86
116
  "refuse", // host rejects with operator-meaningful error
@@ -92,6 +122,24 @@ var ACTIONS = Object.freeze([
92
122
  "deny-and-revoke", // host rejects + invalidates the actor's session
93
123
  ]);
94
124
 
125
+ /**
126
+ * @primitive b.gateContract.MODES
127
+ * @signature b.gateContract.MODES
128
+ * @since 0.7.5
129
+ * @status stable
130
+ * @related b.gateContract.ACTIONS, b.gateContract.defineGate
131
+ *
132
+ * Frozen list of mode-posture values a gate can run in. `enforce`
133
+ * honors the decision; `warn-only` translates every `refuse` to
134
+ * `warn` for staged rollout; `shadow` runs alongside a primary and
135
+ * never refuses (observability-only); `audit-only` and `log-only`
136
+ * emit an audit entry but never block; `canary` enforces on a
137
+ * sampled subset and warns on the rest.
138
+ *
139
+ * @example
140
+ * b.gateContract.MODES.indexOf("enforce"); // → 0
141
+ * b.gateContract.MODES.indexOf("yolo"); // → -1
142
+ */
95
143
  var MODES = Object.freeze([
96
144
  "enforce", // gate decision honored
97
145
  "warn-only", // gate emits but never refuses (staged rollout)
@@ -101,6 +149,22 @@ var MODES = Object.freeze([
101
149
  "canary", // enforce on N% of requests; warn on the rest
102
150
  ]);
103
151
 
152
+ /**
153
+ * @primitive b.gateContract.ISSUE_SEVERITIES
154
+ * @signature b.gateContract.ISSUE_SEVERITIES
155
+ * @since 0.7.5
156
+ * @status stable
157
+ * @related b.gateContract.aggregateIssues, b.gateContract.summarizeIssues
158
+ *
159
+ * Frozen list of severity levels a guard issue may carry. `info` and
160
+ * `warn` are observability-only — `aggregateIssues` keeps `ok: true`
161
+ * with them present. `high` and `critical` flip the result to
162
+ * `ok: false`, refusing the input.
163
+ *
164
+ * @example
165
+ * b.gateContract.ISSUE_SEVERITIES; // → ["info","warn","high","critical"]
166
+ * b.gateContract.ISSUE_SEVERITIES.indexOf("critical"); // → 3
167
+ */
104
168
  var ISSUE_SEVERITIES = Object.freeze([
105
169
  "info",
106
170
  "warn",
@@ -108,13 +172,33 @@ var ISSUE_SEVERITIES = Object.freeze([
108
172
  "critical",
109
173
  ]);
110
174
 
111
- // ---- validateGateShape ----
112
- //
113
- // Throws if `gate` doesn't satisfy the contract. Operator-supplied gates
114
- // (or framework-supplied gates with operator-toggled hooks) all flow
115
- // through this check at host-primitive wire-up time. Shape errors at
116
- // boot are far cheaper than at request time.
117
-
175
+ /**
176
+ * @primitive b.gateContract.validateGateShape
177
+ * @signature b.gateContract.validateGateShape(gate, label, errorClass)
178
+ * @since 0.7.5
179
+ * @status stable
180
+ * @related b.gateContract.defineGate, b.gateContract.runGate
181
+ *
182
+ * Throws when `gate` does not satisfy the contract — `gate.check`
183
+ * must be a function, `gate.mode` (when present) must be one of the
184
+ * `MODES` enum values, and `gate.metrics` / `gate.close` (when
185
+ * present) must be functions. Operator-supplied gates (and
186
+ * framework-supplied gates with operator-toggled hooks) all flow
187
+ * through this check at host-primitive wire-up time. Shape errors at
188
+ * boot are cheaper than at request time. Returns `gate` unchanged on
189
+ * success.
190
+ *
191
+ * @example
192
+ * var gate = b.guardCsv.gate({ profile: "strict" });
193
+ * b.gateContract.validateGateShape(gate, "uploads.csv");
194
+ * // → returns the gate unchanged when shape is valid
195
+ *
196
+ * try {
197
+ * b.gateContract.validateGateShape({}, "broken");
198
+ * } catch (e) {
199
+ * e.code; // → "gate-contract/bad-shape"
200
+ * }
201
+ */
118
202
  function validateGateShape(gate, label, errorClass) {
119
203
  errorClass = errorClass || GateContractError;
120
204
  label = label || "gate";
@@ -142,23 +226,60 @@ function validateGateShape(gate, label, errorClass) {
142
226
  return gate;
143
227
  }
144
228
 
145
- // ---- defineGate factory ----
146
- //
147
- // Primitives use this to build their .gate(opts) implementation. Wraps
148
- // the operator's check() with the cross-cutting concerns (hooks /
149
- // observability / forensic snapshot / runtime cap / cache). Returns a
150
- // gate that satisfies validateGateShape.
151
- //
152
- // defineGate({
153
- // name: "safeCsv:strict",
154
- // version: "1.0.0",
155
- // mode: "enforce",
156
- // check: async (ctx) => decision,
157
- // beforeCheck, afterCheck, onIssue, onSanitize, onRefuse, onAudit,
158
- // audit, observability, forensicEvidenceStore, forensicSnippetBytes,
159
- // cache, cacheTtlMs, maxRuntimeMs, ruleHash,
160
- // }) gate
161
-
229
+ /**
230
+ * @primitive b.gateContract.defineGate
231
+ * @signature b.gateContract.defineGate(opts)
232
+ * @since 0.7.5
233
+ * @status stable
234
+ * @related b.gateContract.buildGuardGate, b.gateContract.validateGateShape
235
+ *
236
+ * Build a gate that satisfies the contract. Wraps the
237
+ * operator-supplied `check(ctx)` with the cross-cutting concerns
238
+ * (hooks, observability, forensic snapshot, runtime cap, decision
239
+ * cache, mode-posture translation) so guards only write the per-guard
240
+ * inspection logic. Returns a gate exposing
241
+ * `{ check, mode, audit, observability, metrics, reset, close, name,
242
+ * version, ruleHash, dryRun, policyDiff }`. Most guards forward through
243
+ * `b.gateContract.buildGuardGate` instead — `defineGate` is the lower-level
244
+ * factory used by host-side composers (`composeGates` / `byRoute` / etc.).
245
+ *
246
+ * @opts
247
+ * name: string, // identifier surfaced in audit / counters
248
+ * version: string, // semver default "1.0.0"
249
+ * mode: string, // one of MODES; default "enforce"
250
+ * check: function, // async (ctx) → decision
251
+ * beforeCheck: function|null, // (ctx) → { skip?, transform? }
252
+ * afterCheck: function|null, // (ctx, decision) → decision
253
+ * onIssue: function|null, // (issue, ctx) → issue|{suppress|promote}
254
+ * onSanitize: function|null, // (bytes, sanitized, ctx) → sanitized
255
+ * onRefuse: function|null, // (ctx, decision) → void
256
+ * onAudit: function|null, // (entry) → entry|false (false suppresses)
257
+ * audit: object|null, // b.audit handle
258
+ * observability: object|null, // b.observability handle
259
+ * forensicEvidenceStore: object|null, // { write({ ... }) }
260
+ * forensicSnippetBytes: number, // 0 = disabled
261
+ * cache: object|null, // b.cache shape
262
+ * cacheTtlMs: number,
263
+ * maxRuntimeMs: number, // 0 = uncapped
264
+ * ruleHash: string, // override fingerprint
265
+ *
266
+ * @example
267
+ * var gate = b.gateContract.defineGate({
268
+ * name: "tenant:csv:strict",
269
+ * mode: "enforce",
270
+ * maxRuntimeMs: 250,
271
+ * check: async function (ctx) {
272
+ * var text = b.gateContract.extractBytesAsText(ctx);
273
+ * if (text.indexOf("=cmd|") === 0) {
274
+ * return { ok: false, action: "refuse",
275
+ * issues: [{ kind: "csv.formula-injection", severity: "high" }] };
276
+ * }
277
+ * return { ok: true, action: "serve" };
278
+ * },
279
+ * });
280
+ * var d = await gate.check({ bytes: Buffer.from("name,age\nada,36") });
281
+ * d.action; // → "serve"
282
+ */
162
283
  function defineGate(opts) {
163
284
  validateOpts.requireObject(opts, "gateContract.defineGate", GateContractError);
164
285
  validateOpts.requireNonEmptyString(opts.name, "gateContract.defineGate: name", GateContractError, "gate-contract/bad-opt");
@@ -439,13 +560,66 @@ function _hashFingerprint(obj) {
439
560
 
440
561
  // ---- Host-side helpers ----
441
562
 
563
+ /**
564
+ * @primitive b.gateContract.runGate
565
+ * @signature b.gateContract.runGate(gate, ctx, opts?)
566
+ * @since 0.7.5
567
+ * @status stable
568
+ * @related b.gateContract.defineGate, b.gateContract.composeGates
569
+ *
570
+ * Execute a gate's `check(ctx)` and return its decision. When `gate`
571
+ * is null or has no `check` function, returns the canonical
572
+ * `{ ok: true, action: "serve" }` shape — host primitives invoke
573
+ * `runGate` with an operator-configurable gate that may legitimately
574
+ * be unset. The `opts` argument is reserved for future host-side
575
+ * cross-cutting concerns and is currently unused.
576
+ *
577
+ * @opts
578
+ * // reserved for future host-side cross-cutting concerns; pass `{}` today
579
+ *
580
+ * @example
581
+ * var gate = b.guardCsv.gate({ profile: "strict" });
582
+ * var decision = await b.gateContract.runGate(gate, {
583
+ * bytes: Buffer.from("name,age\nada,36"),
584
+ * route: "/api/imports",
585
+ * filename: "people.csv",
586
+ * });
587
+ * decision.action; // → "serve"
588
+ *
589
+ * // Unset gate is a no-op serve.
590
+ * (await b.gateContract.runGate(null, {})).action; // → "serve"
591
+ */
442
592
  async function runGate(gate, ctx, opts) {
443
593
  opts = opts || {};
444
594
  if (!gate || typeof gate.check !== "function") return _build({ ok: true, action: "serve" });
445
595
  return await gate.check(ctx);
446
596
  }
447
597
 
448
- // composeGates — chain a list of gates left-to-right; first refusal wins.
598
+ /**
599
+ * @primitive b.gateContract.composeGates
600
+ * @signature b.gateContract.composeGates(gates, opts?)
601
+ * @since 0.7.5
602
+ * @status stable
603
+ * @related b.gateContract.multiplexGates, b.gateContract.contentTypeMux
604
+ *
605
+ * Chain a list of gates left-to-right. First refusal wins. When a
606
+ * gate returns `action: "sanitize"` and `firstRefusalWins` is true
607
+ * (default), the sanitized bytes feed into the next gate's context —
608
+ * letting an HTML sanitizer hand its scrubbed output to a downstream
609
+ * link-shape guard. Returns a wrapping gate that satisfies the
610
+ * contract (so the composition is itself composable).
611
+ *
612
+ * @opts
613
+ * name: string, // wrapper gate name (default "composed")
614
+ * firstRefusalWins: boolean, // default true
615
+ *
616
+ * @example
617
+ * var bidi = b.guardCsv.gate({ profile: "strict" });
618
+ * var pii = b.guardCsv.gate({ compliancePosture: "hipaa" });
619
+ * var chain = b.gateContract.composeGates([bidi, pii], { name: "csv:chain" });
620
+ * var d = await chain.check({ bytes: Buffer.from("name,ssn\nada,123-45-6789") });
621
+ * d.action; // → "refuse"
622
+ */
449
623
  function composeGates(gates, opts) {
450
624
  opts = opts || {};
451
625
  var firstRefusalWins = opts.firstRefusalWins !== false;
@@ -464,7 +638,32 @@ function composeGates(gates, opts) {
464
638
  });
465
639
  }
466
640
 
467
- // multiplexGates — extension-keyed dispatch.
641
+ /**
642
+ * @primitive b.gateContract.multiplexGates
643
+ * @signature b.gateContract.multiplexGates(gateMap, opts?)
644
+ * @since 0.7.5
645
+ * @status stable
646
+ * @related b.gateContract.contentTypeMux, b.gateContract.composeGates
647
+ *
648
+ * File-extension-keyed gate dispatch. Looks at `ctx.filename`,
649
+ * extracts the lowercased final extension (`.csv` / `.html` / etc.),
650
+ * and dispatches to the matching gate. The `"default"` key serves as
651
+ * the fallback; missing entries (no key match, no fallback) return
652
+ * the canonical serve decision so host primitives can wire a single
653
+ * mux gate without per-extension special-casing.
654
+ *
655
+ * @opts
656
+ * name: string, // wrapper gate name (default "multiplex")
657
+ *
658
+ * @example
659
+ * var mux = b.gateContract.multiplexGates({
660
+ * ".csv": b.guardCsv.gate({ profile: "strict" }),
661
+ * ".html": b.guardHtml.gate({ profile: "strict" }),
662
+ * "default": b.guardCsv.gate({ profile: "permissive" }),
663
+ * });
664
+ * var d = await mux.check({ bytes: Buffer.from("a,b\n1,2"), filename: "x.csv" });
665
+ * d.action; // → "serve"
666
+ */
468
667
  function multiplexGates(gateMap, opts) {
469
668
  opts = opts || {};
470
669
  var lookup = Object.create(null);
@@ -484,8 +683,34 @@ function multiplexGates(gateMap, opts) {
484
683
  });
485
684
  }
486
685
 
487
- // contentTypeMux — Content-Type-keyed dispatch. Match on the bare type
488
- // (strip parameters like `; charset=utf-8`).
686
+ /**
687
+ * @primitive b.gateContract.contentTypeMux
688
+ * @signature b.gateContract.contentTypeMux(gateMap, opts?)
689
+ * @since 0.7.5
690
+ * @status stable
691
+ * @related b.gateContract.multiplexGates, b.gateContract.composeGates
692
+ *
693
+ * Content-Type-keyed gate dispatch. Reads `ctx.contentType`, strips
694
+ * parameters (`; charset=utf-8`), lowercases, and routes to the
695
+ * matching gate. The `"default"` key is the fallback; unknown types
696
+ * (no key match, no fallback) serve uninspected. Useful when one
697
+ * route accepts multiple media types and each needs its own guard.
698
+ *
699
+ * @opts
700
+ * name: string, // wrapper gate name (default "contentTypeMux")
701
+ *
702
+ * @example
703
+ * var mux = b.gateContract.contentTypeMux({
704
+ * "text/csv": b.guardCsv.gate({ profile: "strict" }),
705
+ * "text/html": b.guardHtml.gate({ profile: "strict" }),
706
+ * "default": b.guardCsv.gate({ profile: "permissive" }),
707
+ * });
708
+ * var d = await mux.check({
709
+ * bytes: Buffer.from("name,age\nada,36"),
710
+ * contentType: "text/csv; charset=utf-8",
711
+ * });
712
+ * d.action; // → "serve"
713
+ */
489
714
  function contentTypeMux(gateMap, opts) {
490
715
  opts = opts || {};
491
716
  var lookup = Object.create(null);
@@ -503,7 +728,35 @@ function contentTypeMux(gateMap, opts) {
503
728
  });
504
729
  }
505
730
 
506
- // byActorTier — actor-tier-keyed dispatch (free / paid / admin).
731
+ /**
732
+ * @primitive b.gateContract.byActorTier
733
+ * @signature b.gateContract.byActorTier(gateMap, opts?)
734
+ * @since 0.7.5
735
+ * @status stable
736
+ * @related b.gateContract.byRoute, b.gateContract.byDirection
737
+ *
738
+ * Actor-tier-keyed gate dispatch. Reads `ctx.actor.tier` (e.g.
739
+ * `"free"` / `"paid"` / `"admin"`) and routes to the matching gate.
740
+ * Falls back to `gateMap["default"]` when the tier is missing or
741
+ * unmapped; missing fallback serves uninspected. Lets free-tier
742
+ * tenants run a stricter posture than paid customers without
743
+ * branching at every call site.
744
+ *
745
+ * @opts
746
+ * name: string, // wrapper gate name (default "byActorTier")
747
+ *
748
+ * @example
749
+ * var byTier = b.gateContract.byActorTier({
750
+ * free: b.guardCsv.gate({ profile: "strict" }),
751
+ * paid: b.guardCsv.gate({ profile: "balanced" }),
752
+ * default: b.guardCsv.gate({ profile: "strict" }),
753
+ * });
754
+ * var d = await byTier.check({
755
+ * bytes: Buffer.from("name,age\nada,36"),
756
+ * actor: { tier: "paid" },
757
+ * });
758
+ * d.action; // → "serve"
759
+ */
507
760
  function byActorTier(gateMap, opts) {
508
761
  opts = opts || {};
509
762
  return defineGate({
@@ -517,8 +770,35 @@ function byActorTier(gateMap, opts) {
517
770
  });
518
771
  }
519
772
 
520
- // byRoute — route-pattern-keyed dispatch. Patterns are simple
521
- // glob-prefix matches: "/admin/*" matches "/admin/foo".
773
+ /**
774
+ * @primitive b.gateContract.byRoute
775
+ * @signature b.gateContract.byRoute(gateMap, opts?)
776
+ * @since 0.7.5
777
+ * @status stable
778
+ * @related b.gateContract.byActorTier, b.gateContract.byDirection
779
+ *
780
+ * Route-pattern-keyed gate dispatch. Patterns are simple glob-prefix
781
+ * matches — `/admin/*` matches every path beginning with `/admin/`.
782
+ * Tries each entry in declaration order, falling back to `"*"` /
783
+ * `"default"`; missing fallback serves uninspected. Lets `/admin/*`
784
+ * routes apply a stricter guard than the public surface without
785
+ * threading per-route opts through every call site.
786
+ *
787
+ * @opts
788
+ * name: string, // wrapper gate name (default "byRoute")
789
+ *
790
+ * @example
791
+ * var byPath = b.gateContract.byRoute({
792
+ * "/admin/*": b.guardCsv.gate({ profile: "strict" }),
793
+ * "/api/*": b.guardCsv.gate({ profile: "balanced" }),
794
+ * "*": b.guardCsv.gate({ profile: "permissive" }),
795
+ * });
796
+ * var d = await byPath.check({
797
+ * bytes: Buffer.from("name,age\nada,36"),
798
+ * route: "/admin/imports",
799
+ * });
800
+ * d.action; // → "serve"
801
+ */
522
802
  function byRoute(gateMap, opts) {
523
803
  opts = opts || {};
524
804
  var entries = Object.keys(gateMap).map(function (pattern) {
@@ -540,7 +820,33 @@ function byRoute(gateMap, opts) {
540
820
  });
541
821
  }
542
822
 
543
- // byDirection — inbound vs outbound dispatch.
823
+ /**
824
+ * @primitive b.gateContract.byDirection
825
+ * @signature b.gateContract.byDirection(gateMap, opts?)
826
+ * @since 0.7.5
827
+ * @status stable
828
+ * @related b.gateContract.byRoute, b.gateContract.byActorTier
829
+ *
830
+ * Direction-aware gate dispatch. Reads `ctx.direction` (`"inbound"`
831
+ * or `"outbound"`; default `"outbound"`) and routes to the matching
832
+ * gate. Lets a single guard wiring run a stricter posture on bytes
833
+ * arriving from an external source than on bytes the framework is
834
+ * about to emit. Missing direction maps serve uninspected.
835
+ *
836
+ * @opts
837
+ * name: string, // wrapper gate name (default "byDirection")
838
+ *
839
+ * @example
840
+ * var byDir = b.gateContract.byDirection({
841
+ * inbound: b.guardCsv.gate({ profile: "strict" }),
842
+ * outbound: b.guardCsv.gate({ profile: "balanced" }),
843
+ * });
844
+ * var d = await byDir.check({
845
+ * bytes: Buffer.from("name,age\nada,36"),
846
+ * direction: "inbound",
847
+ * });
848
+ * d.action; // → "serve"
849
+ */
544
850
  function byDirection(gateMap, opts) {
545
851
  opts = opts || {};
546
852
  return defineGate({
@@ -554,8 +860,30 @@ function byDirection(gateMap, opts) {
554
860
  });
555
861
  }
556
862
 
557
- // shadowMode — run candidate alongside primary; emit divergence.
558
- // Primary's decision is honored; candidate is observability-only.
863
+ /**
864
+ * @primitive b.gateContract.shadowMode
865
+ * @signature b.gateContract.shadowMode(primary, candidate, opts?)
866
+ * @since 0.7.5
867
+ * @status stable
868
+ * @related b.gateContract.canaryGate, b.gateContract.composeGates
869
+ *
870
+ * Run a candidate gate alongside the primary; emit a divergence
871
+ * counter when their actions disagree. The primary's decision is the
872
+ * one honored — candidate runs are observability-only and don't
873
+ * block the request. Useful for staged rollout of a new profile
874
+ * (run it shadowed for a week, watch the divergence rate, then
875
+ * promote it to primary).
876
+ *
877
+ * @opts
878
+ * name: string, // wrapper gate name (default "shadow")
879
+ *
880
+ * @example
881
+ * var primary = b.guardCsv.gate({ profile: "strict" });
882
+ * var candidate = b.guardCsv.gate({ profile: "balanced" });
883
+ * var staged = b.gateContract.shadowMode(primary, candidate, { name: "csv:staged" });
884
+ * var d = await staged.check({ bytes: Buffer.from("name,age\nada,36") });
885
+ * d.action; // → "serve" (primary's decision)
886
+ */
559
887
  function shadowMode(primary, candidate, opts) {
560
888
  opts = opts || {};
561
889
  return defineGate({
@@ -575,7 +903,28 @@ function shadowMode(primary, candidate, opts) {
575
903
  });
576
904
  }
577
905
 
578
- // canaryGate — enforce on rate%; warn on the rest.
906
+ /**
907
+ * @primitive b.gateContract.canaryGate
908
+ * @signature b.gateContract.canaryGate(gate, opts?)
909
+ * @since 0.7.5
910
+ * @status stable
911
+ * @related b.gateContract.shadowMode, b.gateContract.cachingGate
912
+ *
913
+ * Enforce the wrapped gate's refuse decisions on `rate` of requests;
914
+ * downgrade the rest to `warn`. Default rate is `0.1` (10% enforced,
915
+ * 90% warned). Sampling uses a non-cryptographic random source — fine
916
+ * for rollout shaping, never for security-critical sampling.
917
+ *
918
+ * @opts
919
+ * rate: number, // 0..1, default 0.1
920
+ * name: string, // wrapper gate name (default "canary")
921
+ *
922
+ * @example
923
+ * var strict = b.guardCsv.gate({ profile: "strict" });
924
+ * var canary = b.gateContract.canaryGate(strict, { rate: 0.25 });
925
+ * var d = await canary.check({ bytes: Buffer.from("name,age\nada,36") });
926
+ * d.ok; // → true
927
+ */
579
928
  function canaryGate(gate, opts) {
580
929
  opts = opts || {};
581
930
  var rate = typeof opts.rate === "number" ? opts.rate : 0.1;
@@ -591,9 +940,34 @@ function canaryGate(gate, opts) {
591
940
  });
592
941
  }
593
942
 
594
- // cachingGate — wrap with explicit TTL cache (separate from the
595
- // per-gate built-in cache; useful when the operator wants a SHARED
596
- // cache across multiple gates).
943
+ /**
944
+ * @primitive b.gateContract.cachingGate
945
+ * @signature b.gateContract.cachingGate(gate, opts)
946
+ * @since 0.7.5
947
+ * @status stable
948
+ * @related b.gateContract.defineGate, b.gateContract.canaryGate
949
+ *
950
+ * Wrap a gate with an explicit shared cache backend. The per-gate
951
+ * built-in cache (configured via `defineGate({ cache, cacheTtlMs })`)
952
+ * is per-gate-instance; this wrapper is the operator-side variant
953
+ * for sharing one cache across multiple gates. `opts.backend` must
954
+ * expose the `b.cache` shape (`{ get(key), set(key, value, opts) }`).
955
+ *
956
+ * @opts
957
+ * backend: object, // b.cache-shaped { get, set } (required)
958
+ * ttlMs: number, // cache TTL
959
+ * name: string, // wrapper gate name (default "<gate>:cached")
960
+ *
961
+ * @example
962
+ * var cache = b.cache.create({ backend: "memory", maxEntries: 10000 });
963
+ * var strict = b.guardCsv.gate({ profile: "strict" });
964
+ * var cached = b.gateContract.cachingGate(strict, {
965
+ * backend: cache,
966
+ * ttlMs: 60000,
967
+ * });
968
+ * var d = await cached.check({ bytes: Buffer.from("name,age\nada,36") });
969
+ * d.action; // → "serve"
970
+ */
597
971
  function cachingGate(gate, opts) {
598
972
  opts = opts || {};
599
973
  var backend = opts.backend;
@@ -617,7 +991,30 @@ function cachingGate(gate, opts) {
617
991
  });
618
992
  }
619
993
 
620
- // workerThreadGate — offload check() to a worker thread.
994
+ /**
995
+ * @primitive b.gateContract.workerThreadGate
996
+ * @signature b.gateContract.workerThreadGate(gate, opts)
997
+ * @since 0.7.5
998
+ * @status stable
999
+ * @related b.gateContract.defineGate, b.gateContract.cachingGate
1000
+ *
1001
+ * Offload a gate's `check(ctx)` to a worker. `opts.worker` must
1002
+ * expose `run({ gate, ctx })` returning the decision (matches the
1003
+ * `b.worker` shape). Useful when a guard's per-request CPU cost
1004
+ * (large-doc HTML parsing, archive entry inspection) is high enough
1005
+ * that running it on the request thread would impact throughput.
1006
+ *
1007
+ * @opts
1008
+ * worker: object, // b.worker-shaped { run } (required)
1009
+ * name: string, // wrapper gate name (default "<gate>:worker")
1010
+ *
1011
+ * @example
1012
+ * var worker = b.worker.create({ pool: 4, modulePath: "./guards/csv-worker.js" });
1013
+ * var strict = b.guardCsv.gate({ profile: "strict" });
1014
+ * var offloaded = b.gateContract.workerThreadGate(strict, { worker: worker });
1015
+ * var d = await offloaded.check({ bytes: Buffer.from("name,age\nada,36") });
1016
+ * d.action; // → "serve"
1017
+ */
621
1018
  function workerThreadGate(gate, opts) {
622
1019
  opts = opts || {};
623
1020
  if (!opts.worker) {
@@ -632,10 +1029,33 @@ function workerThreadGate(gate, opts) {
632
1029
  });
633
1030
  }
634
1031
 
635
- // makeProfileBuilder — closes over a guard's PROFILES map and returns
636
- // a buildProfile(opts) function that delegates to the recursive
637
- // composition entry point. Used so each guard's buildProfile export is
638
- // just a binding instead of a duplicate forwarding wrapper.
1032
+ /**
1033
+ * @primitive b.gateContract.makeProfileBuilder
1034
+ * @signature b.gateContract.makeProfileBuilder(profiles)
1035
+ * @since 0.7.5
1036
+ * @status stable
1037
+ * @related b.gateContract.buildProfile, b.gateContract.resolveProfileAndPosture
1038
+ *
1039
+ * Closes over a guard's `PROFILES` map and returns a `buildProfile(opts)`
1040
+ * function that delegates to the recursive composition entry point.
1041
+ * Every guard's `buildProfile` export is therefore a single binding,
1042
+ * not a duplicate forwarding wrapper. The returned function accepts
1043
+ * `{ baseProfile, extends, overrides, removes }` plus inline keys, and
1044
+ * resolves names through the closed-over profile table.
1045
+ *
1046
+ * @example
1047
+ * var PROFILES = {
1048
+ * strict: { formulaInjectionPolicy: "reject", bidiCharPolicy: "reject" },
1049
+ * balanced: { formulaInjectionPolicy: "prefix-tab", bidiCharPolicy: "strip" },
1050
+ * };
1051
+ * var buildProfile = b.gateContract.makeProfileBuilder(PROFILES);
1052
+ * var custom = buildProfile({
1053
+ * baseProfile: "strict",
1054
+ * overrides: { trailingWhitespacePolicy: "preserve" },
1055
+ * });
1056
+ * custom.formulaInjectionPolicy; // → "reject"
1057
+ * custom.trailingWhitespacePolicy; // → "preserve"
1058
+ */
639
1059
  function makeProfileBuilder(profiles) {
640
1060
  return function (opts) {
641
1061
  return buildProfile(Object.assign({}, opts, {
@@ -644,10 +1064,30 @@ function makeProfileBuilder(profiles) {
644
1064
  };
645
1065
  }
646
1066
 
647
- // lookupCompliancePosture — throws errorFactory(prefix + ".bad-posture")
648
- // when the name is not in the posture map; returns a shallow clone of
649
- // the posture object otherwise. Used by every guard's
650
- // compliancePosture(name) export.
1067
+ /**
1068
+ * @primitive b.gateContract.lookupCompliancePosture
1069
+ * @signature b.gateContract.lookupCompliancePosture(name, postures, errorFactory, codePrefix)
1070
+ * @since 0.7.5
1071
+ * @status stable
1072
+ * @compliance hipaa, pci-dss, gdpr, soc2
1073
+ * @related b.gateContract.resolveProfileAndPosture, b.gateContract.makeProfileBuilder
1074
+ *
1075
+ * Look up a compliance-posture overlay by name. Throws
1076
+ * `errorFactory(codePrefix + ".bad-posture")` when the name is not in
1077
+ * the posture map; returns a shallow clone of the posture object
1078
+ * otherwise. Every guard's `compliancePosture(name)` export forwards
1079
+ * here so the error code, error class, and clone semantics stay
1080
+ * identical across the family.
1081
+ *
1082
+ * @example
1083
+ * var POSTURES = {
1084
+ * hipaa: { piiPolicy: "redact", bidiCharPolicy: "reject" },
1085
+ * "pci-dss": { piiPolicy: "redact", bidiCharPolicy: "reject" },
1086
+ * };
1087
+ * var posture = b.gateContract.lookupCompliancePosture(
1088
+ * "hipaa", POSTURES, b.guardCsv.GuardCsvError.factory, "csv");
1089
+ * posture.piiPolicy; // → "redact"
1090
+ */
651
1091
  function lookupCompliancePosture(name, postures, errorFactory, codePrefix) {
652
1092
  if (!postures || !postures[name]) {
653
1093
  throw errorFactory(codePrefix + ".bad-posture",
@@ -656,10 +1096,30 @@ function lookupCompliancePosture(name, postures, errorFactory, codePrefix) {
656
1096
  return Object.assign({}, postures[name]);
657
1097
  }
658
1098
 
659
- // makeRulePackLoader — returns a `loadRulePack(pack)` closure with
660
- // per-guard storage. Validates pack shape via validateOpts; the
661
- // closure stores accepted packs in a closed-over map keyed by pack.id.
662
- // Operators can later inspect via the returned `list()` function.
1099
+ /**
1100
+ * @primitive b.gateContract.makeRulePackLoader
1101
+ * @signature b.gateContract.makeRulePackLoader(errorClass, codePrefix)
1102
+ * @since 0.7.5
1103
+ * @status stable
1104
+ * @related b.gateContract.makeProfileBuilder, b.gateContract.lookupCompliancePosture
1105
+ *
1106
+ * Build a per-guard rule-pack registry. Returns
1107
+ * `{ load(pack), list(), get(id) }`. `load` validates that `pack`
1108
+ * is an object with a non-empty string `pack.id` (throwing
1109
+ * `errorClass(codePrefix + ".bad-opt")` when not) and stores it in
1110
+ * a closed-over map keyed by `pack.id`. `list` returns the stored
1111
+ * packs; `get(id)` returns one or `null`. Used so every guard's
1112
+ * `loadRulePack` export shares storage shape and validation.
1113
+ *
1114
+ * @example
1115
+ * var packs = b.gateContract.makeRulePackLoader(b.guardCsv.GuardCsvError, "csv");
1116
+ * packs.load({
1117
+ * id: "pii-extra",
1118
+ * rules: [{ id: "ssn", severity: "critical",
1119
+ * detect: function (cell) { return /^\d{3}-\d{2}-\d{4}$/.test(cell); } }],
1120
+ * });
1121
+ * packs.get("pii-extra").rules.length; // → 1
1122
+ */
663
1123
  function makeRulePackLoader(errorClass, codePrefix) {
664
1124
  var store = Object.create(null);
665
1125
  return {
@@ -677,15 +1137,26 @@ function makeRulePackLoader(errorClass, codePrefix) {
677
1137
  };
678
1138
  }
679
1139
 
680
- // extractBytesAsText — every guard's check(ctx) opens by reading
681
- // ctx.bytes and converting to a UTF-8 string for inspection.
682
- // Centralizes the string|Buffer|empty handling so each guard's check
683
- // body just deals with the inspection logic.
684
- //
685
- // var text = gateContract.extractBytesAsText(ctx);
686
- // if (!text) return { ok: true, action: "serve" };
687
- //
688
- // Returns "" if ctx.bytes is missing caller treats empty as serve.
1140
+ /**
1141
+ * @primitive b.gateContract.extractBytesAsText
1142
+ * @signature b.gateContract.extractBytesAsText(ctx)
1143
+ * @since 0.7.5
1144
+ * @status stable
1145
+ * @related b.gateContract.buildGuardGate, b.gateContract.aggregateIssues
1146
+ *
1147
+ * Read `ctx.bytes` and return a UTF-8 string for inspection. Centralizes
1148
+ * the string-or-Buffer-or-empty handling so each guard's `check(ctx)`
1149
+ * body deals with the inspection logic only. Returns `""` when
1150
+ * `ctx.bytes` is missing — callers treat empty as the serve case.
1151
+ *
1152
+ * @example
1153
+ * var ctx = { bytes: Buffer.from("name,age\nada,36") };
1154
+ * var text = b.gateContract.extractBytesAsText(ctx);
1155
+ * text; // → "name,age\nada,36"
1156
+ *
1157
+ * b.gateContract.extractBytesAsText({}); // → ""
1158
+ * b.gateContract.extractBytesAsText({ bytes: "x,y" }); // → "x,y"
1159
+ */
689
1160
  function extractBytesAsText(ctx) {
690
1161
  if (!ctx) return "";
691
1162
  var bytes = ctx.bytes;
@@ -693,16 +1164,52 @@ function extractBytesAsText(ctx) {
693
1164
  return Buffer.isBuffer(bytes) ? bytes.toString("utf8") : String(bytes);
694
1165
  }
695
1166
 
696
- // buildGuardGate — gate-construction shorthand. Every guard-*
697
- // primitive's gate(opts) factory forwards the same ~16-key opts bag
698
- // (mode, audit, observability, forensicEvidenceStore, cache, hooks,
699
- // runtime cap, ...) to defineGate. Centralizes that forwarding so each
700
- // guard's gate() body is just the check function plus a label.
701
- //
702
- // gateContract.buildGuardGate(
703
- // opts.name || "guardCsv:" + (opts.profile || "default"),
704
- // opts,
705
- // async function (ctx) { ... per-guard check ... });
1167
+ /**
1168
+ * @primitive b.gateContract.buildGuardGate
1169
+ * @signature b.gateContract.buildGuardGate(name, opts, check)
1170
+ * @since 0.7.5
1171
+ * @status stable
1172
+ * @related b.gateContract.defineGate, b.gateContract.extractBytesAsText
1173
+ *
1174
+ * Gate-construction shorthand for guard-* primitives. Forwards the
1175
+ * uniform ~16-key opts bag (`mode`, `audit`, `observability`,
1176
+ * `forensicEvidenceStore`, `forensicSnippetBytes`, `cache`,
1177
+ * `cacheTtlMs`, `maxRuntimeMs`, all six lifecycle hooks) to
1178
+ * `defineGate`, so each guard's `gate(opts)` body is just the per-guard
1179
+ * `check` function plus a label. Result satisfies `validateGateShape`.
1180
+ *
1181
+ * @opts
1182
+ * mode: string, // one of MODES; default "enforce"
1183
+ * audit: object|null, // b.audit handle for emission
1184
+ * observability: object|null, // b.observability handle
1185
+ * forensicEvidenceStore: object|null, // { write({ ... }) }
1186
+ * forensicSnippetBytes: number, // 0 = disabled
1187
+ * cache: object|null, // b.cache shape
1188
+ * cacheTtlMs: number,
1189
+ * maxRuntimeMs: number, // 0 = uncapped
1190
+ * beforeCheck: function|null,
1191
+ * afterCheck: function|null,
1192
+ * onIssue: function|null,
1193
+ * onSanitize: function|null,
1194
+ * onRefuse: function|null,
1195
+ * onAudit: function|null,
1196
+ *
1197
+ * @example
1198
+ * var myGuardGate = b.gateContract.buildGuardGate(
1199
+ * "myGuard:strict",
1200
+ * { mode: "enforce", maxRuntimeMs: 250 },
1201
+ * async function (ctx) {
1202
+ * var text = b.gateContract.extractBytesAsText(ctx);
1203
+ * if (text.length === 0) return { ok: true, action: "serve" };
1204
+ * if (/\s/.test(text)) {
1205
+ * return { ok: false, action: "refuse",
1206
+ * issues: [{ kind: "whitespace", severity: "high" }] };
1207
+ * }
1208
+ * return { ok: true, action: "serve" };
1209
+ * });
1210
+ * var d = await myGuardGate.check({ bytes: Buffer.from("hello") });
1211
+ * d.action; // → "serve"
1212
+ */
706
1213
  function buildGuardGate(name, opts, check) {
707
1214
  opts = opts || {};
708
1215
  return defineGate({
@@ -726,10 +1233,28 @@ function buildGuardGate(name, opts, check) {
726
1233
  });
727
1234
  }
728
1235
 
729
- // aggregateIssues — wrap an issues array in the canonical
730
- // `{ ok, issues }` validate-result shape. ok=true when no issue is
731
- // `critical` or `high` severity. Used by guards whose validate path
732
- // can't go through runIssueValidator (raw-Buffer input cases).
1236
+ /**
1237
+ * @primitive b.gateContract.aggregateIssues
1238
+ * @signature b.gateContract.aggregateIssues(issues)
1239
+ * @since 0.7.5
1240
+ * @status stable
1241
+ * @related b.gateContract.runIssueValidator, b.gateContract.summarizeIssues
1242
+ *
1243
+ * Wrap an issues array in the canonical `{ ok, issues }` validate-result
1244
+ * shape. `ok` is `true` only when no issue carries `critical` or `high`
1245
+ * severity — `info` and `warn` issues do not flip `ok`. Used by guards
1246
+ * whose validate path can't route through `runIssueValidator` (raw-Buffer
1247
+ * input cases such as svg magic detection or filename byte scans).
1248
+ *
1249
+ * @example
1250
+ * var result = b.gateContract.aggregateIssues([
1251
+ * { kind: "csv.bidi", severity: "high",
1252
+ * snippet: "U+202E embedded in cell" },
1253
+ * { kind: "csv.trailing-whitespace", severity: "info" },
1254
+ * ]);
1255
+ * result.ok; // → false
1256
+ * result.issues.length; // → 2
1257
+ */
733
1258
  function aggregateIssues(issues) {
734
1259
  return {
735
1260
  ok: !issues.some(function (i) {
@@ -739,12 +1264,27 @@ function aggregateIssues(issues) {
739
1264
  };
740
1265
  }
741
1266
 
742
- // badInputResultIfNotStringOrBuffer — returns the canonical
743
- // `{ ok: false, issues: [{ kind: "bad-input", ... }] }` result when
744
- // `input` is neither a string nor a Buffer; null otherwise. Used by
745
- // guards whose validate path can't pre-convert (e.g. guard-svg needs
746
- // raw bytes for SVGZ magic detection; guard-filename needs raw bytes
747
- // for overlong-UTF-8 byte scan).
1267
+ /**
1268
+ * @primitive b.gateContract.badInputResultIfNotStringOrBuffer
1269
+ * @signature b.gateContract.badInputResultIfNotStringOrBuffer(input)
1270
+ * @since 0.7.5
1271
+ * @status stable
1272
+ * @related b.gateContract.runIssueValidator, b.gateContract.aggregateIssues
1273
+ *
1274
+ * Type-guard for guard-* validate entry points. Returns the canonical
1275
+ * `{ ok: false, issues: [{ kind: "bad-input", severity: "high", ... }] }`
1276
+ * result when `input` is neither a string nor a Buffer; `null`
1277
+ * otherwise. Used by guards whose validate path can't pre-convert —
1278
+ * `b.guardSvg` needs raw bytes for SVGZ magic detection,
1279
+ * `b.guardFilename` needs raw bytes for the overlong-UTF-8 byte scan.
1280
+ *
1281
+ * @example
1282
+ * b.gateContract.badInputResultIfNotStringOrBuffer("hello"); // → null
1283
+ * b.gateContract.badInputResultIfNotStringOrBuffer(Buffer.from("x")); // → null
1284
+ * var bad = b.gateContract.badInputResultIfNotStringOrBuffer(42);
1285
+ * bad.ok; // → false
1286
+ * bad.issues[0].kind; // → "bad-input"
1287
+ */
748
1288
  function badInputResultIfNotStringOrBuffer(input) {
749
1289
  if (typeof input === "string" || Buffer.isBuffer(input)) return null;
750
1290
  return {
@@ -754,17 +1294,40 @@ function badInputResultIfNotStringOrBuffer(input) {
754
1294
  };
755
1295
  }
756
1296
 
757
- // runIssueValidator — boilerplate for guard-* validate(input, opts)
758
- // entry points. Normalizes string|Buffer input, returns the canonical
759
- // { ok: false, issues: [{ kind: "bad-input", ... }] } shape on type
760
- // mismatch, otherwise calls the operator-supplied detector and
761
- // computes ok = no issue is critical/high. Used so every guard's
762
- // validate() body is identical scaffolding around the per-guard
763
- // _detectIssues function.
764
- //
765
- // gateContract.runIssueValidator(input, opts, function (text, opts) {
766
- // return _detectIssues(text, opts);
767
- // });
1297
+ /**
1298
+ * @primitive b.gateContract.runIssueValidator
1299
+ * @signature b.gateContract.runIssueValidator(input, opts, detector)
1300
+ * @since 0.7.5
1301
+ * @status stable
1302
+ * @related b.gateContract.aggregateIssues, b.gateContract.badInputResultIfNotStringOrBuffer
1303
+ *
1304
+ * Boilerplate for guard-* `validate(input, opts)` entry points.
1305
+ * Normalizes string-or-Buffer input to a UTF-8 string, returns the
1306
+ * canonical `{ ok: false, issues: [{ kind: "bad-input", ... }] }` shape
1307
+ * on type mismatch, otherwise calls the operator-supplied `detector`
1308
+ * and aggregates its issues array. Result `ok` is `true` only when no
1309
+ * detected issue is `critical` / `high` severity. Lets every guard's
1310
+ * `validate()` body be identical scaffolding around the per-guard
1311
+ * detector. The `opts` argument is forwarded verbatim as the second
1312
+ * argument to `detector(text, opts)` — its shape is detector-defined,
1313
+ * not constrained by gate-contract.
1314
+ *
1315
+ * @opts
1316
+ * ...: any, // detector-defined; passed through to detector(text, opts)
1317
+ *
1318
+ * @example
1319
+ * function detectFormulaTrigger(text) {
1320
+ * if (/^[=+\-@]/.test(text)) {
1321
+ * return [{ kind: "csv.formula-injection", severity: "high",
1322
+ * snippet: text.slice(0, 16) }];
1323
+ * }
1324
+ * return [];
1325
+ * }
1326
+ * var bad = b.gateContract.runIssueValidator("=cmd|x", {}, detectFormulaTrigger);
1327
+ * bad.ok; // → false
1328
+ * var ok = b.gateContract.runIssueValidator("ada,36", {}, detectFormulaTrigger);
1329
+ * ok.ok; // → true
1330
+ */
768
1331
  function runIssueValidator(input, opts, detector) {
769
1332
  var text = typeof input === "string"
770
1333
  ? input
@@ -779,18 +1342,52 @@ function runIssueValidator(input, opts, detector) {
779
1342
  return aggregateIssues(detector(text, opts));
780
1343
  }
781
1344
 
782
- // resolveProfileAndPosture — overlay opts.profile + opts.compliancePosture
783
- // over a defaults object using guard-supplied tables. Used by every guard
784
- // primitive's create()/factory entry point so the resolution shape is
785
- // identical across the family.
786
- //
787
- // resolveProfileAndPosture(opts, {
788
- // profiles: PROFILES,
789
- // compliancePostures: COMPLIANCE_POSTURES,
790
- // defaults: DEFAULTS,
791
- // errorClass: GuardCsvError,
792
- // errCodePrefix: "csv", // throws "csv.bad-profile" / "csv.bad-posture"
793
- // })
1345
+ /**
1346
+ * @primitive b.gateContract.resolveProfileAndPosture
1347
+ * @signature b.gateContract.resolveProfileAndPosture(opts, cfg)
1348
+ * @since 0.7.5
1349
+ * @status stable
1350
+ * @compliance hipaa, pci-dss, gdpr, soc2
1351
+ * @related b.gateContract.makeProfileBuilder, b.gateContract.lookupCompliancePosture
1352
+ *
1353
+ * Overlay `opts.profile` and `opts.compliancePosture` on top of a
1354
+ * defaults object using guard-supplied tables. Every guard primitive's
1355
+ * factory routes through this so the resolution shape — defaults
1356
+ * first, profile overlay, posture overlay, inline opts last — stays
1357
+ * identical across the family. When `opts.compliancePosture` is unset
1358
+ * and `b.compliance.set()` has declared a global posture, the global
1359
+ * posture takes effect (the value-add of the top-level coordinator).
1360
+ *
1361
+ * Throws `cfg.errorClass.factory(cfg.errCodePrefix + ".bad-profile")`
1362
+ * for unknown profile names and `... + ".bad-posture"` for unknown
1363
+ * postures.
1364
+ *
1365
+ * @opts
1366
+ * profiles: object, // PROFILES table (required)
1367
+ * compliancePostures: object, // COMPLIANCE_POSTURES table (required)
1368
+ * defaults: object, // baseline before overlay
1369
+ * errorClass: FrameworkError,// throws via .factory(code, msg)
1370
+ * errCodePrefix: string, // e.g. "csv" → "csv.bad-profile"
1371
+ *
1372
+ * @example
1373
+ * var PROFILES = {
1374
+ * strict: { formulaInjectionPolicy: "reject", bidiCharPolicy: "reject" },
1375
+ * balanced: { formulaInjectionPolicy: "prefix-tab", bidiCharPolicy: "strip" },
1376
+ * };
1377
+ * var POSTURES = { hipaa: { piiPolicy: "redact" } };
1378
+ * var resolved = b.gateContract.resolveProfileAndPosture(
1379
+ * { profile: "balanced", compliancePosture: "hipaa", maxCellBytes: 65536 },
1380
+ * {
1381
+ * profiles: PROFILES,
1382
+ * compliancePostures: POSTURES,
1383
+ * defaults: { maxCellBytes: 1024 },
1384
+ * errorClass: b.guardCsv.GuardCsvError,
1385
+ * errCodePrefix: "csv",
1386
+ * });
1387
+ * resolved.formulaInjectionPolicy; // → "prefix-tab"
1388
+ * resolved.piiPolicy; // → "redact"
1389
+ * resolved.maxCellBytes; // → 65536
1390
+ */
794
1391
  function resolveProfileAndPosture(opts, cfg) {
795
1392
  opts = opts || {};
796
1393
  validateOpts.requireObject(cfg, "gateContract.resolveProfileAndPosture",
@@ -830,15 +1427,47 @@ function resolveProfileAndPosture(opts, cfg) {
830
1427
  return Object.assign({}, cfg.defaults || {}, overlay, opts);
831
1428
  }
832
1429
 
833
- // buildProfile — recursive profile composition with cycle detection.
834
- //
835
- // buildProfile({
836
- // baseProfile: "blog-post",
837
- // extends: ["custom-tags"],
838
- // overrides: { allowedTags: [...] },
839
- // removes: { allowedAttrs: { a: ["target"] } },
840
- // resolveProfile: name => profile, // operator-supplied resolver
841
- // })
1430
+ /**
1431
+ * @primitive b.gateContract.buildProfile
1432
+ * @signature b.gateContract.buildProfile(opts)
1433
+ * @since 0.7.5
1434
+ * @status stable
1435
+ * @related b.gateContract.makeProfileBuilder, b.gateContract.resolveProfileAndPosture
1436
+ *
1437
+ * Recursive profile composition with cycle detection. Walks
1438
+ * `opts.baseProfile` and every name in `opts.extends` through the
1439
+ * caller-supplied `opts.resolveProfile` resolver, deep-merging arrays
1440
+ * (set-union, later-wins on duplicates) and objects (recursive). Then
1441
+ * applies inline `opts.overrides` and finally `opts.removes` (which
1442
+ * can drop array entries or whole keys). Cycles throw
1443
+ * `gate-contract/profile-cycle`; unknown names throw
1444
+ * `gate-contract/unknown-profile`. Most guards bind through
1445
+ * `makeProfileBuilder` and never call `buildProfile` directly.
1446
+ *
1447
+ * @opts
1448
+ * baseProfile: string, // start from this profile name
1449
+ * extends: string[], // additional bases (later-wins)
1450
+ * overrides: object, // inline merge after extends
1451
+ * removes: object, // drop array entries or keys
1452
+ * resolveProfile: function, // (name) → profile|null (required)
1453
+ *
1454
+ * @example
1455
+ * var PROFILES = {
1456
+ * "blog-post": {
1457
+ * allowedTags: ["p", "a", "strong"],
1458
+ * allowedAttrs: { a: ["href", "target"] },
1459
+ * },
1460
+ * "with-images": { extends: ["blog-post"], allowedTags: ["img"] },
1461
+ * };
1462
+ * var resolved = b.gateContract.buildProfile({
1463
+ * baseProfile: "with-images",
1464
+ * overrides: { allowedTags: ["em"] },
1465
+ * removes: { allowedAttrs: { a: ["target"] } },
1466
+ * resolveProfile: function (n) { return PROFILES[n] || null; },
1467
+ * });
1468
+ * resolved.allowedTags; // → ["p","a","strong","img","em"]
1469
+ * resolved.allowedAttrs.a; // → ["href"]
1470
+ */
842
1471
  function buildProfile(opts) {
843
1472
  validateOpts.requireObject(opts, "gateContract.buildProfile", GateContractError);
844
1473
  var resolve = opts.resolveProfile;
@@ -924,11 +1553,30 @@ function _applyRemoves(target, removes) {
924
1553
  return out;
925
1554
  }
926
1555
 
927
- // summarizeIssues — project a gate decision's `issues` array down to the
928
- // audit-shape (kind / severity / ruleId only — full snippets stay in the
929
- // forensic store). Replaces the per-host inline `(d.issues || []).map(
930
- // function (i) { return { kind: i.kind, severity: i.severity, ruleId:
931
- // i.ruleId }; })` shape.
1556
+ /**
1557
+ * @primitive b.gateContract.summarizeIssues
1558
+ * @signature b.gateContract.summarizeIssues(issues)
1559
+ * @since 0.7.5
1560
+ * @status stable
1561
+ * @related b.gateContract.aggregateIssues, b.gateContract.defineGate
1562
+ *
1563
+ * Project a gate decision's `issues` array down to the audit-friendly
1564
+ * shape — `{ kind, severity, ruleId }` only. Full snippets stay in the
1565
+ * forensic evidence store; the audit log records the classification
1566
+ * without the offending bytes. Replaces the inline
1567
+ * `(d.issues || []).map(...)` pattern host primitives previously
1568
+ * carried per emit site.
1569
+ *
1570
+ * @example
1571
+ * var summary = b.gateContract.summarizeIssues([
1572
+ * { kind: "csv.bidi", severity: "high", ruleId: "BIDI-OVERRIDE",
1573
+ * snippet: "<offending bytes redacted>" },
1574
+ * { kind: "csv.trailing-whitespace", severity: "info", ruleId: "TRIM" },
1575
+ * ]);
1576
+ * summary.length; // → 2
1577
+ * summary[0].snippet; // → undefined (stripped)
1578
+ * summary[0].ruleId; // → "BIDI-OVERRIDE"
1579
+ */
932
1580
  function summarizeIssues(issues) {
933
1581
  if (!Array.isArray(issues)) return [];
934
1582
  return issues.map(function (i) {
@@ -936,8 +1584,34 @@ function summarizeIssues(issues) {
936
1584
  });
937
1585
  }
938
1586
 
939
- // composeHooks — chain operator hooks. First non-null filter result
940
- // wins; transformer hooks run sequentially.
1587
+ /**
1588
+ * @primitive b.gateContract.composeHooks
1589
+ * @signature b.gateContract.composeHooks(hooks)
1590
+ * @since 0.7.5
1591
+ * @status stable
1592
+ * @related b.gateContract.defineGate, b.gateContract.buildGuardGate
1593
+ *
1594
+ * Chain a list of operator hooks into a single async hook. Empty
1595
+ * arrays return `null` (so `defineGate` can pass the result through
1596
+ * its `hooks.X || null` slot); single-element arrays return the
1597
+ * lone hook unchanged. Multi-element chains run sequentially —
1598
+ * `{ suppress: true }` or `{ skip: true }` from any hook short-
1599
+ * circuits and returns; otherwise the last non-null hook result wins.
1600
+ *
1601
+ * @example
1602
+ * var redactPii = function (issue) {
1603
+ * return Object.assign({}, issue, { snippet: "<redacted>" });
1604
+ * };
1605
+ * var dropInfo = function (issue) {
1606
+ * return issue.severity === "info" ? { suppress: true } : null;
1607
+ * };
1608
+ * var onIssue = b.gateContract.composeHooks([dropInfo, redactPii]);
1609
+ * var infoHit = await onIssue({ kind: "csv.trim", severity: "info" });
1610
+ * infoHit.suppress; // → true
1611
+ * var bidi = await onIssue({ kind: "csv.bidi", severity: "high",
1612
+ * snippet: "U+202E" });
1613
+ * bidi.snippet; // → "<redacted>"
1614
+ */
941
1615
  function composeHooks(hooks) {
942
1616
  hooks = (hooks || []).filter(Boolean);
943
1617
  if (hooks.length === 0) return null;