@blamejs/core 0.12.15 → 0.12.17
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 +161 -7
- package/lib/safe-archive.js +33 -1
- 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.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`.
|
|
12
|
+
|
|
13
|
+
- 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.
|
|
14
|
+
|
|
11
15
|
- 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.
|
|
12
16
|
|
|
13
17
|
- v0.12.14 (2026-05-23) — **`b.archive.sniffEnvelope(bytes)` — identify recipient vs passphrase vs raw payload without attempting decryption.** Small helper closing a gap in the archive-wrap surface. `b.archive.sniffEnvelope(bytes)` reads the first 5 bytes of a buffer and returns one of `"recipient"` (v0.12.10 BAWRP envelope), `"passphrase"` (v0.12.11 BAWPP envelope), or `"none"` (raw payload or unrelated bytes). The sniff does NO cryptographic work — no Argon2id round, no decapsulation, no allocation beyond a 5-byte ASCII compare — so it's safe to call on adversarial input. Operators dispatching between unwrap paths get a clean predicate instead of trial-decrypting under multiple key candidates. **Added:** *`b.archive.sniffEnvelope(bytes)` — magic-byte envelope identifier* — Returns `"recipient"` (BAWRP / v0.12.10 hybrid PQC envelope), `"passphrase"` (BAWPP / v0.12.11 Argon2id + XChaCha20 envelope), or `"none"` (raw archive bytes / unrelated payload). Accepts Buffer + Uint8Array; non-buffer / null / undefined / empty-buffer inputs return `"none"` upfront. Operators wire the result into a switch that dispatches to the matching unwrap primitive — no trial decryption, no per-key candidate attempts.
|
package/lib/backup/index.js
CHANGED
|
@@ -1367,6 +1367,19 @@ function bundleAdapterStorage(opts) {
|
|
|
1367
1367
|
},
|
|
1368
1368
|
async listBundles() {
|
|
1369
1369
|
// Get every key, partition by bundleId prefix, return sorted.
|
|
1370
|
+
// v0.12.17 — each bundle now carries the inferred format
|
|
1371
|
+
// (tar / tar.gz / directory) so operators picking which
|
|
1372
|
+
// bundle to restore can filter by format without touching
|
|
1373
|
+
// bytes. Format is inferred from the key suffix the
|
|
1374
|
+
// writeBundle path produced (rule §2 — the format is part
|
|
1375
|
+
// of the storage layout, not behind a probe).
|
|
1376
|
+
//
|
|
1377
|
+
// Codex P2 on v0.12.17 PR #168 — track WHICH suffixes a
|
|
1378
|
+
// bundle carries (set of booleans) then apply explicit
|
|
1379
|
+
// precedence at the end: tar.gz > tar > directory. Matches
|
|
1380
|
+
// readBundle's preference (which checks hasTarGz first)
|
|
1381
|
+
// so listBundles' reported format aligns with restore
|
|
1382
|
+
// behavior regardless of adapter.listKeys() order.
|
|
1370
1383
|
var allKeys = await adapter.listKeys("");
|
|
1371
1384
|
var byBundle = new Map();
|
|
1372
1385
|
for (var i = 0; i < allKeys.length; i += 1) {
|
|
@@ -1377,28 +1390,103 @@ function bundleAdapterStorage(opts) {
|
|
|
1377
1390
|
if (!_isValidBundleId(bid)) continue;
|
|
1378
1391
|
var stats = byBundle.get(bid);
|
|
1379
1392
|
if (!stats) {
|
|
1380
|
-
stats = { count: 0,
|
|
1393
|
+
stats = { count: 0, hasTar: false, hasTarGz: false, hasOther: false };
|
|
1381
1394
|
byBundle.set(bid, stats);
|
|
1382
1395
|
}
|
|
1383
1396
|
stats.count += 1;
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1397
|
+
var rest = key.slice(slash + 1);
|
|
1398
|
+
if (rest === "bundle.tar") stats.hasTar = true;
|
|
1399
|
+
else if (rest === "bundle.tar.gz") stats.hasTarGz = true;
|
|
1400
|
+
else stats.hasOther = true;
|
|
1388
1401
|
}
|
|
1389
1402
|
var out = [];
|
|
1390
1403
|
var entries = Array.from(byBundle.entries());
|
|
1391
1404
|
for (var j = 0; j < entries.length; j += 1) {
|
|
1392
1405
|
var bidJ = entries[j][0];
|
|
1406
|
+
var statsJ = entries[j][1];
|
|
1407
|
+
var fmtJ;
|
|
1408
|
+
if (statsJ.hasTarGz) fmtJ = "tar.gz"; // matches readBundle precedence
|
|
1409
|
+
else if (statsJ.hasTar) fmtJ = "tar";
|
|
1410
|
+
else fmtJ = "directory";
|
|
1393
1411
|
out.push({
|
|
1394
1412
|
bundleId: bidJ,
|
|
1395
|
-
|
|
1396
|
-
|
|
1413
|
+
format: fmtJ,
|
|
1414
|
+
createdAt: null, // adapter may not expose mtime
|
|
1415
|
+
size: null, // best-effort; operators with stat-fast adapters call bundleInfo
|
|
1397
1416
|
});
|
|
1398
1417
|
}
|
|
1399
1418
|
out.sort(function (a, b) { return a.bundleId < b.bundleId ? 1 : -1; });
|
|
1400
1419
|
return out;
|
|
1401
1420
|
},
|
|
1421
|
+
// bundleInfo(bundleId) — v0.12.17 per-bundle introspection.
|
|
1422
|
+
// Returns `{ bundleId, format, envelopeKind, sizeBytes }`.
|
|
1423
|
+
// `format` is one of `"tar"` / `"tar.gz"` / `"directory"`
|
|
1424
|
+
// inferred from the storage layout (no byte read).
|
|
1425
|
+
// `envelopeKind` is the result of a 5-byte magic probe on the
|
|
1426
|
+
// bundle payload — `"recipient"` (BAWRP) / `"passphrase"`
|
|
1427
|
+
// (BAWPP) / `"none"` (plaintext). `sizeBytes` is the payload
|
|
1428
|
+
// byte count for tar / tar.gz; null for directory format
|
|
1429
|
+
// (operator's per-file walk if exact size matters).
|
|
1430
|
+
//
|
|
1431
|
+
// Instance method — wiki page documents this under the
|
|
1432
|
+
// bundleAdapterStorage primitive rather than as a top-level
|
|
1433
|
+
// b.X primitive.
|
|
1434
|
+
async bundleInfo(bundleId) {
|
|
1435
|
+
_ensureBundleId(bundleId);
|
|
1436
|
+
var tarKey = bundleId + TAR_KEY_SUFFIX;
|
|
1437
|
+
var tarGzKey = bundleId + TAR_GZ_KEY_SUFFIX;
|
|
1438
|
+
var manifestKey = bundleId + "/manifest.json";
|
|
1439
|
+
var fmt = null;
|
|
1440
|
+
var payloadKey = null;
|
|
1441
|
+
if (await adapter.hasKey(tarGzKey)) {
|
|
1442
|
+
fmt = "tar.gz"; payloadKey = tarGzKey;
|
|
1443
|
+
} else if (await adapter.hasKey(tarKey)) {
|
|
1444
|
+
fmt = "tar"; payloadKey = tarKey;
|
|
1445
|
+
} else if (await adapter.hasKey(manifestKey)) {
|
|
1446
|
+
fmt = "directory";
|
|
1447
|
+
} else {
|
|
1448
|
+
throw new BackupError("backup/bundle-not-found",
|
|
1449
|
+
"bundleInfo: '" + bundleId + "' not in storage");
|
|
1450
|
+
}
|
|
1451
|
+
var envelopeKind = "none";
|
|
1452
|
+
var sizeBytes = null;
|
|
1453
|
+
if (payloadKey !== null) {
|
|
1454
|
+
// Codex P1 on v0.12.17 PR #168 — claim was a 5-byte magic
|
|
1455
|
+
// probe; the implementation was reading the entire bundle
|
|
1456
|
+
// into memory. For multi-GB bundles, an administrative
|
|
1457
|
+
// metadata call would allocate the whole payload and put
|
|
1458
|
+
// memory pressure on the host. Prefer the adapter's
|
|
1459
|
+
// optional `readPartial(key, length)` capability for the
|
|
1460
|
+
// probe. fsAdapter + objectStoreAdapter both expose it as
|
|
1461
|
+
// of v0.12.17; legacy adapters without it fall back to a
|
|
1462
|
+
// capped 16-byte readFile via the fallback path (still
|
|
1463
|
+
// bounded; better than full payload).
|
|
1464
|
+
if (typeof adapter.readPartial === "function") {
|
|
1465
|
+
var probe = await adapter.readPartial(payloadKey, 16); // allow:raw-byte-literal — 16-byte probe head, magic comparison
|
|
1466
|
+
envelopeKind = archiveLazy().sniffEnvelope(probe);
|
|
1467
|
+
} else {
|
|
1468
|
+
// Legacy adapter — readPartial missing. Operators using
|
|
1469
|
+
// a custom adapter without the capability get
|
|
1470
|
+
// envelopeKind: "unknown" rather than an OOM risk. They
|
|
1471
|
+
// can probe themselves by reading the first N bytes via
|
|
1472
|
+
// their own client.
|
|
1473
|
+
envelopeKind = "unknown";
|
|
1474
|
+
}
|
|
1475
|
+
// sizeBytes is reported via a stat-like path when the
|
|
1476
|
+
// adapter exposes one; otherwise stays null. fsAdapter +
|
|
1477
|
+
// objectStoreAdapter expose `statKey`.
|
|
1478
|
+
if (typeof adapter.statKey === "function") {
|
|
1479
|
+
var st = await adapter.statKey(payloadKey);
|
|
1480
|
+
if (st && typeof st.size === "number") sizeBytes = st.size;
|
|
1481
|
+
}
|
|
1482
|
+
}
|
|
1483
|
+
return {
|
|
1484
|
+
bundleId: bundleId,
|
|
1485
|
+
format: fmt,
|
|
1486
|
+
envelopeKind: envelopeKind,
|
|
1487
|
+
sizeBytes: sizeBytes,
|
|
1488
|
+
};
|
|
1489
|
+
},
|
|
1402
1490
|
async deleteBundle(bundleId) {
|
|
1403
1491
|
_ensureBundleId(bundleId);
|
|
1404
1492
|
var keys = await adapter.listKeys(bundleId + "/");
|
|
@@ -1494,6 +1582,42 @@ bundleAdapterStorage.fsAdapter = function (fsOpts) {
|
|
|
1494
1582
|
try { return nodeFs.existsSync(_keyPath(key)); }
|
|
1495
1583
|
catch (_e) { return false; }
|
|
1496
1584
|
},
|
|
1585
|
+
// v0.12.17 — optional capabilities consumed by bundleInfo.
|
|
1586
|
+
// readPartial: open + read up to `length` bytes from the start
|
|
1587
|
+
// of the file without materializing the whole payload.
|
|
1588
|
+
// Bundle-info's envelope probe needs at most 16 bytes — the
|
|
1589
|
+
// partial read keeps multi-GB bundle metadata cheap.
|
|
1590
|
+
async readPartial(key, length) {
|
|
1591
|
+
// CodeQL js/file-system-race + js/insecure-temporary-file —
|
|
1592
|
+
// drop the existsSync probe (TOCTOU) and the default mode on
|
|
1593
|
+
// open. Use openSync with explicit owner-only mode + handle
|
|
1594
|
+
// ENOENT atomically; the system call is itself the existence
|
|
1595
|
+
// check.
|
|
1596
|
+
var p = _keyPath(key);
|
|
1597
|
+
var fd;
|
|
1598
|
+
try {
|
|
1599
|
+
fd = nodeFs.openSync(p, "r", 0o600);
|
|
1600
|
+
} catch (e) {
|
|
1601
|
+
if (e && e.code === "ENOENT") {
|
|
1602
|
+
throw new BackupError("backup/no-key",
|
|
1603
|
+
"fsAdapter.readPartial: key not found: " + JSON.stringify(key));
|
|
1604
|
+
}
|
|
1605
|
+
throw e;
|
|
1606
|
+
}
|
|
1607
|
+
try {
|
|
1608
|
+
var buf = Buffer.alloc(length);
|
|
1609
|
+
var bytesRead = nodeFs.readSync(fd, buf, 0, length, 0);
|
|
1610
|
+
return buf.slice(0, bytesRead);
|
|
1611
|
+
} finally {
|
|
1612
|
+
try { nodeFs.closeSync(fd); } catch (_e) { /* drop-silent */ }
|
|
1613
|
+
}
|
|
1614
|
+
},
|
|
1615
|
+
async statKey(key) {
|
|
1616
|
+
var p = _keyPath(key);
|
|
1617
|
+
if (!nodeFs.existsSync(p)) return null;
|
|
1618
|
+
var st = nodeFs.statSync(p);
|
|
1619
|
+
return { size: st.size, mtimeMs: st.mtimeMs };
|
|
1620
|
+
},
|
|
1497
1621
|
};
|
|
1498
1622
|
};
|
|
1499
1623
|
|
|
@@ -1682,6 +1806,36 @@ bundleAdapterStorage.objectStoreAdapter = function (client, osOpts) {
|
|
|
1682
1806
|
throw e;
|
|
1683
1807
|
}
|
|
1684
1808
|
},
|
|
1809
|
+
// v0.12.17 — readPartial uses the b.objectStore client's range
|
|
1810
|
+
// capability (every shipped backend honours `{ range: [start,
|
|
1811
|
+
// end] }` per the client contract). bundleInfo's envelope probe
|
|
1812
|
+
// reads 16 bytes regardless of bundle size.
|
|
1813
|
+
async readPartial(key, length) {
|
|
1814
|
+
var scoped = _scopedKey(key);
|
|
1815
|
+
try {
|
|
1816
|
+
var body = await client.get(scoped, { range: [0, Math.max(0, length - 1)] });
|
|
1817
|
+
var buf = Buffer.isBuffer(body) ? body : Buffer.from(body);
|
|
1818
|
+
return buf.slice(0, length);
|
|
1819
|
+
} catch (e) {
|
|
1820
|
+
if (e && (e.code === "NOT_FOUND" || /NOT_FOUND|not found/i.test(e.message || ""))) {
|
|
1821
|
+
throw new BackupError("backup/no-key",
|
|
1822
|
+
"objectStoreAdapter.readPartial: key not found: " + JSON.stringify(key));
|
|
1823
|
+
}
|
|
1824
|
+
throw e;
|
|
1825
|
+
}
|
|
1826
|
+
},
|
|
1827
|
+
async statKey(key) {
|
|
1828
|
+
try {
|
|
1829
|
+
var meta = await client.head(_scopedKey(key));
|
|
1830
|
+
if (!meta || typeof meta.size !== "number") return null;
|
|
1831
|
+
return { size: meta.size, mtimeMs: meta.lastModified || null };
|
|
1832
|
+
} catch (e) {
|
|
1833
|
+
if (e && (e.code === "NOT_FOUND" || /NOT_FOUND|not found/i.test(e.message || ""))) {
|
|
1834
|
+
return null;
|
|
1835
|
+
}
|
|
1836
|
+
throw e;
|
|
1837
|
+
}
|
|
1838
|
+
},
|
|
1685
1839
|
};
|
|
1686
1840
|
};
|
|
1687
1841
|
|
package/lib/safe-archive.js
CHANGED
|
@@ -330,6 +330,38 @@ async function inspect(opts) {
|
|
|
330
330
|
var sniff = await _sniffMagic(source);
|
|
331
331
|
format = sniff.format;
|
|
332
332
|
}
|
|
333
|
+
// v0.12.16 — auto-unwrap path for inspect, parallel to the
|
|
334
|
+
// v0.12.15 extract path. Wrap envelopes (BAWRP / BAWPP) are
|
|
335
|
+
// unwrapped inline + re-sniffed so operators can enumerate
|
|
336
|
+
// entries of a sealed archive in a single inspect() call.
|
|
337
|
+
if (format === "wrap-recipient" || format === "wrap-passphrase") {
|
|
338
|
+
var sealedBytes = await _collectSourceBytes(source);
|
|
339
|
+
var inner;
|
|
340
|
+
if (format === "wrap-recipient") {
|
|
341
|
+
if (!opts.recipient) {
|
|
342
|
+
throw new SafeArchiveError("safe-archive/no-recipient-for-wrap",
|
|
343
|
+
"inspect: source is a wrap-recipient envelope (BAWRP) but opts.recipient was not supplied. " +
|
|
344
|
+
"Pass `{ recipient: { privateKey, ecPrivateKey } }` (or peer-cert form) to unwrap inline.");
|
|
345
|
+
}
|
|
346
|
+
inner = archiveWrap().unwrap(sealedBytes, { recipient: opts.recipient });
|
|
347
|
+
} else {
|
|
348
|
+
if (typeof opts.passphrase !== "string" && !Buffer.isBuffer(opts.passphrase)) {
|
|
349
|
+
throw new SafeArchiveError("safe-archive/no-passphrase-for-wrap",
|
|
350
|
+
"inspect: source is a wrap-passphrase envelope (BAWPP) but opts.passphrase was not supplied. " +
|
|
351
|
+
"Pass `{ passphrase: <string|Buffer> }` to unwrap inline.");
|
|
352
|
+
}
|
|
353
|
+
inner = await archiveWrap().unwrapWithPassphrase(sealedBytes, { passphrase: opts.passphrase });
|
|
354
|
+
}
|
|
355
|
+
// v0.12.15 P1 — close the original fs adapter (if string-
|
|
356
|
+
// backed) BEFORE replacing the source reference. v0.12.15 P2
|
|
357
|
+
// — forward opts.signal to the inner buffer adapter.
|
|
358
|
+
if (typeof source.close === "function" && typeof opts.source === "string") {
|
|
359
|
+
try { source.close(); } catch (_e) { /* drop-silent */ }
|
|
360
|
+
}
|
|
361
|
+
source = archiveAdapters().buffer(inner, { signal: opts.signal });
|
|
362
|
+
var innerSniff = await _sniffMagic(source);
|
|
363
|
+
format = innerSniff.format;
|
|
364
|
+
}
|
|
333
365
|
var reader;
|
|
334
366
|
if (format === "zip") {
|
|
335
367
|
reader = archiveRead().zip(source, {
|
|
@@ -343,7 +375,7 @@ async function inspect(opts) {
|
|
|
343
375
|
});
|
|
344
376
|
} else {
|
|
345
377
|
throw new SafeArchiveError("safe-archive/format-unsupported",
|
|
346
|
-
"inspect: format=" + JSON.stringify(format) + " — v0.12.8 ships ZIP + tar");
|
|
378
|
+
"inspect: format=" + JSON.stringify(format) + " — v0.12.8 ships ZIP + tar; v0.12.16 auto-unwraps wrap envelopes");
|
|
347
379
|
}
|
|
348
380
|
var entries = await reader.inspect();
|
|
349
381
|
var totalCompressed = 0;
|
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:5b052d03-601b-4eb1-93d8-60aaaf81b2f7",
|
|
6
6
|
"version": 1,
|
|
7
7
|
"metadata": {
|
|
8
|
-
"timestamp": "2026-05-
|
|
8
|
+
"timestamp": "2026-05-24T04:43:58.102Z",
|
|
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.17",
|
|
23
23
|
"type": "application",
|
|
24
24
|
"name": "blamejs",
|
|
25
|
-
"version": "0.12.
|
|
25
|
+
"version": "0.12.17",
|
|
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.17",
|
|
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.17",
|
|
58
58
|
"dependsOn": []
|
|
59
59
|
}
|
|
60
60
|
]
|