@blamejs/blamejs-shop 0.0.83 → 0.0.98

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 (33) hide show
  1. package/CHANGELOG.md +30 -0
  2. package/lib/admin.js +11 -7
  3. package/lib/customer-import.js +1 -1
  4. package/lib/email-campaigns.js +1 -1
  5. package/lib/pwa-manifest.js +1 -0
  6. package/lib/vendor/MANIFEST.json +2 -2
  7. package/lib/vendor/blamejs/.github/workflows/sha-to-tag-verify.yml +8 -0
  8. package/lib/vendor/blamejs/CHANGELOG.md +6 -0
  9. package/lib/vendor/blamejs/README.md +1 -1
  10. package/lib/vendor/blamejs/SECURITY.md +1 -0
  11. package/lib/vendor/blamejs/api-snapshot.json +167 -2
  12. package/lib/vendor/blamejs/fuzz/safe-archive.fuzz.js +37 -0
  13. package/lib/vendor/blamejs/index.js +15 -1
  14. package/lib/vendor/blamejs/lib/archive-adapters.js +629 -0
  15. package/lib/vendor/blamejs/lib/archive-gz.js +229 -0
  16. package/lib/vendor/blamejs/lib/archive-read.js +781 -0
  17. package/lib/vendor/blamejs/lib/archive-tar-read.js +418 -0
  18. package/lib/vendor/blamejs/lib/archive-tar.js +571 -0
  19. package/lib/vendor/blamejs/lib/archive.js +24 -2
  20. package/lib/vendor/blamejs/lib/audit.js +22 -7
  21. package/lib/vendor/blamejs/lib/backup/index.js +469 -0
  22. package/lib/vendor/blamejs/lib/guard-archive.js +180 -0
  23. package/lib/vendor/blamejs/lib/guard-filename.js +205 -0
  24. package/lib/vendor/blamejs/lib/safe-archive.js +309 -0
  25. package/lib/vendor/blamejs/package.json +1 -1
  26. package/lib/vendor/blamejs/release-notes/v0.12.7.json +86 -0
  27. package/lib/vendor/blamejs/release-notes/v0.12.8.json +81 -0
  28. package/lib/vendor/blamejs/release-notes/v0.12.9.json +61 -0
  29. package/lib/vendor/blamejs/test/layer-0-primitives/archive-gz.test.js +159 -0
  30. package/lib/vendor/blamejs/test/layer-0-primitives/archive-read.test.js +247 -0
  31. package/lib/vendor/blamejs/test/layer-0-primitives/archive-tar.test.js +228 -0
  32. package/lib/vendor/blamejs/test/layer-0-primitives/codebase-patterns.test.js +180 -0
  33. 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,468 @@ 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 !== "tar.gz" && format !== "directory") {
1068
+ throw new BackupError("backup/bad-format",
1069
+ "bundleAdapterStorage: format must be \"tar\" (default) | \"tar.gz\" (v0.12.9 compressed) | \"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` (or `<bundleId>/bundle.tar.gz`
1115
+ // for the v0.12.9 compressed variant). The marker is named that
1116
+ // way so listBundles + hasBundle can locate either format by key
1117
+ // prefix walk.
1118
+ var TAR_KEY_SUFFIX = "/bundle.tar";
1119
+ var TAR_GZ_KEY_SUFFIX = "/bundle.tar.gz";
1120
+
1121
+ function _hasBundleKey(bundleId, format) {
1122
+ if (format === "tar") return adapter.hasKey(bundleId + TAR_KEY_SUFFIX);
1123
+ if (format === "tar.gz") return adapter.hasKey(bundleId + TAR_GZ_KEY_SUFFIX);
1124
+ return adapter.hasKey(bundleId + "/manifest.json");
1125
+ }
1126
+
1127
+ return {
1128
+ name: "adapter",
1129
+ async writeBundle(bundleId, sourceDir) {
1130
+ _ensureBundleId(bundleId);
1131
+ if (!nodeFs.existsSync(sourceDir)) {
1132
+ throw new BackupError("backup/no-source",
1133
+ "writeBundle: sourceDir does not exist: " + sourceDir);
1134
+ }
1135
+ var alreadyHas = await _hasBundleKey(bundleId, format);
1136
+ if (alreadyHas) {
1137
+ throw new BackupError("backup/bundle-exists",
1138
+ "writeBundle: bundle '" + bundleId + "' already exists in storage");
1139
+ }
1140
+ if (format === "tar" || format === "tar.gz") {
1141
+ // Pack the source-directory tree into a single tar archive +
1142
+ // store under one key. Composes b.archive.tar (+ b.archive.gz
1143
+ // for the tar.gz variant which adds gzip compression on the
1144
+ // wire). Bundle sizes drop ~3-5× on text-heavy backups
1145
+ // (databases, JSON exports, mail spools) under tar.gz.
1146
+ //
1147
+ // Codex P2 on v0.12.8 PR #159 — tar bytes are materialized in
1148
+ // memory because the v0.12.8 adapter contract is bytes-in
1149
+ // (writeFile takes a Buffer, no writeStream method). The
1150
+ // maxBundleBytes pre-walk computes the uncompressed payload
1151
+ // size (file bytes only — tar header overhead is bounded at
1152
+ // ~512 B per entry + 1024 B trailer) and refuses upfront so
1153
+ // pathological inputs throw `backup/bundle-too-large` instead
1154
+ // of OOM. The defer-with-condition for true streaming is
1155
+ // gated on the adapter contract gaining writeStream(key).
1156
+ var relPaths = _walkDirSync(sourceDir, []);
1157
+ var projectedBytes = 0;
1158
+ for (var pi = 0; pi < relPaths.length; pi += 1) {
1159
+ var stat = nodeFs.statSync(nodePath.join(sourceDir, relPaths[pi]));
1160
+ projectedBytes += stat.size;
1161
+ }
1162
+ if (projectedBytes > maxBundleBytes) {
1163
+ throw new BackupError("backup/bundle-too-large",
1164
+ "writeBundle: projected uncompressed bundle " + projectedBytes +
1165
+ " bytes exceeds maxBundleBytes=" + maxBundleBytes +
1166
+ " — split the source tree across multiple bundles or raise the cap");
1167
+ }
1168
+ var t = archiveLazy().tar();
1169
+ for (var i = 0; i < relPaths.length; i += 1) {
1170
+ var rel = relPaths[i];
1171
+ var bytes = nodeFs.readFileSync(nodePath.join(sourceDir, rel));
1172
+ t.addFile(rel, bytes);
1173
+ }
1174
+ var keySuffix = format === "tar.gz" ? TAR_GZ_KEY_SUFFIX : TAR_KEY_SUFFIX;
1175
+ var payloadBytes = format === "tar.gz"
1176
+ ? archiveLazy().gz(t.toBuffer()).toBuffer()
1177
+ : t.toBuffer();
1178
+ await adapter.writeFile(bundleId + keySuffix, payloadBytes);
1179
+ return;
1180
+ }
1181
+ // Directory format (v0.12.7 layout).
1182
+ var dirRelPaths = _walkDirSync(sourceDir, []);
1183
+ for (var j = 0; j < dirRelPaths.length; j += 1) {
1184
+ var dirRel = dirRelPaths[j];
1185
+ var dirBytes = nodeFs.readFileSync(nodePath.join(sourceDir, dirRel));
1186
+ await adapter.writeFile(bundleId + "/" + dirRel, dirBytes);
1187
+ }
1188
+ },
1189
+ async readBundle(bundleId, destDir) {
1190
+ _ensureBundleId(bundleId);
1191
+ if (nodeFs.existsSync(destDir)) {
1192
+ throw new BackupError("backup/dest-exists",
1193
+ "readBundle: destDir already exists: " + destDir);
1194
+ }
1195
+ // Detect which format this bundle is in — operators with mixed
1196
+ // pre-v0.12.8 + post-v0.12.8 (+ v0.12.9 tar.gz) bundles can read
1197
+ // every flavor back.
1198
+ var hasTar = await adapter.hasKey(bundleId + TAR_KEY_SUFFIX);
1199
+ var hasTarGz = await adapter.hasKey(bundleId + TAR_GZ_KEY_SUFFIX);
1200
+ var hasManifest = await adapter.hasKey(bundleId + "/manifest.json");
1201
+ if (!hasTar && !hasTarGz && !hasManifest) {
1202
+ throw new BackupError("backup/bundle-not-found",
1203
+ "readBundle: '" + bundleId + "' not in storage");
1204
+ }
1205
+ atomicFile.ensureDir(destDir);
1206
+ if (hasTarGz) {
1207
+ // Codex P1/P2 on v0.12.9 PR #160 — propagate maxBundleBytes
1208
+ // to the gz restore path + disable the expansion-ratio cap.
1209
+ // archive.read.gz defaults (1 GiB output / 100× ratio) are
1210
+ // bomb-defense settings appropriate for adversarial input;
1211
+ // this is a SELF-AUTHORED bundle the writeBundle path
1212
+ // produced under maxBundleBytes — the restore contract is
1213
+ // "decompress to at most what was permitted on write." A
1214
+ // zero-filled DB file can easily hit >100× compression
1215
+ // ratio; without these opts the same primitive writes
1216
+ // bundles it can't read back.
1217
+ var gzBytes = await adapter.readFile(bundleId + TAR_GZ_KEY_SUFFIX);
1218
+ var gzReader = archiveLazy().read.gz(archiveAdaptersLazy().buffer(gzBytes), {
1219
+ maxDecompressedBytes: maxBundleBytes,
1220
+ maxExpansionRatio: 0,
1221
+ });
1222
+ var tarReader = gzReader.asTar();
1223
+ await tarReader.extract({ destination: destDir });
1224
+ return;
1225
+ }
1226
+ if (hasTar) {
1227
+ var tarBytes = await adapter.readFile(bundleId + TAR_KEY_SUFFIX);
1228
+ var reader = archiveLazy().read.tar(archiveAdaptersLazy().buffer(tarBytes));
1229
+ await reader.extract({ destination: destDir });
1230
+ return;
1231
+ }
1232
+ // Directory format readback (v0.12.7 layout).
1233
+ var keys = await adapter.listKeys(bundleId + "/");
1234
+ for (var i = 0; i < keys.length; i += 1) {
1235
+ var key = keys[i];
1236
+ var prefix = bundleId + "/";
1237
+ if (key.indexOf(prefix) !== 0) continue;
1238
+ var rel = key.slice(prefix.length);
1239
+ // Path-safety: rel must not escape destDir.
1240
+ var destPath = nodePath.join(destDir, rel);
1241
+ var resolvedDest = nodePath.resolve(destPath);
1242
+ var resolvedRoot = nodePath.resolve(destDir);
1243
+ if (resolvedDest !== resolvedRoot &&
1244
+ resolvedDest.indexOf(resolvedRoot + nodePath.sep) !== 0) {
1245
+ throw new BackupError("backup/bad-key",
1246
+ "readBundle: storage key " + JSON.stringify(rel) +
1247
+ " escapes destDir " + JSON.stringify(destDir));
1248
+ }
1249
+ atomicFile.ensureDir(nodePath.dirname(destPath));
1250
+ var bytes = await adapter.readFile(key);
1251
+ // Exclusive-create (wx) carries the v0.12.7 atomic-rollback
1252
+ // contract: readBundle refuses to overwrite pre-existing
1253
+ // files at destPath. Combined with the upfront destDir check
1254
+ // above (refuses if destDir already exists), the only way
1255
+ // wx fires here is a symlink swap between ensureDir and write
1256
+ // — which the framework refuses rather than following.
1257
+ nodeFs.writeFileSync(destPath, bytes, { flag: "wx", mode: 0o600 });
1258
+ }
1259
+ },
1260
+ async listBundles() {
1261
+ // Get every key, partition by bundleId prefix, return sorted.
1262
+ var allKeys = await adapter.listKeys("");
1263
+ var byBundle = new Map();
1264
+ for (var i = 0; i < allKeys.length; i += 1) {
1265
+ var key = allKeys[i];
1266
+ var slash = key.indexOf("/");
1267
+ if (slash <= 0) continue;
1268
+ var bid = key.slice(0, slash);
1269
+ if (!_isValidBundleId(bid)) continue;
1270
+ var stats = byBundle.get(bid);
1271
+ if (!stats) {
1272
+ stats = { count: 0, size: 0 };
1273
+ byBundle.set(bid, stats);
1274
+ }
1275
+ stats.count += 1;
1276
+ // Note: size is approximate — we'd need a stat-per-key here,
1277
+ // and many adapter implementations don't expose a fast stat
1278
+ // primitive. listBundles is best-effort; operators wanting
1279
+ // exact size do their own walk.
1280
+ }
1281
+ var out = [];
1282
+ var entries = Array.from(byBundle.entries());
1283
+ for (var j = 0; j < entries.length; j += 1) {
1284
+ var bidJ = entries[j][0];
1285
+ out.push({
1286
+ bundleId: bidJ,
1287
+ createdAt: null, // adapter may not expose mtime
1288
+ size: null, // best-effort
1289
+ });
1290
+ }
1291
+ out.sort(function (a, b) { return a.bundleId < b.bundleId ? 1 : -1; });
1292
+ return out;
1293
+ },
1294
+ async deleteBundle(bundleId) {
1295
+ _ensureBundleId(bundleId);
1296
+ var keys = await adapter.listKeys(bundleId + "/");
1297
+ for (var i = 0; i < keys.length; i += 1) {
1298
+ await adapter.deleteKey(keys[i]);
1299
+ }
1300
+ },
1301
+ async hasBundle(bundleId) {
1302
+ _ensureBundleId(bundleId);
1303
+ // Format-aware: check the storage layout's marker key. Tar
1304
+ // bundles store under <bid>/bundle.tar; tar.gz bundles store
1305
+ // under <bid>/bundle.tar.gz; directory bundles store under
1306
+ // <bid>/manifest.json. Operators with a mixed bundle set
1307
+ // (some tar, some tar.gz, some directory) get true for any.
1308
+ var tarKey = bundleId + TAR_KEY_SUFFIX;
1309
+ var tarGzKey = bundleId + TAR_GZ_KEY_SUFFIX;
1310
+ var dirKey = bundleId + "/manifest.json";
1311
+ if (await adapter.hasKey(tarKey)) return true;
1312
+ if (await adapter.hasKey(tarGzKey)) return true;
1313
+ if (await adapter.hasKey(dirKey)) return true;
1314
+ return false;
1315
+ },
1316
+ };
1317
+ }
1318
+
1319
+ // fsAdapter — default adapter for bundleAdapterStorage backed by the
1320
+ // local filesystem. Provides the same on-disk layout as diskStorage
1321
+ // (bundle directories under a root path) but via the adapter contract
1322
+ // so v0.12.8+ can swap the implementation transparently.
1323
+ bundleAdapterStorage.fsAdapter = function (fsOpts) {
1324
+ fsOpts = fsOpts || {};
1325
+ validateOpts.requireNonEmptyString(fsOpts.root,
1326
+ "bundleAdapterStorage.fsAdapter: opts.root", BackupError, "backup/no-storage-root");
1327
+ var root = fsOpts.root;
1328
+ atomicFile.ensureDir(root);
1329
+
1330
+ function _keyPath(key) {
1331
+ // Refuse keys with traversal segments — defense in depth even
1332
+ // though the storage layer also checks.
1333
+ if (key.indexOf("..") !== -1 || key.indexOf("\0") !== -1) {
1334
+ throw new BackupError("backup/bad-key",
1335
+ "fsAdapter: key contains invalid characters: " + JSON.stringify(key));
1336
+ }
1337
+ return nodePath.join(root, key);
1338
+ }
1339
+
1340
+ return {
1341
+ async writeFile(key, bytes) {
1342
+ var path = _keyPath(key);
1343
+ atomicFile.ensureDir(nodePath.dirname(path));
1344
+ nodeFs.writeFileSync(path, bytes);
1345
+ },
1346
+ async readFile(key) {
1347
+ var path = _keyPath(key);
1348
+ if (!nodeFs.existsSync(path)) {
1349
+ throw new BackupError("backup/no-key", "fsAdapter: key not found: " + JSON.stringify(key));
1350
+ }
1351
+ return nodeFs.readFileSync(path);
1352
+ },
1353
+ async listKeys(prefix) {
1354
+ var out = [];
1355
+ if (!nodeFs.existsSync(root)) return out;
1356
+ function _walk(rel) {
1357
+ var entries = nodeFs.readdirSync(nodePath.join(root, rel || "."), { withFileTypes: true });
1358
+ for (var i = 0; i < entries.length; i += 1) {
1359
+ var name = entries[i].name;
1360
+ var nextRel = rel ? (rel + "/" + name) : name;
1361
+ if (entries[i].isDirectory()) {
1362
+ _walk(nextRel);
1363
+ } else if (entries[i].isFile()) {
1364
+ if (!prefix || nextRel.indexOf(prefix) === 0) {
1365
+ out.push(nextRel);
1366
+ }
1367
+ }
1368
+ }
1369
+ }
1370
+ _walk("");
1371
+ return out;
1372
+ },
1373
+ async deleteKey(key) {
1374
+ var path = _keyPath(key);
1375
+ try { nodeFs.rmSync(path); } catch (_e) { /* drop-silent — key already gone */ }
1376
+ },
1377
+ async hasKey(key) {
1378
+ try { return nodeFs.existsSync(_keyPath(key)); }
1379
+ catch (_e) { return false; }
1380
+ },
1381
+ };
1382
+ };
1383
+
1384
+ // ---- v0.12.8: migrate ----------------------------------------------------
1385
+
1386
+ /**
1387
+ * @primitive b.backup.migrate
1388
+ * @signature b.backup.migrate(opts)
1389
+ * @since 0.12.8
1390
+ * @status stable
1391
+ * @related b.backup.bundleAdapterStorage
1392
+ *
1393
+ * One-shot helper that walks an operator's directory-tree-format
1394
+ * bundle (v0.12.7 layout) and writes the same content as a tar-format
1395
+ * bundle via the v0.12.8 `bundleAdapterStorage`. Idempotent: re-
1396
+ * running on an already-migrated bundle is a no-op. Source stays in
1397
+ * place by default; operators with explicit transition windows opt
1398
+ * into the inline replace via `deleteSourceOnSuccess: true`.
1399
+ *
1400
+ * @opts
1401
+ * from: bundleAdapterStorage with format: "directory",
1402
+ * to: bundleAdapterStorage with format: "tar",
1403
+ * bundleId: string (single-bundle migrate; omit to migrate all),
1404
+ * deleteSourceOnSuccess: boolean (default false; explicit opt-in),
1405
+ *
1406
+ * @example
1407
+ * var from = b.backup.bundleAdapterStorage({
1408
+ * adapter: b.backup.bundleAdapterStorage.fsAdapter({ root: "/var/backups-v7" }),
1409
+ * format: "directory",
1410
+ * });
1411
+ * var to = b.backup.bundleAdapterStorage({
1412
+ * adapter: b.backup.bundleAdapterStorage.fsAdapter({ root: "/var/backups-v8" }),
1413
+ * format: "tar",
1414
+ * });
1415
+ * await b.backup.migrate({ from: from, to: to });
1416
+ */
1417
+ async function migrate(opts) {
1418
+ opts = opts || {};
1419
+ if (!opts.from || typeof opts.from.readBundle !== "function" ||
1420
+ typeof opts.from.listBundles !== "function") {
1421
+ throw new BackupError("backup/bad-from",
1422
+ "migrate: opts.from must be a storage backend (got " + typeof opts.from + ")");
1423
+ }
1424
+ if (!opts.to || typeof opts.to.writeBundle !== "function" ||
1425
+ typeof opts.to.hasBundle !== "function") {
1426
+ throw new BackupError("backup/bad-to",
1427
+ "migrate: opts.to must be a storage backend (got " + typeof opts.to + ")");
1428
+ }
1429
+ var ids;
1430
+ if (opts.bundleId) {
1431
+ if (!_isValidBundleId(opts.bundleId)) {
1432
+ throw new BackupError("backup/bad-bundle-id",
1433
+ "migrate: bundleId must match the framework's timestamp+suffix format");
1434
+ }
1435
+ ids = [opts.bundleId];
1436
+ } else {
1437
+ var list = await opts.from.listBundles();
1438
+ ids = list.map(function (b) { return b.bundleId; });
1439
+ }
1440
+ var migrated = 0;
1441
+ var skipped = 0;
1442
+ for (var i = 0; i < ids.length; i += 1) {
1443
+ var bid = ids[i];
1444
+ // Idempotency: skip if destination already has the bundle.
1445
+ if (await opts.to.hasBundle(bid)) {
1446
+ skipped += 1;
1447
+ continue;
1448
+ }
1449
+ // Stage source-bundle into a tmp dir, then write via destination.
1450
+ var tmpDir = nodeFs.mkdtempSync(nodePath.join(os.tmpdir(),
1451
+ "blamejs-backup-migrate-" + bid + "-"));
1452
+ var stageDir = nodePath.join(tmpDir, "bundle");
1453
+ try {
1454
+ await opts.from.readBundle(bid, stageDir);
1455
+ await opts.to.writeBundle(bid, stageDir);
1456
+ migrated += 1;
1457
+ if (opts.deleteSourceOnSuccess === true) {
1458
+ if (typeof opts.from.deleteBundle === "function") {
1459
+ await opts.from.deleteBundle(bid);
1460
+ }
1461
+ }
1462
+ } finally {
1463
+ try { nodeFs.rmSync(tmpDir, { recursive: true, force: true }); }
1464
+ catch (_e) { /* drop-silent */ }
1465
+ }
1466
+ }
1467
+ return { migrated: migrated, skipped: skipped, total: ids.length };
1468
+ }
1469
+
1470
+
@@ -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
+ }