@blamejs/core 0.8.43 → 0.8.49
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +92 -0
- package/README.md +10 -10
- package/index.js +52 -0
- package/lib/a2a.js +159 -34
- package/lib/acme.js +762 -0
- package/lib/ai-pref.js +166 -43
- package/lib/api-key.js +108 -47
- package/lib/api-snapshot.js +157 -40
- package/lib/app-shutdown.js +113 -77
- package/lib/archive.js +337 -40
- package/lib/arg-parser.js +697 -0
- package/lib/asyncapi.js +99 -55
- package/lib/atomic-file.js +465 -104
- package/lib/audit-chain.js +123 -34
- package/lib/audit-daily-review.js +389 -0
- package/lib/audit-sign.js +302 -56
- package/lib/audit-tools.js +412 -63
- package/lib/audit.js +656 -35
- package/lib/auth/jwt-external.js +17 -0
- package/lib/auth/oauth.js +7 -0
- package/lib/auth-bot-challenge.js +505 -0
- package/lib/auth-header.js +92 -25
- package/lib/backup/bundle.js +26 -0
- package/lib/backup/index.js +512 -89
- package/lib/backup/manifest.js +168 -7
- package/lib/break-glass.js +415 -39
- package/lib/budr.js +103 -30
- package/lib/bundler.js +86 -66
- package/lib/cache.js +192 -72
- package/lib/chain-writer.js +65 -40
- package/lib/circuit-breaker.js +56 -33
- package/lib/cli-helpers.js +106 -75
- package/lib/cli.js +6 -30
- package/lib/cloud-events.js +99 -32
- package/lib/cluster-storage.js +162 -37
- package/lib/cluster.js +340 -49
- package/lib/codepoint-class.js +66 -0
- package/lib/compliance.js +424 -24
- package/lib/config-drift.js +111 -46
- package/lib/config.js +94 -40
- package/lib/consent.js +165 -18
- package/lib/constants.js +1 -0
- package/lib/content-credentials.js +153 -48
- package/lib/cookies.js +154 -62
- package/lib/credential-hash.js +133 -61
- package/lib/crypto-field.js +702 -18
- package/lib/crypto-hpke.js +256 -0
- package/lib/crypto.js +744 -22
- package/lib/csv.js +178 -35
- package/lib/daemon.js +456 -0
- package/lib/dark-patterns.js +186 -55
- package/lib/db-query.js +79 -2
- package/lib/db.js +1431 -60
- package/lib/ddl-change-control.js +523 -0
- package/lib/deprecate.js +195 -40
- package/lib/dev.js +82 -39
- package/lib/dora.js +67 -48
- package/lib/dr-runbook.js +368 -0
- package/lib/dsr.js +142 -11
- package/lib/dual-control.js +91 -56
- package/lib/events.js +120 -41
- package/lib/external-db-migrate.js +192 -2
- package/lib/external-db.js +795 -50
- package/lib/fapi2.js +122 -1
- package/lib/fda-21cfr11.js +395 -0
- package/lib/fdx.js +132 -2
- package/lib/file-type.js +87 -0
- package/lib/file-upload.js +93 -0
- package/lib/flag.js +82 -20
- package/lib/forms.js +132 -29
- package/lib/framework-error.js +169 -0
- package/lib/framework-schema.js +163 -35
- package/lib/gate-contract.js +849 -175
- package/lib/graphql-federation.js +68 -7
- package/lib/guard-all.js +172 -55
- package/lib/guard-archive.js +286 -124
- package/lib/guard-auth.js +194 -21
- package/lib/guard-cidr.js +190 -28
- package/lib/guard-csv.js +397 -51
- package/lib/guard-domain.js +213 -91
- package/lib/guard-email.js +236 -29
- package/lib/guard-filename.js +307 -75
- package/lib/guard-graphql.js +263 -30
- package/lib/guard-html.js +310 -116
- package/lib/guard-image.js +243 -30
- package/lib/guard-json.js +260 -54
- package/lib/guard-jsonpath.js +235 -23
- package/lib/guard-jwt.js +284 -30
- package/lib/guard-markdown.js +204 -22
- package/lib/guard-mime.js +190 -26
- package/lib/guard-oauth.js +277 -28
- package/lib/guard-pdf.js +251 -27
- package/lib/guard-regex.js +226 -18
- package/lib/guard-shell.js +229 -26
- package/lib/guard-svg.js +177 -10
- package/lib/guard-template.js +232 -21
- package/lib/guard-time.js +195 -29
- package/lib/guard-uuid.js +189 -30
- package/lib/guard-xml.js +259 -36
- package/lib/guard-yaml.js +241 -44
- package/lib/honeytoken.js +63 -27
- package/lib/html-balance.js +83 -0
- package/lib/http-client.js +486 -59
- package/lib/http-message-signature.js +582 -0
- package/lib/i18n.js +102 -49
- package/lib/iab-mspa.js +112 -32
- package/lib/iab-tcf.js +107 -2
- package/lib/inbox.js +90 -52
- package/lib/keychain.js +865 -0
- package/lib/legal-hold.js +374 -0
- package/lib/local-db-thin.js +320 -0
- package/lib/log-stream.js +281 -51
- package/lib/log.js +184 -86
- package/lib/mail-bounce.js +107 -62
- package/lib/mail.js +295 -58
- package/lib/mcp.js +108 -27
- package/lib/metrics.js +98 -89
- package/lib/middleware/age-gate.js +36 -0
- package/lib/middleware/ai-act-disclosure.js +37 -0
- package/lib/middleware/api-encrypt.js +45 -0
- package/lib/middleware/assetlinks.js +40 -0
- package/lib/middleware/asyncapi-serve.js +35 -0
- package/lib/middleware/attach-user.js +40 -0
- package/lib/middleware/bearer-auth.js +40 -0
- package/lib/middleware/body-parser.js +230 -0
- package/lib/middleware/bot-disclose.js +34 -0
- package/lib/middleware/bot-guard.js +39 -0
- package/lib/middleware/compression.js +37 -0
- package/lib/middleware/cookies.js +32 -0
- package/lib/middleware/cors.js +40 -0
- package/lib/middleware/csp-nonce.js +40 -0
- package/lib/middleware/csp-report.js +34 -0
- package/lib/middleware/csrf-protect.js +43 -0
- package/lib/middleware/daily-byte-quota.js +53 -85
- package/lib/middleware/db-role-for.js +40 -0
- package/lib/middleware/dpop.js +40 -0
- package/lib/middleware/error-handler.js +37 -14
- package/lib/middleware/fetch-metadata.js +39 -0
- package/lib/middleware/flag-context.js +34 -0
- package/lib/middleware/gpc.js +33 -0
- package/lib/middleware/headers.js +35 -0
- package/lib/middleware/health.js +46 -0
- package/lib/middleware/host-allowlist.js +30 -0
- package/lib/middleware/network-allowlist.js +38 -0
- package/lib/middleware/openapi-serve.js +34 -0
- package/lib/middleware/rate-limit.js +160 -18
- package/lib/middleware/request-id.js +36 -18
- package/lib/middleware/request-log.js +37 -0
- package/lib/middleware/require-aal.js +29 -0
- package/lib/middleware/require-auth.js +32 -0
- package/lib/middleware/require-bound-key.js +41 -0
- package/lib/middleware/require-content-type.js +32 -0
- package/lib/middleware/require-methods.js +27 -0
- package/lib/middleware/require-mtls.js +33 -0
- package/lib/middleware/require-step-up.js +37 -0
- package/lib/middleware/security-headers.js +44 -0
- package/lib/middleware/security-txt.js +38 -0
- package/lib/middleware/span-http-server.js +37 -0
- package/lib/middleware/sse.js +36 -0
- package/lib/middleware/trace-log-correlation.js +33 -0
- package/lib/middleware/trace-propagate.js +32 -0
- package/lib/middleware/tus-upload.js +90 -0
- package/lib/middleware/web-app-manifest.js +53 -0
- package/lib/mtls-ca.js +100 -70
- package/lib/network-byte-quota.js +308 -0
- package/lib/network-heartbeat.js +135 -0
- package/lib/network-tls.js +534 -4
- package/lib/network.js +103 -0
- package/lib/notify.js +114 -43
- package/lib/ntp-check.js +192 -51
- package/lib/observability.js +145 -47
- package/lib/openapi.js +90 -44
- package/lib/outbox.js +99 -1
- package/lib/pagination.js +168 -86
- package/lib/parsers/index.js +16 -5
- package/lib/permissions.js +93 -40
- package/lib/pqc-agent.js +84 -8
- package/lib/pqc-software.js +94 -60
- package/lib/process-spawn.js +95 -21
- package/lib/pubsub.js +96 -66
- package/lib/queue.js +375 -54
- package/lib/redact.js +793 -21
- package/lib/render.js +139 -47
- package/lib/request-helpers.js +485 -121
- package/lib/restore-bundle.js +142 -39
- package/lib/restore-rollback.js +136 -45
- package/lib/retention.js +178 -50
- package/lib/retry.js +116 -33
- package/lib/router.js +475 -23
- package/lib/safe-async.js +543 -94
- package/lib/safe-buffer.js +337 -41
- package/lib/safe-json.js +467 -62
- package/lib/safe-jsonpath.js +285 -0
- package/lib/safe-schema.js +631 -87
- package/lib/safe-sql.js +221 -59
- package/lib/safe-url.js +278 -46
- package/lib/sandbox-worker.js +135 -0
- package/lib/sandbox.js +358 -0
- package/lib/scheduler.js +135 -70
- package/lib/self-update.js +647 -0
- package/lib/session-device-binding.js +431 -0
- package/lib/session.js +259 -49
- package/lib/slug.js +138 -26
- package/lib/ssrf-guard.js +316 -56
- package/lib/storage.js +433 -70
- package/lib/subject.js +405 -23
- package/lib/template.js +148 -8
- package/lib/tenant-quota.js +545 -0
- package/lib/testing.js +440 -53
- package/lib/time.js +291 -23
- package/lib/tls-exporter.js +239 -0
- package/lib/tracing.js +90 -74
- package/lib/uuid.js +97 -22
- package/lib/vault/index.js +284 -22
- package/lib/vault/seal-pem-file.js +66 -0
- package/lib/watcher.js +368 -0
- package/lib/webhook.js +196 -63
- package/lib/websocket.js +393 -68
- package/lib/wiki-concepts.js +338 -0
- package/lib/worker-pool.js +464 -0
- package/package.json +3 -3
- package/sbom.cyclonedx.json +7 -7
package/lib/guard-domain.js
CHANGED
|
@@ -1,55 +1,56 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
/**
|
|
3
|
-
*
|
|
3
|
+
* @module b.guardDomain
|
|
4
|
+
* @nav Guards
|
|
5
|
+
* @title Guard Domain
|
|
4
6
|
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
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
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
* -
|
|
14
|
-
*
|
|
15
|
-
*
|
|
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
|
-
*
|
|
44
|
-
*
|
|
45
|
-
*
|
|
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
|
-
*
|
|
48
|
-
*
|
|
49
|
-
*
|
|
50
|
-
*
|
|
51
|
-
*
|
|
52
|
-
*
|
|
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
|
|
125
|
-
//
|
|
126
|
-
//
|
|
127
|
-
var
|
|
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 = {
|
package/lib/guard-email.js
CHANGED
|
@@ -1,35 +1,38 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
/**
|
|
3
|
-
*
|
|
3
|
+
* @module b.guardEmail
|
|
4
|
+
* @nav Guards
|
|
5
|
+
* @title Guard Email
|
|
4
6
|
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
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
|
-
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
*
|
|
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
|
-
|
|
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
|
}
|