@blamejs/core 0.7.42 → 0.7.44

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 CHANGED
@@ -8,6 +8,10 @@ upgrading across more than a few patches at a time.
8
8
 
9
9
  ## v0.7.x
10
10
 
11
+ - **0.7.44** (2026-05-05) — `b.guardUuid` — UUID identifier-safety primitive (KIND="identifier"). Validates user-supplied UUID strings per RFC 9562 (May 2024, obsoletes RFC 4122). Threat catalog: shape malformation across the four canonical forms (hyphenated 8-4-4-4-12, hyphenless 32-hex, Microsoft GUID braces `{…}`, `urn:uuid:` prefix); RFC 9562 §4.2 unassigned version digits (only 1-8 are defined); non-RFC 4122 variant bits (only the `10xx` high-bits family is the canonical UUID variant); nil UUID §5.9 / max UUID §5.10 sentinel-leak refuse; format policy enforcement (strict default = `hyphenated-only`); BIDI / zero-width / control / null-byte universal refuse via `lib/codepoint-class.js`. `sanitize` returns canonical lowercase hyphenated form (strips braces / urn prefix). Profiles: `strict` (hyphenated-only, refuse all sentinels and non-canonical forms), `balanced` (accept any form, audit sentinels), `permissive` (universal-refuse class still refused). Postures: `hipaa` / `pci-dss` / `soc2` strict overlay; `gdpr` balanced overlay. Auto-registers into `b.guardAll` as a STANDALONE_GUARD; the adaptive integration harness picks it up automatically via the KIND="identifier" dispatcher.
12
+
13
+ - **0.7.43** (2026-05-05) — `b.guardDomain` — domain-name identifier-safety primitive (KIND="identifier"). Validates user-supplied DNS names destined for allowlists, redirect targets, webhook endpoints, email-domain extraction, and CORS origin checks. Threat catalog: RFC 1035 §2.3.4 length caps (63 octets per label, 253 octets per FQDN), RFC 952 / 1123 LDH-rule violations (no leading/trailing hyphen, no `--` at positions 3-4 except `xn--`), IDN homograph mixed-script confusables (Latin / Cyrillic / Greek / Cherokee / Armenian / Han / Hiragana / Katakana / Hangul / Arabic / Hebrew range tables), BIDI / zero-width / control / null universal refuse via `lib/codepoint-class.js` (CVE-2021-42574 Trojan Source class), Punycode A-label malformation (bare `xn--`, double-encoded), RFC 6761 special-use suffix matching (`.localhost` / `.local` / `.invalid` / `.test` / `.onion` / `.alt` / `.home.arpa` / `.internal`), IPv4-as-domain confusion (CVE-2021-22931 — dotted-decimal / octal / hex / long-decimal forms), IPv6 bracket-literal, single-label / TLD-only refuse, wildcard `*` label refuse at every profile, RFC 8552 underscore-service-label policy, DGA Shannon-entropy heuristic for high-entropy long single labels (Mirai / Conficker C2 shape). Profiles: `strict` (Latin-only scripts, refuse-on-everything), `balanced` (audit Punycode, allow major international scripts, audit DGA), `permissive` (universal-refuse class still refused; everything else audit / allow). Compliance postures: `hipaa` / `pci-dss` / `soc2` strict overlay; `gdpr` balanced overlay. Auto-registers into `b.guardAll` as a STANDALONE_GUARD; the adaptive integration harness at `test/layer-5-integration/guard-host-integration.test.js` picks it up automatically. Defer-with-condition: full UTS #46 ToASCII / ToUnicode round-trip and Public-Suffix-List boundary enforcement ship behind operator-supplied callbacks (`opts.idnToAscii`, `opts.publicSuffixList`); re-open conditions are documented in the wiki page.
14
+
11
15
  - **0.7.42** (2026-05-05) — gitleaks allowlist for v0.7.28 CHANGELOG doc-snippet false-positive. The v0.7.28 release-notes entry for `b.crypto.encryptMlkem768X25519` includes a JS-object-literal code example with a `privateKey: mlkemPrivateKey` field; gitleaks' default `generic-api-key` rule fires on the high-entropy content adjacent to the `privateKey:` token, blocking every release-tag CI run since v0.7.38. Allowlisted by commit + fingerprint per the same pattern as the existing `74e627e` entry — surgical suppression of the documented false positive, no broader rule weakening. Working-tree-only or rule-disabling alternatives were rejected: gitleaks' `git`-history scan is the stricter gate (catches secrets that landed and were later removed) and the documented snippet contains no real secret.
12
16
 
13
17
  - **0.7.41** (2026-05-05) — wiki primitive-validator BACKLOG sweep: 50 of 101 entries pruned. Audited every entry in `examples/wiki/test/validate-primitive-sections.js`'s `UNDOCUMENTED_BACKLOG` map against the actual wiki page bodies; primitives that already have signature-form headings (`b.X.Y(...)`) discovered by the validator no longer need a BACKLOG entry suppressing them. Removed entries: `consent`, `websocketChannels`, `ssrfGuard`, `htmlBalance`, `csv`, `uuid`, `time`, `mailBounce`, `archive`, `breakGlass`, `forms`, `render`, `errorPage`, `cluster`, `safeBuffer`, `safeSql`, `safeUrl`, `retry`, `fileType`, `scheduler`, `jobs`, `backup`, `restore`, `i18n`, `cache`, `crypto`, `createApp`, `fileUpload`, `mtlsCa`, `pqcGate`, `pqcAgent`, `permissions`, `apiKey`, `webhook`, `notify`, `credentialHash`, `queue`, plus the 11-guard family (`guardEmail` through `guardAll`). The remaining 51 entries fall into three real classes: namespace-level entries with method-only headings (vault primitives, audit chain, etc.), pages structured around backend-builder patterns instead of flat methods (`objectStore`, `backupBundle`, etc.), and internal helpers that aren't part of the operator surface (`cli`, `boot`, `dev`, `protocolDispatcher`). The shrunk BACKLOG narrows the gap operators see between "covered by the parent's wiki page" and "actually drift waiting to be fixed."
package/index.js CHANGED
@@ -110,6 +110,8 @@ var guardYaml = require("./lib/guard-yaml");
110
110
  var guardXml = require("./lib/guard-xml");
111
111
  var guardMarkdown = require("./lib/guard-markdown");
112
112
  var guardEmail = require("./lib/guard-email");
113
+ var guardDomain = require("./lib/guard-domain");
114
+ var guardUuid = require("./lib/guard-uuid");
113
115
  var guardAll = require("./lib/guard-all");
114
116
  var ssrfGuard = require("./lib/ssrf-guard");
115
117
  var authHeader = require("./lib/auth-header");
