@naulon/enforce 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 +89 -0
- package/dist/agentDetect.d.ts +94 -0
- package/dist/agentDetect.d.ts.map +1 -0
- package/dist/agentDetect.js +149 -0
- package/dist/agentDetect.js.map +1 -0
- package/dist/botAuth.d.ts +159 -0
- package/dist/botAuth.d.ts.map +1 -0
- package/dist/botAuth.js +659 -0
- package/dist/botAuth.js.map +1 -0
- package/dist/build402.d.ts +46 -0
- package/dist/build402.d.ts.map +1 -0
- package/dist/build402.js +137 -0
- package/dist/build402.js.map +1 -0
- package/dist/decide.d.ts +125 -0
- package/dist/decide.d.ts.map +1 -0
- package/dist/decide.js +221 -0
- package/dist/decide.js.map +1 -0
- package/dist/discoverability.d.ts +68 -0
- package/dist/discoverability.d.ts.map +1 -0
- package/dist/discoverability.js +62 -0
- package/dist/discoverability.js.map +1 -0
- package/dist/enforce/fetch-handler.d.ts +12 -0
- package/dist/enforce/fetch-handler.d.ts.map +1 -0
- package/dist/enforce/fetch-handler.js +25 -0
- package/dist/enforce/fetch-handler.js.map +1 -0
- package/dist/enforce/index.d.ts +11 -0
- package/dist/enforce/index.d.ts.map +1 -0
- package/dist/enforce/index.js +11 -0
- package/dist/enforce/index.js.map +1 -0
- package/dist/enforce/middleware.d.ts +27 -0
- package/dist/enforce/middleware.d.ts.map +1 -0
- package/dist/enforce/middleware.js +88 -0
- package/dist/enforce/middleware.js.map +1 -0
- package/dist/enforce/next.d.ts +24 -0
- package/dist/enforce/next.d.ts.map +1 -0
- package/dist/enforce/next.js +32 -0
- package/dist/enforce/next.js.map +1 -0
- package/dist/enforce/quote-source.d.ts +31 -0
- package/dist/enforce/quote-source.d.ts.map +1 -0
- package/dist/enforce/quote-source.js +29 -0
- package/dist/enforce/quote-source.js.map +1 -0
- package/dist/index.d.ts +23 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +31 -0
- package/dist/index.js.map +1 -0
- package/dist/license.d.ts +16 -0
- package/dist/license.d.ts.map +1 -0
- package/dist/license.js +18 -0
- package/dist/license.js.map +1 -0
- package/dist/nonce.d.ts +45 -0
- package/dist/nonce.d.ts.map +1 -0
- package/dist/nonce.js +122 -0
- package/dist/nonce.js.map +1 -0
- package/dist/pop.d.ts +15 -0
- package/dist/pop.d.ts.map +1 -0
- package/dist/pop.js +77 -0
- package/dist/pop.js.map +1 -0
- package/dist/pricing.d.ts +53 -0
- package/dist/pricing.d.ts.map +1 -0
- package/dist/pricing.js +44 -0
- package/dist/pricing.js.map +1 -0
- package/dist/revocation.d.ts +6 -0
- package/dist/revocation.d.ts.map +1 -0
- package/dist/revocation.js +45 -0
- package/dist/revocation.js.map +1 -0
- package/package.json +47 -0
package/README.md
ADDED
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
# @naulon/enforce
|
|
2
|
+
|
|
3
|
+
The runtime-agnostic toll-decision kernel plus the in-app enforcement middleware.
|
|
4
|
+
|
|
5
|
+
This is the neutral low-level core that both `@naulon/tollgate` (the gate shell —
|
|
6
|
+
the reverse-proxy that boots `createApp`) and `@naulon/sdk` (the publisher SDK)
|
|
7
|
+
sit **above**, with no dependency cycle. It depends only on `@naulon/shared` (and
|
|
8
|
+
`viem`, for the holder-of-key proof). The heavy settlement path — the Circle
|
|
9
|
+
facilitator, the pending-leg drain — stays in `@naulon/tollgate`; nothing here
|
|
10
|
+
imports `@circle-fin/x402-batching`.
|
|
11
|
+
|
|
12
|
+
## Why it exists
|
|
13
|
+
|
|
14
|
+
`decide()` is one pure function: given a web `Request` and a known publisher, it
|
|
15
|
+
returns a verdict — serve free, refuse, or `402` with the payment legs — and
|
|
16
|
+
performs **no** side effects (no proxy, no settle, no observe). Extracting it lets
|
|
17
|
+
two very different runtimes reach the *same* verdict:
|
|
18
|
+
|
|
19
|
+
- **The gate** (`@naulon/tollgate`) runs `decide()` inside its Hono reverse proxy.
|
|
20
|
+
- **In-app middleware** runs the *identical* `decide()` in the publisher's own app,
|
|
21
|
+
so an agent's request reaches the origin directly instead of routing through the
|
|
22
|
+
fleet's single egress IP (which an origin edge can rate-limit).
|
|
23
|
+
|
|
24
|
+
Both build a byte-identical `402`, because they share this code.
|
|
25
|
+
|
|
26
|
+
## Exports
|
|
27
|
+
|
|
28
|
+
### `@naulon/enforce`
|
|
29
|
+
|
|
30
|
+
The decision kernel and the framework-agnostic middleware core:
|
|
31
|
+
|
|
32
|
+
- `decide(input)` — the pure verdict function.
|
|
33
|
+
- `naulonMiddleware(opts)` — takes a `Request`, returns `{ response, setHeaders }`:
|
|
34
|
+
a `Response` to short-circuit (`402`/`403`), or `null` to let the app render
|
|
35
|
+
(with `setHeaders` to attach to the app's response on a paid pass).
|
|
36
|
+
- `withNaulon(handler, opts)` — wrap a generic `fetch` handler.
|
|
37
|
+
- `localQuoteSource(fn)` / `httpQuoteSource(url, key)` — pluggable price + payees.
|
|
38
|
+
- The classification, Web Bot Auth, nonce, and proof primitives (`classify`,
|
|
39
|
+
`verifyBotAuth`, …) and the x402 build side (`build402`, `buildRequirements`).
|
|
40
|
+
|
|
41
|
+
### `@naulon/enforce/next`
|
|
42
|
+
|
|
43
|
+
- `createNaulonMiddleware(opts, NextResponse)` — the Next.js App Router adapter.
|
|
44
|
+
It has no hard `next` dependency; you inject `NextResponse` (your app already has
|
|
45
|
+
it), keeping the core framework-agnostic.
|
|
46
|
+
|
|
47
|
+
## Usage
|
|
48
|
+
|
|
49
|
+
```ts
|
|
50
|
+
// middleware.ts (Next.js App Router)
|
|
51
|
+
import { NextResponse } from "next/server";
|
|
52
|
+
import { createNaulonMiddleware } from "@naulon/enforce/next";
|
|
53
|
+
import { httpQuoteSource } from "@naulon/enforce";
|
|
54
|
+
|
|
55
|
+
export const middleware = createNaulonMiddleware(
|
|
56
|
+
{
|
|
57
|
+
publisher: { id: "your-site", articlePrefixes: ["/articles/"] },
|
|
58
|
+
quote: httpQuoteSource("https://<your-control-plane>/_naulon/quote", process.env.NAULON_API_KEY!),
|
|
59
|
+
verifyUrl: "https://<your-control-plane>/_naulon/verify",
|
|
60
|
+
apiKey: process.env.NAULON_API_KEY!,
|
|
61
|
+
},
|
|
62
|
+
NextResponse,
|
|
63
|
+
);
|
|
64
|
+
|
|
65
|
+
export const config = { matcher: ["/articles/:path*"] };
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
`quote` and `verifyUrl` point at whatever runs the money + catalog legs — the
|
|
69
|
+
managed control plane, or your own self-hosted `POST /_naulon/verify` +
|
|
70
|
+
`GET /_naulon/quote`. The middleware never holds funds: it forwards the buyer's
|
|
71
|
+
signed payment to `verifyUrl`, which settles buyer → author directly.
|
|
72
|
+
|
|
73
|
+
## Layering
|
|
74
|
+
|
|
75
|
+
```
|
|
76
|
+
@naulon/shared
|
|
77
|
+
▲
|
|
78
|
+
@naulon/enforce ← this package (decision kernel + middleware)
|
|
79
|
+
▲
|
|
80
|
+
@naulon/tollgate (the gate shell runs decide() inside its reverse proxy)
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
A publisher vendors `@naulon/enforce` directly (it builds to its own `dist/`
|
|
84
|
+
tarball) and wires the middleware; the gate consumes the very same package, which
|
|
85
|
+
is what guarantees both reach an identical verdict. `@naulon/enforce` is
|
|
86
|
+
deliberately NOT re-exported through `@naulon/sdk` — the SDK imports `@naulon/shared`,
|
|
87
|
+
which re-exports the SDK, so an `sdk → enforce` edge would form a declaration-build
|
|
88
|
+
cycle. Keeping enforce standalone (a second, small dependency alongside the SDK)
|
|
89
|
+
avoids that and keeps the package graph a clean chain.
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Human vs. machine classification — the hinge of the whole thesis.
|
|
3
|
+
*
|
|
4
|
+
* "Humans read free, forever. Machines pay." Everything downstream trusts this
|
|
5
|
+
* call. The asymmetry that matters:
|
|
6
|
+
*
|
|
7
|
+
* - False positive (human flagged as agent) -> a human hits a 402 paywall.
|
|
8
|
+
* This BREAKS the "open to all" promise. Worst outcome. Bias away from it.
|
|
9
|
+
* - False negative (agent flagged as human) -> a crawler reads free. We lose a
|
|
10
|
+
* micro-toll. Cheap. Tolerable.
|
|
11
|
+
*
|
|
12
|
+
* So the classifier should be conservative: only charge when we're confident the
|
|
13
|
+
* caller is a machine. Signed intent (an agent that *declares* itself via the
|
|
14
|
+
* x402 handshake) is the strongest, cleanest signal — it sidesteps the UA
|
|
15
|
+
* arms race entirely.
|
|
16
|
+
*/
|
|
17
|
+
import type { VerifiedAgent } from "./botAuth.ts";
|
|
18
|
+
export type Verdict = {
|
|
19
|
+
kind: "human" | "agent";
|
|
20
|
+
reason: string;
|
|
21
|
+
confidence: number;
|
|
22
|
+
};
|
|
23
|
+
/** Request facts the classifier reasons over (framework-agnostic). */
|
|
24
|
+
export interface RequestSignals {
|
|
25
|
+
userAgent: string;
|
|
26
|
+
/** Present when the caller already speaks x402 (declared intent to pay). */
|
|
27
|
+
hasPaymentHeader: boolean;
|
|
28
|
+
/** Explicit opt-in header an agent may send: `X-Naulon-Agent: <id>`. */
|
|
29
|
+
declaredAgentId: string | null;
|
|
30
|
+
/** Accept header — browsers ask for text/html; many bots don't. */
|
|
31
|
+
accept: string;
|
|
32
|
+
headers: Record<string, string>;
|
|
33
|
+
/**
|
|
34
|
+
* Cryptographically verified Web Bot Auth identity (RFC 9421/Ed25519), when
|
|
35
|
+
* the request carried a valid signature. Computed once in app.ts — only for
|
|
36
|
+
* requests that present the three signature headers; everyone else pays zero
|
|
37
|
+
* cost and classifies exactly as before.
|
|
38
|
+
*/
|
|
39
|
+
verifiedAgent?: VerifiedAgent | null;
|
|
40
|
+
}
|
|
41
|
+
/** Per-publisher classification policy the gate supplies from `PublisherConfig`. */
|
|
42
|
+
export interface ClassifyPolicy {
|
|
43
|
+
/**
|
|
44
|
+
* Verified search / discovery crawler UA fragments that read FREE for this
|
|
45
|
+
* publisher (their SEO allowlist). Honored ahead of the known-agent list, so an
|
|
46
|
+
* allowlisted crawler is freed even if its UA also looks like a bot — the point
|
|
47
|
+
* is to never toll the crawlers a publisher needs for indexing. Matched
|
|
48
|
+
* case-insensitively against the user-agent. UA is spoofable; verifying crawler
|
|
49
|
+
* identity (reverse DNS / Web Bot Auth) is a later hardening — for now a
|
|
50
|
+
* publisher's own allowlist is the stored intent we honor.
|
|
51
|
+
*/
|
|
52
|
+
seoAllowlist?: string[];
|
|
53
|
+
/**
|
|
54
|
+
* Publisher-charged UA fragments. Honored between the allowlist and the built-in
|
|
55
|
+
* known-agent list: fragments a publisher explicitly charges are classified as
|
|
56
|
+
* agents even when the conservative default would read the UA as browser-shaped or
|
|
57
|
+
* ambiguous (and thus free). Allow wins over charge on overlap.
|
|
58
|
+
*/
|
|
59
|
+
chargeList?: string[];
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* First fragment (case-insensitive substring) of `fragments` found in `ua`,
|
|
63
|
+
* else undefined. The one matching primitive the tri-state policy shares with
|
|
64
|
+
* the allowlist path — swap THIS for verified identity (Web Bot Auth) later.
|
|
65
|
+
*/
|
|
66
|
+
export declare function matchUaFragment(ua: string, fragments: string[] | undefined): string | undefined;
|
|
67
|
+
/**
|
|
68
|
+
* Classify a request as human or agent.
|
|
69
|
+
*
|
|
70
|
+
* TODO(you): implement the core heuristic. A starter is provided so the gate
|
|
71
|
+
* runs today, but this decision shapes the product — own it. Consider:
|
|
72
|
+
*
|
|
73
|
+
* 1. Declared intent first. `signals.hasPaymentHeader` or
|
|
74
|
+
* `signals.declaredAgentId` => almost certainly an agent that WANTS to pay.
|
|
75
|
+
* Treat as high-confidence agent. (Already wired below — keep it first.)
|
|
76
|
+
* 2. Known-bot UA match => agent, but lower confidence (UAs are spoofable and
|
|
77
|
+
* churn). Decide how much weight to give a raw UA match.
|
|
78
|
+
* 3. Browser-shaped requests (Accept: text/html, sec-fetch-* headers,
|
|
79
|
+
* Accept-Language present) => lean human. Decide your "looks like a real
|
|
80
|
+
* browser" bar.
|
|
81
|
+
* 4. The ambiguous middle (empty UA, generic Accept, no browser headers).
|
|
82
|
+
* THIS is the real design call: default-allow (favor humans, lose tolls) or
|
|
83
|
+
* default-charge (capture tolls, risk paywalling a human)? Given the
|
|
84
|
+
* asymmetry above, the starter defaults to HUMAN. Change it if you disagree
|
|
85
|
+
* — and write down why.
|
|
86
|
+
*
|
|
87
|
+
* Return a Verdict with a `reason` (it surfaces in logs + the demo) and a
|
|
88
|
+
* `confidence` in [0,1].
|
|
89
|
+
*
|
|
90
|
+
* `policy` carries per-publisher overrides (the SEO allowlist); the single-tenant
|
|
91
|
+
* default passes none and behaves exactly as before.
|
|
92
|
+
*/
|
|
93
|
+
export declare function classify(signals: RequestSignals, policy?: ClassifyPolicy): Verdict;
|
|
94
|
+
//# sourceMappingURL=agentDetect.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"agentDetect.d.ts","sourceRoot":"","sources":["../src/agentDetect.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAEH,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,cAAc,CAAC;AAElD,MAAM,MAAM,OAAO,GAAG;IAAE,IAAI,EAAE,OAAO,GAAG,OAAO,CAAC;IAAC,MAAM,EAAE,MAAM,CAAC;IAAC,UAAU,EAAE,MAAM,CAAA;CAAE,CAAC;AAEtF,sEAAsE;AACtE,MAAM,WAAW,cAAc;IAC7B,SAAS,EAAE,MAAM,CAAC;IAClB,4EAA4E;IAC5E,gBAAgB,EAAE,OAAO,CAAC;IAC1B,wEAAwE;IACxE,eAAe,EAAE,MAAM,GAAG,IAAI,CAAC;IAC/B,mEAAmE;IACnE,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAChC;;;;;OAKG;IACH,aAAa,CAAC,EAAE,aAAa,GAAG,IAAI,CAAC;CACtC;AAED,oFAAoF;AACpF,MAAM,WAAW,cAAc;IAC7B;;;;;;;;OAQG;IACH,YAAY,CAAC,EAAE,MAAM,EAAE,CAAC;IACxB;;;;;OAKG;IACH,UAAU,CAAC,EAAE,MAAM,EAAE,CAAC;CACvB;AA6CD;;;;GAIG;AACH,wBAAgB,eAAe,CAAC,EAAE,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,GAAG,SAAS,GAAG,MAAM,GAAG,SAAS,CAI/F;AAED;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,wBAAgB,QAAQ,CAAC,OAAO,EAAE,cAAc,EAAE,MAAM,CAAC,EAAE,cAAc,GAAG,OAAO,CA2DlF"}
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Human vs. machine classification — the hinge of the whole thesis.
|
|
3
|
+
*
|
|
4
|
+
* "Humans read free, forever. Machines pay." Everything downstream trusts this
|
|
5
|
+
* call. The asymmetry that matters:
|
|
6
|
+
*
|
|
7
|
+
* - False positive (human flagged as agent) -> a human hits a 402 paywall.
|
|
8
|
+
* This BREAKS the "open to all" promise. Worst outcome. Bias away from it.
|
|
9
|
+
* - False negative (agent flagged as human) -> a crawler reads free. We lose a
|
|
10
|
+
* micro-toll. Cheap. Tolerable.
|
|
11
|
+
*
|
|
12
|
+
* So the classifier should be conservative: only charge when we're confident the
|
|
13
|
+
* caller is a machine. Signed intent (an agent that *declares* itself via the
|
|
14
|
+
* x402 handshake) is the strongest, cleanest signal — it sidesteps the UA
|
|
15
|
+
* arms race entirely.
|
|
16
|
+
*/
|
|
17
|
+
/**
|
|
18
|
+
* Obvious crawler/agent UA fragments. Extend freely — this is a weak signal.
|
|
19
|
+
* Verified against the operators' own published UA docs 2026-07-03 (Anthropic:
|
|
20
|
+
* ClaudeBot/Claude-User/Claude-SearchBot; OpenAI: GPTBot/ChatGPT-User/
|
|
21
|
+
* OAI-SearchBot/OAI-AdsBot). Dropped as undocumented: claude-web, anthropic-ai.
|
|
22
|
+
* Dropped as unmatchable: google-extended (a robots.txt-only token — Google
|
|
23
|
+
* documents NO UA string for it, so a UA fragment could never fire).
|
|
24
|
+
*
|
|
25
|
+
* Two kinds of machine read are charged here:
|
|
26
|
+
* - training/bulk crawlers (gptbot, claudebot, ccbot, bytespider, amazonbot,
|
|
27
|
+
* applebot-extended, meta-externalagent);
|
|
28
|
+
* - user-triggered assistant fetches (chatgpt-user, claude-user,
|
|
29
|
+
* perplexity-user, perplexitybot) — the citation moment itself. These UAs are
|
|
30
|
+
* machine-only (no human browser carries them), so charging them cannot toll
|
|
31
|
+
* a human; an x402-capable agent answers the 402 by paying, which is the
|
|
32
|
+
* product working, not a wall.
|
|
33
|
+
*
|
|
34
|
+
* Note: pure search-indexer UAs (googlebot, bingbot, claude-searchbot,
|
|
35
|
+
* oai-searchbot, …) are deliberately NOT here. Tolling a search crawler silently
|
|
36
|
+
* deindexes the publisher — the opposite of what they want — so indexing reads
|
|
37
|
+
* free by default. A publisher frees additional crawlers (or re-affirms search
|
|
38
|
+
* ones) via `ClassifyPolicy.seoAllowlist`, or charges one via `chargeList`.
|
|
39
|
+
*/
|
|
40
|
+
const KNOWN_AGENT_UA = [
|
|
41
|
+
"gptbot",
|
|
42
|
+
"chatgpt-user",
|
|
43
|
+
"claudebot",
|
|
44
|
+
"claude-user",
|
|
45
|
+
"perplexitybot",
|
|
46
|
+
"perplexity-user",
|
|
47
|
+
"ccbot",
|
|
48
|
+
"bytespider",
|
|
49
|
+
"amazonbot",
|
|
50
|
+
"applebot-extended",
|
|
51
|
+
"meta-externalagent",
|
|
52
|
+
"python-requests",
|
|
53
|
+
"node-fetch",
|
|
54
|
+
"axios",
|
|
55
|
+
"curl",
|
|
56
|
+
"wget",
|
|
57
|
+
"langchain",
|
|
58
|
+
];
|
|
59
|
+
/**
|
|
60
|
+
* First fragment (case-insensitive substring) of `fragments` found in `ua`,
|
|
61
|
+
* else undefined. The one matching primitive the tri-state policy shares with
|
|
62
|
+
* the allowlist path — swap THIS for verified identity (Web Bot Auth) later.
|
|
63
|
+
*/
|
|
64
|
+
export function matchUaFragment(ua, fragments) {
|
|
65
|
+
if (!fragments?.length)
|
|
66
|
+
return undefined;
|
|
67
|
+
const lower = ua.toLowerCase();
|
|
68
|
+
return fragments.find((f) => f.length > 0 && lower.includes(f.toLowerCase()));
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Classify a request as human or agent.
|
|
72
|
+
*
|
|
73
|
+
* TODO(you): implement the core heuristic. A starter is provided so the gate
|
|
74
|
+
* runs today, but this decision shapes the product — own it. Consider:
|
|
75
|
+
*
|
|
76
|
+
* 1. Declared intent first. `signals.hasPaymentHeader` or
|
|
77
|
+
* `signals.declaredAgentId` => almost certainly an agent that WANTS to pay.
|
|
78
|
+
* Treat as high-confidence agent. (Already wired below — keep it first.)
|
|
79
|
+
* 2. Known-bot UA match => agent, but lower confidence (UAs are spoofable and
|
|
80
|
+
* churn). Decide how much weight to give a raw UA match.
|
|
81
|
+
* 3. Browser-shaped requests (Accept: text/html, sec-fetch-* headers,
|
|
82
|
+
* Accept-Language present) => lean human. Decide your "looks like a real
|
|
83
|
+
* browser" bar.
|
|
84
|
+
* 4. The ambiguous middle (empty UA, generic Accept, no browser headers).
|
|
85
|
+
* THIS is the real design call: default-allow (favor humans, lose tolls) or
|
|
86
|
+
* default-charge (capture tolls, risk paywalling a human)? Given the
|
|
87
|
+
* asymmetry above, the starter defaults to HUMAN. Change it if you disagree
|
|
88
|
+
* — and write down why.
|
|
89
|
+
*
|
|
90
|
+
* Return a Verdict with a `reason` (it surfaces in logs + the demo) and a
|
|
91
|
+
* `confidence` in [0,1].
|
|
92
|
+
*
|
|
93
|
+
* `policy` carries per-publisher overrides (the SEO allowlist); the single-tenant
|
|
94
|
+
* default passes none and behaves exactly as before.
|
|
95
|
+
*/
|
|
96
|
+
export function classify(signals, policy) {
|
|
97
|
+
// 1) Declared intent — strongest, spoof-proof-in-spirit signal. Leave first.
|
|
98
|
+
if (signals.hasPaymentHeader) {
|
|
99
|
+
return { kind: "agent", reason: "presented x402 payment header", confidence: 0.99 };
|
|
100
|
+
}
|
|
101
|
+
if (signals.declaredAgentId) {
|
|
102
|
+
return { kind: "agent", reason: `declared agent id ${signals.declaredAgentId}`, confidence: 0.95 };
|
|
103
|
+
}
|
|
104
|
+
// 1b) Verified identity (Web Bot Auth). A valid Ed25519 signature proves WHO
|
|
105
|
+
// is calling, keyed to the operator's own directory — it outranks every
|
|
106
|
+
// UA-derived signal in BOTH directions: a verified allow is the only
|
|
107
|
+
// spoof-proof allowlist (UA rotation can't fake a signature), and a
|
|
108
|
+
// verified agent is charged even behind a browser-shaped UA (rotation
|
|
109
|
+
// can't dodge it). Same fragment vocabulary as the UA lists, matched
|
|
110
|
+
// against the verified directory host — `allow: ["chatgpt.com"]` just
|
|
111
|
+
// works, no new config shape. Publisher block runs in app.ts BEFORE
|
|
112
|
+
// classification (block beats payment) and covers the verified id there.
|
|
113
|
+
if (signals.verifiedAgent) {
|
|
114
|
+
const va = signals.verifiedAgent.agent;
|
|
115
|
+
const allowed = matchUaFragment(va, policy?.seoAllowlist);
|
|
116
|
+
if (allowed) {
|
|
117
|
+
return { kind: "human", reason: `seo allowlist matched verified agent "${allowed}"`, confidence: 0.98 };
|
|
118
|
+
}
|
|
119
|
+
return { kind: "agent", reason: `verified web-bot-auth (${va})`, confidence: 0.98 };
|
|
120
|
+
}
|
|
121
|
+
const ua = signals.userAgent.toLowerCase();
|
|
122
|
+
// 2) SEO allowlist — verified discovery crawlers this publisher wants indexed
|
|
123
|
+
// read FREE. Before the known-bot check so an allowlisted crawler is freed
|
|
124
|
+
// even when its UA also matches a known-bot fragment.
|
|
125
|
+
const allowed = policy?.seoAllowlist?.find((frag) => ua.includes(frag.toLowerCase()));
|
|
126
|
+
if (allowed) {
|
|
127
|
+
return { kind: "human", reason: `seo allowlist matched "${allowed}"`, confidence: 0.9 };
|
|
128
|
+
}
|
|
129
|
+
// 2b) Publisher-charged crawlers — fragments the publisher explicitly tolls.
|
|
130
|
+
// After allow (allow wins) and before the built-in list, so a publisher
|
|
131
|
+
// can charge a crawler the conservative default would read as human.
|
|
132
|
+
const charged = matchUaFragment(signals.userAgent, policy?.chargeList);
|
|
133
|
+
if (charged) {
|
|
134
|
+
return { kind: "agent", reason: `publisher charge policy matched "${charged}"`, confidence: 0.8 };
|
|
135
|
+
}
|
|
136
|
+
// 3) Known-bot UA — weak, spoofable signal.
|
|
137
|
+
const hit = KNOWN_AGENT_UA.find((frag) => ua.includes(frag));
|
|
138
|
+
if (hit) {
|
|
139
|
+
return { kind: "agent", reason: `user-agent matched "${hit}"`, confidence: 0.8 };
|
|
140
|
+
}
|
|
141
|
+
// 4) Browser-shaped => human.
|
|
142
|
+
const looksBrowser = signals.accept.includes("text/html") || "sec-fetch-mode" in signals.headers;
|
|
143
|
+
if (looksBrowser) {
|
|
144
|
+
return { kind: "human", reason: "browser-shaped request", confidence: 0.85 };
|
|
145
|
+
}
|
|
146
|
+
// 5) Ambiguous middle. Starter favors humans (see asymmetry note).
|
|
147
|
+
return { kind: "human", reason: "ambiguous; defaulting to human (free)", confidence: 0.4 };
|
|
148
|
+
}
|
|
149
|
+
//# sourceMappingURL=agentDetect.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"agentDetect.js","sourceRoot":"","sources":["../src/agentDetect.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Web Bot Auth — RFC 9421 HTTP Message Signatures verifier, scoped to the
|
|
3
|
+
* web-bot-auth profile (draft-meunier-webbotauth-httpsig-protocol-00 +
|
|
4
|
+
* Cloudflare's deployed operational profile).
|
|
5
|
+
*
|
|
6
|
+
* Hand-rolled minimal structured-field subset by design: the full RFC 8941
|
|
7
|
+
* grammar is far more than the three headers need, and the one candidate npm
|
|
8
|
+
* package (web-bot-auth@0.1.x) is a 4-package unaudited tree. This module is
|
|
9
|
+
* self-contained on node:crypto.
|
|
10
|
+
*/
|
|
11
|
+
/** One covered component in the signature input: a name plus optional ;key=. */
|
|
12
|
+
export interface CoveredComponent {
|
|
13
|
+
name: string;
|
|
14
|
+
key?: string;
|
|
15
|
+
/**
|
|
16
|
+
* The component identifier exactly as the signer serialized it (quoted name
|
|
17
|
+
* + params). Base lines reuse it verbatim so the base is byte-exact.
|
|
18
|
+
*/
|
|
19
|
+
raw: string;
|
|
20
|
+
}
|
|
21
|
+
export interface SignatureInputEntry {
|
|
22
|
+
label: string;
|
|
23
|
+
components: CoveredComponent[];
|
|
24
|
+
params: {
|
|
25
|
+
created?: number;
|
|
26
|
+
expires?: number;
|
|
27
|
+
keyid?: string;
|
|
28
|
+
tag?: string;
|
|
29
|
+
alg?: string;
|
|
30
|
+
nonce?: string;
|
|
31
|
+
};
|
|
32
|
+
/**
|
|
33
|
+
* The member's verbatim SF text (everything after `label=`). Reused as the
|
|
34
|
+
* `@signature-params` serialization so the base is byte-exact with what the
|
|
35
|
+
* signer signed — no canonical re-serialization bugs.
|
|
36
|
+
*/
|
|
37
|
+
rawMemberText: string;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Parse a `Signature-Input` header: an SF dictionary whose members are inner
|
|
41
|
+
* lists of covered components followed by the signature params. Returns null
|
|
42
|
+
* on any grammar trouble — the caller treats that as an invalid signature.
|
|
43
|
+
*/
|
|
44
|
+
export declare function parseSignatureInput(value: string): SignatureInputEntry[] | null;
|
|
45
|
+
/**
|
|
46
|
+
* Parse a `Signature` header: an SF dictionary of byte sequences
|
|
47
|
+
* (label=:base64:). Returns null on grammar trouble.
|
|
48
|
+
*/
|
|
49
|
+
export declare function parseSignatureHeader(value: string): Map<string, Uint8Array> | null;
|
|
50
|
+
/**
|
|
51
|
+
* Parse a `Signature-Agent` header into the directory URL.
|
|
52
|
+
*
|
|
53
|
+
* Accepts the CF operational profile's plain quoted string (what deployed
|
|
54
|
+
* signers send — CF explicitly REJECTS the dictionary form), tolerates the
|
|
55
|
+
* rev-00 dictionary form (first member wins), and normalizes a bare host to
|
|
56
|
+
* https. Returns null when neither shape parses or the URL is unusable.
|
|
57
|
+
*/
|
|
58
|
+
export declare function parseSignatureAgent(value: string): string | null;
|
|
59
|
+
/** The request facts the base builder serializes components from. */
|
|
60
|
+
export interface RequestFacts {
|
|
61
|
+
/** Host the request targeted (the `Host` header / URL authority). */
|
|
62
|
+
authority: string;
|
|
63
|
+
method: string;
|
|
64
|
+
path: string;
|
|
65
|
+
targetUri: string;
|
|
66
|
+
/** Lowercase header-name → raw value. */
|
|
67
|
+
headers: Record<string, string>;
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Build the RFC 9421 signature base for one Signature-Input entry. Returns
|
|
71
|
+
* null when a covered component is unsupported or unresolvable — the caller
|
|
72
|
+
* treats that as an invalid signature. Supported: `@authority`, `@method`,
|
|
73
|
+
* `@path`, `@target-uri`, plain headers, and dictionary members via `;key=` —
|
|
74
|
+
* everything real web-bot-auth signers cover (CF profile minimum: @authority).
|
|
75
|
+
*/
|
|
76
|
+
export declare function buildSignatureBase(entry: SignatureInputEntry, facts: RequestFacts): string | null;
|
|
77
|
+
/**
|
|
78
|
+
* RFC 7638 JWK thumbprint of an Ed25519 public key (the profile's keyid).
|
|
79
|
+
* Required members in lexicographic order: crv, kty, x.
|
|
80
|
+
*/
|
|
81
|
+
export declare function jwkThumbprint(x: string): string;
|
|
82
|
+
/** Ed25519-verify `sig` over the UTF-8 `base` with a raw JWK x. Never throws. */
|
|
83
|
+
export declare function verifyEd25519(base: string, sig: Uint8Array, x: string): boolean;
|
|
84
|
+
/** keyid (RFC 7638 thumbprint) → raw Ed25519 JWK x, for one directory. */
|
|
85
|
+
export type DirectoryKeys = Map<string, string>;
|
|
86
|
+
/**
|
|
87
|
+
* LRU + TTL cache of fetched key directories. `null` is a NEGATIVE entry
|
|
88
|
+
* (directory unreachable/unusable) with its own shorter TTL, so a broken
|
|
89
|
+
* operator doesn't get hammered but recovers quickly. One fetch per operator
|
|
90
|
+
* per TTL — verification cost amortizes to zero on the hot path.
|
|
91
|
+
*/
|
|
92
|
+
export declare class DirectoryCache {
|
|
93
|
+
private readonly entries;
|
|
94
|
+
private readonly posTtlMs;
|
|
95
|
+
private readonly negTtlMs;
|
|
96
|
+
private readonly cap;
|
|
97
|
+
private readonly now;
|
|
98
|
+
constructor(opts?: {
|
|
99
|
+
posTtlMs?: number;
|
|
100
|
+
negTtlMs?: number;
|
|
101
|
+
cap?: number;
|
|
102
|
+
now?: () => number;
|
|
103
|
+
});
|
|
104
|
+
/** undefined = no usable entry; null = cached negative; Map = cached keys. */
|
|
105
|
+
get(url: string): DirectoryKeys | null | undefined;
|
|
106
|
+
set(url: string, keys: DirectoryKeys | null): void;
|
|
107
|
+
}
|
|
108
|
+
export interface BotAuthOptions {
|
|
109
|
+
fetchFn?: typeof fetch;
|
|
110
|
+
cache?: DirectoryCache;
|
|
111
|
+
/** epoch ms clock, injectable for tests. */
|
|
112
|
+
now?: () => number;
|
|
113
|
+
/**
|
|
114
|
+
* Permit http:// + localhost directories — LOCAL TEST FIXTURES ONLY (the
|
|
115
|
+
* signer walk serves its directory from a loopback port). Never in prod.
|
|
116
|
+
*/
|
|
117
|
+
allowInsecureHttp?: boolean;
|
|
118
|
+
clockSkewSec?: number;
|
|
119
|
+
}
|
|
120
|
+
export interface VerifiedAgent {
|
|
121
|
+
/** The operator's directory host — the identity policy fragments match on. */
|
|
122
|
+
agent: string;
|
|
123
|
+
keyid: string;
|
|
124
|
+
}
|
|
125
|
+
export type BotAuthOutcome =
|
|
126
|
+
/** No web-bot-auth signature on the request (or signed for another protocol). */
|
|
127
|
+
{
|
|
128
|
+
status: "absent";
|
|
129
|
+
} | {
|
|
130
|
+
status: "verified";
|
|
131
|
+
agent: VerifiedAgent;
|
|
132
|
+
}
|
|
133
|
+
/** A signature was presented and is WRONG — masquerade telemetry. */
|
|
134
|
+
| {
|
|
135
|
+
status: "invalid";
|
|
136
|
+
reason: string;
|
|
137
|
+
}
|
|
138
|
+
/** Verifier/network trouble — fail open, treat as unsigned. */
|
|
139
|
+
| {
|
|
140
|
+
status: "unverified";
|
|
141
|
+
reason: string;
|
|
142
|
+
};
|
|
143
|
+
/**
|
|
144
|
+
* Verify a request's Web Bot Auth signature, end to end.
|
|
145
|
+
*
|
|
146
|
+
* Outcome discipline (the product decision, not just plumbing):
|
|
147
|
+
* - absent → no signature / not this protocol: the UA path proceeds as if
|
|
148
|
+
* this module didn't exist (byte-identical regression bar).
|
|
149
|
+
* - verified → cryptographic identity: classify() gets `verifiedAgent`.
|
|
150
|
+
* - invalid → a PRESENTED signature failed: still served via the UA path
|
|
151
|
+
* (fail-open), but flagged `sigInvalid` — masquerade telemetry.
|
|
152
|
+
* - unverified → OUR side couldn't verify (directory down, network): fail
|
|
153
|
+
* open with no flag; a verifier outage must never block traffic.
|
|
154
|
+
*
|
|
155
|
+
* No nonce/replay cache in slice 1: the created/expires window (CF profile
|
|
156
|
+
* ~1 minute) bounds replay; a replayed signature buys the same 402 quote.
|
|
157
|
+
*/
|
|
158
|
+
export declare function verifyBotAuth(facts: RequestFacts, opts?: BotAuthOptions): Promise<BotAuthOutcome>;
|
|
159
|
+
//# sourceMappingURL=botAuth.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"botAuth.d.ts","sourceRoot":"","sources":["../src/botAuth.ts"],"names":[],"mappings":"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"}
|