@blamejs/core 0.7.43 → 0.7.45

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.45** (2026-05-05) — `b.guardCidr` — CIDR identifier-safety primitive (KIND="identifier"). Validates user-supplied CIDR notation strings (IPv4 + IPv6) destined for network allowlists, ACLs, security-group rules, and tenant-boundary configuration. Threat catalog: shape malformation, IPv4 octet overflow + leading-zero refuse, IPv6 zero-group ambiguity, mask out-of-range (IPv4 32 / IPv6 128 ceiling), network-address misalignment (host bits set on a non-/32 / non-/128 prefix — common typo class), reserved-range membership covering all the IPv4 RFC 1918 private blocks (`10/8`, `172.16/12`, `192.168/16`) plus loopback `127/8`, link-local `169.254/16`, multicast `224/4`, class-E `240/4`, RFC 5737 documentation `192.0.2/24` + `198.51.100/24` + `203.0.113/24`, benchmarking `198.18/15`, CGNAT `100.64/10`, this-network `0/8`, plus IPv6 loopback `::1`, unspecified `::/128`, ULA `fc00::/7`, link-local `fe80::/10`, multicast `ff00::/8`, documentation `2001:db8::/32`, teredo, deprecated 6to4 `2002::/16`. Includes IPv4-mapped IPv6 dual-stack confusion detection (`::ffff:0:0/96` — CVE-2021-22931 IPv6 variant). Bare-IP-without-mask policy (strict refuses; balanced audits; permissive allows). BIDI / zero-width / control / null-byte universal refuse. Profiles: `strict` / `balanced` / `permissive`. Postures: `hipaa` / `pci-dss` / `soc2` strict overlay; `gdpr` balanced overlay. Auto-registers into `b.guardAll` as a STANDALONE_GUARD.
12
+
13
+ - **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.
14
+
11
15
  - **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.
12
16
 
13
17
  - **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.
package/index.js CHANGED
@@ -111,6 +111,8 @@ var guardXml = require("./lib/guard-xml");
111
111
  var guardMarkdown = require("./lib/guard-markdown");
112
112
  var guardEmail = require("./lib/guard-email");
113
113
  var guardDomain = require("./lib/guard-domain");
114
+ var guardUuid = require("./lib/guard-uuid");
115
+ var guardCidr = require("./lib/guard-cidr");
114
116
  var guardAll = require("./lib/guard-all");
115
117
  var ssrfGuard = require("./lib/ssrf-guard");
116
118
  var authHeader = require("./lib/auth-header");
