@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 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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blamejs/core",
3
- "version": "0.8.30",
3
+ "version": "0.8.31",
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:e537dde5-9052-47ef-83fb-163bf2d7433c",
5
+ "serialNumber": "urn:uuid:bc3bc35a-2ad5-4132-8b74-52a9a51b8ca1",
6
6
  "version": 1,
7
7
  "metadata": {
8
- "timestamp": "2026-05-07T14:27:38.990Z",
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.30",
22
+ "bom-ref": "@blamejs/core@0.8.31",
23
23
  "type": "library",
24
24
  "name": "blamejs",
25
- "version": "0.8.30",
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.30",
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.30",
57
+ "ref": "@blamejs/core@0.8.31",
58
58
  "dependsOn": []
59
59
  }
60
60
  ]