@blamejs/blamejs-shop 0.4.90 → 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.
Files changed (48) hide show
  1. package/CHANGELOG.md +4 -0
  2. package/lib/asset-manifest.json +1 -1
  3. package/lib/gift-card-ledger.js +8 -2
  4. package/lib/order.js +1 -0
  5. package/lib/vendor/MANIFEST.json +49 -41
  6. package/lib/vendor/blamejs/CHANGELOG.md +6 -0
  7. package/lib/vendor/blamejs/SECURITY.md +1 -0
  8. package/lib/vendor/blamejs/api-snapshot.json +208 -2
  9. package/lib/vendor/blamejs/examples/wiki/test/e2e.js +7 -4
  10. package/lib/vendor/blamejs/examples/wiki/test/integration.js +15 -12
  11. package/lib/vendor/blamejs/index.js +2 -0
  12. package/lib/vendor/blamejs/lib/audit-sign.js +34 -1
  13. package/lib/vendor/blamejs/lib/backup/manifest.js +191 -44
  14. package/lib/vendor/blamejs/lib/codepoint-class.js +284 -77
  15. package/lib/vendor/blamejs/lib/framework-error.js +14 -0
  16. package/lib/vendor/blamejs/lib/fsm.js +80 -24
  17. package/lib/vendor/blamejs/lib/log.js +32 -0
  18. package/lib/vendor/blamejs/lib/middleware/rate-limit.js +18 -2
  19. package/lib/vendor/blamejs/lib/middleware/request-id.js +24 -4
  20. package/lib/vendor/blamejs/lib/request-helpers.js +50 -0
  21. package/lib/vendor/blamejs/lib/safe-path.js +24 -10
  22. package/lib/vendor/blamejs/lib/sql.js +133 -0
  23. package/lib/vendor/blamejs/lib/totp.js +98 -33
  24. package/lib/vendor/blamejs/lib/ws-client.js +39 -28
  25. package/lib/vendor/blamejs/package.json +1 -1
  26. package/lib/vendor/blamejs/release-notes/v0.15.21.json +51 -0
  27. package/lib/vendor/blamejs/release-notes/v0.15.22.json +18 -0
  28. package/lib/vendor/blamejs/release-notes/v0.15.23.json +22 -0
  29. package/lib/vendor/blamejs/test/00-primitives.js +80 -0
  30. package/lib/vendor/blamejs/test/_smoke-worker.js +81 -0
  31. package/lib/vendor/blamejs/test/integration/federation-auth.test.js +7 -4
  32. package/lib/vendor/blamejs/test/integration/mail-crypto-smime.test.js +7 -4
  33. package/lib/vendor/blamejs/test/layer-0-primitives/backup-manifest-signature.test.js +91 -0
  34. package/lib/vendor/blamejs/test/layer-0-primitives/codebase-patterns.test.js +65 -0
  35. package/lib/vendor/blamejs/test/layer-0-primitives/codepoint-class.test.js +58 -0
  36. package/lib/vendor/blamejs/test/layer-0-primitives/defineguard-default-gate-posture-caps.test.js +5 -2
  37. package/lib/vendor/blamejs/test/layer-0-primitives/dpop-middleware-replaystore-required.test.js +9 -1
  38. package/lib/vendor/blamejs/test/layer-0-primitives/fsm.test.js +99 -0
  39. package/lib/vendor/blamejs/test/layer-0-primitives/money.test.js +30 -0
  40. package/lib/vendor/blamejs/test/layer-0-primitives/otlp-attr-redaction.test.js +9 -6
  41. package/lib/vendor/blamejs/test/layer-0-primitives/rate-limit-xff-spoofing.test.js +36 -0
  42. package/lib/vendor/blamejs/test/layer-0-primitives/request-helpers.test.js +33 -0
  43. package/lib/vendor/blamejs/test/layer-0-primitives/request-id-async-context.test.js +117 -0
  44. package/lib/vendor/blamejs/test/layer-0-primitives/safe-path.test.js +64 -0
  45. package/lib/vendor/blamejs/test/layer-0-primitives/sql.test.js +96 -0
  46. package/lib/vendor/blamejs/test/layer-0-primitives/ws-client.test.js +55 -0
  47. package/lib/vendor/blamejs/test/smoke.js +93 -10
  48. package/package.json +1 -1
@@ -1,9 +1,24 @@
1
1
  "use strict";
