@blamejs/core 0.7.44 → 0.7.46
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 +4 -0
- package/index.js +4 -0
- package/lib/framework-error.js +18 -0
- package/lib/guard-all.js +2 -0
- package/lib/guard-cidr.js +513 -0
- package/lib/guard-time.js +420 -0
- package/package.json +1 -1
- package/sbom.cyclonedx.json +6 -6
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.46** (2026-05-05) — `b.guardTime` — RFC 3339 / ISO 8601 datetime identifier-safety primitive (KIND="identifier"). Validates user-supplied datetime strings destined for audit timestamps, scheduling, retention windows, query ranges, and cross-system event correlation. Threat catalog: shape malformation against the RFC 3339 §5.6 grammar; year-window overflow (default `[1970, 9999]`); naive datetime (no offset) refuse; non-UTC offset policy (strict requires `Z` / `+00:00`); leap-second `60` field policy (RFC 3339 §5.6 valid but parser-panic prone); excessive fractional precision cap (default 9 digits / nanoseconds); date-only and time-only refuse for full-datetime contexts; structural range violations (month / day-in-month / hour / minute / second); BIDI / zero-width / control / null-byte universal refuse. `sanitize` normalizes a space date/time separator to `T` and uppercases the trailing `z` UTC marker. Profiles: `strict` (refuse all of the above), `balanced` (refuse naive; audit non-UTC + leap + fractional + date/time-only), `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.
|
|
12
|
+
|
|
13
|
+
- **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.
|
|
14
|
+
|
|
11
15
|
- **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
16
|
|
|
13
17
|
- **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.
|
package/index.js
CHANGED
|
@@ -112,6 +112,8 @@ var guardMarkdown = require("./lib/guard-markdown");
|
|
|
112
112
|
var guardEmail = require("./lib/guard-email");
|
|
113
113
|
var guardDomain = require("./lib/guard-domain");
|
|
114
114
|
var guardUuid = require("./lib/guard-uuid");
|
|
115
|
+
var guardCidr = require("./lib/guard-cidr");
|
|
116
|
+
var guardTime = require("./lib/guard-time");
|
|
115
117
|
var guardAll = require("./lib/guard-all");
|
|
116
118
|
var ssrfGuard = require("./lib/ssrf-guard");
|
|
117
119
|
var authHeader = require("./lib/auth-header");
|
|
@@ -255,6 +257,8 @@ module.exports = {
|
|
|
255
257
|
guardEmail: guardEmail,
|
|
256
258
|
guardDomain: guardDomain,
|
|
257
259
|
guardUuid: guardUuid,
|
|
260
|
+
guardCidr: guardCidr,
|
|
261
|
+
guardTime: guardTime,
|
|
258
262
|
guardAll: guardAll,
|
|
259
263
|
ssrfGuard: ssrfGuard,
|
|
260
264
|
authHeader: authHeader,
|
package/lib/framework-error.js
CHANGED
|
@@ -264,6 +264,22 @@ var GuardDomainError = defineClass("GuardDomainError", { alwaysPermane
|
|
|
264
264
|
// declared formatPolicy, BIDI / zero-width / control-byte / null-byte
|
|
265
265
|
// universal refuse. alwaysPermanent.
|
|
266
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 });
|
|
275
|
+
// GuardTimeError covers RFC 3339 / ISO 8601 datetime identifier
|
|
276
|
+
// violations: shape malformation, year-window overflow (pre-epoch /
|
|
277
|
+
// far-future), naive datetime (no offset), non-UTC offset, leap-second
|
|
278
|
+
// `60` field policy, excessive fractional precision, date-only /
|
|
279
|
+
// time-only refuse, BIDI / zero-width / control / null-byte universal
|
|
280
|
+
// refuse, structural range violations (month / day-in-month / hour /
|
|
281
|
+
// minute / second). alwaysPermanent.
|
|
282
|
+
var GuardTimeError = defineClass("GuardTimeError", { alwaysPermanent: true });
|
|
267
283
|
// DoraError covers DORA Article 17 incident-reporting workflow errors
|
|
268
284
|
// (classification refusal, report-shape validation, ESA-template
|
|
269
285
|
// generation, audit-chain integration). Permanent — these are
|
|
@@ -325,6 +341,8 @@ module.exports = {
|
|
|
325
341
|
GuardEmailError: GuardEmailError,
|
|
326
342
|
GuardDomainError: GuardDomainError,
|
|
327
343
|
GuardUuidError: GuardUuidError,
|
|
344
|
+
GuardCidrError: GuardCidrError,
|
|
345
|
+
GuardTimeError: GuardTimeError,
|
|
328
346
|
DoraError: DoraError,
|
|
329
347
|
ComplianceError: ComplianceError,
|
|
330
348
|
SmtpPolicyError: SmtpPolicyError,
|
package/lib/guard-all.js
CHANGED
|
@@ -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,420 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* guard-time — RFC 3339 / ISO 8601 datetime identifier-safety primitive
|
|
4
|
+
* (b.guardTime).
|
|
5
|
+
*
|
|
6
|
+
* Validates user-supplied datetime strings destined for audit
|
|
7
|
+
* timestamps, scheduling, retention windows, query ranges, and
|
|
8
|
+
* cross-system event correlation. KIND="identifier" — consumes
|
|
9
|
+
* ctx.identifier (or ctx.timestamp).
|
|
10
|
+
*
|
|
11
|
+
* Threat catalog:
|
|
12
|
+
* - Shape malformation — not RFC 3339 / ISO 8601 datetime.
|
|
13
|
+
* - Pre-epoch / far-future — year before 1970 or after the
|
|
14
|
+
* operator's far-future ceiling (default 9999); often a parsing
|
|
15
|
+
* bug or sentinel-leak shape.
|
|
16
|
+
* - Naive datetime (no offset) — strict refuses; downstream
|
|
17
|
+
* interpretation depends on local timezone, breaks
|
|
18
|
+
* cross-region equality.
|
|
19
|
+
* - Non-UTC offset — strict accepts only `Z` / `+00:00`; balanced
|
|
20
|
+
* accepts any offset; permissive allows naive too.
|
|
21
|
+
* - Leap-second `60` in seconds field — RFC 3339 §5.6 explicitly
|
|
22
|
+
* valid (`23:59:60Z` is a real wall-clock time); most parsers
|
|
23
|
+
* panic. Flagged-by-default with operator policy.
|
|
24
|
+
* - Excessive fractional precision — RFC 3339 allows any digits
|
|
25
|
+
* after the dot but every consuming system has a cap; flag > 9
|
|
26
|
+
* fractional digits.
|
|
27
|
+
* - Date-only / time-only — refused for full-datetime contexts.
|
|
28
|
+
* - Whitespace / control / null-byte / BIDI universal refuse.
|
|
29
|
+
*
|
|
30
|
+
* var rv = b.guardTime.validate("2026-05-05T12:34:56Z",
|
|
31
|
+
* { profile: "strict" });
|
|
32
|
+
* var safe = b.guardTime.sanitize("2026-05-05T12:34:56.123+05:30",
|
|
33
|
+
* { profile: "balanced" });
|
|
34
|
+
* var g = b.guardTime.gate({ profile: "strict" });
|
|
35
|
+
*/
|
|
36
|
+
|
|
37
|
+
var codepointClass = require("./codepoint-class");
|
|
38
|
+
var lazyRequire = require("./lazy-require");
|
|
39
|
+
var gateContract = require("./gate-contract");
|
|
40
|
+
var C = require("./constants");
|
|
41
|
+
var numericBounds = require("./numeric-bounds");
|
|
42
|
+
var { GuardTimeError } = require("./framework-error");
|
|
43
|
+
|
|
44
|
+
var observability = lazyRequire(function () { return require("./observability"); });
|
|
45
|
+
void observability;
|
|
46
|
+
|
|
47
|
+
var _err = GuardTimeError.factory;
|
|
48
|
+
|
|
49
|
+
// RFC 3339 §5.6 full-date + full-time grammar — anchored.
|
|
50
|
+
//
|
|
51
|
+
// Capture groups:
|
|
52
|
+
// 1: year (4 digits) 2: month (2) 3: day (2)
|
|
53
|
+
// 4: hour (2) 5: minute (2) 6: second (2; allows 60 for leap-second)
|
|
54
|
+
// 7: fractional incl. dot (optional) 8: offset (Z or +HH:MM/-HH:MM)
|
|
55
|
+
var RFC3339_RE = /^(\d{4})-(\d{2})-(\d{2})[Tt ](\d{2}):(\d{2}):(\d{2})(\.\d+)?([Zz]|[+-]\d{2}:\d{2})?$/;
|
|
56
|
+
|
|
57
|
+
var DEFAULT_MIN_YEAR = 1970; // allow:raw-byte-literal — Unix epoch year
|
|
58
|
+
var DEFAULT_MAX_YEAR = 9999; // allow:raw-byte-literal — RFC 3339 4-digit year ceiling
|
|
59
|
+
var MAX_FRACTIONAL_DIGITS = 9; // allow:raw-byte-literal — nanosecond precision cap
|
|
60
|
+
|
|
61
|
+
// ---- Profile presets ----
|
|
62
|
+
|
|
63
|
+
var PROFILES = Object.freeze({
|
|
64
|
+
"strict": {
|
|
65
|
+
bidiPolicy: "reject",
|
|
66
|
+
controlPolicy: "reject",
|
|
67
|
+
nullBytePolicy: "reject",
|
|
68
|
+
zeroWidthPolicy: "reject",
|
|
69
|
+
naiveDatetimePolicy: "reject",
|
|
70
|
+
nonUtcOffsetPolicy: "reject",
|
|
71
|
+
leapSecondPolicy: "reject",
|
|
72
|
+
fractionalDigitsPolicy: "reject",
|
|
73
|
+
dateOnlyPolicy: "reject",
|
|
74
|
+
timeOnlyPolicy: "reject",
|
|
75
|
+
minYear: DEFAULT_MIN_YEAR,
|
|
76
|
+
maxYear: DEFAULT_MAX_YEAR,
|
|
77
|
+
maxFractionalDigits: MAX_FRACTIONAL_DIGITS,
|
|
78
|
+
maxBytes: C.BYTES.bytes(64),
|
|
79
|
+
maxRuntimeMs: C.TIME.seconds(2),
|
|
80
|
+
},
|
|
81
|
+
"balanced": {
|
|
82
|
+
bidiPolicy: "reject",
|
|
83
|
+
controlPolicy: "reject",
|
|
84
|
+
nullBytePolicy: "reject",
|
|
85
|
+
zeroWidthPolicy: "reject",
|
|
86
|
+
naiveDatetimePolicy: "reject",
|
|
87
|
+
nonUtcOffsetPolicy: "audit",
|
|
88
|
+
leapSecondPolicy: "audit",
|
|
89
|
+
fractionalDigitsPolicy: "audit",
|
|
90
|
+
dateOnlyPolicy: "audit",
|
|
91
|
+
timeOnlyPolicy: "audit",
|
|
92
|
+
minYear: DEFAULT_MIN_YEAR,
|
|
93
|
+
maxYear: DEFAULT_MAX_YEAR,
|
|
94
|
+
maxFractionalDigits: MAX_FRACTIONAL_DIGITS,
|
|
95
|
+
maxBytes: C.BYTES.bytes(64),
|
|
96
|
+
maxRuntimeMs: C.TIME.seconds(2),
|
|
97
|
+
},
|
|
98
|
+
"permissive": {
|
|
99
|
+
bidiPolicy: "reject", // BIDI refused at every profile
|
|
100
|
+
controlPolicy: "reject", // controls refused at every profile
|
|
101
|
+
nullBytePolicy: "reject", // null refused at every profile
|
|
102
|
+
zeroWidthPolicy: "reject", // zero-width refused at every profile
|
|
103
|
+
naiveDatetimePolicy: "audit",
|
|
104
|
+
nonUtcOffsetPolicy: "allow",
|
|
105
|
+
leapSecondPolicy: "allow",
|
|
106
|
+
fractionalDigitsPolicy: "allow",
|
|
107
|
+
dateOnlyPolicy: "allow",
|
|
108
|
+
timeOnlyPolicy: "allow",
|
|
109
|
+
minYear: DEFAULT_MIN_YEAR,
|
|
110
|
+
maxYear: DEFAULT_MAX_YEAR,
|
|
111
|
+
maxFractionalDigits: MAX_FRACTIONAL_DIGITS,
|
|
112
|
+
maxBytes: C.BYTES.bytes(64),
|
|
113
|
+
maxRuntimeMs: C.TIME.seconds(2),
|
|
114
|
+
},
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
var DEFAULTS = Object.freeze(Object.assign({}, PROFILES["strict"], {
|
|
118
|
+
mode: "enforce",
|
|
119
|
+
}));
|
|
120
|
+
|
|
121
|
+
var COMPLIANCE_POSTURES = Object.freeze({
|
|
122
|
+
"hipaa": Object.assign({}, PROFILES["strict"], {
|
|
123
|
+
forensicSnippetBytes: C.BYTES.bytes(128),
|
|
124
|
+
}),
|
|
125
|
+
"pci-dss": Object.assign({}, PROFILES["strict"], {
|
|
126
|
+
forensicSnippetBytes: C.BYTES.bytes(128),
|
|
127
|
+
}),
|
|
128
|
+
"gdpr": Object.assign({}, PROFILES["balanced"], {
|
|
129
|
+
forensicSnippetBytes: C.BYTES.bytes(64),
|
|
130
|
+
}),
|
|
131
|
+
"soc2": Object.assign({}, PROFILES["strict"], {
|
|
132
|
+
forensicSnippetBytes: C.BYTES.bytes(256),
|
|
133
|
+
}),
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
function _resolveOpts(opts) {
|
|
137
|
+
return gateContract.resolveProfileAndPosture(opts, {
|
|
138
|
+
profiles: PROFILES,
|
|
139
|
+
compliancePostures: COMPLIANCE_POSTURES,
|
|
140
|
+
defaults: DEFAULTS,
|
|
141
|
+
errorClass: GuardTimeError,
|
|
142
|
+
errCodePrefix: "time",
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// ---- Detection ----
|
|
147
|
+
|
|
148
|
+
function _detectIssues(input, opts) {
|
|
149
|
+
var issues = [];
|
|
150
|
+
if (typeof input !== "string") {
|
|
151
|
+
return [{ kind: "bad-input", severity: "high",
|
|
152
|
+
ruleId: "time.bad-input",
|
|
153
|
+
snippet: "time is not a string" }];
|
|
154
|
+
}
|
|
155
|
+
if (input.length === 0) {
|
|
156
|
+
return [{ kind: "empty", severity: "high",
|
|
157
|
+
ruleId: "time.empty",
|
|
158
|
+
snippet: "time is empty" }];
|
|
159
|
+
}
|
|
160
|
+
if (Buffer.byteLength(input, "utf8") > opts.maxBytes) {
|
|
161
|
+
return [{ kind: "time-cap", severity: "high",
|
|
162
|
+
ruleId: "time.time-cap",
|
|
163
|
+
snippet: "time input exceeds maxBytes " + opts.maxBytes }];
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
var charThreats = codepointClass.detectCharThreats(input, opts, "time");
|
|
167
|
+
for (var ci = 0; ci < charThreats.length; ci += 1) issues.push(charThreats[ci]);
|
|
168
|
+
|
|
169
|
+
// Date-only / time-only quick checks BEFORE the full RFC 3339 regex
|
|
170
|
+
// so the operator gets a more actionable diagnosis.
|
|
171
|
+
if (/^\d{4}-\d{2}-\d{2}$/.test(input)) { // allow:regex-no-length-cap — input bounded by maxBytes; allow:duplicate-regex — same RFC 3339 full-date shape used by safe-json + safe-schema; not consolidatable across module boundaries
|
|
172
|
+
if (opts.dateOnlyPolicy !== "allow") {
|
|
173
|
+
issues.push({
|
|
174
|
+
kind: "date-only",
|
|
175
|
+
severity: opts.dateOnlyPolicy === "reject" ? "high" : "warn",
|
|
176
|
+
ruleId: "time.date-only",
|
|
177
|
+
snippet: "input is RFC 3339 full-date only — full datetime " +
|
|
178
|
+
"(date + time + offset) required",
|
|
179
|
+
});
|
|
180
|
+
return issues;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
if (/^\d{2}:\d{2}:\d{2}(\.\d+)?(Z|[+-]\d{2}:\d{2})?$/.test(input)) { // allow:regex-no-length-cap — input bounded by maxBytes
|
|
184
|
+
if (opts.timeOnlyPolicy !== "allow") {
|
|
185
|
+
issues.push({
|
|
186
|
+
kind: "time-only",
|
|
187
|
+
severity: opts.timeOnlyPolicy === "reject" ? "high" : "warn",
|
|
188
|
+
ruleId: "time.time-only",
|
|
189
|
+
snippet: "input is RFC 3339 partial-time only — full datetime " +
|
|
190
|
+
"(date + time + offset) required",
|
|
191
|
+
});
|
|
192
|
+
return issues;
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
var match = input.match(RFC3339_RE); // allow:regex-no-length-cap — input bounded by maxBytes
|
|
197
|
+
if (!match) {
|
|
198
|
+
issues.push({
|
|
199
|
+
kind: "datetime-shape", severity: "high",
|
|
200
|
+
ruleId: "time.datetime-shape",
|
|
201
|
+
snippet: "input does not match RFC 3339 §5.6 date-time grammar",
|
|
202
|
+
});
|
|
203
|
+
return issues;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
var year = parseInt(match[1], 10); // allow:raw-byte-literal — base-10 radix
|
|
207
|
+
var month = parseInt(match[2], 10); // allow:raw-byte-literal — base-10 radix
|
|
208
|
+
var day = parseInt(match[3], 10); // allow:raw-byte-literal — base-10 radix
|
|
209
|
+
var hour = parseInt(match[4], 10); // allow:raw-byte-literal — base-10 radix
|
|
210
|
+
var minute = parseInt(match[5], 10); // allow:raw-byte-literal — base-10 radix
|
|
211
|
+
var second = parseInt(match[6], 10); // allow:raw-byte-literal — base-10 radix
|
|
212
|
+
var fractional = match[7] || "";
|
|
213
|
+
var offset = match[8];
|
|
214
|
+
|
|
215
|
+
// Year window.
|
|
216
|
+
if (year < opts.minYear || year > opts.maxYear) {
|
|
217
|
+
issues.push({
|
|
218
|
+
kind: "year-window", severity: "high",
|
|
219
|
+
ruleId: "time.year-window",
|
|
220
|
+
snippet: "year " + year + " outside operator window [" +
|
|
221
|
+
opts.minYear + ", " + opts.maxYear + "]",
|
|
222
|
+
});
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// Month / day / hour / minute structural ranges.
|
|
226
|
+
if (month < 1 || month > 12) { // allow:raw-byte-literal — month range
|
|
227
|
+
issues.push({
|
|
228
|
+
kind: "month-range", severity: "high",
|
|
229
|
+
ruleId: "time.month-range",
|
|
230
|
+
snippet: "month " + month + " outside [1, 12]",
|
|
231
|
+
});
|
|
232
|
+
}
|
|
233
|
+
if (day < 1 || day > 31) { // allow:raw-byte-literal — day-of-month upper bound
|
|
234
|
+
issues.push({
|
|
235
|
+
kind: "day-range", severity: "high",
|
|
236
|
+
ruleId: "time.day-range",
|
|
237
|
+
snippet: "day " + day + " outside [1, 31]",
|
|
238
|
+
});
|
|
239
|
+
}
|
|
240
|
+
if (hour > 23) { // allow:raw-byte-literal — hour ceiling
|
|
241
|
+
issues.push({
|
|
242
|
+
kind: "hour-range", severity: "high",
|
|
243
|
+
ruleId: "time.hour-range",
|
|
244
|
+
snippet: "hour " + hour + " > 23",
|
|
245
|
+
});
|
|
246
|
+
}
|
|
247
|
+
if (minute > 59) { // allow:raw-byte-literal — minute ceiling
|
|
248
|
+
issues.push({
|
|
249
|
+
kind: "minute-range", severity: "high",
|
|
250
|
+
ruleId: "time.minute-range",
|
|
251
|
+
snippet: "minute " + minute + " > 59",
|
|
252
|
+
});
|
|
253
|
+
}
|
|
254
|
+
if (second > 60) { // allow:raw-time-literal — leap-second ceiling, RFC 3339 §5.6 (not seconds-of-time)
|
|
255
|
+
issues.push({
|
|
256
|
+
kind: "second-range", severity: "high",
|
|
257
|
+
ruleId: "time.second-range",
|
|
258
|
+
snippet: "second " + second + " > 60 (RFC 3339 §5.6 ceiling " +
|
|
259
|
+
"including leap)",
|
|
260
|
+
});
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// Leap-second flag.
|
|
264
|
+
if (second === 60 && opts.leapSecondPolicy !== "allow") { // allow:raw-time-literal — leap-second sentinel, RFC 3339 §5.6
|
|
265
|
+
issues.push({
|
|
266
|
+
kind: "leap-second",
|
|
267
|
+
severity: opts.leapSecondPolicy === "reject" ? "high" : "warn",
|
|
268
|
+
ruleId: "time.leap-second",
|
|
269
|
+
snippet: "second field is 60 (leap second; RFC 3339 §5.6 valid " +
|
|
270
|
+
"but most parsers panic)",
|
|
271
|
+
});
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
// Day-in-month structural sanity (light — not full Gregorian
|
|
275
|
+
// rollover; the framework refuses obviously-out-of-bounds dates
|
|
276
|
+
// like Feb 30 / Apr 31).
|
|
277
|
+
var daysInMonth = [31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]; // allow:raw-byte-literal — Gregorian month-day table
|
|
278
|
+
if (month >= 1 && month <= 12 && day > daysInMonth[month - 1]) { // allow:raw-byte-literal — month range
|
|
279
|
+
issues.push({
|
|
280
|
+
kind: "day-in-month", severity: "high",
|
|
281
|
+
ruleId: "time.day-in-month",
|
|
282
|
+
snippet: "day " + day + " not valid in month " + month,
|
|
283
|
+
});
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
// Fractional digits cap.
|
|
287
|
+
var fracLen = fractional.length > 0 ? fractional.length - 1 : 0;
|
|
288
|
+
if (fracLen > opts.maxFractionalDigits &&
|
|
289
|
+
opts.fractionalDigitsPolicy !== "allow") {
|
|
290
|
+
issues.push({
|
|
291
|
+
kind: "fractional-digits",
|
|
292
|
+
severity: opts.fractionalDigitsPolicy === "reject" ? "high" : "warn",
|
|
293
|
+
ruleId: "time.fractional-digits",
|
|
294
|
+
snippet: "fractional precision " + fracLen + " exceeds " +
|
|
295
|
+
opts.maxFractionalDigits + " digits — downstream " +
|
|
296
|
+
"consumers may truncate or reject",
|
|
297
|
+
});
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
// Naive datetime (no offset).
|
|
301
|
+
if (!offset) {
|
|
302
|
+
if (opts.naiveDatetimePolicy !== "allow") {
|
|
303
|
+
issues.push({
|
|
304
|
+
kind: "naive-datetime",
|
|
305
|
+
severity: opts.naiveDatetimePolicy === "reject" ? "high" : "warn",
|
|
306
|
+
ruleId: "time.naive-datetime",
|
|
307
|
+
snippet: "datetime has no offset (`Z` or `+HH:MM`) — naive " +
|
|
308
|
+
"datetimes break cross-region equality",
|
|
309
|
+
});
|
|
310
|
+
}
|
|
311
|
+
} else {
|
|
312
|
+
// Non-UTC offset.
|
|
313
|
+
var isUtc = offset === "Z" || offset === "z" ||
|
|
314
|
+
offset === "+00:00" || offset === "-00:00";
|
|
315
|
+
if (!isUtc && opts.nonUtcOffsetPolicy !== "allow") {
|
|
316
|
+
issues.push({
|
|
317
|
+
kind: "non-utc-offset",
|
|
318
|
+
severity: opts.nonUtcOffsetPolicy === "reject" ? "high" : "warn",
|
|
319
|
+
ruleId: "time.non-utc-offset",
|
|
320
|
+
snippet: "datetime offset `" + offset + "` is not UTC — " +
|
|
321
|
+
"strict requires `Z` or `+00:00` for unambiguous " +
|
|
322
|
+
"cross-system comparison",
|
|
323
|
+
});
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
return issues;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
function validate(input, opts) {
|
|
331
|
+
opts = _resolveOpts(opts);
|
|
332
|
+
numericBounds.requireAllPositiveFiniteIntIfPresent(opts,
|
|
333
|
+
["maxBytes", "minYear", "maxYear", "maxFractionalDigits"],
|
|
334
|
+
"guardTime.validate", GuardTimeError, "time.bad-opt");
|
|
335
|
+
if (typeof input !== "string") {
|
|
336
|
+
return {
|
|
337
|
+
ok: false,
|
|
338
|
+
issues: [{ kind: "bad-input", severity: "high",
|
|
339
|
+
ruleId: "time.bad-input",
|
|
340
|
+
snippet: "time is not a string" }],
|
|
341
|
+
};
|
|
342
|
+
}
|
|
343
|
+
return gateContract.aggregateIssues(_detectIssues(input, opts));
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
function sanitize(input, opts) {
|
|
347
|
+
opts = _resolveOpts(opts);
|
|
348
|
+
if (typeof input !== "string") {
|
|
349
|
+
throw _err("time.bad-input", "sanitize requires string input");
|
|
350
|
+
}
|
|
351
|
+
var issues = _detectIssues(input, opts);
|
|
352
|
+
for (var i = 0; i < issues.length; i += 1) {
|
|
353
|
+
if (issues[i].severity === "critical" || issues[i].severity === "high") {
|
|
354
|
+
throw _err(issues[i].ruleId || "time.refused",
|
|
355
|
+
"guardTime.sanitize: " + issues[i].snippet);
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
// Normalize: lowercase the trailing `T` separator, uppercase the
|
|
359
|
+
// `Z` UTC marker.
|
|
360
|
+
return input.replace(/(\d) /, "$1T").replace(/z$/, "Z");
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
function gate(opts) {
|
|
364
|
+
opts = _resolveOpts(opts);
|
|
365
|
+
return gateContract.buildGuardGate(
|
|
366
|
+
opts.name || "guardTime:" + (opts.profile || "default"),
|
|
367
|
+
opts,
|
|
368
|
+
async function (ctx) {
|
|
369
|
+
var identifier = ctx && (ctx.identifier || ctx.timestamp || ctx.time || "");
|
|
370
|
+
if (!identifier) return { ok: true, action: "serve" };
|
|
371
|
+
var rv = validate(identifier, opts);
|
|
372
|
+
if (rv.issues.length === 0) return { ok: true, action: "serve" };
|
|
373
|
+
var hasCritical = rv.issues.some(function (i) {
|
|
374
|
+
return i.severity === "critical";
|
|
375
|
+
});
|
|
376
|
+
var hasHigh = rv.issues.some(function (i) {
|
|
377
|
+
return i.severity === "high";
|
|
378
|
+
});
|
|
379
|
+
if (!hasCritical && !hasHigh) {
|
|
380
|
+
return { ok: true, action: "audit-only", issues: rv.issues };
|
|
381
|
+
}
|
|
382
|
+
return { ok: false, action: "refuse", issues: rv.issues };
|
|
383
|
+
});
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
var buildProfile = gateContract.makeProfileBuilder(PROFILES);
|
|
387
|
+
|
|
388
|
+
function compliancePosture(name) {
|
|
389
|
+
return gateContract.lookupCompliancePosture(name, COMPLIANCE_POSTURES,
|
|
390
|
+
_err, "time");
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
var _timeRulePacks = gateContract.makeRulePackLoader(GuardTimeError, "time");
|
|
394
|
+
var loadRulePack = _timeRulePacks.load;
|
|
395
|
+
|
|
396
|
+
module.exports = {
|
|
397
|
+
// ---- guard-* family registry exports ----
|
|
398
|
+
NAME: "time",
|
|
399
|
+
KIND: "identifier",
|
|
400
|
+
INTEGRATION_FIXTURES: Object.freeze({
|
|
401
|
+
kind: "identifier",
|
|
402
|
+
benignBytes: Buffer.from("2026-05-05T12:34:56Z", "utf8"),
|
|
403
|
+
hostileBytes: Buffer.from("2026-05-05 12:34:56", "utf8"),
|
|
404
|
+
benignIdentifier: "2026-05-05T12:34:56Z",
|
|
405
|
+
// Hostile: naive datetime (space separator + no offset) — refused
|
|
406
|
+
// at strict (cross-region ambiguity class).
|
|
407
|
+
hostileIdentifier: "2026-05-05 12:34:56",
|
|
408
|
+
}),
|
|
409
|
+
// ---- primitive surface ----
|
|
410
|
+
validate: validate,
|
|
411
|
+
sanitize: sanitize,
|
|
412
|
+
gate: gate,
|
|
413
|
+
buildProfile: buildProfile,
|
|
414
|
+
compliancePosture: compliancePosture,
|
|
415
|
+
loadRulePack: loadRulePack,
|
|
416
|
+
PROFILES: PROFILES,
|
|
417
|
+
DEFAULTS: DEFAULTS,
|
|
418
|
+
COMPLIANCE_POSTURES: COMPLIANCE_POSTURES,
|
|
419
|
+
GuardTimeError: GuardTimeError,
|
|
420
|
+
};
|
package/package.json
CHANGED
package/sbom.cyclonedx.json
CHANGED
|
@@ -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:
|
|
5
|
+
"serialNumber": "urn:uuid:2e0e50c0-9f28-4f7f-9a4e-4be5cb7f3707",
|
|
6
6
|
"version": 1,
|
|
7
7
|
"metadata": {
|
|
8
|
-
"timestamp": "2026-05-05T21:
|
|
8
|
+
"timestamp": "2026-05-05T21:40:55.433Z",
|
|
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.
|
|
22
|
+
"bom-ref": "@blamejs/core@0.7.46",
|
|
23
23
|
"type": "library",
|
|
24
24
|
"name": "blamejs",
|
|
25
|
-
"version": "0.7.
|
|
25
|
+
"version": "0.7.46",
|
|
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.
|
|
29
|
+
"purl": "pkg:npm/%40blamejs/core@0.7.46",
|
|
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.
|
|
57
|
+
"ref": "@blamejs/core@0.7.46",
|
|
58
58
|
"dependsOn": []
|
|
59
59
|
}
|
|
60
60
|
]
|