@blamejs/core 0.8.27 → 0.8.28
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 +0 -0
- package/index.js +2 -0
- package/lib/audit.js +1 -0
- package/lib/content-credentials.js +232 -0
- package/package.json +1 -1
- package/sbom.cyclonedx.json +6 -6
package/CHANGELOG.md
CHANGED
|
Binary file
|
package/index.js
CHANGED
|
@@ -104,6 +104,7 @@ var budr = require("./lib/budr");
|
|
|
104
104
|
var secCyber = require("./lib/sec-cyber");
|
|
105
105
|
var iabTcf = require("./lib/iab-tcf");
|
|
106
106
|
var fapi2 = require("./lib/fapi2");
|
|
107
|
+
var contentCredentials = require("./lib/content-credentials");
|
|
107
108
|
var safeUrl = require("./lib/safe-url");
|
|
108
109
|
var safeRedirect = require("./lib/safe-redirect");
|
|
109
110
|
var pick = require("./lib/pick");
|
|
@@ -295,6 +296,7 @@ module.exports = {
|
|
|
295
296
|
secCyber: secCyber,
|
|
296
297
|
iabTcf: iabTcf,
|
|
297
298
|
fapi2: fapi2,
|
|
299
|
+
contentCredentials: contentCredentials,
|
|
298
300
|
safeUrl: safeUrl,
|
|
299
301
|
safeRedirect: safeRedirect,
|
|
300
302
|
pick: pick,
|
package/lib/audit.js
CHANGED
|
@@ -242,6 +242,7 @@ var FRAMEWORK_NAMESPACES = [
|
|
|
242
242
|
"seccyber", // b.secCyber (seccyber.eight_k_artifact)
|
|
243
243
|
"iabtcf", // b.iabTcf (iabtcf.refused / iabtcf.accepted)
|
|
244
244
|
"fapi2", // b.fapi2 (fapi2.posture_asserted)
|
|
245
|
+
"contentcredentials", // b.contentCredentials (contentcredentials.signed / verified)
|
|
245
246
|
];
|
|
246
247
|
var registeredNamespaces = new Set(FRAMEWORK_NAMESPACES);
|
|
247
248
|
|
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* b.contentCredentials — California SB-942 / AB-853 + C2PA 2.1
|
|
4
|
+
* content-provenance manifest builder for AI-generated assets.
|
|
5
|
+
*
|
|
6
|
+
* California SB-942 (Cal. Bus. & Prof. Code §22757) + AB-853, both
|
|
7
|
+
* effective 2026-08-02, require providers of generative AI systems
|
|
8
|
+
* to embed a latent (machine-readable) provenance disclosure in
|
|
9
|
+
* every AI-generated image / video / audio asset distributed in
|
|
10
|
+
* California. The disclosure MUST carry:
|
|
11
|
+
*
|
|
12
|
+
* - Provider name
|
|
13
|
+
* - System (model) identifier + version
|
|
14
|
+
* - Content timestamp (when generated)
|
|
15
|
+
* - Unique content ID
|
|
16
|
+
*
|
|
17
|
+
* SB-942 specifically cites C2PA (Coalition for Content Provenance
|
|
18
|
+
* and Authenticity) as an acceptable disclosure format. C2PA 2.1+
|
|
19
|
+
* manifests carry signed assertions with the same fields.
|
|
20
|
+
*
|
|
21
|
+
* The framework can't embed the manifest into image/video/audio
|
|
22
|
+
* bytes directly (that requires format-specific muxers — JPEG XMP /
|
|
23
|
+
* PNG iTXt / MP4 ContentBoxes / etc. that vary per codec). What it
|
|
24
|
+
* CAN do:
|
|
25
|
+
*
|
|
26
|
+
* - Build a C2PA-shaped manifest carrying the required fields.
|
|
27
|
+
* - Sign the manifest with the framework's audit-sign keypair
|
|
28
|
+
* (ML-DSA-87 — or operator-supplied SigStore key).
|
|
29
|
+
* - Emit a tamper-evident audit row recording the disclosure.
|
|
30
|
+
* - Validate inbound manifests presented by upstream content
|
|
31
|
+
* pipelines (the receiver side of the same chain).
|
|
32
|
+
*
|
|
33
|
+
* Operator workflow:
|
|
34
|
+
*
|
|
35
|
+
* var manifest = b.contentCredentials.build({
|
|
36
|
+
* provider: "Acme AI Inc.",
|
|
37
|
+
* system: "acme-image-v3",
|
|
38
|
+
* systemVersion: "3.2.1",
|
|
39
|
+
* contentId: "img-2026-05-08-abc123",
|
|
40
|
+
* contentType: "image/png",
|
|
41
|
+
* contentSha3: hashHex,
|
|
42
|
+
* // operator's display attribution + machine-readable fields
|
|
43
|
+
* });
|
|
44
|
+
* var signed = b.contentCredentials.sign(manifest, { signWith: ... });
|
|
45
|
+
* // operator hands `signed.manifest` to their muxer for embedding
|
|
46
|
+
*
|
|
47
|
+
* Public API:
|
|
48
|
+
*
|
|
49
|
+
* contentCredentials.build(opts) -> manifest (unsigned)
|
|
50
|
+
* contentCredentials.sign(manifest, opts) -> { manifest, signature }
|
|
51
|
+
* contentCredentials.verify(envelope, publicKeyPem) -> { valid, claims }
|
|
52
|
+
* contentCredentials.required(opts) -> array of missing-field errors
|
|
53
|
+
* (returns [] when the operator's input satisfies SB-942 minimums)
|
|
54
|
+
*/
|
|
55
|
+
|
|
56
|
+
var crypto = require("./crypto");
|
|
57
|
+
var canonicalJson = require("./canonical-json");
|
|
58
|
+
var validateOpts = require("./validate-opts");
|
|
59
|
+
var audit = require("./audit");
|
|
60
|
+
var { defineClass } = require("./framework-error");
|
|
61
|
+
var ContentCredentialsError = defineClass("ContentCredentialsError", { alwaysPermanent: true });
|
|
62
|
+
|
|
63
|
+
var STR_LEN_MAX = 256; // allow:raw-byte-literal — string-length cap, not bytes
|
|
64
|
+
var ID_LEN_MAX = 128; // allow:raw-byte-literal — string-length cap, not bytes
|
|
65
|
+
var SEMVER_RE = /^[0-9]+\.[0-9]+(?:\.[0-9]+)?(?:[-+][A-Za-z0-9.-]+)?$/;
|
|
66
|
+
var ID_RE = /^[a-zA-Z0-9._:/-]{1,128}$/;
|
|
67
|
+
var SHA3_HEX_LEN = 128; // allow:raw-byte-literal — SHA3-512 hex length, not bytes
|
|
68
|
+
|
|
69
|
+
// Required fields per SB-942 §22757(a) — every AI-generated asset
|
|
70
|
+
// must disclose provider + system + timestamp + contentId.
|
|
71
|
+
var REQUIRED_FIELDS = ["provider", "system", "systemVersion", "contentId"];
|
|
72
|
+
|
|
73
|
+
function _validateBuildOpts(opts) {
|
|
74
|
+
if (!opts || typeof opts !== "object") {
|
|
75
|
+
throw ContentCredentialsError.factory("BAD_OPTS",
|
|
76
|
+
"contentCredentials.build: opts required");
|
|
77
|
+
}
|
|
78
|
+
for (var i = 0; i < REQUIRED_FIELDS.length; i += 1) {
|
|
79
|
+
var f = REQUIRED_FIELDS[i];
|
|
80
|
+
validateOpts.requireNonEmptyString(opts[f],
|
|
81
|
+
"contentCredentials.build: " + f, ContentCredentialsError, "MISSING_" + f.toUpperCase());
|
|
82
|
+
}
|
|
83
|
+
if (opts.provider.length > STR_LEN_MAX) {
|
|
84
|
+
throw ContentCredentialsError.factory("BAD_PROVIDER",
|
|
85
|
+
"provider exceeds " + STR_LEN_MAX + " chars");
|
|
86
|
+
}
|
|
87
|
+
if (opts.system.length > ID_LEN_MAX || !ID_RE.test(opts.system)) {
|
|
88
|
+
throw ContentCredentialsError.factory("BAD_SYSTEM",
|
|
89
|
+
"system must match " + ID_RE);
|
|
90
|
+
}
|
|
91
|
+
if (opts.systemVersion.length > 64 || !SEMVER_RE.test(opts.systemVersion)) { // allow:raw-byte-literal — semver length cap, not bytes
|
|
92
|
+
throw ContentCredentialsError.factory("BAD_VERSION",
|
|
93
|
+
"systemVersion must be semver");
|
|
94
|
+
}
|
|
95
|
+
if (opts.contentId.length > ID_LEN_MAX || !ID_RE.test(opts.contentId)) {
|
|
96
|
+
throw ContentCredentialsError.factory("BAD_CONTENT_ID",
|
|
97
|
+
"contentId must match " + ID_RE);
|
|
98
|
+
}
|
|
99
|
+
if (opts.contentType !== undefined) {
|
|
100
|
+
if (typeof opts.contentType !== "string" || opts.contentType.length === 0 ||
|
|
101
|
+
opts.contentType.length > ID_LEN_MAX || !/^[a-zA-Z]+\/[A-Za-z0-9._+-]+$/.test(opts.contentType)) {
|
|
102
|
+
throw ContentCredentialsError.factory("BAD_CONTENT_TYPE",
|
|
103
|
+
"contentType must be a valid IANA media type");
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
if (opts.contentSha3 !== undefined) {
|
|
107
|
+
if (typeof opts.contentSha3 !== "string" || opts.contentSha3.length !== SHA3_HEX_LEN ||
|
|
108
|
+
!/^[a-f0-9]+$/i.test(opts.contentSha3)) {
|
|
109
|
+
throw ContentCredentialsError.factory("BAD_CONTENT_HASH",
|
|
110
|
+
"contentSha3 must be lowercase hex SHA3-512 (" + SHA3_HEX_LEN + " chars)");
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function build(opts) {
|
|
116
|
+
_validateBuildOpts(opts);
|
|
117
|
+
var generatedAt = typeof opts.generatedAt === "number" ? opts.generatedAt : Date.now();
|
|
118
|
+
var manifest = {
|
|
119
|
+
"@context": "https://c2pa.org/specifications/specifications/2.1/",
|
|
120
|
+
type: "c2pa.manifest",
|
|
121
|
+
aiGenerated: true,
|
|
122
|
+
provider: {
|
|
123
|
+
name: opts.provider,
|
|
124
|
+
contact: opts.providerContact || null,
|
|
125
|
+
},
|
|
126
|
+
system: {
|
|
127
|
+
id: opts.system,
|
|
128
|
+
version: opts.systemVersion,
|
|
129
|
+
},
|
|
130
|
+
content: {
|
|
131
|
+
id: opts.contentId,
|
|
132
|
+
type: opts.contentType || null,
|
|
133
|
+
sha3_512: opts.contentSha3 || null,
|
|
134
|
+
},
|
|
135
|
+
generatedAt: generatedAt,
|
|
136
|
+
generatedAtIso: new Date(generatedAt).toISOString(),
|
|
137
|
+
citations: ["california-sb-942", "california-ab-853", "c2pa-2.1"],
|
|
138
|
+
// Optional operator-supplied display assertion (SB-942 §22757(b))
|
|
139
|
+
visibleDisclosure: opts.visibleDisclosure || null,
|
|
140
|
+
};
|
|
141
|
+
return Object.freeze(manifest);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function required(opts) {
|
|
145
|
+
var errors = [];
|
|
146
|
+
if (!opts || typeof opts !== "object") return ["opts-required"];
|
|
147
|
+
for (var i = 0; i < REQUIRED_FIELDS.length; i += 1) {
|
|
148
|
+
if (typeof opts[REQUIRED_FIELDS[i]] !== "string" || opts[REQUIRED_FIELDS[i]].length === 0) {
|
|
149
|
+
errors.push("missing-" + REQUIRED_FIELDS[i]);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
return errors;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function sign(manifest, opts) {
|
|
156
|
+
opts = opts || {};
|
|
157
|
+
if (!manifest || typeof manifest !== "object") {
|
|
158
|
+
throw ContentCredentialsError.factory("BAD_MANIFEST",
|
|
159
|
+
"contentCredentials.sign: manifest required");
|
|
160
|
+
}
|
|
161
|
+
validateOpts.requireNonEmptyString(opts.privateKeyPem,
|
|
162
|
+
"contentCredentials.sign: privateKeyPem", ContentCredentialsError, "BAD_KEY");
|
|
163
|
+
var canonical = canonicalJson.stringify(manifest);
|
|
164
|
+
var signature = crypto.sign(Buffer.from(canonical, "utf8"), opts.privateKeyPem);
|
|
165
|
+
var auditOn = opts.audit !== false;
|
|
166
|
+
if (auditOn) {
|
|
167
|
+
audit.safeEmit({
|
|
168
|
+
action: "contentcredentials.signed",
|
|
169
|
+
outcome: "success",
|
|
170
|
+
metadata: {
|
|
171
|
+
provider: manifest.provider && manifest.provider.name,
|
|
172
|
+
system: manifest.system && manifest.system.id,
|
|
173
|
+
contentId: manifest.content && manifest.content.id,
|
|
174
|
+
},
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
return {
|
|
178
|
+
manifest: manifest,
|
|
179
|
+
signature: signature.toString("base64"),
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function verify(envelope, publicKeyPem, opts) {
|
|
184
|
+
opts = opts || {};
|
|
185
|
+
if (!envelope || typeof envelope !== "object" || !envelope.manifest || !envelope.signature) {
|
|
186
|
+
return { valid: false, claims: null, reason: "envelope-shape" };
|
|
187
|
+
}
|
|
188
|
+
if (typeof publicKeyPem !== "string" || publicKeyPem.length === 0) {
|
|
189
|
+
return { valid: false, claims: null, reason: "public-key-required" };
|
|
190
|
+
}
|
|
191
|
+
var canonical = canonicalJson.stringify(envelope.manifest);
|
|
192
|
+
var sigBuf;
|
|
193
|
+
try { sigBuf = Buffer.from(envelope.signature, "base64"); }
|
|
194
|
+
catch (_e) {
|
|
195
|
+
return { valid: false, claims: null, reason: "signature-base64-bad" };
|
|
196
|
+
}
|
|
197
|
+
var ok = crypto.verify(Buffer.from(canonical, "utf8"), sigBuf, publicKeyPem);
|
|
198
|
+
if (!ok) {
|
|
199
|
+
return { valid: false, claims: null, reason: "signature-mismatch" };
|
|
200
|
+
}
|
|
201
|
+
// SB-942 §22757(a) field-presence check on the verified manifest.
|
|
202
|
+
var missing = required({
|
|
203
|
+
provider: envelope.manifest.provider && envelope.manifest.provider.name,
|
|
204
|
+
system: envelope.manifest.system && envelope.manifest.system.id,
|
|
205
|
+
systemVersion: envelope.manifest.system && envelope.manifest.system.version,
|
|
206
|
+
contentId: envelope.manifest.content && envelope.manifest.content.id,
|
|
207
|
+
});
|
|
208
|
+
if (missing.length > 0) {
|
|
209
|
+
return { valid: false, claims: null, reason: "missing-required:" + missing.join(",") };
|
|
210
|
+
}
|
|
211
|
+
if (opts.audit !== false) {
|
|
212
|
+
audit.safeEmit({
|
|
213
|
+
action: "contentcredentials.verified",
|
|
214
|
+
outcome: "success",
|
|
215
|
+
metadata: {
|
|
216
|
+
provider: envelope.manifest.provider.name,
|
|
217
|
+
system: envelope.manifest.system.id,
|
|
218
|
+
contentId: envelope.manifest.content.id,
|
|
219
|
+
},
|
|
220
|
+
});
|
|
221
|
+
}
|
|
222
|
+
return { valid: true, claims: envelope.manifest, reason: null };
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
module.exports = {
|
|
226
|
+
build: build,
|
|
227
|
+
sign: sign,
|
|
228
|
+
verify: verify,
|
|
229
|
+
required: required,
|
|
230
|
+
REQUIRED_FIELDS: REQUIRED_FIELDS.slice(),
|
|
231
|
+
ContentCredentialsError: ContentCredentialsError,
|
|
232
|
+
};
|
package/package.json
CHANGED
package/sbom.cyclonedx.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:0b8cc5a2-bd56-46a7-a9e6-2508bf3da424",
|
|
6
6
|
"version": 1,
|
|
7
7
|
"metadata": {
|
|
8
|
-
"timestamp": "2026-05-
|
|
8
|
+
"timestamp": "2026-05-07T14:00:16.533Z",
|
|
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.8.
|
|
22
|
+
"bom-ref": "@blamejs/core@0.8.28",
|
|
23
23
|
"type": "library",
|
|
24
24
|
"name": "blamejs",
|
|
25
|
-
"version": "0.8.
|
|
25
|
+
"version": "0.8.28",
|
|
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.8.
|
|
29
|
+
"purl": "pkg:npm/%40blamejs/core@0.8.28",
|
|
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.8.
|
|
57
|
+
"ref": "@blamejs/core@0.8.28",
|
|
58
58
|
"dependsOn": []
|
|
59
59
|
}
|
|
60
60
|
]
|