2
2
  /**
3
- * codepoint-class — shared codepoint-table threat catalog and regex
4
- * compiler for the guard-* family.
3
+ * @module b.codepointClass
4
+ * @nav Validation
5
+ * @title Codepoint Class
5
6
  *
6
- * Threat detectors that need to match Unicode bidi overrides, C0
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
- // scriptFor(cp) — returns the script-name string for a codepoint, or
124
- // null when the codepoint is in a script not in the catalog (digits,
125
- // punctuation, symbols, etc. are not script-classifying).
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&nbsp;#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
- // detectMixedScripts(label, allowedScripts?) — returns null when the
138
- // label is single-script (or every script appears in the optional
139
- // allowedScripts allowlist), or an array of the detected script names
140
- // when the label mixes scripts (homograph attack shape — Cyrillic 'а'
141
- // inside an otherwise-Latin label, etc.). The result is the FULL set
142
- // of scripts seen; callers decide refuse / audit / strip.
143
- //
144
- // allowedScripts: an array of script names the caller treats as
145
- // acceptable; when supplied, a label whose every script is on the list
146
- // returns null even if multiple scripts appear (legitimate mixed-
147
- // script content like an English word inside a Japanese label).
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&nbsp;#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
- // detectCharThreats — returns an array of issue objects for character-
166
- // class threats (bidi / null / C0-control) per the opts policy. Emits
167
- // at most one issue per class. Used by guard-* primitives' detection
168
- // pass instead of repeating the per-class match-and-push block.
169
- //
170
- // Issue shape mirrors guard-* convention:
171
- // { kind, severity, ruleId, location, snippet }
172
- //
173
- // issues.push.apply(issues,
174
- // codepointClass.detectCharThreats(text, opts, "html"));
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
- // assertNoCharThreats — throws an instance of errorFactory(code, msg)
237
- // when the text contains a class that's set to "reject" in opts.
238
- // Opt-name vocabulary: bidiPolicy / nullBytePolicy / controlPolicy
239
- // (the standard guard-* family naming; older guard-csv uses different
240
- // names and keeps its inline checks).
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
- // applyCharStripPolicies — given a text and a policy object, apply
258
- // strip-mode replacements for each character-class threat. Reads:
259
- // opts.bidiPolicy === "strip" -> strip BIDI overrides
260
- // opts.controlPolicy === "strip" -> strip C0 controls
261
- // opts.nullBytePolicy === "strip" -> strip null bytes
262
- // opts.zeroWidthPolicy === "strip" -> strip zero-widths
263
- // opts.tagsPolicy === "strip" -> strip Unicode Tags (U+E0000..)
264
- // Returns the cleaned string. Used by every guard's sanitize path so
265
- // each one doesn't reinvent the same sequence of replace() calls.
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
- // escapeRegExp — escape every RegExp metacharacter in a string so an
282
- // operator- or input-supplied token matches literally when spliced into a
283
- // `new RegExp(...)`. Three lib sites previously rolled the identical body;
284
- // centralized here so a token destined for dynamic compilation cannot
285
- // inject a pattern.
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
- // isAsciiAlnum — codepoint is an ASCII letter or digit (A-Z / a-z / 0-9).
297
- // The ASCII-alphanumeric range test recurs across every byte-class parser
298
- // (URL unreserved, XML name chars, header tokens); centralized so the three
299
- // range literals live once rather than as a re-rolled `cc>=0x41&&...` chain.
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
- // isUnreserved — codepoint is in the RFC 3986 §2.3 unreserved set:
307
- // ALPHA / DIGIT / "-" / "." / "_" / "~". A percent-escape of an unreserved
308
- // character is over-encoding the URI spec says SHOULD be decoded (§6.2.2.3).
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&nbsp;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
- // isForbiddenControlChar — the header-injection / RFC 5322 control-byte
318
- // predicate every "refuse control bytes in a header / line / value" loop
319
- // shares across the mail guards (dsn / imap / list-id / list-unsubscribe /
320
- // mail-compose / mail-sieve / managesieve) and the safe-* text parsers
321
- // (ical / mime / vcard / toml). TRUE for DEL (0x7f) and any C0 control
322
- // (< 0x20) other than TAB (0x09); LF (0x0a) and CR (0x0d) are refused by
323
- // default but can be permitted per call (a line reader that has already
324
- // split on CRLF, or a grammar that allows folding). Distinct from the
325
- // C0_CTRL_RANGES regex above, which is the stripping/scanning table that
326
- // always exempts LF/CR and never matches DEL.
327
- //
328
- // opts.forbidTab TAB (0x09) is normally permitted (it is folding
329
- // whitespace in RFC 5322 header values). The stricter identifier / key /
330
- // name / single-line-value contexts (idempotency keys, step-up challenge
331
- // values, mailbox / folder / message-id names, assembly ids, DNSBL-free
332
- // single-line values) forbid EVERY C0 control plus DEL there is no
333
- // folding to honour — so they pass `{ forbidTab: true }`, making the
334
- // predicate exactly `code < 0x20 || code === 0x7f`.
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&nbsp;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
- // firstControlCharOffset — index of the first isForbiddenControlChar in `s`
347
- // (under the same opts), or -1 when none. Callers wrap it as a boolean
348
- // (`offset !== -1`), throw with the offending code (`s.charCodeAt(offset)`),
349
- // or derive a byte offset from the index — replacing the open-coded
350
- // `for (i) { c = charCodeAt(i); if (c === 0 || c === 0x7f || (c < 0x20 &&
351
- // c !== 0x09)) … }` scan each parser previously rolled by hand.
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 `&#106avascript:` (no semicolon)
566
+ * the same as `&#106;avascript:`, 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("&#106;avascript:"); // "javascript:"
572
+ * b.codepointClass.decodeNumericEntities("&#106avascript:"); // "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
- function _can(instance, name) {
336
- if (typeof name !== "string") return false;
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 false;
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 false; }
346
- if (verdict !== true) return false;
370
+ catch (_e) { return null; }
371
+ if (verdict !== true) return null;
347
372
  }
348
- return true;
373
+ return t.to;
349
374
  }
350
- return false;
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
- var enterResult = toBody.onEnter(instance.context);
433
- if (enterResult && typeof enterResult.then === "function") {
434
- await enterResult;
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
- // never crash the caller. The .safeEmit wrapper is itself
439
- // drop-silent; the additional try/catch protects against the
440
- // lazy-loaded audit module throwing at first-access time.
441
- try {
442
- audit().namespaced("fsm")(instance._def.name + ".transition", "success", {
443
- from: fromState,
444
- to: toState,
445
- transition: name,
446
- machine: instance._def.name,
447
- callerMeta: opts.metadata || null,
448
- }, { actor: opts.actor ? { id: opts.actor } : { id: "<system>" } });
449
- } catch (_e) { /* drop-silent — audit best-effort */ }
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