@blamejs/core 0.12.16 → 0.12.18
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/backup/index.js +206 -10
- package/package.json +1 -1
- package/sbom.cdx.json +6 -6
package/CHANGELOG.md
CHANGED
|
@@ -8,6 +8,10 @@ upgrading across more than a few patches at a time.
|
|
|
8
8
|
|
|
9
9
|
## v0.12.x
|
|
10
10
|
|
|
11
|
+
- v0.12.18 (2026-05-24) — **`bundleAdapterStorage.listBundles({ withStats })` + `bundleInfo.createdAt` — opt-in mtime + size from `statKey`.** Two additions on `b.backup.bundleAdapterStorage`. `listBundles({ withStats: true })` fans out `statKey` per bundle and populates `createdAt` (ISO string from mtimeMs) + `size` (bytes) on every entry. Without the opt the default fast path stays a single listKeys call — operators rendering a bundle picker UI choose between O(1) listings and O(N) stat-enriched listings explicitly. `bundleInfo(bundleId)` gains the same `createdAt` field for parity. fsAdapter + objectStoreAdapter both expose `statKey`; legacy adapters without the capability leave the fields null. **Added:** *`storage.listBundles({ withStats: true })` — opt-in per-bundle stat fan-out* — When the adapter exposes `statKey`, populates `createdAt` (ISO string from mtimeMs) + `size` (bytes) per entry. Stat fan-out is O(N) round-trips so the opt is OFF by default — operators wanting cheap one-shot listings stay on `listBundles()`. Format precedence (tar.gz > tar > directory) carries through to which payload key gets stat'd. · *`storage.bundleInfo(bundleId)` returns `createdAt`* — The bundle introspection primitive now returns `{ bundleId, format, envelopeKind, sizeBytes, createdAt }`. `createdAt` is the ISO string derived from `statKey.mtimeMs` (when the adapter exposes it) — null otherwise. Matches the listBundles+withStats shape so operators can use bundleInfo for single-bundle drill-downs without re-mapping field names.
|
|
12
|
+
|
|
13
|
+
- v0.12.17 (2026-05-23) — **`bundleAdapterStorage.bundleInfo` + `listBundles.format` — per-bundle introspection for envelope kind + format without restore.** Two introspection additions on `b.backup.bundleAdapterStorage`. `listBundles()` now returns the inferred `format` (`"tar"` / `"tar.gz"` / `"directory"`) per bundle from the storage key suffix — no byte read. `storage.bundleInfo(bundleId)` returns `{ bundleId, format, envelopeKind, sizeBytes }` where `envelopeKind` is the result of a 5-byte magic probe (`"recipient"` / `"passphrase"` / `"none"`). Operators administering a multi-strategy backup repository can now filter bundles by encryption posture or by format without a full restore cycle. **Added:** *`listBundles()` carries inferred format per bundle* — Each entry now includes `format` alongside `bundleId` / `createdAt` / `size`. Inference is from the storage key suffix the writeBundle path produced — `<bid>/bundle.tar` → tar, `<bid>/bundle.tar.gz` → tar.gz, anything else → directory. Cheap: no byte read, no per-key stat call. Operators rendering a bundle picker UI now sort + filter by format from a single list call. · *`storage.bundleInfo(bundleId)` — per-bundle introspection* — Returns `{ bundleId, format, envelopeKind, sizeBytes }`. `format` from the storage layout (no byte read). `envelopeKind` from `b.archive.sniffEnvelope` over the bundle payload — `"recipient"` (BAWRP / v0.12.10 hybrid PQC), `"passphrase"` (BAWPP / v0.12.11 Argon2id), `"none"` (plaintext or directory format). `sizeBytes` is the payload byte count for tar / tar.gz; null for directory format (operator's per-file walk applies if exact size matters). Nonexistent bundles refused with `backup/bundle-not-found`.
|
|
14
|
+
|
|
11
15
|
- v0.12.16 (2026-05-23) — **`b.safeArchive.inspect` auto-unwraps wrap envelopes (parallel to the v0.12.15 extract path).** Mirrors the v0.12.15 auto-unwrap support into `b.safeArchive.inspect`. Operators enumerating entries of a sealed archive get a single inspect() call regardless of envelope shape — pass `opts.recipient` or `opts.passphrase` alongside `source` and the orchestrator unwraps inline before walking the inner format. Missing-key opt surfaces a structured `safe-archive/no-recipient-for-wrap` / `safe-archive/no-passphrase-for-wrap` refusal upfront. Carries the v0.12.15 P1 + P2 fixes (close original source before replacing + forward opts.signal to inner buffer adapter) into the inspect path so the same descriptor-leak + abort-propagation contracts hold. **Added:** *`b.safeArchive.inspect` auto-unwraps `BAWRP` + `BAWPP` envelopes* — The orchestrator's `format: "auto"` sniffer recognises the wrap magics and routes through `b.archive.unwrap` / `b.archive.unwrapWithPassphrase` inline. After unwrap, the inner bytes are wrapped in a buffer adapter + re-sniffed; the resulting summary carries the INNER `format` (`"tar"` / `"zip"` / etc.) — operators querying `summary.format` see the carrier format, not `"wrap-recipient"`. Entry enumeration walks the inner archive after a single key-derivation pass; no temporary file lands on disk.
|
|
12
16
|
|
|
13
17
|
- v0.12.15 (2026-05-23) — **`b.safeArchive.extract` auto-unwraps v0.12.10 recipient and v0.12.11 passphrase envelopes inline.** The safeArchive orchestrator's `format: "auto"` sniffer recognises `BAWRP` (v0.12.10 recipient) and `BAWPP` (v0.12.11 passphrase) envelope magics and routes through `b.archive.unwrap` / `b.archive.unwrapWithPassphrase` inline before re-sniffing the inner format. Operators pass `opts.recipient` (or `opts.passphrase`) alongside `source` + `destination` and get a single extract() call regardless of envelope shape. Missing the matching key opt surfaces a structured `safe-archive/no-recipient-for-wrap` / `safe-archive/no-passphrase-for-wrap` refusal upfront rather than a downstream crypto error. **Added:** *`b.safeArchive.extract` auto-unwraps wrap envelopes* — The sniffer at byte 0-4 recognises `BAWRP` (returns `format: "wrap-recipient"`) and `BAWPP` (returns `format: "wrap-passphrase"`). The extract path collects the sealed adapter into a Buffer, routes through `b.archive.unwrap` (recipient) or `b.archive.unwrapWithPassphrase` (passphrase), wraps the inner bytes in a buffer adapter, re-sniffs the inner format, and dispatches to the appropriate `b.archive.read.*` reader. A wrap-around-tar.gz envelope round-trips through wrap → unwrap → gunzip → untar with no operator intervention beyond passing the key opt. **Security:** *Missing-key opt refused upfront with structured error* — When the sniffer identifies a wrap envelope but the operator hasn't supplied `opts.recipient` (BAWRP) or `opts.passphrase` (BAWPP), extract refuses with `safe-archive/no-recipient-for-wrap` / `safe-archive/no-passphrase-for-wrap` BEFORE any decryption attempt. Operators wiring extract behind an HTTP boundary get a typed refusal instead of a leaked crypto-level error.
|
package/lib/backup/index.js
CHANGED
|
@@ -1365,8 +1365,23 @@ function bundleAdapterStorage(opts) {
|
|
|
1365
1365
|
nodeFs.writeFileSync(destPath, bytes, { flag: "wx", mode: 0o600 });
|
|
1366
1366
|
}
|
|
1367
1367
|
},
|
|
1368
|
-
async listBundles() {
|
|
1368
|
+
async listBundles(listOpts) {
|
|
1369
1369
|
// Get every key, partition by bundleId prefix, return sorted.
|
|
1370
|
+
listOpts = listOpts || {};
|
|
1371
|
+
var withStats = listOpts.withStats === true;
|
|
1372
|
+
// v0.12.17 — each bundle now carries the inferred format
|
|
1373
|
+
// (tar / tar.gz / directory) so operators picking which
|
|
1374
|
+
// bundle to restore can filter by format without touching
|
|
1375
|
+
// bytes. Format is inferred from the key suffix the
|
|
1376
|
+
// writeBundle path produced (rule §2 — the format is part
|
|
1377
|
+
// of the storage layout, not behind a probe).
|
|
1378
|
+
//
|
|
1379
|
+
// Codex P2 on v0.12.17 PR #168 — track WHICH suffixes a
|
|
1380
|
+
// bundle carries (set of booleans) then apply explicit
|
|
1381
|
+
// precedence at the end: tar.gz > tar > directory. Matches
|
|
1382
|
+
// readBundle's preference (which checks hasTarGz first)
|
|
1383
|
+
// so listBundles' reported format aligns with restore
|
|
1384
|
+
// behavior regardless of adapter.listKeys() order.
|
|
1370
1385
|
var allKeys = await adapter.listKeys("");
|
|
1371
1386
|
var byBundle = new Map();
|
|
1372
1387
|
for (var i = 0; i < allKeys.length; i += 1) {
|
|
@@ -1377,28 +1392,143 @@ function bundleAdapterStorage(opts) {
|
|
|
1377
1392
|
if (!_isValidBundleId(bid)) continue;
|
|
1378
1393
|
var stats = byBundle.get(bid);
|
|
1379
1394
|
if (!stats) {
|
|
1380
|
-
stats = { count: 0,
|
|
1395
|
+
stats = { count: 0, hasTar: false, hasTarGz: false, hasOther: false };
|
|
1381
1396
|
byBundle.set(bid, stats);
|
|
1382
1397
|
}
|
|
1383
1398
|
stats.count += 1;
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1399
|
+
var rest = key.slice(slash + 1);
|
|
1400
|
+
if (rest === "bundle.tar") stats.hasTar = true;
|
|
1401
|
+
else if (rest === "bundle.tar.gz") stats.hasTarGz = true;
|
|
1402
|
+
else stats.hasOther = true;
|
|
1388
1403
|
}
|
|
1389
1404
|
var out = [];
|
|
1390
1405
|
var entries = Array.from(byBundle.entries());
|
|
1391
1406
|
for (var j = 0; j < entries.length; j += 1) {
|
|
1392
1407
|
var bidJ = entries[j][0];
|
|
1393
|
-
|
|
1408
|
+
var statsJ = entries[j][1];
|
|
1409
|
+
var fmtJ;
|
|
1410
|
+
if (statsJ.hasTarGz) fmtJ = "tar.gz"; // matches readBundle precedence
|
|
1411
|
+
else if (statsJ.hasTar) fmtJ = "tar";
|
|
1412
|
+
else fmtJ = "directory";
|
|
1413
|
+
var entry = {
|
|
1394
1414
|
bundleId: bidJ,
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1415
|
+
format: fmtJ,
|
|
1416
|
+
createdAt: null, // adapter may not expose mtime
|
|
1417
|
+
size: null, // best-effort; operators with stat-fast adapters call bundleInfo
|
|
1418
|
+
};
|
|
1419
|
+
// v0.12.18 — when opts.withStats is true AND the adapter
|
|
1420
|
+
// exposes statKey, fan-out a stat call per bundle's
|
|
1421
|
+
// payload key. O(N) round-trips so this is opt-in;
|
|
1422
|
+
// listBundles() with no opts stays cheap (single listKeys
|
|
1423
|
+
// call). Operators wanting per-bundle stats but not the
|
|
1424
|
+
// full bundleInfo envelope probe pick this middle ground.
|
|
1425
|
+
if (withStats && typeof adapter.statKey === "function") {
|
|
1426
|
+
var statKey;
|
|
1427
|
+
if (statsJ.hasTarGz) statKey = bidJ + TAR_GZ_KEY_SUFFIX;
|
|
1428
|
+
else if (statsJ.hasTar) statKey = bidJ + TAR_KEY_SUFFIX;
|
|
1429
|
+
else statKey = bidJ + "/manifest.json";
|
|
1430
|
+
var sk = await adapter.statKey(statKey);
|
|
1431
|
+
if (sk && typeof sk.size === "number") entry.size = sk.size;
|
|
1432
|
+
if (sk && typeof sk.mtimeMs === "number") entry.createdAt = new Date(sk.mtimeMs).toISOString();
|
|
1433
|
+
}
|
|
1434
|
+
out.push(entry);
|
|
1398
1435
|
}
|
|
1399
1436
|
out.sort(function (a, b) { return a.bundleId < b.bundleId ? 1 : -1; });
|
|
1400
1437
|
return out;
|
|
1401
1438
|
},
|
|
1439
|
+
// bundleInfo(bundleId) — v0.12.17 per-bundle introspection.
|
|
1440
|
+
// Returns `{ bundleId, format, envelopeKind, sizeBytes }`.
|
|
1441
|
+
// `format` is one of `"tar"` / `"tar.gz"` / `"directory"`
|
|
1442
|
+
// inferred from the storage layout (no byte read).
|
|
1443
|
+
// `envelopeKind` is the result of a 5-byte magic probe on the
|
|
1444
|
+
// bundle payload — `"recipient"` (BAWRP) / `"passphrase"`
|
|
1445
|
+
// (BAWPP) / `"none"` (plaintext). `sizeBytes` is the payload
|
|
1446
|
+
// byte count for tar / tar.gz; null for directory format
|
|
1447
|
+
// (operator's per-file walk if exact size matters).
|
|
1448
|
+
//
|
|
1449
|
+
// Instance method — wiki page documents this under the
|
|
1450
|
+
// bundleAdapterStorage primitive rather than as a top-level
|
|
1451
|
+
// b.X primitive.
|
|
1452
|
+
async bundleInfo(bundleId) {
|
|
1453
|
+
_ensureBundleId(bundleId);
|
|
1454
|
+
var tarKey = bundleId + TAR_KEY_SUFFIX;
|
|
1455
|
+
var tarGzKey = bundleId + TAR_GZ_KEY_SUFFIX;
|
|
1456
|
+
var manifestKey = bundleId + "/manifest.json";
|
|
1457
|
+
var fmt = null;
|
|
1458
|
+
var payloadKey = null;
|
|
1459
|
+
if (await adapter.hasKey(tarGzKey)) {
|
|
1460
|
+
fmt = "tar.gz"; payloadKey = tarGzKey;
|
|
1461
|
+
} else if (await adapter.hasKey(tarKey)) {
|
|
1462
|
+
fmt = "tar"; payloadKey = tarKey;
|
|
1463
|
+
} else if (await adapter.hasKey(manifestKey)) {
|
|
1464
|
+
fmt = "directory";
|
|
1465
|
+
} else {
|
|
1466
|
+
throw new BackupError("backup/bundle-not-found",
|
|
1467
|
+
"bundleInfo: '" + bundleId + "' not in storage");
|
|
1468
|
+
}
|
|
1469
|
+
var envelopeKind = "none";
|
|
1470
|
+
var sizeBytes = null;
|
|
1471
|
+
var createdAt = null;
|
|
1472
|
+
// Codex P2 on v0.12.18 PR #169 — directory-format bundles
|
|
1473
|
+
// leave payloadKey null but DO have a manifest.json that
|
|
1474
|
+
// statKey can read. For createdAt parity with
|
|
1475
|
+
// listBundles({ withStats }), stat the manifest in the
|
|
1476
|
+
// directory case so the bundleInfo return shape is
|
|
1477
|
+
// populated identically across formats.
|
|
1478
|
+
if (payloadKey === null && fmt === "directory" &&
|
|
1479
|
+
typeof adapter.statKey === "function") {
|
|
1480
|
+
// Stat the manifest.json so directory-format bundles
|
|
1481
|
+
// populate createdAt + sizeBytes identically to how
|
|
1482
|
+
// listBundles({ withStats }) reports them. NOTE: sizeBytes
|
|
1483
|
+
// is the manifest's size here, not the total file-tree
|
|
1484
|
+
// payload (operators wanting the true total walk
|
|
1485
|
+
// per-file keys themselves) — same convention as
|
|
1486
|
+
// listBundles({ withStats }) for parity.
|
|
1487
|
+
var dirSt = await adapter.statKey(manifestKey);
|
|
1488
|
+
if (dirSt && typeof dirSt.size === "number") sizeBytes = dirSt.size;
|
|
1489
|
+
if (dirSt && typeof dirSt.mtimeMs === "number") {
|
|
1490
|
+
createdAt = new Date(dirSt.mtimeMs).toISOString();
|
|
1491
|
+
}
|
|
1492
|
+
}
|
|
1493
|
+
if (payloadKey !== null) {
|
|
1494
|
+
// Codex P1 on v0.12.17 PR #168 — claim was a 5-byte magic
|
|
1495
|
+
// probe; the implementation was reading the entire bundle
|
|
1496
|
+
// into memory. For multi-GB bundles, an administrative
|
|
1497
|
+
// metadata call would allocate the whole payload and put
|
|
1498
|
+
// memory pressure on the host. Prefer the adapter's
|
|
1499
|
+
// optional `readPartial(key, length)` capability for the
|
|
1500
|
+
// probe. fsAdapter + objectStoreAdapter both expose it as
|
|
1501
|
+
// of v0.12.17; legacy adapters without it fall back to a
|
|
1502
|
+
// capped 16-byte readFile via the fallback path (still
|
|
1503
|
+
// bounded; better than full payload).
|
|
1504
|
+
if (typeof adapter.readPartial === "function") {
|
|
1505
|
+
var probe = await adapter.readPartial(payloadKey, 16); // allow:raw-byte-literal — 16-byte probe head, magic comparison
|
|
1506
|
+
envelopeKind = archiveLazy().sniffEnvelope(probe);
|
|
1507
|
+
} else {
|
|
1508
|
+
// Legacy adapter — readPartial missing. Operators using
|
|
1509
|
+
// a custom adapter without the capability get
|
|
1510
|
+
// envelopeKind: "unknown" rather than an OOM risk. They
|
|
1511
|
+
// can probe themselves by reading the first N bytes via
|
|
1512
|
+
// their own client.
|
|
1513
|
+
envelopeKind = "unknown";
|
|
1514
|
+
}
|
|
1515
|
+
// sizeBytes is reported via a stat-like path when the
|
|
1516
|
+
// adapter exposes one; otherwise stays null. fsAdapter +
|
|
1517
|
+
// objectStoreAdapter expose `statKey`.
|
|
1518
|
+
if (typeof adapter.statKey === "function") {
|
|
1519
|
+
var st = await adapter.statKey(payloadKey);
|
|
1520
|
+
if (st && typeof st.size === "number") sizeBytes = st.size;
|
|
1521
|
+
if (st && typeof st.mtimeMs === "number") createdAt = new Date(st.mtimeMs).toISOString();
|
|
1522
|
+
}
|
|
1523
|
+
}
|
|
1524
|
+
return {
|
|
1525
|
+
bundleId: bundleId,
|
|
1526
|
+
format: fmt,
|
|
1527
|
+
envelopeKind: envelopeKind,
|
|
1528
|
+
sizeBytes: sizeBytes,
|
|
1529
|
+
createdAt: createdAt,
|
|
1530
|
+
};
|
|
1531
|
+
},
|
|
1402
1532
|
async deleteBundle(bundleId) {
|
|
1403
1533
|
_ensureBundleId(bundleId);
|
|
1404
1534
|
var keys = await adapter.listKeys(bundleId + "/");
|
|
@@ -1494,6 +1624,42 @@ bundleAdapterStorage.fsAdapter = function (fsOpts) {
|
|
|
1494
1624
|
try { return nodeFs.existsSync(_keyPath(key)); }
|
|
1495
1625
|
catch (_e) { return false; }
|
|
1496
1626
|
},
|
|
1627
|
+
// v0.12.17 — optional capabilities consumed by bundleInfo.
|
|
1628
|
+
// readPartial: open + read up to `length` bytes from the start
|
|
1629
|
+
// of the file without materializing the whole payload.
|
|
1630
|
+
// Bundle-info's envelope probe needs at most 16 bytes — the
|
|
1631
|
+
// partial read keeps multi-GB bundle metadata cheap.
|
|
1632
|
+
async readPartial(key, length) {
|
|
1633
|
+
// CodeQL js/file-system-race + js/insecure-temporary-file —
|
|
1634
|
+
// drop the existsSync probe (TOCTOU) and the default mode on
|
|
1635
|
+
// open. Use openSync with explicit owner-only mode + handle
|
|
1636
|
+
// ENOENT atomically; the system call is itself the existence
|
|
1637
|
+
// check.
|
|
1638
|
+
var p = _keyPath(key);
|
|
1639
|
+
var fd;
|
|
1640
|
+
try {
|
|
1641
|
+
fd = nodeFs.openSync(p, "r", 0o600);
|
|
1642
|
+
} catch (e) {
|
|
1643
|
+
if (e && e.code === "ENOENT") {
|
|
1644
|
+
throw new BackupError("backup/no-key",
|
|
1645
|
+
"fsAdapter.readPartial: key not found: " + JSON.stringify(key));
|
|
1646
|
+
}
|
|
1647
|
+
throw e;
|
|
1648
|
+
}
|
|
1649
|
+
try {
|
|
1650
|
+
var buf = Buffer.alloc(length);
|
|
1651
|
+
var bytesRead = nodeFs.readSync(fd, buf, 0, length, 0);
|
|
1652
|
+
return buf.slice(0, bytesRead);
|
|
1653
|
+
} finally {
|
|
1654
|
+
try { nodeFs.closeSync(fd); } catch (_e) { /* drop-silent */ }
|
|
1655
|
+
}
|
|
1656
|
+
},
|
|
1657
|
+
async statKey(key) {
|
|
1658
|
+
var p = _keyPath(key);
|
|
1659
|
+
if (!nodeFs.existsSync(p)) return null;
|
|
1660
|
+
var st = nodeFs.statSync(p);
|
|
1661
|
+
return { size: st.size, mtimeMs: st.mtimeMs };
|
|
1662
|
+
},
|
|
1497
1663
|
};
|
|
1498
1664
|
};
|
|
1499
1665
|
|
|
@@ -1682,6 +1848,36 @@ bundleAdapterStorage.objectStoreAdapter = function (client, osOpts) {
|
|
|
1682
1848
|
throw e;
|
|
1683
1849
|
}
|
|
1684
1850
|
},
|
|
1851
|
+
// v0.12.17 — readPartial uses the b.objectStore client's range
|
|
1852
|
+
// capability (every shipped backend honours `{ range: [start,
|
|
1853
|
+
// end] }` per the client contract). bundleInfo's envelope probe
|
|
1854
|
+
// reads 16 bytes regardless of bundle size.
|
|
1855
|
+
async readPartial(key, length) {
|
|
1856
|
+
var scoped = _scopedKey(key);
|
|
1857
|
+
try {
|
|
1858
|
+
var body = await client.get(scoped, { range: [0, Math.max(0, length - 1)] });
|
|
1859
|
+
var buf = Buffer.isBuffer(body) ? body : Buffer.from(body);
|
|
1860
|
+
return buf.slice(0, length);
|
|
1861
|
+
} catch (e) {
|
|
1862
|
+
if (e && (e.code === "NOT_FOUND" || /NOT_FOUND|not found/i.test(e.message || ""))) {
|
|
1863
|
+
throw new BackupError("backup/no-key",
|
|
1864
|
+
"objectStoreAdapter.readPartial: key not found: " + JSON.stringify(key));
|
|
1865
|
+
}
|
|
1866
|
+
throw e;
|
|
1867
|
+
}
|
|
1868
|
+
},
|
|
1869
|
+
async statKey(key) {
|
|
1870
|
+
try {
|
|
1871
|
+
var meta = await client.head(_scopedKey(key));
|
|
1872
|
+
if (!meta || typeof meta.size !== "number") return null;
|
|
1873
|
+
return { size: meta.size, mtimeMs: meta.lastModified || null };
|
|
1874
|
+
} catch (e) {
|
|
1875
|
+
if (e && (e.code === "NOT_FOUND" || /NOT_FOUND|not found/i.test(e.message || ""))) {
|
|
1876
|
+
return null;
|
|
1877
|
+
}
|
|
1878
|
+
throw e;
|
|
1879
|
+
}
|
|
1880
|
+
},
|
|
1685
1881
|
};
|
|
1686
1882
|
};
|
|
1687
1883
|
|
package/package.json
CHANGED
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:
|
|
5
|
+
"serialNumber": "urn:uuid:355b5273-a761-4332-ae06-53ebc6f35a42",
|
|
6
6
|
"version": 1,
|
|
7
7
|
"metadata": {
|
|
8
|
-
"timestamp": "2026-05-
|
|
8
|
+
"timestamp": "2026-05-24T05:15:21.238Z",
|
|
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.
|
|
22
|
+
"bom-ref": "@blamejs/core@0.12.18",
|
|
23
23
|
"type": "application",
|
|
24
24
|
"name": "blamejs",
|
|
25
|
-
"version": "0.12.
|
|
25
|
+
"version": "0.12.18",
|
|
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.
|
|
29
|
+
"purl": "pkg:npm/%40blamejs/core@0.12.18",
|
|
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.
|
|
57
|
+
"ref": "@blamejs/core@0.12.18",
|
|
58
58
|
"dependsOn": []
|
|
59
59
|
}
|
|
60
60
|
]
|