agent402-tollbooth 0.6.0 → 0.6.2
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/edge.js +36 -1
- package/package.json +1 -1
- package/replay.js +53 -0
package/edge.js
CHANGED
|
@@ -130,6 +130,11 @@ export function createEdgeTollbooth(config = {}) {
|
|
|
130
130
|
powDifficulty,
|
|
131
131
|
secret,
|
|
132
132
|
store,
|
|
133
|
+
// Same contract as the Node build's verifyX402: (request, requirements) =>
|
|
134
|
+
// boolean | Promise<boolean>. Without it this build has NO way to accept a
|
|
135
|
+
// payment, which is why the accepts block below is withheld when it is
|
|
136
|
+
// absent - see the note there.
|
|
137
|
+
verifyX402 = null,
|
|
133
138
|
botUserAgents = AI_BOTS,
|
|
134
139
|
// "bots" (default, charge AI crawler UAs) | "all" (charge all but free()) |
|
|
135
140
|
// "strict" (charge anything that isn't a real-browser request). An explicit
|
|
@@ -148,6 +153,16 @@ export function createEdgeTollbooth(config = {}) {
|
|
|
148
153
|
message = "This resource charges automated / AI clients per request. Humans browse free; bots pay in USDC via x402 or by solving a proof-of-work.",
|
|
149
154
|
} = config;
|
|
150
155
|
|
|
156
|
+
// Say it at construction, not when a buyer gives up. An operator who sets a
|
|
157
|
+
// payTo here reasonably believes they are charging; without a verifier this
|
|
158
|
+
// build can only ever serve proof-of-work, and the price would be fiction.
|
|
159
|
+
if (payTo && !verifyX402) {
|
|
160
|
+
console.warn(
|
|
161
|
+
"agent402-tollbooth: payTo is set but no verifyX402 was supplied, so this edge build cannot accept a USDC payment. " +
|
|
162
|
+
"The USDC quote is being WITHHELD from the 402 (proof-of-work still works). Pass verifyX402 to charge, or drop payTo to stop implying it."
|
|
163
|
+
);
|
|
164
|
+
}
|
|
165
|
+
|
|
151
166
|
const isBot = makeBotMatcher(botUserAgents);
|
|
152
167
|
const powEngine = pow ? createEdgePow({ secret, difficulty: powDifficulty, store }) : null;
|
|
153
168
|
const mem = memorySink();
|
|
@@ -194,6 +209,21 @@ export function createEdgeTollbooth(config = {}) {
|
|
|
194
209
|
// stays a plain URL for wire compatibility).
|
|
195
210
|
const powResource = `${request.method || "GET"} ${resource}`;
|
|
196
211
|
|
|
212
|
+
// An x402 payment presented on either header name. @x402/core emits
|
|
213
|
+
// PAYMENT-SIGNATURE while older clients send X-PAYMENT, and a gate that
|
|
214
|
+
// reads only one of them refuses buyers who did everything right.
|
|
215
|
+
const paymentHeader = request.headers.get("payment-signature") || request.headers.get("x-payment");
|
|
216
|
+
if (paymentHeader && verifyX402) {
|
|
217
|
+
let paid = false;
|
|
218
|
+
try {
|
|
219
|
+
paid = await verifyX402(request, { price, network, asset, payTo, resource });
|
|
220
|
+
} catch {
|
|
221
|
+
// Fail CLOSED: a verifier that throws must not open the gate.
|
|
222
|
+
paid = false;
|
|
223
|
+
}
|
|
224
|
+
if (paid) { incr("paid"); return null; }
|
|
225
|
+
}
|
|
226
|
+
|
|
197
227
|
const sol = request.headers.get("x-pow-solution");
|
|
198
228
|
if (powEngine && sol) {
|
|
199
229
|
const r = await powEngine.verify(sol, powResource);
|
|
@@ -204,7 +234,12 @@ export function createEdgeTollbooth(config = {}) {
|
|
|
204
234
|
const body = {
|
|
205
235
|
error: "Payment Required",
|
|
206
236
|
message,
|
|
207
|
-
|
|
237
|
+
// Only advertise USDC when this build can actually ACCEPT it. Emitting an
|
|
238
|
+
// accepts block with no verifier configured published a price that no
|
|
239
|
+
// code path could honour: a crawler that paid correctly was 402'd
|
|
240
|
+
// forever, money never moved, and the operator believed they were
|
|
241
|
+
// charging. Quoting a price you cannot take is worse than quoting none.
|
|
242
|
+
accepts: (payTo && verifyX402) ? [{ scheme: "exact", network, maxAmountRequired: String(price), asset, payTo, resource }] : [],
|
|
208
243
|
};
|
|
209
244
|
if (powEngine) body.proofOfWork = await powEngine.challenge(powResource);
|
|
210
245
|
return new Response(JSON.stringify(body), {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "agent402-tollbooth",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.2",
|
|
4
4
|
"description": "Open-source, self-hostable x402 pay-per-crawl tollbooth: charge AI bots per request (USDC via x402 — or USDG on Robinhood Chain — or free proof-of-work) while humans browse free. Drop-in Express middleware or reverse proxy — no Cloudflare, no Stripe, no signup.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "index.js",
|
package/replay.js
CHANGED
|
@@ -100,8 +100,61 @@ export function redisReplayStore(client, { prefix = "tollbooth:pow:" } = {}) {
|
|
|
100
100
|
// mode of guessing wrong is not an exception but a SET without the NX flag,
|
|
101
101
|
// which overwrites the record and grants every replay.
|
|
102
102
|
const isIoredis = typeof client.defineCommand === "function";
|
|
103
|
+
const isNodeRedis = typeof client.sendCommand === "function" || typeof client.sSet === "function" || Boolean(client.options && client.options.socket);
|
|
104
|
+
if (!isIoredis && !isNodeRedis) {
|
|
105
|
+
// Fail CLOSED on an unrecognised client instead of guessing a SET shape.
|
|
106
|
+
// The failure mode of a wrong guess is not an exception: the flag argument
|
|
107
|
+
// is silently ignored, the key is overwritten every time, and single-use
|
|
108
|
+
// stops existing while everything still looks healthy.
|
|
109
|
+
throw new TypeError(
|
|
110
|
+
"redisReplayStore(client): could not identify this client as node-redis or ioredis. " +
|
|
111
|
+
"Pass one of those, or supply your own { claim(token, expiresAtMs) } store - guessing the SET argument shape risks dropping NX and granting every replay."
|
|
112
|
+
);
|
|
113
|
+
}
|
|
114
|
+
// Prove NX is actually being honoured, once, before this store is trusted.
|
|
115
|
+
// Detection above is a heuristic; this is the observation. Two claims of a
|
|
116
|
+
// throwaway key must produce a win and then a refusal. Anything else means
|
|
117
|
+
// single-use is not working, and the store refuses rather than pretending.
|
|
118
|
+
// Tri-state on PURPOSE: null = not yet observed, true = proven, false = proven
|
|
119
|
+
// BROKEN and permanently refused.
|
|
120
|
+
//
|
|
121
|
+
// A verdict of false must keep throwing on EVERY later call, not just the one
|
|
122
|
+
// that discovered it. Memoising false and returning it as a resolved value
|
|
123
|
+
// would make the store refuse exactly once and then grant every replay
|
|
124
|
+
// forever - the precise failure this self-test exists to prevent, reached one
|
|
125
|
+
// request later. A caller that solves one challenge could then replay that
|
|
126
|
+
// token for its whole TTL.
|
|
127
|
+
//
|
|
128
|
+
// A probe that THROWS (Redis unreachable) leaves the verdict at null so the
|
|
129
|
+
// next call re-probes: a transient outage must not be remembered as a verdict.
|
|
130
|
+
let nxProven = null;
|
|
131
|
+
const NX_BROKEN =
|
|
132
|
+
"redisReplayStore: SET NX is not honoured by this client, so proof-of-work single-use cannot be enforced. " +
|
|
133
|
+
"The gate refuses rather than granting replays. Pass a client whose SET supports NX, or supply your own { claim(token, expiresAtMs) } store.";
|
|
134
|
+
const proveNx = async () => {
|
|
135
|
+
if (nxProven === true) return true;
|
|
136
|
+
if (nxProven === false) throw new Error(NX_BROKEN);
|
|
137
|
+
const probe = `${prefix}__nxselftest__${Math.random().toString(36).slice(2)}${Date.now()}`;
|
|
138
|
+
const px = 5000;
|
|
139
|
+
const set = (k) => (isIoredis ? client.set(k, "1", "PX", px, "NX") : client.set(k, "1", { NX: true, PX: px }));
|
|
140
|
+
// Deliberately NOT wrapped: a throw here leaves nxProven null (re-probe).
|
|
141
|
+
const first = await set(probe);
|
|
142
|
+
const second = await set(probe);
|
|
143
|
+
const firstWon = first === "OK" || first === true;
|
|
144
|
+
const secondRefused = !(second === "OK" || second === true);
|
|
145
|
+
nxProven = firstWon && secondRefused;
|
|
146
|
+
if (!nxProven) {
|
|
147
|
+
throw new Error(
|
|
148
|
+
`${NX_BROKEN} (first claim won: ${firstWon}, second refused: ${secondRefused})`
|
|
149
|
+
);
|
|
150
|
+
}
|
|
151
|
+
return true;
|
|
152
|
+
};
|
|
103
153
|
return {
|
|
104
154
|
claim: async (token, expiresAtMs) => {
|
|
155
|
+
// Respect the verdict: proveNx throws when unproven, but assert anyway so
|
|
156
|
+
// no future refactor can turn "unproven" into a silently ignored value.
|
|
157
|
+
if ((await proveNx()) !== true) throw new Error(NX_BROKEN);
|
|
105
158
|
const key = prefix + token;
|
|
106
159
|
// Floor of 1ms: Redis rejects a non-positive PX. pow.js has already
|
|
107
160
|
// refused anything actually expired, so a tiny window here just means the
|