@blamejs/core 0.8.29 → 0.8.30
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 +2 -0
- package/lib/ai-pref.js +211 -0
- package/lib/audit.js +1 -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,7 @@ 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");
|
|
108
109
|
var safeUrl = require("./lib/safe-url");
|
|
109
110
|
var safeRedirect = require("./lib/safe-redirect");
|
|
110
111
|
var pick = require("./lib/pick");
|
|
@@ -297,6 +298,7 @@ module.exports = {
|
|
|
297
298
|
iabTcf: iabTcf,
|
|
298
299
|
fapi2: fapi2,
|
|
299
300
|
contentCredentials: contentCredentials,
|
|
301
|
+
aiPref: aiPref,
|
|
300
302
|
safeUrl: safeUrl,
|
|
301
303
|
safeRedirect: safeRedirect,
|
|
302
304
|
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,7 @@ 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)
|
|
246
247
|
];
|
|
247
248
|
var registeredNamespaces = new Set(FRAMEWORK_NAMESPACES);
|
|
248
249
|
|
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:e537dde5-9052-47ef-83fb-163bf2d7433c",
|
|
6
6
|
"version": 1,
|
|
7
7
|
"metadata": {
|
|
8
|
-
"timestamp": "2026-05-07T14:
|
|
8
|
+
"timestamp": "2026-05-07T14:27:38.990Z",
|
|
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.30",
|
|
23
23
|
"type": "library",
|
|
24
24
|
"name": "blamejs",
|
|
25
|
-
"version": "0.8.
|
|
25
|
+
"version": "0.8.30",
|
|
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.30",
|
|
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.30",
|
|
58
58
|
"dependsOn": []
|
|
59
59
|
}
|
|
60
60
|
]
|