@@ -251,6 +253,8 @@ module.exports = {
251
253
  guardXml: guardXml,
252
254
  guardMarkdown: guardMarkdown,
253
255
  guardEmail: guardEmail,
256
+ guardDomain: guardDomain,
257
+ guardUuid: guardUuid,
254
258
  guardAll: guardAll,
255
259
  ssrfGuard: ssrfGuard,
256
260
  authHeader: authHeader,
@@ -249,6 +249,21 @@ var GuardMarkdownError = defineClass("GuardMarkdownError", { alwaysPermane
249
249
  // in addresses, bidi/null/control chars in headers + addresses, header-
250
250
  // folding smuggling, BOM injection. alwaysPermanent.
251
251
  var GuardEmailError = defineClass("GuardEmailError", { alwaysPermanent: true });
252
+ // GuardDomainError covers domain-name identifier violations: RFC 1035
253
+ // length-cap overflow, RFC 952/1123 LDH-rule violations, IDN homograph /
254
+ // mixed-script confusables, BIDI / zero-width / control-byte injection,
255
+ // Punycode malformation, RFC 6761 special-use domains, IPv4-as-domain
256
+ // confusion (CVE-2021-22931), IPv6 bracket literals, single-label / TLD-
257
+ // only strings, wildcard labels, RFC 8552 underscore-label misuse, DGA
258
+ // high-entropy labels. alwaysPermanent.
259
+ var GuardDomainError = defineClass("GuardDomainError", { alwaysPermanent: true });
260
+ // GuardUuidError covers UUID identifier violations: shape malformation
261
+ // (non-canonical / non-hex), RFC 9562 §4.2 unassigned version digits,
262
+ // non-RFC 4122 variant bits, nil UUID (§5.9) / max UUID (§5.10) sentinel
263
+ // leakage, urn:uuid: + Microsoft GUID braces forms outside the operator's
264
+ // declared formatPolicy, BIDI / zero-width / control-byte / null-byte
265
+ // universal refuse. alwaysPermanent.
266
+ var GuardUuidError = defineClass("GuardUuidError", { alwaysPermanent: true });
252
267
  // DoraError covers DORA Article 17 incident-reporting workflow errors
253
268
  // (classification refusal, report-shape validation, ESA-template
254
269
  // generation, audit-chain integration). Permanent — these are
@@ -308,6 +323,8 @@ module.exports = {
308
323
  GuardXmlError: GuardXmlError,
309
324
  GuardMarkdownError: GuardMarkdownError,
310
325
  GuardEmailError: GuardEmailError,
326
+ GuardDomainError: GuardDomainError,
327
+ GuardUuidError: GuardUuidError,
311
328
  DoraError: DoraError,
312
329
  ComplianceError: ComplianceError,
313
330
  SmtpPolicyError: SmtpPolicyError,
package/lib/guard-all.js CHANGED
@@ -89,6 +89,8 @@ var GUARDS = [
89
89
  // adaptive integration harness iterates `allGuards()` to pick them up.
90
90
  var STANDALONE_GUARDS = [
91
91
  require("./guard-filename"),
92
+ require("./guard-domain"),
93
+ require("./guard-uuid"),
92
94
  ];
93
95
 
94
96
  // Framework-wide profile + posture vocabulary that every guard MUST
@@ -0,0 +1,692 @@
1
+ "use strict";
2
+ /**
3
+ * guard-domain — Domain-name identifier-safety primitive (b.guardDomain).
4
+ *
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).
8
+ *
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).
42
+ *
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" });
46
+ *
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.
53
+ */
54
+
55
+ var codepointClass = require("./codepoint-class");
56
+ var lazyRequire = require("./lazy-require");
57
+ var gateContract = require("./gate-contract");
58
+ var C = require("./constants");
59
+ var numericBounds = require("./numeric-bounds");
60
+ var { GuardDomainError } = require("./framework-error");
61
+
62
+ var observability = lazyRequire(function () { return require("./observability"); });
63
+ void observability;
64
+
65
+ var _err = GuardDomainError.factory;
66
+
67
+ // ---- RFC 1035 §2.3.4 length caps ----
68
+
69
+ var LIMIT_LABEL_OCTETS = 63; // allow:raw-byte-literal — RFC 1035 §2.3.4
70
+ var LIMIT_DOMAIN_OCTETS = 253; // allow:raw-byte-literal — RFC 1035 §2.3.4 (255 wire minus length prefixes)
71
+
72
+ // ---- Static patterns (built from explicit codepoint tables) ----
73
+
74
+ // LDH label — letters / digits / hyphens, with leading-and-trailing
75
+ // hyphen rejection enforced separately. Length checked separately.
76
+ var LDH_LABEL_RE = /^[A-Za-z0-9](?:[A-Za-z0-9-]*[A-Za-z0-9])?$/;
77
+
78
+ // Service-prefix label per RFC 8552 — `_dmarc`, `_acme-challenge`, …
79
+ var SERVICE_LABEL_RE = /^_[A-Za-z0-9](?:[A-Za-z0-9-]*[A-Za-z0-9])?$/;
80
+
81
+ // Punycode A-label prefix.
82
+ var PUNYCODE_LABEL_RE = /^xn--/i;
83
+
84
+ // Bare `xn--` with no payload after — malformed A-label.
85
+ var BARE_XN_RE = /^xn--$/i;
86
+
87
+ // Wildcard label — `*` alone in any label position.
88
+ var WILDCARD_LABEL_RE = /^\*$/;
89
+
90
+ // IPv4 decimal-dotted form.
91
+ var IPV4_DOTTED_RE = /^(?:25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])(?:\.(?:25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])){3}$/;
92
+
93
+ // Looser IPv4 detection — every dot-segment is a numeric form
94
+ // (decimal, octal with leading 0, or hex with 0x prefix), or the whole
95
+ // input is a long-decimal / long-hex (no dots). Catches the parser-
96
+ // permissive forms `0177.0.0.1` (octal), `0xC0.0xA8.0x01.0x01` (hex),
97
+ // `3232235777` (long-decimal), `0xC0A80101` (long-hex).
98
+ //
99
+ // Detection requires at least one digit AND that every dot-segment is a
100
+ // number, so labels like `a.b` (purely alphabetic) don't false-positive
101
+ // as IPv4 even though their codepoints overlap the hex alphabet.
102
+ var IPV4_NUMERIC_SEGMENT_RE = /^(?:0[xX][0-9a-fA-F]+|[0-9]+)$/;
103
+ function _looksLikeIpv4Permissive(s) {
104
+ if (!/[0-9]/.test(s)) return false;
105
+ if (IPV4_NUMERIC_SEGMENT_RE.test(s)) {
106
+ // Long-decimal / long-hex without dots, e.g. `3232235777`.
107
+ return s.length > 0 && !/^[0-9]+$/.test(s) ? true :
108
+ // Pure long-decimal — at least 8 digits to count as IPv4
109
+ // representation, otherwise it's a port-shaped number.
110
+ s.length >= 8; // allow:raw-byte-literal — minimum digits to recognize long-decimal IPv4
111
+ }
112
+ if (s.indexOf(".") === -1) return false;
113
+ var parts = s.split(".");
114
+ if (parts.length !== 4) return false;
115
+ for (var i = 0; i < parts.length; i += 1) {
116
+ if (!IPV4_NUMERIC_SEGMENT_RE.test(parts[i])) return false;
117
+ }
118
+ return true;
119
+ }
120
+
121
+ // IPv6 bracket-literal.
122
+ var IPV6_BRACKET_RE = /^\[[0-9a-fA-F:.]+\]$/;
123
+
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
+ }
169
+
170
+ // RFC 6761 special-use domains + IETF reserved. Lowercase, no trailing
171
+ // dot. Match by suffix — `_acme-challenge.app.localhost` → `.localhost`.
172
+ //
173
+ // Excluded deliberately: `example.com` / `example.net` / `example.org`.
174
+ // Those are documentation-reserved but legitimately appear in test
175
+ // fixtures and SSO redirect-URI examples; refusing them at strict
176
+ // trips operators on benign inputs. A future `documentation-reserved`
177
+ // posture can flag them as warn-only when operators ask.
178
+ var SPECIAL_USE_DOMAINS = Object.freeze([
179
+ "localhost",
180
+ "local", // RFC 6762 mDNS
181
+ "invalid",
182
+ "test",
183
+ "onion", // RFC 7686
184
+ "alt", // RFC 9476
185
+ "home.arpa", // RFC 8375
186
+ "internal", // ICANN reserved 2024
187
+ ]);
188
+
189
+ function _matchesSpecialUse(name) {
190
+ var lower = name.toLowerCase().replace(/\.$/, "");
191
+ for (var i = 0; i < SPECIAL_USE_DOMAINS.length; i += 1) {
192
+ var su = SPECIAL_USE_DOMAINS[i];
193
+ if (lower === su || lower.endsWith("." + su)) return su;
194
+ }
195
+ return null;
196
+ }
197
+
198
+ // Shannon entropy in bits per character over a-z0-9 alphabet, used as
199
+ // a DGA heuristic. Returns 0 for trivial inputs.
200
+ function _shannonEntropy(s) {
201
+ if (!s || s.length < 2) return 0;
202
+ var counts = Object.create(null);
203
+ for (var i = 0; i < s.length; i += 1) {
204
+ var c = s.charAt(i).toLowerCase();
205
+ counts[c] = (counts[c] || 0) + 1;
206
+ }
207
+ var len = s.length;
208
+ var h = 0;
209
+ var keys = Object.keys(counts);
210
+ for (var k = 0; k < keys.length; k += 1) {
211
+ var p = counts[keys[k]] / len;
212
+ h -= p * Math.log2(p);
213
+ }
214
+ return h;
215
+ }
216
+
217
+ // ---- Profile presets ----
218
+
219
+ var PROFILES = Object.freeze({
220
+ "strict": {
221
+ bidiPolicy: "reject",
222
+ controlPolicy: "reject",
223
+ nullBytePolicy: "reject",
224
+ zeroWidthPolicy: "reject",
225
+ ldhPolicy: "reject",
226
+ underscorePolicy: "reject", // strict refuses service labels too
227
+ punycodePolicy: "reject",
228
+ mixedScriptPolicy: "reject",
229
+ specialUsePolicy: "reject",
230
+ ipLiteralPolicy: "reject",
231
+ wildcardPolicy: "reject",
232
+ singleLabelPolicy: "reject",
233
+ trailingDotPolicy: "normalize",
234
+ dgaPolicy: "reject",
235
+ allowedScripts: ["latin"],
236
+ dgaEntropyThreshold: 3.8, // allow:raw-byte-literal — Shannon entropy bits/char threshold (DGA heuristic)
237
+ dgaMinLabelLen: 12, // allow:raw-byte-literal — DGA heuristic floor
238
+ maxLabelOctets: LIMIT_LABEL_OCTETS,
239
+ maxDomainOctets: LIMIT_DOMAIN_OCTETS,
240
+ maxBytes: C.BYTES.bytes(2048),
241
+ maxRuntimeMs: C.TIME.seconds(2),
242
+ },
243
+ "balanced": {
244
+ bidiPolicy: "reject",
245
+ controlPolicy: "reject",
246
+ nullBytePolicy: "reject",
247
+ zeroWidthPolicy: "reject",
248
+ ldhPolicy: "reject",
249
+ underscorePolicy: "reject",
250
+ punycodePolicy: "audit",
251
+ mixedScriptPolicy: "reject",
252
+ specialUsePolicy: "reject",
253
+ ipLiteralPolicy: "reject",
254
+ wildcardPolicy: "reject",
255
+ singleLabelPolicy: "reject",
256
+ trailingDotPolicy: "normalize",
257
+ dgaPolicy: "audit",
258
+ allowedScripts: ["latin", "cyrillic", "greek", "han", "hiragana",
259
+ "katakana", "hangul"],
260
+ dgaEntropyThreshold: 3.8, // allow:raw-byte-literal — Shannon entropy bits/char threshold (DGA heuristic)
261
+ dgaMinLabelLen: 12, // allow:raw-byte-literal — DGA heuristic floor
262
+ maxLabelOctets: LIMIT_LABEL_OCTETS,
263
+ maxDomainOctets: LIMIT_DOMAIN_OCTETS,
264
+ maxBytes: C.BYTES.bytes(2048),
265
+ maxRuntimeMs: C.TIME.seconds(2),
266
+ },
267
+ "permissive": {
268
+ bidiPolicy: "reject", // BIDI refused at every profile — universal forgery
269
+ controlPolicy: "reject", // control bytes refused at every profile
270
+ nullBytePolicy: "reject", // null refused at every profile
271
+ zeroWidthPolicy: "reject", // zero-width refused at every profile — invisible label-segmentation
272
+ ldhPolicy: "audit",
273
+ underscorePolicy: "allow", // service labels permitted in permissive
274
+ punycodePolicy: "allow",
275
+ mixedScriptPolicy: "audit",
276
+ specialUsePolicy: "audit",
277
+ ipLiteralPolicy: "allow",
278
+ wildcardPolicy: "reject", // wildcard refused at every profile — never user-input
279
+ singleLabelPolicy: "audit",
280
+ trailingDotPolicy: "normalize",
281
+ dgaPolicy: "allow",
282
+ allowedScripts: null,
283
+ dgaEntropyThreshold: 3.8, // allow:raw-byte-literal — Shannon entropy bits/char threshold (DGA heuristic)
284
+ dgaMinLabelLen: 12, // allow:raw-byte-literal — DGA heuristic floor
285
+ maxLabelOctets: LIMIT_LABEL_OCTETS,
286
+ maxDomainOctets: LIMIT_DOMAIN_OCTETS,
287
+ maxBytes: C.BYTES.bytes(2048),
288
+ maxRuntimeMs: C.TIME.seconds(2),
289
+ },
290
+ });
291
+
292
+ var DEFAULTS = Object.freeze(Object.assign({}, PROFILES["strict"], {
293
+ mode: "enforce",
294
+ }));
295
+
296
+ var COMPLIANCE_POSTURES = Object.freeze({
297
+ "hipaa": Object.assign({}, PROFILES["strict"], {
298
+ forensicSnippetBytes: C.BYTES.bytes(256),
299
+ }),
300
+ "pci-dss": Object.assign({}, PROFILES["strict"], {
301
+ forensicSnippetBytes: C.BYTES.bytes(256),
302
+ }),
303
+ "gdpr": Object.assign({}, PROFILES["balanced"], {
304
+ forensicSnippetBytes: C.BYTES.bytes(128),
305
+ }),
306
+ "soc2": Object.assign({}, PROFILES["strict"], {
307
+ forensicSnippetBytes: C.BYTES.bytes(512),
308
+ }),
309
+ });
310
+
311
+ function _resolveOpts(opts) {
312
+ return gateContract.resolveProfileAndPosture(opts, {
313
+ profiles: PROFILES,
314
+ compliancePostures: COMPLIANCE_POSTURES,
315
+ defaults: DEFAULTS,
316
+ errorClass: GuardDomainError,
317
+ errCodePrefix: "domain",
318
+ });
319
+ }
320
+
321
+ // ---- Detection ----
322
+
323
+ function _detectIssues(input, opts) {
324
+ var issues = [];
325
+ if (typeof input !== "string") {
326
+ return [{ kind: "bad-input", severity: "high",
327
+ ruleId: "domain.bad-input",
328
+ snippet: "domain is not a string" }];
329
+ }
330
+
331
+ // Total-length cap (UTF-8 byte count, not codepoint count, per RFC 1035).
332
+ var byteLen = Buffer.byteLength(input, "utf8");
333
+ if (byteLen > opts.maxDomainOctets) {
334
+ issues.push({
335
+ kind: "domain-cap", severity: "high",
336
+ ruleId: "domain.domain-cap",
337
+ snippet: "domain " + byteLen + " octets exceeds " +
338
+ opts.maxDomainOctets + " (RFC 1035 §2.3.4)",
339
+ });
340
+ return issues; // size cap is structural — abort further checks
341
+ }
342
+
343
+ // Codepoint-class threats (BIDI / control / null / zero-width). These
344
+ // are universal-refuse — running them first lets the more specific
345
+ // structural checks operate on a sanitized-or-refused input.
346
+ var charThreats = codepointClass.detectCharThreats(input, opts, "domain");
347
+ for (var ci = 0; ci < charThreats.length; ci += 1) issues.push(charThreats[ci]);
348
+
349
+ // Trailing-dot — FQDN distinguisher. Normalize for downstream checks
350
+ // but record as audit if operator wants to know.
351
+ var hadTrailingDot = input.charAt(input.length - 1) === ".";
352
+ var name = hadTrailingDot ? input.slice(0, -1) : input;
353
+
354
+ // Empty string after trim.
355
+ if (name.length === 0) {
356
+ issues.push({
357
+ kind: "empty", severity: "high",
358
+ ruleId: "domain.empty",
359
+ snippet: "domain is empty",
360
+ });
361
+ return issues;
362
+ }
363
+
364
+ // Bracketed IPv6 literal.
365
+ if (IPV6_BRACKET_RE.test(name)) {
366
+ if (opts.ipLiteralPolicy !== "allow") {
367
+ issues.push({
368
+ kind: "ipv6-literal",
369
+ severity: opts.ipLiteralPolicy === "reject" ? "high" : "warn",
370
+ ruleId: "domain.ipv6-literal",
371
+ snippet: "input is an IPv6 bracket literal — bypasses DNS-name " +
372
+ "validation; pass through opts.allowIp if intended",
373
+ });
374
+ }
375
+ return issues;
376
+ }
377
+
378
+ // IPv4 detection — strict dotted-decimal AND loose (octal/hex/long).
379
+ if (IPV4_DOTTED_RE.test(name) || _looksLikeIpv4Permissive(name)) {
380
+ if (opts.ipLiteralPolicy !== "allow") {
381
+ issues.push({
382
+ kind: "ipv4-as-domain",
383
+ severity: opts.ipLiteralPolicy === "reject" ? "high" : "warn",
384
+ ruleId: "domain.ipv4-as-domain",
385
+ snippet: "input parses as IPv4 (CVE-2021-22931 class) — " +
386
+ "DNS-rebinding risk against allowlist matchers",
387
+ });
388
+ // Don't continue to label parsing — IPv4-shaped strings would
389
+ // collide with single-label / LDH errors and confuse the operator.
390
+ return issues;
391
+ }
392
+ }
393
+
394
+ // RFC 6761 special-use suffix.
395
+ var su = _matchesSpecialUse(name);
396
+ if (su && opts.specialUsePolicy !== "allow") {
397
+ issues.push({
398
+ kind: "special-use",
399
+ severity: opts.specialUsePolicy === "reject" ? "high" : "warn",
400
+ ruleId: "domain.special-use",
401
+ snippet: "domain matches RFC 6761 / IETF reserved suffix `." + su + "` " +
402
+ "— would route to loopback / mDNS / Tor / LAN",
403
+ });
404
+ }
405
+
406
+ // Label split + per-label structural checks.
407
+ var labels = name.split(".");
408
+
409
+ // Single-label / TLD-only.
410
+ if (labels.length < 2) {
411
+ if (opts.singleLabelPolicy !== "allow") {
412
+ issues.push({
413
+ kind: "single-label",
414
+ severity: opts.singleLabelPolicy === "reject" ? "high" : "warn",
415
+ ruleId: "domain.single-label",
416
+ snippet: "single-label / TLD-only domain — risks search-domain " +
417
+ "suffixing on misconfigured stub resolvers",
418
+ });
419
+ }
420
+ }
421
+
422
+ for (var li = 0; li < labels.length; li += 1) {
423
+ var label = labels[li];
424
+
425
+ // Empty label (e.g. `foo..bar` or leading `.foo`).
426
+ if (label.length === 0) {
427
+ issues.push({
428
+ kind: "empty-label", severity: "high",
429
+ ruleId: "domain.empty-label",
430
+ snippet: "label " + (li + 1) + " is empty (consecutive or " +
431
+ "leading dots)",
432
+ });
433
+ continue;
434
+ }
435
+
436
+ var labelBytes = Buffer.byteLength(label, "utf8");
437
+ if (labelBytes > opts.maxLabelOctets) {
438
+ issues.push({
439
+ kind: "label-cap", severity: "high",
440
+ ruleId: "domain.label-cap",
441
+ snippet: "label " + (li + 1) + " is " + labelBytes +
442
+ " octets, exceeds " + opts.maxLabelOctets +
443
+ " (RFC 1035 §2.3.4)",
444
+ });
445
+ continue; // label-cap masks downstream rule failures
446
+ }
447
+
448
+ // Wildcard `*`.
449
+ if (WILDCARD_LABEL_RE.test(label)) { // allow:regex-no-length-cap — label bounded by maxLabelOctets above
450
+ if (opts.wildcardPolicy !== "allow") {
451
+ issues.push({
452
+ kind: "wildcard", severity: "high",
453
+ ruleId: "domain.wildcard",
454
+ snippet: "wildcard label `*` — valid in TLS SAN / DNS RR but " +
455
+ "never in a user-input identifier",
456
+ });
457
+ }
458
+ continue;
459
+ }
460
+
461
+ // Service-prefix label (RFC 8552). Underscore allowed only if
462
+ // operator opts in.
463
+ if (label.charAt(0) === "_") {
464
+ if (SERVICE_LABEL_RE.test(label)) { // allow:regex-no-length-cap — label bounded by maxLabelOctets above
465
+ if (opts.underscorePolicy !== "allow") {
466
+ issues.push({
467
+ kind: "underscore-label",
468
+ severity: opts.underscorePolicy === "reject" ? "high" : "warn",
469
+ ruleId: "domain.underscore-label",
470
+ snippet: "label " + (li + 1) + " starts with `_` (RFC 8552 " +
471
+ "service label) — never valid as a hostname",
472
+ });
473
+ }
474
+ } else {
475
+ issues.push({
476
+ kind: "underscore-malformed", severity: "high",
477
+ ruleId: "domain.underscore-malformed",
478
+ snippet: "label " + (li + 1) + " starts with `_` but doesn't " +
479
+ "match the service-label grammar",
480
+ });
481
+ }
482
+ continue;
483
+ }
484
+
485
+ // Punycode A-label.
486
+ if (PUNYCODE_LABEL_RE.test(label)) { // allow:regex-no-length-cap — label bounded by maxLabelOctets above
487
+ if (BARE_XN_RE.test(label)) { // allow:regex-no-length-cap — label bounded by maxLabelOctets above
488
+ issues.push({
489
+ kind: "punycode-bare", severity: "high",
490
+ ruleId: "domain.punycode-bare",
491
+ snippet: "label " + (li + 1) + " is bare `xn--` with no " +
492
+ "Punycode payload",
493
+ });
494
+ continue;
495
+ }
496
+ if (opts.punycodePolicy !== "allow") {
497
+ issues.push({
498
+ kind: "punycode-label",
499
+ severity: opts.punycodePolicy === "reject" ? "high" : "warn",
500
+ ruleId: "domain.punycode-label",
501
+ snippet: "label " + (li + 1) + " is an IDN A-label (`xn--`) — " +
502
+ "homograph-spoofing class without round-trip validation",
503
+ });
504
+ }
505
+ // ASCII LDH check still applies.
506
+ if (!LDH_LABEL_RE.test(label) && opts.ldhPolicy !== "allow") { // allow:regex-no-length-cap — label bounded by maxLabelOctets above
507
+ issues.push({
508
+ kind: "ldh-violation", severity: "high",
509
+ ruleId: "domain.ldh-violation",
510
+ snippet: "label " + (li + 1) + " (Punycode form) violates LDH " +
511
+ "rule (RFC 952 / 1123 §2.1)",
512
+ });
513
+ }
514
+ continue;
515
+ }
516
+
517
+ // ASCII LDH or Unicode label.
518
+ var allAscii = true;
519
+ for (var ai = 0; ai < label.length; ai += 1) {
520
+ if (label.charCodeAt(ai) > 0x7F) { allAscii = false; break; } // allow:raw-byte-literal — ASCII boundary codepoint
521
+ }
522
+
523
+ if (allAscii) {
524
+ if (!LDH_LABEL_RE.test(label) && opts.ldhPolicy !== "allow") { // allow:regex-no-length-cap — label bounded by maxLabelOctets above
525
+ issues.push({
526
+ kind: "ldh-violation",
527
+ severity: opts.ldhPolicy === "reject" ? "high" : "warn",
528
+ ruleId: "domain.ldh-violation",
529
+ snippet: "label " + (li + 1) + " " + JSON.stringify(label) +
530
+ " violates LDH rule (RFC 952 / 1123 §2.1)",
531
+ });
532
+ }
533
+ // Position-3-4 double-hyphen check excluding the `xn--` prefix.
534
+ if (label.length >= 4 && label.charAt(2) === "-" &&
535
+ label.charAt(3) === "-" && !PUNYCODE_LABEL_RE.test(label)) {
536
+ issues.push({
537
+ kind: "double-hyphen", severity: "warn",
538
+ ruleId: "domain.double-hyphen",
539
+ snippet: "label " + (li + 1) + " has `--` at positions 3-4 " +
540
+ "without the `xn--` IDN prefix",
541
+ });
542
+ }
543
+ } else {
544
+ // Unicode label — flag mixed-script confusables and strict-LDH
545
+ // operators that didn't opt into IDN.
546
+ if (opts.punycodePolicy !== "allow") {
547
+ // Operator wants Punycode-only; reject raw Unicode labels.
548
+ issues.push({
549
+ kind: "raw-unicode-label",
550
+ severity: opts.punycodePolicy === "reject" ? "high" : "warn",
551
+ ruleId: "domain.raw-unicode-label",
552
+ snippet: "label " + (li + 1) + " contains raw Unicode " +
553
+ "(non-ASCII) — IDN labels must be Punycode-encoded " +
554
+ "(`xn--…`) for transport-safe comparison",
555
+ });
556
+ }
557
+ var mixed = _detectMixedScripts(label, opts.allowedScripts);
558
+ if (mixed && opts.mixedScriptPolicy !== "allow") {
559
+ issues.push({
560
+ kind: "mixed-script",
561
+ severity: opts.mixedScriptPolicy === "reject" ? "critical" : "high",
562
+ ruleId: "domain.mixed-script",
563
+ snippet: "label " + (li + 1) + " mixes scripts (" +
564
+ mixed.join(", ") + ") — IDN homograph spoofing class",
565
+ });
566
+ }
567
+ }
568
+
569
+ // DGA entropy heuristic — high-entropy long single label is C2-shape.
570
+ if (label.length >= opts.dgaMinLabelLen && opts.dgaPolicy !== "allow") {
571
+ var h = _shannonEntropy(label);
572
+ if (h >= opts.dgaEntropyThreshold) {
573
+ issues.push({
574
+ kind: "dga-entropy",
575
+ severity: opts.dgaPolicy === "reject" ? "high" : "warn",
576
+ ruleId: "domain.dga-entropy",
577
+ snippet: "label " + (li + 1) + " has Shannon entropy " +
578
+ h.toFixed(2) + " bits/char (>= " +
579
+ opts.dgaEntropyThreshold + ") — C2 / DGA shape",
580
+ });
581
+ }
582
+ }
583
+ }
584
+
585
+ // Trailing-dot audit signal (after structural checks, before return).
586
+ if (hadTrailingDot && opts.trailingDotPolicy === "audit") {
587
+ issues.push({
588
+ kind: "trailing-dot", severity: "warn",
589
+ ruleId: "domain.trailing-dot",
590
+ snippet: "input had trailing dot (FQDN-marker) — normalize/strip " +
591
+ "before allowlist comparison",
592
+ });
593
+ }
594
+
595
+ return issues;
596
+ }
597
+
598
+ function validate(input, opts) {
599
+ opts = _resolveOpts(opts);
600
+ numericBounds.requireAllPositiveFiniteIntIfPresent(opts,
601
+ ["maxLabelOctets", "maxDomainOctets", "maxBytes", "dgaMinLabelLen"],
602
+ "guardDomain.validate", GuardDomainError, "domain.bad-opt");
603
+ if (typeof input !== "string") {
604
+ return {
605
+ ok: false,
606
+ issues: [{ kind: "bad-input", severity: "high",
607
+ ruleId: "domain.bad-input",
608
+ snippet: "domain is not a string" }],
609
+ };
610
+ }
611
+ return gateContract.aggregateIssues(_detectIssues(input, opts));
612
+ }
613
+
614
+ function sanitize(input, opts) {
615
+ opts = _resolveOpts(opts);
616
+ if (typeof input !== "string") {
617
+ throw _err("domain.bad-input", "sanitize requires string input");
618
+ }
619
+ // Critical refuses can't be repaired.
620
+ var issues = _detectIssues(input, opts);
621
+ for (var i = 0; i < issues.length; i += 1) {
622
+ if (issues[i].severity === "critical" || issues[i].severity === "high") {
623
+ throw _err(issues[i].ruleId || "domain.refused",
624
+ "guardDomain.sanitize: " + issues[i].snippet);
625
+ }
626
+ }
627
+ // Safe transforms: lowercase ASCII, strip trailing dot.
628
+ var out = input.toLowerCase();
629
+ if (out.charAt(out.length - 1) === ".") out = out.slice(0, -1);
630
+ return out;
631
+ }
632
+
633
+ function gate(opts) {
634
+ opts = _resolveOpts(opts);
635
+ return gateContract.buildGuardGate(
636
+ opts.name || "guardDomain:" + (opts.profile || "default"),
637
+ opts,
638
+ async function (ctx) {
639
+ // Identifier-shape ctx — operator passes via ctx.identifier or
640
+ // ctx.domain.
641
+ var identifier = ctx && (ctx.identifier || ctx.domain || "");
642
+ if (!identifier) return { ok: true, action: "serve" };
643
+ var rv = validate(identifier, opts);
644
+ if (rv.issues.length === 0) return { ok: true, action: "serve" };
645
+ var hasCritical = rv.issues.some(function (i) {
646
+ return i.severity === "critical";
647
+ });
648
+ var hasHigh = rv.issues.some(function (i) {
649
+ return i.severity === "high";
650
+ });
651
+ if (!hasCritical && !hasHigh) {
652
+ return { ok: true, action: "audit-only", issues: rv.issues };
653
+ }
654
+ return { ok: false, action: "refuse", issues: rv.issues };
655
+ });
656
+ }
657
+
658
+ var buildProfile = gateContract.makeProfileBuilder(PROFILES);
659
+
660
+ function compliancePosture(name) {
661
+ return gateContract.lookupCompliancePosture(name, COMPLIANCE_POSTURES,
662
+ _err, "domain");
663
+ }
664
+
665
+ var _domainRulePacks = gateContract.makeRulePackLoader(GuardDomainError, "domain");
666
+ var loadRulePack = _domainRulePacks.load;
667
+
668
+ module.exports = {
669
+ // ---- guard-* family registry exports ----
670
+ NAME: "domain",
671
+ KIND: "identifier",
672
+ INTEGRATION_FIXTURES: Object.freeze({
673
+ kind: "identifier",
674
+ benignBytes: Buffer.from("example.com", "utf8"),
675
+ // Hostile: dotted-decimal IPv4 (CVE-2021-22931 class) — every
676
+ // profile refuses (allowlist-bypass via DNS rebinding).
677
+ hostileBytes: Buffer.from("192.168.1.1", "utf8"),
678
+ benignIdentifier: "example.com",
679
+ hostileIdentifier: "192.168.1.1",
680
+ }),
681
+ // ---- primitive surface ----
682
+ validate: validate,
683
+ sanitize: sanitize,
684
+ gate: gate,
685
+ buildProfile: buildProfile,
686
+ compliancePosture: compliancePosture,
687
+ loadRulePack: loadRulePack,
688
+ PROFILES: PROFILES,
689
+ DEFAULTS: DEFAULTS,
690
+ COMPLIANCE_POSTURES: COMPLIANCE_POSTURES,
691
+ GuardDomainError: GuardDomainError,
692
+ };
@@ -0,0 +1,389 @@
1
+ "use strict";
2
+ /**
3
+ * guard-uuid — UUID identifier-safety primitive (b.guardUuid).
4
+ *
5
+ * Validates user-supplied UUID strings per RFC 9562 (May 2024,
6
+ * obsoletes RFC 4122). KIND="identifier" — consumes ctx.identifier
7
+ * (or ctx.uuid).
8
+ *
9
+ * Threat catalog:
10
+ * - Wrong length / shape — UUIDs are 36 chars with hyphens, 32 hex
11
+ * without, or 38 with Microsoft GUID braces; anything else is
12
+ * malformed and a downstream parser may diverge.
13
+ * - Wrong character class — non-hex characters anywhere.
14
+ * - Invalid version field (RFC 9562 §4.2) — versions 1-8 are
15
+ * defined; 0 and 9-F are reserved/unassigned and indicate
16
+ * hand-rolled or attacker-shaped IDs.
17
+ * - Variant bits (RFC 9562 §4.1) — only 10xx (RFC 4122/9562
18
+ * variant) is the canonical UUID variant; other variants
19
+ * (NCS-reserved 0xxx, Microsoft-reserved 110x, future-reserved
20
+ * 111x) often indicate non-UUID payloads coerced into the slot.
21
+ * - Nil UUID (RFC 9562 §5.9 — all zeros) — usually represents
22
+ * "no UUID set"; passing through can mask a missing-key bug.
23
+ * - Max UUID (RFC 9562 §5.10 — all FF) — sentinel value with the
24
+ * same semantic risk as nil.
25
+ * - urn:uuid: prefix (RFC 4122 §3) — when not requested by the
26
+ * caller, can disguise a UUID inside a URN-shape parser.
27
+ * - Microsoft GUID braces `{...}` — disguise a UUID inside a
28
+ * COM-style serialization parser.
29
+ * - BIDI / zero-width / control / null-byte — universal-refuse.
30
+ *
31
+ * var rv = b.guardUuid.validate("550e8400-e29b-41d4-a716-446655440000",
32
+ * { profile: "strict" });
33
+ * var safe = b.guardUuid.sanitize("urn:uuid:550E8400-...",
34
+ * { profile: "balanced" });
35
+ * var g = b.guardUuid.gate({ profile: "strict" });
36
+ */
37
+
38
+ var codepointClass = require("./codepoint-class");
39
+ var lazyRequire = require("./lazy-require");
40
+ var gateContract = require("./gate-contract");
41
+ var C = require("./constants");
42
+ var numericBounds = require("./numeric-bounds");
43
+ var { GuardUuidError } = require("./framework-error");
44
+
45
+ var observability = lazyRequire(function () { return require("./observability"); });
46
+ void observability;
47
+
48
+ var _err = GuardUuidError.factory;
49
+
50
+ // ---- Static patterns ----
51
+
52
+ // Canonical RFC 9562 form: 8-4-4-4-12 hex chars with dashes.
53
+ var UUID_HYPHENATED_RE = /^([0-9a-f]{8})-([0-9a-f]{4})-([0-9a-f]{4})-([0-9a-f]{4})-([0-9a-f]{12})$/i;
54
+
55
+ // Hyphenless 32-hex form (some serializers strip the hyphens).
56
+ var UUID_HYPHENLESS_RE = /^[0-9a-f]{32}$/i;
57
+
58
+ // Microsoft GUID-with-braces form.
59
+ var UUID_BRACED_RE = /^\{([0-9a-f]{8})-([0-9a-f]{4})-([0-9a-f]{4})-([0-9a-f]{4})-([0-9a-f]{12})\}$/i;
60
+
61
+ // urn:uuid: prefix form.
62
+ var UUID_URN_RE = /^urn:uuid:([0-9a-f]{8})-([0-9a-f]{4})-([0-9a-f]{4})-([0-9a-f]{4})-([0-9a-f]{12})$/i;
63
+
64
+ var NIL_HEX = "00000000000000000000000000000000";
65
+ var MAX_HEX = "ffffffffffffffffffffffffffffffff";
66
+
67
+ // ---- Profile presets ----
68
+
69
+ var PROFILES = Object.freeze({
70
+ "strict": {
71
+ bidiPolicy: "reject",
72
+ controlPolicy: "reject",
73
+ nullBytePolicy: "reject",
74
+ zeroWidthPolicy: "reject",
75
+ formatPolicy: "hyphenated-only", // hyphenated | hyphenless | braced | urn | hyphenated-only | any
76
+ versionPolicy: "reject-unassigned", // reject-unassigned | audit | allow
77
+ variantPolicy: "reject-non-rfc", // reject-non-rfc | audit | allow
78
+ nilPolicy: "reject",
79
+ maxPolicy: "reject",
80
+ urnPolicy: "reject",
81
+ bracedPolicy: "reject",
82
+ allowedVersions: [1, 2, 3, 4, 5, 6, 7, 8], // allow:raw-byte-literal — UUID version digits
83
+ maxBytes: C.BYTES.bytes(64),
84
+ maxRuntimeMs: C.TIME.seconds(2),
85
+ },
86
+ "balanced": {
87
+ bidiPolicy: "reject",
88
+ controlPolicy: "reject",
89
+ nullBytePolicy: "reject",
90
+ zeroWidthPolicy: "reject",
91
+ formatPolicy: "any",
92
+ versionPolicy: "reject-unassigned",
93
+ variantPolicy: "audit",
94
+ nilPolicy: "audit",
95
+ maxPolicy: "audit",
96
+ urnPolicy: "audit",
97
+ bracedPolicy: "audit",
98
+ allowedVersions: [1, 2, 3, 4, 5, 6, 7, 8], // allow:raw-byte-literal — UUID version digits
99
+ maxBytes: C.BYTES.bytes(64),
100
+ maxRuntimeMs: C.TIME.seconds(2),
101
+ },
102
+ "permissive": {
103
+ bidiPolicy: "reject", // BIDI refused at every profile
104
+ controlPolicy: "reject", // controls refused at every profile
105
+ nullBytePolicy: "reject", // null refused at every profile
106
+ zeroWidthPolicy: "reject", // zero-width refused at every profile
107
+ formatPolicy: "any",
108
+ versionPolicy: "audit",
109
+ variantPolicy: "allow",
110
+ nilPolicy: "allow",
111
+ maxPolicy: "allow",
112
+ urnPolicy: "allow",
113
+ bracedPolicy: "allow",
114
+ allowedVersions: null, // any version
115
+ maxBytes: C.BYTES.bytes(64),
116
+ maxRuntimeMs: C.TIME.seconds(2),
117
+ },
118
+ });
119
+
120
+ var DEFAULTS = Object.freeze(Object.assign({}, PROFILES["strict"], {
121
+ mode: "enforce",
122
+ }));
123
+
124
+ var COMPLIANCE_POSTURES = Object.freeze({
125
+ "hipaa": Object.assign({}, PROFILES["strict"], {
126
+ forensicSnippetBytes: C.BYTES.bytes(128),
127
+ }),
128
+ "pci-dss": Object.assign({}, PROFILES["strict"], {
129
+ forensicSnippetBytes: C.BYTES.bytes(128),
130
+ }),
131
+ "gdpr": Object.assign({}, PROFILES["balanced"], {
132
+ forensicSnippetBytes: C.BYTES.bytes(64),
133
+ }),
134
+ "soc2": Object.assign({}, PROFILES["strict"], {
135
+ forensicSnippetBytes: C.BYTES.bytes(256),
136
+ }),
137
+ });
138
+
139
+ function _resolveOpts(opts) {
140
+ return gateContract.resolveProfileAndPosture(opts, {
141
+ profiles: PROFILES,
142
+ compliancePostures: COMPLIANCE_POSTURES,
143
+ defaults: DEFAULTS,
144
+ errorClass: GuardUuidError,
145
+ errCodePrefix: "uuid",
146
+ });
147
+ }
148
+
149
+ function _classifyForm(input) {
150
+ if (UUID_URN_RE.test(input)) return "urn"; // allow:regex-no-length-cap — input bounded by maxBytes
151
+ if (UUID_BRACED_RE.test(input)) return "braced"; // allow:regex-no-length-cap — input bounded by maxBytes
152
+ if (UUID_HYPHENATED_RE.test(input)) return "hyphenated"; // allow:regex-no-length-cap — input bounded by maxBytes
153
+ if (UUID_HYPHENLESS_RE.test(input)) return "hyphenless"; // allow:regex-no-length-cap — input bounded by maxBytes
154
+ return null;
155
+ }
156
+
157
+ function _toCanonicalHex(input, form) {
158
+ // Strips dashes / braces / urn prefix, returns 32-char lowercase hex.
159
+ var s = input.toLowerCase();
160
+ if (form === "urn") s = s.slice("urn:uuid:".length); // allow:raw-byte-literal — string-length offset
161
+ if (form === "braced") s = s.slice(1, -1); // allow:raw-byte-literal — string-length offset
162
+ return s.replace(/-/g, "");
163
+ }
164
+
165
+ function _detectIssues(input, opts) {
166
+ var issues = [];
167
+ if (typeof input !== "string") {
168
+ return [{ kind: "bad-input", severity: "high",
169
+ ruleId: "uuid.bad-input",
170
+ snippet: "uuid is not a string" }];
171
+ }
172
+ if (input.length === 0) {
173
+ return [{ kind: "empty", severity: "high",
174
+ ruleId: "uuid.empty",
175
+ snippet: "uuid is empty" }];
176
+ }
177
+ if (Buffer.byteLength(input, "utf8") > opts.maxBytes) {
178
+ return [{ kind: "uuid-cap", severity: "high",
179
+ ruleId: "uuid.uuid-cap",
180
+ snippet: "uuid input exceeds maxBytes " + opts.maxBytes }];
181
+ }
182
+
183
+ // Codepoint-class threats (universal refuse — runs first).
184
+ var charThreats = codepointClass.detectCharThreats(input, opts, "uuid");
185
+ for (var ci = 0; ci < charThreats.length; ci += 1) issues.push(charThreats[ci]);
186
+
187
+ // Format classification.
188
+ var form = _classifyForm(input);
189
+ if (form === null) {
190
+ issues.push({
191
+ kind: "uuid-shape", severity: "high",
192
+ ruleId: "uuid.uuid-shape",
193
+ snippet: "input does not match any RFC 9562 UUID form " +
194
+ "(hyphenated / hyphenless / braced / urn:uuid:)",
195
+ });
196
+ return issues;
197
+ }
198
+
199
+ // Format-policy enforcement.
200
+ var formatPolicy = opts.formatPolicy;
201
+ var formAllowed = (
202
+ formatPolicy === "any" ||
203
+ formatPolicy === form ||
204
+ (formatPolicy === "hyphenated-only" && form === "hyphenated")
205
+ );
206
+ if (!formAllowed) {
207
+ issues.push({
208
+ kind: "uuid-form-disallowed",
209
+ severity: "high",
210
+ ruleId: "uuid.uuid-form-disallowed",
211
+ snippet: "uuid form `" + form + "` not permitted by formatPolicy `" +
212
+ formatPolicy + "`",
213
+ });
214
+ }
215
+ if (form === "urn" && opts.urnPolicy !== "allow") {
216
+ issues.push({
217
+ kind: "urn-prefix",
218
+ severity: opts.urnPolicy === "reject" ? "high" : "warn",
219
+ ruleId: "uuid.urn-prefix",
220
+ snippet: "uuid carries `urn:uuid:` prefix — would be processed " +
221
+ "by URN-shape parsers downstream",
222
+ });
223
+ }
224
+ if (form === "braced" && opts.bracedPolicy !== "allow") {
225
+ issues.push({
226
+ kind: "braced",
227
+ severity: opts.bracedPolicy === "reject" ? "high" : "warn",
228
+ ruleId: "uuid.braced",
229
+ snippet: "uuid uses Microsoft GUID braces `{...}` — non-canonical",
230
+ });
231
+ }
232
+
233
+ var hex = _toCanonicalHex(input, form);
234
+
235
+ // Nil / Max sentinel checks.
236
+ if (hex === NIL_HEX && opts.nilPolicy !== "allow") {
237
+ issues.push({
238
+ kind: "nil-uuid",
239
+ severity: opts.nilPolicy === "reject" ? "high" : "warn",
240
+ ruleId: "uuid.nil-uuid",
241
+ snippet: "uuid is the nil UUID (RFC 9562 §5.9) — sentinel often " +
242
+ "indicates missing-key bug",
243
+ });
244
+ }
245
+ if (hex === MAX_HEX && opts.maxPolicy !== "allow") {
246
+ issues.push({
247
+ kind: "max-uuid",
248
+ severity: opts.maxPolicy === "reject" ? "high" : "warn",
249
+ ruleId: "uuid.max-uuid",
250
+ snippet: "uuid is the max UUID (RFC 9562 §5.10) — sentinel often " +
251
+ "indicates missing-key bug",
252
+ });
253
+ }
254
+
255
+ // Version + variant inspection (skip for nil / max — those bypass the
256
+ // version-bits check by definition).
257
+ if (hex !== NIL_HEX && hex !== MAX_HEX) {
258
+ var versionDigit = parseInt(hex.charAt(12), 16); // allow:raw-byte-literal — hex digit position 12
259
+ var variantNibble = parseInt(hex.charAt(16), 16); // allow:raw-byte-literal — hex digit position 16
260
+
261
+ if (opts.versionPolicy !== "allow") {
262
+ var allowed = opts.allowedVersions;
263
+ var versionOk = !allowed || allowed.indexOf(versionDigit) !== -1;
264
+ if (!versionOk) {
265
+ issues.push({
266
+ kind: "version-unassigned",
267
+ severity: opts.versionPolicy === "reject-unassigned" ? "high" : "warn",
268
+ ruleId: "uuid.version-unassigned",
269
+ snippet: "uuid version digit " + versionDigit + " not in " +
270
+ "allowedVersions " + JSON.stringify(allowed) +
271
+ " (RFC 9562 §4.2 defines 1-8)",
272
+ });
273
+ }
274
+ }
275
+
276
+ if (opts.variantPolicy !== "allow") {
277
+ // RFC 4122 / 9562 variant: high two bits of the variant nibble are
278
+ // 10xx (i.e. nibble in 8/9/a/b).
279
+ var isRfcVariant = (variantNibble & 0xC) === 0x8; // allow:raw-byte-literal — variant-bit mask
280
+ if (!isRfcVariant) {
281
+ issues.push({
282
+ kind: "variant-non-rfc",
283
+ severity: opts.variantPolicy === "reject-non-rfc" ? "high" : "warn",
284
+ ruleId: "uuid.variant-non-rfc",
285
+ snippet: "uuid variant nibble `" + hex.charAt(16) + "` is not " + // allow:raw-byte-literal — hex digit position 16
286
+ "the RFC 4122 / 9562 variant (10xx — nibble 8-b)",
287
+ });
288
+ }
289
+ }
290
+ }
291
+
292
+ return issues;
293
+ }
294
+
295
+ function validate(input, opts) {
296
+ opts = _resolveOpts(opts);
297
+ numericBounds.requireAllPositiveFiniteIntIfPresent(opts,
298
+ ["maxBytes"],
299
+ "guardUuid.validate", GuardUuidError, "uuid.bad-opt");
300
+ if (typeof input !== "string") {
301
+ return {
302
+ ok: false,
303
+ issues: [{ kind: "bad-input", severity: "high",
304
+ ruleId: "uuid.bad-input",
305
+ snippet: "uuid is not a string" }],
306
+ };
307
+ }
308
+ return gateContract.aggregateIssues(_detectIssues(input, opts));
309
+ }
310
+
311
+ function sanitize(input, opts) {
312
+ opts = _resolveOpts(opts);
313
+ if (typeof input !== "string") {
314
+ throw _err("uuid.bad-input", "sanitize requires string input");
315
+ }
316
+ var issues = _detectIssues(input, opts);
317
+ for (var i = 0; i < issues.length; i += 1) {
318
+ if (issues[i].severity === "critical" || issues[i].severity === "high") {
319
+ throw _err(issues[i].ruleId || "uuid.refused",
320
+ "guardUuid.sanitize: " + issues[i].snippet);
321
+ }
322
+ }
323
+ // Safe transforms: lowercase + strip braces / urn prefix → canonical
324
+ // hyphenated form.
325
+ var form = _classifyForm(input);
326
+ if (!form) return input;
327
+ var hex = _toCanonicalHex(input, form);
328
+ return hex.slice(0, 8) + "-" + hex.slice(8, 12) + "-" + // allow:raw-byte-literal — UUID hex slice positions
329
+ hex.slice(12, 16) + "-" + hex.slice(16, 20) + "-" + // allow:raw-byte-literal — UUID hex slice positions
330
+ hex.slice(20); // allow:raw-byte-literal — UUID hex slice positions
331
+ }
332
+
333
+ function gate(opts) {
334
+ opts = _resolveOpts(opts);
335
+ return gateContract.buildGuardGate(
336
+ opts.name || "guardUuid:" + (opts.profile || "default"),
337
+ opts,
338
+ async function (ctx) {
339
+ var identifier = ctx && (ctx.identifier || ctx.uuid || "");
340
+ if (!identifier) return { ok: true, action: "serve" };
341
+ var rv = validate(identifier, opts);
342
+ if (rv.issues.length === 0) return { ok: true, action: "serve" };
343
+ var hasCritical = rv.issues.some(function (i) {
344
+ return i.severity === "critical";
345
+ });
346
+ var hasHigh = rv.issues.some(function (i) {
347
+ return i.severity === "high";
348
+ });
349
+ if (!hasCritical && !hasHigh) {
350
+ return { ok: true, action: "audit-only", issues: rv.issues };
351
+ }
352
+ return { ok: false, action: "refuse", issues: rv.issues };
353
+ });
354
+ }
355
+
356
+ var buildProfile = gateContract.makeProfileBuilder(PROFILES);
357
+
358
+ function compliancePosture(name) {
359
+ return gateContract.lookupCompliancePosture(name, COMPLIANCE_POSTURES,
360
+ _err, "uuid");
361
+ }
362
+
363
+ var _uuidRulePacks = gateContract.makeRulePackLoader(GuardUuidError, "uuid");
364
+ var loadRulePack = _uuidRulePacks.load;
365
+
366
+ module.exports = {
367
+ // ---- guard-* family registry exports ----
368
+ NAME: "uuid",
369
+ KIND: "identifier",
370
+ INTEGRATION_FIXTURES: Object.freeze({
371
+ kind: "identifier",
372
+ benignBytes: Buffer.from("550e8400-e29b-41d4-a716-446655440000", "utf8"),
373
+ hostileBytes: Buffer.from("00000000-0000-0000-0000-000000000000", "utf8"),
374
+ benignIdentifier: "550e8400-e29b-41d4-a716-446655440000",
375
+ // Hostile: nil UUID — refused at strict (sentinel-leak class).
376
+ hostileIdentifier: "00000000-0000-0000-0000-000000000000",
377
+ }),
378
+ // ---- primitive surface ----
379
+ validate: validate,
380
+ sanitize: sanitize,
381
+ gate: gate,
382
+ buildProfile: buildProfile,
383
+ compliancePosture: compliancePosture,
384
+ loadRulePack: loadRulePack,
385
+ PROFILES: PROFILES,
386
+ DEFAULTS: DEFAULTS,
387
+ COMPLIANCE_POSTURES: COMPLIANCE_POSTURES,
388
+ GuardUuidError: GuardUuidError,
389
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blamejs/core",
3
- "version": "0.7.42",
3
+ "version": "0.7.44",
4
4
  "description": "The Node framework that owns its stack.",
5
5
  "license": "Apache-2.0",
6
6
  "author": "blamejs contributors",
@@ -2,10 +2,10 @@
2
2
  "$schema": "http://cyclonedx.org/schema/bom-1.5.schema.json",
3
3
  "bomFormat": "CycloneDX",
4
4
  "specVersion": "1.5",
5
- "serialNumber": "urn:uuid:bc5e4436-ca0f-43ac-a77d-f32a765eb230",
5
+ "serialNumber": "urn:uuid:2273cc28-4cb1-4b48-8b09-c33dd95383b2",
6
6
  "version": 1,
7
7
  "metadata": {
8
- "timestamp": "2026-05-05T20:27:13.830Z",
8
+ "timestamp": "2026-05-05T21:06:13.872Z",
9
9
  "lifecycles": [
10
10
  {
11
11
  "phase": "build"
@@ -19,14 +19,14 @@
19
19
  }
20
20
  ],
21
21
  "component": {
22
- "bom-ref": "@blamejs/core@0.7.42",
22
+ "bom-ref": "@blamejs/core@0.7.44",
23
23
  "type": "library",
24
24
  "name": "blamejs",
25
- "version": "0.7.42",
25
+ "version": "0.7.44",
26
26
  "scope": "required",
27
27
  "author": "blamejs contributors",
28
28
  "description": "The Node framework that owns its stack.",
29
- "purl": "pkg:npm/%40blamejs/core@0.7.42",
29
+ "purl": "pkg:npm/%40blamejs/core@0.7.44",
30
30
  "properties": [],
31
31
  "externalReferences": [
32
32
  {
@@ -54,7 +54,7 @@
54
54
  "components": [],
55
55
  "dependencies": [
56
56
  {
57
- "ref": "@blamejs/core@0.7.42",
57
+ "ref": "@blamejs/core@0.7.44",
58
58
  "dependsOn": []
59
59
  }
60
60
  ]