agent402-tollbooth 0.6.0 → 0.6.1
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 +35 -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.1",
|
|
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,43 @@ 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
|
+
let nxProven = null;
|
|
119
|
+
const proveNx = async () => {
|
|
120
|
+
if (nxProven !== null) return nxProven;
|
|
121
|
+
const probe = `${prefix}__nxselftest__${Math.random().toString(36).slice(2)}${Date.now()}`;
|
|
122
|
+
const px = 5000;
|
|
123
|
+
const set = (k) => (isIoredis ? client.set(k, "1", "PX", px, "NX") : client.set(k, "1", { NX: true, PX: px }));
|
|
124
|
+
const first = await set(probe);
|
|
125
|
+
const second = await set(probe);
|
|
126
|
+
const firstWon = first === "OK" || first === true;
|
|
127
|
+
const secondRefused = !(second === "OK" || second === true);
|
|
128
|
+
nxProven = firstWon && secondRefused;
|
|
129
|
+
if (!nxProven) {
|
|
130
|
+
throw new Error(
|
|
131
|
+
"redisReplayStore: SET NX did not behave as required (first claim won: " + firstWon + ", second refused: " + secondRefused + "). " +
|
|
132
|
+
"Proof-of-work single-use cannot be enforced against this client, so the gate refuses rather than granting replays."
|
|
133
|
+
);
|
|
134
|
+
}
|
|
135
|
+
return nxProven;
|
|
136
|
+
};
|
|
103
137
|
return {
|
|
104
138
|
claim: async (token, expiresAtMs) => {
|
|
139
|
+
await proveNx();
|
|
105
140
|
const key = prefix + token;
|
|
106
141
|
// Floor of 1ms: Redis rejects a non-positive PX. pow.js has already
|
|
107
142
|
// refused anything actually expired, so a tiny window here just means the
|