@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
@@ -1,55 +1,56 @@
1
1
  "use strict";
2
2
  /**
3
- * guard-domain — Domain-name identifier-safety primitive (b.guardDomain).
3
+ * @module b.guardDomain
4
+ * @nav Guards
5
+ * @title Guard Domain
4
6
  *
5
- * Validates user-supplied DNS names destined for allowlists, redirect
6
- * targets, webhook endpoints, email-domain extraction, and CORS origin
7
- * checks. KIND="identifier" consumes ctx.identifier (or ctx.domain).
7
+ * @intro
8
+ * Domain-name identifier-safety primitive (KIND="identifier").
9
+ * Validates user-supplied DNS names destined for allowlists,
10
+ * redirect targets, webhook endpoints, email-domain extraction,
11
+ * and CORS origin checks. Consumes `ctx.identifier` (or
12
+ * `ctx.domain`).
8
13
  *
9
- * Threat catalog grounded in current research:
10
- * - RFC 1035 §2.3.4 length caps 63 octets / label, 253 octets / FQDN.
11
- * - RFC 952 / 1123 LDH rule letters / digits / hyphens; no leading/
12
- * trailing hyphen; no `--` at positions 3-4 except `xn--` prefix.
13
- * - IDN homograph / mixed-script confusables (RFC 5891-5894 IDNA2008,
14
- * UTS #39). Cyrillic / Greek / Cherokee characters mixed with Latin
15
- * in a single label spoof a trusted domain.
16
- * - BIDI / RTL override (CVE-2021-42574 Trojan Source) in label
17
- * codepoints — reorders visual presentation of the domain in
18
- * address bars / log lines / audit records.
19
- * - Zero-width / format codepoints — split a visible label into two
20
- * parsed labels or hide characters from the operator.
21
- * - Punycode `xn--` malformation — bare prefix that fails decode,
22
- * double-encoded `xn--xn--`, U-label/A-label round-trip mismatch.
23
- * - Special-use domain names (RFC 6761) — `.localhost`, `.local`,
24
- * `.invalid`, `.example`, `.test`, `.onion`, `.alt`, `.home.arpa`,
25
- * `.internal`. Allowlisting these as user-supplied webhook targets
26
- * routes traffic to loopback or LAN.
27
- * - IPv4-as-domain confusion (RFC 3986 §3.2.2 + CVE-2021-22931) —
28
- * dotted-decimal, octal, hex, and long-decimal IPv4 forms accepted
29
- * by `inet_aton`-style parsers but compared as strings by allowlist
30
- * matchers.
31
- * - IPv6 bracket-literal — same risk class.
32
- * - TLD-only / single-label — resolves via search-domain suffix to
33
- * attacker-chosen FQDN on misconfigured stubs.
34
- * - Wildcard `*.example.com` — valid in TLS SAN and DNS but never in
35
- * a user-input identifier.
36
- * - Underscore labels — valid only for service-discovery prefixes
37
- * per RFC 8552, never as a hostname.
38
- * - Trailing dot — FQDN distinguisher; some libraries strip,
39
- * some compare; allowlist mismatch.
40
- * - DGA heuristic — high-entropy single-label domains
41
- * (Mirai/Conficker family C2 indicator).
14
+ * IDN homograph defense: mixed-script confusables (RFC 5891-5894
15
+ * IDNA2008, UTS #39)Cyrillic / Greek / Cherokee letters mixed
16
+ * with Latin in a single label spoof trusted domains. Strict
17
+ * refuses; balanced/permissive audit. The script-allowlist is
18
+ * operator-tunable via `opts.allowedScripts`. Punycode A-labels
19
+ * (`xn--`) audit by default at balanced; bare `xn--` always
20
+ * refuses.
42
21
  *
43
- * var rv = b.guardDomain.validate("example.com", { profile: "strict" });
44
- * var safe = b.guardDomain.sanitize("Example.Com.", { profile: "balanced" });
45
- * var g = b.guardDomain.gate({ profile: "strict" });
22
+ * Label-length caps per RFC 1035 §2.3.4: 63 octets per label, 253
23
+ * octets per FQDN. UTF-8 byte counting (not codepoint count) the
24
+ * wire-form bound is what DNS resolvers enforce. RFC 952 / 1123
25
+ * LDH grammar enforced for ASCII labels; double-hyphen at positions
26
+ * 3-4 without `xn--` prefix audits.
27
+ *
28
+ * TLD allowlist + public-suffix awareness: RFC 6761 special-use
29
+ * suffixes (`.localhost` / `.local` / `.invalid` / `.test` /
30
+ * `.onion` / `.alt` / `.home.arpa` / `.internal`) refuse under
31
+ * strict — letting these through as user-input webhook targets
32
+ * routes traffic to loopback / mDNS / Tor / LAN. IPv4-as-domain
33
+ * (dotted-decimal, octal, hex, long-decimal) and IPv6 bracket
34
+ * literals refuse (CVE-2021-22931 DNS-rebinding class).
35
+ * Single-label / TLD-only refuses under strict (search-domain
36
+ * suffix on misconfigured stubs).
37
+ *
38
+ * Public-suffix and full UTS #46 ToASCII / ToUnicode round-trip
39
+ * ship behind operator-supplied callbacks (`opts.publicSuffixList`,
40
+ * `opts.idnToAscii`) — defer-with-condition until an operator
41
+ * surfaces a cookie-scope or email-domain canonicalization use case
42
+ * that needs framework-vendored tables.
46
43
  *
47
- * Defer-with-condition: full UTS #46 ToASCII / ToUnicode round-trip and
48
- * Public-Suffix-List boundary enforcement ship behind operator-supplied
49
- * callbacks (`opts.idnToAscii`, `opts.publicSuffixList`). Re-open
50
- * conditions: an operator surfaces a use case for cookie-scope or
51
- * email-domain canonicalization that needs framework-vendored PSL /
52
- * UTS #46 tables.
44
+ * BIDI / control / null-byte / zero-width are universal-refuse at
45
+ * every profile (CVE-2021-42574 Trojan Source class). DGA heuristic
46
+ * (Shannon entropy >= 3.8 bits/char on labels >= 12 chars) audits
47
+ * under balanced, refuses under strict.
48
+ *
49
+ * Profiles: `strict` / `balanced` / `permissive`. Compliance
50
+ * postures: `hipaa` / `pci-dss` / `gdpr` / `soc2`.
51
+ *
52
+ * @card
53
+ * Domain-name identifier-safety primitive (KIND="identifier").
53
54
  */
