@blamejs/core 0.8.30 → 0.8.31
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 +2 -0
- package/index.js +2 -0
- package/lib/audit.js +1 -0
- package/lib/fdx.js +240 -0
- package/package.json +1 -1
- package/sbom.cyclonedx.json +6 -6
package/CHANGELOG.md
CHANGED
|
@@ -8,6 +8,8 @@ upgrading across more than a few patches at a time.
|
|
|
8
8
|
|
|
9
9
|
## v0.8.x
|
|
10
10
|
|
|
11
|
+
- **0.8.31** (2026-05-08) — `b.fdx` CFPB §1033 / Financial Data Exchange (FDX) consumer-financial-data sharing wrapper. CFPB §1033 (12 CFR §1033.121-461, final rule 2024-10-22) gives US consumers the right to authorize a third party to access their financial data through the data provider's developer interface. Compliance deadline ⏰ 2026-04-01 already past for $250B+ asset-size banks. **`b.fdx.bind({authServer, resources})`** binds the operator's authorization server config to the FAPI 2.0 profile (the §1033.351 security requirements ≈ FAPI 2.0); refuses if PKCE/DPoP/PAR are misconfigured per `b.fapi2.assertOAuthConfig`. **`b.fdx.validateResponse(resourceType, body)`** validates a response shape against the FDX 6.0 minimum schema for accounts / transactions / statements / payment-networks / rewards / tax-forms — refuses missing-required fields. **`b.fdx.consentReceipt(opts)`** generates the §1033.401(b) consent receipt the authorization server gives the consumer at authorization time (data provider, consumer, third-party recipient, scopes, revocation URL, issued+expires timestamps).
|
|
12
|
+
|
|
11
13
|
- **0.8.30** (2026-05-08) — `b.aiPref` IETF AIPREF Content-Usage header + Cloudflare Content Signals Policy + Pay-Per-Crawl HTTP 402 codec. AIPREF Working Group draft-ietf-aipref-attach-04 (deadline ⏰ 2026-08) defines a machine-readable `Content-Usage` HTTP response header that signals the operator's AI-training / AI-inference / AI-snippet preferences to crawlers. Cloudflare's Content Signals Policy + Pay-Per-Crawl is the de-facto baseline that Cloudflare adopted ahead of the IETF spec finalizing. **`b.aiPref.middleware({train, infer, snippet, price?})`** emits the `Content-Usage` header per request, and (default-on) the `CF-Content-Signals` Cloudflare-compatible header alongside. Values: `allow` / `deny` / `paid` for train+infer, `allow` / `deny` for snippet. **`b.aiPref.serializeHeader(opts)`** + **`b.aiPref.parseHeader(value)`** for round-trip cases. **`b.aiPref.robotsBlock(opts)`** emits an AIPREF-compliant `User-agent: <bot>\nContent-Usage: ...` block for `robots.txt`. **`b.aiPref.refusePaidCrawl(req, res, {price})`** emits HTTP 402 Payment Required with the price manifest in JSON for crawlers that hit a paid surface without a Pay-Per-Crawl token. Audit emission on refused-crawl.
|
|
12
14
|
|
|
13
15
|
- **0.8.29** (2026-05-08) — CI smoke heap bump (macOS-arm64 OOM fix). The v0.8.26 worker-fan-out cap (4 workers) reduced the parallel-shingle-pass peak memory but didn't fully close the OOM on macOS-arm64 runners — the duplicate-block detector still accumulates ~4M shingle-fingerprint entries across the lib/ corpus, peaking above the 2 GB Node default heap. `.github/workflows/ci.yml` now sets `NODE_OPTIONS: --max-old-space-size=4096` on the smoke step. Operators running the smoke suite locally on memory-constrained machines pass `HS_PATTERNS_NO_THREADS=1` to fall back to the single-threaded scan path. (Hash-fingerprint experiment in v0.8.27-dev surfaced previously-grouped-by-shape duplicates as distinct hash-bucketed clusters that broke the existing KNOWN_CLUSTERS allowlist matching; reverted to the join-on-space form.)
|
package/index.js
CHANGED
|
@@ -106,6 +106,7 @@ var iabTcf = require("./lib/iab-tcf");
|
|
|
106
106
|
var fapi2 = require("./lib/fapi2");
|
|
107
107
|
var contentCredentials = require("./lib/content-credentials");
|
|
108
108
|
var aiPref = require("./lib/ai-pref");
|
|
109
|
+
var fdx = require("./lib/fdx");
|
|
109
110
|
var safeUrl = require("./lib/safe-url");
|
|
110
111
|
var safeRedirect = require("./lib/safe-redirect");
|
|
111
112
|
var pick = require("./lib/pick");
|
|
@@ -299,6 +300,7 @@ module.exports = {
|
|
|
299
300
|
fapi2: fapi2,
|
|
300
301
|
contentCredentials: contentCredentials,
|
|
301
302
|
aiPref: aiPref,
|
|
303
|
+
fdx: fdx,
|
|
302
304
|
safeUrl: safeUrl,
|
|
303
305
|
safeRedirect: safeRedirect,
|
|
304
306
|
pick: pick,
|
package/lib/audit.js
CHANGED
|
@@ -244,6 +244,7 @@ var FRAMEWORK_NAMESPACES = [
|
|
|
244
244
|
"fapi2", // b.fapi2 (fapi2.posture_asserted)
|
|
245
245
|
"contentcredentials", // b.contentCredentials (contentcredentials.signed / verified)
|
|
246
246
|
"aipref", // b.aiPref (aipref.paid_crawl_refused)
|
|
247
|
+
"fdx", // b.fdx (fdx.bound / fdx.consent_receipt_issued)
|
|
247
248
|
];
|
|
248
249
|
var registeredNamespaces = new Set(FRAMEWORK_NAMESPACES);
|
|
249
250
|
|
package/lib/fdx.js
ADDED
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* b.fdx — CFPB §1033 / Financial Data Exchange (FDX) consumer-
|
|
4
|
+
* financial-data sharing wrapper.
|
|
5
|
+
*
|
|
6
|
+
* CFPB §1033 (12 CFR §1033.121-461, final rule 2024-10-22) gives US
|
|
7
|
+
* consumers the right to authorize a third party to access their
|
|
8
|
+
* financial data through a covered data provider's developer
|
|
9
|
+
* interface. FDX (https://financialdataexchange.org) is the
|
|
10
|
+
* industry-standard schema + protocol the CFPB rule effectively
|
|
11
|
+
* codifies (FDX 6.0+ aligns with the §1033 final rule).
|
|
12
|
+
*
|
|
13
|
+
* Compliance deadline ⏰ 2026-04-01 already past for $250B+ asset-
|
|
14
|
+
* size banks. Mid-size banks 2026-04-01 to 2027-04-01. Small banks
|
|
15
|
+
* later. Every covered data provider should be live now.
|
|
16
|
+
*
|
|
17
|
+
* The framework can't be the operator's authorization server,
|
|
18
|
+
* resource server, or FDX-data origin (those are the operator's
|
|
19
|
+
* core banking system). What it CAN do:
|
|
20
|
+
*
|
|
21
|
+
* - Bind the operator's authorization server config to the FAPI
|
|
22
|
+
* 2.0 profile (which §1033 effectively requires via the
|
|
23
|
+
* security requirements in §1033.351).
|
|
24
|
+
* - Validate FDX response shapes — refuse a payload that doesn't
|
|
25
|
+
* match the FDX 6.0 schema for accounts / transactions /
|
|
26
|
+
* statements / payment-networks.
|
|
27
|
+
* - Emit a §1033-shape audit-chain event on every authorized data
|
|
28
|
+
* access (the regulator-facing record).
|
|
29
|
+
* - Generate the "consent receipt" the consumer gets from the
|
|
30
|
+
* authorization server per §1033.401(b).
|
|
31
|
+
*
|
|
32
|
+
* Public API:
|
|
33
|
+
*
|
|
34
|
+
* b.fdx.bind(opts) -> { fapi2Posture, schemaValidator, consent }
|
|
35
|
+
* opts:
|
|
36
|
+
* authServer: { issuer, jwksUri, fapi2 }
|
|
37
|
+
* resources: ["accounts" | "transactions" | "statements" |
|
|
38
|
+
* "payment-networks" | "rewards" | "tax-forms"]
|
|
39
|
+
*
|
|
40
|
+
* b.fdx.validateResponse(resourceType, body) -> { valid, errors }
|
|
41
|
+
* Validates an FDX response shape for the named resource.
|
|
42
|
+
* Refuses extra-keys / missing-required.
|
|
43
|
+
*
|
|
44
|
+
* b.fdx.consentReceipt(opts) -> string (JSON)
|
|
45
|
+
* §1033.401(b) consent receipt the authorization server gives
|
|
46
|
+
* the consumer at authorization time. Contains:
|
|
47
|
+
* - data provider name + identifier
|
|
48
|
+
* - data subject (consumer) reference
|
|
49
|
+
* - third-party recipient name + duration
|
|
50
|
+
* - data scopes (account ids, resources)
|
|
51
|
+
* - revocation URL
|
|
52
|
+
* - issued + expires timestamps
|
|
53
|
+
*/
|
|
54
|
+
|
|
55
|
+
var fapi2 = require("./fapi2");
|
|
56
|
+
var C = require("./constants");
|
|
57
|
+
var audit = require("./audit");
|
|
58
|
+
var validateOpts = require("./validate-opts");
|
|
59
|
+
var nb = require("./numeric-bounds");
|
|
60
|
+
var { defineClass } = require("./framework-error");
|
|
61
|
+
var FdxError = defineClass("FdxError", { alwaysPermanent: true });
|
|
62
|
+
|
|
63
|
+
var FDX_RESOURCES = [
|
|
64
|
+
"accounts",
|
|
65
|
+
"transactions",
|
|
66
|
+
"statements",
|
|
67
|
+
"payment-networks",
|
|
68
|
+
"rewards",
|
|
69
|
+
"tax-forms",
|
|
70
|
+
];
|
|
71
|
+
|
|
72
|
+
// FDX 6.0 minimum schemas — operator-facing required-field gates.
|
|
73
|
+
// Not exhaustive validation (operators with strict needs route
|
|
74
|
+
// through `b.safeSchema` against the full FDX OpenAPI spec).
|
|
75
|
+
var FDX_SCHEMAS = {
|
|
76
|
+
accounts: {
|
|
77
|
+
required: ["accountId", "accountType", "accountNumberDisplay",
|
|
78
|
+
"currency", "currentBalance"],
|
|
79
|
+
},
|
|
80
|
+
transactions: {
|
|
81
|
+
required: ["transactionId", "accountId", "postedTimestamp",
|
|
82
|
+
"amount", "description", "transactionType"],
|
|
83
|
+
},
|
|
84
|
+
statements: {
|
|
85
|
+
required: ["statementId", "accountId", "statementDate", "amount"],
|
|
86
|
+
},
|
|
87
|
+
"payment-networks": {
|
|
88
|
+
required: ["paymentNetworkId", "name", "currency"],
|
|
89
|
+
},
|
|
90
|
+
rewards: {
|
|
91
|
+
required: ["rewardsProgramId", "accountId", "balance", "currency"],
|
|
92
|
+
},
|
|
93
|
+
"tax-forms": {
|
|
94
|
+
required: ["taxFormId", "taxYear", "formType"],
|
|
95
|
+
},
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
function bind(opts) {
|
|
99
|
+
if (!opts || typeof opts !== "object") {
|
|
100
|
+
throw FdxError.factory("BAD_OPTS", "fdx.bind: opts required");
|
|
101
|
+
}
|
|
102
|
+
if (!opts.authServer || typeof opts.authServer !== "object") {
|
|
103
|
+
throw FdxError.factory("BAD_AUTH_SERVER",
|
|
104
|
+
"fdx.bind: authServer object required");
|
|
105
|
+
}
|
|
106
|
+
validateOpts.requireNonEmptyString(opts.authServer.issuer,
|
|
107
|
+
"fdx.bind: authServer.issuer", FdxError, "BAD_ISSUER");
|
|
108
|
+
validateOpts.requireNonEmptyString(opts.authServer.jwksUri,
|
|
109
|
+
"fdx.bind: authServer.jwksUri", FdxError, "BAD_JWKS_URI");
|
|
110
|
+
|
|
111
|
+
if (!Array.isArray(opts.resources) || opts.resources.length === 0) {
|
|
112
|
+
throw FdxError.factory("BAD_RESOURCES",
|
|
113
|
+
"fdx.bind: resources must be a non-empty array");
|
|
114
|
+
}
|
|
115
|
+
for (var i = 0; i < opts.resources.length; i += 1) {
|
|
116
|
+
if (FDX_RESOURCES.indexOf(opts.resources[i]) === -1) {
|
|
117
|
+
throw FdxError.factory("UNKNOWN_RESOURCE",
|
|
118
|
+
"fdx.bind: unknown resource '" + opts.resources[i] +
|
|
119
|
+
"' (allowed: " + FDX_RESOURCES.join(", ") + ")");
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// §1033.351 security requirements ≈ FAPI 2.0 — assert the operator
|
|
124
|
+
// pinned the FAPI 2.0 profile. fapi2.assertOAuthConfig refuses
|
|
125
|
+
// PKCE-disabled / no-sender-constraint / etc.
|
|
126
|
+
var fapi2Opts = opts.authServer.fapi2 || { pkce: true, dpop: true, par: true };
|
|
127
|
+
fapi2.assertOAuthConfig(fapi2Opts);
|
|
128
|
+
|
|
129
|
+
audit.safeEmit({
|
|
130
|
+
action: "fdx.bound",
|
|
131
|
+
outcome: "success",
|
|
132
|
+
metadata: {
|
|
133
|
+
issuer: opts.authServer.issuer,
|
|
134
|
+
resources: opts.resources.slice(),
|
|
135
|
+
},
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
return {
|
|
139
|
+
fapi2Posture: "fapi-2.0",
|
|
140
|
+
schemaValidator: function (resourceType, body) {
|
|
141
|
+
return validateResponse(resourceType, body);
|
|
142
|
+
},
|
|
143
|
+
consent: {
|
|
144
|
+
receipt: function (consentOpts) {
|
|
145
|
+
return consentReceipt(Object.assign({
|
|
146
|
+
dataProvider: opts.authServer.issuer,
|
|
147
|
+
}, consentOpts || {}));
|
|
148
|
+
},
|
|
149
|
+
},
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function validateResponse(resourceType, body) {
|
|
154
|
+
var schema = FDX_SCHEMAS[resourceType];
|
|
155
|
+
if (!schema) {
|
|
156
|
+
throw FdxError.factory("UNKNOWN_RESOURCE",
|
|
157
|
+
"fdx.validateResponse: unknown resource '" + resourceType + "'");
|
|
158
|
+
}
|
|
159
|
+
if (!body || typeof body !== "object") {
|
|
160
|
+
return { valid: false, errors: ["body-not-object"] };
|
|
161
|
+
}
|
|
162
|
+
// FDX responses are envelopes carrying an array under the resource
|
|
163
|
+
// name (e.g. { accounts: [...] }) OR a single record. Accept both.
|
|
164
|
+
var records = Array.isArray(body[resourceType]) ? body[resourceType] :
|
|
165
|
+
Array.isArray(body) ? body :
|
|
166
|
+
[body];
|
|
167
|
+
var errors = [];
|
|
168
|
+
for (var i = 0; i < records.length; i += 1) {
|
|
169
|
+
var rec = records[i];
|
|
170
|
+
if (!rec || typeof rec !== "object") {
|
|
171
|
+
errors.push("record[" + i + "]: not-an-object");
|
|
172
|
+
continue;
|
|
173
|
+
}
|
|
174
|
+
for (var j = 0; j < schema.required.length; j += 1) {
|
|
175
|
+
var f = schema.required[j];
|
|
176
|
+
if (rec[f] === undefined || rec[f] === null) {
|
|
177
|
+
errors.push("record[" + i + "]: missing-" + f);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
return { valid: errors.length === 0, errors: errors };
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function consentReceipt(opts) {
|
|
185
|
+
if (!opts || typeof opts !== "object") {
|
|
186
|
+
throw FdxError.factory("BAD_OPTS", "fdx.consentReceipt: opts required");
|
|
187
|
+
}
|
|
188
|
+
validateOpts.requireNonEmptyString(opts.dataProvider,
|
|
189
|
+
"fdx.consentReceipt: dataProvider", FdxError, "BAD_DATA_PROVIDER");
|
|
190
|
+
validateOpts.requireNonEmptyString(opts.consumerRef,
|
|
191
|
+
"fdx.consentReceipt: consumerRef", FdxError, "BAD_CONSUMER_REF");
|
|
192
|
+
validateOpts.requireNonEmptyString(opts.thirdParty,
|
|
193
|
+
"fdx.consentReceipt: thirdParty", FdxError, "BAD_THIRD_PARTY");
|
|
194
|
+
validateOpts.requireNonEmptyString(opts.revocationUrl,
|
|
195
|
+
"fdx.consentReceipt: revocationUrl", FdxError, "BAD_REVOCATION_URL");
|
|
196
|
+
if (!Array.isArray(opts.scopes) || opts.scopes.length === 0) {
|
|
197
|
+
throw FdxError.factory("BAD_SCOPES",
|
|
198
|
+
"fdx.consentReceipt: scopes must be a non-empty array");
|
|
199
|
+
}
|
|
200
|
+
nb.requirePositiveFiniteIntIfPresent(opts.durationMs,
|
|
201
|
+
"fdx.consentReceipt: durationMs", FdxError, "BAD_DURATION");
|
|
202
|
+
|
|
203
|
+
var issuedAt = Date.now();
|
|
204
|
+
var expiresAt = issuedAt + (opts.durationMs || C.TIME.weeks(52));
|
|
205
|
+
|
|
206
|
+
var receipt = {
|
|
207
|
+
"@context": "https://financialdataexchange.org/fdx/consent-receipt/1.0",
|
|
208
|
+
type: "fdx.consent-receipt",
|
|
209
|
+
dataProvider: opts.dataProvider,
|
|
210
|
+
consumer: opts.consumerRef,
|
|
211
|
+
thirdParty: opts.thirdParty,
|
|
212
|
+
scopes: opts.scopes.slice(),
|
|
213
|
+
revocationUrl: opts.revocationUrl,
|
|
214
|
+
issuedAt: issuedAt,
|
|
215
|
+
expiresAt: expiresAt,
|
|
216
|
+
issuedAtIso: new Date(issuedAt).toISOString(),
|
|
217
|
+
expiresAtIso: new Date(expiresAt).toISOString(),
|
|
218
|
+
citations: ["cfpb-1033", "fdx-6.0"],
|
|
219
|
+
};
|
|
220
|
+
audit.safeEmit({
|
|
221
|
+
action: "fdx.consent_receipt_issued",
|
|
222
|
+
outcome: "success",
|
|
223
|
+
metadata: {
|
|
224
|
+
dataProvider: opts.dataProvider,
|
|
225
|
+
consumer: opts.consumerRef,
|
|
226
|
+
thirdParty: opts.thirdParty,
|
|
227
|
+
scopes: receipt.scopes,
|
|
228
|
+
durationMs: expiresAt - issuedAt,
|
|
229
|
+
},
|
|
230
|
+
});
|
|
231
|
+
return receipt;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
module.exports = {
|
|
235
|
+
bind: bind,
|
|
236
|
+
validateResponse: validateResponse,
|
|
237
|
+
consentReceipt: consentReceipt,
|
|
238
|
+
FDX_RESOURCES: FDX_RESOURCES.slice(),
|
|
239
|
+
FdxError: FdxError,
|
|
240
|
+
};
|
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:bc3bc35a-2ad5-4132-8b74-52a9a51b8ca1",
|
|
6
6
|
"version": 1,
|
|
7
7
|
"metadata": {
|
|
8
|
-
"timestamp": "2026-05-07T14:
|
|
8
|
+
"timestamp": "2026-05-07T14:44:41.234Z",
|
|
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.31",
|
|
23
23
|
"type": "library",
|
|
24
24
|
"name": "blamejs",
|
|
25
|
-
"version": "0.8.
|
|
25
|
+
"version": "0.8.31",
|
|
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.31",
|
|
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.31",
|
|
58
58
|
"dependsOn": []
|
|
59
59
|
}
|
|
60
60
|
]
|