@blamejs/core 0.18.36 → 0.18.38

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -8,6 +8,62 @@ upgrading across more than a few patches at a time.
8
8
 
9
9
  ## v0.18.x
10
10
 
11
+ - v0.18.38 (2026-08-19) — **`COPY ... TO STDIN` was reported as a server-side file access whenever it carried more than one space.** `b.guardSql`'s `copy-file` detector finds a `COPY` that reads or writes a file on the database server, and excludes the client-streaming `STDIN` and `STDOUT` forms because those touch no file. The exclusion was a negative lookahead, and it only worked when the whitespace before the keyword was exactly one character: `COPY t TO STDIN` was quiet, `COPY t TO STDIN` was reported critical.
12
+
13
+ The exclusion is now decided by reading the following word rather than by a lookahead, so the spacing no longer changes the verdict.
14
+
15
+ No API changes. Upgrade if you pass SQL fragments through `b.guardSql`. **Changed:** *The content-safety gate now refuses a pattern built at runtime* — `blamejs/no-regex-in-content-safety` reported pattern literals only, so `new RegExp(source)` passed it unnoticed anywhere under `lib/**/safe-*.js` or `lib/**/guard-*.js`. It now reports `new RegExp(...)` and `RegExp(...)`, in both the bare and member spellings — `globalThis.RegExp(src)` puts a MemberExpression in the callee and an identifier-only check permits it silently.
16
+
17
+ Three call sites carry a suppression with its reason recorded beside it. Two of them never match anything against input: `b.guardRegex` asks the parser whether an operator-supplied pattern compiles at all and discards the result, and `b.safeJson` compiles a schema `pattern` precisely so `assertSafe` can screen the compiled form. The third is `b.guardSql`'s detector table, described below. · *The SQL detectors stay on the platform engine, and the reason is now recorded* — These 32 detectors are built with `new RegExp` and run against a caller's SQL, which reads like something the content-safety rule should forbid. Moving them to `b.regexLinear` — whose cost is the length of the subject whatever the pattern says — was tried and measured, and it is worse:
18
+
19
+ | subject | linear | platform |
20
+ | --- | --- | --- |
21
+ | 158-byte benign SELECT | 1.4 ms | 0.001 ms |
22
+ | 4 KiB benign SELECT | 15.4 ms | 0.012 ms |
23
+
24
+ The engine expands each `{0,4000}` span into thousands of states, so every ordinary fragment pays. The platform engine's worst case over these same patterns, on adversarial input built to defeat every lazy span, is 5 ms at 32 KiB — less than the linear engine charges for a benign 4 KiB one. The swap would have made every request cost more than the attack it was meant to prevent.
25
+
26
+ What bounds these patterns is the patterns: every span is explicitly capped and none pairs two quantifiers over an overlapping alphabet. That is a property of this table rather than a general licence, and it is written where the table is defined so a new detector gets the same check.
27
+
28
+ SECURITY.md is corrected to match. It named a build gate retired in 0.18.37, said no primitive in the family "contains a regular expression" where the enforced claim is that none SCREENS with one, and now states plainly that a runtime-built pattern is not yet reported by the gate. **Fixed:** *The COPY file-access detector no longer depends on how much whitespace precedes STDIN* — The pattern excluded the safe forms with `\s+(?!STDIN\b|STDOUT\b)`. `\s+` is greedy but backtracks: given two spaces it could give one back, leaving ` STDIN` in front of the lookahead, which then read as "not STDIN" and reported a statement that opens no file.
29
+
30
+ ```
31
+ COPY t TO STDIN quiet
32
+ COPY t TO STDIN reported as "reads or writes a server-side file"
33
+ COPY t TO STDIN reported
34
+ COPY t TO \n STDIN reported
35
+ ```
36
+
37
+ The exclusion is now tested BEFORE the whitespace is consumed — `\b(?:TO|FROM)\b(?!\s+(?:STDIN|STDOUT)\b)\s+` — so it is evaluated once, at a fixed position, and cannot interact with that quantifier at all.
38
+
39
+ The obvious repair is worse. Teaching the lookahead to skip whitespace where it stood, `\s+(?!\s*(?:STDIN|STDOUT)\b)`, gives the two quantifiers the same alphabet: every backtrack re-scans the remainder of the run, which measures 45 ms on a 16,000-space run and grows quadratically — a denial of service introduced into the primitive whose job is to prevent one. Moving the test in front of `\s+` measures flat, and a cost regression check now covers it.
40
+
41
+ What the detector catches is unchanged, and the lookahead is kept rather than replaced with a scan for a specific reason: its backtracking is what finds the file in `COPY (SELECT x FROM STDOUT) TO '/tmp/x'`, where the first candidate is excluded and the one that matters appears later in the same statement. A scan that resumed past the excluded match would miss it, because every match has to begin at `COPY`. `COPY t TO '/etc/passwd'` fires, `COPY t TO STDINX` fires because the word does not end at `STDIN`, and `COPY t TO STDOUT; COPY u TO '/tmp/x'` fires on the second statement.
42
+
43
+ Twenty regression checks cover the whitespace forms, the nested case and the boundary. **References:** [PostgreSQL COPY — TO/FROM STDIN and STDOUT stream to the client](https://www.postgresql.org/docs/current/sql-copy.html)
44
+
45
+ - v0.18.37 (2026-08-19) — **The no-regex rule for content-safety primitives never covered lib/parsers, where 55 patterns were screening adversarial input.** SECURITY.md states that no `b.guard*` or `b.safe*` primitive contains a regular expression, so a screen costs the length of its input and the byte cap that bounds the input bounds the screen. The build gate enforcing that selected files with `lib/(safe-|guard-)[^/]+\.js` — top level only. Every nested primitive was therefore outside it, and all five of them are the ones that parse untrusted bytes: `b.parsers.yaml`, `b.parsers.toml`, `b.parsers.ini`, `b.parsers.env` and `b.parsers.xml`. Between them they carried 55 pattern literals.
46
+
47
+ All 55 are gone, and the gate now covers nested paths. Nothing about the parsers' behaviour changes: each screen was differential-tested against the pattern it replaced before the call site was swapped.
48
+
49
+ Upgrade if you parse YAML, TOML, INI, `.env` or XML from anything you do not control. **Changed:** *The no-regex gate asks a parser instead of walking the source itself* — Deciding whether a `/` opens a regular expression or divides means implementing the ECMAScript lexical grammar, and the previous scanner kept meeting parts of it that were not implemented yet — template substitutions nest, `of` is contextual, a labelled `break` ends at the terminator after its label, `<!--` is a comment, a shebang is not JavaScript.
50
+
51
+ The check now runs as an ESLint rule, which sees the file after it has already been parsed. All of those questions are answered before the rule is asked. It needs no new dependency: ESLint already runs over the repository as the first static gate, and a flat config takes an inline rule.
52
+
53
+ Suppression, if a case ever warrants it, is the standard `// eslint-disable-next-line blamejs/no-regex-in-content-safety` with the reason on the line above, replacing a bespoke marker that was never used.
54
+
55
+ The hand-written scanner is gone with it — about 1,240 lines, including the fixture table that had grown to justify it. Leaving both in place would have meant a justified exception passing one gate and failing the other, since the old one honoured only its own marker.
56
+
57
+ The rule reports pattern literals. `new RegExp(...)` is not yet included: three call sites use it and they are not one thing — two compile a pattern in order to validate it, which is the opposite of matching against input, while `b.guardSql` builds its detectors that way and does match input. Reporting all three today would mean either an allowlist entry for the one that should be caught, or a rushed conversion of a set of injection detectors. It is tracked instead, and the rule stays exactly as strict as the scanner it replaces. **Fixed:** *A YAML tag beginning with a digit slipped past the tag ban* — `b.parsers.yaml` refuses anchors, aliases, tags and directives. The tag check accepted `!` followed by a letter or `<`, while the comment beside it described the rule as "alphanumeric or `<`" — and the code was the weaker of the two. `a: !123` is a local tag, and it parsed as the plain string `"!123"` rather than being refused.
58
+
59
+ It is now refused, along with `!!123`. A quoted `"!123"` is still an ordinary string, because a quoted scalar is not a tag. **Security:** *Every pattern literal is gone from the five parsers that consume adversarial bytes* — Fifty-five of them: `safe-yaml` 24, `safe-toml` 16, `safe-env` 8, `safe-ini` 7, `safe-xml` 1.
60
+
61
+ The replacements walk characters: YAML's core-schema scalar resolution is now exact-set membership and a left-to-right digit walk; TOML's date, time and offset fields are index comparisons against fixed widths; INI's integer, float and hex classification and its `[name "subsection"]` header are single passes; `.env`'s inline-comment split, `export ` prefix and `$VAR` refusal are scans.
62
+
63
+ Every replacement was compared against the pattern it replaced over exhaustive short strings and adversarial cases before the call site changed — between roughly 60,000 and 1,400,000 inputs each, all with zero divergence. Where a reported line number derives from a match position, the position was compared too, not just the verdict.
64
+
65
+ One honest note on scale: these patterns were not all hanging. `.env`'s inline-comment matcher looked like catastrophic backtracking and is not — its separator and token classes are disjoint, so the partition of a line is unique, and it measures flat to 40,000 characters. The rule removes it anyway, and that is the point: the cost of a screen in this family holds by construction, rather than by someone re-deriving the ambiguity of each pattern correctly every time one is edited. **References:** [ESTree — RegExpLiteral carries the pattern and flags as parsed data](https://github.com/estree/estree/blob/master/es5.md#regexpliteral) · [OWASP — Regular expression Denial of Service (ReDoS)](https://owasp.org/www-community/attacks/Regular_expression_Denial_of_Service_-_ReDoS)
66
+
11
67
  - v0.18.36 (2026-08-18) — **Six constant-time comparisons told you which candidate matched, by how long they took.** A comparison that runs in constant time answers one question without leaking anything. A loop of them that stops at the first match answers a second question nobody asked: how far down the list the match was. Six places in the framework compared a presented value against a list of candidates and returned as soon as one matched, so the response time reported the match's position — which deny-list entry a certificate hit, which verifier's nonce a token carried, where in a key rotation the sender currently sits.
12
68
 
13
69
  Every one of them returned the correct answer. The defect is only in how long it took, which is why no test caught it and why the guard that replaces them is structural.
@@ -2321,6 +2321,12 @@ function _ambiguityFindings(src, flags) {
2321
2321
  // at all" and returned every finding false, so a quadratic pattern using
2322
2322
  // any of that syntax walked straight past this gate.
2323
2323
  var compiles = true;
2324
+ // The result is discarded and the pattern is never run against a subject —
2325
+ // this asks the parser whether `text` is a regular expression at all, which
2326
+ // is the opposite direction from screening input with one. Nothing a caller
2327
+ // supplies is matched here; a source that does not compile simply reports as
2328
+ // not-a-regex.
2329
+ // eslint-disable-next-line blamejs/no-regex-in-content-safety
2324
2330
  try { RegExp(text, typeof flags === "string" ? flags : ""); }
2325
2331
  catch (_e) { compiles = false; }
2326
2332
  if (compiles) {
package/lib/guard-sql.js CHANGED
@@ -248,11 +248,32 @@ var MASK_SPACE = " ";
248
248
 
249
249
  function _re(source) {
250
250
  // Construct each detector regex from an ASCII source string so the
251
- // source file embeds no attack-character literals. Case-insensitive;
252
- // detectors are intentionally global-free (first match is enough).
253
- return new RegExp(source, "i"); // allow:dynamic-regex detector source is a compile-time ASCII literal table below
251
+ // source file embeds no attack-character literals. Case-insensitive, and
252
+ // deliberately NOT global: `.test()` on a `g` regex advances `lastIndex` and
253
+ // answers differently on the next call with the same input.
254
+ //
255
+ // These stay on the platform engine, and that was measured rather than
256
+ // assumed. Compiling all 32 on b.regexLinear — whose cost is the length of the
257
+ // subject whatever the pattern says — makes a BENIGN 4 KiB fragment cost 15 ms
258
+ // against 0.012 ms here, because the NFA expands each `{0,4000}` span into
259
+ // thousands of states. The platform engine's own worst case over these
260
+ // patterns is 5 ms at 32 KiB of adversarial SQL, so the swap would have made
261
+ // every request pay more than the attack it was meant to prevent.
262
+ //
263
+ // What bounds these is the patterns themselves: every span is explicitly
264
+ // capped, and none pairs two quantifiers over an overlapping alphabet. That is
265
+ // a property of THIS table, not a general licence — a new detector needs the
266
+ // same check before it is added.
267
+ //
268
+ // The suppression below is the ONE audited exception in the family, and it is
269
+ // recorded rather than assumed: the alternative was measured and is worse, the
270
+ // sources are a fixed compile-time table rather than anything a caller
271
+ // supplies, and the cost is covered by a regression check in guard-sql.test.js.
272
+ // eslint-disable-next-line blamejs/no-regex-in-content-safety
273
+ return new RegExp(source, "i"); // allow:dynamic-regex — compile-time ASCII literal table, every span bounded
254
274
  }
255
275
 
276
+
256
277
  // \b word-boundary + optional whitespace/paren tolerance baked into
257
278
  // each source string. `[\s]` spans the comment-collapsed single spaces.
258
279
  var DETECTORS = [
@@ -263,7 +284,31 @@ var DETECTORS = [
263
284
  reason: "COPY ... PROGRAM executes a shell command (Postgres RCE)" },
264
285
  { code: "sql.file-access", severity: "critical", kind: "copy-file",
265
286
  family: "floor", dialect: "postgres",
266
- re: _re("\\bCOPY\\b[\\s\\S]{0,4000}?\\b(?:TO|FROM)\\b\\s+(?!STDIN\\b|STDOUT\\b)"),
287
+ // STDIN / STDOUT stream to the client and touch no server-side file, so
288
+ // they are excluded — and the exclusion is tested BEFORE the whitespace is
289
+ // consumed, which is the whole trick here.
290
+ //
291
+ // Written the other way round, after `\s+`, it is wrong: `\s+` is greedy but
292
+ // backtracks, so given two spaces it gives one back, leaves " STDIN" in
293
+ // front of a lookahead that only knows how to reject "STDIN", and reports a
294
+ // client-side `COPY t TO STDIN` as a server-side file access. One space was
295
+ // quiet and two were critical.
296
+ //
297
+ // Teaching that lookahead to skip whitespace (`(?!\s*(?:STDIN|STDOUT)\b)`)
298
+ // fixes the verdict and introduces a worse problem: `\s+` and the `\s*`
299
+ // range over the same characters, so every backtrack re-scans the rest of
300
+ // the run — 45 ms on a 16k-space run, quadratic, in the one primitive whose
301
+ // job is to not be a denial of service.
302
+ //
303
+ // In front of `\s+` the lookahead is evaluated once, at a fixed position,
304
+ // and cannot interact with that quantifier at all. Measured flat.
305
+ //
306
+ // A lookahead rather than a scan-and-compare because its backtracking is
307
+ // what finds the file in `COPY (SELECT x FROM STDOUT) TO '/tmp/x'`: the
308
+ // first candidate is excluded and the one that matters comes later in the
309
+ // SAME statement, which a scan resuming past the excluded match would miss,
310
+ // since every match has to begin at COPY.
311
+ re: _re("\\bCOPY\\b[\\s\\S]{0,4000}?\\b(?:TO|FROM)\\b(?!\\s+(?:STDIN|STDOUT)\\b)\\s+"),
267
312
  reason: "COPY TO/FROM <file> reads or writes a server-side file" },
268
313
  { code: "sql.file-access", severity: "critical", kind: "large-object",
269
314
  family: "floor", dialect: "postgres",
@@ -64,6 +64,7 @@ var lazyRequire = require("../lazy-require");
64
64
  var numericBounds = require("../numeric-bounds");
65
65
  var safeBuffer = require("../safe-buffer");
66
66
  var safeJson = require("../safe-json");
67
+ var codepointClass = require("../codepoint-class");
67
68
  var { FrameworkError } = require("../framework-error");
68
69
  var { boot } = require("../log");
69
70
 
@@ -90,10 +91,117 @@ class SafeEnvError extends FrameworkError {
90
91
  }
91
92
  }
92
93
 
94
+ // Split an unquoted value at an inline `# comment`, returning the text BEFORE
95
+ // the comment, or null when there is no comment to strip. Replaces
96
+ // `/^([^\s#]*(?:[ \t]+[^#\s]+)*)\s+#.*$/`.
97
+ //
98
+ // That pattern was NOT catastrophic, and it is worth saying so rather than
99
+ // implying the family rule only ever removes live bugs. `[ \t]+` and `[^#\s]+`
100
+ // are disjoint, so the partition of a line into separators and tokens is unique
101
+ // and there is nothing for the engine to backtrack through — measured flat to
102
+ // 40k characters across every shape that defeats the match. It goes because the
103
+ // rule is uniform: a screen in this family costs the length of its input, and
104
+ // that holds by construction instead of by someone re-deriving the ambiguity of
105
+ // each pattern correctly, every time one is edited. The neighbouring
106
+ // trailing-whitespace strip is already a walk, for a reason that WAS measured —
107
+ // `.replace(/[ \t]+$/)` is quadratic in V8 and the parser caps total bytes, not
108
+ // bytes per line.
109
+ //
110
+ // Reading the pattern back gives a rule with no search in it. Group 1 admits no
111
+ // `#` at all, so the `#` the pattern finds is always the FIRST one; it must be
112
+ // preceded by whitespace; and everything before that whitespace run may only be
113
+ // separated by spaces and tabs (a `\v`, `\f` or U+00A0 is whitespace to `\s` but
114
+ // not to `[ \t]`, so the pattern would not match and neither does this).
115
+ function _splitInlineComment(rest) {
116
+ var hash = rest.indexOf("#");
117
+ if (hash <= 0) return null; // absent, or nothing before it
118
+ if (!codepointClass.inRanges(rest.charCodeAt(hash - 1), codepointClass.WHITESPACE_RANGES)) {
119
+ return null; // `color#red` keeps its `#`
120
+ }
121
+ // Walk back over the whitespace run that `\s+` consumed.
122
+ var end = hash - 1;
123
+ while (end >= 0 && codepointClass.inRanges(rest.charCodeAt(end), codepointClass.WHITESPACE_RANGES)) {
124
+ end -= 1;
125
+ }
126
+ var head = rest.slice(0, end + 1);
127
+ // The separators inside group 1 are `[ \t]` only.
128
+ for (var i = 0; i < head.length; i += 1) {
129
+ var cc = head.charCodeAt(i);
130
+ if (!codepointClass.inRanges(cc, codepointClass.WHITESPACE_RANGES)) continue;
131
+ if (cc !== 0x20 && cc !== 0x09) return null;
132
+ }
133
+ // `#.*$` cannot cross a line terminator, and `$` without `m` is end-of-string.
134
+ // Lines arrive split on LF and CR already, but U+2028 and U+2029 survive that
135
+ // and are terminators to the grammar, so a comment containing one meant the
136
+ // pattern did not match and the whole value was kept.
137
+ for (var t = hash; t < rest.length; t += 1) {
138
+ var tc = rest.charCodeAt(t);
139
+ if (tc === 0x0A || tc === 0x0D || tc === 0x2028 || tc === 0x2029) return null;
140
+ }
141
+ return head;
142
+ }
143
+
144
+ // `export FOO=bar` — the POSIX shell convention. Replaces a `/^export\s+/` test
145
+ // paired with an identical `.replace`, which walked the prefix twice.
146
+ // The default key shape, `^[A-Z_][A-Z0-9_]*$`, as a walk. The label is what the
147
+ // refusal message quotes, so an operator still sees the shape they violated
148
+ // rather than the word "function".
149
+ var _ASCII_UPPER = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
150
+ var _KEY_HEAD_CHARS = _ASCII_UPPER + "_";
151
+ var _KEY_TAIL_CHARS = _ASCII_UPPER + codepointClass.ASCII_DIGITS + "_";
152
+ var DEFAULT_KEY_SHAPE = "^[A-Z_][A-Z0-9_]*$";
153
+
154
+ function _matchesDefaultKeyShape(key) {
155
+ if (typeof key !== "string" || key.length === 0) return false;
156
+ if (_KEY_HEAD_CHARS.indexOf(key.charAt(0)) === -1) return false;
157
+ if (key.length === 1) return true; // isRunOf("") is false
158
+ return codepointClass.isRunOf(key.slice(1), _KEY_TAIL_CHARS);
159
+ }
160
+
161
+ var _EXPORT = "export";
162
+
163
+ function _stripExportPrefix(line) {
164
+ if (line.slice(0, _EXPORT.length) !== _EXPORT) return line;
165
+ var i = _EXPORT.length;
166
+ var afterWord = i;
167
+ while (i < line.length &&
168
+ codepointClass.inRanges(line.charCodeAt(i), codepointClass.WHITESPACE_RANGES)) i += 1;
169
+ if (i === afterWord) return line; // `exported=1` is a key, not a prefix
170
+ return line.slice(i);
171
+ }
172
+
173
+ // `$VAR` and `${VAR}` references, which this parser refuses rather than expands.
174
+ // Replaces `/\$(\{[A-Za-z_]|[A-Za-z_])/` — a `$` followed either by a brace and
175
+ // an identifier head, or by an identifier head directly. A trailing `$`, `$1` or
176
+ // `$ ` is a literal dollar and stays one.
177
+ function _isIdentifierHead(ch) {
178
+ // The length guard is load-bearing: `charAt` past the end returns "", and
179
+ // `indexOf("")` is 0 on every string, so without it a trailing `$` reads as a
180
+ // reference and a literal dollar at end-of-value is refused.
181
+ if (ch.length !== 1) return false;
182
+ return ch === "_" || codepointClass.ASCII_ALPHA.indexOf(ch) !== -1;
183
+ }
184
+
185
+ function _hasVariableReference(text) {
186
+ for (var i = 0; i < text.length; i += 1) {
187
+ if (text.charAt(i) !== "$") continue;
188
+ var next = text.charAt(i + 1);
189
+ if (next === "{") {
190
+ if (_isIdentifierHead(text.charAt(i + 2))) return true;
191
+ continue;
192
+ }
193
+ if (_isIdentifierHead(next)) return true;
194
+ }
195
+ return false;
196
+ }
197
+
93
198
  var DEFAULTS = {
94
199
  maxBytes: C.BYTES.kib(64),
95
200
  maxKeys: 1_000,
96
- keyShape: /^[A-Z_][A-Z0-9_]*$/,
201
+ // The DEFAULT shape is a walk, not a pattern. An operator may still pass a
202
+ // RegExp — there the pattern is the INPUT, which is the one case this family
203
+ // allows — and that override is honoured unchanged below.
204
+ keyShape: null,
97
205
  applyToProcess: false,
98
206
  allowOverwrite: false,
99
207
  rejectUnknown: false,
@@ -120,7 +228,11 @@ function parse(input, opts) {
120
228
  ? Math.min(opts.maxBytes, C.BYTES.mib(1)) : DEFAULTS.maxBytes;
121
229
  var maxKeys = opts.maxKeys !== undefined
122
230
  ? Math.min(opts.maxKeys, 100_000) : DEFAULTS.maxKeys;
123
- var keyShape = opts.keyShape instanceof RegExp ? opts.keyShape : DEFAULTS.keyShape;
231
+ var operatorKeyShape = opts.keyShape instanceof RegExp ? opts.keyShape : null;
232
+ var keyShapeLabel = operatorKeyShape ? String(operatorKeyShape) : DEFAULT_KEY_SHAPE;
233
+ function keyShapeAccepts(key) {
234
+ return operatorKeyShape ? operatorKeyShape.test(key) : _matchesDefaultKeyShape(key);
235
+ }
124
236
 
125
237
  input = safeBuffer.normalizeText(input, {
126
238
  maxBytes: maxBytes,
@@ -129,7 +241,9 @@ function parse(input, opts) {
129
241
  sizeCode: "env/too-large",
130
242
  });
131
243
 
132
- var rawLines = input.split(/\r\n|\r|\n/);
244
+ // splitLinesAny breaks on LF, CR and CRLF-as-one — the same three the
245
+ // `/\r\n|\r|\n/` alternation covered, in the same order of preference.
246
+ var rawLines = codepointClass.splitLinesAny(input);
133
247
  var values = Object.create(null);
134
248
  var seen = new Set();
135
249
 
@@ -137,14 +251,12 @@ function parse(input, opts) {
137
251
  var line = rawLines[i];
138
252
  var lineNumber = i + 1;
139
253
  // Trim leading whitespace (operators sometimes indent for readability)
140
- var trimmed = line.replace(/^[ \t]+/, "");
254
+ var trimmed = codepointClass.trimChars(line, " \t", { trailing: false });
141
255
  if (trimmed.length === 0) continue;
142
256
  if (trimmed.charAt(0) === "#") continue;
143
257
 
144
258
  // Optional `export ` prefix (POSIX shell convention)
145
- if (/^export\s+/.test(trimmed)) {
146
- trimmed = trimmed.replace(/^export\s+/, "");
147
- }
259
+ trimmed = _stripExportPrefix(trimmed);
148
260
 
149
261
  var eqIdx = trimmed.indexOf("=");
150
262
  if (eqIdx < 0) {
@@ -159,9 +271,9 @@ function parse(input, opts) {
159
271
  if (pick.isPoisonedKey(key)) {
160
272
  throw new SafeEnvError("forbidden key '" + key + "'", "env/poisoned-key", lineNumber);
161
273
  }
162
- if (!keyShape.test(key)) {
274
+ if (!keyShapeAccepts(key)) {
163
275
  throw new SafeEnvError(
164
- "key '" + key + "' does not match keyShape " + keyShape,
276
+ "key '" + key + "' does not match keyShape " + keyShapeLabel,
165
277
  "env/bad-key-shape", lineNumber
166
278
  );
167
279
  }
@@ -188,18 +300,14 @@ function parse(input, opts) {
188
300
  // Unquoted value: strip trailing whitespace + inline `# comment`.
189
301
  // The comment marker MUST be preceded by whitespace to count
190
302
  // (so a value like `KEY=color#red` keeps the literal `#`).
191
- var commentMatch = rest.match(/^([^\s#]*(?:[ \t]+[^#\s]+)*)\s+#.*$/);
303
+ var beforeComment = _splitInlineComment(rest);
192
304
  // stripTrailingHspace is a linear char-scan; .replace(/[ \t]+$/) is O(n^2)
193
305
  // in V8 and the env parser only caps TOTAL bytes, not per-line, so a
194
306
  // single huge-whitespace value line would otherwise hang the parser.
195
- if (commentMatch) {
196
- value = safeBuffer.stripTrailingHspace(commentMatch[1]);
197
- } else {
198
- value = safeBuffer.stripTrailingHspace(rest);
199
- }
307
+ value = safeBuffer.stripTrailingHspace(beforeComment === null ? rest : beforeComment);
200
308
  // Reject `$VAR` style references — explicit error so operators
201
309
  // see the policy rather than silently getting unexpanded text.
202
- if (/\$(\{[A-Za-z_]|[A-Za-z_])/.test(value)) {
310
+ if (_hasVariableReference(value)) {
203
311
  throw new SafeEnvError(
204
312
  "$VAR / ${VAR} expansion not supported (escape with \\$ if literal, or quote and expand yourself)",
205
313
  "env/expansion-banned", lineNumber
@@ -254,7 +362,8 @@ function _decodeDoubleQuoted(rest, lineNumber) {
254
362
  // Optional inline comment after closing quote — ignore.
255
363
  return out;
256
364
  }
257
- if (ch === "$" && /^[{A-Za-z_]/.test(rest.charAt(i + 1) || "")) {
365
+ var afterDollar = rest.charAt(i + 1);
366
+ if (ch === "$" && (afterDollar === "{" || _isIdentifierHead(afterDollar))) {
258
367
  throw new SafeEnvError(
259
368
  "$VAR / ${VAR} expansion not supported in double-quoted value (use \\$ for literal $)",
260
369
  "env/expansion-banned", lineNumber
@@ -41,6 +41,7 @@ var C = require("../constants");
41
41
  var pick = require("../pick");
42
42
  var numericBounds = require("../numeric-bounds");
43
43
  var safeBuffer = require("../safe-buffer");
44
+ var codepointClass = require("../codepoint-class");
44
45
  var { defineClass } = require("../framework-error");
45
46
 
46
47
  var IniSafeError = defineClass("IniSafeError", { alwaysPermanent: true });
@@ -69,7 +70,7 @@ function _stripComment(line) {
69
70
  if (c === "\"" && !inSingle) { inDouble = !inDouble; continue; }
70
71
  if (c === "'" && !inDouble) { inSingle = !inSingle; continue; }
71
72
  if (!inSingle && !inDouble && (c === ";" || c === "#")) {
72
- if (i === 0 || /\s/.test(line.charAt(i - 1))) {
73
+ if (i === 0 || codepointClass.inRanges(line.charCodeAt(i - 1), codepointClass.WHITESPACE_RANGES)) {
73
74
  return line.slice(0, i);
74
75
  }
75
76
  }
@@ -110,6 +111,113 @@ function _unquote(raw) {
110
111
  return s;
111
112
  }
112
113
 
114
+ // Lexical shape tests for the value forms INI coerces. Each replaces an
115
+ // anchored pattern and walks the string once, so the cost is its length —
116
+ // values arrive from a file an operator may not control, and a screen whose
117
+ // cost depends on the arrangement of the characters is what this family avoids.
118
+ //
119
+ // `\d` in the patterns these replace is ASCII-only, so isAsciiDigit matches it
120
+ // exactly; a walk using a Unicode digit test would accept more than the pattern
121
+ // did and silently widen what parses as a number.
122
+
123
+ // `/^0x[0-9a-f]+$/i` — the prefix, then at least one hex digit, nothing else.
124
+ function _isHexInteger(s) {
125
+ if (s.length < 3) return false;
126
+ if (s.charAt(0) !== "0") return false;
127
+ var x = s.charAt(1);
128
+ if (x !== "x" && x !== "X") return false;
129
+ return codepointClass.isRunOf(s.slice(2), codepointClass.ASCII_HEX);
130
+ }
131
+
132
+ // Digits from `from` to the end, at least one. The shared tail of every shape
133
+ // below, and the reason none of them needs a quantifier.
134
+ function _digitsToEnd(s, from) {
135
+ if (from >= s.length) return false;
136
+ return codepointClass.isRunOf(s.slice(from), codepointClass.ASCII_DIGITS);
137
+ }
138
+
139
+ function _digitRunEnd(s, from) {
140
+ var i = from;
141
+ while (i < s.length && codepointClass.isAsciiDigit(s.charCodeAt(i))) i += 1;
142
+ return i; // === from when no digits
143
+ }
144
+
145
+ // `/^-?\d+$/`
146
+ function _isDecimalInteger(s) {
147
+ return _digitsToEnd(s, s.charAt(0) === "-" ? 1 : 0);
148
+ }
149
+
150
+ // `/^-?\d+\.\d+([eE][+-]?\d+)?$/` or `/^-?\d+[eE][+-]?\d+$/` — a float needs
151
+ // either a fractional part, an exponent, or both, which is what keeps a bare
152
+ // integer out of this branch and in the one above it.
153
+ function _isDecimalFloat(s) {
154
+ var i = s.charAt(0) === "-" ? 1 : 0;
155
+ var afterInt = _digitRunEnd(s, i);
156
+ if (afterInt === i) return false; // no integer part
157
+ i = afterInt;
158
+ var sawFraction = false;
159
+ if (s.charAt(i) === ".") {
160
+ var afterFrac = _digitRunEnd(s, i + 1);
161
+ if (afterFrac === i + 1) return false; // `1.` is not a float here
162
+ i = afterFrac;
163
+ sawFraction = true;
164
+ }
165
+ var e = s.charAt(i);
166
+ if (e === "e" || e === "E") {
167
+ var j = i + 1;
168
+ var sign = s.charAt(j);
169
+ if (sign === "+" || sign === "-") j += 1;
170
+ if (!_digitsToEnd(s, j)) return false;
171
+ return true; // exponent consumed the rest
172
+ }
173
+ return sawFraction && i === s.length;
174
+ }
175
+
176
+ // `[name "subsection"]` — the git-config form. Replaces
177
+ // `/^([A-Za-z0-9._-]+)\s+"([^"\\]*(?:\\.[^"\\]*)*)"$/`, whose inner group is the
178
+ // unrolled-loop idiom for a quoted string; walking it is both clearer and free
179
+ // of the backtracking that idiom exists to avoid.
180
+ //
181
+ // Two details of the pattern are load-bearing and preserved here. The value is
182
+ // captured VERBATIM, escapes included — `_unquote` does the unescaping later, so
183
+ // consuming the backslash here would double-unescape. And `\\.` cannot match a
184
+ // line terminator, so a backslash before one is not an escape and the whole
185
+ // header fails to match rather than swallowing the break.
186
+ var _SECTION_NAME_CHARS = codepointClass.ASCII_ALNUM + "._-";
187
+
188
+ function _isDotAtom(ch) {
189
+ // The characters `.` does NOT match without the `s` flag.
190
+ return ch !== "\n" && ch !== "\r" && ch !== "\u2028" && ch !== "\u2029";
191
+ }
192
+
193
+ function _parseQuotedSectionHeader(inner) {
194
+ var i = 0;
195
+ while (i < inner.length && _SECTION_NAME_CHARS.indexOf(inner.charAt(i)) !== -1) i += 1;
196
+ if (i === 0) return null; // name needs one char
197
+ var name = inner.slice(0, i);
198
+ var afterName = i;
199
+ while (i < inner.length &&
200
+ codepointClass.inRanges(inner.charCodeAt(i), codepointClass.WHITESPACE_RANGES)) i += 1;
201
+ if (i === afterName) return null; // `\s+` needs one
202
+ if (inner.charAt(i) !== "\"") return null;
203
+ i += 1;
204
+ var value = "";
205
+ while (i < inner.length) {
206
+ var ch = inner.charAt(i);
207
+ if (ch === "\\") {
208
+ var next = inner.charAt(i + 1);
209
+ if (i + 1 >= inner.length || !_isDotAtom(next)) return null;
210
+ value += ch + next;
211
+ i += 2;
212
+ continue;
213
+ }
214
+ if (ch === "\"") return i === inner.length - 1 ? [name, value] : null;
215
+ value += ch;
216
+ i += 1;
217
+ }
218
+ return null; // unterminated quote
219
+ }
220
+
113
221
  function _coerceValue(raw) {
114
222
  if (raw.length === 0) return raw;
115
223
  var first = raw.charAt(0);
@@ -117,21 +225,21 @@ function _coerceValue(raw) {
117
225
  var lower = raw.toLowerCase();
118
226
  if (TRUE_VALUES.has(lower)) return true;
119
227
  if (FALSE_VALUES.has(lower)) return false;
120
- if (/^0x[0-9a-f]+$/i.test(raw)) {
228
+ if (_isHexInteger(raw)) {
121
229
  var hex = parseInt(raw, RADIX_HEX);
122
230
  if (!Number.isSafeInteger(hex)) {
123
231
  throw _err("ini/value-out-of-range", "hex integer exceeds safe-integer range: " + raw);
124
232
  }
125
233
  return hex;
126
234
  }
127
- if (/^-?\d+$/.test(raw)) {
235
+ if (_isDecimalInteger(raw)) {
128
236
  var n = Number(raw);
129
237
  if (!Number.isSafeInteger(n)) {
130
238
  throw _err("ini/value-out-of-range", "integer exceeds safe-integer range: " + raw);
131
239
  }
132
240
  return n;
133
241
  }
134
- if (/^-?\d+\.\d+([eE][+-]?\d+)?$/.test(raw) || /^-?\d+[eE][+-]?\d+$/.test(raw)) {
242
+ if (_isDecimalFloat(raw)) {
135
243
  var f = Number(raw);
136
244
  // Float overflow (e.g. 1e999) coerces to ±Infinity — the same
137
245
  // never-silently-coerce refusal the integer/hex branches enforce with
@@ -178,10 +286,8 @@ function _parseSectionHeader(line) {
178
286
  if (inner.length === 0) {
179
287
  throw _err("ini/empty-section", "section header [] has no name");
180
288
  }
181
- var quotedMatch = /^([A-Za-z0-9._-]+)\s+"([^"\\]*(?:\\.[^"\\]*)*)"$/.exec(inner);
182
- if (quotedMatch) {
183
- return [quotedMatch[1], quotedMatch[2]];
184
- }
289
+ var quoted = _parseQuotedSectionHeader(inner);
290
+ if (quoted) return quoted;
185
291
  var parts = inner.split(".");
186
292
  for (var i = 0; i < parts.length; i++) {
187
293
  if (parts[i].length === 0) {
@@ -240,7 +346,7 @@ function parse(input, opts) {
240
346
  var sectionCount = 0;
241
347
  var keysInCurrentSection = 0;
242
348
 
243
- var lines = input.split(/\r?\n/);
349
+ var lines = codepointClass.splitLines(input);
244
350
  for (var li = 0; li < lines.length; li++) {
245
351
  var raw = lines[li];
246
352
  var stripped = _stripComment(raw).trim();
@@ -71,6 +71,61 @@ var RADIX_BIN = 0x2;
71
71
  var RADIX_OCTAL = 0x8;
72
72
  var RADIX_HEX = 0x10;
73
73
 
74
+ // Character sets and fixed-width shapes, as walks. TOML's grammar is all
75
+ // fixed-width date and time fields and single-character classes, so every one of
76
+ // these replaces a pattern with an index comparison — nothing here needs a
77
+ // search, and the parser's cost stays the length of the document.
78
+ var _OCTAL_DIGITS = "01234567";
79
+ var _BINARY_DIGITS = "01";
80
+ var _BARE_KEY_TAIL = codepointClass.ASCII_ALNUM + "_";
81
+ var _NUMBER_LEADS = codepointClass.ASCII_DIGITS + "+-i n"; // `inf`, `nan`, sign, space
82
+
83
+ function _isDigit(ch) { return ch.length === 1 && codepointClass.ASCII_DIGITS.indexOf(ch) !== -1; }
84
+ function _isHexDigit(ch) { return ch.length === 1 && codepointClass.ASCII_HEX.indexOf(ch) !== -1; }
85
+ function _isOctalDigit(ch) { return ch.length === 1 && _OCTAL_DIGITS.indexOf(ch) !== -1; }
86
+ function _isBinaryDigit(ch){ return ch.length === 1 && _BINARY_DIGITS.indexOf(ch) !== -1; }
87
+
88
+ // `charAt` past the end returns "", and `"".indexOf` is 0 on every string, so
89
+ // each of these guards its length before asking. Without it an out-of-range read
90
+ // answers "yes" and the shape checks accept a truncated document.
91
+ function _isBareKeyChar(ch) { return ch.length === 1 && _BARE_KEY_TAIL.indexOf(ch) !== -1; }
92
+ function _isNumberLead(ch) { return ch.length === 1 && _NUMBER_LEADS.indexOf(ch) !== -1; }
93
+
94
+ function _digitsAt(text, from, count) {
95
+ if (from + count > text.length) return false;
96
+ for (var i = 0; i < count; i += 1) {
97
+ if (!_isDigit(text.charAt(from + i))) return false;
98
+ }
99
+ return true;
100
+ }
101
+
102
+ // `\d{4}-\d{2}-\d{2}` anchored at the start of `text`.
103
+ function _looksLikeDate(text) {
104
+ return _digitsAt(text, 0, 4) && text.charAt(4) === "-" &&
105
+ _digitsAt(text, 5, 2) && text.charAt(7) === "-" &&
106
+ _digitsAt(text, 8, 2);
107
+ }
108
+
109
+ // `\d{2}:\d{2}:\d{2}` anchored at the start of `text`.
110
+ function _looksLikeTime(text) {
111
+ return _digitsAt(text, 0, 2) && text.charAt(2) === ":" &&
112
+ _digitsAt(text, 3, 2) && text.charAt(5) === ":" &&
113
+ _digitsAt(text, 6, 2);
114
+ }
115
+
116
+ // `^[+-]\d{2}:\d{2}$` — a whole numeric UTC offset, nothing after it.
117
+ function _isNumericOffset(text) {
118
+ if (text.length !== 6) return false;
119
+ var sign = text.charAt(0);
120
+ if (sign !== "+" && sign !== "-") return false;
121
+ return _digitsAt(text, 1, 2) && text.charAt(3) === ":" && _digitsAt(text, 4, 2);
122
+ }
123
+
124
+ // Strip the `_` digit separators TOML allows inside numbers.
125
+ function _stripUnderscores(text) {
126
+ return text.indexOf("_") === -1 ? text : codepointClass.stripRanges(text, [0x5F]);
127
+ }
128
+
74
129
  // Date-time literal character widths (per TOML / RFC 3339).
75
130
  var TIME_CHARS = 0x8; // "HH:MM:SS"
76
131
  var OFFSET_CHARS = 0x6; // "+HH:MM"
@@ -285,7 +340,7 @@ function parse(input, opts) {
285
340
  case "U": {
286
341
  var hexLen = 0x8;
287
342
  var hex8 = input.substring(pos, pos + hexLen);
288
- if (hex8.length < hexLen || !/^[0-9a-fA-F]{8}$/.test(hex8)) {
343
+ if (hex8.length < hexLen || hex8.length !== 8 || !codepointClass.isRunOf(hex8, codepointClass.ASCII_HEX)) {
289
344
  throw _err("bad \\U escape", "toml/bad-escape");
290
345
  }
291
346
  _advance(hexLen);
@@ -404,19 +459,19 @@ function parse(input, opts) {
404
459
  // current position doesn't start a date-time literal.
405
460
  function _tryParseDateTime() {
406
461
  // Date-time form: YYYY-MM-DD followed by 'T'/' ' followed by time
407
- if (pos + 10 <= len && /^\d{4}-\d{2}-\d{2}/.test(input.substr(pos, 10))) {
462
+ if (pos + 10 <= len && _looksLikeDate(input.substr(pos, 10))) {
408
463
  var sep = _peek(10);
409
464
  if (sep === "T" || sep === "t" || sep === " ") {
410
465
  // Look ahead for time portion (HH:MM:SS = 8 chars)
411
466
  var timeStart = pos + 11;
412
467
  var timeChars = TIME_CHARS;
413
- if (/^\d{2}:\d{2}:\d{2}/.test(input.substr(timeStart, timeChars))) {
468
+ if (_looksLikeTime(input.substr(timeStart, timeChars))) {
414
469
  var datePart = input.substr(pos, 10);
415
470
  var timeEnd = timeStart + timeChars;
416
471
  // Optional fractional
417
472
  if (_peek(timeEnd - pos) === ".") {
418
473
  timeEnd += 1;
419
- while (timeEnd < len && /\d/.test(input.charAt(timeEnd))) timeEnd += 1;
474
+ while (timeEnd < len && _isDigit(input.charAt(timeEnd))) timeEnd += 1;
420
475
  }
421
476
  var timePart = input.substring(timeStart, timeEnd);
422
477
  // Optional offset
@@ -427,7 +482,7 @@ function parse(input, opts) {
427
482
  // Expect ±HH:MM (6 chars)
428
483
  var offsetChars = OFFSET_CHARS;
429
484
  var off = input.substr(timeEnd, offsetChars);
430
- if (/^[+-]\d{2}:\d{2}$/.test(off)) {
485
+ if (_isNumericOffset(off)) {
431
486
  offsetStr = off;
432
487
  timeEnd += offsetChars;
433
488
  }
@@ -445,7 +500,7 @@ function parse(input, opts) {
445
500
  }
446
501
  // Date-only — but only if followed by something OTHER than digit/colon
447
502
  var after = _peek(10);
448
- if (!after || !/[0-9:.]/.test(after)) {
503
+ if (!after || !(_isDigit(after) || after === ":" || after === ".")) {
449
504
  var ds = input.substr(pos, 10);
450
505
  _advance(10);
451
506
  return { kind: "local-date", value: ds };
@@ -453,11 +508,11 @@ function parse(input, opts) {
453
508
  }
454
509
  // Time-only: HH:MM:SS (8 chars)
455
510
  var timeOnlyChars = TIME_CHARS;
456
- if (pos + timeOnlyChars <= len && /^\d{2}:\d{2}:\d{2}/.test(input.substr(pos, timeOnlyChars))) {
511
+ if (pos + timeOnlyChars <= len && _looksLikeTime(input.substr(pos, timeOnlyChars))) {
457
512
  var teEnd = pos + timeOnlyChars;
458
513
  if (input.charAt(teEnd) === ".") {
459
514
  teEnd += 1;
460
- while (teEnd < len && /\d/.test(input.charAt(teEnd))) teEnd += 1;
515
+ while (teEnd < len && _isDigit(input.charAt(teEnd))) teEnd += 1;
461
516
  }
462
517
  var ts = input.substring(pos, teEnd);
463
518
  _advance(teEnd - pos);
@@ -496,12 +551,12 @@ function parse(input, opts) {
496
551
  while (!_eof()) {
497
552
  var ch = _peek();
498
553
  if (ch === "_") { _advance(); continue; }
499
- if (radix === RADIX_HEX && /[0-9a-fA-F]/.test(ch)) { _advance(); continue; }
500
- if (radix === RADIX_OCTAL && /[0-7]/.test(ch)) { _advance(); continue; }
501
- if (radix === RADIX_BIN && /[01]/.test(ch)) { _advance(); continue; }
554
+ if (radix === RADIX_HEX && _isHexDigit(ch)) { _advance(); continue; }
555
+ if (radix === RADIX_OCTAL && _isOctalDigit(ch)) { _advance(); continue; }
556
+ if (radix === RADIX_BIN && _isBinaryDigit(ch)) { _advance(); continue; }
502
557
  break;
503
558
  }
504
- var digits = input.substring(digitsStart, pos).replace(/_/g, "");
559
+ var digits = _stripUnderscores(input.substring(digitsStart, pos));
505
560
  if (digits.length === 0) throw _err("expected digits after radix prefix", "toml/bad-number");
506
561
  var n = parseInt(digits, radix);
507
562
  if (!Number.isSafeInteger(n)) {
@@ -527,7 +582,7 @@ function parse(input, opts) {
527
582
  }
528
583
  break;
529
584
  }
530
- var raw = input.substring(startPos, pos).replace(/_/g, "");
585
+ var raw = _stripUnderscores(input.substring(startPos, pos));
531
586
  if (raw === "" || raw === "-" || raw === "+") {
532
587
  throw new SafeTomlError("invalid number", "toml/bad-number", startLine, startCol);
533
588
  }
@@ -560,17 +615,17 @@ function parse(input, opts) {
560
615
  if (c === "[") return _parseArray(depth + 1);
561
616
  if (c === "{") return _parseInlineTable(depth + 1);
562
617
 
563
- if (input.substr(pos, 4) === "true" && !/[A-Za-z0-9_]/.test(input.charAt(pos + 4) || "")) {
618
+ if (input.substr(pos, 4) === "true" && !_isBareKeyChar(input.charAt(pos + 4))) {
564
619
  _advance(4); return true;
565
620
  }
566
- if (input.substr(pos, 5) === "false" && !/[A-Za-z0-9_]/.test(input.charAt(pos + 5) || "")) {
621
+ if (input.substr(pos, 5) === "false" && !_isBareKeyChar(input.charAt(pos + 5))) {
567
622
  _advance(5); return false;
568
623
  }
569
624
 
570
625
  var dt = _tryParseDateTime();
571
626
  if (dt !== null) return dt.value;
572
627
 
573
- if (/[0-9+\-i n]/.test(c)) return _parseNumber(c);
628
+ if (_isNumberLead(c)) return _parseNumber(c);
574
629
 
575
630
  throw _err("unexpected character '" + c + "'", "toml/expected-value");
576
631
  }
@@ -53,6 +53,7 @@ var C = require("../constants");
53
53
  var pick = require("../pick");
54
54
  var numericBounds = require("../numeric-bounds");
55
55
  var safeBuffer = require("../safe-buffer");
56
+ var codepointClass = require("../codepoint-class");
56
57
  var { FrameworkError } = require("../framework-error");
57
58
 
58
59
  class SafeXmlError extends FrameworkError {
@@ -405,7 +406,12 @@ function parse(input, opts) {
405
406
  }
406
407
  }
407
408
  Object.assign(obj, grouped);
408
- var combinedText = textParts.join("").replace(/\s+/g, " ").trim();
409
+ // Collapse whitespace runs to a single space and trim, by walking rather
410
+ // than matching: the text came off the wire and a screen whose cost depends
411
+ // on the shape of the input is the thing this family does not do.
412
+ // splitOnWhitespace drops empty segments, so joining restores exactly the
413
+ // collapsed-and-trimmed form.
414
+ var combinedText = codepointClass.splitOnWhitespace(textParts.join("")).join(" ");
409
415
  if (combinedText.length > 0) obj["#text"] = combinedText;
410
416
  return _make(name, obj);
411
417
  }
@@ -59,6 +59,7 @@ var pick = require("../pick");
59
59
  var boundedMap = require("../bounded-map");
60
60
  var numericBounds = require("../numeric-bounds");
61
61
  var safeBuffer = require("../safe-buffer");
62
+ var codepointClass = require("../codepoint-class");
62
63
  var { FrameworkError } = require("../framework-error");
63
64
 
64
65
  class SafeYamlError extends FrameworkError {
@@ -93,14 +94,187 @@ var DEFAULTS = {
93
94
  // YAML 1.2 core-schema scalar resolution. Order matters: null first
94
95
  // (covers ~ and empty), then bool, then int (with base prefixes), then
95
96
  // float, then string fallback.
96
- var NULL_RE = /^(null|Null|NULL|~|)$/;
97
- var BOOL_RE = /^(true|True|TRUE|false|False|FALSE)$/;
98
- var INT_RE = /^[-+]?(0|[1-9][0-9]*)$/;
99
- var INT_OCT = /^0o[0-7]+$/;
100
- var INT_HEX = /^0x[0-9a-fA-F]+$/;
101
- var FLOAT_RE = /^[-+]?(\.[0-9]+|[0-9]+(\.[0-9]*)?)([eE][-+]?[0-9]+)?$/;
102
- var FLOAT_INF = /^[-+]?\.(inf|Inf|INF)$/;
103
- var FLOAT_NAN = /^\.(nan|NaN|NAN)$/;
97
+ // The resolvers are exact-set membership or a left-to-right walk. YAML's core
98
+ // schema spells each type out, so none of these needs to search: the anchored
99
+ // alternations were only ever a compact way of writing a fixed vocabulary and a
100
+ // digit shape.
101
+ var NULL_TOKENS = { "": 1, "null": 1, "Null": 1, "NULL": 1, "~": 1 };
102
+ var BOOL_TRUE = { "true": 1, "True": 1, "TRUE": 1 };
103
+ var BOOL_FALSE = { "false": 1, "False": 1, "FALSE": 1 };
104
+ var INF_TOKENS = { ".inf": 1, ".Inf": 1, ".INF": 1 };
105
+ var NAN_TOKENS = { ".nan": 1, ".NaN": 1, ".NAN": 1 };
106
+ var OCTAL_DIGITS = "01234567";
107
+
108
+ function _isNull(s) { return Object.prototype.hasOwnProperty.call(NULL_TOKENS, s); }
109
+ function _isBool(s) {
110
+ return Object.prototype.hasOwnProperty.call(BOOL_TRUE, s) ||
111
+ Object.prototype.hasOwnProperty.call(BOOL_FALSE, s);
112
+ }
113
+
114
+ function _signOffset(s) { var c = s.charAt(0); return (c === "-" || c === "+") ? 1 : 0; }
115
+
116
+ // `^[-+]?(0|[1-9][0-9]*)$` — no leading zeros, because `010` is a string in the
117
+ // core schema rather than an octal.
118
+ function _isDecimalInt(s) {
119
+ var i = _signOffset(s);
120
+ var rest = s.slice(i);
121
+ if (rest.length === 0) return false;
122
+ if (rest === "0") return true;
123
+ if (rest.charAt(0) === "0") return false;
124
+ return codepointClass.isRunOf(rest, codepointClass.ASCII_DIGITS);
125
+ }
126
+
127
+ function _isPrefixedInt(s, marker, alphabet) {
128
+ if (s.length < 3) return false;
129
+ if (s.charAt(0) !== "0" || s.charAt(1) !== marker) return false;
130
+ return codepointClass.isRunOf(s.slice(2), alphabet);
131
+ }
132
+ function _isOctalInt(s) { return _isPrefixedInt(s, "o", OCTAL_DIGITS); }
133
+ function _isHexInt(s) { return _isPrefixedInt(s, "x", codepointClass.ASCII_HEX); }
134
+
135
+ // `^[-+]?(\.[0-9]+|[0-9]+(\.[0-9]*)?)([eE][-+]?[0-9]+)?$` — either a leading dot
136
+ // with digits after it, or digits with an optional dot and optional digits, then
137
+ // an optional exponent that must carry at least one digit.
138
+ function _isFloat(s) {
139
+ var i = _signOffset(s);
140
+ var n = s.length;
141
+ // Both branches below return early when they find no digit, so reaching the
142
+ // exponent means at least one was consumed — no separate flag needed.
143
+ if (s.charAt(i) === ".") {
144
+ i += 1;
145
+ var fracStart = i;
146
+ while (i < n && codepointClass.ASCII_DIGITS.indexOf(s.charAt(i)) !== -1) i += 1;
147
+ if (i === fracStart) return false; // `.` alone is not a float
148
+ } else {
149
+ var intStart = i;
150
+ while (i < n && codepointClass.ASCII_DIGITS.indexOf(s.charAt(i)) !== -1) i += 1;
151
+ if (i === intStart) return false;
152
+ if (s.charAt(i) === ".") {
153
+ i += 1;
154
+ while (i < n && codepointClass.ASCII_DIGITS.indexOf(s.charAt(i)) !== -1) i += 1;
155
+ }
156
+ }
157
+ if (i === n) return true;
158
+ var e = s.charAt(i);
159
+ if (e !== "e" && e !== "E") return false;
160
+ i += 1;
161
+ var expSign = s.charAt(i);
162
+ if (expSign === "+" || expSign === "-") i += 1;
163
+ if (i >= n) return false;
164
+ return codepointClass.isRunOf(s.slice(i), codepointClass.ASCII_DIGITS);
165
+ }
166
+
167
+ function _isInfinity(s) {
168
+ return Object.prototype.hasOwnProperty.call(INF_TOKENS, s.slice(_signOffset(s)));
169
+ }
170
+ function _isNotANumber(s) {
171
+ return Object.prototype.hasOwnProperty.call(NAN_TOKENS, s);
172
+ }
173
+
174
+ function _isWhitespaceChar(ch) {
175
+ return ch.length === 1 && codepointClass.inRanges(ch.charCodeAt(0), codepointClass.WHITESPACE_RANGES);
176
+ }
177
+
178
+ // The four characters `.` does not match without the `s` flag.
179
+ function _isLineTerminator(ch) {
180
+ return ch === "\n" || ch === "\r" || ch === "\u2028" || ch === "\u2029";
181
+ }
182
+
183
+ // `^---(\s|$)` / `^\.\.\.(\s|$)` — a document marker is the three characters and
184
+ // then either the end of the line or a space, so `---foo` is not one.
185
+ function _isDocumentMarker(line, marker) {
186
+ if (line.slice(0, marker.length) !== marker) return false;
187
+ if (line.length === marker.length) return true;
188
+ return _isWhitespaceChar(line.charAt(marker.length));
189
+ }
190
+
191
+ // `^[|>][-+]?[0-9]?\s*$` and its variant allowing a trailing `# comment`.
192
+ // A block-scalar header: the indicator, an optional chomping sign, an optional
193
+ // single explicit-indent digit, then nothing that matters.
194
+ function _isBlockScalarHeader(content, allowComment) {
195
+ var i = 0;
196
+ var lead = content.charAt(0);
197
+ if (lead !== "|" && lead !== ">") return false;
198
+ i += 1;
199
+ var sign = content.charAt(i);
200
+ if (sign === "-" || sign === "+") i += 1;
201
+ var digit = content.charAt(i);
202
+ if (digit.length === 1 && codepointClass.ASCII_DIGITS.indexOf(digit) !== -1) i += 1;
203
+ while (i < content.length && _isWhitespaceChar(content.charAt(i))) i += 1;
204
+ if (i === content.length) return true;
205
+ if (!allowComment) return false;
206
+ if (content.charAt(i) !== "#") return false;
207
+ // `.*` stops at a line terminator, and `$` without `m` is end-of-string, so a
208
+ // comment may not contain one. ECMAScript counts four, not just LF: U+2028 and
209
+ // U+2029 are line terminators to the grammar even though nothing else here
210
+ // treats them as breaks, and accepting them would admit a header the pattern
211
+ // refused.
212
+ var tail = content.slice(i);
213
+ for (var t = 0; t < tail.length; t += 1) {
214
+ if (_isLineTerminator(tail.charAt(t))) return false;
215
+ }
216
+ return true;
217
+ }
218
+
219
+ // `(^|\s)([&*])([A-Za-z0-9_][A-Za-z0-9_-]*)` — an anchor or alias sigil that
220
+ // opens a token. Returns the index of the match INCLUDING the leading separator,
221
+ // matching what the pattern captured, so the reported line is unchanged.
222
+ var _NAME_HEAD = codepointClass.ASCII_ALNUM + "_";
223
+ var _NAME_TAIL = codepointClass.ASCII_ALNUM + "_-";
224
+
225
+ function _findAnchorOrAlias(text) {
226
+ for (var i = 0; i < text.length; i += 1) {
227
+ var ch = text.charAt(i);
228
+ if (ch !== "&" && ch !== "*") continue;
229
+ var atStart = i === 0;
230
+ if (!atStart && !_isWhitespaceChar(text.charAt(i - 1))) continue;
231
+ if (_NAME_HEAD.indexOf(text.charAt(i + 1)) === -1 || text.charAt(i + 1) === "") continue;
232
+ return { index: atStart ? i : i - 1, sigil: ch };
233
+ }
234
+ return null;
235
+ }
236
+
237
+ // `(^|[\s-])(!{1,2}[A-Za-z<])` — one or two `!` opening a tag.
238
+ function _findTag(text) {
239
+ for (var i = 0; i < text.length; i += 1) {
240
+ if (text.charAt(i) !== "!") continue;
241
+ var atStart = i === 0;
242
+ var before = text.charAt(i - 1);
243
+ if (!atStart && !(before === "-" || _isWhitespaceChar(before))) continue;
244
+ // The pattern is greedy, so it takes two `!` when both are present.
245
+ var after = text.charAt(i + 1) === "!" ? text.charAt(i + 2) : text.charAt(i + 1);
246
+ if (after.length !== 1) continue;
247
+ // Digits count. The pattern this replaces used `[A-Za-z<]` while the comment
248
+ // above it said "alphanumeric or `<`", and the code was the weaker of the
249
+ // two: `a: !123` is a local tag and parsed as the plain string "!123"
250
+ // instead of being refused, so the documented ban had a hole in it. This is
251
+ // not a regression from the rewrite — `main` behaves the same way — but the
252
+ // promise is that tags are refused, so the stricter reading wins.
253
+ if (after !== "<" && codepointClass.ASCII_ALNUM.indexOf(after) === -1) continue;
254
+ return { index: atStart ? i : i - 1 };
255
+ }
256
+ return null;
257
+ }
258
+
259
+ // `(^|\n)%(YAML|TAG)\b` — a directive at column zero.
260
+ var _DIRECTIVE_NAMES = ["YAML", "TAG"];
261
+
262
+ function _findDirective(text) {
263
+ for (var i = 0; i < text.length; i += 1) {
264
+ if (text.charAt(i) !== "%") continue;
265
+ var atStart = i === 0;
266
+ if (!atStart && text.charAt(i - 1) !== "\n") continue;
267
+ for (var d = 0; d < _DIRECTIVE_NAMES.length; d += 1) {
268
+ var name = _DIRECTIVE_NAMES[d];
269
+ if (text.slice(i + 1, i + 1 + name.length) !== name) continue;
270
+ // `\b` — the character after the name must not continue a word.
271
+ var next = text.charAt(i + 1 + name.length);
272
+ if (next.length === 1 && (_NAME_HEAD.indexOf(next) !== -1)) continue;
273
+ return { index: atStart ? i : i - 1, precededByNewline: !atStart };
274
+ }
275
+ }
276
+ return null;
277
+ }
104
278
 
105
279
  function _resolveScalar(s) {
106
280
  // Fall back to string for any token whose length exceeds the scalar cap
@@ -109,31 +283,31 @@ function _resolveScalar(s) {
109
283
  // type-inference regexes never see a pathologically long string.
110
284
  if (typeof s !== "string" || s.length > MAX_SCALAR_BYTES) return s;
111
285
  // Below: every regex test sees an `s` whose s.length <= MAX_SCALAR_BYTES.
112
- if (NULL_RE.test(s)) return null;
113
- if (BOOL_RE.test(s)) return s.toLowerCase() === "true";
286
+ if (_isNull(s)) return null;
287
+ if (_isBool(s)) return s.toLowerCase() === "true";
114
288
  // s.length <= MAX_SCALAR_BYTES asserted at function entry above.
115
- if (INT_RE.test(s)) {
289
+ if (_isDecimalInt(s)) {
116
290
  var n = parseInt(s, 10);
117
291
  if (Number.isSafeInteger(n)) return n;
118
292
  return s; // fallback to string for huge ints (don't lose precision silently)
119
293
  }
120
294
  // s.length <= MAX_SCALAR_BYTES asserted at function entry above.
121
- if (INT_OCT.test(s)) {
295
+ if (_isOctalInt(s)) {
122
296
  var oct = parseInt(s.substring(2), RADIX_OCTAL);
123
297
  if (Number.isSafeInteger(oct)) return oct;
124
298
  return s;
125
299
  }
126
300
  // s.length <= MAX_SCALAR_BYTES asserted at function entry above.
127
- if (INT_HEX.test(s)) {
301
+ if (_isHexInt(s)) {
128
302
  var hex = parseInt(s.substring(2), RADIX_HEX);
129
303
  if (Number.isSafeInteger(hex)) return hex;
130
304
  return s;
131
305
  }
132
306
  // s.length <= MAX_SCALAR_BYTES asserted at function entry above.
133
- if (FLOAT_INF.test(s)) return s.charAt(0) === "-" ? -Infinity : Infinity;
134
- if (FLOAT_NAN.test(s)) return NaN;
307
+ if (_isInfinity(s)) return s.charAt(0) === "-" ? -Infinity : Infinity;
308
+ if (_isNotANumber(s)) return NaN;
135
309
  // s.length <= MAX_SCALAR_BYTES asserted at function entry above.
136
- if (FLOAT_RE.test(s)) {
310
+ if (_isFloat(s)) {
137
311
  var f = parseFloat(s);
138
312
  if (!isNaN(f)) return f;
139
313
  }
@@ -179,7 +353,9 @@ function parse(input, opts) {
179
353
  _preValidate(input);
180
354
 
181
355
  // Normalize line endings: CRLF / CR → LF for consistent line-based work.
182
- input = input.replace(/\r\n/g, "\n").replace(/\r/g, "\n");
356
+ // Normalise CRLF and lone CR to LF. splitLinesAny treats all three as one
357
+ // break, so rejoining with "\n" is the same substitution in one pass.
358
+ input = codepointClass.splitLinesAny(input).join("\n");
183
359
 
184
360
  // Split into raw lines preserving line numbers.
185
361
  var rawLines = input.split("\n");
@@ -212,11 +388,11 @@ function parse(input, opts) {
212
388
  // Reject any later `---` or `...` (multi-document streams not supported).
213
389
  var idx = 0;
214
390
  while (idx < lines.length && (lines[idx].isBlank || lines[idx].isComment)) idx += 1;
215
- if (idx < lines.length && /^---(\s|$)/.test(lines[idx].content)) idx += 1;
391
+ if (idx < lines.length && _isDocumentMarker(lines[idx].content, "---")) idx += 1;
216
392
  // Subsequent doc markers anywhere = reject.
217
393
  for (var j = idx; j < lines.length; j++) {
218
394
  var c = lines[j].content;
219
- if (/^---(\s|$)/.test(c) || /^\.\.\.(\s|$)/.test(c)) {
395
+ if (_isDocumentMarker(c, "---") || _isDocumentMarker(c, "...")) {
220
396
  throw new SafeYamlError(
221
397
  "multi-document YAML streams are not supported",
222
398
  "yaml/multi-document", lines[j].lineNumber, 1
@@ -277,7 +453,7 @@ function parse(input, opts) {
277
453
  }
278
454
 
279
455
  // Block scalar header: `|` or `>` (with optional chomp/indent indicator)
280
- if (/^[|>][-+]?[0-9]?\s*(#.*)?$/.test(content)) {
456
+ if (_isBlockScalarHeader(content, true)) {
281
457
  return _parseBlockScalar(k, indent, content);
282
458
  }
283
459
 
@@ -398,7 +574,8 @@ function parse(input, opts) {
398
574
  // 1. if there's content after the colon, that's a flow value or
399
575
  // a plain scalar continuation on the same line.
400
576
  // 2. otherwise the value lives on subsequent more-indented lines.
401
- var afterColon = ln.content.substring(keyRange.valueStart).replace(/^[ \t]+/, "");
577
+ var afterColon = codepointClass.trimChars(
578
+ ln.content.substring(keyRange.valueStart), " \t", { trailing: false });
402
579
  // Strip end-of-line comment
403
580
  afterColon = _stripEolComment(afterColon);
404
581
  var value;
@@ -406,7 +583,7 @@ function parse(input, opts) {
406
583
  // Inline value on this line.
407
584
  if (afterColon.charAt(0) === "|" || afterColon.charAt(0) === ">") {
408
585
  // Block scalar header inline with key
409
- if (!/^[|>][-+]?[0-9]?\s*$/.test(afterColon)) {
586
+ if (!_isBlockScalarHeader(afterColon, false)) {
410
587
  throw new SafeYamlError("malformed block scalar header",
411
588
  "yaml/bad-block-scalar", ln.lineNumber, ln.indent + 1);
412
589
  }
@@ -506,7 +683,8 @@ function parse(input, opts) {
506
683
  // branch below) using _parseFlowValue's end position, then return the value
507
684
  // via the direct parser so the shape is identical to the pre-existing path.
508
685
  var fend = _parseFlowValue(t, 0, lineNumber, col, 0).nextPos;
509
- var afterFlow = t.slice(fend).replace(/^\s+/, "");
686
+ var afterFlow = codepointClass.trimRanges(
687
+ t.slice(fend), codepointClass.WHITESPACE_RANGES, { trailing: false });
510
688
  if (afterFlow.length > 0 && afterFlow.charAt(0) !== "#") {
511
689
  throw new SafeYamlError("unexpected content after flow collection",
512
690
  "yaml/trailing-content", lineNumber, col);
@@ -517,7 +695,8 @@ function parse(input, opts) {
517
695
  if (t.charAt(0) === '"') {
518
696
  var dq = _decodeDoubleQuoted(t, lineNumber, col);
519
697
  var afterDq = _trailingAfterQuoted(t, '"');
520
- if (afterDq.length > 0 && afterDq.replace(/^\s+/, "") !== "") {
698
+ if (afterDq.length > 0 &&
699
+ codepointClass.trimRanges(afterDq, codepointClass.WHITESPACE_RANGES, { trailing: false }) !== "") {
521
700
  throw new SafeYamlError("unexpected content after quoted string",
522
701
  "yaml/trailing-content", lineNumber, col);
523
702
  }
@@ -759,7 +938,7 @@ function parse(input, opts) {
759
938
  }
760
939
  case "U": {
761
940
  var hex8 = raw.substring(i + 2, i + 10);
762
- if (!/^[0-9a-fA-F]{8}$/.test(hex8)) {
941
+ if (hex8.length !== 8 || !codepointClass.isRunOf(hex8, codepointClass.ASCII_HEX)) {
763
942
  throw new SafeYamlError("bad \\U escape", "yaml/bad-escape", lineNumber, col + i);
764
943
  }
765
944
  var code = parseInt(hex8, RADIX_HEX);
@@ -904,7 +1083,7 @@ function parse(input, opts) {
904
1083
 
905
1084
  if (chomp === "-") {
906
1085
  // strip — remove trailing newline(s)
907
- body = body.replace(/\n+$/, "");
1086
+ body = codepointClass.trimRanges(body, [0x0A], { leading: false });
908
1087
  } else if (chomp === "+") {
909
1088
  // keep — restore trailing blanks we popped
910
1089
  body += "\n".repeat(trailingBlanks);
@@ -1026,14 +1205,12 @@ function _preValidate(input) {
1026
1205
  // (or start of value position). A simple heuristic: any unescaped `&`
1027
1206
  // that's followed by an identifier char and is preceded by space or
1028
1207
  // line start is an anchor. Same for `*`.
1029
- var anchorOrAliasRe = /(^|\s)([&*])([A-Za-z0-9_][A-Za-z0-9_-]*)/;
1030
- var m = safe.match(anchorOrAliasRe);
1208
+ var m = _findAnchorOrAlias(safe);
1031
1209
  if (m) {
1032
- var posIdx = safe.indexOf(m[0]);
1033
- var lineCount = safe.substring(0, posIdx).split("\n").length;
1210
+ var lineCount = safe.substring(0, m.index).split("\n").length;
1034
1211
  throw new SafeYamlError(
1035
- m[2] === "&" ? "anchors are not supported" : "aliases are not supported",
1036
- m[2] === "&" ? "yaml/anchors-banned" : "yaml/aliases-banned",
1212
+ m.sigil === "&" ? "anchors are not supported" : "aliases are not supported",
1213
+ m.sigil === "&" ? "yaml/anchors-banned" : "yaml/aliases-banned",
1037
1214
  lineCount, 1
1038
1215
  );
1039
1216
  }
@@ -1041,21 +1218,17 @@ function _preValidate(input) {
1041
1218
  // Tags: `!` at start of value or after `: ` / `- `. False-positive risk:
1042
1219
  // "key: !something" vs "key: ![bracket". We match `!` followed by
1043
1220
  // alphanumeric or `<`.
1044
- var tagRe = /(^|[\s-])(!{1,2}[A-Za-z<])/;
1045
- var mt = safe.match(tagRe);
1221
+ var mt = _findTag(safe);
1046
1222
  if (mt) {
1047
- var tagIdx = safe.indexOf(mt[0]);
1048
- var tagLine = safe.substring(0, tagIdx).split("\n").length;
1223
+ var tagLine = safe.substring(0, mt.index).split("\n").length;
1049
1224
  throw new SafeYamlError("tags are not supported",
1050
1225
  "yaml/tags-banned", tagLine, 1);
1051
1226
  }
1052
1227
 
1053
1228
  // Directives: `%YAML` or `%TAG` at column 0
1054
- var dirRe = /(^|\n)%(YAML|TAG)\b/;
1055
- var md = safe.match(dirRe);
1229
+ var md = _findDirective(safe);
1056
1230
  if (md) {
1057
- var dirIdx = safe.indexOf(md[0]);
1058
- var dirLine = safe.substring(0, dirIdx).split("\n").length + (md[1] === "\n" ? 1 : 0);
1231
+ var dirLine = safe.substring(0, md.index).split("\n").length + (md.precededByNewline ? 1 : 0);
1059
1232
  throw new SafeYamlError("directives are not supported",
1060
1233
  "yaml/directives-banned", dirLine, 1);
1061
1234
  }
package/lib/safe-json.js CHANGED
@@ -837,6 +837,12 @@ function _patternMatcher(pattern) {
837
837
  // branch twice. That is the overlap the alternation rule exists to
838
838
  // catch, so `/^(?=(a|A)+$)a+$/i` would pass a source-only screen and
839
839
  // then run, under those flags, against a value from the wire.
840
+ // Compiled precisely so assertSafe can screen the COMPILED form, per the
841
+ // comment above: a source-only screen misreads `(a|A)+` under `i`. The
842
+ // pattern here is the operator's schema `pattern` keyword — the input
843
+ // being validated rather than the implementation of a screen — and it is
844
+ // refused before it ever runs against a value.
845
+ // eslint-disable-next-line blamejs/no-regex-in-content-safety
840
846
  var native = new RegExp(source, flags);
841
847
  // assertSafe builds `new ErrorClass(code, message)`; SafeJsonError takes
842
848
  // them the other way round, so the screen reports through its own error.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blamejs/core",
3
- "version": "0.18.36",
3
+ "version": "0.18.38",
4
4
  "description": "The Node framework that owns its stack.",
5
5
  "license": "Apache-2.0",
6
6
  "author": "blamejs contributors",
package/sbom.cdx.json CHANGED
@@ -2,10 +2,10 @@
2
2
  "$schema": "http://cyclonedx.org/schema/bom-1.5.schema.json",
3
3
  "bomFormat": "CycloneDX",
4
4
  "specVersion": "1.5",
5
- "serialNumber": "urn:uuid:f384e0aa-ad27-4c4f-a0b0-eceb0cac068e",
5
+ "serialNumber": "urn:uuid:3e525c66-4a72-4004-90a2-8985bbb5daa4",
6
6
  "version": 1,
7
7
  "metadata": {
8
- "timestamp": "2026-08-18T23:20:05.652Z",
8
+ "timestamp": "2026-08-19T06:46:25.351Z",
9
9
  "lifecycles": [
10
10
  {
11
11
  "phase": "build"
@@ -19,14 +19,14 @@
19
19
  }
20
20
  ],
21
21
  "component": {
22
- "bom-ref": "@blamejs/core@0.18.36",
22
+ "bom-ref": "@blamejs/core@0.18.38",
23
23
  "type": "application",
24
24
  "name": "blamejs",
25
- "version": "0.18.36",
25
+ "version": "0.18.38",
26
26
  "scope": "required",
27
27
  "author": "blamejs contributors",
28
28
  "description": "The Node framework that owns its stack.",
29
- "purl": "pkg:npm/%40blamejs/core@0.18.36",
29
+ "purl": "pkg:npm/%40blamejs/core@0.18.38",
30
30
  "properties": [],
31
31
  "externalReferences": [
32
32
  {
@@ -54,7 +54,7 @@
54
54
  "components": [],
55
55
  "dependencies": [
56
56
  {
57
- "ref": "@blamejs/core@0.18.36",
57
+ "ref": "@blamejs/core@0.18.38",
58
58
  "dependsOn": []
59
59
  }
60
60
  ]