@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.
Files changed (82) hide show
  1. package/LICENSE +21 -0
  2. package/dist/bootstrap.d.ts +18 -0
  3. package/dist/bootstrap.js +125 -0
  4. package/dist/cli-config.d.ts +11 -0
  5. package/dist/cli-config.js +59 -0
  6. package/dist/cli.d.ts +2 -0
  7. package/dist/cli.js +33 -0
  8. package/dist/control-plane-origin.d.ts +4 -0
  9. package/dist/control-plane-origin.js +27 -0
  10. package/dist/detect.d.ts +10 -0
  11. package/dist/detect.js +74 -0
  12. package/dist/durable-mcp-payment.d.ts +40 -0
  13. package/dist/durable-mcp-payment.js +105 -0
  14. package/dist/durable-payment-fetch.d.ts +19 -0
  15. package/dist/durable-payment-fetch.js +117 -0
  16. package/dist/eip3009-authorization.d.ts +59 -0
  17. package/dist/eip3009-authorization.js +142 -0
  18. package/dist/errors.d.ts +4 -0
  19. package/dist/errors.js +7 -0
  20. package/dist/fetch-wire.d.ts +46 -0
  21. package/dist/fetch-wire.js +143 -0
  22. package/dist/fetch-wrapper.d.ts +24 -0
  23. package/dist/fetch-wrapper.js +70 -0
  24. package/dist/fetch.d.ts +3 -0
  25. package/dist/fetch.js +1 -0
  26. package/dist/index.d.ts +6 -0
  27. package/dist/index.js +4 -0
  28. package/dist/origin-context.d.ts +5 -0
  29. package/dist/origin-context.js +15 -0
  30. package/dist/paid-fetch-server.d.ts +49 -0
  31. package/dist/paid-fetch-server.js +104 -0
  32. package/dist/payment-authorization-state.d.ts +48 -0
  33. package/dist/payment-authorization-state.js +150 -0
  34. package/dist/payment-client.d.ts +4 -0
  35. package/dist/payment-client.js +14 -0
  36. package/dist/payment-correlation.d.ts +9 -0
  37. package/dist/payment-correlation.js +36 -0
  38. package/dist/payment-envelope-capacity.d.ts +2 -0
  39. package/dist/payment-envelope-capacity.js +28 -0
  40. package/dist/payment-envelope-disk.d.ts +5 -0
  41. package/dist/payment-envelope-disk.js +97 -0
  42. package/dist/payment-envelope-journal.d.ts +54 -0
  43. package/dist/payment-envelope-journal.js +116 -0
  44. package/dist/payment-envelope-lock.d.ts +5 -0
  45. package/dist/payment-envelope-lock.js +88 -0
  46. package/dist/payment-envelope-model.d.ts +32 -0
  47. package/dist/payment-envelope-model.js +123 -0
  48. package/dist/payment-envelope-store.d.ts +31 -0
  49. package/dist/payment-envelope-store.js +210 -0
  50. package/dist/payment-envelope.d.ts +17 -0
  51. package/dist/payment-envelope.js +145 -0
  52. package/dist/payment-idempotency.d.ts +2 -0
  53. package/dist/payment-idempotency.js +8 -0
  54. package/dist/payment-observation-reporter.d.ts +31 -0
  55. package/dist/payment-observation-reporter.js +151 -0
  56. package/dist/payment-profile.d.ts +3 -0
  57. package/dist/payment-profile.js +4 -0
  58. package/dist/payment-request-fingerprint.d.ts +5 -0
  59. package/dist/payment-request-fingerprint.js +54 -0
  60. package/dist/payment-settlement-observation.d.ts +9 -0
  61. package/dist/payment-settlement-observation.js +74 -0
  62. package/dist/payment-signal.d.ts +2 -0
  63. package/dist/payment-signal.js +8 -0
  64. package/dist/payment-target-network.d.ts +16 -0
  65. package/dist/payment-target-network.js +178 -0
  66. package/dist/payment-target-policy.d.ts +10 -0
  67. package/dist/payment-target-policy.js +75 -0
  68. package/dist/proxy.d.ts +47 -0
  69. package/dist/proxy.js +72 -0
  70. package/dist/remote-signer-http.d.ts +16 -0
  71. package/dist/remote-signer-http.js +182 -0
  72. package/dist/remote-signer.d.ts +34 -0
  73. package/dist/remote-signer.js +157 -0
  74. package/dist/resource-url.d.ts +5 -0
  75. package/dist/resource-url.js +36 -0
  76. package/dist/routing.d.ts +13 -0
  77. package/dist/routing.js +42 -0
  78. package/dist/runtime.d.ts +13 -0
  79. package/dist/runtime.js +92 -0
  80. package/dist/upstream.d.ts +43 -0
  81. package/dist/upstream.js +38 -0
  82. package/package.json +65 -0
