@blamejs/core 0.12.7 → 0.12.8

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.
@@ -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");
@@ -993,6 +995,7 @@ module.exports = {
993
995
  create: create,
994
996
  diskStorage: diskStorage,
995
997
  bundleAdapterStorage: bundleAdapterStorage,
998
+ migrate: migrate,
996
999
  recommendedFiles: recommendedFiles,
997
1000
  runInWorker: runInWorker,
998
1001
  verifyManifestSignature: verifyManifestSignature,
@@ -1055,6 +1058,35 @@ function bundleAdapterStorage(opts) {
1055
1058
  "bundleAdapterStorage: adapter missing method '" + required[i] + "'");
1056
1059
  }
1057
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
+ }
1058
1090
 
1059
1091
  function _ensureBundleId(bundleId) {
1060
1092
  if (!_isValidBundleId(bundleId)) {
@@ -1078,6 +1110,17 @@ function bundleAdapterStorage(opts) {
1078
1110
  return out;
1079
1111
  }
1080
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
+
1081
1124
  return {
1082
1125
  name: "adapter",
1083
1126
  async writeBundle(bundleId, sourceDir) {
@@ -1086,16 +1129,52 @@ function bundleAdapterStorage(opts) {
1086
1129
  throw new BackupError("backup/no-source",
1087
1130
  "writeBundle: sourceDir does not exist: " + sourceDir);
1088
1131
  }
1089
- var alreadyHas = await adapter.hasKey(bundleId + "/manifest.json");
1132
+ var alreadyHas = await _hasBundleKey(bundleId, format);
1090
1133
  if (alreadyHas) {
1091
1134
  throw new BackupError("backup/bundle-exists",
1092
1135
  "writeBundle: bundle '" + bundleId + "' already exists in storage");
1093
1136
  }
1094
- var relPaths = _walkDirSync(sourceDir, []);
1095
- for (var i = 0; i < relPaths.length; i += 1) {
1096
- var rel = relPaths[i];
1097
- var bytes = nodeFs.readFileSync(nodePath.join(sourceDir, rel));
1098
- await adapter.writeFile(bundleId + "/" + rel, bytes);
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);
1099
1178
  }
1100
1179
  },
1101
1180
  async readBundle(bundleId, destDir) {
@@ -1104,20 +1183,29 @@ function bundleAdapterStorage(opts) {
1104
1183
  throw new BackupError("backup/dest-exists",
1105
1184
  "readBundle: destDir already exists: " + destDir);
1106
1185
  }
1107
- var keys = await adapter.listKeys(bundleId + "/");
1108
- if (keys.length === 0) {
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) {
1109
1191
  throw new BackupError("backup/bundle-not-found",
1110
1192
  "readBundle: '" + bundleId + "' not in storage");
1111
1193
  }
1112
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 + "/");
1113
1203
  for (var i = 0; i < keys.length; i += 1) {
1114
1204
  var key = keys[i];
1115
1205
  var prefix = bundleId + "/";
1116
1206
  if (key.indexOf(prefix) !== 0) continue;
1117
1207
  var rel = key.slice(prefix.length);
1118
- // Path-safety: rel must not escape destDir. Reuse the
1119
- // verifyExtractionPath dual-check primitive when the dest
1120
- // already exists (writeable directory just created).
1208
+ // Path-safety: rel must not escape destDir.
1121
1209
  var destPath = nodePath.join(destDir, rel);
1122
1210
  var resolvedDest = nodePath.resolve(destPath);
1123
1211
  var resolvedRoot = nodePath.resolve(destDir);
@@ -1175,7 +1263,15 @@ function bundleAdapterStorage(opts) {
1175
1263
  },
1176
1264
  async hasBundle(bundleId) {
1177
1265
  _ensureBundleId(bundleId);
1178
- return adapter.hasKey(bundleId + "/manifest.json");
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;
1179
1275
  },
1180
1276
  };
1181
1277
  }
@@ -1244,3 +1340,91 @@ bundleAdapterStorage.fsAdapter = function (fsOpts) {
1244
1340
  },
1245
1341
  };
1246
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
+
@@ -902,6 +902,7 @@ module.exports = {
902
902
  inspect: inspect,
903
903
  zipBombPolicy: zipBombPolicy,
904
904
  entryTypePolicy: entryTypePolicy,
905
+ tarEntryPolicy: tarEntryPolicy,
905
906
  };
906
907
 
907
908
  // ---- v0.12.7 extensions ---------------------------------------------------
