@furlpay/gateway 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/LICENSE +21 -0
- package/README.md +246 -0
- package/SECURITY.md +65 -0
- package/dist/adapters/express.d.ts +43 -0
- package/dist/adapters/express.js +81 -0
- package/dist/adapters/fetch.d.ts +27 -0
- package/dist/adapters/fetch.js +63 -0
- package/dist/adapters/proxy.d.ts +26 -0
- package/dist/adapters/proxy.js +119 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +141 -0
- package/dist/facilitator.d.ts +55 -0
- package/dist/facilitator.js +78 -0
- package/dist/gateway.d.ts +122 -0
- package/dist/gateway.js +369 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.js +5 -0
- package/dist/protocol.d.ts +173 -0
- package/dist/protocol.js +186 -0
- package/dist/store.d.ts +60 -0
- package/dist/store.js +134 -0
- package/package.json +51 -0
- package/src/adapters/express.ts +122 -0
- package/src/adapters/fetch.ts +88 -0
- package/src/adapters/proxy.ts +157 -0
- package/src/cli.ts +164 -0
- package/src/facilitator.ts +133 -0
- package/src/gateway.ts +514 -0
- package/src/index.ts +76 -0
- package/src/protocol.ts +371 -0
- package/src/store.ts +157 -0
package/dist/gateway.js
ADDED
|
@@ -0,0 +1,369 @@
|
|
|
1
|
+
import crypto from "node:crypto";
|
|
2
|
+
import { DEFAULT_QUOTE_TTL_SECONDS, HEADER_V1_RECEIPT, HEADER_V2_RECEIPT, HEADER_V2_REQUIRED, LATEST_X402_VERSION, NETWORK_USDC, REPLAY_CLAIM_TTL_SECONDS, USDC_DECIMALS, atomicAmount, b64encode, bindingMac, decodePayment, detectVersion, encodeReceiptV1, encodeReceiptV2, hashBody, noStoreHeaders, requiredConfirmations, timingSafeEqualHex, } from "./protocol.js";
|
|
3
|
+
import { FURLPAY_FACILITATOR, httpFacilitator, } from "./facilitator.js";
|
|
4
|
+
import { MemoryClaimStore } from "./store.js";
|
|
5
|
+
const BODY_METHODS = new Set(["POST", "PUT", "PATCH"]);
|
|
6
|
+
function lookup(headers) {
|
|
7
|
+
if (typeof headers.get === "function") {
|
|
8
|
+
return (name) => headers.get(name);
|
|
9
|
+
}
|
|
10
|
+
const rec = headers;
|
|
11
|
+
const lower = {};
|
|
12
|
+
for (const [k, v] of Object.entries(rec))
|
|
13
|
+
if (v !== undefined)
|
|
14
|
+
lower[k.toLowerCase()] = v;
|
|
15
|
+
return (name) => lower[name.toLowerCase()] ?? null;
|
|
16
|
+
}
|
|
17
|
+
export class Gateway {
|
|
18
|
+
opts;
|
|
19
|
+
facilitator;
|
|
20
|
+
claims;
|
|
21
|
+
network;
|
|
22
|
+
asset;
|
|
23
|
+
ttl;
|
|
24
|
+
constructor(opts) {
|
|
25
|
+
this.opts = opts;
|
|
26
|
+
if (!opts.quoteSecret || opts.quoteSecret.length < 16) {
|
|
27
|
+
throw new Error("@furlpay/gateway: `quoteSecret` is required and must be at least 16 chars. " +
|
|
28
|
+
"Generate one with: npx @furlpay/gateway secret");
|
|
29
|
+
}
|
|
30
|
+
if (!opts.payTo)
|
|
31
|
+
throw new Error("@furlpay/gateway: `payTo` is required.");
|
|
32
|
+
this.network = opts.network ?? "base";
|
|
33
|
+
const asset = opts.asset ?? NETWORK_USDC[this.network];
|
|
34
|
+
if (!asset) {
|
|
35
|
+
throw new Error(`@furlpay/gateway: no canonical USDC address known for network "${this.network}" — pass \`asset\` explicitly.`);
|
|
36
|
+
}
|
|
37
|
+
this.asset = asset;
|
|
38
|
+
this.ttl = opts.quoteTtlSeconds ?? DEFAULT_QUOTE_TTL_SECONDS;
|
|
39
|
+
this.claims = opts.claimStore ?? new MemoryClaimStore();
|
|
40
|
+
this.facilitator =
|
|
41
|
+
typeof opts.facilitator === "string"
|
|
42
|
+
? httpFacilitator(opts.facilitator)
|
|
43
|
+
: (opts.facilitator ?? httpFacilitator(FURLPAY_FACILITATOR));
|
|
44
|
+
}
|
|
45
|
+
/** True when replay claims are shared across instances. Assert this at boot in
|
|
46
|
+
* any multi-instance deployment. */
|
|
47
|
+
get replayProtectionDurable() {
|
|
48
|
+
return this.claims.isDurable;
|
|
49
|
+
}
|
|
50
|
+
// ── quoting ─────────────────────────────────────────────────────────────
|
|
51
|
+
resourceId(method, url) {
|
|
52
|
+
if (this.opts.resource)
|
|
53
|
+
return this.opts.resource({ method, url });
|
|
54
|
+
return `${url.origin}${url.pathname}${url.search}`;
|
|
55
|
+
}
|
|
56
|
+
context(input) {
|
|
57
|
+
const url = input.url instanceof URL ? input.url : new URL(input.url);
|
|
58
|
+
const method = input.method.toUpperCase();
|
|
59
|
+
return {
|
|
60
|
+
method,
|
|
61
|
+
resource: this.resourceId(method, url),
|
|
62
|
+
url,
|
|
63
|
+
headers: input.headers instanceof Headers ? input.headers : new Headers(),
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
async priceFor(ctx) {
|
|
67
|
+
return typeof this.opts.price === "function" ? await this.opts.price(ctx) : this.opts.price;
|
|
68
|
+
}
|
|
69
|
+
describe(ctx) {
|
|
70
|
+
if (typeof this.opts.description === "function")
|
|
71
|
+
return this.opts.description(ctx);
|
|
72
|
+
return this.opts.description ?? `${ctx.method} ${ctx.url.pathname} (per call)`;
|
|
73
|
+
}
|
|
74
|
+
shouldBindBody(method, body) {
|
|
75
|
+
if (this.opts.bindBody === false)
|
|
76
|
+
return false;
|
|
77
|
+
if (!BODY_METHODS.has(method))
|
|
78
|
+
return false;
|
|
79
|
+
if (body === undefined || body === null)
|
|
80
|
+
return false;
|
|
81
|
+
return body.length > 0;
|
|
82
|
+
}
|
|
83
|
+
/** Mint a fresh quote for this request. */
|
|
84
|
+
async mint(input) {
|
|
85
|
+
const ctx = this.context(input);
|
|
86
|
+
const amount = atomicAmount(await this.priceFor(ctx), USDC_DECIMALS);
|
|
87
|
+
return {
|
|
88
|
+
method: ctx.method,
|
|
89
|
+
resource: ctx.resource,
|
|
90
|
+
amount,
|
|
91
|
+
quoteId: crypto.randomBytes(16).toString("hex"),
|
|
92
|
+
expiresAt: Math.floor(Date.now() / 1000) + this.ttl,
|
|
93
|
+
bodyHash: this.shouldBindBody(ctx.method, input.body) ? hashBody(input.body) : undefined,
|
|
94
|
+
description: this.describe(ctx),
|
|
95
|
+
mimeType: this.opts.mimeType ?? "application/json",
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
extraFor(q) {
|
|
99
|
+
return {
|
|
100
|
+
name: this.opts.assetName ?? "USD Coin",
|
|
101
|
+
version: this.opts.assetVersion ?? "2",
|
|
102
|
+
quoteId: q.quoteId,
|
|
103
|
+
expiresAt: q.expiresAt,
|
|
104
|
+
binding: bindingMac(this.opts.quoteSecret, {
|
|
105
|
+
method: q.method,
|
|
106
|
+
resource: q.resource,
|
|
107
|
+
amount: q.amount,
|
|
108
|
+
quoteId: q.quoteId,
|
|
109
|
+
expiresAt: q.expiresAt,
|
|
110
|
+
bodyHash: q.bodyHash,
|
|
111
|
+
}),
|
|
112
|
+
...(q.bodyHash ? { bodyHash: q.bodyHash } : {}),
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
toV1(q) {
|
|
116
|
+
return {
|
|
117
|
+
scheme: "exact",
|
|
118
|
+
network: this.network,
|
|
119
|
+
maxAmountRequired: q.amount,
|
|
120
|
+
resource: q.resource,
|
|
121
|
+
description: q.description,
|
|
122
|
+
mimeType: q.mimeType,
|
|
123
|
+
payTo: this.opts.payTo,
|
|
124
|
+
maxTimeoutSeconds: this.ttl,
|
|
125
|
+
asset: this.asset,
|
|
126
|
+
extra: this.extraFor(q),
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
toV2(q) {
|
|
130
|
+
return {
|
|
131
|
+
scheme: "exact",
|
|
132
|
+
network: this.network,
|
|
133
|
+
asset: this.asset,
|
|
134
|
+
amount: q.amount,
|
|
135
|
+
payTo: this.opts.payTo,
|
|
136
|
+
maxTimeoutSeconds: this.ttl,
|
|
137
|
+
extra: this.extraFor(q),
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
resourceInfo(q) {
|
|
141
|
+
return {
|
|
142
|
+
url: q.resource,
|
|
143
|
+
description: q.description,
|
|
144
|
+
mimeType: q.mimeType,
|
|
145
|
+
...(this.opts.serviceName ? { serviceName: this.opts.serviceName } : {}),
|
|
146
|
+
...(this.opts.tags?.length ? { tags: this.opts.tags } : {}),
|
|
147
|
+
...(this.opts.iconUrl ? { iconUrl: this.opts.iconUrl } : {}),
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
/** Public quote surface — the v2 402 payload for this request. */
|
|
151
|
+
async quote(input) {
|
|
152
|
+
const q = await this.mint(input);
|
|
153
|
+
return { x402Version: 2, resource: this.resourceInfo(q), accepts: [this.toV2(q)] };
|
|
154
|
+
}
|
|
155
|
+
/**
|
|
156
|
+
* A 402 advertises BOTH dialects: v2 in the PAYMENT-REQUIRED header, v1 in the
|
|
157
|
+
* body. v2 moving payment data out of the body is exactly what makes this
|
|
158
|
+
* possible — a v2 client reads the header, a v1 client reads the body, and
|
|
159
|
+
* neither needs to know the other exists.
|
|
160
|
+
*/
|
|
161
|
+
async challenge(input, error) {
|
|
162
|
+
const q = await this.mint(input);
|
|
163
|
+
const v2 = {
|
|
164
|
+
x402Version: 2,
|
|
165
|
+
error,
|
|
166
|
+
resource: this.resourceInfo(q),
|
|
167
|
+
accepts: [this.toV2(q)],
|
|
168
|
+
};
|
|
169
|
+
const v1 = { x402Version: 1, error, accepts: [this.toV1(q)] };
|
|
170
|
+
return {
|
|
171
|
+
kind: "challenge",
|
|
172
|
+
status: 402,
|
|
173
|
+
body: v1,
|
|
174
|
+
headers: {
|
|
175
|
+
...noStoreHeaders,
|
|
176
|
+
"Content-Type": "application/json",
|
|
177
|
+
[HEADER_V2_REQUIRED]: b64encode(v2),
|
|
178
|
+
},
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
reject(error) {
|
|
182
|
+
return {
|
|
183
|
+
kind: "reject",
|
|
184
|
+
status: 400,
|
|
185
|
+
body: { error },
|
|
186
|
+
headers: { ...noStoreHeaders, "Content-Type": "application/json" },
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
// ── the gate ────────────────────────────────────────────────────────────
|
|
190
|
+
/**
|
|
191
|
+
* Returns `allow` only when a payment has been verified against a
|
|
192
|
+
* server-derived quote, burned as single-use, and settled to the confirmation
|
|
193
|
+
* depth its value requires.
|
|
194
|
+
*/
|
|
195
|
+
async authorize(input) {
|
|
196
|
+
const ctx = this.context(input);
|
|
197
|
+
const get = lookup(input.headers);
|
|
198
|
+
const clientVersion = detectVersion(get);
|
|
199
|
+
if (clientVersion === null)
|
|
200
|
+
return this.challenge(input, "Payment required");
|
|
201
|
+
const decoded = decodePayment(get);
|
|
202
|
+
if (!decoded.ok) {
|
|
203
|
+
// Malformed is the client's bug, not a pricing failure — a 402 here would
|
|
204
|
+
// invite the agent to pay again against a payload we could not even read.
|
|
205
|
+
await this.opts.onRejected?.({ reason: decoded.error, resource: ctx.resource });
|
|
206
|
+
return this.reject(decoded.error);
|
|
207
|
+
}
|
|
208
|
+
const p = decoded.payment;
|
|
209
|
+
const auth = p.authorization;
|
|
210
|
+
const fail = async (reason) => {
|
|
211
|
+
await this.opts.onRejected?.({ reason, resource: ctx.resource, payer: auth.from });
|
|
212
|
+
return this.challenge(input, reason);
|
|
213
|
+
};
|
|
214
|
+
// ── re-derive the expectation; the client's echo is never the source ──
|
|
215
|
+
const expectedAmount = atomicAmount(await this.priceFor(ctx), USDC_DECIMALS);
|
|
216
|
+
if (p.scheme !== "exact")
|
|
217
|
+
return fail("Unsupported scheme");
|
|
218
|
+
if (p.network !== this.network)
|
|
219
|
+
return fail(`Wrong network (expected ${this.network})`);
|
|
220
|
+
if (auth.to.toLowerCase() !== this.opts.payTo.toLowerCase()) {
|
|
221
|
+
return fail("Payment not addressed to this recipient");
|
|
222
|
+
}
|
|
223
|
+
// Integer compare — "9000" > "10000" lexicographically.
|
|
224
|
+
try {
|
|
225
|
+
if (BigInt(auth.value) < BigInt(expectedAmount)) {
|
|
226
|
+
return fail(`Insufficient amount (expected ${expectedAmount} atomic units)`);
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
catch {
|
|
230
|
+
return fail("Invalid amount");
|
|
231
|
+
}
|
|
232
|
+
// ── binding: this resource, this price, this body? ──
|
|
233
|
+
const { quoteId, binding, expiresAt } = p;
|
|
234
|
+
if (!quoteId || !binding || typeof expiresAt !== "number") {
|
|
235
|
+
return fail("Missing quote binding (re-request the 402 quote)");
|
|
236
|
+
}
|
|
237
|
+
let bodyHash;
|
|
238
|
+
if (this.shouldBindBody(ctx.method, input.body)) {
|
|
239
|
+
bodyHash = hashBody(input.body);
|
|
240
|
+
if (!p.bodyHash)
|
|
241
|
+
return fail("Quote is not body-bound (re-request the 402 quote)");
|
|
242
|
+
if (!timingSafeEqualHex(p.bodyHash, bodyHash)) {
|
|
243
|
+
return fail("Body does not match the body this quote was issued for");
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
const expectedMac = bindingMac(this.opts.quoteSecret, {
|
|
247
|
+
method: ctx.method,
|
|
248
|
+
resource: ctx.resource,
|
|
249
|
+
amount: expectedAmount,
|
|
250
|
+
quoteId,
|
|
251
|
+
expiresAt,
|
|
252
|
+
bodyHash,
|
|
253
|
+
});
|
|
254
|
+
if (!timingSafeEqualHex(expectedMac, binding)) {
|
|
255
|
+
// The most important line in this file: a payment minted for a cheap
|
|
256
|
+
// resource dies here when presented to an expensive one.
|
|
257
|
+
return fail("Quote binding mismatch (payment bound to a different resource)");
|
|
258
|
+
}
|
|
259
|
+
// ── freshness ──
|
|
260
|
+
const now = Math.floor(Date.now() / 1000);
|
|
261
|
+
if (now > expiresAt)
|
|
262
|
+
return fail("Quote expired (re-request the 402 quote)");
|
|
263
|
+
const validAfter = Number(auth.validAfter);
|
|
264
|
+
const validBefore = Number(auth.validBefore);
|
|
265
|
+
if (Number.isFinite(validAfter) && now < validAfter)
|
|
266
|
+
return fail("Authorization not yet valid");
|
|
267
|
+
if (Number.isFinite(validBefore) && now > validBefore)
|
|
268
|
+
return fail("Authorization expired");
|
|
269
|
+
// ── single-use. Nonce first: it is the money-level token, so burning it
|
|
270
|
+
// before the quote means a racing replay never reaches settlement. ──
|
|
271
|
+
if (!(await this.claims.claim(`x402:nonce:${auth.nonce}`, REPLAY_CLAIM_TTL_SECONDS))) {
|
|
272
|
+
return fail("Authorization already used (replay)");
|
|
273
|
+
}
|
|
274
|
+
if (!(await this.claims.claim(`x402:quote:${quoteId}`, REPLAY_CLAIM_TTL_SECONDS))) {
|
|
275
|
+
return fail("Quote already redeemed (replay)");
|
|
276
|
+
}
|
|
277
|
+
// ── verify + settle, in the dialect the facilitator speaks ──
|
|
278
|
+
const q = {
|
|
279
|
+
method: ctx.method,
|
|
280
|
+
resource: ctx.resource,
|
|
281
|
+
amount: expectedAmount,
|
|
282
|
+
quoteId,
|
|
283
|
+
expiresAt,
|
|
284
|
+
bodyHash,
|
|
285
|
+
description: this.describe(ctx),
|
|
286
|
+
mimeType: this.opts.mimeType ?? "application/json",
|
|
287
|
+
};
|
|
288
|
+
const fReq = this.facilitatorRequest(q, p, clientVersion);
|
|
289
|
+
if (this.opts.settle === false) {
|
|
290
|
+
const verdict = await this.facilitator.verify(fReq);
|
|
291
|
+
if (!verdict.isValid)
|
|
292
|
+
return fail(verdict.invalidReason ?? "Payment verification failed");
|
|
293
|
+
return this.allow(clientVersion, {
|
|
294
|
+
payer: verdict.payer ?? auth.from,
|
|
295
|
+
transaction: "",
|
|
296
|
+
network: this.network,
|
|
297
|
+
asset: this.asset,
|
|
298
|
+
amountAtomic: auth.value,
|
|
299
|
+
resource: ctx.resource,
|
|
300
|
+
settledAt: new Date().toISOString(),
|
|
301
|
+
});
|
|
302
|
+
}
|
|
303
|
+
const settled = await this.facilitator.settle(fReq);
|
|
304
|
+
if (!settled.success) {
|
|
305
|
+
// The nonce stays burned. A settle that errored may STILL land on-chain,
|
|
306
|
+
// so freeing it would reopen replay against a payment that does confirm.
|
|
307
|
+
// An honest retry signs a fresh nonce, so this never blocks a real payer.
|
|
308
|
+
return fail(settled.errorReason ?? "Settlement failed");
|
|
309
|
+
}
|
|
310
|
+
const needed = requiredConfirmations(auth.value);
|
|
311
|
+
if (typeof settled.confirmations === "number" && settled.confirmations < needed) {
|
|
312
|
+
return fail(`Insufficient confirmations (${settled.confirmations}/${needed})`);
|
|
313
|
+
}
|
|
314
|
+
return this.allow(clientVersion, {
|
|
315
|
+
payer: settled.payer ?? auth.from,
|
|
316
|
+
transaction: settled.transaction ?? "",
|
|
317
|
+
network: settled.network ?? this.network,
|
|
318
|
+
asset: this.asset,
|
|
319
|
+
// v2's `upto` scheme can settle less than the authorized maximum.
|
|
320
|
+
amountAtomic: settled.amount ?? auth.value,
|
|
321
|
+
resource: ctx.resource,
|
|
322
|
+
confirmations: settled.confirmations,
|
|
323
|
+
settledAt: new Date().toISOString(),
|
|
324
|
+
});
|
|
325
|
+
}
|
|
326
|
+
/** Rebuild the requirements the quote was minted with, so the facilitator
|
|
327
|
+
* checks the payment against the SERVER's terms, not the client's copy. */
|
|
328
|
+
facilitatorRequest(q, payment, clientVersion) {
|
|
329
|
+
const version = this.opts.facilitatorVersion ?? clientVersion;
|
|
330
|
+
if (version === 2) {
|
|
331
|
+
return {
|
|
332
|
+
x402Version: 2,
|
|
333
|
+
paymentPayload: {
|
|
334
|
+
x402Version: 2,
|
|
335
|
+
resource: this.resourceInfo(q),
|
|
336
|
+
accepted: this.toV2(q),
|
|
337
|
+
payload: { signature: payment.signature, authorization: payment.authorization },
|
|
338
|
+
},
|
|
339
|
+
paymentRequirements: this.toV2(q),
|
|
340
|
+
};
|
|
341
|
+
}
|
|
342
|
+
return {
|
|
343
|
+
x402Version: 1,
|
|
344
|
+
paymentPayload: {
|
|
345
|
+
x402Version: 1,
|
|
346
|
+
scheme: "exact",
|
|
347
|
+
network: this.network,
|
|
348
|
+
payload: { signature: payment.signature, authorization: payment.authorization },
|
|
349
|
+
extra: this.extraFor(q),
|
|
350
|
+
},
|
|
351
|
+
paymentRequirements: this.toV1(q),
|
|
352
|
+
};
|
|
353
|
+
}
|
|
354
|
+
/** Answer in the dialect the client spoke — and, harmlessly, the other one too,
|
|
355
|
+
* so a proxy or SDK expecting either finds its receipt. */
|
|
356
|
+
async allow(version, receipt) {
|
|
357
|
+
await this.opts.onSettled?.(receipt);
|
|
358
|
+
return {
|
|
359
|
+
kind: "allow",
|
|
360
|
+
receipt,
|
|
361
|
+
headers: {
|
|
362
|
+
...noStoreHeaders,
|
|
363
|
+
[HEADER_V1_RECEIPT]: encodeReceiptV1(receipt),
|
|
364
|
+
[HEADER_V2_RECEIPT]: encodeReceiptV2(receipt),
|
|
365
|
+
},
|
|
366
|
+
};
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
export { LATEST_X402_VERSION };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export { LATEST_X402_VERSION, HEADER_V1_PAYMENT, HEADER_V1_RECEIPT, HEADER_V2_PAYMENT, HEADER_V2_RECEIPT, HEADER_V2_REQUIRED, MAX_HEADER_BYTES, DEFAULT_QUOTE_TTL_SECONDS, REPLAY_CLAIM_TTL_SECONDS, NETWORK_USDC, USDC_DECIMALS, noStoreHeaders, requiredConfirmations, atomicAmount, bindingMac, hashBody, timingSafeEqualHex, b64encode, b64decode, detectVersion, decodePayment, encodeReceiptV1, encodeReceiptV2, decodeReceipt, type X402Version, type EIP3009Authorization, type QuoteExtra, type ResourceInfo, type PaymentRequirementsV1, type PaymentRequiredV1, type PaymentPayloadV1, type PaymentRequirementsV2, type PaymentRequiredV2, type PaymentPayloadV2, type NormalizedPayment, type SettlementReceipt, type BindingInput, type DecodedPayment, type HeaderLookup, } from "./protocol.js";
|
|
2
|
+
export { FURLPAY_FACILITATOR, httpFacilitator, type FacilitatorClient, type FacilitatorRequest, type VerifyResult, type SettleResult, type SupportedKind, type HttpFacilitatorOptions, } from "./facilitator.js";
|
|
3
|
+
export { MemoryClaimStore, claimStoreFrom, upstashClaimStore, type ClaimStore, type UpstashClaimStoreOptions, } from "./store.js";
|
|
4
|
+
export { Gateway, type GatewayOptions, type GatewayDecision, type AuthorizeInput, type RequestContext, type PriceResolver, } from "./gateway.js";
|
|
5
|
+
export { paywall, createGateway, type FetchHandler, type PaidHandler, } from "./adapters/fetch.js";
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export { LATEST_X402_VERSION, HEADER_V1_PAYMENT, HEADER_V1_RECEIPT, HEADER_V2_PAYMENT, HEADER_V2_RECEIPT, HEADER_V2_REQUIRED, MAX_HEADER_BYTES, DEFAULT_QUOTE_TTL_SECONDS, REPLAY_CLAIM_TTL_SECONDS, NETWORK_USDC, USDC_DECIMALS, noStoreHeaders, requiredConfirmations, atomicAmount, bindingMac, hashBody, timingSafeEqualHex, b64encode, b64decode, detectVersion, decodePayment, encodeReceiptV1, encodeReceiptV2, decodeReceipt, } from "./protocol.js";
|
|
2
|
+
export { FURLPAY_FACILITATOR, httpFacilitator, } from "./facilitator.js";
|
|
3
|
+
export { MemoryClaimStore, claimStoreFrom, upstashClaimStore, } from "./store.js";
|
|
4
|
+
export { Gateway, } from "./gateway.js";
|
|
5
|
+
export { paywall, createGateway, } from "./adapters/fetch.js";
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
export type X402Version = 1 | 2;
|
|
2
|
+
/** Default dialect for freshly-minted quotes when the client hasn't spoken yet. */
|
|
3
|
+
export declare const LATEST_X402_VERSION: X402Version;
|
|
4
|
+
export declare const HEADER_V1_PAYMENT = "x-payment";
|
|
5
|
+
export declare const HEADER_V1_RECEIPT = "X-PAYMENT-RESPONSE";
|
|
6
|
+
export declare const HEADER_V2_PAYMENT = "payment-signature";
|
|
7
|
+
export declare const HEADER_V2_RECEIPT = "PAYMENT-RESPONSE";
|
|
8
|
+
export declare const HEADER_V2_REQUIRED = "PAYMENT-REQUIRED";
|
|
9
|
+
/** Web-layer hardening: cap the header before we spend cycles parsing it. */
|
|
10
|
+
export declare const MAX_HEADER_BYTES = 8192;
|
|
11
|
+
export declare const DEFAULT_QUOTE_TTL_SECONDS = 300;
|
|
12
|
+
/** Must outlive any EIP-3009 validity window, or a late replay could re-claim a
|
|
13
|
+
* freed key. */
|
|
14
|
+
export declare const REPLAY_CLAIM_TTL_SECONDS = 86400;
|
|
15
|
+
/** Canonical (native-issuance, not bridged) USDC per network. */
|
|
16
|
+
export declare const NETWORK_USDC: Record<string, string>;
|
|
17
|
+
export declare const USDC_DECIMALS = 6;
|
|
18
|
+
export interface EIP3009Authorization {
|
|
19
|
+
from: string;
|
|
20
|
+
to: string;
|
|
21
|
+
value: string;
|
|
22
|
+
validAfter: string;
|
|
23
|
+
validBefore: string;
|
|
24
|
+
nonce: string;
|
|
25
|
+
}
|
|
26
|
+
/** The quote fields we carry in `extra` — identical across v1 and v2, which is
|
|
27
|
+
* what lets one binding scheme serve both. */
|
|
28
|
+
export interface QuoteExtra {
|
|
29
|
+
name: string;
|
|
30
|
+
version: string;
|
|
31
|
+
quoteId: string;
|
|
32
|
+
expiresAt: number;
|
|
33
|
+
binding: string;
|
|
34
|
+
bodyHash?: string;
|
|
35
|
+
[k: string]: unknown;
|
|
36
|
+
}
|
|
37
|
+
export interface PaymentRequirementsV1 {
|
|
38
|
+
scheme: "exact";
|
|
39
|
+
network: string;
|
|
40
|
+
maxAmountRequired: string;
|
|
41
|
+
resource: string;
|
|
42
|
+
description: string;
|
|
43
|
+
mimeType: string;
|
|
44
|
+
outputSchema?: Record<string, unknown>;
|
|
45
|
+
payTo: string;
|
|
46
|
+
maxTimeoutSeconds: number;
|
|
47
|
+
asset: string;
|
|
48
|
+
extra: QuoteExtra;
|
|
49
|
+
}
|
|
50
|
+
export interface PaymentRequiredV1 {
|
|
51
|
+
x402Version: 1;
|
|
52
|
+
error: string;
|
|
53
|
+
accepts: PaymentRequirementsV1[];
|
|
54
|
+
}
|
|
55
|
+
export interface PaymentPayloadV1 {
|
|
56
|
+
x402Version: 1;
|
|
57
|
+
scheme: string;
|
|
58
|
+
network: string;
|
|
59
|
+
payload: {
|
|
60
|
+
signature: string;
|
|
61
|
+
authorization: EIP3009Authorization;
|
|
62
|
+
};
|
|
63
|
+
extra?: Partial<QuoteExtra>;
|
|
64
|
+
}
|
|
65
|
+
/** v2 hoists resource metadata out of each requirement. `tags` / `serviceName`
|
|
66
|
+
* / `iconUrl` are the protocol's native discovery fields — a facilitator can
|
|
67
|
+
* crawl these, which is why a proprietary discovery index is the wrong build. */
|
|
68
|
+
export interface ResourceInfo {
|
|
69
|
+
url: string;
|
|
70
|
+
description?: string;
|
|
71
|
+
mimeType?: string;
|
|
72
|
+
serviceName?: string;
|
|
73
|
+
tags?: string[];
|
|
74
|
+
iconUrl?: string;
|
|
75
|
+
}
|
|
76
|
+
export interface PaymentRequirementsV2 {
|
|
77
|
+
scheme: "exact";
|
|
78
|
+
network: string;
|
|
79
|
+
asset: string;
|
|
80
|
+
amount: string;
|
|
81
|
+
payTo: string;
|
|
82
|
+
maxTimeoutSeconds: number;
|
|
83
|
+
extra: QuoteExtra;
|
|
84
|
+
}
|
|
85
|
+
export interface PaymentRequiredV2 {
|
|
86
|
+
x402Version: 2;
|
|
87
|
+
error?: string;
|
|
88
|
+
resource: ResourceInfo;
|
|
89
|
+
accepts: PaymentRequirementsV2[];
|
|
90
|
+
extensions?: Record<string, unknown>;
|
|
91
|
+
}
|
|
92
|
+
export interface PaymentPayloadV2 {
|
|
93
|
+
x402Version: 2;
|
|
94
|
+
resource?: ResourceInfo;
|
|
95
|
+
/** The client echoes the FULL requirements it is paying against. Still not
|
|
96
|
+
* trusted — every field is re-derived server-side and compared. */
|
|
97
|
+
accepted: PaymentRequirementsV2;
|
|
98
|
+
payload: {
|
|
99
|
+
signature: string;
|
|
100
|
+
authorization: EIP3009Authorization;
|
|
101
|
+
};
|
|
102
|
+
extensions?: Record<string, unknown>;
|
|
103
|
+
}
|
|
104
|
+
/** One internal shape both dialects collapse to, so the gate is written once. */
|
|
105
|
+
export interface NormalizedPayment {
|
|
106
|
+
version: X402Version;
|
|
107
|
+
scheme: string;
|
|
108
|
+
network: string;
|
|
109
|
+
signature: string;
|
|
110
|
+
authorization: EIP3009Authorization;
|
|
111
|
+
/** Quote echo — the lookup key, never an assertion. */
|
|
112
|
+
quoteId?: string;
|
|
113
|
+
binding?: string;
|
|
114
|
+
expiresAt?: number;
|
|
115
|
+
bodyHash?: string;
|
|
116
|
+
}
|
|
117
|
+
/** A paid 200 that a CDN caches is a paid resource served free to the next
|
|
118
|
+
* unpaid caller. `Vary` covers both dialects' request headers. */
|
|
119
|
+
export declare const noStoreHeaders: Record<string, string>;
|
|
120
|
+
export declare function requiredConfirmations(amountAtomic: string): number;
|
|
121
|
+
export declare function atomicAmount(priceUsd: number, decimals?: number): string;
|
|
122
|
+
export interface BindingInput {
|
|
123
|
+
method: string;
|
|
124
|
+
resource: string;
|
|
125
|
+
amount: string;
|
|
126
|
+
quoteId: string;
|
|
127
|
+
expiresAt: number;
|
|
128
|
+
bodyHash?: string;
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* HMAC binding a quote to exactly one (method, resource, amount) — and, when a
|
|
132
|
+
* body is present, exactly one body.
|
|
133
|
+
*
|
|
134
|
+
* Deliberately version-independent: the same MAC validates a v1 or a v2 payment,
|
|
135
|
+
* because both dialects carry the quote in `extra`. It is also byte-identical to
|
|
136
|
+
* apps/web/src/lib/x402.ts, so quotes stay interchangeable with FurlPay's
|
|
137
|
+
* first-party resources.
|
|
138
|
+
*/
|
|
139
|
+
export declare function bindingMac(secret: string | Buffer, input: BindingInput): string;
|
|
140
|
+
export declare function hashBody(body: string | Buffer): string;
|
|
141
|
+
export declare function timingSafeEqualHex(a: string, b: string): boolean;
|
|
142
|
+
export declare function b64encode(value: unknown): string;
|
|
143
|
+
export declare function b64decode<T>(value: string): T;
|
|
144
|
+
export type HeaderLookup = (name: string) => string | null;
|
|
145
|
+
/** Which dialect did the client speak? `null` when it presented no payment. */
|
|
146
|
+
export declare function detectVersion(get: HeaderLookup): X402Version | null;
|
|
147
|
+
export type DecodedPayment = {
|
|
148
|
+
ok: true;
|
|
149
|
+
payment: NormalizedPayment;
|
|
150
|
+
} | {
|
|
151
|
+
ok: false;
|
|
152
|
+
error: string;
|
|
153
|
+
};
|
|
154
|
+
/**
|
|
155
|
+
* Strict parse of whichever payment header is present, normalized to one shape.
|
|
156
|
+
* Malformed input is an error, never a silently-released resource.
|
|
157
|
+
*/
|
|
158
|
+
export declare function decodePayment(get: HeaderLookup): DecodedPayment;
|
|
159
|
+
export interface SettlementReceipt {
|
|
160
|
+
payer: string;
|
|
161
|
+
transaction: string;
|
|
162
|
+
network: string;
|
|
163
|
+
asset: string;
|
|
164
|
+
amountAtomic: string;
|
|
165
|
+
resource: string;
|
|
166
|
+
confirmations?: number;
|
|
167
|
+
settledAt: string;
|
|
168
|
+
}
|
|
169
|
+
/** v1 receipt — FurlPay's own X-PAYMENT-RESPONSE shape. */
|
|
170
|
+
export declare function encodeReceiptV1(receipt: SettlementReceipt): string;
|
|
171
|
+
/** v2 receipt — the reference SDK's SettleResponse shape. */
|
|
172
|
+
export declare function encodeReceiptV2(receipt: SettlementReceipt): string;
|
|
173
|
+
export declare function decodeReceipt(header: string): SettlementReceipt | null;
|