@@ -253,6 +255,8 @@ module.exports = {
253
255
  guardMarkdown: guardMarkdown,
254
256
  guardEmail: guardEmail,
255
257
  guardDomain: guardDomain,
258
+ guardUuid: guardUuid,
259
+ guardCidr: guardCidr,
256
260
  guardAll: guardAll,
257
261
  ssrfGuard: ssrfGuard,
258
262
  authHeader: authHeader,
@@ -257,6 +257,21 @@ var GuardEmailError = defineClass("GuardEmailError", { alwaysPermane
257
257
  // only strings, wildcard labels, RFC 8552 underscore-label misuse, DGA
258
258
  // high-entropy labels. alwaysPermanent.
259
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 });
267
+ // GuardCidrError covers CIDR identifier violations: shape malformation,
268
+ // IPv4 octet overflow, IPv6 zero-group ambiguity, mask out-of-range,
269
+ // network-address misalignment (host bits set), reserved-range membership
270
+ // (RFC 1918, loopback, link-local, multicast, documentation, benchmarking,
271
+ // CGNAT, IPv6 ULA / link-local / multicast / documentation), IPv4-mapped
272
+ // IPv6 dual-stack confusion, BIDI / zero-width / control / null-byte
273
+ // universal refuse. alwaysPermanent.
274
+ var GuardCidrError = defineClass("GuardCidrError", { alwaysPermanent: true });
260
275
  // DoraError covers DORA Article 17 incident-reporting workflow errors
261
276
  // (classification refusal, report-shape validation, ESA-template
262
277
  // generation, audit-chain integration). Permanent — these are
@@ -317,6 +332,8 @@ module.exports = {
317
332
  GuardMarkdownError: GuardMarkdownError,
318
333
  GuardEmailError: GuardEmailError,
319
334
  GuardDomainError: GuardDomainError,
335
+ GuardUuidError: GuardUuidError,
336
+ GuardCidrError: GuardCidrError,
320
337
  DoraError: DoraError,
321
338
  ComplianceError: ComplianceError,
322
339
  SmtpPolicyError: SmtpPolicyError,
package/lib/guard-all.js CHANGED
@@ -90,6 +90,8 @@ var GUARDS = [
90
90
  var STANDALONE_GUARDS = [
91
91
  require("./guard-filename"),
92
92
  require("./guard-domain"),
93
+ require("./guard-uuid"),
94
+ require("./guard-cidr"),
93
95
  ];
94
96
 
95
97
  // Framework-wide profile + posture vocabulary that every guard MUST
@@ -0,0 +1,513 @@
1
+ "use strict";
2
+ /**
3
+ * guard-cidr — CIDR identifier-safety primitive (b.guardCidr).
4
+ *
5
+ * Validates user-supplied CIDR notation strings (IPv4 + IPv6) destined
6
+ * for network-allowlists, ACLs, security-group rules, and tenant-
7
+ * boundary configuration. KIND="identifier" — consumes ctx.identifier
8
+ * (or ctx.cidr).
9
+ *
10
+ * Threat catalog:
11
+ * - Shape malformation — not "address/mask".
12
+ * - IPv4 octet out of range (> 255), wrong number of octets.
13
+ * - IPv6 zero-group inflation, multiple `::` (ambiguous).
14
+ * - Mask out of range (IPv4: 0-32; IPv6: 0-128); negative.
15
+ * - Network-address misalignment — `10.0.0.1/24` carries host bits
16
+ * set when /24 implies the pure network address `10.0.0.0/24`.
17
+ * Often a typo that produces unexpected match semantics.
18
+ * - Reserved IPv4 ranges (RFC 1918 private 10/8, 172.16/12,
19
+ * 192.168/16; loopback 127/8; link-local 169.254/16; multicast
20
+ * 224/4; reserved 240/4; documentation 192.0.2/24, 198.51.100/24,
21
+ * 203.0.113/24; benchmarking 198.18/15; CGNAT 100.64/10).
22
+ * - Reserved IPv6 ranges — loopback `::1`, unspecified `::/128`,
23
+ * ULA `fc00::/7`, link-local `fe80::/10`, multicast `ff00::/8`,
24
+ * IPv4-mapped `::ffff:0:0/96`, documentation `2001:db8::/32`,
25
+ * teredo `2001::/32`, deprecated 6to4 `2002::/16`.
26
+ * - IPv4-mapped IPv6 confusion (CVE-2021-22931 IPv6 / IPv4 dual-
27
+ * stack class) — `::ffff:192.168.1.1` represents the IPv4 address
28
+ * in IPv6 namespace and trips dual-stack allowlist matchers.
29
+ * - BIDI / zero-width / control / null-byte universal refuse.
30
+ *
31
+ * var rv = b.guardCidr.validate("10.0.0.0/8", { profile: "strict" });
32
+ * var safe = b.guardCidr.sanitize("10.0.0.1/24", { profile: "balanced" });
33
+ * var g = b.guardCidr.gate({ profile: "strict" });
34
+ */
35
+
36
+ var codepointClass = require("./codepoint-class");
37
+ var lazyRequire = require("./lazy-require");
38
+ var gateContract = require("./gate-contract");
39
+ var C = require("./constants");
40
+ var numericBounds = require("./numeric-bounds");
41
+ var { GuardCidrError } = require("./framework-error");
42
+
43
+ var observability = lazyRequire(function () { return require("./observability"); });
44
+ void observability;
45
+
46
+ var _err = GuardCidrError.factory;
47
+
48
+ var IPV4_OCTET_MAX = 255; // allow:raw-byte-literal — RFC 791 octet ceiling
49
+ var IPV4_MASK_MAX = 32; // allow:raw-byte-literal — IPv4 prefix ceiling
50
+ var IPV6_MASK_MAX = 128; // allow:raw-byte-literal — IPv6 prefix ceiling
51
+ var IPV4_OCTETS = 4; // allow:raw-byte-literal — IPv4 dotted-quad count
52
+ var IPV6_GROUPS = 8; // allow:raw-byte-literal — IPv6 16-bit group count
53
+
54
+ // ---- IPv4 reserved ranges (CIDR network, /mask) ----
55
+ //
56
+ // Each entry: [networkAsUint32, maskBits, label].
57
+ function _ipv4ToUint32(o) { return ((o[0] << 24) >>> 0) + (o[1] << 16) + (o[2] << 8) + o[3]; } // allow:raw-byte-literal — IPv4 octet shifts
58
+ var IPV4_RESERVED = Object.freeze([
59
+ { net: _ipv4ToUint32([10, 0, 0, 0]), prefix: 8, label: "rfc1918-private-10" }, // allow:raw-byte-literal — IPv4 octets
60
+ { net: _ipv4ToUint32([172, 16, 0, 0]), prefix: 12, label: "rfc1918-private-172.16" }, // allow:raw-byte-literal — IPv4 octets
61
+ { net: _ipv4ToUint32([192, 168, 0, 0]), prefix: 16, label: "rfc1918-private-192.168" }, // allow:raw-byte-literal — IPv4 octets
62
+ { net: _ipv4ToUint32([127, 0, 0, 0]), prefix: 8, label: "loopback" }, // allow:raw-byte-literal — IPv4 octets
63
+ { net: _ipv4ToUint32([169, 254, 0, 0]), prefix: 16, label: "link-local" }, // allow:raw-byte-literal — IPv4 octets
64
+ { net: _ipv4ToUint32([224, 0, 0, 0]), prefix: 4, label: "multicast" }, // allow:raw-byte-literal — IPv4 octets
65
+ { net: _ipv4ToUint32([240, 0, 0, 0]), prefix: 4, label: "reserved-class-e" }, // allow:raw-byte-literal — IPv4 octets allow:raw-time-literal — 240 is an IPv4 octet not seconds
66
+ { net: _ipv4ToUint32([192, 0, 2, 0]), prefix: 24, label: "documentation-test-net-1" }, // allow:raw-byte-literal — IPv4 octets
67
+ { net: _ipv4ToUint32([198, 51, 100, 0]), prefix: 24, label: "documentation-test-net-2" }, // allow:raw-byte-literal — IPv4 octets
68
+ { net: _ipv4ToUint32([203, 0, 113, 0]), prefix: 24, label: "documentation-test-net-3" }, // allow:raw-byte-literal — IPv4 octets
69
+ { net: _ipv4ToUint32([198, 18, 0, 0]), prefix: 15, label: "benchmarking" }, // allow:raw-byte-literal — IPv4 octets
70
+ { net: _ipv4ToUint32([100, 64, 0, 0]), prefix: 10, label: "cgnat" }, // allow:raw-byte-literal — IPv4 octets
71
+ { net: _ipv4ToUint32([0, 0, 0, 0]), prefix: 8, label: "this-network" }, // allow:raw-byte-literal — IPv4 octets
72
+ ]);
73
+
74
+ // ---- IPv6 reserved prefixes ----
75
+ //
76
+ // Stored as a normalized "first 32 hex chars (no colons)" prefix-byte
77
+ // string. Match by string-prefix on the first ceil(prefix/4) hex chars.
78
+ var IPV6_RESERVED = Object.freeze([
79
+ { prefix: 128, hexPrefix: "00000000000000000000000000000001", label: "loopback" }, // allow:raw-byte-literal — IPv6 hex form
80
+ { prefix: 128, hexPrefix: "00000000000000000000000000000000", label: "unspecified" }, // allow:raw-byte-literal — IPv6 hex form
81
+ { prefix: 7, hexPrefix: "fc", label: "ula" }, // allow:raw-byte-literal — IPv6 hex form
82
+ { prefix: 10, hexPrefix: "fe8", label: "link-local" }, // allow:raw-byte-literal — IPv6 hex form
83
+ { prefix: 8, hexPrefix: "ff", label: "multicast" }, // allow:raw-byte-literal — IPv6 hex form
84
+ { prefix: 96, hexPrefix: "00000000000000000000ffff", label: "ipv4-mapped" }, // allow:raw-byte-literal — IPv6 hex form
85
+ { prefix: 32, hexPrefix: "20010db8", label: "documentation" }, // allow:raw-byte-literal — IPv6 hex form
86
+ { prefix: 32, hexPrefix: "20010000", label: "teredo" }, // allow:raw-byte-literal — IPv6 hex form
87
+ { prefix: 16, hexPrefix: "2002", label: "deprecated-6to4" }, // allow:raw-byte-literal — IPv6 hex form
88
+ ]);
89
+
90
+ // ---- Profile presets ----
91
+
92
+ var PROFILES = Object.freeze({
93
+ "strict": {
94
+ bidiPolicy: "reject",
95
+ controlPolicy: "reject",
96
+ nullBytePolicy: "reject",
97
+ zeroWidthPolicy: "reject",
98
+ networkAlignmentPolicy: "reject",
99
+ reservedRangesPolicy: "reject",
100
+ ipv4MappedIpv6Policy: "reject",
101
+ requireMaskPolicy: "reject-bare-ip", // bare ip/no mask refused
102
+ family: "either", // "either" | "ipv4-only" | "ipv6-only"
103
+ maxBytes: C.BYTES.bytes(64),
104
+ maxRuntimeMs: C.TIME.seconds(2),
105
+ },
106
+ "balanced": {
107
+ bidiPolicy: "reject",
108
+ controlPolicy: "reject",
109
+ nullBytePolicy: "reject",
110
+ zeroWidthPolicy: "reject",
111
+ networkAlignmentPolicy: "audit",
112
+ reservedRangesPolicy: "audit",
113
+ ipv4MappedIpv6Policy: "audit",
114
+ requireMaskPolicy: "audit-bare-ip", // bare ip → audit; treat as host-only prefix
115
+ family: "either",
116
+ maxBytes: C.BYTES.bytes(64),
117
+ maxRuntimeMs: C.TIME.seconds(2),
118
+ },
119
+ "permissive": {
120
+ bidiPolicy: "reject", // BIDI refused at every profile
121
+ controlPolicy: "reject", // controls refused at every profile
122
+ nullBytePolicy: "reject", // null refused at every profile
123
+ zeroWidthPolicy: "reject", // zero-width refused at every profile
124
+ networkAlignmentPolicy: "audit",
125
+ reservedRangesPolicy: "allow",
126
+ ipv4MappedIpv6Policy: "allow",
127
+ requireMaskPolicy: "allow-bare-ip",
128
+ family: "either",
129
+ maxBytes: C.BYTES.bytes(64),
130
+ maxRuntimeMs: C.TIME.seconds(2),
131
+ },
132
+ });
133
+
134
+ var DEFAULTS = Object.freeze(Object.assign({}, PROFILES["strict"], {
135
+ mode: "enforce",
136
+ }));
137
+
138
+ var COMPLIANCE_POSTURES = Object.freeze({
139
+ "hipaa": Object.assign({}, PROFILES["strict"], {
140
+ forensicSnippetBytes: C.BYTES.bytes(128),
141
+ }),
142
+ "pci-dss": Object.assign({}, PROFILES["strict"], {
143
+ forensicSnippetBytes: C.BYTES.bytes(128),
144
+ }),
145
+ "gdpr": Object.assign({}, PROFILES["balanced"], {
146
+ forensicSnippetBytes: C.BYTES.bytes(64),
147
+ }),
148
+ "soc2": Object.assign({}, PROFILES["strict"], {
149
+ forensicSnippetBytes: C.BYTES.bytes(256),
150
+ }),
151
+ });
152
+
153
+ function _resolveOpts(opts) {
154
+ return gateContract.resolveProfileAndPosture(opts, {
155
+ profiles: PROFILES,
156
+ compliancePostures: COMPLIANCE_POSTURES,
157
+ defaults: DEFAULTS,
158
+ errorClass: GuardCidrError,
159
+ errCodePrefix: "cidr",
160
+ });
161
+ }
162
+
163
+ // ---- Parsers ----
164
+
165
+ function _parseIpv4(s) {
166
+ // Strict dotted-decimal — every octet is 0-255 with no leading zeros
167
+ // (octal/hex/long forms refused — see guard-domain for that class).
168
+ var parts = s.split(".");
169
+ if (parts.length !== IPV4_OCTETS) return null;
170
+ var octets = [];
171
+ for (var i = 0; i < parts.length; i += 1) {
172
+ var p = parts[i];
173
+ if (!/^[0-9]+$/.test(p)) return null;
174
+ if (p.length > 1 && p.charAt(0) === "0") return null; // leading-zero octal/forms refused
175
+ var n = parseInt(p, 10); // allow:raw-byte-literal — base-10 radix
176
+ if (n > IPV4_OCTET_MAX) return null;
177
+ octets.push(n);
178
+ }
179
+ return octets;
180
+ }
181
+
182
+ function _parseIpv6(s) {
183
+ // IPv6 — supports `::` zero-group compression. Returns 8 hex groups
184
+ // expanded to 4-char-each lowercase string, or null on malformation.
185
+ if (s.indexOf(":::") !== -1) return null;
186
+ var doubleColons = s.split("::");
187
+ if (doubleColons.length > 2) return null;
188
+
189
+ function _parseGroups(seg) {
190
+ if (seg === "") return [];
191
+ var parts = seg.split(":");
192
+ var out = [];
193
+ for (var i = 0; i < parts.length; i += 1) {
194
+ var p = parts[i];
195
+ if (!/^[0-9a-fA-F]{1,4}$/.test(p)) return null;
196
+ out.push(p.toLowerCase());
197
+ }
198
+ return out;
199
+ }
200
+
201
+ var groups;
202
+ if (doubleColons.length === 1) {
203
+ // No `::` — must be exactly 8 groups.
204
+ groups = _parseGroups(doubleColons[0]);
205
+ if (!groups || groups.length !== IPV6_GROUPS) return null;
206
+ } else {
207
+ var left = _parseGroups(doubleColons[0]);
208
+ var right = _parseGroups(doubleColons[1]);
209
+ if (left === null || right === null) return null;
210
+ var pad = IPV6_GROUPS - left.length - right.length;
211
+ if (pad < 0) return null;
212
+ var zeros = [];
213
+ for (var z = 0; z < pad; z += 1) zeros.push("0000"); // allow:raw-byte-literal — IPv6 zero group
214
+ groups = left.concat(zeros).concat(right);
215
+ if (groups.length !== IPV6_GROUPS) return null;
216
+ }
217
+ // Pad each group to 4 chars.
218
+ for (var g = 0; g < groups.length; g += 1) {
219
+ while (groups[g].length < 4) groups[g] = "0" + groups[g]; // allow:raw-byte-literal — IPv6 group width
220
+ }
221
+ return groups;
222
+ }
223
+
224
+ function _hostBitsSetIpv4(octets, prefix) {
225
+ if (prefix === IPV4_MASK_MAX) return false;
226
+ var addr = _ipv4ToUint32(octets);
227
+ var hostMask = prefix === 0 ? 0xFFFFFFFF : ((1 << (IPV4_MASK_MAX - prefix)) - 1) >>> 0;
228
+ return (addr & hostMask) !== 0;
229
+ }
230
+
231
+ function _hostBitsSetIpv6(groups, prefix) {
232
+ if (prefix === IPV6_MASK_MAX) return false;
233
+ // Walk groups from most-significant; once we cross the prefix
234
+ // boundary, every remaining bit must be zero.
235
+ var bitIdx = 0;
236
+ for (var i = 0; i < groups.length; i += 1) {
237
+ var grp = parseInt(groups[i], 16); // allow:raw-byte-literal — base-16 radix
238
+ for (var b = 15; b >= 0; b -= 1) { // allow:raw-byte-literal — bits per group
239
+ if (bitIdx >= prefix) {
240
+ if ((grp >> b) & 1) return true;
241
+ }
242
+ bitIdx += 1;
243
+ }
244
+ }
245
+ return false;
246
+ }
247
+
248
+ function _ipv4InReservedRange(octets, prefix) {
249
+ var addr = _ipv4ToUint32(octets);
250
+ var hits = [];
251
+ for (var i = 0; i < IPV4_RESERVED.length; i += 1) {
252
+ var r = IPV4_RESERVED[i];
253
+ if (prefix < r.prefix) continue; // user range broader than reserved → not contained
254
+ var mask = r.prefix === 0 ? 0 : (0xFFFFFFFF << (IPV4_MASK_MAX - r.prefix)) >>> 0;
255
+ // JS `&` is signed int32 — `>>> 0` reinterprets back to uint32 so
256
+ // addresses with the high bit set compare correctly.
257
+ if (((addr & mask) >>> 0) === r.net) hits.push(r.label);
258
+ }
259
+ return hits;
260
+ }
261
+
262
+ function _ipv6InReservedRange(groups, prefix) {
263
+ var hex = groups.join("");
264
+ var hits = [];
265
+ for (var i = 0; i < IPV6_RESERVED.length; i += 1) {
266
+ var r = IPV6_RESERVED[i];
267
+ if (prefix < r.prefix) continue;
268
+ if (hex.startsWith(r.hexPrefix)) hits.push(r.label);
269
+ }
270
+ return hits;
271
+ }
272
+
273
+ // ---- Detection ----
274
+
275
+ function _detectIssues(input, opts) {
276
+ var issues = [];
277
+ if (typeof input !== "string") {
278
+ return [{ kind: "bad-input", severity: "high",
279
+ ruleId: "cidr.bad-input",
280
+ snippet: "cidr is not a string" }];
281
+ }
282
+ if (input.length === 0) {
283
+ return [{ kind: "empty", severity: "high",
284
+ ruleId: "cidr.empty",
285
+ snippet: "cidr is empty" }];
286
+ }
287
+ if (Buffer.byteLength(input, "utf8") > opts.maxBytes) {
288
+ return [{ kind: "cidr-cap", severity: "high",
289
+ ruleId: "cidr.cidr-cap",
290
+ snippet: "cidr input exceeds maxBytes " + opts.maxBytes }];
291
+ }
292
+
293
+ var charThreats = codepointClass.detectCharThreats(input, opts, "cidr");
294
+ for (var ci = 0; ci < charThreats.length; ci += 1) issues.push(charThreats[ci]);
295
+
296
+ // Split address from mask.
297
+ var slashAt = input.indexOf("/");
298
+ var addrPart = slashAt === -1 ? input : input.slice(0, slashAt);
299
+ var maskPart = slashAt === -1 ? null : input.slice(slashAt + 1);
300
+
301
+ var hasMask = maskPart !== null;
302
+ if (!hasMask) {
303
+ if (opts.requireMaskPolicy === "reject-bare-ip") {
304
+ issues.push({
305
+ kind: "bare-ip", severity: "high",
306
+ ruleId: "cidr.bare-ip",
307
+ snippet: "input has no `/mask` — bare IP refused at strict; " +
308
+ "use /32 (IPv4) or /128 (IPv6) for a single host",
309
+ });
310
+ return issues;
311
+ } else if (opts.requireMaskPolicy === "audit-bare-ip") {
312
+ issues.push({
313
+ kind: "bare-ip", severity: "warn",
314
+ ruleId: "cidr.bare-ip",
315
+ snippet: "input has no `/mask` — treating as /32 or /128 host",
316
+ });
317
+ }
318
+ }
319
+
320
+ // Determine address family.
321
+ var ipv4Octets = _parseIpv4(addrPart);
322
+ var ipv6Groups = ipv4Octets ? null : _parseIpv6(addrPart);
323
+
324
+ if (!ipv4Octets && !ipv6Groups) {
325
+ issues.push({
326
+ kind: "address-shape", severity: "high",
327
+ ruleId: "cidr.address-shape",
328
+ snippet: "address `" + addrPart + "` is not a valid IPv4 dotted-" +
329
+ "decimal or IPv6 hex-group form",
330
+ });
331
+ return issues;
332
+ }
333
+
334
+ var family = ipv4Octets ? "ipv4" : "ipv6";
335
+
336
+ // Family-restriction enforcement.
337
+ if (opts.family === "ipv4-only" && family !== "ipv4") {
338
+ issues.push({
339
+ kind: "family-mismatch", severity: "high",
340
+ ruleId: "cidr.family-mismatch",
341
+ snippet: "address is " + family + " but family policy is `ipv4-only`",
342
+ });
343
+ }
344
+ if (opts.family === "ipv6-only" && family !== "ipv6") {
345
+ issues.push({
346
+ kind: "family-mismatch", severity: "high",
347
+ ruleId: "cidr.family-mismatch",
348
+ snippet: "address is " + family + " but family policy is `ipv6-only`",
349
+ });
350
+ }
351
+
352
+ // Mask validation.
353
+ var maskMax = family === "ipv4" ? IPV4_MASK_MAX : IPV6_MASK_MAX;
354
+ var prefix = hasMask ? -1 : maskMax;
355
+ if (hasMask) {
356
+ if (!/^[0-9]+$/.test(maskPart)) {
357
+ issues.push({
358
+ kind: "mask-shape", severity: "high",
359
+ ruleId: "cidr.mask-shape",
360
+ snippet: "mask `" + maskPart + "` is not a non-negative integer",
361
+ });
362
+ return issues;
363
+ }
364
+ prefix = parseInt(maskPart, 10); // allow:raw-byte-literal — base-10 radix
365
+ if (prefix > maskMax) {
366
+ issues.push({
367
+ kind: "mask-cap", severity: "high",
368
+ ruleId: "cidr.mask-cap",
369
+ snippet: "mask /" + prefix + " exceeds " + family + " maximum /" +
370
+ maskMax,
371
+ });
372
+ return issues;
373
+ }
374
+ }
375
+
376
+ // Network-address alignment — host bits must be zero unless the
377
+ // mask is /32 or /128 (single host).
378
+ var misaligned;
379
+ if (family === "ipv4") misaligned = _hostBitsSetIpv4(ipv4Octets, prefix);
380
+ else misaligned = _hostBitsSetIpv6(ipv6Groups, prefix);
381
+ if (misaligned && opts.networkAlignmentPolicy !== "allow") {
382
+ issues.push({
383
+ kind: "network-misaligned",
384
+ severity: opts.networkAlignmentPolicy === "reject" ? "high" : "warn",
385
+ ruleId: "cidr.network-misaligned",
386
+ snippet: "host bits set in `" + addrPart + "/" + prefix + "` — " +
387
+ "network address would be different; common typo class",
388
+ });
389
+ }
390
+
391
+ // Reserved-range membership.
392
+ var reserved = family === "ipv4"
393
+ ? _ipv4InReservedRange(ipv4Octets, prefix)
394
+ : _ipv6InReservedRange(ipv6Groups, prefix);
395
+ if (reserved.length > 0 && opts.reservedRangesPolicy !== "allow") {
396
+ issues.push({
397
+ kind: "reserved-range",
398
+ severity: opts.reservedRangesPolicy === "reject" ? "high" : "warn",
399
+ ruleId: "cidr.reserved-range",
400
+ snippet: "cidr falls inside reserved range(s): " + reserved.join(", "),
401
+ });
402
+ }
403
+
404
+ // IPv4-mapped IPv6 confusion check.
405
+ if (family === "ipv6" &&
406
+ reserved.indexOf("ipv4-mapped") !== -1 &&
407
+ opts.ipv4MappedIpv6Policy !== "allow") {
408
+ issues.push({
409
+ kind: "ipv4-mapped-ipv6",
410
+ severity: opts.ipv4MappedIpv6Policy === "reject" ? "high" : "warn",
411
+ ruleId: "cidr.ipv4-mapped-ipv6",
412
+ snippet: "address is IPv4-mapped IPv6 (`::ffff:0:0/96`) — dual-" +
413
+ "stack allowlist confusion class (CVE-2021-22931 IPv6 " +
414
+ "variant)",
415
+ });
416
+ }
417
+
418
+ return issues;
419
+ }
420
+
421
+ function validate(input, opts) {
422
+ opts = _resolveOpts(opts);
423
+ numericBounds.requireAllPositiveFiniteIntIfPresent(opts,
424
+ ["maxBytes"],
425
+ "guardCidr.validate", GuardCidrError, "cidr.bad-opt");
426
+ if (typeof input !== "string") {
427
+ return {
428
+ ok: false,
429
+ issues: [{ kind: "bad-input", severity: "high",
430
+ ruleId: "cidr.bad-input",
431
+ snippet: "cidr is not a string" }],
432
+ };
433
+ }
434
+ return gateContract.aggregateIssues(_detectIssues(input, opts));
435
+ }
436
+
437
+ function sanitize(input, opts) {
438
+ opts = _resolveOpts(opts);
439
+ if (typeof input !== "string") {
440
+ throw _err("cidr.bad-input", "sanitize requires string input");
441
+ }
442
+ var issues = _detectIssues(input, opts);
443
+ for (var i = 0; i < issues.length; i += 1) {
444
+ if (issues[i].severity === "critical" || issues[i].severity === "high") {
445
+ throw _err(issues[i].ruleId || "cidr.refused",
446
+ "guardCidr.sanitize: " + issues[i].snippet);
447
+ }
448
+ }
449
+ // Normalize: lowercase IPv6 groups + canonical mask form.
450
+ var slashAt = input.indexOf("/");
451
+ var addr = slashAt === -1 ? input : input.slice(0, slashAt);
452
+ var mask = slashAt === -1 ? null : input.slice(slashAt + 1);
453
+ if (_parseIpv4(addr)) return mask === null ? addr : addr + "/" + mask;
454
+ return mask === null ? addr.toLowerCase() : addr.toLowerCase() + "/" + mask;
455
+ }
456
+
457
+ function gate(opts) {
458
+ opts = _resolveOpts(opts);
459
+ return gateContract.buildGuardGate(
460
+ opts.name || "guardCidr:" + (opts.profile || "default"),
461
+ opts,
462
+ async function (ctx) {
463
+ var identifier = ctx && (ctx.identifier || ctx.cidr || "");
464
+ if (!identifier) return { ok: true, action: "serve" };
465
+ var rv = validate(identifier, opts);
466
+ if (rv.issues.length === 0) return { ok: true, action: "serve" };
467
+ var hasCritical = rv.issues.some(function (i) {
468
+ return i.severity === "critical";
469
+ });
470
+ var hasHigh = rv.issues.some(function (i) {
471
+ return i.severity === "high";
472
+ });
473
+ if (!hasCritical && !hasHigh) {
474
+ return { ok: true, action: "audit-only", issues: rv.issues };
475
+ }
476
+ return { ok: false, action: "refuse", issues: rv.issues };
477
+ });
478
+ }
479
+
480
+ var buildProfile = gateContract.makeProfileBuilder(PROFILES);
481
+
482
+ function compliancePosture(name) {
483
+ return gateContract.lookupCompliancePosture(name, COMPLIANCE_POSTURES,
484
+ _err, "cidr");
485
+ }
486
+
487
+ var _cidrRulePacks = gateContract.makeRulePackLoader(GuardCidrError, "cidr");
488
+ var loadRulePack = _cidrRulePacks.load;
489
+
490
+ module.exports = {
491
+ // ---- guard-* family registry exports ----
492
+ NAME: "cidr",
493
+ KIND: "identifier",
494
+ INTEGRATION_FIXTURES: Object.freeze({
495
+ kind: "identifier",
496
+ benignBytes: Buffer.from("8.8.8.0/24", "utf8"),
497
+ hostileBytes: Buffer.from("10.0.0.0/8", "utf8"),
498
+ benignIdentifier: "8.8.8.0/24",
499
+ // Hostile: RFC 1918 private range — refused at strict.
500
+ hostileIdentifier: "10.0.0.0/8",
501
+ }),
502
+ // ---- primitive surface ----
503
+ validate: validate,
504
+ sanitize: sanitize,
505
+ gate: gate,
506
+ buildProfile: buildProfile,
507
+ compliancePosture: compliancePosture,
508
+ loadRulePack: loadRulePack,
509
+ PROFILES: PROFILES,
510
+ DEFAULTS: DEFAULTS,
511
+ COMPLIANCE_POSTURES: COMPLIANCE_POSTURES,
512
+ GuardCidrError: GuardCidrError,
513
+ };
@@ -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.43",
3
+ "version": "0.7.45",
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:5c9d460f-9bbe-4b28-8506-6d5ecf7f0090",
5
+ "serialNumber": "urn:uuid:94d573bc-862b-4efb-98b3-c7a31895edf2",
6
6
  "version": 1,
7
7
  "metadata": {
8
- "timestamp": "2026-05-05T20:56:37.841Z",
8
+ "timestamp": "2026-05-05T21:17:15.705Z",
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.43",
22
+ "bom-ref": "@blamejs/core@0.7.45",
23
23
  "type": "library",
24
24
  "name": "blamejs",
25
- "version": "0.7.43",
25
+ "version": "0.7.45",
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.43",
29
+ "purl": "pkg:npm/%40blamejs/core@0.7.45",
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.43",
57
+ "ref": "@blamejs/core@0.7.45",
58
58
  "dependsOn": []
59
59
  }
60
60
  ]