@blamejs/blamejs-shop 0.4.49 → 0.4.50

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 (53) hide show
  1. package/CHANGELOG.md +2 -0
  2. package/lib/asset-manifest.json +1 -1
  3. package/lib/vendor/MANIFEST.json +58 -46
  4. package/lib/vendor/blamejs/.github/workflows/ci.yml +134 -1
  5. package/lib/vendor/blamejs/.gitignore +5 -1
  6. package/lib/vendor/blamejs/CHANGELOG.md +4 -0
  7. package/lib/vendor/blamejs/README.md +1 -1
  8. package/lib/vendor/blamejs/SECURITY.md +3 -1
  9. package/lib/vendor/blamejs/api-snapshot.json +10 -2
  10. package/lib/vendor/blamejs/lib/bundler.js +2 -7
  11. package/lib/vendor/blamejs/lib/config-drift.js +17 -3
  12. package/lib/vendor/blamejs/lib/crypto-field.js +30 -0
  13. package/lib/vendor/blamejs/lib/db-declare-row-policy.js +20 -1
  14. package/lib/vendor/blamejs/lib/db-schema.js +29 -0
  15. package/lib/vendor/blamejs/lib/db.js +7 -0
  16. package/lib/vendor/blamejs/lib/guard-csv.js +13 -4
  17. package/lib/vendor/blamejs/lib/local-db-thin.js +23 -1
  18. package/lib/vendor/blamejs/lib/mail-bimi.js +16 -3
  19. package/lib/vendor/blamejs/lib/mail-scan.js +2 -5
  20. package/lib/vendor/blamejs/lib/mail.js +16 -9
  21. package/lib/vendor/blamejs/lib/mcp.js +28 -6
  22. package/lib/vendor/blamejs/lib/middleware/bot-disclose.js +7 -5
  23. package/lib/vendor/blamejs/lib/middleware/speculation-rules.js +6 -4
  24. package/lib/vendor/blamejs/lib/numeric-bounds.js +32 -0
  25. package/lib/vendor/blamejs/lib/object-store/azure-blob.js +12 -1
  26. package/lib/vendor/blamejs/lib/object-store/gcs.js +12 -1
  27. package/lib/vendor/blamejs/lib/object-store/http-put.js +11 -1
  28. package/lib/vendor/blamejs/lib/object-store/index.js +4 -0
  29. package/lib/vendor/blamejs/lib/object-store/local.js +11 -1
  30. package/lib/vendor/blamejs/lib/object-store/sigv4.js +86 -5
  31. package/lib/vendor/blamejs/lib/parsers/safe-env.js +6 -3
  32. package/lib/vendor/blamejs/lib/parsers/safe-yaml.js +6 -6
  33. package/lib/vendor/blamejs/lib/safe-buffer.js +69 -1
  34. package/lib/vendor/blamejs/lib/safe-decompress.js +3 -12
  35. package/lib/vendor/blamejs/lib/seeders.js +33 -39
  36. package/lib/vendor/blamejs/lib/storage.js +71 -7
  37. package/lib/vendor/blamejs/lib/vault/rotate.js +4 -13
  38. package/lib/vendor/blamejs/package.json +1 -1
  39. package/lib/vendor/blamejs/release-notes/v0.15.10.json +53 -0
  40. package/lib/vendor/blamejs/release-notes/v0.15.11.json +52 -0
  41. package/lib/vendor/blamejs/test/integration/object-store-worm-lock.test.js +90 -16
  42. package/lib/vendor/blamejs/test/layer-0-primitives/codebase-patterns.test.js +150 -39
  43. package/lib/vendor/blamejs/test/layer-0-primitives/config-drift.test.js +19 -0
  44. package/lib/vendor/blamejs/test/layer-0-primitives/crypto-field-aad-downgrade.test.js +96 -0
  45. package/lib/vendor/blamejs/test/layer-0-primitives/db-schema-transaction.test.js +110 -0
  46. package/lib/vendor/blamejs/test/layer-0-primitives/declare-row-policy.test.js +43 -1
  47. package/lib/vendor/blamejs/test/layer-0-primitives/local-db-thin.test.js +28 -0
  48. package/lib/vendor/blamejs/test/layer-0-primitives/mcp.test.js +25 -0
  49. package/lib/vendor/blamejs/test/layer-0-primitives/numeric-bounds.test.js +29 -0
  50. package/lib/vendor/blamejs/test/layer-0-primitives/object-store-versioned-delete.test.js +97 -0
  51. package/lib/vendor/blamejs/test/layer-0-primitives/safe-buffer-linear-scans.test.js +94 -0
  52. package/lib/vendor/blamejs/test/layer-5-integration/bundler-output.test.js +52 -0
  53. package/package.json +1 -1
