@blamejs/blamejs-shop 0.4.91 → 0.4.92
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 +2 -0
- package/lib/asset-manifest.json +1 -1
- package/lib/vendor/MANIFEST.json +49 -41
- package/lib/vendor/blamejs/CHANGELOG.md +6 -0
- package/lib/vendor/blamejs/SECURITY.md +1 -0
- package/lib/vendor/blamejs/api-snapshot.json +208 -2
- package/lib/vendor/blamejs/examples/wiki/test/e2e.js +7 -4
- package/lib/vendor/blamejs/examples/wiki/test/integration.js +15 -12
- package/lib/vendor/blamejs/index.js +2 -0
- package/lib/vendor/blamejs/lib/audit-sign.js +34 -1
- package/lib/vendor/blamejs/lib/backup/manifest.js +191 -44
- package/lib/vendor/blamejs/lib/codepoint-class.js +284 -77
- package/lib/vendor/blamejs/lib/framework-error.js +14 -0
- package/lib/vendor/blamejs/lib/fsm.js +80 -24
- package/lib/vendor/blamejs/lib/log.js +32 -0
- package/lib/vendor/blamejs/lib/middleware/rate-limit.js +18 -2
- package/lib/vendor/blamejs/lib/middleware/request-id.js +24 -4
- package/lib/vendor/blamejs/lib/request-helpers.js +50 -0
- package/lib/vendor/blamejs/lib/safe-path.js +24 -10
- package/lib/vendor/blamejs/lib/sql.js +133 -0
- package/lib/vendor/blamejs/lib/totp.js +98 -33
- package/lib/vendor/blamejs/lib/ws-client.js +39 -28
- package/lib/vendor/blamejs/package.json +1 -1
- package/lib/vendor/blamejs/release-notes/v0.15.21.json +51 -0
- package/lib/vendor/blamejs/release-notes/v0.15.22.json +18 -0
- package/lib/vendor/blamejs/release-notes/v0.15.23.json +22 -0
- package/lib/vendor/blamejs/test/00-primitives.js +80 -0
- package/lib/vendor/blamejs/test/_smoke-worker.js +81 -0
- package/lib/vendor/blamejs/test/integration/federation-auth.test.js +7 -4
- package/lib/vendor/blamejs/test/integration/mail-crypto-smime.test.js +7 -4
- package/lib/vendor/blamejs/test/layer-0-primitives/backup-manifest-signature.test.js +91 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/codebase-patterns.test.js +65 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/codepoint-class.test.js +58 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/defineguard-default-gate-posture-caps.test.js +5 -2
- package/lib/vendor/blamejs/test/layer-0-primitives/dpop-middleware-replaystore-required.test.js +9 -1
- package/lib/vendor/blamejs/test/layer-0-primitives/fsm.test.js +99 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/money.test.js +30 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/otlp-attr-redaction.test.js +9 -6
- package/lib/vendor/blamejs/test/layer-0-primitives/rate-limit-xff-spoofing.test.js +36 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/request-helpers.test.js +33 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/request-id-async-context.test.js +117 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/safe-path.test.js +64 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/sql.test.js +96 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/ws-client.test.js +55 -0
- package/lib/vendor/blamejs/test/smoke.js +93 -10
- package/package.json +1 -1
|
@@ -1,9 +1,24 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
/**
|
|
3
|
-
*
|
|
4
|
-
*
|
|
3
|
+
* @module b.codepointClass
|
|
4
|
+
* @nav Validation
|
|
5
|
+
* @title Codepoint Class
|
|
5
6
|
*
|
|
6
|
-
*
|
|
7
|
+
* @intro
|
|
8
|
+
* Shared codepoint-table threat catalog and regex compiler — the Unicode
|
|
9
|
+
* bidi-override / C0-control / zero-width / null-byte / Unicode-Tags tables
|
|
10
|
+
* (plus UTS #39 confusable-script detection) that the
|
|
11
|
+
* <code>b.guard*</code> family composes internally, exposed on the public
|
|
12
|
+
* surface so a consumer can build a custom free-text screen without
|
|
13
|
+
* re-rolling the regexes (where the zero-width class is mistyped and the
|
|
14
|
+
* astral Unicode-Tags "ASCII smuggling" block forgotten) or coupling to an
|
|
15
|
+
* internal module path. For a ready-made unconstrained-free-text guard reach
|
|
16
|
+
* for <code>b.guardText</code>; use this catalog when you need the raw
|
|
17
|
+
* detectors, tables, or script classifier to compose your own. Detectors
|
|
18
|
+
* emit attack characters programmatically from numeric codepoint tables
|
|
19
|
+
* (never as source literals), so files that use them stay pure ASCII.
|
|
20
|
+
*
|
|
21
|
+
* Threat detectors that need to match Unicode bidi overrides, C0
|
|
7
22
|
* control characters, zero-width / invisible chars, etc. compose
|
|
8
23
|
* regex character classes from numeric codepoint range tables here
|
|
9
24
|
* instead of embedding the attack characters directly in their
|
|
@@ -48,20 +63,68 @@
|
|
|
48
63
|
* attackers use to hide payloads:
|
|
49
64
|
* SHY U+00AD ZWSP U+200B ZWNJ U+200C ZWJ U+200D
|
|
50
65
|
* WJ U+2060 BOM U+FEFF
|
|
66
|
+
*
|
|
67
|
+
* @card
|
|
68
|
+
* The Unicode threat-codepoint catalog (bidi / control / zero-width / Tags
|
|
69
|
+
* tables + confusable-script detection) the guard family composes — exposed
|
|
70
|
+
* so you can build a custom free-text screen without re-rolling the regexes.
|
|
51
71
|
*/
|
|
52
72
|
|
|
53
73
|
var HEX_RADIX = 16; // base-16 radix, not byte size
|
|
54
74
|
|
|
75
|
+
/**
|
|
76
|
+
* @primitive b.codepointClass.hex4
|
|
77
|
+
* @signature b.codepointClass.hex4(cp)
|
|
78
|
+
* @since 0.15.21
|
|
79
|
+
* @status stable
|
|
80
|
+
* @related b.codepointClass.charClass, b.codepointClass.fromCp
|
|
81
|
+
*
|
|
82
|
+
* Format a codepoint as a 4-digit `\uXXXX` regex escape (zero-padded, upper
|
|
83
|
+
* case) — the building block `charClass` uses to compile a range table into a
|
|
84
|
+
* character-class body without embedding the attack character as a literal.
|
|
85
|
+
*
|
|
86
|
+
* @example
|
|
87
|
+
* b.codepointClass.hex4(0x202E); // returns the escape "\\u202E"
|
|
88
|
+
*/
|
|
55
89
|
function hex4(cp) {
|
|
56
90
|
var s = cp.toString(HEX_RADIX).toUpperCase();
|
|
57
91
|
while (s.length < 4) s = "0" + s;
|
|
58
92
|
return "\\u" + s;
|
|
59
93
|
}
|
|
94
|
+
/**
|
|
95
|
+
* @primitive b.codepointClass.charClass
|
|
96
|
+
* @signature b.codepointClass.charClass(rangeList)
|
|
97
|
+
* @since 0.15.21
|
|
98
|
+
* @status stable
|
|
99
|
+
* @related b.codepointClass.hex4, b.codepointClass.detectCharThreats
|
|
100
|
+
*
|
|
101
|
+
* Compile a codepoint range table — numbers and `[lo, hi]` pairs — into a regex
|
|
102
|
+
* character-class body (the inner text of `[...]`), so a detector can build its
|
|
103
|
+
* own class from a catalog table without typing the codepoints as literals.
|
|
104
|
+
*
|
|
105
|
+
* @example
|
|
106
|
+
* var body = b.codepointClass.charClass([0x200E, [0x202A, 0x202E]]);
|
|
107
|
+
* var re = new RegExp("[" + body + "]");
|
|
108
|
+
*/
|
|
60
109
|
function charClass(rangeList) {
|
|
61
110
|
return rangeList.map(function (r) {
|
|
62
111
|
return Array.isArray(r) ? hex4(r[0]) + "-" + hex4(r[1]) : hex4(r);
|
|
63
112
|
}).join("");
|
|
64
113
|
}
|
|
114
|
+
/**
|
|
115
|
+
* @primitive b.codepointClass.fromCp
|
|
116
|
+
* @signature b.codepointClass.fromCp(cp)
|
|
117
|
+
* @since 0.15.21
|
|
118
|
+
* @status stable
|
|
119
|
+
* @related b.codepointClass.hex4
|
|
120
|
+
*
|
|
121
|
+
* `String.fromCharCode` shorthand — emit the actual character for a codepoint
|
|
122
|
+
* at runtime (e.g. to build a test fixture) instead of typing the attack
|
|
123
|
+
* character as a source literal.
|
|
124
|
+
*
|
|
125
|
+
* @example
|
|
126
|
+
* var rlo = b.codepointClass.fromCp(0x202E); // the U+202E override char
|
|
127
|
+
*/
|
|
65
128
|
function fromCp(cp) { return String.fromCharCode(cp); }
|
|
66
129
|
|
|
67
130
|
var BIDI_RANGES = [0x200E, 0x200F, 0x061C, [0x202A, 0x202E], [0x2066, 0x2069]];
|
|
@@ -120,9 +183,22 @@ var SCRIPT_RANGES = {
|
|
|
120
183
|
hebrew: [[0x0590, 0x05FF]], // Hebrew
|
|
121
184
|
};
|
|
122
185
|
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
186
|
+
/**
|
|
187
|
+
* @primitive b.codepointClass.scriptFor
|
|
188
|
+
* @signature b.codepointClass.scriptFor(cp)
|
|
189
|
+
* @since 0.15.21
|
|
190
|
+
* @status stable
|
|
191
|
+
* @related b.codepointClass.detectMixedScripts
|
|
192
|
+
*
|
|
193
|
+
* Return the Unicode script name for a codepoint (`"latin"`, `"cyrillic"`,
|
|
194
|
+
* `"greek"`, `"han"`, ...), or `null` when the codepoint is script-neutral
|
|
195
|
+
* (digits, punctuation, symbols). The classifier `detectMixedScripts` uses to
|
|
196
|
+
* spot homograph / confusable mixing (UTS #39).
|
|
197
|
+
*
|
|
198
|
+
* @example
|
|
199
|
+
* b.codepointClass.scriptFor("a".charCodeAt(0)); // returns "latin"
|
|
200
|
+
* b.codepointClass.scriptFor(0x0430); // returns "cyrillic" (the confusable a)
|
|
201
|
+
*/
|
|
126
202
|
function scriptFor(cp) {
|
|
127
203
|
var keys = Object.keys(SCRIPT_RANGES);
|
|
128
204
|
for (var i = 0; i < keys.length; i += 1) {
|
|
@@ -134,17 +210,26 @@ function scriptFor(cp) {
|
|
|
134
210
|
return null;
|
|
135
211
|
}
|
|
136
212
|
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
213
|
+
/**
|
|
214
|
+
* @primitive b.codepointClass.detectMixedScripts
|
|
215
|
+
* @signature b.codepointClass.detectMixedScripts(label, allowedScripts)
|
|
216
|
+
* @since 0.15.21
|
|
217
|
+
* @status stable
|
|
218
|
+
* @related b.codepointClass.scriptFor, b.guardText
|
|
219
|
+
*
|
|
220
|
+
* UTS #39 confusable detection: return `null` when `label` is single-script
|
|
221
|
+
* (or every script it uses is in the optional `allowedScripts` allowlist), or
|
|
222
|
+
* the full array of script names when it mixes scripts — the homograph attack
|
|
223
|
+
* shape (a Cyrillic confusable letter inside an otherwise-Latin label). Callers
|
|
224
|
+
* decide refuse / audit / strip. Pass `allowedScripts` to permit legitimate
|
|
225
|
+
* mixing (an ASCII word inside a non-Latin label).
|
|
226
|
+
*
|
|
227
|
+
* @example
|
|
228
|
+
* b.codepointClass.detectMixedScripts("paypal"); // null (single-script)
|
|
229
|
+
* var spoof = "pa" + b.codepointClass.fromCp(0x0443) + "pal"; // Cyrillic u (U+0443)
|
|
230
|
+
* b.codepointClass.detectMixedScripts(spoof); // ["latin", "cyrillic"]
|
|
231
|
+
* b.codepointClass.detectMixedScripts(spoof, ["latin", "cyrillic"]); // null (allowlisted)
|
|
232
|
+
*/
|
|
148
233
|
function detectMixedScripts(label, allowedScripts) {
|
|
149
234
|
if (typeof label !== "string" || label.length === 0) return null;
|
|
150
235
|
var seen = {};
|
|
@@ -162,16 +247,32 @@ function detectMixedScripts(label, allowedScripts) {
|
|
|
162
247
|
return null;
|
|
163
248
|
}
|
|
164
249
|
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
250
|
+
/**
|
|
251
|
+
* @primitive b.codepointClass.detectCharThreats
|
|
252
|
+
* @signature b.codepointClass.detectCharThreats(text, opts, codePrefix, zeroWidthSeverity)
|
|
253
|
+
* @since 0.15.21
|
|
254
|
+
* @status stable
|
|
255
|
+
* @related b.codepointClass.assertNoCharThreats, b.codepointClass.applyCharStripPolicies, b.guardText
|
|
256
|
+
*
|
|
257
|
+
* Scan `text` for the character-class threats — bidi override, null byte, C0
|
|
258
|
+
* control, and (opt-in) zero-width — and return an array of issue objects
|
|
259
|
+
* `{ kind, severity, ruleId, location, snippet }`, at most one per class. Each
|
|
260
|
+
* class is gated by an opts policy that isn't `"allow"`; `ruleId` is prefixed
|
|
261
|
+
* with `codePrefix`. The non-throwing detection pass the `b.guard*` family
|
|
262
|
+
* shares instead of re-rolling the per-class match-and-push. `zeroWidthSeverity`
|
|
263
|
+
* opts the zero-width scan in and stamps its severity.
|
|
264
|
+
*
|
|
265
|
+
* @opts
|
|
266
|
+
* bidiPolicy: string, // non-"allow" -> flag bidi overrides
|
|
267
|
+
* nullBytePolicy: string, // non-"allow" -> flag null bytes
|
|
268
|
+
* controlPolicy: string, // non-"allow" -> flag C0 controls
|
|
269
|
+
* zeroWidthPolicy: string, // non-"allow" (+ zeroWidthSeverity) -> flag zero-width
|
|
270
|
+
*
|
|
271
|
+
* @example
|
|
272
|
+
* var issues = b.codepointClass.detectCharThreats(
|
|
273
|
+
* userText, { bidiPolicy: "reject", nullBytePolicy: "reject" }, "comment");
|
|
274
|
+
* if (issues.length) refuse(issues[0].ruleId);
|
|
275
|
+
*/
|
|
175
276
|
function detectCharThreats(text, opts, codePrefix, zeroWidthSeverity) {
|
|
176
277
|
var issues = [];
|
|
177
278
|
if (typeof text !== "string") return issues;
|
|
@@ -233,11 +334,29 @@ function detectCharThreats(text, opts, codePrefix, zeroWidthSeverity) {
|
|
|
233
334
|
return issues;
|
|
234
335
|
}
|
|
235
336
|
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
337
|
+
/**
|
|
338
|
+
* @primitive b.codepointClass.assertNoCharThreats
|
|
339
|
+
* @signature b.codepointClass.assertNoCharThreats(text, opts, errorFactory, codePrefix)
|
|
340
|
+
* @since 0.15.21
|
|
341
|
+
* @status stable
|
|
342
|
+
* @related b.codepointClass.detectCharThreats, b.guardText
|
|
343
|
+
*
|
|
344
|
+
* Throw — via `errorFactory(code, message)` — when `text` contains a character
|
|
345
|
+
* class whose opts policy is `"reject"` (bidi / null byte / C0 control). The
|
|
346
|
+
* throwing counterpart of `detectCharThreats`; `errorFactory` lets the caller
|
|
347
|
+
* raise its own typed error and `codePrefix` namespaces the rule code. The
|
|
348
|
+
* caller bounds the input length before calling (the regexes are unbounded).
|
|
349
|
+
*
|
|
350
|
+
* @opts
|
|
351
|
+
* bidiPolicy: string, // "reject" -> throw on a bidi override
|
|
352
|
+
* nullBytePolicy: string, // "reject" -> throw on a null byte
|
|
353
|
+
* controlPolicy: string, // "reject" -> throw on a C0 control
|
|
354
|
+
*
|
|
355
|
+
* @example
|
|
356
|
+
* b.codepointClass.assertNoCharThreats(value,
|
|
357
|
+
* { bidiPolicy: "reject", nullBytePolicy: "reject" },
|
|
358
|
+
* function (code, msg) { return new TypeError(code + ": " + msg); }, "note");
|
|
359
|
+
*/
|
|
241
360
|
function assertNoCharThreats(text, opts, errorFactory, codePrefix) {
|
|
242
361
|
if (typeof text !== "string") return;
|
|
243
362
|
if (opts && opts.bidiPolicy === "reject" && BIDI_RE.test(text)) { // allow:regex-no-length-cap — caller bounds length before invoking
|
|
@@ -254,15 +373,30 @@ function assertNoCharThreats(text, opts, errorFactory, codePrefix) {
|
|
|
254
373
|
}
|
|
255
374
|
}
|
|
256
375
|
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
376
|
+
/**
|
|
377
|
+
* @primitive b.codepointClass.applyCharStripPolicies
|
|
378
|
+
* @signature b.codepointClass.applyCharStripPolicies(text, opts)
|
|
379
|
+
* @since 0.15.21
|
|
380
|
+
* @status stable
|
|
381
|
+
* @related b.codepointClass.detectCharThreats, b.guardText
|
|
382
|
+
*
|
|
383
|
+
* Strip each character-class threat whose opts policy is `"strip"` and return
|
|
384
|
+
* the cleaned string — the sanitize counterpart of `detectCharThreats`, shared
|
|
385
|
+
* by every guard's sanitize path so none re-rolls the same sequence of
|
|
386
|
+
* `replace()` calls. Removes bidi overrides, C0 controls, null bytes,
|
|
387
|
+
* zero-width chars, and the Unicode-Tags block ("ASCII smuggling") per policy.
|
|
388
|
+
*
|
|
389
|
+
* @opts
|
|
390
|
+
* bidiPolicy: string, // "strip" -> remove bidi overrides
|
|
391
|
+
* controlPolicy: string, // "strip" -> remove C0 controls
|
|
392
|
+
* nullBytePolicy: string, // "strip" -> remove null bytes
|
|
393
|
+
* zeroWidthPolicy: string, // "strip" -> remove zero-width / invisible chars
|
|
394
|
+
* tagsPolicy: string, // "strip" -> remove the Unicode Tags block
|
|
395
|
+
*
|
|
396
|
+
* @example
|
|
397
|
+
* var clean = b.codepointClass.applyCharStripPolicies(userText,
|
|
398
|
+
* { bidiPolicy: "strip", zeroWidthPolicy: "strip", tagsPolicy: "strip" });
|
|
399
|
+
*/
|
|
266
400
|
function applyCharStripPolicies(text, opts) {
|
|
267
401
|
if (typeof text !== "string") return text;
|
|
268
402
|
var out = text;
|
|
@@ -278,11 +412,21 @@ function applyCharStripPolicies(text, opts) {
|
|
|
278
412
|
// (. * + ? ^ $ { } ( ) | [ ] \).
|
|
279
413
|
var REGEXP_META_RE = /[.*+?^${}()|[\]\\]/g;
|
|
280
414
|
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
415
|
+
/**
|
|
416
|
+
* @primitive b.codepointClass.escapeRegExp
|
|
417
|
+
* @signature b.codepointClass.escapeRegExp(s)
|
|
418
|
+
* @since 0.15.21
|
|
419
|
+
* @status stable
|
|
420
|
+
* @related b.codepointClass.charClass
|
|
421
|
+
*
|
|
422
|
+
* Escape every ECMAScript RegExp metacharacter in a string so an operator- or
|
|
423
|
+
* input-supplied token matches literally when spliced into a `new RegExp(...)`
|
|
424
|
+
* — a token destined for dynamic compilation cannot inject a pattern.
|
|
425
|
+
*
|
|
426
|
+
* @example
|
|
427
|
+
* var re = new RegExp(b.codepointClass.escapeRegExp("a.b*c"));
|
|
428
|
+
* re.test("a.b*c"); // true — the . and * are literal
|
|
429
|
+
*/
|
|
286
430
|
function escapeRegExp(s) {
|
|
287
431
|
return String(s).replace(REGEXP_META_RE, "\\$&");
|
|
288
432
|
}
|
|
@@ -293,19 +437,43 @@ function escapeRegExp(s) {
|
|
|
293
437
|
// lives once.
|
|
294
438
|
var HEX_PAIR_RE = /^[0-9A-Fa-f]{2}$/;
|
|
295
439
|
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
440
|
+
/**
|
|
441
|
+
* @primitive b.codepointClass.isAsciiAlnum
|
|
442
|
+
* @signature b.codepointClass.isAsciiAlnum(cc)
|
|
443
|
+
* @since 0.15.21
|
|
444
|
+
* @status stable
|
|
445
|
+
* @related b.codepointClass.isUnreserved
|
|
446
|
+
*
|
|
447
|
+
* Test whether a char code is an ASCII letter or digit (`A-Z` / `a-z` / `0-9`)
|
|
448
|
+
* — the alphanumeric range check that recurs across every byte-class parser
|
|
449
|
+
* (URL unreserved, XML name chars, header tokens), centralized so the range
|
|
450
|
+
* literals live once.
|
|
451
|
+
*
|
|
452
|
+
* @example
|
|
453
|
+
* b.codepointClass.isAsciiAlnum("Z".charCodeAt(0)); // true
|
|
454
|
+
* b.codepointClass.isAsciiAlnum("-".charCodeAt(0)); // false
|
|
455
|
+
*/
|
|
300
456
|
function isAsciiAlnum(cc) {
|
|
301
457
|
return (cc >= 0x41 && cc <= 0x5a) || // A-Z
|
|
302
458
|
(cc >= 0x61 && cc <= 0x7a) || // a-z
|
|
303
459
|
(cc >= 0x30 && cc <= 0x39); // 0-9
|
|
304
460
|
}
|
|
305
461
|
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
462
|
+
/**
|
|
463
|
+
* @primitive b.codepointClass.isUnreserved
|
|
464
|
+
* @signature b.codepointClass.isUnreserved(cc)
|
|
465
|
+
* @since 0.15.21
|
|
466
|
+
* @status stable
|
|
467
|
+
* @related b.codepointClass.isAsciiAlnum
|
|
468
|
+
*
|
|
469
|
+
* Test whether a char code is in the RFC 3986 §2.3 unreserved set —
|
|
470
|
+
* `ALPHA` / `DIGIT` / `-` / `.` / `_` / `~`. A percent-escape of an unreserved
|
|
471
|
+
* character is over-encoding the URI spec says SHOULD be decoded (§6.2.2.3).
|
|
472
|
+
*
|
|
473
|
+
* @example
|
|
474
|
+
* b.codepointClass.isUnreserved("~".charCodeAt(0)); // true
|
|
475
|
+
* b.codepointClass.isUnreserved("/".charCodeAt(0)); // false
|
|
476
|
+
*/
|
|
309
477
|
function isUnreserved(cc) {
|
|
310
478
|
return isAsciiAlnum(cc) ||
|
|
311
479
|
cc === 0x2d || // -
|
|
@@ -314,24 +482,29 @@ function isUnreserved(cc) {
|
|
|
314
482
|
cc === 0x7e; // ~
|
|
315
483
|
}
|
|
316
484
|
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
//
|
|
333
|
-
|
|
334
|
-
//
|
|
485
|
+
/**
|
|
486
|
+
* @primitive b.codepointClass.isForbiddenControlChar
|
|
487
|
+
* @signature b.codepointClass.isForbiddenControlChar(code, opts)
|
|
488
|
+
* @since 0.15.21
|
|
489
|
+
* @status stable
|
|
490
|
+
* @related b.codepointClass.firstControlCharOffset
|
|
491
|
+
*
|
|
492
|
+
* The header-injection / RFC 5322 control-byte predicate every "refuse
|
|
493
|
+
* control bytes in a header / line / value" loop shares. Returns `true` for DEL
|
|
494
|
+
* (`0x7f`) and any C0 control (`< 0x20`) other than TAB (`0x09`); LF and CR are
|
|
495
|
+
* refused by default but can be permitted per call (a reader that already split
|
|
496
|
+
* on CRLF, or a folding grammar). Distinct from the `C0_CTRL_RE` scanning table
|
|
497
|
+
* which always exempts LF/CR and never matches DEL.
|
|
498
|
+
*
|
|
499
|
+
* @opts
|
|
500
|
+
* forbidTab: boolean, // also forbid TAB -> predicate is `code < 0x20 || code === 0x7f`
|
|
501
|
+
* allowLf: boolean, // permit LF (0x0a)
|
|
502
|
+
* allowCr: boolean, // permit CR (0x0d)
|
|
503
|
+
*
|
|
504
|
+
* @example
|
|
505
|
+
* b.codepointClass.isForbiddenControlChar(0x00); // true (NUL)
|
|
506
|
+
* b.codepointClass.isForbiddenControlChar(0x09, { forbidTab: true }); // true (TAB forbidden)
|
|
507
|
+
*/
|
|
335
508
|
function isForbiddenControlChar(code, opts) {
|
|
336
509
|
if (code === 0x7f) return true; // DEL
|
|
337
510
|
if (code >= 0x20) return false;
|
|
@@ -343,12 +516,28 @@ function isForbiddenControlChar(code, opts) {
|
|
|
343
516
|
return true;
|
|
344
517
|
}
|
|
345
518
|
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
519
|
+
/**
|
|
520
|
+
* @primitive b.codepointClass.firstControlCharOffset
|
|
521
|
+
* @signature b.codepointClass.firstControlCharOffset(s, opts)
|
|
522
|
+
* @since 0.15.21
|
|
523
|
+
* @status stable
|
|
524
|
+
* @related b.codepointClass.isForbiddenControlChar
|
|
525
|
+
*
|
|
526
|
+
* Return the index of the first forbidden control char in `s` (under the same
|
|
527
|
+
* `opts` as `isForbiddenControlChar`), or `-1` when none. Callers wrap it as a
|
|
528
|
+
* boolean (`!== -1`), throw with the offending code (`s.charCodeAt(offset)`),
|
|
529
|
+
* or derive a byte offset — replacing the open-coded control-byte scan each
|
|
530
|
+
* parser previously rolled by hand.
|
|
531
|
+
*
|
|
532
|
+
* @opts
|
|
533
|
+
* forbidTab: boolean, // also treat TAB as forbidden
|
|
534
|
+
* allowLf: boolean, // permit LF (0x0a)
|
|
535
|
+
* allowCr: boolean, // permit CR (0x0d)
|
|
536
|
+
*
|
|
537
|
+
* @example
|
|
538
|
+
* b.codepointClass.firstControlCharOffset("ok\x00bad"); // 2 (the NUL)
|
|
539
|
+
* b.codepointClass.firstControlCharOffset("clean"); // -1
|
|
540
|
+
*/
|
|
352
541
|
function firstControlCharOffset(s, opts) {
|
|
353
542
|
for (var i = 0; i < s.length; i += 1) {
|
|
354
543
|
if (isForbiddenControlChar(s.charCodeAt(i), opts)) return i;
|
|
@@ -364,6 +553,24 @@ function firstControlCharOffset(s, opts) {
|
|
|
364
553
|
// / guard-markdown cannot drift on this (the bug class that shipped one buggy
|
|
365
554
|
// + one correct copy).
|
|
366
555
|
var NUMERIC_ENTITY_RE_G = /&#(?:x([0-9a-f]+)|(\d+));?/gi;
|
|
556
|
+
/**
|
|
557
|
+
* @primitive b.codepointClass.decodeNumericEntities
|
|
558
|
+
* @signature b.codepointClass.decodeNumericEntities(s)
|
|
559
|
+
* @since 0.15.21
|
|
560
|
+
* @status stable
|
|
561
|
+
* @related b.codepointClass.detectCharThreats
|
|
562
|
+
*
|
|
563
|
+
* Decode HTML numeric character references (hex `&#x..;` and decimal `&#..;`)
|
|
564
|
+
* just enough to expose a scheme hidden behind entity-encoding. The trailing
|
|
565
|
+
* semicolon is OPTIONAL — a browser decodes `javascript:` (no semicolon)
|
|
566
|
+
* the same as `javascript:`, so a semicolon-required decoder lets the
|
|
567
|
+
* no-semicolon form slip a scheme past an allowlist. Shared so the markup
|
|
568
|
+
* guards cannot drift on this.
|
|
569
|
+
*
|
|
570
|
+
* @example
|
|
571
|
+
* b.codepointClass.decodeNumericEntities("javascript:"); // "javascript:"
|
|
572
|
+
* b.codepointClass.decodeNumericEntities("javascript:"); // "javascript:" (no semicolon)
|
|
573
|
+
*/
|
|
367
574
|
function decodeNumericEntities(s) {
|
|
368
575
|
return String(s == null ? "" : s).replace(NUMERIC_ENTITY_RE_G, function (m, hex, dec) {
|
|
369
576
|
var cp = hex !== undefined ? parseInt(hex, 16) : parseInt(dec, 10);
|
|
@@ -73,9 +73,19 @@ function defineClass(name, opts) {
|
|
|
73
73
|
var alwaysPermanent = !!opts.alwaysPermanent;
|
|
74
74
|
var withStatusCode = !!opts.withStatusCode;
|
|
75
75
|
var withCause = !!opts.withCause;
|
|
76
|
+
// permanentClassifier(code, statusCode) => bool — DERIVES this.permanent from
|
|
77
|
+
// the error's own code (+ optional status) at EVERY construction, so a class
|
|
78
|
+
// whose terminal/transient nature depends on the code (a network client whose
|
|
79
|
+
// 4xx is terminal but 5xx is transient) classifies identically no matter which
|
|
80
|
+
// call site or helper builds it. The classifier's default governs a forgotten
|
|
81
|
+
// code, so it can fail closed. Constructor shape: (code, message, statusCode).
|
|
82
|
+
var permanentClassifier = typeof opts.permanentClassifier === "function" ? opts.permanentClassifier : null;
|
|
76
83
|
if (alwaysPermanent && (withStatusCode || withCause)) {
|
|
77
84
|
throw new Error("defineClass: alwaysPermanent is mutually exclusive with withStatusCode / withCause");
|
|
78
85
|
}
|
|
86
|
+
if (permanentClassifier && (alwaysPermanent || withStatusCode || withCause)) {
|
|
87
|
+
throw new Error("defineClass: permanentClassifier is mutually exclusive with alwaysPermanent / withStatusCode / withCause");
|
|
88
|
+
}
|
|
79
89
|
var flagKey = "is" + name;
|
|
80
90
|
|
|
81
91
|
// Generated class — uses an anonymous class expression so we can set
|
|
@@ -88,6 +98,10 @@ function defineClass(name, opts) {
|
|
|
88
98
|
this[flagKey] = true;
|
|
89
99
|
if (alwaysPermanent) {
|
|
90
100
|
this.permanent = true;
|
|
101
|
+
} else if (permanentClassifier) {
|
|
102
|
+
// (code, message, statusCode) — permanent derived from the code (+ status).
|
|
103
|
+
this.statusCode = arg3;
|
|
104
|
+
this.permanent = !!permanentClassifier(code, arg3);
|
|
91
105
|
} else if (withCause) {
|
|
92
106
|
this.cause = arg3;
|
|
93
107
|
} else {
|
|
@@ -71,7 +71,19 @@
|
|
|
71
71
|
* other concurrent calls await before they start.
|
|
72
72
|
* * Every transition emits `fsm.<machineName>.transition` via
|
|
73
73
|
* `audit.safeEmit` (drop-silent — operator audit-sink failures
|
|
74
|
-
* don't crash the caller).
|
|
74
|
+
* don't crash the caller). The state commits before the
|
|
75
|
+
* destination's `onEnter` runs, so a throwing `onEnter` still
|
|
76
|
+
* records the transition (with outcome `failure` + the error)
|
|
77
|
+
* rather than silently losing the audit entry.
|
|
78
|
+
* * `instance.target(event)` resolves a transition's destination
|
|
79
|
+
* state side-effect-free — same edge + guard check as `can()` but
|
|
80
|
+
* returns the to-state (or `null` when the edge is illegal /
|
|
81
|
+
* guard-refused). Use it to compose an external compare-and-swap
|
|
82
|
+
* (the cross-instance claim on autocommit-only substrates) without
|
|
83
|
+
* calling `transition()` before the claim is known to land.
|
|
84
|
+
* * `transition(event, { audit: false })` suppresses the built-in
|
|
85
|
+
* emit so that composition can emit its own enriched record once
|
|
86
|
+
* the external claim resolves.
|
|
75
87
|
*
|
|
76
88
|
* ## Serialization
|
|
77
89
|
*
|
|
@@ -309,6 +321,12 @@ function _buildInstance(def, initialState, initialHistory, initialContext) {
|
|
|
309
321
|
};
|
|
310
322
|
instance.allowed = function () { return _allowed(instance); };
|
|
311
323
|
instance.can = function (name) { return _can(instance, name); };
|
|
324
|
+
// Side-effect-free destination resolver — mirrors can() but returns the
|
|
325
|
+
// to-state (or null). Lets a consumer composing an EXTERNAL compare-and-swap
|
|
326
|
+
// (e.g. b.sql.guardedUpdate on an autocommit-only substrate) build the
|
|
327
|
+
// `SET status = <to>` claim without calling transition(), which would mutate
|
|
328
|
+
// state and emit an audit before the cross-instance claim is known to land.
|
|
329
|
+
instance.target = function (name) { return _resolveTarget(instance, name); };
|
|
312
330
|
instance.transition = function (name, opts) { return _enqueueTransition(instance, name, opts || {}); };
|
|
313
331
|
instance.toJSON = function () { return _toJSON(instance); };
|
|
314
332
|
return instance;
|
|
@@ -332,22 +350,35 @@ function _allowed(instance) {
|
|
|
332
350
|
return out;
|
|
333
351
|
}
|
|
334
352
|
|
|
335
|
-
|
|
336
|
-
|
|
353
|
+
// Resolve the destination state for `name` from the current state,
|
|
354
|
+
// side-effect-free: matches the (from, on) edge and runs the PURE guard
|
|
355
|
+
// (guards are contractually side-effect-free). Returns the to-state string
|
|
356
|
+
// when the transition is allowed, or null when the edge is illegal from the
|
|
357
|
+
// current state or the guard refuses / throws. The single source of truth
|
|
358
|
+
// for both can() (boolean) and target() (destination) so they can never
|
|
359
|
+
// disagree about whether an edge is takeable.
|
|
360
|
+
function _resolveTarget(instance, name) {
|
|
361
|
+
if (typeof name !== "string") return null;
|
|
337
362
|
var defs = instance._def._byName[name];
|
|
338
|
-
if (!defs) return
|
|
363
|
+
if (!defs) return null;
|
|
339
364
|
for (var i = 0; i < defs.length; i++) {
|
|
340
365
|
var t = defs[i];
|
|
341
366
|
if (t.from !== instance.state) continue;
|
|
342
367
|
if (t.guard) {
|
|
343
368
|
var verdict;
|
|
344
369
|
try { verdict = t.guard(instance.context); }
|
|
345
|
-
catch (_e) { return
|
|
346
|
-
if (verdict !== true) return
|
|
370
|
+
catch (_e) { return null; }
|
|
371
|
+
if (verdict !== true) return null;
|
|
347
372
|
}
|
|
348
|
-
return
|
|
373
|
+
return t.to;
|
|
349
374
|
}
|
|
350
|
-
return
|
|
375
|
+
return null;
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
// A to-state is always a non-empty identifier (validated at define-time), so
|
|
379
|
+
// "resolved !== null" is an exact "is this edge takeable" test.
|
|
380
|
+
function _can(instance, name) {
|
|
381
|
+
return _resolveTarget(instance, name) !== null;
|
|
351
382
|
}
|
|
352
383
|
|
|
353
384
|
function _enqueueTransition(instance, name, opts) {
|
|
@@ -428,25 +459,50 @@ async function _runTransition(instance, name, opts) {
|
|
|
428
459
|
if (opts.actor != null) historyEntry.actor = opts.actor;
|
|
429
460
|
if (opts.metadata != null) historyEntry.metadata = opts.metadata;
|
|
430
461
|
instance.history.push(historyEntry);
|
|
462
|
+
// onEnter on the destination state. The state is ALREADY committed and the
|
|
463
|
+
// history entry pushed, so the transition is durable no matter what onEnter
|
|
464
|
+
// does. A throw from onEnter must still surface to the caller (operators
|
|
465
|
+
// wrap it to roll back), but it must NOT skip the audit emit below: the
|
|
466
|
+
// state moved, and "every transition lands in the audit chain" is the
|
|
467
|
+
// primitive's contract. Capture the onEnter error, always emit, then
|
|
468
|
+
// re-throw — otherwise a throwing onEnter leaves a committed state change
|
|
469
|
+
// with no audit record (a compliance hole for HIPAA/SOX/PCI state trails).
|
|
470
|
+
var enterErr = null;
|
|
431
471
|
if (toBody && typeof toBody.onEnter === "function") {
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
472
|
+
try {
|
|
473
|
+
var enterResult = toBody.onEnter(instance.context);
|
|
474
|
+
if (enterResult && typeof enterResult.then === "function") {
|
|
475
|
+
await enterResult;
|
|
476
|
+
}
|
|
477
|
+
} catch (e) {
|
|
478
|
+
enterErr = e;
|
|
435
479
|
}
|
|
436
480
|
}
|
|
437
|
-
// Audit emission is drop-silent — operator audit-sink failures
|
|
438
|
-
//
|
|
439
|
-
//
|
|
440
|
-
//
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
481
|
+
// Audit emission is drop-silent — operator audit-sink failures never crash
|
|
482
|
+
// the caller. The .safeEmit wrapper is itself drop-silent; the additional
|
|
483
|
+
// try/catch protects against the lazy-loaded audit module throwing at
|
|
484
|
+
// first-access time. Suppressed when the caller passes { audit: false } so a
|
|
485
|
+
// composition driving an external compare-and-swap can emit its OWN enriched
|
|
486
|
+
// record (with the claim's rowCount / external txn id) once the claim is
|
|
487
|
+
// known to have landed — see instance.target(). When onEnter threw, the
|
|
488
|
+
// record still fires but stamps outcome "failure" + the error so an auditor
|
|
489
|
+
// sees the transition committed with a failed entry hook.
|
|
490
|
+
if (opts.audit !== false) {
|
|
491
|
+
try {
|
|
492
|
+
var auditMeta = {
|
|
493
|
+
from: fromState,
|
|
494
|
+
to: toState,
|
|
495
|
+
transition: name,
|
|
496
|
+
machine: instance._def.name,
|
|
497
|
+
callerMeta: opts.metadata || null,
|
|
498
|
+
};
|
|
499
|
+
if (enterErr) auditMeta.onEnterError = enterErr.message || String(enterErr);
|
|
500
|
+
audit().namespaced("fsm")(instance._def.name + ".transition",
|
|
501
|
+
enterErr ? "failure" : "success", auditMeta,
|
|
502
|
+
{ actor: opts.actor ? { id: opts.actor } : { id: "<system>" } });
|
|
503
|
+
} catch (_e) { /* drop-silent — audit best-effort */ }
|
|
504
|
+
}
|
|
505
|
+
if (enterErr) throw enterErr;
|
|
450
506
|
return { from: fromState, to: toState, on: name };
|
|
451
507
|
}
|
|
452
508
|
|