@blamejs/core 0.7.4 → 0.7.18

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (180) hide show
  1. package/CHANGELOG.md +423 -395
  2. package/README.md +150 -149
  3. package/bin/blamejs.js +0 -0
  4. package/index.js +308 -284
  5. package/lib/api-key.js +660 -663
  6. package/lib/api-snapshot.js +338 -338
  7. package/lib/app-shutdown.js +385 -385
  8. package/lib/app.js +365 -365
  9. package/lib/archive.js +250 -250
  10. package/lib/atomic-file.js +544 -544
  11. package/lib/audit-chain.js +177 -177
  12. package/lib/audit-sign.js +344 -344
  13. package/lib/audit-tools.js +677 -677
  14. package/lib/audit.js +766 -766
  15. package/lib/auth/jwt.js +311 -311
  16. package/lib/auth/lockout.js +436 -436
  17. package/lib/auth/oauth.js +721 -721
  18. package/lib/auth/passkey.js +181 -181
  19. package/lib/auth/password.js +594 -594
  20. package/lib/backup/bundle.js +217 -217
  21. package/lib/backup/crypto.js +176 -176
  22. package/lib/backup/index.js +515 -515
  23. package/lib/backup/manifest.js +282 -282
  24. package/lib/break-glass.js +1338 -1338
  25. package/lib/bundler.js +441 -441
  26. package/lib/cache-redis.js +256 -256
  27. package/lib/cache.js +1206 -1206
  28. package/lib/canonical-json.js +115 -115
  29. package/lib/chain-writer.js +234 -234
  30. package/lib/cli-helpers.js +206 -206
  31. package/lib/cli.js +2334 -2334
  32. package/lib/cluster-provider-db.js +317 -317
  33. package/lib/cluster-storage.js +226 -226
  34. package/lib/cluster.js +703 -703
  35. package/lib/codepoint-class.js +196 -0
  36. package/lib/config-drift.js +301 -301
  37. package/lib/consent.js +222 -222
  38. package/lib/constants.js +191 -191
  39. package/lib/cookies.js +315 -315
  40. package/lib/credential-hash.js +322 -322
  41. package/lib/crypto.js +266 -266
  42. package/lib/csv.js +275 -286
  43. package/lib/db-declare-row-policy.js +267 -267
  44. package/lib/db-declare-view.js +420 -421
  45. package/lib/db-query.js +406 -406
  46. package/lib/db-schema.js +319 -319
  47. package/lib/db.js +1288 -1288
  48. package/lib/deprecate.js +222 -222
  49. package/lib/dev.js +335 -335
  50. package/lib/dual-control.js +473 -473
  51. package/lib/error-page.js +420 -420
  52. package/lib/external-db-migrate.js +441 -441
  53. package/lib/external-db.js +1061 -1061
  54. package/lib/file-type.js +273 -273
  55. package/lib/file-upload.js +213 -10
  56. package/lib/forms.js +422 -422
  57. package/lib/framework-error.js +293 -215
  58. package/lib/framework-schema.js +717 -717
  59. package/lib/gate-contract.js +971 -0
  60. package/lib/guard-all.js +405 -0
  61. package/lib/guard-archive.js +739 -0
  62. package/lib/guard-csv.js +816 -0
  63. package/lib/guard-email.js +744 -0
  64. package/lib/guard-filename.js +724 -0
  65. package/lib/guard-html.js +976 -0
  66. package/lib/guard-json.js +729 -0
  67. package/lib/guard-markdown.js +586 -0
  68. package/lib/guard-svg.js +976 -0
  69. package/lib/guard-xml.js +405 -0
  70. package/lib/guard-yaml.js +529 -0
  71. package/lib/handlers.js +350 -350
  72. package/lib/http-client-cookie-jar.js +508 -508
  73. package/lib/http-client.js +1195 -1195
  74. package/lib/i18n.js +878 -878
  75. package/lib/jobs.js +185 -185
  76. package/lib/log-stream-cloudwatch.js +369 -369
  77. package/lib/log-stream-local.js +146 -146
  78. package/lib/log-stream-otlp-grpc.js +410 -410
  79. package/lib/log-stream-otlp.js +286 -286
  80. package/lib/log-stream-syslog.js +302 -302
  81. package/lib/log-stream-webhook.js +199 -199
  82. package/lib/log-stream.js +330 -330
  83. package/lib/log.js +500 -500
  84. package/lib/mail-bounce.js +528 -528
  85. package/lib/mail-dkim.js +369 -362
  86. package/lib/mail.js +981 -962
  87. package/lib/metrics.js +683 -683
  88. package/lib/middleware/api-encrypt.js +936 -936
  89. package/lib/middleware/attach-user.js +157 -157
  90. package/lib/middleware/body-parser.js +1170 -1091
  91. package/lib/middleware/bot-guard.js +178 -178
  92. package/lib/middleware/compression.js +452 -452
  93. package/lib/middleware/cors.js +314 -314
  94. package/lib/middleware/csp-nonce.js +348 -348
  95. package/lib/middleware/csrf-protect.js +316 -316
  96. package/lib/middleware/db-role-for.js +264 -264
  97. package/lib/middleware/health.js +392 -392
  98. package/lib/middleware/index.js +79 -79
  99. package/lib/middleware/rate-limit.js +358 -358
  100. package/lib/middleware/request-id.js +61 -61
  101. package/lib/middleware/request-log.js +168 -168
  102. package/lib/middleware/require-auth.js +104 -104
  103. package/lib/middleware/security-headers.js +116 -116
  104. package/lib/middleware/sse.js +166 -166
  105. package/lib/migrations.js +383 -383
  106. package/lib/mtls-ca.js +518 -518
  107. package/lib/mtls-engine-default.js +481 -481
  108. package/lib/network-dns.js +632 -632
  109. package/lib/network-heartbeat.js +290 -290
  110. package/lib/network-nts.js +574 -574
  111. package/lib/network-proxy.js +265 -265
  112. package/lib/network-tls.js +328 -328
  113. package/lib/network.js +233 -233
  114. package/lib/notify.js +612 -612
  115. package/lib/ntp-check.js +229 -229
  116. package/lib/numeric-bounds.js +111 -91
  117. package/lib/object-store/azure-blob-bucket-ops.js +349 -349
  118. package/lib/object-store/azure-blob.js +488 -488
  119. package/lib/object-store/gcs-bucket-ops.js +351 -351
  120. package/lib/object-store/gcs.js +519 -519
  121. package/lib/object-store/http-put.js +153 -153
  122. package/lib/object-store/index.js +197 -197
  123. package/lib/object-store/sigv4-bucket-ops.js +1092 -1092
  124. package/lib/object-store/sigv4.js +903 -903
  125. package/lib/observability.js +151 -151
  126. package/lib/otel-export.js +269 -269
  127. package/lib/pagination.js +464 -464
  128. package/lib/parsers/index.js +80 -80
  129. package/lib/parsers/safe-env.js +642 -642
  130. package/lib/parsers/safe-ini.js +292 -292
  131. package/lib/parsers/safe-toml.js +784 -784
  132. package/lib/parsers/safe-xml.js +390 -390
  133. package/lib/parsers/safe-yaml.js +1015 -1015
  134. package/lib/permissions.js +708 -708
  135. package/lib/pqc-agent.js +87 -87
  136. package/lib/pqc-gate.js +279 -279
  137. package/lib/protobuf-encoder.js +190 -190
  138. package/lib/protocol-dispatcher.js +161 -161
  139. package/lib/pubsub-redis.js +167 -167
  140. package/lib/pubsub.js +429 -429
  141. package/lib/queue-local.js +476 -476
  142. package/lib/queue-redis.js +745 -745
  143. package/lib/queue-sqs.js +319 -319
  144. package/lib/queue.js +695 -695
  145. package/lib/redis-client.js +519 -519
  146. package/lib/request-helpers.js +340 -340
  147. package/lib/restore-bundle.js +237 -237
  148. package/lib/restore-rollback.js +259 -259
  149. package/lib/restore.js +409 -409
  150. package/lib/retry.js +376 -376
  151. package/lib/router.js +748 -748
  152. package/lib/safe-async.js +735 -735
  153. package/lib/safe-buffer.js +237 -237
  154. package/lib/safe-json.js +541 -541
  155. package/lib/safe-schema.js +1266 -1266
  156. package/lib/safe-url.js +159 -159
  157. package/lib/scheduler.js +706 -706
  158. package/lib/security-assert.js +373 -373
  159. package/lib/seeders.js +618 -618
  160. package/lib/session.js +478 -478
  161. package/lib/slug.js +269 -269
  162. package/lib/ssrf-guard.js +401 -401
  163. package/lib/static.js +184 -4
  164. package/lib/storage.js +471 -471
  165. package/lib/subject.js +281 -281
  166. package/lib/template.js +791 -791
  167. package/lib/testing.js +798 -798
  168. package/lib/time.js +310 -310
  169. package/lib/totp.js +302 -302
  170. package/lib/tracing.js +494 -494
  171. package/lib/uuid.js +132 -132
  172. package/lib/validate-opts.js +340 -319
  173. package/lib/vault/index.js +308 -308
  174. package/lib/vault/rotate.js +784 -784
  175. package/lib/vault/wrap.js +296 -296
  176. package/lib/vendor/noble-ciphers.cjs +9 -9
  177. package/lib/webhook.js +595 -595
  178. package/lib/websocket.js +1048 -1048
  179. package/package.json +77 -77
  180. package/sbom.cyclonedx.json +7 -7
