@blamejs/core 0.13.21 → 0.13.22
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 +2 -0
- package/lib/archive-read.js +53 -40
- package/package.json +1 -1
- package/sbom.cdx.json +6 -6
package/CHANGELOG.md
CHANGED
|
@@ -8,6 +8,8 @@ upgrading across more than a few patches at a time.
|
|
|
8
8
|
|
|
9
9
|
## v0.13.x
|
|
10
10
|
|
|
11
|
+
- v0.13.22 (2026-05-27) — **`b.archive.read.zip.fromTrustedStream` reads a ZIP from a Readable — no longer an experimental stub.** fromTrustedStream was an experimental stub whose inspect / entries / extract methods threw, forcing callers to buffer the stream themselves and use the random-access reader. It now works, with the same shape as the tar trusted-stream reader: pass b.archive.adapters.trustedStream(readable) and the bytes are collected into a size-capped buffer (1 GiB hard ceiling) and read through the same bomb-cap, path-traversal, and entry-type decode as the random-access reader — so bombPolicy, guardProfile, entryTypePolicy, and audit all apply, and inspect / entries / extract / extractEntries all return data. This is a bounded-memory reader (the archive is held in memory under the ceiling), not zero-buffer streaming; a future forward-inflate walker shared with the tar reader would lift the ceiling. **Added:** *`b.archive.read.zip.fromTrustedStream` now reads — `inspect` / `entries` / `extract` / `extractEntries`* — The ZIP trusted-stream reader is implemented (was an experimental stub that threw). Pass `b.archive.adapters.trustedStream(readable)` to read a ZIP straight from a Node Readable without buffering it yourself. The stream is collected into a size-capped buffer (1 GiB ceiling, matching `b.archive.read.tar`'s trusted-stream reader) and decoded through the same adversarial-safe path as the random-access reader, so `bombPolicy` / `guardProfile` / `entryTypePolicy` / `audit` are honored on decode. Adversarial archives remain fully bomb-capped; "trusted" refers only to the source-size bound. A non-trusted-stream adapter is refused with `archive-read/bad-adapter`.
|
|
12
|
+
|
|
11
13
|
- v0.13.21 (2026-05-27) — **`b.cose.exportKey` — serialize a public key as a COSE_Key, the inverse of `b.cose.importKey`.** b.cose could import a COSE_Key (RFC 9052 §7) into a node:crypto key for verification, but had no way to produce one — so a key used with b.cose.sign could not be shipped to a verifier in COSE form without hand-building the CBOR map. b.cose.exportKey(keyObject, opts?) closes the round-trip: it serializes an EC2 (P-256 / P-384 / P-521) or OKP (Ed25519) public key as the CBOR-encoded COSE_Key map, with optional alg and kid common parameters. A private key has its public half exported; unsupported curves / key types are refused rather than emitting a COSE_Key no verifier here would accept. The bytes round-trip through b.cose.importKey, and feed the mdoc MSO / COSE_Key header / SCITT / C2PA verification-key paths. **Added:** *`b.cose.exportKey(keyObject, { alg?, kid? })` — KeyObject → COSE_Key (RFC 9052 §7)* — Serialize a `node:crypto` public key as the CBOR-encoded COSE_Key map — the inverse of `b.cose.importKey`. Supports EC2 (P-256 / P-384 / P-521) and OKP (Ed25519), the same key types `b.cose.verify` accepts; `opts.alg` (e.g. `"ES256"`) and `opts.kid` populate the COSE_Key alg (label 3) and kid (label 2) common parameters. A private key exports its public half; unsupported curves / key types throw rather than producing a COSE_Key no verifier would accept. `b.cose.importKey(b.cbor.decode(exportKey(k)))` round-trips, so a key signed with `b.cose.sign` can be shipped to a verifier as bytes — the mdoc MSO / COSE_Key header / SCITT / C2PA verification-key paths.
|
|
12
14
|
|
|
13
15
|
- v0.13.20 (2026-05-27) — **`b.archive.wrap` can seal an archive for a tenant with no key-pair to manage — `recipient: "tenant"`.** b.archive.wrap previously sealed only to an explicit hybrid-PQC key-pair or a peer certificate; the documented recipient: "tenant" strategy threw. It now works: pass { recipient: "tenant", tenantId } and the archive is sealed under a deterministic per-tenant key derived from the vault root (SHAKE256 KDF) with XChaCha20-Poly1305, the tenant id mixed into the AEAD additional-authenticated-data so one tenant's envelope cannot be opened under another tenant's key. There is no recipient key-pair for the operator to generate, store, or rotate — b.archive.unwrap re-derives the key from the same tenantId. Rotating the vault re-keys every tenant (rotation intent is re-seal). The derivation is exposed directly as b.agent.tenant.derivedKey(tenantId, purpose) for operators who need the raw per-tenant key for their own AEAD. Requires an initialized vault. **Added:** *`b.archive.wrap` / `b.archive.unwrap` `recipient: "tenant"` — per-tenant archive sealing, no key-pair* — `b.archive.wrap(bytes, { recipient: "tenant", tenantId })` seals under a deterministic per-tenant key derived from the vault root with XChaCha20-Poly1305 (draft-irtf-cfrg-xchacha-03) and a SHAKE256 KDF (FIPS 202); the tenant id is bound into the AEAD AAD so a tenant-A envelope cannot decrypt under tenant-B's key even if an attacker swaps envelope headers. `b.archive.unwrap(sealed, { recipient: "tenant", tenantId })` (or just `{ tenantId }`) re-derives the key and recovers the bytes — no recipient key-pair to manage. The tenant envelope carries a distinct version byte so it is never fed to the hybrid-KEM decrypt path. The static-key and peer-cert recipient strategies are unchanged. · *`b.agent.tenant.derivedKey(tenantId, purpose)` — direct per-tenant key derivation* — The deterministic, domain-separated per-tenant key derivation (vault root + tenantId + purpose, SHAKE256, NUL-separated) is now exported at the module level, returning a 64-char hex key. Previously reachable only as a method on a created tenant manager; operators who need the raw key for their own AEAD can now call it directly. Throws if the vault is not initialized.
|
package/lib/archive-read.js
CHANGED
|
@@ -51,6 +51,8 @@ var ArchiveReadError = defineClass("ArchiveReadError", { alwaysPermanent: true }
|
|
|
51
51
|
var guardFilename = lazyRequire(function () { return require("./guard-filename"); });
|
|
52
52
|
var guardArchive = lazyRequire(function () { return require("./guard-archive"); });
|
|
53
53
|
var safeDecompress = lazyRequire(function () { return require("./safe-decompress"); });
|
|
54
|
+
var safeBuffer = lazyRequire(function () { return require("./safe-buffer"); });
|
|
55
|
+
var archiveAdapters = lazyRequire(function () { return require("./archive-adapters"); });
|
|
54
56
|
|
|
55
57
|
// ---- Wire-format constants ------------------------------------------------
|
|
56
58
|
// Aligned with the write-side `lib/archive.js`. APPNOTE.TXT § references
|
|
@@ -775,29 +777,31 @@ function zip(adapter, opts) {
|
|
|
775
777
|
* @status experimental
|
|
776
778
|
* @related b.archive.read.zip, b.archive.adapters.trustedStream
|
|
777
779
|
*
|
|
778
|
-
*
|
|
779
|
-
*
|
|
780
|
-
*
|
|
781
|
-
*
|
|
782
|
-
*
|
|
780
|
+
* ZIP reader for a Readable source — pass `b.archive.adapters.trustedStream(readable)`
|
|
781
|
+
* instead of buffering the stream yourself. The bytes are collected
|
|
782
|
+
* into a size-capped buffer (1 GiB hard ceiling, like the tar
|
|
783
|
+
* trusted-stream reader) and then read through the same bomb-cap /
|
|
784
|
+
* path-traversal / entry-policy decode as the random-access reader, so
|
|
785
|
+
* `bombPolicy`, `guardProfile`, `entryTypePolicy`, and `audit` all
|
|
786
|
+
* apply. "Trusted" means the source size is bounded by the operator —
|
|
787
|
+
* the collection ceiling is the only guard against an unbounded
|
|
788
|
+
* producer; adversarial archives are still fully bomb-capped on decode.
|
|
783
789
|
*
|
|
784
|
-
*
|
|
785
|
-
*
|
|
786
|
-
*
|
|
787
|
-
* are accepted but not yet honored. Re-opens when a streaming
|
|
788
|
-
* consumer needs it. Until then, collect the stream into a buffer and
|
|
789
|
-
* use the random-access reader, which is the supported path for both
|
|
790
|
-
* trusted round-trip verification and adversarial input.
|
|
790
|
+
* The collection ceiling means this is not zero-buffer streaming (the
|
|
791
|
+
* whole archive is held in memory, capped); a future bounded-memory
|
|
792
|
+
* forward-inflate walker would lift that, shared with the tar reader.
|
|
791
793
|
*
|
|
792
794
|
* @opts
|
|
793
|
-
* bombPolicy: {
|
|
794
|
-
*
|
|
795
|
+
* bombPolicy: { maxEntries, maxEntryDecompressedBytes,
|
|
796
|
+
* maxTotalDecompressedBytes, maxExpansionRatio },
|
|
797
|
+
* entryTypePolicy: { ... },
|
|
798
|
+
* guardProfile: "strict" | "balanced" | "permissive",
|
|
799
|
+
* audit: b.audit,
|
|
795
800
|
*
|
|
796
801
|
* @example
|
|
797
|
-
*
|
|
798
|
-
* var bytes = await someStreamToBuffer(producedZipStream);
|
|
799
|
-
* var reader = b.archive.read.zip(b.archive.adapters.buffer(bytes));
|
|
802
|
+
* var reader = b.archive.read.zip.fromTrustedStream(b.archive.adapters.trustedStream(readable));
|
|
800
803
|
* var entries = await reader.inspect();
|
|
804
|
+
* void entries;
|
|
801
805
|
*/
|
|
802
806
|
function fromTrustedStream(adapter, opts) {
|
|
803
807
|
if (!adapter || adapter.kind !== "trusted-sequential") {
|
|
@@ -805,36 +809,45 @@ function fromTrustedStream(adapter, opts) {
|
|
|
805
809
|
"fromTrustedStream: adapter must come from b.archive.adapters.trustedStream(readable)");
|
|
806
810
|
}
|
|
807
811
|
opts = opts || {};
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
//
|
|
812
|
-
//
|
|
813
|
-
//
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
812
|
+
|
|
813
|
+
// Collect the Readable into a size-capped buffer once (tar-parity:
|
|
814
|
+
// boundedChunkCollector with a 1 GiB ceiling), then delegate to the
|
|
815
|
+
// random-access reader so the full bomb-cap / guard / audit decode
|
|
816
|
+
// applies. Lazy + memoized — construction stays cheap and the stream
|
|
817
|
+
// is consumed only on the first method call.
|
|
818
|
+
var readerPromise = null;
|
|
819
|
+
function _reader() {
|
|
820
|
+
if (!readerPromise) {
|
|
821
|
+
readerPromise = (async function () {
|
|
822
|
+
var collector = safeBuffer().boundedChunkCollector({
|
|
823
|
+
maxBytes: C.BYTES.gib(1),
|
|
824
|
+
errorClass: ArchiveReadError,
|
|
825
|
+
sizeCode: "archive-read/trusted-stream-too-large",
|
|
826
|
+
});
|
|
827
|
+
for await (var chunk of adapter.readable) { collector.push(chunk); }
|
|
828
|
+
return zip(archiveAdapters().buffer(collector.result()), opts);
|
|
829
|
+
})();
|
|
830
|
+
}
|
|
831
|
+
return readerPromise;
|
|
821
832
|
}
|
|
822
833
|
|
|
834
|
+
async function inspect() { return (await _reader()).inspect(); }
|
|
823
835
|
async function* entries() {
|
|
824
|
-
|
|
825
|
-
|
|
836
|
+
var r = await _reader();
|
|
837
|
+
for await (var e of r.entries()) { yield e; }
|
|
826
838
|
}
|
|
827
|
-
|
|
828
|
-
async function
|
|
829
|
-
|
|
830
|
-
|
|
839
|
+
async function extract(extractOpts) { return (await _reader()).extract(extractOpts); }
|
|
840
|
+
async function* extractEntries(extractOpts) {
|
|
841
|
+
var r = await _reader();
|
|
842
|
+
for await (var e of r.extractEntries(extractOpts)) { yield e; }
|
|
831
843
|
}
|
|
832
844
|
|
|
833
845
|
return {
|
|
834
|
-
kind:
|
|
835
|
-
inspect:
|
|
836
|
-
entries:
|
|
837
|
-
extract:
|
|
846
|
+
kind: "zip-trusted-sequential",
|
|
847
|
+
inspect: inspect,
|
|
848
|
+
entries: entries,
|
|
849
|
+
extract: extract,
|
|
850
|
+
extractEntries: extractEntries,
|
|
838
851
|
};
|
|
839
852
|
}
|
|
840
853
|
|
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:81d23e48-6a96-4d2b-ac47-bada0b15cf1f",
|
|
6
6
|
"version": 1,
|
|
7
7
|
"metadata": {
|
|
8
|
-
"timestamp": "2026-05-
|
|
8
|
+
"timestamp": "2026-05-28T00:00:09.259Z",
|
|
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.13.
|
|
22
|
+
"bom-ref": "@blamejs/core@0.13.22",
|
|
23
23
|
"type": "application",
|
|
24
24
|
"name": "blamejs",
|
|
25
|
-
"version": "0.13.
|
|
25
|
+
"version": "0.13.22",
|
|
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.13.
|
|
29
|
+
"purl": "pkg:npm/%40blamejs/core@0.13.22",
|
|
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.13.
|
|
57
|
+
"ref": "@blamejs/core@0.13.22",
|
|
58
58
|
"dependsOn": []
|
|
59
59
|
}
|
|
60
60
|
]
|