agent402-tollbooth 0.1.3 → 0.1.4
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 +13 -6
- package/package.json +1 -1
- package/worker.js +8 -2
package/edge.js
CHANGED
|
@@ -37,22 +37,28 @@ function constEq(a, b) {
|
|
|
37
37
|
/**
|
|
38
38
|
* Web Crypto proof-of-work. `secret` is required (edge runtimes can't keep a
|
|
39
39
|
* random per-process secret across invocations). `store` is an optional
|
|
40
|
-
* single-use replay backend
|
|
41
|
-
*
|
|
40
|
+
* single-use replay backend: { claim(token, expMs) => bool|Promise<bool> } that
|
|
41
|
+
* atomically returns true only the first time a token is seen — e.g. a Cloudflare
|
|
42
|
+
* KV wrapper; defaults to in-isolate memory.
|
|
42
43
|
*/
|
|
43
44
|
export function createEdgePow({ secret, difficulty = 18, ttlMs = 5 * 60 * 1000, store } = {}) {
|
|
44
45
|
if (!secret) throw new Error("createEdgePow requires a stable `secret` string");
|
|
45
46
|
const mem = new Map();
|
|
46
47
|
const MAX = 50_000; // bound memory: edge runtimes have no timer to sweep, so prune on write
|
|
47
48
|
const used = store || {
|
|
48
|
-
|
|
49
|
-
|
|
49
|
+
// Atomic single-use claim. The body is synchronous (no await), so two
|
|
50
|
+
// concurrent claims of the same token cannot both win within an isolate —
|
|
51
|
+
// closing the TOCTOU that a separate has()+add() would open.
|
|
52
|
+
claim: (k, exp) => {
|
|
53
|
+
const e = mem.get(k);
|
|
54
|
+
if (e && e >= Date.now()) return false; // a still-valid entry => already used
|
|
50
55
|
if (mem.size >= MAX) {
|
|
51
56
|
const now = Date.now();
|
|
52
57
|
for (const [kk, ee] of mem) if (ee < now) mem.delete(kk); // drop expired first
|
|
53
58
|
if (mem.size >= MAX) mem.delete(mem.keys().next().value); // hard cap: evict oldest
|
|
54
59
|
}
|
|
55
60
|
mem.set(k, exp);
|
|
61
|
+
return true;
|
|
56
62
|
},
|
|
57
63
|
};
|
|
58
64
|
|
|
@@ -88,10 +94,11 @@ export function createEdgePow({ secret, difficulty = 18, ttlMs = 5 * 60 * 1000,
|
|
|
88
94
|
if (!constEq(sig, expected)) return { ok: false, reason: "bad signature" };
|
|
89
95
|
if (res !== resource) return { ok: false, reason: "wrong resource" };
|
|
90
96
|
if (Date.now() > Number(expStr)) return { ok: false, reason: "expired" };
|
|
91
|
-
if (await used.has(token)) return { ok: false, reason: "already used" };
|
|
92
97
|
const h = await sha256(`${chal}:${nonce}`);
|
|
93
98
|
if (leadingZeroBits(h) < Number(diffStr)) return { ok: false, reason: "insufficient work" };
|
|
94
|
-
|
|
99
|
+
// Atomically claim single-use only AFTER the work is validated, so an invalid
|
|
100
|
+
// attempt never consumes the token and concurrent dupes can't both pass.
|
|
101
|
+
if (!(await used.claim(token, Number(expStr)))) return { ok: false, reason: "already used" };
|
|
95
102
|
return { ok: true };
|
|
96
103
|
}
|
|
97
104
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "agent402-tollbooth",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.4",
|
|
4
4
|
"description": "Open-source, self-hostable x402 pay-per-crawl tollbooth: charge AI bots per request (USDC via x402, 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/worker.js
CHANGED
|
@@ -13,8 +13,14 @@
|
|
|
13
13
|
import { createEdgeTollbooth } from "./edge.js";
|
|
14
14
|
|
|
15
15
|
const kvStore = (kv) => ({
|
|
16
|
-
|
|
17
|
-
|
|
16
|
+
// Best-effort atomic claim. KV has no native compare-and-set, so this is
|
|
17
|
+
// get-then-put (eventually consistent); for strict single-use, back it with a
|
|
18
|
+
// Durable Object. Returns true only the first time a token is seen.
|
|
19
|
+
claim: async (k, expMs) => {
|
|
20
|
+
if ((await kv.get(k)) != null) return false;
|
|
21
|
+
await kv.put(k, "1", { expiration: Math.ceil(expMs / 1000) });
|
|
22
|
+
return true;
|
|
23
|
+
},
|
|
18
24
|
});
|
|
19
25
|
|
|
20
26
|
export default {
|