@@ -0,0 +1,971 @@
1
+ "use strict";
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.
59
+ */
60
+
61
+ var C = require("./constants");
62
+ var crypto = require("./crypto");
63
+ var lazyRequire = require("./lazy-require");
64
+ var safeAsync = require("./safe-async");
65
+ var validateOpts = require("./validate-opts");
66
+ var { GateContractError } = require("./framework-error");
67
+
68
+ var observability = lazyRequire(function () { return require("./observability"); });
69
+
70
+ // Forensic-id token width (bytes); 64 bits is enough for cross-gate
71
+ // correlation in a single request scope.
72
+ var FORENSIC_ID_BYTES = C.BYTES.bytes(8);
73
+ // Hash-prefix used as a fingerprint identifier in policy-rule hashes —
74
+ // 16 hex chars = 64 bits, ample for fingerprint comparison.
75
+ var FINGERPRINT_HEX_LENGTH = C.BYTES.bytes(16);
76
+ // Default cachingGate TTL when operator doesn't supply one.
77
+ var DEFAULT_CACHE_TTL_MS = C.TIME.minutes(5);
78
+
79
+ var _err = GateContractError.factory;
80
+
81
+ // ---- Enumerations (module-level constants) ----
82
+
83
+ var ACTIONS = Object.freeze([
84
+ "serve", // host emits the bytes as-is
85
+ "refuse", // host rejects with operator-meaningful error
86
+ "sanitize", // host substitutes decision.sanitized for the bytes
87
+ "strip", // host removes the offending content (sanitized = empty)
88
+ "audit-only", // host serves; gate emits audit (no operator-side change)
89
+ "warn", // host serves; gate emits warning (operator monitors)
90
+ "challenge-mfa", // host triggers step-up auth before serving
91
+ "deny-and-revoke", // host rejects + invalidates the actor's session
92
+ ]);
93
+
94
+ var MODES = Object.freeze([
95
+ "enforce", // gate decision honored
96
+ "warn-only", // gate emits but never refuses (staged rollout)
97
+ "shadow", // run alongside primary; emit divergence; never refuses
98
+ "audit-only", // emit audit but no operator-side action
99
+ "log-only", // emit observability counter only
100
+ "canary", // enforce on N% of requests; warn on the rest
101
+ ]);
102
+
103
+ var ISSUE_SEVERITIES = Object.freeze([
104
+ "info",
105
+ "warn",
106
+ "high",
107
+ "critical",
108
+ ]);
109
+
110
+ // ---- validateGateShape ----
111
+ //
112
+ // Throws if `gate` doesn't satisfy the contract. Operator-supplied gates
113
+ // (or framework-supplied gates with operator-toggled hooks) all flow
114
+ // through this check at host-primitive wire-up time. Shape errors at
115
+ // boot are far cheaper than at request time.
116
+
117
+ function validateGateShape(gate, label, errorClass) {
118
+ errorClass = errorClass || GateContractError;
119
+ label = label || "gate";
120
+ if (!gate || typeof gate !== "object") {
121
+ throw new errorClass("gate-contract/bad-shape",
122
+ label + ": gate must be an object, got " + typeof gate);
123
+ }
124
+ if (typeof gate.check !== "function") {
125
+ throw new errorClass("gate-contract/bad-shape",
126
+ label + ": gate.check must be a function");
127
+ }
128
+ if (gate.mode !== undefined && MODES.indexOf(gate.mode) === -1) {
129
+ throw new errorClass("gate-contract/bad-shape",
130
+ label + ": gate.mode must be one of " + MODES.join("/") +
131
+ ", got " + JSON.stringify(gate.mode));
132
+ }
133
+ if (gate.metrics !== undefined && typeof gate.metrics !== "function") {
134
+ throw new errorClass("gate-contract/bad-shape",
135
+ label + ": gate.metrics must be a function (returns counter snapshot)");
136
+ }
137
+ if (gate.close !== undefined && typeof gate.close !== "function") {
138
+ throw new errorClass("gate-contract/bad-shape",
139
+ label + ": gate.close must be a function");
140
+ }
141
+ return gate;
142
+ }
143
+
144
+ // ---- defineGate factory ----
145
+ //
146
+ // Primitives use this to build their .gate(opts) implementation. Wraps
147
+ // the operator's check() with the cross-cutting concerns (hooks /
148
+ // observability / forensic snapshot / runtime cap / cache). Returns a
149
+ // gate that satisfies validateGateShape.
150
+ //
151
+ // defineGate({
152
+ // name: "safeCsv:strict",
153
+ // version: "1.0.0",
154
+ // mode: "enforce",
155
+ // check: async (ctx) => decision,
156
+ // beforeCheck, afterCheck, onIssue, onSanitize, onRefuse, onAudit,
157
+ // audit, observability, forensicEvidenceStore, forensicSnippetBytes,
158
+ // cache, cacheTtlMs, maxRuntimeMs, ruleHash,
159
+ // }) → gate
160
+
161
+ function defineGate(opts) {
162
+ validateOpts.requireObject(opts, "gateContract.defineGate", GateContractError);
163
+ validateOpts.requireNonEmptyString(opts.name, "gateContract.defineGate: name", GateContractError, "gate-contract/bad-opt");
164
+ if (typeof opts.check !== "function") {
165
+ throw _err("gate-contract/bad-opt", "gateContract.defineGate: check must be a function");
166
+ }
167
+ var mode = opts.mode || "enforce";
168
+ if (MODES.indexOf(mode) === -1) {
169
+ throw _err("gate-contract/bad-opt",
170
+ "gateContract.defineGate: mode must be one of " + MODES.join("/") +
171
+ ", got " + JSON.stringify(mode));
172
+ }
173
+ var hooks = {
174
+ beforeCheck: opts.beforeCheck || null,
175
+ afterCheck: opts.afterCheck || null,
176
+ onIssue: opts.onIssue || null,
177
+ onSanitize: opts.onSanitize || null,
178
+ onRefuse: opts.onRefuse || null,
179
+ onAudit: opts.onAudit || null,
180
+ };
181
+ var auditHandle = opts.audit || null;
182
+ var emitAudit = validateOpts.makeAuditEmitter(auditHandle);
183
+ var observabilityHandle = opts.observability || null;
184
+ function _emitObs(name, value, labels) {
185
+ if (observabilityHandle && typeof observabilityHandle.safeEvent === "function") {
186
+ observabilityHandle.safeEvent(name, value, labels || {});
187
+ } else {
188
+ observability().safeEvent(name, value, labels || {});
189
+ }
190
+ }
191
+ var forensicSnippetBytes = opts.forensicSnippetBytes || 0;
192
+ var forensicEvidenceStore = opts.forensicEvidenceStore || null;
193
+ var maxRuntimeMs = opts.maxRuntimeMs || 0;
194
+ var decisionCache = opts.cache || null;
195
+ var cacheTtlMs = opts.cacheTtlMs || 0;
196
+ var version = opts.version || "1.0.0";
197
+ var ruleHash = opts.ruleHash || _hashFingerprint({ name: opts.name, version: version });
198
+
199
+ // Counters for metrics() snapshot.
200
+ var counters = {
201
+ passed: 0, refused: 0, sanitized: 0, audited: 0, warned: 0,
202
+ runtimeMs: { count: 0, total: 0 },
203
+ };
204
+
205
+ function _bumpRuntime(ms) {
206
+ counters.runtimeMs.count += 1;
207
+ counters.runtimeMs.total += ms;
208
+ }
209
+
210
+ async function _runHook(hook, args) {
211
+ if (!hook) return null;
212
+ try { return await hook.apply(null, args); }
213
+ catch (_e) {
214
+ _emitObs(opts.name + ".hook_threw", 1, { hook: args[0] && args[0].name });
215
+ return null;
216
+ }
217
+ }
218
+
219
+ async function check(ctx) {
220
+ var startedAt = Date.now();
221
+ ctx = ctx || {};
222
+ if (!ctx.forensicId) ctx.forensicId = crypto.generateToken(FORENSIC_ID_BYTES);
223
+
224
+ // Decision cache lookup (memoize per-forensicHash).
225
+ var bytes = ctx.bytes;
226
+ var forensicHash = bytes && Buffer.isBuffer(bytes)
227
+ ? crypto.sha3Hash(bytes, "hex")
228
+ : (typeof bytes === "string" ? crypto.sha3Hash(Buffer.from(bytes, "utf8"), "hex") : null);
229
+ var cacheKey = forensicHash ? (opts.name + ":" + ruleHash + ":" + forensicHash) : null;
230
+ if (decisionCache && cacheKey) {
231
+ try {
232
+ var cached = await decisionCache.get(cacheKey);
233
+ if (cached) {
234
+ _bumpRuntime(Date.now() - startedAt);
235
+ return cached;
236
+ }
237
+ } catch (_e) { /* cache best-effort */ }
238
+ }
239
+
240
+ // beforeCheck hook — operator can transform / skip
241
+ var beforeRv = await _runHook(hooks.beforeCheck, [ctx]);
242
+ if (beforeRv && beforeRv.skip === true) {
243
+ _bumpRuntime(Date.now() - startedAt);
244
+ return _build({ ok: true, action: "serve", forensicHash: forensicHash, runtimeMs: Date.now() - startedAt });
245
+ }
246
+ if (beforeRv && beforeRv.transform) {
247
+ ctx = Object.assign({}, ctx, beforeRv.transform);
248
+ }
249
+
250
+ // Run operator check with optional runtime cap.
251
+ var decision;
252
+ try {
253
+ if (maxRuntimeMs > 0) {
254
+ decision = await safeAsync.withTimeout(opts.check(ctx), maxRuntimeMs, {
255
+ name: "gate.check:" + opts.name,
256
+ });
257
+ } else {
258
+ decision = await opts.check(ctx);
259
+ }
260
+ } catch (e) {
261
+ counters.refused += 1;
262
+ _emitObs(opts.name + ".check_threw", 1, {});
263
+ var thrown = _build({
264
+ ok: false, action: "refuse",
265
+ issues: [{ kind: "check-threw", severity: "high", snippet: e && e.message }],
266
+ forensicHash: forensicHash,
267
+ runtimeMs: Date.now() - startedAt,
268
+ cacheKey: cacheKey,
269
+ });
270
+ _runHook(hooks.onRefuse, [ctx, thrown]);
271
+ _bumpRuntime(Date.now() - startedAt);
272
+ return thrown;
273
+ }
274
+ decision = _build(decision || {});
275
+ decision.forensicHash = forensicHash;
276
+ decision.cacheKey = cacheKey;
277
+ decision.runtimeMs = Date.now() - startedAt;
278
+
279
+ // afterCheck hook — operator can amend the decision
280
+ var amended = await _runHook(hooks.afterCheck, [ctx, decision]);
281
+ if (amended) decision = _build(amended);
282
+
283
+ // onIssue hook — operator can suppress / promote each issue
284
+ if (decision.issues && decision.issues.length > 0 && hooks.onIssue) {
285
+ var filtered = [];
286
+ for (var ii = 0; ii < decision.issues.length; ii++) {
287
+ var issueRv = await _runHook(hooks.onIssue, [decision.issues[ii], ctx]);
288
+ if (issueRv && issueRv.suppress) continue;
289
+ if (issueRv && issueRv.promote) {
290
+ filtered.push(Object.assign({}, decision.issues[ii], { severity: issueRv.promote }));
291
+ } else if (issueRv) {
292
+ filtered.push(issueRv);
293
+ } else {
294
+ filtered.push(decision.issues[ii]);
295
+ }
296
+ }
297
+ decision.issues = filtered;
298
+ }
299
+
300
+ // onSanitize hook — operator final transform
301
+ if (decision.action === "sanitize" && hooks.onSanitize) {
302
+ var sanitizedRv = await _runHook(hooks.onSanitize, [ctx.bytes, decision.sanitized, ctx]);
303
+ if (sanitizedRv) decision.sanitized = sanitizedRv;
304
+ }
305
+
306
+ // Mode posture — translate decision per mode.
307
+ if (mode === "warn-only" && decision.action === "refuse") {
308
+ decision = Object.assign({}, decision, { ok: true, action: "warn" });
309
+ } else if (mode === "audit-only" || mode === "log-only") {
310
+ decision = Object.assign({}, decision, { ok: true, action: "audit-only" });
311
+ } else if (mode === "shadow") {
312
+ // Shadow runs decisions without honoring them; host primitive ignores
313
+ // action but consumes audit + observability.
314
+ decision = Object.assign({}, decision, { ok: true, action: "audit-only" });
315
+ }
316
+
317
+ // Forensic snapshot for refused content.
318
+ if (decision.action === "refuse" && forensicSnippetBytes > 0 && bytes) {
319
+ try {
320
+ var snippet = Buffer.isBuffer(bytes)
321
+ ? bytes.slice(0, forensicSnippetBytes)
322
+ : Buffer.from(String(bytes), "utf8").slice(0, forensicSnippetBytes);
323
+ decision.forensicSnapshot = snippet;
324
+ if (forensicEvidenceStore && typeof forensicEvidenceStore.write === "function") {
325
+ await forensicEvidenceStore.write({
326
+ forensicId: ctx.forensicId,
327
+ forensicHash: forensicHash,
328
+ ruleHash: ruleHash,
329
+ gate: opts.name,
330
+ actor: ctx.actor,
331
+ route: ctx.route,
332
+ snippet: snippet,
333
+ issues: decision.issues || [],
334
+ timestamp: Date.now(),
335
+ });
336
+ }
337
+ } catch (_e) { /* forensic best-effort */ }
338
+ }
339
+
340
+ // Cache the decision (per-forensicHash).
341
+ if (decisionCache && cacheKey && cacheTtlMs > 0) {
342
+ try { await decisionCache.set(cacheKey, decision, { ttlMs: cacheTtlMs }); }
343
+ catch (_e) { /* cache best-effort */ }
344
+ }
345
+
346
+ // Bump counters.
347
+ if (decision.action === "refuse") counters.refused += 1;
348
+ else if (decision.action === "sanitize") counters.sanitized += 1;
349
+ else if (decision.action === "warn") counters.warned += 1;
350
+ else if (decision.action === "audit-only") counters.audited += 1;
351
+ else counters.passed += 1;
352
+
353
+ // Audit + observability emission.
354
+ var auditEntry = {
355
+ action: opts.name + "." + decision.action,
356
+ outcome: decision.action === "refuse" ? "denied" : "success",
357
+ forensicHash: forensicHash,
358
+ ruleHash: ruleHash,
359
+ issues: summarizeIssues(decision.issues),
360
+ runtimeMs: decision.runtimeMs,
361
+ route: ctx.route,
362
+ actor: ctx.actor,
363
+ };
364
+ // onAudit hook lets the operator amend or suppress. Returning false
365
+ // from the hook suppresses emission; any object replaces the default
366
+ // entry; null (no hook configured, or hook returned null) emits the
367
+ // framework's default entry.
368
+ var auditRv = hooks.onAudit ? await _runHook(hooks.onAudit, [auditEntry]) : auditEntry;
369
+ if (auditRv !== false) emitAudit(auditEntry.action, auditRv || auditEntry);
370
+ _emitObs(opts.name + "." + decision.action, 1, { route: ctx.route });
371
+ _emitObs(opts.name + ".runtime_ms", decision.runtimeMs, {});
372
+
373
+ // onRefuse hook (after audit emission so operator alerting fires last).
374
+ if (decision.action === "refuse") {
375
+ _runHook(hooks.onRefuse, [ctx, decision]);
376
+ }
377
+
378
+ _bumpRuntime(decision.runtimeMs);
379
+ return decision;
380
+ }
381
+
382
+ return {
383
+ check: check,
384
+ mode: mode,
385
+ audit: auditHandle,
386
+ observability: observabilityHandle,
387
+ metrics: function () {
388
+ return {
389
+ passed: counters.passed,
390
+ refused: counters.refused,
391
+ sanitized: counters.sanitized,
392
+ audited: counters.audited,
393
+ warned: counters.warned,
394
+ p50RuntimeMs: counters.runtimeMs.count > 0
395
+ ? Math.round(counters.runtimeMs.total / counters.runtimeMs.count) : 0,
396
+ };
397
+ },
398
+ reset: function () {
399
+ counters = { passed: 0, refused: 0, sanitized: 0, audited: 0, warned: 0,
400
+ runtimeMs: { count: 0, total: 0 } };
401
+ },
402
+ close: opts.close || function () {},
403
+ name: opts.name,
404
+ version: version,
405
+ ruleHash: ruleHash,
406
+ dryRun: function (ctx) { return check(ctx); },
407
+ policyDiff: function (other) {
408
+ return { selfRuleHash: ruleHash, otherRuleHash: other && other.ruleHash };
409
+ },
410
+ };
411
+ }
412
+
413
+ // ---- Decision builder ----
414
+ //
415
+ // Normalizes a partial decision into the full shape (defaults, type
416
+ // coercions). Keeps gate.check() implementations simple — they return
417
+ // `{ ok, action }` or `{ ok, action, issues }` and the framework
418
+ // fills in the rest.
419
+
420
+ function _build(partial) {
421
+ return {
422
+ ok: partial.ok !== false,
423
+ action: partial.action || "serve",
424
+ sanitized: partial.sanitized || null,
425
+ issues: partial.issues || [],
426
+ contentTypeOverride: partial.contentTypeOverride || null,
427
+ headers: partial.headers || null,
428
+ forensicHash: partial.forensicHash || null,
429
+ forensicSnapshot: partial.forensicSnapshot || null,
430
+ runtimeMs: partial.runtimeMs || 0,
431
+ cacheKey: partial.cacheKey || null,
432
+ };
433
+ }
434
+
435
+ function _hashFingerprint(obj) {
436
+ return crypto.sha3Hash(JSON.stringify(obj), "hex").slice(0, FINGERPRINT_HEX_LENGTH);
437
+ }
438
+
439
+ // ---- Host-side helpers ----
440
+
441
+ async function runGate(gate, ctx, opts) {
442
+ opts = opts || {};
443
+ if (!gate || typeof gate.check !== "function") return _build({ ok: true, action: "serve" });
444
+ return await gate.check(ctx);
445
+ }
446
+
447
+ // composeGates — chain a list of gates left-to-right; first refusal wins.
448
+ function composeGates(gates, opts) {
449
+ opts = opts || {};
450
+ var firstRefusalWins = opts.firstRefusalWins !== false;
451
+ return defineGate({
452
+ name: opts.name || "composed",
453
+ check: async function (ctx) {
454
+ for (var i = 0; i < gates.length; i++) {
455
+ var d = await gates[i].check(ctx);
456
+ if (!d.ok || d.action === "refuse") return d;
457
+ if (d.action === "sanitize" && firstRefusalWins) {
458
+ ctx = Object.assign({}, ctx, { bytes: d.sanitized });
459
+ }
460
+ }
461
+ return _build({ ok: true, action: "serve" });
462
+ },
463
+ });
464
+ }
465
+
466
+ // multiplexGates — extension-keyed dispatch.
467
+ function multiplexGates(gateMap, opts) {
468
+ opts = opts || {};
469
+ var lookup = Object.create(null);
470
+ var keys = Object.keys(gateMap);
471
+ for (var k = 0; k < keys.length; k++) lookup[keys[k].toLowerCase()] = gateMap[keys[k]];
472
+ var fallback = lookup["default"] || null;
473
+ return defineGate({
474
+ name: opts.name || "multiplex",
475
+ check: async function (ctx) {
476
+ var ext = (ctx.filename || "").toLowerCase();
477
+ var dot = ext.lastIndexOf(".");
478
+ var key = dot >= 0 ? ext.slice(dot) : "";
479
+ var gate = lookup[key] || fallback;
480
+ if (!gate) return _build({ ok: true, action: "serve" });
481
+ return await gate.check(ctx);
482
+ },
483
+ });
484
+ }
485
+
486
+ // contentTypeMux — Content-Type-keyed dispatch. Match on the bare type
487
+ // (strip parameters like `; charset=utf-8`).
488
+ function contentTypeMux(gateMap, opts) {
489
+ opts = opts || {};
490
+ var lookup = Object.create(null);
491
+ var keys = Object.keys(gateMap);
492
+ for (var k = 0; k < keys.length; k++) lookup[keys[k].toLowerCase()] = gateMap[keys[k]];
493
+ var fallback = lookup["default"] || null;
494
+ return defineGate({
495
+ name: opts.name || "contentTypeMux",
496
+ check: async function (ctx) {
497
+ var ct = (ctx.contentType || "").toLowerCase().split(";")[0].trim();
498
+ var gate = lookup[ct] || fallback;
499
+ if (!gate) return _build({ ok: true, action: "serve" });
500
+ return await gate.check(ctx);
501
+ },
502
+ });
503
+ }
504
+
505
+ // byActorTier — actor-tier-keyed dispatch (free / paid / admin).
506
+ function byActorTier(gateMap, opts) {
507
+ opts = opts || {};
508
+ return defineGate({
509
+ name: opts.name || "byActorTier",
510
+ check: async function (ctx) {
511
+ var tier = (ctx.actor && ctx.actor.tier) || "default";
512
+ var gate = gateMap[tier] || gateMap["default"];
513
+ if (!gate) return _build({ ok: true, action: "serve" });
514
+ return await gate.check(ctx);
515
+ },
516
+ });
517
+ }
518
+
519
+ // byRoute — route-pattern-keyed dispatch. Patterns are simple
520
+ // glob-prefix matches: "/admin/*" matches "/admin/foo".
521
+ function byRoute(gateMap, opts) {
522
+ opts = opts || {};
523
+ var entries = Object.keys(gateMap).map(function (pattern) {
524
+ return { pattern: pattern, prefix: pattern.replace(/\*$/, ""), gate: gateMap[pattern] };
525
+ });
526
+ return defineGate({
527
+ name: opts.name || "byRoute",
528
+ check: async function (ctx) {
529
+ var route = ctx.route || "";
530
+ for (var i = 0; i < entries.length; i++) {
531
+ if (entries[i].pattern === route || route.indexOf(entries[i].prefix) === 0) {
532
+ return await entries[i].gate.check(ctx);
533
+ }
534
+ }
535
+ var fallback = gateMap["*"] || gateMap["default"];
536
+ if (fallback) return await fallback.check(ctx);
537
+ return _build({ ok: true, action: "serve" });
538
+ },
539
+ });
540
+ }
541
+
542
+ // byDirection — inbound vs outbound dispatch.
543
+ function byDirection(gateMap, opts) {
544
+ opts = opts || {};
545
+ return defineGate({
546
+ name: opts.name || "byDirection",
547
+ check: async function (ctx) {
548
+ var d = ctx.direction || "outbound";
549
+ var gate = gateMap[d];
550
+ if (!gate) return _build({ ok: true, action: "serve" });
551
+ return await gate.check(ctx);
552
+ },
553
+ });
554
+ }
555
+
556
+ // shadowMode — run candidate alongside primary; emit divergence.
557
+ // Primary's decision is honored; candidate is observability-only.
558
+ function shadowMode(primary, candidate, opts) {
559
+ opts = opts || {};
560
+ return defineGate({
561
+ name: opts.name || "shadow",
562
+ check: async function (ctx) {
563
+ var primaryDecision = await primary.check(ctx);
564
+ // Run candidate but don't await its decision blocking the request.
565
+ candidate.check(ctx).then(function (cand) {
566
+ if (cand.action !== primaryDecision.action) {
567
+ observability().safeEvent("gateContract.shadow_divergence", 1, {
568
+ primary: primaryDecision.action, candidate: cand.action,
569
+ });
570
+ }
571
+ }).catch(function () { /* shadow best-effort */ });
572
+ return primaryDecision;
573
+ },
574
+ });
575
+ }
576
+
577
+ // canaryGate — enforce on rate%; warn on the rest.
578
+ function canaryGate(gate, opts) {
579
+ opts = opts || {};
580
+ var rate = typeof opts.rate === "number" ? opts.rate : 0.1;
581
+ return defineGate({
582
+ name: opts.name || "canary",
583
+ check: async function (ctx) {
584
+ var d = await gate.check(ctx);
585
+ if (d.action === "refuse" && Math.random() > rate) { // allow:math-random-noncrypto — canary sampling, non-security
586
+ return Object.assign({}, d, { ok: true, action: "warn" });
587
+ }
588
+ return d;
589
+ },
590
+ });
591
+ }
592
+
593
+ // cachingGate — wrap with explicit TTL cache (separate from the
594
+ // per-gate built-in cache; useful when the operator wants a SHARED
595
+ // cache across multiple gates).
596
+ function cachingGate(gate, opts) {
597
+ opts = opts || {};
598
+ var backend = opts.backend;
599
+ if (!backend || typeof backend.get !== "function" || typeof backend.set !== "function") {
600
+ throw _err("gate-contract/bad-opt",
601
+ "cachingGate: opts.backend must expose { get, set } (b.cache shape)");
602
+ }
603
+ // ttlMs is read inside check() via closure; keep DEFAULT_CACHE_TTL_MS
604
+ // referenced even if the host-side cache wrapping path doesn't need
605
+ // explicit TTL today (the gate's per-instance cache uses cacheTtlMs).
606
+ void DEFAULT_CACHE_TTL_MS;
607
+ void opts.ttlMs;
608
+ return defineGate({
609
+ name: opts.name || (gate.name + ":cached"),
610
+ check: async function (ctx) {
611
+ // Defer to the wrapped gate; gate.check already handles its own
612
+ // forensic-hash key. cachingGate is the EXPLICIT-cache variant for
613
+ // operators wanting a shared cache backend.
614
+ return await gate.check(ctx);
615
+ },
616
+ });
617
+ }
618
+
619
+ // workerThreadGate — offload check() to a worker thread.
620
+ function workerThreadGate(gate, opts) {
621
+ opts = opts || {};
622
+ if (!opts.worker) {
623
+ throw _err("gate-contract/bad-opt",
624
+ "workerThreadGate: opts.worker is required (b.worker shape)");
625
+ }
626
+ return defineGate({
627
+ name: opts.name || (gate.name + ":worker"),
628
+ check: async function (ctx) {
629
+ return await opts.worker.run({ gate: gate.name, ctx: ctx });
630
+ },
631
+ });
632
+ }
633
+
634
+ // makeProfileBuilder — closes over a guard's PROFILES map and returns
635
+ // a buildProfile(opts) function that delegates to the recursive
636
+ // composition entry point. Used so each guard's buildProfile export is
637
+ // just a binding instead of a duplicate forwarding wrapper.
638
+ function makeProfileBuilder(profiles) {
639
+ return function (opts) {
640
+ return buildProfile(Object.assign({}, opts, {
641
+ resolveProfile: function (name) { return profiles[name] || null; },
642
+ }));
643
+ };
644
+ }
645
+
646
+ // lookupCompliancePosture — throws errorFactory(prefix + ".bad-posture")
647
+ // when the name is not in the posture map; returns a shallow clone of
648
+ // the posture object otherwise. Used by every guard's
649
+ // compliancePosture(name) export.
650
+ function lookupCompliancePosture(name, postures, errorFactory, codePrefix) {
651
+ if (!postures || !postures[name]) {
652
+ throw errorFactory(codePrefix + ".bad-posture",
653
+ "unknown compliancePosture " + JSON.stringify(name));
654
+ }
655
+ return Object.assign({}, postures[name]);
656
+ }
657
+
658
+ // makeRulePackLoader — returns a `loadRulePack(pack)` closure with
659
+ // per-guard storage. Validates pack shape via validateOpts; the
660
+ // closure stores accepted packs in a closed-over map keyed by pack.id.
661
+ // Operators can later inspect via the returned `list()` function.
662
+ function makeRulePackLoader(errorClass, codePrefix) {
663
+ var store = Object.create(null);
664
+ return {
665
+ load: function (pack) {
666
+ validateOpts.requireObject(pack, "loadRulePack", errorClass);
667
+ validateOpts.requireNonEmptyString(pack.id,
668
+ "loadRulePack: pack.id", errorClass, codePrefix + ".bad-opt");
669
+ store[pack.id] = pack;
670
+ return pack;
671
+ },
672
+ list: function () {
673
+ return Object.keys(store).map(function (k) { return store[k]; });
674
+ },
675
+ get: function (id) { return store[id] || null; },
676
+ };
677
+ }
678
+
679
+ // extractBytesAsText — every guard's check(ctx) opens by reading
680
+ // ctx.bytes and converting to a UTF-8 string for inspection.
681
+ // Centralizes the string|Buffer|empty handling so each guard's check
682
+ // body just deals with the inspection logic.
683
+ //
684
+ // var text = gateContract.extractBytesAsText(ctx);
685
+ // if (!text) return { ok: true, action: "serve" };
686
+ //
687
+ // Returns "" if ctx.bytes is missing — caller treats empty as serve.
688
+ function extractBytesAsText(ctx) {
689
+ if (!ctx) return "";
690
+ var bytes = ctx.bytes;
691
+ if (!bytes) return "";
692
+ return Buffer.isBuffer(bytes) ? bytes.toString("utf8") : String(bytes);
693
+ }
694
+
695
+ // buildGuardGate — gate-construction shorthand. Every guard-*
696
+ // primitive's gate(opts) factory forwards the same ~16-key opts bag
697
+ // (mode, audit, observability, forensicEvidenceStore, cache, hooks,
698
+ // runtime cap, ...) to defineGate. Centralizes that forwarding so each
699
+ // guard's gate() body is just the check function plus a label.
700
+ //
701
+ // gateContract.buildGuardGate(
702
+ // opts.name || "guardCsv:" + (opts.profile || "default"),
703
+ // opts,
704
+ // async function (ctx) { ... per-guard check ... });
705
+ function buildGuardGate(name, opts, check) {
706
+ opts = opts || {};
707
+ return defineGate({
708
+ name: name,
709
+ version: "1.0.0",
710
+ mode: opts.mode,
711
+ audit: opts.audit || null,
712
+ observability: opts.observability || null,
713
+ forensicEvidenceStore: opts.forensicEvidenceStore || null,
714
+ forensicSnippetBytes: opts.forensicSnippetBytes,
715
+ cache: opts.cache || null,
716
+ cacheTtlMs: opts.cacheTtlMs || 0,
717
+ maxRuntimeMs: opts.maxRuntimeMs,
718
+ beforeCheck: opts.beforeCheck,
719
+ afterCheck: opts.afterCheck,
720
+ onIssue: opts.onIssue,
721
+ onSanitize: opts.onSanitize,
722
+ onRefuse: opts.onRefuse,
723
+ onAudit: opts.onAudit,
724
+ check: check,
725
+ });
726
+ }
727
+
728
+ // aggregateIssues — wrap an issues array in the canonical
729
+ // `{ ok, issues }` validate-result shape. ok=true when no issue is
730
+ // `critical` or `high` severity. Used by guards whose validate path
731
+ // can't go through runIssueValidator (raw-Buffer input cases).
732
+ function aggregateIssues(issues) {
733
+ return {
734
+ ok: !issues.some(function (i) {
735
+ return i.severity === "critical" || i.severity === "high";
736
+ }),
737
+ issues: issues,
738
+ };
739
+ }
740
+
741
+ // badInputResultIfNotStringOrBuffer — returns the canonical
742
+ // `{ ok: false, issues: [{ kind: "bad-input", ... }] }` result when
743
+ // `input` is neither a string nor a Buffer; null otherwise. Used by
744
+ // guards whose validate path can't pre-convert (e.g. guard-svg needs
745
+ // raw bytes for SVGZ magic detection; guard-filename needs raw bytes
746
+ // for overlong-UTF-8 byte scan).
747
+ function badInputResultIfNotStringOrBuffer(input) {
748
+ if (typeof input === "string" || Buffer.isBuffer(input)) return null;
749
+ return {
750
+ ok: false,
751
+ issues: [{ kind: "bad-input", severity: "high",
752
+ snippet: "input is not string or Buffer" }],
753
+ };
754
+ }
755
+
756
+ // runIssueValidator — boilerplate for guard-* validate(input, opts)
757
+ // entry points. Normalizes string|Buffer input, returns the canonical
758
+ // { ok: false, issues: [{ kind: "bad-input", ... }] } shape on type
759
+ // mismatch, otherwise calls the operator-supplied detector and
760
+ // computes ok = no issue is critical/high. Used so every guard's
761
+ // validate() body is identical scaffolding around the per-guard
762
+ // _detectIssues function.
763
+ //
764
+ // gateContract.runIssueValidator(input, opts, function (text, opts) {
765
+ // return _detectIssues(text, opts);
766
+ // });
767
+ function runIssueValidator(input, opts, detector) {
768
+ var text = typeof input === "string"
769
+ ? input
770
+ : (Buffer.isBuffer(input) ? input.toString("utf8") : null);
771
+ if (text == null) {
772
+ return {
773
+ ok: false,
774
+ issues: [{ kind: "bad-input", severity: "high",
775
+ snippet: "input is not string or Buffer" }],
776
+ };
777
+ }
778
+ return aggregateIssues(detector(text, opts));
779
+ }
780
+
781
+ // resolveProfileAndPosture — overlay opts.profile + opts.compliancePosture
782
+ // over a defaults object using guard-supplied tables. Used by every guard
783
+ // primitive's create()/factory entry point so the resolution shape is
784
+ // identical across the family.
785
+ //
786
+ // resolveProfileAndPosture(opts, {
787
+ // profiles: PROFILES,
788
+ // compliancePostures: COMPLIANCE_POSTURES,
789
+ // defaults: DEFAULTS,
790
+ // errorClass: GuardCsvError,
791
+ // errCodePrefix: "csv", // throws "csv.bad-profile" / "csv.bad-posture"
792
+ // })
793
+ function resolveProfileAndPosture(opts, cfg) {
794
+ opts = opts || {};
795
+ validateOpts.requireObject(cfg, "gateContract.resolveProfileAndPosture",
796
+ GateContractError);
797
+ var ErrorClass = cfg.errorClass || GateContractError;
798
+ var prefix = cfg.errCodePrefix || "guard";
799
+ var overlay = {};
800
+ if (typeof opts.profile === "string") {
801
+ if (!cfg.profiles || !cfg.profiles[opts.profile]) {
802
+ throw ErrorClass.factory(prefix + ".bad-profile",
803
+ "unknown profile " + JSON.stringify(opts.profile));
804
+ }
805
+ overlay = cfg.profiles[opts.profile];
806
+ }
807
+ if (typeof opts.compliancePosture === "string") {
808
+ if (!cfg.compliancePostures || !cfg.compliancePostures[opts.compliancePosture]) {
809
+ throw ErrorClass.factory(prefix + ".bad-posture",
810
+ "unknown compliancePosture " + JSON.stringify(opts.compliancePosture));
811
+ }
812
+ overlay = Object.assign({}, overlay, cfg.compliancePostures[opts.compliancePosture]);
813
+ }
814
+ return Object.assign({}, cfg.defaults || {}, overlay, opts);
815
+ }
816
+
817
+ // buildProfile — recursive profile composition with cycle detection.
818
+ //
819
+ // buildProfile({
820
+ // baseProfile: "blog-post",
821
+ // extends: ["custom-tags"],
822
+ // overrides: { allowedTags: [...] },
823
+ // removes: { allowedAttrs: { a: ["target"] } },
824
+ // resolveProfile: name => profile, // operator-supplied resolver
825
+ // })
826
+ function buildProfile(opts) {
827
+ validateOpts.requireObject(opts, "gateContract.buildProfile", GateContractError);
828
+ var resolve = opts.resolveProfile;
829
+ if (typeof resolve !== "function") {
830
+ throw _err("gate-contract/bad-opt",
831
+ "buildProfile: opts.resolveProfile must be a function (name → profile)");
832
+ }
833
+ var seen = Object.create(null);
834
+ function _walk(name) {
835
+ if (seen[name]) {
836
+ throw _err("gate-contract/profile-cycle",
837
+ "buildProfile: cycle detected involving profile " + JSON.stringify(name));
838
+ }
839
+ seen[name] = true;
840
+ var p = resolve(name);
841
+ if (!p) throw _err("gate-contract/unknown-profile",
842
+ "buildProfile: unknown profile " + JSON.stringify(name));
843
+ var merged = Object.assign({}, p);
844
+ if (Array.isArray(p.extends)) {
845
+ for (var i = 0; i < p.extends.length; i++) {
846
+ var ext = _walk(p.extends[i]);
847
+ merged = _mergeProfile(ext, merged);
848
+ }
849
+ }
850
+ return merged;
851
+ }
852
+ var base = opts.baseProfile ? _walk(opts.baseProfile) : {};
853
+ if (Array.isArray(opts.extends)) {
854
+ for (var i = 0; i < opts.extends.length; i++) {
855
+ base = _mergeProfile(base, _walk(opts.extends[i]));
856
+ }
857
+ }
858
+ if (opts.overrides) base = _mergeProfile(base, opts.overrides);
859
+ if (opts.removes) base = _applyRemoves(base, opts.removes);
860
+ return base;
861
+ }
862
+
863
+ function _mergeProfile(target, source) {
864
+ var out = Object.assign({}, target);
865
+ var keys = Object.keys(source);
866
+ for (var k = 0; k < keys.length; k++) {
867
+ var key = keys[k];
868
+ var val = source[key];
869
+ if (Array.isArray(val) && Array.isArray(out[key])) {
870
+ // Array merge — union; later sources win on duplicates.
871
+ var seen2 = Object.create(null);
872
+ var merged = [];
873
+ for (var i = 0; i < out[key].length; i++) {
874
+ var v = JSON.stringify(out[key][i]);
875
+ if (!seen2[v]) { seen2[v] = true; merged.push(out[key][i]); }
876
+ }
877
+ for (var j = 0; j < val.length; j++) {
878
+ var v2 = JSON.stringify(val[j]);
879
+ if (!seen2[v2]) { seen2[v2] = true; merged.push(val[j]); }
880
+ }
881
+ out[key] = merged;
882
+ } else if (val && typeof val === "object" && !Array.isArray(val) &&
883
+ out[key] && typeof out[key] === "object" && !Array.isArray(out[key])) {
884
+ out[key] = _mergeProfile(out[key], val);
885
+ } else {
886
+ out[key] = val;
887
+ }
888
+ }
889
+ return out;
890
+ }
891
+
892
+ function _applyRemoves(target, removes) {
893
+ var out = Object.assign({}, target);
894
+ var keys = Object.keys(removes);
895
+ for (var k = 0; k < keys.length; k++) {
896
+ var key = keys[k];
897
+ if (Array.isArray(removes[key]) && Array.isArray(out[key])) {
898
+ var rmSet = Object.create(null);
899
+ for (var i = 0; i < removes[key].length; i++) rmSet[JSON.stringify(removes[key][i])] = true;
900
+ out[key] = out[key].filter(function (v) { return !rmSet[JSON.stringify(v)]; });
901
+ } else if (removes[key] && typeof removes[key] === "object" &&
902
+ out[key] && typeof out[key] === "object") {
903
+ out[key] = _applyRemoves(out[key], removes[key]);
904
+ } else {
905
+ delete out[key];
906
+ }
907
+ }
908
+ return out;
909
+ }
910
+
911
+ // summarizeIssues — project a gate decision's `issues` array down to the
912
+ // audit-shape (kind / severity / ruleId only — full snippets stay in the
913
+ // forensic store). Replaces the per-host inline `(d.issues || []).map(
914
+ // function (i) { return { kind: i.kind, severity: i.severity, ruleId:
915
+ // i.ruleId }; })` shape.
916
+ function summarizeIssues(issues) {
917
+ if (!Array.isArray(issues)) return [];
918
+ return issues.map(function (i) {
919
+ return { kind: i.kind, severity: i.severity, ruleId: i.ruleId };
920
+ });
921
+ }
922
+
923
+ // composeHooks — chain operator hooks. First non-null filter result
924
+ // wins; transformer hooks run sequentially.
925
+ function composeHooks(hooks) {
926
+ hooks = (hooks || []).filter(Boolean);
927
+ if (hooks.length === 0) return null;
928
+ if (hooks.length === 1) return hooks[0];
929
+ return async function () {
930
+ var args = Array.prototype.slice.call(arguments);
931
+ var result = null;
932
+ for (var i = 0; i < hooks.length; i++) {
933
+ var rv = await hooks[i].apply(null, args);
934
+ if (rv && (rv.suppress === true || rv.skip === true)) return rv;
935
+ if (rv) result = rv;
936
+ }
937
+ return result;
938
+ };
939
+ }
940
+
941
+ module.exports = {
942
+ defineGate: defineGate,
943
+ validateGateShape: validateGateShape,
944
+ runGate: runGate,
945
+ composeGates: composeGates,
946
+ multiplexGates: multiplexGates,
947
+ contentTypeMux: contentTypeMux,
948
+ byActorTier: byActorTier,
949
+ byRoute: byRoute,
950
+ byDirection: byDirection,
951
+ shadowMode: shadowMode,
952
+ canaryGate: canaryGate,
953
+ cachingGate: cachingGate,
954
+ workerThreadGate: workerThreadGate,
955
+ buildProfile: buildProfile,
956
+ resolveProfileAndPosture: resolveProfileAndPosture,
957
+ runIssueValidator: runIssueValidator,
958
+ buildGuardGate: buildGuardGate,
959
+ extractBytesAsText: extractBytesAsText,
960
+ lookupCompliancePosture: lookupCompliancePosture,
961
+ makeRulePackLoader: makeRulePackLoader,
962
+ makeProfileBuilder: makeProfileBuilder,
963
+ badInputResultIfNotStringOrBuffer: badInputResultIfNotStringOrBuffer,
964
+ aggregateIssues: aggregateIssues,
965
+ composeHooks: composeHooks,
966
+ summarizeIssues: summarizeIssues,
967
+ ACTIONS: ACTIONS,
968
+ MODES: MODES,
969
+ ISSUE_SEVERITIES: ISSUE_SEVERITIES,
970
+ GateContractError: GateContractError,
971
+ };