@intentfi/agent-sdk 0.1.0 → 0.1.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/README.md +53 -0
- package/dist/cli.js +62 -2
- package/dist/client.d.ts +115 -5
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +652 -47
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/types.d.ts +117 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/x402Signing.d.ts +10 -0
- package/dist/x402Signing.d.ts.map +1 -0
- package/dist/x402Signing.js +107 -0
- package/package.json +3 -3
package/dist/client.js
CHANGED
|
@@ -1,15 +1,96 @@
|
|
|
1
1
|
const MCP_PROTOCOL_VERSION = "2025-11-25";
|
|
2
|
+
const X402_VERSION = 2;
|
|
3
|
+
const PAYMENT_REQUIRED_HEADER = "PAYMENT-REQUIRED";
|
|
4
|
+
const PAYMENT_SIGNATURE_HEADER = "PAYMENT-SIGNATURE";
|
|
5
|
+
const PAYMENT_RESPONSE_HEADER = "PAYMENT-RESPONSE";
|
|
6
|
+
const PAYMENT_IDENTIFIER_EXTENSION = "payment-identifier";
|
|
7
|
+
export class DefaultX402PaymentManager {
|
|
8
|
+
policy;
|
|
9
|
+
approve;
|
|
10
|
+
schemes = new Map();
|
|
11
|
+
constructor(options = {}) {
|
|
12
|
+
this.policy = options.policy ?? {};
|
|
13
|
+
this.approve = options.approve;
|
|
14
|
+
for (const [scheme, handler] of Object.entries(options.schemes ?? {})) {
|
|
15
|
+
this.schemes.set(scheme.toLowerCase(), handler);
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
registerScheme(scheme, handler) {
|
|
19
|
+
this.schemes.set(scheme.trim().toLowerCase(), handler);
|
|
20
|
+
return this;
|
|
21
|
+
}
|
|
22
|
+
async buildPaymentPayload(args) {
|
|
23
|
+
const requirement = this.selectRequirement(args.paymentRequired);
|
|
24
|
+
const context = {
|
|
25
|
+
transport: args.transport,
|
|
26
|
+
operation: args.operation,
|
|
27
|
+
...(args.idempotencyKey ? { idempotencyKey: args.idempotencyKey } : {}),
|
|
28
|
+
paymentRequired: args.paymentRequired,
|
|
29
|
+
requirement,
|
|
30
|
+
};
|
|
31
|
+
if (this.approve) {
|
|
32
|
+
const approved = await this.approve({ context });
|
|
33
|
+
if (!approved) {
|
|
34
|
+
throw new Error("x402 payment was not approved.");
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
const handler = this.schemes.get(requirement.scheme.trim().toLowerCase());
|
|
38
|
+
if (!handler) {
|
|
39
|
+
throw new Error(`No x402 scheme handler is registered for '${requirement.scheme}'.`);
|
|
40
|
+
}
|
|
41
|
+
const payload = await handler({ context });
|
|
42
|
+
const extensions = echoRequiredExtensions(args.paymentRequired.extensions, args.idempotencyKey);
|
|
43
|
+
return {
|
|
44
|
+
x402Version: X402_VERSION,
|
|
45
|
+
resource: args.paymentRequired.resource,
|
|
46
|
+
accepted: requirement,
|
|
47
|
+
payload,
|
|
48
|
+
...(extensions ? { extensions } : {}),
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
async onSettlement(args) {
|
|
52
|
+
void args;
|
|
53
|
+
}
|
|
54
|
+
selectRequirement(paymentRequired) {
|
|
55
|
+
for (const requirement of paymentRequired.accepts) {
|
|
56
|
+
if (!this.passesPolicy(requirement)) {
|
|
57
|
+
continue;
|
|
58
|
+
}
|
|
59
|
+
if (!this.schemes.has(requirement.scheme.trim().toLowerCase())) {
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
62
|
+
return requirement;
|
|
63
|
+
}
|
|
64
|
+
throw new Error("No x402 payment requirement matched the configured policy or available schemes.");
|
|
65
|
+
}
|
|
66
|
+
passesPolicy(requirement) {
|
|
67
|
+
if (this.policy.allowedNetworks
|
|
68
|
+
&& this.policy.allowedNetworks.length > 0
|
|
69
|
+
&& !this.policy.allowedNetworks.includes(requirement.network)) {
|
|
70
|
+
return false;
|
|
71
|
+
}
|
|
72
|
+
if (this.policy.maxAmountAtomic !== undefined) {
|
|
73
|
+
const amount = parseAtomicAmount(requirement.amount);
|
|
74
|
+
if (amount > this.policy.maxAmountAtomic) {
|
|
75
|
+
return false;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
return true;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
2
81
|
export class IntentFiClient {
|
|
3
82
|
baseUrl;
|
|
4
83
|
apiKey;
|
|
5
84
|
transport;
|
|
6
85
|
fetchImpl;
|
|
86
|
+
paymentManager;
|
|
7
87
|
mcpSessionId;
|
|
8
88
|
constructor(options = {}) {
|
|
9
89
|
this.baseUrl = (options.baseUrl ?? process.env.INTENTFI_BASE_URL ?? "https://agentic.intentfi.io").replace(/\/+$/, "");
|
|
10
90
|
this.apiKey = options.apiKey ?? process.env.INTENTFI_API_KEY;
|
|
11
91
|
this.transport = resolveTransportMode(options.transport ?? process.env.INTENTFI_TRANSPORT);
|
|
12
92
|
this.fetchImpl = options.fetchImpl ?? fetch;
|
|
93
|
+
this.paymentManager = options.paymentManager;
|
|
13
94
|
}
|
|
14
95
|
async requestSiweChallenge(args) {
|
|
15
96
|
const response = await this.fetchJson("/agent/v1/auth/siwe/challenge", {
|
|
@@ -31,14 +112,14 @@ export class IntentFiClient {
|
|
|
31
112
|
});
|
|
32
113
|
}
|
|
33
114
|
async createWorkflow(args) {
|
|
34
|
-
this.
|
|
115
|
+
this.assertCredential("createWorkflow", { allowX402: true });
|
|
35
116
|
if (this.transport === "mcp") {
|
|
36
|
-
return await this.mcpToolCall("intentfi.create_workflow", args);
|
|
117
|
+
return await this.mcpToolCall("intentfi.create_workflow", args, { idempotencyKey: args.operationId });
|
|
37
118
|
}
|
|
38
119
|
return await this.createWorkflowRest(args);
|
|
39
120
|
}
|
|
40
121
|
async getWorkflow(args) {
|
|
41
|
-
this.
|
|
122
|
+
this.assertCredential("getWorkflow");
|
|
42
123
|
if (this.transport === "mcp") {
|
|
43
124
|
const result = await this.mcpToolCall("intentfi.get_workflow", args, { stream: (args.waitMs ?? 0) > 0 });
|
|
44
125
|
const workflow = (result.workflow ??
|
|
@@ -54,9 +135,12 @@ export class IntentFiClient {
|
|
|
54
135
|
return await this.getWorkflowRest(args);
|
|
55
136
|
}
|
|
56
137
|
async prepareActive(args) {
|
|
57
|
-
this.
|
|
138
|
+
this.assertCredential("prepareActive", { allowX402: true });
|
|
58
139
|
if (this.transport === "mcp") {
|
|
59
|
-
const result = await this.mcpToolCall("intentfi.prepare_active", args, {
|
|
140
|
+
const result = await this.mcpToolCall("intentfi.prepare_active", args, {
|
|
141
|
+
stream: (args.waitMs ?? 0) > 0,
|
|
142
|
+
idempotencyKey: args.requestId,
|
|
143
|
+
});
|
|
60
144
|
return {
|
|
61
145
|
result,
|
|
62
146
|
status: result.kind === "pending" ? 202 : 200,
|
|
@@ -64,8 +148,16 @@ export class IntentFiClient {
|
|
|
64
148
|
}
|
|
65
149
|
return await this.prepareActiveRest(args);
|
|
66
150
|
}
|
|
151
|
+
async prepareOffchainActive(args) {
|
|
152
|
+
this.assertCredential("prepareOffchainActive");
|
|
153
|
+
if (this.transport === "mcp") {
|
|
154
|
+
const result = await this.mcpToolCall("intentfi.prepare_offchain_active", args);
|
|
155
|
+
return { result, status: 200 };
|
|
156
|
+
}
|
|
157
|
+
return await this.prepareOffchainActiveRest(args);
|
|
158
|
+
}
|
|
67
159
|
async getActiveSubmissionContext(args) {
|
|
68
|
-
this.
|
|
160
|
+
this.assertCredential("getActiveSubmissionContext");
|
|
69
161
|
if (this.transport === "mcp") {
|
|
70
162
|
return await this.mcpToolCall("intentfi.get_active_submission_context", args);
|
|
71
163
|
}
|
|
@@ -73,8 +165,56 @@ export class IntentFiClient {
|
|
|
73
165
|
headers: this.withApiKey(),
|
|
74
166
|
});
|
|
75
167
|
}
|
|
168
|
+
async submitOffchainActive(args) {
|
|
169
|
+
this.assertCredential("submitOffchainActive");
|
|
170
|
+
if (this.transport === "mcp") {
|
|
171
|
+
const result = await this.mcpToolCall("intentfi.submit_offchain_active", args, { stream: (args.waitMs ?? 0) > 0 });
|
|
172
|
+
const waitKind = result.wait?.kind;
|
|
173
|
+
return {
|
|
174
|
+
result,
|
|
175
|
+
status: waitKind === "timeout" ? 202 : 200,
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
return await this.submitOffchainActiveRest(args);
|
|
179
|
+
}
|
|
180
|
+
async reportOffchainResultActive(args) {
|
|
181
|
+
this.assertCredential("reportOffchainResultActive");
|
|
182
|
+
if (this.transport === "mcp") {
|
|
183
|
+
const result = await this.mcpToolCall("intentfi.report_offchain_result_active", args, { stream: (args.waitMs ?? 0) > 0 });
|
|
184
|
+
const waitKind = result.wait?.kind;
|
|
185
|
+
return {
|
|
186
|
+
result,
|
|
187
|
+
status: waitKind === "timeout" ? 202 : 200,
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
return await this.reportOffchainResultActiveRest(args);
|
|
191
|
+
}
|
|
192
|
+
async reportOffchainFailureActive(args) {
|
|
193
|
+
this.assertCredential("reportOffchainFailureActive");
|
|
194
|
+
if (this.transport === "mcp") {
|
|
195
|
+
const result = await this.mcpToolCall("intentfi.report_offchain_failure_active", args, { stream: (args.waitMs ?? 0) > 0 });
|
|
196
|
+
const waitKind = result.wait?.kind;
|
|
197
|
+
return {
|
|
198
|
+
result,
|
|
199
|
+
status: waitKind === "timeout" ? 202 : 200,
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
return await this.reportOffchainFailureActiveRest(args);
|
|
203
|
+
}
|
|
204
|
+
async cancelOffchainActive(args) {
|
|
205
|
+
this.assertCredential("cancelOffchainActive");
|
|
206
|
+
if (this.transport === "mcp") {
|
|
207
|
+
const result = await this.mcpToolCall("intentfi.cancel_offchain_active", args, { stream: (args.waitMs ?? 0) > 0 });
|
|
208
|
+
const waitKind = result.wait?.kind;
|
|
209
|
+
return {
|
|
210
|
+
result,
|
|
211
|
+
status: waitKind === "timeout" ? 202 : 200,
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
return await this.cancelOffchainActiveRest(args);
|
|
215
|
+
}
|
|
76
216
|
async submitActiveTransactions(args) {
|
|
77
|
-
this.
|
|
217
|
+
this.assertCredential("submitActiveTransactions");
|
|
78
218
|
if (this.transport === "mcp") {
|
|
79
219
|
const result = await this.mcpToolCall("intentfi.submit_active_transactions", args, { stream: (args.waitMs ?? 0) > 0 });
|
|
80
220
|
const waitKind = result.wait?.kind;
|
|
@@ -86,7 +226,7 @@ export class IntentFiClient {
|
|
|
86
226
|
return await this.submitActiveTransactionsRest(args);
|
|
87
227
|
}
|
|
88
228
|
async submitActiveSendCalls(args) {
|
|
89
|
-
this.
|
|
229
|
+
this.assertCredential("submitActiveSendCalls");
|
|
90
230
|
if (this.transport === "mcp") {
|
|
91
231
|
const result = await this.mcpToolCall("intentfi.submit_active_sendcalls", args, { stream: (args.waitMs ?? 0) > 0 });
|
|
92
232
|
const waitKind = result.wait?.kind;
|
|
@@ -98,13 +238,17 @@ export class IntentFiClient {
|
|
|
98
238
|
return await this.submitActiveSendCallsRest(args);
|
|
99
239
|
}
|
|
100
240
|
async createWorkflowRest(args) {
|
|
101
|
-
|
|
241
|
+
const response = await this.fetchPaidEnvelope("/agent/v1/workflows", {
|
|
102
242
|
method: "POST",
|
|
103
243
|
headers: this.withApiKey({
|
|
104
244
|
"content-type": "application/json",
|
|
105
245
|
}),
|
|
106
246
|
body: JSON.stringify(args),
|
|
247
|
+
}, {
|
|
248
|
+
operation: "createWorkflow",
|
|
249
|
+
idempotencyKey: args.operationId,
|
|
107
250
|
});
|
|
251
|
+
return response.data;
|
|
108
252
|
}
|
|
109
253
|
async getWorkflowRest(args) {
|
|
110
254
|
const query = args.waitMs !== undefined ? `?waitMs=${args.waitMs}` : "";
|
|
@@ -120,7 +264,21 @@ export class IntentFiClient {
|
|
|
120
264
|
};
|
|
121
265
|
}
|
|
122
266
|
async prepareActiveRest(args) {
|
|
123
|
-
const response = await this.
|
|
267
|
+
const response = await this.fetchPaidEnvelope(`/agent/v1/workflows/${args.workflowId}/prepare`, {
|
|
268
|
+
method: "POST",
|
|
269
|
+
headers: this.withApiKey({ "content-type": "application/json" }),
|
|
270
|
+
body: JSON.stringify({
|
|
271
|
+
requestId: args.requestId,
|
|
272
|
+
...(args.waitMs !== undefined ? { waitMs: args.waitMs } : {}),
|
|
273
|
+
}),
|
|
274
|
+
}, {
|
|
275
|
+
operation: "prepareActive",
|
|
276
|
+
idempotencyKey: args.requestId,
|
|
277
|
+
});
|
|
278
|
+
return { result: response.data, status: response.status };
|
|
279
|
+
}
|
|
280
|
+
async prepareOffchainActiveRest(args) {
|
|
281
|
+
const response = await this.fetchEnvelope(`/agent/v1/workflows/${args.workflowId}/prepare-offchain`, {
|
|
124
282
|
method: "POST",
|
|
125
283
|
headers: this.withApiKey({ "content-type": "application/json" }),
|
|
126
284
|
body: JSON.stringify({
|
|
@@ -130,19 +288,57 @@ export class IntentFiClient {
|
|
|
130
288
|
});
|
|
131
289
|
return { result: response.data, status: response.status };
|
|
132
290
|
}
|
|
291
|
+
async submitOffchainActiveRest(args) {
|
|
292
|
+
const { workflowId, ...payload } = args;
|
|
293
|
+
const response = await this.fetchEnvelope(`/agent/v1/workflows/${workflowId}/submit-offchain`, {
|
|
294
|
+
method: "POST",
|
|
295
|
+
headers: this.withApiKey({ "content-type": "application/json" }),
|
|
296
|
+
body: JSON.stringify(payload),
|
|
297
|
+
});
|
|
298
|
+
return { result: response.data, status: response.status };
|
|
299
|
+
}
|
|
300
|
+
async reportOffchainResultActiveRest(args) {
|
|
301
|
+
const { workflowId, ...payload } = args;
|
|
302
|
+
const response = await this.fetchEnvelope(`/agent/v1/workflows/${workflowId}/report-offchain-result`, {
|
|
303
|
+
method: "POST",
|
|
304
|
+
headers: this.withApiKey({ "content-type": "application/json" }),
|
|
305
|
+
body: JSON.stringify(payload),
|
|
306
|
+
});
|
|
307
|
+
return { result: response.data, status: response.status };
|
|
308
|
+
}
|
|
309
|
+
async reportOffchainFailureActiveRest(args) {
|
|
310
|
+
const { workflowId, ...payload } = args;
|
|
311
|
+
const response = await this.fetchEnvelope(`/agent/v1/workflows/${workflowId}/report-offchain-failure`, {
|
|
312
|
+
method: "POST",
|
|
313
|
+
headers: this.withApiKey({ "content-type": "application/json" }),
|
|
314
|
+
body: JSON.stringify(payload),
|
|
315
|
+
});
|
|
316
|
+
return { result: response.data, status: response.status };
|
|
317
|
+
}
|
|
318
|
+
async cancelOffchainActiveRest(args) {
|
|
319
|
+
const { workflowId, ...payload } = args;
|
|
320
|
+
const response = await this.fetchEnvelope(`/agent/v1/workflows/${workflowId}/cancel-offchain`, {
|
|
321
|
+
method: "POST",
|
|
322
|
+
headers: this.withApiKey({ "content-type": "application/json" }),
|
|
323
|
+
body: JSON.stringify(payload),
|
|
324
|
+
});
|
|
325
|
+
return { result: response.data, status: response.status };
|
|
326
|
+
}
|
|
133
327
|
async submitActiveTransactionsRest(args) {
|
|
134
|
-
const
|
|
328
|
+
const { workflowId, ...payload } = args;
|
|
329
|
+
const response = await this.fetchEnvelope(`/agent/v1/workflows/${workflowId}/submit-transactions`, {
|
|
135
330
|
method: "POST",
|
|
136
331
|
headers: this.withApiKey({ "content-type": "application/json" }),
|
|
137
|
-
body: JSON.stringify(
|
|
332
|
+
body: JSON.stringify(payload),
|
|
138
333
|
});
|
|
139
334
|
return { result: response.data, status: response.status };
|
|
140
335
|
}
|
|
141
336
|
async submitActiveSendCallsRest(args) {
|
|
142
|
-
const
|
|
337
|
+
const { workflowId, ...payload } = args;
|
|
338
|
+
const response = await this.fetchEnvelope(`/agent/v1/workflows/${workflowId}/submit-sendcalls`, {
|
|
143
339
|
method: "POST",
|
|
144
340
|
headers: this.withApiKey({ "content-type": "application/json" }),
|
|
145
|
-
body: JSON.stringify(
|
|
341
|
+
body: JSON.stringify(payload),
|
|
146
342
|
});
|
|
147
343
|
return { result: response.data, status: response.status };
|
|
148
344
|
}
|
|
@@ -185,10 +381,19 @@ export class IntentFiClient {
|
|
|
185
381
|
throw new Error(`MCP initialized notification failed (${notification.status})`);
|
|
186
382
|
}
|
|
187
383
|
}
|
|
188
|
-
async mcpToolCall(name, args, options, attempt = 0) {
|
|
384
|
+
async mcpToolCall(name, args, options, attempt = 0, paymentAttempt = 0) {
|
|
189
385
|
if (!this.mcpSessionId) {
|
|
190
386
|
await this.mcpInitialize();
|
|
191
387
|
}
|
|
388
|
+
const params = {
|
|
389
|
+
name,
|
|
390
|
+
arguments: args,
|
|
391
|
+
};
|
|
392
|
+
if (options?.paymentPayload) {
|
|
393
|
+
params._meta = {
|
|
394
|
+
"x402/payment": options.paymentPayload,
|
|
395
|
+
};
|
|
396
|
+
}
|
|
192
397
|
const response = await this.fetchImpl(`${this.baseUrl}/mcp`, {
|
|
193
398
|
method: "POST",
|
|
194
399
|
headers: {
|
|
@@ -202,52 +407,63 @@ export class IntentFiClient {
|
|
|
202
407
|
jsonrpc: "2.0",
|
|
203
408
|
id: Date.now(),
|
|
204
409
|
method: "tools/call",
|
|
205
|
-
params
|
|
206
|
-
name,
|
|
207
|
-
arguments: args,
|
|
208
|
-
},
|
|
410
|
+
params,
|
|
209
411
|
}),
|
|
210
412
|
});
|
|
211
413
|
const returnedSessionId = response.headers.get("MCP-Session-Id");
|
|
212
414
|
if (returnedSessionId) {
|
|
213
415
|
this.mcpSessionId = returnedSessionId;
|
|
214
416
|
}
|
|
417
|
+
let body;
|
|
215
418
|
const contentType = (response.headers.get("content-type") ?? "").toLowerCase();
|
|
216
419
|
if (contentType.includes("text/event-stream")) {
|
|
217
|
-
const
|
|
218
|
-
const
|
|
219
|
-
.split("\n\n")
|
|
220
|
-
.map((chunk) => chunk.trim())
|
|
221
|
-
.filter((chunk) => chunk.startsWith("data:"))
|
|
222
|
-
.map((chunk) => chunk.slice(5).trim())
|
|
223
|
-
.map((chunk) => JSON.parse(chunk));
|
|
224
|
-
const final = messages.reverse().find((entry) => entry.result !== undefined || entry.error !== undefined);
|
|
420
|
+
const parsed = await parseMcpSseResponse(response);
|
|
421
|
+
const final = parsed.finalMessage;
|
|
225
422
|
if (!final) {
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
if (final.error) {
|
|
229
|
-
if (final.error.data?.code === "MCP_SESSION_REQUIRED" && attempt === 0) {
|
|
230
|
-
await this.mcpInitialize();
|
|
231
|
-
return await this.mcpToolCall(name, args, options, attempt + 1);
|
|
423
|
+
if (parsed.parseErrorCount > 0) {
|
|
424
|
+
throw new Error(`MCP stream response did not contain a final message (failed to parse ${parsed.parseErrorCount} JSON payload(s))`);
|
|
232
425
|
}
|
|
233
|
-
throw
|
|
426
|
+
throw new Error("MCP stream response did not contain a final message");
|
|
234
427
|
}
|
|
235
|
-
|
|
236
|
-
}
|
|
237
|
-
const text = await response.text();
|
|
238
|
-
let body;
|
|
239
|
-
try {
|
|
240
|
-
body = JSON.parse(text);
|
|
428
|
+
body = final;
|
|
241
429
|
}
|
|
242
|
-
|
|
243
|
-
|
|
430
|
+
else {
|
|
431
|
+
const text = await response.text();
|
|
432
|
+
try {
|
|
433
|
+
body = JSON.parse(text);
|
|
434
|
+
}
|
|
435
|
+
catch {
|
|
436
|
+
throw new Error(`MCP tool call failed (${response.status})`);
|
|
437
|
+
}
|
|
244
438
|
}
|
|
245
439
|
if (body.error?.data?.code === "MCP_SESSION_REQUIRED" && attempt === 0) {
|
|
246
440
|
await this.mcpInitialize();
|
|
247
|
-
return await this.mcpToolCall(name, args, options, attempt + 1);
|
|
441
|
+
return await this.mcpToolCall(name, args, options, attempt + 1, paymentAttempt);
|
|
248
442
|
}
|
|
249
443
|
if (!response.ok || body.error) {
|
|
250
|
-
throw createClientError(body.error?.message ?? `MCP tool call failed (${response.status})`, body.error?.data?.code, response.status);
|
|
444
|
+
throw createClientError(body.error?.message ?? `MCP tool call failed (${response.status})`, body.error?.data?.code, response.status, toErrorDetails(body.error?.data?.details));
|
|
445
|
+
}
|
|
446
|
+
await this.handleSettlement({
|
|
447
|
+
transport: "mcp",
|
|
448
|
+
operation: name,
|
|
449
|
+
result: body.result,
|
|
450
|
+
});
|
|
451
|
+
const paymentRequired = parseMcpPaymentChallenge(body.result);
|
|
452
|
+
if (paymentRequired) {
|
|
453
|
+
const challengeError = parseMcpChallengeError(body.result);
|
|
454
|
+
if (paymentAttempt > 0) {
|
|
455
|
+
throw createClientError(paymentRequired.error ?? "x402 payment challenge persisted after retry.", challengeError?.code ?? "PAYMENT_REQUIRED", challengeError?.status ?? 402, challengeError?.details);
|
|
456
|
+
}
|
|
457
|
+
const paymentPayload = await this.buildPaymentPayload({
|
|
458
|
+
transport: "mcp",
|
|
459
|
+
operation: name,
|
|
460
|
+
idempotencyKey: options?.idempotencyKey,
|
|
461
|
+
paymentRequired,
|
|
462
|
+
});
|
|
463
|
+
return await this.mcpToolCall(name, args, {
|
|
464
|
+
...options,
|
|
465
|
+
paymentPayload,
|
|
466
|
+
}, attempt, paymentAttempt + 1);
|
|
251
467
|
}
|
|
252
468
|
return body.result;
|
|
253
469
|
}
|
|
@@ -260,22 +476,95 @@ export class IntentFiClient {
|
|
|
260
476
|
"x-api-key": this.apiKey,
|
|
261
477
|
};
|
|
262
478
|
}
|
|
263
|
-
|
|
479
|
+
assertCredential(operation, options = {}) {
|
|
480
|
+
if (this.apiKey) {
|
|
481
|
+
return;
|
|
482
|
+
}
|
|
483
|
+
if (options.allowX402 && this.paymentManager) {
|
|
484
|
+
return;
|
|
485
|
+
}
|
|
486
|
+
if (options.allowX402) {
|
|
487
|
+
throw new Error(`INTENTFI_API_KEY or paymentManager is required for ${operation}. Set apiKey in IntentFiClient options/INTENTFI_API_KEY, or provide an x402 paymentManager.`);
|
|
488
|
+
}
|
|
264
489
|
if (!this.apiKey) {
|
|
265
490
|
throw new Error(`INTENTFI_API_KEY is required for ${operation}. Set apiKey in IntentFiClient options or INTENTFI_API_KEY in the environment.`);
|
|
266
491
|
}
|
|
267
492
|
}
|
|
493
|
+
async buildPaymentPayload(args) {
|
|
494
|
+
if (!this.paymentManager) {
|
|
495
|
+
throw createClientError("x402 payment challenge received but no paymentManager is configured.", "PAYMENT_REQUIRED", 402);
|
|
496
|
+
}
|
|
497
|
+
return await this.paymentManager.buildPaymentPayload(args);
|
|
498
|
+
}
|
|
499
|
+
async handleSettlement(args) {
|
|
500
|
+
if (!this.paymentManager?.onSettlement) {
|
|
501
|
+
return;
|
|
502
|
+
}
|
|
503
|
+
const settlement = parseSettlementFromResultMeta(args.result);
|
|
504
|
+
if (!settlement) {
|
|
505
|
+
return;
|
|
506
|
+
}
|
|
507
|
+
await this.paymentManager.onSettlement({
|
|
508
|
+
transport: args.transport,
|
|
509
|
+
operation: args.operation,
|
|
510
|
+
settlement,
|
|
511
|
+
});
|
|
512
|
+
}
|
|
268
513
|
async fetchJson(path, init) {
|
|
269
514
|
const result = await this.fetchEnvelope(path, init);
|
|
270
515
|
return result.data;
|
|
271
516
|
}
|
|
517
|
+
async fetchPaidEnvelope(path, init, context) {
|
|
518
|
+
const response = await this.fetchImpl(`${this.baseUrl}${path}`, init);
|
|
519
|
+
const encodedPaymentRequired = response.headers.get(PAYMENT_REQUIRED_HEADER);
|
|
520
|
+
if (response.status !== 402 || !encodedPaymentRequired) {
|
|
521
|
+
return await this.parseEnvelopeResponse(response);
|
|
522
|
+
}
|
|
523
|
+
const paymentRequired = decodePaymentRequiredHeader(encodedPaymentRequired);
|
|
524
|
+
const paymentPayload = await this.buildPaymentPayload({
|
|
525
|
+
transport: "rest",
|
|
526
|
+
operation: context.operation,
|
|
527
|
+
idempotencyKey: context.idempotencyKey,
|
|
528
|
+
paymentRequired,
|
|
529
|
+
});
|
|
530
|
+
const retryHeaders = mergeHeaders(init.headers, {
|
|
531
|
+
[PAYMENT_SIGNATURE_HEADER]: encodeBase64Json(paymentPayload),
|
|
532
|
+
});
|
|
533
|
+
const retryResponse = await this.fetchImpl(`${this.baseUrl}${path}`, {
|
|
534
|
+
...init,
|
|
535
|
+
headers: retryHeaders,
|
|
536
|
+
});
|
|
537
|
+
const retryPaymentRequired = retryResponse.headers.get(PAYMENT_REQUIRED_HEADER);
|
|
538
|
+
if (retryResponse.status === 402 && retryPaymentRequired) {
|
|
539
|
+
await this.handleSettlement({
|
|
540
|
+
transport: "rest",
|
|
541
|
+
operation: context.operation,
|
|
542
|
+
result: { _meta: decodeSettlementMetaFromHeaders(retryResponse.headers) },
|
|
543
|
+
});
|
|
544
|
+
const retryChallenge = decodePaymentRequiredHeader(retryPaymentRequired);
|
|
545
|
+
const retryError = await readEnvelopeError(retryResponse);
|
|
546
|
+
throw createClientError(retryError?.message
|
|
547
|
+
?? retryChallenge.error
|
|
548
|
+
?? "x402 payment challenge persisted after retry.", retryError?.code ?? "PAYMENT_REQUIRED", 402, retryError?.details);
|
|
549
|
+
}
|
|
550
|
+
const envelope = await this.parseEnvelopeResponse(retryResponse);
|
|
551
|
+
await this.handleSettlement({
|
|
552
|
+
transport: "rest",
|
|
553
|
+
operation: context.operation,
|
|
554
|
+
result: { _meta: decodeSettlementMetaFromHeaders(retryResponse.headers) },
|
|
555
|
+
});
|
|
556
|
+
return envelope;
|
|
557
|
+
}
|
|
272
558
|
async fetchEnvelope(path, init) {
|
|
273
559
|
const response = await this.fetchImpl(`${this.baseUrl}${path}`, init);
|
|
560
|
+
return await this.parseEnvelopeResponse(response);
|
|
561
|
+
}
|
|
562
|
+
async parseEnvelopeResponse(response) {
|
|
274
563
|
const body = (await response.json());
|
|
275
564
|
if (!response.ok || body.ok === false) {
|
|
276
565
|
const errorCode = body.ok === false ? body.error.code : "HTTP_ERROR";
|
|
277
566
|
const errorMessage = body.ok === false ? body.error.message : `HTTP ${response.status}`;
|
|
278
|
-
throw createClientError(`${errorCode}: ${errorMessage}`, errorCode, response.status);
|
|
567
|
+
throw createClientError(`${errorCode}: ${errorMessage}`, errorCode, response.status, body.ok === false ? toErrorDetails(body.error.details) : undefined);
|
|
279
568
|
}
|
|
280
569
|
return {
|
|
281
570
|
data: body.data,
|
|
@@ -284,7 +573,7 @@ export class IntentFiClient {
|
|
|
284
573
|
};
|
|
285
574
|
}
|
|
286
575
|
}
|
|
287
|
-
function createClientError(message, code, status) {
|
|
576
|
+
function createClientError(message, code, status, details) {
|
|
288
577
|
const error = new Error(message);
|
|
289
578
|
if (code) {
|
|
290
579
|
error.code = code;
|
|
@@ -292,8 +581,324 @@ function createClientError(message, code, status) {
|
|
|
292
581
|
if (status !== undefined) {
|
|
293
582
|
error.status = status;
|
|
294
583
|
}
|
|
584
|
+
if (details) {
|
|
585
|
+
error.details = details;
|
|
586
|
+
}
|
|
295
587
|
return error;
|
|
296
588
|
}
|
|
589
|
+
function parseMcpSseMessages(payload) {
|
|
590
|
+
const lines = payload.split(/\r?\n/);
|
|
591
|
+
let currentDataLines = [];
|
|
592
|
+
let finalMessage = null;
|
|
593
|
+
let parseErrorCount = 0;
|
|
594
|
+
const flushCurrentData = () => {
|
|
595
|
+
if (currentDataLines.length === 0) {
|
|
596
|
+
return;
|
|
597
|
+
}
|
|
598
|
+
const chunk = currentDataLines.join("\n");
|
|
599
|
+
currentDataLines = [];
|
|
600
|
+
const trimmed = chunk.trim();
|
|
601
|
+
if (!trimmed || trimmed === "[DONE]") {
|
|
602
|
+
return;
|
|
603
|
+
}
|
|
604
|
+
try {
|
|
605
|
+
const parsed = JSON.parse(trimmed);
|
|
606
|
+
if (parsed.result !== undefined || parsed.error !== undefined) {
|
|
607
|
+
finalMessage = parsed;
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
catch {
|
|
611
|
+
parseErrorCount += 1;
|
|
612
|
+
}
|
|
613
|
+
};
|
|
614
|
+
for (const rawLine of lines) {
|
|
615
|
+
const line = rawLine.trimEnd();
|
|
616
|
+
if (line.length === 0) {
|
|
617
|
+
flushCurrentData();
|
|
618
|
+
continue;
|
|
619
|
+
}
|
|
620
|
+
if (!line.startsWith("data:")) {
|
|
621
|
+
continue;
|
|
622
|
+
}
|
|
623
|
+
const value = line.slice(5);
|
|
624
|
+
currentDataLines.push(value.startsWith(" ") ? value.slice(1) : value);
|
|
625
|
+
}
|
|
626
|
+
flushCurrentData();
|
|
627
|
+
return {
|
|
628
|
+
finalMessage,
|
|
629
|
+
parseErrorCount,
|
|
630
|
+
};
|
|
631
|
+
}
|
|
632
|
+
async function parseMcpSseResponse(response) {
|
|
633
|
+
if (!response.body) {
|
|
634
|
+
return parseMcpSseMessages(await response.text());
|
|
635
|
+
}
|
|
636
|
+
const reader = response.body.getReader();
|
|
637
|
+
const decoder = new TextDecoder();
|
|
638
|
+
let parseErrorCount = 0;
|
|
639
|
+
let finalMessage = null;
|
|
640
|
+
let currentDataLines = [];
|
|
641
|
+
let pendingLine = "";
|
|
642
|
+
const flushCurrentData = () => {
|
|
643
|
+
if (currentDataLines.length === 0) {
|
|
644
|
+
return;
|
|
645
|
+
}
|
|
646
|
+
const chunk = currentDataLines.join("\n");
|
|
647
|
+
currentDataLines = [];
|
|
648
|
+
const trimmed = chunk.trim();
|
|
649
|
+
if (!trimmed || trimmed === "[DONE]") {
|
|
650
|
+
return;
|
|
651
|
+
}
|
|
652
|
+
try {
|
|
653
|
+
const parsed = JSON.parse(trimmed);
|
|
654
|
+
if (parsed.result !== undefined || parsed.error !== undefined) {
|
|
655
|
+
finalMessage = parsed;
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
catch {
|
|
659
|
+
parseErrorCount += 1;
|
|
660
|
+
}
|
|
661
|
+
};
|
|
662
|
+
const processLines = (text) => {
|
|
663
|
+
const lines = text.split(/\r?\n/);
|
|
664
|
+
for (const rawLine of lines) {
|
|
665
|
+
const line = rawLine.trimEnd();
|
|
666
|
+
if (line.length === 0) {
|
|
667
|
+
flushCurrentData();
|
|
668
|
+
continue;
|
|
669
|
+
}
|
|
670
|
+
if (!line.startsWith("data:")) {
|
|
671
|
+
continue;
|
|
672
|
+
}
|
|
673
|
+
const value = line.slice(5);
|
|
674
|
+
currentDataLines.push(value.startsWith(" ") ? value.slice(1) : value);
|
|
675
|
+
}
|
|
676
|
+
};
|
|
677
|
+
try {
|
|
678
|
+
while (true) {
|
|
679
|
+
const { done, value } = await reader.read();
|
|
680
|
+
if (done) {
|
|
681
|
+
break;
|
|
682
|
+
}
|
|
683
|
+
pendingLine += decoder.decode(value, { stream: true });
|
|
684
|
+
const lastBreak = pendingLine.lastIndexOf("\n");
|
|
685
|
+
if (lastBreak < 0) {
|
|
686
|
+
continue;
|
|
687
|
+
}
|
|
688
|
+
const completeText = pendingLine.slice(0, lastBreak + 1);
|
|
689
|
+
pendingLine = pendingLine.slice(lastBreak + 1);
|
|
690
|
+
processLines(completeText);
|
|
691
|
+
}
|
|
692
|
+
pendingLine += decoder.decode();
|
|
693
|
+
if (pendingLine.length > 0) {
|
|
694
|
+
processLines(`${pendingLine}\n`);
|
|
695
|
+
}
|
|
696
|
+
flushCurrentData();
|
|
697
|
+
}
|
|
698
|
+
finally {
|
|
699
|
+
reader.releaseLock();
|
|
700
|
+
}
|
|
701
|
+
return {
|
|
702
|
+
finalMessage,
|
|
703
|
+
parseErrorCount,
|
|
704
|
+
};
|
|
705
|
+
}
|
|
706
|
+
async function readEnvelopeError(response) {
|
|
707
|
+
try {
|
|
708
|
+
const body = (await response.json());
|
|
709
|
+
if (body.ok === false
|
|
710
|
+
&& body.error
|
|
711
|
+
&& typeof body.error.code === "string"
|
|
712
|
+
&& typeof body.error.message === "string") {
|
|
713
|
+
const details = toErrorDetails(body.error.details);
|
|
714
|
+
return {
|
|
715
|
+
code: body.error.code,
|
|
716
|
+
message: `${body.error.code}: ${body.error.message}`,
|
|
717
|
+
...(details ? { details } : {}),
|
|
718
|
+
};
|
|
719
|
+
}
|
|
720
|
+
}
|
|
721
|
+
catch {
|
|
722
|
+
return null;
|
|
723
|
+
}
|
|
724
|
+
return null;
|
|
725
|
+
}
|
|
726
|
+
function parseAtomicAmount(value) {
|
|
727
|
+
try {
|
|
728
|
+
return BigInt(value);
|
|
729
|
+
}
|
|
730
|
+
catch {
|
|
731
|
+
throw new Error(`Invalid x402 amount '${value}'.`);
|
|
732
|
+
}
|
|
733
|
+
}
|
|
734
|
+
function echoRequiredExtensions(extensions, idempotencyKey) {
|
|
735
|
+
if (!extensions) {
|
|
736
|
+
return undefined;
|
|
737
|
+
}
|
|
738
|
+
const echoedExtensions = {
|
|
739
|
+
...extensions,
|
|
740
|
+
};
|
|
741
|
+
if (!idempotencyKey) {
|
|
742
|
+
return echoedExtensions;
|
|
743
|
+
}
|
|
744
|
+
const paymentIdentifier = extensions[PAYMENT_IDENTIFIER_EXTENSION];
|
|
745
|
+
if (!paymentIdentifier) {
|
|
746
|
+
return echoedExtensions;
|
|
747
|
+
}
|
|
748
|
+
return {
|
|
749
|
+
...echoedExtensions,
|
|
750
|
+
[PAYMENT_IDENTIFIER_EXTENSION]: {
|
|
751
|
+
schema: paymentIdentifier.schema,
|
|
752
|
+
info: {
|
|
753
|
+
...paymentIdentifier.info,
|
|
754
|
+
id: idempotencyKey,
|
|
755
|
+
},
|
|
756
|
+
},
|
|
757
|
+
};
|
|
758
|
+
}
|
|
759
|
+
function parseMcpPaymentChallenge(value) {
|
|
760
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
761
|
+
return null;
|
|
762
|
+
}
|
|
763
|
+
const record = value;
|
|
764
|
+
if (record.isError !== true) {
|
|
765
|
+
return null;
|
|
766
|
+
}
|
|
767
|
+
const structured = record.structuredContent;
|
|
768
|
+
if (isX402PaymentRequired(structured)) {
|
|
769
|
+
return structured;
|
|
770
|
+
}
|
|
771
|
+
const content = record.content;
|
|
772
|
+
if (!Array.isArray(content)) {
|
|
773
|
+
return null;
|
|
774
|
+
}
|
|
775
|
+
const first = content[0];
|
|
776
|
+
if (!first || typeof first !== "object" || Array.isArray(first)) {
|
|
777
|
+
return null;
|
|
778
|
+
}
|
|
779
|
+
const text = first.text;
|
|
780
|
+
if (typeof text !== "string") {
|
|
781
|
+
return null;
|
|
782
|
+
}
|
|
783
|
+
try {
|
|
784
|
+
const parsed = JSON.parse(text);
|
|
785
|
+
return isX402PaymentRequired(parsed) ? parsed : null;
|
|
786
|
+
}
|
|
787
|
+
catch {
|
|
788
|
+
return null;
|
|
789
|
+
}
|
|
790
|
+
}
|
|
791
|
+
function parseMcpChallengeError(value) {
|
|
792
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
793
|
+
return null;
|
|
794
|
+
}
|
|
795
|
+
const meta = value._meta;
|
|
796
|
+
if (!meta || typeof meta !== "object" || Array.isArray(meta)) {
|
|
797
|
+
return null;
|
|
798
|
+
}
|
|
799
|
+
const challengeError = meta["x402/error"];
|
|
800
|
+
if (!challengeError || typeof challengeError !== "object" || Array.isArray(challengeError)) {
|
|
801
|
+
return null;
|
|
802
|
+
}
|
|
803
|
+
const code = challengeError.code;
|
|
804
|
+
const status = challengeError.status;
|
|
805
|
+
if (typeof code !== "string" || typeof status !== "number" || !Number.isFinite(status)) {
|
|
806
|
+
return null;
|
|
807
|
+
}
|
|
808
|
+
const details = toErrorDetails(challengeError.details);
|
|
809
|
+
return {
|
|
810
|
+
code,
|
|
811
|
+
status: Math.trunc(status),
|
|
812
|
+
...(details ? { details } : {}),
|
|
813
|
+
};
|
|
814
|
+
}
|
|
815
|
+
function toErrorDetails(value) {
|
|
816
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
817
|
+
return undefined;
|
|
818
|
+
}
|
|
819
|
+
return value;
|
|
820
|
+
}
|
|
821
|
+
function parseSettlementFromResultMeta(result) {
|
|
822
|
+
if (!result || typeof result !== "object" || Array.isArray(result)) {
|
|
823
|
+
return null;
|
|
824
|
+
}
|
|
825
|
+
const meta = result._meta;
|
|
826
|
+
if (!meta || typeof meta !== "object" || Array.isArray(meta)) {
|
|
827
|
+
return null;
|
|
828
|
+
}
|
|
829
|
+
const settlement = meta["x402/payment-response"];
|
|
830
|
+
return isX402SettlementResponse(settlement) ? settlement : null;
|
|
831
|
+
}
|
|
832
|
+
function decodeSettlementMetaFromHeaders(headers) {
|
|
833
|
+
const encoded = headers.get(PAYMENT_RESPONSE_HEADER);
|
|
834
|
+
if (!encoded) {
|
|
835
|
+
return undefined;
|
|
836
|
+
}
|
|
837
|
+
const decoded = decodeBase64Json(encoded);
|
|
838
|
+
if (!isX402SettlementResponse(decoded)) {
|
|
839
|
+
return undefined;
|
|
840
|
+
}
|
|
841
|
+
return {
|
|
842
|
+
"x402/payment-response": decoded,
|
|
843
|
+
};
|
|
844
|
+
}
|
|
845
|
+
function decodePaymentRequiredHeader(header) {
|
|
846
|
+
const decoded = decodeBase64Json(header);
|
|
847
|
+
if (!isX402PaymentRequired(decoded)) {
|
|
848
|
+
throw createClientError("PAYMENT-REQUIRED header contained an invalid x402 payload.", "PAYMENT_REQUIRED", 402);
|
|
849
|
+
}
|
|
850
|
+
return decoded;
|
|
851
|
+
}
|
|
852
|
+
function mergeHeaders(original, extra) {
|
|
853
|
+
const headers = new Headers(original ?? {});
|
|
854
|
+
for (const [key, value] of Object.entries(extra)) {
|
|
855
|
+
headers.set(key, value);
|
|
856
|
+
}
|
|
857
|
+
return headers;
|
|
858
|
+
}
|
|
859
|
+
function encodeBase64Json(value) {
|
|
860
|
+
const json = JSON.stringify(value);
|
|
861
|
+
const bytes = new TextEncoder().encode(json);
|
|
862
|
+
let binary = "";
|
|
863
|
+
for (const byte of bytes) {
|
|
864
|
+
binary += String.fromCharCode(byte);
|
|
865
|
+
}
|
|
866
|
+
return btoa(binary);
|
|
867
|
+
}
|
|
868
|
+
function decodeBase64Json(encoded) {
|
|
869
|
+
const normalized = encoded.trim().replace(/-/g, "+").replace(/_/g, "/");
|
|
870
|
+
const padded = normalized + "=".repeat((4 - (normalized.length % 4)) % 4);
|
|
871
|
+
const binary = atob(padded);
|
|
872
|
+
const bytes = new Uint8Array(binary.length);
|
|
873
|
+
for (let index = 0; index < binary.length; index += 1) {
|
|
874
|
+
bytes[index] = binary.charCodeAt(index);
|
|
875
|
+
}
|
|
876
|
+
const text = new TextDecoder().decode(bytes);
|
|
877
|
+
return JSON.parse(text);
|
|
878
|
+
}
|
|
879
|
+
function isX402PaymentRequired(value) {
|
|
880
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
881
|
+
return false;
|
|
882
|
+
}
|
|
883
|
+
const record = value;
|
|
884
|
+
if (record.x402Version !== X402_VERSION) {
|
|
885
|
+
return false;
|
|
886
|
+
}
|
|
887
|
+
if (!record.resource || typeof record.resource !== "object" || Array.isArray(record.resource)) {
|
|
888
|
+
return false;
|
|
889
|
+
}
|
|
890
|
+
const accepts = record.accepts;
|
|
891
|
+
return Array.isArray(accepts) && accepts.length > 0;
|
|
892
|
+
}
|
|
893
|
+
function isX402SettlementResponse(value) {
|
|
894
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
895
|
+
return false;
|
|
896
|
+
}
|
|
897
|
+
const record = value;
|
|
898
|
+
return (typeof record.success === "boolean"
|
|
899
|
+
&& typeof record.transaction === "string"
|
|
900
|
+
&& typeof record.network === "string");
|
|
901
|
+
}
|
|
297
902
|
function parseRetryAfterHeader(value) {
|
|
298
903
|
if (!value) {
|
|
299
904
|
return undefined;
|