@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 +0 -0
- package/index.js +4 -0
- package/lib/audit.js +2 -0
- package/lib/content-credentials.js +232 -0
- package/lib/fapi2.js +165 -0
- package/package.json +1 -1
- package/sbom.cyclonedx.json +6 -6
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
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
|
]
|