@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
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Blockchain Oracle (Tab)
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { type PaymentProfile } from "./payment-profile.js";
|
|
2
|
+
interface ConnectLeashAgentOptions {
|
|
3
|
+
apiBaseUrl: string;
|
|
4
|
+
apiKey: string;
|
|
5
|
+
fetch?: typeof globalThis.fetch;
|
|
6
|
+
signal?: AbortSignal;
|
|
7
|
+
}
|
|
8
|
+
export interface ConnectedLeashAgent {
|
|
9
|
+
address: `0x${string}` | null;
|
|
10
|
+
paymentProfile: PaymentProfile;
|
|
11
|
+
}
|
|
12
|
+
export declare class LeashConnectError extends Error {
|
|
13
|
+
readonly code: string;
|
|
14
|
+
readonly status: number;
|
|
15
|
+
constructor(code: string, message: string, status: number);
|
|
16
|
+
}
|
|
17
|
+
export declare function connectLeashAgent(options: ConnectLeashAgentOptions): Promise<ConnectedLeashAgent>;
|
|
18
|
+
export {};
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import { isAddress } from "viem";
|
|
2
|
+
import { validateControlPlaneOrigin } from "./control-plane-origin.js";
|
|
3
|
+
import { isPaymentProfile } from "./payment-profile.js";
|
|
4
|
+
const MAX_CONNECT_RESPONSE_BYTES = 65_536;
|
|
5
|
+
export class LeashConnectError extends Error {
|
|
6
|
+
code;
|
|
7
|
+
status;
|
|
8
|
+
constructor(code, message, status) {
|
|
9
|
+
super(message);
|
|
10
|
+
this.code = code;
|
|
11
|
+
this.status = status;
|
|
12
|
+
this.name = "LeashConnectError";
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
function record(value) {
|
|
16
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
17
|
+
}
|
|
18
|
+
function errorCode(value) {
|
|
19
|
+
if (!record(value) || !record(value.error) || typeof value.error.code !== "string") {
|
|
20
|
+
return "CONNECT_FAILED";
|
|
21
|
+
}
|
|
22
|
+
return /^[A-Z0-9_]{1,64}$/.test(value.error.code) ? value.error.code : "CONNECT_FAILED";
|
|
23
|
+
}
|
|
24
|
+
function invalidConnectResponse() {
|
|
25
|
+
throw new LeashConnectError("INVALID_CONNECT_RESPONSE", "The agent control plane returned an invalid response.", 502);
|
|
26
|
+
}
|
|
27
|
+
async function responseJson(response, signal) {
|
|
28
|
+
if (!response.body)
|
|
29
|
+
invalidConnectResponse();
|
|
30
|
+
const reader = response.body.getReader();
|
|
31
|
+
const chunks = [];
|
|
32
|
+
let length = 0;
|
|
33
|
+
let rejectAbort;
|
|
34
|
+
const aborted = new Promise((_resolve, reject) => {
|
|
35
|
+
rejectAbort = reject;
|
|
36
|
+
});
|
|
37
|
+
const cancelReader = () => {
|
|
38
|
+
void reader.cancel().catch(() => undefined);
|
|
39
|
+
};
|
|
40
|
+
const onAbort = () => {
|
|
41
|
+
cancelReader();
|
|
42
|
+
rejectAbort?.(signal.reason ?? new Error("Aborted"));
|
|
43
|
+
};
|
|
44
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
45
|
+
try {
|
|
46
|
+
if (signal.aborted)
|
|
47
|
+
onAbort();
|
|
48
|
+
while (true) {
|
|
49
|
+
const result = await Promise.race([reader.read(), aborted]);
|
|
50
|
+
if (result.done)
|
|
51
|
+
break;
|
|
52
|
+
length += result.value.byteLength;
|
|
53
|
+
if (length > MAX_CONNECT_RESPONSE_BYTES) {
|
|
54
|
+
cancelReader();
|
|
55
|
+
invalidConnectResponse();
|
|
56
|
+
}
|
|
57
|
+
chunks.push(result.value);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
catch (error) {
|
|
61
|
+
if (error instanceof LeashConnectError)
|
|
62
|
+
throw error;
|
|
63
|
+
cancelReader();
|
|
64
|
+
throw new LeashConnectError("CONNECT_FAILED", "The agent control plane could not be reached.", 503);
|
|
65
|
+
}
|
|
66
|
+
finally {
|
|
67
|
+
signal.removeEventListener("abort", onAbort);
|
|
68
|
+
try {
|
|
69
|
+
reader.releaseLock();
|
|
70
|
+
}
|
|
71
|
+
catch {
|
|
72
|
+
// An aborted read may hold the lock until cancellation finishes.
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
const bytes = new Uint8Array(length);
|
|
76
|
+
let offset = 0;
|
|
77
|
+
for (const chunk of chunks) {
|
|
78
|
+
bytes.set(chunk, offset);
|
|
79
|
+
offset += chunk.byteLength;
|
|
80
|
+
}
|
|
81
|
+
try {
|
|
82
|
+
return JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(bytes));
|
|
83
|
+
}
|
|
84
|
+
catch {
|
|
85
|
+
invalidConnectResponse();
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
export async function connectLeashAgent(options) {
|
|
89
|
+
const fetch_ = options.fetch ?? globalThis.fetch;
|
|
90
|
+
const apiBaseUrl = validateControlPlaneOrigin(options.apiBaseUrl);
|
|
91
|
+
const timeout = AbortSignal.timeout(10_000);
|
|
92
|
+
const signal = options.signal ? AbortSignal.any([options.signal, timeout]) : timeout;
|
|
93
|
+
let response;
|
|
94
|
+
try {
|
|
95
|
+
response = await fetch_(new URL("/api/agent/connect", apiBaseUrl), {
|
|
96
|
+
body: JSON.stringify({ transport: "mcp" }),
|
|
97
|
+
headers: {
|
|
98
|
+
accept: "application/json",
|
|
99
|
+
authorization: `Bearer ${options.apiKey}`,
|
|
100
|
+
"content-type": "application/json",
|
|
101
|
+
},
|
|
102
|
+
method: "POST",
|
|
103
|
+
redirect: "error",
|
|
104
|
+
signal,
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
catch {
|
|
108
|
+
throw new LeashConnectError("CONNECT_FAILED", "The agent control plane could not be reached.", 503);
|
|
109
|
+
}
|
|
110
|
+
const body = await responseJson(response, signal);
|
|
111
|
+
if (!response.ok) {
|
|
112
|
+
throw new LeashConnectError(errorCode(body), "The agent control plane rejected the connection.", response.status);
|
|
113
|
+
}
|
|
114
|
+
if (!record(body) ||
|
|
115
|
+
!record(body.agent) ||
|
|
116
|
+
!("address" in body.agent) ||
|
|
117
|
+
!isPaymentProfile(body.paymentProfile)) {
|
|
118
|
+
throw new LeashConnectError("INVALID_CONNECT_RESPONSE", "The agent control plane returned an invalid response.", 502);
|
|
119
|
+
}
|
|
120
|
+
const address = body.agent.address;
|
|
121
|
+
if (address !== null && (typeof address !== "string" || !isAddress(address))) {
|
|
122
|
+
throw new LeashConnectError("INVALID_CONNECT_RESPONSE", "The agent control plane returned an invalid response.", 502);
|
|
123
|
+
}
|
|
124
|
+
return { address, paymentProfile: body.paymentProfile };
|
|
125
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export interface LeashCliConfig {
|
|
2
|
+
allowDevelopmentLoopback: boolean;
|
|
3
|
+
apiBaseUrl: string;
|
|
4
|
+
apiKey: string;
|
|
5
|
+
upstreamUrl: string | null;
|
|
6
|
+
}
|
|
7
|
+
export declare class CliConfigurationError extends Error {
|
|
8
|
+
readonly code = "INVALID_CONFIGURATION";
|
|
9
|
+
constructor(message: string);
|
|
10
|
+
}
|
|
11
|
+
export declare function parseLeashCliConfig(arguments_: readonly string[], environment: Readonly<Record<string, string | undefined>>): LeashCliConfig;
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { validateControlPlaneOrigin } from "./control-plane-origin.js";
|
|
2
|
+
import { validatePaymentTarget } from "./payment-target-policy.js";
|
|
3
|
+
const LEASH_KEY_PATTERN = /^agent_sk_[A-Za-z0-9_-]{43}$/;
|
|
4
|
+
export class CliConfigurationError extends Error {
|
|
5
|
+
code = "INVALID_CONFIGURATION";
|
|
6
|
+
constructor(message) {
|
|
7
|
+
super(message);
|
|
8
|
+
this.name = "CliConfigurationError";
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
function httpUrl(value, field, allowDevelopmentLoopback) {
|
|
12
|
+
try {
|
|
13
|
+
return validatePaymentTarget(value, { allowDevelopmentLoopback });
|
|
14
|
+
}
|
|
15
|
+
catch {
|
|
16
|
+
throw new CliConfigurationError(`${field} must use HTTPS, except for loopback development.`);
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
function apiBaseUrl(value) {
|
|
20
|
+
if (!value || value.trim() !== value) {
|
|
21
|
+
throw new CliConfigurationError("TAB_API_BASE_URL is required.");
|
|
22
|
+
}
|
|
23
|
+
try {
|
|
24
|
+
return validateControlPlaneOrigin(value);
|
|
25
|
+
}
|
|
26
|
+
catch {
|
|
27
|
+
throw new CliConfigurationError("TAB_API_BASE_URL must be an HTTPS origin, except for loopback development.");
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
function apiKey(value) {
|
|
31
|
+
if (!value || !LEASH_KEY_PATTERN.test(value)) {
|
|
32
|
+
throw new CliConfigurationError("TAB_AGENT_KEY is missing or malformed.");
|
|
33
|
+
}
|
|
34
|
+
return value;
|
|
35
|
+
}
|
|
36
|
+
function developmentLoopback(value) {
|
|
37
|
+
if (value === undefined || value === "0")
|
|
38
|
+
return false;
|
|
39
|
+
if (value === "1")
|
|
40
|
+
return true;
|
|
41
|
+
throw new CliConfigurationError("TAB_ALLOW_DEVELOPMENT_LOOPBACK must be 0 or 1.");
|
|
42
|
+
}
|
|
43
|
+
function upstreamUrl(arguments_, allowDevelopmentLoopback) {
|
|
44
|
+
if (arguments_.length === 0)
|
|
45
|
+
return null;
|
|
46
|
+
if (arguments_.length !== 2 || arguments_[0] !== "--upstream" || !arguments_[1]) {
|
|
47
|
+
throw new CliConfigurationError("Usage: tab-mcp [--upstream <absolute-http-url>]");
|
|
48
|
+
}
|
|
49
|
+
return httpUrl(arguments_[1], "--upstream", allowDevelopmentLoopback);
|
|
50
|
+
}
|
|
51
|
+
export function parseLeashCliConfig(arguments_, environment) {
|
|
52
|
+
const allowDevelopmentLoopback = developmentLoopback(environment.TAB_ALLOW_DEVELOPMENT_LOOPBACK);
|
|
53
|
+
return {
|
|
54
|
+
allowDevelopmentLoopback,
|
|
55
|
+
apiBaseUrl: apiBaseUrl(environment.TAB_API_BASE_URL),
|
|
56
|
+
apiKey: apiKey(environment.TAB_AGENT_KEY),
|
|
57
|
+
upstreamUrl: upstreamUrl(arguments_, allowDevelopmentLoopback),
|
|
58
|
+
};
|
|
59
|
+
}
|
package/dist/cli.d.ts
ADDED
package/dist/cli.js
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { LeashConnectError } from "./bootstrap.js";
|
|
3
|
+
import { CliConfigurationError, parseLeashCliConfig } from "./cli-config.js";
|
|
4
|
+
import { startLeashMcp } from "./runtime.js";
|
|
5
|
+
function startupMessage(error) {
|
|
6
|
+
if (error instanceof CliConfigurationError || error instanceof LeashConnectError) {
|
|
7
|
+
return error.message;
|
|
8
|
+
}
|
|
9
|
+
return "Tab MCP could not start.";
|
|
10
|
+
}
|
|
11
|
+
async function main() {
|
|
12
|
+
const config = parseLeashCliConfig(process.argv.slice(2), process.env);
|
|
13
|
+
const runtime = await startLeashMcp({ config });
|
|
14
|
+
let closing = false;
|
|
15
|
+
const close = () => {
|
|
16
|
+
if (closing)
|
|
17
|
+
return;
|
|
18
|
+
closing = true;
|
|
19
|
+
void runtime.close().catch(() => {
|
|
20
|
+
process.exitCode = 1;
|
|
21
|
+
});
|
|
22
|
+
};
|
|
23
|
+
process.stdin.once("end", close);
|
|
24
|
+
process.once("SIGINT", close);
|
|
25
|
+
process.once("SIGTERM", close);
|
|
26
|
+
}
|
|
27
|
+
try {
|
|
28
|
+
await main();
|
|
29
|
+
}
|
|
30
|
+
catch (error) {
|
|
31
|
+
process.stderr.write(`tab-mcp: ${startupMessage(error)}\n`);
|
|
32
|
+
process.exitCode = 1;
|
|
33
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
const LOOPBACK_HOSTS = new Set(["127.0.0.1", "[::1]", "localhost"]);
|
|
2
|
+
export class InvalidControlPlaneOriginError extends Error {
|
|
3
|
+
constructor() {
|
|
4
|
+
super("The agent control-plane origin is invalid.");
|
|
5
|
+
this.name = "InvalidControlPlaneOriginError";
|
|
6
|
+
}
|
|
7
|
+
}
|
|
8
|
+
export function validateControlPlaneOrigin(value) {
|
|
9
|
+
let url;
|
|
10
|
+
try {
|
|
11
|
+
url = new URL(value);
|
|
12
|
+
}
|
|
13
|
+
catch {
|
|
14
|
+
throw new InvalidControlPlaneOriginError();
|
|
15
|
+
}
|
|
16
|
+
const secure = url.protocol === "https:";
|
|
17
|
+
const loopbackDevelopment = url.protocol === "http:" && LOOPBACK_HOSTS.has(url.hostname);
|
|
18
|
+
if ((!secure && !loopbackDevelopment) ||
|
|
19
|
+
url.username.length > 0 ||
|
|
20
|
+
url.password.length > 0 ||
|
|
21
|
+
url.pathname !== "/" ||
|
|
22
|
+
url.search.length > 0 ||
|
|
23
|
+
url.hash.length > 0) {
|
|
24
|
+
throw new InvalidControlPlaneOriginError();
|
|
25
|
+
}
|
|
26
|
+
return url.toString();
|
|
27
|
+
}
|
package/dist/detect.d.ts
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { PaymentRequired } from "@x402/core/types";
|
|
2
|
+
interface LegacyPaymentRequired {
|
|
3
|
+
accepts: unknown[];
|
|
4
|
+
error?: string;
|
|
5
|
+
x402Version: 1;
|
|
6
|
+
}
|
|
7
|
+
export type DetectedPaymentRequired = PaymentRequired | LegacyPaymentRequired;
|
|
8
|
+
export declare function detectMcpPaymentRequired(value: unknown): PaymentRequired | null;
|
|
9
|
+
export declare function detectHttpPaymentRequired(response: Response): Promise<DetectedPaymentRequired | null>;
|
|
10
|
+
export {};
|
package/dist/detect.js
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { decodePaymentRequiredHeader } from "@x402/core/http";
|
|
2
|
+
import { extractPaymentRequiredFromError, isPaymentRequiredError } from "@x402/mcp";
|
|
3
|
+
function record(value) {
|
|
4
|
+
return typeof value === "object" && value !== null;
|
|
5
|
+
}
|
|
6
|
+
function paymentRequired(value) {
|
|
7
|
+
if (!record(value) || typeof value.x402Version !== "number")
|
|
8
|
+
return null;
|
|
9
|
+
if (!Array.isArray(value.accepts) || value.accepts.length === 0)
|
|
10
|
+
return null;
|
|
11
|
+
if (!record(value.resource) || typeof value.resource.url !== "string")
|
|
12
|
+
return null;
|
|
13
|
+
return value;
|
|
14
|
+
}
|
|
15
|
+
function legacyPaymentRequired(value) {
|
|
16
|
+
if (!record(value) || value.x402Version !== 1)
|
|
17
|
+
return null;
|
|
18
|
+
if (!Array.isArray(value.accepts) || value.accepts.length === 0)
|
|
19
|
+
return null;
|
|
20
|
+
return value;
|
|
21
|
+
}
|
|
22
|
+
function paymentRequiredFromText(value) {
|
|
23
|
+
if (!record(value) || value.type !== "text" || typeof value.text !== "string")
|
|
24
|
+
return null;
|
|
25
|
+
try {
|
|
26
|
+
return paymentRequired(JSON.parse(value.text));
|
|
27
|
+
}
|
|
28
|
+
catch {
|
|
29
|
+
return null;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
export function detectMcpPaymentRequired(value) {
|
|
33
|
+
if (isPaymentRequiredError(value)) {
|
|
34
|
+
const extracted = extractPaymentRequiredFromError(value);
|
|
35
|
+
if (extracted)
|
|
36
|
+
return extracted;
|
|
37
|
+
const data = value.data;
|
|
38
|
+
if (record(data)) {
|
|
39
|
+
return paymentRequired(data.x402) ?? paymentRequired(data);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
if (!record(value) || value.isError !== true)
|
|
43
|
+
return null;
|
|
44
|
+
const structured = paymentRequired(value.structuredContent);
|
|
45
|
+
if (structured)
|
|
46
|
+
return structured;
|
|
47
|
+
if (!Array.isArray(value.content))
|
|
48
|
+
return null;
|
|
49
|
+
for (const item of value.content) {
|
|
50
|
+
const detected = paymentRequiredFromText(item);
|
|
51
|
+
if (detected)
|
|
52
|
+
return detected;
|
|
53
|
+
}
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
export async function detectHttpPaymentRequired(response) {
|
|
57
|
+
if (response.status !== 402)
|
|
58
|
+
return null;
|
|
59
|
+
const header = response.headers.get("PAYMENT-REQUIRED");
|
|
60
|
+
if (header) {
|
|
61
|
+
try {
|
|
62
|
+
return paymentRequired(decodePaymentRequiredHeader(header));
|
|
63
|
+
}
|
|
64
|
+
catch {
|
|
65
|
+
return null;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
try {
|
|
69
|
+
return legacyPaymentRequired(await response.clone().json());
|
|
70
|
+
}
|
|
71
|
+
catch {
|
|
72
|
+
return null;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { type x402Client } from "@x402/core/client";
|
|
2
|
+
import type { PaymentPayload, PaymentRequired, SettleResponse } from "@x402/core/types";
|
|
3
|
+
import type { readPaymentAuthorizationState } from "./payment-authorization-state.js";
|
|
4
|
+
import type { PaymentEnvelopeStore } from "./payment-envelope-store.js";
|
|
5
|
+
import type { PaymentProfile } from "./payment-profile.js";
|
|
6
|
+
import type { TabRemoteSigner } from "./remote-signer.js";
|
|
7
|
+
export declare const MCP_PAYMENT_IDEMPOTENCY_META_KEY = "tab/payment-idempotency-key";
|
|
8
|
+
interface DurableMcpPaymentOptions {
|
|
9
|
+
address: `0x${string}`;
|
|
10
|
+
authorizationState?: typeof readPaymentAuthorizationState;
|
|
11
|
+
client: x402Client;
|
|
12
|
+
nowSeconds?: () => number;
|
|
13
|
+
paymentProfile: PaymentProfile;
|
|
14
|
+
signer: TabRemoteSigner;
|
|
15
|
+
store: PaymentEnvelopeStore;
|
|
16
|
+
}
|
|
17
|
+
export interface McpPaymentContext {
|
|
18
|
+
idempotencyKey: string;
|
|
19
|
+
payload: PaymentPayload;
|
|
20
|
+
requestFingerprint: string;
|
|
21
|
+
}
|
|
22
|
+
export declare function createDurableMcpPayment(options: DurableMcpPaymentOptions): {
|
|
23
|
+
create(params: Record<string, unknown>, challenge: PaymentRequired): Promise<McpPaymentContext>;
|
|
24
|
+
load(params: Record<string, unknown>): Promise<McpPaymentContext | null>;
|
|
25
|
+
observe(context: McpPaymentContext, settlement: SettleResponse): Promise<{
|
|
26
|
+
status: "failed";
|
|
27
|
+
verified: true;
|
|
28
|
+
} | {
|
|
29
|
+
status: "pending";
|
|
30
|
+
verified: false;
|
|
31
|
+
} | {
|
|
32
|
+
status: "settled";
|
|
33
|
+
verified: true;
|
|
34
|
+
} | {
|
|
35
|
+
readonly status: "ignored";
|
|
36
|
+
readonly verified: false;
|
|
37
|
+
}>;
|
|
38
|
+
};
|
|
39
|
+
export type DurableMcpPayment = ReturnType<typeof createDurableMcpPayment>;
|
|
40
|
+
export {};
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { x402HTTPClient } from "@x402/core/client";
|
|
3
|
+
import { MCP_PAYMENT_META_KEY } from "@x402/mcp";
|
|
4
|
+
import { newPaymentEnvelope } from "./payment-envelope.js";
|
|
5
|
+
import { PaymentEnvelopeJournal, PaymentIdempotencyRequiredError, } from "./payment-envelope-journal.js";
|
|
6
|
+
import { validatePaymentIdempotencyKey } from "./payment-envelope-model.js";
|
|
7
|
+
export const MCP_PAYMENT_IDEMPOTENCY_META_KEY = "tab/payment-idempotency-key";
|
|
8
|
+
const MAX_MCP_PAYMENT_REQUEST_BYTES = 1_048_576;
|
|
9
|
+
function record(value) {
|
|
10
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
11
|
+
}
|
|
12
|
+
function canonical(value) {
|
|
13
|
+
if (value === null || typeof value === "string" || typeof value === "boolean")
|
|
14
|
+
return value;
|
|
15
|
+
if (typeof value === "number" && Number.isFinite(value))
|
|
16
|
+
return value;
|
|
17
|
+
if (Array.isArray(value))
|
|
18
|
+
return value.map(canonical);
|
|
19
|
+
if (record(value)) {
|
|
20
|
+
return Object.fromEntries(Object.keys(value)
|
|
21
|
+
.sort()
|
|
22
|
+
.map((key) => [key, canonical(value[key])]));
|
|
23
|
+
}
|
|
24
|
+
throw new Error("The MCP payment request is invalid");
|
|
25
|
+
}
|
|
26
|
+
function paymentRequest(params) {
|
|
27
|
+
const copy = { ...params };
|
|
28
|
+
if (record(copy._meta)) {
|
|
29
|
+
const meta = { ...copy._meta };
|
|
30
|
+
delete meta[MCP_PAYMENT_IDEMPOTENCY_META_KEY];
|
|
31
|
+
delete meta[MCP_PAYMENT_META_KEY];
|
|
32
|
+
copy._meta = meta;
|
|
33
|
+
}
|
|
34
|
+
const serialized = JSON.stringify(canonical(copy));
|
|
35
|
+
if (Buffer.byteLength(serialized) > MAX_MCP_PAYMENT_REQUEST_BYTES) {
|
|
36
|
+
throw new Error("The MCP payment request is invalid");
|
|
37
|
+
}
|
|
38
|
+
return createHash("sha256").update(serialized).digest("hex");
|
|
39
|
+
}
|
|
40
|
+
function paymentKey(params) {
|
|
41
|
+
const value = record(params._meta) ? params._meta[MCP_PAYMENT_IDEMPOTENCY_META_KEY] : undefined;
|
|
42
|
+
if (value === undefined)
|
|
43
|
+
return undefined;
|
|
44
|
+
if (typeof value !== "string")
|
|
45
|
+
throw new PaymentIdempotencyRequiredError();
|
|
46
|
+
return validatePaymentIdempotencyKey(value);
|
|
47
|
+
}
|
|
48
|
+
export function createDurableMcpPayment(options) {
|
|
49
|
+
const httpClient = new x402HTTPClient(options.client);
|
|
50
|
+
const journal = new PaymentEnvelopeJournal({
|
|
51
|
+
address: options.address,
|
|
52
|
+
...(options.authorizationState ? { authorizationState: options.authorizationState } : {}),
|
|
53
|
+
...(options.nowSeconds ? { nowSeconds: options.nowSeconds } : {}),
|
|
54
|
+
paymentProfile: options.paymentProfile,
|
|
55
|
+
signer: options.signer,
|
|
56
|
+
store: options.store,
|
|
57
|
+
});
|
|
58
|
+
async function parseContext(params, requireKey) {
|
|
59
|
+
const idempotencyKey = paymentKey(params);
|
|
60
|
+
if (requireKey && !idempotencyKey)
|
|
61
|
+
throw new PaymentIdempotencyRequiredError();
|
|
62
|
+
return {
|
|
63
|
+
...(idempotencyKey ? { idempotencyKey } : {}),
|
|
64
|
+
requestFingerprint: paymentRequest(params),
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
return {
|
|
68
|
+
async create(params, challenge) {
|
|
69
|
+
const context = await parseContext(params, true);
|
|
70
|
+
const idempotencyKey = context.idempotencyKey;
|
|
71
|
+
const envelope = await journal.getOrCreate(idempotencyKey, context.requestFingerprint, async () => {
|
|
72
|
+
const payload = await options.client.createPaymentPayload(challenge);
|
|
73
|
+
const header = httpClient.encodePaymentSignatureHeader(payload)["PAYMENT-SIGNATURE"];
|
|
74
|
+
if (!header)
|
|
75
|
+
throw new Error("The x402 payment payload is invalid");
|
|
76
|
+
return newPaymentEnvelope(payload, header, options.signer);
|
|
77
|
+
});
|
|
78
|
+
const parsed = await journal.parse(envelope);
|
|
79
|
+
return { ...context, idempotencyKey, payload: parsed.payload };
|
|
80
|
+
},
|
|
81
|
+
async load(params) {
|
|
82
|
+
const context = await parseContext(params, false);
|
|
83
|
+
if (!context.idempotencyKey)
|
|
84
|
+
return null;
|
|
85
|
+
const envelope = await journal.load(context.idempotencyKey, context.requestFingerprint);
|
|
86
|
+
if (!envelope)
|
|
87
|
+
return null;
|
|
88
|
+
const parsed = await journal.parse(envelope);
|
|
89
|
+
await journal.reconcileObserved(context.idempotencyKey, context.requestFingerprint, envelope, parsed);
|
|
90
|
+
return { ...context, idempotencyKey: context.idempotencyKey, payload: parsed.payload };
|
|
91
|
+
},
|
|
92
|
+
async observe(context, settlement) {
|
|
93
|
+
const parsed = { payload: context.payload };
|
|
94
|
+
const observed = await journal.recordObservation(context.idempotencyKey, context.requestFingerprint, parsed, settlement);
|
|
95
|
+
if (!observed)
|
|
96
|
+
return { status: "ignored", verified: false };
|
|
97
|
+
await options.client.handlePaymentResponse({
|
|
98
|
+
paymentPayload: context.payload,
|
|
99
|
+
requirements: context.payload.accepted,
|
|
100
|
+
settleResponse: settlement,
|
|
101
|
+
});
|
|
102
|
+
return journal.reconcileObserved(context.idempotencyKey, context.requestFingerprint, observed, parsed);
|
|
103
|
+
},
|
|
104
|
+
};
|
|
105
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { type x402Client } from "@x402/core/client";
|
|
2
|
+
import type { readPaymentAuthorizationState } from "./payment-authorization-state.js";
|
|
3
|
+
import { PaymentIdempotencyRequiredError, PaymentReconciliationRequiredError } from "./payment-envelope-journal.js";
|
|
4
|
+
import type { PaymentEnvelopeStore } from "./payment-envelope-store.js";
|
|
5
|
+
import type { PaymentProfile } from "./payment-profile.js";
|
|
6
|
+
import type { TabRemoteSigner } from "./remote-signer.js";
|
|
7
|
+
interface DurablePaymentFetchOptions {
|
|
8
|
+
address: `0x${string}`;
|
|
9
|
+
authorizationState?: typeof readPaymentAuthorizationState;
|
|
10
|
+
client: x402Client;
|
|
11
|
+
fetch: typeof globalThis.fetch;
|
|
12
|
+
idempotencyKey: () => string | undefined;
|
|
13
|
+
nowSeconds?: () => number;
|
|
14
|
+
paymentProfile: PaymentProfile;
|
|
15
|
+
signer: TabRemoteSigner;
|
|
16
|
+
store: PaymentEnvelopeStore;
|
|
17
|
+
}
|
|
18
|
+
export declare function createDurablePaymentFetch(options: DurablePaymentFetchOptions): (input: Request | string | URL, init?: RequestInit) => Promise<Response>;
|
|
19
|
+
export { PaymentIdempotencyRequiredError, PaymentReconciliationRequiredError };
|