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 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
@@ -0,0 +1,3 @@
1
+ // index.js
2
+
3
+ module.exports = require("./src/brownstone");
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;
@@ -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;