@blamejs/core 0.15.10 → 0.15.12

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/lib/notify.js CHANGED
@@ -408,11 +408,6 @@ function create(opts) {
408
408
  channels[n] = registry;
409
409
  }
410
410
 
411
- function _emitObs(name, labels) {
412
- try { observability().event(name, 1, labels || {}); }
413
- catch (_e) { /* drop-silent — observability sink must not crash send() */ }
414
- }
415
-
416
411
  var _emitAudit = validateOpts.makeAuditEmitter(audit);
417
412
 
418
413
  function _actor(callerOpts) {
@@ -465,7 +460,7 @@ function create(opts) {
465
460
  // mechanism — no double-retry inside the breaker or transport).
466
461
  async function _oneAttempt(attemptIdx) {
467
462
  attemptCount = attemptIdx;
468
- _emitObs("notify.send.attempt", { channel: channel, attempt: attemptIdx });
463
+ observability().safeEvent("notify.send.attempt", 1, { channel: channel, attempt: attemptIdx });
469
464
  var sendPromise = entry.transport.send(message, input.sendOpts || null);
470
465
  // withTimeout from b.safeAsync — never re-implement timer races.
471
466
  var timed = (perCallTimeoutMs > 0)
@@ -478,7 +473,7 @@ function create(opts) {
478
473
  // classifies it correctly (operators can still opt OUT by setting
479
474
  // err.permanent in their transport).
480
475
  if (e && e.code === "async/timeout") {
481
- _emitObs("notify.send.timeout", { channel: channel });
476
+ observability().safeEvent("notify.send.timeout", 1, { channel: channel });
482
477
  var te = _err("TIMEOUT",
483
478
  "notify.send: '" + channel + "' transport timed out after " + perCallTimeoutMs + "ms");
484
479
  // Mark transient via a NETWORK-style code so b.retry.isRetryable
@@ -497,7 +492,7 @@ function create(opts) {
497
492
  return await entry.breaker.wrap(function () { return _oneAttempt(attemptIdx); });
498
493
  } catch (e) {
499
494
  if (e && e.code === "CIRCUIT_OPEN") {
500
- _emitObs("notify.send.breaker.open", { channel: channel });
495
+ observability().safeEvent("notify.send.breaker.open", 1, { channel: channel });
501
496
  }
502
497
  throw e;
503
498
  }
@@ -524,7 +519,7 @@ function create(opts) {
524
519
  }, perCallRetry);
525
520
 
526
521
  var durationMs = clock() - startedAt;
527
- _emitObs("notify.send.success", { channel: channel, durationMs: durationMs });
522
+ observability().safeEvent("notify.send.success", 1, { channel: channel, durationMs: durationMs });
528
523
  if (auditSuccess) {
529
524
  _emitAudit("notify.send.success", {
530
525
  actor: _actor(input),
@@ -543,7 +538,7 @@ function create(opts) {
543
538
  durationMs: durationMs,
544
539
  });
545
540
  } catch (e) {
546
- _emitObs("notify.send.failure", {
541
+ observability().safeEvent("notify.send.failure", 1, {
547
542
  channel: channel,
548
543
  reason: (e && e.code) || "unknown",
549
544
  });
@@ -594,7 +589,7 @@ function create(opts) {
594
589
  if (results[i] && results[i].isNotifyError) failed++;
595
590
  else ok++;
596
591
  }
597
- _emitObs("notify.batch", { size: inputs.length, ok: ok, failed: failed });
592
+ observability().safeEvent("notify.batch", 1, { size: inputs.length, ok: ok, failed: failed });
598
593
  return results;
599
594
  }
600
595
 
@@ -626,7 +621,7 @@ function create(opts) {
626
621
  } catch (_e) { /* operator may register their own handler */ }
627
622
  }
628
623
  var jobId = await q.enqueue(queueName, input);
629
- _emitObs("notify.queue.enqueued", { channel: input.channel, queueName: queueName });
624
+ observability().safeEvent("notify.queue.enqueued", 1, { channel: input.channel, queueName: queueName });
630
625
  return { jobId: jobId };
631
626
  }
632
627
 
@@ -147,7 +147,7 @@ function parse(input, opts) {
147
147
  if (eqIdx < 0) {
148
148
  throw new SafeEnvError("missing '=' separator", "env/bad-line", lineNumber);
149
149
  }
150
- var key = trimmed.substring(0, eqIdx).replace(safeBuffer.TRAILING_HSPACE_RE, "");
150
+ var key = safeBuffer.stripTrailingHspace(trimmed.substring(0, eqIdx));
151
151
  var rest = trimmed.substring(eqIdx + 1);
152
152
 
153
153
  if (key.length === 0) {
@@ -186,10 +186,13 @@ function parse(input, opts) {
186
186
  // The comment marker MUST be preceded by whitespace to count
187
187
  // (so a value like `KEY=color#red` keeps the literal `#`).
188
188
  var commentMatch = rest.match(/^([^\s#]*(?:[ \t]+[^#\s]+)*)\s+#.*$/);
189
+ // stripTrailingHspace is a linear char-scan; .replace(/[ \t]+$/) is O(n^2)
190
+ // in V8 and the env parser only caps TOTAL bytes, not per-line, so a
191
+ // single huge-whitespace value line would otherwise hang the parser.
189
192
  if (commentMatch) {
190
- value = commentMatch[1].replace(safeBuffer.TRAILING_HSPACE_RE, "");
193
+ value = safeBuffer.stripTrailingHspace(commentMatch[1]);
191
194
  } else {
192
- value = rest.replace(safeBuffer.TRAILING_HSPACE_RE, "");
195
+ value = safeBuffer.stripTrailingHspace(rest);
193
196
  }
194
197
  // Reject `$VAR` style references — explicit error so operators
195
198
  // see the policy rather than silently getting unexpanded text.
@@ -185,7 +185,7 @@ function parse(input, opts) {
185
185
  var raw = rawLines[i];
186
186
  // Strip trailing whitespace from the line for analysis (block-scalar
187
187
  // content paths preserve their own internal spacing separately).
188
- var trimmed = raw.replace(safeBuffer.TRAILING_HSPACE_RE, "");
188
+ var trimmed = safeBuffer.stripTrailingHspace(raw);
189
189
  var indent = 0;
190
190
  while (indent < raw.length && raw.charAt(indent) === " ") indent += 1;
191
191
  if (indent < raw.length && raw.charAt(indent) === "\t") {
@@ -332,7 +332,7 @@ function parse(input, opts) {
332
332
  if (raw.charAt(0) === "'") {
333
333
  return _decodeSingleQuoted(raw, lineNumber, col);
334
334
  }
335
- var trimmed = raw.replace(safeBuffer.TRAILING_HSPACE_RE, "");
335
+ var trimmed = safeBuffer.stripTrailingHspace(raw);
336
336
  if (POISONED_KEYS.has(trimmed)) {
337
337
  throw new SafeYamlError("forbidden key '" + trimmed + "'",
338
338
  "yaml/poisoned-key", lineNumber, col);
@@ -506,7 +506,7 @@ function parse(input, opts) {
506
506
  return sq;
507
507
  }
508
508
  // Plain scalar — strip trailing space, resolve type.
509
- return _resolveScalar(t.replace(safeBuffer.TRAILING_HSPACE_RE, ""));
509
+ return _resolveScalar(safeBuffer.stripTrailingHspace(t));
510
510
  }
511
511
 
512
512
  // For quoted-string termination check on a single-line value.
@@ -622,7 +622,7 @@ function parse(input, opts) {
622
622
  if (c === "," || c === "}" || c === "]") break;
623
623
  p += 1;
624
624
  }
625
- var raw = text.substring(start, p).replace(safeBuffer.TRAILING_HSPACE_RE, "");
625
+ var raw = safeBuffer.stripTrailingHspace(text.substring(start, p));
626
626
  return { value: _resolveScalar(raw), nextPos: p };
627
627
  }
628
628
 
@@ -645,7 +645,7 @@ function parse(input, opts) {
645
645
  if (c === ":" || c === "," || c === "}" || c === "]") break;
646
646
  p += 1;
647
647
  }
648
- return { key: text.substring(start, p).replace(safeBuffer.TRAILING_HSPACE_RE, ""), nextPos: p };
648
+ return { key: safeBuffer.stripTrailingHspace(text.substring(start, p)), nextPos: p };
649
649
  }
650
650
 
651
651
  function _findMatchingBracket(text, start, open, close, lineNumber, col) {
@@ -772,7 +772,7 @@ function parse(input, opts) {
772
772
  function _stripEolComment(text) {
773
773
  // Strip ` #...` comments (must be preceded by whitespace) at end of line.
774
774
  var match = text.match(/^(.*?)(\s+#.*)?$/);
775
- return (match && match[1] != null ? match[1] : text).replace(safeBuffer.TRAILING_HSPACE_RE, "");
775
+ return safeBuffer.stripTrailingHspace(match && match[1] != null ? match[1] : text);
776
776
  }
777
777
 
778
778
  // ---- Block scalars (| literal, > folded) ----
@@ -494,7 +494,74 @@ var TRAILING_HSPACE_RE = /[ \t]+$/;
494
494
  */
495
495
  function stripTrailingHspace(s) {
496
496
  if (typeof s !== "string") return s;
497
- return s.replace(TRAILING_HSPACE_RE, "");
497
+ // Linear backward scan over trailing spaces/tabs — NOT s.replace(/[ \t]+$/).
498
+ // The `$`-after-greedy-`[ \t]+` regex is O(n^2) in V8 on adversarial input
499
+ // (a long run of spaces followed by a non-space: the engine retries the
500
+ // greedy match from every offset). normalizeText callers cap TOTAL bytes but
501
+ // not per-line, so a single ~500K-space value hangs (~85s). The char-scan is
502
+ // O(trailing-whitespace) and byte-identical to the regex on every input
503
+ // (JS `$` without /m matches only the absolute end, so a trailing \n is not
504
+ // stripped — the scan stops at it too).
505
+ var e = s.length;
506
+ while (e > 0) {
507
+ var c = s.charCodeAt(e - 1);
508
+ if (c === 0x20 || c === 0x09) { e -= 1; } else { break; }
509
+ }
510
+ return e === s.length ? s : s.slice(0, e);
511
+ }
512
+
513
+ /**
514
+ * @primitive b.safeBuffer.indexAfterOpenTag
515
+ * @signature b.safeBuffer.indexAfterOpenTag(html, tagName)
516
+ * @since 0.15.11
517
+ * @related b.safeBuffer.stripTrailingHspace
518
+ *
519
+ * Find the offset in `html` just past the first `<tagName ...>` opening
520
+ * tag (case-insensitive), or `-1` when the tag is absent or unterminated.
521
+ * The insertion point a response rewriter uses to splice content right
522
+ * after `<body>` / `<head>` without a regex.
523
+ *
524
+ * This replaces the `html.match(/<body[^>]*>/i)` shape, which is O(n^2)
525
+ * in V8: a body carrying many `<body` starts with no closing `>` (e.g.
526
+ * rendered user content) makes the engine retry the greedy `[^>]*` from
527
+ * every offset — a `<body`-repeated 200K-char body benchmarks in
528
+ * seconds. This is a single forward `indexOf` walk: linear in the input,
529
+ * and stricter than the regex — it requires a real tag boundary after
530
+ * the name (whitespace, `>`, or `/`), so `<bodyfoo>` is not mistaken for
531
+ * `<body>`. Non-string input returns `-1`.
532
+ *
533
+ * @example
534
+ * var b = require("blamejs");
535
+ * b.safeBuffer.indexAfterOpenTag("<html><body class=x>hi", "body");
536
+ * // → 19 (just past the '>' of <body class=x>)
537
+ *
538
+ * b.safeBuffer.indexAfterOpenTag("<p>no body here</p>", "body");
539
+ * // → -1
540
+ */
541
+ function indexAfterOpenTag(html, tagName) {
542
+ if (typeof html !== "string" || typeof tagName !== "string" || tagName.length === 0) return -1;
543
+ var needle = "<" + tagName.toLowerCase();
544
+ var nlen = needle.length;
545
+ // One O(n) lowercase pass keeps the case-insensitive search linear; the
546
+ // forward indexOf walk below never re-scans a region it has passed.
547
+ var lower = html.toLowerCase();
548
+ var from = 0;
549
+ for (;;) {
550
+ var lt = lower.indexOf(needle, from);
551
+ if (lt === -1) return -1;
552
+ var after = lt + nlen;
553
+ var boundary = after < html.length ? html.charCodeAt(after) : -1;
554
+ // The char after "<tag" must end the tag name — '>' (0x3e), '/' (0x2f),
555
+ // or ASCII whitespace — else this is a longer name like "<bodyfoo".
556
+ if (boundary === 0x3e || boundary === 0x2f ||
557
+ boundary === 0x20 || boundary === 0x09 ||
558
+ boundary === 0x0a || boundary === 0x0d || boundary === 0x0c) {
559
+ var gt = html.indexOf(">", after);
560
+ if (gt === -1) return -1; // unterminated opening tag — no insertion point
561
+ return gt + 1;
562
+ }
563
+ from = lt + 1;
564
+ }
498
565
  }
499
566
 
500
567
  /**
@@ -612,6 +679,7 @@ module.exports = {
612
679
  hasCrlf: hasCrlf,
613
680
  stripCrlf: stripCrlf,
614
681
  stripTrailingHspace: stripTrailingHspace,
682
+ indexAfterOpenTag: indexAfterOpenTag,
615
683
  HEX_RE: HEX_RE,
616
684
  BASE64URL_RE: BASE64URL_RE,
617
685
  BASE64_RE: BASE64_RE,
package/lib/seeders.js CHANGED
@@ -438,11 +438,6 @@ function create(opts) {
438
438
  var audit = opts.audit || null;
439
439
  var clock = opts.clock || function () { return Date.now(); };
440
440
 
441
- function _emitObs(name, labels) {
442
- try { observability().event(name, 1, labels || {}); }
443
- catch (_e) { /* drop-silent — observability sink must not crash seeders */ }
444
- }
445
-
446
441
  var _emitAudit = validateOpts.makeAuditEmitter(audit);
447
442
 
448
443
  function _actor(callerOpts) {
@@ -512,7 +507,7 @@ function create(opts) {
512
507
  }
513
508
 
514
509
  var startedAt = clock();
515
- _emitObs("seeders.run.start", { env: env, count: loaded.ordered.length });
510
+ observability().safeEvent("seeders.run.start", 1, { env: env, count: loaded.ordered.length });
516
511
 
517
512
  var holder = _acquireLock(db, lockStaleAfterMs, clock);
518
513
  try {
@@ -538,7 +533,7 @@ function create(opts) {
538
533
 
539
534
  if (!shouldRun) {
540
535
  skipped.push(name);
541
- _emitObs("seeders.skipped", { env: env, name: name, reason: "already-applied" });
536
+ observability().safeEvent("seeders.skipped", 1, { env: env, name: name, reason: "already-applied" });
542
537
  continue;
543
538
  }
544
539
 
@@ -585,7 +580,7 @@ function create(opts) {
585
580
 
586
581
  var auditAction = (alreadyApplied && force) ? "seeders.force_applied" : "seeders.applied";
587
582
  var auditEvt = { env: env, name: name };
588
- _emitObs(auditAction, auditEvt);
583
+ observability().safeEvent(auditAction, 1, auditEvt);
589
584
  if (auditApplied) {
590
585
  _emitAudit(auditAction, {
591
586
  actor: _actor(callerOpts),
@@ -598,7 +593,7 @@ function create(opts) {
598
593
  failed = name;
599
594
  var msg = (e && e.message) || String(e);
600
595
  var code = (e && e.code) || "RUN_FAILED";
601
- _emitObs("seeders.failed", { env: env, name: name });
596
+ observability().safeEvent("seeders.failed", 1, { env: env, name: name });
602
597
  if (auditFailures) {
603
598
  _emitAudit("seeders.failed", {
604
599
  actor: _actor(callerOpts),
@@ -622,7 +617,7 @@ function create(opts) {
622
617
  failed: failed,
623
618
  durationMs: clock() - startedAt,
624
619
  };
625
- _emitObs("seeders.run.completed", {
620
+ observability().safeEvent("seeders.run.completed", 1, {
626
621
  env: env,
627
622
  applied: applied.length,
628
623
  skipped: skipped.length,
@@ -204,7 +204,43 @@ function unquoteSfString(s) {
204
204
  if (t.length === 0) return "";
205
205
  if (t.charAt(0) !== "\"") return t;
206
206
  if (t.length < 2 || t.charAt(t.length - 1) !== "\"") return null;
207
- return t.slice(1, -1).replace(/\\"/g, "\"").replace(/\\\\/g, "\\");
207
+ return unescapeSfStringBody(t.slice(1, -1));
208
+ }
209
+
210
+ /**
211
+ * @primitive b.structuredFields.unescapeSfStringBody
212
+ * @signature b.structuredFields.unescapeSfStringBody(body)
213
+ * @since 0.15.12
214
+ * @related b.structuredFields.unquoteSfString
215
+ *
216
+ * Undo the RFC 8941 §3.3.3 quoted-string backslash-escapes from the BODY of
217
+ * an sf-string (the bytes BETWEEN the surrounding double quotes). Only `\\\\`
218
+ * and `\\"` are legal escapes; every other backslash is literal.
219
+ *
220
+ * This is a single left-to-right scan, NOT two chained `.replace()` passes.
221
+ * The two-pass form (`.replace(/\\\\/g,"\\").replace(/\\"/g,'"')`, in either
222
+ * order) is not equivalent to a single decode: whichever pass runs first can
223
+ * rewrite a backslash the other escape sequence legitimately owns, so a lone
224
+ * escaped backslash (`\\\\`) decodes to two backslashes instead of one. The
225
+ * single pass consumes each escape exactly once. Non-string input passes
226
+ * through unchanged.
227
+ *
228
+ * @example
229
+ * b.structuredFields.unescapeSfStringBody('a\\"b\\\\c');
230
+ * // → 'a"b\c'
231
+ */
232
+ function unescapeSfStringBody(body) {
233
+ if (typeof body !== "string") return body;
234
+ var out = "";
235
+ for (var i = 0; i < body.length; i++) {
236
+ var c = body.charAt(i);
237
+ if (c === "\\" && i + 1 < body.length) {
238
+ var n = body.charAt(i + 1);
239
+ if (n === "\\" || n === "\"") { out += n; i += 1; continue; }
240
+ }
241
+ out += c;
242
+ }
243
+ return out;
208
244
  }
209
245
 
210
246
  /**
@@ -674,6 +710,7 @@ module.exports = {
674
710
  refuseControlBytes: refuseControlBytes,
675
711
  containsControlBytes: containsControlBytes,
676
712
  unquoteSfString: unquoteSfString,
713
+ unescapeSfStringBody: unescapeSfStringBody,
677
714
  parse: parse,
678
715
  serialize: serialize,
679
716
  Token: SfToken,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blamejs/core",
3
- "version": "0.15.10",
3
+ "version": "0.15.12",
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:67b3105d-f49c-44bd-bf4e-afbb994d35e2",
5
+ "serialNumber": "urn:uuid:caf34b84-1358-4d28-a517-e360819dc35a",
6
6
  "version": 1,
7
7
  "metadata": {
8
- "timestamp": "2026-06-14T03:47:28.687Z",
8
+ "timestamp": "2026-06-14T16:52:10.733Z",
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.15.10",
22
+ "bom-ref": "@blamejs/core@0.15.12",
23
23
  "type": "application",
24
24
  "name": "blamejs",
25
- "version": "0.15.10",
25
+ "version": "0.15.12",
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.15.10",
29
+ "purl": "pkg:npm/%40blamejs/core@0.15.12",
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.15.10",
57
+ "ref": "@blamejs/core@0.15.12",
58
58
  "dependsOn": []
59
59
  }
60
60
  ]