@elizaos/plugin-x402 2.0.0-alpha.6 → 2.0.3-beta.5
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/LICENSE +21 -0
- package/README.md +151 -0
- package/dist/index.d.ts +57 -2
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +2542 -1915
- package/dist/index.js.map +14 -21
- package/dist/payment-config.d.ts +256 -0
- package/dist/payment-config.d.ts.map +1 -0
- package/dist/payment-wrapper.d.ts +42 -0
- package/dist/payment-wrapper.d.ts.map +1 -0
- package/dist/startup-validator.d.ts +28 -0
- package/dist/startup-validator.d.ts.map +1 -0
- package/dist/types.d.ts +158 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/x402-facilitator-binding.d.ts +9 -0
- package/dist/x402-facilitator-binding.d.ts.map +1 -0
- package/dist/x402-replay-durable.d.ts +30 -0
- package/dist/x402-replay-durable.d.ts.map +1 -0
- package/dist/x402-replay-guard.d.ts +28 -0
- package/dist/x402-replay-guard.d.ts.map +1 -0
- package/dist/x402-replay-keys.d.ts +21 -0
- package/dist/x402-replay-keys.d.ts.map +1 -0
- package/dist/x402-resolve.d.ts +6 -0
- package/dist/x402-resolve.d.ts.map +1 -0
- package/dist/x402-standard-payment.d.ts +130 -0
- package/dist/x402-standard-payment.d.ts.map +1 -0
- package/dist/x402-types.d.ts +130 -0
- package/dist/x402-types.d.ts.map +1 -0
- package/package.json +63 -94
- package/src/__tests__/core-test-mock.ts +10 -0
- package/src/index.ts +115 -0
- package/src/payment-config.ts +737 -0
- package/src/payment-wrapper.test.ts +234 -0
- package/src/payment-wrapper.ts +1997 -0
- package/src/startup-validator.test.ts +86 -0
- package/src/startup-validator.ts +351 -0
- package/src/types.ts +177 -0
- package/src/x402-facilitator-binding.ts +104 -0
- package/src/x402-replay-durable.ts +320 -0
- package/src/x402-replay-guard.ts +165 -0
- package/src/x402-replay-keys.ts +151 -0
- package/src/x402-resolve.ts +43 -0
- package/src/x402-standard-payment.ts +519 -0
- package/src/x402-types.ts +376 -0
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
2
|
+
import {
|
|
3
|
+
applyPaymentProtection,
|
|
4
|
+
isRoutePaymentWrapped,
|
|
5
|
+
} from "./payment-wrapper.js";
|
|
6
|
+
|
|
7
|
+
function makeResponse() {
|
|
8
|
+
const res = {
|
|
9
|
+
headersSent: false,
|
|
10
|
+
statusCode: 200,
|
|
11
|
+
headers: new Map<string, string>(),
|
|
12
|
+
setHeader: vi.fn((name: string, value: string) => {
|
|
13
|
+
res.headers.set(name, value);
|
|
14
|
+
return res;
|
|
15
|
+
}),
|
|
16
|
+
status: vi.fn((code: number) => {
|
|
17
|
+
res.statusCode = code;
|
|
18
|
+
return res;
|
|
19
|
+
}),
|
|
20
|
+
json: vi.fn((body: unknown) => body),
|
|
21
|
+
};
|
|
22
|
+
return res;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function decodePaymentRequiredHeader(res: ReturnType<typeof makeResponse>) {
|
|
26
|
+
const encoded = res.headers.get("PAYMENT-REQUIRED");
|
|
27
|
+
expect(encoded).toEqual(expect.any(String));
|
|
28
|
+
return JSON.parse(Buffer.from(String(encoded), "base64").toString("utf8")) as
|
|
29
|
+
| Record<string, unknown>
|
|
30
|
+
| undefined;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
describe("applyPaymentProtection", () => {
|
|
34
|
+
afterEach(() => {
|
|
35
|
+
vi.restoreAllMocks();
|
|
36
|
+
vi.unstubAllGlobals();
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it("rejects non-array route input", () => {
|
|
40
|
+
expect(() => applyPaymentProtection({} as never)).toThrow(
|
|
41
|
+
"routes must be an array",
|
|
42
|
+
);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it("leaves unprotected routes unchanged", () => {
|
|
46
|
+
const route = { path: "/free", type: "GET", handler: vi.fn() } as never;
|
|
47
|
+
|
|
48
|
+
const [result] = applyPaymentProtection([route]);
|
|
49
|
+
|
|
50
|
+
expect(result).toBe(route);
|
|
51
|
+
expect(isRoutePaymentWrapped(result)).toBe(false);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it("wraps protected routes and returns payment-required responses", async () => {
|
|
55
|
+
const handler = vi.fn();
|
|
56
|
+
const route = {
|
|
57
|
+
path: "/paid",
|
|
58
|
+
type: "GET",
|
|
59
|
+
handler,
|
|
60
|
+
x402: { priceInCents: 25, paymentConfigs: ["base_usdc"] },
|
|
61
|
+
} as never;
|
|
62
|
+
const runtime = { agentId: "agent-1", emitEvent: vi.fn() };
|
|
63
|
+
const res = makeResponse();
|
|
64
|
+
|
|
65
|
+
const [wrapped] = applyPaymentProtection([route], {
|
|
66
|
+
agentId: "agent-1",
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
expect(wrapped).not.toBe(route);
|
|
70
|
+
expect(isRoutePaymentWrapped(wrapped)).toBe(true);
|
|
71
|
+
|
|
72
|
+
await wrapped.handler?.(
|
|
73
|
+
{ method: "GET", headers: {}, query: {} } as never,
|
|
74
|
+
res as never,
|
|
75
|
+
runtime as never,
|
|
76
|
+
);
|
|
77
|
+
|
|
78
|
+
expect(handler).not.toHaveBeenCalled();
|
|
79
|
+
expect(res.status).toHaveBeenCalledWith(402);
|
|
80
|
+
expect(res.json).toHaveBeenCalledWith(
|
|
81
|
+
expect.objectContaining({ x402Version: 1 }),
|
|
82
|
+
);
|
|
83
|
+
expect(runtime.emitEvent).toHaveBeenCalledWith(
|
|
84
|
+
"PAYMENT_REQUIRED",
|
|
85
|
+
expect.objectContaining({
|
|
86
|
+
path: "/paid",
|
|
87
|
+
reason: "payment_required",
|
|
88
|
+
}),
|
|
89
|
+
);
|
|
90
|
+
expect(res.headers.get("Access-Control-Expose-Headers")).toContain(
|
|
91
|
+
"PAYMENT-REQUIRED",
|
|
92
|
+
);
|
|
93
|
+
expect(decodePaymentRequiredHeader(res)).toEqual(
|
|
94
|
+
expect.objectContaining({
|
|
95
|
+
error: "Payment Required",
|
|
96
|
+
}),
|
|
97
|
+
);
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it("handles route requests with missing optional headers and query", async () => {
|
|
101
|
+
const handler = vi.fn();
|
|
102
|
+
const route = {
|
|
103
|
+
path: "/paid",
|
|
104
|
+
type: "POST",
|
|
105
|
+
handler,
|
|
106
|
+
x402: { priceInCents: 10, paymentConfigs: ["base_usdc"] },
|
|
107
|
+
} as never;
|
|
108
|
+
const runtime = { agentId: "agent-1", emitEvent: vi.fn() };
|
|
109
|
+
const res = makeResponse();
|
|
110
|
+
const [wrapped] = applyPaymentProtection([route], {
|
|
111
|
+
agentId: "agent-1",
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
await wrapped.handler?.({ method: "POST" } as never, res as never, runtime as never);
|
|
115
|
+
|
|
116
|
+
expect(handler).not.toHaveBeenCalled();
|
|
117
|
+
expect(res.status).toHaveBeenCalledWith(402);
|
|
118
|
+
expect(runtime.emitEvent).toHaveBeenCalledWith(
|
|
119
|
+
"PAYMENT_REQUIRED",
|
|
120
|
+
expect.objectContaining({ reason: "payment_required" }),
|
|
121
|
+
);
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
it("returns payment-required responses when validators fail before proof checks", async () => {
|
|
125
|
+
const handler = vi.fn();
|
|
126
|
+
const route = {
|
|
127
|
+
path: "/paid",
|
|
128
|
+
type: "GET",
|
|
129
|
+
handler,
|
|
130
|
+
validator: vi.fn(async () => ({
|
|
131
|
+
valid: false,
|
|
132
|
+
error: {
|
|
133
|
+
message: "bad request",
|
|
134
|
+
details: { field: "amount" },
|
|
135
|
+
},
|
|
136
|
+
})),
|
|
137
|
+
x402: { priceInCents: 10, paymentConfigs: ["base_usdc"] },
|
|
138
|
+
} as never;
|
|
139
|
+
const runtime = { agentId: "agent-1", emitEvent: vi.fn() };
|
|
140
|
+
const res = makeResponse();
|
|
141
|
+
const [wrapped] = applyPaymentProtection([route], {
|
|
142
|
+
agentId: "agent-1",
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
await wrapped.handler?.(
|
|
146
|
+
{
|
|
147
|
+
method: "GET",
|
|
148
|
+
headers: { "x-payment-id": "valid-looking-id" },
|
|
149
|
+
query: {},
|
|
150
|
+
} as never,
|
|
151
|
+
res as never,
|
|
152
|
+
runtime as never,
|
|
153
|
+
);
|
|
154
|
+
|
|
155
|
+
expect(handler).not.toHaveBeenCalled();
|
|
156
|
+
expect(route.validator).toHaveBeenCalledTimes(1);
|
|
157
|
+
expect(res.status).toHaveBeenCalledWith(402);
|
|
158
|
+
expect(res.json).toHaveBeenCalledWith(
|
|
159
|
+
expect.objectContaining({
|
|
160
|
+
error: 'bad request: {"field":"amount"}',
|
|
161
|
+
}),
|
|
162
|
+
);
|
|
163
|
+
expect(runtime.emitEvent).toHaveBeenCalledWith(
|
|
164
|
+
"PAYMENT_REQUIRED",
|
|
165
|
+
expect.objectContaining({ reason: "validator_failed" }),
|
|
166
|
+
);
|
|
167
|
+
expect(decodePaymentRequiredHeader(res)).toEqual(
|
|
168
|
+
expect.objectContaining({
|
|
169
|
+
error: 'bad request: {"field":"amount"}',
|
|
170
|
+
}),
|
|
171
|
+
);
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
it("rejects hostile payment ids before facilitator fetch", async () => {
|
|
175
|
+
const handler = vi.fn();
|
|
176
|
+
const fetchMock = vi.fn();
|
|
177
|
+
vi.stubGlobal("fetch", fetchMock);
|
|
178
|
+
const route = {
|
|
179
|
+
path: "/paid",
|
|
180
|
+
type: "GET",
|
|
181
|
+
handler,
|
|
182
|
+
x402: { priceInCents: 10, paymentConfigs: ["base_usdc"] },
|
|
183
|
+
} as never;
|
|
184
|
+
const runtime = {
|
|
185
|
+
agentId: "agent-1",
|
|
186
|
+
emitEvent: vi.fn(),
|
|
187
|
+
getSetting: vi.fn(() => "https://facilitator.test"),
|
|
188
|
+
};
|
|
189
|
+
const res = makeResponse();
|
|
190
|
+
const [wrapped] = applyPaymentProtection([route], {
|
|
191
|
+
agentId: "agent-1",
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
await wrapped.handler?.(
|
|
195
|
+
{
|
|
196
|
+
method: "GET",
|
|
197
|
+
headers: { "x-payment-id": "../paid\n" },
|
|
198
|
+
query: {},
|
|
199
|
+
} as never,
|
|
200
|
+
res as never,
|
|
201
|
+
runtime as never,
|
|
202
|
+
);
|
|
203
|
+
|
|
204
|
+
expect(fetchMock).not.toHaveBeenCalled();
|
|
205
|
+
expect(handler).not.toHaveBeenCalled();
|
|
206
|
+
expect(res.status).toHaveBeenCalledWith(402);
|
|
207
|
+
expect(res.json).toHaveBeenCalledWith(
|
|
208
|
+
expect.objectContaining({
|
|
209
|
+
error: "Payment verification failed",
|
|
210
|
+
}),
|
|
211
|
+
);
|
|
212
|
+
expect(runtime.emitEvent).toHaveBeenCalledWith(
|
|
213
|
+
"PAYMENT_REQUIRED",
|
|
214
|
+
expect.objectContaining({ reason: "verification_failed" }),
|
|
215
|
+
);
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
it("does not wrap routes that are already marked as wrapped", () => {
|
|
219
|
+
const route = {
|
|
220
|
+
path: "/paid",
|
|
221
|
+
type: "GET",
|
|
222
|
+
handler: vi.fn(),
|
|
223
|
+
x402: { priceInCents: 25, paymentConfigs: ["base_usdc"] },
|
|
224
|
+
} as never;
|
|
225
|
+
|
|
226
|
+
const [wrapped] = applyPaymentProtection([route]);
|
|
227
|
+
const firstHandler = wrapped.handler;
|
|
228
|
+
const [again] = applyPaymentProtection([wrapped]);
|
|
229
|
+
|
|
230
|
+
expect(again).toBe(wrapped);
|
|
231
|
+
expect(again.handler).toBe(firstHandler);
|
|
232
|
+
expect(isRoutePaymentWrapped(again)).toBe(true);
|
|
233
|
+
});
|
|
234
|
+
});
|