@blamejs/core 0.8.29 → 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 +0 -0
- package/index.js +4 -0
- package/lib/ai-pref.js +211 -0
- package/lib/audit.js +2 -0
- package/lib/fdx.js +240 -0
- package/package.json +1 -1
- package/sbom.cyclonedx.json +6 -6
package/CHANGELOG.md
CHANGED
|
Binary file
|
package/index.js
CHANGED
|
@@ -105,6 +105,8 @@ var secCyber = require("./lib/sec-cyber");
|
|
|
105
105
|
var iabTcf = require("./lib/iab-tcf");
|
|
106
106
|
var fapi2 = require("./lib/fapi2");
|
|
107
107
|
var contentCredentials = require("./lib/content-credentials");
|
|
108
|
+
var aiPref = require("./lib/ai-pref");
|
|
109
|
+
var fdx = require("./lib/fdx");
|
|
108
110
|
var safeUrl = require("./lib/safe-url");
|
|
109
111
|
var safeRedirect = require("./lib/safe-redirect");
|
|
110
112
|
var pick = require("./lib/pick");
|
|
@@ -297,6 +299,8 @@ module.exports = {
|
|
|
297
299
|
iabTcf: iabTcf,
|
|
298
300
|
fapi2: fapi2,
|
|
299
301
|
contentCredentials: contentCredentials,
|
|
302
|
+
aiPref: aiPref,
|
|
303
|
+
fdx: fdx,
|
|
300
304
|
safeUrl: safeUrl,
|
|
301
305
|
safeRedirect: safeRedirect,
|
|
302
306
|
pick: pick,
|
package/lib/ai-pref.js
ADDED
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* b.aiPref — IETF AIPREF Working Group Content-Usage HTTP response
|
|
4
|
+
* header + robots.txt grammar + Cloudflare Content Signals Policy +
|
|
5
|
+
* Pay-Per-Crawl (HTTP 402) coordination.
|
|
6
|
+
*
|
|
7
|
+
* IETF AIPREF (Authors / Information Providers' Preference for AI
|
|
8
|
+
* Use) draft-ietf-aipref-attach-04 (deadline ⏰ 2026-08) defines a
|
|
9
|
+
* machine-readable Content-Usage HTTP response header that signals
|
|
10
|
+
* the operator's AI-training / AI-inference / AI-snippet preferences
|
|
11
|
+
* to crawlers. Cloudflare's Content Signals Policy + Pay-Per-Crawl
|
|
12
|
+
* (HTTP 402) is the de-facto baseline that Cloudflare adopted ahead
|
|
13
|
+
* of the IETF spec finalizing.
|
|
14
|
+
*
|
|
15
|
+
* Public API:
|
|
16
|
+
*
|
|
17
|
+
* b.aiPref.middleware(opts) -> middleware(req, res, next)
|
|
18
|
+
* opts:
|
|
19
|
+
* train: "allow" | "deny" | "paid" — default "deny"
|
|
20
|
+
* infer: "allow" | "deny" | "paid" — default "allow"
|
|
21
|
+
* snippet: "allow" | "deny" — default "allow"
|
|
22
|
+
* price: { amountUsd, perTokens? } when any of
|
|
23
|
+
* train/infer is "paid".
|
|
24
|
+
* cloudflareSignals: bool, default true — emit the Cloudflare
|
|
25
|
+
* Content-Signals header alongside Content-Usage.
|
|
26
|
+
* robotsContext: "default" | "<user-agent>" — emit
|
|
27
|
+
* per-user-agent rules in robots.txt rather
|
|
28
|
+
* than the catch-all default.
|
|
29
|
+
*
|
|
30
|
+
* b.aiPref.robotsBlock(opts) -> string
|
|
31
|
+
* Returns a robots.txt block per AIPREF §3 grammar:
|
|
32
|
+
*
|
|
33
|
+
* User-agent: GPTBot
|
|
34
|
+
* Content-Usage: train=deny, infer=allow, snippet=allow
|
|
35
|
+
*
|
|
36
|
+
* b.aiPref.serializeHeader(opts) -> string
|
|
37
|
+
* Returns the Content-Usage HTTP response header value.
|
|
38
|
+
*
|
|
39
|
+
* b.aiPref.parseHeader(value) -> { train, infer, snippet, price? }
|
|
40
|
+
* Parses an inbound Content-Usage header (used when the framework
|
|
41
|
+
* plays the role of crawler: respect declared preferences).
|
|
42
|
+
*
|
|
43
|
+
* b.aiPref.refusePaidCrawl(req, res, opts)
|
|
44
|
+
* Convenience: emits HTTP 402 Payment Required with the price
|
|
45
|
+
* manifest in the Cloudflare-compatible JSON body.
|
|
46
|
+
*/
|
|
47
|
+
|
|
48
|
+
var audit = require("./audit");
|
|
49
|
+
var requestHelpers = require("./request-helpers");
|
|
50
|
+
var { defineClass } = require("./framework-error");
|
|
51
|
+
var AiPrefError = defineClass("AiPrefError", { alwaysPermanent: true });
|
|
52
|
+
|
|
53
|
+
var TRAIN_VALUES = ["allow", "deny", "paid"];
|
|
54
|
+
var INFER_VALUES = ["allow", "deny", "paid"];
|
|
55
|
+
var SNIPPET_VALUES = ["allow", "deny"];
|
|
56
|
+
|
|
57
|
+
function _validate(opts) {
|
|
58
|
+
if (!opts || typeof opts !== "object") {
|
|
59
|
+
throw AiPrefError.factory("BAD_OPTS",
|
|
60
|
+
"aiPref: opts required");
|
|
61
|
+
}
|
|
62
|
+
var train = opts.train || "deny";
|
|
63
|
+
var infer = opts.infer || "allow";
|
|
64
|
+
var snippet = opts.snippet || "allow";
|
|
65
|
+
if (TRAIN_VALUES.indexOf(train) === -1) {
|
|
66
|
+
throw AiPrefError.factory("BAD_TRAIN", "aiPref: train must be one of " + TRAIN_VALUES.join(", "));
|
|
67
|
+
}
|
|
68
|
+
if (INFER_VALUES.indexOf(infer) === -1) {
|
|
69
|
+
throw AiPrefError.factory("BAD_INFER", "aiPref: infer must be one of " + INFER_VALUES.join(", "));
|
|
70
|
+
}
|
|
71
|
+
if (SNIPPET_VALUES.indexOf(snippet) === -1) {
|
|
72
|
+
throw AiPrefError.factory("BAD_SNIPPET", "aiPref: snippet must be one of " + SNIPPET_VALUES.join(", "));
|
|
73
|
+
}
|
|
74
|
+
if ((train === "paid" || infer === "paid") &&
|
|
75
|
+
(!opts.price || typeof opts.price.amountUsd !== "number" ||
|
|
76
|
+
!isFinite(opts.price.amountUsd) || opts.price.amountUsd <= 0)) {
|
|
77
|
+
throw AiPrefError.factory("BAD_PRICE",
|
|
78
|
+
"aiPref: price.amountUsd (positive finite number) required when train or infer is 'paid'");
|
|
79
|
+
}
|
|
80
|
+
return { train: train, infer: infer, snippet: snippet, price: opts.price || null };
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function serializeHeader(opts) {
|
|
84
|
+
var v = _validate(opts);
|
|
85
|
+
// RFC 8941 structured-fields list of token=token pairs. AIPREF §4.2.
|
|
86
|
+
var parts = [
|
|
87
|
+
"train=" + v.train,
|
|
88
|
+
"infer=" + v.infer,
|
|
89
|
+
"snippet=" + v.snippet,
|
|
90
|
+
];
|
|
91
|
+
if (v.price) {
|
|
92
|
+
parts.push('price-usd=' + v.price.amountUsd.toFixed(6));
|
|
93
|
+
if (typeof v.price.perTokens === "number" && isFinite(v.price.perTokens) && v.price.perTokens > 0) {
|
|
94
|
+
parts.push("per-tokens=" + Math.floor(v.price.perTokens));
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
return parts.join(", ");
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function parseHeader(value) {
|
|
101
|
+
if (typeof value !== "string" || value.length === 0) {
|
|
102
|
+
throw AiPrefError.factory("BAD_HEADER", "aiPref.parseHeader: value required");
|
|
103
|
+
}
|
|
104
|
+
if (value.length > 1024) { // allow:raw-byte-literal — header value cap, not bytes
|
|
105
|
+
throw AiPrefError.factory("HEADER_TOO_LARGE",
|
|
106
|
+
"aiPref.parseHeader: value exceeds 1024 chars");
|
|
107
|
+
}
|
|
108
|
+
var out = { train: null, infer: null, snippet: null, price: null };
|
|
109
|
+
var pairs = value.split(",");
|
|
110
|
+
for (var i = 0; i < pairs.length; i += 1) {
|
|
111
|
+
var p = pairs[i].trim();
|
|
112
|
+
var eq = p.indexOf("=");
|
|
113
|
+
if (eq === -1) continue;
|
|
114
|
+
var k = p.slice(0, eq).trim().toLowerCase();
|
|
115
|
+
var val = p.slice(eq + 1).trim();
|
|
116
|
+
if (k === "train" && TRAIN_VALUES.indexOf(val) !== -1) out.train = val;
|
|
117
|
+
else if (k === "infer" && INFER_VALUES.indexOf(val) !== -1) out.infer = val;
|
|
118
|
+
else if (k === "snippet" && SNIPPET_VALUES.indexOf(val) !== -1) out.snippet = val;
|
|
119
|
+
else if (k === "price-usd") {
|
|
120
|
+
var amt = parseFloat(val);
|
|
121
|
+
if (isFinite(amt) && amt > 0) out.price = Object.assign({ amountUsd: amt }, out.price || {});
|
|
122
|
+
} else if (k === "per-tokens") {
|
|
123
|
+
var pt = parseInt(val, 10);
|
|
124
|
+
if (isFinite(pt) && pt > 0) out.price = Object.assign({ perTokens: pt }, out.price || {});
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
return out;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function robotsBlock(opts) {
|
|
131
|
+
var v = _validate(opts);
|
|
132
|
+
var ua = opts.userAgent || "*";
|
|
133
|
+
if (typeof ua !== "string" || ua.length === 0 || ua.length > 256) { // allow:raw-byte-literal — UA-string cap, not bytes
|
|
134
|
+
throw AiPrefError.factory("BAD_USER_AGENT",
|
|
135
|
+
"aiPref.robotsBlock: userAgent must be 1-256 char string (or omit for *)");
|
|
136
|
+
}
|
|
137
|
+
return "User-agent: " + ua + "\n" +
|
|
138
|
+
"Content-Usage: " + serializeHeader(v) + "\n";
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function _cfSignalsHeader(v) {
|
|
142
|
+
// Cloudflare Content Signals Policy emits a header named
|
|
143
|
+
// `cf-content-signals` with a similar grammar. As of Cloudflare's
|
|
144
|
+
// 2025-12 beta the canonical key names are: `ai-training`,
|
|
145
|
+
// `ai-inference`, `ai-snippet`. Keep close to that vocabulary.
|
|
146
|
+
var parts = [
|
|
147
|
+
"ai-training=" + v.train,
|
|
148
|
+
"ai-inference=" + v.infer,
|
|
149
|
+
"ai-snippet=" + v.snippet,
|
|
150
|
+
];
|
|
151
|
+
if (v.price) parts.push("price-usd=" + v.price.amountUsd.toFixed(6));
|
|
152
|
+
return parts.join("; ");
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function middleware(opts) {
|
|
156
|
+
var v = _validate(opts);
|
|
157
|
+
var emitCf = opts.cloudflareSignals !== false;
|
|
158
|
+
var header = serializeHeader(v);
|
|
159
|
+
var cfHeader = emitCf ? _cfSignalsHeader(v) : null;
|
|
160
|
+
|
|
161
|
+
return function aiPrefMw(req, res, next) {
|
|
162
|
+
if (typeof res.setHeader === "function") {
|
|
163
|
+
res.setHeader("Content-Usage", header);
|
|
164
|
+
if (cfHeader) res.setHeader("CF-Content-Signals", cfHeader);
|
|
165
|
+
}
|
|
166
|
+
if (typeof next === "function") next();
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function refusePaidCrawl(req, res, opts) {
|
|
171
|
+
if (!opts || !opts.price || typeof opts.price.amountUsd !== "number") {
|
|
172
|
+
throw AiPrefError.factory("BAD_PRICE",
|
|
173
|
+
"aiPref.refusePaidCrawl: opts.price.amountUsd required");
|
|
174
|
+
}
|
|
175
|
+
var body = JSON.stringify({
|
|
176
|
+
error: "payment_required",
|
|
177
|
+
pricingModel: "pay-per-crawl",
|
|
178
|
+
price: {
|
|
179
|
+
amountUsd: opts.price.amountUsd,
|
|
180
|
+
perTokens: opts.price.perTokens || null,
|
|
181
|
+
},
|
|
182
|
+
contact: opts.contact || null,
|
|
183
|
+
});
|
|
184
|
+
if (typeof res.setHeader === "function") {
|
|
185
|
+
res.setHeader("Content-Type", "application/json");
|
|
186
|
+
res.setHeader("Cache-Control", "no-store");
|
|
187
|
+
}
|
|
188
|
+
res.statusCode = 402; // allow:raw-byte-literal — HTTP 402 Payment Required (RFC 9110)
|
|
189
|
+
res.end(body);
|
|
190
|
+
audit.safeEmit({
|
|
191
|
+
action: "aipref.paid_crawl_refused",
|
|
192
|
+
outcome: "denied",
|
|
193
|
+
metadata: {
|
|
194
|
+
ip: requestHelpers.clientIp(req),
|
|
195
|
+
userAgent: req && req.headers && req.headers["user-agent"],
|
|
196
|
+
amountUsd: opts.price.amountUsd,
|
|
197
|
+
},
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
module.exports = {
|
|
202
|
+
middleware: middleware,
|
|
203
|
+
serializeHeader: serializeHeader,
|
|
204
|
+
parseHeader: parseHeader,
|
|
205
|
+
robotsBlock: robotsBlock,
|
|
206
|
+
refusePaidCrawl: refusePaidCrawl,
|
|
207
|
+
TRAIN_VALUES: TRAIN_VALUES.slice(),
|
|
208
|
+
INFER_VALUES: INFER_VALUES.slice(),
|
|
209
|
+
SNIPPET_VALUES: SNIPPET_VALUES.slice(),
|
|
210
|
+
AiPrefError: AiPrefError,
|
|
211
|
+
};
|
package/lib/audit.js
CHANGED
|
@@ -243,6 +243,8 @@ var FRAMEWORK_NAMESPACES = [
|
|
|
243
243
|
"iabtcf", // b.iabTcf (iabtcf.refused / iabtcf.accepted)
|
|
244
244
|
"fapi2", // b.fapi2 (fapi2.posture_asserted)
|
|
245
245
|
"contentcredentials", // b.contentCredentials (contentcredentials.signed / verified)
|
|
246
|
+
"aipref", // b.aiPref (aipref.paid_crawl_refused)
|
|
247
|
+
"fdx", // b.fdx (fdx.bound / fdx.consent_receipt_issued)
|
|
246
248
|
];
|
|
247
249
|
var registeredNamespaces = new Set(FRAMEWORK_NAMESPACES);
|
|
248
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
|
]
|