@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
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import http from "node:http";
|
|
2
|
+
import { Gateway } from "../gateway.js";
|
|
3
|
+
/** Headers that are meaningful only for a single hop and must not be forwarded. */
|
|
4
|
+
const HOP_BY_HOP = new Set([
|
|
5
|
+
"connection",
|
|
6
|
+
"keep-alive",
|
|
7
|
+
"proxy-authenticate",
|
|
8
|
+
"proxy-authorization",
|
|
9
|
+
"te",
|
|
10
|
+
"trailer",
|
|
11
|
+
"transfer-encoding",
|
|
12
|
+
"upgrade",
|
|
13
|
+
"host",
|
|
14
|
+
"content-length",
|
|
15
|
+
]);
|
|
16
|
+
function isFree(pathname, freePaths) {
|
|
17
|
+
return freePaths.some((p) => p.endsWith("*") ? pathname.startsWith(p.slice(0, -1)) : pathname === p);
|
|
18
|
+
}
|
|
19
|
+
function readRequestBody(req) {
|
|
20
|
+
return new Promise((resolve, reject) => {
|
|
21
|
+
const chunks = [];
|
|
22
|
+
req.on("data", (c) => chunks.push(c));
|
|
23
|
+
req.on("end", () => resolve(Buffer.concat(chunks)));
|
|
24
|
+
req.on("error", reject);
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Build (but do not start) the paywalling reverse proxy.
|
|
29
|
+
* Returns a standard `http.Server` — call `.listen()` yourself, or use
|
|
30
|
+
* `startProxy` which does it for you.
|
|
31
|
+
*/
|
|
32
|
+
export function createProxy(opts) {
|
|
33
|
+
const gateway = new Gateway(opts);
|
|
34
|
+
const origin = opts.origin.replace(/\/+$/, "");
|
|
35
|
+
const freePaths = opts.freePaths ?? [];
|
|
36
|
+
return http.createServer(async (req, res) => {
|
|
37
|
+
const send = (status, body, headers = {}) => {
|
|
38
|
+
const payload = JSON.stringify(body);
|
|
39
|
+
res.writeHead(status, {
|
|
40
|
+
"Content-Type": "application/json",
|
|
41
|
+
"Content-Length": Buffer.byteLength(payload),
|
|
42
|
+
...headers,
|
|
43
|
+
});
|
|
44
|
+
res.end(payload);
|
|
45
|
+
};
|
|
46
|
+
try {
|
|
47
|
+
const base = opts.publicUrl ?? `http://${req.headers.host ?? "localhost"}`;
|
|
48
|
+
const url = new URL(req.url ?? "/", base);
|
|
49
|
+
const method = (req.method ?? "GET").toUpperCase();
|
|
50
|
+
const body = await readRequestBody(req);
|
|
51
|
+
if (!isFree(url.pathname, freePaths)) {
|
|
52
|
+
const decision = await gateway.authorize({
|
|
53
|
+
method,
|
|
54
|
+
url,
|
|
55
|
+
headers: req.headers,
|
|
56
|
+
body: body.length ? body : undefined,
|
|
57
|
+
});
|
|
58
|
+
if (decision.kind !== "allow") {
|
|
59
|
+
send(decision.status, decision.body, decision.headers);
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
// Both dialects' receipts, plus the cache guards.
|
|
63
|
+
for (const [k, v] of Object.entries(decision.headers))
|
|
64
|
+
res.setHeader(k, v);
|
|
65
|
+
}
|
|
66
|
+
// Forward to the origin.
|
|
67
|
+
const forwardHeaders = {};
|
|
68
|
+
for (const [k, v] of Object.entries(req.headers)) {
|
|
69
|
+
const key = k.toLowerCase();
|
|
70
|
+
if (HOP_BY_HOP.has(key))
|
|
71
|
+
continue;
|
|
72
|
+
// The origin has no use for the payment credential, in either dialect.
|
|
73
|
+
if (key === "x-payment" || key === "payment-signature")
|
|
74
|
+
continue;
|
|
75
|
+
if (typeof v === "string")
|
|
76
|
+
forwardHeaders[k] = v;
|
|
77
|
+
else if (Array.isArray(v))
|
|
78
|
+
forwardHeaders[k] = v.join(", ");
|
|
79
|
+
}
|
|
80
|
+
const upstream = await fetch(`${origin}${url.pathname}${url.search}`, {
|
|
81
|
+
method,
|
|
82
|
+
headers: forwardHeaders,
|
|
83
|
+
body: method === "GET" || method === "HEAD" ? undefined : body,
|
|
84
|
+
});
|
|
85
|
+
upstream.headers.forEach((v, k) => {
|
|
86
|
+
const key = k.toLowerCase();
|
|
87
|
+
if (HOP_BY_HOP.has(key))
|
|
88
|
+
return;
|
|
89
|
+
// Never let the origin's cache policy override ours on a paid response.
|
|
90
|
+
if (key === "cache-control" || key === "vary")
|
|
91
|
+
return;
|
|
92
|
+
res.setHeader(k, v);
|
|
93
|
+
});
|
|
94
|
+
res.writeHead(upstream.status);
|
|
95
|
+
const buf = Buffer.from(await upstream.arrayBuffer());
|
|
96
|
+
res.end(buf);
|
|
97
|
+
}
|
|
98
|
+
catch (e) {
|
|
99
|
+
send(502, { error: "origin_unreachable", detail: e.message });
|
|
100
|
+
}
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
export function startProxy(opts) {
|
|
104
|
+
const server = createProxy(opts);
|
|
105
|
+
const port = opts.port ?? 4402;
|
|
106
|
+
const hostname = opts.hostname ?? "0.0.0.0";
|
|
107
|
+
return new Promise((resolve, reject) => {
|
|
108
|
+
server.once("error", reject);
|
|
109
|
+
server.listen(port, hostname, () => {
|
|
110
|
+
const addr = server.address();
|
|
111
|
+
const actual = typeof addr === "object" && addr ? addr.port : port;
|
|
112
|
+
resolve({
|
|
113
|
+
server,
|
|
114
|
+
port: actual,
|
|
115
|
+
close: () => new Promise((r) => server.close(() => r())),
|
|
116
|
+
});
|
|
117
|
+
});
|
|
118
|
+
});
|
|
119
|
+
}
|
package/dist/cli.d.ts
ADDED
package/dist/cli.js
ADDED
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import crypto from "node:crypto";
|
|
3
|
+
import { startProxy } from "./adapters/proxy.js";
|
|
4
|
+
import { NETWORK_USDC } from "./protocol.js";
|
|
5
|
+
function parseFlags(argv) {
|
|
6
|
+
const positional = [];
|
|
7
|
+
const flags = {};
|
|
8
|
+
for (let i = 0; i < argv.length; i++) {
|
|
9
|
+
const arg = argv[i];
|
|
10
|
+
if (arg.startsWith("--")) {
|
|
11
|
+
const key = arg.slice(2);
|
|
12
|
+
const next = argv[i + 1];
|
|
13
|
+
if (next === undefined || next.startsWith("--")) {
|
|
14
|
+
flags[key] = true;
|
|
15
|
+
}
|
|
16
|
+
else {
|
|
17
|
+
flags[key] = next;
|
|
18
|
+
i++;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
else {
|
|
22
|
+
positional.push(arg);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
return { positional, flags };
|
|
26
|
+
}
|
|
27
|
+
const USAGE = `
|
|
28
|
+
furlpay-gateway — x402 monetization gateway
|
|
29
|
+
|
|
30
|
+
USAGE
|
|
31
|
+
furlpay-gateway expose <origin> [options]
|
|
32
|
+
furlpay-gateway secret
|
|
33
|
+
|
|
34
|
+
COMMANDS
|
|
35
|
+
expose <origin> Paywall an origin and serve it on a public port.
|
|
36
|
+
secret Generate a quote secret.
|
|
37
|
+
|
|
38
|
+
OPTIONS (expose)
|
|
39
|
+
--price <usd> Price per call in USD. (required)
|
|
40
|
+
--pay-to <address> Wallet that receives payment. (required, or PAY_TO)
|
|
41
|
+
--secret <hex> Quote HMAC secret. (or QUOTE_SECRET)
|
|
42
|
+
--network <name> ${Object.keys(NETWORK_USDC).join(", ")} (default: base)
|
|
43
|
+
--port <n> Listen port. (default: 4402)
|
|
44
|
+
--facilitator <url> x402 facilitator. (default: FurlPay)
|
|
45
|
+
--free <paths> Comma-separated free paths, e.g. /health,/docs*
|
|
46
|
+
--public-url <url> Public base URL if behind TLS termination.
|
|
47
|
+
--no-settle Verify only; do not settle on-chain. (unsafe)
|
|
48
|
+
|
|
49
|
+
ENVIRONMENT
|
|
50
|
+
PAY_TO, QUOTE_SECRET, FURLPAY_FACILITATOR
|
|
51
|
+
|
|
52
|
+
EXAMPLE
|
|
53
|
+
furlpay-gateway secret
|
|
54
|
+
export QUOTE_SECRET=<the value it printed>
|
|
55
|
+
furlpay-gateway expose http://localhost:8000 \\
|
|
56
|
+
--price 0.01 --pay-to 0xYourWallet --network base --free /health
|
|
57
|
+
|
|
58
|
+
SECURITY
|
|
59
|
+
The origin must NOT be publicly reachable, or agents will route around the
|
|
60
|
+
paywall. Bind it to loopback or firewall it to this process.
|
|
61
|
+
`;
|
|
62
|
+
function die(msg) {
|
|
63
|
+
process.stderr.write(`error: ${msg}\n\nRun \`furlpay-gateway --help\` for usage.\n`);
|
|
64
|
+
process.exit(1);
|
|
65
|
+
}
|
|
66
|
+
async function main() {
|
|
67
|
+
const { positional, flags } = parseFlags(process.argv.slice(2));
|
|
68
|
+
const command = positional[0];
|
|
69
|
+
if (!command || flags.help || flags.h) {
|
|
70
|
+
process.stdout.write(USAGE);
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
if (command === "secret") {
|
|
74
|
+
process.stdout.write(crypto.randomBytes(32).toString("hex") + "\n");
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
if (command !== "expose")
|
|
78
|
+
die(`unknown command "${command}"`);
|
|
79
|
+
const origin = positional[1];
|
|
80
|
+
if (!origin)
|
|
81
|
+
die("expose requires an origin, e.g. http://localhost:8000");
|
|
82
|
+
if (!/^https?:\/\//.test(origin))
|
|
83
|
+
die(`origin must start with http:// or https:// (got "${origin}")`);
|
|
84
|
+
const price = Number(flags.price);
|
|
85
|
+
if (!Number.isFinite(price) || price <= 0)
|
|
86
|
+
die("--price must be a positive number of USD");
|
|
87
|
+
const payTo = flags["pay-to"] || process.env.PAY_TO;
|
|
88
|
+
if (!payTo)
|
|
89
|
+
die("--pay-to (or PAY_TO) is required — nobody gets paid otherwise");
|
|
90
|
+
const quoteSecret = flags.secret || process.env.QUOTE_SECRET;
|
|
91
|
+
if (!quoteSecret) {
|
|
92
|
+
die("--secret (or QUOTE_SECRET) is required. Generate one: furlpay-gateway secret");
|
|
93
|
+
}
|
|
94
|
+
const network = flags.network || "base";
|
|
95
|
+
if (!NETWORK_USDC[network]) {
|
|
96
|
+
die(`unknown network "${network}" — one of: ${Object.keys(NETWORK_USDC).join(", ")}`);
|
|
97
|
+
}
|
|
98
|
+
const freePaths = typeof flags.free === "string" ? flags.free.split(",").map((s) => s.trim()) : [];
|
|
99
|
+
const port = flags.port ? Number(flags.port) : 4402;
|
|
100
|
+
const facilitator = flags.facilitator || process.env.FURLPAY_FACILITATOR;
|
|
101
|
+
const running = await startProxy({
|
|
102
|
+
origin,
|
|
103
|
+
port,
|
|
104
|
+
price,
|
|
105
|
+
payTo,
|
|
106
|
+
quoteSecret,
|
|
107
|
+
network,
|
|
108
|
+
freePaths,
|
|
109
|
+
settle: flags["no-settle"] !== true,
|
|
110
|
+
publicUrl: flags["public-url"],
|
|
111
|
+
...(facilitator ? { facilitator } : {}),
|
|
112
|
+
onSettled: (r) => {
|
|
113
|
+
process.stdout.write(`paid $${(Number(r.amountAtomic) / 1e6).toFixed(6)} ${r.payer.slice(0, 10)}… ${r.resource}\n`);
|
|
114
|
+
},
|
|
115
|
+
});
|
|
116
|
+
const lines = [
|
|
117
|
+
``,
|
|
118
|
+
` FurlPay Gateway`,
|
|
119
|
+
``,
|
|
120
|
+
` paywalling ${origin}`,
|
|
121
|
+
` listening http://localhost:${running.port}`,
|
|
122
|
+
` price $${price.toFixed(6)} per call`,
|
|
123
|
+
` pay to ${payTo}`,
|
|
124
|
+
` network ${network} (USDC ${NETWORK_USDC[network].slice(0, 10)}…)`,
|
|
125
|
+
` facilitator ${facilitator ?? "furlpay.com (default)"}`,
|
|
126
|
+
freePaths.length ? ` free ${freePaths.join(", ")}` : ``,
|
|
127
|
+
flags["no-settle"] === true ? ` WARNING --no-settle: payments are verified but NOT collected` : ``,
|
|
128
|
+
``,
|
|
129
|
+
` Replay protection is in-memory: correct for ONE instance only.`,
|
|
130
|
+
` Run more than one and pass a shared claim store (see README).`,
|
|
131
|
+
``,
|
|
132
|
+
].filter(Boolean);
|
|
133
|
+
process.stdout.write(lines.join("\n") + "\n");
|
|
134
|
+
const shutdown = async () => {
|
|
135
|
+
await running.close();
|
|
136
|
+
process.exit(0);
|
|
137
|
+
};
|
|
138
|
+
process.on("SIGINT", shutdown);
|
|
139
|
+
process.on("SIGTERM", shutdown);
|
|
140
|
+
}
|
|
141
|
+
main().catch((e) => die(e.message));
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import type { PaymentPayloadV1, PaymentPayloadV2, PaymentRequirementsV1, PaymentRequirementsV2 } from "./protocol.js";
|
|
2
|
+
export declare const FURLPAY_FACILITATOR = "https://furlpay.com/api/x402/facilitator";
|
|
3
|
+
/** The envelope is versioned: v1 and v2 carry different inner shapes. */
|
|
4
|
+
export type FacilitatorRequest = {
|
|
5
|
+
x402Version: 1;
|
|
6
|
+
paymentPayload: PaymentPayloadV1;
|
|
7
|
+
paymentRequirements: PaymentRequirementsV1;
|
|
8
|
+
} | {
|
|
9
|
+
x402Version: 2;
|
|
10
|
+
paymentPayload: PaymentPayloadV2;
|
|
11
|
+
paymentRequirements: PaymentRequirementsV2;
|
|
12
|
+
};
|
|
13
|
+
export interface VerifyResult {
|
|
14
|
+
isValid: boolean;
|
|
15
|
+
payer?: string;
|
|
16
|
+
invalidReason?: string;
|
|
17
|
+
invalidMessage?: string;
|
|
18
|
+
}
|
|
19
|
+
export interface SettleResult {
|
|
20
|
+
success: boolean;
|
|
21
|
+
transaction?: string;
|
|
22
|
+
network: string;
|
|
23
|
+
payer?: string;
|
|
24
|
+
errorReason?: string;
|
|
25
|
+
errorMessage?: string;
|
|
26
|
+
/** Actual amount settled. v2's `upto` scheme can settle LESS than the
|
|
27
|
+
* authorized maximum, so the receipt must report this, not the authorization. */
|
|
28
|
+
amount?: string;
|
|
29
|
+
confirmations?: number;
|
|
30
|
+
requiredConfirmations?: number;
|
|
31
|
+
}
|
|
32
|
+
export interface SupportedKind {
|
|
33
|
+
x402Version: number;
|
|
34
|
+
scheme: string;
|
|
35
|
+
network: string;
|
|
36
|
+
}
|
|
37
|
+
export interface FacilitatorClient {
|
|
38
|
+
verify(req: FacilitatorRequest): Promise<VerifyResult>;
|
|
39
|
+
settle(req: FacilitatorRequest): Promise<SettleResult>;
|
|
40
|
+
supported(): Promise<SupportedKind[]>;
|
|
41
|
+
}
|
|
42
|
+
export interface HttpFacilitatorOptions {
|
|
43
|
+
apiKey?: string;
|
|
44
|
+
/** A hung facilitator must not hang the origin. */
|
|
45
|
+
timeoutMs?: number;
|
|
46
|
+
fetchImpl?: typeof fetch;
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* HTTP facilitator client.
|
|
50
|
+
*
|
|
51
|
+
* Every failure path fails CLOSED: a timeout, a non-2xx, or an unparseable body
|
|
52
|
+
* resolves to "not valid" / "not settled", never to a released resource. A
|
|
53
|
+
* facilitator you cannot reach must cost you a sale, not the resource.
|
|
54
|
+
*/
|
|
55
|
+
export declare function httpFacilitator(baseUrl?: string, opts?: HttpFacilitatorOptions): FacilitatorClient;
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
// ---------------------------------------------------------------------------
|
|
2
|
+
// Facilitator client.
|
|
3
|
+
//
|
|
4
|
+
// The facilitator is the only party that touches a chain: it verifies the
|
|
5
|
+
// EIP-3009 authorization, submits it, and pays the gas. Keeping it behind an
|
|
6
|
+
// interface is the whole point — the gateway needs no private key, no RPC
|
|
7
|
+
// endpoint and no gas balance, which is what makes it safe to self-host.
|
|
8
|
+
//
|
|
9
|
+
// Default: FurlPay's facilitator. Any x402-compliant facilitator works; point
|
|
10
|
+
// `baseUrl` at Coinbase's and settlement goes through Coinbase, unchanged.
|
|
11
|
+
// ---------------------------------------------------------------------------
|
|
12
|
+
export const FURLPAY_FACILITATOR = "https://furlpay.com/api/x402/facilitator";
|
|
13
|
+
/**
|
|
14
|
+
* HTTP facilitator client.
|
|
15
|
+
*
|
|
16
|
+
* Every failure path fails CLOSED: a timeout, a non-2xx, or an unparseable body
|
|
17
|
+
* resolves to "not valid" / "not settled", never to a released resource. A
|
|
18
|
+
* facilitator you cannot reach must cost you a sale, not the resource.
|
|
19
|
+
*/
|
|
20
|
+
export function httpFacilitator(baseUrl = FURLPAY_FACILITATOR, opts = {}) {
|
|
21
|
+
const base = baseUrl.replace(/\/+$/, "");
|
|
22
|
+
const timeoutMs = opts.timeoutMs ?? 10_000;
|
|
23
|
+
const doFetch = opts.fetchImpl ?? globalThis.fetch;
|
|
24
|
+
if (typeof doFetch !== "function") {
|
|
25
|
+
throw new Error("No fetch implementation available — pass `fetchImpl` (Node >=18 has a global fetch).");
|
|
26
|
+
}
|
|
27
|
+
async function post(path, body, onError) {
|
|
28
|
+
const ctl = new AbortController();
|
|
29
|
+
const timer = setTimeout(() => ctl.abort(), timeoutMs);
|
|
30
|
+
try {
|
|
31
|
+
const res = await doFetch(`${base}${path}`, {
|
|
32
|
+
method: "POST",
|
|
33
|
+
headers: {
|
|
34
|
+
"Content-Type": "application/json",
|
|
35
|
+
...(opts.apiKey ? { Authorization: `Bearer ${opts.apiKey}` } : {}),
|
|
36
|
+
},
|
|
37
|
+
body: JSON.stringify(body),
|
|
38
|
+
signal: ctl.signal,
|
|
39
|
+
});
|
|
40
|
+
if (!res.ok)
|
|
41
|
+
return onError(`facilitator_http_${res.status}`);
|
|
42
|
+
return (await res.json());
|
|
43
|
+
}
|
|
44
|
+
catch (e) {
|
|
45
|
+
const reason = e?.name === "AbortError" ? "facilitator_timeout" : "facilitator_unreachable";
|
|
46
|
+
return onError(reason);
|
|
47
|
+
}
|
|
48
|
+
finally {
|
|
49
|
+
clearTimeout(timer);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
return {
|
|
53
|
+
verify(req) {
|
|
54
|
+
return post("/verify", req, (invalidReason) => ({ isValid: false, invalidReason }));
|
|
55
|
+
},
|
|
56
|
+
settle(req) {
|
|
57
|
+
const network = req.x402Version === 2 ? req.paymentRequirements.network : req.paymentRequirements.network;
|
|
58
|
+
return post("/settle", req, (errorReason) => ({ success: false, network, errorReason }));
|
|
59
|
+
},
|
|
60
|
+
async supported() {
|
|
61
|
+
const ctl = new AbortController();
|
|
62
|
+
const timer = setTimeout(() => ctl.abort(), timeoutMs);
|
|
63
|
+
try {
|
|
64
|
+
const res = await doFetch(`${base}/supported`, { signal: ctl.signal });
|
|
65
|
+
if (!res.ok)
|
|
66
|
+
return [];
|
|
67
|
+
const body = (await res.json());
|
|
68
|
+
return Array.isArray(body) ? body : (body.kinds ?? []);
|
|
69
|
+
}
|
|
70
|
+
catch {
|
|
71
|
+
return [];
|
|
72
|
+
}
|
|
73
|
+
finally {
|
|
74
|
+
clearTimeout(timer);
|
|
75
|
+
}
|
|
76
|
+
},
|
|
77
|
+
};
|
|
78
|
+
}
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import { LATEST_X402_VERSION, type PaymentRequiredV1, type PaymentRequiredV2, type SettlementReceipt, type X402Version } from "./protocol.js";
|
|
2
|
+
import { type FacilitatorClient } from "./facilitator.js";
|
|
3
|
+
import { type ClaimStore } from "./store.js";
|
|
4
|
+
export interface RequestContext {
|
|
5
|
+
method: string;
|
|
6
|
+
/** Canonical resource identity — the absolute URL, including query. */
|
|
7
|
+
resource: string;
|
|
8
|
+
url: URL;
|
|
9
|
+
headers: Headers;
|
|
10
|
+
}
|
|
11
|
+
export type PriceResolver = number | ((ctx: RequestContext) => number | Promise<number>);
|
|
12
|
+
export interface GatewayOptions {
|
|
13
|
+
price: PriceResolver;
|
|
14
|
+
payTo: string;
|
|
15
|
+
/** HMAC secret binding quotes to resources. No default — a published default
|
|
16
|
+
* would be a forgeable quote for every deployment that didn't override it. */
|
|
17
|
+
quoteSecret: string;
|
|
18
|
+
network?: string;
|
|
19
|
+
asset?: string;
|
|
20
|
+
assetName?: string;
|
|
21
|
+
assetVersion?: string;
|
|
22
|
+
facilitator?: FacilitatorClient | string;
|
|
23
|
+
/** Pin the dialect used to talk to the facilitator. Default: mirror the
|
|
24
|
+
* client. Set to 1 when pointing at a v1-only facilitator. */
|
|
25
|
+
facilitatorVersion?: X402Version;
|
|
26
|
+
claimStore?: ClaimStore;
|
|
27
|
+
quoteTtlSeconds?: number;
|
|
28
|
+
description?: string | ((ctx: RequestContext) => string);
|
|
29
|
+
mimeType?: string;
|
|
30
|
+
/** v2 discovery metadata. These are the protocol's native fields — a
|
|
31
|
+
* facilitator can crawl them, so filling them in is how the resource becomes
|
|
32
|
+
* discoverable without any proprietary index. */
|
|
33
|
+
serviceName?: string;
|
|
34
|
+
tags?: string[];
|
|
35
|
+
iconUrl?: string;
|
|
36
|
+
/** Bind the quote to a hash of the request body (default on for POST/PUT/PATCH).
|
|
37
|
+
* Without it, a payment for {"model":"small"} also unlocks {"model":"large"}. */
|
|
38
|
+
bindBody?: boolean;
|
|
39
|
+
/** Settle on-chain before releasing (default). `false` verifies but does not
|
|
40
|
+
* collect — only correct if you settle out of band. */
|
|
41
|
+
settle?: boolean;
|
|
42
|
+
resource?: (ctx: {
|
|
43
|
+
method: string;
|
|
44
|
+
url: URL;
|
|
45
|
+
}) => string;
|
|
46
|
+
onSettled?: (receipt: SettlementReceipt) => void | Promise<void>;
|
|
47
|
+
onRejected?: (info: {
|
|
48
|
+
reason: string;
|
|
49
|
+
resource: string;
|
|
50
|
+
payer?: string;
|
|
51
|
+
}) => void | Promise<void>;
|
|
52
|
+
}
|
|
53
|
+
export type GatewayDecision = {
|
|
54
|
+
kind: "allow";
|
|
55
|
+
receipt: SettlementReceipt;
|
|
56
|
+
headers: Record<string, string>;
|
|
57
|
+
} | {
|
|
58
|
+
kind: "challenge";
|
|
59
|
+
status: 402;
|
|
60
|
+
body: PaymentRequiredV1;
|
|
61
|
+
headers: Record<string, string>;
|
|
62
|
+
} | {
|
|
63
|
+
kind: "reject";
|
|
64
|
+
status: 400;
|
|
65
|
+
body: {
|
|
66
|
+
error: string;
|
|
67
|
+
};
|
|
68
|
+
headers: Record<string, string>;
|
|
69
|
+
};
|
|
70
|
+
export interface AuthorizeInput {
|
|
71
|
+
method: string;
|
|
72
|
+
url: string | URL;
|
|
73
|
+
headers: Headers | Record<string, string | undefined>;
|
|
74
|
+
/** Raw request body. Required for body binding on POST/PUT/PATCH. */
|
|
75
|
+
body?: string | Buffer;
|
|
76
|
+
}
|
|
77
|
+
export declare class Gateway {
|
|
78
|
+
private readonly opts;
|
|
79
|
+
private readonly facilitator;
|
|
80
|
+
private readonly claims;
|
|
81
|
+
private readonly network;
|
|
82
|
+
private readonly asset;
|
|
83
|
+
private readonly ttl;
|
|
84
|
+
constructor(opts: GatewayOptions);
|
|
85
|
+
/** True when replay claims are shared across instances. Assert this at boot in
|
|
86
|
+
* any multi-instance deployment. */
|
|
87
|
+
get replayProtectionDurable(): boolean;
|
|
88
|
+
private resourceId;
|
|
89
|
+
private context;
|
|
90
|
+
private priceFor;
|
|
91
|
+
private describe;
|
|
92
|
+
private shouldBindBody;
|
|
93
|
+
/** Mint a fresh quote for this request. */
|
|
94
|
+
private mint;
|
|
95
|
+
private extraFor;
|
|
96
|
+
private toV1;
|
|
97
|
+
private toV2;
|
|
98
|
+
private resourceInfo;
|
|
99
|
+
/** Public quote surface — the v2 402 payload for this request. */
|
|
100
|
+
quote(input: AuthorizeInput): Promise<PaymentRequiredV2>;
|
|
101
|
+
/**
|
|
102
|
+
* A 402 advertises BOTH dialects: v2 in the PAYMENT-REQUIRED header, v1 in the
|
|
103
|
+
* body. v2 moving payment data out of the body is exactly what makes this
|
|
104
|
+
* possible — a v2 client reads the header, a v1 client reads the body, and
|
|
105
|
+
* neither needs to know the other exists.
|
|
106
|
+
*/
|
|
107
|
+
private challenge;
|
|
108
|
+
private reject;
|
|
109
|
+
/**
|
|
110
|
+
* Returns `allow` only when a payment has been verified against a
|
|
111
|
+
* server-derived quote, burned as single-use, and settled to the confirmation
|
|
112
|
+
* depth its value requires.
|
|
113
|
+
*/
|
|
114
|
+
authorize(input: AuthorizeInput): Promise<GatewayDecision>;
|
|
115
|
+
/** Rebuild the requirements the quote was minted with, so the facilitator
|
|
116
|
+
* checks the payment against the SERVER's terms, not the client's copy. */
|
|
117
|
+
private facilitatorRequest;
|
|
118
|
+
/** Answer in the dialect the client spoke — and, harmlessly, the other one too,
|
|
119
|
+
* so a proxy or SDK expecting either finds its receipt. */
|
|
120
|
+
private allow;
|
|
121
|
+
}
|
|
122
|
+
export { LATEST_X402_VERSION };
|