@@ -1039,3 +1040,42 @@ function entryTypePolicy(opts) {
1039
1040
  sockets: opts.sockets === true,
1040
1041
  });
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
+ }
@@ -45,6 +45,7 @@ var SafeArchiveError = defineClass("SafeArchiveError", { alwaysPermanent: true }
45
45
 
46
46
  var archiveRead = lazyRequire(function () { return require("./archive-read"); });
47
47
  var archiveAdapters = lazyRequire(function () { return require("./archive-adapters"); });
48
+ var archiveTarRead = lazyRequire(function () { return require("./archive-tar-read"); });
48
49
 
49
50
  // ---- Format sniffing ----------------------------------------------------
50
51
 
@@ -178,18 +179,30 @@ async function extract(opts) {
178
179
  var sniff = await _sniffMagic(source);
179
180
  format = sniff.format;
180
181
  }
181
- if (format !== "zip") {
182
+ var reader;
183
+ if (format === "zip") {
184
+ reader = archiveRead().zip(source, {
185
+ bombPolicy: opts.bombPolicy,
186
+ entryTypePolicy: opts.entryTypePolicy,
187
+ guardProfile: opts.guardProfile,
188
+ audit: opts.audit,
189
+ });
190
+ } else if (format === "tar") {
191
+ reader = archiveTarRead().tar(source, {
192
+ bombPolicy: opts.bombPolicy,
193
+ entryTypePolicy: opts.entryTypePolicy,
194
+ guardProfile: opts.guardProfile,
195
+ audit: opts.audit,
196
+ });
197
+ } else {
182
198
  throw new SafeArchiveError("safe-archive/format-unsupported",
183
- "extract: format=" + JSON.stringify(format) + " — v0.12.7 ships ZIP only " +
184
- "(tar lands v0.12.8, gz lands v0.12.9, encryptPacked-wrap lands v0.12.10)");
199
+ "extract: format=" + JSON.stringify(format) + " — v0.12.8 ships ZIP + tar " +
200
+ "(gz lands v0.12.9, encryptPacked-wrap lands v0.12.10)");
185
201
  }
186
- var reader = archiveRead().zip(source, {
187
- bombPolicy: opts.bombPolicy,
188
- entryTypePolicy: opts.entryTypePolicy,
189
- guardProfile: opts.guardProfile,
190
- audit: opts.audit,
202
+ var result = await reader.extract({
203
+ destination: opts.destination,
204
+ allowDangerous: opts.allowDangerous,
191
205
  });
192
- var result = await reader.extract({ destination: opts.destination });
193
206
  return Object.assign({ format: format }, result);
194
207
  } finally {
195
208
  if (typeof source.close === "function" && typeof opts.source === "string") {
@@ -238,14 +251,21 @@ async function inspect(opts) {
238
251
  var sniff = await _sniffMagic(source);
239
252
  format = sniff.format;
240
253
  }
241
- if (format !== "zip") {
254
+ var reader;
255
+ if (format === "zip") {
256
+ reader = archiveRead().zip(source, {
257
+ bombPolicy: opts.bombPolicy,
258
+ audit: opts.audit,
259
+ });
260
+ } else if (format === "tar") {
261
+ reader = archiveTarRead().tar(source, {
262
+ bombPolicy: opts.bombPolicy,
263
+ audit: opts.audit,
264
+ });
265
+ } else {
242
266
  throw new SafeArchiveError("safe-archive/format-unsupported",
243
- "inspect: format=" + JSON.stringify(format) + " — v0.12.7 ships ZIP only");
267
+ "inspect: format=" + JSON.stringify(format) + " — v0.12.8 ships ZIP + tar");
244
268
  }
245
- var reader = archiveRead().zip(source, {
246
- bombPolicy: opts.bombPolicy,
247
- audit: opts.audit,
248
- });
249
269
  var entries = await reader.inspect();
250
270
  var totalCompressed = 0;
251
271
  var totalUncompressed = 0;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blamejs/core",
3
- "version": "0.12.7",
3
+ "version": "0.12.8",
4
4
  "description": "The Node framework that owns its stack.",
5
5
  "license": "Apache-2.0",
6
6
  "author": "blamejs contributors",
package/sbom.cdx.json CHANGED
@@ -2,10 +2,10 @@
2
2
  "$schema": "http://cyclonedx.org/schema/bom-1.5.schema.json",
3
3
  "bomFormat": "CycloneDX",
4
4
  "specVersion": "1.5",
5
- "serialNumber": "urn:uuid:6ee9d122-fc29-4cdb-a4fb-bd4afab5f35d",
5
+ "serialNumber": "urn:uuid:4e14c3b7-7b58-4756-ba67-d2bcef61b25b",
6
6
  "version": 1,
7
7
  "metadata": {
8
- "timestamp": "2026-05-23T15:14:39.191Z",
8
+ "timestamp": "2026-05-23T16:36:21.469Z",
9
9
  "lifecycles": [
10
10
  {
11
11
  "phase": "build"
@@ -19,14 +19,14 @@
19
19
  }
20
20
  ],
21
21
  "component": {
22
- "bom-ref": "@blamejs/core@0.12.7",
22
+ "bom-ref": "@blamejs/core@0.12.8",
23
23
  "type": "application",
24
24
  "name": "blamejs",
25
- "version": "0.12.7",
25
+ "version": "0.12.8",
26
26
  "scope": "required",
27
27
  "author": "blamejs contributors",
28
28
  "description": "The Node framework that owns its stack.",
29
- "purl": "pkg:npm/%40blamejs/core@0.12.7",
29
+ "purl": "pkg:npm/%40blamejs/core@0.12.8",
30
30
  "properties": [],
31
31
  "externalReferences": [
32
32
  {
@@ -54,7 +54,7 @@
54
54
  "components": [],
55
55
  "dependencies": [
56
56
  {
57
- "ref": "@blamejs/core@0.12.7",
57
+ "ref": "@blamejs/core@0.12.8",
58
58
  "dependsOn": []
59
59
  }
60
60
  ]