@@ -0,0 +1,178 @@
1
+ import { lookup as dnsLookup } from "node:dns/promises";
2
+ import { BlockList, isIP } from "node:net";
3
+ import { Agent, buildConnector } from "undici";
4
+ import { isLoopbackPaymentHostname, PaymentTargetPolicyError, safePaymentRequestInit, validatePaymentTarget, } from "./payment-target-policy.js";
5
+ const DNS_TIMEOUT_MS = 3_000;
6
+ const MAX_DNS_ANSWERS = 16;
7
+ const MAX_PINNED_HOSTS = 128;
8
+ const blockedIpv4 = new BlockList();
9
+ for (const [network, prefix] of [
10
+ ["0.0.0.0", 8],
11
+ ["10.0.0.0", 8],
12
+ ["100.64.0.0", 10],
13
+ ["127.0.0.0", 8],
14
+ ["169.254.0.0", 16],
15
+ ["172.16.0.0", 12],
16
+ ["192.0.0.0", 24],
17
+ ["192.0.2.0", 24],
18
+ ["192.168.0.0", 16],
19
+ ["198.18.0.0", 15],
20
+ ["198.51.100.0", 24],
21
+ ["203.0.113.0", 24],
22
+ ["224.0.0.0", 4],
23
+ ["240.0.0.0", 4],
24
+ ]) {
25
+ blockedIpv4.addSubnet(network, prefix, "ipv4");
26
+ }
27
+ const blockedIpv6 = new BlockList();
28
+ for (const [network, prefix] of [
29
+ ["::", 128],
30
+ ["::1", 128],
31
+ ["::ffff:0:0", 96],
32
+ ["100::", 64],
33
+ ["2001::", 32],
34
+ ["2001:2::", 48],
35
+ ["2001:10::", 28],
36
+ ["2001:20::", 28],
37
+ ["2001:db8::", 32],
38
+ ["2002::", 16],
39
+ ["fc00::", 7],
40
+ ["fe80::", 10],
41
+ ["fec0::", 10],
42
+ ["ff00::", 8],
43
+ ]) {
44
+ blockedIpv6.addSubnet(network, prefix, "ipv6");
45
+ }
46
+ function normalizeHostname(value) {
47
+ const lower = value.toLowerCase();
48
+ return lower.startsWith("[") && lower.endsWith("]") ? lower.slice(1, -1) : lower;
49
+ }
50
+ function loopbackAddress(value) {
51
+ const address = normalizeHostname(value);
52
+ if (address === "::1")
53
+ return true;
54
+ return isIP(address) === 4 && Number(address.split(".")[0]) === 127;
55
+ }
56
+ function publicAddress(value, family) {
57
+ const address = normalizeHostname(value);
58
+ if (isIP(address) !== family)
59
+ return false;
60
+ if (family === 4)
61
+ return !blockedIpv4.check(address, "ipv4");
62
+ return /^[23]/.test(address) && !blockedIpv6.check(address, "ipv6");
63
+ }
64
+ const defaultLookup = async (hostname) => {
65
+ const answers = await dnsLookup(hostname, { all: true, verbatim: true });
66
+ return answers.flatMap((answer) => answer.family === 4 || answer.family === 6
67
+ ? [{ address: answer.address, family: answer.family }]
68
+ : []);
69
+ };
70
+ async function boundedLookup(lookup, hostname) {
71
+ let timeout;
72
+ const deadline = new Promise((_resolve, reject) => {
73
+ timeout = setTimeout(() => reject(new PaymentTargetPolicyError()), DNS_TIMEOUT_MS);
74
+ });
75
+ try {
76
+ return await Promise.race([lookup(hostname), deadline]);
77
+ }
78
+ catch {
79
+ throw new PaymentTargetPolicyError();
80
+ }
81
+ finally {
82
+ if (timeout)
83
+ clearTimeout(timeout);
84
+ }
85
+ }
86
+ export function createPinnedPaymentFetch(options) {
87
+ const allowDevelopmentLoopback = options.allowDevelopmentLoopback === true;
88
+ const lookup = options.lookup ?? defaultLookup;
89
+ const maxPinnedHosts = options.maxPinnedHosts ?? MAX_PINNED_HOSTS;
90
+ if (!Number.isSafeInteger(maxPinnedHosts) || maxPinnedHosts < 1 || maxPinnedHosts > 1_024) {
91
+ throw new Error("Payment target pin capacity is invalid");
92
+ }
93
+ const pins = new Map();
94
+ const resolving = new Map();
95
+ const connector = buildConnector({ timeout: 10_000 });
96
+ const dispatcher = new Agent({
97
+ connections: 4,
98
+ connect(connectOptions, callback) {
99
+ const hostname = normalizeHostname(connectOptions.hostname);
100
+ const pin = pins.get(hostname);
101
+ if (!pin) {
102
+ callback(new PaymentTargetPolicyError(), null);
103
+ return;
104
+ }
105
+ connector({
106
+ ...connectOptions,
107
+ hostname: pin.address,
108
+ ...(connectOptions.protocol === "https:" && isIP(hostname) === 0
109
+ ? { servername: hostname }
110
+ : {}),
111
+ }, callback);
112
+ },
113
+ pipelining: 1,
114
+ });
115
+ async function resolve(url) {
116
+ const hostname = normalizeHostname(url.hostname);
117
+ const existing = pins.get(hostname);
118
+ if (existing)
119
+ return existing;
120
+ const active = resolving.get(hostname);
121
+ if (active)
122
+ return active;
123
+ const occupiedHosts = new Set([...pins.keys(), ...resolving.keys()]);
124
+ if (occupiedHosts.size >= maxPinnedHosts)
125
+ throw new PaymentTargetPolicyError();
126
+ const task = (async () => {
127
+ const family = isIP(hostname);
128
+ const lexicalLoopback = isLoopbackPaymentHostname(hostname);
129
+ let answers;
130
+ if (family === 4 || family === 6) {
131
+ answers = [{ address: hostname, family }];
132
+ }
133
+ else {
134
+ answers = await boundedLookup(lookup, hostname);
135
+ }
136
+ const developmentTarget = allowDevelopmentLoopback && url.protocol === "http:" && lexicalLoopback;
137
+ if (answers.length < 1 ||
138
+ answers.length > MAX_DNS_ANSWERS ||
139
+ answers.some((answer) => developmentTarget
140
+ ? !loopbackAddress(answer.address)
141
+ : !publicAddress(answer.address, answer.family))) {
142
+ throw new PaymentTargetPolicyError();
143
+ }
144
+ const selected = answers[0];
145
+ if (!selected)
146
+ throw new PaymentTargetPolicyError();
147
+ pins.set(hostname, selected);
148
+ return selected;
149
+ })();
150
+ resolving.set(hostname, task);
151
+ try {
152
+ return await task;
153
+ }
154
+ finally {
155
+ resolving.delete(hostname);
156
+ }
157
+ }
158
+ const fetch = async (input, init) => {
159
+ const rawUrl = input instanceof Request ? input.url : input.toString();
160
+ const url = new URL(validatePaymentTarget(rawUrl, {
161
+ allowDevelopmentLoopback,
162
+ }));
163
+ await resolve(url);
164
+ const requestInit = {
165
+ ...safePaymentRequestInit(init),
166
+ dispatcher,
167
+ };
168
+ return options.fetch(input, requestInit);
169
+ };
170
+ let closePromise;
171
+ return {
172
+ close() {
173
+ closePromise ??= dispatcher.close();
174
+ return closePromise;
175
+ },
176
+ fetch,
177
+ };
178
+ }
@@ -0,0 +1,10 @@
1
+ export declare class PaymentTargetPolicyError extends Error {
2
+ readonly code = "UNSAFE_PAYMENT_TARGET";
3
+ constructor();
4
+ }
5
+ export interface PaymentTargetPolicyOptions {
6
+ allowDevelopmentLoopback?: boolean;
7
+ }
8
+ export declare function isLoopbackPaymentHostname(hostname: string): boolean;
9
+ export declare function validatePaymentTarget(value: string, options?: PaymentTargetPolicyOptions): string;
10
+ export declare function safePaymentRequestInit(init?: RequestInit): RequestInit;
@@ -0,0 +1,75 @@
1
+ import { isIP } from "node:net";
2
+ const LOOPBACK_HOSTS = new Set(["127.0.0.1", "::1", "localhost"]);
3
+ const PRIVATE_HOST_SUFFIXES = [".home.arpa", ".internal", ".local", ".localhost"];
4
+ export class PaymentTargetPolicyError extends Error {
5
+ code = "UNSAFE_PAYMENT_TARGET";
6
+ constructor() {
7
+ super("The payment target is not permitted by Agent network policy.");
8
+ this.name = "PaymentTargetPolicyError";
9
+ }
10
+ }
11
+ function unbracket(hostname) {
12
+ return hostname.startsWith("[") && hostname.endsWith("]") ? hostname.slice(1, -1) : hostname;
13
+ }
14
+ function privateIpv4(hostname) {
15
+ const parts = hostname.split(".").map(Number);
16
+ if (parts.length !== 4 || parts.some((part) => !Number.isInteger(part)))
17
+ return true;
18
+ const [first = 0, second = 0] = parts;
19
+ return (first === 0 ||
20
+ first === 10 ||
21
+ first === 127 ||
22
+ (first === 100 && second >= 64 && second <= 127) ||
23
+ (first === 169 && second === 254) ||
24
+ (first === 172 && second >= 16 && second <= 31) ||
25
+ (first === 192 && (second === 0 || second === 168)) ||
26
+ (first === 198 && (second === 18 || second === 19)) ||
27
+ first >= 224);
28
+ }
29
+ function privateHostname(hostname) {
30
+ const normalized = unbracket(hostname.toLowerCase());
31
+ if (LOOPBACK_HOSTS.has(normalized))
32
+ return true;
33
+ const version = isIP(normalized);
34
+ if (version === 4)
35
+ return privateIpv4(normalized);
36
+ if (version === 6) {
37
+ return !/^[23]/.test(normalized) || normalized.startsWith("2001:db8:");
38
+ }
39
+ return (!normalized.includes(".") ||
40
+ normalized === "metadata.google.internal" ||
41
+ PRIVATE_HOST_SUFFIXES.some((suffix) => normalized.endsWith(suffix)));
42
+ }
43
+ function loopbackHostname(hostname) {
44
+ const normalized = unbracket(hostname.toLowerCase());
45
+ if (LOOPBACK_HOSTS.has(normalized))
46
+ return true;
47
+ if (isIP(normalized) !== 4)
48
+ return false;
49
+ return Number(normalized.split(".")[0]) === 127;
50
+ }
51
+ export function isLoopbackPaymentHostname(hostname) {
52
+ return loopbackHostname(hostname);
53
+ }
54
+ export function validatePaymentTarget(value, options = {}) {
55
+ let url;
56
+ try {
57
+ url = new URL(value);
58
+ }
59
+ catch {
60
+ throw new PaymentTargetPolicyError();
61
+ }
62
+ const loopbackDevelopment = options.allowDevelopmentLoopback === true &&
63
+ url.protocol === "http:" &&
64
+ loopbackHostname(url.hostname);
65
+ if ((url.protocol !== "https:" && !loopbackDevelopment) ||
66
+ url.username.length > 0 ||
67
+ url.password.length > 0 ||
68
+ (privateHostname(url.hostname) && !loopbackDevelopment)) {
69
+ throw new PaymentTargetPolicyError();
70
+ }
71
+ return url.toString();
72
+ }
73
+ export function safePaymentRequestInit(init) {
74
+ return { ...init, redirect: "error" };
75
+ }
@@ -0,0 +1,47 @@
1
+ import type { Client } from "@modelcontextprotocol/sdk/client/index.js";
2
+ import { Server } from "@modelcontextprotocol/sdk/server/index.js";
3
+ import type { SettleResponse } from "@x402/core/types";
4
+ import type { DurableMcpPayment } from "./durable-mcp-payment.js";
5
+ interface LeashProxyOptions {
6
+ payment?: DurableMcpPayment;
7
+ upstream: Client;
8
+ }
9
+ export declare function readMcpSettlementMetadata(result: unknown, expected?: {
10
+ amount?: string;
11
+ network?: string;
12
+ }): SettleResponse | null;
13
+ export declare function createLeashProxyServer(options: LeashProxyOptions): Server<{
14
+ method: string;
15
+ params?: {
16
+ [x: string]: unknown;
17
+ _meta?: {
18
+ [x: string]: unknown;
19
+ progressToken?: string | number | undefined;
20
+ "io.modelcontextprotocol/related-task"?: {
21
+ taskId: string;
22
+ } | undefined;
23
+ } | undefined;
24
+ } | undefined;
25
+ }, {
26
+ method: string;
27
+ params?: {
28
+ [x: string]: unknown;
29
+ _meta?: {
30
+ [x: string]: unknown;
31
+ progressToken?: string | number | undefined;
32
+ "io.modelcontextprotocol/related-task"?: {
33
+ taskId: string;
34
+ } | undefined;
35
+ } | undefined;
36
+ } | undefined;
37
+ }, {
38
+ [x: string]: unknown;
39
+ _meta?: {
40
+ [x: string]: unknown;
41
+ progressToken?: string | number | undefined;
42
+ "io.modelcontextprotocol/related-task"?: {
43
+ taskId: string;
44
+ } | undefined;
45
+ } | undefined;
46
+ }>;
47
+ export {};
package/dist/proxy.js ADDED
@@ -0,0 +1,72 @@
1
+ import { Server } from "@modelcontextprotocol/sdk/server/index.js";
2
+ import { CallToolRequestSchema, ListToolsRequestSchema } from "@modelcontextprotocol/sdk/types.js";
3
+ import { MCP_PAYMENT_META_KEY, MCP_PAYMENT_RESPONSE_META_KEY } from "@x402/mcp";
4
+ import { detectMcpPaymentRequired } from "./detect.js";
5
+ import { SignerNotConfiguredError } from "./errors.js";
6
+ import { withPaymentOrigin, withPaymentResourceUrl } from "./origin-context.js";
7
+ import { parsePaymentSettlementObservation } from "./payment-settlement-observation.js";
8
+ import { withPaymentSignal } from "./payment-signal.js";
9
+ function record(value) {
10
+ return typeof value === "object" && value !== null;
11
+ }
12
+ export function readMcpSettlementMetadata(result, expected = {}) {
13
+ if (!record(result) || !record(result._meta))
14
+ return null;
15
+ return parsePaymentSettlementObservation(result._meta[MCP_PAYMENT_RESPONSE_META_KEY], expected);
16
+ }
17
+ export function createLeashProxyServer(options) {
18
+ const server = new Server({ name: "tab-mcp", version: "0.0.1" }, { capabilities: { tools: {} } });
19
+ server.setRequestHandler(ListToolsRequestSchema, (request, extra) => options.upstream.listTools(request.params, { signal: extra.signal }));
20
+ async function paidCall(params, context, signal) {
21
+ const paidResult = await options.upstream.callTool({
22
+ ...params,
23
+ _meta: {
24
+ ...params._meta,
25
+ [MCP_PAYMENT_META_KEY]: context.payload,
26
+ },
27
+ }, undefined, { signal });
28
+ const settlement = readMcpSettlementMetadata(paidResult, {
29
+ amount: context.payload.accepted.amount,
30
+ network: context.payload.accepted.network,
31
+ });
32
+ if (settlement)
33
+ await options.payment?.observe(context, settlement);
34
+ return paidResult;
35
+ }
36
+ server.setRequestHandler(CallToolRequestSchema, (request, extra) => {
37
+ const signal = AbortSignal.any([extra.signal, AbortSignal.timeout(30_000)]);
38
+ return withPaymentSignal(signal, () => withPaymentOrigin({
39
+ clientName: server.getClientVersion()?.name ?? "Unknown client",
40
+ toolName: request.params.name,
41
+ transport: "mcp",
42
+ }, async () => {
43
+ const pending = await options.payment?.load(request.params);
44
+ if (pending)
45
+ return paidCall(request.params, pending, signal);
46
+ let challenge = null;
47
+ let initialResult;
48
+ try {
49
+ initialResult = await options.upstream.callTool(request.params, undefined, {
50
+ signal,
51
+ });
52
+ challenge = detectMcpPaymentRequired(initialResult);
53
+ }
54
+ catch (error) {
55
+ challenge = detectMcpPaymentRequired(error);
56
+ if (!challenge)
57
+ throw error;
58
+ }
59
+ if (!challenge) {
60
+ if (!initialResult)
61
+ throw new Error("Upstream tool returned no result");
62
+ return initialResult;
63
+ }
64
+ const payment = options.payment;
65
+ if (!payment)
66
+ throw new SignerNotConfiguredError();
67
+ const context = await withPaymentResourceUrl(challenge.resource.url, () => payment.create(request.params, challenge));
68
+ return paidCall(request.params, context, signal);
69
+ }));
70
+ });
71
+ return server;
72
+ }
@@ -0,0 +1,16 @@
1
+ export declare class RemoteSignerError extends Error {
2
+ readonly code: string;
3
+ readonly status: number;
4
+ constructor(code: string, message: string, status: number);
5
+ }
6
+ export declare function readRemoteSignerJson(response: Response, signal?: AbortSignal): Promise<unknown>;
7
+ interface SignerPostOptions {
8
+ apiKey: string;
9
+ body: unknown;
10
+ endpoint: URL;
11
+ fetch: typeof globalThis.fetch;
12
+ signal?: AbortSignal;
13
+ timeoutMs: number;
14
+ }
15
+ export declare function postRemoteSignerJson(options: SignerPostOptions): Promise<unknown>;
16
+ export {};
@@ -0,0 +1,182 @@
1
+ const MAX_SIGNER_RESPONSE_BYTES = 64 * 1_024;
2
+ const SAFE_SERVER_MESSAGES = {
3
+ AGENT_CANCELLED: "The signing request cannot proceed.",
4
+ AGENT_FROZEN: "The signing request cannot proceed.",
5
+ AGENT_PAUSED: "The signing request cannot proceed.",
6
+ AUTHORIZATION_EXPIRED: "The signing authorization has expired.",
7
+ CAP_CYCLE_CHANGED: "The signing request cannot proceed.",
8
+ FLOAT_CHECK_UNAVAILABLE: "The agent balance could not be verified.",
9
+ FLOAT_EMPTY: "The agent does not have enough available funds.",
10
+ INVALID_AGENT_KEY: "The agent key was rejected.",
11
+ INVALID_SIGN_REQUEST: "The signing request is invalid.",
12
+ CAP_EXCEEDED: "The signing request exceeds the active cap.",
13
+ CAP_NOT_SET: "The agent does not have an active cap.",
14
+ SIGNER_IDENTITY_MISMATCH: "The signing provider returned the wrong identity.",
15
+ SIGNER_NOT_CONFIGURED: "Agent signing is not configured for this agent.",
16
+ SIGNER_PROVIDER_INVALID_RESPONSE: "The signing provider returned an invalid response.",
17
+ SIGNER_PROVIDER_RATE_LIMITED: "The signing provider is rate limited.",
18
+ SIGNER_PROVIDER_REJECTED: "The signing provider rejected the request.",
19
+ SIGNER_PROVIDER_TIMEOUT: "The signing provider timed out.",
20
+ SIGNER_PROVIDER_UNAVAILABLE: "The signing provider is unavailable.",
21
+ SIGN_RATE_LIMITED: "Signing is temporarily rate limited.",
22
+ SIGN_REQUEST_CONFLICT: "The signing request conflicts with an existing request.",
23
+ SIGN_REQUEST_IN_PROGRESS: "A matching signing request is already in progress.",
24
+ SIGN_REQUEST_RECONCILING: "A matching signing request is being reconciled.",
25
+ };
26
+ export class RemoteSignerError extends Error {
27
+ code;
28
+ status;
29
+ constructor(code, message, status) {
30
+ super(message);
31
+ this.code = code;
32
+ this.status = status;
33
+ this.name = "RemoteSignerError";
34
+ }
35
+ }
36
+ function invalidResponse() {
37
+ throw new RemoteSignerError("INVALID_SIGNER_RESPONSE", "The signer response is invalid.", 502);
38
+ }
39
+ function isJsonContentType(value) {
40
+ if (!value)
41
+ return false;
42
+ const mediaType = value.split(";", 1)[0]?.trim().toLowerCase() ?? "";
43
+ return (mediaType === "application/json" || /^application\/[a-z0-9!#$&^_.+-]+\+json$/.test(mediaType));
44
+ }
45
+ function contentLength(response) {
46
+ const value = response.headers.get("content-length");
47
+ if (value === null)
48
+ return null;
49
+ if (!/^(0|[1-9]\d*)$/.test(value))
50
+ invalidResponse();
51
+ const length = Number(value);
52
+ if (!Number.isSafeInteger(length))
53
+ invalidResponse();
54
+ return length;
55
+ }
56
+ export async function readRemoteSignerJson(response, signal) {
57
+ const cancelBody = () => response.body?.cancel().catch(() => undefined);
58
+ if (!isJsonContentType(response.headers.get("content-type"))) {
59
+ await cancelBody();
60
+ invalidResponse();
61
+ }
62
+ let declaredLength;
63
+ try {
64
+ declaredLength = contentLength(response);
65
+ }
66
+ catch {
67
+ await cancelBody();
68
+ invalidResponse();
69
+ }
70
+ if (declaredLength !== null && declaredLength > MAX_SIGNER_RESPONSE_BYTES) {
71
+ await cancelBody();
72
+ invalidResponse();
73
+ }
74
+ if (!response.body)
75
+ invalidResponse();
76
+ const reader = response.body.getReader();
77
+ const chunks = [];
78
+ let length = 0;
79
+ let rejectAbort;
80
+ const aborted = new Promise((_resolve, reject) => {
81
+ rejectAbort = reject;
82
+ });
83
+ const onAbort = () => {
84
+ void reader.cancel().catch(() => undefined);
85
+ rejectAbort?.(signal?.reason ?? new Error("Aborted"));
86
+ };
87
+ signal?.addEventListener("abort", onAbort, { once: true });
88
+ try {
89
+ if (signal?.aborted)
90
+ onAbort();
91
+ while (true) {
92
+ const result = signal ? await Promise.race([reader.read(), aborted]) : await reader.read();
93
+ if (result.done)
94
+ break;
95
+ length += result.value.byteLength;
96
+ if (length > MAX_SIGNER_RESPONSE_BYTES) {
97
+ await reader.cancel().catch(() => undefined);
98
+ invalidResponse();
99
+ }
100
+ chunks.push(result.value);
101
+ }
102
+ }
103
+ finally {
104
+ signal?.removeEventListener("abort", onAbort);
105
+ reader.releaseLock();
106
+ }
107
+ const bytes = new Uint8Array(length);
108
+ let offset = 0;
109
+ for (const chunk of chunks) {
110
+ bytes.set(chunk, offset);
111
+ offset += chunk.byteLength;
112
+ }
113
+ try {
114
+ return JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(bytes));
115
+ }
116
+ catch {
117
+ invalidResponse();
118
+ }
119
+ }
120
+ function record(value) {
121
+ return typeof value === "object" && value !== null && !Array.isArray(value);
122
+ }
123
+ function serverError(response, body) {
124
+ const code = record(body) && record(body.error) ? body.error.code : undefined;
125
+ if (typeof code === "string" && Object.hasOwn(SAFE_SERVER_MESSAGES, code)) {
126
+ return new RemoteSignerError(code, SAFE_SERVER_MESSAGES[code], response.status);
127
+ }
128
+ return new RemoteSignerError("SIGNER_REQUEST_FAILED", "The signer request failed.", response.status);
129
+ }
130
+ function jsonBody(value) {
131
+ return JSON.stringify(value, (_key, field) => typeof field === "bigint" ? field.toString() : field);
132
+ }
133
+ export async function postRemoteSignerJson(options) {
134
+ const controller = new AbortController();
135
+ let timedOut = false;
136
+ let timer;
137
+ const deadline = new Promise((_resolve, reject) => {
138
+ timer = setTimeout(() => {
139
+ timedOut = true;
140
+ controller.abort();
141
+ reject(new RemoteSignerError("SIGNER_REQUEST_TIMEOUT", "The agent control plane timed out.", 504));
142
+ }, options.timeoutMs);
143
+ });
144
+ const request = async () => {
145
+ const signal = options.signal
146
+ ? AbortSignal.any([controller.signal, options.signal])
147
+ : controller.signal;
148
+ const response = await options.fetch(options.endpoint, {
149
+ body: jsonBody(options.body),
150
+ headers: {
151
+ accept: "application/json",
152
+ authorization: `Bearer ${options.apiKey}`,
153
+ "content-type": "application/json",
154
+ },
155
+ method: "POST",
156
+ redirect: "error",
157
+ signal,
158
+ });
159
+ const body = await readRemoteSignerJson(response, signal);
160
+ if (!response.ok)
161
+ throw serverError(response, body);
162
+ return body;
163
+ };
164
+ try {
165
+ return await Promise.race([request(), deadline]);
166
+ }
167
+ catch (error) {
168
+ if (error instanceof RemoteSignerError)
169
+ throw error;
170
+ if (options.signal?.aborted) {
171
+ throw new RemoteSignerError("SIGNER_REQUEST_CANCELLED", "The Agent signing request was cancelled.", 499);
172
+ }
173
+ if (timedOut) {
174
+ throw new RemoteSignerError("SIGNER_REQUEST_TIMEOUT", "The agent control plane timed out.", 504);
175
+ }
176
+ throw new RemoteSignerError("SIGNER_REQUEST_UNAVAILABLE", "The agent control plane could not be reached.", 503);
177
+ }
178
+ finally {
179
+ if (timer)
180
+ clearTimeout(timer);
181
+ }
182
+ }
@@ -0,0 +1,34 @@
1
+ import type { ClientEvmSigner } from "@x402/evm";
2
+ import { type SignerRequest } from "./eip3009-authorization.js";
3
+ export { RemoteSignerError } from "./remote-signer-http.js";
4
+ export interface PaymentOrigin {
5
+ clientName: string;
6
+ toolName: string;
7
+ transport: "http" | "mcp";
8
+ }
9
+ interface RemoteSignerOptions {
10
+ address: `0x${string}`;
11
+ apiBaseUrl: string;
12
+ apiKey: string;
13
+ fetch?: typeof globalThis.fetch;
14
+ nowSeconds?: () => number;
15
+ origin?: () => PaymentOrigin | undefined;
16
+ paymentProfile: import("./payment-profile.js").PaymentProfile;
17
+ resourceUrl?: () => string | undefined;
18
+ reportAttempts?: number;
19
+ reportRetryDelayMs?: number;
20
+ reportTimeoutMs?: number;
21
+ signal?: () => AbortSignal | undefined;
22
+ signTimeoutMs?: number;
23
+ }
24
+ export declare class TabRemoteSigner implements ClientEvmSigner {
25
+ #private;
26
+ readonly address: `0x${string}`;
27
+ constructor(options: RemoteSignerOptions);
28
+ signTypedData(signerRequest: SignerRequest): Promise<`0x${string}`>;
29
+ receiptIdForSignature(signature: string): string | null;
30
+ restorePaymentCorrelation(signature: string, receiptId: string, validBeforeSeconds: number): void;
31
+ reconcileExpiredPayment(receiptId: string): Promise<boolean>;
32
+ reportPaymentObservation(context: import("@x402/core/client").PaymentResponseContext): Promise<import("./payment-observation-reporter.js").PaymentObservationResult>;
33
+ flushPaymentObservations(): Promise<void>;
34
+ }