@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.
- package/CHANGELOG.md +2 -0
- package/lib/asset-manifest.json +1 -1
- package/lib/vendor/MANIFEST.json +58 -46
- package/lib/vendor/blamejs/.github/workflows/ci.yml +134 -1
- package/lib/vendor/blamejs/.gitignore +5 -1
- package/lib/vendor/blamejs/CHANGELOG.md +4 -0
- package/lib/vendor/blamejs/README.md +1 -1
- package/lib/vendor/blamejs/SECURITY.md +3 -1
- package/lib/vendor/blamejs/api-snapshot.json +10 -2
- package/lib/vendor/blamejs/lib/bundler.js +2 -7
- package/lib/vendor/blamejs/lib/config-drift.js +17 -3
- package/lib/vendor/blamejs/lib/crypto-field.js +30 -0
- package/lib/vendor/blamejs/lib/db-declare-row-policy.js +20 -1
- package/lib/vendor/blamejs/lib/db-schema.js +29 -0
- package/lib/vendor/blamejs/lib/db.js +7 -0
- package/lib/vendor/blamejs/lib/guard-csv.js +13 -4
- package/lib/vendor/blamejs/lib/local-db-thin.js +23 -1
- package/lib/vendor/blamejs/lib/mail-bimi.js +16 -3
- package/lib/vendor/blamejs/lib/mail-scan.js +2 -5
- package/lib/vendor/blamejs/lib/mail.js +16 -9
- package/lib/vendor/blamejs/lib/mcp.js +28 -6
- package/lib/vendor/blamejs/lib/middleware/bot-disclose.js +7 -5
- package/lib/vendor/blamejs/lib/middleware/speculation-rules.js +6 -4
- package/lib/vendor/blamejs/lib/numeric-bounds.js +32 -0
- package/lib/vendor/blamejs/lib/object-store/azure-blob.js +12 -1
- package/lib/vendor/blamejs/lib/object-store/gcs.js +12 -1
- package/lib/vendor/blamejs/lib/object-store/http-put.js +11 -1
- package/lib/vendor/blamejs/lib/object-store/index.js +4 -0
- package/lib/vendor/blamejs/lib/object-store/local.js +11 -1
- package/lib/vendor/blamejs/lib/object-store/sigv4.js +86 -5
- package/lib/vendor/blamejs/lib/parsers/safe-env.js +6 -3
- package/lib/vendor/blamejs/lib/parsers/safe-yaml.js +6 -6
- package/lib/vendor/blamejs/lib/safe-buffer.js +69 -1
- package/lib/vendor/blamejs/lib/safe-decompress.js +3 -12
- package/lib/vendor/blamejs/lib/seeders.js +33 -39
- package/lib/vendor/blamejs/lib/storage.js +71 -7
- package/lib/vendor/blamejs/lib/vault/rotate.js +4 -13
- package/lib/vendor/blamejs/package.json +1 -1
- package/lib/vendor/blamejs/release-notes/v0.15.10.json +53 -0
- package/lib/vendor/blamejs/release-notes/v0.15.11.json +52 -0
- package/lib/vendor/blamejs/test/integration/object-store-worm-lock.test.js +90 -16
- package/lib/vendor/blamejs/test/layer-0-primitives/codebase-patterns.test.js +150 -39
- package/lib/vendor/blamejs/test/layer-0-primitives/config-drift.test.js +19 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/crypto-field-aad-downgrade.test.js +96 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/db-schema-transaction.test.js +110 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/declare-row-policy.test.js +43 -1
- package/lib/vendor/blamejs/test/layer-0-primitives/local-db-thin.test.js +28 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/mcp.test.js +25 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/numeric-bounds.test.js +29 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/object-store-versioned-delete.test.js +97 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/safe-buffer-linear-scans.test.js +94 -0
- package/lib/vendor/blamejs/test/layer-5-integration/bundler-output.test.js +52 -0
- 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
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
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 {
|
|
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 {
|
|
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
|
-
|
|
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
|
-
|
|
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)
|
|
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]
|
|
193
|
+
value = safeBuffer.stripTrailingHspace(commentMatch[1]);
|
|
191
194
|
} else {
|
|
192
|
-
value =
|
|
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 =
|
|
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 =
|
|
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(
|
|
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)
|
|
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)
|
|
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)
|
|
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
|
-
|
|
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.
|
|
175
|
-
|
|
176
|
-
|
|
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
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
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
|
|