@blamejs/core 0.8.43 → 0.8.50

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (222) hide show
  1. package/CHANGELOG.md +93 -0
  2. package/README.md +10 -10
  3. package/index.js +52 -0
  4. package/lib/a2a.js +159 -34
  5. package/lib/acme.js +762 -0
  6. package/lib/ai-pref.js +166 -43
  7. package/lib/api-key.js +108 -47
  8. package/lib/api-snapshot.js +157 -40
  9. package/lib/app-shutdown.js +113 -77
  10. package/lib/archive.js +337 -40
  11. package/lib/arg-parser.js +697 -0
  12. package/lib/asyncapi.js +99 -55
  13. package/lib/atomic-file.js +465 -104
  14. package/lib/audit-chain.js +123 -34
  15. package/lib/audit-daily-review.js +389 -0
  16. package/lib/audit-sign.js +302 -56
  17. package/lib/audit-tools.js +412 -63
  18. package/lib/audit.js +656 -35
  19. package/lib/auth/jwt-external.js +17 -0
  20. package/lib/auth/oauth.js +7 -0
  21. package/lib/auth-bot-challenge.js +505 -0
  22. package/lib/auth-header.js +92 -25
  23. package/lib/backup/bundle.js +26 -0
  24. package/lib/backup/index.js +512 -89
  25. package/lib/backup/manifest.js +168 -7
  26. package/lib/break-glass.js +415 -39
  27. package/lib/budr.js +103 -30
  28. package/lib/bundler.js +86 -66
  29. package/lib/cache.js +192 -72
  30. package/lib/chain-writer.js +65 -40
  31. package/lib/circuit-breaker.js +56 -33
  32. package/lib/cli-helpers.js +106 -75
  33. package/lib/cli.js +6 -30
  34. package/lib/cloud-events.js +99 -32
  35. package/lib/cluster-storage.js +162 -37
  36. package/lib/cluster.js +340 -49
  37. package/lib/codepoint-class.js +66 -0
  38. package/lib/compliance.js +424 -24
  39. package/lib/config-drift.js +111 -46
  40. package/lib/config.js +94 -40
  41. package/lib/consent.js +165 -18
  42. package/lib/constants.js +1 -0
  43. package/lib/content-credentials.js +153 -48
  44. package/lib/cookies.js +154 -62
  45. package/lib/credential-hash.js +133 -61
  46. package/lib/crypto-field.js +702 -18
  47. package/lib/crypto-hpke.js +256 -0
  48. package/lib/crypto.js +744 -22
  49. package/lib/csv.js +178 -35
  50. package/lib/daemon.js +456 -0
  51. package/lib/dark-patterns.js +186 -55
  52. package/lib/db-query.js +79 -2
  53. package/lib/db.js +1431 -60
  54. package/lib/ddl-change-control.js +523 -0
  55. package/lib/deprecate.js +195 -40
  56. package/lib/dev.js +82 -39
  57. package/lib/dora.js +67 -48
  58. package/lib/dr-runbook.js +368 -0
  59. package/lib/dsr.js +142 -11
  60. package/lib/dual-control.js +91 -56
  61. package/lib/events.js +120 -41
  62. package/lib/external-db-migrate.js +192 -2
  63. package/lib/external-db.js +795 -50
  64. package/lib/fapi2.js +122 -1
  65. package/lib/fda-21cfr11.js +395 -0
  66. package/lib/fdx.js +132 -2
  67. package/lib/file-type.js +87 -0
  68. package/lib/file-upload.js +93 -0
  69. package/lib/flag.js +82 -20
  70. package/lib/forms.js +132 -29
  71. package/lib/framework-error.js +169 -0
  72. package/lib/framework-schema.js +163 -35
  73. package/lib/gate-contract.js +849 -175
  74. package/lib/graphql-federation.js +68 -7
  75. package/lib/guard-all.js +172 -55
  76. package/lib/guard-archive.js +286 -124
  77. package/lib/guard-auth.js +194 -21
  78. package/lib/guard-cidr.js +190 -28
  79. package/lib/guard-csv.js +397 -51
  80. package/lib/guard-domain.js +213 -91
  81. package/lib/guard-email.js +236 -29
  82. package/lib/guard-filename.js +307 -75
  83. package/lib/guard-graphql.js +263 -30
  84. package/lib/guard-html.js +310 -116
  85. package/lib/guard-image.js +243 -30
  86. package/lib/guard-json.js +260 -54
  87. package/lib/guard-jsonpath.js +235 -23
  88. package/lib/guard-jwt.js +284 -30
  89. package/lib/guard-markdown.js +204 -22
  90. package/lib/guard-mime.js +190 -26
  91. package/lib/guard-oauth.js +277 -28
  92. package/lib/guard-pdf.js +251 -27
  93. package/lib/guard-regex.js +226 -18
  94. package/lib/guard-shell.js +229 -26
  95. package/lib/guard-svg.js +177 -10
  96. package/lib/guard-template.js +232 -21
  97. package/lib/guard-time.js +195 -29
  98. package/lib/guard-uuid.js +189 -30
  99. package/lib/guard-xml.js +259 -36
  100. package/lib/guard-yaml.js +241 -44
  101. package/lib/honeytoken.js +63 -27
  102. package/lib/html-balance.js +83 -0
  103. package/lib/http-client.js +486 -59
  104. package/lib/http-message-signature.js +582 -0
  105. package/lib/i18n.js +102 -49
  106. package/lib/iab-mspa.js +112 -32
  107. package/lib/iab-tcf.js +107 -2
  108. package/lib/inbox.js +90 -52
  109. package/lib/keychain.js +865 -0
  110. package/lib/legal-hold.js +374 -0
  111. package/lib/local-db-thin.js +320 -0
  112. package/lib/log-stream.js +281 -51
  113. package/lib/log.js +184 -86
  114. package/lib/mail-bounce.js +107 -62
  115. package/lib/mail.js +295 -58
  116. package/lib/mcp.js +108 -27
  117. package/lib/metrics.js +98 -89
  118. package/lib/middleware/age-gate.js +36 -0
  119. package/lib/middleware/ai-act-disclosure.js +37 -0
  120. package/lib/middleware/api-encrypt.js +45 -0
  121. package/lib/middleware/assetlinks.js +40 -0
  122. package/lib/middleware/asyncapi-serve.js +35 -0
  123. package/lib/middleware/attach-user.js +40 -0
  124. package/lib/middleware/bearer-auth.js +40 -0
  125. package/lib/middleware/body-parser.js +230 -0
  126. package/lib/middleware/bot-disclose.js +34 -0
  127. package/lib/middleware/bot-guard.js +39 -0
  128. package/lib/middleware/compression.js +37 -0
  129. package/lib/middleware/cookies.js +32 -0
  130. package/lib/middleware/cors.js +40 -0
  131. package/lib/middleware/csp-nonce.js +40 -0
  132. package/lib/middleware/csp-report.js +34 -0
  133. package/lib/middleware/csrf-protect.js +43 -0
  134. package/lib/middleware/daily-byte-quota.js +53 -85
  135. package/lib/middleware/db-role-for.js +40 -0
  136. package/lib/middleware/dpop.js +40 -0
  137. package/lib/middleware/error-handler.js +37 -14
  138. package/lib/middleware/fetch-metadata.js +39 -0
  139. package/lib/middleware/flag-context.js +34 -0
  140. package/lib/middleware/gpc.js +33 -0
  141. package/lib/middleware/headers.js +35 -0
  142. package/lib/middleware/health.js +46 -0
  143. package/lib/middleware/host-allowlist.js +30 -0
  144. package/lib/middleware/network-allowlist.js +38 -0
  145. package/lib/middleware/openapi-serve.js +34 -0
  146. package/lib/middleware/rate-limit.js +160 -18
  147. package/lib/middleware/request-id.js +36 -18
  148. package/lib/middleware/request-log.js +37 -0
  149. package/lib/middleware/require-aal.js +29 -0
  150. package/lib/middleware/require-auth.js +32 -0
  151. package/lib/middleware/require-bound-key.js +41 -0
  152. package/lib/middleware/require-content-type.js +32 -0
  153. package/lib/middleware/require-methods.js +27 -0
  154. package/lib/middleware/require-mtls.js +33 -0
  155. package/lib/middleware/require-step-up.js +37 -0
  156. package/lib/middleware/security-headers.js +44 -0
  157. package/lib/middleware/security-txt.js +38 -0
  158. package/lib/middleware/span-http-server.js +37 -0
  159. package/lib/middleware/sse.js +36 -0
  160. package/lib/middleware/trace-log-correlation.js +33 -0
  161. package/lib/middleware/trace-propagate.js +32 -0
  162. package/lib/middleware/tus-upload.js +90 -0
  163. package/lib/middleware/web-app-manifest.js +53 -0
  164. package/lib/mtls-ca.js +100 -70
  165. package/lib/network-byte-quota.js +308 -0
  166. package/lib/network-heartbeat.js +135 -0
  167. package/lib/network-tls.js +534 -4
  168. package/lib/network.js +103 -0
  169. package/lib/notify.js +114 -43
  170. package/lib/ntp-check.js +192 -51
  171. package/lib/observability.js +145 -47
  172. package/lib/openapi.js +90 -44
  173. package/lib/outbox.js +99 -1
  174. package/lib/pagination.js +168 -86
  175. package/lib/parsers/index.js +16 -5
  176. package/lib/permissions.js +93 -40
  177. package/lib/pqc-agent.js +84 -8
  178. package/lib/pqc-software.js +94 -60
  179. package/lib/process-spawn.js +95 -21
  180. package/lib/pubsub.js +96 -66
  181. package/lib/queue.js +375 -54
  182. package/lib/redact.js +793 -21
  183. package/lib/render.js +139 -47
  184. package/lib/request-helpers.js +485 -121
  185. package/lib/restore-bundle.js +142 -39
  186. package/lib/restore-rollback.js +136 -45
  187. package/lib/retention.js +178 -50
  188. package/lib/retry.js +116 -33
  189. package/lib/router.js +475 -23
  190. package/lib/safe-async.js +543 -94
  191. package/lib/safe-buffer.js +337 -41
  192. package/lib/safe-json.js +467 -62
  193. package/lib/safe-jsonpath.js +285 -0
  194. package/lib/safe-schema.js +631 -87
  195. package/lib/safe-sql.js +221 -59
  196. package/lib/safe-url.js +278 -46
  197. package/lib/sandbox-worker.js +135 -0
  198. package/lib/sandbox.js +358 -0
  199. package/lib/scheduler.js +135 -70
  200. package/lib/self-update.js +647 -0
  201. package/lib/session-device-binding.js +431 -0
  202. package/lib/session.js +259 -49
  203. package/lib/slug.js +138 -26
  204. package/lib/ssrf-guard.js +316 -56
  205. package/lib/storage.js +433 -70
  206. package/lib/subject.js +405 -23
  207. package/lib/template.js +148 -8
  208. package/lib/tenant-quota.js +545 -0
  209. package/lib/testing.js +440 -53
  210. package/lib/time.js +291 -23
  211. package/lib/tls-exporter.js +239 -0
  212. package/lib/tracing.js +90 -74
  213. package/lib/uuid.js +97 -22
  214. package/lib/vault/index.js +284 -22
  215. package/lib/vault/seal-pem-file.js +66 -0
  216. package/lib/watcher.js +368 -0
  217. package/lib/webhook.js +196 -63
  218. package/lib/websocket.js +393 -68
  219. package/lib/wiki-concepts.js +338 -0
  220. package/lib/worker-pool.js +464 -0
  221. package/package.json +3 -3
  222. package/sbom.cyclonedx.json +7 -7