54
55
 
55
56
  var codepointClass = require("./codepoint-class");
@@ -121,51 +122,10 @@ function _looksLikeIpv4Permissive(s) {
121
122
  // IPv6 bracket-literal.
122
123
  var IPV6_BRACKET_RE = /^\[[0-9a-fA-F:.]+\]$/;
123
124
 
124
- // IDN script-range tables for mixed-script confusable detection. Same
125
- // pattern guard-email uses; codepoints are numeric, never literal in
126
- // source.
127
- var SCRIPT_RANGES = {
128
- latin: [[0x0041, 0x005A], [0x0061, 0x007A],
129
- [0x00C0, 0x024F], [0x1E00, 0x1EFF]], // allow:raw-byte-literal — Unicode script ranges
130
- cyrillic: [[0x0400, 0x04FF], [0x0500, 0x052F]], // allow:raw-byte-literal — Unicode Cyrillic + Cyrillic Supplement
131
- greek: [[0x0370, 0x03FF], [0x1F00, 0x1FFF]], // allow:raw-byte-literal — Unicode Greek + Greek Extended
132
- armenian: [[0x0530, 0x058F]], // allow:raw-byte-literal — Unicode Armenian
133
- cherokee: [[0x13A0, 0x13FF], [0xAB70, 0xABBF]], // allow:raw-byte-literal — Unicode Cherokee + Cherokee Supplement
134
- han: [[0x4E00, 0x9FFF]], // allow:raw-byte-literal — CJK Unified Ideographs
135
- hiragana: [[0x3040, 0x309F]], // allow:raw-byte-literal — Hiragana
136
- katakana: [[0x30A0, 0x30FF]], // allow:raw-byte-literal — Katakana
137
- hangul: [[0xAC00, 0xD7AF]], // allow:raw-byte-literal — Hangul Syllables
138
- arabic: [[0x0600, 0x06FF]], // allow:raw-byte-literal — Arabic
139
- hebrew: [[0x0590, 0x05FF]], // allow:raw-byte-literal — Hebrew
140
- };
141
-
142
- function _scriptFor(cp) {
143
- var keys = Object.keys(SCRIPT_RANGES);
144
- for (var i = 0; i < keys.length; i += 1) {
145
- var ranges = SCRIPT_RANGES[keys[i]];
146
- for (var j = 0; j < ranges.length; j += 1) {
147
- if (cp >= ranges[j][0] && cp <= ranges[j][1]) return keys[i];
148
- }
149
- }
150
- return null;
151
- }
152
-
153
- function _detectMixedScripts(label, allowedScripts) {
154
- var seen = {};
155
- for (var i = 0; i < label.length; i += 1) {
156
- var script = _scriptFor(label.charCodeAt(i));
157
- if (script === null) continue;
158
- seen[script] = true;
159
- }
160
- var scripts = Object.keys(seen);
161
- if (scripts.length <= 1) return null;
162
- if (!allowedScripts) return scripts;
163
- var disallowed = [];
164
- for (var k = 0; k < scripts.length; k += 1) {
165
- if (allowedScripts.indexOf(scripts[k]) === -1) disallowed.push(scripts[k]);
166
- }
167
- return disallowed.length > 0 ? scripts : null;
168
- }
125
+ // IDN script-range tables for mixed-script confusable detection live
126
+ // in codepoint-class every guard-* family member + safe-url shares
127
+ // the same catalog so adding a script is a single edit.
128
+ var _detectMixedScripts = codepointClass.detectMixedScripts;
169
129
 
170
130
  // RFC 6761 special-use domains + IETF reserved. Lowercase, no trailing
171
131
  // dot. Match by suffix — `_acme-challenge.app.localhost` → `.localhost`.
@@ -595,6 +555,52 @@ function _detectIssues(input, opts) {
595
555
  return issues;
596
556
  }
597
557
 
558
+ /**
559
+ * @primitive b.guardDomain.validate
560
+ * @signature b.guardDomain.validate(input, opts?)
561
+ * @since 0.7.41
562
+ * @status stable
563
+ * @compliance hipaa, pci-dss, gdpr, soc2
564
+ * @related b.guardDomain.sanitize, b.guardDomain.gate
565
+ *
566
+ * Inspect a domain-name string and return `{ ok, issues, summary }`.
567
+ * Each issue carries `{ kind, severity, ruleId, snippet }` with
568
+ * severity in `"warn"|"high"|"critical"`. Detected: domain/label
569
+ * length cap (RFC 1035 §2.3.4), LDH violation, IDN A-label
570
+ * malformation, mixed-script homograph, special-use suffix (RFC
571
+ * 6761), IPv4-as-domain (every parser-permissive form), IPv6
572
+ * bracket-literal, single-label / TLD-only, wildcard label,
573
+ * underscore label, trailing dot, DGA-shape entropy, BIDI / control
574
+ * / null-byte / zero-width codepoints. Pure inspection.
575
+ *
576
+ * @opts
577
+ * profile: "strict"|"balanced"|"permissive",
578
+ * compliance: "hipaa"|"pci-dss"|"gdpr"|"soc2",
579
+ * ldhPolicy: "reject"|"audit"|"allow",
580
+ * punycodePolicy: "reject"|"audit"|"allow",
581
+ * mixedScriptPolicy: "reject"|"audit"|"allow",
582
+ * specialUsePolicy: "reject"|"audit"|"allow",
583
+ * ipLiteralPolicy: "reject"|"audit"|"allow",
584
+ * wildcardPolicy: "reject"|"audit"|"allow",
585
+ * singleLabelPolicy: "reject"|"audit"|"allow",
586
+ * underscorePolicy: "reject"|"audit"|"allow",
587
+ * dgaPolicy: "reject"|"audit"|"allow",
588
+ * trailingDotPolicy: "normalize"|"audit"|"reject",
589
+ * allowedScripts: string[]|null,
590
+ * dgaEntropyThreshold: number,
591
+ * dgaMinLabelLen: number,
592
+ * maxLabelOctets: number, // default 63 (RFC 1035 §2.3.4)
593
+ * maxDomainOctets: number, // default 253 (RFC 1035 §2.3.4)
594
+ * maxBytes: number, // total input byte cap
595
+ *
596
+ * @example
597
+ * var rv = b.guardDomain.validate("192.168.1.1", { profile: "strict" });
598
+ * rv.ok; // → false
599
+ * rv.issues.some(function (i) { return i.kind === "ipv4-as-domain"; }); // → true
600
+ *
601
+ * var ok = b.guardDomain.validate("example.com", { profile: "strict" });
602
+ * ok.ok; // → true
603
+ */
598
604
  function validate(input, opts) {
599
605
  opts = _resolveOpts(opts);
600
606
  numericBounds.requireAllPositiveFiniteIntIfPresent(opts,
@@ -611,6 +617,30 @@ function validate(input, opts) {
611
617
  return gateContract.aggregateIssues(_detectIssues(input, opts));
612
618
  }
613
619
 
620
+ /**
621
+ * @primitive b.guardDomain.sanitize
622
+ * @signature b.guardDomain.sanitize(input, opts?)
623
+ * @since 0.7.41
624
+ * @status stable
625
+ * @related b.guardDomain.validate, b.guardDomain.gate
626
+ *
627
+ * Normalize a domain-name string when no critical/high issues fire.
628
+ * Throws `GuardDomainError` on any high/critical refusal (homograph
629
+ * mix, IPv4-as-domain, special-use suffix, BIDI, malformed Punycode).
630
+ * Safe transforms applied otherwise: ASCII lowercasing, trailing-dot
631
+ * strip. Refuses to canonicalize Unicode labels — operators wanting
632
+ * IDN ToASCII supply `opts.idnToAscii` so the framework doesn't
633
+ * silently rewrite a label the operator's allowlist would treat as
634
+ * different.
635
+ *
636
+ * @opts
637
+ * profile: "strict"|"balanced"|"permissive",
638
+ * compliance: "hipaa"|"pci-dss"|"gdpr"|"soc2",
639
+ *
640
+ * @example
641
+ * var safe = b.guardDomain.sanitize("Example.Com.", { profile: "balanced" });
642
+ * safe; // → "example.com"
643
+ */
614
644
  function sanitize(input, opts) {
615
645
  opts = _resolveOpts(opts);
616
646
  if (typeof input !== "string") {
@@ -630,6 +660,31 @@ function sanitize(input, opts) {
630
660
  return out;
631
661
  }
632
662
 
663
+ /**
664
+ * @primitive b.guardDomain.gate
665
+ * @signature b.guardDomain.gate(opts?)
666
+ * @since 0.7.41
667
+ * @status stable
668
+ * @compliance hipaa, pci-dss, gdpr, soc2
669
+ * @related b.guardDomain.validate, b.guardDomain.sanitize
670
+ *
671
+ * Build a `b.gateContract` gate that consumes `ctx.identifier` (or
672
+ * `ctx.domain`) and dispatches `serve` (no input or clean) →
673
+ * `audit-only` (warn-only issues) → `refuse` (any critical or high
674
+ * issue). No `sanitize` action — domain canonicalization is
675
+ * caller-driven via `b.guardDomain.sanitize` so an allowlist gate
676
+ * never silently rewrites the operator's stored allowlist key.
677
+ *
678
+ * @opts
679
+ * profile: "strict"|"balanced"|"permissive",
680
+ * compliance: "hipaa"|"pci-dss"|"gdpr"|"soc2",
681
+ * name: string, // gate identity for audit / observability
682
+ *
683
+ * @example
684
+ * var domGate = b.guardDomain.gate({ profile: "strict" });
685
+ * var verdict = await domGate.check({ identifier: "myhost.localhost" });
686
+ * verdict.action; // → "refuse"
687
+ */
633
688
  function gate(opts) {
634
689
  opts = _resolveOpts(opts);
635
690
  return gateContract.buildGuardGate(
@@ -655,14 +710,81 @@ function gate(opts) {
655
710
  });
656
711
  }
657
712
 
713
+ /**
714
+ * @primitive b.guardDomain.buildProfile
715
+ * @signature b.guardDomain.buildProfile(opts)
716
+ * @since 0.7.41
717
+ * @status stable
718
+ * @related b.guardDomain.gate, b.guardDomain.compliancePosture
719
+ *
720
+ * Compose a derived profile from one or more named bases plus inline
721
+ * overrides. `opts.extends` is a profile name (`"strict"` /
722
+ * `"balanced"` / `"permissive"`) or an array of names; later entries
723
+ * shadow earlier ones. Inline `opts` keys win last.
724
+ *
725
+ * @opts
726
+ * extends: string|string[], // base profile name(s) to compose
727
+ *
728
+ * @example
729
+ * var custom = b.guardDomain.buildProfile({
730
+ * extends: "balanced",
731
+ * allowedScripts: ["latin"],
732
+ * punycodePolicy: "reject",
733
+ * });
734
+ * custom.punycodePolicy; // → "reject"
735
+ * custom.bidiPolicy; // → "reject"
736
+ */
658
737
  var buildProfile = gateContract.makeProfileBuilder(PROFILES);
659
738
 
739
+ /**
740
+ * @primitive b.guardDomain.compliancePosture
741
+ * @signature b.guardDomain.compliancePosture(name)
742
+ * @since 0.7.41
743
+ * @status stable
744
+ * @compliance hipaa, pci-dss, gdpr, soc2
745
+ * @related b.guardDomain.gate, b.guardDomain.buildProfile
746
+ *
747
+ * Look up a compliance-posture overlay by name (`"hipaa"` /
748
+ * `"pci-dss"` / `"gdpr"` / `"soc2"`). Returns a shallow clone of the
749
+ * posture object — the caller may mutate freely. Throws
750
+ * `GuardDomainError("domain.bad-posture")` on unknown name.
751
+ *
752
+ * @example
753
+ * var posture = b.guardDomain.compliancePosture("hipaa");
754
+ * posture.specialUsePolicy; // → "reject"
755
+ * posture.forensicSnippetBytes; // → 256
756
+ */
660
757
  function compliancePosture(name) {
661
758
  return gateContract.lookupCompliancePosture(name, COMPLIANCE_POSTURES,
662
759
  _err, "domain");
663
760
  }
664
761
 
665
762
  var _domainRulePacks = gateContract.makeRulePackLoader(GuardDomainError, "domain");
763
+ /**
764
+ * @primitive b.guardDomain.loadRulePack
765
+ * @signature b.guardDomain.loadRulePack(pack)
766
+ * @since 0.7.41
767
+ * @status stable
768
+ * @related b.guardDomain.gate
769
+ *
770
+ * Register an operator-supplied rule pack with the guard-domain
771
+ * registry. The pack is identified by `pack.id` (non-empty string)
772
+ * and stored for later inspection / dispatch by gates that opt in
773
+ * via `opts.rulePackId`. Returns the pack object unchanged on
774
+ * success; throws `GuardDomainError("domain.bad-opt")` when `pack`
775
+ * is missing or `pack.id` is not a non-empty string.
776
+ *
777
+ * @example
778
+ * var pack = b.guardDomain.loadRulePack({
779
+ * id: "tenant-corp-only",
780
+ * rules: [
781
+ * { id: "tenant-suffix", severity: "high",
782
+ * detect: function (d) { return !/\.example\.com$/i.test(d); },
783
+ * reason: "tenant policy: only example.com suffixes permitted" },
784
+ * ],
785
+ * });
786
+ * pack.id; // → "tenant-corp-only"
787
+ */
666
788
  var loadRulePack = _domainRulePacks.load;
667
789
 
668
790
  module.exports = {
@@ -1,35 +1,38 @@
1
1
  "use strict";
2
2
  /**
3
- * guard-email — Email content-safety primitive (b.guardEmail).
3
+ * @module b.guardEmail
4
+ * @nav Guards
5
+ * @title Guard Email
4
6
  *
5
- * Threat catalog grounded in current research:
6
- * - SMTP smuggling (CVE-2023-51764 Postfix; CVE-2023-51765 Sendmail;
7
- * CVE-2023-51766 Exim; CVE-2026-32178 .NET System.Net.Mail)
8
- * embedded SMTP verbs after bare-CR / bare-LF / dot-stuffing
9
- * manipulation lets an attacker inject a second message in the
10
- * same SMTP session with a forged envelope.
11
- * - CRLF header injection — `\r\n` inside any header field value
12
- * splits the header section and lets the attacker forge `From:`,
13
- * `Bcc:`, or smuggle a body.
14
- * - IDN homograph spoofing — mixed-script Unicode in the domain part
15
- * (Cyrillic а / Greek α / Armenian / Cherokee letters that look
16
- * like Latin lowercase). Most filters miss confusables.
17
- * - Display-name spoofing — `"support@apple.com" <attacker@evil>` —
18
- * the rendered name impersonates a trusted address while the
19
- * envelope routes elsewhere.
20
- * - Bare IP literal addresses — `user@[1.2.3.4]` / `user@[IPv6:...]`.
21
- * - Comment syntax in addresses — `(comment)` per RFC 5322 — most
22
- * receivers reject; senders that accept it are a smuggling vector.
23
- * - RFC 5321 / 5322 length caps — local-part 64; domain 255; total
24
- * address 320; per-line 998.
25
- * - Multiple @ characters / multiple addresses in a single field.
26
- * - Bidi / null / control / zero-width chars in addresses + headers.
27
- * - BOM injection at the start of a header.
7
+ * @intro
8
+ * RFC 822 / 5322 single-address validator + RFC 5322 message gate
9
+ * with header-injection defense, EAI / SMTPUTF8 support, label
10
+ * length caps, IP-literal denial, and sub-address handling.
28
11
  *
29
- * var rv = b.guardEmail.validateAddress(addr, { profile: "strict" });
30
- * var rv = b.guardEmail.validateMessage(rfc822, { profile: "strict" });
31
- * var safe = b.guardEmail.sanitize(input, { profile: "balanced" });
32
- * var g = b.guardEmail.gate({ profile: "strict" });
12
+ * Two entry shapes:
13
+ * - `validateAddress(addr, opts)` single mailbox (RFC 5321
14
+ * atext@DNS-domain). Caps RFC 5321 §4.5.3.1 local-part 64 /
15
+ * domain 255 / address 320. Flags multi-`@`, IP literals,
16
+ * Punycode, mixed-script confusables, and codepoint-class
17
+ * threats (BIDI / control / null / zero-width).
18
+ * - `validateMessage(rfc822, opts)` — full RFC 5322 message.
19
+ * Splits header section, unfolds folded headers, walks every
20
+ * single-line header for embedded CR/LF, drives address checks
21
+ * on `From` / `To` / `Cc` / `Bcc` / `Reply-To` / `Sender` /
22
+ * `Return-Path`, and scans the message body for SMTP-smuggling
23
+ * (bare-CR / bare-LF / `\r?\n.\r?\nMAIL FROM:` class —
24
+ * CVE-2023-51764 / 51765 / 51766) plus RFC 5322 §2.1.1 line cap.
25
+ *
26
+ * Profiles ship in pairs:
27
+ * - `strict` / `balanced` / `permissive` — operator scope.
28
+ * - `hipaa` / `pci-dss` / `gdpr` / `soc2` — compliance posture.
29
+ *
30
+ * Header injection, SMTP smuggling, multi-`@`, and null-byte are
31
+ * `reject` at every profile — universally exploitable, no
32
+ * sanitization is safe.
33
+ *
34
+ * @card
35
+ * RFC 822 / 5322 single-address validator + RFC 5322 message gate with header-injection defense, EAI / SMTPUTF8 support, label length caps, IP-literal denial, and sub-address handling.
33
36
  */
34
37
 
35
38
  var codepointClass = require("./codepoint-class");
@@ -423,6 +426,50 @@ function _detectAddressIssues(input, opts) {
423
426
  return issues;
424
427
  }
425
428
 
429
+ /**
430
+ * @primitive b.guardEmail.validateAddress
431
+ * @signature b.guardEmail.validateAddress(input, opts)
432
+ * @since 0.7.17
433
+ * @status stable
434
+ * @related b.guardEmail.validateMessage, b.guardEmail.gate, b.guardEmail.sanitize
435
+ *
436
+ * Validate a single email address against RFC 5321 atext@DNS-domain
437
+ * shape with the active profile's policies. Returns `{ ok, issues }`;
438
+ * `issues[]` carries `kind` / `severity` / `ruleId` / `snippet` for
439
+ * every detector that fired. Never throws on input — bad shapes
440
+ * surface as `bad-input` issues so the caller can route on them.
441
+ *
442
+ * Detectors run in order: total-address cap, multi-`@` count,
443
+ * RFC 5322 comment syntax, IP literal `[...]`, local-part / domain
444
+ * caps, Punycode (`xn--`) labels, mixed-script confusables (Latin /
445
+ * Cyrillic / Greek / Armenian / Cherokee), strict-ASCII regex shape,
446
+ * and codepoint-class threats (BIDI / null / control / zero-width).
447
+ *
448
+ * @opts
449
+ * profile: "strict" | "balanced" | "permissive",
450
+ * compliancePosture: "hipaa" | "pci-dss" | "gdpr" | "soc2",
451
+ * multiAtPolicy: "reject" | "audit" | "allow",
452
+ * ipLiteralPolicy: "reject" | "audit" | "allow",
453
+ * addressCommentPolicy: "reject" | "audit" | "allow",
454
+ * punycodePolicy: "reject" | "audit" | "allow",
455
+ * mixedScriptPolicy: "reject" | "audit" | "allow",
456
+ * allowedScripts: string[] | null,
457
+ * maxLocalPartBytes: number,
458
+ * maxDomainBytes: number,
459
+ * maxAddressBytes: number,
460
+ *
461
+ * @example
462
+ * var guardEmail = require("./lib/guard-email");
463
+ * var rv = guardEmail.validateAddress("alice@example.com",
464
+ * { profile: "strict" });
465
+ * rv.ok; // → true
466
+ * rv.issues.length; // → 0
467
+ *
468
+ * var bad = guardEmail.validateAddress("user@[10.0.0.1]",
469
+ * { profile: "strict" });
470
+ * bad.ok; // → false
471
+ * bad.issues[0].kind; // → "ip-literal"
472
+ */
426
473
  function validateAddress(input, opts) {
427
474
  opts = _resolveOpts(opts);
428
475
  numericBounds.requireAllPositiveFiniteIntIfPresent(opts,
@@ -633,6 +680,53 @@ function _checkAddressHeaderValue(value, opts, headerName) {
633
680
  return issues;
634
681
  }
635
682
 
683
+ /**
684
+ * @primitive b.guardEmail.validateMessage
685
+ * @signature b.guardEmail.validateMessage(input, opts)
686
+ * @since 0.7.17
687
+ * @status stable
688
+ * @related b.guardEmail.validateAddress, b.guardEmail.gate, b.guardEmail.sanitize
689
+ *
690
+ * Validate a complete RFC 5322 message (headers + body) against the
691
+ * active profile. Splits the header section, unfolds folded
692
+ * continuation lines, walks every single-line header for embedded
693
+ * CR/LF (header-injection class), and runs `validateAddress` on each
694
+ * envelope under address-bearing headers (`From` / `To` / `Cc` /
695
+ * `Bcc` / `Reply-To` / `Sender` / `Return-Path`). Body is scanned
696
+ * for SMTP-smuggling vectors (bare CR / bare LF / smuggled
697
+ * `MAIL FROM:` after a bare line ending — CVE-2023-51764 / 51765 /
698
+ * 51766 class). Caps RFC 5322 §2.1.1 998-byte line, configurable
699
+ * header count, and total `maxBytes`.
700
+ *
701
+ * @opts
702
+ * profile: "strict" | "balanced" | "permissive",
703
+ * compliancePosture: "hipaa" | "pci-dss" | "gdpr" | "soc2",
704
+ * crlfHeaderInjectionPolicy: "reject" | "audit" | "allow",
705
+ * smtpSmugglingPolicy: "reject" | "audit" | "allow",
706
+ * bareCrPolicy: "reject" | "audit" | "allow",
707
+ * bareLfPolicy: "reject" | "audit" | "allow",
708
+ * displayNameSpoofPolicy: "reject" | "audit" | "allow",
709
+ * bomPolicy: "reject" | "audit" | "strip" | "allow",
710
+ * maxHeaderLineBytes: number,
711
+ * maxHeaders: number,
712
+ * maxBytes: number,
713
+ *
714
+ * @example
715
+ * var guardEmail = require("./lib/guard-email");
716
+ * var msg = "From: alice@example.com\r\n" +
717
+ * "To: bob@example.com\r\n" +
718
+ * "Subject: hello\r\n" +
719
+ * "Date: Mon, 5 May 2026 10:00:00 +0000\r\n\r\n" +
720
+ * "Hello.\r\n";
721
+ * var rv = guardEmail.validateMessage(msg, { profile: "strict" });
722
+ * rv.ok; // → true
723
+ *
724
+ * // Header injection: a CRLF inside the From value forges a Bcc.
725
+ * var bad = "From: alice@example.com\r\nBcc: leak@evil\r\n" +
726
+ * "To: bob@example.com\r\nSubject: hi\r\n\r\nbody\r\n";
727
+ * var injected = guardEmail.validateMessage(bad, { profile: "strict" });
728
+ * injected.ok; // → true (well-formed; injected-line is its own header)
729
+ */
636
730
  function validateMessage(input, opts) {
637
731
  opts = _resolveOpts(opts);
638
732
  numericBounds.requireAllPositiveFiniteIntIfPresent(opts,
@@ -649,7 +743,33 @@ function validateMessage(input, opts) {
649
743
  return gateContract.aggregateIssues(_detectMessageIssues(input, opts));
650
744
  }
651
745
 
652
- // validate(input, opts) — auto-detect single address vs full message.
746
+ /**
747
+ * @primitive b.guardEmail.validate
748
+ * @signature b.guardEmail.validate(input, opts)
749
+ * @since 0.7.17
750
+ * @status stable
751
+ * @related b.guardEmail.validateAddress, b.guardEmail.validateMessage
752
+ *
753
+ * Auto-routing entry: a string with no newline AND no `:` is treated
754
+ * as a single address (delegates to `validateAddress`); otherwise the
755
+ * input is treated as a full RFC 5322 message (delegates to
756
+ * `validateMessage`). Operators who want a fixed shape — never the
757
+ * heuristic — call the specific entry directly.
758
+ *
759
+ * @opts
760
+ * profile: "strict" | "balanced" | "permissive",
761
+ * compliancePosture: "hipaa" | "pci-dss" | "gdpr" | "soc2",
762
+ *
763
+ * @example
764
+ * var guardEmail = require("./lib/guard-email");
765
+ * guardEmail.validate("alice@example.com",
766
+ * { profile: "strict" }).ok; // → true
767
+ *
768
+ * var msg = "From: a@example.com\r\nTo: b@example.com\r\n" +
769
+ * "Subject: x\r\nDate: Mon, 5 May 2026 10:00:00 +0000\r\n\r\nhi\r\n";
770
+ * guardEmail.validate(msg,
771
+ * { profile: "strict" }).ok; // → true
772
+ */
653
773
  function validate(input, opts) {
654
774
  if (typeof input === "string" && input.indexOf("\n") === -1 &&
655
775
  input.indexOf(":") === -1) {
@@ -658,6 +778,44 @@ function validate(input, opts) {
658
778
  return validateMessage(input, opts);
659
779
  }
660
780
 
781
+ /**
782
+ * @primitive b.guardEmail.sanitize
783
+ * @signature b.guardEmail.sanitize(input, opts)
784
+ * @since 0.7.17
785
+ * @status stable
786
+ * @related b.guardEmail.validate, b.guardEmail.gate
787
+ *
788
+ * Best-effort sanitize for email content. THROWS on critical-severity
789
+ * issues (SMTP smuggling / CRLF header injection / multi-`@` /
790
+ * mixed-script confusable / null byte) — these have no safe
791
+ * sanitization. Lower-severity codepoint-class threats (BIDI / zero-
792
+ * width / control / BOM) are stripped per the active profile. Never
793
+ * silently drops a smuggling vector: the caller either gets
794
+ * sanitized text or a thrown `GuardEmailError`.
795
+ *
796
+ * @opts
797
+ * profile: "strict" | "balanced" | "permissive",
798
+ * compliancePosture: "hipaa" | "pci-dss" | "gdpr" | "soc2",
799
+ * bidiPolicy: "reject" | "audit" | "strip" | "allow",
800
+ * controlPolicy: "reject" | "audit" | "strip" | "allow",
801
+ * zeroWidthPolicy: "reject" | "audit" | "strip" | "allow",
802
+ *
803
+ * @example
804
+ * var guardEmail = require("./lib/guard-email");
805
+ * // CRLF in the From value is a header-injection vector — sanitize
806
+ * // refuses rather than silently dropping the bytes.
807
+ * var hostile = "From: alice@example.com\rBcc: leak@evil\r\n" +
808
+ * "To: bob@example.com\r\nSubject: hi\r\n\r\nbody\r\n";
809
+ * var threw = false;
810
+ * try { guardEmail.sanitize(hostile, { profile: "strict" }); }
811
+ * catch (e) { threw = (e.code || "").indexOf("email.") === 0; }
812
+ * threw; // → true
813
+ *
814
+ * // Benign input with a stray BIDI override is stripped under balanced.
815
+ * var clean = guardEmail.sanitize("hello world",
816
+ * { profile: "balanced" });
817
+ * clean; // → "hello world"
818
+ */
661
819
  function sanitize(input, opts) {
662
820
  opts = _resolveOpts(opts);
663
821
  if (typeof input !== "string") {
@@ -675,6 +833,35 @@ function sanitize(input, opts) {
675
833
  return codepointClass.applyCharStripPolicies(input, opts);
676
834
  }
677
835
 
836
+ /**
837
+ * @primitive b.guardEmail.gate
838
+ * @signature b.guardEmail.gate(opts)
839
+ * @since 0.7.17
840
+ * @status stable
841
+ * @related b.guardEmail.validateMessage, b.guardEmail.sanitize, b.guardAll.gate
842
+ *
843
+ * Build a guard gate function compatible with the `b.guardAll` family
844
+ * dispatch. The returned async gate accepts a request-shaped context,
845
+ * runs `validateMessage` against the extracted bytes, and returns
846
+ * `{ ok, action, issues? }` where `action` is `serve` (no issues),
847
+ * `audit-only` (warn-level), or `refuse` (high / critical severity).
848
+ *
849
+ * @opts
850
+ * profile: "strict" | "balanced" | "permissive",
851
+ * compliancePosture: "hipaa" | "pci-dss" | "gdpr" | "soc2",
852
+ * name: string, // gate identifier surfaced in audit metadata
853
+ *
854
+ * @example
855
+ * var guardEmail = require("./lib/guard-email");
856
+ * var g = guardEmail.gate({ profile: "strict" });
857
+ * typeof g; // → "function"
858
+ *
859
+ * var msg = "From: alice@example.com\r\nTo: bob@example.com\r\n" +
860
+ * "Subject: hi\r\nDate: Mon, 5 May 2026 10:00:00 +0000\r\n\r\nbody\r\n";
861
+ * g({ body: Buffer.from(msg, "utf8") }).then(function (rv) {
862
+ * rv.action; // → "serve"
863
+ * });
864
+ */
678
865
  function gate(opts) {
679
866
  opts = _resolveOpts(opts);
680
867
  return gateContract.buildGuardGate(
@@ -700,6 +887,26 @@ function gate(opts) {
700
887
 
701
888
  var buildProfile = gateContract.makeProfileBuilder(PROFILES);
702
889
 
890
+ /**
891
+ * @primitive b.guardEmail.compliancePosture
892
+ * @signature b.guardEmail.compliancePosture(name)
893
+ * @since 0.7.17
894
+ * @status stable
895
+ * @compliance hipaa, pci-dss, gdpr, soc2
896
+ * @related b.guardEmail.gate, b.guardEmail.validateMessage
897
+ *
898
+ * Look up a compliance posture by name and return its frozen opts
899
+ * bundle. Throws `GuardEmailError` (`email.unknown-posture`) for
900
+ * names outside `hipaa` / `pci-dss` / `gdpr` / `soc2`. The returned
901
+ * opts can be merged into a `gate()` / `validateMessage()` call to
902
+ * apply the posture's defaults (forensic-snippet length included).
903
+ *
904
+ * @example
905
+ * var guardEmail = require("./lib/guard-email");
906
+ * var posture = guardEmail.compliancePosture("hipaa");
907
+ * posture.bareCrPolicy; // → "reject"
908
+ * posture.forensicSnippetBytes; // → 256
909
+ */
703
910
  function compliancePosture(name) {
704
911
  return gateContract.lookupCompliancePosture(name, COMPLIANCE_POSTURES, _err, "email");
705
912
  }