@runtab/mcp 0.0.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/LICENSE +21 -0
- package/dist/bootstrap.d.ts +18 -0
- package/dist/bootstrap.js +125 -0
- package/dist/cli-config.d.ts +11 -0
- package/dist/cli-config.js +59 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +33 -0
- package/dist/control-plane-origin.d.ts +4 -0
- package/dist/control-plane-origin.js +27 -0
- package/dist/detect.d.ts +10 -0
- package/dist/detect.js +74 -0
- package/dist/durable-mcp-payment.d.ts +40 -0
- package/dist/durable-mcp-payment.js +105 -0
- package/dist/durable-payment-fetch.d.ts +19 -0
- package/dist/durable-payment-fetch.js +117 -0
- package/dist/eip3009-authorization.d.ts +59 -0
- package/dist/eip3009-authorization.js +142 -0
- package/dist/errors.d.ts +4 -0
- package/dist/errors.js +7 -0
- package/dist/fetch-wire.d.ts +46 -0
- package/dist/fetch-wire.js +143 -0
- package/dist/fetch-wrapper.d.ts +24 -0
- package/dist/fetch-wrapper.js +70 -0
- package/dist/fetch.d.ts +3 -0
- package/dist/fetch.js +1 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.js +4 -0
- package/dist/origin-context.d.ts +5 -0
- package/dist/origin-context.js +15 -0
- package/dist/paid-fetch-server.d.ts +49 -0
- package/dist/paid-fetch-server.js +104 -0
- package/dist/payment-authorization-state.d.ts +48 -0
- package/dist/payment-authorization-state.js +150 -0
- package/dist/payment-client.d.ts +4 -0
- package/dist/payment-client.js +14 -0
- package/dist/payment-correlation.d.ts +9 -0
- package/dist/payment-correlation.js +36 -0
- package/dist/payment-envelope-capacity.d.ts +2 -0
- package/dist/payment-envelope-capacity.js +28 -0
- package/dist/payment-envelope-disk.d.ts +5 -0
- package/dist/payment-envelope-disk.js +97 -0
- package/dist/payment-envelope-journal.d.ts +54 -0
- package/dist/payment-envelope-journal.js +116 -0
- package/dist/payment-envelope-lock.d.ts +5 -0
- package/dist/payment-envelope-lock.js +88 -0
- package/dist/payment-envelope-model.d.ts +32 -0
- package/dist/payment-envelope-model.js +123 -0
- package/dist/payment-envelope-store.d.ts +31 -0
- package/dist/payment-envelope-store.js +210 -0
- package/dist/payment-envelope.d.ts +17 -0
- package/dist/payment-envelope.js +145 -0
- package/dist/payment-idempotency.d.ts +2 -0
- package/dist/payment-idempotency.js +8 -0
- package/dist/payment-observation-reporter.d.ts +31 -0
- package/dist/payment-observation-reporter.js +151 -0
- package/dist/payment-profile.d.ts +3 -0
- package/dist/payment-profile.js +4 -0
- package/dist/payment-request-fingerprint.d.ts +5 -0
- package/dist/payment-request-fingerprint.js +54 -0
- package/dist/payment-settlement-observation.d.ts +9 -0
- package/dist/payment-settlement-observation.js +74 -0
- package/dist/payment-signal.d.ts +2 -0
- package/dist/payment-signal.js +8 -0
- package/dist/payment-target-network.d.ts +16 -0
- package/dist/payment-target-network.js +178 -0
- package/dist/payment-target-policy.d.ts +10 -0
- package/dist/payment-target-policy.js +75 -0
- package/dist/proxy.d.ts +47 -0
- package/dist/proxy.js +72 -0
- package/dist/remote-signer-http.d.ts +16 -0
- package/dist/remote-signer-http.js +182 -0
- package/dist/remote-signer.d.ts +34 -0
- package/dist/remote-signer.js +157 -0
- package/dist/resource-url.d.ts +5 -0
- package/dist/resource-url.js +36 -0
- package/dist/routing.d.ts +13 -0
- package/dist/routing.js +42 -0
- package/dist/runtime.d.ts +13 -0
- package/dist/runtime.js +92 -0
- package/dist/upstream.d.ts +43 -0
- package/dist/upstream.js +38 -0
- package/package.json +65 -0
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
2
|
+
import type { PaymentProfile } from "./payment-profile.js";
|
|
3
|
+
import { type PaymentTargetLookup } from "./payment-target-network.js";
|
|
4
|
+
import type { TabRemoteSigner } from "./remote-signer.js";
|
|
5
|
+
interface PaidFetchServerOptions {
|
|
6
|
+
address: `0x${string}` | null;
|
|
7
|
+
allowDevelopmentLoopback?: boolean;
|
|
8
|
+
apiBaseUrl: string;
|
|
9
|
+
apiKey: string;
|
|
10
|
+
fetch?: typeof globalThis.fetch;
|
|
11
|
+
lookup?: PaymentTargetLookup;
|
|
12
|
+
paymentProfile: PaymentProfile;
|
|
13
|
+
signer?: TabRemoteSigner;
|
|
14
|
+
}
|
|
15
|
+
export declare function createPaidFetchServer(options: PaidFetchServerOptions): Server<{
|
|
16
|
+
method: string;
|
|
17
|
+
params?: {
|
|
18
|
+
[x: string]: unknown;
|
|
19
|
+
_meta?: {
|
|
20
|
+
[x: string]: unknown;
|
|
21
|
+
progressToken?: string | number | undefined;
|
|
22
|
+
"io.modelcontextprotocol/related-task"?: {
|
|
23
|
+
taskId: string;
|
|
24
|
+
} | undefined;
|
|
25
|
+
} | undefined;
|
|
26
|
+
} | undefined;
|
|
27
|
+
}, {
|
|
28
|
+
method: string;
|
|
29
|
+
params?: {
|
|
30
|
+
[x: string]: unknown;
|
|
31
|
+
_meta?: {
|
|
32
|
+
[x: string]: unknown;
|
|
33
|
+
progressToken?: string | number | undefined;
|
|
34
|
+
"io.modelcontextprotocol/related-task"?: {
|
|
35
|
+
taskId: string;
|
|
36
|
+
} | undefined;
|
|
37
|
+
} | undefined;
|
|
38
|
+
} | undefined;
|
|
39
|
+
}, {
|
|
40
|
+
[x: string]: unknown;
|
|
41
|
+
_meta?: {
|
|
42
|
+
[x: string]: unknown;
|
|
43
|
+
progressToken?: string | number | undefined;
|
|
44
|
+
"io.modelcontextprotocol/related-task"?: {
|
|
45
|
+
taskId: string;
|
|
46
|
+
} | undefined;
|
|
47
|
+
} | undefined;
|
|
48
|
+
}>;
|
|
49
|
+
export {};
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
2
|
+
import { CallToolRequestSchema, ListToolsRequestSchema } from "@modelcontextprotocol/sdk/types.js";
|
|
3
|
+
import { PAID_FETCH_INPUT_SCHEMA, parsePaidFetchRequest, readBoundedResponse, readResponseHeaders, } from "./fetch-wire.js";
|
|
4
|
+
import { createTabFetch } from "./fetch-wrapper.js";
|
|
5
|
+
import { withPaymentIdempotencyKey } from "./payment-idempotency.js";
|
|
6
|
+
import { withPaymentSignal } from "./payment-signal.js";
|
|
7
|
+
import { createPinnedPaymentFetch } from "./payment-target-network.js";
|
|
8
|
+
function record(value) {
|
|
9
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
10
|
+
}
|
|
11
|
+
function toolResponse(value, isError = false) {
|
|
12
|
+
return {
|
|
13
|
+
content: [{ text: JSON.stringify(value), type: "text" }],
|
|
14
|
+
...(isError ? { isError: true } : {}),
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
function invalidRequest() {
|
|
18
|
+
return toolResponse({ error: { code: "INVALID_FETCH_REQUEST", message: "The fetch request is invalid." } }, true);
|
|
19
|
+
}
|
|
20
|
+
export function createPaidFetchServer(options) {
|
|
21
|
+
const server = new Server({ name: "tab-mcp", version: "0.0.1" }, { capabilities: { tools: {} } });
|
|
22
|
+
const allowDevelopmentLoopback = options.allowDevelopmentLoopback === true;
|
|
23
|
+
const baseFetch = options.fetch ?? globalThis.fetch;
|
|
24
|
+
const standalonePolicy = options.address
|
|
25
|
+
? undefined
|
|
26
|
+
: createPinnedPaymentFetch({
|
|
27
|
+
allowDevelopmentLoopback,
|
|
28
|
+
fetch: baseFetch,
|
|
29
|
+
...(options.lookup ? { lookup: options.lookup } : {}),
|
|
30
|
+
});
|
|
31
|
+
const leashFetch = options.address
|
|
32
|
+
? createTabFetch({
|
|
33
|
+
address: options.address,
|
|
34
|
+
allowDevelopmentLoopback,
|
|
35
|
+
apiBaseUrl: options.apiBaseUrl,
|
|
36
|
+
apiKey: options.apiKey,
|
|
37
|
+
clientName: () => server.getClientVersion()?.name ?? "Unknown client",
|
|
38
|
+
fetch: baseFetch,
|
|
39
|
+
...(options.lookup ? { lookup: options.lookup } : {}),
|
|
40
|
+
paymentProfile: options.paymentProfile,
|
|
41
|
+
...(options.signer ? { signer: options.signer } : {}),
|
|
42
|
+
})
|
|
43
|
+
: undefined;
|
|
44
|
+
const fetch_ = leashFetch ?? standalonePolicy?.fetch;
|
|
45
|
+
if (!fetch_)
|
|
46
|
+
throw new Error("The paid fetch target policy is unavailable");
|
|
47
|
+
server.onclose = () => {
|
|
48
|
+
void (leashFetch?.close() ?? standalonePolicy?.close());
|
|
49
|
+
};
|
|
50
|
+
server.setRequestHandler(ListToolsRequestSchema, () => ({
|
|
51
|
+
tools: [
|
|
52
|
+
{
|
|
53
|
+
description: "Fetch an HTTP resource and pay a supported x402 challenge within owner policy.",
|
|
54
|
+
inputSchema: PAID_FETCH_INPUT_SCHEMA,
|
|
55
|
+
name: "paid_fetch",
|
|
56
|
+
},
|
|
57
|
+
],
|
|
58
|
+
}));
|
|
59
|
+
server.setRequestHandler(CallToolRequestSchema, async (request, extra) => {
|
|
60
|
+
if (request.params.name !== "paid_fetch")
|
|
61
|
+
return invalidRequest();
|
|
62
|
+
let input;
|
|
63
|
+
try {
|
|
64
|
+
input = parsePaidFetchRequest(request.params.arguments, {
|
|
65
|
+
allowDevelopmentLoopback,
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
catch {
|
|
69
|
+
return invalidRequest();
|
|
70
|
+
}
|
|
71
|
+
try {
|
|
72
|
+
const signal = AbortSignal.any([extra.signal, AbortSignal.timeout(30_000)]);
|
|
73
|
+
const response = await withPaymentSignal(signal, () => withPaymentIdempotencyKey(input.idempotencyKey, () => fetch_(input.url, {
|
|
74
|
+
...(input.body === undefined ? {} : { body: input.body }),
|
|
75
|
+
...(input.headers === undefined ? {} : { headers: input.headers }),
|
|
76
|
+
method: input.method,
|
|
77
|
+
signal,
|
|
78
|
+
})));
|
|
79
|
+
if (!options.address && response.status === 402) {
|
|
80
|
+
void response.body?.cancel().catch(() => undefined);
|
|
81
|
+
return toolResponse({
|
|
82
|
+
error: {
|
|
83
|
+
code: "SIGNER_NOT_CONFIGURED",
|
|
84
|
+
message: "Agent signing is not configured for this agent.",
|
|
85
|
+
},
|
|
86
|
+
}, true);
|
|
87
|
+
}
|
|
88
|
+
const content = await readBoundedResponse(response);
|
|
89
|
+
return toolResponse({
|
|
90
|
+
...content,
|
|
91
|
+
headers: readResponseHeaders(response),
|
|
92
|
+
status: response.status,
|
|
93
|
+
statusText: response.statusText,
|
|
94
|
+
url: response.url,
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
catch (error) {
|
|
98
|
+
const remoteCode = record(error) && typeof error.code === "string" ? error.code : "";
|
|
99
|
+
const code = /^[A-Z0-9_]{1,64}$/.test(remoteCode) ? remoteCode : "FETCH_FAILED";
|
|
100
|
+
return toolResponse({ error: { code, message: "The fetch request failed." } }, true);
|
|
101
|
+
}
|
|
102
|
+
});
|
|
103
|
+
return server;
|
|
104
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
export declare const AUTHORIZATION_STATE_ABI: readonly [{
|
|
2
|
+
readonly inputs: readonly [{
|
|
3
|
+
readonly name: "authorizer";
|
|
4
|
+
readonly type: "address";
|
|
5
|
+
}, {
|
|
6
|
+
readonly name: "nonce";
|
|
7
|
+
readonly type: "bytes32";
|
|
8
|
+
}];
|
|
9
|
+
readonly name: "authorizationState";
|
|
10
|
+
readonly outputs: readonly [{
|
|
11
|
+
readonly name: "";
|
|
12
|
+
readonly type: "bool";
|
|
13
|
+
}];
|
|
14
|
+
readonly stateMutability: "view";
|
|
15
|
+
readonly type: "function";
|
|
16
|
+
}];
|
|
17
|
+
declare const NETWORKS: {
|
|
18
|
+
readonly "eip155:42161": {
|
|
19
|
+
readonly chainId: 42161;
|
|
20
|
+
readonly rpcUrl: "https://arb1.arbitrum.io/rpc";
|
|
21
|
+
};
|
|
22
|
+
readonly "eip155:8453": {
|
|
23
|
+
readonly chainId: 8453;
|
|
24
|
+
readonly rpcUrl: "https://mainnet.base.org";
|
|
25
|
+
};
|
|
26
|
+
readonly "eip155:84532": {
|
|
27
|
+
readonly chainId: 84532;
|
|
28
|
+
readonly rpcUrl: "https://sepolia.base.org";
|
|
29
|
+
};
|
|
30
|
+
};
|
|
31
|
+
interface PaymentAuthorizationIdentity {
|
|
32
|
+
asset: `0x${string}`;
|
|
33
|
+
from: `0x${string}`;
|
|
34
|
+
network: keyof typeof NETWORKS;
|
|
35
|
+
nonce: `0x${string}`;
|
|
36
|
+
validBefore: number;
|
|
37
|
+
}
|
|
38
|
+
interface AuthorizationStateOptions {
|
|
39
|
+
fetch?: typeof globalThis.fetch;
|
|
40
|
+
rpcUrl?: string;
|
|
41
|
+
signal?: AbortSignal;
|
|
42
|
+
}
|
|
43
|
+
export declare class PaymentReconciliationUnavailableError extends Error {
|
|
44
|
+
readonly code = "PAYMENT_RECONCILIATION_UNAVAILABLE";
|
|
45
|
+
constructor();
|
|
46
|
+
}
|
|
47
|
+
export declare function readPaymentAuthorizationState(identity: PaymentAuthorizationIdentity, options?: AuthorizationStateOptions): Promise<"unused" | "used">;
|
|
48
|
+
export {};
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import { decodeFunctionResult, encodeFunctionData } from "viem";
|
|
2
|
+
import { validatePaymentTarget } from "./payment-target-policy.js";
|
|
3
|
+
import { ARBITRUM_NETWORK, BASE_NETWORK, BASE_SEPOLIA_NETWORK } from "./routing.js";
|
|
4
|
+
export const AUTHORIZATION_STATE_ABI = [
|
|
5
|
+
{
|
|
6
|
+
inputs: [
|
|
7
|
+
{ name: "authorizer", type: "address" },
|
|
8
|
+
{ name: "nonce", type: "bytes32" },
|
|
9
|
+
],
|
|
10
|
+
name: "authorizationState",
|
|
11
|
+
outputs: [{ name: "", type: "bool" }],
|
|
12
|
+
stateMutability: "view",
|
|
13
|
+
type: "function",
|
|
14
|
+
},
|
|
15
|
+
];
|
|
16
|
+
const NETWORKS = {
|
|
17
|
+
[ARBITRUM_NETWORK]: { chainId: 42_161, rpcUrl: "https://arb1.arbitrum.io/rpc" },
|
|
18
|
+
[BASE_NETWORK]: { chainId: 8_453, rpcUrl: "https://mainnet.base.org" },
|
|
19
|
+
[BASE_SEPOLIA_NETWORK]: { chainId: 84_532, rpcUrl: "https://sepolia.base.org" },
|
|
20
|
+
};
|
|
21
|
+
const MAX_RPC_RESPONSE_BYTES = 65_536;
|
|
22
|
+
export class PaymentReconciliationUnavailableError extends Error {
|
|
23
|
+
code = "PAYMENT_RECONCILIATION_UNAVAILABLE";
|
|
24
|
+
constructor() {
|
|
25
|
+
super("Independent payment authorization state is unavailable.");
|
|
26
|
+
this.name = "PaymentReconciliationUnavailableError";
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
function record(value) {
|
|
30
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
31
|
+
}
|
|
32
|
+
async function boundedJson(response, signal) {
|
|
33
|
+
if (!response.ok || !response.body)
|
|
34
|
+
throw new PaymentReconciliationUnavailableError();
|
|
35
|
+
const reader = response.body.getReader();
|
|
36
|
+
const chunks = [];
|
|
37
|
+
let length = 0;
|
|
38
|
+
let rejectAbort;
|
|
39
|
+
const aborted = new Promise((_resolve, reject) => {
|
|
40
|
+
rejectAbort = reject;
|
|
41
|
+
});
|
|
42
|
+
const onAbort = () => {
|
|
43
|
+
void reader.cancel().catch(() => undefined);
|
|
44
|
+
rejectAbort?.(signal.reason);
|
|
45
|
+
};
|
|
46
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
47
|
+
try {
|
|
48
|
+
if (signal.aborted)
|
|
49
|
+
onAbort();
|
|
50
|
+
while (true) {
|
|
51
|
+
const read = await Promise.race([reader.read(), aborted]);
|
|
52
|
+
if (read.done)
|
|
53
|
+
break;
|
|
54
|
+
length += read.value.byteLength;
|
|
55
|
+
if (length > MAX_RPC_RESPONSE_BYTES) {
|
|
56
|
+
await reader.cancel().catch(() => undefined);
|
|
57
|
+
throw new PaymentReconciliationUnavailableError();
|
|
58
|
+
}
|
|
59
|
+
chunks.push(read.value);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
finally {
|
|
63
|
+
signal.removeEventListener("abort", onAbort);
|
|
64
|
+
reader.releaseLock();
|
|
65
|
+
}
|
|
66
|
+
const bytes = new Uint8Array(length);
|
|
67
|
+
let offset = 0;
|
|
68
|
+
for (const chunk of chunks) {
|
|
69
|
+
bytes.set(chunk, offset);
|
|
70
|
+
offset += chunk.byteLength;
|
|
71
|
+
}
|
|
72
|
+
return JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(bytes));
|
|
73
|
+
}
|
|
74
|
+
export async function readPaymentAuthorizationState(identity, options = {}) {
|
|
75
|
+
try {
|
|
76
|
+
const network = NETWORKS[identity.network];
|
|
77
|
+
if (!network)
|
|
78
|
+
throw new PaymentReconciliationUnavailableError();
|
|
79
|
+
const rpcUrl = validatePaymentTarget(options.rpcUrl ?? network.rpcUrl);
|
|
80
|
+
const timeout = AbortSignal.timeout(10_000);
|
|
81
|
+
const signal = options.signal ? AbortSignal.any([options.signal, timeout]) : timeout;
|
|
82
|
+
const fetch_ = options.fetch ?? globalThis.fetch;
|
|
83
|
+
const request = async (body) => boundedJson(await fetch_(rpcUrl, {
|
|
84
|
+
body: JSON.stringify(body),
|
|
85
|
+
headers: { accept: "application/json", "content-type": "application/json" },
|
|
86
|
+
method: "POST",
|
|
87
|
+
redirect: "error",
|
|
88
|
+
signal,
|
|
89
|
+
}), signal);
|
|
90
|
+
const head = await request([
|
|
91
|
+
{ id: 1, jsonrpc: "2.0", method: "eth_chainId", params: [] },
|
|
92
|
+
{
|
|
93
|
+
id: 2,
|
|
94
|
+
jsonrpc: "2.0",
|
|
95
|
+
method: "eth_getBlockByNumber",
|
|
96
|
+
params: ["finalized", false],
|
|
97
|
+
},
|
|
98
|
+
]);
|
|
99
|
+
if (!Array.isArray(head))
|
|
100
|
+
throw new PaymentReconciliationUnavailableError();
|
|
101
|
+
const chain = head.find((entry) => record(entry) && entry.id === 1);
|
|
102
|
+
const block = head.find((entry) => record(entry) && entry.id === 2);
|
|
103
|
+
const finalized = record(block) && record(block.result) ? block.result : undefined;
|
|
104
|
+
if (!record(chain) ||
|
|
105
|
+
chain.result !== `0x${network.chainId.toString(16)}` ||
|
|
106
|
+
!finalized ||
|
|
107
|
+
typeof finalized.hash !== "string" ||
|
|
108
|
+
!/^0x[0-9a-fA-F]{64}$/.test(finalized.hash) ||
|
|
109
|
+
typeof finalized.number !== "string" ||
|
|
110
|
+
!/^0x[0-9a-fA-F]+$/.test(finalized.number) ||
|
|
111
|
+
typeof finalized.timestamp !== "string" ||
|
|
112
|
+
!/^0x[0-9a-fA-F]+$/.test(finalized.timestamp) ||
|
|
113
|
+
BigInt(finalized.timestamp) < BigInt(identity.validBefore)) {
|
|
114
|
+
throw new PaymentReconciliationUnavailableError();
|
|
115
|
+
}
|
|
116
|
+
const state = await request({
|
|
117
|
+
id: 3,
|
|
118
|
+
jsonrpc: "2.0",
|
|
119
|
+
method: "eth_call",
|
|
120
|
+
params: [
|
|
121
|
+
{
|
|
122
|
+
data: encodeFunctionData({
|
|
123
|
+
abi: AUTHORIZATION_STATE_ABI,
|
|
124
|
+
args: [identity.from, identity.nonce],
|
|
125
|
+
functionName: "authorizationState",
|
|
126
|
+
}),
|
|
127
|
+
to: identity.asset,
|
|
128
|
+
},
|
|
129
|
+
{ blockHash: finalized.hash, requireCanonical: true },
|
|
130
|
+
],
|
|
131
|
+
});
|
|
132
|
+
if (!record(state) ||
|
|
133
|
+
state.id !== 3 ||
|
|
134
|
+
typeof state.result !== "string" ||
|
|
135
|
+
!/^0x[0-9a-fA-F]+$/.test(state.result)) {
|
|
136
|
+
throw new PaymentReconciliationUnavailableError();
|
|
137
|
+
}
|
|
138
|
+
const used = decodeFunctionResult({
|
|
139
|
+
abi: AUTHORIZATION_STATE_ABI,
|
|
140
|
+
data: state.result,
|
|
141
|
+
functionName: "authorizationState",
|
|
142
|
+
});
|
|
143
|
+
return used ? "used" : "unused";
|
|
144
|
+
}
|
|
145
|
+
catch (error) {
|
|
146
|
+
if (error instanceof PaymentReconciliationUnavailableError)
|
|
147
|
+
throw error;
|
|
148
|
+
throw new PaymentReconciliationUnavailableError();
|
|
149
|
+
}
|
|
150
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import { x402Client } from "@x402/core/client";
|
|
2
|
+
import type { PaymentProfile } from "./payment-profile.js";
|
|
3
|
+
import type { TabRemoteSigner } from "./remote-signer.js";
|
|
4
|
+
export declare function createLeashPaymentClient(signer: TabRemoteSigner, paymentProfile: PaymentProfile): x402Client;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { x402Client } from "@x402/core/client";
|
|
2
|
+
import { ExactEvmScheme } from "@x402/evm/exact/client";
|
|
3
|
+
import { ARBITRUM_NETWORK, BASE_NETWORK, BASE_SEPOLIA_NETWORK, selectLeashPaymentRequirements, } from "./routing.js";
|
|
4
|
+
export function createLeashPaymentClient(signer, paymentProfile) {
|
|
5
|
+
const scheme = new ExactEvmScheme(signer);
|
|
6
|
+
const client = new x402Client((version, requirements) => selectLeashPaymentRequirements(paymentProfile, version, requirements));
|
|
7
|
+
if (paymentProfile === "base_sepolia_integration") {
|
|
8
|
+
client.register(BASE_SEPOLIA_NETWORK, scheme);
|
|
9
|
+
}
|
|
10
|
+
else {
|
|
11
|
+
client.register(BASE_NETWORK, scheme).register(ARBITRUM_NETWORK, scheme);
|
|
12
|
+
}
|
|
13
|
+
return client;
|
|
14
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export declare class PaymentCorrelations {
|
|
2
|
+
#private;
|
|
3
|
+
readonly nowSeconds: () => number;
|
|
4
|
+
constructor(nowSeconds: () => number);
|
|
5
|
+
set(signature: string, receiptId: string, validBeforeSeconds: number): void;
|
|
6
|
+
restore(signature: string, receiptId: string, validBeforeSeconds: number): void;
|
|
7
|
+
get(signature: string): string | null;
|
|
8
|
+
deleteIf(signature: string, receiptId: string): void;
|
|
9
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
export class PaymentCorrelations {
|
|
2
|
+
nowSeconds;
|
|
3
|
+
#entries = new Map();
|
|
4
|
+
constructor(nowSeconds) {
|
|
5
|
+
this.nowSeconds = nowSeconds;
|
|
6
|
+
}
|
|
7
|
+
set(signature, receiptId, validBeforeSeconds) {
|
|
8
|
+
this.#prune();
|
|
9
|
+
this.#entries.set(signature.toLowerCase(), { receiptId, validBeforeSeconds });
|
|
10
|
+
}
|
|
11
|
+
restore(signature, receiptId, validBeforeSeconds) {
|
|
12
|
+
if (!/^0x[0-9a-fA-F]{130}$/.test(signature) ||
|
|
13
|
+
!/^\S{1,256}$/.test(receiptId) ||
|
|
14
|
+
!Number.isSafeInteger(validBeforeSeconds) ||
|
|
15
|
+
validBeforeSeconds < 1) {
|
|
16
|
+
throw new Error("The persisted payment correlation is invalid.");
|
|
17
|
+
}
|
|
18
|
+
this.set(signature, receiptId, Math.max(validBeforeSeconds, this.nowSeconds() + 3_600));
|
|
19
|
+
}
|
|
20
|
+
get(signature) {
|
|
21
|
+
this.#prune();
|
|
22
|
+
return this.#entries.get(signature.toLowerCase())?.receiptId ?? null;
|
|
23
|
+
}
|
|
24
|
+
deleteIf(signature, receiptId) {
|
|
25
|
+
const key = signature.toLowerCase();
|
|
26
|
+
if (this.#entries.get(key)?.receiptId === receiptId)
|
|
27
|
+
this.#entries.delete(key);
|
|
28
|
+
}
|
|
29
|
+
#prune() {
|
|
30
|
+
const now = this.nowSeconds();
|
|
31
|
+
for (const [signature, correlation] of this.#entries) {
|
|
32
|
+
if (correlation.validBeforeSeconds <= now)
|
|
33
|
+
this.#entries.delete(signature);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { MAX_PAYMENT_ENVELOPE_RECORDS, MAX_PAYMENT_ENVELOPE_STORE_BYTES, PaymentEnvelopeStoreError, } from "./payment-envelope-model.js";
|
|
2
|
+
const MAX_PAYMENT_SIGNATURE = "A".repeat(32_768);
|
|
3
|
+
const MAX_RECEIPT_ID = "r".repeat(128);
|
|
4
|
+
function capacityError() {
|
|
5
|
+
throw new PaymentEnvelopeStoreError("PAYMENT_ENVELOPE_CAPACITY", "The durable payment envelope store reached its capacity bound.");
|
|
6
|
+
}
|
|
7
|
+
export function assertNewPaymentEnvelopeCapacity(document, key, requestFingerprint, timestamp) {
|
|
8
|
+
if (Object.keys(document.records).length >= MAX_PAYMENT_ENVELOPE_RECORDS)
|
|
9
|
+
capacityError();
|
|
10
|
+
const worstCase = {
|
|
11
|
+
records: {
|
|
12
|
+
...document.records,
|
|
13
|
+
[key]: {
|
|
14
|
+
createdAt: timestamp,
|
|
15
|
+
paymentSignature: MAX_PAYMENT_SIGNATURE,
|
|
16
|
+
receiptId: MAX_RECEIPT_ID,
|
|
17
|
+
requestFingerprint,
|
|
18
|
+
state: "pending",
|
|
19
|
+
updatedAt: timestamp,
|
|
20
|
+
validBefore: Number.MAX_SAFE_INTEGER,
|
|
21
|
+
},
|
|
22
|
+
},
|
|
23
|
+
version: 1,
|
|
24
|
+
};
|
|
25
|
+
if (Buffer.byteLength(JSON.stringify(worstCase)) > MAX_PAYMENT_ENVELOPE_STORE_BYTES) {
|
|
26
|
+
capacityError();
|
|
27
|
+
}
|
|
28
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import { type PaymentEnvelopeDocument } from "./payment-envelope-model.js";
|
|
2
|
+
export declare const PAYMENT_ENVELOPE_FILE = "payment-envelopes.v1.json";
|
|
3
|
+
export declare function preparePaymentEnvelopeDirectory(directory: string): Promise<void>;
|
|
4
|
+
export declare function readPaymentEnvelopeDocument(directory: string): Promise<PaymentEnvelopeDocument>;
|
|
5
|
+
export declare function writePaymentEnvelopeDocument(directory: string, document: PaymentEnvelopeDocument): Promise<void>;
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { constants } from "node:fs";
|
|
3
|
+
import { chmod, lstat, mkdir, open, rename, rm } from "node:fs/promises";
|
|
4
|
+
import { TextDecoder } from "node:util";
|
|
5
|
+
import { MAX_PAYMENT_ENVELOPE_STORE_BYTES, PaymentEnvelopeStoreError, parsePaymentEnvelopeDocument, } from "./payment-envelope-model.js";
|
|
6
|
+
export const PAYMENT_ENVELOPE_FILE = "payment-envelopes.v1.json";
|
|
7
|
+
function ioError() {
|
|
8
|
+
return new PaymentEnvelopeStoreError("PAYMENT_ENVELOPE_CORRUPT", "The durable payment envelope store could not be read safely.");
|
|
9
|
+
}
|
|
10
|
+
export async function preparePaymentEnvelopeDirectory(directory) {
|
|
11
|
+
try {
|
|
12
|
+
await mkdir(directory, { mode: 0o700, recursive: true });
|
|
13
|
+
const metadata = await lstat(directory);
|
|
14
|
+
if (!metadata.isDirectory() || metadata.isSymbolicLink())
|
|
15
|
+
throw ioError();
|
|
16
|
+
await chmod(directory, 0o700);
|
|
17
|
+
}
|
|
18
|
+
catch {
|
|
19
|
+
throw ioError();
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
export async function readPaymentEnvelopeDocument(directory) {
|
|
23
|
+
const path = `${directory}/${PAYMENT_ENVELOPE_FILE}`;
|
|
24
|
+
let handle;
|
|
25
|
+
try {
|
|
26
|
+
handle = await open(path, constants.O_RDONLY | constants.O_NOFOLLOW);
|
|
27
|
+
}
|
|
28
|
+
catch (error) {
|
|
29
|
+
if (error.code === "ENOENT")
|
|
30
|
+
return { records: {}, version: 1 };
|
|
31
|
+
throw ioError();
|
|
32
|
+
}
|
|
33
|
+
try {
|
|
34
|
+
await handle.chmod(0o600);
|
|
35
|
+
const metadata = await handle.stat();
|
|
36
|
+
if (!metadata.isFile())
|
|
37
|
+
throw ioError();
|
|
38
|
+
if (metadata.size > MAX_PAYMENT_ENVELOPE_STORE_BYTES) {
|
|
39
|
+
throw new PaymentEnvelopeStoreError("PAYMENT_ENVELOPE_OVERSIZE", "The durable payment envelope store exceeds its size bound.");
|
|
40
|
+
}
|
|
41
|
+
const chunks = [];
|
|
42
|
+
let length = 0;
|
|
43
|
+
while (true) {
|
|
44
|
+
const chunk = Buffer.allocUnsafe(64 * 1_024);
|
|
45
|
+
const { bytesRead } = await handle.read(chunk, 0, chunk.length, null);
|
|
46
|
+
if (bytesRead === 0)
|
|
47
|
+
break;
|
|
48
|
+
length += bytesRead;
|
|
49
|
+
if (length > MAX_PAYMENT_ENVELOPE_STORE_BYTES) {
|
|
50
|
+
throw new PaymentEnvelopeStoreError("PAYMENT_ENVELOPE_OVERSIZE", "The durable payment envelope store exceeds its size bound.");
|
|
51
|
+
}
|
|
52
|
+
chunks.push(chunk.subarray(0, bytesRead));
|
|
53
|
+
}
|
|
54
|
+
const text = new TextDecoder("utf-8", { fatal: true }).decode(Buffer.concat(chunks, length));
|
|
55
|
+
return parsePaymentEnvelopeDocument(JSON.parse(text));
|
|
56
|
+
}
|
|
57
|
+
catch (error) {
|
|
58
|
+
if (error instanceof PaymentEnvelopeStoreError)
|
|
59
|
+
throw error;
|
|
60
|
+
throw ioError();
|
|
61
|
+
}
|
|
62
|
+
finally {
|
|
63
|
+
await handle.close();
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
export async function writePaymentEnvelopeDocument(directory, document) {
|
|
67
|
+
const contents = JSON.stringify(document);
|
|
68
|
+
if (Buffer.byteLength(contents) > MAX_PAYMENT_ENVELOPE_STORE_BYTES) {
|
|
69
|
+
throw new PaymentEnvelopeStoreError("PAYMENT_ENVELOPE_OVERSIZE", "The durable payment envelope store exceeds its size bound.");
|
|
70
|
+
}
|
|
71
|
+
const destination = `${directory}/${PAYMENT_ENVELOPE_FILE}`;
|
|
72
|
+
const temporary = `${destination}.tmp-${process.pid}-${randomUUID()}`;
|
|
73
|
+
let handle;
|
|
74
|
+
try {
|
|
75
|
+
handle = await open(temporary, constants.O_CREAT | constants.O_EXCL | constants.O_WRONLY | constants.O_NOFOLLOW, 0o600);
|
|
76
|
+
await handle.writeFile(contents, "utf8");
|
|
77
|
+
await handle.sync();
|
|
78
|
+
await handle.close();
|
|
79
|
+
handle = undefined;
|
|
80
|
+
await rename(temporary, destination);
|
|
81
|
+
await chmod(destination, 0o600);
|
|
82
|
+
const directoryHandle = await open(directory, constants.O_RDONLY);
|
|
83
|
+
try {
|
|
84
|
+
await directoryHandle.sync();
|
|
85
|
+
}
|
|
86
|
+
finally {
|
|
87
|
+
await directoryHandle.close();
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
catch (error) {
|
|
91
|
+
await handle?.close().catch(() => undefined);
|
|
92
|
+
await rm(temporary, { force: true }).catch(() => undefined);
|
|
93
|
+
if (error instanceof PaymentEnvelopeStoreError)
|
|
94
|
+
throw error;
|
|
95
|
+
throw ioError();
|
|
96
|
+
}
|
|
97
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { readPaymentAuthorizationState } from "./payment-authorization-state.js";
|
|
2
|
+
import { parsePaymentEnvelope } from "./payment-envelope.js";
|
|
3
|
+
import type { NewPaymentEnvelope, PaymentEnvelopeRecord, PaymentEnvelopeStore } from "./payment-envelope-store.js";
|
|
4
|
+
import type { PaymentProfile } from "./payment-profile.js";
|
|
5
|
+
import type { TabRemoteSigner } from "./remote-signer.js";
|
|
6
|
+
export declare class PaymentIdempotencyRequiredError extends Error {
|
|
7
|
+
readonly code = "PAYMENT_IDEMPOTENCY_KEY_REQUIRED";
|
|
8
|
+
constructor();
|
|
9
|
+
}
|
|
10
|
+
export declare class PaymentReconciliationRequiredError extends Error {
|
|
11
|
+
readonly code = "PAYMENT_RECONCILIATION_REQUIRED";
|
|
12
|
+
constructor();
|
|
13
|
+
}
|
|
14
|
+
interface PaymentEnvelopeJournalOptions {
|
|
15
|
+
address: `0x${string}`;
|
|
16
|
+
authorizationState?: typeof readPaymentAuthorizationState;
|
|
17
|
+
nowSeconds?: () => number;
|
|
18
|
+
paymentProfile: PaymentProfile;
|
|
19
|
+
signer: TabRemoteSigner;
|
|
20
|
+
store: PaymentEnvelopeStore;
|
|
21
|
+
}
|
|
22
|
+
type ParsedEnvelope = Awaited<ReturnType<typeof parsePaymentEnvelope>>;
|
|
23
|
+
type ParsedEnvelopePayload = Pick<ParsedEnvelope, "payload">;
|
|
24
|
+
export declare class PaymentEnvelopeJournal {
|
|
25
|
+
#private;
|
|
26
|
+
constructor(options: PaymentEnvelopeJournalOptions);
|
|
27
|
+
load(idempotencyKey: string, requestFingerprint: string): Promise<PaymentEnvelopeRecord | null>;
|
|
28
|
+
getOrCreate(idempotencyKey: string, requestFingerprint: string, factory: () => Promise<NewPaymentEnvelope>): Promise<PaymentEnvelopeRecord>;
|
|
29
|
+
parse(envelope: PaymentEnvelopeRecord): Promise<{
|
|
30
|
+
asset: `0x${string}`;
|
|
31
|
+
from: `0x${string}`;
|
|
32
|
+
network: "eip155:8453" | "eip155:42161" | "eip155:84532";
|
|
33
|
+
nonce: `0x${string}`;
|
|
34
|
+
payload: import("@x402/mcp").PaymentPayload;
|
|
35
|
+
signature: `0x${string}`;
|
|
36
|
+
validBefore: number;
|
|
37
|
+
}>;
|
|
38
|
+
recordObservation(idempotencyKey: string, requestFingerprint: string, parsed: ParsedEnvelopePayload, value: unknown): Promise<PaymentEnvelopeRecord | null>;
|
|
39
|
+
reconcileObserved(idempotencyKey: string, requestFingerprint: string, envelope: PaymentEnvelopeRecord, parsed: ParsedEnvelopePayload): Promise<{
|
|
40
|
+
status: "failed";
|
|
41
|
+
verified: true;
|
|
42
|
+
} | {
|
|
43
|
+
status: "pending";
|
|
44
|
+
verified: false;
|
|
45
|
+
} | {
|
|
46
|
+
status: "settled";
|
|
47
|
+
verified: true;
|
|
48
|
+
} | {
|
|
49
|
+
readonly status: "ignored";
|
|
50
|
+
readonly verified: false;
|
|
51
|
+
}>;
|
|
52
|
+
markSettled(idempotencyKey: string, requestFingerprint: string): Promise<PaymentEnvelopeRecord>;
|
|
53
|
+
}
|
|
54
|
+
export {};
|