package/lib/redact.js CHANGED
@@ -1,29 +1,48 @@
1
1
  "use strict";
2
2
  /**
3
- * Operational-log redaction layer.
4
- *
5
- * Wraps every operational log emit so PHI / PCI / personal data never
6
- * reaches a debug sink, file, or external SIEM. Two complementary signals:
7
- *
8
- * 1. Field-name allow/deny: known sensitive fields are scrubbed by name
9
- * regardless of the value's contents.
10
- * 2. Pattern detection: even if a field name slips past, common-shape
11
- * detectors (credit-card via Luhn, JWT, PEM blocks, AWS-key-shape)
12
- * catch the value and replace with a marker.
13
- *
14
- * The redactor returns a NEW object — the original log payload is never
15
- * mutated. This matters because the same object may be passed to the
16
- * audit-log path (which DOES seal sensitive content via vault) AND the
17
- * operational path (which redacts) in the same call.
18
- *
19
- * Public API:
20
- * redact.redact(value, opts?) → redacted value
21
- * redact.registerFieldRule(pattern, replacement)
22
- * redact.registerValueDetector(name, fn, replacement)
23
- * redact.MARKER → '[REDACTED]'
3
+ * @module b.redact
4
+ * @nav Observability
5
+ * @title Redact
6
+ *
7
+ * @intro
8
+ * Operational-log redaction regex-shape and field-name rules that
9
+ * strip PII / secrets out of every log payload before it reaches a
10
+ * file, debug sink, or external SIEM.
11
+ *
12
+ * Two complementary signals run on every walk: a sensitive-field
13
+ * name set (case-insensitive substring match against keys like
14
+ * `password`, `api_key`, `authorization`, `dpop`, `client_secret`,
15
+ * `refresh_token`) and a value-shape detector chain (Luhn-validated
16
+ * credit-card numbers, JWS triplets, PEM / OpenSSH private-key
17
+ * blocks, AWS access-key prefixes, vault-sealed ciphertexts,
18
+ * connection-string credential leaks). Field-name hits replace the
19
+ * whole value with the configured marker; value-shape hits replace
20
+ * with a per-detector marker (`[REDACTED-CC]`, `[REDACTED-JWT]`).
21
+ *
22
+ * The redactor never mutates the input — every call returns a fresh
23
+ * object. The same payload commonly lands in two paths simultaneously
24
+ * (audit-log seals via vault; operational log redacts here) so
25
+ * in-place mutation would corrupt the sealed-then-archived copy.
26
+ *
27
+ * `classifyDefaults` and `installOutboundDlp` extend the same primitive
28
+ * set into outbound-DLP duty: the classifier produces a verdict
29
+ * ("clean" / "redact" / "refuse") for a request body + headers, and
30
+ * the installer wraps `httpClient` / `mail` / `webhook` instances so
31
+ * refused requests fail with `DlpError` and redacted ones proceed
32
+ * with sanitized payloads. Posture presets (`pci-dss` / `hipaa` /
33
+ * `fapi2` / `soc2` / `gdpr`) pick a sensible default classifier.
34
+ *
35
+ * @card
36
+ * Operational-log redaction — regex-shape and field-name rules that strip PII / secrets out of every log payload before it reaches a file, debug sink, or external SIEM.
24
37
  */
