@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 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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blamejs/core",
3
- "version": "0.8.26",
3
+ "version": "0.8.27",
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:7acc2751-65c7-408f-8206-0688a7f5439e",
6
6
  "version": 1,
7
7
  "metadata": {
8
- "timestamp": "2026-05-07T13:46:19.961Z",
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.26",
22
+ "bom-ref": "@blamejs/core@0.8.27",
23
23
  "type": "library",
24
24
  "name": "blamejs",
25
- "version": "0.8.26",
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.26",
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.26",
57
+ "ref": "@blamejs/core@0.8.27",
58
58
  "dependsOn": []
59
59
  }
60
60
  ]