@blamejs/blamejs-shop 0.0.83 → 0.0.85
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 +4 -0
- package/lib/email-campaigns.js +1 -1
- package/lib/vendor/MANIFEST.json +2 -2
- package/lib/vendor/blamejs/CHANGELOG.md +4 -0
- package/lib/vendor/blamejs/README.md +1 -1
- package/lib/vendor/blamejs/SECURITY.md +1 -0
- package/lib/vendor/blamejs/api-snapshot.json +151 -2
- package/lib/vendor/blamejs/fuzz/safe-archive.fuzz.js +37 -0
- package/lib/vendor/blamejs/index.js +15 -1
- package/lib/vendor/blamejs/lib/archive-adapters.js +629 -0
- package/lib/vendor/blamejs/lib/archive-read.js +781 -0
- package/lib/vendor/blamejs/lib/archive-tar-read.js +418 -0
- package/lib/vendor/blamejs/lib/archive-tar.js +557 -0
- package/lib/vendor/blamejs/lib/archive.js +17 -0
- package/lib/vendor/blamejs/lib/audit.js +22 -7
- package/lib/vendor/blamejs/lib/backup/index.js +429 -0
- package/lib/vendor/blamejs/lib/guard-archive.js +180 -0
- package/lib/vendor/blamejs/lib/guard-filename.js +205 -0
- package/lib/vendor/blamejs/lib/safe-archive.js +295 -0
- package/lib/vendor/blamejs/package.json +1 -1
- package/lib/vendor/blamejs/release-notes/v0.12.7.json +86 -0
- package/lib/vendor/blamejs/release-notes/v0.12.8.json +81 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/archive-read.test.js +247 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/archive-tar.test.js +228 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/codebase-patterns.test.js +127 -0
- package/package.json +2 -2
|
@@ -65,6 +65,8 @@ var compliance = lazyRequire(function () { return require("../compliance"); });
|
|
|
65
65
|
// module graph (CLI tools, stand-alone backup runners). The db()
|
|
66
66
|
// callable resolves on first access.
|
|
67
67
|
var dbModuleLazy = lazyRequire(function () { return require("../db"); });
|
|
68
|
+
var archiveLazy = lazyRequire(function () { return require("../archive"); });
|
|
69
|
+
var archiveAdaptersLazy = lazyRequire(function () { return require("../archive-adapters"); });
|
|
68
70
|
var { defineClass } = require("../framework-error");
|
|
69
71
|
|
|
70
72
|
var BackupError = defineClass("BackupError");
|
|
@@ -992,6 +994,8 @@ function runInWorker(opts) {
|
|
|
992
994
|
module.exports = {
|
|
993
995
|
create: create,
|
|
994
996
|
diskStorage: diskStorage,
|
|
997
|
+
bundleAdapterStorage: bundleAdapterStorage,
|
|
998
|
+
migrate: migrate,
|
|
995
999
|
recommendedFiles: recommendedFiles,
|
|
996
1000
|
runInWorker: runInWorker,
|
|
997
1001
|
verifyManifestSignature: verifyManifestSignature,
|
|
@@ -999,3 +1003,428 @@ module.exports = {
|
|
|
999
1003
|
BackupError: BackupError,
|
|
1000
1004
|
BUNDLE_ID_RE: BUNDLE_ID_RE,
|
|
1001
1005
|
};
|
|
1006
|
+
|
|
1007
|
+
// ---- v0.12.7: bundleAdapterStorage ---------------------------------------
|
|
1008
|
+
|
|
1009
|
+
/**
|
|
1010
|
+
* @primitive b.backup.bundleAdapterStorage
|
|
1011
|
+
* @signature b.backup.bundleAdapterStorage(opts)
|
|
1012
|
+
* @since 0.12.7
|
|
1013
|
+
* @status stable
|
|
1014
|
+
* @compliance hipaa, pci-dss, gdpr, soc2
|
|
1015
|
+
* @related b.backup.diskStorage, b.backup.create
|
|
1016
|
+
*
|
|
1017
|
+
* Adapter-driven storage backend. Wraps the bundle directory's file
|
|
1018
|
+
* tree into per-file key-value pairs routed through an operator-
|
|
1019
|
+
* supplied byte-store adapter so backup bundles can land anywhere
|
|
1020
|
+
* that exposes the contract (local fs is the v0.12.7 default; tar
|
|
1021
|
+
* folding lands v0.12.8, tar.gz v0.12.9, S3/MinIO/Azure/GCS
|
|
1022
|
+
* objectStore v0.12.11).
|
|
1023
|
+
*
|
|
1024
|
+
* The adapter contract (small surface; an `fs` implementation is the
|
|
1025
|
+
* default + ships in `lib/backup/_adapter-fs.js`):
|
|
1026
|
+
*
|
|
1027
|
+
* adapter.writeFile(key, bytes): Promise<void>
|
|
1028
|
+
* adapter.readFile(key): Promise<Buffer>
|
|
1029
|
+
* adapter.listKeys(prefix): Promise<string[]>
|
|
1030
|
+
* adapter.deleteKey(key): Promise<void>
|
|
1031
|
+
* adapter.hasKey(key): Promise<boolean>
|
|
1032
|
+
*
|
|
1033
|
+
* Keys are `<bundleId>/<relative/path/within/bundle>`. Operators
|
|
1034
|
+
* pointing at an objectStore implementation pass an adapter that
|
|
1035
|
+
* routes keys to S3 paths; pointing at an HTTP-backed store, ditto.
|
|
1036
|
+
*
|
|
1037
|
+
* @opts
|
|
1038
|
+
* adapter: { writeFile, readFile, listKeys, deleteKey, hasKey },
|
|
1039
|
+
*
|
|
1040
|
+
* @example
|
|
1041
|
+
* var storage = b.backup.bundleAdapterStorage({
|
|
1042
|
+
* adapter: b.backup.bundleAdapterStorage.fsAdapter({ root: "/var/backups" }),
|
|
1043
|
+
* });
|
|
1044
|
+
* storage.name; // → "adapter"
|
|
1045
|
+
* typeof storage.writeBundle; // → "function"
|
|
1046
|
+
*/
|
|
1047
|
+
function bundleAdapterStorage(opts) {
|
|
1048
|
+
opts = opts || {};
|
|
1049
|
+
if (!opts.adapter || typeof opts.adapter !== "object") {
|
|
1050
|
+
throw new BackupError("backup/bad-adapter",
|
|
1051
|
+
"bundleAdapterStorage: opts.adapter is required (an object with writeFile/readFile/listKeys/deleteKey/hasKey)");
|
|
1052
|
+
}
|
|
1053
|
+
var adapter = opts.adapter;
|
|
1054
|
+
var required = ["writeFile", "readFile", "listKeys", "deleteKey", "hasKey"];
|
|
1055
|
+
for (var i = 0; i < required.length; i += 1) {
|
|
1056
|
+
if (typeof adapter[required[i]] !== "function") {
|
|
1057
|
+
throw new BackupError("backup/bad-adapter",
|
|
1058
|
+
"bundleAdapterStorage: adapter missing method '" + required[i] + "'");
|
|
1059
|
+
}
|
|
1060
|
+
}
|
|
1061
|
+
// v0.12.8 — `format: "tar"` becomes the default for new bundles.
|
|
1062
|
+
// `format: "directory"` opts back into the v0.12.7 file-by-file
|
|
1063
|
+
// layout for operators with existing bundles. The format is
|
|
1064
|
+
// operator-supplied so a single backup engine can transition over
|
|
1065
|
+
// time + b.backup.migrate() handles the directory → tar conversion.
|
|
1066
|
+
var format = opts.format || "tar";
|
|
1067
|
+
if (format !== "tar" && format !== "directory") {
|
|
1068
|
+
throw new BackupError("backup/bad-format",
|
|
1069
|
+
"bundleAdapterStorage: format must be \"tar\" (default) or \"directory\" (legacy v0.12.7)");
|
|
1070
|
+
}
|
|
1071
|
+
// Codex P2 on v0.12.8 PR #159 — tar mode builds the whole archive
|
|
1072
|
+
// in memory before adapter.writeFile because the v0.12.8 adapter
|
|
1073
|
+
// contract is bytes-in (no writeStream method). The OOM-prevention
|
|
1074
|
+
// gate is maxBundleBytes: writeBundle pre-walks the source tree,
|
|
1075
|
+
// sums file sizes, and refuses upfront if the projected uncompressed
|
|
1076
|
+
// tar would exceed the cap. Default 8 GiB — accommodates typical
|
|
1077
|
+
// db + mail-spool + log bundles while refusing pathological inputs.
|
|
1078
|
+
// Defer-with-condition for true streaming: when the adapter
|
|
1079
|
+
// contract gains writeStream(key) (slated for v0.13+ alongside
|
|
1080
|
+
// multipart S3 upload primitives), this path switches to
|
|
1081
|
+
// tarBuilder.toStream() and writes chunks as they're produced.
|
|
1082
|
+
var maxBundleBytes = opts.maxBundleBytes !== undefined
|
|
1083
|
+
? opts.maxBundleBytes
|
|
1084
|
+
: 8 * 1024 * 1024 * 1024; // allow:raw-byte-literal — 8 GiB default cap; uses C.BYTES.bytes covers numeric-literal rule
|
|
1085
|
+
if (!numericBounds.isPositiveFiniteInt(maxBundleBytes)) { // allow:inline-numeric-bounds-cascade — required-with-default opt
|
|
1086
|
+
throw new BackupError("backup/bad-arg",
|
|
1087
|
+
"bundleAdapterStorage: maxBundleBytes must be a positive finite integer; got " +
|
|
1088
|
+
numericBounds.shape(opts.maxBundleBytes));
|
|
1089
|
+
}
|
|
1090
|
+
|
|
1091
|
+
function _ensureBundleId(bundleId) {
|
|
1092
|
+
if (!_isValidBundleId(bundleId)) {
|
|
1093
|
+
throw new BackupError("backup/bad-bundle-id",
|
|
1094
|
+
"bundleId must match the framework's timestamp+suffix format");
|
|
1095
|
+
}
|
|
1096
|
+
}
|
|
1097
|
+
|
|
1098
|
+
function _walkDirSync(rootDir, out, rel) {
|
|
1099
|
+
rel = rel || "";
|
|
1100
|
+
var entries = nodeFs.readdirSync(nodePath.join(rootDir, rel), { withFileTypes: true });
|
|
1101
|
+
for (var i = 0; i < entries.length; i += 1) {
|
|
1102
|
+
var name = entries[i].name;
|
|
1103
|
+
var relPath = rel ? (rel + "/" + name) : name;
|
|
1104
|
+
if (entries[i].isDirectory()) {
|
|
1105
|
+
_walkDirSync(rootDir, out, relPath);
|
|
1106
|
+
} else if (entries[i].isFile()) {
|
|
1107
|
+
out.push(relPath);
|
|
1108
|
+
}
|
|
1109
|
+
}
|
|
1110
|
+
return out;
|
|
1111
|
+
}
|
|
1112
|
+
|
|
1113
|
+
// Tar-format bundle storage stores the whole bundle as a single
|
|
1114
|
+
// key under `<bundleId>/bundle.tar`. The marker is named that way
|
|
1115
|
+
// so listBundles + hasBundle can locate either format by key
|
|
1116
|
+
// prefix walk.
|
|
1117
|
+
var TAR_KEY_SUFFIX = "/bundle.tar";
|
|
1118
|
+
|
|
1119
|
+
function _hasBundleKey(bundleId, format) {
|
|
1120
|
+
if (format === "tar") return adapter.hasKey(bundleId + TAR_KEY_SUFFIX);
|
|
1121
|
+
return adapter.hasKey(bundleId + "/manifest.json");
|
|
1122
|
+
}
|
|
1123
|
+
|
|
1124
|
+
return {
|
|
1125
|
+
name: "adapter",
|
|
1126
|
+
async writeBundle(bundleId, sourceDir) {
|
|
1127
|
+
_ensureBundleId(bundleId);
|
|
1128
|
+
if (!nodeFs.existsSync(sourceDir)) {
|
|
1129
|
+
throw new BackupError("backup/no-source",
|
|
1130
|
+
"writeBundle: sourceDir does not exist: " + sourceDir);
|
|
1131
|
+
}
|
|
1132
|
+
var alreadyHas = await _hasBundleKey(bundleId, format);
|
|
1133
|
+
if (alreadyHas) {
|
|
1134
|
+
throw new BackupError("backup/bundle-exists",
|
|
1135
|
+
"writeBundle: bundle '" + bundleId + "' already exists in storage");
|
|
1136
|
+
}
|
|
1137
|
+
if (format === "tar") {
|
|
1138
|
+
// Pack the source-directory tree into a single tar archive +
|
|
1139
|
+
// store under one key. Composes b.archive.tar.
|
|
1140
|
+
//
|
|
1141
|
+
// Codex P2 on v0.12.8 PR #159 — tar bytes are materialized in
|
|
1142
|
+
// memory because the v0.12.8 adapter contract is bytes-in
|
|
1143
|
+
// (writeFile takes a Buffer, no writeStream method). The
|
|
1144
|
+
// maxBundleBytes pre-walk computes the uncompressed payload
|
|
1145
|
+
// size (file bytes only — tar header overhead is bounded at
|
|
1146
|
+
// ~512 B per entry + 1024 B trailer) and refuses upfront so
|
|
1147
|
+
// pathological inputs throw `backup/bundle-too-large` instead
|
|
1148
|
+
// of OOM. The defer-with-condition for true streaming is
|
|
1149
|
+
// gated on the adapter contract gaining writeStream(key).
|
|
1150
|
+
var relPaths = _walkDirSync(sourceDir, []);
|
|
1151
|
+
var projectedBytes = 0;
|
|
1152
|
+
for (var pi = 0; pi < relPaths.length; pi += 1) {
|
|
1153
|
+
var stat = nodeFs.statSync(nodePath.join(sourceDir, relPaths[pi]));
|
|
1154
|
+
projectedBytes += stat.size;
|
|
1155
|
+
}
|
|
1156
|
+
if (projectedBytes > maxBundleBytes) {
|
|
1157
|
+
throw new BackupError("backup/bundle-too-large",
|
|
1158
|
+
"writeBundle: projected uncompressed bundle " + projectedBytes +
|
|
1159
|
+
" bytes exceeds maxBundleBytes=" + maxBundleBytes +
|
|
1160
|
+
" — split the source tree across multiple bundles or raise the cap");
|
|
1161
|
+
}
|
|
1162
|
+
var t = archiveLazy().tar();
|
|
1163
|
+
for (var i = 0; i < relPaths.length; i += 1) {
|
|
1164
|
+
var rel = relPaths[i];
|
|
1165
|
+
var bytes = nodeFs.readFileSync(nodePath.join(sourceDir, rel));
|
|
1166
|
+
t.addFile(rel, bytes);
|
|
1167
|
+
}
|
|
1168
|
+
var tarBytes = t.toBuffer();
|
|
1169
|
+
await adapter.writeFile(bundleId + TAR_KEY_SUFFIX, tarBytes);
|
|
1170
|
+
return;
|
|
1171
|
+
}
|
|
1172
|
+
// Directory format (v0.12.7 layout).
|
|
1173
|
+
var dirRelPaths = _walkDirSync(sourceDir, []);
|
|
1174
|
+
for (var j = 0; j < dirRelPaths.length; j += 1) {
|
|
1175
|
+
var dirRel = dirRelPaths[j];
|
|
1176
|
+
var dirBytes = nodeFs.readFileSync(nodePath.join(sourceDir, dirRel));
|
|
1177
|
+
await adapter.writeFile(bundleId + "/" + dirRel, dirBytes);
|
|
1178
|
+
}
|
|
1179
|
+
},
|
|
1180
|
+
async readBundle(bundleId, destDir) {
|
|
1181
|
+
_ensureBundleId(bundleId);
|
|
1182
|
+
if (nodeFs.existsSync(destDir)) {
|
|
1183
|
+
throw new BackupError("backup/dest-exists",
|
|
1184
|
+
"readBundle: destDir already exists: " + destDir);
|
|
1185
|
+
}
|
|
1186
|
+
// Detect which format this bundle is in — operators with mixed
|
|
1187
|
+
// pre-v0.12.8 + post-v0.12.8 bundles can read either back.
|
|
1188
|
+
var hasTar = await adapter.hasKey(bundleId + TAR_KEY_SUFFIX);
|
|
1189
|
+
var hasManifest = await adapter.hasKey(bundleId + "/manifest.json");
|
|
1190
|
+
if (!hasTar && !hasManifest) {
|
|
1191
|
+
throw new BackupError("backup/bundle-not-found",
|
|
1192
|
+
"readBundle: '" + bundleId + "' not in storage");
|
|
1193
|
+
}
|
|
1194
|
+
atomicFile.ensureDir(destDir);
|
|
1195
|
+
if (hasTar) {
|
|
1196
|
+
var tarBytes = await adapter.readFile(bundleId + TAR_KEY_SUFFIX);
|
|
1197
|
+
var reader = archiveLazy().read.tar(archiveAdaptersLazy().buffer(tarBytes));
|
|
1198
|
+
await reader.extract({ destination: destDir });
|
|
1199
|
+
return;
|
|
1200
|
+
}
|
|
1201
|
+
// Directory format readback (v0.12.7 layout).
|
|
1202
|
+
var keys = await adapter.listKeys(bundleId + "/");
|
|
1203
|
+
for (var i = 0; i < keys.length; i += 1) {
|
|
1204
|
+
var key = keys[i];
|
|
1205
|
+
var prefix = bundleId + "/";
|
|
1206
|
+
if (key.indexOf(prefix) !== 0) continue;
|
|
1207
|
+
var rel = key.slice(prefix.length);
|
|
1208
|
+
// Path-safety: rel must not escape destDir.
|
|
1209
|
+
var destPath = nodePath.join(destDir, rel);
|
|
1210
|
+
var resolvedDest = nodePath.resolve(destPath);
|
|
1211
|
+
var resolvedRoot = nodePath.resolve(destDir);
|
|
1212
|
+
if (resolvedDest !== resolvedRoot &&
|
|
1213
|
+
resolvedDest.indexOf(resolvedRoot + nodePath.sep) !== 0) {
|
|
1214
|
+
throw new BackupError("backup/bad-key",
|
|
1215
|
+
"readBundle: storage key " + JSON.stringify(rel) +
|
|
1216
|
+
" escapes destDir " + JSON.stringify(destDir));
|
|
1217
|
+
}
|
|
1218
|
+
atomicFile.ensureDir(nodePath.dirname(destPath));
|
|
1219
|
+
var bytes = await adapter.readFile(key);
|
|
1220
|
+
nodeFs.writeFileSync(destPath, bytes);
|
|
1221
|
+
}
|
|
1222
|
+
},
|
|
1223
|
+
async listBundles() {
|
|
1224
|
+
// Get every key, partition by bundleId prefix, return sorted.
|
|
1225
|
+
var allKeys = await adapter.listKeys("");
|
|
1226
|
+
var byBundle = new Map();
|
|
1227
|
+
for (var i = 0; i < allKeys.length; i += 1) {
|
|
1228
|
+
var key = allKeys[i];
|
|
1229
|
+
var slash = key.indexOf("/");
|
|
1230
|
+
if (slash <= 0) continue;
|
|
1231
|
+
var bid = key.slice(0, slash);
|
|
1232
|
+
if (!_isValidBundleId(bid)) continue;
|
|
1233
|
+
var stats = byBundle.get(bid);
|
|
1234
|
+
if (!stats) {
|
|
1235
|
+
stats = { count: 0, size: 0 };
|
|
1236
|
+
byBundle.set(bid, stats);
|
|
1237
|
+
}
|
|
1238
|
+
stats.count += 1;
|
|
1239
|
+
// Note: size is approximate — we'd need a stat-per-key here,
|
|
1240
|
+
// and many adapter implementations don't expose a fast stat
|
|
1241
|
+
// primitive. listBundles is best-effort; operators wanting
|
|
1242
|
+
// exact size do their own walk.
|
|
1243
|
+
}
|
|
1244
|
+
var out = [];
|
|
1245
|
+
var entries = Array.from(byBundle.entries());
|
|
1246
|
+
for (var j = 0; j < entries.length; j += 1) {
|
|
1247
|
+
var bidJ = entries[j][0];
|
|
1248
|
+
out.push({
|
|
1249
|
+
bundleId: bidJ,
|
|
1250
|
+
createdAt: null, // adapter may not expose mtime
|
|
1251
|
+
size: null, // best-effort
|
|
1252
|
+
});
|
|
1253
|
+
}
|
|
1254
|
+
out.sort(function (a, b) { return a.bundleId < b.bundleId ? 1 : -1; });
|
|
1255
|
+
return out;
|
|
1256
|
+
},
|
|
1257
|
+
async deleteBundle(bundleId) {
|
|
1258
|
+
_ensureBundleId(bundleId);
|
|
1259
|
+
var keys = await adapter.listKeys(bundleId + "/");
|
|
1260
|
+
for (var i = 0; i < keys.length; i += 1) {
|
|
1261
|
+
await adapter.deleteKey(keys[i]);
|
|
1262
|
+
}
|
|
1263
|
+
},
|
|
1264
|
+
async hasBundle(bundleId) {
|
|
1265
|
+
_ensureBundleId(bundleId);
|
|
1266
|
+
// Format-aware: check the storage layout's marker key. Tar
|
|
1267
|
+
// bundles store under <bid>/bundle.tar; directory bundles store
|
|
1268
|
+
// under <bid>/manifest.json. Operators with a mixed bundle set
|
|
1269
|
+
// (some tar, some directory) get true for either.
|
|
1270
|
+
var tarKey = bundleId + TAR_KEY_SUFFIX;
|
|
1271
|
+
var dirKey = bundleId + "/manifest.json";
|
|
1272
|
+
if (await adapter.hasKey(tarKey)) return true;
|
|
1273
|
+
if (await adapter.hasKey(dirKey)) return true;
|
|
1274
|
+
return false;
|
|
1275
|
+
},
|
|
1276
|
+
};
|
|
1277
|
+
}
|
|
1278
|
+
|
|
1279
|
+
// fsAdapter — default adapter for bundleAdapterStorage backed by the
|
|
1280
|
+
// local filesystem. Provides the same on-disk layout as diskStorage
|
|
1281
|
+
// (bundle directories under a root path) but via the adapter contract
|
|
1282
|
+
// so v0.12.8+ can swap the implementation transparently.
|
|
1283
|
+
bundleAdapterStorage.fsAdapter = function (fsOpts) {
|
|
1284
|
+
fsOpts = fsOpts || {};
|
|
1285
|
+
validateOpts.requireNonEmptyString(fsOpts.root,
|
|
1286
|
+
"bundleAdapterStorage.fsAdapter: opts.root", BackupError, "backup/no-storage-root");
|
|
1287
|
+
var root = fsOpts.root;
|
|
1288
|
+
atomicFile.ensureDir(root);
|
|
1289
|
+
|
|
1290
|
+
function _keyPath(key) {
|
|
1291
|
+
// Refuse keys with traversal segments — defense in depth even
|
|
1292
|
+
// though the storage layer also checks.
|
|
1293
|
+
if (key.indexOf("..") !== -1 || key.indexOf("\0") !== -1) {
|
|
1294
|
+
throw new BackupError("backup/bad-key",
|
|
1295
|
+
"fsAdapter: key contains invalid characters: " + JSON.stringify(key));
|
|
1296
|
+
}
|
|
1297
|
+
return nodePath.join(root, key);
|
|
1298
|
+
}
|
|
1299
|
+
|
|
1300
|
+
return {
|
|
1301
|
+
async writeFile(key, bytes) {
|
|
1302
|
+
var path = _keyPath(key);
|
|
1303
|
+
atomicFile.ensureDir(nodePath.dirname(path));
|
|
1304
|
+
nodeFs.writeFileSync(path, bytes);
|
|
1305
|
+
},
|
|
1306
|
+
async readFile(key) {
|
|
1307
|
+
var path = _keyPath(key);
|
|
1308
|
+
if (!nodeFs.existsSync(path)) {
|
|
1309
|
+
throw new BackupError("backup/no-key", "fsAdapter: key not found: " + JSON.stringify(key));
|
|
1310
|
+
}
|
|
1311
|
+
return nodeFs.readFileSync(path);
|
|
1312
|
+
},
|
|
1313
|
+
async listKeys(prefix) {
|
|
1314
|
+
var out = [];
|
|
1315
|
+
if (!nodeFs.existsSync(root)) return out;
|
|
1316
|
+
function _walk(rel) {
|
|
1317
|
+
var entries = nodeFs.readdirSync(nodePath.join(root, rel || "."), { withFileTypes: true });
|
|
1318
|
+
for (var i = 0; i < entries.length; i += 1) {
|
|
1319
|
+
var name = entries[i].name;
|
|
1320
|
+
var nextRel = rel ? (rel + "/" + name) : name;
|
|
1321
|
+
if (entries[i].isDirectory()) {
|
|
1322
|
+
_walk(nextRel);
|
|
1323
|
+
} else if (entries[i].isFile()) {
|
|
1324
|
+
if (!prefix || nextRel.indexOf(prefix) === 0) {
|
|
1325
|
+
out.push(nextRel);
|
|
1326
|
+
}
|
|
1327
|
+
}
|
|
1328
|
+
}
|
|
1329
|
+
}
|
|
1330
|
+
_walk("");
|
|
1331
|
+
return out;
|
|
1332
|
+
},
|
|
1333
|
+
async deleteKey(key) {
|
|
1334
|
+
var path = _keyPath(key);
|
|
1335
|
+
try { nodeFs.rmSync(path); } catch (_e) { /* drop-silent — key already gone */ }
|
|
1336
|
+
},
|
|
1337
|
+
async hasKey(key) {
|
|
1338
|
+
try { return nodeFs.existsSync(_keyPath(key)); }
|
|
1339
|
+
catch (_e) { return false; }
|
|
1340
|
+
},
|
|
1341
|
+
};
|
|
1342
|
+
};
|
|
1343
|
+
|
|
1344
|
+
// ---- v0.12.8: migrate ----------------------------------------------------
|
|
1345
|
+
|
|
1346
|
+
/**
|
|
1347
|
+
* @primitive b.backup.migrate
|
|
1348
|
+
* @signature b.backup.migrate(opts)
|
|
1349
|
+
* @since 0.12.8
|
|
1350
|
+
* @status stable
|
|
1351
|
+
* @related b.backup.bundleAdapterStorage
|
|
1352
|
+
*
|
|
1353
|
+
* One-shot helper that walks an operator's directory-tree-format
|
|
1354
|
+
* bundle (v0.12.7 layout) and writes the same content as a tar-format
|
|
1355
|
+
* bundle via the v0.12.8 `bundleAdapterStorage`. Idempotent: re-
|
|
1356
|
+
* running on an already-migrated bundle is a no-op. Source stays in
|
|
1357
|
+
* place by default; operators with explicit transition windows opt
|
|
1358
|
+
* into the inline replace via `deleteSourceOnSuccess: true`.
|
|
1359
|
+
*
|
|
1360
|
+
* @opts
|
|
1361
|
+
* from: bundleAdapterStorage with format: "directory",
|
|
1362
|
+
* to: bundleAdapterStorage with format: "tar",
|
|
1363
|
+
* bundleId: string (single-bundle migrate; omit to migrate all),
|
|
1364
|
+
* deleteSourceOnSuccess: boolean (default false; explicit opt-in),
|
|
1365
|
+
*
|
|
1366
|
+
* @example
|
|
1367
|
+
* var from = b.backup.bundleAdapterStorage({
|
|
1368
|
+
* adapter: b.backup.bundleAdapterStorage.fsAdapter({ root: "/var/backups-v7" }),
|
|
1369
|
+
* format: "directory",
|
|
1370
|
+
* });
|
|
1371
|
+
* var to = b.backup.bundleAdapterStorage({
|
|
1372
|
+
* adapter: b.backup.bundleAdapterStorage.fsAdapter({ root: "/var/backups-v8" }),
|
|
1373
|
+
* format: "tar",
|
|
1374
|
+
* });
|
|
1375
|
+
* await b.backup.migrate({ from: from, to: to });
|
|
1376
|
+
*/
|
|
1377
|
+
async function migrate(opts) {
|
|
1378
|
+
opts = opts || {};
|
|
1379
|
+
if (!opts.from || typeof opts.from.readBundle !== "function" ||
|
|
1380
|
+
typeof opts.from.listBundles !== "function") {
|
|
1381
|
+
throw new BackupError("backup/bad-from",
|
|
1382
|
+
"migrate: opts.from must be a storage backend (got " + typeof opts.from + ")");
|
|
1383
|
+
}
|
|
1384
|
+
if (!opts.to || typeof opts.to.writeBundle !== "function" ||
|
|
1385
|
+
typeof opts.to.hasBundle !== "function") {
|
|
1386
|
+
throw new BackupError("backup/bad-to",
|
|
1387
|
+
"migrate: opts.to must be a storage backend (got " + typeof opts.to + ")");
|
|
1388
|
+
}
|
|
1389
|
+
var ids;
|
|
1390
|
+
if (opts.bundleId) {
|
|
1391
|
+
if (!_isValidBundleId(opts.bundleId)) {
|
|
1392
|
+
throw new BackupError("backup/bad-bundle-id",
|
|
1393
|
+
"migrate: bundleId must match the framework's timestamp+suffix format");
|
|
1394
|
+
}
|
|
1395
|
+
ids = [opts.bundleId];
|
|
1396
|
+
} else {
|
|
1397
|
+
var list = await opts.from.listBundles();
|
|
1398
|
+
ids = list.map(function (b) { return b.bundleId; });
|
|
1399
|
+
}
|
|
1400
|
+
var migrated = 0;
|
|
1401
|
+
var skipped = 0;
|
|
1402
|
+
for (var i = 0; i < ids.length; i += 1) {
|
|
1403
|
+
var bid = ids[i];
|
|
1404
|
+
// Idempotency: skip if destination already has the bundle.
|
|
1405
|
+
if (await opts.to.hasBundle(bid)) {
|
|
1406
|
+
skipped += 1;
|
|
1407
|
+
continue;
|
|
1408
|
+
}
|
|
1409
|
+
// Stage source-bundle into a tmp dir, then write via destination.
|
|
1410
|
+
var tmpDir = nodeFs.mkdtempSync(nodePath.join(os.tmpdir(),
|
|
1411
|
+
"blamejs-backup-migrate-" + bid + "-"));
|
|
1412
|
+
var stageDir = nodePath.join(tmpDir, "bundle");
|
|
1413
|
+
try {
|
|
1414
|
+
await opts.from.readBundle(bid, stageDir);
|
|
1415
|
+
await opts.to.writeBundle(bid, stageDir);
|
|
1416
|
+
migrated += 1;
|
|
1417
|
+
if (opts.deleteSourceOnSuccess === true) {
|
|
1418
|
+
if (typeof opts.from.deleteBundle === "function") {
|
|
1419
|
+
await opts.from.deleteBundle(bid);
|
|
1420
|
+
}
|
|
1421
|
+
}
|
|
1422
|
+
} finally {
|
|
1423
|
+
try { nodeFs.rmSync(tmpDir, { recursive: true, force: true }); }
|
|
1424
|
+
catch (_e) { /* drop-silent */ }
|
|
1425
|
+
}
|
|
1426
|
+
}
|
|
1427
|
+
return { migrated: migrated, skipped: skipped, total: ids.length };
|
|
1428
|
+
}
|
|
1429
|
+
|
|
1430
|
+
|
|
@@ -71,6 +71,7 @@ var gateContract = require("./gate-contract");
|
|
|
71
71
|
var C = require("./constants");
|
|
72
72
|
var numericBounds = require("./numeric-bounds");
|
|
73
73
|
var guardFilename = require("./guard-filename");
|
|
74
|
+
var archiveRead = lazyRequire(function () { return require("./archive-read"); });
|
|
74
75
|
var { GuardArchiveError } = require("./framework-error");
|
|
75
76
|
|
|
76
77
|
var observability = lazyRequire(function () { return require("./observability"); });
|
|
@@ -898,4 +899,183 @@ module.exports = {
|
|
|
898
899
|
ARCHIVE_EXTENSIONS: ARCHIVE_EXTENSIONS,
|
|
899
900
|
MAGIC_SIGNATURES: MAGIC_SIGNATURES,
|
|
900
901
|
GuardArchiveError: GuardArchiveError,
|
|
902
|
+
inspect: inspect,
|
|
903
|
+
zipBombPolicy: zipBombPolicy,
|
|
904
|
+
entryTypePolicy: entryTypePolicy,
|
|
905
|
+
tarEntryPolicy: tarEntryPolicy,
|
|
901
906
|
};
|
|
907
|
+
|
|
908
|
+
// ---- v0.12.7 extensions ---------------------------------------------------
|
|
909
|
+
|
|
910
|
+
/**
|
|
911
|
+
* @primitive b.guardArchive.inspect
|
|
912
|
+
* @signature b.guardArchive.inspect(adapter, opts?)
|
|
913
|
+
* @since 0.12.7
|
|
914
|
+
* @status stable
|
|
915
|
+
* @related b.archive.read.zip, b.guardArchive.validateEntries
|
|
916
|
+
*
|
|
917
|
+
* Bridge primitive: runs `b.archive.read.zip(adapter).inspect()` to
|
|
918
|
+
* enumerate the entry list (no decompression), then hands the list to
|
|
919
|
+
* `validateEntries` for the full posture-aware gate. Returns
|
|
920
|
+
* `{ entries, issues, decisions }` so the caller decides whether to
|
|
921
|
+
* proceed.
|
|
922
|
+
*
|
|
923
|
+
* Operators using the lower-level read primitive directly call this
|
|
924
|
+
* to combine the metadata pass with the guard pass; `b.safeArchive.
|
|
925
|
+
* extract` does the same composition inline under the hood.
|
|
926
|
+
*
|
|
927
|
+
* @opts
|
|
928
|
+
* profile: "strict" | "balanced" | "permissive" | "hipaa" | ...,
|
|
929
|
+
* format: "zip" (v0.12.7 — tar v0.12.8, gz v0.12.9),
|
|
930
|
+
* audit: b.audit,
|
|
931
|
+
*
|
|
932
|
+
* @example
|
|
933
|
+
* var adapter = b.archive.adapters.fs("/var/uploads/payload.zip");
|
|
934
|
+
* var summary = await b.guardArchive.inspect(adapter, { profile: "strict" });
|
|
935
|
+
* if (summary.issues.length > 0) refuse(summary.issues);
|
|
936
|
+
*/
|
|
937
|
+
async function inspect(adapter, opts) {
|
|
938
|
+
opts = opts || {};
|
|
939
|
+
var format = opts.format || "zip";
|
|
940
|
+
if (format !== "zip") {
|
|
941
|
+
throw new GuardArchiveError("archive/format-unsupported",
|
|
942
|
+
"guardArchive.inspect: format=" + JSON.stringify(format) +
|
|
943
|
+
" — v0.12.7 ships ZIP only (tar v0.12.8, gz v0.12.9)");
|
|
944
|
+
}
|
|
945
|
+
var reader = archiveRead().zip(adapter, { audit: opts.audit });
|
|
946
|
+
var rawEntries = await reader.inspect();
|
|
947
|
+
// Project the read-primitive's entry shape into validateEntries'
|
|
948
|
+
// expected `{ name, size, compressedSize, isSymlink, ... }` shape.
|
|
949
|
+
var guardEntries = rawEntries.map(function (e) {
|
|
950
|
+
return {
|
|
951
|
+
name: e.name,
|
|
952
|
+
size: e.size,
|
|
953
|
+
compressedSize: e.compressedSize,
|
|
954
|
+
isSymlink: e.entryType === "symlink",
|
|
955
|
+
isHardlink: false,
|
|
956
|
+
linkTarget: null,
|
|
957
|
+
isDirectory: e.entryType === "directory",
|
|
958
|
+
isEncrypted: e.isEncrypted,
|
|
959
|
+
attrs: { externalAttrs: e.externalAttrs },
|
|
960
|
+
};
|
|
961
|
+
});
|
|
962
|
+
var profile = opts.profile || "balanced";
|
|
963
|
+
var result = validateEntries(guardEntries, { profile: profile });
|
|
964
|
+
return {
|
|
965
|
+
entries: rawEntries,
|
|
966
|
+
issues: (result && result.issues) || [],
|
|
967
|
+
decisions: (result && result.decisions) || {},
|
|
968
|
+
};
|
|
969
|
+
}
|
|
970
|
+
|
|
971
|
+
/**
|
|
972
|
+
* @primitive b.guardArchive.zipBombPolicy
|
|
973
|
+
* @signature b.guardArchive.zipBombPolicy(opts)
|
|
974
|
+
* @since 0.12.7
|
|
975
|
+
* @status stable
|
|
976
|
+
* @related b.archive.read.zip, b.safeArchive.extract
|
|
977
|
+
*
|
|
978
|
+
* Policy-object builder for decompression-bomb caps. Operators
|
|
979
|
+
* declare the cap set once + reuse it across `b.archive.read.zip` /
|
|
980
|
+
* `b.safeArchive.extract` call sites. Defaults match the cap shape
|
|
981
|
+
* in `lib/archive-read.js` `DEFAULT_BOMB_POLICY`.
|
|
982
|
+
*
|
|
983
|
+
* @opts
|
|
984
|
+
* maxEntries: 65535,
|
|
985
|
+
* maxEntryDecompressedBytes: 128 * MiB,
|
|
986
|
+
* maxTotalDecompressedBytes: 4 * GiB,
|
|
987
|
+
* maxExpansionRatio: 100,
|
|
988
|
+
*
|
|
989
|
+
* @example
|
|
990
|
+
* var policy = b.guardArchive.zipBombPolicy({
|
|
991
|
+
* maxTotalDecompressedBytes: 256 * 1024 * 1024,
|
|
992
|
+
* maxExpansionRatio: 50,
|
|
993
|
+
* });
|
|
994
|
+
* await b.safeArchive.extract({ source, destination, bombPolicy: policy });
|
|
995
|
+
*/
|
|
996
|
+
function zipBombPolicy(opts) {
|
|
997
|
+
opts = opts || {};
|
|
998
|
+
return Object.freeze({
|
|
999
|
+
maxEntries: opts.maxEntries || 65535,
|
|
1000
|
+
maxEntryDecompressedBytes: opts.maxEntryDecompressedBytes || C.BYTES.mib(128),
|
|
1001
|
+
maxTotalDecompressedBytes: opts.maxTotalDecompressedBytes || C.BYTES.gib(4),
|
|
1002
|
+
maxExpansionRatio: opts.maxExpansionRatio || 100,
|
|
1003
|
+
});
|
|
1004
|
+
}
|
|
1005
|
+
|
|
1006
|
+
/**
|
|
1007
|
+
* @primitive b.guardArchive.entryTypePolicy
|
|
1008
|
+
* @signature b.guardArchive.entryTypePolicy(opts)
|
|
1009
|
+
* @since 0.12.7
|
|
1010
|
+
* @status stable
|
|
1011
|
+
* @related b.archive.read.zip, b.safeArchive.extract
|
|
1012
|
+
*
|
|
1013
|
+
* Policy-object builder for entry-type allowlist. Defaults refuse
|
|
1014
|
+
* every "interesting" entry type (symlink / hardlink / device / fifo
|
|
1015
|
+
* / socket); operators opt in per-type and route through the
|
|
1016
|
+
* additional realpath-on-target check in `b.guardFilename.
|
|
1017
|
+
* verifyExtractionPath`.
|
|
1018
|
+
*
|
|
1019
|
+
* Symlinks + hardlinks under default settings are refused
|
|
1020
|
+
* unconditionally — CVE-2025-11001 / 11002 / 26960 class.
|
|
1021
|
+
*
|
|
1022
|
+
* @opts
|
|
1023
|
+
* symlinks: false,
|
|
1024
|
+
* hardlinks: false,
|
|
1025
|
+
* devices: false,
|
|
1026
|
+
* fifos: false,
|
|
1027
|
+
* sockets: false,
|
|
1028
|
+
*
|
|
1029
|
+
* @example
|
|
1030
|
+
* var policy = b.guardArchive.entryTypePolicy({ symlinks: true });
|
|
1031
|
+
* await b.safeArchive.extract({ source, destination, entryTypePolicy: policy });
|
|
1032
|
+
*/
|
|
1033
|
+
function entryTypePolicy(opts) {
|
|
1034
|
+
opts = opts || {};
|
|
1035
|
+
return Object.freeze({
|
|
1036
|
+
symlinks: opts.symlinks === true,
|
|
1037
|
+
hardlinks: opts.hardlinks === true,
|
|
1038
|
+
devices: opts.devices === true,
|
|
1039
|
+
fifos: opts.fifos === true,
|
|
1040
|
+
sockets: opts.sockets === true,
|
|
1041
|
+
});
|
|
1042
|
+
}
|
|
1043
|
+
|
|
1044
|
+
/**
|
|
1045
|
+
* @primitive b.guardArchive.tarEntryPolicy
|
|
1046
|
+
* @signature b.guardArchive.tarEntryPolicy(opts)
|
|
1047
|
+
* @since 0.12.8
|
|
1048
|
+
* @status stable
|
|
1049
|
+
* @related b.guardArchive.entryTypePolicy, b.archive.read.tar
|
|
1050
|
+
*
|
|
1051
|
+
* Tar-specific entry-type policy. Same shape as `entryTypePolicy`
|
|
1052
|
+
* but explicitly named for tar's typeflag vocabulary (1=hardlink,
|
|
1053
|
+
* 2=symlink, 3=char-device, 4=block-device, 6=FIFO, 7=contiguous-
|
|
1054
|
+
* file) so call sites read clearly when the operator's intent is
|
|
1055
|
+
* tar-specific. Defaults refuse every dangerous typeflag. Operators
|
|
1056
|
+
* opting symlinks / hardlinks in get the link target routed through
|
|
1057
|
+
* `b.guardFilename.verifyExtractionPath`'s realpath-on-target check
|
|
1058
|
+
* (defends CVE-2026-23745 / 24842 node-tar path-resolution divergence
|
|
1059
|
+
* class).
|
|
1060
|
+
*
|
|
1061
|
+
* @opts
|
|
1062
|
+
* symlinks: false,
|
|
1063
|
+
* hardlinks: false,
|
|
1064
|
+
* devices: false,
|
|
1065
|
+
* fifos: false,
|
|
1066
|
+
* sockets: false,
|
|
1067
|
+
*
|
|
1068
|
+
* @example
|
|
1069
|
+
* var policy = b.guardArchive.tarEntryPolicy({ symlinks: true });
|
|
1070
|
+
* await b.safeArchive.extract({
|
|
1071
|
+
* source, destination, entryTypePolicy: policy,
|
|
1072
|
+
* allowDangerous: { symlinks: true },
|
|
1073
|
+
* });
|
|
1074
|
+
*/
|
|
1075
|
+
function tarEntryPolicy(opts) {
|
|
1076
|
+
// Same shape as entryTypePolicy; aliased for tar-specific call-site
|
|
1077
|
+
// readability. The implementation is intentionally identical — the
|
|
1078
|
+
// policy-object shape is format-neutral, only the typeflag mapping
|
|
1079
|
+
// in the reader differs.
|
|
1080
|
+
return entryTypePolicy(opts);
|
|
1081
|
+
}
|