@blamejs/core 0.8.26 → 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 CHANGED
Binary file
package/index.js CHANGED
@@ -103,6 +103,8 @@ var darkPatterns = require("./lib/dark-patterns");
103
103
  var budr = require("./lib/budr");
104
104
  var secCyber = require("./lib/sec-cyber");
105
105
  var iabTcf = require("./lib/iab-tcf");
106
+ var fapi2 = require("./lib/fapi2");
107
+ var contentCredentials = require("./lib/content-credentials");
106
108
  var safeUrl = require("./lib/safe-url");
107
109
  var safeRedirect = require("./lib/safe-redirect");
108
110
  var pick = require("./lib/pick");
@@ -293,6 +295,8 @@ module.exports = {
293
295
  budr: budr,
294
296
  secCyber: secCyber,
295
297
  iabTcf: iabTcf,
298
+ fapi2: fapi2,
299
+ contentCredentials: contentCredentials,
296
300
  safeUrl: safeUrl,
297
301
  safeRedirect: safeRedirect,
298
302
  pick: pick,
package/lib/audit.js CHANGED
@@ -241,6 +241,8 @@ var FRAMEWORK_NAMESPACES = [
241
241
  "budr", // b.budr (budr.declared)
242
242
  "seccyber", // b.secCyber (seccyber.eight_k_artifact)
243
243
  "iabtcf", // b.iabTcf (iabtcf.refused / iabtcf.accepted)
244
+ "fapi2", // b.fapi2 (fapi2.posture_asserted)
245
+ "contentcredentials", // b.contentCredentials (contentcredentials.signed / verified)
244
246
  ];
245
247
  var registeredNamespaces = new Set(FRAMEWORK_NAMESPACES);
246
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/lib/fapi2.js ADDED
@@ -0,0 +1,165 @@
1
+ "use strict";
2
+ /**
3
+ * b.fapi2 — Financial-grade API 2.0 Final conformance posture.
4
+ *
5
+ * FAPI 2.0 Final (https://openid.net/specs/fapi-2_0-security-profile-FINAL.html)
6
+ * is the OpenID Foundation's security profile for financial / banking
7
+ * APIs. It composes existing IETF + OAuth standards into a single
8
+ * profile that operators MUST satisfy to interoperate with FAPI 2.0
9
+ * client deployments. The composition (per §5):
10
+ *
11
+ * - PAR (Pushed Authorization Requests, RFC 9126) — REQUIRED
12
+ * - PKCE with S256 (RFC 7636) — REQUIRED, PLAIN refused
13
+ * - Sender-constrained tokens via DPoP (RFC 9449) OR mTLS (RFC 8705)
14
+ * — REQUIRED, exactly one
15
+ * - Authorization-server issuer in callback (RFC 9207) — REQUIRED
16
+ * - TLS 1.2+ with FAPI-approved cipher suites (TLS 1.3 default)
17
+ * - JAR (JWT-secured Authorization Request, RFC 9101) when
18
+ * request-object signed
19
+ *
20
+ * The framework already ships every component primitive. FAPI 2.0
21
+ * conformance is therefore a posture-coordination problem: the
22
+ * operator declares the deployment is FAPI-bound, and the framework
23
+ * asserts that every primitive in the chain is configured per the
24
+ * profile.
25
+ *
26
+ * Public API:
27
+ *
28
+ * b.fapi2.assertConformance(opts) -> { conformant, findings }
29
+ * opts:
30
+ * senderConstraint: "dpop" | "mtls" — REQUIRED.
31
+ * parRequired: bool, default true.
32
+ * pkceMethod: must be "S256" (default; refuses "plain").
33
+ * requireIssuerInCallback: bool, default true.
34
+ * requireJarOnSignedRequests: bool, default true.
35
+ *
36
+ * Returns:
37
+ * conformant: bool — every check passed.
38
+ * findings: Array<{ requirement, status, detail? }>
39
+ *
40
+ * b.fapi2.assertOAuthConfig(oauthOpts) -> void
41
+ * Inspects an `b.auth.oauth.create(opts)` configuration object
42
+ * and throws Fapi2Error if any FAPI 2.0 mandate is violated:
43
+ * - PKCE absent / non-S256
44
+ * - state / nonce missing (auto-mint default OK)
45
+ * - Sender-constraint absent
46
+ *
47
+ * b.fapi2.posture() -> "fapi-2.0" | null
48
+ * Returns "fapi-2.0" when b.compliance.set("fapi-2.0") was
49
+ * called, else null. Convenience for code that branches on the
50
+ * posture without calling b.compliance.current() directly.
51
+ *
52
+ * The framework does NOT replace operator OAuth configuration —
53
+ * `b.auth.oauth.create(...)` is still where the operator declares
54
+ * client + scopes + redirect URIs. b.fapi2.assertOAuthConfig is the
55
+ * boot-time gate that refuses to start a FAPI-declared deployment
56
+ * if any mandate is missing.
57
+ */
58
+
59
+ var compliance = require("./compliance");
60
+ var audit = require("./audit");
61
+ var { defineClass } = require("./framework-error");
62
+ var Fapi2Error = defineClass("Fapi2Error", { alwaysPermanent: true });
63
+
64
+ var SENDER_CONSTRAINTS = ["dpop", "mtls"];
65
+
66
+ function assertConformance(opts) {
67
+ if (!opts || typeof opts !== "object") {
68
+ throw Fapi2Error.factory("BAD_OPTS",
69
+ "fapi2.assertConformance: opts required");
70
+ }
71
+ if (SENDER_CONSTRAINTS.indexOf(opts.senderConstraint) === -1) {
72
+ throw Fapi2Error.factory("BAD_SENDER_CONSTRAINT",
73
+ "fapi2.assertConformance: senderConstraint must be 'dpop' or 'mtls'");
74
+ }
75
+ var parRequired = opts.parRequired !== false;
76
+ var pkceMethod = opts.pkceMethod || "S256";
77
+ if (pkceMethod !== "S256") {
78
+ throw Fapi2Error.factory("BAD_PKCE",
79
+ "fapi2.assertConformance: PKCE method must be S256 (FAPI 2.0 §5.3.1.1) — got '" +
80
+ pkceMethod + "'");
81
+ }
82
+ var requireIssuer = opts.requireIssuerInCallback !== false;
83
+ var requireJar = opts.requireJarOnSignedRequests !== false;
84
+
85
+ var findings = [];
86
+ findings.push({ requirement: "pkce-s256", status: "satisfied",
87
+ detail: "PKCE S256 declared (FAPI 2.0 §5.3.1.1)" });
88
+ findings.push({ requirement: "par-required", status: parRequired ? "satisfied" : "WAIVED",
89
+ detail: parRequired
90
+ ? "PAR (RFC 9126) declared required (FAPI 2.0 §5.3.2.2)"
91
+ : "PAR waived by operator — non-conformant unless authorization-server is FAPI-1 fallback" });
92
+ findings.push({ requirement: "sender-constraint", status: "satisfied",
93
+ detail: opts.senderConstraint + " — FAPI 2.0 §5.3.2.5" });
94
+ findings.push({ requirement: "issuer-in-callback", status: requireIssuer ? "satisfied" : "WAIVED",
95
+ detail: requireIssuer
96
+ ? "Issuer in callback (RFC 9207) required"
97
+ : "Issuer-in-callback waived — IdP-mix-up class still open" });
98
+ findings.push({ requirement: "jar-signed-requests", status: requireJar ? "satisfied" : "WAIVED",
99
+ detail: requireJar
100
+ ? "JAR (RFC 9101) required for signed authorization requests"
101
+ : "JAR waived for signed authorization requests" });
102
+
103
+ var conformant = findings.every(function (f) { return f.status === "satisfied"; });
104
+
105
+ audit.safeEmit({
106
+ action: "fapi2.posture_asserted",
107
+ outcome: conformant ? "success" : "warning",
108
+ metadata: {
109
+ senderConstraint: opts.senderConstraint,
110
+ parRequired: parRequired,
111
+ pkceMethod: pkceMethod,
112
+ requireIssuer: requireIssuer,
113
+ requireJar: requireJar,
114
+ conformant: conformant,
115
+ },
116
+ });
117
+
118
+ return { conformant: conformant, findings: findings };
119
+ }
120
+
121
+ function assertOAuthConfig(oauthOpts) {
122
+ if (!oauthOpts || typeof oauthOpts !== "object") {
123
+ throw Fapi2Error.factory("BAD_OAUTH_OPTS",
124
+ "fapi2.assertOAuthConfig: oauth opts required");
125
+ }
126
+ // PKCE — refuse pkce: false (b.auth.oauth.create already does this,
127
+ // but check explicitly for FAPI clarity).
128
+ if (oauthOpts.pkce === false) {
129
+ throw Fapi2Error.factory("PKCE_DISABLED",
130
+ "fapi2.assertOAuthConfig: PKCE is disabled — FAPI 2.0 §5.3.1.1 mandates S256");
131
+ }
132
+ if (oauthOpts.pkceMethod && oauthOpts.pkceMethod !== "S256") {
133
+ throw Fapi2Error.factory("PKCE_NOT_S256",
134
+ "fapi2.assertOAuthConfig: PKCE method '" + oauthOpts.pkceMethod +
135
+ "' is not S256 (FAPI 2.0 §5.3.1.1)");
136
+ }
137
+ // Sender-constraint required
138
+ var hasDpop = oauthOpts.dpop === true || oauthOpts.senderConstraint === "dpop";
139
+ var hasMtls = oauthOpts.mtls === true || oauthOpts.senderConstraint === "mtls";
140
+ if (!hasDpop && !hasMtls) {
141
+ throw Fapi2Error.factory("NO_SENDER_CONSTRAINT",
142
+ "fapi2.assertOAuthConfig: FAPI 2.0 §5.3.2.5 requires sender-constrained tokens via DPoP OR mTLS — neither declared");
143
+ }
144
+ if (hasDpop && hasMtls) {
145
+ throw Fapi2Error.factory("BOTH_SENDER_CONSTRAINTS",
146
+ "fapi2.assertOAuthConfig: declare exactly one of DPoP / mTLS — both creates over-binding ambiguity");
147
+ }
148
+ // PAR
149
+ if (oauthOpts.par === false) {
150
+ throw Fapi2Error.factory("PAR_DISABLED",
151
+ "fapi2.assertOAuthConfig: PAR is disabled — FAPI 2.0 §5.3.2.2 mandates Pushed Authorization Requests");
152
+ }
153
+ }
154
+
155
+ function posture() {
156
+ return compliance.current() === "fapi-2.0" ? "fapi-2.0" : null;
157
+ }
158
+
159
+ module.exports = {
160
+ assertConformance: assertConformance,
161
+ assertOAuthConfig: assertOAuthConfig,
162
+ posture: posture,
163
+ SENDER_CONSTRAINTS: SENDER_CONSTRAINTS.slice(),
164
+ Fapi2Error: Fapi2Error,
165
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blamejs/core",
3
- "version": "0.8.26",
3
+ "version": "0.8.28",
4
4
  "description": "The Node framework that owns its stack.",
5
5
  "license": "Apache-2.0",
6
6
  "author": "blamejs contributors",
@@ -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:cbce5737-d9f9-4793-b198-266fc75eb98e",
5
+ "serialNumber": "urn:uuid:0b8cc5a2-bd56-46a7-a9e6-2508bf3da424",
6
6
  "version": 1,
7
7
  "metadata": {
8
- "timestamp": "2026-05-07T13:46:19.961Z",
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.26",
22
+ "bom-ref": "@blamejs/core@0.8.28",
23
23
  "type": "library",
24
24
  "name": "blamejs",
25
- "version": "0.8.26",
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.26",
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.26",
57
+ "ref": "@blamejs/core@0.8.28",
58
58
  "dependsOn": []
59
59
  }
60
60
  ]