@blamejs/core 0.12.23 → 0.12.25
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/ai-disclosure.js +107 -0
- package/lib/backup/index.js +17 -0
- 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.25 (2026-05-24) — **`b.ai.disclosure.applyAll(scenario)` — bundle Art. 50(1) / 50(3) / 50(4) disclosures for mixed-modality AI systems.** Composes the three v0.12.12 disclosure primitives (chatbot / deepfake / emotion) into a single bundled emit. Operators running mixed-modality AI systems (e.g. a chatbot that also generates images, or an emotion-recognition system embedded in a chat flow) declare which Art. 50 obligations apply via `scenario.kinds` and the primitive fans out to the per-obligation emit calls in one pass. Shared opts (jurisdiction, language, audit, correlationId) propagate to every per-kind emission so the cross-walk + audit-chain entries stay correlated across the bundle. **Added:** *`b.ai.disclosure.applyAll(scenario)` — multi-obligation bundled emit* — `scenario.kinds: ["chatbot", "deepfake", "emotion"]` (subset) selects which Art. 50 obligations to satisfy. Per-kind required fields (session for chatbot, content + contentType for deepfake) refused upfront when missing. Returns `{ disclosures: { chatbot?, deepfake?, emotion? } }` with each entry being the corresponding primitive's emission payload. Shared opts propagate: `scenario.jurisdiction` / `scenario.language` / `scenario.audit` / `scenario.correlationId` reach every per-kind call so a US-CA deployment serving chat + image gets both the AB-853 cross-walk AND the Art. 50(1) audit event under the same correlationId.
|
|
12
|
+
|
|
13
|
+
- v0.12.24 (2026-05-24) — **`bundleAdapterStorage.findBundles(predicate, opts?)` — predicate-based filtering over listBundles entries.** Small helper that composes with listBundles for operators wanting to filter the bundle set without hand-rolling the walk. `storage.findBundles(predicate, opts?)` iterates listBundles + returns entries where `predicate(entry)` is truthy. Predicate sees the listBundles entry shape (`{ bundleId, format, createdAt, size }`); `opts.withStats: true` enables `createdAt` + `size` for predicates that need them. Common operator filters — by format (`b => b.format === "tar.gz"`), by age (`b => Date.parse(b.createdAt) < cutoff`), by size — now read as a single call. **Added:** *`storage.findBundles(predicate, opts?)` — predicate-based bundle filter* — Operator-supplied predicate runs against every listBundles entry; matches accumulate into the returned array. `opts.withStats: true` is forwarded to listBundles so predicates relying on `createdAt` / `size` see populated values. Non-function predicate refused upfront with `backup/bad-arg`. Predicate throws bubble up to the caller (operators see their own filter errors, not swallowed). Stable ordering is whatever listBundles produces (reverse-chronological by bundleId).
|
|
14
|
+
|
|
11
15
|
- v0.12.23 (2026-05-24) — **`bundleAdapterStorage.cloneBundle(src, dst, opts?)` — same-storage byte-verbatim bundle clone for pre-rotation snapshots.** `storage.cloneBundle(srcBundleId, dstBundleId, opts?)` copies a bundle's adapter payload (bundle.tar / bundle.tar.gz / every directory key) from src to dst WITHOUT touching the envelope or inner archive. Encrypted bundles are cloned byte-verbatim — the new bundleId carries the same envelope under the same recipient/passphrase. Operators preserving a known-good snapshot before a destructive operation (rewrap, key rotation, schema migration, manual operator-side editing) get a single-call atomic clone instead of a manual readBundle → writeBundle cycle (which would re-encode through the envelope and adapter contracts, breaking byte-identity). **Added:** *`storage.cloneBundle(src, dst, opts?)` — byte-verbatim payload clone* — Reads the source bundle's storage keys + writes them under the destination bundleId without invoking the wrap layer, gunzip path, or tar walker. Encrypted bundles produce byte-identical clones (a tar.gz wrap-recipient envelope cloned via cloneBundle has bit-for-bit equal bytes to the source). Returns `{ srcBundleId, dstBundleId, format, keysCopied, bytesCopied }`. `opts.overwrite` (default false) gates whether to refuse if dstBundleId already exists. Same-id clones refused upfront with `backup/clone-same-id`.
|
|
12
16
|
|
|
13
17
|
- v0.12.22 (2026-05-24) — **`bundleAdapterStorage.rewrapAllBundles(opts)` — bounded-parallel batch envelope rotation with mixed-storage skip semantics.** Batch wrapper over the v0.12.21 rewrapBundle primitive. `storage.rewrapAllBundles(opts?)` iterates `listBundles()` + rotates each bundle's wrap envelope through a bounded-parallel pool (default 4 workers). Plaintext bundles + directory-format bundles get skipped cleanly (recorded as `status: "skipped"` with a `reason` field); rewrap failures get bucketed into `status: "failed"`. Operators completing a key-rotation event across an entire backup repository now have a single call that handles mixed-strategy storage correctly. `opts.newRecipient` / `opts.newPassphrase` / `opts.oldRecipient` / `opts.oldPassphrase` / `opts.concurrency` / `opts.stopOnFirstFailure` mirror the verifyAllBundles + rewrapBundle surface. **Added:** *`storage.rewrapAllBundles(opts?)` — batch envelope rotation* — Iterates listBundles() + dispatches each bundle through rewrapBundle with the operator-supplied new key. Returns `{ total, rotated, skipped, failed, results }` where the per-bundle results carry `{ status: "rotated" | "skipped" | "failed", oldEnvelopeKind, newEnvelopeKind, reason }`. Bounded-parallel fan-out (default 4) keeps the storage backend under control; opts.stopOnFirstFailure short-circuits on the first rotation that throws an unexpected error (skips don't trip the short-circuit — they're expected for mixed-strategy storage). Plaintext + directory bundles skipped with `reason: "format-not-wrappable"` / `reason: "no-envelope"` rather than reported as failures.
|
package/lib/ai-disclosure.js
CHANGED
|
@@ -339,10 +339,117 @@ function _emitAudit(opts, action, outcome, metadata) {
|
|
|
339
339
|
}
|
|
340
340
|
}
|
|
341
341
|
|
|
342
|
+
/**
|
|
343
|
+
* @primitive b.ai.disclosure.applyAll
|
|
344
|
+
* @signature b.ai.disclosure.applyAll(scenario)
|
|
345
|
+
* @since 0.12.25
|
|
346
|
+
* @status stable
|
|
347
|
+
* @compliance eu-ai-act, ca-ab-853, cac-genai-label
|
|
348
|
+
* @related b.ai.disclosure.chatbot, b.ai.disclosure.deepfake, b.ai.disclosure.emotion
|
|
349
|
+
*
|
|
350
|
+
* Bundles multiple Art. 50 disclosures into a single call.
|
|
351
|
+
* Operators with mixed-modality AI systems (e.g. a chatbot that
|
|
352
|
+
* also generates images) declare which obligations apply via
|
|
353
|
+
* `scenario.kinds` and the primitive composes the per-obligation
|
|
354
|
+
* emit calls in one pass. Returns `{ disclosures: { chatbot?,
|
|
355
|
+
* deepfake?, emotion? } }` with each entry being the per-
|
|
356
|
+
* primitive emission payload.
|
|
357
|
+
*
|
|
358
|
+
* @opts
|
|
359
|
+
* scenario.kinds: ["chatbot", "deepfake", "emotion"] // required (subset)
|
|
360
|
+
* scenario.session: object, // required when "chatbot" is included
|
|
361
|
+
* scenario.content: string | Buffer, // required when "deepfake" is included
|
|
362
|
+
* scenario.contentType: string, // required when "deepfake" is included
|
|
363
|
+
* scenario.jurisdiction: string, // forwarded to all
|
|
364
|
+
* scenario.language: string,
|
|
365
|
+
* scenario.audit: object,
|
|
366
|
+
* scenario.correlationId: string,
|
|
367
|
+
*
|
|
368
|
+
* @example
|
|
369
|
+
* var bundle = b.ai.disclosure.applyAll({
|
|
370
|
+
* kinds: ["chatbot", "deepfake"],
|
|
371
|
+
* session: { id: "s1" },
|
|
372
|
+
* content: imageBytes,
|
|
373
|
+
* contentType: "image",
|
|
374
|
+
* jurisdiction: "us-ca",
|
|
375
|
+
* });
|
|
376
|
+
* // bundle.disclosures.chatbot.text → "You are interacting with an AI system."
|
|
377
|
+
* // bundle.disclosures.deepfake.label → "This content has been ..."
|
|
378
|
+
*/
|
|
379
|
+
function applyAll(scenario) {
|
|
380
|
+
if (!scenario || typeof scenario !== "object") {
|
|
381
|
+
throw new AiDisclosureError("ai-disclosure/bad-scenario",
|
|
382
|
+
"applyAll: scenario must be an object with kinds + per-kind required fields");
|
|
383
|
+
}
|
|
384
|
+
if (!Array.isArray(scenario.kinds) || scenario.kinds.length === 0) {
|
|
385
|
+
throw new AiDisclosureError("ai-disclosure/bad-scenario",
|
|
386
|
+
"applyAll: scenario.kinds must be a non-empty array of " +
|
|
387
|
+
"\"chatbot\" / \"deepfake\" / \"emotion\"");
|
|
388
|
+
}
|
|
389
|
+
// Codex P1 on v0.12.25 PR #176 — validate every kind +
|
|
390
|
+
// per-kind required field UP FRONT, before any emission.
|
|
391
|
+
// Previously a later-kind failure (e.g. deepfake missing
|
|
392
|
+
// contentType, unknown trailing kind) ran AFTER earlier kinds
|
|
393
|
+
// had already mutated session.aiDisclosureEmitted + emitted
|
|
394
|
+
// audit events — non-atomic execution suppressed future
|
|
395
|
+
// first-message disclosures even though applyAll threw.
|
|
396
|
+
var SUPPORTED_KINDS = ["chatbot", "deepfake", "emotion"];
|
|
397
|
+
for (var vi = 0; vi < scenario.kinds.length; vi += 1) {
|
|
398
|
+
var vk = scenario.kinds[vi];
|
|
399
|
+
if (SUPPORTED_KINDS.indexOf(vk) === -1) {
|
|
400
|
+
throw new AiDisclosureError("ai-disclosure/bad-scenario",
|
|
401
|
+
"applyAll: unknown kind " + JSON.stringify(vk) +
|
|
402
|
+
" — supported: \"chatbot\" / \"deepfake\" / \"emotion\"");
|
|
403
|
+
}
|
|
404
|
+
if (vk === "chatbot" && !scenario.session) {
|
|
405
|
+
throw new AiDisclosureError("ai-disclosure/bad-scenario",
|
|
406
|
+
"applyAll: scenario.session is required when kinds includes \"chatbot\"");
|
|
407
|
+
}
|
|
408
|
+
if (vk === "deepfake" &&
|
|
409
|
+
(scenario.content === undefined || scenario.content === null)) {
|
|
410
|
+
throw new AiDisclosureError("ai-disclosure/bad-scenario",
|
|
411
|
+
"applyAll: scenario.content is required when kinds includes \"deepfake\"");
|
|
412
|
+
}
|
|
413
|
+
if (vk === "deepfake" &&
|
|
414
|
+
(typeof scenario.contentType !== "string" || scenario.contentType.length === 0)) {
|
|
415
|
+
throw new AiDisclosureError("ai-disclosure/bad-scenario",
|
|
416
|
+
"applyAll: scenario.contentType is required (string) when kinds includes \"deepfake\"");
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
// Shared opts forwarded to each per-kind call.
|
|
420
|
+
var shared = {
|
|
421
|
+
jurisdiction: scenario.jurisdiction,
|
|
422
|
+
language: scenario.language,
|
|
423
|
+
audit: scenario.audit,
|
|
424
|
+
correlationId: scenario.correlationId,
|
|
425
|
+
};
|
|
426
|
+
var out = { disclosures: {} };
|
|
427
|
+
for (var i = 0; i < scenario.kinds.length; i += 1) {
|
|
428
|
+
var kind = scenario.kinds[i];
|
|
429
|
+
if (kind === "chatbot") {
|
|
430
|
+
out.disclosures.chatbot = chatbot(scenario.session, Object.assign({
|
|
431
|
+
placement: scenario.chatbotPlacement,
|
|
432
|
+
requested: scenario.chatbotRequested,
|
|
433
|
+
}, shared));
|
|
434
|
+
} else if (kind === "deepfake") {
|
|
435
|
+
out.disclosures.deepfake = deepfake(scenario.content, Object.assign({
|
|
436
|
+
contentType: scenario.contentType,
|
|
437
|
+
placement: scenario.deepfakePlacement,
|
|
438
|
+
}, shared));
|
|
439
|
+
} else if (kind === "emotion") {
|
|
440
|
+
out.disclosures.emotion = emotion(Object.assign({
|
|
441
|
+
systemType: scenario.emotionSystemType,
|
|
442
|
+
}, shared));
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
return out;
|
|
446
|
+
}
|
|
447
|
+
|
|
342
448
|
module.exports = {
|
|
343
449
|
chatbot: chatbot,
|
|
344
450
|
deepfake: deepfake,
|
|
345
451
|
emotion: emotion,
|
|
452
|
+
applyAll: applyAll,
|
|
346
453
|
AiDisclosureError: AiDisclosureError,
|
|
347
454
|
SUPPORTED_JURISDICTIONS: Object.freeze(SUPPORTED_JURISDICTIONS.slice()),
|
|
348
455
|
DEEPFAKE_CONTENT_TYPES: Object.freeze(DEEPFAKE_CONTENT_TYPES.slice()),
|
package/lib/backup/index.js
CHANGED
|
@@ -1541,6 +1541,23 @@ function bundleAdapterStorage(opts) {
|
|
|
1541
1541
|
// bytesRewritten }`. Refuses cross-kind rotation (recipient ↔
|
|
1542
1542
|
// passphrase) — that's a separate migration the operator
|
|
1543
1543
|
// configures explicitly.
|
|
1544
|
+
// findBundles(predicate, opts?) — v0.12.24 query helper.
|
|
1545
|
+
// Iterates listBundles() + returns every entry where
|
|
1546
|
+
// predicate(entry) is truthy. Predicate sees the listBundles
|
|
1547
|
+
// shape: `{ bundleId, format, createdAt, size }` (size +
|
|
1548
|
+
// createdAt populated when opts.withStats === true).
|
|
1549
|
+
async findBundles(predicate, findOpts) {
|
|
1550
|
+
if (typeof predicate !== "function") {
|
|
1551
|
+
throw new BackupError("backup/bad-arg",
|
|
1552
|
+
"findBundles: predicate must be a function (entry) => boolean");
|
|
1553
|
+
}
|
|
1554
|
+
var list = await this.listBundles(findOpts || {});
|
|
1555
|
+
var out = [];
|
|
1556
|
+
for (var i = 0; i < list.length; i += 1) {
|
|
1557
|
+
if (predicate(list[i])) out.push(list[i]);
|
|
1558
|
+
}
|
|
1559
|
+
return out;
|
|
1560
|
+
},
|
|
1544
1561
|
// cloneBundle(srcBundleId, dstBundleId, opts?) — v0.12.23
|
|
1545
1562
|
// same-storage bundle clone. Copies the bundle's adapter
|
|
1546
1563
|
// payload (bundle.tar / bundle.tar.gz / every directory key)
|
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:aa862c44-8a5c-4f04-a375-88786c3e2443",
|
|
6
6
|
"version": 1,
|
|
7
7
|
"metadata": {
|
|
8
|
-
"timestamp": "2026-05-
|
|
8
|
+
"timestamp": "2026-05-24T10:04:34.547Z",
|
|
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.25",
|
|
23
23
|
"type": "application",
|
|
24
24
|
"name": "blamejs",
|
|
25
|
-
"version": "0.12.
|
|
25
|
+
"version": "0.12.25",
|
|
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.25",
|
|
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.25",
|
|
58
58
|
"dependsOn": []
|
|
59
59
|
}
|
|
60
60
|
]
|