brownstone-middleware 0.1.0
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/README.md +80 -0
- package/index.js +3 -0
- package/package.json +27 -0
- package/src/brownstone.js +40 -0
- package/src/detectAI.js +26 -0
- package/src/enforce.js +17 -0
- package/src/injectMetadata.js +23 -0
- package/src/logUnknown.js +25 -0
- package/src/meterUsage.js +18 -0
- package/src/serveLlmsTxt.js +35 -0
package/README.md
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
# brownstone-middleware
|
|
2
|
+
|
|
3
|
+
Express middleware for detecting, logging, and enforcing licensing around AI agent access to your application.
|
|
4
|
+
|
|
5
|
+
Brownstone identifies AI crawlers and bots by their user-agent, injects licensing metadata into your HTML responses, and optionally logs hits and blocks unauthorized AI access — all in a few lines of code.
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npm install brownstone-middleware
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Usage
|
|
14
|
+
|
|
15
|
+
```js
|
|
16
|
+
const express = require("express");
|
|
17
|
+
const brownstone = require("brownstone-middleware");
|
|
18
|
+
|
|
19
|
+
const app = express();
|
|
20
|
+
|
|
21
|
+
app.use(
|
|
22
|
+
brownstone({
|
|
23
|
+
apiKey: "your-brownstone-api-key",
|
|
24
|
+
metering: true,
|
|
25
|
+
enforcement: true,
|
|
26
|
+
license: {
|
|
27
|
+
model: "paid",
|
|
28
|
+
rate: "$0.001-per-1000-tokens",
|
|
29
|
+
permission: "allowed",
|
|
30
|
+
},
|
|
31
|
+
})
|
|
32
|
+
);
|
|
33
|
+
|
|
34
|
+
app.get("/", (req, res) => {
|
|
35
|
+
res.send("<html><head></head><body>Hello</body></html>");
|
|
36
|
+
});
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## How it works
|
|
40
|
+
|
|
41
|
+
1. Each incoming request is checked for a known AI user-agent
|
|
42
|
+
2. If an AI is detected and `metering` is enabled, the hit is logged to your Brownstone dashboard
|
|
43
|
+
3. If `enforcement` is enabled, the request is checked against your site's license — blocked AIs receive a `403` response
|
|
44
|
+
4. A `<meta name="ai-usage">` tag is injected into every HTML response so AI crawlers can read your licensing terms directly from the page
|
|
45
|
+
|
|
46
|
+
## Options
|
|
47
|
+
|
|
48
|
+
| Option | Type | Required | Description |
|
|
49
|
+
|---|---|---|---|
|
|
50
|
+
| `apiKey` | string | Yes (if metering or enforcement enabled) | Your Brownstone site API key |
|
|
51
|
+
| `metering` | boolean | No | Log AI hits to the Brownstone API |
|
|
52
|
+
| `enforcement` | boolean | No | Block AIs based on your site's configuration |
|
|
53
|
+
| `license` | object | No | License info injected into HTML meta tag |
|
|
54
|
+
| `license.model` | string | No | `"free"` or `"paid"` |
|
|
55
|
+
| `license.rate` | string | No | e.g. `"$0.001-per-1000-tokens"` |
|
|
56
|
+
| `license.permission` | string | No | `"allowed"`, `"restricted"`, or `"blocked"` |
|
|
57
|
+
|
|
58
|
+
## Detected AI agents
|
|
59
|
+
|
|
60
|
+
Brownstone detects the following bots out of the box:
|
|
61
|
+
|
|
62
|
+
- ChatGPT (OpenAI)
|
|
63
|
+
- Claude (Anthropic)
|
|
64
|
+
- BingAI (Microsoft)
|
|
65
|
+
- Perplexity
|
|
66
|
+
- Gemini (Google)
|
|
67
|
+
- Grok (xAI)
|
|
68
|
+
- Meta AI
|
|
69
|
+
- Common Crawl
|
|
70
|
+
- Cohere
|
|
71
|
+
- Diffbot
|
|
72
|
+
- Bytespider (ByteDance)
|
|
73
|
+
|
|
74
|
+
## Get an API key
|
|
75
|
+
|
|
76
|
+
Register your site and get an API key at [brownstone.dev](https://brownstone.dev) (coming soon) or by running the Brownstone API locally.
|
|
77
|
+
|
|
78
|
+
## License
|
|
79
|
+
|
|
80
|
+
MIT
|
package/index.js
ADDED
package/package.json
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "brownstone-middleware",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Express middleware for detecting, logging, and enforcing licensing around AI agent access",
|
|
5
|
+
"main": "index.js",
|
|
6
|
+
"files": [
|
|
7
|
+
"index.js",
|
|
8
|
+
"src/"
|
|
9
|
+
],
|
|
10
|
+
"scripts": {
|
|
11
|
+
"test": "node --test test/detectAI.test.js"
|
|
12
|
+
},
|
|
13
|
+
"peerDependencies": {
|
|
14
|
+
"express": ">=4.0.0"
|
|
15
|
+
},
|
|
16
|
+
"keywords": [
|
|
17
|
+
"ai",
|
|
18
|
+
"licensing",
|
|
19
|
+
"middleware",
|
|
20
|
+
"express",
|
|
21
|
+
"bot-detection",
|
|
22
|
+
"crawlers"
|
|
23
|
+
],
|
|
24
|
+
"author": "Gisselle Petty",
|
|
25
|
+
"license": "MIT",
|
|
26
|
+
"type": "commonjs"
|
|
27
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
// brownstone.js
|
|
2
|
+
|
|
3
|
+
const detectAI = require("./detectAI");
|
|
4
|
+
const injectMetadata = require("./injectMetadata");
|
|
5
|
+
const logHit = require("./meterUsage");
|
|
6
|
+
const checkLicense = require("./enforce");
|
|
7
|
+
const logUnknown = require("./logUnknown");
|
|
8
|
+
const serveLlmsTxt = require("./serveLlmsTxt");
|
|
9
|
+
|
|
10
|
+
function brownstone(options = {}) {
|
|
11
|
+
return async function (req, res, next) {
|
|
12
|
+
if (serveLlmsTxt(req, res, options)) return;
|
|
13
|
+
|
|
14
|
+
const userAgent = req.headers["user-agent"] || "";
|
|
15
|
+
const aiSource = detectAI(userAgent);
|
|
16
|
+
|
|
17
|
+
// Inject meta tag into HTML responses
|
|
18
|
+
injectMetadata(res, options);
|
|
19
|
+
|
|
20
|
+
if (!aiSource) {
|
|
21
|
+
if (options.metering) await logUnknown(req, options);
|
|
22
|
+
return next();
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
if (options.metering) {
|
|
26
|
+
await logHit(aiSource, req, options);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
if (options.enforcement) {
|
|
30
|
+
const allowed = await checkLicense(aiSource, options);
|
|
31
|
+
if (!allowed) {
|
|
32
|
+
return res.status(403).send("AI blocked.");
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
next();
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
module.exports = brownstone;
|
package/src/detectAI.js
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
// detectAI.js
|
|
2
|
+
|
|
3
|
+
const aiAgents = [
|
|
4
|
+
{ name: "ChatGPT", ua: ["ChatGPTBot", "OAI-SearchBot"] },
|
|
5
|
+
{ name: "Claude", ua: ["ClaudeBot", "Claude-Web", "Anthropic"] },
|
|
6
|
+
{ name: "BingAI", ua: ["Bingbot", "EdgeAI"] },
|
|
7
|
+
{ name: "Perplexity", ua: ["PerplexityBot"] },
|
|
8
|
+
{ name: "Gemini", ua: ["Google-Extended", "Googlebot-Extended"] },
|
|
9
|
+
{ name: "Grok", ua: ["xAI"] },
|
|
10
|
+
{ name: "Meta", ua: ["meta-externalagent", "FacebookBot"] },
|
|
11
|
+
{ name: "CommonCrawl", ua: ["CCBot"] },
|
|
12
|
+
{ name: "Cohere", ua: ["cohere-ai"] },
|
|
13
|
+
{ name: "Diffbot", ua: ["Diffbot"] },
|
|
14
|
+
{ name: "Bytespider", ua: ["Bytespider"] },
|
|
15
|
+
];
|
|
16
|
+
|
|
17
|
+
function detectAI(userAgent = "") {
|
|
18
|
+
for (const agent of aiAgents) {
|
|
19
|
+
if (agent.ua.some((str) => userAgent.includes(str))) {
|
|
20
|
+
return agent.name;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
return null;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
module.exports = detectAI;
|
package/src/enforce.js
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
// enforce.js
|
|
2
|
+
|
|
3
|
+
async function checkLicense(aiSource, options) {
|
|
4
|
+
const response = await fetch("http://localhost:4000/check", {
|
|
5
|
+
method: "POST",
|
|
6
|
+
headers: {
|
|
7
|
+
"Content-Type": "application/json",
|
|
8
|
+
"x-api-key": options.apiKey,
|
|
9
|
+
},
|
|
10
|
+
body: JSON.stringify({ ai: aiSource }),
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
const data = await response.json();
|
|
14
|
+
return data.allowed;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
module.exports = checkLicense;
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
// injectMetadata.js
|
|
2
|
+
|
|
3
|
+
function injectMetadata(res, licenseOption) {
|
|
4
|
+
const originalSend = res.send;
|
|
5
|
+
|
|
6
|
+
res.send = function (body) {
|
|
7
|
+
if (typeof body === "string" && body.includes("</head>")) {
|
|
8
|
+
const license = licenseOption.license || {};
|
|
9
|
+
const content = [
|
|
10
|
+
license.model || "free",
|
|
11
|
+
license.rate || null,
|
|
12
|
+
license.permission || "allowed",
|
|
13
|
+
]
|
|
14
|
+
.filter(Boolean)
|
|
15
|
+
.join("|");
|
|
16
|
+
const metaTag = `<meta name="ai-usage" content="${content}">`;
|
|
17
|
+
body = body.replace("</head>", `${metaTag}</head>`);
|
|
18
|
+
}
|
|
19
|
+
return originalSend.call(this, body);
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
module.exports = injectMetadata;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
// logUnknown.js
|
|
2
|
+
|
|
3
|
+
async function logUnknown(req, options) {
|
|
4
|
+
const userAgent = req.headers["user-agent"] || "";
|
|
5
|
+
if (!userAgent) return;
|
|
6
|
+
|
|
7
|
+
try {
|
|
8
|
+
await fetch("http://localhost:4000/unknown-hit", {
|
|
9
|
+
method: "POST",
|
|
10
|
+
headers: {
|
|
11
|
+
"Content-Type": "application/json",
|
|
12
|
+
"x-api-key": options.apiKey,
|
|
13
|
+
},
|
|
14
|
+
body: JSON.stringify({
|
|
15
|
+
userAgent,
|
|
16
|
+
path: req.originalUrl,
|
|
17
|
+
ip: req.ip,
|
|
18
|
+
}),
|
|
19
|
+
});
|
|
20
|
+
} catch (err) {
|
|
21
|
+
console.error("Unknown bot log failed:", err.message);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
module.exports = logUnknown;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
// meterUsage.js
|
|
2
|
+
|
|
3
|
+
async function logHit(aiSource, req, options) {
|
|
4
|
+
await fetch("http://localhost:4000/log", {
|
|
5
|
+
method: "POST",
|
|
6
|
+
headers: {
|
|
7
|
+
"Content-Type": "application/json",
|
|
8
|
+
"x-api-key": options.apiKey,
|
|
9
|
+
},
|
|
10
|
+
body: JSON.stringify({
|
|
11
|
+
ai: aiSource,
|
|
12
|
+
path: req.originalUrl,
|
|
13
|
+
ip: req.ip,
|
|
14
|
+
}),
|
|
15
|
+
});
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
module.exports = logHit;
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
// serveLlmsTxt.js
|
|
2
|
+
|
|
3
|
+
function generateLlmsTxt(options) {
|
|
4
|
+
const license = options.license || {};
|
|
5
|
+
const lines = [];
|
|
6
|
+
|
|
7
|
+
lines.push(`# ${options.name || "This site"}`);
|
|
8
|
+
lines.push("");
|
|
9
|
+
lines.push("> AI licensing terms for this site, powered by Brownstone.");
|
|
10
|
+
lines.push("");
|
|
11
|
+
|
|
12
|
+
lines.push("## License");
|
|
13
|
+
lines.push("");
|
|
14
|
+
if (license.model) lines.push(`- Model: ${license.model}`);
|
|
15
|
+
if (license.rate) lines.push(`- Rate: ${license.rate}`);
|
|
16
|
+
if (license.permission) lines.push(`- Permission: ${license.permission}`);
|
|
17
|
+
lines.push("");
|
|
18
|
+
|
|
19
|
+
lines.push("## Contact");
|
|
20
|
+
lines.push("");
|
|
21
|
+
lines.push("- Licensing powered by Brownstone: https://brownstone.dev");
|
|
22
|
+
|
|
23
|
+
return lines.join("\n");
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function serveLlmsTxt(req, res, options) {
|
|
27
|
+
if (req.method === "GET" && req.path === "/llms.txt") {
|
|
28
|
+
res.setHeader("Content-Type", "text/plain");
|
|
29
|
+
res.send(generateLlmsTxt(options));
|
|
30
|
+
return true;
|
|
31
|
+
}
|
|
32
|
+
return false;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
module.exports = serveLlmsTxt;
|