@@ -65,6 +65,7 @@
65
65
  */
66
66
 
67
67
  var validateOpts = require("../validate-opts");
68
+ var safeBuffer = require("../safe-buffer");
68
69
 
69
70
  // Per W3C draft + Chromium implementation. `immediate` triggers the
70
71
  // speculation as soon as the rules are seen; `conservative` waits
@@ -287,10 +288,11 @@ function create(opts) {
287
288
  if (headClose !== -1) {
288
289
  body = body.slice(0, headClose) + tag + body.slice(headClose);
289
290
  } else {
290
- var bodyOpen = body.match(/<body[^>]*>/i);
291
- if (bodyOpen) {
292
- var idx = bodyOpen.index + bodyOpen[0].length;
293
- body = body.slice(0, idx) + tag + body.slice(idx);
291
+ // Linear tag-find — NOT body.match(/<body[^>]*>/i) (O(n^2) in V8
292
+ // on a body with many `<body` starts and no closing `>`).
293
+ var bodyIdx = safeBuffer.indexAfterOpenTag(body, "body");
294
+ if (bodyIdx !== -1) {
295
+ body = body.slice(0, bodyIdx) + tag + body.slice(bodyIdx);
294
296
  } else {
295
297
  // No <head> or <body> — prepend so the rules at least
296
298
  // reach the parser. This matches the bot-disclose fallback.
@@ -82,6 +82,37 @@ function requireNonNegativeFiniteIntIfPresent(value, label, errorClass, code) {
82
82
  return value;
83
83
  }
84
84
 
85
+ // requirePositiveFiniteInt — REQUIRED-shape gate (the non-optional sibling
86
+ // of requirePositiveFiniteIntIfPresent): throws when the value is absent OR
87
+ // not a positive finite integer, and — when range bounds are supplied —
88
+ // when it falls outside them. Replaces the per-file
89
+ // `if (!nb.isPositiveFiniteInt(opts.X) || opts.X < MIN || opts.X > MAX) throw`
90
+ // cascade that bundler / mail-scan / safe-decompress rolled by hand for
91
+ // REQUIRED numeric opts (the IfPresent helper can't be used there — it
92
+ // skips when undefined, so a missing required opt would pass).
93
+ //
94
+ // nb.requirePositiveFiniteInt(opts.maxOutputBytes,
95
+ // "safeDecompress: maxOutputBytes", SafeDecompressError, "safe-decompress/bad-arg");
96
+ // nb.requirePositiveFiniteInt(opts.hashLen, "bundler.create: opts.hashLen",
97
+ // BundlerError, "bundler/bad-hash-len", { min: MIN_HASH_LEN, max: MAX_HASH_LEN });
98
+ function _rangeSuffix(range) {
99
+ if (!range) return "";
100
+ if (range.min != null && range.max != null) return " in [" + range.min + ", " + range.max + "]";
101
+ if (range.max != null) return " <= " + range.max;
102
+ if (range.min != null) return " >= " + range.min;
103
+ return "";
104
+ }
105
+ function requirePositiveFiniteInt(value, label, errorClass, code, range) {
106
+ var inRange = !range ||
107
+ ((range.min == null || value >= range.min) && (range.max == null || value <= range.max));
108
+ if (!isPositiveFiniteInt(value) || !inRange) {
109
+ throw new errorClass(code,
110
+ (label || "value") + " must be a positive finite integer" +
111
+ _rangeSuffix(range) + "; got " + shape(value));
112
+ }
113
+ return value;
114
+ }
115
+
85
116
  // requireAllPositiveFiniteIntIfPresent — batch validator. Walk each
86
117
  // opt-name in the list; for any that is present in opts, require it to
87
118
  // be a positive finite integer (otherwise throw via errorClass with the
@@ -105,6 +136,7 @@ module.exports = {
105
136
  shape: shape,
106
137
  isPositiveFiniteInt: isPositiveFiniteInt,
107
138
  isNonNegativeFiniteInt: isNonNegativeFiniteInt,
139
+ requirePositiveFiniteInt: requirePositiveFiniteInt,
108
140
  requirePositiveFiniteIntIfPresent: requirePositiveFiniteIntIfPresent,
109
141
  requireNonNegativeFiniteIntIfPresent: requireNonNegativeFiniteIntIfPresent,
110
142
  requireAllPositiveFiniteIntIfPresent: requireAllPositiveFiniteIntIfPresent,
@@ -356,7 +356,18 @@ function create(config) {
356
356
  });
357
357
  }
358
358
 
359
- function deleteKey(key) {
359
+ function deleteKey(key, opts) {
360
+ opts = opts || {};
361
+ // Versioned erasure (opts.versionId) is the S3 Object-Lock workflow and is
362
+ // sigv4-only today. Refuse loudly rather than silently delete the current
363
+ // blob — a silent drop on an erasure path would let a caller believe a
364
+ // specific version was shredded when it was not.
365
+ if (opts.versionId) {
366
+ throw _err("VERSIONID_UNSUPPORTED",
367
+ "deleteKey: versioned delete (opts.versionId) is S3/sigv4-only; the Azure " +
368
+ "Blob backend has no version surface here. Use a sigv4 backend for " +
369
+ "Object-Lock version erasure.", true);
370
+ }
360
371
  var url = _blobUrl(key);
361
372
  var headers = _signed("DELETE", url, {});
362
373
  return _httpRequest("DELETE", url, headers, null, reqOpts).then(
@@ -281,7 +281,18 @@ function create(config) {
281
281
  };
282
282
  }
283
283
 
284
- async function deleteKey(key) {
284
+ async function deleteKey(key, opts) {
285
+ opts = opts || {};
286
+ // Versioned erasure (opts.versionId) is the S3 Object-Lock workflow and is
287
+ // sigv4-only today. Refuse loudly rather than silently delete the live
288
+ // object — a silent drop on an erasure path would let a caller believe a
289
+ // specific version was shredded when it was not.
290
+ if (opts.versionId) {
291
+ throw _err("VERSIONID_UNSUPPORTED",
292
+ "deleteKey: versioned delete (opts.versionId) is S3/sigv4-only; the GCS " +
293
+ "backend has no version surface here. Use a sigv4 backend for Object-Lock " +
294
+ "version erasure.", true);
295
+ }
285
296
  var token = await _ensureToken();
286
297
  var url = _objectUrl(key);
287
298
  try {
@@ -110,7 +110,17 @@ function create(config) {
110
110
  });
111
111
  }
112
112
 
113
- function deleteKey(key) {
113
+ function deleteKey(key, opts) {
114
+ opts = opts || {};
115
+ // Versioned erasure (opts.versionId) is the S3 Object-Lock workflow and is
116
+ // sigv4-only. A bare PUT target has no version surface, so refuse loudly
117
+ // rather than issue a plain DELETE and report a specific version erased —
118
+ // a silent drop on an erasure path is the footgun.
119
+ if (opts.versionId) {
120
+ throw _err("VERSIONID_UNSUPPORTED",
121
+ "deleteKey: versioned delete (opts.versionId) is S3/sigv4-only; the http-put " +
122
+ "backend has no version surface. Use a sigv4 backend for Object-Lock version erasure.", true);
123
+ }
114
124
  var url = _keyToUrl(baseUrl, key);
115
125
  return _request("DELETE", url, null, headers, reqOpts).then(
116
126
  function () { return true; },
@@ -122,6 +122,10 @@ function buildBackend(config) {
122
122
  head: wrap("head"),
123
123
  delete: wrap("delete"),
124
124
  list: wrap("list"),
125
+ // listVersions is S3/sigv4-only (the ?versions subresource backs the
126
+ // WORM erasure workflow). Backends without it expose null so callers can
127
+ // feature-detect rather than hit a "wrap of undefined" at boot.
128
+ listVersions: typeof raw.listVersions === "function" ? wrap("listVersions") : null,
125
129
  // presigned*Url are sync URL-builders (no network call), so they
126
130
  // bypass retry + circuit-breaker — propagate any throw directly.
127
131
  presignedUploadUrl: typeof raw.presignedUploadUrl === "function"
@@ -101,7 +101,17 @@ function create(config) {
101
101
  });
102
102
  }
103
103
 
104
- function deleteKey(key) {
104
+ function deleteKey(key, opts) {
105
+ opts = opts || {};
106
+ // A filesystem has no object versions; a versioned-delete request can only
107
+ // be a caller mistake. Refuse loudly rather than unlink the single on-disk
108
+ // file and report a version was erased.
109
+ if (opts.versionId) {
110
+ throw _err("VERSIONID_UNSUPPORTED",
111
+ "deleteKey: versioned delete (opts.versionId) is not supported on the " +
112
+ "filesystem backend — a local file has no version history. Use a sigv4 " +
113
+ "(S3 Object-Lock) backend for version erasure.", true);
114
+ }
105
115
  cluster.requireLeader();
106
116
  var full = _resolveSafe(rootDir, key);
107
117
  if (!nodeFs.existsSync(full)) return Promise.resolve(false);
@@ -499,7 +499,15 @@ function create(config) {
499
499
  var headers = _makeSigned("PUT", url, payloadHash, extra);
500
500
  return _request("PUT", url, headers, buf, reqOpts).then(function (res) {
501
501
  _verifySseResponse(sseRequested, res.headers);
502
- return { size: buf.length, etag: res.headers.etag };
502
+ return {
503
+ size: buf.length,
504
+ etag: res.headers.etag,
505
+ // On a versioning-enabled (Object-Lock) bucket S3/MinIO returns the
506
+ // version this PUT created. Surface it so callers can target the
507
+ // exact version for a later versioned delete / erasure — without it
508
+ // the only way to find the version is a separate listVersions() call.
509
+ versionId: res.headers && res.headers["x-amz-version-id"] || null,
510
+ };
503
511
  });
504
512
  }
505
513
 
@@ -601,7 +609,12 @@ function create(config) {
601
609
  // response may or may not echo the header depending on vendor;
602
610
  // re-verifying here would double-fault on otherwise-fine setups.
603
611
  var result = completeDoc.CompleteMultipartUploadResult || {};
604
- return { size: totalSize, etag: result.ETag || completeRes.headers.etag, multipart: true };
612
+ return {
613
+ size: totalSize,
614
+ etag: result.ETag || completeRes.headers.etag,
615
+ multipart: true,
616
+ versionId: completeRes.headers && completeRes.headers["x-amz-version-id"] || null,
617
+ };
605
618
  } catch (e) {
606
619
  // Abort cleans up server-side storage for the partial upload.
607
620
  // Failures here are silently swallowed — the caller's original
@@ -639,6 +652,11 @@ function create(config) {
639
652
  function getResponse(key, opts) {
640
653
  opts = opts || {};
641
654
  var url = _keyToUrl(key);
655
+ // Reading a specific version (opts.versionId) is the read half of the
656
+ // WORM erasure workflow — verify a protected version is present before /
657
+ // gone after a versioned delete. Set it before signing so the query
658
+ // param is in the SigV4 canonical request.
659
+ if (opts.versionId) url.searchParams.set("versionId", opts.versionId);
642
660
  var headers = _makeSigned("GET", url, sha256Hex(Buffer.alloc(0)));
643
661
  if (opts.range) {
644
662
  headers["Range"] = "bytes=" + opts.range.start + "-" + opts.range.end;
@@ -674,8 +692,10 @@ function create(config) {
674
692
  });
675
693
  }
676
694
 
677
- function head(key) {
695
+ function head(key, opts) {
696
+ opts = opts || {};
678
697
  var url = _keyToUrl(key);
698
+ if (opts.versionId) url.searchParams.set("versionId", opts.versionId);
679
699
  var headers = _makeSigned("HEAD", url, sha256Hex(Buffer.alloc(0)));
680
700
  return _request("HEAD", url, headers, null, reqOpts).then(function (res) {
681
701
  return {
@@ -696,9 +716,25 @@ function create(config) {
696
716
  });
697
717
  }
698
718
 
699
- function deleteKey(key) {
719
+ // deleteKey(key, opts?) — opts.versionId targets a specific version;
720
+ // opts.bypassGovernanceRetention signs x-amz-bypass-governance-retention so
721
+ // a GOVERNANCE-mode retention can be lifted by a caller with the permission
722
+ // (COMPLIANCE mode is immutable to everyone and stays refused).
723
+ //
724
+ // WORM-awareness: an UNVERSIONED delete on a versioning-enabled bucket only
725
+ // writes a delete-marker — the data version survives and the call still
726
+ // resolves true. To actually erase a version (e.g. crypto-shred / GDPR Art.
727
+ // 17 on an Object-Lock bucket) pass the versionId from put()/listVersions().
728
+ // A delete refused by an active retention surfaces as a thrown error (S3 403
729
+ // / MinIO 400), never a silent success, so the caller learns the version is
730
+ // still protected.
731
+ function deleteKey(key, opts) {
732
+ opts = opts || {};
700
733
  var url = _keyToUrl(key);
701
- var headers = _makeSigned("DELETE", url, sha256Hex(Buffer.alloc(0)));
734
+ if (opts.versionId) url.searchParams.set("versionId", opts.versionId);
735
+ var extra = {};
736
+ if (opts.bypassGovernanceRetention) extra["x-amz-bypass-governance-retention"] = "true";
737
+ var headers = _makeSigned("DELETE", url, sha256Hex(Buffer.alloc(0)), extra);
702
738
  return _request("DELETE", url, headers, null, reqOpts).then(
703
739
  function () { return true; },
704
740
  function (e) { if (e.statusCode === 404) return false; throw e; }
@@ -953,6 +989,50 @@ function create(config) {
953
989
  });
954
990
  }
955
991
 
992
+ // listVersions(prefix, opts?) — enumerate every object VERSION and
993
+ // delete-marker under prefix (S3 ListObjectVersions / the ?versions
994
+ // subresource). Plain list() only sees current versions; to erase prior
995
+ // versions on a versioning / Object-Lock bucket you first need their
996
+ // versionIds, which only this call surfaces. Each item carries
997
+ // { key, versionId, isLatest, deleteMarker, size, lastModified, etag };
998
+ // deleteMarker:true rows are tombstones (no data, size null). Pagination
999
+ // walks (keyMarker, versionIdMarker) the way list() walks continuationToken.
1000
+ function listVersions(prefix, opts) {
1001
+ opts = opts || {};
1002
+ var params = { versions: "" };
1003
+ if (prefix) params["prefix"] = prefix;
1004
+ if (opts.maxResults) params["max-keys"] = String(opts.maxResults);
1005
+ if (opts.keyMarker) params["key-marker"] = opts.keyMarker;
1006
+ if (opts.versionIdMarker) params["version-id-marker"] = opts.versionIdMarker;
1007
+
1008
+ var url = _bucketUrl(params);
1009
+ var headers = _makeSigned("GET", url, sha256Hex(Buffer.alloc(0)));
1010
+ return _request("GET", url, headers, null, reqOpts).then(function (res) {
1011
+ var doc = safeXml.parse(res.body, LIST_PARSE_OPTS);
1012
+ var result = doc.ListVersionsResult || {};
1013
+ function _mapEntry(e, isDeleteMarker) {
1014
+ return {
1015
+ key: e.Key,
1016
+ versionId: e.VersionId != null ? String(e.VersionId) : null,
1017
+ isLatest: e.IsLatest === "true",
1018
+ deleteMarker: isDeleteMarker,
1019
+ size: isDeleteMarker ? null : (e.Size != null ? parseInt(e.Size, 10) : null),
1020
+ lastModified: e.LastModified ? Date.parse(e.LastModified) : null,
1021
+ etag: isDeleteMarker ? null : (e.ETag || null),
1022
+ };
1023
+ }
1024
+ var versions = _arrayify(result.Version).map(function (v) { return _mapEntry(v, false); });
1025
+ var markers = _arrayify(result.DeleteMarker).map(function (m) { return _mapEntry(m, true); });
1026
+ var items = versions.concat(markers).filter(function (it) { return it.key; });
1027
+ return {
1028
+ items: items,
1029
+ truncated: result.IsTruncated === "true",
1030
+ keyMarker: result.NextKeyMarker || null,
1031
+ versionIdMarker: result.NextVersionIdMarker || null,
1032
+ };
1033
+ });
1034
+ }
1035
+
956
1036
  return {
957
1037
  protocol: "sigv4",
958
1038
  endpoint: endpoint,
@@ -966,6 +1046,7 @@ function create(config) {
966
1046
  head: head,
967
1047
  delete: deleteKey,
968
1048
  list: list,
1049
+ listVersions: listVersions,
969
1050
  presignedUploadUrl: presignedUploadUrl,
970
1051
  presignedDownloadUrl: presignedDownloadUrl,
971
1052
  presignedUploadPolicy: presignedUploadPolicy,
@@ -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,
@@ -171,18 +171,9 @@ function safeDecompress(input, opts) {
171
171
  JSON.stringify(opts.algorithm));
172
172
  }
173
173
 
174
- // maxOutputBytes — required, positive finite integer. Inline gate
175
- // is intentional: it's a REQUIRED opt (not optional), so the
176
- // `requirePositiveFiniteIntIfPresent` helper doesn't apply (it skips
177
- // when undefined). The numericBounds.requirePositiveFiniteInt helper
178
- // would fit, but the existing call surface across the framework
179
- // uses the inline shape for required-opt validation.
180
- if (!numericBounds.isPositiveFiniteInt(opts.maxOutputBytes)) { // allow:inline-numeric-bounds-cascade — required (non-optional) opt; requirePositiveFiniteIntIfPresent skips when undefined
181
- throw new SafeDecompressError(
182
- "safe-decompress/bad-arg",
183
- "safeDecompress: maxOutputBytes must be a positive finite integer; got " +
184
- numericBounds.shape(opts.maxOutputBytes));
185
- }
174
+ // maxOutputBytes — required, positive finite integer.
175
+ numericBounds.requirePositiveFiniteInt(opts.maxOutputBytes,
176
+ "safeDecompress: maxOutputBytes", SafeDecompressError, "safe-decompress/bad-arg");
186
177
 
187
178
  // Input shape
188
179
  var buf;
@@ -546,46 +546,40 @@ function create(opts) {
546
546
 
547
547
  try {
548
548
 
549
- await (async function () {
550
- // Per-seed transaction: SQLite txns are sync, but the
551
- // seed's run() may be async so we begin/commit around
552
- // an awaited body. Failures roll back this seed only.
553
- _runSql(db, "BEGIN");
554
- try {
555
- await mod.run(db, ctx);
556
- var nowIso = new Date(clock()).toISOString();
557
- var writeBuilt;
558
- if (alreadyApplied && mod.rerunnable) {
559
- writeBuilt = sql.update(_seedersTable(), _sqlOpts(db))
560
- .set({ appliedAt: nowIso, description: mod.description || "",
561
- rerunnable: mod.rerunnable ? 1 : 0 })
562
- .where("env", env).where("name", name).toSql();
563
- } else if (alreadyApplied && force) {
564
- writeBuilt = sql.update(_seedersTable(), _sqlOpts(db))
565
- .set({ appliedAt: nowIso, description: mod.description || "" })
566
- .where("env", env).where("name", name).toSql();
567
- } else {
568
- writeBuilt = sql.insert(_seedersTable(), _sqlOpts(db))
569
- .values({ env: env, name: name, description: mod.description || "",
570
- appliedAt: nowIso, rerunnable: mod.rerunnable ? 1 : 0 })
571
- .toSql();
572
- }
573
- var writeStmt = db.prepare(writeBuilt.sql);
574
- writeStmt.run.apply(writeStmt, writeBuilt.params);
575
- _runSql(db, "COMMIT");
576
- } catch (e) {
577
- try { _runSql(db, "ROLLBACK"); }
578
- catch (rollbackErr) {
579
- log.debug("rollback-failed", {
580
- op: "seed-apply",
581
- env: env,
582
- name: name,
583
- error: rollbackErr && rollbackErr.message,
584
- });
585
- }
586
- throw e;
549
+ // Per-seed transaction: SQLite txns are sync, but the seed's
550
+ // run() may be async runInTransactionAsync wraps BEGIN/COMMIT
551
+ // around the awaited body and rolls back this seed only on failure.
552
+ await dbSchema.runInTransactionAsync(db, async function () {
553
+ await mod.run(db, ctx);
554
+ var nowIso = new Date(clock()).toISOString();
555
+ var writeBuilt;
556
+ if (alreadyApplied && mod.rerunnable) {
557
+ writeBuilt = sql.update(_seedersTable(), _sqlOpts(db))
558
+ .set({ appliedAt: nowIso, description: mod.description || "",
559
+ rerunnable: mod.rerunnable ? 1 : 0 })
560
+ .where("env", env).where("name", name).toSql();
561
+ } else if (alreadyApplied && force) {
562
+ writeBuilt = sql.update(_seedersTable(), _sqlOpts(db))
563
+ .set({ appliedAt: nowIso, description: mod.description || "" })
564
+ .where("env", env).where("name", name).toSql();
565
+ } else {
566
+ writeBuilt = sql.insert(_seedersTable(), _sqlOpts(db))
567
+ .values({ env: env, name: name, description: mod.description || "",
568
+ appliedAt: nowIso, rerunnable: mod.rerunnable ? 1 : 0 })
569
+ .toSql();
587
570
  }
588
- })();
571
+ var writeStmt = db.prepare(writeBuilt.sql);
572
+ writeStmt.run.apply(writeStmt, writeBuilt.params);
573
+ }, {
574
+ onRollbackFail: function (rollbackErr) {
575
+ log.debug("rollback-failed", {
576
+ op: "seed-apply",
577
+ env: env,
578
+ name: name,
579
+ error: rollbackErr && rollbackErr.message,
580
+ });
581
+ },
582
+ });
589
583
  applied.push(name);
590
584
  appliedSet.add(name);
591
585