@blamejs/core 0.18.43 → 0.18.44
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +106 -0
- package/NOTICE +1 -1
- package/README.md +3 -2
- package/index.js +2 -0
- package/lib/chain-writer.js +103 -4
- package/lib/file-type.js +19 -2
- package/lib/framework-error.js +10 -0
- package/lib/gate-contract.js +255 -12
- package/lib/guard-all.js +1 -0
- package/lib/guard-archive.js +14 -0
- package/lib/guard-auth.js +1 -3
- package/lib/guard-country.js +480 -0
- package/lib/guard-csv.js +5 -0
- package/lib/guard-email.js +7 -7
- package/lib/guard-filename.js +11 -9
- package/lib/guard-graphql.js +1 -3
- package/lib/guard-html.js +6 -7
- package/lib/guard-image.js +15 -1
- package/lib/guard-json.js +3 -7
- package/lib/guard-jwt.js +8 -3
- package/lib/guard-markdown.js +946 -2
- package/lib/guard-oauth.js +1 -3
- package/lib/guard-pdf.js +8 -0
- package/lib/guard-regex.js +5 -4
- package/lib/guard-smtp-command.js +4 -2
- package/lib/guard-sql.js +10 -0
- package/lib/guard-svg.js +2 -1
- package/lib/guard-text.js +10 -0
- package/lib/guard-yaml.js +13 -2
- package/lib/middleware/index.js +3 -0
- package/lib/middleware/shared-secret-header.js +334 -0
- package/lib/time.js +282 -0
- package/lib/vendor/MANIFEST.json +12 -12
- package/lib/vendor/blamejs-pki.cjs +858 -304
- package/package.json +1 -1
- package/sbom.cdx.json +6 -6
|
@@ -0,0 +1,480 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
// Copyright (c) blamejs contributors
|
|
3
|
+
"use strict";
|
|
4
|
+
/**
|
|
5
|
+
* @module b.guardCountry
|
|
6
|
+
* @nav Guards
|
|
7
|
+
* @title Guard Country
|
|
8
|
+
*
|
|
9
|
+
* @intro
|
|
10
|
+
* ISO 3166-1 alpha-2 country-code guard. Accepts the 249
|
|
11
|
+
* officially assigned codes and refuses everything else, from a
|
|
12
|
+
* bundled table. KIND="identifier" - the gate consumes
|
|
13
|
+
* `ctx.identifier` (or `ctx.country` / `ctx.countryCode`).
|
|
14
|
+
*
|
|
15
|
+
* The framework already routes on these codes: compliance
|
|
16
|
+
* postures are keyed by jurisdiction, and data-residency, tax
|
|
17
|
+
* nexus, geo-restriction and DSR-jurisdiction decisions all turn
|
|
18
|
+
* on one. A two-letter string that looks like a country but is
|
|
19
|
+
* not one produces a wrong answer at every one of those, wearing
|
|
20
|
+
* the shape of a right one.
|
|
21
|
+
*
|
|
22
|
+
* Threat catalog: not-a-code (`UK` - the United Kingdom is `GB`);
|
|
23
|
+
* user-assigned codes (`AA`, `QM`-`QZ`, `XA`-`XZ`, `ZZ`, which
|
|
24
|
+
* includes CLDR's unknown-region sentinel and `XK` for Kosovo);
|
|
25
|
+
* exceptionally reserved codes (`EU`, `EZ`, `UN`, `AC`, `CP`,
|
|
26
|
+
* `CQ`, `DG`, `EA`, `IC`, `TA`) that name unions, organisations
|
|
27
|
+
* and territories rather than countries; formerly used codes
|
|
28
|
+
* (`AN`, `BU`, `CS`, `DD`, `FX`, `NT`, `SU`, `TP`, `YD`, `YU`,
|
|
29
|
+
* `ZR`) that a legacy dataset still carries; BIDI / zero-width /
|
|
30
|
+
* C0-control / null-byte universal-refuse; and homoglyph or
|
|
31
|
+
* fullwidth spellings that render as a valid code.
|
|
32
|
+
*
|
|
33
|
+
* The answer comes from the bundled table and never from `Intl`.
|
|
34
|
+
* A `small-icu` or `no-icu` Node build has no
|
|
35
|
+
* `Intl.DisplayNames`, so the widespread hand-rolled version -
|
|
36
|
+
* which asks `DisplayNames` whether a code echoes back and
|
|
37
|
+
* returns true from its `catch` - accepts every two-letter string
|
|
38
|
+
* on those runtimes. That is a deployment-shaped behaviour change
|
|
39
|
+
* with no signal.
|
|
40
|
+
*
|
|
41
|
+
* Profiles: `strict` / `balanced` / `permissive`. Compliance
|
|
42
|
+
* postures: `hipaa` / `pci-dss` / `gdpr` / `soc2`.
|
|
43
|
+
*
|
|
44
|
+
* @card
|
|
45
|
+
* ISO 3166-1 alpha-2 country-code guard, answered from a bundled table.
|
|
46
|
+
*/
|
|
47
|
+
|
|
48
|
+
var gateContract = require("./gate-contract");
|
|
49
|
+
var codepointClass = require("./codepoint-class");
|
|
50
|
+
var C = require("./constants");
|
|
51
|
+
var { GuardCountryError } = require("./framework-error");
|
|
52
|
+
|
|
53
|
+
void GuardCountryError;
|
|
54
|
+
|
|
55
|
+
// ---- Code tables ----------------------------------------------------------
|
|
56
|
+
//
|
|
57
|
+
// The 249 officially assigned ISO 3166-1 alpha-2 codes.
|
|
58
|
+
//
|
|
59
|
+
// Derived, not recalled. Two independent machine-readable sources were read
|
|
60
|
+
// and diffed, and they agree on exactly this set:
|
|
61
|
+
//
|
|
62
|
+
// - the IANA language-subtag-registry (File-Date 2026-08-08): its 261 live
|
|
63
|
+
// two-letter `Type: region` subtags, minus the twelve named in
|
|
64
|
+
// USER_ASSIGNED / EXCEPTIONALLY_RESERVED below;
|
|
65
|
+
// - Unicode CLDR common/validity/region.xml: its 257 `idStatus='regular'`
|
|
66
|
+
// entries, minus the eight non-country codes CLDR keeps there because it
|
|
67
|
+
// formats them (AC, CP, CQ, DG, EA, IC, TA and XK).
|
|
68
|
+
//
|
|
69
|
+
// Neither source carries ISO assignment status on its own - in the IANA
|
|
70
|
+
// registry, AC (exceptionally reserved) and GB (officially assigned) are
|
|
71
|
+
// byte-identical record shapes - which is why the exclusions are enumerated
|
|
72
|
+
// here rather than filtered on a status field that does not exist. Both
|
|
73
|
+
// derivations land on 249, the published count of officially assigned
|
|
74
|
+
// alpha-2 codes.
|
|
75
|
+
//
|
|
76
|
+
// Refreshing: re-derive from both sources and diff before editing. A code
|
|
77
|
+
// added to one source and not the other is a signal, not a merge conflict.
|
|
78
|
+
var ASSIGNED_CODES = Object.freeze([
|
|
79
|
+
"AD", "AE", "AF", "AG", "AI", "AL", "AM", "AO", "AQ", "AR", "AS", "AT",
|
|
80
|
+
"AU", "AW", "AX", "AZ", "BA", "BB", "BD", "BE", "BF", "BG", "BH", "BI",
|
|
81
|
+
"BJ", "BL", "BM", "BN", "BO", "BQ", "BR", "BS", "BT", "BV", "BW", "BY",
|
|
82
|
+
"BZ", "CA", "CC", "CD", "CF", "CG", "CH", "CI", "CK", "CL", "CM", "CN",
|
|
83
|
+
"CO", "CR", "CU", "CV", "CW", "CX", "CY", "CZ", "DE", "DJ", "DK", "DM",
|
|
84
|
+
"DO", "DZ", "EC", "EE", "EG", "EH", "ER", "ES", "ET", "FI", "FJ", "FK",
|
|
85
|
+
"FM", "FO", "FR", "GA", "GB", "GD", "GE", "GF", "GG", "GH", "GI", "GL",
|
|
86
|
+
"GM", "GN", "GP", "GQ", "GR", "GS", "GT", "GU", "GW", "GY", "HK", "HM",
|
|
87
|
+
"HN", "HR", "HT", "HU", "ID", "IE", "IL", "IM", "IN", "IO", "IQ", "IR",
|
|
88
|
+
"IS", "IT", "JE", "JM", "JO", "JP", "KE", "KG", "KH", "KI", "KM", "KN",
|
|
89
|
+
"KP", "KR", "KW", "KY", "KZ", "LA", "LB", "LC", "LI", "LK", "LR", "LS",
|
|
90
|
+
"LT", "LU", "LV", "LY", "MA", "MC", "MD", "ME", "MF", "MG", "MH", "MK",
|
|
91
|
+
"ML", "MM", "MN", "MO", "MP", "MQ", "MR", "MS", "MT", "MU", "MV", "MW",
|
|
92
|
+
"MX", "MY", "MZ", "NA", "NC", "NE", "NF", "NG", "NI", "NL", "NO", "NP",
|
|
93
|
+
"NR", "NU", "NZ", "OM", "PA", "PE", "PF", "PG", "PH", "PK", "PL", "PM",
|
|
94
|
+
"PN", "PR", "PS", "PT", "PW", "PY", "QA", "RE", "RO", "RS", "RU", "RW",
|
|
95
|
+
"SA", "SB", "SC", "SD", "SE", "SG", "SH", "SI", "SJ", "SK", "SL", "SM",
|
|
96
|
+
"SN", "SO", "SR", "SS", "ST", "SV", "SX", "SY", "SZ", "TC", "TD", "TF",
|
|
97
|
+
"TG", "TH", "TJ", "TK", "TL", "TM", "TN", "TO", "TR", "TT", "TV", "TW",
|
|
98
|
+
"TZ", "UA", "UG", "UM", "US", "UY", "UZ", "VA", "VC", "VE", "VG", "VI",
|
|
99
|
+
"VN", "VU", "WF", "WS", "YE", "YT", "ZA", "ZM", "ZW",
|
|
100
|
+
]);
|
|
101
|
+
|
|
102
|
+
var ASSIGNED = new Set(ASSIGNED_CODES);
|
|
103
|
+
|
|
104
|
+
// ISO 3166-1 exceptionally reserved: names an entity that is not a country,
|
|
105
|
+
// or a territory that already resolves to one. Present in the IANA registry
|
|
106
|
+
// because BCP 47 needs them for locale data, absent from ISO's assigned list.
|
|
107
|
+
var EXCEPTIONALLY_RESERVED = Object.freeze({
|
|
108
|
+
AC: "Ascension Island",
|
|
109
|
+
CP: "Clipperton Island",
|
|
110
|
+
CQ: "Sark",
|
|
111
|
+
DG: "Diego Garcia",
|
|
112
|
+
EA: "Ceuta and Melilla",
|
|
113
|
+
EU: "European Union",
|
|
114
|
+
EZ: "Eurozone",
|
|
115
|
+
IC: "Canary Islands",
|
|
116
|
+
TA: "Tristan da Cunha",
|
|
117
|
+
UN: "United Nations",
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
// Codes the IANA language-subtag-registry marks Deprecated. Where the record
|
|
121
|
+
// carries a Preferred-Value it is reproduced verbatim; where it does not, the
|
|
122
|
+
// value here is null and no successor is stated. NT (Neutral Zone), AN
|
|
123
|
+
// (Netherlands Antilles), CS (Serbia and Montenegro) and YU (Yugoslavia)
|
|
124
|
+
// dissolved into several successors, so the registry names none and neither
|
|
125
|
+
// does this guard.
|
|
126
|
+
var FORMERLY_USED = Object.freeze({
|
|
127
|
+
AN: null, // Netherlands Antilles
|
|
128
|
+
BU: "MM", // Burma
|
|
129
|
+
CS: null, // Serbia and Montenegro
|
|
130
|
+
DD: "DE", // German Democratic Republic
|
|
131
|
+
FX: "FR", // Metropolitan France
|
|
132
|
+
NT: null, // Neutral Zone
|
|
133
|
+
SU: null, // Union of Soviet Socialist Republics
|
|
134
|
+
TP: "TL", // East Timor
|
|
135
|
+
YD: "YE", // Democratic Yemen
|
|
136
|
+
YU: null, // Yugoslavia
|
|
137
|
+
ZR: "CD", // Zaire
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
// UK is the single most common wrong answer: it is the ccTLD and the everyday
|
|
141
|
+
// abbreviation, and it is not an ISO 3166-1 code at all.
|
|
142
|
+
var COMMON_MISTAKES = Object.freeze({ UK: "GB" });
|
|
143
|
+
|
|
144
|
+
// ---- Profile presets ------------------------------------------------------
|
|
145
|
+
|
|
146
|
+
var PROFILES = Object.freeze({
|
|
147
|
+
"strict": {
|
|
148
|
+
...gateContract.CHAR_THREATS_REJECT_ALL,
|
|
149
|
+
reservedPolicy: "reject", // reject | audit | allow
|
|
150
|
+
userAssignedPolicy: "reject", // reject | audit | allow
|
|
151
|
+
formerlyUsedPolicy: "reject", // reject | audit | allow
|
|
152
|
+
maxBytes: C.BYTES.bytes(16),
|
|
153
|
+
maxRuntimeMs: C.TIME.seconds(2),
|
|
154
|
+
},
|
|
155
|
+
"balanced": {
|
|
156
|
+
...gateContract.CHAR_THREATS_REJECT_ALL,
|
|
157
|
+
// A formerly-used code is a data-age signal, not an attack: an operator
|
|
158
|
+
// migrating records written before 1993 wants to see them, not to have
|
|
159
|
+
// the import refused row by row. A user-assigned or reserved code still
|
|
160
|
+
// refuses - those are answers a routing decision must never act on.
|
|
161
|
+
reservedPolicy: "reject",
|
|
162
|
+
userAssignedPolicy: "reject",
|
|
163
|
+
formerlyUsedPolicy: "audit",
|
|
164
|
+
maxBytes: C.BYTES.bytes(16),
|
|
165
|
+
maxRuntimeMs: C.TIME.seconds(2),
|
|
166
|
+
},
|
|
167
|
+
"permissive": {
|
|
168
|
+
...gateContract.CHAR_THREATS_REJECT_ALL,
|
|
169
|
+
reservedPolicy: "audit",
|
|
170
|
+
userAssignedPolicy: "audit",
|
|
171
|
+
formerlyUsedPolicy: "audit",
|
|
172
|
+
maxBytes: C.BYTES.bytes(16),
|
|
173
|
+
maxRuntimeMs: C.TIME.seconds(2),
|
|
174
|
+
},
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
var DEFAULTS = gateContract.strictDefaults(PROFILES);
|
|
178
|
+
|
|
179
|
+
var COMPLIANCE_POSTURES = gateContract.compliancePostures(PROFILES, { base: 32 });
|
|
180
|
+
|
|
181
|
+
// ---- Detection ------------------------------------------------------------
|
|
182
|
+
|
|
183
|
+
// ISO 3166-1 user-assigned codes: AA, QM-QZ, XA-XZ and ZZ. These are reserved
|
|
184
|
+
// for private use, so two systems can both use one and mean different things.
|
|
185
|
+
// CLDR spends ZZ on "unknown region" and XK on Kosovo; neither is a promise
|
|
186
|
+
// any other system keeps.
|
|
187
|
+
function _isUserAssigned(code) {
|
|
188
|
+
if (code === "AA" || code === "ZZ") return true;
|
|
189
|
+
var first = code.charCodeAt(0);
|
|
190
|
+
var second = code.charCodeAt(1);
|
|
191
|
+
// Q M..Z
|
|
192
|
+
if (first === 0x51 && second >= 0x4D && second <= 0x5A) return true;
|
|
193
|
+
// X A..Z
|
|
194
|
+
if (first === 0x58) return true;
|
|
195
|
+
return false;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// Exactly two ASCII letters, in either case. Deliberately not a character
|
|
199
|
+
// class over "letters": a fullwidth U+FF35 or a Cyrillic U+0413 renders as a
|
|
200
|
+
// Latin letter and must not pass.
|
|
201
|
+
function _isTwoAsciiLetters(input) {
|
|
202
|
+
if (input.length !== 2) return false;
|
|
203
|
+
for (var i = 0; i < 2; i += 1) {
|
|
204
|
+
if (!codepointClass.isAsciiLetter(input.charCodeAt(i))) return false;
|
|
205
|
+
}
|
|
206
|
+
return true;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function _upper(input) {
|
|
210
|
+
var out = "";
|
|
211
|
+
for (var i = 0; i < input.length; i += 1) {
|
|
212
|
+
var cc = input.charCodeAt(i);
|
|
213
|
+
out += (cc >= 0x61 && cc <= 0x7A) ? String.fromCharCode(cc - 32) : input.charAt(i);
|
|
214
|
+
}
|
|
215
|
+
return out;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
var POLICY_VALUES = ["reject", "audit", "allow"];
|
|
219
|
+
|
|
220
|
+
// These decide whether a code is refused, so a value outside the enum is
|
|
221
|
+
// config-time input to throw on, not something to interpret. Read leniently, a
|
|
222
|
+
// typo took the "not reject" branch and turned a refusal into an audit entry:
|
|
223
|
+
// `{ reservedPolicy: "rejcet" }` served `EU` and reported ok.
|
|
224
|
+
//
|
|
225
|
+
// Declared as `enumOpts` so the check runs where options are RESOLVED. Putting
|
|
226
|
+
// it on the detection path would have left gate() and resolveOpts() accepting
|
|
227
|
+
// the typo at boot and only failing once requests arrived, which is the
|
|
228
|
+
// opposite of what a config-time refusal is for.
|
|
229
|
+
var POLICY_ENUM = {
|
|
230
|
+
reservedPolicy: POLICY_VALUES,
|
|
231
|
+
userAssignedPolicy: POLICY_VALUES,
|
|
232
|
+
formerlyUsedPolicy: POLICY_VALUES,
|
|
233
|
+
};
|
|
234
|
+
|
|
235
|
+
function _severity(policy) { return policy === "reject" ? "high" : "warn"; }
|
|
236
|
+
|
|
237
|
+
// `allow` means the operator has decided this class of code is fine here - a
|
|
238
|
+
// residency router that deliberately accepts `EU`, say. Emitting a warn finding
|
|
239
|
+
// anyway made `allow` and `audit` the same setting, because any finding at all
|
|
240
|
+
// dispositions the gate to audit-only. The rest of the family skips the emit
|
|
241
|
+
// when the policy says allow; this says the same thing once for all three
|
|
242
|
+
// classes rather than at each of their emit sites.
|
|
243
|
+
function _allowed(policy) { return policy === "allow"; }
|
|
244
|
+
|
|
245
|
+
function _detectIssues(input, opts) {
|
|
246
|
+
var pre = gateContract.detectStringInput(input, opts, {
|
|
247
|
+
name: "country", cap: { bytes: opts.maxBytes },
|
|
248
|
+
});
|
|
249
|
+
if (pre.done) return pre.issues;
|
|
250
|
+
var issues = pre.issues;
|
|
251
|
+
|
|
252
|
+
if (!_isTwoAsciiLetters(input)) {
|
|
253
|
+
issues.push({
|
|
254
|
+
kind: "country-shape", severity: "high",
|
|
255
|
+
ruleId: "country.country-shape",
|
|
256
|
+
snippet: "input is not two ASCII letters, so it cannot be an " +
|
|
257
|
+
"ISO 3166-1 alpha-2 code",
|
|
258
|
+
});
|
|
259
|
+
return issues;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
var code = _upper(input);
|
|
263
|
+
if (ASSIGNED.has(code)) return issues;
|
|
264
|
+
|
|
265
|
+
// Not assigned. Say WHICH non-assignment it is, but only where a record can
|
|
266
|
+
// be pointed at. ISO also reserves codes this guard has no machine-readable
|
|
267
|
+
// source for (the indeterminately reserved set), so an unrecognised code is
|
|
268
|
+
// reported as not-assigned rather than claimed to be unassigned - the
|
|
269
|
+
// second is a statement about ISO that this table cannot support.
|
|
270
|
+
if (Object.prototype.hasOwnProperty.call(COMMON_MISTAKES, code)) {
|
|
271
|
+
issues.push({
|
|
272
|
+
kind: "country-not-assigned", severity: "high",
|
|
273
|
+
ruleId: "country.country-not-assigned",
|
|
274
|
+
snippet: "`" + code + "` is not an ISO 3166-1 alpha-2 code; the code " +
|
|
275
|
+
"for that country is `" + COMMON_MISTAKES[code] + "`",
|
|
276
|
+
});
|
|
277
|
+
return issues;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
if (_isUserAssigned(code)) {
|
|
281
|
+
if (_allowed(opts.userAssignedPolicy)) return issues;
|
|
282
|
+
issues.push({
|
|
283
|
+
kind: "country-user-assigned", severity: _severity(opts.userAssignedPolicy),
|
|
284
|
+
ruleId: "country.country-user-assigned",
|
|
285
|
+
snippet: "`" + code + "` is in an ISO 3166-1 user-assigned range " +
|
|
286
|
+
"(AA, QM-QZ, XA-XZ, ZZ) - reserved for private use, so it " +
|
|
287
|
+
"means whatever the system that wrote it decided",
|
|
288
|
+
});
|
|
289
|
+
return issues;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
if (Object.prototype.hasOwnProperty.call(EXCEPTIONALLY_RESERVED, code)) {
|
|
293
|
+
if (_allowed(opts.reservedPolicy)) return issues;
|
|
294
|
+
issues.push({
|
|
295
|
+
kind: "country-exceptionally-reserved", severity: _severity(opts.reservedPolicy),
|
|
296
|
+
ruleId: "country.country-exceptionally-reserved",
|
|
297
|
+
snippet: "`" + code + "` is exceptionally reserved for " +
|
|
298
|
+
EXCEPTIONALLY_RESERVED[code] + ", which is not a country",
|
|
299
|
+
});
|
|
300
|
+
return issues;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
if (Object.prototype.hasOwnProperty.call(FORMERLY_USED, code)) {
|
|
304
|
+
if (_allowed(opts.formerlyUsedPolicy)) return issues;
|
|
305
|
+
var successor = FORMERLY_USED[code];
|
|
306
|
+
issues.push({
|
|
307
|
+
kind: "country-formerly-used", severity: _severity(opts.formerlyUsedPolicy),
|
|
308
|
+
ruleId: "country.country-formerly-used",
|
|
309
|
+
snippet: "`" + code + "` is a formerly used code, withdrawn from " +
|
|
310
|
+
"ISO 3166-1" + (successor
|
|
311
|
+
? "; the registry records it as replaced by `" + successor + "`"
|
|
312
|
+
: "; no single successor is recorded"),
|
|
313
|
+
});
|
|
314
|
+
return issues;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
issues.push({
|
|
318
|
+
kind: "country-not-assigned", severity: "high",
|
|
319
|
+
ruleId: "country.country-not-assigned",
|
|
320
|
+
snippet: "`" + code + "` is not an officially assigned ISO 3166-1 " +
|
|
321
|
+
"alpha-2 code",
|
|
322
|
+
});
|
|
323
|
+
return issues;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
/**
|
|
327
|
+
* @primitive b.guardCountry.validate
|
|
328
|
+
* @signature b.guardCountry.validate(input, opts?)
|
|
329
|
+
* @since 0.18.44
|
|
330
|
+
* @status stable
|
|
331
|
+
* @compliance hipaa, pci-dss, gdpr, soc2
|
|
332
|
+
* @related b.guardCountry.sanitize, b.guardCountry.isValid, b.guardCountry.gate
|
|
333
|
+
*
|
|
334
|
+
* Inspect a country code against the resolved profile and return
|
|
335
|
+
* `{ ok, issues }`. Each issue carries `kind` / `severity`
|
|
336
|
+
* (`critical` | `high` | `medium` | `low`) / `ruleId` / `snippet`.
|
|
337
|
+
* Non-string input returns a single `country.bad-input` issue
|
|
338
|
+
* rather than throwing - callers that prefer an exception use
|
|
339
|
+
* `b.guardCountry.sanitize`.
|
|
340
|
+
*
|
|
341
|
+
* Issue kinds name only what a record can be pointed at:
|
|
342
|
+
* `country-user-assigned`, `country-exceptionally-reserved`,
|
|
343
|
+
* `country-formerly-used`, and `country-not-assigned` for anything
|
|
344
|
+
* else well-formed. There is deliberately no "unassigned" kind -
|
|
345
|
+
* ISO reserves codes this table has no source for, so the stronger
|
|
346
|
+
* claim would be unsupported.
|
|
347
|
+
*
|
|
348
|
+
* @opts
|
|
349
|
+
* profile: "strict"|"balanced"|"permissive",
|
|
350
|
+
* compliancePosture: "hipaa"|"pci-dss"|"gdpr"|"soc2",
|
|
351
|
+
* bidiPolicy: "reject"|"strip"|"audit"|"allow",
|
|
352
|
+
* controlPolicy: "reject"|"strip"|"allow",
|
|
353
|
+
* nullBytePolicy: "reject"|"strip"|"allow",
|
|
354
|
+
* zeroWidthPolicy: "reject"|"strip"|"allow",
|
|
355
|
+
* reservedPolicy: "reject"|"audit"|"allow",
|
|
356
|
+
* userAssignedPolicy: "reject"|"audit"|"allow",
|
|
357
|
+
* formerlyUsedPolicy: "reject"|"audit"|"allow",
|
|
358
|
+
* maxBytes: number,
|
|
359
|
+
*
|
|
360
|
+
* @example
|
|
361
|
+
* b.guardCountry.validate("GB", { profile: "strict" }).ok; // -> true
|
|
362
|
+
*
|
|
363
|
+
* var uk = b.guardCountry.validate("UK", { profile: "strict" });
|
|
364
|
+
* uk.ok; // -> false
|
|
365
|
+
* uk.issues[0].ruleId; // -> "country.country-not-assigned"
|
|
366
|
+
*/
|
|
367
|
+
// validate is assembled by gateContract.defineGuard from `detect`
|
|
368
|
+
// (_detectIssues) above.
|
|
369
|
+
|
|
370
|
+
/**
|
|
371
|
+
* @primitive b.guardCountry.sanitize
|
|
372
|
+
* @signature b.guardCountry.sanitize(input, opts?)
|
|
373
|
+
* @since 0.18.44
|
|
374
|
+
* @status stable
|
|
375
|
+
* @related b.guardCountry.validate, b.guardCountry.isValid
|
|
376
|
+
*
|
|
377
|
+
* Normalize a country code to its canonical uppercase form and
|
|
378
|
+
* return it. Throws `GuardCountryError` when any `critical` or
|
|
379
|
+
* `high` issue fires - a malformed shape, or a code the resolved
|
|
380
|
+
* profile refuses. Use `validate` to inspect issues without
|
|
381
|
+
* throwing.
|
|
382
|
+
*
|
|
383
|
+
* @opts
|
|
384
|
+
* profile: "strict"|"balanced"|"permissive",
|
|
385
|
+
* compliancePosture: "hipaa"|"pci-dss"|"gdpr"|"soc2",
|
|
386
|
+
* ...: same shape as b.guardCountry.validate opts,
|
|
387
|
+
*
|
|
388
|
+
* @example
|
|
389
|
+
* b.guardCountry.sanitize("gb", { profile: "strict" }); // -> "GB"
|
|
390
|
+
*
|
|
391
|
+
* try {
|
|
392
|
+
* b.guardCountry.sanitize("ZZ", { profile: "strict" });
|
|
393
|
+
* } catch (e) {
|
|
394
|
+
* e.code; // -> "country.country-user-assigned"
|
|
395
|
+
* }
|
|
396
|
+
*/
|
|
397
|
+
function _sanitizeTransform(input) {
|
|
398
|
+
return _upper(input);
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
/**
|
|
402
|
+
* @primitive b.guardCountry.isValid
|
|
403
|
+
* @signature b.guardCountry.isValid(input)
|
|
404
|
+
* @since 0.18.44
|
|
405
|
+
* @status stable
|
|
406
|
+
* @related b.guardCountry.validate, b.guardCountry.sanitize
|
|
407
|
+
*
|
|
408
|
+
* Whether `input` is an officially assigned ISO 3166-1 alpha-2
|
|
409
|
+
* code, as a boolean. Case-insensitive; never throws, so a
|
|
410
|
+
* non-string is `false` rather than an exception.
|
|
411
|
+
*
|
|
412
|
+
* Takes NO profile, deliberately. The profiles differ in how they
|
|
413
|
+
* DISPOSE of a non-assigned code — `strict` refuses it, `balanced`
|
|
414
|
+
* and `permissive` downgrade some findings to a warning — and none
|
|
415
|
+
* of them changes whether ISO assigned the code. A predicate that
|
|
416
|
+
* accepted a profile would answer `true` for `EU`, `ZZ` or `SU`
|
|
417
|
+
* under `permissive`, which is exactly the reserved-and-sentinel
|
|
418
|
+
* value a residency or jurisdiction decision must never act on.
|
|
419
|
+
*
|
|
420
|
+
* Reach for `validate(input, { profile })` when the
|
|
421
|
+
* profile-dependent disposition is what you want, and for the
|
|
422
|
+
* reason a code was refused.
|
|
423
|
+
*
|
|
424
|
+
* @example
|
|
425
|
+
* b.guardCountry.isValid("gb"); // -> true
|
|
426
|
+
* b.guardCountry.isValid("UK"); // -> false
|
|
427
|
+
* b.guardCountry.isValid("ZZ"); // -> false
|
|
428
|
+
* b.guardCountry.isValid(null); // -> false
|
|
429
|
+
*/
|
|
430
|
+
// Hostile: ZZ - the CLDR unknown-region sentinel, refused at strict because a
|
|
431
|
+
// routing decision must never act on "we do not know".
|
|
432
|
+
var INTEGRATION_FIXTURES = gateContract.identifierFixtures("GB", "ZZ");
|
|
433
|
+
|
|
434
|
+
// Assembled from the gate-contract guard factory: error class, registry
|
|
435
|
+
// exports (NAME / KIND / INTEGRATION_FIXTURES), buildProfile /
|
|
436
|
+
// compliancePosture / loadRulePack wiring, plus validate / sanitize. The
|
|
437
|
+
// gate is the factory default - serve -> audit-only -> refuse - reading
|
|
438
|
+
// ctx.identifier || ctx.country || ctx.countryCode via ctxFields.
|
|
439
|
+
//
|
|
440
|
+
// The factory call is the module.exports initializer rather than a variable
|
|
441
|
+
// the file exports afterwards: the wiki's source-doc parser detects a
|
|
442
|
+
// guard-family factory only in that position, and a guard it cannot see gets
|
|
443
|
+
// no generated ABI page.
|
|
444
|
+
module.exports = gateContract.defineGuard({
|
|
445
|
+
name: "country",
|
|
446
|
+
kind: "identifier",
|
|
447
|
+
errorClass: GuardCountryError,
|
|
448
|
+
profiles: PROFILES,
|
|
449
|
+
defaults: DEFAULTS,
|
|
450
|
+
postures: COMPLIANCE_POSTURES,
|
|
451
|
+
integrationFixtures: INTEGRATION_FIXTURES,
|
|
452
|
+
detect: _detectIssues,
|
|
453
|
+
sanitizeTransform: _sanitizeTransform,
|
|
454
|
+
intOpts: ["maxBytes"],
|
|
455
|
+
enumOpts: POLICY_ENUM,
|
|
456
|
+
ctxFields: ["identifier", "country", "countryCode"],
|
|
457
|
+
extra: {
|
|
458
|
+
// The assigned set, exposed so a consumer can populate a picker from the
|
|
459
|
+
// same table the guard answers from rather than shipping a second one.
|
|
460
|
+
ASSIGNED_CODES: ASSIGNED_CODES,
|
|
461
|
+
// Answers membership in the assigned table directly, and takes NO opts.
|
|
462
|
+
//
|
|
463
|
+
// Forwarding a profile here was wrong: the profiles differ only in how
|
|
464
|
+
// they DISPOSE of a non-assigned code — strict refuses, balanced and
|
|
465
|
+
// permissive downgrade some findings to `warn`, which leaves
|
|
466
|
+
// validate().ok true. A predicate built on that reported `EU`, `ZZ`, `SU`
|
|
467
|
+
// and `AN` as valid under permissive, contradicting its own documented
|
|
468
|
+
// promise and feeding reserved and sentinel values into exactly the
|
|
469
|
+
// residency and jurisdiction routing this guard exists to protect.
|
|
470
|
+
//
|
|
471
|
+
// "Is this an officially assigned code" is one question with one answer,
|
|
472
|
+
// so it is answered from the table rather than from a policy that was
|
|
473
|
+
// never about assignment. A caller who wants the profile-dependent
|
|
474
|
+
// disposition calls validate.
|
|
475
|
+
isValid: function isValid(input) {
|
|
476
|
+
if (typeof input !== "string" || !_isTwoAsciiLetters(input)) return false;
|
|
477
|
+
return ASSIGNED.has(_upper(input));
|
|
478
|
+
},
|
|
479
|
+
},
|
|
480
|
+
});
|
package/lib/guard-csv.js
CHANGED
|
@@ -1428,6 +1428,11 @@ module.exports = gateContract.defineGuard({
|
|
|
1428
1428
|
sanitizeTransform: _stripIssues,
|
|
1429
1429
|
sanitizeSeverities: [],
|
|
1430
1430
|
sanitizeAmplificationCap: "sanitizeAmplificationCap",
|
|
1431
|
+
// The options that are genuinely CAPS, where zero is not a setting. Declared
|
|
1432
|
+
// so a caller learns about `maxRows: 0` at the call that sets it rather than
|
|
1433
|
+
// from a parse that refuses every row. maxRuntimeMs is deliberately absent:
|
|
1434
|
+
// zero there means no runtime budget.
|
|
1435
|
+
intOpts: ["maxRows", "maxColumns", "maxCellBytes", "maxTotalBytes"],
|
|
1431
1436
|
gate: gate,
|
|
1432
1437
|
extra: {
|
|
1433
1438
|
_gateDispositionForTest: _gateDispositionFor,
|
package/lib/guard-email.js
CHANGED
|
@@ -433,14 +433,10 @@ function _stripOuterQuotes(s) {
|
|
|
433
433
|
return out;
|
|
434
434
|
}
|
|
435
435
|
|
|
436
|
+
// Delegates to the guard's own resolver rather than repeating its binding, so
|
|
437
|
+
// every entry point below is held to this guard's cap list. See guard-archive.
|
|
436
438
|
function _resolveOpts(opts) {
|
|
437
|
-
return
|
|
438
|
-
profiles: PROFILES,
|
|
439
|
-
compliancePostures: COMPLIANCE_POSTURES,
|
|
440
|
-
defaults: DEFAULTS,
|
|
441
|
-
errorClass: GuardEmailError,
|
|
442
|
-
errCodePrefix: "email",
|
|
443
|
-
});
|
|
439
|
+
return module.exports.resolveOpts(opts);
|
|
444
440
|
}
|
|
445
441
|
|
|
446
442
|
// ---- Address validation ----
|
|
@@ -1131,6 +1127,10 @@ module.exports = gateContract.defineGuard({
|
|
|
1131
1127
|
integrationFixtures: INTEGRATION_FIXTURES,
|
|
1132
1128
|
validate: validate,
|
|
1133
1129
|
sanitize: sanitize,
|
|
1130
|
+
// Genuine caps, where zero is not a setting — see guard-csv. maxRuntimeMs is
|
|
1131
|
+
// deliberately absent: zero there means no runtime budget.
|
|
1132
|
+
intOpts: ["maxBytes", "maxLocalPartBytes", "maxDomainBytes",
|
|
1133
|
+
"maxAddressBytes", "maxHeaderLineBytes", "maxHeaders"],
|
|
1134
1134
|
gate: gate,
|
|
1135
1135
|
extra: {
|
|
1136
1136
|
validateAddress: validateAddress,
|
package/lib/guard-filename.js
CHANGED
|
@@ -211,14 +211,10 @@ var COMPLIANCE_POSTURES = gateContract.compliancePostures(PROFILES, { base: 256,
|
|
|
211
211
|
|
|
212
212
|
// ---- Helpers ----
|
|
213
213
|
|
|
214
|
+
// Delegates to the guard's own resolver rather than repeating its binding, so
|
|
215
|
+
// every entry point below is held to this guard's cap list. See guard-archive.
|
|
214
216
|
function _resolveOpts(opts) {
|
|
215
|
-
return
|
|
216
|
-
profiles: PROFILES,
|
|
217
|
-
compliancePostures: COMPLIANCE_POSTURES,
|
|
218
|
-
defaults: DEFAULTS,
|
|
219
|
-
errorClass: GuardFilenameError,
|
|
220
|
-
errCodePrefix: "filename",
|
|
221
|
-
});
|
|
217
|
+
return module.exports.resolveOpts(opts);
|
|
222
218
|
}
|
|
223
219
|
|
|
224
220
|
function _normalizeNFC(s) {
|
|
@@ -959,8 +955,11 @@ function gate(opts) {
|
|
|
959
955
|
opts,
|
|
960
956
|
async function (ctx) {
|
|
961
957
|
// Filename-shape ctx — operator passes filename via ctx.filename.
|
|
962
|
-
|
|
963
|
-
|
|
958
|
+
// Read through the shared reader rather than an `||` chain: the chain
|
|
959
|
+
// cannot tell an ABSENT field from one present as "", and served both,
|
|
960
|
+
// while validate("") refuses an empty filename.
|
|
961
|
+
var name = gateContract.ctxValueFrom(ctx, ["filename", "name"]);
|
|
962
|
+
if (name === undefined || name === null) return { ok: true, action: "serve" };
|
|
964
963
|
var rv = validate(name, opts);
|
|
965
964
|
if (rv.issues.length === 0) return { ok: true, action: "serve" };
|
|
966
965
|
|
|
@@ -1314,6 +1313,9 @@ module.exports = gateContract.defineGuard({
|
|
|
1314
1313
|
integrationFixtures: INTEGRATION_FIXTURES,
|
|
1315
1314
|
validate: validate,
|
|
1316
1315
|
sanitize: sanitize,
|
|
1316
|
+
// Genuine caps, where zero is not a setting — see guard-csv. maxRuntimeMs is
|
|
1317
|
+
// deliberately absent: zero there means no runtime budget.
|
|
1318
|
+
intOpts: ["maxBytes", "maxComponents"],
|
|
1317
1319
|
gate: gate,
|
|
1318
1320
|
extra: {
|
|
1319
1321
|
WIN_RESERVED_NAMES: WIN_RESERVED_NAMES,
|
package/lib/guard-graphql.js
CHANGED
|
@@ -540,9 +540,7 @@ function _detectIssues(req, opts) {
|
|
|
540
540
|
// generated sanitize AFTER resolve → detect → throw-on-refusal. GraphQL
|
|
541
541
|
// request bundles can't be partially repaired; once detection passes with no
|
|
542
542
|
// critical/high issue, the input is returned unchanged.
|
|
543
|
-
|
|
544
|
-
return input;
|
|
545
|
-
}
|
|
543
|
+
var _sanitizeTransform = gateContract.identitySanitize;
|
|
546
544
|
|
|
547
545
|
/**
|
|
548
546
|
* @primitive b.guardGraphql.gate
|
package/lib/guard-html.js
CHANGED
|
@@ -283,14 +283,10 @@ var COMPLIANCE_POSTURES = gateContract.compliancePostures(PROFILES, { base: 256
|
|
|
283
283
|
|
|
284
284
|
// ---- Internal helpers ----
|
|
285
285
|
|
|
286
|
+
// Delegates to the guard's own resolver rather than repeating its binding, so
|
|
287
|
+
// every entry point below is held to this guard's cap list. See guard-archive.
|
|
286
288
|
function _resolveOpts(opts) {
|
|
287
|
-
return
|
|
288
|
-
profiles: PROFILES,
|
|
289
|
-
compliancePostures: COMPLIANCE_POSTURES,
|
|
290
|
-
defaults: DEFAULTS,
|
|
291
|
-
errorClass: GuardHtmlError,
|
|
292
|
-
errCodePrefix: "html",
|
|
293
|
-
});
|
|
289
|
+
return module.exports.resolveOpts(opts);
|
|
294
290
|
}
|
|
295
291
|
|
|
296
292
|
/**
|
|
@@ -996,6 +992,9 @@ module.exports = gateContract.defineGuard({
|
|
|
996
992
|
integrationFixtures: INTEGRATION_FIXTURES,
|
|
997
993
|
validate: validate,
|
|
998
994
|
sanitize: sanitize,
|
|
995
|
+
// Genuine caps, where zero is not a setting — see guard-csv. maxRuntimeMs is
|
|
996
|
+
// deliberately absent: zero there means no runtime budget.
|
|
997
|
+
intOpts: ["maxBytes", "maxAttrValueBytes", "maxTagDepth", "maxAttrsPerTag"],
|
|
999
998
|
gate: gate,
|
|
1000
999
|
extra: {
|
|
1001
1000
|
_gateDispositionForTest: _gateDispositionFor,
|
package/lib/guard-image.js
CHANGED
|
@@ -154,6 +154,13 @@ var PROFILES = Object.freeze({
|
|
|
154
154
|
|
|
155
155
|
var DEFAULTS = gateContract.strictDefaults(PROFILES);
|
|
156
156
|
|
|
157
|
+
// The options that are genuinely CAPS, where zero is not a setting. Named once
|
|
158
|
+
// because this guard's gate binds its own resolver and must hold a caller to
|
|
159
|
+
// the same rules the generated validate() does. Everything else numeric is
|
|
160
|
+
// derived from DEFAULTS and held only to being a non-negative integer —
|
|
161
|
+
// maxRuntimeMs among them, where zero means no runtime budget.
|
|
162
|
+
var INT_OPTS = ["maxBytes", "maxWidth", "maxHeight", "maxFrames"];
|
|
163
|
+
|
|
157
164
|
var COMPLIANCE_POSTURES = gateContract.compliancePostures(PROFILES, { base: 256 });
|
|
158
165
|
|
|
159
166
|
function _bytesAt(buf, offset, sig) {
|
|
@@ -662,6 +669,13 @@ function gate(opts) {
|
|
|
662
669
|
defaults: DEFAULTS,
|
|
663
670
|
errorClass: GuardImageError,
|
|
664
671
|
errCodePrefix: "image",
|
|
672
|
+
// This gate binds its own resolver, so it declares its own limits. Without
|
|
673
|
+
// them a malformed limit reaches the comparisons as NaN and disables the
|
|
674
|
+
// bound that validate() would have refused. Both lists, and the SAME cap
|
|
675
|
+
// list the module exports, so building a gate and calling validate hold a
|
|
676
|
+
// caller to identical rules.
|
|
677
|
+
intOpts: INT_OPTS,
|
|
678
|
+
nonNegativeOpts: gateContract.capKeysOf(DEFAULTS),
|
|
665
679
|
});
|
|
666
680
|
return gateContract.buildGuardGate(
|
|
667
681
|
opts.name || "guardImage:" + (opts.profile || "default"),
|
|
@@ -743,7 +757,7 @@ module.exports = gateContract.defineGuard({
|
|
|
743
757
|
// contract hands the bag straight to detect (which owns its own bad-input).
|
|
744
758
|
detect: _detectIssues,
|
|
745
759
|
sanitizeTransform: _sanitizeTransform,
|
|
746
|
-
intOpts:
|
|
760
|
+
intOpts: INT_OPTS,
|
|
747
761
|
gate: gate,
|
|
748
762
|
extra: {
|
|
749
763
|
inspectMagic: inspectMagic,
|
package/lib/guard-json.js
CHANGED
|
@@ -388,14 +388,10 @@ var COMPLIANCE_POSTURES = gateContract.compliancePostures(PROFILES, { base: 256
|
|
|
388
388
|
|
|
389
389
|
// ---- Helpers ----
|
|
390
390
|
|
|
391
|
+
// Delegates to the guard's own resolver rather than repeating its binding, so
|
|
392
|
+
// every entry point below is held to this guard's cap list. See guard-archive.
|
|
391
393
|
function _resolveOpts(opts) {
|
|
392
|
-
return
|
|
393
|
-
profiles: PROFILES,
|
|
394
|
-
compliancePostures: COMPLIANCE_POSTURES,
|
|
395
|
-
defaults: DEFAULTS,
|
|
396
|
-
errorClass: GuardJsonError,
|
|
397
|
-
errCodePrefix: "json",
|
|
398
|
-
});
|
|
394
|
+
return module.exports.resolveOpts(opts);
|
|
399
395
|
}
|
|
400
396
|
|
|
401
397
|
function _isPollutionKey(key) {
|