@cmdoss/suipay-mcp 1.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/README.md +87 -0
- package/dist/bin/suipay.d.ts +1 -0
- package/dist/bin/suipay.js +11 -0
- package/dist/chunk-5I6V7N3R.js +67 -0
- package/dist/chunk-772CHNGT.js +3879 -0
- package/dist/http-B5iGH5mf.d.ts +427 -0
- package/dist/http.d.ts +1 -0
- package/dist/http.js +6 -0
- package/dist/index.d.ts +283 -0
- package/dist/index.js +32 -0
- package/package.json +33 -0
|
@@ -0,0 +1,3879 @@
|
|
|
1
|
+
// src/http.ts
|
|
2
|
+
import { WebStandardStreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/webStandardStreamableHttp.js";
|
|
3
|
+
|
|
4
|
+
// src/server.ts
|
|
5
|
+
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
6
|
+
import {
|
|
7
|
+
CallToolRequestSchema,
|
|
8
|
+
ListToolsRequestSchema
|
|
9
|
+
} from "@modelcontextprotocol/sdk/types.js";
|
|
10
|
+
|
|
11
|
+
// ../../src/oauth/scopes.ts
|
|
12
|
+
var OAUTH_SCOPES_SUPPORTED = [
|
|
13
|
+
"suipay:discover",
|
|
14
|
+
"suipay:receipts",
|
|
15
|
+
"suipay:pay",
|
|
16
|
+
"suipay:policy:read"
|
|
17
|
+
];
|
|
18
|
+
var SUPPORTED_SET = new Set(OAUTH_SCOPES_SUPPORTED);
|
|
19
|
+
var OAUTH_DEFAULT_SCOPE = OAUTH_SCOPES_SUPPORTED.join(" ");
|
|
20
|
+
function parseScopeString(scope) {
|
|
21
|
+
if (!scope?.trim()) return [];
|
|
22
|
+
return scope.trim().split(/\s+/).filter(Boolean);
|
|
23
|
+
}
|
|
24
|
+
function scopeAllows(grantedScope, required) {
|
|
25
|
+
const set = new Set(parseScopeString(grantedScope));
|
|
26
|
+
return set.has(required);
|
|
27
|
+
}
|
|
28
|
+
var BUYER_ACTION_SCOPES = {
|
|
29
|
+
read_access: "suipay:policy:read",
|
|
30
|
+
list_payers: "suipay:policy:read",
|
|
31
|
+
pay: "suipay:pay",
|
|
32
|
+
read_receipts: "suipay:receipts",
|
|
33
|
+
discover: "suipay:discover"
|
|
34
|
+
};
|
|
35
|
+
var MCP_TOOL_ACTIONS = {
|
|
36
|
+
access_context: "read_access",
|
|
37
|
+
pay: "pay",
|
|
38
|
+
discover: "discover",
|
|
39
|
+
receipts: "read_receipts",
|
|
40
|
+
// Login/logout are local-stdio flows; remote HTTP still requires discover.
|
|
41
|
+
suipay_login: "discover",
|
|
42
|
+
suipay_logout: "discover"
|
|
43
|
+
};
|
|
44
|
+
function scopeForMcpTool(toolName) {
|
|
45
|
+
const action = MCP_TOOL_ACTIONS[toolName];
|
|
46
|
+
return action ? BUYER_ACTION_SCOPES[action] : void 0;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// src/auth-context.ts
|
|
50
|
+
function toolAllowedByScope(toolName, grantedScope) {
|
|
51
|
+
const required = scopeForMcpTool(toolName);
|
|
52
|
+
if (!required) {
|
|
53
|
+
return { ok: true };
|
|
54
|
+
}
|
|
55
|
+
if (!scopeAllows(grantedScope, required)) {
|
|
56
|
+
return { ok: false, required };
|
|
57
|
+
}
|
|
58
|
+
return { ok: true };
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// ../../src/allowance/chain-sui.ts
|
|
62
|
+
import { SuiGrpcClient } from "@mysten/sui/grpc";
|
|
63
|
+
import { Transaction, coinWithBalance } from "@mysten/sui/transactions";
|
|
64
|
+
import { fromBase64, normalizeSuiAddress, toBase64 } from "@mysten/sui/utils";
|
|
65
|
+
|
|
66
|
+
// ../../src/chain/grpc-finality.ts
|
|
67
|
+
function isGrpcTxNotFound(err) {
|
|
68
|
+
const code = err?.code;
|
|
69
|
+
return code === 5 || code === "NOT_FOUND";
|
|
70
|
+
}
|
|
71
|
+
async function fetchGrpcTransaction(client, digest, include = {
|
|
72
|
+
events: true,
|
|
73
|
+
effects: true,
|
|
74
|
+
transaction: true
|
|
75
|
+
}) {
|
|
76
|
+
try {
|
|
77
|
+
return await client.getTransaction({
|
|
78
|
+
digest,
|
|
79
|
+
include
|
|
80
|
+
});
|
|
81
|
+
} catch (err) {
|
|
82
|
+
if (isGrpcTxNotFound(err)) return null;
|
|
83
|
+
throw err;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// ../../src/allowance/chain-sui.ts
|
|
88
|
+
var ALLOWANCE_MODULE = "allowance";
|
|
89
|
+
function createSuiAllowanceChain(options) {
|
|
90
|
+
const network = options.network ?? "testnet";
|
|
91
|
+
const client = options.client ?? new SuiGrpcClient({
|
|
92
|
+
network,
|
|
93
|
+
baseUrl: options.url ?? `https://fullnode.${network}.sui.io:443`
|
|
94
|
+
});
|
|
95
|
+
const packageId = normalizeSuiAddress(options.packageId);
|
|
96
|
+
const target = (fn) => `${packageId}::${ALLOWANCE_MODULE}::${fn}`;
|
|
97
|
+
async function build(tx) {
|
|
98
|
+
return { txBytes: toBase64(await tx.build({ client })) };
|
|
99
|
+
}
|
|
100
|
+
return {
|
|
101
|
+
network: `sui:${network}`,
|
|
102
|
+
packageId,
|
|
103
|
+
client,
|
|
104
|
+
async buildCreateTx(req) {
|
|
105
|
+
const tx = new Transaction();
|
|
106
|
+
tx.setSender(req.owner);
|
|
107
|
+
const funding = coinWithBalance({ type: req.coinType, balance: BigInt(req.funding) });
|
|
108
|
+
tx.moveCall({
|
|
109
|
+
target: target("create_and_share"),
|
|
110
|
+
typeArguments: [req.coinType],
|
|
111
|
+
arguments: [
|
|
112
|
+
funding,
|
|
113
|
+
tx.pure.address(req.delegate),
|
|
114
|
+
tx.pure.address(req.recipient),
|
|
115
|
+
tx.pure.u64(BigInt(req.maxPerPayment))
|
|
116
|
+
]
|
|
117
|
+
});
|
|
118
|
+
return build(tx);
|
|
119
|
+
},
|
|
120
|
+
async buildPayTx(req) {
|
|
121
|
+
const tx = new Transaction();
|
|
122
|
+
tx.setSender(req.delegate);
|
|
123
|
+
tx.moveCall({
|
|
124
|
+
target: target("pay"),
|
|
125
|
+
typeArguments: [req.coinType],
|
|
126
|
+
arguments: [
|
|
127
|
+
// The shared allowance object; its version is resolved at build time.
|
|
128
|
+
tx.object(req.allowanceId),
|
|
129
|
+
tx.pure.u64(BigInt(req.expectedNonce)),
|
|
130
|
+
tx.pure.u64(BigInt(req.amount)),
|
|
131
|
+
tx.pure.vector("u8", Array.from(req.paymentIdHash)),
|
|
132
|
+
tx.pure.vector("u8", Array.from(req.termsHash))
|
|
133
|
+
]
|
|
134
|
+
});
|
|
135
|
+
return build(tx);
|
|
136
|
+
},
|
|
137
|
+
async buildPayTxKind(req) {
|
|
138
|
+
const tx = new Transaction();
|
|
139
|
+
tx.setSender(req.delegate);
|
|
140
|
+
tx.moveCall({
|
|
141
|
+
target: target("pay"),
|
|
142
|
+
typeArguments: [req.coinType],
|
|
143
|
+
arguments: [
|
|
144
|
+
tx.object(req.allowanceId),
|
|
145
|
+
tx.pure.u64(BigInt(req.expectedNonce)),
|
|
146
|
+
tx.pure.u64(BigInt(req.amount)),
|
|
147
|
+
tx.pure.vector("u8", Array.from(req.paymentIdHash)),
|
|
148
|
+
tx.pure.vector("u8", Array.from(req.termsHash))
|
|
149
|
+
]
|
|
150
|
+
});
|
|
151
|
+
return { kindBytes: toBase64(await tx.build({ client, onlyTransactionKind: true })) };
|
|
152
|
+
},
|
|
153
|
+
async buildTopUpTx(req) {
|
|
154
|
+
const tx = new Transaction();
|
|
155
|
+
tx.setSender(req.owner);
|
|
156
|
+
const funding = coinWithBalance({ type: req.coinType, balance: BigInt(req.amount) });
|
|
157
|
+
tx.moveCall({
|
|
158
|
+
target: target("top_up"),
|
|
159
|
+
typeArguments: [req.coinType],
|
|
160
|
+
arguments: [tx.object(req.allowanceId), funding]
|
|
161
|
+
});
|
|
162
|
+
return build(tx);
|
|
163
|
+
},
|
|
164
|
+
async buildPauseTx(req) {
|
|
165
|
+
return build(manageTx(req, target("pause")));
|
|
166
|
+
},
|
|
167
|
+
async buildResumeTx(req) {
|
|
168
|
+
return build(manageTx(req, target("resume")));
|
|
169
|
+
},
|
|
170
|
+
async buildRevokeTx(req) {
|
|
171
|
+
return build(manageTx(req, target("revoke_and_refund")));
|
|
172
|
+
},
|
|
173
|
+
async inspectPay(txBytes, expected) {
|
|
174
|
+
let data;
|
|
175
|
+
try {
|
|
176
|
+
data = Transaction.from(fromBase64(txBytes)).getData();
|
|
177
|
+
} catch (err) {
|
|
178
|
+
return { ok: false, reason: `undecodable transaction: ${errText(err)}` };
|
|
179
|
+
}
|
|
180
|
+
if (!data.sender) return { ok: false, reason: "transaction has no sender" };
|
|
181
|
+
const want = `${normalizeSuiAddress(expected.packageId)}::${ALLOWANCE_MODULE}::pay`;
|
|
182
|
+
const hit = data.commands.some((c) => {
|
|
183
|
+
const mc = c.$kind === "MoveCall" ? c.MoveCall : void 0;
|
|
184
|
+
if (!mc) return false;
|
|
185
|
+
return `${normalizeSuiAddress(mc.package)}::${mc.module}::${mc.function}` === want;
|
|
186
|
+
});
|
|
187
|
+
if (!hit) return { ok: false, reason: `transaction does not call ${want}` };
|
|
188
|
+
return { ok: true, sender: normalizeSuiAddress(data.sender) };
|
|
189
|
+
},
|
|
190
|
+
async execute(txBytes, signature) {
|
|
191
|
+
let digest;
|
|
192
|
+
try {
|
|
193
|
+
const submitted = await client.core.executeTransaction({
|
|
194
|
+
transaction: fromBase64(txBytes),
|
|
195
|
+
signatures: [signature]
|
|
196
|
+
});
|
|
197
|
+
if (submitted.FailedTransaction) {
|
|
198
|
+
return {
|
|
199
|
+
status: "rejected",
|
|
200
|
+
reason: executionError(submitted.FailedTransaction.status)
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
digest = submitted.Transaction.digest;
|
|
204
|
+
} catch (err) {
|
|
205
|
+
const text2 = errText(err);
|
|
206
|
+
if (isDeterministicRejection(text2)) return { status: "rejected", reason: text2 };
|
|
207
|
+
return { status: "ambiguous", reason: text2 };
|
|
208
|
+
}
|
|
209
|
+
try {
|
|
210
|
+
const result = await client.core.waitForTransaction({
|
|
211
|
+
digest,
|
|
212
|
+
include: { events: true, effects: true },
|
|
213
|
+
timeout: 6e4
|
|
214
|
+
});
|
|
215
|
+
const finalized = result.Transaction ?? result.FailedTransaction;
|
|
216
|
+
if (!finalized.status.success) {
|
|
217
|
+
return { status: "rejected", reason: executionError(finalized.status) };
|
|
218
|
+
}
|
|
219
|
+
const events = (finalized.events ?? []).map(parseAllowanceEvent).filter((e) => e !== null);
|
|
220
|
+
return { status: "ok", txDigest: digest, events };
|
|
221
|
+
} catch (err) {
|
|
222
|
+
return { status: "ambiguous", reason: errText(err), txDigest: digest };
|
|
223
|
+
}
|
|
224
|
+
},
|
|
225
|
+
async readAllowance(id) {
|
|
226
|
+
const { object } = await client.core.getObject({
|
|
227
|
+
objectId: id,
|
|
228
|
+
include: { json: true }
|
|
229
|
+
});
|
|
230
|
+
if (!object.json) return null;
|
|
231
|
+
const f = object.json;
|
|
232
|
+
return {
|
|
233
|
+
id,
|
|
234
|
+
owner: normalizeSuiAddress(String(f.owner)),
|
|
235
|
+
delegate: normalizeSuiAddress(String(f.delegate)),
|
|
236
|
+
recipient: normalizeSuiAddress(String(f.recipient)),
|
|
237
|
+
balance: readBalance(f.funds),
|
|
238
|
+
maxPerPayment: String(f.max_per_payment),
|
|
239
|
+
nextNonce: String(f.next_nonce),
|
|
240
|
+
paused: Boolean(f.paused),
|
|
241
|
+
revoked: Boolean(f.revoked),
|
|
242
|
+
coinType: coinTypeArg(object.type)
|
|
243
|
+
};
|
|
244
|
+
},
|
|
245
|
+
async listPayments(id) {
|
|
246
|
+
const target2 = normalizeSuiAddress(id);
|
|
247
|
+
const out = [];
|
|
248
|
+
let after = null;
|
|
249
|
+
for (let page = 0; page < 20; page++) {
|
|
250
|
+
const res = await client.core.listEvents({
|
|
251
|
+
filter: { eventType: `${packageId}::${ALLOWANCE_MODULE}::PaymentMade` },
|
|
252
|
+
after,
|
|
253
|
+
order: "ascending"
|
|
254
|
+
});
|
|
255
|
+
for (const ev of res.events) {
|
|
256
|
+
const parsed = parseAllowanceEvent(ev);
|
|
257
|
+
if (parsed?.kind === "paid" && normalizeSuiAddress(parsed.allowance) === target2) out.push(parsed);
|
|
258
|
+
}
|
|
259
|
+
if (!res.hasNextPage) return out;
|
|
260
|
+
if (!res.endCursor) {
|
|
261
|
+
throw new Error("gRPC event query returned no continuation cursor");
|
|
262
|
+
}
|
|
263
|
+
after = res.endCursor;
|
|
264
|
+
}
|
|
265
|
+
throw new Error("gRPC event query exceeded the 20-page scan limit");
|
|
266
|
+
},
|
|
267
|
+
async getFinalizedTx(txDigest) {
|
|
268
|
+
const result = await fetchGrpcTransaction(client, txDigest, {
|
|
269
|
+
events: true,
|
|
270
|
+
effects: true,
|
|
271
|
+
transaction: true,
|
|
272
|
+
protoJson: true
|
|
273
|
+
});
|
|
274
|
+
if (!result) return null;
|
|
275
|
+
const finalized = result.Transaction ?? result.FailedTransaction;
|
|
276
|
+
if (!finalized) {
|
|
277
|
+
throw new Error("finality reader: response has no Transaction variant");
|
|
278
|
+
}
|
|
279
|
+
if (finalized.digest != null && String(finalized.digest) !== txDigest) {
|
|
280
|
+
throw new Error("finality reader: response digest does not match request");
|
|
281
|
+
}
|
|
282
|
+
const proto = result.protoJson;
|
|
283
|
+
const checkpoint = proto?.checkpoint == null ? null : String(proto.checkpoint);
|
|
284
|
+
const sender = finalized.transaction?.sender ? normalizeSuiAddress(finalized.transaction.sender) : "";
|
|
285
|
+
const success = finalized.status?.success;
|
|
286
|
+
if (success !== true && success !== false) {
|
|
287
|
+
throw new Error("finality reader: response has no success status");
|
|
288
|
+
}
|
|
289
|
+
if (success === false) {
|
|
290
|
+
return {
|
|
291
|
+
status: "failed",
|
|
292
|
+
txDigest,
|
|
293
|
+
checkpoint,
|
|
294
|
+
reason: executionError(finalized.status)
|
|
295
|
+
};
|
|
296
|
+
}
|
|
297
|
+
const events = (finalized.events ?? []).map((e) => parseAllowanceEvent(e)).filter((e) => e !== null);
|
|
298
|
+
return { status: "success", txDigest, checkpoint, sender, events };
|
|
299
|
+
}
|
|
300
|
+
};
|
|
301
|
+
function manageTx(req, fn) {
|
|
302
|
+
const tx = new Transaction();
|
|
303
|
+
tx.setSender(req.owner);
|
|
304
|
+
tx.moveCall({ target: fn, typeArguments: [req.coinType], arguments: [tx.object(req.allowanceId)] });
|
|
305
|
+
return tx;
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
function readBalance(funds) {
|
|
309
|
+
if (typeof funds === "string") return funds;
|
|
310
|
+
if (funds && typeof funds === "object") {
|
|
311
|
+
const v = funds;
|
|
312
|
+
if (v.value != null) return String(v.value);
|
|
313
|
+
if (v.fields?.value != null) return String(v.fields.value);
|
|
314
|
+
}
|
|
315
|
+
return "0";
|
|
316
|
+
}
|
|
317
|
+
function coinTypeArg(objectType) {
|
|
318
|
+
if (!objectType) return "";
|
|
319
|
+
const lt = objectType.indexOf("<");
|
|
320
|
+
const gt = objectType.lastIndexOf(">");
|
|
321
|
+
return lt >= 0 && gt > lt ? prefix0x(objectType.slice(lt + 1, gt)) : "";
|
|
322
|
+
}
|
|
323
|
+
function parseAllowanceEvent(ev) {
|
|
324
|
+
const j = ev.json;
|
|
325
|
+
if (!j) return null;
|
|
326
|
+
if (ev.eventType.endsWith(`::${ALLOWANCE_MODULE}::PaymentMade`)) {
|
|
327
|
+
return {
|
|
328
|
+
kind: "paid",
|
|
329
|
+
allowance: normalizeSuiAddress(String(j.allowance)),
|
|
330
|
+
payer: normalizeSuiAddress(String(j.payer)),
|
|
331
|
+
recipient: normalizeSuiAddress(String(j.recipient)),
|
|
332
|
+
amount: String(j.amount),
|
|
333
|
+
nonce: String(j.nonce),
|
|
334
|
+
remaining: String(j.remaining),
|
|
335
|
+
paymentIdHash: hexOf(j.payment_id_hash),
|
|
336
|
+
termsHash: hexOf(j.terms_hash),
|
|
337
|
+
coinType: prefix0x(String(j.coin_type))
|
|
338
|
+
};
|
|
339
|
+
}
|
|
340
|
+
if (ev.eventType.endsWith(`::${ALLOWANCE_MODULE}::AllowanceCreated`)) {
|
|
341
|
+
return {
|
|
342
|
+
kind: "created",
|
|
343
|
+
allowance: normalizeSuiAddress(String(j.allowance)),
|
|
344
|
+
owner: normalizeSuiAddress(String(j.owner)),
|
|
345
|
+
delegate: normalizeSuiAddress(String(j.delegate)),
|
|
346
|
+
recipient: normalizeSuiAddress(String(j.recipient)),
|
|
347
|
+
funded: String(j.funded),
|
|
348
|
+
maxPerPayment: String(j.max_per_payment),
|
|
349
|
+
coinType: prefix0x(String(j.coin_type))
|
|
350
|
+
};
|
|
351
|
+
}
|
|
352
|
+
if (ev.eventType.endsWith(`::${ALLOWANCE_MODULE}::AllowanceToppedUp`)) {
|
|
353
|
+
return { kind: "toppedUp", allowance: normalizeSuiAddress(String(j.allowance)), amount: String(j.amount), balance: String(j.balance) };
|
|
354
|
+
}
|
|
355
|
+
if (ev.eventType.endsWith(`::${ALLOWANCE_MODULE}::AllowancePauseSet`)) {
|
|
356
|
+
return { kind: "pauseSet", allowance: normalizeSuiAddress(String(j.allowance)), paused: Boolean(j.paused) };
|
|
357
|
+
}
|
|
358
|
+
if (ev.eventType.endsWith(`::${ALLOWANCE_MODULE}::AllowanceRevoked`)) {
|
|
359
|
+
return { kind: "revoked", allowance: normalizeSuiAddress(String(j.allowance)), refunded: String(j.refunded) };
|
|
360
|
+
}
|
|
361
|
+
return null;
|
|
362
|
+
}
|
|
363
|
+
function hexOf(raw) {
|
|
364
|
+
if (Array.isArray(raw)) return Buffer.from(raw).toString("hex");
|
|
365
|
+
if (typeof raw === "string") return Buffer.from(raw, "base64").toString("hex");
|
|
366
|
+
return "";
|
|
367
|
+
}
|
|
368
|
+
function prefix0x(coinType) {
|
|
369
|
+
return `0x${coinType.replace(/^0x/, "")}`;
|
|
370
|
+
}
|
|
371
|
+
function isDeterministicRejection(text2) {
|
|
372
|
+
return /invalid signature|signature is not valid|objectnotfound|insufficient|balance|gas|dependent package|invaliduserinput|object is not available|abort/i.test(
|
|
373
|
+
text2
|
|
374
|
+
);
|
|
375
|
+
}
|
|
376
|
+
function errText(err) {
|
|
377
|
+
return err instanceof Error ? err.message : String(err);
|
|
378
|
+
}
|
|
379
|
+
function executionError(status) {
|
|
380
|
+
return status.error?.message ?? "transaction execution failed";
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
// ../../src/buyer-control/chain.ts
|
|
384
|
+
import { bcs } from "@mysten/sui/bcs";
|
|
385
|
+
import { SuiGrpcClient as SuiGrpcClient2 } from "@mysten/sui/grpc";
|
|
386
|
+
import { Transaction as Transaction2, coinWithBalance as coinWithBalance2 } from "@mysten/sui/transactions";
|
|
387
|
+
import {
|
|
388
|
+
normalizeStructTag as normalizeStructTag3,
|
|
389
|
+
normalizeSuiAddress as normalizeSuiAddress2,
|
|
390
|
+
toBase64 as toBase642
|
|
391
|
+
} from "@mysten/sui/utils";
|
|
392
|
+
|
|
393
|
+
// ../../src/chain/address.ts
|
|
394
|
+
function hexOf2(address) {
|
|
395
|
+
return address.trim().replace(/^0x/i, "").toLowerCase();
|
|
396
|
+
}
|
|
397
|
+
function isZeroAddress(address) {
|
|
398
|
+
const hex = hexOf2(address);
|
|
399
|
+
return hex.length === 0 || /^0+$/.test(hex);
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
// ../../src/policy/target.ts
|
|
403
|
+
import { createHash } from "crypto";
|
|
404
|
+
import { normalizeStructTag as normalizeStructTag2 } from "@mysten/sui/utils";
|
|
405
|
+
|
|
406
|
+
// ../../src/assets/testnet.ts
|
|
407
|
+
import { normalizeStructTag } from "@mysten/sui/utils";
|
|
408
|
+
|
|
409
|
+
// ../../src/policy/target.ts
|
|
410
|
+
function targetHashBytes(hexHash) {
|
|
411
|
+
if (!/^[0-9a-f]{64}$/i.test(hexHash)) {
|
|
412
|
+
throw new Error("targetHash must be 32-byte hex.");
|
|
413
|
+
}
|
|
414
|
+
return Uint8Array.from(Buffer.from(hexHash, "hex"));
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
// ../../src/buyer-control/chain.ts
|
|
418
|
+
var MODULE = "shared_pool";
|
|
419
|
+
var SUI_COIN_TYPE = "0x2::sui::SUI";
|
|
420
|
+
function isSuiCoinType(coinType) {
|
|
421
|
+
try {
|
|
422
|
+
return normalizeStructTag3(coinType) === normalizeStructTag3(SUI_COIN_TYPE);
|
|
423
|
+
} catch {
|
|
424
|
+
return /::sui::SUI$/i.test(coinType);
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
var SUI_CLOCK = "0x6";
|
|
428
|
+
function createSharedPoolChain(options) {
|
|
429
|
+
const network = options.network ?? "testnet";
|
|
430
|
+
const rpcUrl = options.url ?? `https://fullnode.${network}.sui.io:443`;
|
|
431
|
+
const client = options.client ?? new SuiGrpcClient2({
|
|
432
|
+
network,
|
|
433
|
+
baseUrl: rpcUrl
|
|
434
|
+
});
|
|
435
|
+
const packageId = normalizeSuiAddress2(options.packageId);
|
|
436
|
+
const target = (fn) => `${packageId}::${MODULE}::${fn}`;
|
|
437
|
+
async function kindBytes(tx) {
|
|
438
|
+
return {
|
|
439
|
+
kindBytes: toBase642(await tx.build({ client, onlyTransactionKind: true }))
|
|
440
|
+
};
|
|
441
|
+
}
|
|
442
|
+
async function fundingCoin(tx, owner, coinType, amount) {
|
|
443
|
+
if (!isSuiCoinType(coinType)) {
|
|
444
|
+
return coinWithBalance2({ type: coinType, balance: amount });
|
|
445
|
+
}
|
|
446
|
+
const listed = await client.core.listCoins({
|
|
447
|
+
owner,
|
|
448
|
+
coinType: SUI_COIN_TYPE
|
|
449
|
+
});
|
|
450
|
+
const coins = [...listed.objects ?? []].sort(
|
|
451
|
+
(a, b) => BigInt(b.balance) > BigInt(a.balance) ? 1 : -1
|
|
452
|
+
);
|
|
453
|
+
const picked = [];
|
|
454
|
+
let total = 0n;
|
|
455
|
+
for (const c of coins) {
|
|
456
|
+
if (BigInt(c.balance) <= 0n) continue;
|
|
457
|
+
picked.push(c);
|
|
458
|
+
total += BigInt(c.balance);
|
|
459
|
+
if (total >= amount) break;
|
|
460
|
+
}
|
|
461
|
+
if (total < amount || picked.length === 0) {
|
|
462
|
+
throw new Error(
|
|
463
|
+
`owner has insufficient SUI for funding (need ${amount}, have ${total})`
|
|
464
|
+
);
|
|
465
|
+
}
|
|
466
|
+
const primary = tx.objectRef({
|
|
467
|
+
objectId: picked[0].objectId,
|
|
468
|
+
version: picked[0].version,
|
|
469
|
+
digest: picked[0].digest
|
|
470
|
+
});
|
|
471
|
+
if (picked.length > 1) {
|
|
472
|
+
tx.mergeCoins(
|
|
473
|
+
primary,
|
|
474
|
+
picked.slice(1).map(
|
|
475
|
+
(c) => tx.objectRef({
|
|
476
|
+
objectId: c.objectId,
|
|
477
|
+
version: c.version,
|
|
478
|
+
digest: c.digest
|
|
479
|
+
})
|
|
480
|
+
)
|
|
481
|
+
);
|
|
482
|
+
}
|
|
483
|
+
const [split] = tx.splitCoins(primary, [amount]);
|
|
484
|
+
return split;
|
|
485
|
+
}
|
|
486
|
+
return {
|
|
487
|
+
packageId,
|
|
488
|
+
client,
|
|
489
|
+
network: `sui:${network}`,
|
|
490
|
+
async readPoolBalance(poolObjectId) {
|
|
491
|
+
const { object } = await client.core.getObject({
|
|
492
|
+
objectId: poolObjectId,
|
|
493
|
+
include: { json: true }
|
|
494
|
+
});
|
|
495
|
+
const json = object?.json;
|
|
496
|
+
if (!json || typeof json !== "object") {
|
|
497
|
+
throw new Error("pool object has no Move json contents");
|
|
498
|
+
}
|
|
499
|
+
const funds = json.funds;
|
|
500
|
+
if (typeof funds === "string" && /^\d+$/.test(funds)) return funds;
|
|
501
|
+
if (typeof funds === "number" && Number.isFinite(funds)) {
|
|
502
|
+
return String(Math.trunc(funds));
|
|
503
|
+
}
|
|
504
|
+
if (funds && typeof funds === "object") {
|
|
505
|
+
const v = typeof funds.fields?.value === "string" ? funds.fields.value : typeof funds.value === "string" ? funds.value : null;
|
|
506
|
+
if (v && /^\d+$/.test(v)) return v;
|
|
507
|
+
}
|
|
508
|
+
throw new Error("could not parse SharedPool.funds balance");
|
|
509
|
+
},
|
|
510
|
+
async buildCreatePoolKind(input) {
|
|
511
|
+
const tx = new Transaction2();
|
|
512
|
+
tx.setSender(input.owner);
|
|
513
|
+
const funding = await fundingCoin(
|
|
514
|
+
tx,
|
|
515
|
+
input.owner,
|
|
516
|
+
input.coinType,
|
|
517
|
+
BigInt(input.funding)
|
|
518
|
+
);
|
|
519
|
+
tx.moveCall({
|
|
520
|
+
target: target("create_pool"),
|
|
521
|
+
typeArguments: [input.coinType],
|
|
522
|
+
arguments: [funding]
|
|
523
|
+
});
|
|
524
|
+
return kindBytes(tx);
|
|
525
|
+
},
|
|
526
|
+
async buildTopUpKind(input) {
|
|
527
|
+
const tx = new Transaction2();
|
|
528
|
+
tx.setSender(input.owner);
|
|
529
|
+
const funding = await fundingCoin(
|
|
530
|
+
tx,
|
|
531
|
+
input.owner,
|
|
532
|
+
input.coinType,
|
|
533
|
+
BigInt(input.amount)
|
|
534
|
+
);
|
|
535
|
+
tx.moveCall({
|
|
536
|
+
target: target("top_up"),
|
|
537
|
+
typeArguments: [input.coinType],
|
|
538
|
+
arguments: [tx.object(input.poolObjectId), funding]
|
|
539
|
+
});
|
|
540
|
+
return kindBytes(tx);
|
|
541
|
+
},
|
|
542
|
+
async buildWithdrawKind(input) {
|
|
543
|
+
const tx = new Transaction2();
|
|
544
|
+
tx.setSender(input.owner);
|
|
545
|
+
tx.moveCall({
|
|
546
|
+
target: target("withdraw"),
|
|
547
|
+
typeArguments: [input.coinType],
|
|
548
|
+
arguments: [
|
|
549
|
+
tx.object(input.poolObjectId),
|
|
550
|
+
tx.pure.u64(BigInt(input.amount))
|
|
551
|
+
]
|
|
552
|
+
});
|
|
553
|
+
return kindBytes(tx);
|
|
554
|
+
},
|
|
555
|
+
async buildPausePoolKind(input) {
|
|
556
|
+
const tx = new Transaction2();
|
|
557
|
+
tx.setSender(input.owner);
|
|
558
|
+
tx.moveCall({
|
|
559
|
+
target: target("pause_pool"),
|
|
560
|
+
typeArguments: [input.coinType],
|
|
561
|
+
arguments: [tx.object(input.poolObjectId)]
|
|
562
|
+
});
|
|
563
|
+
return kindBytes(tx);
|
|
564
|
+
},
|
|
565
|
+
async buildResumePoolKind(input) {
|
|
566
|
+
const tx = new Transaction2();
|
|
567
|
+
tx.setSender(input.owner);
|
|
568
|
+
tx.moveCall({
|
|
569
|
+
target: target("resume_pool"),
|
|
570
|
+
typeArguments: [input.coinType],
|
|
571
|
+
arguments: [tx.object(input.poolObjectId)]
|
|
572
|
+
});
|
|
573
|
+
return kindBytes(tx);
|
|
574
|
+
},
|
|
575
|
+
async buildCreatePolicyKind(input) {
|
|
576
|
+
const tx = new Transaction2();
|
|
577
|
+
tx.setSender(input.owner);
|
|
578
|
+
const targetResults = input.targets.map((t) => {
|
|
579
|
+
const hash = targetHashBytes(t.targetHash);
|
|
580
|
+
return tx.moveCall({
|
|
581
|
+
target: target("make_target"),
|
|
582
|
+
arguments: [
|
|
583
|
+
tx.pure.vector("u8", Array.from(hash)),
|
|
584
|
+
tx.pure.address(t.recipient),
|
|
585
|
+
tx.pure.u64(BigInt(t.maxPerPayment))
|
|
586
|
+
]
|
|
587
|
+
});
|
|
588
|
+
});
|
|
589
|
+
const targetsVec = tx.makeMoveVec({
|
|
590
|
+
type: `${packageId}::${MODULE}::PolicyTarget`,
|
|
591
|
+
elements: targetResults
|
|
592
|
+
});
|
|
593
|
+
const expires = input.expiresAtMs === void 0 || input.expiresAtMs === null ? 0n : BigInt(input.expiresAtMs);
|
|
594
|
+
tx.moveCall({
|
|
595
|
+
target: target("create_policy"),
|
|
596
|
+
typeArguments: [input.coinType],
|
|
597
|
+
arguments: [
|
|
598
|
+
tx.object(input.poolObjectId),
|
|
599
|
+
targetsVec,
|
|
600
|
+
tx.pure.u64(BigInt(input.totalCap)),
|
|
601
|
+
tx.pure.u64(BigInt(input.maxPerPayment)),
|
|
602
|
+
tx.pure.u64(expires)
|
|
603
|
+
]
|
|
604
|
+
});
|
|
605
|
+
return kindBytes(tx);
|
|
606
|
+
},
|
|
607
|
+
async buildPausePolicyKind(input) {
|
|
608
|
+
const tx = new Transaction2();
|
|
609
|
+
tx.setSender(input.owner);
|
|
610
|
+
tx.moveCall({
|
|
611
|
+
target: target("pause_policy"),
|
|
612
|
+
typeArguments: [input.coinType],
|
|
613
|
+
arguments: [
|
|
614
|
+
tx.object(input.poolObjectId),
|
|
615
|
+
tx.pure.u64(BigInt(input.onChainPolicyId))
|
|
616
|
+
]
|
|
617
|
+
});
|
|
618
|
+
return kindBytes(tx);
|
|
619
|
+
},
|
|
620
|
+
async buildResumePolicyKind(input) {
|
|
621
|
+
const tx = new Transaction2();
|
|
622
|
+
tx.setSender(input.owner);
|
|
623
|
+
tx.moveCall({
|
|
624
|
+
target: target("resume_policy"),
|
|
625
|
+
typeArguments: [input.coinType],
|
|
626
|
+
arguments: [
|
|
627
|
+
tx.object(input.poolObjectId),
|
|
628
|
+
tx.pure.u64(BigInt(input.onChainPolicyId))
|
|
629
|
+
]
|
|
630
|
+
});
|
|
631
|
+
return kindBytes(tx);
|
|
632
|
+
},
|
|
633
|
+
async buildRevokePolicyKind(input) {
|
|
634
|
+
const tx = new Transaction2();
|
|
635
|
+
tx.setSender(input.owner);
|
|
636
|
+
tx.moveCall({
|
|
637
|
+
target: target("revoke_policy"),
|
|
638
|
+
typeArguments: [input.coinType],
|
|
639
|
+
arguments: [
|
|
640
|
+
tx.object(input.poolObjectId),
|
|
641
|
+
tx.pure.u64(BigInt(input.onChainPolicyId))
|
|
642
|
+
]
|
|
643
|
+
});
|
|
644
|
+
return kindBytes(tx);
|
|
645
|
+
},
|
|
646
|
+
async buildCreateGrantKind(input) {
|
|
647
|
+
if (!input.onChainPolicyIds.length) {
|
|
648
|
+
throw new Error("create_grant requires at least one on-chain policy id");
|
|
649
|
+
}
|
|
650
|
+
if (!input.maxPerPayment || BigInt(input.maxPerPayment) <= 0n) {
|
|
651
|
+
throw new Error("create_grant requires a non-zero max_per_payment");
|
|
652
|
+
}
|
|
653
|
+
if (!input.sessionCap || BigInt(input.sessionCap) <= 0n) {
|
|
654
|
+
throw new Error("create_grant requires a non-zero session_cap");
|
|
655
|
+
}
|
|
656
|
+
const tx = new Transaction2();
|
|
657
|
+
tx.setSender(input.owner);
|
|
658
|
+
const expires = input.expiresAtMs === void 0 || input.expiresAtMs === null ? 0n : BigInt(input.expiresAtMs);
|
|
659
|
+
tx.moveCall({
|
|
660
|
+
target: target("create_grant"),
|
|
661
|
+
typeArguments: [input.coinType],
|
|
662
|
+
arguments: [
|
|
663
|
+
tx.object(input.poolObjectId),
|
|
664
|
+
tx.pure.address(input.delegate),
|
|
665
|
+
tx.pure.u64(BigInt(input.sessionCap)),
|
|
666
|
+
tx.pure.u64(BigInt(input.maxPerPayment)),
|
|
667
|
+
tx.pure.u64(expires),
|
|
668
|
+
tx.pure.vector(
|
|
669
|
+
"u64",
|
|
670
|
+
input.onChainPolicyIds.map((id) => BigInt(id))
|
|
671
|
+
)
|
|
672
|
+
]
|
|
673
|
+
});
|
|
674
|
+
return kindBytes(tx);
|
|
675
|
+
},
|
|
676
|
+
async buildCreateSpendGrantKind(input) {
|
|
677
|
+
if (!input.onChainPolicyIds.length) {
|
|
678
|
+
throw new Error(
|
|
679
|
+
"create_spend_grant requires at least one on-chain policy id"
|
|
680
|
+
);
|
|
681
|
+
}
|
|
682
|
+
if (input.sessionCap !== null && input.sessionCap <= 0n) {
|
|
683
|
+
throw new Error(
|
|
684
|
+
"create_spend_grant session_cap must be null or > 0"
|
|
685
|
+
);
|
|
686
|
+
}
|
|
687
|
+
if (input.maxPerPayment !== null && input.maxPerPayment <= 0n) {
|
|
688
|
+
throw new Error(
|
|
689
|
+
"create_spend_grant max_per_payment must be null or > 0"
|
|
690
|
+
);
|
|
691
|
+
}
|
|
692
|
+
const tx = new Transaction2();
|
|
693
|
+
tx.setSender(input.owner);
|
|
694
|
+
const expires = input.expiresAtMs === void 0 || input.expiresAtMs === null ? 0n : BigInt(input.expiresAtMs);
|
|
695
|
+
tx.moveCall({
|
|
696
|
+
target: target("create_spend_grant"),
|
|
697
|
+
typeArguments: [input.coinType],
|
|
698
|
+
arguments: [
|
|
699
|
+
tx.object(input.poolObjectId),
|
|
700
|
+
tx.pure.address(input.delegate),
|
|
701
|
+
tx.pure.option("u64", input.sessionCap),
|
|
702
|
+
tx.pure.option("u64", input.maxPerPayment),
|
|
703
|
+
tx.pure.u64(expires),
|
|
704
|
+
tx.pure.vector(
|
|
705
|
+
"u64",
|
|
706
|
+
input.onChainPolicyIds.map((id) => BigInt(id))
|
|
707
|
+
)
|
|
708
|
+
]
|
|
709
|
+
});
|
|
710
|
+
return kindBytes(tx);
|
|
711
|
+
},
|
|
712
|
+
async buildCreateOpenGrantKind(input) {
|
|
713
|
+
if (input.sessionCap == null || input.sessionCap <= 0n) {
|
|
714
|
+
throw new Error("create_open_grant session_cap must be > 0");
|
|
715
|
+
}
|
|
716
|
+
if (input.maxPerPayment == null || input.maxPerPayment <= 0n) {
|
|
717
|
+
throw new Error("create_open_grant max_per_payment must be > 0");
|
|
718
|
+
}
|
|
719
|
+
const expires = BigInt(input.expiresAtMs);
|
|
720
|
+
if (expires <= 0n) {
|
|
721
|
+
throw new Error(
|
|
722
|
+
"create_open_grant expires_at_ms must be a finite future expiry (> 0)"
|
|
723
|
+
);
|
|
724
|
+
}
|
|
725
|
+
const tx = new Transaction2();
|
|
726
|
+
tx.setSender(input.owner);
|
|
727
|
+
tx.moveCall({
|
|
728
|
+
target: target("create_open_grant"),
|
|
729
|
+
typeArguments: [input.coinType],
|
|
730
|
+
arguments: [
|
|
731
|
+
tx.object(input.poolObjectId),
|
|
732
|
+
tx.pure.address(input.delegate),
|
|
733
|
+
tx.pure.u64(input.sessionCap),
|
|
734
|
+
tx.pure.u64(input.maxPerPayment),
|
|
735
|
+
tx.pure.u64(expires),
|
|
736
|
+
tx.object(input.clockId ?? SUI_CLOCK)
|
|
737
|
+
]
|
|
738
|
+
});
|
|
739
|
+
return kindBytes(tx);
|
|
740
|
+
},
|
|
741
|
+
async buildPayOpenKind(input) {
|
|
742
|
+
if (!input.termsHash || input.termsHash.length === 0) {
|
|
743
|
+
throw new Error("termsHash must be non-empty for shared_pool::pay_open");
|
|
744
|
+
}
|
|
745
|
+
if (!input.paymentIdHash || input.paymentIdHash.length === 0) {
|
|
746
|
+
throw new Error(
|
|
747
|
+
"paymentIdHash must be non-empty for shared_pool::pay_open"
|
|
748
|
+
);
|
|
749
|
+
}
|
|
750
|
+
if (!input.recipient || isZeroAddress(input.recipient)) {
|
|
751
|
+
throw new Error("pay_open recipient must be a non-zero address");
|
|
752
|
+
}
|
|
753
|
+
if (!input.amount || BigInt(input.amount) <= 0n) {
|
|
754
|
+
throw new Error("pay_open amount must be > 0");
|
|
755
|
+
}
|
|
756
|
+
const hash = targetHashBytes(input.targetHash);
|
|
757
|
+
const tx = new Transaction2();
|
|
758
|
+
tx.setSender(input.delegate);
|
|
759
|
+
tx.moveCall({
|
|
760
|
+
target: target("pay_open"),
|
|
761
|
+
typeArguments: [input.coinType],
|
|
762
|
+
arguments: [
|
|
763
|
+
tx.object(input.poolObjectId),
|
|
764
|
+
tx.object(input.grantObjectId),
|
|
765
|
+
tx.pure.address(input.recipient),
|
|
766
|
+
tx.pure.u64(BigInt(input.amount)),
|
|
767
|
+
tx.pure.vector("u8", Array.from(input.paymentIdHash)),
|
|
768
|
+
tx.pure.vector("u8", Array.from(input.termsHash)),
|
|
769
|
+
tx.pure.vector("u8", Array.from(hash)),
|
|
770
|
+
tx.object(input.clockId ?? SUI_CLOCK)
|
|
771
|
+
]
|
|
772
|
+
});
|
|
773
|
+
return kindBytes(tx);
|
|
774
|
+
},
|
|
775
|
+
async buildPauseGrantKind(input) {
|
|
776
|
+
const tx = new Transaction2();
|
|
777
|
+
tx.setSender(input.owner);
|
|
778
|
+
tx.moveCall({
|
|
779
|
+
target: target("pause_grant"),
|
|
780
|
+
arguments: [tx.object(input.grantObjectId)]
|
|
781
|
+
});
|
|
782
|
+
return kindBytes(tx);
|
|
783
|
+
},
|
|
784
|
+
async buildResumeGrantKind(input) {
|
|
785
|
+
const tx = new Transaction2();
|
|
786
|
+
tx.setSender(input.owner);
|
|
787
|
+
tx.moveCall({
|
|
788
|
+
target: target("resume_grant"),
|
|
789
|
+
arguments: [tx.object(input.grantObjectId)]
|
|
790
|
+
});
|
|
791
|
+
return kindBytes(tx);
|
|
792
|
+
},
|
|
793
|
+
async buildRevokeGrantKind(input) {
|
|
794
|
+
const tx = new Transaction2();
|
|
795
|
+
tx.setSender(input.owner);
|
|
796
|
+
tx.moveCall({
|
|
797
|
+
target: target("revoke_grant"),
|
|
798
|
+
arguments: [tx.object(input.grantObjectId)]
|
|
799
|
+
});
|
|
800
|
+
return kindBytes(tx);
|
|
801
|
+
},
|
|
802
|
+
async buildRevokeOpenGrantKind(input) {
|
|
803
|
+
const tx = new Transaction2();
|
|
804
|
+
tx.setSender(input.owner);
|
|
805
|
+
tx.moveCall({
|
|
806
|
+
target: target("revoke_open_grant"),
|
|
807
|
+
typeArguments: [input.coinType],
|
|
808
|
+
arguments: [tx.object(input.grantObjectId)]
|
|
809
|
+
});
|
|
810
|
+
return kindBytes(tx);
|
|
811
|
+
},
|
|
812
|
+
async buildTightenGrantKind(input) {
|
|
813
|
+
const tx = new Transaction2();
|
|
814
|
+
tx.setSender(input.owner);
|
|
815
|
+
const expires = input.newExpiresAtMs === void 0 || input.newExpiresAtMs === null ? 0n : BigInt(input.newExpiresAtMs);
|
|
816
|
+
tx.moveCall({
|
|
817
|
+
target: target("tighten_grant"),
|
|
818
|
+
arguments: [
|
|
819
|
+
tx.object(input.grantObjectId),
|
|
820
|
+
tx.pure.u64(BigInt(input.newSessionCap)),
|
|
821
|
+
tx.pure.u64(expires)
|
|
822
|
+
]
|
|
823
|
+
});
|
|
824
|
+
return kindBytes(tx);
|
|
825
|
+
},
|
|
826
|
+
async buildPayKind(input) {
|
|
827
|
+
if (!input.termsHash || input.termsHash.length === 0) {
|
|
828
|
+
throw new Error("termsHash must be non-empty for shared_pool::pay");
|
|
829
|
+
}
|
|
830
|
+
if (!input.paymentIdHash || input.paymentIdHash.length === 0) {
|
|
831
|
+
throw new Error("paymentIdHash must be non-empty for shared_pool::pay");
|
|
832
|
+
}
|
|
833
|
+
const hash = targetHashBytes(input.targetHash);
|
|
834
|
+
const tx = new Transaction2();
|
|
835
|
+
tx.setSender(input.delegate);
|
|
836
|
+
tx.moveCall({
|
|
837
|
+
target: target("pay"),
|
|
838
|
+
typeArguments: [input.coinType],
|
|
839
|
+
arguments: [
|
|
840
|
+
tx.object(input.poolObjectId),
|
|
841
|
+
tx.object(input.grantObjectId),
|
|
842
|
+
tx.pure.u64(BigInt(input.policyId)),
|
|
843
|
+
tx.pure.vector("u8", Array.from(hash)),
|
|
844
|
+
tx.pure.u64(BigInt(input.amount)),
|
|
845
|
+
tx.pure.vector("u8", Array.from(input.paymentIdHash)),
|
|
846
|
+
tx.pure.vector("u8", Array.from(input.termsHash)),
|
|
847
|
+
tx.object(input.clockId ?? SUI_CLOCK)
|
|
848
|
+
]
|
|
849
|
+
});
|
|
850
|
+
return kindBytes(tx);
|
|
851
|
+
}
|
|
852
|
+
};
|
|
853
|
+
}
|
|
854
|
+
var SUI_NETWORKS = ["testnet", "devnet", "localnet", "mainnet"];
|
|
855
|
+
function toGrpcNetwork(network) {
|
|
856
|
+
const short = (network ?? "testnet").replace(/^sui:/, "");
|
|
857
|
+
if (!SUI_NETWORKS.includes(short)) {
|
|
858
|
+
throw new Error(
|
|
859
|
+
`unsupported Sui network "${network}" (expected one of ${SUI_NETWORKS.join(", ")})`
|
|
860
|
+
);
|
|
861
|
+
}
|
|
862
|
+
return short;
|
|
863
|
+
}
|
|
864
|
+
function createSharedPoolFinalityReader(options) {
|
|
865
|
+
const network = toGrpcNetwork(options.network);
|
|
866
|
+
const client = options.client ?? new SuiGrpcClient2({
|
|
867
|
+
network,
|
|
868
|
+
baseUrl: options.url ?? `https://fullnode.${network}.sui.io:443`
|
|
869
|
+
});
|
|
870
|
+
return {
|
|
871
|
+
async getFinalizedTx(digest) {
|
|
872
|
+
const result = await fetchGrpcTransaction(client, digest, {
|
|
873
|
+
events: true,
|
|
874
|
+
effects: true,
|
|
875
|
+
transaction: true
|
|
876
|
+
});
|
|
877
|
+
if (!result) return null;
|
|
878
|
+
const finalized = result.Transaction ?? result.FailedTransaction;
|
|
879
|
+
if (!finalized) {
|
|
880
|
+
throw new Error("finality reader: response has no Transaction variant");
|
|
881
|
+
}
|
|
882
|
+
if (finalized.digest != null && String(finalized.digest) !== digest) {
|
|
883
|
+
throw new Error("finality reader: response digest does not match request");
|
|
884
|
+
}
|
|
885
|
+
const success = finalized.status?.success;
|
|
886
|
+
if (success !== true && success !== false) {
|
|
887
|
+
throw new Error("finality reader: response has no success status");
|
|
888
|
+
}
|
|
889
|
+
if (success === false) {
|
|
890
|
+
return {
|
|
891
|
+
status: "failed",
|
|
892
|
+
txDigest: digest,
|
|
893
|
+
reason: finalized.status.error?.message ?? "shared_pool transaction failed on chain"
|
|
894
|
+
};
|
|
895
|
+
}
|
|
896
|
+
const sender = finalized.transaction?.sender ? normalizeSuiAddress2(String(finalized.transaction.sender)) : void 0;
|
|
897
|
+
const moveCalls = (finalized.transaction?.commands ?? []).map((c) => {
|
|
898
|
+
const mc = c && typeof c === "object" && "MoveCall" in c ? c.MoveCall : void 0;
|
|
899
|
+
return mc ? `${normalizeSuiAddress2(mc.package)}::${mc.module}::${mc.function}` : null;
|
|
900
|
+
}).filter((t) => t !== null);
|
|
901
|
+
const objectIds = (finalized.effects?.changedObjects ?? []).filter((o) => o.idOperation === "Created").map((o) => normalizeSuiAddress2(o.objectId));
|
|
902
|
+
const events = (finalized.events ?? []).map((e) => ({
|
|
903
|
+
// Spread the on-chain payload FIRST so a field named `type`/`kind` in
|
|
904
|
+
// the event json can never shadow the derived event type - reconcile
|
|
905
|
+
// matches on `type`, so a shadowed value would misroute the event.
|
|
906
|
+
...e.json ?? {},
|
|
907
|
+
type: e.eventType,
|
|
908
|
+
kind: e.eventType.split("::").pop() ?? e.eventType
|
|
909
|
+
}));
|
|
910
|
+
return {
|
|
911
|
+
status: "success",
|
|
912
|
+
txDigest: digest,
|
|
913
|
+
sender,
|
|
914
|
+
moveCalls,
|
|
915
|
+
objectIds,
|
|
916
|
+
events
|
|
917
|
+
};
|
|
918
|
+
}
|
|
919
|
+
};
|
|
920
|
+
}
|
|
921
|
+
|
|
922
|
+
// ../../src/protocol/challenge.ts
|
|
923
|
+
import { createHmac, randomBytes, timingSafeEqual } from "crypto";
|
|
924
|
+
var SIGNED_FIELDS_V1 = [
|
|
925
|
+
"challengeId",
|
|
926
|
+
"resource",
|
|
927
|
+
"amount",
|
|
928
|
+
"asset",
|
|
929
|
+
"decimals",
|
|
930
|
+
"network",
|
|
931
|
+
"payTo",
|
|
932
|
+
"packageId",
|
|
933
|
+
"issuedAt",
|
|
934
|
+
"expiresAt"
|
|
935
|
+
];
|
|
936
|
+
var SIGNED_FIELDS_V2 = [
|
|
937
|
+
...SIGNED_FIELDS_V1,
|
|
938
|
+
"digestVersion",
|
|
939
|
+
"settlementRail",
|
|
940
|
+
"serviceId",
|
|
941
|
+
"serviceRevision",
|
|
942
|
+
"targetHash",
|
|
943
|
+
"authorizationEpoch",
|
|
944
|
+
"method",
|
|
945
|
+
"path",
|
|
946
|
+
"netAmount",
|
|
947
|
+
"platformFee",
|
|
948
|
+
// Offer-pinned sponsor gas for shared_pool x402 (omit-when-absent).
|
|
949
|
+
// Appended so pre-existing v2 digests without these fields stay byte-identical.
|
|
950
|
+
"sponsorGasOwner",
|
|
951
|
+
"sponsorGasBudget",
|
|
952
|
+
"sponsorGasPrice",
|
|
953
|
+
"sponsorGasPayment"
|
|
954
|
+
];
|
|
955
|
+
var SIGNED_FIELDS_V3 = [
|
|
956
|
+
...SIGNED_FIELDS_V2,
|
|
957
|
+
"bodyHash",
|
|
958
|
+
"contentType",
|
|
959
|
+
"intent",
|
|
960
|
+
"periodMs",
|
|
961
|
+
"periodCount",
|
|
962
|
+
"refundable",
|
|
963
|
+
"refundWindowMs",
|
|
964
|
+
"decideWindowMs",
|
|
965
|
+
// payment_key identity - omit-when-absent so non-refundable digests stay
|
|
966
|
+
// byte-identical; when present they are gateway-authenticated under HMAC.
|
|
967
|
+
"chainId",
|
|
968
|
+
"paymentSalt",
|
|
969
|
+
"refundTo",
|
|
970
|
+
"settlementRef"
|
|
971
|
+
];
|
|
972
|
+
var BODY_HASH_RE = /^[A-Za-z0-9_-]{43}$/;
|
|
973
|
+
var CONTENT_TYPE_RE = /^[a-z0-9!#$%&'*+.^_`|~-]+\/[a-z0-9!#$%&'*+.^_`|~-]+(?:; ?[a-z0-9!#$%&'*+.^_`|~-]+=(?:[a-zA-Z0-9!#$%&'*+.^_`|~-]+|"[^"\n\r]*"))*$/;
|
|
974
|
+
function isBodyHash(value) {
|
|
975
|
+
return typeof value === "string" && BODY_HASH_RE.test(value);
|
|
976
|
+
}
|
|
977
|
+
function isOfferContentType(value) {
|
|
978
|
+
return typeof value === "string" && value.length <= 255 && CONTENT_TYPE_RE.test(value);
|
|
979
|
+
}
|
|
980
|
+
|
|
981
|
+
// ../../src/protocol/request-digest.ts
|
|
982
|
+
import { createHash as createHash2 } from "crypto";
|
|
983
|
+
function hashRequestBody(body) {
|
|
984
|
+
const bytes = body == null ? new Uint8Array() : typeof body === "string" ? new TextEncoder().encode(body) : body;
|
|
985
|
+
return createHash2("sha256").update(bytes).digest("base64url");
|
|
986
|
+
}
|
|
987
|
+
|
|
988
|
+
// ../../src/protocol/paid-http-request.ts
|
|
989
|
+
var MAX_PAID_BODY_BYTES = 65536;
|
|
990
|
+
var MAX_PAID_RESPONSE_BYTES = 16 * 1024 * 1024;
|
|
991
|
+
var MAX_UNMANAGED_RESPONSE_BYTES = 256 * 1024 * 1024;
|
|
992
|
+
var MCP_JSON_CONTENT_TYPE = "application/json";
|
|
993
|
+
var EMPTY_BODY = new Uint8Array();
|
|
994
|
+
function assertPaidUrl(url) {
|
|
995
|
+
if (typeof url !== "string" || url.trim() === "") {
|
|
996
|
+
throw new Error("paid request url is required");
|
|
997
|
+
}
|
|
998
|
+
let parsed;
|
|
999
|
+
try {
|
|
1000
|
+
parsed = new URL(url);
|
|
1001
|
+
} catch {
|
|
1002
|
+
throw new Error(`paid request url must be an absolute URL, got ${url}`);
|
|
1003
|
+
}
|
|
1004
|
+
if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
|
|
1005
|
+
throw new Error("paid request url must be an http(s) URL");
|
|
1006
|
+
}
|
|
1007
|
+
return url;
|
|
1008
|
+
}
|
|
1009
|
+
function assertPaidMethod(method) {
|
|
1010
|
+
if (method === void 0 || method === null) return "GET";
|
|
1011
|
+
if (typeof method !== "string") {
|
|
1012
|
+
throw new Error("paid request method must be a string");
|
|
1013
|
+
}
|
|
1014
|
+
const upper = method.trim().toUpperCase();
|
|
1015
|
+
if (upper === "GET" || upper === "POST") return upper;
|
|
1016
|
+
throw new Error(`paid request method must be GET or POST, got ${method}`);
|
|
1017
|
+
}
|
|
1018
|
+
function normalizeContentType(value) {
|
|
1019
|
+
const normalized = value.trim().toLowerCase().replace(/\s*;\s*/g, "; ").replace(/\s*=\s*/g, "=");
|
|
1020
|
+
if (!isOfferContentType(normalized)) {
|
|
1021
|
+
throw new Error(`paid request content type is malformed: ${value}`);
|
|
1022
|
+
}
|
|
1023
|
+
return normalized;
|
|
1024
|
+
}
|
|
1025
|
+
function toBytes(body) {
|
|
1026
|
+
return typeof body === "string" ? new TextEncoder().encode(body) : body;
|
|
1027
|
+
}
|
|
1028
|
+
function assertWithinCap(bytes) {
|
|
1029
|
+
if (bytes.byteLength > MAX_PAID_BODY_BYTES) {
|
|
1030
|
+
throw new Error(
|
|
1031
|
+
`paid request body is ${bytes.byteLength} bytes; too large, max is ${MAX_PAID_BODY_BYTES}`
|
|
1032
|
+
);
|
|
1033
|
+
}
|
|
1034
|
+
return bytes;
|
|
1035
|
+
}
|
|
1036
|
+
function freeze(request) {
|
|
1037
|
+
return Object.freeze(request);
|
|
1038
|
+
}
|
|
1039
|
+
function normalizePaidHttpRequest(input) {
|
|
1040
|
+
const url = assertPaidUrl(input.url);
|
|
1041
|
+
const method = assertPaidMethod(input.method);
|
|
1042
|
+
if (input.body !== void 0 && input.body !== null && typeof input.body !== "string" && !(input.body instanceof Uint8Array)) {
|
|
1043
|
+
throw new Error("paid request body must be a string, Uint8Array, or null");
|
|
1044
|
+
}
|
|
1045
|
+
if (method === "GET") {
|
|
1046
|
+
const hasBody = input.body !== void 0 && input.body !== null && toBytes(input.body).byteLength > 0;
|
|
1047
|
+
if (hasBody) {
|
|
1048
|
+
throw new Error("a paid GET request must not carry a body");
|
|
1049
|
+
}
|
|
1050
|
+
if (input.contentType !== void 0 && input.contentType !== null) {
|
|
1051
|
+
throw new Error("a paid GET request must not declare a content type");
|
|
1052
|
+
}
|
|
1053
|
+
return freeze({
|
|
1054
|
+
url,
|
|
1055
|
+
method,
|
|
1056
|
+
body: EMPTY_BODY,
|
|
1057
|
+
contentType: null,
|
|
1058
|
+
bodyHash: hashRequestBody(null)
|
|
1059
|
+
});
|
|
1060
|
+
}
|
|
1061
|
+
const body = assertWithinCap(
|
|
1062
|
+
input.body === void 0 || input.body === null ? EMPTY_BODY : toBytes(input.body)
|
|
1063
|
+
);
|
|
1064
|
+
const contentType = input.contentType === void 0 || input.contentType === null ? null : normalizeContentType(input.contentType);
|
|
1065
|
+
return freeze({
|
|
1066
|
+
url,
|
|
1067
|
+
method,
|
|
1068
|
+
body,
|
|
1069
|
+
contentType,
|
|
1070
|
+
bodyHash: hashRequestBody(body)
|
|
1071
|
+
});
|
|
1072
|
+
}
|
|
1073
|
+
function canonicalJson(value, seen = /* @__PURE__ */ new Set()) {
|
|
1074
|
+
if (value === null) return "null";
|
|
1075
|
+
const type = typeof value;
|
|
1076
|
+
if (type === "boolean") return value ? "true" : "false";
|
|
1077
|
+
if (type === "number") {
|
|
1078
|
+
if (!Number.isFinite(value)) {
|
|
1079
|
+
throw new Error("json value must be finite; NaN and Infinity are not payable");
|
|
1080
|
+
}
|
|
1081
|
+
return JSON.stringify(value);
|
|
1082
|
+
}
|
|
1083
|
+
if (type === "string") return JSON.stringify(value);
|
|
1084
|
+
if (type === "bigint") {
|
|
1085
|
+
throw new Error("json value must not contain a bigint");
|
|
1086
|
+
}
|
|
1087
|
+
if (type === "undefined" || type === "function" || type === "symbol") {
|
|
1088
|
+
throw new Error(`json value must not contain ${type}`);
|
|
1089
|
+
}
|
|
1090
|
+
const object = value;
|
|
1091
|
+
if (seen.has(object)) {
|
|
1092
|
+
throw new Error("json value must not be cyclic");
|
|
1093
|
+
}
|
|
1094
|
+
seen.add(object);
|
|
1095
|
+
try {
|
|
1096
|
+
if (Array.isArray(object)) {
|
|
1097
|
+
return `[${object.map((item) => canonicalJson(item, seen)).join(",")}]`;
|
|
1098
|
+
}
|
|
1099
|
+
const entries = Object.entries(object).sort(
|
|
1100
|
+
([a], [b]) => a < b ? -1 : a > b ? 1 : 0
|
|
1101
|
+
);
|
|
1102
|
+
return `{${entries.map(([key, item]) => `${JSON.stringify(key)}:${canonicalJson(item, seen)}`).join(",")}}`;
|
|
1103
|
+
} finally {
|
|
1104
|
+
seen.delete(object);
|
|
1105
|
+
}
|
|
1106
|
+
}
|
|
1107
|
+
function isPlainJsonObject(value) {
|
|
1108
|
+
return typeof value === "object" && value !== null && !Array.isArray(value) && Object.getPrototypeOf(value) !== null && (Object.getPrototypeOf(value) === Object.prototype || Object.getPrototypeOf(Object.getPrototypeOf(value)) === null);
|
|
1109
|
+
}
|
|
1110
|
+
function normalizeMcpPaidHttpRequest(input) {
|
|
1111
|
+
const method = assertPaidMethod(input.method);
|
|
1112
|
+
if (method === "GET") {
|
|
1113
|
+
if (input.json !== void 0) {
|
|
1114
|
+
throw new Error("a paid GET request must not carry json");
|
|
1115
|
+
}
|
|
1116
|
+
return normalizePaidHttpRequest({ url: input.url, method });
|
|
1117
|
+
}
|
|
1118
|
+
if (!isPlainJsonObject(input.json)) {
|
|
1119
|
+
throw new Error("paid POST json must be a JSON object");
|
|
1120
|
+
}
|
|
1121
|
+
return normalizePaidHttpRequest({
|
|
1122
|
+
url: input.url,
|
|
1123
|
+
method,
|
|
1124
|
+
body: canonicalJson(input.json),
|
|
1125
|
+
contentType: MCP_JSON_CONTENT_TYPE
|
|
1126
|
+
});
|
|
1127
|
+
}
|
|
1128
|
+
|
|
1129
|
+
// src/auth.ts
|
|
1130
|
+
import { homedir } from "os";
|
|
1131
|
+
import { join } from "path";
|
|
1132
|
+
import {
|
|
1133
|
+
mkdirSync,
|
|
1134
|
+
readFileSync,
|
|
1135
|
+
renameSync,
|
|
1136
|
+
chmodSync,
|
|
1137
|
+
unlinkSync,
|
|
1138
|
+
existsSync,
|
|
1139
|
+
fsyncSync,
|
|
1140
|
+
openSync,
|
|
1141
|
+
closeSync,
|
|
1142
|
+
writeSync
|
|
1143
|
+
} from "fs";
|
|
1144
|
+
import { Ed25519Keypair } from "@mysten/sui/keypairs/ed25519";
|
|
1145
|
+
import { fromBase64 as fromBase642 } from "@mysten/sui/utils";
|
|
1146
|
+
import { bytesToHex } from "@noble/hashes/utils";
|
|
1147
|
+
function credsDir() {
|
|
1148
|
+
return join(homedir(), ".suipay");
|
|
1149
|
+
}
|
|
1150
|
+
function credsPath() {
|
|
1151
|
+
return join(credsDir(), "credentials.json");
|
|
1152
|
+
}
|
|
1153
|
+
var HEX64 = /^[0-9a-fA-F]{64}$/;
|
|
1154
|
+
var ADDR = /^0x[0-9a-fA-F]{64}$/;
|
|
1155
|
+
function isValid(obj) {
|
|
1156
|
+
if (!obj || typeof obj !== "object") return false;
|
|
1157
|
+
const c = obj;
|
|
1158
|
+
return typeof c.delegatePrivateKey === "string" && HEX64.test(c.delegatePrivateKey) && typeof c.delegatePublicKeyHex === "string" && HEX64.test(c.delegatePublicKeyHex) && typeof c.delegateAddress === "string" && ADDR.test(c.delegateAddress) && typeof c.ownerAddress === "string" && ADDR.test(c.ownerAddress) && typeof c.allowanceId === "string" && ADDR.test(c.allowanceId) && typeof c.packageId === "string" && ADDR.test(c.packageId) && typeof c.recipient === "string" && ADDR.test(c.recipient) && typeof c.coinType === "string" && c.coinType.length > 0 && typeof c.gatewayUrl === "string" && typeof c.network === "string" && typeof c.maxPerPayment === "string" && /^\d+$/.test(c.maxPerPayment) && typeof c.createdAt === "string" && c.version === 1;
|
|
1159
|
+
}
|
|
1160
|
+
function loadCreds() {
|
|
1161
|
+
const path = credsPath();
|
|
1162
|
+
if (!existsSync(path)) return null;
|
|
1163
|
+
let parsed;
|
|
1164
|
+
try {
|
|
1165
|
+
parsed = JSON.parse(readFileSync(path, "utf8"));
|
|
1166
|
+
} catch {
|
|
1167
|
+
throw new Error(`credentials file at ${path} is not valid JSON - refusing to continue`);
|
|
1168
|
+
}
|
|
1169
|
+
if (!isValid(parsed)) {
|
|
1170
|
+
throw new Error(`credentials file at ${path} failed schema validation - refusing to continue`);
|
|
1171
|
+
}
|
|
1172
|
+
return parsed;
|
|
1173
|
+
}
|
|
1174
|
+
function clearCreds() {
|
|
1175
|
+
const path = credsPath();
|
|
1176
|
+
try {
|
|
1177
|
+
unlinkSync(path);
|
|
1178
|
+
} catch (error2) {
|
|
1179
|
+
if (error2.code === "ENOENT") return;
|
|
1180
|
+
throw new Error(`could not remove credentials at ${path}`);
|
|
1181
|
+
}
|
|
1182
|
+
}
|
|
1183
|
+
function walletFromSeedHex(seedHex) {
|
|
1184
|
+
const kp = Ed25519Keypair.fromSecretKey(Uint8Array.from(Buffer.from(seedHex, "hex")));
|
|
1185
|
+
return {
|
|
1186
|
+
address: kp.getPublicKey().toSuiAddress(),
|
|
1187
|
+
async signTransaction(txBytes) {
|
|
1188
|
+
const { signature } = await kp.signTransaction(fromBase642(txBytes));
|
|
1189
|
+
return signature;
|
|
1190
|
+
}
|
|
1191
|
+
};
|
|
1192
|
+
}
|
|
1193
|
+
function walletFromEnvSecret(raw) {
|
|
1194
|
+
const key = raw.trim();
|
|
1195
|
+
if (!key) throw new Error("SUI_SECRET_KEY is set but empty");
|
|
1196
|
+
let kp;
|
|
1197
|
+
try {
|
|
1198
|
+
kp = /^(0x)?[0-9a-fA-F]{64}$/.test(key) ? Ed25519Keypair.fromSecretKey(Uint8Array.from(Buffer.from(key.replace(/^0x/, ""), "hex"))) : Ed25519Keypair.fromSecretKey(key);
|
|
1199
|
+
} catch {
|
|
1200
|
+
throw new Error("SUI_SECRET_KEY is malformed (expected 64-hex or suiprivkey1\u2026 Ed25519 secret)");
|
|
1201
|
+
}
|
|
1202
|
+
return {
|
|
1203
|
+
address: kp.getPublicKey().toSuiAddress(),
|
|
1204
|
+
async signTransaction(txBytes) {
|
|
1205
|
+
const { signature } = await kp.signTransaction(fromBase642(txBytes));
|
|
1206
|
+
return signature;
|
|
1207
|
+
}
|
|
1208
|
+
};
|
|
1209
|
+
}
|
|
1210
|
+
function loadPaymentProfile() {
|
|
1211
|
+
const creds = loadCreds();
|
|
1212
|
+
if (!creds) return null;
|
|
1213
|
+
const seed = Uint8Array.from(Buffer.from(creds.delegatePrivateKey, "hex"));
|
|
1214
|
+
const kp = Ed25519Keypair.fromSecretKey(seed);
|
|
1215
|
+
const pubHex = bytesToHex(kp.getPublicKey().toRawBytes());
|
|
1216
|
+
if (pubHex !== creds.delegatePublicKeyHex.toLowerCase()) {
|
|
1217
|
+
throw new Error("credentials file tampered: stored public key does not match the private key");
|
|
1218
|
+
}
|
|
1219
|
+
if (kp.getPublicKey().toSuiAddress() !== creds.delegateAddress) {
|
|
1220
|
+
throw new Error("credentials file tampered: stored delegate address does not match the private key");
|
|
1221
|
+
}
|
|
1222
|
+
const env = process.env.SUI_SECRET_KEY;
|
|
1223
|
+
if (env !== void 0) {
|
|
1224
|
+
const wallet = walletFromEnvSecret(env);
|
|
1225
|
+
if (wallet.address !== creds.delegateAddress) {
|
|
1226
|
+
throw new Error("SUI_SECRET_KEY does not match the stored delegate address - refusing to sign");
|
|
1227
|
+
}
|
|
1228
|
+
return { creds, wallet, source: "secret-key" };
|
|
1229
|
+
}
|
|
1230
|
+
return { creds, wallet: walletFromSeedHex(creds.delegatePrivateKey), source: "credentials" };
|
|
1231
|
+
}
|
|
1232
|
+
|
|
1233
|
+
// src/shared-pool-pay.ts
|
|
1234
|
+
import { setTimeout as delay2 } from "timers/promises";
|
|
1235
|
+
import { Transaction as Transaction4 } from "@mysten/sui/transactions";
|
|
1236
|
+
import {
|
|
1237
|
+
fromBase64 as fromBase644,
|
|
1238
|
+
normalizeStructTag as normalizeStructTag5,
|
|
1239
|
+
normalizeSuiAddress as normalizeSuiAddress4,
|
|
1240
|
+
toBase64 as toBase643
|
|
1241
|
+
} from "@mysten/sui/utils";
|
|
1242
|
+
import { Ed25519Keypair as Ed25519Keypair2 } from "@mysten/sui/keypairs/ed25519";
|
|
1243
|
+
|
|
1244
|
+
// ../../src/policy/grant-target.ts
|
|
1245
|
+
function normalizeHash(hex) {
|
|
1246
|
+
return hex.replace(/^0x/i, "").toLowerCase();
|
|
1247
|
+
}
|
|
1248
|
+
function grantAuthorityUsable(snapshot) {
|
|
1249
|
+
if (snapshot.status === "paused") {
|
|
1250
|
+
return { ok: false, reason: "grant is paused" };
|
|
1251
|
+
}
|
|
1252
|
+
if (snapshot.status === "revoked") {
|
|
1253
|
+
return { ok: false, reason: "grant is revoked" };
|
|
1254
|
+
}
|
|
1255
|
+
if (snapshot.status === "expired") {
|
|
1256
|
+
return { ok: false, reason: "grant is expired" };
|
|
1257
|
+
}
|
|
1258
|
+
if (snapshot.status !== "active") {
|
|
1259
|
+
return { ok: false, reason: `grant status is ${snapshot.status}` };
|
|
1260
|
+
}
|
|
1261
|
+
if (!snapshot.grantObjectId?.trim()) {
|
|
1262
|
+
return { ok: false, reason: "grant is not finalized (missing grantObjectId)" };
|
|
1263
|
+
}
|
|
1264
|
+
if (!snapshot.poolObjectId?.trim()) {
|
|
1265
|
+
return { ok: false, reason: "poolObjectId is required" };
|
|
1266
|
+
}
|
|
1267
|
+
return {
|
|
1268
|
+
ok: true,
|
|
1269
|
+
grantObjectId: snapshot.grantObjectId,
|
|
1270
|
+
poolObjectId: snapshot.poolObjectId
|
|
1271
|
+
};
|
|
1272
|
+
}
|
|
1273
|
+
function resolveGrantTarget(snapshot, offerTargetHash) {
|
|
1274
|
+
if (!offerTargetHash?.trim()) {
|
|
1275
|
+
return { ok: false, reason: "offer.targetHash is required" };
|
|
1276
|
+
}
|
|
1277
|
+
const wanted = normalizeHash(offerTargetHash);
|
|
1278
|
+
if (!/^[0-9a-f]{64}$/.test(wanted)) {
|
|
1279
|
+
return { ok: false, reason: "offer.targetHash must be 32-byte hex" };
|
|
1280
|
+
}
|
|
1281
|
+
const usable = grantAuthorityUsable(snapshot);
|
|
1282
|
+
if (!usable.ok) {
|
|
1283
|
+
return usable;
|
|
1284
|
+
}
|
|
1285
|
+
const hits = [];
|
|
1286
|
+
for (const policy2 of snapshot.policies) {
|
|
1287
|
+
if (policy2.status !== "active") continue;
|
|
1288
|
+
for (const target of policy2.targets) {
|
|
1289
|
+
if (normalizeHash(target.targetHash) === wanted) {
|
|
1290
|
+
hits.push({
|
|
1291
|
+
policyId: policy2.policyId,
|
|
1292
|
+
onChainPolicyId: policy2.onChainPolicyId,
|
|
1293
|
+
maxPerPayment: target.maxPerPayment
|
|
1294
|
+
});
|
|
1295
|
+
}
|
|
1296
|
+
}
|
|
1297
|
+
}
|
|
1298
|
+
if (hits.length > 1) {
|
|
1299
|
+
return {
|
|
1300
|
+
ok: false,
|
|
1301
|
+
reason: `target hash resolves to ${hits.length} active policies (EPolicyConflict)`
|
|
1302
|
+
};
|
|
1303
|
+
}
|
|
1304
|
+
if (hits.length === 1) {
|
|
1305
|
+
const hit = hits[0];
|
|
1306
|
+
return {
|
|
1307
|
+
ok: true,
|
|
1308
|
+
policyId: hit.policyId,
|
|
1309
|
+
onChainPolicyId: hit.onChainPolicyId,
|
|
1310
|
+
targetHash: wanted,
|
|
1311
|
+
maxPerPayment: hit.maxPerPayment,
|
|
1312
|
+
poolObjectId: usable.poolObjectId,
|
|
1313
|
+
grantObjectId: usable.grantObjectId,
|
|
1314
|
+
coinType: snapshot.coinType,
|
|
1315
|
+
delegateAddress: snapshot.delegateAddress
|
|
1316
|
+
};
|
|
1317
|
+
}
|
|
1318
|
+
return {
|
|
1319
|
+
ok: false,
|
|
1320
|
+
reason: "no active policy in grant snapshot matches offer.targetHash"
|
|
1321
|
+
};
|
|
1322
|
+
}
|
|
1323
|
+
|
|
1324
|
+
// ../../src/protocol/terms.ts
|
|
1325
|
+
import { createHash as createHash3 } from "crypto";
|
|
1326
|
+
function paymentIdHash(challengeId) {
|
|
1327
|
+
if (typeof challengeId !== "string" || !challengeId.trim()) {
|
|
1328
|
+
throw new Error("challengeId is required for paymentIdHash");
|
|
1329
|
+
}
|
|
1330
|
+
return createHash3("sha256").update(challengeId, "utf8").digest();
|
|
1331
|
+
}
|
|
1332
|
+
function termsHash(offer) {
|
|
1333
|
+
if (typeof offer.digest !== "string" || !offer.digest.trim()) {
|
|
1334
|
+
throw new Error("offer.digest is required for termsHash");
|
|
1335
|
+
}
|
|
1336
|
+
return createHash3("sha256").update(offer.digest, "utf8").digest();
|
|
1337
|
+
}
|
|
1338
|
+
|
|
1339
|
+
// ../../src/protocol/rail.ts
|
|
1340
|
+
var SETTLEMENT_RAILS = [
|
|
1341
|
+
"direct",
|
|
1342
|
+
"allowance",
|
|
1343
|
+
"shared_pool",
|
|
1344
|
+
"auth_capture",
|
|
1345
|
+
"subscription"
|
|
1346
|
+
];
|
|
1347
|
+
var DIALECT_ORDER = ["mpp", "x402"];
|
|
1348
|
+
function isSettlementRail(value) {
|
|
1349
|
+
return typeof value === "string" && SETTLEMENT_RAILS.includes(value);
|
|
1350
|
+
}
|
|
1351
|
+
function assertSettlementRail(value) {
|
|
1352
|
+
if (!isSettlementRail(value)) {
|
|
1353
|
+
throw new Error(
|
|
1354
|
+
`invalid settlement rail: ${String(value)} (expected ${SETTLEMENT_RAILS.join(" | ")})`
|
|
1355
|
+
);
|
|
1356
|
+
}
|
|
1357
|
+
return value;
|
|
1358
|
+
}
|
|
1359
|
+
function isDialect(value) {
|
|
1360
|
+
return value === "mpp" || value === "x402";
|
|
1361
|
+
}
|
|
1362
|
+
function isDialectMode(value) {
|
|
1363
|
+
return value === "transaction" || value === "finalized-proof";
|
|
1364
|
+
}
|
|
1365
|
+
function dialectsFromModes(modes) {
|
|
1366
|
+
return DIALECT_ORDER.filter((d) => modes[d] != null);
|
|
1367
|
+
}
|
|
1368
|
+
function dialectMode(authority, dialect) {
|
|
1369
|
+
const mode = authority.dialectModes[dialect];
|
|
1370
|
+
if (!mode) {
|
|
1371
|
+
throw new Error(
|
|
1372
|
+
`${authority.rail} has no dialectMode for ${dialect} (ceiling: ${authority.supportedDialects.join(",")})`
|
|
1373
|
+
);
|
|
1374
|
+
}
|
|
1375
|
+
return mode;
|
|
1376
|
+
}
|
|
1377
|
+
function createSettlementRailAuthority(input) {
|
|
1378
|
+
assertSettlementRail(input.rail);
|
|
1379
|
+
if (typeof input.packageId !== "string" || !input.packageId.trim()) {
|
|
1380
|
+
throw new Error("packageId is required");
|
|
1381
|
+
}
|
|
1382
|
+
if (typeof input.module !== "string" || !input.module.trim()) {
|
|
1383
|
+
throw new Error("module is required");
|
|
1384
|
+
}
|
|
1385
|
+
if (typeof input.payFunction !== "string" || !input.payFunction.trim()) {
|
|
1386
|
+
throw new Error("payFunction is required");
|
|
1387
|
+
}
|
|
1388
|
+
const keys = Object.keys(input.dialectModes ?? {});
|
|
1389
|
+
if (!keys.length) {
|
|
1390
|
+
throw new Error("dialectModes must be non-empty");
|
|
1391
|
+
}
|
|
1392
|
+
for (const d of keys) {
|
|
1393
|
+
if (!isDialect(d)) {
|
|
1394
|
+
throw new Error(`unknown dialect in dialectModes: ${String(d)}`);
|
|
1395
|
+
}
|
|
1396
|
+
const mode = input.dialectModes[d];
|
|
1397
|
+
if (!isDialectMode(mode)) {
|
|
1398
|
+
throw new Error(
|
|
1399
|
+
`invalid dialectMode for ${d}: ${String(mode)}`
|
|
1400
|
+
);
|
|
1401
|
+
}
|
|
1402
|
+
}
|
|
1403
|
+
const dialectModes = Object.freeze({ ...input.dialectModes });
|
|
1404
|
+
const supportedDialects = Object.freeze(
|
|
1405
|
+
dialectsFromModes(dialectModes)
|
|
1406
|
+
);
|
|
1407
|
+
const proofMode = dialectModes.mpp ?? dialectModes.x402 ?? "transaction";
|
|
1408
|
+
return Object.freeze({
|
|
1409
|
+
rail: input.rail,
|
|
1410
|
+
packageId: input.packageId.trim(),
|
|
1411
|
+
module: input.module.trim(),
|
|
1412
|
+
payFunction: input.payFunction.trim(),
|
|
1413
|
+
dialectModes,
|
|
1414
|
+
supportedDialects,
|
|
1415
|
+
proofMode
|
|
1416
|
+
});
|
|
1417
|
+
}
|
|
1418
|
+
function defaultAuthorityForRail(rail, packageId, opts) {
|
|
1419
|
+
switch (rail) {
|
|
1420
|
+
case "shared_pool": {
|
|
1421
|
+
const dual = opts?.enableSharedPoolX402 === true;
|
|
1422
|
+
return createSettlementRailAuthority({
|
|
1423
|
+
rail: "shared_pool",
|
|
1424
|
+
packageId,
|
|
1425
|
+
module: "shared_pool",
|
|
1426
|
+
payFunction: "pay",
|
|
1427
|
+
dialectModes: dual ? { mpp: "finalized-proof", x402: "transaction" } : { mpp: "finalized-proof" }
|
|
1428
|
+
});
|
|
1429
|
+
}
|
|
1430
|
+
case "allowance":
|
|
1431
|
+
return createSettlementRailAuthority({
|
|
1432
|
+
rail: "allowance",
|
|
1433
|
+
packageId,
|
|
1434
|
+
module: "allowance",
|
|
1435
|
+
payFunction: "pay",
|
|
1436
|
+
dialectModes: {
|
|
1437
|
+
mpp: "finalized-proof",
|
|
1438
|
+
x402: "transaction"
|
|
1439
|
+
}
|
|
1440
|
+
});
|
|
1441
|
+
case "subscription":
|
|
1442
|
+
return createSettlementRailAuthority({
|
|
1443
|
+
rail: "subscription",
|
|
1444
|
+
packageId,
|
|
1445
|
+
module: "subscription",
|
|
1446
|
+
payFunction: "charge",
|
|
1447
|
+
dialectModes: {
|
|
1448
|
+
mpp: "transaction",
|
|
1449
|
+
x402: "transaction"
|
|
1450
|
+
}
|
|
1451
|
+
});
|
|
1452
|
+
case "direct":
|
|
1453
|
+
return createSettlementRailAuthority({
|
|
1454
|
+
rail: "direct",
|
|
1455
|
+
packageId,
|
|
1456
|
+
module: "payment",
|
|
1457
|
+
payFunction: "pay",
|
|
1458
|
+
dialectModes: {
|
|
1459
|
+
mpp: "transaction",
|
|
1460
|
+
x402: "transaction"
|
|
1461
|
+
}
|
|
1462
|
+
});
|
|
1463
|
+
case "auth_capture":
|
|
1464
|
+
return createSettlementRailAuthority({
|
|
1465
|
+
rail: "auth_capture",
|
|
1466
|
+
packageId,
|
|
1467
|
+
module: "auth_capture",
|
|
1468
|
+
payFunction: "authorize",
|
|
1469
|
+
dialectModes: {
|
|
1470
|
+
x402: "transaction"
|
|
1471
|
+
}
|
|
1472
|
+
});
|
|
1473
|
+
default: {
|
|
1474
|
+
const _exhaustive = rail;
|
|
1475
|
+
throw new Error(`unhandled rail: ${String(_exhaustive)}`);
|
|
1476
|
+
}
|
|
1477
|
+
}
|
|
1478
|
+
}
|
|
1479
|
+
|
|
1480
|
+
// ../../src/protocol/auth-params.ts
|
|
1481
|
+
var TOKEN = /^[A-Za-z0-9!#$%&'*+\-.^_`|~]+$/;
|
|
1482
|
+
function formatAuthParams(params) {
|
|
1483
|
+
return Object.entries(params).map(([k, v]) => `${k}=${TOKEN.test(v) ? v : quote(v)}`).join(", ");
|
|
1484
|
+
}
|
|
1485
|
+
function quote(value) {
|
|
1486
|
+
return `"${value.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`;
|
|
1487
|
+
}
|
|
1488
|
+
function parseAuthHeader(header) {
|
|
1489
|
+
const trimmed = header.trim();
|
|
1490
|
+
const firstSpace = trimmed.indexOf(" ");
|
|
1491
|
+
if (firstSpace === -1) return null;
|
|
1492
|
+
const scheme = trimmed.slice(0, firstSpace);
|
|
1493
|
+
if (!TOKEN.test(scheme)) return null;
|
|
1494
|
+
const params = parseParams(trimmed.slice(firstSpace + 1));
|
|
1495
|
+
return params === null ? null : { scheme, params };
|
|
1496
|
+
}
|
|
1497
|
+
function parseParams(input) {
|
|
1498
|
+
const params = {};
|
|
1499
|
+
let i = 0;
|
|
1500
|
+
const skipSpace = () => {
|
|
1501
|
+
while (i < input.length && (input[i] === " " || input[i] === " ")) i++;
|
|
1502
|
+
};
|
|
1503
|
+
skipSpace();
|
|
1504
|
+
while (i < input.length) {
|
|
1505
|
+
const keyStart = i;
|
|
1506
|
+
while (i < input.length && input[i] !== "=") i++;
|
|
1507
|
+
if (i >= input.length) return null;
|
|
1508
|
+
const key = input.slice(keyStart, i).trim();
|
|
1509
|
+
if (!TOKEN.test(key)) return null;
|
|
1510
|
+
i++;
|
|
1511
|
+
let value;
|
|
1512
|
+
if (input[i] === '"') {
|
|
1513
|
+
i++;
|
|
1514
|
+
let out = "";
|
|
1515
|
+
let closed = false;
|
|
1516
|
+
while (i < input.length) {
|
|
1517
|
+
const ch = input[i];
|
|
1518
|
+
if (ch === "\\" && i + 1 < input.length) {
|
|
1519
|
+
out += input[i + 1];
|
|
1520
|
+
i += 2;
|
|
1521
|
+
continue;
|
|
1522
|
+
}
|
|
1523
|
+
if (ch === '"') {
|
|
1524
|
+
i++;
|
|
1525
|
+
closed = true;
|
|
1526
|
+
break;
|
|
1527
|
+
}
|
|
1528
|
+
out += ch;
|
|
1529
|
+
i++;
|
|
1530
|
+
}
|
|
1531
|
+
if (!closed) return null;
|
|
1532
|
+
value = out;
|
|
1533
|
+
} else {
|
|
1534
|
+
const start = i;
|
|
1535
|
+
while (i < input.length && input[i] !== ",") i++;
|
|
1536
|
+
value = input.slice(start, i).trim();
|
|
1537
|
+
}
|
|
1538
|
+
params[key] = value;
|
|
1539
|
+
skipSpace();
|
|
1540
|
+
if (i < input.length) {
|
|
1541
|
+
if (input[i] !== ",") return null;
|
|
1542
|
+
i++;
|
|
1543
|
+
skipSpace();
|
|
1544
|
+
}
|
|
1545
|
+
}
|
|
1546
|
+
return params;
|
|
1547
|
+
}
|
|
1548
|
+
|
|
1549
|
+
// ../../src/protocol/mpp.ts
|
|
1550
|
+
var MPP_SCHEME = "Payment";
|
|
1551
|
+
var MPP_METHOD = "sui.charge";
|
|
1552
|
+
var MPP_CHALLENGE_HEADER = "WWW-Authenticate";
|
|
1553
|
+
var MPP_CREDENTIAL_HEADER = "Authorization";
|
|
1554
|
+
var MPP_RECEIPT_HEADER = "Payment-Receipt";
|
|
1555
|
+
function decodeChallenge(header) {
|
|
1556
|
+
const parsed = parseAuthHeader(header);
|
|
1557
|
+
if (!parsed || parsed.scheme !== MPP_SCHEME) return null;
|
|
1558
|
+
const p = parsed.params;
|
|
1559
|
+
if (p.method !== MPP_METHOD) return null;
|
|
1560
|
+
const required = [
|
|
1561
|
+
"challenge",
|
|
1562
|
+
"amount",
|
|
1563
|
+
"asset",
|
|
1564
|
+
"decimals",
|
|
1565
|
+
"network",
|
|
1566
|
+
"recipient",
|
|
1567
|
+
"package",
|
|
1568
|
+
"resource",
|
|
1569
|
+
"issued",
|
|
1570
|
+
"expires",
|
|
1571
|
+
"digest"
|
|
1572
|
+
];
|
|
1573
|
+
if (required.some((k) => !p[k])) return null;
|
|
1574
|
+
const issuedAt = Date.parse(p.issued);
|
|
1575
|
+
const expiresAt = Date.parse(p.expires);
|
|
1576
|
+
const decimals = Number(p.decimals);
|
|
1577
|
+
if (Number.isNaN(issuedAt) || Number.isNaN(expiresAt) || Number.isNaN(decimals)) return null;
|
|
1578
|
+
const offer = {
|
|
1579
|
+
challengeId: p.challenge,
|
|
1580
|
+
resource: p.resource,
|
|
1581
|
+
description: p.description ?? "",
|
|
1582
|
+
mimeType: p.type ?? "application/json",
|
|
1583
|
+
amount: p.amount,
|
|
1584
|
+
asset: p.asset,
|
|
1585
|
+
decimals,
|
|
1586
|
+
network: p.network,
|
|
1587
|
+
payTo: p.recipient,
|
|
1588
|
+
packageId: p.package,
|
|
1589
|
+
issuedAt,
|
|
1590
|
+
expiresAt,
|
|
1591
|
+
digest: p.digest
|
|
1592
|
+
};
|
|
1593
|
+
if (p.net_amount !== void 0) offer.netAmount = p.net_amount;
|
|
1594
|
+
if (p.platform_fee !== void 0) offer.platformFee = p.platform_fee;
|
|
1595
|
+
if (p.digest_version !== void 0) {
|
|
1596
|
+
const v = Number(p.digest_version);
|
|
1597
|
+
if (v === 1 || v === 2 || v === 3) offer.digestVersion = v;
|
|
1598
|
+
}
|
|
1599
|
+
if (p.settlement_rail === "direct" || p.settlement_rail === "allowance" || p.settlement_rail === "shared_pool" || p.settlement_rail === "auth_capture" || p.settlement_rail === "subscription") {
|
|
1600
|
+
offer.settlementRail = p.settlement_rail;
|
|
1601
|
+
}
|
|
1602
|
+
if (p.service_id !== void 0) offer.serviceId = p.service_id;
|
|
1603
|
+
if (p.service_revision !== void 0) {
|
|
1604
|
+
const rev = Number(p.service_revision);
|
|
1605
|
+
if (Number.isFinite(rev)) offer.serviceRevision = rev;
|
|
1606
|
+
}
|
|
1607
|
+
if (p.target_hash !== void 0) offer.targetHash = p.target_hash;
|
|
1608
|
+
if (p.authorization_epoch !== void 0) {
|
|
1609
|
+
const epoch = Number(p.authorization_epoch);
|
|
1610
|
+
if (Number.isFinite(epoch)) offer.authorizationEpoch = epoch;
|
|
1611
|
+
}
|
|
1612
|
+
if (p.http_method !== void 0) offer.method = p.http_method;
|
|
1613
|
+
if (p.http_path !== void 0) offer.path = p.http_path;
|
|
1614
|
+
if (p.body_hash !== void 0) {
|
|
1615
|
+
if (!isBodyHash(p.body_hash)) return null;
|
|
1616
|
+
offer.bodyHash = p.body_hash;
|
|
1617
|
+
}
|
|
1618
|
+
if (p.content_type !== void 0) {
|
|
1619
|
+
if (!isOfferContentType(p.content_type)) return null;
|
|
1620
|
+
offer.contentType = p.content_type;
|
|
1621
|
+
}
|
|
1622
|
+
if (p.intent === "charge" || p.intent === "subscription") offer.intent = p.intent;
|
|
1623
|
+
if (p.period_ms !== void 0) {
|
|
1624
|
+
const ms = Number(p.period_ms);
|
|
1625
|
+
if (Number.isFinite(ms)) offer.periodMs = ms;
|
|
1626
|
+
}
|
|
1627
|
+
if (p.period_count !== void 0) {
|
|
1628
|
+
const count = Number(p.period_count);
|
|
1629
|
+
if (Number.isFinite(count)) offer.periodCount = count;
|
|
1630
|
+
}
|
|
1631
|
+
if (p.refundable === "true") offer.refundable = true;
|
|
1632
|
+
else if (p.refundable === "false") offer.refundable = false;
|
|
1633
|
+
if (p.refund_window_ms !== void 0) {
|
|
1634
|
+
const ms = Number(p.refund_window_ms);
|
|
1635
|
+
if (Number.isFinite(ms)) offer.refundWindowMs = ms;
|
|
1636
|
+
}
|
|
1637
|
+
if (p.decide_window_ms !== void 0) {
|
|
1638
|
+
const ms = Number(p.decide_window_ms);
|
|
1639
|
+
if (Number.isFinite(ms)) offer.decideWindowMs = ms;
|
|
1640
|
+
}
|
|
1641
|
+
if (p.chain_id !== void 0) {
|
|
1642
|
+
const id = Number(p.chain_id);
|
|
1643
|
+
if (Number.isFinite(id)) offer.chainId = id;
|
|
1644
|
+
}
|
|
1645
|
+
if (p.payment_salt !== void 0) offer.paymentSalt = p.payment_salt;
|
|
1646
|
+
if (p.refund_to !== void 0) offer.refundTo = p.refund_to;
|
|
1647
|
+
if (p.sponsor_gas_owner !== void 0) {
|
|
1648
|
+
offer.sponsorGasOwner = p.sponsor_gas_owner;
|
|
1649
|
+
}
|
|
1650
|
+
if (p.sponsor_gas_budget !== void 0) {
|
|
1651
|
+
offer.sponsorGasBudget = p.sponsor_gas_budget;
|
|
1652
|
+
}
|
|
1653
|
+
if (p.sponsor_gas_price !== void 0) {
|
|
1654
|
+
offer.sponsorGasPrice = p.sponsor_gas_price;
|
|
1655
|
+
}
|
|
1656
|
+
if (p.sponsor_gas_payment !== void 0) {
|
|
1657
|
+
offer.sponsorGasPayment = p.sponsor_gas_payment;
|
|
1658
|
+
}
|
|
1659
|
+
return offer;
|
|
1660
|
+
}
|
|
1661
|
+
function encodeProof(proof) {
|
|
1662
|
+
return `${MPP_SCHEME} ${formatAuthParams({
|
|
1663
|
+
method: MPP_METHOD,
|
|
1664
|
+
challenge: proof.challengeId,
|
|
1665
|
+
payer: proof.payer,
|
|
1666
|
+
offer: Buffer.from(JSON.stringify(proof.offer), "utf8").toString("base64url"),
|
|
1667
|
+
digest: proof.txDigest
|
|
1668
|
+
})}`;
|
|
1669
|
+
}
|
|
1670
|
+
function decodeReceipt(header) {
|
|
1671
|
+
const parsed = parseAuthHeader(`X ${header}`);
|
|
1672
|
+
if (!parsed) return null;
|
|
1673
|
+
const p = parsed.params;
|
|
1674
|
+
if (!p.challenge || !p.tx_digest || !p.payer || !p.amount || !p.asset || !p.network) return null;
|
|
1675
|
+
const receipt = {
|
|
1676
|
+
challengeId: p.challenge,
|
|
1677
|
+
txDigest: p.tx_digest,
|
|
1678
|
+
network: p.network,
|
|
1679
|
+
payer: p.payer,
|
|
1680
|
+
amount: p.amount,
|
|
1681
|
+
asset: p.asset,
|
|
1682
|
+
status: "settled"
|
|
1683
|
+
};
|
|
1684
|
+
if (p.request_digest) receipt.requestDigest = p.request_digest;
|
|
1685
|
+
if (p.net_amount !== void 0) receipt.netAmount = p.net_amount;
|
|
1686
|
+
if (p.platform_fee !== void 0) receipt.platformFee = p.platform_fee;
|
|
1687
|
+
if (p.intent === "charge" || p.intent === "subscription") receipt.intent = p.intent;
|
|
1688
|
+
if (p.mandate_id !== void 0) receipt.mandateId = p.mandate_id;
|
|
1689
|
+
if (p.period_index !== void 0) receipt.periodIndex = p.period_index;
|
|
1690
|
+
return receipt;
|
|
1691
|
+
}
|
|
1692
|
+
|
|
1693
|
+
// ../../src/protocol/x402.ts
|
|
1694
|
+
var X402_VERSION = 2;
|
|
1695
|
+
var X402_SCHEME = "exact";
|
|
1696
|
+
var X402_SCHEME_AUTH_CAPTURE = "auth-capture";
|
|
1697
|
+
var X402_CREDENTIAL_HEADER = "X-PAYMENT";
|
|
1698
|
+
var X402_RECEIPT_HEADER = "X-PAYMENT-RESPONSE";
|
|
1699
|
+
function toAccept(offer, authority) {
|
|
1700
|
+
const auth = authority ?? defaultAuthorityForRail(offer.settlementRail ?? "direct", offer.packageId);
|
|
1701
|
+
const suipay = {
|
|
1702
|
+
challengeId: offer.challengeId,
|
|
1703
|
+
packageId: offer.packageId,
|
|
1704
|
+
module: auth.module,
|
|
1705
|
+
function: auth.payFunction,
|
|
1706
|
+
decimals: offer.decimals,
|
|
1707
|
+
issuedAt: offer.issuedAt,
|
|
1708
|
+
expiresAt: offer.expiresAt,
|
|
1709
|
+
digest: offer.digest
|
|
1710
|
+
};
|
|
1711
|
+
if (offer.netAmount !== void 0) suipay.netAmount = offer.netAmount;
|
|
1712
|
+
if (offer.platformFee !== void 0) suipay.platformFee = offer.platformFee;
|
|
1713
|
+
if (offer.digestVersion !== void 0) suipay.digestVersion = offer.digestVersion;
|
|
1714
|
+
if (offer.settlementRail !== void 0) suipay.settlementRail = offer.settlementRail;
|
|
1715
|
+
if (offer.serviceId !== void 0) suipay.serviceId = offer.serviceId;
|
|
1716
|
+
if (offer.serviceRevision !== void 0) suipay.serviceRevision = offer.serviceRevision;
|
|
1717
|
+
if (offer.targetHash !== void 0) suipay.targetHash = offer.targetHash;
|
|
1718
|
+
if (offer.authorizationEpoch !== void 0) {
|
|
1719
|
+
suipay.authorizationEpoch = offer.authorizationEpoch;
|
|
1720
|
+
}
|
|
1721
|
+
if (offer.method !== void 0) suipay.method = offer.method;
|
|
1722
|
+
if (offer.path !== void 0) suipay.path = offer.path;
|
|
1723
|
+
if (offer.bodyHash !== void 0) suipay.bodyHash = offer.bodyHash;
|
|
1724
|
+
if (offer.contentType !== void 0) suipay.contentType = offer.contentType;
|
|
1725
|
+
if (offer.intent !== void 0) suipay.intent = offer.intent;
|
|
1726
|
+
if (offer.periodMs !== void 0) suipay.periodMs = offer.periodMs;
|
|
1727
|
+
if (offer.periodCount !== void 0) suipay.periodCount = offer.periodCount;
|
|
1728
|
+
if (offer.refundable !== void 0) suipay.refundable = offer.refundable;
|
|
1729
|
+
if (offer.refundWindowMs !== void 0) suipay.refundWindowMs = offer.refundWindowMs;
|
|
1730
|
+
if (offer.decideWindowMs !== void 0) suipay.decideWindowMs = offer.decideWindowMs;
|
|
1731
|
+
if (offer.chainId !== void 0) suipay.chainId = offer.chainId;
|
|
1732
|
+
if (offer.paymentSalt !== void 0) suipay.paymentSalt = offer.paymentSalt;
|
|
1733
|
+
if (offer.refundTo !== void 0) suipay.refundTo = offer.refundTo;
|
|
1734
|
+
if (offer.sponsorGasOwner !== void 0) {
|
|
1735
|
+
suipay.sponsorGasOwner = offer.sponsorGasOwner;
|
|
1736
|
+
}
|
|
1737
|
+
if (offer.sponsorGasBudget !== void 0) {
|
|
1738
|
+
suipay.sponsorGasBudget = offer.sponsorGasBudget;
|
|
1739
|
+
}
|
|
1740
|
+
if (offer.sponsorGasPrice !== void 0) {
|
|
1741
|
+
suipay.sponsorGasPrice = offer.sponsorGasPrice;
|
|
1742
|
+
}
|
|
1743
|
+
if (offer.sponsorGasPayment !== void 0) {
|
|
1744
|
+
suipay.sponsorGasPayment = offer.sponsorGasPayment;
|
|
1745
|
+
}
|
|
1746
|
+
const scheme = offer.settlementRail === "auth_capture" ? X402_SCHEME_AUTH_CAPTURE : X402_SCHEME;
|
|
1747
|
+
return {
|
|
1748
|
+
scheme,
|
|
1749
|
+
network: offer.network,
|
|
1750
|
+
amount: offer.amount,
|
|
1751
|
+
asset: offer.asset,
|
|
1752
|
+
payTo: offer.payTo,
|
|
1753
|
+
maxTimeoutSeconds: Math.max(1, Math.round((offer.expiresAt - offer.issuedAt) / 1e3)),
|
|
1754
|
+
extra: { suipay }
|
|
1755
|
+
};
|
|
1756
|
+
}
|
|
1757
|
+
function decodeChallenge2(body) {
|
|
1758
|
+
const b = body;
|
|
1759
|
+
if (!b || typeof b !== "object" || !Array.isArray(b.accepts) || !b.resource) return null;
|
|
1760
|
+
const accept = b.accepts.find(
|
|
1761
|
+
(a) => (a?.scheme === X402_SCHEME || a?.scheme === X402_SCHEME_AUTH_CAPTURE) && a?.extra?.suipay
|
|
1762
|
+
);
|
|
1763
|
+
if (!accept) return null;
|
|
1764
|
+
return fromAccept(accept, b.resource);
|
|
1765
|
+
}
|
|
1766
|
+
function fromAccept(accept, resource) {
|
|
1767
|
+
const s = accept.extra?.suipay;
|
|
1768
|
+
if (!s || !s.challengeId || !s.packageId || !s.digest) return null;
|
|
1769
|
+
if (!accept.amount || !accept.asset || !accept.payTo || !accept.network) return null;
|
|
1770
|
+
const offer = {
|
|
1771
|
+
challengeId: s.challengeId,
|
|
1772
|
+
resource: resource.url,
|
|
1773
|
+
description: resource.description ?? "",
|
|
1774
|
+
mimeType: resource.mimeType ?? "application/json",
|
|
1775
|
+
amount: accept.amount,
|
|
1776
|
+
asset: accept.asset,
|
|
1777
|
+
decimals: s.decimals,
|
|
1778
|
+
network: accept.network,
|
|
1779
|
+
payTo: accept.payTo,
|
|
1780
|
+
packageId: s.packageId,
|
|
1781
|
+
issuedAt: s.issuedAt,
|
|
1782
|
+
expiresAt: s.expiresAt,
|
|
1783
|
+
digest: s.digest
|
|
1784
|
+
};
|
|
1785
|
+
if (s.netAmount !== void 0) offer.netAmount = s.netAmount;
|
|
1786
|
+
if (s.platformFee !== void 0) offer.platformFee = s.platformFee;
|
|
1787
|
+
if (s.digestVersion === 1 || s.digestVersion === 2 || s.digestVersion === 3) {
|
|
1788
|
+
offer.digestVersion = s.digestVersion;
|
|
1789
|
+
}
|
|
1790
|
+
if (s.settlementRail !== void 0 && isSettlementRail(s.settlementRail)) {
|
|
1791
|
+
offer.settlementRail = s.settlementRail;
|
|
1792
|
+
}
|
|
1793
|
+
if (s.serviceId !== void 0) offer.serviceId = s.serviceId;
|
|
1794
|
+
if (s.serviceRevision !== void 0) offer.serviceRevision = s.serviceRevision;
|
|
1795
|
+
if (s.targetHash !== void 0) offer.targetHash = s.targetHash;
|
|
1796
|
+
if (s.authorizationEpoch !== void 0) offer.authorizationEpoch = s.authorizationEpoch;
|
|
1797
|
+
if (s.method !== void 0) offer.method = s.method;
|
|
1798
|
+
if (s.path !== void 0) offer.path = s.path;
|
|
1799
|
+
if (s.bodyHash !== void 0) {
|
|
1800
|
+
if (!isBodyHash(s.bodyHash)) return null;
|
|
1801
|
+
offer.bodyHash = s.bodyHash;
|
|
1802
|
+
}
|
|
1803
|
+
if (s.contentType !== void 0) {
|
|
1804
|
+
if (!isOfferContentType(s.contentType)) return null;
|
|
1805
|
+
offer.contentType = s.contentType;
|
|
1806
|
+
}
|
|
1807
|
+
if (s.intent === "charge" || s.intent === "subscription") offer.intent = s.intent;
|
|
1808
|
+
if (s.periodMs !== void 0) offer.periodMs = s.periodMs;
|
|
1809
|
+
if (s.periodCount !== void 0) offer.periodCount = s.periodCount;
|
|
1810
|
+
if (s.refundable !== void 0) offer.refundable = s.refundable;
|
|
1811
|
+
if (s.refundWindowMs !== void 0) offer.refundWindowMs = s.refundWindowMs;
|
|
1812
|
+
if (s.decideWindowMs !== void 0) offer.decideWindowMs = s.decideWindowMs;
|
|
1813
|
+
if (s.chainId !== void 0) offer.chainId = s.chainId;
|
|
1814
|
+
if (s.paymentSalt !== void 0) offer.paymentSalt = s.paymentSalt;
|
|
1815
|
+
if (s.refundTo !== void 0) offer.refundTo = s.refundTo;
|
|
1816
|
+
if (s.sponsorGasOwner !== void 0) offer.sponsorGasOwner = s.sponsorGasOwner;
|
|
1817
|
+
if (s.sponsorGasBudget !== void 0) {
|
|
1818
|
+
offer.sponsorGasBudget = s.sponsorGasBudget;
|
|
1819
|
+
}
|
|
1820
|
+
if (s.sponsorGasPrice !== void 0) offer.sponsorGasPrice = s.sponsorGasPrice;
|
|
1821
|
+
if (s.sponsorGasPayment !== void 0) {
|
|
1822
|
+
offer.sponsorGasPayment = s.sponsorGasPayment;
|
|
1823
|
+
}
|
|
1824
|
+
return offer;
|
|
1825
|
+
}
|
|
1826
|
+
function encodeCredential(cred) {
|
|
1827
|
+
const payload = {
|
|
1828
|
+
x402Version: X402_VERSION,
|
|
1829
|
+
resource: {
|
|
1830
|
+
url: cred.offer.resource,
|
|
1831
|
+
description: cred.offer.description,
|
|
1832
|
+
mimeType: cred.offer.mimeType
|
|
1833
|
+
},
|
|
1834
|
+
accepted: toAccept(cred.offer),
|
|
1835
|
+
payload: {
|
|
1836
|
+
challengeId: cred.challengeId,
|
|
1837
|
+
payer: cred.payer,
|
|
1838
|
+
txBytes: cred.txBytes,
|
|
1839
|
+
signature: cred.signature
|
|
1840
|
+
}
|
|
1841
|
+
};
|
|
1842
|
+
return Buffer.from(JSON.stringify(payload), "utf8").toString("base64");
|
|
1843
|
+
}
|
|
1844
|
+
function decodeReceipt2(header) {
|
|
1845
|
+
try {
|
|
1846
|
+
const r = JSON.parse(Buffer.from(header, "base64").toString("utf8"));
|
|
1847
|
+
if (!r?.success || !r.transaction) return null;
|
|
1848
|
+
const receipt = {
|
|
1849
|
+
challengeId: r.challengeId,
|
|
1850
|
+
txDigest: r.transaction,
|
|
1851
|
+
network: r.network,
|
|
1852
|
+
payer: r.payer,
|
|
1853
|
+
amount: r.amount,
|
|
1854
|
+
asset: r.asset,
|
|
1855
|
+
status: "settled"
|
|
1856
|
+
};
|
|
1857
|
+
if (typeof r.requestDigest === "string") receipt.requestDigest = r.requestDigest;
|
|
1858
|
+
if (typeof r.netAmount === "string") receipt.netAmount = r.netAmount;
|
|
1859
|
+
if (typeof r.platformFee === "string") receipt.platformFee = r.platformFee;
|
|
1860
|
+
return receipt;
|
|
1861
|
+
} catch {
|
|
1862
|
+
return null;
|
|
1863
|
+
}
|
|
1864
|
+
}
|
|
1865
|
+
|
|
1866
|
+
// ../../src/protocol/carrier.ts
|
|
1867
|
+
async function readChallenge(res, dialect) {
|
|
1868
|
+
if (dialect === "mpp") {
|
|
1869
|
+
const header = res.headers.get(MPP_CHALLENGE_HEADER);
|
|
1870
|
+
return header ? decodeChallenge(header) : null;
|
|
1871
|
+
}
|
|
1872
|
+
return decodeChallenge2(await readBody(res));
|
|
1873
|
+
}
|
|
1874
|
+
function readReceipt(res, dialect) {
|
|
1875
|
+
const header = dialect === "mpp" ? res.headers.get(MPP_RECEIPT_HEADER) : res.headers.get(X402_RECEIPT_HEADER);
|
|
1876
|
+
if (!header) return null;
|
|
1877
|
+
return dialect === "mpp" ? decodeReceipt(header) : decodeReceipt2(header);
|
|
1878
|
+
}
|
|
1879
|
+
var MAX_INLINE_BINARY_BYTES = 256 * 1024;
|
|
1880
|
+
function decodeUtf8OrNull(bytes) {
|
|
1881
|
+
try {
|
|
1882
|
+
return new TextDecoder("utf-8", { fatal: true }).decode(bytes);
|
|
1883
|
+
} catch {
|
|
1884
|
+
return null;
|
|
1885
|
+
}
|
|
1886
|
+
}
|
|
1887
|
+
async function readBody(res) {
|
|
1888
|
+
const bytes = new Uint8Array(await res.arrayBuffer());
|
|
1889
|
+
const text2 = decodeUtf8OrNull(bytes);
|
|
1890
|
+
if (text2 !== null) {
|
|
1891
|
+
try {
|
|
1892
|
+
return JSON.parse(text2);
|
|
1893
|
+
} catch {
|
|
1894
|
+
return text2;
|
|
1895
|
+
}
|
|
1896
|
+
}
|
|
1897
|
+
const contentType = res.headers.get("content-type")?.split(";")[0]?.trim() || "application/octet-stream";
|
|
1898
|
+
const binary = {
|
|
1899
|
+
__binary: true,
|
|
1900
|
+
contentType,
|
|
1901
|
+
byteLength: bytes.byteLength
|
|
1902
|
+
};
|
|
1903
|
+
if (bytes.byteLength <= MAX_INLINE_BINARY_BYTES) {
|
|
1904
|
+
binary.base64 = Buffer.from(bytes).toString("base64");
|
|
1905
|
+
}
|
|
1906
|
+
return binary;
|
|
1907
|
+
}
|
|
1908
|
+
|
|
1909
|
+
// ../../src/trace/context.ts
|
|
1910
|
+
import { createHmac as createHmac2, timingSafeEqual as timingSafeEqual2 } from "crypto";
|
|
1911
|
+
|
|
1912
|
+
// ../../src/trace/types.ts
|
|
1913
|
+
var DEFAULT_TRACE_LIMITS = {
|
|
1914
|
+
maxEventsPerRun: 200,
|
|
1915
|
+
maxPayloadBytesPerEvent: 16 * 1024,
|
|
1916
|
+
maxPayloadBytesPerRun: 2 * 1024 * 1024,
|
|
1917
|
+
retentionMs: 24 * 60 * 60 * 1e3
|
|
1918
|
+
};
|
|
1919
|
+
|
|
1920
|
+
// ../../src/trace/redact.ts
|
|
1921
|
+
function policy(publicMetadata, technicalMetadata, content = {}) {
|
|
1922
|
+
return {
|
|
1923
|
+
public: { metadata: publicMetadata, content: content.public ?? [] },
|
|
1924
|
+
technical: { metadata: technicalMetadata, content: content.technical ?? [] }
|
|
1925
|
+
};
|
|
1926
|
+
}
|
|
1927
|
+
var TRACE_FIELD_POLICY = {
|
|
1928
|
+
mcp_connected: policy(
|
|
1929
|
+
["clientName", "protocolVersion", "connectionState"],
|
|
1930
|
+
["clientName", "clientVersion", "protocolVersion", "transport", "connectionState"]
|
|
1931
|
+
),
|
|
1932
|
+
tools_listed: policy(["toolCount"], ["toolCount", "toolNames"]),
|
|
1933
|
+
tool_called: policy(["tool", "resource"], ["jsonRpcId", "tool", "resource"], {
|
|
1934
|
+
public: ["question"],
|
|
1935
|
+
technical: ["arguments"]
|
|
1936
|
+
}),
|
|
1937
|
+
tool_result: policy(
|
|
1938
|
+
["tool", "resultCount", "durationMs"],
|
|
1939
|
+
["jsonRpcId", "tool", "resultCount", "durationMs"],
|
|
1940
|
+
{ public: ["answer"], technical: ["result"] }
|
|
1941
|
+
),
|
|
1942
|
+
tool_error: policy(["tool", "errorCode"], ["jsonRpcId", "tool", "errorCode"], {
|
|
1943
|
+
technical: ["errorMessage"]
|
|
1944
|
+
}),
|
|
1945
|
+
challenge_received: policy(
|
|
1946
|
+
["serviceId", "serviceName", "amountAtomic", "asset", "decimals", "rail", "dialect", "httpStatus"],
|
|
1947
|
+
[
|
|
1948
|
+
"serviceId",
|
|
1949
|
+
"serviceName",
|
|
1950
|
+
"amountAtomic",
|
|
1951
|
+
"asset",
|
|
1952
|
+
"decimals",
|
|
1953
|
+
"rail",
|
|
1954
|
+
"dialect",
|
|
1955
|
+
"httpStatus",
|
|
1956
|
+
"network",
|
|
1957
|
+
"payTo",
|
|
1958
|
+
"challengeId",
|
|
1959
|
+
"offerCount"
|
|
1960
|
+
]
|
|
1961
|
+
),
|
|
1962
|
+
authority_matched: policy(
|
|
1963
|
+
["serviceId", "rail", "decision"],
|
|
1964
|
+
["serviceId", "rail", "decision", "grantId", "policyId", "profileId", "capsRemaining", "authorityEpoch"]
|
|
1965
|
+
),
|
|
1966
|
+
authority_revalidated: policy(
|
|
1967
|
+
["decision"],
|
|
1968
|
+
["decision", "grantId", "authorityEpoch"]
|
|
1969
|
+
),
|
|
1970
|
+
sponsor_requested: policy(["sponsor"], ["sponsor", "intentDigest", "serviceId"]),
|
|
1971
|
+
transaction_sponsored: policy(
|
|
1972
|
+
["sponsor", "gasBudget"],
|
|
1973
|
+
["sponsor", "gasBudget", "intentDigest", "serviceId"]
|
|
1974
|
+
),
|
|
1975
|
+
delegate_signed: policy(["delegateAddress"], ["delegateAddress", "intentDigest"]),
|
|
1976
|
+
transaction_submitted: policy(["digest"], ["digest", "network"]),
|
|
1977
|
+
payment_finalized: policy(
|
|
1978
|
+
["digest", "amountAtomic", "asset", "decimals"],
|
|
1979
|
+
["digest", "amountAtomic", "asset", "decimals", "network", "eventType", "checkpoint", "payTo"]
|
|
1980
|
+
),
|
|
1981
|
+
proof_presented: policy(["dialect", "serviceId"], ["dialect", "serviceId", "digest", "proofScheme"]),
|
|
1982
|
+
upstream_delivered: policy(
|
|
1983
|
+
// mimeType and byteCount are safe structural metadata (a content type and a
|
|
1984
|
+
// length, never body bytes), promoted to public so the owner audience view
|
|
1985
|
+
// can tell a delivery was an image and offer an inline preview.
|
|
1986
|
+
["serviceId", "httpStatus", "durationMs", "mimeType", "byteCount"],
|
|
1987
|
+
["serviceId", "httpStatus", "durationMs", "mimeType", "byteCount"],
|
|
1988
|
+
{ public: ["answer"], technical: ["body"] }
|
|
1989
|
+
),
|
|
1990
|
+
upstream_delivery_failed: policy(
|
|
1991
|
+
["serviceId", "httpStatus", "failureCode"],
|
|
1992
|
+
["serviceId", "httpStatus", "failureCode", "durationMs"]
|
|
1993
|
+
),
|
|
1994
|
+
settlement_projected: policy(
|
|
1995
|
+
["serviceId", "amountAtomic", "asset"],
|
|
1996
|
+
["serviceId", "amountAtomic", "asset", "digest", "settlementId", "rail"]
|
|
1997
|
+
),
|
|
1998
|
+
settlement_projection_failed: policy(
|
|
1999
|
+
["serviceId", "failureCode"],
|
|
2000
|
+
["serviceId", "failureCode", "digest"]
|
|
2001
|
+
),
|
|
2002
|
+
receipt_issued: policy(
|
|
2003
|
+
["serviceId", "amountAtomic", "asset", "decimals", "digest", "payTo"],
|
|
2004
|
+
[
|
|
2005
|
+
"serviceId",
|
|
2006
|
+
"amountAtomic",
|
|
2007
|
+
"asset",
|
|
2008
|
+
"decimals",
|
|
2009
|
+
"digest",
|
|
2010
|
+
"payTo",
|
|
2011
|
+
"receiptId",
|
|
2012
|
+
"rail",
|
|
2013
|
+
"network",
|
|
2014
|
+
"explorerUrl"
|
|
2015
|
+
]
|
|
2016
|
+
),
|
|
2017
|
+
payment_failed: policy(
|
|
2018
|
+
["serviceId", "stage", "failureCode"],
|
|
2019
|
+
["serviceId", "stage", "failureCode", "errorCode"],
|
|
2020
|
+
{ technical: ["errorMessage"] }
|
|
2021
|
+
),
|
|
2022
|
+
payment_ambiguous: policy(
|
|
2023
|
+
["serviceId", "stage", "digest"],
|
|
2024
|
+
["serviceId", "stage", "digest", "reasonCode"]
|
|
2025
|
+
)
|
|
2026
|
+
};
|
|
2027
|
+
var MIN_OPAQUE_RUN = 20;
|
|
2028
|
+
var OPAQUE_RUN = new RegExp(`[A-Za-z0-9+/=_~.-]{${MIN_OPAQUE_RUN},}`, "g");
|
|
2029
|
+
|
|
2030
|
+
// ../../src/trace/context.ts
|
|
2031
|
+
var DEFAULT_TRACE_IDLE_MS = 5 * 60 * 1e3;
|
|
2032
|
+
var TRACE_CONTEXT_VERSION = 1;
|
|
2033
|
+
var TRACE_HEADER_ID = "x-suipay-trace-id";
|
|
2034
|
+
var TRACE_HEADER_CONNECTION = "x-suipay-trace-connection";
|
|
2035
|
+
var TRACE_HEADER_REQUEST = "x-suipay-trace-request";
|
|
2036
|
+
var TRACE_HEADER_ISSUED_AT = "x-suipay-trace-issued-at";
|
|
2037
|
+
var TRACE_HEADER_VERSION = "x-suipay-trace-version";
|
|
2038
|
+
var TRACE_HEADER_SIGNATURE = "x-suipay-trace-signature";
|
|
2039
|
+
var NOTIFICATION = "notification";
|
|
2040
|
+
function canonicalTraceContext(tuple) {
|
|
2041
|
+
return JSON.stringify([
|
|
2042
|
+
tuple.version,
|
|
2043
|
+
tuple.traceId,
|
|
2044
|
+
tuple.connectionId,
|
|
2045
|
+
tuple.requestId,
|
|
2046
|
+
tuple.issuedAt,
|
|
2047
|
+
tuple.method,
|
|
2048
|
+
tuple.pathname
|
|
2049
|
+
]);
|
|
2050
|
+
}
|
|
2051
|
+
function sign(tuple, secret) {
|
|
2052
|
+
return createHmac2("sha256", secret).update(canonicalTraceContext(tuple)).digest("base64url");
|
|
2053
|
+
}
|
|
2054
|
+
function signInternalTraceContext(input, secret) {
|
|
2055
|
+
if (!secret) {
|
|
2056
|
+
throw new Error("trace context signing requires a secret");
|
|
2057
|
+
}
|
|
2058
|
+
const tuple = {
|
|
2059
|
+
version: TRACE_CONTEXT_VERSION,
|
|
2060
|
+
traceId: input.traceId,
|
|
2061
|
+
connectionId: input.connectionId,
|
|
2062
|
+
requestId: input.requestId,
|
|
2063
|
+
issuedAt: input.issuedAt ?? Date.now(),
|
|
2064
|
+
method: input.method.toUpperCase(),
|
|
2065
|
+
pathname: input.pathname
|
|
2066
|
+
};
|
|
2067
|
+
const headers = new Headers();
|
|
2068
|
+
headers.set(TRACE_HEADER_VERSION, String(tuple.version));
|
|
2069
|
+
headers.set(TRACE_HEADER_ID, tuple.traceId);
|
|
2070
|
+
headers.set(TRACE_HEADER_CONNECTION, tuple.connectionId);
|
|
2071
|
+
headers.set(TRACE_HEADER_REQUEST, tuple.requestId ?? NOTIFICATION);
|
|
2072
|
+
headers.set(TRACE_HEADER_ISSUED_AT, String(tuple.issuedAt));
|
|
2073
|
+
headers.set(TRACE_HEADER_SIGNATURE, sign(tuple, secret));
|
|
2074
|
+
return headers;
|
|
2075
|
+
}
|
|
2076
|
+
function internalTraceHeaderRecord(input, secret) {
|
|
2077
|
+
return Object.fromEntries(signInternalTraceContext(input, secret).entries());
|
|
2078
|
+
}
|
|
2079
|
+
|
|
2080
|
+
// ../../src/protocol/request-binding.ts
|
|
2081
|
+
function sameResource(offered, requested) {
|
|
2082
|
+
try {
|
|
2083
|
+
return new URL(offered).href === new URL(requested).href;
|
|
2084
|
+
} catch {
|
|
2085
|
+
return false;
|
|
2086
|
+
}
|
|
2087
|
+
}
|
|
2088
|
+
function requestBindingViolation(offer, request) {
|
|
2089
|
+
if (offer.method !== void 0 && offer.method.toUpperCase() !== request.method) {
|
|
2090
|
+
return {
|
|
2091
|
+
code: "METHOD_MISMATCH",
|
|
2092
|
+
reason: `offer is for ${offer.method}, request is ${request.method}`
|
|
2093
|
+
};
|
|
2094
|
+
}
|
|
2095
|
+
if (!sameResource(offer.resource ?? "", request.url)) {
|
|
2096
|
+
return {
|
|
2097
|
+
code: "RESOURCE_MISMATCH",
|
|
2098
|
+
reason: `offer resource ${offer.resource ?? "(none)"} does not match requested URL ${request.url}`
|
|
2099
|
+
};
|
|
2100
|
+
}
|
|
2101
|
+
if (offer.path !== void 0 && offer.path !== new URL(request.url).pathname) {
|
|
2102
|
+
return {
|
|
2103
|
+
code: "PATH_MISMATCH",
|
|
2104
|
+
reason: `offer path ${offer.path} does not match requested path`
|
|
2105
|
+
};
|
|
2106
|
+
}
|
|
2107
|
+
if (request.method === "POST") {
|
|
2108
|
+
if (offer.intent === "subscription" && offer.bodyHash === void 0 && offer.contentType === void 0) {
|
|
2109
|
+
return null;
|
|
2110
|
+
}
|
|
2111
|
+
if (offer.digestVersion !== 3) {
|
|
2112
|
+
return {
|
|
2113
|
+
code: "OFFER_NOT_BODY_BOUND",
|
|
2114
|
+
reason: "a paid POST requires an offer digest v3 that signs the request body"
|
|
2115
|
+
};
|
|
2116
|
+
}
|
|
2117
|
+
if (offer.bodyHash !== request.bodyHash) {
|
|
2118
|
+
return {
|
|
2119
|
+
code: "BODY_HASH_MISMATCH",
|
|
2120
|
+
reason: "offer body hash does not cover this request body"
|
|
2121
|
+
};
|
|
2122
|
+
}
|
|
2123
|
+
if ((offer.contentType ?? null) !== request.contentType) {
|
|
2124
|
+
return {
|
|
2125
|
+
code: "CONTENT_TYPE_MISMATCH",
|
|
2126
|
+
reason: "offer content type does not match this request body"
|
|
2127
|
+
};
|
|
2128
|
+
}
|
|
2129
|
+
return null;
|
|
2130
|
+
}
|
|
2131
|
+
if (offer.bodyHash !== void 0 || offer.contentType !== void 0) {
|
|
2132
|
+
return {
|
|
2133
|
+
code: "UNEXPECTED_BODY_BINDING",
|
|
2134
|
+
reason: "offer binds a request body, but this request carries none"
|
|
2135
|
+
};
|
|
2136
|
+
}
|
|
2137
|
+
return null;
|
|
2138
|
+
}
|
|
2139
|
+
|
|
2140
|
+
// src/sponsor.ts
|
|
2141
|
+
import { setTimeout as delay } from "timers/promises";
|
|
2142
|
+
import { Transaction as Transaction3 } from "@mysten/sui/transactions";
|
|
2143
|
+
import { fromBase64 as fromBase643, normalizeStructTag as normalizeStructTag4, normalizeSuiAddress as normalizeSuiAddress3 } from "@mysten/sui/utils";
|
|
2144
|
+
function paidRequestInit(request, extraHeaders = {}) {
|
|
2145
|
+
const headers = { ...extraHeaders };
|
|
2146
|
+
if (request.contentType) headers["content-type"] = request.contentType;
|
|
2147
|
+
return {
|
|
2148
|
+
method: request.method,
|
|
2149
|
+
redirect: "manual",
|
|
2150
|
+
headers,
|
|
2151
|
+
// Uint8Array is a valid BodyInit at runtime; the cast only bridges the
|
|
2152
|
+
// ArrayBufferLike generic that lib.dom's BodyInit does not spell out.
|
|
2153
|
+
...request.method === "POST" ? { body: request.body } : {}
|
|
2154
|
+
};
|
|
2155
|
+
}
|
|
2156
|
+
|
|
2157
|
+
// src/shared-pool-pay.ts
|
|
2158
|
+
var FINALIZATION_ATTEMPTS = 20;
|
|
2159
|
+
var FINALIZATION_DELAY_MS = 250;
|
|
2160
|
+
var SUI_CLOCK2 = "0x6";
|
|
2161
|
+
function traceEmitter(trace) {
|
|
2162
|
+
if (!trace) return () => void 0;
|
|
2163
|
+
const now = trace.now ?? Date.now;
|
|
2164
|
+
return (input) => {
|
|
2165
|
+
try {
|
|
2166
|
+
void Promise.resolve(
|
|
2167
|
+
trace.sink.emit({
|
|
2168
|
+
traceId: trace.traceId,
|
|
2169
|
+
requestId: trace.requestId,
|
|
2170
|
+
buyerAccountId: trace.buyerAccountId,
|
|
2171
|
+
connectionId: trace.connectionId,
|
|
2172
|
+
at: new Date(now()).toISOString(),
|
|
2173
|
+
source: input.source ?? "suipay",
|
|
2174
|
+
kind: input.kind,
|
|
2175
|
+
status: input.status,
|
|
2176
|
+
summary: input.summary,
|
|
2177
|
+
publicPayload: input.publicPayload,
|
|
2178
|
+
...input.technicalPayload ? { technicalPayload: input.technicalPayload } : {}
|
|
2179
|
+
})
|
|
2180
|
+
).catch(() => void 0);
|
|
2181
|
+
} catch {
|
|
2182
|
+
}
|
|
2183
|
+
};
|
|
2184
|
+
}
|
|
2185
|
+
function traceHeadersFor(deps, method, url) {
|
|
2186
|
+
const trace = deps.trace;
|
|
2187
|
+
if (!trace?.secret) return {};
|
|
2188
|
+
try {
|
|
2189
|
+
return internalTraceHeaderRecord(
|
|
2190
|
+
{
|
|
2191
|
+
traceId: trace.traceId,
|
|
2192
|
+
connectionId: trace.connectionId,
|
|
2193
|
+
requestId: trace.requestId,
|
|
2194
|
+
method,
|
|
2195
|
+
pathname: new URL(url).pathname
|
|
2196
|
+
},
|
|
2197
|
+
trace.secret
|
|
2198
|
+
);
|
|
2199
|
+
} catch {
|
|
2200
|
+
return {};
|
|
2201
|
+
}
|
|
2202
|
+
}
|
|
2203
|
+
function failed(code, detail, txDigest) {
|
|
2204
|
+
return {
|
|
2205
|
+
status: "failed",
|
|
2206
|
+
paid: false,
|
|
2207
|
+
code,
|
|
2208
|
+
detail,
|
|
2209
|
+
...txDigest ? { txDigest } : {}
|
|
2210
|
+
};
|
|
2211
|
+
}
|
|
2212
|
+
function sameAddress(a, b) {
|
|
2213
|
+
try {
|
|
2214
|
+
return normalizeSuiAddress4(a) === normalizeSuiAddress4(b);
|
|
2215
|
+
} catch {
|
|
2216
|
+
return false;
|
|
2217
|
+
}
|
|
2218
|
+
}
|
|
2219
|
+
function sameStructTag(a, b) {
|
|
2220
|
+
try {
|
|
2221
|
+
return normalizeStructTag5(a) === normalizeStructTag5(b);
|
|
2222
|
+
} catch {
|
|
2223
|
+
return false;
|
|
2224
|
+
}
|
|
2225
|
+
}
|
|
2226
|
+
async function sponsorPost(fetchImpl, url, body, traceHeaders = {}) {
|
|
2227
|
+
try {
|
|
2228
|
+
return await fetchImpl(url, {
|
|
2229
|
+
method: "POST",
|
|
2230
|
+
headers: { "Content-Type": "application/json", ...traceHeaders },
|
|
2231
|
+
body: JSON.stringify(body)
|
|
2232
|
+
});
|
|
2233
|
+
} catch {
|
|
2234
|
+
return null;
|
|
2235
|
+
}
|
|
2236
|
+
}
|
|
2237
|
+
var PRE_BROADCAST_ERROR_CODES = /* @__PURE__ */ new Set([
|
|
2238
|
+
"ECONNREFUSED",
|
|
2239
|
+
"ENOTFOUND",
|
|
2240
|
+
"EAI_AGAIN",
|
|
2241
|
+
"EHOSTUNREACH",
|
|
2242
|
+
"ENETUNREACH"
|
|
2243
|
+
]);
|
|
2244
|
+
function isProvablyPreBroadcast(err) {
|
|
2245
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2246
|
+
let cur = err;
|
|
2247
|
+
while (cur && typeof cur === "object" && !seen.has(cur)) {
|
|
2248
|
+
seen.add(cur);
|
|
2249
|
+
const code = cur.code;
|
|
2250
|
+
if (typeof code === "string" && PRE_BROADCAST_ERROR_CODES.has(code)) {
|
|
2251
|
+
return true;
|
|
2252
|
+
}
|
|
2253
|
+
cur = cur.cause;
|
|
2254
|
+
}
|
|
2255
|
+
return false;
|
|
2256
|
+
}
|
|
2257
|
+
async function responseDetail(response) {
|
|
2258
|
+
const body = await readBody(response);
|
|
2259
|
+
return typeof body === "string" ? body : JSON.stringify(body);
|
|
2260
|
+
}
|
|
2261
|
+
async function buildSharedPoolPayKind(input) {
|
|
2262
|
+
if (!input.termsHash.length) throw new Error("termsHash must be non-empty");
|
|
2263
|
+
if (!input.paymentIdHash.length) throw new Error("paymentIdHash must be non-empty");
|
|
2264
|
+
const auth = defaultAuthorityForRail("shared_pool", input.packageId);
|
|
2265
|
+
const target = `${normalizeSuiAddress4(auth.packageId)}::${auth.module}::${auth.payFunction}`;
|
|
2266
|
+
const hashHex = input.targetHash.replace(/^0x/i, "").toLowerCase();
|
|
2267
|
+
if (!/^[0-9a-f]{64}$/.test(hashHex)) throw new Error("targetHash must be 32-byte hex");
|
|
2268
|
+
const hash = Uint8Array.from(Buffer.from(hashHex, "hex"));
|
|
2269
|
+
const tx = new Transaction4();
|
|
2270
|
+
tx.setSender(input.delegate);
|
|
2271
|
+
tx.moveCall({
|
|
2272
|
+
target,
|
|
2273
|
+
typeArguments: [input.coinType],
|
|
2274
|
+
arguments: [
|
|
2275
|
+
tx.sharedObjectRef({
|
|
2276
|
+
objectId: input.poolObjectId,
|
|
2277
|
+
initialSharedVersion: "1",
|
|
2278
|
+
mutable: true
|
|
2279
|
+
}),
|
|
2280
|
+
tx.sharedObjectRef({
|
|
2281
|
+
objectId: input.grantObjectId,
|
|
2282
|
+
initialSharedVersion: "1",
|
|
2283
|
+
mutable: true
|
|
2284
|
+
}),
|
|
2285
|
+
tx.pure.u64(BigInt(input.policyId)),
|
|
2286
|
+
tx.pure.vector("u8", Array.from(hash)),
|
|
2287
|
+
tx.pure.u64(BigInt(input.amount)),
|
|
2288
|
+
tx.pure.vector("u8", Array.from(input.paymentIdHash)),
|
|
2289
|
+
tx.pure.vector("u8", Array.from(input.termsHash)),
|
|
2290
|
+
tx.sharedObjectRef({
|
|
2291
|
+
objectId: SUI_CLOCK2,
|
|
2292
|
+
initialSharedVersion: "1",
|
|
2293
|
+
mutable: false
|
|
2294
|
+
})
|
|
2295
|
+
]
|
|
2296
|
+
});
|
|
2297
|
+
const bytes = await tx.build({ onlyTransactionKind: true });
|
|
2298
|
+
return { kindBytes: toBase643(bytes) };
|
|
2299
|
+
}
|
|
2300
|
+
function assertSharedPoolPayReconstructs(bytes, expected) {
|
|
2301
|
+
let data;
|
|
2302
|
+
try {
|
|
2303
|
+
data = Transaction4.from(fromBase644(bytes)).getData();
|
|
2304
|
+
} catch {
|
|
2305
|
+
throw new Error("sponsored transaction is not valid Sui transaction bytes");
|
|
2306
|
+
}
|
|
2307
|
+
if (!data.sender || !sameAddress(data.sender, expected.delegate)) {
|
|
2308
|
+
throw new Error("sponsored transaction sender does not match delegate");
|
|
2309
|
+
}
|
|
2310
|
+
if (!data.commands || data.commands.length !== 1) {
|
|
2311
|
+
throw new Error("sponsored transaction must contain exactly one command");
|
|
2312
|
+
}
|
|
2313
|
+
const auth = defaultAuthorityForRail("shared_pool", expected.packageId);
|
|
2314
|
+
const wanted = `${normalizeSuiAddress4(auth.packageId)}::${auth.module}::${auth.payFunction}`;
|
|
2315
|
+
const cmd = data.commands[0];
|
|
2316
|
+
if (cmd.$kind !== "MoveCall" || !cmd.MoveCall) {
|
|
2317
|
+
throw new Error("sponsored transaction is not a MoveCall");
|
|
2318
|
+
}
|
|
2319
|
+
const got = `${normalizeSuiAddress4(String(cmd.MoveCall.package))}::${cmd.MoveCall.module}::${cmd.MoveCall.function}`;
|
|
2320
|
+
if (got !== wanted) {
|
|
2321
|
+
throw new Error(`sponsored transaction does not reconstruct ${wanted}`);
|
|
2322
|
+
}
|
|
2323
|
+
let expectedKind;
|
|
2324
|
+
try {
|
|
2325
|
+
expectedKind = Transaction4.fromKind(fromBase644(expected.kindBytes)).getData();
|
|
2326
|
+
} catch {
|
|
2327
|
+
throw new Error("sponsored transaction kind baseline invalid");
|
|
2328
|
+
}
|
|
2329
|
+
if (JSON.stringify(data.commands) !== JSON.stringify(expectedKind.commands)) {
|
|
2330
|
+
throw new Error("sponsored transaction commands do not match the intended payment");
|
|
2331
|
+
}
|
|
2332
|
+
if (JSON.stringify(data.inputs) !== JSON.stringify(expectedKind.inputs)) {
|
|
2333
|
+
throw new Error(
|
|
2334
|
+
"sponsored transaction inputs do not match the intended payment (pool/grant/amount/target tampered)"
|
|
2335
|
+
);
|
|
2336
|
+
}
|
|
2337
|
+
void expected.poolObjectId;
|
|
2338
|
+
void expected.grantObjectId;
|
|
2339
|
+
void expected.amount;
|
|
2340
|
+
}
|
|
2341
|
+
async function signWithSeed(seed, txBytes, expectedDelegate) {
|
|
2342
|
+
const kp = Ed25519Keypair2.fromSecretKey(Uint8Array.from(seed));
|
|
2343
|
+
const address = kp.getPublicKey().toSuiAddress();
|
|
2344
|
+
if (!sameAddress(address, expectedDelegate)) {
|
|
2345
|
+
throw new Error("decrypted delegate seed does not match grant delegateAddress");
|
|
2346
|
+
}
|
|
2347
|
+
const { signature } = await kp.signTransaction(fromBase644(txBytes));
|
|
2348
|
+
return signature;
|
|
2349
|
+
}
|
|
2350
|
+
async function paySharedPoolResource(args) {
|
|
2351
|
+
const { session, cfg } = args;
|
|
2352
|
+
const deps = args.deps ?? {};
|
|
2353
|
+
const fetchImpl = deps.fetchImpl ?? fetch;
|
|
2354
|
+
const preferred = args.preferredDialect ?? "mpp";
|
|
2355
|
+
const trace = traceEmitter(deps.trace);
|
|
2356
|
+
let request;
|
|
2357
|
+
let resourceUrl;
|
|
2358
|
+
let gatewayOrigin;
|
|
2359
|
+
try {
|
|
2360
|
+
request = args.request ?? normalizePaidHttpRequest({ url: args.url, method: "GET" });
|
|
2361
|
+
resourceUrl = new URL(request.url);
|
|
2362
|
+
gatewayOrigin = new URL(cfg.gatewayUrl).origin;
|
|
2363
|
+
} catch {
|
|
2364
|
+
return failed("POLICY_REJECTED", "resource and gateway URLs must be valid");
|
|
2365
|
+
}
|
|
2366
|
+
if (!["http:", "https:"].includes(resourceUrl.protocol) || resourceUrl.origin !== gatewayOrigin) {
|
|
2367
|
+
return failed("POLICY_REJECTED", "pay only accepts configured gateway origin");
|
|
2368
|
+
}
|
|
2369
|
+
const first = await fetchImpl(resourceUrl, paidRequestInit(request));
|
|
2370
|
+
if (first.status !== 402) {
|
|
2371
|
+
return { status: "ok", paid: false, body: await readBody(first) };
|
|
2372
|
+
}
|
|
2373
|
+
let dialect = preferred;
|
|
2374
|
+
let offer = await readChallenge(first, preferred);
|
|
2375
|
+
if (!offer && preferred === "mpp") {
|
|
2376
|
+
offer = await readChallenge(first, "x402");
|
|
2377
|
+
dialect = "x402";
|
|
2378
|
+
} else if (!offer && preferred === "x402") {
|
|
2379
|
+
offer = await readChallenge(first, "mpp");
|
|
2380
|
+
dialect = "mpp";
|
|
2381
|
+
}
|
|
2382
|
+
if (!offer) {
|
|
2383
|
+
trace({
|
|
2384
|
+
kind: "payment_failed",
|
|
2385
|
+
status: "error",
|
|
2386
|
+
summary: "Payment required, but the challenge could not be read",
|
|
2387
|
+
publicPayload: { stage: "challenge", failureCode: "MALFORMED_CHALLENGE" }
|
|
2388
|
+
});
|
|
2389
|
+
return failed("MALFORMED_CHALLENGE", "402 carried no readable mpp/x402 challenge");
|
|
2390
|
+
}
|
|
2391
|
+
trace({
|
|
2392
|
+
kind: "challenge_received",
|
|
2393
|
+
status: "ok",
|
|
2394
|
+
summary: `Payment required: ${offer.amount} ${offer.asset}`,
|
|
2395
|
+
publicPayload: {
|
|
2396
|
+
serviceId: offer.serviceId,
|
|
2397
|
+
amountAtomic: offer.amount,
|
|
2398
|
+
asset: offer.asset,
|
|
2399
|
+
decimals: offer.decimals,
|
|
2400
|
+
rail: offer.settlementRail ?? "shared_pool",
|
|
2401
|
+
dialect,
|
|
2402
|
+
httpStatus: 402
|
|
2403
|
+
},
|
|
2404
|
+
technicalPayload: {
|
|
2405
|
+
serviceId: offer.serviceId,
|
|
2406
|
+
amountAtomic: offer.amount,
|
|
2407
|
+
asset: offer.asset,
|
|
2408
|
+
decimals: offer.decimals,
|
|
2409
|
+
rail: offer.settlementRail ?? "shared_pool",
|
|
2410
|
+
dialect,
|
|
2411
|
+
httpStatus: 402,
|
|
2412
|
+
network: offer.network,
|
|
2413
|
+
payTo: offer.payTo,
|
|
2414
|
+
challengeId: offer.challengeId
|
|
2415
|
+
}
|
|
2416
|
+
});
|
|
2417
|
+
const binding = requestBindingViolation(offer, request);
|
|
2418
|
+
if (binding) return failed("POLICY_REJECTED", binding.reason);
|
|
2419
|
+
if (offer.settlementRail && offer.settlementRail !== "shared_pool") {
|
|
2420
|
+
return failed(
|
|
2421
|
+
"POLICY_REJECTED",
|
|
2422
|
+
`offer settlementRail is ${offer.settlementRail}, not shared_pool`
|
|
2423
|
+
);
|
|
2424
|
+
}
|
|
2425
|
+
if (!offer.targetHash) {
|
|
2426
|
+
return failed("POLICY_REJECTED", "shared_pool offer missing targetHash");
|
|
2427
|
+
}
|
|
2428
|
+
if (!offer.packageId?.trim()) {
|
|
2429
|
+
return failed("POLICY_REJECTED", "shared_pool offer missing packageId");
|
|
2430
|
+
}
|
|
2431
|
+
try {
|
|
2432
|
+
if (normalizeSuiAddress4(offer.packageId) !== normalizeSuiAddress4(session.packageId)) {
|
|
2433
|
+
return failed(
|
|
2434
|
+
"POLICY_REJECTED",
|
|
2435
|
+
`offer.packageId ${offer.packageId} !== session.packageId ${session.packageId}`
|
|
2436
|
+
);
|
|
2437
|
+
}
|
|
2438
|
+
} catch {
|
|
2439
|
+
return failed("POLICY_REJECTED", "invalid package id on offer or session");
|
|
2440
|
+
}
|
|
2441
|
+
if (offer.network !== `sui:${cfg.network}`) {
|
|
2442
|
+
return failed(
|
|
2443
|
+
"POLICY_REJECTED",
|
|
2444
|
+
`offer.network ${offer.network ?? "(none)"} does not match sui:${cfg.network}`
|
|
2445
|
+
);
|
|
2446
|
+
}
|
|
2447
|
+
const authority = defaultAuthorityForRail("shared_pool", session.packageId);
|
|
2448
|
+
let mode;
|
|
2449
|
+
try {
|
|
2450
|
+
mode = dialectMode(authority, dialect);
|
|
2451
|
+
} catch (err) {
|
|
2452
|
+
return failed(
|
|
2453
|
+
"POLICY_REJECTED",
|
|
2454
|
+
err instanceof Error ? err.message : "dialect not in shared_pool ceiling"
|
|
2455
|
+
);
|
|
2456
|
+
}
|
|
2457
|
+
const resolved = resolveGrantTarget(session.snapshot, offer.targetHash);
|
|
2458
|
+
if (!resolved.ok) {
|
|
2459
|
+
trace({
|
|
2460
|
+
kind: "payment_failed",
|
|
2461
|
+
status: "error",
|
|
2462
|
+
summary: "Authority denied this service",
|
|
2463
|
+
publicPayload: {
|
|
2464
|
+
serviceId: offer.serviceId,
|
|
2465
|
+
stage: "authority",
|
|
2466
|
+
failureCode: "GRANT_DENIED"
|
|
2467
|
+
},
|
|
2468
|
+
technicalPayload: {
|
|
2469
|
+
serviceId: offer.serviceId,
|
|
2470
|
+
stage: "authority",
|
|
2471
|
+
failureCode: "GRANT_DENIED",
|
|
2472
|
+
errorMessage: resolved.reason
|
|
2473
|
+
}
|
|
2474
|
+
});
|
|
2475
|
+
return failed("GRANT_DENIED", resolved.reason);
|
|
2476
|
+
}
|
|
2477
|
+
trace({
|
|
2478
|
+
kind: "authority_matched",
|
|
2479
|
+
status: "ok",
|
|
2480
|
+
summary: "Service allowed, caps live, grant active",
|
|
2481
|
+
publicPayload: {
|
|
2482
|
+
serviceId: offer.serviceId,
|
|
2483
|
+
rail: "shared_pool",
|
|
2484
|
+
decision: "allowed"
|
|
2485
|
+
},
|
|
2486
|
+
technicalPayload: {
|
|
2487
|
+
serviceId: offer.serviceId,
|
|
2488
|
+
rail: "shared_pool",
|
|
2489
|
+
decision: "allowed",
|
|
2490
|
+
grantId: session.snapshot.grantId,
|
|
2491
|
+
policyId: resolved.policyId,
|
|
2492
|
+
...resolved.maxPerPayment !== void 0 ? { capsRemaining: resolved.maxPerPayment } : {}
|
|
2493
|
+
}
|
|
2494
|
+
});
|
|
2495
|
+
if (!sameStructTag(resolved.coinType, offer.asset)) {
|
|
2496
|
+
return failed(
|
|
2497
|
+
"POLICY_REJECTED",
|
|
2498
|
+
`grant coin ${resolved.coinType} does not match offer asset ${offer.asset}`
|
|
2499
|
+
);
|
|
2500
|
+
}
|
|
2501
|
+
if (resolved.maxPerPayment !== void 0) {
|
|
2502
|
+
if (BigInt(offer.amount) > BigInt(resolved.maxPerPayment)) {
|
|
2503
|
+
return failed(
|
|
2504
|
+
"POLICY_REJECTED",
|
|
2505
|
+
`price ${offer.amount} exceeds target maxPerPayment ${resolved.maxPerPayment}`
|
|
2506
|
+
);
|
|
2507
|
+
}
|
|
2508
|
+
}
|
|
2509
|
+
const payIdHash = paymentIdHash(offer.challengeId);
|
|
2510
|
+
const terms = termsHash(offer);
|
|
2511
|
+
const build = deps.buildPayKind ?? ((input) => buildSharedPoolPayKind({
|
|
2512
|
+
packageId: session.packageId,
|
|
2513
|
+
...input
|
|
2514
|
+
}));
|
|
2515
|
+
let kindBytes;
|
|
2516
|
+
try {
|
|
2517
|
+
const built = await build({
|
|
2518
|
+
delegate: resolved.delegateAddress,
|
|
2519
|
+
coinType: resolved.coinType,
|
|
2520
|
+
poolObjectId: resolved.poolObjectId,
|
|
2521
|
+
grantObjectId: resolved.grantObjectId,
|
|
2522
|
+
policyId: resolved.onChainPolicyId,
|
|
2523
|
+
targetHash: resolved.targetHash,
|
|
2524
|
+
amount: offer.amount,
|
|
2525
|
+
paymentIdHash: payIdHash,
|
|
2526
|
+
termsHash: terms
|
|
2527
|
+
});
|
|
2528
|
+
kindBytes = built.kindBytes;
|
|
2529
|
+
} catch (err) {
|
|
2530
|
+
return failed(
|
|
2531
|
+
"BUILD_FAILED",
|
|
2532
|
+
err instanceof Error ? err.message : "failed to build shared_pool pay kind"
|
|
2533
|
+
);
|
|
2534
|
+
}
|
|
2535
|
+
const payTarget = `${normalizeSuiAddress4(authority.packageId)}::${authority.module}::${authority.payFunction}`;
|
|
2536
|
+
const sponsorUrl = cfg.sponsorUrl.replace(/\/$/, "");
|
|
2537
|
+
trace({
|
|
2538
|
+
kind: "sponsor_requested",
|
|
2539
|
+
status: "started",
|
|
2540
|
+
summary: "Requesting gas sponsorship from SuiPay",
|
|
2541
|
+
publicPayload: { sponsor: "suipay" },
|
|
2542
|
+
technicalPayload: { sponsor: "suipay", serviceId: offer.serviceId }
|
|
2543
|
+
});
|
|
2544
|
+
const createResponse = await sponsorPost(
|
|
2545
|
+
fetchImpl,
|
|
2546
|
+
sponsorUrl,
|
|
2547
|
+
{
|
|
2548
|
+
transactionKindBytes: kindBytes,
|
|
2549
|
+
sender: resolved.delegateAddress,
|
|
2550
|
+
allowedMoveCallTargets: [payTarget]
|
|
2551
|
+
},
|
|
2552
|
+
{
|
|
2553
|
+
...traceHeadersFor(deps, "POST", sponsorUrl),
|
|
2554
|
+
...session.sponsorAuthorization ? { Authorization: session.sponsorAuthorization } : {}
|
|
2555
|
+
}
|
|
2556
|
+
);
|
|
2557
|
+
if (!createResponse || !createResponse.ok) {
|
|
2558
|
+
trace({
|
|
2559
|
+
kind: "payment_failed",
|
|
2560
|
+
status: "error",
|
|
2561
|
+
summary: "Gas sponsor unavailable",
|
|
2562
|
+
publicPayload: {
|
|
2563
|
+
serviceId: offer.serviceId,
|
|
2564
|
+
stage: "sponsor",
|
|
2565
|
+
failureCode: "SPONSOR_UNAVAILABLE"
|
|
2566
|
+
}
|
|
2567
|
+
});
|
|
2568
|
+
return failed("SPONSOR_UNAVAILABLE", "platform sponsor could not create transaction");
|
|
2569
|
+
}
|
|
2570
|
+
let sponsored;
|
|
2571
|
+
try {
|
|
2572
|
+
const body = await createResponse.json();
|
|
2573
|
+
if (typeof body.bytes !== "string" || typeof body.digest !== "string") {
|
|
2574
|
+
throw new Error("invalid");
|
|
2575
|
+
}
|
|
2576
|
+
sponsored = { bytes: body.bytes, digest: body.digest };
|
|
2577
|
+
} catch {
|
|
2578
|
+
trace({
|
|
2579
|
+
kind: "payment_failed",
|
|
2580
|
+
status: "error",
|
|
2581
|
+
summary: "Gas sponsor returned an unusable transaction",
|
|
2582
|
+
publicPayload: {
|
|
2583
|
+
serviceId: offer.serviceId,
|
|
2584
|
+
stage: "sponsor",
|
|
2585
|
+
failureCode: "SPONSOR_INVALID_RESPONSE"
|
|
2586
|
+
}
|
|
2587
|
+
});
|
|
2588
|
+
return failed(
|
|
2589
|
+
"SPONSOR_INVALID_RESPONSE",
|
|
2590
|
+
"platform sponsor returned invalid transaction response"
|
|
2591
|
+
);
|
|
2592
|
+
}
|
|
2593
|
+
trace({
|
|
2594
|
+
kind: "transaction_sponsored",
|
|
2595
|
+
status: "ok",
|
|
2596
|
+
summary: "Gas attached by SuiPay",
|
|
2597
|
+
publicPayload: { sponsor: "suipay" },
|
|
2598
|
+
technicalPayload: { sponsor: "suipay", serviceId: offer.serviceId }
|
|
2599
|
+
});
|
|
2600
|
+
try {
|
|
2601
|
+
assertSharedPoolPayReconstructs(sponsored.bytes, {
|
|
2602
|
+
kindBytes,
|
|
2603
|
+
packageId: session.packageId,
|
|
2604
|
+
delegate: resolved.delegateAddress,
|
|
2605
|
+
poolObjectId: resolved.poolObjectId,
|
|
2606
|
+
grantObjectId: resolved.grantObjectId,
|
|
2607
|
+
amount: offer.amount
|
|
2608
|
+
});
|
|
2609
|
+
} catch (error2) {
|
|
2610
|
+
trace({
|
|
2611
|
+
kind: "payment_failed",
|
|
2612
|
+
status: "error",
|
|
2613
|
+
summary: "Sponsored transaction did not reconstruct the intended payment",
|
|
2614
|
+
publicPayload: {
|
|
2615
|
+
serviceId: offer.serviceId,
|
|
2616
|
+
stage: "sponsored_bytes",
|
|
2617
|
+
failureCode: "SPONSORED_TX_MISMATCH"
|
|
2618
|
+
}
|
|
2619
|
+
});
|
|
2620
|
+
return failed(
|
|
2621
|
+
"SPONSORED_TX_MISMATCH",
|
|
2622
|
+
error2 instanceof Error ? error2.message : "sponsored transaction verification failed"
|
|
2623
|
+
);
|
|
2624
|
+
}
|
|
2625
|
+
if (session.assertLive) {
|
|
2626
|
+
try {
|
|
2627
|
+
await session.assertLive();
|
|
2628
|
+
} catch (err) {
|
|
2629
|
+
trace({
|
|
2630
|
+
kind: "payment_failed",
|
|
2631
|
+
status: "error",
|
|
2632
|
+
summary: "Authority changed before signing; payment stopped",
|
|
2633
|
+
publicPayload: {
|
|
2634
|
+
serviceId: offer.serviceId,
|
|
2635
|
+
stage: "authority_revalidate",
|
|
2636
|
+
failureCode: "GRANT_DENIED"
|
|
2637
|
+
},
|
|
2638
|
+
technicalPayload: {
|
|
2639
|
+
serviceId: offer.serviceId,
|
|
2640
|
+
stage: "authority_revalidate",
|
|
2641
|
+
failureCode: "GRANT_DENIED",
|
|
2642
|
+
errorMessage: err instanceof Error ? err.message : "grant no longer live"
|
|
2643
|
+
}
|
|
2644
|
+
});
|
|
2645
|
+
return failed(
|
|
2646
|
+
"GRANT_DENIED",
|
|
2647
|
+
err instanceof Error ? err.message : "grant no longer live at decrypt"
|
|
2648
|
+
);
|
|
2649
|
+
}
|
|
2650
|
+
trace({
|
|
2651
|
+
kind: "authority_revalidated",
|
|
2652
|
+
status: "ok",
|
|
2653
|
+
summary: "Authority unchanged before signing",
|
|
2654
|
+
publicPayload: { decision: "allowed" },
|
|
2655
|
+
technicalPayload: {
|
|
2656
|
+
decision: "allowed",
|
|
2657
|
+
grantId: session.snapshot.grantId
|
|
2658
|
+
}
|
|
2659
|
+
});
|
|
2660
|
+
}
|
|
2661
|
+
let signature;
|
|
2662
|
+
{
|
|
2663
|
+
let seed = null;
|
|
2664
|
+
try {
|
|
2665
|
+
seed = await session.decryptDelegateSeed();
|
|
2666
|
+
signature = await signWithSeed(
|
|
2667
|
+
seed,
|
|
2668
|
+
sponsored.bytes,
|
|
2669
|
+
resolved.delegateAddress
|
|
2670
|
+
);
|
|
2671
|
+
} catch (err) {
|
|
2672
|
+
trace({
|
|
2673
|
+
kind: "payment_failed",
|
|
2674
|
+
status: "error",
|
|
2675
|
+
summary: "Delegate signature failed",
|
|
2676
|
+
publicPayload: {
|
|
2677
|
+
serviceId: offer.serviceId,
|
|
2678
|
+
stage: "sign",
|
|
2679
|
+
failureCode: "SIGN_FAILED"
|
|
2680
|
+
}
|
|
2681
|
+
});
|
|
2682
|
+
return failed(
|
|
2683
|
+
"SIGN_FAILED",
|
|
2684
|
+
err instanceof Error ? err.message : "delegate sign failed"
|
|
2685
|
+
);
|
|
2686
|
+
} finally {
|
|
2687
|
+
if (seed) {
|
|
2688
|
+
if (Buffer.isBuffer(seed)) seed.fill(0);
|
|
2689
|
+
else seed.fill(0);
|
|
2690
|
+
}
|
|
2691
|
+
seed = null;
|
|
2692
|
+
}
|
|
2693
|
+
}
|
|
2694
|
+
trace({
|
|
2695
|
+
kind: "delegate_signed",
|
|
2696
|
+
status: "ok",
|
|
2697
|
+
summary: "Delegate approved transaction",
|
|
2698
|
+
publicPayload: { delegateAddress: resolved.delegateAddress },
|
|
2699
|
+
technicalPayload: { delegateAddress: resolved.delegateAddress }
|
|
2700
|
+
});
|
|
2701
|
+
if (dialect === "x402" && mode === "transaction") {
|
|
2702
|
+
void sponsored.digest;
|
|
2703
|
+
const credential = encodeCredential({
|
|
2704
|
+
challengeId: offer.challengeId,
|
|
2705
|
+
offer,
|
|
2706
|
+
txBytes: sponsored.bytes,
|
|
2707
|
+
signature,
|
|
2708
|
+
payer: resolved.delegateAddress
|
|
2709
|
+
});
|
|
2710
|
+
const second2 = await fetchImpl(
|
|
2711
|
+
resourceUrl,
|
|
2712
|
+
paidRequestInit(request, { [X402_CREDENTIAL_HEADER]: credential })
|
|
2713
|
+
);
|
|
2714
|
+
if (second2.status === 503) {
|
|
2715
|
+
return {
|
|
2716
|
+
status: "ambiguous",
|
|
2717
|
+
paid: false,
|
|
2718
|
+
code: second2.headers.get("X-Suipay-Error") ?? "AMBIGUOUS",
|
|
2719
|
+
detail: await responseDetail(second2),
|
|
2720
|
+
txDigest: sponsored.digest
|
|
2721
|
+
};
|
|
2722
|
+
}
|
|
2723
|
+
if (second2.status !== 200) {
|
|
2724
|
+
return failed(
|
|
2725
|
+
second2.headers.get("X-Suipay-Error") ?? `HTTP_${second2.status}`,
|
|
2726
|
+
await responseDetail(second2),
|
|
2727
|
+
sponsored.digest
|
|
2728
|
+
);
|
|
2729
|
+
}
|
|
2730
|
+
return {
|
|
2731
|
+
status: "ok",
|
|
2732
|
+
paid: true,
|
|
2733
|
+
body: await readBody(second2),
|
|
2734
|
+
receipt: readReceipt(second2, "x402"),
|
|
2735
|
+
txDigest: sponsored.digest
|
|
2736
|
+
};
|
|
2737
|
+
}
|
|
2738
|
+
const executeUrl = `${sponsorUrl}/execute`;
|
|
2739
|
+
let executeResponse;
|
|
2740
|
+
try {
|
|
2741
|
+
executeResponse = await fetchImpl(executeUrl, {
|
|
2742
|
+
method: "POST",
|
|
2743
|
+
headers: {
|
|
2744
|
+
"Content-Type": "application/json",
|
|
2745
|
+
...traceHeadersFor(deps, "POST", executeUrl),
|
|
2746
|
+
...session.sponsorAuthorization ? { Authorization: session.sponsorAuthorization } : {}
|
|
2747
|
+
},
|
|
2748
|
+
body: JSON.stringify({ digest: sponsored.digest, signature })
|
|
2749
|
+
});
|
|
2750
|
+
} catch (err) {
|
|
2751
|
+
if (isProvablyPreBroadcast(err)) {
|
|
2752
|
+
trace({
|
|
2753
|
+
kind: "payment_failed",
|
|
2754
|
+
status: "error",
|
|
2755
|
+
summary: "Sponsor could not submit the payment",
|
|
2756
|
+
publicPayload: {
|
|
2757
|
+
serviceId: offer.serviceId,
|
|
2758
|
+
stage: "submit",
|
|
2759
|
+
failureCode: "SPONSOR_UNAVAILABLE"
|
|
2760
|
+
}
|
|
2761
|
+
});
|
|
2762
|
+
return failed("SPONSOR_UNAVAILABLE", "platform sponsor could not execute transaction");
|
|
2763
|
+
}
|
|
2764
|
+
trace({
|
|
2765
|
+
kind: "payment_ambiguous",
|
|
2766
|
+
status: "ambiguous",
|
|
2767
|
+
summary: "Sponsor submission response was lost; outcome unknown",
|
|
2768
|
+
publicPayload: {
|
|
2769
|
+
serviceId: offer.serviceId,
|
|
2770
|
+
stage: "submit",
|
|
2771
|
+
digest: sponsored.digest
|
|
2772
|
+
},
|
|
2773
|
+
technicalPayload: {
|
|
2774
|
+
serviceId: offer.serviceId,
|
|
2775
|
+
stage: "submit",
|
|
2776
|
+
digest: sponsored.digest,
|
|
2777
|
+
reasonCode: "SPONSOR_UNREACHABLE"
|
|
2778
|
+
}
|
|
2779
|
+
});
|
|
2780
|
+
return {
|
|
2781
|
+
status: "ambiguous",
|
|
2782
|
+
paid: false,
|
|
2783
|
+
code: "SPONSOR_UNREACHABLE",
|
|
2784
|
+
detail: "sponsor execute response was lost after send; outcome unknown",
|
|
2785
|
+
txDigest: sponsored.digest
|
|
2786
|
+
};
|
|
2787
|
+
}
|
|
2788
|
+
if (!executeResponse.ok) {
|
|
2789
|
+
trace({
|
|
2790
|
+
kind: "payment_failed",
|
|
2791
|
+
status: "error",
|
|
2792
|
+
summary: "Sponsor could not submit the payment",
|
|
2793
|
+
publicPayload: {
|
|
2794
|
+
serviceId: offer.serviceId,
|
|
2795
|
+
stage: "submit",
|
|
2796
|
+
failureCode: "SPONSOR_UNAVAILABLE"
|
|
2797
|
+
}
|
|
2798
|
+
});
|
|
2799
|
+
return failed("SPONSOR_UNAVAILABLE", "platform sponsor could not execute transaction");
|
|
2800
|
+
}
|
|
2801
|
+
let finalDigest;
|
|
2802
|
+
try {
|
|
2803
|
+
const body = await executeResponse.json();
|
|
2804
|
+
if (typeof body.digest !== "string") throw new Error("invalid");
|
|
2805
|
+
finalDigest = body.digest;
|
|
2806
|
+
} catch {
|
|
2807
|
+
trace({
|
|
2808
|
+
kind: "payment_ambiguous",
|
|
2809
|
+
status: "ambiguous",
|
|
2810
|
+
summary: "Submission result is unreadable; outcome unknown",
|
|
2811
|
+
publicPayload: {
|
|
2812
|
+
serviceId: offer.serviceId,
|
|
2813
|
+
stage: "submit",
|
|
2814
|
+
digest: sponsored.digest
|
|
2815
|
+
},
|
|
2816
|
+
technicalPayload: {
|
|
2817
|
+
serviceId: offer.serviceId,
|
|
2818
|
+
stage: "submit",
|
|
2819
|
+
digest: sponsored.digest,
|
|
2820
|
+
reasonCode: "SPONSOR_INVALID_RESPONSE"
|
|
2821
|
+
}
|
|
2822
|
+
});
|
|
2823
|
+
return {
|
|
2824
|
+
status: "ambiguous",
|
|
2825
|
+
paid: false,
|
|
2826
|
+
code: "SPONSOR_INVALID_RESPONSE",
|
|
2827
|
+
detail: "platform sponsor returned an unreadable execution response",
|
|
2828
|
+
txDigest: sponsored.digest
|
|
2829
|
+
};
|
|
2830
|
+
}
|
|
2831
|
+
trace({
|
|
2832
|
+
kind: "transaction_submitted",
|
|
2833
|
+
status: "ok",
|
|
2834
|
+
source: "sui",
|
|
2835
|
+
summary: `Submitted ${finalDigest}`,
|
|
2836
|
+
publicPayload: { digest: finalDigest },
|
|
2837
|
+
technicalPayload: { digest: finalDigest, network: offer.network }
|
|
2838
|
+
});
|
|
2839
|
+
const getFinalized = deps.getFinalizedTx ?? (async () => null);
|
|
2840
|
+
let finalized = null;
|
|
2841
|
+
for (let attempt = 0; attempt < FINALIZATION_ATTEMPTS; attempt += 1) {
|
|
2842
|
+
finalized = await getFinalized(finalDigest);
|
|
2843
|
+
if (finalized) break;
|
|
2844
|
+
if (attempt + 1 < FINALIZATION_ATTEMPTS) await delay2(FINALIZATION_DELAY_MS);
|
|
2845
|
+
}
|
|
2846
|
+
if (!finalized) {
|
|
2847
|
+
trace({
|
|
2848
|
+
kind: "payment_ambiguous",
|
|
2849
|
+
status: "ambiguous",
|
|
2850
|
+
source: "sui",
|
|
2851
|
+
summary: "Finality not observed in time; outcome unknown",
|
|
2852
|
+
publicPayload: {
|
|
2853
|
+
serviceId: offer.serviceId,
|
|
2854
|
+
stage: "finality",
|
|
2855
|
+
digest: finalDigest
|
|
2856
|
+
},
|
|
2857
|
+
technicalPayload: {
|
|
2858
|
+
serviceId: offer.serviceId,
|
|
2859
|
+
stage: "finality",
|
|
2860
|
+
digest: finalDigest,
|
|
2861
|
+
reasonCode: "FINALIZATION_TIMEOUT"
|
|
2862
|
+
}
|
|
2863
|
+
});
|
|
2864
|
+
return {
|
|
2865
|
+
status: "ambiguous",
|
|
2866
|
+
paid: false,
|
|
2867
|
+
code: "FINALIZATION_TIMEOUT",
|
|
2868
|
+
detail: "sponsored payment submission is unresolved",
|
|
2869
|
+
txDigest: finalDigest
|
|
2870
|
+
};
|
|
2871
|
+
}
|
|
2872
|
+
if (finalized.status === "failed") {
|
|
2873
|
+
trace({
|
|
2874
|
+
kind: "payment_failed",
|
|
2875
|
+
status: "error",
|
|
2876
|
+
source: "sui",
|
|
2877
|
+
summary: "Payment transaction was rejected on chain",
|
|
2878
|
+
publicPayload: {
|
|
2879
|
+
serviceId: offer.serviceId,
|
|
2880
|
+
stage: "settlement",
|
|
2881
|
+
failureCode: "SETTLEMENT_REJECTED"
|
|
2882
|
+
},
|
|
2883
|
+
technicalPayload: {
|
|
2884
|
+
serviceId: offer.serviceId,
|
|
2885
|
+
stage: "settlement",
|
|
2886
|
+
failureCode: "SETTLEMENT_REJECTED",
|
|
2887
|
+
errorMessage: finalized.reason ?? "tx failed"
|
|
2888
|
+
}
|
|
2889
|
+
});
|
|
2890
|
+
return failed("SETTLEMENT_REJECTED", finalized.reason ?? "tx failed", finalDigest);
|
|
2891
|
+
}
|
|
2892
|
+
trace({
|
|
2893
|
+
kind: "payment_finalized",
|
|
2894
|
+
status: "ok",
|
|
2895
|
+
source: "sui",
|
|
2896
|
+
summary: "PaymentMade matched on Sui",
|
|
2897
|
+
publicPayload: {
|
|
2898
|
+
digest: finalDigest,
|
|
2899
|
+
amountAtomic: offer.amount,
|
|
2900
|
+
asset: offer.asset,
|
|
2901
|
+
decimals: offer.decimals
|
|
2902
|
+
},
|
|
2903
|
+
technicalPayload: {
|
|
2904
|
+
digest: finalDigest,
|
|
2905
|
+
amountAtomic: offer.amount,
|
|
2906
|
+
asset: offer.asset,
|
|
2907
|
+
decimals: offer.decimals,
|
|
2908
|
+
network: offer.network,
|
|
2909
|
+
payTo: offer.payTo
|
|
2910
|
+
}
|
|
2911
|
+
});
|
|
2912
|
+
const proof = encodeProof({
|
|
2913
|
+
challengeId: offer.challengeId,
|
|
2914
|
+
offer,
|
|
2915
|
+
txDigest: finalDigest,
|
|
2916
|
+
payer: resolved.delegateAddress
|
|
2917
|
+
});
|
|
2918
|
+
trace({
|
|
2919
|
+
kind: "proof_presented",
|
|
2920
|
+
status: "ok",
|
|
2921
|
+
summary: "Presenting finalized payment proof",
|
|
2922
|
+
publicPayload: { dialect: "mpp", serviceId: offer.serviceId },
|
|
2923
|
+
technicalPayload: {
|
|
2924
|
+
dialect: "mpp",
|
|
2925
|
+
serviceId: offer.serviceId,
|
|
2926
|
+
digest: finalDigest,
|
|
2927
|
+
proofScheme: "finalized-digest"
|
|
2928
|
+
}
|
|
2929
|
+
});
|
|
2930
|
+
let second;
|
|
2931
|
+
try {
|
|
2932
|
+
second = await fetchImpl(
|
|
2933
|
+
resourceUrl,
|
|
2934
|
+
paidRequestInit(request, {
|
|
2935
|
+
[MPP_CREDENTIAL_HEADER]: proof,
|
|
2936
|
+
...traceHeadersFor(deps, request.method, resourceUrl)
|
|
2937
|
+
})
|
|
2938
|
+
);
|
|
2939
|
+
} catch (err) {
|
|
2940
|
+
trace({
|
|
2941
|
+
kind: "payment_ambiguous",
|
|
2942
|
+
status: "ambiguous",
|
|
2943
|
+
summary: "Payment settled on chain but proof presentation failed",
|
|
2944
|
+
publicPayload: {
|
|
2945
|
+
serviceId: offer.serviceId,
|
|
2946
|
+
stage: "settle",
|
|
2947
|
+
digest: finalDigest
|
|
2948
|
+
},
|
|
2949
|
+
technicalPayload: {
|
|
2950
|
+
serviceId: offer.serviceId,
|
|
2951
|
+
stage: "settle",
|
|
2952
|
+
digest: finalDigest,
|
|
2953
|
+
reasonCode: "PROOF_PRESENTATION_FAILED",
|
|
2954
|
+
errorMessage: err instanceof Error ? err.message : "proof presentation failed"
|
|
2955
|
+
}
|
|
2956
|
+
});
|
|
2957
|
+
return {
|
|
2958
|
+
status: "ambiguous",
|
|
2959
|
+
paid: false,
|
|
2960
|
+
code: "PROOF_PRESENTATION_FAILED",
|
|
2961
|
+
detail: "payment finalized on chain but proof presentation failed; outcome unknown",
|
|
2962
|
+
txDigest: finalDigest
|
|
2963
|
+
};
|
|
2964
|
+
}
|
|
2965
|
+
if (second.status === 503) {
|
|
2966
|
+
trace({
|
|
2967
|
+
kind: "payment_ambiguous",
|
|
2968
|
+
status: "ambiguous",
|
|
2969
|
+
summary: "Settlement outcome unknown at the gateway",
|
|
2970
|
+
publicPayload: {
|
|
2971
|
+
serviceId: offer.serviceId,
|
|
2972
|
+
stage: "settle",
|
|
2973
|
+
digest: finalDigest
|
|
2974
|
+
},
|
|
2975
|
+
technicalPayload: {
|
|
2976
|
+
serviceId: offer.serviceId,
|
|
2977
|
+
stage: "settle",
|
|
2978
|
+
digest: finalDigest,
|
|
2979
|
+
reasonCode: second.headers.get("X-Suipay-Error") ?? "AMBIGUOUS"
|
|
2980
|
+
}
|
|
2981
|
+
});
|
|
2982
|
+
return {
|
|
2983
|
+
status: "ambiguous",
|
|
2984
|
+
paid: false,
|
|
2985
|
+
code: second.headers.get("X-Suipay-Error") ?? "AMBIGUOUS",
|
|
2986
|
+
detail: await responseDetail(second),
|
|
2987
|
+
txDigest: finalDigest
|
|
2988
|
+
};
|
|
2989
|
+
}
|
|
2990
|
+
const receipt = readReceipt(second, "mpp");
|
|
2991
|
+
if (second.status !== 200) {
|
|
2992
|
+
if (!receipt) {
|
|
2993
|
+
trace({
|
|
2994
|
+
kind: "payment_failed",
|
|
2995
|
+
status: "error",
|
|
2996
|
+
summary: "Gateway refused the payment proof",
|
|
2997
|
+
publicPayload: {
|
|
2998
|
+
serviceId: offer.serviceId,
|
|
2999
|
+
stage: "settle",
|
|
3000
|
+
failureCode: second.headers.get("X-Suipay-Error") ?? `HTTP_${second.status}`
|
|
3001
|
+
}
|
|
3002
|
+
});
|
|
3003
|
+
}
|
|
3004
|
+
return failed(
|
|
3005
|
+
second.headers.get("X-Suipay-Error") ?? `HTTP_${second.status}`,
|
|
3006
|
+
await responseDetail(second),
|
|
3007
|
+
finalDigest
|
|
3008
|
+
);
|
|
3009
|
+
}
|
|
3010
|
+
return {
|
|
3011
|
+
status: "ok",
|
|
3012
|
+
paid: true,
|
|
3013
|
+
body: await readBody(second),
|
|
3014
|
+
receipt,
|
|
3015
|
+
txDigest: finalDigest
|
|
3016
|
+
};
|
|
3017
|
+
}
|
|
3018
|
+
|
|
3019
|
+
// src/tools.ts
|
|
3020
|
+
function payerTrace(deps) {
|
|
3021
|
+
const hooks = deps.trace;
|
|
3022
|
+
const buyerAccountId = deps.auth?.buyerAccountId;
|
|
3023
|
+
if (!hooks || !buyerAccountId) return null;
|
|
3024
|
+
return {
|
|
3025
|
+
sink: hooks.sink,
|
|
3026
|
+
traceId: hooks.context.traceId,
|
|
3027
|
+
requestId: hooks.context.requestId,
|
|
3028
|
+
buyerAccountId,
|
|
3029
|
+
connectionId: hooks.runtime.connectionId,
|
|
3030
|
+
...hooks.runtime.secret ? { secret: hooks.runtime.secret } : {},
|
|
3031
|
+
...hooks.runtime.now ? { now: hooks.runtime.now } : {}
|
|
3032
|
+
};
|
|
3033
|
+
}
|
|
3034
|
+
var AUTH_REQUIRED = "No SuiPay credentials - use remote MCP OAuth (/mcp) with a shared_pool grant, or provision a delegated signer for SDK/REST.";
|
|
3035
|
+
function text(value) {
|
|
3036
|
+
return {
|
|
3037
|
+
content: [{
|
|
3038
|
+
type: "text",
|
|
3039
|
+
text: typeof value === "string" ? value : JSON.stringify(value, null, 2)
|
|
3040
|
+
}]
|
|
3041
|
+
};
|
|
3042
|
+
}
|
|
3043
|
+
function error(value) {
|
|
3044
|
+
return { ...text(value), isError: true };
|
|
3045
|
+
}
|
|
3046
|
+
function paidResult(result) {
|
|
3047
|
+
const body = result.body;
|
|
3048
|
+
if (!body || body.__binary !== true) return text(result);
|
|
3049
|
+
const { base64, ...shape } = body;
|
|
3050
|
+
const content = [
|
|
3051
|
+
{ type: "text", text: JSON.stringify({ ...result, body: shape }, null, 2) }
|
|
3052
|
+
];
|
|
3053
|
+
if (base64 && shape.contentType?.startsWith("image/")) {
|
|
3054
|
+
content.push({ type: "image", data: base64, mimeType: shape.contentType });
|
|
3055
|
+
}
|
|
3056
|
+
return { content };
|
|
3057
|
+
}
|
|
3058
|
+
function profile(deps) {
|
|
3059
|
+
return (deps.profileLoader ?? loadPaymentProfile)();
|
|
3060
|
+
}
|
|
3061
|
+
async function pay(args, cfg, deps = {}) {
|
|
3062
|
+
if (!args.url) return error("url is required");
|
|
3063
|
+
let request;
|
|
3064
|
+
try {
|
|
3065
|
+
request = normalizeMcpPaidHttpRequest(args);
|
|
3066
|
+
} catch (err) {
|
|
3067
|
+
return error(err instanceof Error ? err.message : "request could not be normalized");
|
|
3068
|
+
}
|
|
3069
|
+
if (deps.auth?.sharedPoolPay) {
|
|
3070
|
+
const sp = deps.auth.sharedPoolPay;
|
|
3071
|
+
const finality = createSharedPoolFinalityReader({
|
|
3072
|
+
network: cfg.network,
|
|
3073
|
+
url: cfg.rpcUrl
|
|
3074
|
+
});
|
|
3075
|
+
const chain = createSharedPoolChain({
|
|
3076
|
+
packageId: sp.packageId,
|
|
3077
|
+
network: cfg.network,
|
|
3078
|
+
url: cfg.rpcUrl
|
|
3079
|
+
});
|
|
3080
|
+
const trace = payerTrace(deps);
|
|
3081
|
+
const result = await paySharedPoolResource({
|
|
3082
|
+
url: request.url,
|
|
3083
|
+
request,
|
|
3084
|
+
session: {
|
|
3085
|
+
snapshot: sp.snapshot,
|
|
3086
|
+
packageId: sp.packageId,
|
|
3087
|
+
decryptDelegateSeed: sp.decryptDelegateSeed,
|
|
3088
|
+
// Forward the pay-time live gate so paySharedPoolResource re-verifies
|
|
3089
|
+
// the paying authority against a fresh graph before decrypt (P0-5).
|
|
3090
|
+
...sp.assertLive ? { assertLive: sp.assertLive } : {},
|
|
3091
|
+
// Forward the bearer so the gateway-internal /api/sponsor call can
|
|
3092
|
+
// authenticate the delegate (remote /mcp runs the payer server-side).
|
|
3093
|
+
...sp.sponsorAuthorization ? { sponsorAuthorization: sp.sponsorAuthorization } : {}
|
|
3094
|
+
},
|
|
3095
|
+
cfg,
|
|
3096
|
+
deps: {
|
|
3097
|
+
...deps.fetchImpl ? { fetchImpl: deps.fetchImpl } : {},
|
|
3098
|
+
...trace ? { trace } : {},
|
|
3099
|
+
buildPayKind: (input) => chain.buildPayKind(input),
|
|
3100
|
+
getFinalizedTx: async (digest) => {
|
|
3101
|
+
const f = await finality.getFinalizedTx(digest);
|
|
3102
|
+
return f ? { status: f.status, reason: f.reason } : null;
|
|
3103
|
+
}
|
|
3104
|
+
}
|
|
3105
|
+
});
|
|
3106
|
+
return result.status === "ok" ? paidResult(result) : error(result);
|
|
3107
|
+
}
|
|
3108
|
+
if (deps.auth) {
|
|
3109
|
+
return error(
|
|
3110
|
+
"authenticated MCP pay requires a finalized shared_pool grant with session key; local allowance profile is not used on /mcp"
|
|
3111
|
+
);
|
|
3112
|
+
}
|
|
3113
|
+
const paymentProfile = profile(deps);
|
|
3114
|
+
if (paymentProfile) {
|
|
3115
|
+
return error(
|
|
3116
|
+
"local V1 allowance pay is retired; use remote MCP OAuth (/mcp) with a shared_pool grant, or provision a delegated signer for SDK/REST"
|
|
3117
|
+
);
|
|
3118
|
+
}
|
|
3119
|
+
return error(AUTH_REQUIRED);
|
|
3120
|
+
}
|
|
3121
|
+
async function discover(args, cfg, deps = {}) {
|
|
3122
|
+
const url = new URL("/v1/services", cfg.gatewayUrl);
|
|
3123
|
+
if (args.query) url.searchParams.set("q", args.query);
|
|
3124
|
+
const response = await (deps.fetchImpl ?? fetch)(url);
|
|
3125
|
+
if (!response.ok) return error(`gateway unavailable: HTTP ${response.status}`);
|
|
3126
|
+
return text(await response.json());
|
|
3127
|
+
}
|
|
3128
|
+
async function accessContext(deps = {}) {
|
|
3129
|
+
if (!deps.auth) return error(AUTH_REQUIRED);
|
|
3130
|
+
if (!deps.auth.access) {
|
|
3131
|
+
return error(
|
|
3132
|
+
"SuiPay access context unavailable. Reconnect this MCP session or inspect the connection in buyer console."
|
|
3133
|
+
);
|
|
3134
|
+
}
|
|
3135
|
+
return text(deps.auth.access);
|
|
3136
|
+
}
|
|
3137
|
+
async function receipts(args, _cfg, deps = {}) {
|
|
3138
|
+
if (deps.auth) {
|
|
3139
|
+
if (!deps.auth.listReceipts) {
|
|
3140
|
+
return error(
|
|
3141
|
+
"receipts unavailable: this authenticated MCP session has no server-scoped settlement reader"
|
|
3142
|
+
);
|
|
3143
|
+
}
|
|
3144
|
+
try {
|
|
3145
|
+
const settlements = await deps.auth.listReceipts(args.challengeId);
|
|
3146
|
+
return text({ count: settlements.length, settlements });
|
|
3147
|
+
} catch (err) {
|
|
3148
|
+
return error(err instanceof Error ? err.message : String(err));
|
|
3149
|
+
}
|
|
3150
|
+
}
|
|
3151
|
+
if (!profile(deps)) return error(AUTH_REQUIRED);
|
|
3152
|
+
return error(
|
|
3153
|
+
"receipts is available on remote OAuth (/mcp) sessions only; the local allowance profile has no settlement feed access"
|
|
3154
|
+
);
|
|
3155
|
+
}
|
|
3156
|
+
async function suipayLogin(_args, _cfg, _deps = {}) {
|
|
3157
|
+
return error(
|
|
3158
|
+
"V1 allowance login is retired; use remote MCP OAuth (/mcp) with a shared_pool grant, or provision a delegated signer for SDK/REST"
|
|
3159
|
+
);
|
|
3160
|
+
}
|
|
3161
|
+
async function logout(args, cfg, deps = {}) {
|
|
3162
|
+
if (args.force) {
|
|
3163
|
+
clearCreds();
|
|
3164
|
+
return text("SuiPay logged out. Local credentials removed.");
|
|
3165
|
+
}
|
|
3166
|
+
const paymentProfile = profile(deps);
|
|
3167
|
+
if (!paymentProfile) return text("No SuiPay credentials found.");
|
|
3168
|
+
const chain = deps.chainImpl ?? createSuiAllowanceChain({
|
|
3169
|
+
packageId: paymentProfile.creds.packageId,
|
|
3170
|
+
network: cfg.network,
|
|
3171
|
+
url: cfg.rpcUrl
|
|
3172
|
+
});
|
|
3173
|
+
const allowance = await chain.readAllowance(paymentProfile.creds.allowanceId);
|
|
3174
|
+
if (allowance && !allowance.revoked && BigInt(allowance.balance) > 0n) {
|
|
3175
|
+
return error(
|
|
3176
|
+
"Allowance still holds funds. Revoke and refund it in SuiPay console before logout, or pass force=true."
|
|
3177
|
+
);
|
|
3178
|
+
}
|
|
3179
|
+
clearCreds();
|
|
3180
|
+
return text("SuiPay logged out. Local credentials removed.");
|
|
3181
|
+
}
|
|
3182
|
+
|
|
3183
|
+
// src/server.ts
|
|
3184
|
+
var PACKAGE_NAME = "@cmdoss/suipay-mcp";
|
|
3185
|
+
var TOOLS = [
|
|
3186
|
+
{
|
|
3187
|
+
name: "access_context",
|
|
3188
|
+
description: "Inspect profiles, policies, service targets, spend caps, usage, and lifecycle state enforced for this authenticated SuiPay session. Call before choosing a paid service.",
|
|
3189
|
+
inputSchema: {
|
|
3190
|
+
type: "object",
|
|
3191
|
+
properties: {}
|
|
3192
|
+
}
|
|
3193
|
+
},
|
|
3194
|
+
{
|
|
3195
|
+
name: "pay",
|
|
3196
|
+
description: "Fetch a paid HTTP resource and settle its MPP challenge via shared_pool (remote OAuth MCP). Local V1 allowance pay is retired. Use method POST with json to pay for a JSON request body; the body you send is signed into the offer, so it cannot be changed after payment.",
|
|
3197
|
+
inputSchema: {
|
|
3198
|
+
type: "object",
|
|
3199
|
+
properties: {
|
|
3200
|
+
url: { type: "string", description: "Full gateway resource URL." },
|
|
3201
|
+
method: {
|
|
3202
|
+
type: "string",
|
|
3203
|
+
enum: ["GET", "POST"],
|
|
3204
|
+
description: "HTTP method of the paid request. Defaults to GET."
|
|
3205
|
+
},
|
|
3206
|
+
json: {
|
|
3207
|
+
type: "object",
|
|
3208
|
+
description: "JSON request body for a POST. Sent as application/json and bound to the payment."
|
|
3209
|
+
}
|
|
3210
|
+
},
|
|
3211
|
+
required: ["url"]
|
|
3212
|
+
}
|
|
3213
|
+
},
|
|
3214
|
+
{
|
|
3215
|
+
name: "discover",
|
|
3216
|
+
description: "Search SuiPay gateway resources.",
|
|
3217
|
+
inputSchema: {
|
|
3218
|
+
type: "object",
|
|
3219
|
+
properties: {
|
|
3220
|
+
query: {
|
|
3221
|
+
type: "string",
|
|
3222
|
+
description: "Optional service search terms."
|
|
3223
|
+
}
|
|
3224
|
+
}
|
|
3225
|
+
}
|
|
3226
|
+
},
|
|
3227
|
+
{
|
|
3228
|
+
name: "receipts",
|
|
3229
|
+
description: "List SuiPay settlement receipts, optionally filtered by challenge id.",
|
|
3230
|
+
inputSchema: {
|
|
3231
|
+
type: "object",
|
|
3232
|
+
properties: {
|
|
3233
|
+
challengeId: {
|
|
3234
|
+
type: "string",
|
|
3235
|
+
description: "Optional payment challenge id."
|
|
3236
|
+
}
|
|
3237
|
+
}
|
|
3238
|
+
}
|
|
3239
|
+
},
|
|
3240
|
+
{
|
|
3241
|
+
name: "suipay_login",
|
|
3242
|
+
description: "Legacy V1 allowance login (retired). Returns an error directing you to remote MCP OAuth or delegated-signer provisioning.",
|
|
3243
|
+
inputSchema: {
|
|
3244
|
+
type: "object",
|
|
3245
|
+
properties: {
|
|
3246
|
+
label: { type: "string", description: "Label shown in SuiPay console." },
|
|
3247
|
+
gatewayResourceUrl: {
|
|
3248
|
+
type: "string",
|
|
3249
|
+
description: "Gateway resource URL whose 402 defines recipient, asset, and package."
|
|
3250
|
+
}
|
|
3251
|
+
}
|
|
3252
|
+
}
|
|
3253
|
+
},
|
|
3254
|
+
{
|
|
3255
|
+
name: "suipay_logout",
|
|
3256
|
+
description: "Remove local SuiPay credentials. Refuses while live allowance holds funds unless forced.",
|
|
3257
|
+
inputSchema: {
|
|
3258
|
+
type: "object",
|
|
3259
|
+
properties: {
|
|
3260
|
+
force: {
|
|
3261
|
+
type: "boolean",
|
|
3262
|
+
description: "Remove local credentials even if allowance still holds funds."
|
|
3263
|
+
}
|
|
3264
|
+
}
|
|
3265
|
+
}
|
|
3266
|
+
}
|
|
3267
|
+
];
|
|
3268
|
+
function accessInstructions(auth) {
|
|
3269
|
+
if (!auth) {
|
|
3270
|
+
return "Local stdio session: V1 allowance pay is retired. Use remote MCP OAuth (/mcp) with a shared_pool grant, or a delegated signer for SDK/REST.";
|
|
3271
|
+
}
|
|
3272
|
+
const access = auth.access;
|
|
3273
|
+
if (!access) {
|
|
3274
|
+
return [
|
|
3275
|
+
"Authenticated SuiPay session.",
|
|
3276
|
+
"Policy context is unavailable; do not assume unrestricted spending.",
|
|
3277
|
+
"Call access_context and reconnect if context remains unavailable."
|
|
3278
|
+
].join("\n");
|
|
3279
|
+
}
|
|
3280
|
+
const profiles = access.profiles.length ? access.profiles.map((p) => p.name).join(", ") : "none";
|
|
3281
|
+
const policies = access.policies.length ? access.policies.map(
|
|
3282
|
+
(p) => `${p.name} [${p.status}] asset=${p.asset} cap=${p.totalCap} spent=${p.spent} max/payment=${p.maxPerPayment}`
|
|
3283
|
+
).join("; ") : "none";
|
|
3284
|
+
return [
|
|
3285
|
+
`SuiPay access mode: ${access.accessMode}.`,
|
|
3286
|
+
`Agent profiles: ${profiles}.`,
|
|
3287
|
+
`Enforced policies: ${policies}.`,
|
|
3288
|
+
"Call access_context before selecting a paid service. Pay only services covered by active policy targets."
|
|
3289
|
+
].join("\n");
|
|
3290
|
+
}
|
|
3291
|
+
function toolError(value) {
|
|
3292
|
+
return {
|
|
3293
|
+
content: [
|
|
3294
|
+
{
|
|
3295
|
+
type: "text",
|
|
3296
|
+
text: value instanceof Error ? value.message : String(value)
|
|
3297
|
+
}
|
|
3298
|
+
],
|
|
3299
|
+
isError: true
|
|
3300
|
+
};
|
|
3301
|
+
}
|
|
3302
|
+
function createSuipayMcpServer(cfg, auth, trace) {
|
|
3303
|
+
const server = new Server(
|
|
3304
|
+
{ name: "suipay", version: "1.0.0" },
|
|
3305
|
+
{
|
|
3306
|
+
capabilities: { tools: {} },
|
|
3307
|
+
instructions: accessInstructions(auth)
|
|
3308
|
+
}
|
|
3309
|
+
);
|
|
3310
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
3311
|
+
tools: [...TOOLS]
|
|
3312
|
+
}));
|
|
3313
|
+
server.setRequestHandler(CallToolRequestSchema, async (request, extra) => {
|
|
3314
|
+
const { name, arguments: args = {} } = request.params;
|
|
3315
|
+
try {
|
|
3316
|
+
if (auth) {
|
|
3317
|
+
const gate = toolAllowedByScope(name, auth.scope);
|
|
3318
|
+
if (!gate.ok) {
|
|
3319
|
+
return toolError(
|
|
3320
|
+
`insufficient_scope: tool "${name}" requires ${gate.required}`
|
|
3321
|
+
);
|
|
3322
|
+
}
|
|
3323
|
+
}
|
|
3324
|
+
switch (name) {
|
|
3325
|
+
case "access_context":
|
|
3326
|
+
return await accessContext({ auth });
|
|
3327
|
+
case "pay":
|
|
3328
|
+
return await pay(
|
|
3329
|
+
{
|
|
3330
|
+
url: String(args.url ?? ""),
|
|
3331
|
+
...args.method !== void 0 ? { method: args.method } : {},
|
|
3332
|
+
...args.json !== void 0 ? { json: args.json } : {}
|
|
3333
|
+
},
|
|
3334
|
+
cfg,
|
|
3335
|
+
{ auth, ...trace ? { trace } : {} }
|
|
3336
|
+
);
|
|
3337
|
+
case "discover":
|
|
3338
|
+
return await discover(
|
|
3339
|
+
{
|
|
3340
|
+
query: typeof args.query === "string" ? args.query : void 0
|
|
3341
|
+
},
|
|
3342
|
+
cfg
|
|
3343
|
+
);
|
|
3344
|
+
case "receipts":
|
|
3345
|
+
return await receipts(
|
|
3346
|
+
{
|
|
3347
|
+
challengeId: typeof args.challengeId === "string" ? args.challengeId : void 0
|
|
3348
|
+
},
|
|
3349
|
+
cfg,
|
|
3350
|
+
{ auth }
|
|
3351
|
+
);
|
|
3352
|
+
case "suipay_login": {
|
|
3353
|
+
if (auth) {
|
|
3354
|
+
return toolError(
|
|
3355
|
+
"suipay_login is for local stdio only; remote MCP uses OAuth bearer"
|
|
3356
|
+
);
|
|
3357
|
+
}
|
|
3358
|
+
const progressToken = request.params._meta?.progressToken;
|
|
3359
|
+
return await suipayLogin(
|
|
3360
|
+
{
|
|
3361
|
+
label: typeof args.label === "string" ? args.label : void 0,
|
|
3362
|
+
gatewayResourceUrl: typeof args.gatewayResourceUrl === "string" ? args.gatewayResourceUrl : void 0
|
|
3363
|
+
},
|
|
3364
|
+
cfg,
|
|
3365
|
+
{
|
|
3366
|
+
onUrl(url) {
|
|
3367
|
+
if (progressToken === void 0) return;
|
|
3368
|
+
void extra.sendNotification({
|
|
3369
|
+
method: "notifications/progress",
|
|
3370
|
+
params: {
|
|
3371
|
+
progressToken,
|
|
3372
|
+
progress: 0,
|
|
3373
|
+
message: url
|
|
3374
|
+
}
|
|
3375
|
+
}).catch(() => void 0);
|
|
3376
|
+
}
|
|
3377
|
+
}
|
|
3378
|
+
);
|
|
3379
|
+
}
|
|
3380
|
+
case "suipay_logout":
|
|
3381
|
+
if (auth) {
|
|
3382
|
+
return toolError(
|
|
3383
|
+
"suipay_logout is for local stdio only; remote MCP uses OAuth revoke at the AS"
|
|
3384
|
+
);
|
|
3385
|
+
}
|
|
3386
|
+
return await logout({ force: args.force === true }, cfg);
|
|
3387
|
+
default:
|
|
3388
|
+
return toolError(`unknown tool: ${name}`);
|
|
3389
|
+
}
|
|
3390
|
+
} catch (error2) {
|
|
3391
|
+
return toolError(error2);
|
|
3392
|
+
}
|
|
3393
|
+
});
|
|
3394
|
+
return server;
|
|
3395
|
+
}
|
|
3396
|
+
|
|
3397
|
+
// src/trace-proxy.ts
|
|
3398
|
+
var EMPTY_ENVELOPE = {
|
|
3399
|
+
batch: false,
|
|
3400
|
+
entries: [],
|
|
3401
|
+
method: null,
|
|
3402
|
+
requestId: null,
|
|
3403
|
+
clientName: null
|
|
3404
|
+
};
|
|
3405
|
+
var DEFAULT_MAX_OBSERVED_BYTES = 64 * 1024;
|
|
3406
|
+
var DEFAULT_RUN_SETUP_TIMEOUT_MS = 500;
|
|
3407
|
+
function canonicalMcpRequestId(id) {
|
|
3408
|
+
if (typeof id === "number") {
|
|
3409
|
+
return Number.isFinite(id) ? `n:${JSON.stringify(id)}` : null;
|
|
3410
|
+
}
|
|
3411
|
+
if (typeof id === "string") {
|
|
3412
|
+
return `s:${Buffer.from(id, "utf8").toString("base64url")}`;
|
|
3413
|
+
}
|
|
3414
|
+
return null;
|
|
3415
|
+
}
|
|
3416
|
+
function asRecord(value) {
|
|
3417
|
+
return typeof value === "object" && value !== null && !Array.isArray(value) ? value : null;
|
|
3418
|
+
}
|
|
3419
|
+
function readEntry(message) {
|
|
3420
|
+
const record = asRecord(message);
|
|
3421
|
+
if (!record) return null;
|
|
3422
|
+
const params = asRecord(record.params);
|
|
3423
|
+
const clientInfo = params ? asRecord(params.clientInfo) : null;
|
|
3424
|
+
const rawId = record.id;
|
|
3425
|
+
return {
|
|
3426
|
+
method: typeof record.method === "string" ? record.method : null,
|
|
3427
|
+
jsonRpcId: typeof rawId === "string" || typeof rawId === "number" ? rawId : null,
|
|
3428
|
+
requestId: canonicalMcpRequestId(rawId),
|
|
3429
|
+
toolName: params && typeof params.name === "string" ? params.name : null,
|
|
3430
|
+
toolArguments: params ? params.arguments : void 0,
|
|
3431
|
+
clientName: clientInfo && typeof clientInfo.name === "string" ? clientInfo.name : null,
|
|
3432
|
+
clientVersion: clientInfo && typeof clientInfo.version === "string" ? clientInfo.version : null,
|
|
3433
|
+
protocolVersion: params && typeof params.protocolVersion === "string" ? params.protocolVersion : null
|
|
3434
|
+
};
|
|
3435
|
+
}
|
|
3436
|
+
async function readBoundedBody(request, maxBytes) {
|
|
3437
|
+
const declared = Number(request.headers.get("content-length"));
|
|
3438
|
+
if (Number.isFinite(declared) && declared > maxBytes) return null;
|
|
3439
|
+
const body = request.body;
|
|
3440
|
+
if (!body) {
|
|
3441
|
+
try {
|
|
3442
|
+
const text2 = await request.text();
|
|
3443
|
+
return Buffer.byteLength(text2, "utf8") > maxBytes ? null : text2;
|
|
3444
|
+
} catch {
|
|
3445
|
+
return null;
|
|
3446
|
+
}
|
|
3447
|
+
}
|
|
3448
|
+
const reader = body.getReader();
|
|
3449
|
+
const decoder = new TextDecoder();
|
|
3450
|
+
let out = "";
|
|
3451
|
+
let bytes = 0;
|
|
3452
|
+
try {
|
|
3453
|
+
for (; ; ) {
|
|
3454
|
+
const { done, value } = await reader.read();
|
|
3455
|
+
if (done) break;
|
|
3456
|
+
bytes += value.byteLength;
|
|
3457
|
+
if (bytes > maxBytes) return null;
|
|
3458
|
+
out += decoder.decode(value, { stream: true });
|
|
3459
|
+
}
|
|
3460
|
+
return out + decoder.decode();
|
|
3461
|
+
} catch {
|
|
3462
|
+
return null;
|
|
3463
|
+
} finally {
|
|
3464
|
+
void reader.cancel().catch(() => void 0);
|
|
3465
|
+
}
|
|
3466
|
+
}
|
|
3467
|
+
async function inspectMcpEnvelope(request, maxBytes = DEFAULT_MAX_OBSERVED_BYTES) {
|
|
3468
|
+
if (request.method !== "POST") return EMPTY_ENVELOPE;
|
|
3469
|
+
const text2 = await readBoundedBody(request, maxBytes);
|
|
3470
|
+
if (text2 === null) return EMPTY_ENVELOPE;
|
|
3471
|
+
let parsed;
|
|
3472
|
+
try {
|
|
3473
|
+
parsed = JSON.parse(text2);
|
|
3474
|
+
} catch {
|
|
3475
|
+
return EMPTY_ENVELOPE;
|
|
3476
|
+
}
|
|
3477
|
+
const batch = Array.isArray(parsed);
|
|
3478
|
+
const messages = batch ? parsed : [parsed];
|
|
3479
|
+
const entries = [];
|
|
3480
|
+
for (const message of messages) {
|
|
3481
|
+
const entry = readEntry(message);
|
|
3482
|
+
if (entry) entries.push(entry);
|
|
3483
|
+
}
|
|
3484
|
+
const first = entries[0];
|
|
3485
|
+
return {
|
|
3486
|
+
batch,
|
|
3487
|
+
entries,
|
|
3488
|
+
method: first?.method ?? null,
|
|
3489
|
+
requestId: first?.requestId ?? null,
|
|
3490
|
+
clientName: entries.find((e) => e.clientName)?.clientName ?? null
|
|
3491
|
+
};
|
|
3492
|
+
}
|
|
3493
|
+
function safeClock(now) {
|
|
3494
|
+
let last = 0;
|
|
3495
|
+
return () => {
|
|
3496
|
+
try {
|
|
3497
|
+
const value = now();
|
|
3498
|
+
if (Number.isFinite(value)) {
|
|
3499
|
+
last = value;
|
|
3500
|
+
return value;
|
|
3501
|
+
}
|
|
3502
|
+
} catch {
|
|
3503
|
+
}
|
|
3504
|
+
return last;
|
|
3505
|
+
};
|
|
3506
|
+
}
|
|
3507
|
+
function emit(ctx, input) {
|
|
3508
|
+
try {
|
|
3509
|
+
const event = {
|
|
3510
|
+
traceId: ctx.run.id,
|
|
3511
|
+
requestId: input.requestId,
|
|
3512
|
+
buyerAccountId: ctx.runtime.buyerAccountId,
|
|
3513
|
+
connectionId: ctx.runtime.connectionId,
|
|
3514
|
+
at: new Date(ctx.now()).toISOString(),
|
|
3515
|
+
source: "mcp_wire",
|
|
3516
|
+
kind: input.kind,
|
|
3517
|
+
status: input.status,
|
|
3518
|
+
summary: input.summary,
|
|
3519
|
+
publicPayload: input.publicPayload,
|
|
3520
|
+
...input.technicalPayload ? { technicalPayload: input.technicalPayload } : {}
|
|
3521
|
+
};
|
|
3522
|
+
void Promise.resolve(ctx.runtime.sink.emit(event)).catch(() => void 0);
|
|
3523
|
+
} catch {
|
|
3524
|
+
}
|
|
3525
|
+
}
|
|
3526
|
+
function firstText(content) {
|
|
3527
|
+
if (!Array.isArray(content)) return null;
|
|
3528
|
+
for (const block of content) {
|
|
3529
|
+
const record = asRecord(block);
|
|
3530
|
+
if (record && record.type === "text" && typeof record.text === "string") {
|
|
3531
|
+
return record.text;
|
|
3532
|
+
}
|
|
3533
|
+
}
|
|
3534
|
+
return null;
|
|
3535
|
+
}
|
|
3536
|
+
function questionOf(args) {
|
|
3537
|
+
const record = asRecord(args);
|
|
3538
|
+
if (!record) return null;
|
|
3539
|
+
for (const key of ["question", "query", "prompt"]) {
|
|
3540
|
+
const value = record[key];
|
|
3541
|
+
if (typeof value === "string") return value;
|
|
3542
|
+
}
|
|
3543
|
+
return null;
|
|
3544
|
+
}
|
|
3545
|
+
function resourceOf(args) {
|
|
3546
|
+
const record = asRecord(args);
|
|
3547
|
+
if (!record) return null;
|
|
3548
|
+
for (const key of ["url", "gatewayResourceUrl", "resource"]) {
|
|
3549
|
+
const value = record[key];
|
|
3550
|
+
if (typeof value === "string" && value.length > 0) return value;
|
|
3551
|
+
}
|
|
3552
|
+
return null;
|
|
3553
|
+
}
|
|
3554
|
+
function emitToolCalled(ctx, entry) {
|
|
3555
|
+
const tool = entry.toolName ?? "unknown";
|
|
3556
|
+
const question = questionOf(entry.toolArguments);
|
|
3557
|
+
const resource = resourceOf(entry.toolArguments);
|
|
3558
|
+
const metadata = {
|
|
3559
|
+
tool,
|
|
3560
|
+
...resource !== null ? { resource } : {}
|
|
3561
|
+
};
|
|
3562
|
+
emit(ctx, {
|
|
3563
|
+
requestId: entry.requestId,
|
|
3564
|
+
kind: "tool_called",
|
|
3565
|
+
status: "started",
|
|
3566
|
+
summary: resource !== null ? `Tool call ${tool} -> ${resource}` : `Tool call ${tool}`,
|
|
3567
|
+
publicPayload: { ...metadata, ...question !== null ? { question } : {} },
|
|
3568
|
+
technicalPayload: {
|
|
3569
|
+
...metadata,
|
|
3570
|
+
...entry.jsonRpcId !== null ? { jsonRpcId: entry.jsonRpcId } : {},
|
|
3571
|
+
...entry.toolArguments !== void 0 ? { arguments: entry.toolArguments } : {}
|
|
3572
|
+
}
|
|
3573
|
+
});
|
|
3574
|
+
}
|
|
3575
|
+
function emitTerminal(ctx, input) {
|
|
3576
|
+
const { entry, result, error: error2 } = input;
|
|
3577
|
+
const method = entry.method;
|
|
3578
|
+
if (error2) {
|
|
3579
|
+
if (method !== "tools/call") return;
|
|
3580
|
+
const tool2 = entry.toolName ?? "unknown";
|
|
3581
|
+
emit(ctx, {
|
|
3582
|
+
requestId: entry.requestId,
|
|
3583
|
+
kind: "tool_error",
|
|
3584
|
+
status: "error",
|
|
3585
|
+
summary: `Tool error from ${tool2}`,
|
|
3586
|
+
publicPayload: {
|
|
3587
|
+
tool: tool2,
|
|
3588
|
+
...typeof error2.code === "number" ? { errorCode: error2.code } : {}
|
|
3589
|
+
},
|
|
3590
|
+
technicalPayload: {
|
|
3591
|
+
tool: tool2,
|
|
3592
|
+
...entry.jsonRpcId !== null ? { jsonRpcId: entry.jsonRpcId } : {},
|
|
3593
|
+
...typeof error2.code === "number" ? { errorCode: error2.code } : {},
|
|
3594
|
+
...typeof error2.message === "string" ? { errorMessage: error2.message } : {}
|
|
3595
|
+
}
|
|
3596
|
+
});
|
|
3597
|
+
return;
|
|
3598
|
+
}
|
|
3599
|
+
if (!result) return;
|
|
3600
|
+
if (method === "initialize") {
|
|
3601
|
+
emit(ctx, {
|
|
3602
|
+
requestId: entry.requestId,
|
|
3603
|
+
kind: "mcp_connected",
|
|
3604
|
+
status: "ok",
|
|
3605
|
+
summary: "OAuth connection verified",
|
|
3606
|
+
publicPayload: {
|
|
3607
|
+
...entry.clientName ? { clientName: entry.clientName } : {},
|
|
3608
|
+
...typeof result.protocolVersion === "string" ? { protocolVersion: result.protocolVersion } : {},
|
|
3609
|
+
connectionState: "connected"
|
|
3610
|
+
},
|
|
3611
|
+
technicalPayload: {
|
|
3612
|
+
...entry.clientName ? { clientName: entry.clientName } : {},
|
|
3613
|
+
...entry.clientVersion ? { clientVersion: entry.clientVersion } : {},
|
|
3614
|
+
...typeof result.protocolVersion === "string" ? { protocolVersion: result.protocolVersion } : {},
|
|
3615
|
+
transport: "streamable_http",
|
|
3616
|
+
connectionState: "connected"
|
|
3617
|
+
}
|
|
3618
|
+
});
|
|
3619
|
+
return;
|
|
3620
|
+
}
|
|
3621
|
+
if (method === "tools/list") {
|
|
3622
|
+
const tools = Array.isArray(result.tools) ? result.tools : [];
|
|
3623
|
+
const names = tools.map((tool2) => asRecord(tool2)?.name).filter((name) => typeof name === "string");
|
|
3624
|
+
emit(ctx, {
|
|
3625
|
+
requestId: entry.requestId,
|
|
3626
|
+
kind: "tools_listed",
|
|
3627
|
+
status: "ok",
|
|
3628
|
+
summary: `${tools.length} tools listed`,
|
|
3629
|
+
publicPayload: { toolCount: tools.length },
|
|
3630
|
+
technicalPayload: { toolCount: tools.length, toolNames: names }
|
|
3631
|
+
});
|
|
3632
|
+
return;
|
|
3633
|
+
}
|
|
3634
|
+
if (method !== "tools/call") return;
|
|
3635
|
+
const tool = entry.toolName ?? "unknown";
|
|
3636
|
+
if (result.isError === true) {
|
|
3637
|
+
emit(ctx, {
|
|
3638
|
+
requestId: entry.requestId,
|
|
3639
|
+
kind: "tool_error",
|
|
3640
|
+
status: "error",
|
|
3641
|
+
summary: `Tool error from ${tool}`,
|
|
3642
|
+
publicPayload: { tool },
|
|
3643
|
+
technicalPayload: {
|
|
3644
|
+
tool,
|
|
3645
|
+
...entry.jsonRpcId !== null ? { jsonRpcId: entry.jsonRpcId } : {},
|
|
3646
|
+
...firstText(result.content) !== null ? { errorMessage: firstText(result.content) } : {}
|
|
3647
|
+
}
|
|
3648
|
+
});
|
|
3649
|
+
return;
|
|
3650
|
+
}
|
|
3651
|
+
const content = Array.isArray(result.content) ? result.content : [];
|
|
3652
|
+
const answer = firstText(result.content);
|
|
3653
|
+
emit(ctx, {
|
|
3654
|
+
requestId: entry.requestId,
|
|
3655
|
+
kind: "tool_result",
|
|
3656
|
+
status: "ok",
|
|
3657
|
+
summary: `Result from ${tool}`,
|
|
3658
|
+
publicPayload: {
|
|
3659
|
+
tool,
|
|
3660
|
+
resultCount: content.length,
|
|
3661
|
+
durationMs: input.durationMs,
|
|
3662
|
+
...answer !== null ? { answer } : {}
|
|
3663
|
+
},
|
|
3664
|
+
technicalPayload: {
|
|
3665
|
+
tool,
|
|
3666
|
+
...entry.jsonRpcId !== null ? { jsonRpcId: entry.jsonRpcId } : {},
|
|
3667
|
+
resultCount: content.length,
|
|
3668
|
+
durationMs: input.durationMs,
|
|
3669
|
+
result
|
|
3670
|
+
}
|
|
3671
|
+
});
|
|
3672
|
+
}
|
|
3673
|
+
function emitTruncationWarning(ctx, entry) {
|
|
3674
|
+
if (entry.method !== "tools/call") return;
|
|
3675
|
+
const tool = entry.toolName ?? "unknown";
|
|
3676
|
+
emit(ctx, {
|
|
3677
|
+
requestId: entry.requestId,
|
|
3678
|
+
kind: "tool_result",
|
|
3679
|
+
status: "warning",
|
|
3680
|
+
summary: `Result from ${tool} exceeded the trace observation limit`,
|
|
3681
|
+
publicPayload: { tool },
|
|
3682
|
+
technicalPayload: {
|
|
3683
|
+
tool,
|
|
3684
|
+
...entry.jsonRpcId !== null ? { jsonRpcId: entry.jsonRpcId } : {}
|
|
3685
|
+
}
|
|
3686
|
+
});
|
|
3687
|
+
}
|
|
3688
|
+
function parseMessages(text2) {
|
|
3689
|
+
let parsed;
|
|
3690
|
+
try {
|
|
3691
|
+
parsed = JSON.parse(text2);
|
|
3692
|
+
} catch {
|
|
3693
|
+
return [];
|
|
3694
|
+
}
|
|
3695
|
+
const list = Array.isArray(parsed) ? parsed : [parsed];
|
|
3696
|
+
return list.map(asRecord).filter((record) => record !== null);
|
|
3697
|
+
}
|
|
3698
|
+
function sseFrameData(frame) {
|
|
3699
|
+
const lines = frame.split("\n");
|
|
3700
|
+
const data = [];
|
|
3701
|
+
for (const line of lines) {
|
|
3702
|
+
if (!line.startsWith("data:")) continue;
|
|
3703
|
+
data.push(line.slice("data:".length).replace(/^ /, ""));
|
|
3704
|
+
}
|
|
3705
|
+
return data.length ? data.join("\n") : null;
|
|
3706
|
+
}
|
|
3707
|
+
function entryFor(observation, message) {
|
|
3708
|
+
const key = canonicalMcpRequestId(message.id);
|
|
3709
|
+
if (key === null) return null;
|
|
3710
|
+
return observation.entries.get(key) ?? null;
|
|
3711
|
+
}
|
|
3712
|
+
function consumeMessage(observation, message) {
|
|
3713
|
+
const entry = entryFor(observation, message);
|
|
3714
|
+
if (!entry || entry.requestId === null) return;
|
|
3715
|
+
if (observation.terminated.has(entry.requestId)) return;
|
|
3716
|
+
const result = asRecord(message.result);
|
|
3717
|
+
const error2 = asRecord(message.error);
|
|
3718
|
+
if (!result && !error2) return;
|
|
3719
|
+
observation.terminated.add(entry.requestId);
|
|
3720
|
+
emitTerminal(observation.ctx, {
|
|
3721
|
+
entry,
|
|
3722
|
+
result,
|
|
3723
|
+
error: error2,
|
|
3724
|
+
durationMs: observation.ctx.now() - observation.startedAt
|
|
3725
|
+
});
|
|
3726
|
+
}
|
|
3727
|
+
async function observeSse(body, observation, maxBytes) {
|
|
3728
|
+
const reader = body.getReader();
|
|
3729
|
+
const decoder = new TextDecoder();
|
|
3730
|
+
let buffered = "";
|
|
3731
|
+
let bytes = 0;
|
|
3732
|
+
let truncated = false;
|
|
3733
|
+
try {
|
|
3734
|
+
for (; ; ) {
|
|
3735
|
+
const { done, value } = await reader.read();
|
|
3736
|
+
if (done) break;
|
|
3737
|
+
bytes += value.byteLength;
|
|
3738
|
+
buffered += decoder.decode(value, { stream: true }).replace(/\r\n/g, "\n");
|
|
3739
|
+
let boundary = buffered.indexOf("\n\n");
|
|
3740
|
+
while (boundary !== -1) {
|
|
3741
|
+
const frame = buffered.slice(0, boundary);
|
|
3742
|
+
buffered = buffered.slice(boundary + 2);
|
|
3743
|
+
const data = sseFrameData(frame);
|
|
3744
|
+
if (data) {
|
|
3745
|
+
for (const message of parseMessages(data)) {
|
|
3746
|
+
consumeMessage(observation, message);
|
|
3747
|
+
}
|
|
3748
|
+
}
|
|
3749
|
+
boundary = buffered.indexOf("\n\n");
|
|
3750
|
+
}
|
|
3751
|
+
if (bytes > maxBytes) {
|
|
3752
|
+
truncated = true;
|
|
3753
|
+
break;
|
|
3754
|
+
}
|
|
3755
|
+
}
|
|
3756
|
+
} catch {
|
|
3757
|
+
} finally {
|
|
3758
|
+
void reader.cancel().catch(() => void 0);
|
|
3759
|
+
}
|
|
3760
|
+
if (!truncated) return;
|
|
3761
|
+
for (const entry of observation.entries.values()) {
|
|
3762
|
+
if (entry.requestId === null) continue;
|
|
3763
|
+
if (observation.terminated.has(entry.requestId)) continue;
|
|
3764
|
+
observation.terminated.add(entry.requestId);
|
|
3765
|
+
emitTruncationWarning(observation.ctx, entry);
|
|
3766
|
+
}
|
|
3767
|
+
}
|
|
3768
|
+
async function observeResponse(clone, observation, maxBytes) {
|
|
3769
|
+
const contentType = clone.headers.get("content-type") ?? "";
|
|
3770
|
+
if (contentType.includes("text/event-stream")) {
|
|
3771
|
+
if (!clone.body) return;
|
|
3772
|
+
await observeSse(clone.body, observation, maxBytes);
|
|
3773
|
+
return;
|
|
3774
|
+
}
|
|
3775
|
+
if (!contentType.includes("application/json")) {
|
|
3776
|
+
void clone.body?.cancel().catch(() => void 0);
|
|
3777
|
+
return;
|
|
3778
|
+
}
|
|
3779
|
+
const text2 = await clone.text();
|
|
3780
|
+
for (const message of parseMessages(text2)) consumeMessage(observation, message);
|
|
3781
|
+
}
|
|
3782
|
+
async function withDeadline(work, ms) {
|
|
3783
|
+
work.catch(() => void 0);
|
|
3784
|
+
let timer;
|
|
3785
|
+
try {
|
|
3786
|
+
return await Promise.race([
|
|
3787
|
+
work,
|
|
3788
|
+
new Promise((resolve) => {
|
|
3789
|
+
timer = setTimeout(() => resolve(null), ms);
|
|
3790
|
+
timer.unref?.();
|
|
3791
|
+
})
|
|
3792
|
+
]);
|
|
3793
|
+
} catch {
|
|
3794
|
+
return null;
|
|
3795
|
+
} finally {
|
|
3796
|
+
if (timer !== void 0) clearTimeout(timer);
|
|
3797
|
+
}
|
|
3798
|
+
}
|
|
3799
|
+
async function traceMcpHttpRequest(input) {
|
|
3800
|
+
const runtime = input.runtime;
|
|
3801
|
+
if (!runtime) return input.handle(input.request);
|
|
3802
|
+
const now = safeClock(runtime.now ?? Date.now);
|
|
3803
|
+
const maxBytes = input.maxObservedBytes ?? DEFAULT_MAX_OBSERVED_BYTES;
|
|
3804
|
+
const prepared = await withDeadline(
|
|
3805
|
+
(async () => {
|
|
3806
|
+
const parsed = await inspectMcpEnvelope(input.request.clone(), maxBytes);
|
|
3807
|
+
const resolved = await runtime.runs.resolveRun({
|
|
3808
|
+
buyerAccountId: runtime.buyerAccountId,
|
|
3809
|
+
connectionId: runtime.connectionId,
|
|
3810
|
+
jsonRpcMethod: parsed.method,
|
|
3811
|
+
clientName: parsed.clientName,
|
|
3812
|
+
now: now()
|
|
3813
|
+
});
|
|
3814
|
+
return { envelope: parsed, run: resolved };
|
|
3815
|
+
})().catch(() => null),
|
|
3816
|
+
input.runSetupTimeoutMs ?? DEFAULT_RUN_SETUP_TIMEOUT_MS
|
|
3817
|
+
);
|
|
3818
|
+
if (!prepared?.run) return input.handle(input.request);
|
|
3819
|
+
const { envelope, run } = prepared;
|
|
3820
|
+
const ctx = { runtime, run, now };
|
|
3821
|
+
const hooks = {
|
|
3822
|
+
runtime,
|
|
3823
|
+
context: { traceId: run.id, requestId: envelope.requestId },
|
|
3824
|
+
sink: runtime.sink
|
|
3825
|
+
};
|
|
3826
|
+
const entries = /* @__PURE__ */ new Map();
|
|
3827
|
+
for (const entry of envelope.entries) {
|
|
3828
|
+
if (entry.requestId === null) continue;
|
|
3829
|
+
entries.set(entry.requestId, entry);
|
|
3830
|
+
if (entry.method === "tools/call") emitToolCalled(ctx, entry);
|
|
3831
|
+
}
|
|
3832
|
+
const startedAt = now();
|
|
3833
|
+
const response = await input.handle(input.request, hooks);
|
|
3834
|
+
if (entries.size > 0) {
|
|
3835
|
+
try {
|
|
3836
|
+
const clone = response.clone();
|
|
3837
|
+
const observation = {
|
|
3838
|
+
ctx,
|
|
3839
|
+
entries,
|
|
3840
|
+
startedAt,
|
|
3841
|
+
terminated: /* @__PURE__ */ new Set()
|
|
3842
|
+
};
|
|
3843
|
+
void observeResponse(clone, observation, maxBytes).catch(() => void 0);
|
|
3844
|
+
} catch {
|
|
3845
|
+
}
|
|
3846
|
+
}
|
|
3847
|
+
return response;
|
|
3848
|
+
}
|
|
3849
|
+
|
|
3850
|
+
// src/http.ts
|
|
3851
|
+
async function handleSuipayMcpHttpRequest(req, cfg, auth, trace) {
|
|
3852
|
+
return traceMcpHttpRequest({
|
|
3853
|
+
request: req,
|
|
3854
|
+
runtime: trace ?? null,
|
|
3855
|
+
handle: async (request, hooks) => {
|
|
3856
|
+
const transport = new WebStandardStreamableHTTPServerTransport({
|
|
3857
|
+
sessionIdGenerator: void 0,
|
|
3858
|
+
enableJsonResponse: true
|
|
3859
|
+
});
|
|
3860
|
+
const server = createSuipayMcpServer(cfg, auth, hooks);
|
|
3861
|
+
await server.connect(transport);
|
|
3862
|
+
return transport.handleRequest(request);
|
|
3863
|
+
}
|
|
3864
|
+
});
|
|
3865
|
+
}
|
|
3866
|
+
|
|
3867
|
+
export {
|
|
3868
|
+
loadPaymentProfile,
|
|
3869
|
+
resolveGrantTarget,
|
|
3870
|
+
buildSharedPoolPayKind,
|
|
3871
|
+
paySharedPoolResource,
|
|
3872
|
+
PACKAGE_NAME,
|
|
3873
|
+
TOOLS,
|
|
3874
|
+
createSuipayMcpServer,
|
|
3875
|
+
canonicalMcpRequestId,
|
|
3876
|
+
inspectMcpEnvelope,
|
|
3877
|
+
traceMcpHttpRequest,
|
|
3878
|
+
handleSuipayMcpHttpRequest
|
|
3879
|
+
};
|