25
38
 
26
39
  var C = require("./constants");
40
+ var lazyRequire = require("./lazy-require");
41
+ var safeJson = require("./safe-json");
42
+ var validateOpts = require("./validate-opts");
43
+ var { DlpError } = require("./framework-error");
44
+
45
+ var audit = lazyRequire(function () { return require("./audit"); });
27
46
 
28
47
  var DEFAULT_MARKER = "[REDACTED]";
29
48
 
@@ -142,6 +161,23 @@ var VALUE_DETECTORS = [
142
161
  var sensitiveFieldsSet = new Set(SENSITIVE_FIELDS);
143
162
  var customDetectors = [];
144
163
 
164
+ /**
165
+ * @primitive b.redact.registerFieldRule
166
+ * @signature b.redact.registerFieldRule(name, replacement?)
167
+ * @since 0.1.0
168
+ * @related b.redact.redact, b.redact.registerValueDetector
169
+ *
170
+ * Add a field name to the always-redact set. Match is
171
+ * case-insensitive substring, so registering `secret` also redacts
172
+ * `appSecret` / `customer_secret`. The `replacement` argument is
173
+ * accepted for symmetry with `registerValueDetector` but ignored —
174
+ * field-name hits always use the redactor's configured marker.
175
+ *
176
+ * @example
177
+ * b.redact.registerFieldRule("internal_token");
178
+ * var out = b.redact.redact({ internal_token: "abc-123" });
179
+ * // → { internal_token: "[REDACTED]" }
180
+ */
145
181
  function registerFieldRule(name, replacement) {
146
182
  void replacement; // marker not used here; redact replaces with marker
147
183
  if (typeof name === "string") {
@@ -151,6 +187,28 @@ function registerFieldRule(name, replacement) {
151
187
  throw new Error("registerFieldRule expects a string field name");
152
188
  }
153
189
 
190
+ /**
191
+ * @primitive b.redact.registerValueDetector
192
+ * @signature b.redact.registerValueDetector(name, testFn, replacement)
193
+ * @since 0.1.0
194
+ * @related b.redact.redact, b.redact.registerFieldRule
195
+ *
196
+ * Register a custom value-shape detector. `testFn(value)` runs against
197
+ * every string value the redactor walks; truthy result substitutes the
198
+ * `replacement` (string or function — function receives the matched
199
+ * value and returns the substitution). Custom detectors run AFTER the
200
+ * built-in chain.
201
+ *
202
+ * @example
203
+ * // Redact internal employee IDs (shape: EMP-NNNNNN).
204
+ * b.redact.registerValueDetector("employee-id",
205
+ * function (v) { return /^EMP-\d{6}$/.test(v); },
206
+ * "[REDACTED-EMPID]");
207
+ * var out = b.redact.redact({ note: "owner EMP-123456" });
208
+ * // → { note: "owner EMP-123456" } — value-shape detectors only fire
209
+ * // on full-string match; in-string matches need a custom regex
210
+ * // replacement function.
211
+ */
154
212
  function registerValueDetector(name, testFn, replacement) {
155
213
  if (typeof testFn !== "function") {
156
214
  throw new Error("registerValueDetector requires a test function");
@@ -181,6 +239,34 @@ function _redactValue(value) {
181
239
  return value;
182
240
  }
183
241
 
242
+ /**
243
+ * @primitive b.redact.redact
244
+ * @signature b.redact.redact(value, opts?)
245
+ * @since 0.1.0
246
+ * @related b.redact.registerFieldRule, b.redact.registerValueDetector, b.redact.classifyDefaults
247
+ *
248
+ * Walk `value` and return a NEW value with sensitive fields and
249
+ * sensitive-shaped strings replaced by the marker. Handles plain
250
+ * objects, arrays, primitives, Buffers (always replaced — never log
251
+ * raw binary). The original input is never mutated.
252
+ *
253
+ * @opts
254
+ * marker: string, // replacement marker; default "[REDACTED]"
255
+ * maxDepth: number, // recursion cap; default 50
256
+ * parentKey: string | null, // seed parent-key for top-level scalars
257
+ *
258
+ * @example
259
+ * var safe = b.redact.redact({
260
+ * email: "alice@example.com",
261
+ * password: "hunter2",
262
+ * card: "4111 1111 1111 1111",
263
+ * note: "see eyJabcdefghijk.eyJxyz.signature for proof",
264
+ * });
265
+ * // → { email: "alice@example.com",
266
+ * // password: "[REDACTED]",
267
+ * // card: "[REDACTED-CC]",
268
+ * // note: "see eyJabcdefghijk.eyJxyz.signature for proof" }
269
+ */
184
270
  function redact(value, opts) {
185
271
  opts = opts || {};
186
272
  var marker = opts.marker || DEFAULT_MARKER;
@@ -225,11 +311,697 @@ function _resetForTest() {
225
311
  customDetectors = [];
226
312
  }
227
313
 
314
+ // ---- Classifier presets (for outbound DLP) ----
315
+ //
316
+ // Each pattern maps to a verdict-producing predicate. The default
317
+ // classifier walks the request body and headers, surfaces a verdict
318
+ // per-pattern, and returns either "clean", "redact" (if any pattern
319
+ // is sanitizable in-place by the redactor), or "refuse" (if any
320
+ // pattern is operator-flagged as refuse-only).
321
+ //
322
+ // Patterns:
323
+ // pan, credit-card — Luhn-validated card numbers
324
+ // ssn — US SSN shape
325
+ // ein — US EIN shape (NN-NNNNNNN)
326
+ // iban — IBAN shape + mod-97 checksum
327
+ // api-key-shape — generic high-entropy long token in known-key
328
+ // header / field names
329
+ // pem, ssh-private — private-key blocks
330
+ // jwt — JWS triplet
331
+ // aws-access-key — AWS access-key-id shape
332
+ // phi-shape — composite of US SSN + DOB-shape near a name field
333
+ // (used for HIPAA posture)
334
+ var CLASSIFIER_PATTERNS = Object.freeze({
335
+ "pan": {
336
+ detect: function (v) {
337
+ if (typeof v !== "string") return false;
338
+ // Two-stage match: full-field exact PAN OR embedded 13-19 digit
339
+ // run anywhere in a longer string. Both pass through Luhn before
340
+ // being flagged so high-digit-count IDs (timestamps, monotonic
341
+ // sequence numbers) don't false-positive.
342
+ var dExact = v.replace(/\s|-/g, "");
343
+ if (/^\d{13,19}$/.test(dExact) && _luhnCheck(dExact)) return true;
344
+ var m = v.match(/\b\d{4}[\s-]?\d{4}[\s-]?\d{4}[\s-]?\d{1,7}\b/);
345
+ if (m) {
346
+ var inner = m[0].replace(/\s|-/g, "");
347
+ if (inner.length >= 13 && inner.length <= 19 && _luhnCheck(inner)) return true;
348
+ }
349
+ return false;
350
+ },
351
+ action: "refuse",
352
+ label: "pan",
353
+ },
354
+ "credit-card": {
355
+ detect: function (v) {
356
+ if (typeof v !== "string") return false;
357
+ var dExact = v.replace(/\s|-/g, "");
358
+ if (/^\d{13,19}$/.test(dExact) && _luhnCheck(dExact)) return true;
359
+ var m = v.match(/\b\d{4}[\s-]?\d{4}[\s-]?\d{4}[\s-]?\d{1,7}\b/);
360
+ if (m) {
361
+ var inner = m[0].replace(/\s|-/g, "");
362
+ if (inner.length >= 13 && inner.length <= 19 && _luhnCheck(inner)) return true;
363
+ }
364
+ return false;
365
+ },
366
+ action: "refuse",
367
+ label: "credit-card",
368
+ },
369
+ "ssn": {
370
+ detect: function (v) { return typeof v === "string" && /\b\d{3}-\d{2}-\d{4}\b/.test(v); },
371
+ action: "redact",
372
+ label: "ssn",
373
+ },
374
+ "ein": {
375
+ detect: function (v) { return typeof v === "string" && /\b\d{2}-\d{7}\b/.test(v); },
376
+ action: "redact",
377
+ label: "ein",
378
+ },
379
+ "iban": {
380
+ detect: function (v) {
381
+ if (typeof v !== "string") return false;
382
+ var s = v.replace(/\s/g, "").toUpperCase();
383
+ if (!/^[A-Z]{2}\d{2}[A-Z0-9]{11,30}$/.test(s)) return false;
384
+ // mod-97 checksum
385
+ var rearranged = s.slice(4) + s.slice(0, 4);
386
+ var num = "";
387
+ for (var i = 0; i < rearranged.length; i += 1) {
388
+ var c = rearranged.charCodeAt(i);
389
+ if (c >= 48 && c <= 57) num += rearranged.charAt(i); // allow:raw-byte-literal — ASCII '0'..'9' codepoint range
390
+ else if (c >= 65 && c <= 90) num += String(c - 55);
391
+ else return false;
392
+ }
393
+ // Long-integer mod 97 in chunks
394
+ var rem = 0;
395
+ for (var j = 0; j < num.length; j += 7) {
396
+ rem = parseInt(String(rem) + num.slice(j, j + 7), 10) % 97;
397
+ }
398
+ return rem === 1;
399
+ },
400
+ action: "refuse",
401
+ label: "iban",
402
+ },
403
+ "api-key-shape": {
404
+ detect: function (v) {
405
+ if (typeof v !== "string") return false;
406
+ // High-entropy string with at least one digit + one uppercase + length >= 24.
407
+ if (v.length < 24) return false; // allow:raw-byte-literal — minimum entropy-bearing string length in chars, not bytes
408
+ if (!/[A-Z]/.test(v)) return false;
409
+ if (!/[0-9]/.test(v)) return false;
410
+ if (!/^[A-Za-z0-9_-]+$/.test(v)) return false;
411
+ return true;
412
+ },
413
+ action: "redact",
414
+ label: "api-key-shape",
415
+ },
416
+ "pem": {
417
+ detect: function (v) { return typeof v === "string" && /-----BEGIN [A-Z ]+-----/.test(v); },
418
+ action: "refuse",
419
+ label: "pem",
420
+ },
421
+ "ssh-private": {
422
+ detect: function (v) { return typeof v === "string" && /-----BEGIN OPENSSH PRIVATE KEY-----/.test(v); },
423
+ action: "refuse",
424
+ label: "ssh-private",
425
+ },
426
+ "jwt": {
427
+ detect: function (v) {
428
+ return typeof v === "string" &&
429
+ /^eyJ[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+$/.test(v);
430
+ },
431
+ action: "redact",
432
+ label: "jwt",
433
+ },
434
+ "aws-access-key": {
435
+ detect: function (v) {
436
+ return typeof v === "string" &&
437
+ /\b(AKIA|ASIA|AGPA|AIDA|AROA|AIPA|ANPA|ANVA|ASCA)[A-Z0-9]{16}\b/.test(v);
438
+ },
439
+ action: "refuse",
440
+ label: "aws-access-key",
441
+ },
442
+ "phi-shape": {
443
+ detect: function (v) {
444
+ if (typeof v !== "string") return false;
445
+ // SSN, DOB, MRN, ICD-10 shape — any one is enough to flag PHI
446
+ // adjacency in a body fragment. Operators using HIPAA posture
447
+ // get this composite by default.
448
+ if (/\b\d{3}-\d{2}-\d{4}\b/.test(v)) return true; // SSN
449
+ if (/\b(0[1-9]|1[0-2])\/(0[1-9]|[12]\d|3[01])\/(19|20)\d{2}\b/.test(v)) return true; // DOB
450
+ if (/\bMRN[:#]?\s*\d{4,12}\b/i.test(v)) return true; // MRN
451
+ if (/\b[A-TV-Z][0-9][0-9AB](\.[0-9A-TV-Z]{1,4})?\b/.test(v)) return true; // ICD-10
452
+ return false;
453
+ },
454
+ action: "refuse",
455
+ label: "phi-shape",
456
+ },
457
+ });
458
+
459
+ // classifyDefaults — build a classifier function from a list of pattern
460
+ // names. The returned classifier inspects body + headers and yields:
461
+ //
462
+ // { verdict: "clean" | "redact" | "refuse",
463
+ // hits: [ { label, action, where } ],
464
+ // redacted?: <body with matches replaced by marker> }
465
+ //
466
+ // "refuse" wins over "redact" wins over "clean". Operators choosing
467
+ // "redact" actions still get a redacted body so the request can proceed
468
+ // without leaking the matched value; "refuse" means the host primitive
469
+ // throws DlpError.
470
+ /**
471
+ * @primitive b.redact.classifyDefaults
472
+ * @signature b.redact.classifyDefaults(opts)
473
+ * @since 0.7.46
474
+ * @status stable
475
+ * @compliance hipaa, pci-dss, gdpr, soc2, fapi2
476
+ * @related b.redact.installOutboundDlp, b.redact.installForPosture
477
+ *
478
+ * Build a classifier function from a list of pattern names. The
479
+ * returned `classify({ body, headers, url })` walks the body (object,
480
+ * string, or Buffer), inspects every header value, and returns
481
+ * `{ verdict, hits, redactedBody }`. Verdict precedence is
482
+ * `refuse > redact > audit-only > clean`.
483
+ *
484
+ * @opts
485
+ * patterns: string[], // names from CLASSIFIER_PATTERNS
486
+ * extra: object, // additional { name: { detect, action, label } }
487
+ * overrideAction: "refuse" | "redact" | "audit-only",
488
+ * marker: string, // default "[REDACTED]"
489
+ *
490
+ * @example
491
+ * var classify = b.redact.classifyDefaults({
492
+ * patterns: ["pan", "ssn", "jwt", "aws-access-key"],
493
+ * });
494
+ * var v = classify({
495
+ * body: { card: "4111111111111111", note: "ok" },
496
+ * headers: { authorization: "Bearer eyJabc.eyJdef.sig" },
497
+ * });
498
+ * // → v.verdict === "refuse" (PAN match defaults to refuse)
499
+ */
500
+ function classifyDefaults(opts) {
501
+ opts = opts || {};
502
+ validateOpts(opts, ["patterns", "extra", "overrideAction", "marker"], "redact.classifyDefaults");
503
+ var patterns = Array.isArray(opts.patterns) ? opts.patterns : Object.keys(CLASSIFIER_PATTERNS);
504
+ if (patterns.length === 0) {
505
+ throw new DlpError("redact-dlp/no-patterns",
506
+ "redact.classifyDefaults: opts.patterns must be a non-empty array");
507
+ }
508
+ for (var p = 0; p < patterns.length; p += 1) {
509
+ if (typeof patterns[p] !== "string") {
510
+ throw new DlpError("redact-dlp/bad-pattern",
511
+ "redact.classifyDefaults: patterns[" + p + "] must be a string, got " +
512
+ typeof patterns[p]);
513
+ }
514
+ if (!CLASSIFIER_PATTERNS[patterns[p]] &&
515
+ !(opts.extra && opts.extra[patterns[p]])) {
516
+ throw new DlpError("redact-dlp/unknown-pattern",
517
+ "redact.classifyDefaults: unknown pattern '" + patterns[p] +
518
+ "'. Known: " + Object.keys(CLASSIFIER_PATTERNS).join(", "));
519
+ }
520
+ }
521
+ var marker = typeof opts.marker === "string" && opts.marker.length > 0
522
+ ? opts.marker : DEFAULT_MARKER;
523
+ var overrideAction = opts.overrideAction || null;
524
+ if (overrideAction && overrideAction !== "refuse" && overrideAction !== "redact" && overrideAction !== "audit-only") {
525
+ throw new DlpError("redact-dlp/bad-action",
526
+ "redact.classifyDefaults: overrideAction must be refuse|redact|audit-only");
527
+ }
528
+ var extra = opts.extra || {};
529
+
530
+ function _resolve(name) {
531
+ var spec = CLASSIFIER_PATTERNS[name] || extra[name];
532
+ return spec;
533
+ }
534
+
535
+ return function classify(input) {
536
+ var hits = [];
537
+ var bodyAccumulator = [];
538
+
539
+ function _scanString(str, where) {
540
+ if (typeof str !== "string" || str.length === 0) return str;
541
+ var out = str;
542
+ for (var i = 0; i < patterns.length; i += 1) {
543
+ var spec = _resolve(patterns[i]);
544
+ if (!spec) continue;
545
+ if (spec.detect(out)) {
546
+ var action = overrideAction || spec.action;
547
+ hits.push({ label: spec.label || patterns[i], action: action, where: where });
548
+ if (action === "redact") {
549
+ // Best-effort scrub of the matched fragment. Field-name
550
+ // redaction inside the body is handled by walking the
551
+ // structure separately.
552
+ out = out.replace(/\b\d{3}-\d{2}-\d{4}\b/g, marker)
553
+ .replace(/\b\d{2}-\d{7}\b/g, marker);
554
+ // For other shapes, replace the full string when matched.
555
+ if (spec.label !== "ssn" && spec.label !== "ein") out = marker;
556
+ }
557
+ }
558
+ }
559
+ return out;
560
+ }
561
+
562
+ function _walk(value, where) {
563
+ if (value === null || value === undefined) return value;
564
+ if (typeof value === "string") {
565
+ var scanned = _scanString(value, where);
566
+ bodyAccumulator.push(scanned);
567
+ return scanned;
568
+ }
569
+ if (typeof value === "number" || typeof value === "boolean") return value;
570
+ if (Buffer.isBuffer(value) || value instanceof Uint8Array) {
571
+ return value; // raw bytes — scanned separately when input.body
572
+ }
573
+ if (Array.isArray(value)) {
574
+ return value.map(function (item, idx) {
575
+ return _walk(item, where + "[" + idx + "]");
576
+ });
577
+ }
578
+ if (typeof value === "object") {
579
+ var copy = {};
580
+ for (var k in value) {
581
+ if (!Object.prototype.hasOwnProperty.call(value, k)) continue;
582
+ copy[k] = _walk(value[k], where + "." + k);
583
+ }
584
+ return copy;
585
+ }
586
+ return value;
587
+ }
588
+
589
+ var redactedBody;
590
+ var input2 = input || {};
591
+ var bodyVal = input2.body;
592
+ if (Buffer.isBuffer(bodyVal) || bodyVal instanceof Uint8Array) {
593
+ var asText;
594
+ try { asText = Buffer.from(bodyVal).toString("utf8"); }
595
+ catch (_e) { asText = ""; }
596
+ var scannedText = _scanString(asText, "body");
597
+ redactedBody = scannedText === asText ? bodyVal : Buffer.from(scannedText, "utf8");
598
+ } else if (typeof bodyVal === "string") {
599
+ redactedBody = _scanString(bodyVal, "body");
600
+ } else if (bodyVal && typeof bodyVal === "object") {
601
+ redactedBody = _walk(bodyVal, "body");
602
+ } else {
603
+ redactedBody = bodyVal;
604
+ }
605
+
606
+ if (input2.headers && typeof input2.headers === "object") {
607
+ for (var hk in input2.headers) {
608
+ if (!Object.prototype.hasOwnProperty.call(input2.headers, hk)) continue;
609
+ _scanString(String(input2.headers[hk]), "headers." + hk);
610
+ }
611
+ }
612
+
613
+ // Verdict precedence: refuse > redact > audit-only > clean.
614
+ var verdict = "clean";
615
+ for (var hi = 0; hi < hits.length; hi += 1) {
616
+ if (hits[hi].action === "refuse") { verdict = "refuse"; break; }
617
+ if (hits[hi].action === "redact") verdict = "redact";
618
+ else if (hits[hi].action === "audit-only" && verdict === "clean") verdict = "audit-only";
619
+ }
620
+ return { verdict: verdict, hits: hits, redactedBody: redactedBody };
621
+ };
622
+ }
623
+
624
+ // ---- Outbound DLP installer ----
625
+
626
+ var OUTBOUND_INSTALL_REGISTRY = new WeakMap();
627
+
628
+ function _emitDlp(action, outcome, metadata) {
629
+ try {
630
+ audit().safeEmit({
631
+ action: action,
632
+ outcome: outcome,
633
+ metadata: metadata || {},
634
+ });
635
+ } catch (_e) { /* drop-silent */ }
636
+ }
637
+
638
+ function _wrapClassifier(fn, where) {
639
+ if (typeof fn !== "function") {
640
+ throw new DlpError("redact-dlp/bad-classifier",
641
+ where + ": classifier must be a function");
642
+ }
643
+ return function safeClassify(input) {
644
+ var v;
645
+ try { v = fn(input || {}); }
646
+ catch (e) {
647
+ // Classifier threw — treat as refuse (fail-closed, since the
648
+ // classifier is the gate; an unknown verdict cannot be
649
+ // sanitized as "clean").
650
+ return { verdict: "refuse", hits: [{ label: "classifier-error", action: "refuse", where: "classifier" }],
651
+ redactedBody: input && input.body, error: e && e.message };
652
+ }
653
+ if (!v || typeof v !== "object" || typeof v.verdict !== "string") {
654
+ return { verdict: "refuse", hits: [{ label: "classifier-bad-verdict", action: "refuse", where: "classifier" }],
655
+ redactedBody: input && input.body };
656
+ }
657
+ return v;
658
+ };
659
+ }
660
+
661
+ // installOutboundDlp({ httpClient, mail, webhook, classifier?, posture?,
662
+ // onRefuse?, onRedact?, onScan? })
663
+ //
664
+ // Installs interceptors on each of the operator-supplied primitive
665
+ // instances. The installer is idempotent per primitive — a second
666
+ // install with the same instance no-ops on that instance. Each
667
+ // interceptor wraps the request-emit boundary; the original instance
668
+ // keeps its surface unchanged and any callers see DlpError on refuse
669
+ // or a sanitized payload on redact.
670
+ //
671
+ // Returns { uninstall(), installed: { httpClient, mail, webhook } }.
672
+ /**
673
+ * @primitive b.redact.installOutboundDlp
674
+ * @signature b.redact.installOutboundDlp(opts)
675
+ * @since 0.7.46
676
+ * @status stable
677
+ * @compliance hipaa, pci-dss, gdpr, soc2, fapi2
678
+ * @related b.redact.classifyDefaults, b.redact.installForPosture
679
+ *
680
+ * Install request-time interceptors on `httpClient` / `mail` /
681
+ * `webhook` instances so every outbound payload runs through a DLP
682
+ * classifier first. Refused requests reject with `DlpError`; redacted
683
+ * requests proceed with a sanitized body. Idempotent per primitive
684
+ * instance — installing twice on the same client no-ops.
685
+ *
686
+ * @opts
687
+ * httpClient: object, // instance with .request(opts)
688
+ * mail: object, // instance with .send(message)
689
+ * webhook: object, // signer instance with .send(input)
690
+ * classifier: function, // override the default classifier
691
+ * posture: string, // "pci-dss" | "hipaa" | "fapi2" | "soc2" | "gdpr"
692
+ * onRefuse: function, // hook fired on refuse verdict
693
+ * onRedact: function, // hook fired on redact verdict
694
+ * onScan: function, // hook fired on every classify call
695
+ *
696
+ * @example
697
+ * var http = b.httpClient.create({ baseUrl: "https://api.example.com" });
698
+ * var mail = b.mail.create({ host: "smtp.example.com", port: 587 });
699
+ * var dlp = b.redact.installOutboundDlp({
700
+ * httpClient: http,
701
+ * mail: mail,
702
+ * posture: "pci-dss",
703
+ * onRefuse: function (info) { console.warn("DLP refused", info.verdict.hits); },
704
+ * });
705
+ * // dlp.installed → { httpClient: true, mail: true, webhook: false }
706
+ * // dlp.uninstall() restores the original .request / .send methods.
707
+ */
708
+ function installOutboundDlp(opts) {
709
+ opts = opts || {};
710
+ validateOpts(opts, [
711
+ "httpClient", "mail", "webhook", "classifier", "posture",
712
+ "onRefuse", "onRedact", "onScan",
713
+ ], "redact.installOutboundDlp");
714
+
715
+ // Posture default-on. When posture is given but no classifier, we
716
+ // build a posture-derived default. PCI-DSS → pan + credit-card +
717
+ // pem + aws-access-key. HIPAA → phi-shape + ssn + ein + pem.
718
+ var classifier = opts.classifier;
719
+ var posturePatterns = null;
720
+ if (typeof opts.posture === "string" && opts.posture.length > 0) {
721
+ posturePatterns = _resolvePosturePatterns(opts.posture);
722
+ }
723
+ if (!classifier) {
724
+ classifier = classifyDefaults({
725
+ patterns: posturePatterns ||
726
+ ["pan", "ssn", "ein", "iban", "credit-card", "api-key-shape", "pem", "ssh-private", "aws-access-key"],
727
+ });
728
+ }
729
+ classifier = _wrapClassifier(classifier, "redact.installOutboundDlp");
730
+
731
+ validateOpts.optionalFunction(opts.onRefuse, "redact.installOutboundDlp: onRefuse",
732
+ DlpError, "redact-dlp/bad-hook");
733
+ validateOpts.optionalFunction(opts.onRedact, "redact.installOutboundDlp: onRedact",
734
+ DlpError, "redact-dlp/bad-hook");
735
+ validateOpts.optionalFunction(opts.onScan, "redact.installOutboundDlp: onScan",
736
+ DlpError, "redact-dlp/bad-hook");
737
+
738
+ var uninstallers = [];
739
+ var installed = { httpClient: false, mail: false, webhook: false };
740
+
741
+ if (opts.httpClient) {
742
+ var u1 = _installHttpClient(opts.httpClient, classifier, opts);
743
+ if (u1) { uninstallers.push(u1); installed.httpClient = true; }
744
+ }
745
+ if (opts.mail) {
746
+ var u2 = _installMail(opts.mail, classifier, opts);
747
+ if (u2) { uninstallers.push(u2); installed.mail = true; }
748
+ }
749
+ if (opts.webhook) {
750
+ var u3 = _installWebhook(opts.webhook, classifier, opts);
751
+ if (u3) { uninstallers.push(u3); installed.webhook = true; }
752
+ }
753
+
754
+ _emitDlp("dlp.outbound.installed", "success", {
755
+ posture: opts.posture || null,
756
+ primitives: Object.keys(installed).filter(function (k) { return installed[k]; }),
757
+ });
758
+
759
+ return {
760
+ installed: installed,
761
+ uninstall: function () {
762
+ while (uninstallers.length > 0) {
763
+ var fn = uninstallers.pop();
764
+ try { fn(); } catch (_e) { /* best-effort */ }
765
+ }
766
+ },
767
+ };
768
+ }
769
+
770
+ function _resolvePosturePatterns(name) {
771
+ var n = String(name).toLowerCase();
772
+ if (n === "pci-dss" || n === "pci") {
773
+ return ["pan", "credit-card", "pem", "aws-access-key", "api-key-shape"];
774
+ }
775
+ if (n === "hipaa") {
776
+ return ["phi-shape", "ssn", "ein", "pem", "aws-access-key", "api-key-shape"];
777
+ }
778
+ if (n === "fapi2") {
779
+ return ["pan", "credit-card", "iban", "pem", "aws-access-key", "jwt", "api-key-shape"];
780
+ }
781
+ if (n === "soc2" || n === "gdpr") {
782
+ return ["ssn", "ein", "pem", "ssh-private", "aws-access-key", "api-key-shape"];
783
+ }
784
+ throw new DlpError("redact-dlp/unknown-posture",
785
+ "redact.installOutboundDlp: unknown posture '" + name +
786
+ "'. Known: pci-dss | hipaa | fapi2 | soc2 | gdpr");
787
+ }
788
+
789
+ function _runHook(hook, payload) {
790
+ if (typeof hook !== "function") return;
791
+ try { hook(payload); } catch (_e) { /* drop-silent */ }
792
+ }
793
+
794
+ function _installHttpClient(client, classifier, opts) {
795
+ if (OUTBOUND_INSTALL_REGISTRY.has(client)) return null;
796
+ if (typeof client.request !== "function") {
797
+ throw new DlpError("redact-dlp/bad-target",
798
+ "redact.installOutboundDlp: httpClient must expose a request() function");
799
+ }
800
+ var original = client.request.bind(client);
801
+ client.request = function dlpScannedRequest(reqOpts) {
802
+ reqOpts = reqOpts || {};
803
+ var verdict = classifier({ body: reqOpts.body, headers: reqOpts.headers, url: reqOpts.url });
804
+ _runHook(opts.onScan, { primitive: "httpClient", verdict: verdict, opts: reqOpts });
805
+ if (verdict.verdict === "refuse") {
806
+ _emitDlp("dlp.outbound.refused", "denied", {
807
+ primitive: "httpClient",
808
+ url: reqOpts.url || null,
809
+ hits: verdict.hits.map(_summarizeHit),
810
+ });
811
+ _runHook(opts.onRefuse, { primitive: "httpClient", verdict: verdict, opts: reqOpts });
812
+ return Promise.reject(new DlpError("redact-dlp/refused",
813
+ "outbound httpClient.request refused by DLP classifier — hits: " +
814
+ verdict.hits.map(function (h) { return h.label; }).join(", ")));
815
+ }
816
+ if (verdict.verdict === "redact") {
817
+ // Mutate the body field on a defensive shallow clone built via
818
+ // explicit field copy, not Object.assign, so operator-shaped opts
819
+ // can't smuggle keys past the existing httpClient.request opts
820
+ // validator.
821
+ var newOpts = {};
822
+ for (var rk in reqOpts) {
823
+ if (Object.prototype.hasOwnProperty.call(reqOpts, rk)) newOpts[rk] = reqOpts[rk];
824
+ }
825
+ newOpts.body = verdict.redactedBody;
826
+ _emitDlp("dlp.outbound.redacted", "success", {
827
+ primitive: "httpClient",
828
+ url: reqOpts.url || null,
829
+ hits: verdict.hits.map(_summarizeHit),
830
+ });
831
+ _runHook(opts.onRedact, { primitive: "httpClient", verdict: verdict, opts: newOpts });
832
+ return original(newOpts);
833
+ }
834
+ return original(reqOpts);
835
+ };
836
+ OUTBOUND_INSTALL_REGISTRY.set(client, true);
837
+ return function uninstall() {
838
+ client.request = original;
839
+ OUTBOUND_INSTALL_REGISTRY.delete(client);
840
+ };
841
+ }
842
+
843
+ function _installMail(mailInstance, classifier, opts) {
844
+ if (OUTBOUND_INSTALL_REGISTRY.has(mailInstance)) return null;
845
+ if (typeof mailInstance.send !== "function") {
846
+ throw new DlpError("redact-dlp/bad-target",
847
+ "redact.installOutboundDlp: mail must expose a send() function");
848
+ }
849
+ var original = mailInstance.send.bind(mailInstance);
850
+ mailInstance.send = function dlpScannedSend(message) {
851
+ message = message || {};
852
+ var bodyParts = {
853
+ text: message.text,
854
+ html: message.html,
855
+ subject: message.subject,
856
+ };
857
+ var verdict = classifier({ body: bodyParts, headers: message.headers || {} });
858
+ _runHook(opts.onScan, { primitive: "mail", verdict: verdict, message: message });
859
+ if (verdict.verdict === "refuse") {
860
+ _emitDlp("dlp.outbound.refused", "denied", {
861
+ primitive: "mail",
862
+ to: message.to || null,
863
+ hits: verdict.hits.map(_summarizeHit),
864
+ });
865
+ _runHook(opts.onRefuse, { primitive: "mail", verdict: verdict, message: message });
866
+ return Promise.reject(new DlpError("redact-dlp/refused",
867
+ "outbound mail.send refused by DLP classifier — hits: " +
868
+ verdict.hits.map(function (h) { return h.label; }).join(", ")));
869
+ }
870
+ if (verdict.verdict === "redact") {
871
+ var newMessage = {};
872
+ for (var mk in message) {
873
+ if (Object.prototype.hasOwnProperty.call(message, mk)) newMessage[mk] = message[mk];
874
+ }
875
+ newMessage.text = verdict.redactedBody && verdict.redactedBody.text;
876
+ newMessage.html = verdict.redactedBody && verdict.redactedBody.html;
877
+ newMessage.subject = verdict.redactedBody && verdict.redactedBody.subject;
878
+ _emitDlp("dlp.outbound.redacted", "success", {
879
+ primitive: "mail",
880
+ to: message.to || null,
881
+ hits: verdict.hits.map(_summarizeHit),
882
+ });
883
+ _runHook(opts.onRedact, { primitive: "mail", verdict: verdict, message: newMessage });
884
+ return original(newMessage);
885
+ }
886
+ return original(message);
887
+ };
888
+ OUTBOUND_INSTALL_REGISTRY.set(mailInstance, true);
889
+ return function uninstall() {
890
+ mailInstance.send = original;
891
+ OUTBOUND_INSTALL_REGISTRY.delete(mailInstance);
892
+ };
893
+ }
894
+
895
+ function _installWebhook(signerInstance, classifier, opts) {
896
+ if (OUTBOUND_INSTALL_REGISTRY.has(signerInstance)) return null;
897
+ if (typeof signerInstance.send !== "function") {
898
+ throw new DlpError("redact-dlp/bad-target",
899
+ "redact.installOutboundDlp: webhook must expose a send() function (signer instance)");
900
+ }
901
+ var original = signerInstance.send.bind(signerInstance);
902
+ signerInstance.send = function dlpScannedWebhookSend(input) {
903
+ input = input || {};
904
+ var bodyForScan = input.body;
905
+ // body may be a JSON string — try parsing for a richer scan.
906
+ var parsedBody = null;
907
+ if (typeof bodyForScan === "string") {
908
+ try { parsedBody = safeJson.parse(bodyForScan); }
909
+ catch (_e) { parsedBody = null; }
910
+ }
911
+ var verdict = classifier({
912
+ body: parsedBody !== null ? parsedBody : bodyForScan,
913
+ headers: input.headers || {},
914
+ url: input.url,
915
+ });
916
+ _runHook(opts.onScan, { primitive: "webhook", verdict: verdict, input: input });
917
+ if (verdict.verdict === "refuse") {
918
+ _emitDlp("dlp.outbound.refused", "denied", {
919
+ primitive: "webhook",
920
+ url: input.url || null,
921
+ hits: verdict.hits.map(_summarizeHit),
922
+ });
923
+ _runHook(opts.onRefuse, { primitive: "webhook", verdict: verdict, input: input });
924
+ return Promise.reject(new DlpError("redact-dlp/refused",
925
+ "outbound webhook.send refused by DLP classifier — hits: " +
926
+ verdict.hits.map(function (h) { return h.label; }).join(", ")));
927
+ }
928
+ if (verdict.verdict === "redact") {
929
+ var newBody = parsedBody !== null
930
+ ? JSON.stringify(verdict.redactedBody)
931
+ : verdict.redactedBody;
932
+ // Build the rebuilt input from a fixed allowlist of fields rather
933
+ // than a spread, so an operator-shaped input object cannot smuggle
934
+ // unexpected keys into the downstream signer-send call.
935
+ var newInput = {
936
+ url: input.url,
937
+ body: newBody,
938
+ kid: input.kid,
939
+ headers: input.headers,
940
+ };
941
+ _emitDlp("dlp.outbound.redacted", "success", {
942
+ primitive: "webhook",
943
+ url: input.url || null,
944
+ hits: verdict.hits.map(_summarizeHit),
945
+ });
946
+ _runHook(opts.onRedact, { primitive: "webhook", verdict: verdict, input: newInput });
947
+ return original(newInput);
948
+ }
949
+ return original(input);
950
+ };
951
+ OUTBOUND_INSTALL_REGISTRY.set(signerInstance, true);
952
+ return function uninstall() {
953
+ signerInstance.send = original;
954
+ OUTBOUND_INSTALL_REGISTRY.delete(signerInstance);
955
+ };
956
+ }
957
+
958
+ function _summarizeHit(h) {
959
+ return { label: h.label, action: h.action, where: h.where };
960
+ }
961
+
962
+ // Posture-coordinated install — a thin wrapper used by b.compliance.set
963
+ // to wire DLP automatically when the posture is set. Operators using
964
+ // b.compliance can rely on this; direct callers use installOutboundDlp.
965
+ /**
966
+ * @primitive b.redact.installForPosture
967
+ * @signature b.redact.installForPosture(posture, primitives)
968
+ * @since 0.7.46
969
+ * @status stable
970
+ * @compliance hipaa, pci-dss, gdpr, soc2, fapi2
971
+ * @related b.redact.installOutboundDlp, b.redact.classifyDefaults
972
+ *
973
+ * Posture-coordinated install — a thin wrapper used by
974
+ * `b.compliance.set` so picking a posture also wires outbound DLP
975
+ * automatically. Direct callers usually want `installOutboundDlp`
976
+ * because it accepts the full hook surface.
977
+ *
978
+ * @example
979
+ * var dlp = b.redact.installForPosture("hipaa", {
980
+ * httpClient: myHttp,
981
+ * mail: myMail,
982
+ * webhook: myWebhook,
983
+ * });
984
+ * // → dlp.installed.httpClient === true
985
+ */
986
+ function installForPosture(posture, primitives) {
987
+ return installOutboundDlp({
988
+ httpClient: primitives && primitives.httpClient,
989
+ mail: primitives && primitives.mail,
990
+ webhook: primitives && primitives.webhook,
991
+ posture: posture,
992
+ });
993
+ }
994
+
228
995
  module.exports = {
229
996
  redact: redact,
230
997
  registerFieldRule: registerFieldRule,
231
998
  registerValueDetector: registerValueDetector,
999
+ classifyDefaults: classifyDefaults,
1000
+ installOutboundDlp: installOutboundDlp,
1001
+ installForPosture: installForPosture,
1002
+ CLASSIFIER_PATTERNS: CLASSIFIER_PATTERNS,
232
1003
  MARKER: DEFAULT_MARKER,
233
1004
  SENSITIVE_FIELDS: SENSITIVE_FIELDS,
1005
+ DlpError: DlpError,
234
1006
  _resetForTest: _resetForTest,
235
1007
  };