@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 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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blamejs/core",
3
- "version": "0.8.29",
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:34fc0ebb-9c11-4245-a15a-7919aa7897e2",
5
+ "serialNumber": "urn:uuid:bc3bc35a-2ad5-4132-8b74-52a9a51b8ca1",
6
6
  "version": 1,
7
7
  "metadata": {
8
- "timestamp": "2026-05-07T14:08:07.987Z",
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.29",
22
+ "bom-ref": "@blamejs/core@0.8.31",
23
23
  "type": "library",
24
24
  "name": "blamejs",
25
- "version": "0.8.29",
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",
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.29",
57
+ "ref": "@blamejs/core@0.8.31",
58
58
  "dependsOn": []
59
59
  }
60
60
  ]