@blamejs/core 0.12.13 → 0.12.14
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-wrap.js +58 -0
- package/lib/archive.js +1 -0
- 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.12.x
|
|
10
10
|
|
|
11
|
+
- 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.
|
|
12
|
+
|
|
11
13
|
- v0.12.13 (2026-05-23) — **`b.backup.bundleAdapterStorage.objectStoreAdapter` — wraps any `b.objectStore` backend (local / SigV4 / GCS / Azure-Blob) into the backup-adapter contract; closes the v0.11.2 "any custom backend" promise.** `b.backup.bundleAdapterStorage.objectStoreAdapter(client, opts)` adapts a `b.objectStore`-shaped client into the `{ writeFile, readFile, listKeys, deleteKey, hasKey }` adapter contract that `bundleAdapterStorage` consumes. Operators wire any of the four shipped object-store backends (`protocol: "local"` / `"sigv4"` for S3+MinIO / `"gcs"` / `"azure-blob"`) through the same recipient / passphrase wrap layers shipped in v0.12.10 and v0.12.11 — the bundle bytes hit the object-store `put` as an opaque envelope. `opts.prefix` namespaces every key under a fixed root inside the bucket so operators sharing a bucket across deployments keep listings scoped. Closes the deferral surfaced in v0.11.2 JSDoc and the v0.12.10 release-notes follow-up list: "S3 or any custom backend" is now wired with no operator-supplied adapter glue. **Added:** *`b.backup.bundleAdapterStorage.objectStoreAdapter(client, opts?)` — object-store-backed bundle storage* — Adapts any `b.objectStore.buildBackend({ protocol })` client (local / sigv4 / gcs / azure-blob) into the `{ writeFile, readFile, listKeys, deleteKey, hasKey }` adapter contract. NOT_FOUND errors from the underlying client translate to `backup/no-key` for the readFile path and to idempotent return-without-throw for deleteKey (matching the fsAdapter contract). hasKey routes through `client.head(key)` — NOT_FOUND → false; any other error propagates so operators can distinguish network failure from missing-key. Composes transparently with v0.12.10 `cryptoStrategy: "recipient"` and v0.12.11 `cryptoStrategy: "passphrase"` — the wrap envelope is the bytes hitting the object-store put. · *`opts.prefix` — per-deployment key namespacing inside the bucket* — Operators sharing one bucket across multiple deployments (per-environment / per-tenant / per-region) pass distinct prefixes so listings stay scoped. The prefix gets a trailing slash inserted automatically; traversal segments (`..`) and NUL bytes refused upfront with `backup/bad-arg` so a misconfigured prefix can't escape the operator's intended scope. listKeys strips the prefix on return so the adapter surface looks identical to the fsAdapter — operators switching backends don't see key-shape drift. **Security:** *Key path validation — traversal + NUL byte refusal at every adapter call* — Every `_scopedKey(key)` invocation refuses keys containing `..` traversal segments or NUL bytes upfront with `backup/bad-key` so a misconfigured bundleId or an attacker-controlled value never reaches the underlying `client.put(...)` / `client.get(...)`. Matches the same defensive posture the fsAdapter carries against operator-supplied key shapes.
|
|
12
14
|
|
|
13
15
|
- v0.12.12 (2026-05-23) — **`b.ai.disclosure.chatbot` + `b.ai.disclosure.deepfake` + `b.ai.disclosure.emotion` — EU AI Act Art. 50 transparency obligations (calendar-locked 2026-08-02) with US-CA AB-853 + China CAC GenAI cross-walk.** EU AI Act Art. 50 transparency primitives land ahead of the 2026-08-02 enforcement deadline. `b.ai.disclosure.chatbot(session, opts)` emits the Art. 50(1) first-contact "you are interacting with an AI system" disclosure with placement control (`first-message` / `always` / `on-request`). `b.ai.disclosure.deepfake(content, { contentType, placement, jurisdiction })` emits the Art. 50(4) synthetic-content label + machine-readable metadata payload for image / audio / video / text. `b.ai.disclosure.emotion({ systemType })` emits the Art. 50(3) emotion-recognition / biometric-categorisation notice. Each primitive emits a tamper-evident `ai-act/*-disclosure-applied` audit event so the compliance trail backs the user-facing notice. Cross-jurisdiction cross-walk lives in `opts.jurisdiction`: `"eu"` (default), `"us-ca"` adds AB-853 §22949.91 to the cross-walk array, `"cn"` adds CAC GenAI Measures Art. 12. The deepfake primitive returns a `schema: "c2pa-v1.4-ready"` metadata field that the v0.12.21 `b.contentCredentials` C2PA adapter will consume when it lands — this patch ships the label markup + schema; the C2PA manifest emission is the next composition. **Added:** *`b.ai.disclosure.chatbot(session, opts)` — Art. 50(1) first-contact disclosure* — Operators interacting with natural persons via an AI system get a primitive that emits the "you are interacting with an AI system" notice + audits the emission. `placement` opts: `"first-message"` (default — emit on first contact only, tracked via `session.aiDisclosureEmitted`), `"always"` (every response), `"on-request"` (operator wires their own trigger). Returns `{ text, language, jurisdiction, placement, shouldEmit, article, regulation }` — `shouldEmit` is the operator-consumable boolean for response-wire-up logic. · *`b.ai.disclosure.deepfake(content, opts)` — Art. 50(4) synthetic-content label* — Operators emitting model-generated or model-manipulated content get a primitive that returns both the visible label markup AND the machine-readable metadata payload. `contentType: "image" | "audio" | "video" | "text"` is required; `placement: "label" | "metadata" | "both"` (default `"both"`) controls what the primitive populates. The metadata payload includes `schema: "c2pa-v1.4-ready"` — the v0.12.21 `b.contentCredentials` C2PA adapter will consume this schema field when it lands. `crossWalk` array carries `["eu-ai-act/Art. 50(4)"]` plus the per-jurisdiction reference (AB-853 §22949.91 / CAC GenAI Art. 12). · *`b.ai.disclosure.emotion(opts)` — Art. 50(3) emotion-recognition / biometric-categorisation notice* — Operators deploying emotion-recognition or biometric-categorisation systems get the consent-flow notice primitive. `systemType: "emotion" | "biometric-categorisation"` (default `"emotion"`) selects which Art. 50(3) sub-obligation applies. Returns the notice payload + emits an `ai-act/emotion-disclosure-applied` audit event. · *Cross-jurisdiction cross-walk: EU + US-CA + China in a single primitive* — The `opts.jurisdiction` opt accepts `"eu"` (default — Regulation (EU) 2024/1689), `"us-ca"` (California AB-853 effective 2026), or `"cn"` (China CAC GenAI Measures). The chatbot + deepfake primitives both honour the cross-walk: the deepfake response's `crossWalk` array carries every jurisdiction-specific legal reference the same emission satisfies, so operators serving multi-region traffic emit one notice + audit one event + reference all applicable regimes. **Security:** *Drop-silent audit emission preserves the disclosure path under audit-bus failure* — If `opts.audit` is supplied but its `safeEmit` throws (network bus down, audit-sign chain malformed), the disclosure primitive still returns the user-facing notice payload. The Art. 50 obligation is the user-facing notice itself; the audit emission is a parallel best-effort chain-of-custody record. Refusing the disclosure to defend the audit chain would fail the wrong direction — the regulatory contract is satisfied by emitting the notice. Matches the framework's `audit.safeEmit` drop-silent contract for hot-path observability sinks. **Migration:** *C2PA manifest emission lands in v0.12.21* — The deepfake primitive's metadata payload includes a `schema: "c2pa-v1.4-ready"` field that the v0.12.21 `b.contentCredentials` adapter will consume. Operators emitting image / audio / video for v0.12.12-0.12.20 get the label markup + structured metadata; the actual C2PA manifest (signed JUMBF assertion chain) is the next composition layer.
|
package/lib/archive-wrap.js
CHANGED
|
@@ -240,6 +240,63 @@ function _isPassphraseMagic(buf) {
|
|
|
240
240
|
buf.slice(0, 5).toString("ascii") === ARCH_PASSPHRASE_MAGIC;
|
|
241
241
|
}
|
|
242
242
|
|
|
243
|
+
/**
|
|
244
|
+
* @primitive b.archive.sniffEnvelope
|
|
245
|
+
* @signature b.archive.sniffEnvelope(bytes)
|
|
246
|
+
* @since 0.12.14
|
|
247
|
+
* @status stable
|
|
248
|
+
* @related b.archive.wrap, b.archive.unwrap, b.archive.wrapWithPassphrase, b.archive.unwrapWithPassphrase
|
|
249
|
+
*
|
|
250
|
+
* Identify the envelope shape carried by a buffer without attempting
|
|
251
|
+
* decryption. Returns one of:
|
|
252
|
+
* - `"recipient"` — `BAWRP` header (v0.12.10 hybrid PQC envelope).
|
|
253
|
+
* Operator routes through `b.archive.unwrap(bytes, { recipient })`.
|
|
254
|
+
* - `"passphrase"` — `BAWPP` header (v0.12.11 Argon2id + XChaCha20
|
|
255
|
+
* envelope). Operator routes through
|
|
256
|
+
* `b.archive.unwrapWithPassphrase(bytes, { passphrase })`.
|
|
257
|
+
* - `"none"` — no archive-wrap envelope magic. The bytes are
|
|
258
|
+
* either raw archive content (gz / tar / zip) or an unrelated
|
|
259
|
+
* payload; operator routes to the appropriate `b.archive.read.*`
|
|
260
|
+
* primitive (or refuses entirely).
|
|
261
|
+
*
|
|
262
|
+
* The sniff is byte 0-4 inspection ONLY — no cryptographic work,
|
|
263
|
+
* no allocation beyond a 5-byte ASCII compare. Safe to call on
|
|
264
|
+
* adversarial input.
|
|
265
|
+
*
|
|
266
|
+
* @example
|
|
267
|
+
* var kind = b.archive.sniffEnvelope(payloadBytes);
|
|
268
|
+
* switch (kind) {
|
|
269
|
+
* case "recipient": return b.archive.unwrap(payloadBytes, { recipient });
|
|
270
|
+
* case "passphrase": return b.archive.unwrapWithPassphrase(payloadBytes, { passphrase });
|
|
271
|
+
* case "none": return payloadBytes;
|
|
272
|
+
* }
|
|
273
|
+
*/
|
|
274
|
+
function sniffEnvelope(bytes) {
|
|
275
|
+
if (!Buffer.isBuffer(bytes) && !(bytes instanceof Uint8Array)) {
|
|
276
|
+
return "none";
|
|
277
|
+
}
|
|
278
|
+
// Codex P2A on v0.12.14 PR #165 — `Buffer.from(uint8Array)` copies
|
|
279
|
+
// the entire input, turning a constant-time 5-byte probe into an
|
|
280
|
+
// O(n) allocation. Use the zero-copy view form so the sniff is
|
|
281
|
+
// truly cheap regardless of input size.
|
|
282
|
+
var buf = Buffer.isBuffer(bytes)
|
|
283
|
+
? bytes
|
|
284
|
+
: Buffer.from(bytes.buffer, bytes.byteOffset, bytes.byteLength);
|
|
285
|
+
if (buf.length < 5) return "none";
|
|
286
|
+
// Codex P2B on v0.12.14 PR #165 — match on the 5-byte ASCII magic
|
|
287
|
+
// alone, NOT on the full header (which requires version + saltLen
|
|
288
|
+
// bytes). A truncated envelope (`BAWRP` + nothing else) is still a
|
|
289
|
+
// recipient envelope; the unwrap call surfaces the truncation with
|
|
290
|
+
// a structured `archive-wrap/truncated-envelope` error. Returning
|
|
291
|
+
// "none" on truncated input would misclassify damaged envelopes as
|
|
292
|
+
// raw payload and the operator's dispatch switch would skip the
|
|
293
|
+
// wrap error path entirely.
|
|
294
|
+
var magic = buf.slice(0, 5).toString("ascii");
|
|
295
|
+
if (magic === ARCH_WRAP_MAGIC) return "recipient";
|
|
296
|
+
if (magic === ARCH_PASSPHRASE_MAGIC) return "passphrase";
|
|
297
|
+
return "none";
|
|
298
|
+
}
|
|
299
|
+
|
|
243
300
|
/**
|
|
244
301
|
* @primitive b.archive.wrapWithPassphrase
|
|
245
302
|
* @signature b.archive.wrapWithPassphrase(bytes, opts)
|
|
@@ -438,6 +495,7 @@ module.exports = {
|
|
|
438
495
|
unwrap: unwrap,
|
|
439
496
|
wrapWithPassphrase: wrapWithPassphrase,
|
|
440
497
|
unwrapWithPassphrase: unwrapWithPassphrase,
|
|
498
|
+
sniffEnvelope: sniffEnvelope,
|
|
441
499
|
ArchiveWrapError: ArchiveWrapError,
|
|
442
500
|
// Exposed for sibling modules + sniffer
|
|
443
501
|
_isWrapMagic: _isWrapMagic,
|
package/lib/archive.js
CHANGED
|
@@ -556,6 +556,7 @@ module.exports = {
|
|
|
556
556
|
unwrap: archiveWrap.unwrap,
|
|
557
557
|
wrapWithPassphrase: archiveWrap.wrapWithPassphrase,
|
|
558
558
|
unwrapWithPassphrase: archiveWrap.unwrapWithPassphrase,
|
|
559
|
+
sniffEnvelope: archiveWrap.sniffEnvelope,
|
|
559
560
|
ArchiveError: ArchiveError,
|
|
560
561
|
TarError: archiveTar.TarError,
|
|
561
562
|
ArchiveGzError: archiveGz.ArchiveGzError,
|
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:e3025640-8314-45f6-9dc8-7e1f45fb1063",
|
|
6
6
|
"version": 1,
|
|
7
7
|
"metadata": {
|
|
8
|
-
"timestamp": "2026-05-24T02:
|
|
8
|
+
"timestamp": "2026-05-24T02:31:34.805Z",
|
|
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.14",
|
|
23
23
|
"type": "application",
|
|
24
24
|
"name": "blamejs",
|
|
25
|
-
"version": "0.12.
|
|
25
|
+
"version": "0.12.14",
|
|
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.14",
|
|
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.14",
|
|
58
58
|
"dependsOn": []
|
|
59
59
|
}
|
|
60
60
|
]
|