@blamejs/core 0.8.26 → 0.8.27
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/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,7 @@ 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");
|
|
106
107
|
var safeUrl = require("./lib/safe-url");
|
|
107
108
|
var safeRedirect = require("./lib/safe-redirect");
|
|
108
109
|
var pick = require("./lib/pick");
|
|
@@ -293,6 +294,7 @@ module.exports = {
|
|
|
293
294
|
budr: budr,
|
|
294
295
|
secCyber: secCyber,
|
|
295
296
|
iabTcf: iabTcf,
|
|
297
|
+
fapi2: fapi2,
|
|
296
298
|
safeUrl: safeUrl,
|
|
297
299
|
safeRedirect: safeRedirect,
|
|
298
300
|
pick: pick,
|
package/lib/audit.js
CHANGED
|
@@ -241,6 +241,7 @@ 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)
|
|
244
245
|
];
|
|
245
246
|
var registeredNamespaces = new Set(FRAMEWORK_NAMESPACES);
|
|
246
247
|
|
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:7acc2751-65c7-408f-8206-0688a7f5439e",
|
|
6
6
|
"version": 1,
|
|
7
7
|
"metadata": {
|
|
8
|
-
"timestamp": "2026-05-07T13:
|
|
8
|
+
"timestamp": "2026-05-07T13:52:40.926Z",
|
|
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.27",
|
|
23
23
|
"type": "library",
|
|
24
24
|
"name": "blamejs",
|
|
25
|
-
"version": "0.8.
|
|
25
|
+
"version": "0.8.27",
|
|
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.27",
|
|
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.27",
|
|
58
58
|
"dependsOn": []
|
|
59
59
|
}
|
|
60
60
|
]
|