@cmdoss/suipay-mcp 0.2.2-dev.4 → 0.2.2-dev.6
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 +66 -9
- package/dist/bin/suipay.js +2 -2
- package/dist/chunk-AEV6Q6JC.js +46 -0
- package/dist/{chunk-KDOGSB5I.js → chunk-UTKVBAEZ.js} +2119 -318
- package/dist/{http-C8xW9F1c.d.ts → http-DBP9qgVw.d.ts} +42 -4
- package/dist/http.d.ts +1 -1
- package/dist/http.js +1 -1
- package/dist/index.d.ts +38 -9
- package/dist/index.js +10 -2
- package/package.json +2 -2
- package/dist/chunk-XAM2YQC6.js +0 -39
|
@@ -151,6 +151,46 @@ import { normalizeStructTag as normalizeStructTag2 } from "@mysten/sui/utils";
|
|
|
151
151
|
|
|
152
152
|
// ../core/dist/assets/testnet.js
|
|
153
153
|
import { normalizeStructTag } from "@mysten/sui/utils";
|
|
154
|
+
var TESTNET_SUI_COIN_TYPE = "0x2::sui::SUI";
|
|
155
|
+
var TESTNET_USDC_COIN_TYPE = "0xa1ec7fc00a6f40db9693ad1415d0c193ad3906494428cf252621037bd7117e29::usdc::USDC";
|
|
156
|
+
var TESTNET_SUI_DECIMALS = 9;
|
|
157
|
+
var TESTNET_USDC_DECIMALS = 6;
|
|
158
|
+
var TESTNET_ASSETS = [
|
|
159
|
+
{
|
|
160
|
+
id: "sui",
|
|
161
|
+
symbol: "SUI",
|
|
162
|
+
coinType: TESTNET_SUI_COIN_TYPE,
|
|
163
|
+
decimals: TESTNET_SUI_DECIMALS,
|
|
164
|
+
network: "sui:testnet"
|
|
165
|
+
},
|
|
166
|
+
{
|
|
167
|
+
id: "usdc",
|
|
168
|
+
symbol: "USDC",
|
|
169
|
+
coinType: TESTNET_USDC_COIN_TYPE,
|
|
170
|
+
decimals: TESTNET_USDC_DECIMALS,
|
|
171
|
+
network: "sui:testnet"
|
|
172
|
+
}
|
|
173
|
+
];
|
|
174
|
+
function supportedAssetByCoinType(coinType) {
|
|
175
|
+
const normalized = tryNormalizeStructTag(coinType);
|
|
176
|
+
if (!normalized)
|
|
177
|
+
return null;
|
|
178
|
+
for (const asset of TESTNET_ASSETS) {
|
|
179
|
+
if (tryNormalizeStructTag(asset.coinType) === normalized)
|
|
180
|
+
return asset;
|
|
181
|
+
}
|
|
182
|
+
return null;
|
|
183
|
+
}
|
|
184
|
+
function tryNormalizeStructTag(coinType) {
|
|
185
|
+
const trimmed = coinType.trim();
|
|
186
|
+
if (!trimmed)
|
|
187
|
+
return null;
|
|
188
|
+
try {
|
|
189
|
+
return normalizeStructTag(trimmed);
|
|
190
|
+
} catch {
|
|
191
|
+
return null;
|
|
192
|
+
}
|
|
193
|
+
}
|
|
154
194
|
|
|
155
195
|
// ../core/dist/service-state/index.js
|
|
156
196
|
var STATE_DIMENSIONS = Object.freeze([
|
|
@@ -371,6 +411,101 @@ var SERVICE_STATE_TRANSITIONS = Object.freeze([
|
|
|
371
411
|
]);
|
|
372
412
|
|
|
373
413
|
// ../core/dist/policy/target.js
|
|
414
|
+
var AUTHORIZATION_PROTOCOL_VERSION = "suipay-target-v1";
|
|
415
|
+
function normalizeHexAddress(address) {
|
|
416
|
+
const raw = address.trim().toLowerCase();
|
|
417
|
+
if (!raw.startsWith("0x")) {
|
|
418
|
+
throw new Error("Address must be a 0x-prefixed Sui address.");
|
|
419
|
+
}
|
|
420
|
+
const hex = raw.slice(2);
|
|
421
|
+
if (!/^[0-9a-f]+$/.test(hex) || hex.length === 0 || hex.length > 64) {
|
|
422
|
+
throw new Error("Invalid Sui address.");
|
|
423
|
+
}
|
|
424
|
+
if (/^0+$/.test(hex)) {
|
|
425
|
+
throw new Error("Recipient must be a non-zero address.");
|
|
426
|
+
}
|
|
427
|
+
return `0x${hex.padStart(64, "0")}`;
|
|
428
|
+
}
|
|
429
|
+
function normalizePath(path) {
|
|
430
|
+
const p = path.trim();
|
|
431
|
+
if (!p.startsWith("/")) {
|
|
432
|
+
throw new Error("Path must be an absolute path starting with /.");
|
|
433
|
+
}
|
|
434
|
+
if (p.length > 1 && p.endsWith("/"))
|
|
435
|
+
return p.replace(/\/+$/, "");
|
|
436
|
+
return p;
|
|
437
|
+
}
|
|
438
|
+
function normalizeMethod(method) {
|
|
439
|
+
const m = method.trim().toUpperCase();
|
|
440
|
+
if (!m)
|
|
441
|
+
throw new Error("HTTP method is required.");
|
|
442
|
+
return m;
|
|
443
|
+
}
|
|
444
|
+
function normalizeNetwork(network) {
|
|
445
|
+
const n = network.trim().toLowerCase();
|
|
446
|
+
if (n !== "sui:testnet") {
|
|
447
|
+
throw new Error(`Unsupported network: ${network}`);
|
|
448
|
+
}
|
|
449
|
+
return n;
|
|
450
|
+
}
|
|
451
|
+
function normalizeAsset(asset) {
|
|
452
|
+
const trimmed = asset.trim();
|
|
453
|
+
let normalized;
|
|
454
|
+
try {
|
|
455
|
+
normalized = normalizeStructTag2(trimmed);
|
|
456
|
+
} catch {
|
|
457
|
+
throw new Error(`Invalid coin type: ${asset}`);
|
|
458
|
+
}
|
|
459
|
+
if (!supportedAssetByCoinType(normalized)) {
|
|
460
|
+
throw new Error(`Unsupported asset: ${asset}`);
|
|
461
|
+
}
|
|
462
|
+
return normalized;
|
|
463
|
+
}
|
|
464
|
+
function normalizeId(id, label) {
|
|
465
|
+
const v = id.trim();
|
|
466
|
+
if (!v)
|
|
467
|
+
throw new Error(`${label} is required.`);
|
|
468
|
+
if (/[\n\r\0]/.test(v))
|
|
469
|
+
throw new Error(`${label} contains control characters.`);
|
|
470
|
+
return v;
|
|
471
|
+
}
|
|
472
|
+
function normalizeEpoch(epoch) {
|
|
473
|
+
if (!Number.isInteger(epoch) || epoch < 1) {
|
|
474
|
+
throw new Error("authorizationEpoch must be a positive integer.");
|
|
475
|
+
}
|
|
476
|
+
return epoch;
|
|
477
|
+
}
|
|
478
|
+
function canonicalizeTargetFields(input) {
|
|
479
|
+
return {
|
|
480
|
+
network: normalizeNetwork(input.network),
|
|
481
|
+
providerId: normalizeHexAddress(input.providerId),
|
|
482
|
+
serviceId: normalizeId(input.serviceId, "serviceId"),
|
|
483
|
+
endpointId: normalizeId(input.endpointId, "endpointId"),
|
|
484
|
+
method: normalizeMethod(input.method),
|
|
485
|
+
path: normalizePath(input.path),
|
|
486
|
+
asset: normalizeAsset(input.asset),
|
|
487
|
+
recipient: normalizeHexAddress(input.recipient),
|
|
488
|
+
authorizationEpoch: normalizeEpoch(input.authorizationEpoch)
|
|
489
|
+
};
|
|
490
|
+
}
|
|
491
|
+
function authorizationFingerprint(input) {
|
|
492
|
+
const c = canonicalizeTargetFields(input);
|
|
493
|
+
return [
|
|
494
|
+
AUTHORIZATION_PROTOCOL_VERSION,
|
|
495
|
+
c.network,
|
|
496
|
+
c.providerId,
|
|
497
|
+
c.serviceId,
|
|
498
|
+
c.endpointId,
|
|
499
|
+
c.method,
|
|
500
|
+
c.path,
|
|
501
|
+
c.asset,
|
|
502
|
+
c.recipient,
|
|
503
|
+
String(c.authorizationEpoch)
|
|
504
|
+
].join("\n");
|
|
505
|
+
}
|
|
506
|
+
function targetHashFromFingerprint(fingerprint) {
|
|
507
|
+
return createHash("sha256").update(fingerprint, "utf8").digest("hex");
|
|
508
|
+
}
|
|
374
509
|
function targetHashBytes(hexHash) {
|
|
375
510
|
if (!/^[0-9a-f]{64}$/i.test(hexHash)) {
|
|
376
511
|
throw new Error("targetHash must be 32-byte hex.");
|
|
@@ -4332,8 +4467,10 @@ import {
|
|
|
4332
4467
|
fsyncSync,
|
|
4333
4468
|
openSync,
|
|
4334
4469
|
closeSync,
|
|
4335
|
-
writeSync
|
|
4470
|
+
writeSync,
|
|
4471
|
+
linkSync
|
|
4336
4472
|
} from "fs";
|
|
4473
|
+
import { decodeSuiPrivateKey } from "@mysten/sui/cryptography";
|
|
4337
4474
|
import { Ed25519Keypair as Ed25519Keypair2 } from "@mysten/sui/keypairs/ed25519";
|
|
4338
4475
|
import { fromBase64 as fromBase644 } from "@mysten/sui/utils";
|
|
4339
4476
|
import { bytesToHex as bytesToHex4 } from "@noble/hashes/utils.js";
|
|
@@ -4347,6 +4484,16 @@ function mcpCredentialsPath() {
|
|
|
4347
4484
|
const home = process.env.HOME ?? process.env.USERPROFILE ?? homedir();
|
|
4348
4485
|
return join(home, ".suipay", "credentials.json");
|
|
4349
4486
|
}
|
|
4487
|
+
var MCP_TARGET_PRESETS = {
|
|
4488
|
+
dev: {
|
|
4489
|
+
consoleUrl: "https://suipay-dev.up.railway.app",
|
|
4490
|
+
gatewayUrl: "https://gateway-dev-ba84.up.railway.app"
|
|
4491
|
+
},
|
|
4492
|
+
local: {
|
|
4493
|
+
consoleUrl: "http://localhost:3000",
|
|
4494
|
+
gatewayUrl: "http://127.0.0.1:4340"
|
|
4495
|
+
}
|
|
4496
|
+
};
|
|
4350
4497
|
var GRPC_URLS = {
|
|
4351
4498
|
mainnet: "https://fullnode.mainnet.sui.io:443",
|
|
4352
4499
|
testnet: "https://fullnode.testnet.sui.io:443",
|
|
@@ -4390,11 +4537,56 @@ function resolveActiveMovePackageId(env = process.env) {
|
|
|
4390
4537
|
}
|
|
4391
4538
|
return configured[0]?.value;
|
|
4392
4539
|
}
|
|
4393
|
-
function
|
|
4540
|
+
function parseMcpTargetFlag(argv) {
|
|
4541
|
+
let target;
|
|
4542
|
+
for (const arg of argv) {
|
|
4543
|
+
const next = arg === "--local" || arg === "-local" ? "local" : arg === "--dev" || arg === "-dev" ? "dev" : void 0;
|
|
4544
|
+
if (!next) continue;
|
|
4545
|
+
if (target && target !== next) {
|
|
4546
|
+
throw new Error("pass only one of --local or --dev");
|
|
4547
|
+
}
|
|
4548
|
+
target = next;
|
|
4549
|
+
}
|
|
4550
|
+
return target;
|
|
4551
|
+
}
|
|
4552
|
+
function parseMcpTargetName(value) {
|
|
4553
|
+
const trimmed = value?.trim().toLowerCase();
|
|
4554
|
+
if (!trimmed) return void 0;
|
|
4555
|
+
if (trimmed === "dev" || trimmed === "local") return trimmed;
|
|
4556
|
+
throw new Error(`SUIPAY_MCP_TARGET must be "dev" or "local", got ${JSON.stringify(value)}`);
|
|
4557
|
+
}
|
|
4558
|
+
function resolveMcpTarget(env = process.env, argv = []) {
|
|
4559
|
+
return parseMcpTargetFlag(argv) ?? parseMcpTargetName(env.SUIPAY_MCP_TARGET) ?? "dev";
|
|
4560
|
+
}
|
|
4561
|
+
function loopbackKey(url) {
|
|
4562
|
+
if (url.protocol !== "http:" && url.protocol !== "https:") return null;
|
|
4563
|
+
if (url.hostname !== "127.0.0.1" && url.hostname !== "localhost") return null;
|
|
4564
|
+
const port = url.port || (url.protocol === "https:" ? "443" : "80");
|
|
4565
|
+
return `${url.protocol}//loopback:${port}`;
|
|
4566
|
+
}
|
|
4567
|
+
function sameMcpGateway(a, b) {
|
|
4568
|
+
let ua;
|
|
4569
|
+
let ub;
|
|
4570
|
+
try {
|
|
4571
|
+
ua = new URL(a);
|
|
4572
|
+
ub = new URL(b);
|
|
4573
|
+
} catch {
|
|
4574
|
+
return a.replace(/\/+$/, "") === b.replace(/\/+$/, "");
|
|
4575
|
+
}
|
|
4576
|
+
if (ua.origin === ub.origin) return true;
|
|
4577
|
+
const left = loopbackKey(ua);
|
|
4578
|
+
const right = loopbackKey(ub);
|
|
4579
|
+
return left !== null && left === right;
|
|
4580
|
+
}
|
|
4581
|
+
function loadMcpConfig(env = process.env, argv = []) {
|
|
4394
4582
|
const network = env.SUIPAY_NETWORK ?? "testnet";
|
|
4395
|
-
const
|
|
4583
|
+
const target = resolveMcpTarget(env, argv);
|
|
4584
|
+
const preset = MCP_TARGET_PRESETS[target];
|
|
4585
|
+
const gatewayUrl = env.SUIPAY_GATEWAY_URL?.trim() || preset.gatewayUrl;
|
|
4586
|
+
const consoleUrl = env.SUIPAY_CONSOLE_URL?.trim() || preset.consoleUrl;
|
|
4396
4587
|
return {
|
|
4397
|
-
|
|
4588
|
+
target,
|
|
4589
|
+
consoleUrl,
|
|
4398
4590
|
gatewayUrl,
|
|
4399
4591
|
rpcUrl: env.SUIPAY_RPC_URL ?? GRPC_URLS[network],
|
|
4400
4592
|
network,
|
|
@@ -4443,10 +4635,23 @@ function isValid(obj) {
|
|
|
4443
4635
|
const c = obj;
|
|
4444
4636
|
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;
|
|
4445
4637
|
}
|
|
4638
|
+
function isHttpUrl(value) {
|
|
4639
|
+
if (typeof value !== "string" || !value.trim()) return false;
|
|
4640
|
+
try {
|
|
4641
|
+
const parsed = new URL(value);
|
|
4642
|
+
return parsed.protocol === "http:" || parsed.protocol === "https:";
|
|
4643
|
+
} catch {
|
|
4644
|
+
return false;
|
|
4645
|
+
}
|
|
4646
|
+
}
|
|
4446
4647
|
function isLocalSigningKeyRecord(obj) {
|
|
4447
4648
|
if (!obj || typeof obj !== "object") return false;
|
|
4448
4649
|
const c = obj;
|
|
4449
|
-
|
|
4650
|
+
if (c.version !== 2 || 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.createdAt !== "string") {
|
|
4651
|
+
return false;
|
|
4652
|
+
}
|
|
4653
|
+
if (c.gatewayUrl !== void 0 && !isHttpUrl(c.gatewayUrl)) return false;
|
|
4654
|
+
return true;
|
|
4450
4655
|
}
|
|
4451
4656
|
function readCredentialsJson() {
|
|
4452
4657
|
const path = credsPath();
|
|
@@ -4482,15 +4687,19 @@ function loadCreds() {
|
|
|
4482
4687
|
}
|
|
4483
4688
|
return parsed;
|
|
4484
4689
|
}
|
|
4485
|
-
function
|
|
4486
|
-
const dir = credsDir();
|
|
4487
|
-
mkdirSync(dir, { recursive: true, mode: 448 });
|
|
4690
|
+
function fsyncDir(dir) {
|
|
4488
4691
|
try {
|
|
4489
|
-
|
|
4692
|
+
const fd = openSync(dir, "r");
|
|
4693
|
+
try {
|
|
4694
|
+
fsyncSync(fd);
|
|
4695
|
+
} finally {
|
|
4696
|
+
closeSync(fd);
|
|
4697
|
+
}
|
|
4490
4698
|
} catch {
|
|
4491
4699
|
}
|
|
4492
|
-
|
|
4493
|
-
|
|
4700
|
+
}
|
|
4701
|
+
function writeTmpCredentials(dir, payload) {
|
|
4702
|
+
const tmp = join2(dir, `.credentials.${process.pid}.${randomSuffix()}.tmp`);
|
|
4494
4703
|
const fd = openSync(tmp, "w", 384);
|
|
4495
4704
|
try {
|
|
4496
4705
|
writeSync(fd, JSON.stringify(payload, null, 2));
|
|
@@ -4498,7 +4707,23 @@ function writeCredentialsFile(payload) {
|
|
|
4498
4707
|
} finally {
|
|
4499
4708
|
closeSync(fd);
|
|
4500
4709
|
}
|
|
4710
|
+
return tmp;
|
|
4711
|
+
}
|
|
4712
|
+
function randomSuffix() {
|
|
4713
|
+
return Math.random().toString(16).slice(2, 10);
|
|
4714
|
+
}
|
|
4715
|
+
function writeCredentialsFile(payload) {
|
|
4716
|
+
const dir = credsDir();
|
|
4717
|
+
mkdirSync(dir, { recursive: true, mode: 448 });
|
|
4718
|
+
try {
|
|
4719
|
+
chmodSync(dir, 448);
|
|
4720
|
+
} catch {
|
|
4721
|
+
}
|
|
4722
|
+
const path = credsPath();
|
|
4723
|
+
const tmp = writeTmpCredentials(dir, payload);
|
|
4724
|
+
fsyncDir(dir);
|
|
4501
4725
|
renameSync(tmp, path);
|
|
4726
|
+
fsyncDir(dir);
|
|
4502
4727
|
try {
|
|
4503
4728
|
chmodSync(path, 384);
|
|
4504
4729
|
} catch {
|
|
@@ -4512,19 +4737,23 @@ function createCredentialsFileExclusive(payload) {
|
|
|
4512
4737
|
} catch {
|
|
4513
4738
|
}
|
|
4514
4739
|
const path = credsPath();
|
|
4515
|
-
|
|
4740
|
+
const tmp = writeTmpCredentials(dir, payload);
|
|
4741
|
+
fsyncDir(dir);
|
|
4516
4742
|
try {
|
|
4517
|
-
|
|
4743
|
+
linkSync(tmp, path);
|
|
4518
4744
|
} catch (error2) {
|
|
4745
|
+
try {
|
|
4746
|
+
unlinkSync(tmp);
|
|
4747
|
+
} catch {
|
|
4748
|
+
}
|
|
4519
4749
|
if (error2.code === "EEXIST") return false;
|
|
4520
4750
|
throw error2;
|
|
4521
4751
|
}
|
|
4522
4752
|
try {
|
|
4523
|
-
|
|
4524
|
-
|
|
4525
|
-
} finally {
|
|
4526
|
-
closeSync(fd);
|
|
4753
|
+
unlinkSync(tmp);
|
|
4754
|
+
} catch {
|
|
4527
4755
|
}
|
|
4756
|
+
fsyncDir(dir);
|
|
4528
4757
|
try {
|
|
4529
4758
|
chmodSync(path, 384);
|
|
4530
4759
|
} catch {
|
|
@@ -4613,12 +4842,90 @@ function rewriteV1ToLocalSigningKey(creds, opts = {}) {
|
|
|
4613
4842
|
delegatePublicKeyHex: creds.delegatePublicKeyHex,
|
|
4614
4843
|
delegateAddress: creds.delegateAddress,
|
|
4615
4844
|
createdAt: creds.createdAt,
|
|
4845
|
+
...isHttpUrl(creds.gatewayUrl) ? { gatewayUrl: creds.gatewayUrl } : {},
|
|
4616
4846
|
...opts.label?.trim() ? { label: opts.label.trim() } : creds.label?.trim() ? { label: creds.label.trim() } : {}
|
|
4617
4847
|
};
|
|
4618
4848
|
writeCredentialsFile(record);
|
|
4619
4849
|
return identity;
|
|
4620
4850
|
}
|
|
4851
|
+
function keypairFromOperatorSecret(raw) {
|
|
4852
|
+
const key = raw.trim();
|
|
4853
|
+
if (!key) throw new Error("SUIPAY_MCP_KEY is set but empty");
|
|
4854
|
+
try {
|
|
4855
|
+
return /^(0x)?[0-9a-fA-F]{64}$/.test(key) ? Ed25519Keypair2.fromSecretKey(
|
|
4856
|
+
Uint8Array.from(Buffer.from(key.replace(/^0x/i, ""), "hex"))
|
|
4857
|
+
) : Ed25519Keypair2.fromSecretKey(key);
|
|
4858
|
+
} catch {
|
|
4859
|
+
throw new Error(
|
|
4860
|
+
"SUIPAY_MCP_KEY is malformed (expected 64-hex or suiprivkey1\u2026 Ed25519 secret)"
|
|
4861
|
+
);
|
|
4862
|
+
}
|
|
4863
|
+
}
|
|
4864
|
+
function seedHexFromKeypair(kp, operatorSecret) {
|
|
4865
|
+
const hex = operatorSecret.trim().replace(/^0x/i, "");
|
|
4866
|
+
if (/^[0-9a-fA-F]{64}$/.test(hex)) return hex.toLowerCase();
|
|
4867
|
+
try {
|
|
4868
|
+
return bytesToHex4(decodeSuiPrivateKey(kp.getSecretKey()).secretKey);
|
|
4869
|
+
} catch {
|
|
4870
|
+
throw new Error(
|
|
4871
|
+
"SUIPAY_MCP_KEY is malformed (expected 64-hex or suiprivkey1\u2026 Ed25519 secret)"
|
|
4872
|
+
);
|
|
4873
|
+
}
|
|
4874
|
+
}
|
|
4875
|
+
function recordFromKeypair(kp, opts = {}, existing, operatorSecret) {
|
|
4876
|
+
const seed = seedHexFromKeypair(kp, operatorSecret);
|
|
4877
|
+
const publicKeyHex = bytesToHex4(kp.getPublicKey().toRawBytes());
|
|
4878
|
+
const delegateAddress = kp.getPublicKey().toSuiAddress();
|
|
4879
|
+
const label = opts.label?.trim() || existing?.label?.trim();
|
|
4880
|
+
return {
|
|
4881
|
+
version: 2,
|
|
4882
|
+
delegatePrivateKey: seed,
|
|
4883
|
+
delegatePublicKeyHex: publicKeyHex,
|
|
4884
|
+
delegateAddress,
|
|
4885
|
+
createdAt: existing?.createdAt ?? (/* @__PURE__ */ new Date()).toISOString(),
|
|
4886
|
+
...existing && isHttpUrl(existing.gatewayUrl ?? "") ? { gatewayUrl: existing.gatewayUrl } : {},
|
|
4887
|
+
...label ? { label } : {}
|
|
4888
|
+
};
|
|
4889
|
+
}
|
|
4890
|
+
var ENV_KEY_MISMATCH = "SUIPAY_MCP_KEY does not match the stored delegate key - refusing to overwrite";
|
|
4891
|
+
function existingRecordForEnv(parsed) {
|
|
4892
|
+
if (parsed === null) return null;
|
|
4893
|
+
if (isLocalSigningKeyRecord(parsed) || isValid(parsed)) return parsed;
|
|
4894
|
+
throw new Error(
|
|
4895
|
+
`credentials file at ${credsPath()} failed schema validation - refusing to continue`
|
|
4896
|
+
);
|
|
4897
|
+
}
|
|
4621
4898
|
async function ensureLocalSigningKey(opts = {}) {
|
|
4899
|
+
const envKey = process.env.SUIPAY_MCP_KEY?.trim();
|
|
4900
|
+
if (envKey) {
|
|
4901
|
+
const kp2 = keypairFromOperatorSecret(envKey);
|
|
4902
|
+
const parsed2 = readCredentialsJson();
|
|
4903
|
+
const existing = existingRecordForEnv(parsed2);
|
|
4904
|
+
const asV2 = existing ? isLocalSigningKeyRecord(existing) ? existing : {
|
|
4905
|
+
version: 2,
|
|
4906
|
+
delegatePrivateKey: existing.delegatePrivateKey,
|
|
4907
|
+
delegatePublicKeyHex: existing.delegatePublicKeyHex,
|
|
4908
|
+
delegateAddress: existing.delegateAddress,
|
|
4909
|
+
createdAt: existing.createdAt,
|
|
4910
|
+
...isHttpUrl(existing.gatewayUrl) ? { gatewayUrl: existing.gatewayUrl } : {},
|
|
4911
|
+
...existing.label?.trim() ? { label: existing.label.trim() } : {}
|
|
4912
|
+
} : null;
|
|
4913
|
+
const record2 = recordFromKeypair(kp2, opts, asV2, envKey);
|
|
4914
|
+
if (existing) {
|
|
4915
|
+
if (existing.delegateAddress !== record2.delegateAddress || existing.delegatePrivateKey !== record2.delegatePrivateKey) {
|
|
4916
|
+
throw new Error(ENV_KEY_MISMATCH);
|
|
4917
|
+
}
|
|
4918
|
+
if (!isLocalSigningKeyRecord(existing)) {
|
|
4919
|
+
writeCredentialsFile(record2);
|
|
4920
|
+
}
|
|
4921
|
+
} else {
|
|
4922
|
+
writeCredentialsFile(record2);
|
|
4923
|
+
}
|
|
4924
|
+
return {
|
|
4925
|
+
delegateAddress: record2.delegateAddress,
|
|
4926
|
+
publicKeyHex: record2.delegatePublicKeyHex
|
|
4927
|
+
};
|
|
4928
|
+
}
|
|
4622
4929
|
const parsed = readCredentialsJson();
|
|
4623
4930
|
if (parsed !== null) {
|
|
4624
4931
|
if (isLocalSigningKeyRecord(parsed)) {
|
|
@@ -4688,6 +4995,67 @@ function loadLocalSigner() {
|
|
|
4688
4995
|
}
|
|
4689
4996
|
};
|
|
4690
4997
|
}
|
|
4998
|
+
function loadLocalSigningKeyRecord() {
|
|
4999
|
+
const parsed = readCredentialsJson();
|
|
5000
|
+
if (parsed === null) return null;
|
|
5001
|
+
if (isLocalSigningKeyRecord(parsed)) {
|
|
5002
|
+
identityFromStored(
|
|
5003
|
+
parsed.delegatePrivateKey,
|
|
5004
|
+
parsed.delegatePublicKeyHex,
|
|
5005
|
+
parsed.delegateAddress
|
|
5006
|
+
);
|
|
5007
|
+
return parsed;
|
|
5008
|
+
}
|
|
5009
|
+
if (isValid(parsed)) return null;
|
|
5010
|
+
throw new Error(
|
|
5011
|
+
`credentials file at ${credsPath()} failed schema validation - refusing to continue`
|
|
5012
|
+
);
|
|
5013
|
+
}
|
|
5014
|
+
function loadLocalKeypair() {
|
|
5015
|
+
const parsed = readCredentialsJson();
|
|
5016
|
+
if (parsed === null) return null;
|
|
5017
|
+
if (!isLocalSigningKeyRecord(parsed) && !isValid(parsed)) {
|
|
5018
|
+
throw new Error(
|
|
5019
|
+
`credentials file at ${credsPath()} failed schema validation - refusing to continue`
|
|
5020
|
+
);
|
|
5021
|
+
}
|
|
5022
|
+
identityFromStored(
|
|
5023
|
+
parsed.delegatePrivateKey,
|
|
5024
|
+
parsed.delegatePublicKeyHex,
|
|
5025
|
+
parsed.delegateAddress
|
|
5026
|
+
);
|
|
5027
|
+
return Ed25519Keypair2.fromSecretKey(
|
|
5028
|
+
Uint8Array.from(Buffer.from(parsed.delegatePrivateKey, "hex"))
|
|
5029
|
+
);
|
|
5030
|
+
}
|
|
5031
|
+
function persistGatewayUrl(gatewayUrl) {
|
|
5032
|
+
if (!isHttpUrl(gatewayUrl)) {
|
|
5033
|
+
throw new Error("gatewayUrl must be an http(s) origin");
|
|
5034
|
+
}
|
|
5035
|
+
const parsed = readCredentialsJson();
|
|
5036
|
+
if (parsed === null) {
|
|
5037
|
+
throw new Error(
|
|
5038
|
+
`credentials file at ${credsPath()} is missing - refusing to persist gatewayUrl`
|
|
5039
|
+
);
|
|
5040
|
+
}
|
|
5041
|
+
if (isLocalSigningKeyRecord(parsed)) {
|
|
5042
|
+
identityFromStored(
|
|
5043
|
+
parsed.delegatePrivateKey,
|
|
5044
|
+
parsed.delegatePublicKeyHex,
|
|
5045
|
+
parsed.delegateAddress
|
|
5046
|
+
);
|
|
5047
|
+
writeCredentialsFile({ ...parsed, gatewayUrl });
|
|
5048
|
+
return;
|
|
5049
|
+
}
|
|
5050
|
+
if (isValid(parsed)) {
|
|
5051
|
+
rewriteV1ToLocalSigningKey(parsed);
|
|
5052
|
+
persistGatewayUrl(gatewayUrl);
|
|
5053
|
+
return;
|
|
5054
|
+
}
|
|
5055
|
+
throw new Error(
|
|
5056
|
+
`credentials file at ${credsPath()} failed schema validation - refusing to continue`
|
|
5057
|
+
);
|
|
5058
|
+
}
|
|
4691
5059
|
|
|
4692
5060
|
// src/agent-held.ts
|
|
4693
5061
|
var processLedger = null;
|
|
@@ -4715,38 +5083,6 @@ function offerSecret(env) {
|
|
|
4715
5083
|
const secret = env.SUIPAY_SECRET?.trim();
|
|
4716
5084
|
return secret ? secret : null;
|
|
4717
5085
|
}
|
|
4718
|
-
function settlementFromLocalSigner(cfg, local, packageId, env = process.env) {
|
|
4719
|
-
const chain = createSpendAccountChain({
|
|
4720
|
-
packageId,
|
|
4721
|
-
network: cfg.network,
|
|
4722
|
-
url: cfg.rpcUrl
|
|
4723
|
-
});
|
|
4724
|
-
const finality = createSpendAccountFinalityReader({ client: chain.client });
|
|
4725
|
-
const getObjectJson = async (objectId) => {
|
|
4726
|
-
const { object } = await chain.client.core.getObject({
|
|
4727
|
-
objectId,
|
|
4728
|
-
include: { json: true }
|
|
4729
|
-
});
|
|
4730
|
-
return objectWithOwner(object);
|
|
4731
|
-
};
|
|
4732
|
-
const secret = offerSecret(env);
|
|
4733
|
-
return {
|
|
4734
|
-
signer: {
|
|
4735
|
-
address: local.address,
|
|
4736
|
-
signTransaction: local.signTransaction,
|
|
4737
|
-
signPersonalMessage: local.signPersonalMessage
|
|
4738
|
-
},
|
|
4739
|
-
getObjectJson,
|
|
4740
|
-
payerDeps: {
|
|
4741
|
-
...secret ? { offerSecret: secret } : {},
|
|
4742
|
-
paymentLedger: sharedProcessLedger(),
|
|
4743
|
-
sponsor: { sponsorUrl: cfg.gatewayUrl },
|
|
4744
|
-
gasMode: "sponsored",
|
|
4745
|
-
resolveSharedObjectVersions: (ids) => resolveSharedObjectVersionsFromGetObject(getObjectJson, ids),
|
|
4746
|
-
getFinalizedTx: (digest) => finality.getFinalizedTx(digest)
|
|
4747
|
-
}
|
|
4748
|
-
};
|
|
4749
|
-
}
|
|
4750
5086
|
function resolveAgentHeldSettlement(cfg, spendAccountPay, env = process.env) {
|
|
4751
5087
|
const secret = offerSecret(env);
|
|
4752
5088
|
if (!secret) return null;
|
|
@@ -4840,65 +5176,324 @@ function toolAllowedByScope(toolName, grantedScope) {
|
|
|
4840
5176
|
}
|
|
4841
5177
|
|
|
4842
5178
|
// src/tools.ts
|
|
4843
|
-
import { isValidSuiAddress, normalizeSuiAddress as
|
|
5179
|
+
import { isValidSuiAddress, normalizeSuiAddress as normalizeSuiAddress12 } from "@mysten/sui/utils";
|
|
4844
5180
|
|
|
4845
|
-
//
|
|
4846
|
-
|
|
4847
|
-
|
|
4848
|
-
|
|
4849
|
-
|
|
4850
|
-
|
|
4851
|
-
|
|
4852
|
-
|
|
4853
|
-
|
|
4854
|
-
|
|
4855
|
-
|
|
4856
|
-
|
|
4857
|
-
|
|
4858
|
-
|
|
4859
|
-
|
|
4860
|
-
|
|
4861
|
-
|
|
4862
|
-
|
|
4863
|
-
|
|
4864
|
-
|
|
4865
|
-
|
|
4866
|
-
|
|
4867
|
-
|
|
4868
|
-
|
|
4869
|
-
*/
|
|
4870
|
-
GRANT_VERIFY_UNAVAILABLE: "GRANT_VERIFY_UNAVAILABLE"
|
|
4871
|
-
};
|
|
4872
|
-
function fail(code, detail) {
|
|
4873
|
-
return { ok: false, error: { code, detail } };
|
|
4874
|
-
}
|
|
4875
|
-
var OBJECT_ID_RE = /^0x[0-9a-fA-F]{1,64}$/;
|
|
4876
|
-
var HEX32_RE = /^(0x)?[0-9a-fA-F]{64}$/;
|
|
4877
|
-
var U64_MAX = (1n << 64n) - 1n;
|
|
4878
|
-
function isNonEmptyString(v) {
|
|
4879
|
-
return typeof v === "string" && v.trim().length > 0;
|
|
4880
|
-
}
|
|
4881
|
-
function isObjectId(v) {
|
|
4882
|
-
return typeof v === "string" && OBJECT_ID_RE.test(v.trim());
|
|
4883
|
-
}
|
|
4884
|
-
function parseU64PolicyId(onChain) {
|
|
4885
|
-
if (typeof onChain === "number") {
|
|
4886
|
-
if (!Number.isSafeInteger(onChain) || onChain < 0)
|
|
4887
|
-
return null;
|
|
4888
|
-
return String(onChain);
|
|
4889
|
-
}
|
|
4890
|
-
if (typeof onChain === "string" && /^\d+$/.test(onChain.trim())) {
|
|
4891
|
-
const s = onChain.trim();
|
|
4892
|
-
try {
|
|
4893
|
-
if (BigInt(s) <= U64_MAX)
|
|
4894
|
-
return s;
|
|
4895
|
-
} catch {
|
|
4896
|
-
return null;
|
|
5181
|
+
// src/settle.ts
|
|
5182
|
+
async function settlePreparedMcpPayment(args) {
|
|
5183
|
+
try {
|
|
5184
|
+
await verifyPreparedSettlementKind({
|
|
5185
|
+
kindBytes: args.signingRequest.unsignedKindBytes,
|
|
5186
|
+
expected: {
|
|
5187
|
+
packageId: args.signingRequest.packageId,
|
|
5188
|
+
delegateAddress: args.expected.delegateAddress,
|
|
5189
|
+
amount: args.expected.amount,
|
|
5190
|
+
recipient: args.expected.recipient,
|
|
5191
|
+
accessMode: args.signingRequest.submit.moveCallTarget.endsWith(
|
|
5192
|
+
"settle_open_recipient_payment"
|
|
5193
|
+
) ? "open" : "scoped"
|
|
5194
|
+
},
|
|
5195
|
+
getObjectJson: args.getObjectJson
|
|
5196
|
+
});
|
|
5197
|
+
} catch (err) {
|
|
5198
|
+
if (err instanceof PreparedVerificationUnavailableError) {
|
|
5199
|
+
return {
|
|
5200
|
+
status: "failed",
|
|
5201
|
+
paid: false,
|
|
5202
|
+
code: DELEGATED_PAY_ERROR_CODES.PREPARED_VERIFY_UNAVAILABLE,
|
|
5203
|
+
detail: err.message
|
|
5204
|
+
};
|
|
4897
5205
|
}
|
|
4898
|
-
|
|
4899
|
-
|
|
4900
|
-
|
|
4901
|
-
|
|
5206
|
+
return {
|
|
5207
|
+
status: "failed",
|
|
5208
|
+
paid: false,
|
|
5209
|
+
code: DELEGATED_PAY_ERROR_CODES.TX_MISMATCH,
|
|
5210
|
+
detail: err instanceof Error ? err.message : "prepared transaction mismatch"
|
|
5211
|
+
};
|
|
5212
|
+
}
|
|
5213
|
+
const prepared = args.signingRequest.resource;
|
|
5214
|
+
if (args.request.url !== prepared.url || args.request.method !== prepared.method) {
|
|
5215
|
+
return {
|
|
5216
|
+
status: "failed",
|
|
5217
|
+
paid: false,
|
|
5218
|
+
code: DELEGATED_PAY_ERROR_CODES.UNSUPPORTED_REQUEST,
|
|
5219
|
+
detail: "prepared resource request cannot be replayed"
|
|
5220
|
+
};
|
|
5221
|
+
}
|
|
5222
|
+
return payDelegatedSpendAccount({
|
|
5223
|
+
url: args.signingRequest.resource.url,
|
|
5224
|
+
request: args.request,
|
|
5225
|
+
session: args.session,
|
|
5226
|
+
cfg: args.cfg,
|
|
5227
|
+
deps: args.deps,
|
|
5228
|
+
preparedTerms: {
|
|
5229
|
+
recipient: args.expected.recipient,
|
|
5230
|
+
amount: args.expected.amount,
|
|
5231
|
+
asset: args.signingRequest.coinType,
|
|
5232
|
+
targetHash: args.signingRequest.targetHash
|
|
5233
|
+
},
|
|
5234
|
+
...args.intent ? { intent: args.intent } : {}
|
|
5235
|
+
});
|
|
5236
|
+
}
|
|
5237
|
+
|
|
5238
|
+
// ../sdk/dist/agent-sdk/delegate-agent.js
|
|
5239
|
+
import { normalizeSuiAddress as normalizeSuiAddress10 } from "@mysten/sui/utils";
|
|
5240
|
+
|
|
5241
|
+
// ../sdk/dist/agent-sdk/verify-prepared-settlement.js
|
|
5242
|
+
import { normalizeSuiAddress as normalizeSuiAddress9 } from "@mysten/sui/utils";
|
|
5243
|
+
import { Transaction as Transaction5 } from "@mysten/sui/transactions";
|
|
5244
|
+
import { fromBase64 as fromBase645 } from "@mysten/sui/utils";
|
|
5245
|
+
|
|
5246
|
+
// ../sdk/dist/protocol/prepared-settlement.js
|
|
5247
|
+
var PREPARED_SETTLEMENT_VERSION = 1;
|
|
5248
|
+
function asPreparedSettlement(value) {
|
|
5249
|
+
if (!value || typeof value !== "object")
|
|
5250
|
+
return null;
|
|
5251
|
+
const v = value;
|
|
5252
|
+
if (v.version !== PREPARED_SETTLEMENT_VERSION)
|
|
5253
|
+
return null;
|
|
5254
|
+
const accessMode = v.accessMode;
|
|
5255
|
+
if (accessMode !== "open" && accessMode !== "scoped")
|
|
5256
|
+
return null;
|
|
5257
|
+
const strings = [
|
|
5258
|
+
"transactionBytes",
|
|
5259
|
+
"transactionKindBytes",
|
|
5260
|
+
"sender",
|
|
5261
|
+
"packageId",
|
|
5262
|
+
"moveTarget",
|
|
5263
|
+
"poolObjectId",
|
|
5264
|
+
"grantObjectId",
|
|
5265
|
+
"poolInitialSharedVersion",
|
|
5266
|
+
"grantInitialSharedVersion",
|
|
5267
|
+
"clockInitialSharedVersion",
|
|
5268
|
+
"policyId",
|
|
5269
|
+
"coinType",
|
|
5270
|
+
"amount",
|
|
5271
|
+
"recipient",
|
|
5272
|
+
"payUrl"
|
|
5273
|
+
];
|
|
5274
|
+
for (const key of strings) {
|
|
5275
|
+
const got = v[key];
|
|
5276
|
+
if (typeof got !== "string" || !got.trim())
|
|
5277
|
+
return null;
|
|
5278
|
+
}
|
|
5279
|
+
return {
|
|
5280
|
+
version: PREPARED_SETTLEMENT_VERSION,
|
|
5281
|
+
accessMode,
|
|
5282
|
+
transactionBytes: v.transactionBytes,
|
|
5283
|
+
transactionKindBytes: v.transactionKindBytes,
|
|
5284
|
+
sender: v.sender,
|
|
5285
|
+
packageId: v.packageId,
|
|
5286
|
+
moveTarget: v.moveTarget,
|
|
5287
|
+
poolObjectId: v.poolObjectId,
|
|
5288
|
+
grantObjectId: v.grantObjectId,
|
|
5289
|
+
poolInitialSharedVersion: v.poolInitialSharedVersion,
|
|
5290
|
+
grantInitialSharedVersion: v.grantInitialSharedVersion,
|
|
5291
|
+
clockInitialSharedVersion: v.clockInitialSharedVersion,
|
|
5292
|
+
policyId: v.policyId,
|
|
5293
|
+
coinType: v.coinType,
|
|
5294
|
+
amount: v.amount,
|
|
5295
|
+
recipient: v.recipient,
|
|
5296
|
+
payUrl: v.payUrl
|
|
5297
|
+
};
|
|
5298
|
+
}
|
|
5299
|
+
|
|
5300
|
+
// ../sdk/dist/agent-sdk/verify-prepared-settlement.js
|
|
5301
|
+
var PreparedSettlementRejected = class extends Error {
|
|
5302
|
+
code = "PREPARED_SETTLEMENT_REJECTED";
|
|
5303
|
+
/** Which check failed, for a caller that logs rather than reads prose. */
|
|
5304
|
+
field;
|
|
5305
|
+
constructor(field, message) {
|
|
5306
|
+
super(message);
|
|
5307
|
+
this.name = "PreparedSettlementRejected";
|
|
5308
|
+
this.field = field;
|
|
5309
|
+
}
|
|
5310
|
+
};
|
|
5311
|
+
function reject(field, message) {
|
|
5312
|
+
throw new PreparedSettlementRejected(field, message);
|
|
5313
|
+
}
|
|
5314
|
+
function sameAddress3(a, b) {
|
|
5315
|
+
try {
|
|
5316
|
+
return normalizeSuiAddress9(a) === normalizeSuiAddress9(b);
|
|
5317
|
+
} catch {
|
|
5318
|
+
return false;
|
|
5319
|
+
}
|
|
5320
|
+
}
|
|
5321
|
+
function sameCoinType(a, b) {
|
|
5322
|
+
const norm = (value) => {
|
|
5323
|
+
const [pkg, ...rest] = value.trim().split("::");
|
|
5324
|
+
if (pkg === void 0 || rest.length !== 2)
|
|
5325
|
+
return value.trim();
|
|
5326
|
+
try {
|
|
5327
|
+
return `${normalizeSuiAddress9(pkg)}::${rest.join("::")}`;
|
|
5328
|
+
} catch {
|
|
5329
|
+
return value.trim();
|
|
5330
|
+
}
|
|
5331
|
+
};
|
|
5332
|
+
return norm(a) === norm(b);
|
|
5333
|
+
}
|
|
5334
|
+
function sameAmount(a, b) {
|
|
5335
|
+
try {
|
|
5336
|
+
return BigInt(a) === BigInt(b);
|
|
5337
|
+
} catch {
|
|
5338
|
+
return false;
|
|
5339
|
+
}
|
|
5340
|
+
}
|
|
5341
|
+
async function verifyPreparedPayment(input) {
|
|
5342
|
+
const prepared = asPreparedSettlement(input.prepared);
|
|
5343
|
+
if (!prepared) {
|
|
5344
|
+
reject("shape", "the 402 carried no usable prepared settlement");
|
|
5345
|
+
}
|
|
5346
|
+
const { offer, delegateAddress } = input;
|
|
5347
|
+
if (!sameAddress3(prepared.sender, delegateAddress)) {
|
|
5348
|
+
reject("sender", `prepared sender ${prepared.sender} is not this agent's address`);
|
|
5349
|
+
}
|
|
5350
|
+
let data;
|
|
5351
|
+
try {
|
|
5352
|
+
data = Transaction5.from(fromBase645(prepared.transactionBytes)).getData();
|
|
5353
|
+
} catch {
|
|
5354
|
+
reject("transactionBytes", "prepared transaction bytes are not a Sui transaction");
|
|
5355
|
+
}
|
|
5356
|
+
const gasOwner = data.gasData?.owner;
|
|
5357
|
+
if (!gasOwner || sameAddress3(gasOwner, delegateAddress)) {
|
|
5358
|
+
reject("gasOwner", "prepared transaction is not sponsored: gas would be paid by this agent");
|
|
5359
|
+
}
|
|
5360
|
+
if (input.expectedGrantObjectId && !sameAddress3(prepared.grantObjectId, input.expectedGrantObjectId)) {
|
|
5361
|
+
reject("grantObjectId", `prepared settlement draws on grant ${prepared.grantObjectId}, not the one this agent resolved`);
|
|
5362
|
+
}
|
|
5363
|
+
if (input.expectedPoolObjectId && !sameAddress3(prepared.poolObjectId, input.expectedPoolObjectId)) {
|
|
5364
|
+
reject("poolObjectId", `prepared settlement draws on pool ${prepared.poolObjectId}, not the one this agent resolved`);
|
|
5365
|
+
}
|
|
5366
|
+
if (!offer.packageId || !sameAddress3(prepared.packageId, offer.packageId)) {
|
|
5367
|
+
reject("packageId", "prepared package id does not match the offer");
|
|
5368
|
+
}
|
|
5369
|
+
const wantedTarget = payMoveCallTarget(offer.packageId, prepared.accessMode);
|
|
5370
|
+
if (prepared.moveTarget !== wantedTarget) {
|
|
5371
|
+
reject("moveTarget", `prepared Move target ${prepared.moveTarget} is not ${wantedTarget}`);
|
|
5372
|
+
}
|
|
5373
|
+
if (!sameCoinType(prepared.coinType, offer.asset)) {
|
|
5374
|
+
reject("coinType", "prepared coin type does not match the offer asset");
|
|
5375
|
+
}
|
|
5376
|
+
if (!sameAmount(prepared.amount, offer.amount)) {
|
|
5377
|
+
reject("amount", "prepared amount does not match the offer amount");
|
|
5378
|
+
}
|
|
5379
|
+
if (!sameAddress3(prepared.recipient, offer.payTo)) {
|
|
5380
|
+
reject("recipient", "prepared recipient does not match the offer payTo");
|
|
5381
|
+
}
|
|
5382
|
+
const targetHash = offer.targetHash?.trim();
|
|
5383
|
+
if (!targetHash) {
|
|
5384
|
+
reject("targetHash", "offer carries no targetHash to bind the settlement to");
|
|
5385
|
+
}
|
|
5386
|
+
if (!offer.digest?.trim()) {
|
|
5387
|
+
reject("digest", "offer carries no digest, so its terms hash cannot be derived");
|
|
5388
|
+
}
|
|
5389
|
+
const common = {
|
|
5390
|
+
packageId: offer.packageId,
|
|
5391
|
+
delegate: prepared.sender,
|
|
5392
|
+
coinType: prepared.coinType,
|
|
5393
|
+
poolObjectId: prepared.poolObjectId,
|
|
5394
|
+
grantObjectId: prepared.grantObjectId,
|
|
5395
|
+
poolInitialSharedVersion: prepared.poolInitialSharedVersion,
|
|
5396
|
+
grantInitialSharedVersion: prepared.grantInitialSharedVersion,
|
|
5397
|
+
clockInitialSharedVersion: prepared.clockInitialSharedVersion,
|
|
5398
|
+
targetHash,
|
|
5399
|
+
amount: offer.amount,
|
|
5400
|
+
paymentIdHash: paymentIdHash(offer.challengeId),
|
|
5401
|
+
termsHash: termsHash(offer)
|
|
5402
|
+
};
|
|
5403
|
+
let rebuilt;
|
|
5404
|
+
try {
|
|
5405
|
+
rebuilt = prepared.accessMode === "open" ? (await buildSpendAccountSettleOpenRecipientKind({
|
|
5406
|
+
...common,
|
|
5407
|
+
recipient: prepared.recipient
|
|
5408
|
+
})).kindBytes : (await buildSpendAccountPayKind({
|
|
5409
|
+
...common,
|
|
5410
|
+
policyId: prepared.policyId
|
|
5411
|
+
})).kindBytes;
|
|
5412
|
+
} catch (err) {
|
|
5413
|
+
reject("rebuild", `the prepared settlement could not be rebuilt offline: ${err instanceof Error ? err.message : String(err)}`);
|
|
5414
|
+
}
|
|
5415
|
+
if (rebuilt !== prepared.transactionKindBytes) {
|
|
5416
|
+
reject("transactionKindBytes", "the prepared transaction kind is not the one this offer describes");
|
|
5417
|
+
}
|
|
5418
|
+
try {
|
|
5419
|
+
assertPayTransaction(prepared.transactionBytes, {
|
|
5420
|
+
packageId: offer.packageId,
|
|
5421
|
+
delegate: prepared.sender,
|
|
5422
|
+
poolObjectId: prepared.poolObjectId,
|
|
5423
|
+
grantObjectId: prepared.grantObjectId,
|
|
5424
|
+
policyId: prepared.policyId,
|
|
5425
|
+
targetHash,
|
|
5426
|
+
amount: offer.amount,
|
|
5427
|
+
paymentIdHash: common.paymentIdHash,
|
|
5428
|
+
termsHash: common.termsHash,
|
|
5429
|
+
coinType: prepared.coinType,
|
|
5430
|
+
kindBytes: rebuilt,
|
|
5431
|
+
accessMode: prepared.accessMode,
|
|
5432
|
+
recipient: prepared.recipient
|
|
5433
|
+
});
|
|
5434
|
+
} catch (err) {
|
|
5435
|
+
reject("transactionBytes", `prepared transaction does not reconstruct the intended settlement: ${err instanceof Error ? err.message : String(err)}`);
|
|
5436
|
+
}
|
|
5437
|
+
return prepared;
|
|
5438
|
+
}
|
|
5439
|
+
|
|
5440
|
+
// ../sdk/dist/agent-sdk/manifest.js
|
|
5441
|
+
var MANIFEST_ERROR_CODES = {
|
|
5442
|
+
MALFORMED_MANIFEST: "MALFORMED_MANIFEST",
|
|
5443
|
+
MISSING_NETWORK: "MISSING_NETWORK",
|
|
5444
|
+
MISSING_PACKAGE: "MISSING_PACKAGE",
|
|
5445
|
+
MISSING_DELEGATE: "MISSING_DELEGATE",
|
|
5446
|
+
NO_GRANTS: "NO_GRANTS",
|
|
5447
|
+
MISSING_COIN_TYPE: "MISSING_COIN_TYPE",
|
|
5448
|
+
MISSING_POOL_OBJECT: "MISSING_POOL_OBJECT",
|
|
5449
|
+
MISSING_GRANT_OBJECT: "MISSING_GRANT_OBJECT",
|
|
5450
|
+
MISSING_POLICY_BINDING: "MISSING_POLICY_BINDING",
|
|
5451
|
+
INVALID_ACCESS_MODE: "INVALID_ACCESS_MODE",
|
|
5452
|
+
GRANT_NOT_FOUND: "GRANT_NOT_FOUND",
|
|
5453
|
+
AMBIGUOUS_GRANT: "AMBIGUOUS_GRANT",
|
|
5454
|
+
/**
|
|
5455
|
+
* The chain does not agree that this grant belongs to this delegate. The
|
|
5456
|
+
* gateway said it did; an unsigned `getObject` says otherwise, and the chain
|
|
5457
|
+
* wins (ADR-0016 §3).
|
|
5458
|
+
*/
|
|
5459
|
+
GRANT_NOT_VERIFIED: "GRANT_NOT_VERIFIED",
|
|
5460
|
+
/**
|
|
5461
|
+
* The on-chain check could not run to a verdict — the fullnode read was
|
|
5462
|
+
* unreachable or returned nothing. "Could not verify" is not "verified and it
|
|
5463
|
+
* failed": both refuse, but only one is a security event.
|
|
5464
|
+
*/
|
|
5465
|
+
GRANT_VERIFY_UNAVAILABLE: "GRANT_VERIFY_UNAVAILABLE"
|
|
5466
|
+
};
|
|
5467
|
+
function fail(code, detail) {
|
|
5468
|
+
return { ok: false, error: { code, detail } };
|
|
5469
|
+
}
|
|
5470
|
+
var OBJECT_ID_RE = /^0x[0-9a-fA-F]{1,64}$/;
|
|
5471
|
+
var HEX32_RE = /^(0x)?[0-9a-fA-F]{64}$/;
|
|
5472
|
+
var U64_MAX = (1n << 64n) - 1n;
|
|
5473
|
+
function isNonEmptyString(v) {
|
|
5474
|
+
return typeof v === "string" && v.trim().length > 0;
|
|
5475
|
+
}
|
|
5476
|
+
function isObjectId(v) {
|
|
5477
|
+
return typeof v === "string" && OBJECT_ID_RE.test(v.trim());
|
|
5478
|
+
}
|
|
5479
|
+
function parseU64PolicyId(onChain) {
|
|
5480
|
+
if (typeof onChain === "number") {
|
|
5481
|
+
if (!Number.isSafeInteger(onChain) || onChain < 0)
|
|
5482
|
+
return null;
|
|
5483
|
+
return String(onChain);
|
|
5484
|
+
}
|
|
5485
|
+
if (typeof onChain === "string" && /^\d+$/.test(onChain.trim())) {
|
|
5486
|
+
const s = onChain.trim();
|
|
5487
|
+
try {
|
|
5488
|
+
if (BigInt(s) <= U64_MAX)
|
|
5489
|
+
return s;
|
|
5490
|
+
} catch {
|
|
5491
|
+
return null;
|
|
5492
|
+
}
|
|
5493
|
+
}
|
|
5494
|
+
return null;
|
|
5495
|
+
}
|
|
5496
|
+
function normalizePolicyBinding(raw) {
|
|
4902
5497
|
if (!raw || typeof raw !== "object") {
|
|
4903
5498
|
return fail(MANIFEST_ERROR_CODES.MISSING_POLICY_BINDING, "policy is not an object");
|
|
4904
5499
|
}
|
|
@@ -5272,71 +5867,489 @@ async function resolvePayerSession(options) {
|
|
|
5272
5867
|
});
|
|
5273
5868
|
}
|
|
5274
5869
|
|
|
5275
|
-
//
|
|
5276
|
-
|
|
5870
|
+
// ../sdk/dist/buyer/transport.js
|
|
5871
|
+
var FORBIDDEN_SECRET_KEYS = [
|
|
5872
|
+
"privateKey",
|
|
5873
|
+
"private_key",
|
|
5874
|
+
"privateKeyHex",
|
|
5875
|
+
"secretKey",
|
|
5876
|
+
"secret_key",
|
|
5877
|
+
"secret",
|
|
5878
|
+
"seed",
|
|
5879
|
+
"seedPhrase",
|
|
5880
|
+
"seed_phrase",
|
|
5881
|
+
"mnemonic",
|
|
5882
|
+
"privkey",
|
|
5883
|
+
"suiprivkey"
|
|
5884
|
+
];
|
|
5885
|
+
var FORBIDDEN_SECRET_KEY_SET = new Set(FORBIDDEN_SECRET_KEYS.map((k) => k.toLowerCase()));
|
|
5277
5886
|
|
|
5278
|
-
//
|
|
5279
|
-
|
|
5887
|
+
// ../sdk/dist/agent-sdk/smoke.js
|
|
5888
|
+
function mergeObjectEnvelope(object) {
|
|
5889
|
+
if (!object || typeof object.json !== "object" || object.json === null) {
|
|
5890
|
+
return null;
|
|
5891
|
+
}
|
|
5892
|
+
return { ...object.json, owner: object.owner };
|
|
5893
|
+
}
|
|
5894
|
+
var SECRET_KEY_SET = new Set(FORBIDDEN_SECRET_KEYS.map((k) => k.toLowerCase()));
|
|
5895
|
+
|
|
5896
|
+
// ../sdk/dist/agent-sdk/delegate-agent.js
|
|
5897
|
+
async function verifyGrantOnChain(input) {
|
|
5898
|
+
let state;
|
|
5280
5899
|
try {
|
|
5281
|
-
await
|
|
5282
|
-
|
|
5283
|
-
|
|
5284
|
-
|
|
5285
|
-
|
|
5286
|
-
|
|
5287
|
-
|
|
5288
|
-
|
|
5289
|
-
|
|
5290
|
-
|
|
5291
|
-
|
|
5292
|
-
|
|
5900
|
+
state = await readPublicGrantState(input.getObjectJson, input.grantObjectId);
|
|
5901
|
+
} catch {
|
|
5902
|
+
return {
|
|
5903
|
+
ok: false,
|
|
5904
|
+
error: {
|
|
5905
|
+
code: MANIFEST_ERROR_CODES.GRANT_VERIFY_UNAVAILABLE,
|
|
5906
|
+
detail: `the on-chain read of grant ${input.grantObjectId} did not complete, so this session was not verified and no payment was prepared`
|
|
5907
|
+
}
|
|
5908
|
+
};
|
|
5909
|
+
}
|
|
5910
|
+
if (!state) {
|
|
5911
|
+
return {
|
|
5912
|
+
ok: false,
|
|
5913
|
+
error: {
|
|
5914
|
+
code: MANIFEST_ERROR_CODES.GRANT_VERIFY_UNAVAILABLE,
|
|
5915
|
+
detail: `grant ${input.grantObjectId} returned no readable on-chain state`
|
|
5916
|
+
}
|
|
5917
|
+
};
|
|
5918
|
+
}
|
|
5919
|
+
const notVerified = (detail) => ({
|
|
5920
|
+
ok: false,
|
|
5921
|
+
error: { code: MANIFEST_ERROR_CODES.GRANT_NOT_VERIFIED, detail }
|
|
5922
|
+
});
|
|
5923
|
+
if (!state.delegateAddress || normalizeSuiAddress10(state.delegateAddress) !== normalizeSuiAddress10(input.delegateAddress)) {
|
|
5924
|
+
return notVerified(`grant ${input.grantObjectId} does not name this delegate on chain`);
|
|
5925
|
+
}
|
|
5926
|
+
if (!state.poolObjectId || normalizeSuiAddress10(state.poolObjectId) !== normalizeSuiAddress10(input.poolObjectId)) {
|
|
5927
|
+
return notVerified(`grant ${input.grantObjectId} draws on a different pool than the one resolved`);
|
|
5928
|
+
}
|
|
5929
|
+
if (state.revoked === true) {
|
|
5930
|
+
return notVerified(`grant ${input.grantObjectId} is revoked on chain`);
|
|
5931
|
+
}
|
|
5932
|
+
if (state.paused === true) {
|
|
5933
|
+
return notVerified(`grant ${input.grantObjectId} is paused on chain`);
|
|
5934
|
+
}
|
|
5935
|
+
return { ok: true, value: null };
|
|
5936
|
+
}
|
|
5937
|
+
async function createPayer(options) {
|
|
5938
|
+
const keypair = options.keypair;
|
|
5939
|
+
const address = keypair.getPublicKey().toSuiAddress();
|
|
5940
|
+
const resolved = await resolvePayerSession({
|
|
5941
|
+
gatewayUrl: options.gatewayUrl,
|
|
5942
|
+
delegateAddress: address,
|
|
5943
|
+
// The bootstrap signature and the payment signature come from the same key.
|
|
5944
|
+
// A caller never wires a second signer for either this or the sponsor personal-message.
|
|
5945
|
+
signPersonalMessage: async (message) => {
|
|
5946
|
+
const { signature } = await keypair.signPersonalMessage(message);
|
|
5947
|
+
return signature;
|
|
5948
|
+
},
|
|
5949
|
+
signer: keypair,
|
|
5950
|
+
...options.select ? { select: options.select } : {},
|
|
5951
|
+
...options.deployment ? { deployment: options.deployment } : {},
|
|
5952
|
+
...options.fetchImpl ? { fetchImpl: options.fetchImpl } : {}
|
|
5953
|
+
});
|
|
5954
|
+
if (!resolved.ok)
|
|
5955
|
+
return resolved;
|
|
5956
|
+
const { session, binding } = resolved.value;
|
|
5957
|
+
const deployment = {
|
|
5958
|
+
network: session.network,
|
|
5959
|
+
packageId: session.packageId
|
|
5960
|
+
};
|
|
5961
|
+
let network;
|
|
5962
|
+
try {
|
|
5963
|
+
network = toGrpcNetwork(deployment.network);
|
|
5964
|
+
} catch {
|
|
5965
|
+
return {
|
|
5966
|
+
ok: false,
|
|
5967
|
+
error: {
|
|
5968
|
+
code: MANIFEST_ERROR_CODES.MISSING_NETWORK,
|
|
5969
|
+
detail: `the gateway reports network "${deployment.network}", which has no Sui fullnode this SDK can read; pass fullnodeUrl to name one`
|
|
5970
|
+
}
|
|
5971
|
+
};
|
|
5972
|
+
}
|
|
5973
|
+
let chain;
|
|
5974
|
+
try {
|
|
5975
|
+
chain = createSpendAccountChain({
|
|
5976
|
+
packageId: deployment.packageId,
|
|
5977
|
+
network,
|
|
5978
|
+
...options.fullnodeUrl ? { url: options.fullnodeUrl } : {},
|
|
5979
|
+
...options.client ? { client: options.client } : {}
|
|
5293
5980
|
});
|
|
5294
|
-
} catch
|
|
5295
|
-
|
|
5981
|
+
} catch {
|
|
5982
|
+
return {
|
|
5983
|
+
ok: false,
|
|
5984
|
+
error: {
|
|
5985
|
+
code: MANIFEST_ERROR_CODES.GRANT_VERIFY_UNAVAILABLE,
|
|
5986
|
+
detail: "the agent could not open a Sui fullnode client, so this grant was not verified and no payment was prepared"
|
|
5987
|
+
}
|
|
5988
|
+
};
|
|
5989
|
+
}
|
|
5990
|
+
const finality = createSpendAccountFinalityReader({ client: chain.client });
|
|
5991
|
+
const getObjectJson = async (objectId) => {
|
|
5992
|
+
const { object } = await chain.client.core.getObject({
|
|
5993
|
+
objectId,
|
|
5994
|
+
include: { json: true }
|
|
5995
|
+
});
|
|
5996
|
+
return mergeObjectEnvelope(object);
|
|
5997
|
+
};
|
|
5998
|
+
const verified = await verifyGrantOnChain({
|
|
5999
|
+
getObjectJson,
|
|
6000
|
+
grantObjectId: binding.grantObjectId,
|
|
6001
|
+
poolObjectId: binding.poolObjectId,
|
|
6002
|
+
delegateAddress: address
|
|
6003
|
+
});
|
|
6004
|
+
if (!verified.ok)
|
|
6005
|
+
return verified;
|
|
6006
|
+
const deps = {
|
|
6007
|
+
paymentLedger: options.paymentLedger ?? createMemoryLedger(),
|
|
6008
|
+
// Sponsorship IS a gateway service: the agent holds no SUI for gas, and the
|
|
6009
|
+
// sponsor can only attach gas to a transaction this agent signed.
|
|
6010
|
+
sponsor: { sponsorUrl: options.gatewayUrl },
|
|
6011
|
+
gasMode: "sponsored",
|
|
6012
|
+
resolveSharedObjectVersions: (ids) => resolveSharedObjectVersionsFromGetObject(getObjectJson, ids),
|
|
6013
|
+
getFinalizedTx: finality.getFinalizedTx,
|
|
6014
|
+
...options.offerSecret ? { offerSecret: options.offerSecret } : {},
|
|
6015
|
+
...options.fetchImpl ? { fetchImpl: options.fetchImpl } : {},
|
|
6016
|
+
...options.logError ? { logError: options.logError } : {}
|
|
6017
|
+
};
|
|
6018
|
+
async function pay2(url, input) {
|
|
6019
|
+
let request;
|
|
6020
|
+
try {
|
|
6021
|
+
request = normalizePaidHttpRequest({
|
|
6022
|
+
url,
|
|
6023
|
+
method: input.method,
|
|
6024
|
+
...input.body !== void 0 ? { body: input.body } : {},
|
|
6025
|
+
...input.contentType !== void 0 ? { contentType: input.contentType } : {}
|
|
6026
|
+
});
|
|
6027
|
+
} catch (err) {
|
|
6028
|
+
options.logError?.("normalize paid request failed", err);
|
|
5296
6029
|
return {
|
|
5297
6030
|
status: "failed",
|
|
5298
6031
|
paid: false,
|
|
5299
|
-
code: DELEGATED_PAY_ERROR_CODES.
|
|
5300
|
-
detail:
|
|
6032
|
+
code: DELEGATED_PAY_ERROR_CODES.UNSUPPORTED_REQUEST,
|
|
6033
|
+
detail: "request cannot be paid for"
|
|
5301
6034
|
};
|
|
5302
6035
|
}
|
|
5303
|
-
return {
|
|
5304
|
-
|
|
5305
|
-
|
|
5306
|
-
|
|
5307
|
-
|
|
5308
|
-
|
|
6036
|
+
return payDelegatedSpendAccount({
|
|
6037
|
+
url,
|
|
6038
|
+
request,
|
|
6039
|
+
session,
|
|
6040
|
+
cfg: { gatewayUrl: options.gatewayUrl },
|
|
6041
|
+
deps,
|
|
6042
|
+
intent: input.intent,
|
|
6043
|
+
...input.preparedTerms ? { preparedTerms: input.preparedTerms } : {}
|
|
6044
|
+
});
|
|
5309
6045
|
}
|
|
5310
|
-
|
|
5311
|
-
|
|
5312
|
-
|
|
5313
|
-
|
|
5314
|
-
|
|
5315
|
-
|
|
5316
|
-
|
|
5317
|
-
};
|
|
6046
|
+
async function verifySettlement(prepared, offer) {
|
|
6047
|
+
return verifyPreparedPayment({
|
|
6048
|
+
prepared,
|
|
6049
|
+
offer,
|
|
6050
|
+
delegateAddress: address,
|
|
6051
|
+
expectedGrantObjectId: binding.grantObjectId,
|
|
6052
|
+
expectedPoolObjectId: binding.poolObjectId
|
|
6053
|
+
});
|
|
5318
6054
|
}
|
|
5319
|
-
return
|
|
5320
|
-
|
|
5321
|
-
|
|
5322
|
-
|
|
5323
|
-
|
|
5324
|
-
|
|
5325
|
-
|
|
5326
|
-
|
|
5327
|
-
|
|
5328
|
-
|
|
5329
|
-
|
|
5330
|
-
|
|
5331
|
-
|
|
6055
|
+
return {
|
|
6056
|
+
ok: true,
|
|
6057
|
+
value: { address, deployment, grant: binding, session, pay: pay2, verifySettlement }
|
|
6058
|
+
};
|
|
6059
|
+
}
|
|
6060
|
+
|
|
6061
|
+
// src/stdio-login.ts
|
|
6062
|
+
import { createServer } from "http";
|
|
6063
|
+
import { randomBytes as randomBytes3, timingSafeEqual as timingSafeEqual2 } from "crypto";
|
|
6064
|
+
import open from "open";
|
|
6065
|
+
var HEX642 = /^[0-9a-f]{64}$/;
|
|
6066
|
+
var PUBHEX = /^[0-9a-fA-F]{64}$/;
|
|
6067
|
+
function isPreflight(obj) {
|
|
6068
|
+
if (!obj || typeof obj !== "object") return false;
|
|
6069
|
+
const o = obj;
|
|
6070
|
+
return typeof o.state === "string" && HEX642.test(o.state) && typeof o.publicKey === "string" && PUBHEX.test(o.publicKey) && typeof o.delegateAddress === "string" && /^0x[0-9a-fA-F]{64}$/.test(o.delegateAddress) && typeof o.gateway === "string" && o.gateway.length > 0;
|
|
6071
|
+
}
|
|
6072
|
+
function isCallback(obj) {
|
|
6073
|
+
if (!obj || typeof obj !== "object") return false;
|
|
6074
|
+
const o = obj;
|
|
6075
|
+
return typeof o.state === "string" && HEX642.test(o.state) && typeof o.gatewayUrl === "string" && o.gatewayUrl.length > 0;
|
|
6076
|
+
}
|
|
6077
|
+
function stateEquals(a, b) {
|
|
6078
|
+
if (a.length !== b.length) return false;
|
|
6079
|
+
try {
|
|
6080
|
+
return timingSafeEqual2(Buffer.from(a, "hex"), Buffer.from(b, "hex"));
|
|
6081
|
+
} catch {
|
|
6082
|
+
return false;
|
|
6083
|
+
}
|
|
6084
|
+
}
|
|
6085
|
+
function normalizeOrigin(url) {
|
|
6086
|
+
try {
|
|
6087
|
+
return new URL(url).origin;
|
|
6088
|
+
} catch {
|
|
6089
|
+
return url.replace(/\/+$/, "");
|
|
6090
|
+
}
|
|
6091
|
+
}
|
|
6092
|
+
function loopbackAlternateOrigin(origin) {
|
|
6093
|
+
try {
|
|
6094
|
+
const url = new URL(origin);
|
|
6095
|
+
if (url.protocol !== "http:" && url.protocol !== "https:") return null;
|
|
6096
|
+
if (url.hostname === "localhost") {
|
|
6097
|
+
url.hostname = "127.0.0.1";
|
|
6098
|
+
return url.origin;
|
|
6099
|
+
}
|
|
6100
|
+
if (url.hostname === "127.0.0.1") {
|
|
6101
|
+
url.hostname = "localhost";
|
|
6102
|
+
return url.origin;
|
|
6103
|
+
}
|
|
6104
|
+
return null;
|
|
6105
|
+
} catch {
|
|
6106
|
+
return null;
|
|
6107
|
+
}
|
|
6108
|
+
}
|
|
6109
|
+
function normalizeUrl(url) {
|
|
6110
|
+
return url.replace(/\/+$/, "");
|
|
6111
|
+
}
|
|
6112
|
+
function readBody2(req, maxBytes = 16 * 1024) {
|
|
6113
|
+
return new Promise((resolve, reject2) => {
|
|
6114
|
+
const chunks = [];
|
|
6115
|
+
let len = 0;
|
|
6116
|
+
req.on("data", (c) => {
|
|
6117
|
+
len += c.length;
|
|
6118
|
+
if (len > maxBytes) {
|
|
6119
|
+
reject2(new Error("body too large"));
|
|
6120
|
+
req.destroy();
|
|
6121
|
+
return;
|
|
6122
|
+
}
|
|
6123
|
+
chunks.push(c);
|
|
6124
|
+
});
|
|
6125
|
+
req.on("end", () => resolve(Buffer.concat(chunks).toString("utf8")));
|
|
6126
|
+
req.on("error", reject2);
|
|
6127
|
+
});
|
|
6128
|
+
}
|
|
6129
|
+
var SECURITY_HEADERS = {
|
|
6130
|
+
"cache-control": "no-store",
|
|
6131
|
+
"x-content-type-options": "nosniff",
|
|
6132
|
+
"referrer-policy": "no-referrer",
|
|
6133
|
+
"content-security-policy": "default-src 'none'"
|
|
6134
|
+
};
|
|
6135
|
+
var SUCCESS_HTML = `<!doctype html><meta charset=utf-8><title>SuiPay MCP - Connected</title><body style="font:16px system-ui;max-width:480px;margin:80px auto;padding:0 24px"><h1>\u2713 SuiPay MCP connected</h1><p>Credentials saved to <code>~/.suipay/credentials.json</code>. You can close this tab \u2014 your MCP client will pick up the new credentials automatically.</p>`;
|
|
6136
|
+
var failHtml = (m) => `<!doctype html><meta charset=utf-8><title>SuiPay MCP - Failed</title><body style="font:16px system-ui;max-width:480px;margin:80px auto;padding:0 24px"><h1>\xD7 Login failed</h1><p>${m}</p>`;
|
|
6137
|
+
async function startStdioLogin(opts) {
|
|
6138
|
+
const timeoutMs = opts.timeoutMs ?? 5 * 6e4;
|
|
6139
|
+
const openBrowser = opts.openBrowser ?? false;
|
|
6140
|
+
const label = opts.label ?? "SuiPay MCP";
|
|
6141
|
+
const expectedOrigin = normalizeOrigin(opts.consoleUrl);
|
|
6142
|
+
const allowedCorsOrigins = /* @__PURE__ */ new Set([expectedOrigin]);
|
|
6143
|
+
const loopbackAlt = loopbackAlternateOrigin(expectedOrigin);
|
|
6144
|
+
if (loopbackAlt) allowedCorsOrigins.add(loopbackAlt);
|
|
6145
|
+
const expectedGateway = normalizeUrl(opts.gatewayUrl);
|
|
6146
|
+
const connectState = randomBytes3(32).toString("hex");
|
|
6147
|
+
const server = createServer();
|
|
6148
|
+
server.requestTimeout = 1e4;
|
|
6149
|
+
server.headersTimeout = 1e4;
|
|
6150
|
+
server.keepAliveTimeout = 5e3;
|
|
6151
|
+
await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve));
|
|
6152
|
+
const port = server.address().port;
|
|
6153
|
+
const allowedHosts = /* @__PURE__ */ new Set([`127.0.0.1:${port}`, `localhost:${port}`]);
|
|
6154
|
+
const base = opts.consoleUrl.replace(/\/+$/, "");
|
|
6155
|
+
const connectUrl = `${base}/connect/agent?port=${port}&publicKey=${encodeURIComponent(opts.identity.publicKeyHex)}&delegateAddress=${encodeURIComponent(opts.identity.delegateAddress)}&label=${encodeURIComponent(label)}&gateway=${encodeURIComponent(opts.gatewayUrl)}&connectState=${connectState}`;
|
|
6156
|
+
try {
|
|
6157
|
+
opts.onUrl?.(connectUrl);
|
|
6158
|
+
} catch {
|
|
6159
|
+
}
|
|
6160
|
+
let preflightVerified = false;
|
|
6161
|
+
let completed = false;
|
|
6162
|
+
let error2 = null;
|
|
6163
|
+
const done = new Promise((resolve) => {
|
|
6164
|
+
const timer = setTimeout(() => {
|
|
6165
|
+
if (!completed) {
|
|
6166
|
+
error2 = new Error(`login timed out after ${timeoutMs}ms`);
|
|
6167
|
+
server.close();
|
|
6168
|
+
resolve();
|
|
6169
|
+
}
|
|
6170
|
+
}, timeoutMs);
|
|
6171
|
+
timer.unref?.();
|
|
6172
|
+
server.on("request", async (req, res) => {
|
|
6173
|
+
const hostHeader = (req.headers.host ?? "").toLowerCase();
|
|
6174
|
+
const origin = req.headers.origin;
|
|
6175
|
+
const url = new URL(req.url ?? "/", `http://127.0.0.1:${port}`);
|
|
6176
|
+
const okHost = allowedHosts.has(hostHeader);
|
|
6177
|
+
let requestOrigin = null;
|
|
6178
|
+
if (typeof origin === "string") {
|
|
6179
|
+
try {
|
|
6180
|
+
requestOrigin = new URL(origin).origin;
|
|
6181
|
+
} catch {
|
|
6182
|
+
requestOrigin = null;
|
|
6183
|
+
}
|
|
6184
|
+
}
|
|
6185
|
+
const okOrigin = requestOrigin !== null && allowedCorsOrigins.has(requestOrigin);
|
|
6186
|
+
const cors = {
|
|
6187
|
+
"access-control-allow-origin": okOrigin && requestOrigin ? requestOrigin : expectedOrigin,
|
|
6188
|
+
"access-control-allow-methods": "POST, OPTIONS",
|
|
6189
|
+
"access-control-allow-headers": "content-type",
|
|
6190
|
+
vary: "origin"
|
|
6191
|
+
};
|
|
6192
|
+
const send = (code, body, type = "text/plain", extra = {}) => {
|
|
6193
|
+
res.writeHead(code, { "content-type": type, ...SECURITY_HEADERS, ...extra });
|
|
6194
|
+
res.end(body);
|
|
6195
|
+
};
|
|
6196
|
+
const okPath = url.pathname === "/preflight" || url.pathname === "/callback";
|
|
6197
|
+
if (req.method === "OPTIONS") {
|
|
6198
|
+
const extra = { ...cors };
|
|
6199
|
+
if (okHost && okOrigin && okPath && req.headers["access-control-request-private-network"] === "true") {
|
|
6200
|
+
extra["access-control-allow-private-network"] = "true";
|
|
6201
|
+
}
|
|
6202
|
+
res.writeHead(204, { ...SECURITY_HEADERS, ...extra });
|
|
6203
|
+
res.end();
|
|
6204
|
+
return;
|
|
6205
|
+
}
|
|
6206
|
+
if (req.method !== "POST" || !okPath) {
|
|
6207
|
+
send(404, "Not found");
|
|
6208
|
+
return;
|
|
6209
|
+
}
|
|
6210
|
+
if (!okHost) {
|
|
6211
|
+
send(403, "Forbidden");
|
|
6212
|
+
return;
|
|
6213
|
+
}
|
|
6214
|
+
if (!okOrigin) {
|
|
6215
|
+
send(403, "Forbidden");
|
|
6216
|
+
return;
|
|
6217
|
+
}
|
|
6218
|
+
const ct = (req.headers["content-type"] ?? "").toLowerCase();
|
|
6219
|
+
if (!ct.startsWith("application/json")) {
|
|
6220
|
+
send(415, "Content-Type must be application/json");
|
|
6221
|
+
return;
|
|
6222
|
+
}
|
|
6223
|
+
try {
|
|
6224
|
+
const parsed = JSON.parse(await readBody2(req));
|
|
6225
|
+
if (url.pathname === "/preflight") {
|
|
6226
|
+
if (!isPreflight(parsed)) {
|
|
6227
|
+
res.writeHead(400, {
|
|
6228
|
+
"content-type": "application/json",
|
|
6229
|
+
...cors,
|
|
6230
|
+
...SECURITY_HEADERS
|
|
6231
|
+
});
|
|
6232
|
+
res.end(JSON.stringify({ ok: false, error: "invalid preflight payload" }));
|
|
6233
|
+
return;
|
|
6234
|
+
}
|
|
6235
|
+
const derived = deriveSuiAddress(
|
|
6236
|
+
Uint8Array.from(Buffer.from(parsed.publicKey, "hex"))
|
|
6237
|
+
);
|
|
6238
|
+
const valid = stateEquals(parsed.state, connectState) && stateEquals(
|
|
6239
|
+
parsed.publicKey.toLowerCase(),
|
|
6240
|
+
opts.identity.publicKeyHex.toLowerCase()
|
|
6241
|
+
) && parsed.delegateAddress.toLowerCase() === opts.identity.delegateAddress.toLowerCase() && derived.toLowerCase() === opts.identity.delegateAddress.toLowerCase() && normalizeUrl(parsed.gateway) === expectedGateway;
|
|
6242
|
+
if (!valid) {
|
|
6243
|
+
res.writeHead(403, {
|
|
6244
|
+
"content-type": "application/json",
|
|
6245
|
+
...cors,
|
|
6246
|
+
...SECURITY_HEADERS
|
|
6247
|
+
});
|
|
6248
|
+
res.end(JSON.stringify({ ok: false, error: "preflight mismatch" }));
|
|
6249
|
+
return;
|
|
6250
|
+
}
|
|
6251
|
+
preflightVerified = true;
|
|
6252
|
+
res.writeHead(200, {
|
|
6253
|
+
"content-type": "application/json",
|
|
6254
|
+
...cors,
|
|
6255
|
+
...SECURITY_HEADERS
|
|
6256
|
+
});
|
|
6257
|
+
res.end(
|
|
6258
|
+
JSON.stringify({
|
|
6259
|
+
ok: true,
|
|
6260
|
+
publicKey: opts.identity.publicKeyHex,
|
|
6261
|
+
label,
|
|
6262
|
+
gateway: opts.gatewayUrl
|
|
6263
|
+
})
|
|
6264
|
+
);
|
|
6265
|
+
return;
|
|
6266
|
+
}
|
|
6267
|
+
if (!preflightVerified) {
|
|
6268
|
+
res.writeHead(428, {
|
|
6269
|
+
"content-type": "text/html",
|
|
6270
|
+
...cors,
|
|
6271
|
+
...SECURITY_HEADERS
|
|
6272
|
+
});
|
|
6273
|
+
res.end(
|
|
6274
|
+
failHtml("Local MCP bridge verification is required before the callback.")
|
|
6275
|
+
);
|
|
6276
|
+
return;
|
|
6277
|
+
}
|
|
6278
|
+
if (!isCallback(parsed)) {
|
|
6279
|
+
res.writeHead(400, {
|
|
6280
|
+
"content-type": "text/html",
|
|
6281
|
+
...cors,
|
|
6282
|
+
...SECURITY_HEADERS
|
|
6283
|
+
});
|
|
6284
|
+
res.end(failHtml("Callback payload missing required fields."));
|
|
6285
|
+
return;
|
|
6286
|
+
}
|
|
6287
|
+
if (!stateEquals(parsed.state, connectState)) {
|
|
6288
|
+
res.writeHead(403, {
|
|
6289
|
+
"content-type": "text/html",
|
|
6290
|
+
...cors,
|
|
6291
|
+
...SECURITY_HEADERS
|
|
6292
|
+
});
|
|
6293
|
+
res.end(failHtml("Callback state mismatch \u2014 refusing to save."));
|
|
6294
|
+
return;
|
|
6295
|
+
}
|
|
6296
|
+
if (normalizeUrl(parsed.gatewayUrl) !== expectedGateway) {
|
|
6297
|
+
res.writeHead(403, {
|
|
6298
|
+
"content-type": "text/html",
|
|
6299
|
+
...cors,
|
|
6300
|
+
...SECURITY_HEADERS
|
|
6301
|
+
});
|
|
6302
|
+
res.end(failHtml("Callback gateway mismatch \u2014 refusing to save."));
|
|
6303
|
+
return;
|
|
6304
|
+
}
|
|
6305
|
+
preflightVerified = false;
|
|
6306
|
+
persistGatewayUrl(opts.gatewayUrl);
|
|
6307
|
+
completed = true;
|
|
6308
|
+
res.writeHead(200, {
|
|
6309
|
+
"content-type": "text/html",
|
|
6310
|
+
...cors,
|
|
6311
|
+
...SECURITY_HEADERS
|
|
6312
|
+
});
|
|
6313
|
+
res.end(SUCCESS_HTML);
|
|
6314
|
+
setTimeout(() => server.close(), 100);
|
|
6315
|
+
resolve();
|
|
6316
|
+
} catch (err) {
|
|
6317
|
+
error2 = err instanceof Error ? err : new Error(String(err));
|
|
6318
|
+
res.writeHead(500, {
|
|
6319
|
+
"content-type": "text/html",
|
|
6320
|
+
...cors,
|
|
6321
|
+
...SECURITY_HEADERS
|
|
6322
|
+
});
|
|
6323
|
+
res.end(failHtml("Internal error parsing callback."));
|
|
6324
|
+
setTimeout(() => server.close(), 100);
|
|
6325
|
+
resolve();
|
|
6326
|
+
}
|
|
6327
|
+
});
|
|
6328
|
+
});
|
|
6329
|
+
if (openBrowser) {
|
|
6330
|
+
open(connectUrl).catch(() => {
|
|
6331
|
+
});
|
|
6332
|
+
}
|
|
6333
|
+
void done.then(() => {
|
|
6334
|
+
if (error2) {
|
|
6335
|
+
}
|
|
5332
6336
|
});
|
|
6337
|
+
return {
|
|
6338
|
+
connectUrl,
|
|
6339
|
+
port,
|
|
6340
|
+
connectState,
|
|
6341
|
+
done,
|
|
6342
|
+
close: () => {
|
|
6343
|
+
server.close();
|
|
6344
|
+
}
|
|
6345
|
+
};
|
|
5333
6346
|
}
|
|
5334
6347
|
|
|
5335
6348
|
// src/spend-account-pay.ts
|
|
5336
|
-
import { Transaction as
|
|
6349
|
+
import { Transaction as Transaction6 } from "@mysten/sui/transactions";
|
|
5337
6350
|
import {
|
|
5338
|
-
fromBase64 as
|
|
5339
|
-
normalizeSuiAddress as
|
|
6351
|
+
fromBase64 as fromBase646,
|
|
6352
|
+
normalizeSuiAddress as normalizeSuiAddress11,
|
|
5340
6353
|
toBase64 as toBase644
|
|
5341
6354
|
} from "@mysten/sui/utils";
|
|
5342
6355
|
var SUI_CLOCK3 = "0x6";
|
|
@@ -5374,11 +6387,11 @@ async function buildSpendAccountPayKind2(input) {
|
|
|
5374
6387
|
if (!input.termsHash.length) throw new Error("termsHash must be non-empty");
|
|
5375
6388
|
if (!input.paymentIdHash.length) throw new Error("paymentIdHash must be non-empty");
|
|
5376
6389
|
const auth = defaultAuthorityForRail("delegate", input.packageId);
|
|
5377
|
-
const target = `${
|
|
6390
|
+
const target = `${normalizeSuiAddress11(auth.packageId)}::${auth.module}::${auth.payFunction}`;
|
|
5378
6391
|
const hashHex = input.targetHash.replace(/^0x/i, "").toLowerCase();
|
|
5379
6392
|
if (!/^[0-9a-f]{64}$/.test(hashHex)) throw new Error("targetHash must be 32-byte hex");
|
|
5380
6393
|
const hash = Uint8Array.from(Buffer.from(hashHex, "hex"));
|
|
5381
|
-
const tx = new
|
|
6394
|
+
const tx = new Transaction6();
|
|
5382
6395
|
tx.setSender(input.delegate);
|
|
5383
6396
|
tx.moveCall({
|
|
5384
6397
|
target,
|
|
@@ -5412,29 +6425,29 @@ async function buildSpendAccountPayKind2(input) {
|
|
|
5412
6425
|
function verifySpendAccountPayReconstructs(bytes, expected) {
|
|
5413
6426
|
let data;
|
|
5414
6427
|
try {
|
|
5415
|
-
data =
|
|
6428
|
+
data = Transaction6.from(fromBase646(bytes)).getData();
|
|
5416
6429
|
} catch {
|
|
5417
6430
|
throw new Error("sponsored transaction is not valid Sui transaction bytes");
|
|
5418
6431
|
}
|
|
5419
|
-
if (!data.sender ||
|
|
6432
|
+
if (!data.sender || normalizeSuiAddress11(data.sender) !== normalizeSuiAddress11(expected.delegate)) {
|
|
5420
6433
|
throw new Error("sponsored transaction sender does not match delegate");
|
|
5421
6434
|
}
|
|
5422
6435
|
if (!data.commands || data.commands.length !== 1) {
|
|
5423
6436
|
throw new Error("sponsored transaction must contain exactly one command");
|
|
5424
6437
|
}
|
|
5425
6438
|
const auth = defaultAuthorityForRail("delegate", expected.packageId);
|
|
5426
|
-
const wanted = `${
|
|
6439
|
+
const wanted = `${normalizeSuiAddress11(auth.packageId)}::${auth.module}::${auth.payFunction}`;
|
|
5427
6440
|
const cmd = data.commands[0];
|
|
5428
6441
|
if (cmd.$kind !== "MoveCall" || !cmd.MoveCall) {
|
|
5429
6442
|
throw new Error("sponsored transaction is not a MoveCall");
|
|
5430
6443
|
}
|
|
5431
|
-
const got = `${
|
|
6444
|
+
const got = `${normalizeSuiAddress11(String(cmd.MoveCall.package))}::${cmd.MoveCall.module}::${cmd.MoveCall.function}`;
|
|
5432
6445
|
if (got !== wanted) {
|
|
5433
6446
|
throw new Error(`sponsored transaction does not reconstruct ${wanted}`);
|
|
5434
6447
|
}
|
|
5435
6448
|
let expectedKind;
|
|
5436
6449
|
try {
|
|
5437
|
-
expectedKind =
|
|
6450
|
+
expectedKind = Transaction6.fromKind(fromBase646(expected.kindBytes)).getData();
|
|
5438
6451
|
} catch {
|
|
5439
6452
|
throw new Error("sponsored transaction kind baseline invalid");
|
|
5440
6453
|
}
|
|
@@ -5591,7 +6604,7 @@ async function prepareSpendAccountPayment(args) {
|
|
|
5591
6604
|
const signingRequest = {
|
|
5592
6605
|
unsignedKindBytes: kindBytes,
|
|
5593
6606
|
delegateAddress: resolved.delegateAddress,
|
|
5594
|
-
packageId:
|
|
6607
|
+
packageId: normalizeSuiAddress11(authority.packageId),
|
|
5595
6608
|
poolObjectId: resolved.poolObjectId,
|
|
5596
6609
|
grantObjectId: resolved.grantObjectId,
|
|
5597
6610
|
policyId: resolved.onChainPolicyId,
|
|
@@ -5672,28 +6685,256 @@ function mapErrorCode(code) {
|
|
|
5672
6685
|
}
|
|
5673
6686
|
}
|
|
5674
6687
|
|
|
5675
|
-
// src/
|
|
5676
|
-
|
|
5677
|
-
|
|
5678
|
-
|
|
5679
|
-
|
|
5680
|
-
|
|
5681
|
-
|
|
5682
|
-
|
|
5683
|
-
|
|
5684
|
-
|
|
5685
|
-
|
|
5686
|
-
|
|
5687
|
-
|
|
5688
|
-
|
|
5689
|
-
}
|
|
5690
|
-
|
|
6688
|
+
// src/discover-afford.ts
|
|
6689
|
+
import { normalizeStructTag as normalizeStructTag6 } from "@mysten/sui/utils";
|
|
6690
|
+
function sameCoinType2(rowAsset, grantCoin) {
|
|
6691
|
+
if (typeof rowAsset !== "string" || !grantCoin) return false;
|
|
6692
|
+
try {
|
|
6693
|
+
return normalizeStructTag6(rowAsset) === normalizeStructTag6(grantCoin);
|
|
6694
|
+
} catch {
|
|
6695
|
+
return false;
|
|
6696
|
+
}
|
|
6697
|
+
}
|
|
6698
|
+
function parseAtomic(value) {
|
|
6699
|
+
if (typeof value === "bigint") return value >= 0n ? value : null;
|
|
6700
|
+
if (typeof value === "number" && Number.isInteger(value) && value >= 0) {
|
|
6701
|
+
return BigInt(value);
|
|
6702
|
+
}
|
|
6703
|
+
if (typeof value !== "string") return null;
|
|
6704
|
+
const raw = value.trim();
|
|
6705
|
+
if (!/^[0-9]+$/.test(raw)) return null;
|
|
6706
|
+
return BigInt(raw);
|
|
6707
|
+
}
|
|
6708
|
+
function catalogPriceAtomic(row) {
|
|
6709
|
+
return parseAtomic(row.price);
|
|
6710
|
+
}
|
|
6711
|
+
function catalogDecimals(row) {
|
|
6712
|
+
if (typeof row.decimals !== "number" || !Number.isInteger(row.decimals)) return null;
|
|
6713
|
+
if (row.decimals < 0 || row.decimals > 18) return null;
|
|
6714
|
+
return row.decimals;
|
|
6715
|
+
}
|
|
6716
|
+
function formatAtomic(amount, decimals) {
|
|
6717
|
+
if (!Number.isInteger(decimals) || decimals < 0 || decimals > 18) {
|
|
6718
|
+
return amount.toString();
|
|
6719
|
+
}
|
|
6720
|
+
const negative = amount < 0n;
|
|
6721
|
+
const abs = negative ? -amount : amount;
|
|
6722
|
+
const scale = 10n ** BigInt(decimals);
|
|
6723
|
+
const whole = abs / scale;
|
|
6724
|
+
const frac = abs % scale;
|
|
6725
|
+
const sign = negative ? "-" : "";
|
|
6726
|
+
if (decimals === 0 || frac === 0n) return `${sign}${whole.toString()}`;
|
|
6727
|
+
const fracStr = frac.toString().padStart(decimals, "0").replace(/0+$/, "");
|
|
6728
|
+
return `${sign}${whole.toString()}.${fracStr}`;
|
|
6729
|
+
}
|
|
6730
|
+
function minAtomic(left, right) {
|
|
6731
|
+
if (left == null) return right;
|
|
6732
|
+
if (right == null) return left;
|
|
6733
|
+
return left <= right ? left : right;
|
|
6734
|
+
}
|
|
6735
|
+
function isAffordable(price, budget, opts = {}) {
|
|
6736
|
+
if (price == null) return void 0;
|
|
6737
|
+
if (!sameCoinType2(opts.asset, budget.coinType)) return void 0;
|
|
6738
|
+
const remaining = parseAtomic(budget.sessionRemaining);
|
|
6739
|
+
if (remaining == null) return void 0;
|
|
6740
|
+
if (price > remaining) return false;
|
|
6741
|
+
const cap = minAtomic(
|
|
6742
|
+
parseAtomic(budget.maxPerPayment),
|
|
6743
|
+
parseAtomic(opts.targetMaxPerPayment ?? null)
|
|
6744
|
+
);
|
|
6745
|
+
if (cap != null && price > cap) return false;
|
|
6746
|
+
return true;
|
|
6747
|
+
}
|
|
6748
|
+
function withCatalogAffordability(service, budget) {
|
|
6749
|
+
if (!budget) return service;
|
|
6750
|
+
const price = catalogPriceAtomic(service);
|
|
6751
|
+
const decimals = catalogDecimals(service);
|
|
6752
|
+
const targetMax = typeof service.maxPerPayment === "string" ? service.maxPerPayment : null;
|
|
6753
|
+
const sameAsset = sameCoinType2(service.asset, budget.coinType);
|
|
6754
|
+
const affordable = isAffordable(price, budget, {
|
|
6755
|
+
targetMaxPerPayment: targetMax,
|
|
6756
|
+
asset: service.asset
|
|
6757
|
+
});
|
|
6758
|
+
const remaining = sameAsset ? parseAtomic(budget.sessionRemaining) : null;
|
|
6759
|
+
const out = { ...service };
|
|
6760
|
+
if (price != null) out.price = price.toString();
|
|
6761
|
+
if (decimals != null) out.decimals = decimals;
|
|
6762
|
+
if (remaining != null) out.sessionRemaining = remaining.toString();
|
|
6763
|
+
if (affordable !== void 0) out.affordable = affordable;
|
|
6764
|
+
if (decimals != null && price != null) out.priceDisplay = formatAtomic(price, decimals);
|
|
6765
|
+
if (decimals != null && remaining != null) {
|
|
6766
|
+
out.remainingDisplay = formatAtomic(remaining, decimals);
|
|
6767
|
+
}
|
|
6768
|
+
return out;
|
|
6769
|
+
}
|
|
6770
|
+
|
|
6771
|
+
// src/discover-catalog.ts
|
|
6772
|
+
function asString(value) {
|
|
6773
|
+
return typeof value === "string" ? value.trim() : "";
|
|
6774
|
+
}
|
|
6775
|
+
function catalogRows(catalog) {
|
|
6776
|
+
if (Array.isArray(catalog)) return catalog.filter((row) => row && typeof row === "object");
|
|
6777
|
+
if (!catalog || typeof catalog !== "object") return [];
|
|
6778
|
+
const services = catalog.services;
|
|
6779
|
+
if (!Array.isArray(services)) return [];
|
|
6780
|
+
return services.filter((row) => row && typeof row === "object");
|
|
6781
|
+
}
|
|
6782
|
+
function authorizationEpochOf(row) {
|
|
6783
|
+
if (typeof row.authorizationEpoch === "number" && Number.isInteger(row.authorizationEpoch)) {
|
|
6784
|
+
return row.authorizationEpoch;
|
|
6785
|
+
}
|
|
6786
|
+
const state = row.state;
|
|
6787
|
+
if (state && typeof state === "object") {
|
|
6788
|
+
const revision = state.revision;
|
|
6789
|
+
if (typeof revision?.authorizationEpoch === "number" && Number.isInteger(revision.authorizationEpoch)) {
|
|
6790
|
+
return revision.authorizationEpoch;
|
|
6791
|
+
}
|
|
6792
|
+
}
|
|
6793
|
+
return 1;
|
|
6794
|
+
}
|
|
6795
|
+
function catalogRowTargetHash(row) {
|
|
6796
|
+
const id = asString(row.id);
|
|
6797
|
+
const path = asString(row.path);
|
|
6798
|
+
const method = asString(row.method) || "GET";
|
|
6799
|
+
const network = asString(row.network);
|
|
6800
|
+
const asset = asString(row.asset);
|
|
6801
|
+
const recipient = asString(row.payTo) || asString(row.recipient);
|
|
6802
|
+
const providerId = asString(row.ownerAddress) || asString(row.providerId);
|
|
6803
|
+
const endpointId = asString(row.endpointId) || id;
|
|
6804
|
+
if (!id || !path || !network || !asset || !recipient || !providerId) return null;
|
|
6805
|
+
try {
|
|
6806
|
+
return targetHashFromFingerprint(
|
|
6807
|
+
authorizationFingerprint({
|
|
6808
|
+
network,
|
|
6809
|
+
providerId,
|
|
6810
|
+
serviceId: id,
|
|
6811
|
+
endpointId,
|
|
6812
|
+
method,
|
|
6813
|
+
path,
|
|
6814
|
+
asset,
|
|
6815
|
+
recipient,
|
|
6816
|
+
authorizationEpoch: authorizationEpochOf(row)
|
|
6817
|
+
})
|
|
6818
|
+
).toLowerCase();
|
|
6819
|
+
} catch {
|
|
6820
|
+
return null;
|
|
6821
|
+
}
|
|
6822
|
+
}
|
|
6823
|
+
function catalogRowResourceUrl(row, gatewayUrl) {
|
|
6824
|
+
const listed = asString(row.resourceUrl);
|
|
6825
|
+
if (/^https?:\/\//i.test(listed)) return listed.replace(/\/+$/, "");
|
|
6826
|
+
const path = asString(row.path);
|
|
6827
|
+
const base = gatewayUrl.replace(/\/+$/, "");
|
|
6828
|
+
if (!path) return base;
|
|
6829
|
+
return `${base}${path.startsWith("/") ? path : `/${path}`}`;
|
|
6830
|
+
}
|
|
6831
|
+
function matchesQuery(row, query) {
|
|
6832
|
+
if (!query) return true;
|
|
6833
|
+
return JSON.stringify(row).toLowerCase().includes(query);
|
|
6834
|
+
}
|
|
6835
|
+
function joinDiscoverCatalog(opts) {
|
|
6836
|
+
const query = opts.query?.trim().toLowerCase() ?? "";
|
|
6837
|
+
const rows = catalogRows(opts.catalog);
|
|
6838
|
+
const scoped = Array.isArray(opts.policies);
|
|
6839
|
+
const byHash = /* @__PURE__ */ new Map();
|
|
6840
|
+
const allowedTargetHashes = [];
|
|
6841
|
+
if (scoped) {
|
|
6842
|
+
for (const policy of opts.policies) {
|
|
6843
|
+
for (const target of policy.targets) {
|
|
6844
|
+
const hash = asString(target.targetHash).toLowerCase();
|
|
6845
|
+
if (!hash) continue;
|
|
6846
|
+
allowedTargetHashes.push(hash);
|
|
6847
|
+
if (!byHash.has(hash)) byHash.set(hash, { ...policy, target });
|
|
6848
|
+
}
|
|
6849
|
+
}
|
|
6850
|
+
}
|
|
6851
|
+
const services = [];
|
|
6852
|
+
for (const row of rows) {
|
|
6853
|
+
const targetHash = catalogRowTargetHash(row);
|
|
6854
|
+
const url = catalogRowResourceUrl(row, opts.gatewayUrl);
|
|
6855
|
+
const bound = targetHash && scoped ? byHash.get(targetHash) : void 0;
|
|
6856
|
+
if (scoped && !bound) continue;
|
|
6857
|
+
const service = {
|
|
6858
|
+
...row,
|
|
6859
|
+
...targetHash ? { targetHash } : {},
|
|
6860
|
+
url
|
|
6861
|
+
};
|
|
6862
|
+
if (bound) {
|
|
6863
|
+
service.policyId = bound.policyId;
|
|
6864
|
+
service.onChainPolicyId = bound.onChainPolicyId;
|
|
6865
|
+
service.policyStatus = bound.status;
|
|
6866
|
+
if (bound.target.recipient) service.recipient = bound.target.recipient;
|
|
6867
|
+
if (bound.target.maxPerPayment) service.maxPerPayment = bound.target.maxPerPayment;
|
|
6868
|
+
}
|
|
6869
|
+
if (!matchesQuery(service, query)) continue;
|
|
6870
|
+
services.push(withCatalogAffordability(service, opts.budget));
|
|
6871
|
+
}
|
|
6872
|
+
return scoped ? { accessMode: "scoped", count: services.length, services, allowedTargetHashes } : { accessMode: "open", count: services.length, services };
|
|
6873
|
+
}
|
|
6874
|
+
|
|
6875
|
+
// src/tool-result.ts
|
|
6876
|
+
import { pathToFileURL } from "url";
|
|
6877
|
+
|
|
6878
|
+
// src/delivery-file.ts
|
|
6879
|
+
import { mkdirSync as mkdirSync2, writeFileSync, chmodSync as chmodSync2 } from "fs";
|
|
6880
|
+
import { dirname as dirname2, join as join3 } from "path";
|
|
6881
|
+
function mcpDeliveriesDir() {
|
|
6882
|
+
return join3(dirname2(mcpCredentialsPath()), "deliveries");
|
|
6883
|
+
}
|
|
6884
|
+
var EXT = {
|
|
6885
|
+
"image/png": ".png",
|
|
6886
|
+
"image/jpeg": ".jpg",
|
|
6887
|
+
"image/jpg": ".jpg",
|
|
6888
|
+
"image/webp": ".webp",
|
|
6889
|
+
"image/gif": ".gif",
|
|
6890
|
+
"image/svg+xml": ".svg",
|
|
6891
|
+
"audio/mpeg": ".mp3",
|
|
6892
|
+
"audio/wav": ".wav",
|
|
6893
|
+
"audio/ogg": ".ogg",
|
|
6894
|
+
"video/mp4": ".mp4",
|
|
6895
|
+
"application/pdf": ".pdf",
|
|
6896
|
+
"application/json": ".json",
|
|
6897
|
+
"text/plain": ".txt"
|
|
6898
|
+
};
|
|
6899
|
+
function extensionFor(contentType) {
|
|
6900
|
+
const type = contentType.split(";")[0]?.trim().toLowerCase() ?? "";
|
|
6901
|
+
return EXT[type] ?? ".bin";
|
|
6902
|
+
}
|
|
6903
|
+
function fileStem(digest, challengeId) {
|
|
6904
|
+
const raw = (digest ?? challengeId ?? `delivery-${Date.now()}`).trim();
|
|
6905
|
+
const safe = raw.replace(/[^A-Za-z0-9._-]+/g, "-").replace(/^-+|-+$/g, "");
|
|
6906
|
+
return (safe.slice(0, 80) || `delivery-${Date.now()}`).replace(/^\.+$/, "delivery");
|
|
6907
|
+
}
|
|
6908
|
+
function saveDeliveredBody(input) {
|
|
6909
|
+
if (input.bytes.byteLength === 0) return void 0;
|
|
6910
|
+
const dir = mcpDeliveriesDir();
|
|
6911
|
+
mkdirSync2(dir, { recursive: true, mode: 448 });
|
|
6912
|
+
try {
|
|
6913
|
+
chmodSync2(dir, 448);
|
|
6914
|
+
} catch {
|
|
6915
|
+
}
|
|
6916
|
+
const path = join3(
|
|
6917
|
+
dir,
|
|
6918
|
+
`${fileStem(input.digest, input.challengeId)}${extensionFor(input.contentType)}`
|
|
6919
|
+
);
|
|
6920
|
+
writeFileSync(path, input.bytes, { mode: 384 });
|
|
6921
|
+
try {
|
|
6922
|
+
chmodSync2(path, 384);
|
|
6923
|
+
} catch {
|
|
6924
|
+
}
|
|
6925
|
+
return path;
|
|
6926
|
+
}
|
|
6927
|
+
|
|
6928
|
+
// src/tool-result.ts
|
|
6929
|
+
var MAX_MCP_IMAGE_BYTES = 5 * 1024 * 1024;
|
|
5691
6930
|
function text(value) {
|
|
5692
6931
|
return {
|
|
5693
|
-
content: [
|
|
5694
|
-
|
|
5695
|
-
|
|
5696
|
-
|
|
6932
|
+
content: [
|
|
6933
|
+
{
|
|
6934
|
+
type: "text",
|
|
6935
|
+
text: typeof value === "string" ? value : JSON.stringify(value, null, 2)
|
|
6936
|
+
}
|
|
6937
|
+
]
|
|
5697
6938
|
};
|
|
5698
6939
|
}
|
|
5699
6940
|
function error(value) {
|
|
@@ -5705,7 +6946,12 @@ function closedError(code, detail) {
|
|
|
5705
6946
|
function closedPayError(code, detail) {
|
|
5706
6947
|
return error({ status: "failed", paid: false, code, detail });
|
|
5707
6948
|
}
|
|
5708
|
-
|
|
6949
|
+
function receiptChallengeId(receipt) {
|
|
6950
|
+
if (!receipt || typeof receipt !== "object") return void 0;
|
|
6951
|
+
const id = receipt.challengeId;
|
|
6952
|
+
return typeof id === "string" && id.length > 0 ? id : void 0;
|
|
6953
|
+
}
|
|
6954
|
+
var MCP_SAVE_ERROR = "delivery file was not written; do not call pay again";
|
|
5709
6955
|
function paidResult(result) {
|
|
5710
6956
|
if (!isBinaryBody(result.body)) return text(result);
|
|
5711
6957
|
const body = result.body;
|
|
@@ -5714,8 +6960,33 @@ function paidResult(result) {
|
|
|
5714
6960
|
contentType: body.contentType,
|
|
5715
6961
|
byteLength: body.byteLength
|
|
5716
6962
|
};
|
|
6963
|
+
let savedTo;
|
|
6964
|
+
try {
|
|
6965
|
+
savedTo = saveDeliveredBody({
|
|
6966
|
+
bytes: body.bytes,
|
|
6967
|
+
contentType: body.contentType,
|
|
6968
|
+
digest: result.txDigest,
|
|
6969
|
+
challengeId: receiptChallengeId(result.receipt)
|
|
6970
|
+
});
|
|
6971
|
+
} catch {
|
|
6972
|
+
savedTo = void 0;
|
|
6973
|
+
}
|
|
6974
|
+
const payload = savedTo ? {
|
|
6975
|
+
savedTo,
|
|
6976
|
+
savedToUrl: pathToFileURL(savedTo).href,
|
|
6977
|
+
...result,
|
|
6978
|
+
body: shape
|
|
6979
|
+
} : {
|
|
6980
|
+
savedTo: null,
|
|
6981
|
+
saveError: MCP_SAVE_ERROR,
|
|
6982
|
+
...result,
|
|
6983
|
+
body: shape
|
|
6984
|
+
};
|
|
5717
6985
|
const content = [
|
|
5718
|
-
{
|
|
6986
|
+
{
|
|
6987
|
+
type: "text",
|
|
6988
|
+
text: JSON.stringify(payload, null, 2)
|
|
6989
|
+
}
|
|
5719
6990
|
];
|
|
5720
6991
|
if (body.contentType.startsWith("image/") && body.bytes.byteLength > 0 && body.bytes.byteLength <= MAX_MCP_IMAGE_BYTES) {
|
|
5721
6992
|
content.push({
|
|
@@ -5726,6 +6997,227 @@ function paidResult(result) {
|
|
|
5726
6997
|
}
|
|
5727
6998
|
return { content };
|
|
5728
6999
|
}
|
|
7000
|
+
function settlePayResult(result) {
|
|
7001
|
+
if (result.paid === true) return paidResult(result);
|
|
7002
|
+
return error(result);
|
|
7003
|
+
}
|
|
7004
|
+
|
|
7005
|
+
// src/pay-file.ts
|
|
7006
|
+
import { isAbsolute, dirname as dirname3, sep } from "path";
|
|
7007
|
+
import {
|
|
7008
|
+
openSync as openSync2,
|
|
7009
|
+
closeSync as closeSync2,
|
|
7010
|
+
fstatSync,
|
|
7011
|
+
readFileSync as readFileSync2,
|
|
7012
|
+
realpathSync,
|
|
7013
|
+
constants
|
|
7014
|
+
} from "fs";
|
|
7015
|
+
var JSON_WRAP_SLACK_BYTES = 256;
|
|
7016
|
+
var MCP_MAX_FILE_FIELD_BYTES = Math.floor(
|
|
7017
|
+
(MAX_PAID_BODY_BYTES - JSON_WRAP_SLACK_BYTES) * 3 / 4
|
|
7018
|
+
);
|
|
7019
|
+
var FIELD = /^[A-Za-z][A-Za-z0-9_]{0,63}$/;
|
|
7020
|
+
var FORBIDDEN = /* @__PURE__ */ new Set(["constructor", "prototype"]);
|
|
7021
|
+
function fail2(code, detail) {
|
|
7022
|
+
return { code, detail };
|
|
7023
|
+
}
|
|
7024
|
+
function hasLocalFileArgs(input) {
|
|
7025
|
+
return input.file !== void 0 && input.file !== null || input.fileField !== void 0 && input.fileField !== null;
|
|
7026
|
+
}
|
|
7027
|
+
function parseField(value) {
|
|
7028
|
+
if (typeof value !== "string" || !FIELD.test(value) || FORBIDDEN.has(value)) {
|
|
7029
|
+
return fail2("INVALID_FILE_FIELD", "fileField must be a simple JSON key");
|
|
7030
|
+
}
|
|
7031
|
+
return value;
|
|
7032
|
+
}
|
|
7033
|
+
function parsePath(value) {
|
|
7034
|
+
if (typeof value !== "string" || value.length === 0 || value.includes("\0")) {
|
|
7035
|
+
return fail2("INVALID_FILE", "file must be a non-empty absolute path");
|
|
7036
|
+
}
|
|
7037
|
+
if (!isAbsolute(value) || value.includes("://")) {
|
|
7038
|
+
return fail2("INVALID_FILE", "file must be an absolute filesystem path, not a URL");
|
|
7039
|
+
}
|
|
7040
|
+
return value;
|
|
7041
|
+
}
|
|
7042
|
+
function parseMethod(value) {
|
|
7043
|
+
if (value === void 0 || value === null) return "GET";
|
|
7044
|
+
if (typeof value !== "string") {
|
|
7045
|
+
return fail2("MALFORMED_REQUEST", "method must be GET or POST");
|
|
7046
|
+
}
|
|
7047
|
+
const upper = value.trim().toUpperCase();
|
|
7048
|
+
if (upper === "GET" || upper === "POST") return upper;
|
|
7049
|
+
return fail2("MALFORMED_REQUEST", "method must be GET or POST");
|
|
7050
|
+
}
|
|
7051
|
+
function inside(root, candidate) {
|
|
7052
|
+
const prefix = root.endsWith(sep) ? root : root + sep;
|
|
7053
|
+
return candidate === root || candidate.startsWith(prefix);
|
|
7054
|
+
}
|
|
7055
|
+
function resolveContainedPath(raw) {
|
|
7056
|
+
const rootRaw = process.env.SUIPAY_MCP_FILE_ROOT;
|
|
7057
|
+
if (typeof rootRaw !== "string" || rootRaw.trim() === "") {
|
|
7058
|
+
return fail2(
|
|
7059
|
+
"FILE_ROOT_UNSET",
|
|
7060
|
+
"file attach requires SUIPAY_MCP_FILE_ROOT to be an absolute directory"
|
|
7061
|
+
);
|
|
7062
|
+
}
|
|
7063
|
+
if (!isAbsolute(rootRaw) || rootRaw.includes("://")) {
|
|
7064
|
+
return fail2(
|
|
7065
|
+
"FILE_ROOT_UNSET",
|
|
7066
|
+
"SUIPAY_MCP_FILE_ROOT must be an absolute filesystem path"
|
|
7067
|
+
);
|
|
7068
|
+
}
|
|
7069
|
+
let rootReal;
|
|
7070
|
+
let fileReal;
|
|
7071
|
+
try {
|
|
7072
|
+
rootReal = realpathSync(rootRaw);
|
|
7073
|
+
} catch {
|
|
7074
|
+
return fail2("FILE_ROOT_UNSET", "SUIPAY_MCP_FILE_ROOT is not a readable directory");
|
|
7075
|
+
}
|
|
7076
|
+
try {
|
|
7077
|
+
fileReal = realpathSync(raw);
|
|
7078
|
+
} catch (err) {
|
|
7079
|
+
const code = err.code;
|
|
7080
|
+
if (code === "ENOENT" || code === "ENOTDIR") {
|
|
7081
|
+
return fail2("FILE_NOT_FOUND", "file does not exist");
|
|
7082
|
+
}
|
|
7083
|
+
if (code === "EACCES") {
|
|
7084
|
+
return fail2("FILE_EACCES", "file is not readable");
|
|
7085
|
+
}
|
|
7086
|
+
return fail2(
|
|
7087
|
+
"INVALID_FILE",
|
|
7088
|
+
`file could not be read (${code ?? "unknown error"})`
|
|
7089
|
+
);
|
|
7090
|
+
}
|
|
7091
|
+
if (!inside(rootReal, fileReal)) {
|
|
7092
|
+
return fail2("FILE_OUTSIDE_ROOT", "file must be under SUIPAY_MCP_FILE_ROOT");
|
|
7093
|
+
}
|
|
7094
|
+
const credsDir2 = dirname3(mcpCredentialsPath());
|
|
7095
|
+
let credsReal = credsDir2;
|
|
7096
|
+
try {
|
|
7097
|
+
credsReal = realpathSync(credsDir2);
|
|
7098
|
+
} catch {
|
|
7099
|
+
}
|
|
7100
|
+
if (inside(credsReal, fileReal)) {
|
|
7101
|
+
return fail2("FILE_FORBIDDEN", "file must not be under ~/.suipay");
|
|
7102
|
+
}
|
|
7103
|
+
return fileReal;
|
|
7104
|
+
}
|
|
7105
|
+
function readRegularFile(path) {
|
|
7106
|
+
if (constants.O_NOFOLLOW === void 0 || constants.O_NONBLOCK === void 0) {
|
|
7107
|
+
return fail2("INVALID_FILE", "symlink-safe nonblocking open is unavailable on this platform");
|
|
7108
|
+
}
|
|
7109
|
+
let fd;
|
|
7110
|
+
try {
|
|
7111
|
+
const flags = constants.O_RDONLY | constants.O_NOFOLLOW | constants.O_NONBLOCK;
|
|
7112
|
+
fd = openSync2(path, flags);
|
|
7113
|
+
const st = fstatSync(fd);
|
|
7114
|
+
if (!st.isFile()) {
|
|
7115
|
+
return fail2("INVALID_FILE", "file must be a regular file");
|
|
7116
|
+
}
|
|
7117
|
+
if (st.size > MCP_MAX_FILE_FIELD_BYTES) {
|
|
7118
|
+
return fail2(
|
|
7119
|
+
"FILE_TOO_LARGE",
|
|
7120
|
+
`file is ${st.size} bytes; exceeds ${MCP_MAX_FILE_FIELD_BYTES} decoded bytes`
|
|
7121
|
+
);
|
|
7122
|
+
}
|
|
7123
|
+
const bytes = readFileSync2(fd);
|
|
7124
|
+
if (bytes.byteLength === 0) {
|
|
7125
|
+
return fail2("EMPTY_FILE", "file is empty");
|
|
7126
|
+
}
|
|
7127
|
+
if (bytes.byteLength > MCP_MAX_FILE_FIELD_BYTES) {
|
|
7128
|
+
return fail2(
|
|
7129
|
+
"FILE_TOO_LARGE",
|
|
7130
|
+
`file is ${bytes.byteLength} bytes; exceeds ${MCP_MAX_FILE_FIELD_BYTES} decoded bytes`
|
|
7131
|
+
);
|
|
7132
|
+
}
|
|
7133
|
+
return bytes;
|
|
7134
|
+
} catch (err) {
|
|
7135
|
+
const code = err.code;
|
|
7136
|
+
if (code === "ENOENT" || code === "ENOTDIR") {
|
|
7137
|
+
return fail2("FILE_NOT_FOUND", "file does not exist");
|
|
7138
|
+
}
|
|
7139
|
+
if (code === "ELOOP" || code === "EMLINK" || code === "ENXIO") {
|
|
7140
|
+
return fail2("INVALID_FILE", "file must be a regular file");
|
|
7141
|
+
}
|
|
7142
|
+
if (code === "EACCES") return fail2("FILE_EACCES", "file is not readable");
|
|
7143
|
+
return fail2(
|
|
7144
|
+
"INVALID_FILE",
|
|
7145
|
+
`file could not be read (${code ?? "unknown error"})`
|
|
7146
|
+
);
|
|
7147
|
+
} finally {
|
|
7148
|
+
if (fd !== void 0) {
|
|
7149
|
+
try {
|
|
7150
|
+
closeSync2(fd);
|
|
7151
|
+
} catch {
|
|
7152
|
+
}
|
|
7153
|
+
}
|
|
7154
|
+
}
|
|
7155
|
+
}
|
|
7156
|
+
function toCanonicalBase64(bytes) {
|
|
7157
|
+
return Buffer.from(bytes).toString("base64");
|
|
7158
|
+
}
|
|
7159
|
+
function jsonWithLocalFile(input) {
|
|
7160
|
+
const hasFile = input.file !== void 0 && input.file !== null;
|
|
7161
|
+
const hasField = input.fileField !== void 0 && input.fileField !== null;
|
|
7162
|
+
if (!hasFile && !hasField) return { json: input.json };
|
|
7163
|
+
if (hasFile !== hasField) {
|
|
7164
|
+
return {
|
|
7165
|
+
error: fail2(
|
|
7166
|
+
"INVALID_FILE_ARGS",
|
|
7167
|
+
"file and fileField are required together"
|
|
7168
|
+
)
|
|
7169
|
+
};
|
|
7170
|
+
}
|
|
7171
|
+
const method = parseMethod(input.method);
|
|
7172
|
+
if (typeof method !== "string") return { error: method };
|
|
7173
|
+
if (method !== "POST") {
|
|
7174
|
+
return { error: fail2("INVALID_FILE_ARGS", "file is only valid on POST") };
|
|
7175
|
+
}
|
|
7176
|
+
const field = parseField(input.fileField);
|
|
7177
|
+
if (typeof field !== "string") return { error: field };
|
|
7178
|
+
const path = parsePath(input.file);
|
|
7179
|
+
if (typeof path !== "string") return { error: path };
|
|
7180
|
+
let json;
|
|
7181
|
+
if (input.json === void 0) {
|
|
7182
|
+
json = {};
|
|
7183
|
+
} else if (typeof input.json === "object" && input.json !== null && !Array.isArray(input.json)) {
|
|
7184
|
+
json = { ...input.json };
|
|
7185
|
+
} else {
|
|
7186
|
+
return { error: fail2("MALFORMED_REQUEST", "POST requires json object") };
|
|
7187
|
+
}
|
|
7188
|
+
if (Object.prototype.hasOwnProperty.call(json, field)) {
|
|
7189
|
+
return {
|
|
7190
|
+
error: fail2(
|
|
7191
|
+
"FILE_FIELD_EXISTS",
|
|
7192
|
+
"json already has fileField; refuse to overwrite"
|
|
7193
|
+
)
|
|
7194
|
+
};
|
|
7195
|
+
}
|
|
7196
|
+
const contained = resolveContainedPath(path);
|
|
7197
|
+
if (typeof contained !== "string") return { error: contained };
|
|
7198
|
+
const bytes = readRegularFile(contained);
|
|
7199
|
+
if (!(bytes instanceof Uint8Array)) return { error: bytes };
|
|
7200
|
+
json[field] = toCanonicalBase64(bytes);
|
|
7201
|
+
return { json };
|
|
7202
|
+
}
|
|
7203
|
+
|
|
7204
|
+
// src/tools.ts
|
|
7205
|
+
function payerTrace(deps) {
|
|
7206
|
+
const hooks = deps.trace;
|
|
7207
|
+
const buyerAccountId = deps.auth?.buyerAccountId;
|
|
7208
|
+
if (!hooks || !buyerAccountId) return null;
|
|
7209
|
+
return {
|
|
7210
|
+
sink: hooks.sink,
|
|
7211
|
+
traceId: hooks.context.traceId,
|
|
7212
|
+
requestId: hooks.context.requestId,
|
|
7213
|
+
buyerAccountId,
|
|
7214
|
+
connectionId: hooks.runtime.connectionId,
|
|
7215
|
+
...hooks.runtime.secret ? { secret: hooks.runtime.secret } : {},
|
|
7216
|
+
...hooks.runtime.now ? { now: hooks.runtime.now } : {}
|
|
7217
|
+
};
|
|
7218
|
+
}
|
|
7219
|
+
var AUTH_REQUIRED = "No SuiPay credentials - call suipay_login first. It returns a console URL; approve the spend-account grant in the browser. The next tool call picks up ~/.suipay/credentials.json automatically.";
|
|
7220
|
+
var LOGIN_BG_TIMEOUT_MS = 5 * 6e4;
|
|
5729
7221
|
function preparedResult(result) {
|
|
5730
7222
|
return text({
|
|
5731
7223
|
status: "prepared",
|
|
@@ -5745,7 +7237,7 @@ function normalizePayIntent(args) {
|
|
|
5745
7237
|
if (!/^0x[0-9a-fA-F]{1,64}$/.test(raw)) {
|
|
5746
7238
|
throw new Error("recipient must be a 0x-prefixed Sui address");
|
|
5747
7239
|
}
|
|
5748
|
-
const normalized =
|
|
7240
|
+
const normalized = normalizeSuiAddress12(raw);
|
|
5749
7241
|
if (!isValidSuiAddress(normalized) || /^0x0+$/.test(normalized)) {
|
|
5750
7242
|
throw new Error("recipient must be a non-zero Sui address");
|
|
5751
7243
|
}
|
|
@@ -5764,7 +7256,7 @@ function intentViolation(intent, prepared) {
|
|
|
5764
7256
|
if (intent.recipient) {
|
|
5765
7257
|
let preparedRecipient;
|
|
5766
7258
|
try {
|
|
5767
|
-
preparedRecipient =
|
|
7259
|
+
preparedRecipient = normalizeSuiAddress12(prepared.recipient);
|
|
5768
7260
|
} catch {
|
|
5769
7261
|
return "prepared payment does not name a valid recipient";
|
|
5770
7262
|
}
|
|
@@ -5787,18 +7279,34 @@ function intentViolation(intent, prepared) {
|
|
|
5787
7279
|
}
|
|
5788
7280
|
async function pay(args, cfg, deps = {}) {
|
|
5789
7281
|
if (!args.url) return error("url is required");
|
|
7282
|
+
if (hasLocalFileArgs(args) && deps.transport === "http") {
|
|
7283
|
+
return closedPayError(
|
|
7284
|
+
"FILE_STDIO_ONLY",
|
|
7285
|
+
"file is stdio only; hosted /mcp does not read the buyer disk"
|
|
7286
|
+
);
|
|
7287
|
+
}
|
|
5790
7288
|
let intent;
|
|
5791
7289
|
try {
|
|
5792
7290
|
intent = normalizePayIntent(args);
|
|
5793
7291
|
} catch {
|
|
5794
7292
|
return closedPayError("INVALID_INTENT", "payment intent is invalid");
|
|
5795
7293
|
}
|
|
7294
|
+
let attached;
|
|
7295
|
+
try {
|
|
7296
|
+
attached = jsonWithLocalFile(args);
|
|
7297
|
+
} catch {
|
|
7298
|
+
return closedPayError("INVALID_FILE", "file could not be read");
|
|
7299
|
+
}
|
|
7300
|
+
if ("error" in attached) {
|
|
7301
|
+
return closedPayError(attached.error.code, attached.error.detail);
|
|
7302
|
+
}
|
|
7303
|
+
const paidArgs = { ...args, json: attached.json };
|
|
5796
7304
|
let request;
|
|
5797
7305
|
try {
|
|
5798
|
-
request = normalizeMcpPaidHttpRequest(
|
|
7306
|
+
request = normalizeMcpPaidHttpRequest(paidArgs);
|
|
5799
7307
|
} catch (err) {
|
|
5800
7308
|
const raw = err instanceof Error ? err.message : "";
|
|
5801
|
-
const detail = /method/i.test(raw) ? "method must be GET or POST" : /json/i.test(raw) ? "POST requires json" : "request could not be normalized";
|
|
7309
|
+
const detail = /too large/i.test(raw) ? `paid request body exceeds ${MAX_PAID_BODY_BYTES} bytes` : /method/i.test(raw) ? "method must be GET or POST" : /json/i.test(raw) ? "POST requires json" : "request could not be normalized";
|
|
5802
7310
|
return closedPayError("MALFORMED_REQUEST", detail);
|
|
5803
7311
|
}
|
|
5804
7312
|
if (deps.auth?.spendAccountPay) {
|
|
@@ -5870,10 +7378,10 @@ async function pay(args, cfg, deps = {}) {
|
|
|
5870
7378
|
...deps.fetchImpl && !deps.agentHeld.payerDeps.fetchImpl ? { fetchImpl: deps.fetchImpl } : {}
|
|
5871
7379
|
}
|
|
5872
7380
|
});
|
|
5873
|
-
return
|
|
7381
|
+
return settlePayResult(settled);
|
|
5874
7382
|
}
|
|
5875
7383
|
if (result.status === "prepared") return preparedResult(result);
|
|
5876
|
-
return
|
|
7384
|
+
return settlePayResult(result);
|
|
5877
7385
|
}
|
|
5878
7386
|
if (deps.auth) {
|
|
5879
7387
|
return error(
|
|
@@ -5888,122 +7396,259 @@ async function pay(args, cfg, deps = {}) {
|
|
|
5888
7396
|
}
|
|
5889
7397
|
const local = loadLocalSigner();
|
|
5890
7398
|
if (!local) return error(AUTH_REQUIRED);
|
|
5891
|
-
return payStdioWithLocalKey({ request, intent, cfg, deps
|
|
7399
|
+
return payStdioWithLocalKey({ request, intent, cfg, deps });
|
|
5892
7400
|
}
|
|
5893
|
-
function
|
|
5894
|
-
return
|
|
5895
|
-
grantId: binding.grantObjectId,
|
|
5896
|
-
status: "active",
|
|
5897
|
-
grantObjectId: binding.grantObjectId,
|
|
5898
|
-
poolObjectId: binding.poolObjectId,
|
|
5899
|
-
coinType: binding.coinType,
|
|
5900
|
-
delegateAddress,
|
|
5901
|
-
policies: binding.policies,
|
|
5902
|
-
accessMode: binding.accessMode,
|
|
5903
|
-
...binding.maxPerPayment != null ? { maxPerPayment: binding.maxPerPayment } : {},
|
|
5904
|
-
...binding.sessionCap != null ? { sessionCap: binding.sessionCap } : {}
|
|
5905
|
-
};
|
|
7401
|
+
function stdioGatewayUrl(cfg) {
|
|
7402
|
+
return loadLocalSigningKeyRecord()?.gatewayUrl ?? cfg.gatewayUrl;
|
|
5906
7403
|
}
|
|
5907
|
-
|
|
5908
|
-
|
|
5909
|
-
|
|
5910
|
-
|
|
5911
|
-
|
|
5912
|
-
|
|
5913
|
-
|
|
5914
|
-
|
|
5915
|
-
|
|
5916
|
-
|
|
5917
|
-
|
|
5918
|
-
|
|
7404
|
+
function isExpiredBootstrapGrant(expiresAtMs, nowMs) {
|
|
7405
|
+
if (expiresAtMs === void 0 || expiresAtMs === null || expiresAtMs === "" || expiresAtMs === "0" || expiresAtMs === 0) {
|
|
7406
|
+
return false;
|
|
7407
|
+
}
|
|
7408
|
+
const exp = typeof expiresAtMs === "number" ? expiresAtMs : typeof expiresAtMs === "string" && /^\d+$/.test(expiresAtMs) ? Number(expiresAtMs) : NaN;
|
|
7409
|
+
if (!Number.isFinite(exp) || exp <= 0) return false;
|
|
7410
|
+
return exp <= nowMs;
|
|
7411
|
+
}
|
|
7412
|
+
function grantObjectIdFromBootstrap(grants, nowMs) {
|
|
7413
|
+
let named = 0;
|
|
7414
|
+
const live = [];
|
|
7415
|
+
for (const grant of grants) {
|
|
7416
|
+
if (!grant || typeof grant !== "object") continue;
|
|
7417
|
+
const g = grant;
|
|
7418
|
+
const id = g.grantObjectId;
|
|
7419
|
+
if (typeof id !== "string" || !id.trim()) continue;
|
|
7420
|
+
named += 1;
|
|
7421
|
+
if (isExpiredBootstrapGrant(g.expiresAtMs, nowMs)) continue;
|
|
7422
|
+
live.push(id.trim());
|
|
7423
|
+
}
|
|
7424
|
+
if (live.length === 1) return { kind: "live", grantObjectId: live[0] };
|
|
7425
|
+
if (live.length > 1) return { kind: "ambiguous" };
|
|
7426
|
+
if (named > 0) return { kind: "absent" };
|
|
7427
|
+
if (grants.length > 0) return { kind: "unknown", detail: "delegate grants list was not usable" };
|
|
7428
|
+
return { kind: "absent" };
|
|
7429
|
+
}
|
|
7430
|
+
async function checkLiveDelegateGrant(args) {
|
|
7431
|
+
const deployment = await fetchGatewayDeployment({
|
|
7432
|
+
gatewayUrl: args.gatewayUrl,
|
|
7433
|
+
...args.fetchImpl ? { fetchImpl: args.fetchImpl } : {}
|
|
5919
7434
|
});
|
|
5920
|
-
if (!
|
|
5921
|
-
return
|
|
7435
|
+
if (!deployment.ok) {
|
|
7436
|
+
return {
|
|
7437
|
+
kind: "unknown",
|
|
7438
|
+
detail: "gateway deployment identity could not be verified"
|
|
7439
|
+
};
|
|
5922
7440
|
}
|
|
5923
|
-
const
|
|
5924
|
-
|
|
5925
|
-
|
|
5926
|
-
|
|
5927
|
-
|
|
5928
|
-
|
|
5929
|
-
snapshot,
|
|
5930
|
-
packageId,
|
|
5931
|
-
...resolved.value.session.assertLive ? { assertLive: resolved.value.session.assertLive } : {}
|
|
7441
|
+
const listed = await bootstrapPayerGrants({
|
|
7442
|
+
gatewayUrl: args.gatewayUrl,
|
|
7443
|
+
context: {
|
|
7444
|
+
network: deployment.value.network,
|
|
7445
|
+
packageId: deployment.value.packageId,
|
|
7446
|
+
delegateAddress: args.delegateAddress
|
|
5932
7447
|
},
|
|
5933
|
-
|
|
5934
|
-
|
|
5935
|
-
...deps.fetchImpl ? { fetchImpl: deps.fetchImpl } : {},
|
|
5936
|
-
...deps.buildPayKind ? { buildPayKind: deps.buildPayKind } : {}
|
|
5937
|
-
}
|
|
7448
|
+
signPersonalMessage: args.signPersonalMessage,
|
|
7449
|
+
...args.fetchImpl ? { fetchImpl: args.fetchImpl } : {}
|
|
5938
7450
|
});
|
|
5939
|
-
if (
|
|
5940
|
-
|
|
5941
|
-
|
|
5942
|
-
|
|
5943
|
-
|
|
5944
|
-
|
|
5945
|
-
|
|
5946
|
-
|
|
5947
|
-
|
|
5948
|
-
|
|
5949
|
-
|
|
5950
|
-
|
|
5951
|
-
|
|
5952
|
-
|
|
5953
|
-
|
|
5954
|
-
|
|
5955
|
-
|
|
5956
|
-
|
|
5957
|
-
|
|
5958
|
-
|
|
5959
|
-
|
|
5960
|
-
|
|
7451
|
+
if (!listed.ok) {
|
|
7452
|
+
return {
|
|
7453
|
+
kind: "unknown",
|
|
7454
|
+
detail: `delegate grants read ${listed.error} (HTTP ${listed.status})`
|
|
7455
|
+
};
|
|
7456
|
+
}
|
|
7457
|
+
if (!Array.isArray(listed.data.grants) || listed.data.grants.length === 0) {
|
|
7458
|
+
return { kind: "absent" };
|
|
7459
|
+
}
|
|
7460
|
+
return grantObjectIdFromBootstrap(listed.data.grants, Date.now());
|
|
7461
|
+
}
|
|
7462
|
+
async function payStdioWithLocalKey(args) {
|
|
7463
|
+
const { request, intent, cfg, deps } = args;
|
|
7464
|
+
const built = await stdioPayerAndReader(cfg, deps);
|
|
7465
|
+
if (!built.ok) return built.result;
|
|
7466
|
+
const payer = built.payer;
|
|
7467
|
+
const grantCeiling = payer.grant.maxPerPayment;
|
|
7468
|
+
const maxAmount = intent.maxAmount !== void 0 ? intent.maxAmount.toString() : grantCeiling != null && grantCeiling !== "" ? grantCeiling : null;
|
|
7469
|
+
if (maxAmount === null) {
|
|
7470
|
+
return error({
|
|
7471
|
+
status: "failed",
|
|
7472
|
+
paid: false,
|
|
7473
|
+
code: "POLICY_REJECTED",
|
|
7474
|
+
detail: "stdio pay requires maxAmount or a grant maxPerPayment; refusing to default the ceiling to the offer"
|
|
7475
|
+
});
|
|
7476
|
+
}
|
|
7477
|
+
const settled = await payer.pay(request.url, {
|
|
7478
|
+
method: request.method,
|
|
7479
|
+
...request.method === "POST" ? { body: request.body } : {},
|
|
7480
|
+
...request.contentType ? { contentType: request.contentType } : {},
|
|
7481
|
+
intent: {
|
|
5961
7482
|
maxAmount,
|
|
5962
|
-
asset:
|
|
7483
|
+
asset: payer.grant.coinType,
|
|
5963
7484
|
...intent.recipient ? { recipient: intent.recipient } : {}
|
|
5964
|
-
}
|
|
5965
|
-
|
|
5966
|
-
|
|
5967
|
-
|
|
5968
|
-
|
|
5969
|
-
|
|
5970
|
-
|
|
5971
|
-
|
|
5972
|
-
|
|
5973
|
-
|
|
5974
|
-
|
|
5975
|
-
|
|
5976
|
-
|
|
5977
|
-
|
|
5978
|
-
|
|
5979
|
-
|
|
5980
|
-
deps: {
|
|
5981
|
-
...agentHeld.payerDeps,
|
|
5982
|
-
...deps.fetchImpl && !agentHeld.payerDeps.fetchImpl ? { fetchImpl: deps.fetchImpl } : {}
|
|
5983
|
-
},
|
|
5984
|
-
intent: spendIntent
|
|
7485
|
+
}
|
|
7486
|
+
});
|
|
7487
|
+
return settlePayResult(settled);
|
|
7488
|
+
}
|
|
7489
|
+
async function stdioPayerAndReader(cfg, deps) {
|
|
7490
|
+
const keypair = loadLocalKeypair();
|
|
7491
|
+
if (!keypair) return { ok: false, result: error(AUTH_REQUIRED) };
|
|
7492
|
+
const gatewayUrl = stdioGatewayUrl(cfg);
|
|
7493
|
+
const local = loadLocalSigner();
|
|
7494
|
+
let select;
|
|
7495
|
+
const shouldSelect = Boolean(local) && (deps.fetchImpl !== void 0 || deps.createPayer === void 0);
|
|
7496
|
+
if (local && shouldSelect) {
|
|
7497
|
+
const check = await checkLiveDelegateGrant({
|
|
7498
|
+
gatewayUrl,
|
|
7499
|
+
delegateAddress: local.address,
|
|
7500
|
+
signPersonalMessage: local.signPersonalMessage,
|
|
7501
|
+
...deps.fetchImpl ? { fetchImpl: deps.fetchImpl } : {}
|
|
5985
7502
|
});
|
|
5986
|
-
|
|
7503
|
+
if (check.kind === "ambiguous") {
|
|
7504
|
+
return {
|
|
7505
|
+
ok: false,
|
|
7506
|
+
result: closedError(
|
|
7507
|
+
"AMBIGUOUS_GRANT",
|
|
7508
|
+
"multiple live grants name this key; pass a selector (grantObjectId / asset / coinType)"
|
|
7509
|
+
)
|
|
7510
|
+
};
|
|
7511
|
+
}
|
|
7512
|
+
if (check.kind === "live") select = { grantObjectId: check.grantObjectId };
|
|
7513
|
+
}
|
|
7514
|
+
const built = await (deps.createPayer ?? createPayer)({
|
|
7515
|
+
gatewayUrl,
|
|
7516
|
+
keypair,
|
|
7517
|
+
...select ? { select } : {},
|
|
7518
|
+
...deps.fetchImpl ? { fetchImpl: deps.fetchImpl } : {},
|
|
7519
|
+
...cfg.rpcUrl ? { fullnodeUrl: cfg.rpcUrl } : {}
|
|
7520
|
+
});
|
|
7521
|
+
if (!built.ok) {
|
|
7522
|
+
return {
|
|
7523
|
+
ok: false,
|
|
7524
|
+
result: closedError(built.error.code, built.error.detail)
|
|
7525
|
+
};
|
|
5987
7526
|
}
|
|
5988
|
-
return
|
|
7527
|
+
return { ok: true, payer: built.value };
|
|
7528
|
+
}
|
|
7529
|
+
function stdioReaderForPayer(cfg, deps, payer) {
|
|
7530
|
+
return (deps.createReader ?? createReader)({
|
|
7531
|
+
gatewayUrl: stdioGatewayUrl(cfg),
|
|
7532
|
+
context: {
|
|
7533
|
+
network: payer.deployment.network,
|
|
7534
|
+
packageId: payer.deployment.packageId,
|
|
7535
|
+
grantObjectId: payer.grant.grantObjectId,
|
|
7536
|
+
delegateAddress: payer.address
|
|
7537
|
+
},
|
|
7538
|
+
signPersonalMessage: async (message) => {
|
|
7539
|
+
const { signature } = await loadLocalKeypair().signPersonalMessage(message);
|
|
7540
|
+
return signature;
|
|
7541
|
+
},
|
|
7542
|
+
...deps.fetchImpl ? { fetchImpl: deps.fetchImpl } : {}
|
|
7543
|
+
});
|
|
7544
|
+
}
|
|
7545
|
+
function budgetFromBalance(balance, grant) {
|
|
7546
|
+
if (!balance.ok || !balance.data) return void 0;
|
|
7547
|
+
return {
|
|
7548
|
+
sessionRemaining: balance.data.sessionRemaining ?? null,
|
|
7549
|
+
maxPerPayment: balance.data.maxPerPayment ?? grant.maxPerPayment ?? null,
|
|
7550
|
+
coinType: balance.data.coinType ?? grant.coinType ?? null
|
|
7551
|
+
};
|
|
7552
|
+
}
|
|
7553
|
+
async function fetchPublicCatalog(gatewayUrl, fetchImpl) {
|
|
7554
|
+
const catalogUrl = new URL("/v1/services", gatewayUrl);
|
|
7555
|
+
const response = await (fetchImpl ?? fetch)(catalogUrl);
|
|
7556
|
+
if (!response.ok) return { ok: false, result: error(`gateway unavailable: HTTP ${response.status}`) };
|
|
7557
|
+
return { ok: true, catalog: await response.json() };
|
|
7558
|
+
}
|
|
7559
|
+
async function publicCatalog(gatewayUrl, query, fetchImpl) {
|
|
7560
|
+
const loaded = await fetchPublicCatalog(gatewayUrl, fetchImpl);
|
|
7561
|
+
if (!loaded.ok) return loaded.result;
|
|
7562
|
+
return text(
|
|
7563
|
+
joinDiscoverCatalog({
|
|
7564
|
+
catalog: loaded.catalog,
|
|
7565
|
+
gatewayUrl,
|
|
7566
|
+
query
|
|
7567
|
+
})
|
|
7568
|
+
);
|
|
5989
7569
|
}
|
|
5990
7570
|
async function discover(args, cfg, deps = {}) {
|
|
5991
|
-
|
|
5992
|
-
|
|
5993
|
-
const response = await (deps.fetchImpl ?? fetch)(url);
|
|
5994
|
-
if (!response.ok) return error(`gateway unavailable: HTTP ${response.status}`);
|
|
5995
|
-
return text(await response.json());
|
|
5996
|
-
}
|
|
5997
|
-
async function accessContext(deps = {}) {
|
|
5998
|
-
if (!deps.auth) return error(AUTH_REQUIRED);
|
|
5999
|
-
if (!deps.auth.access) {
|
|
6000
|
-
return error(
|
|
6001
|
-
"SuiPay access context unavailable. Reconnect this MCP session or inspect the connection in buyer console."
|
|
6002
|
-
);
|
|
7571
|
+
if (deps.auth || !loadLocalKeypair()) {
|
|
7572
|
+
return publicCatalog(cfg.gatewayUrl, args.query, deps.fetchImpl);
|
|
6003
7573
|
}
|
|
6004
|
-
|
|
7574
|
+
const local = loadLocalSigner();
|
|
7575
|
+
if (local) {
|
|
7576
|
+
const check = await checkLiveDelegateGrant({
|
|
7577
|
+
gatewayUrl: stdioGatewayUrl(cfg),
|
|
7578
|
+
delegateAddress: local.address,
|
|
7579
|
+
signPersonalMessage: local.signPersonalMessage,
|
|
7580
|
+
...deps.fetchImpl ? { fetchImpl: deps.fetchImpl } : {}
|
|
7581
|
+
});
|
|
7582
|
+
if (check.kind !== "live") {
|
|
7583
|
+
return publicCatalog(stdioGatewayUrl(cfg), args.query, deps.fetchImpl);
|
|
7584
|
+
}
|
|
7585
|
+
}
|
|
7586
|
+
const resolved = await stdioPayerAndReader(cfg, deps);
|
|
7587
|
+
if (!resolved.ok) return resolved.result;
|
|
7588
|
+
const gatewayUrl = stdioGatewayUrl(cfg);
|
|
7589
|
+
const loaded = await fetchPublicCatalog(gatewayUrl, deps.fetchImpl);
|
|
7590
|
+
if (!loaded.ok) return loaded.result;
|
|
7591
|
+
const scoped = resolved.payer.grant.accessMode === "scoped";
|
|
7592
|
+
const reader = stdioReaderForPayer(cfg, deps, resolved.payer);
|
|
7593
|
+
const [policies, balance] = await Promise.all([
|
|
7594
|
+
scoped ? reader.policies() : Promise.resolve({ ok: true, data: { policies: void 0 } }),
|
|
7595
|
+
reader.balance()
|
|
7596
|
+
]);
|
|
7597
|
+
if (scoped && !policies.ok) {
|
|
7598
|
+
return closedError("POLICIES_UNAVAILABLE", "grant policies could not be loaded");
|
|
7599
|
+
}
|
|
7600
|
+
return text(
|
|
7601
|
+
joinDiscoverCatalog({
|
|
7602
|
+
catalog: loaded.catalog,
|
|
7603
|
+
gatewayUrl,
|
|
7604
|
+
query: args.query,
|
|
7605
|
+
...scoped && policies.ok ? { policies: policies.data.policies } : {},
|
|
7606
|
+
budget: budgetFromBalance(balance, resolved.payer.grant)
|
|
7607
|
+
})
|
|
7608
|
+
);
|
|
7609
|
+
}
|
|
7610
|
+
async function accessContext(deps = {}, cfg) {
|
|
7611
|
+
if (deps.auth) {
|
|
7612
|
+
if (!deps.auth.access) {
|
|
7613
|
+
return error(
|
|
7614
|
+
"SuiPay access context unavailable. Reconnect this MCP session or inspect the connection in buyer console."
|
|
7615
|
+
);
|
|
7616
|
+
}
|
|
7617
|
+
return text(deps.auth.access);
|
|
7618
|
+
}
|
|
7619
|
+
if (!cfg) return error(AUTH_REQUIRED);
|
|
7620
|
+
const resolved = await stdioPayerAndReader(cfg, deps);
|
|
7621
|
+
if (!resolved.ok) return resolved.result;
|
|
7622
|
+
const gatewayUrl = stdioGatewayUrl(cfg);
|
|
7623
|
+
const reader = stdioReaderForPayer(cfg, deps, resolved.payer);
|
|
7624
|
+
const scoped = resolved.payer.grant.accessMode === "scoped";
|
|
7625
|
+
const [policies, balance, loaded] = await Promise.all([
|
|
7626
|
+
reader.policies(),
|
|
7627
|
+
reader.balance(),
|
|
7628
|
+
fetchPublicCatalog(gatewayUrl, deps.fetchImpl)
|
|
7629
|
+
]);
|
|
7630
|
+
const budget = budgetFromBalance(balance, resolved.payer.grant);
|
|
7631
|
+
const joined = loaded.ok && (!scoped || policies.ok) ? joinDiscoverCatalog({
|
|
7632
|
+
catalog: loaded.catalog,
|
|
7633
|
+
gatewayUrl,
|
|
7634
|
+
...scoped && policies.ok ? { policies: policies.data.policies } : {},
|
|
7635
|
+
budget
|
|
7636
|
+
}) : null;
|
|
7637
|
+
return text({
|
|
7638
|
+
accessMode: resolved.payer.grant.accessMode,
|
|
7639
|
+
grant: {
|
|
7640
|
+
id: resolved.payer.grant.grantObjectId,
|
|
7641
|
+
poolObjectId: resolved.payer.grant.poolObjectId,
|
|
7642
|
+
coinType: resolved.payer.grant.coinType,
|
|
7643
|
+
maxPerPayment: resolved.payer.grant.maxPerPayment ?? null,
|
|
7644
|
+
sessionCap: resolved.payer.grant.sessionCap ?? null
|
|
7645
|
+
},
|
|
7646
|
+
policies: policies.ok ? policies.data : { error: policies.error },
|
|
7647
|
+
balance: balance.ok ? balance.data : { error: balance.error },
|
|
7648
|
+
...joined ? { services: joined.services, count: joined.count } : {}
|
|
7649
|
+
});
|
|
6005
7650
|
}
|
|
6006
|
-
async function receipts(args,
|
|
7651
|
+
async function receipts(args, cfg, deps = {}) {
|
|
6007
7652
|
if (deps.auth) {
|
|
6008
7653
|
if (!deps.auth.listReceipts) {
|
|
6009
7654
|
return error(
|
|
@@ -6017,28 +7662,144 @@ async function receipts(args, _cfg, deps = {}) {
|
|
|
6017
7662
|
return closedError("RECEIPTS_UNAVAILABLE", "receipts could not be loaded");
|
|
6018
7663
|
}
|
|
6019
7664
|
}
|
|
6020
|
-
if (
|
|
6021
|
-
|
|
6022
|
-
|
|
6023
|
-
|
|
7665
|
+
if (profile(deps)) {
|
|
7666
|
+
return error(
|
|
7667
|
+
"receipts is available after suipay_login (version-2 key) or on remote OAuth (/mcp) sessions; the local V1 allowance profile has no settlement feed access"
|
|
7668
|
+
);
|
|
7669
|
+
}
|
|
7670
|
+
const resolved = await stdioPayerAndReader(cfg, deps);
|
|
7671
|
+
if (!resolved.ok) return resolved.result;
|
|
7672
|
+
const reader = (deps.createReader ?? createReader)({
|
|
7673
|
+
gatewayUrl: stdioGatewayUrl(cfg),
|
|
7674
|
+
context: {
|
|
7675
|
+
network: resolved.payer.deployment.network,
|
|
7676
|
+
packageId: resolved.payer.deployment.packageId,
|
|
7677
|
+
grantObjectId: resolved.payer.grant.grantObjectId,
|
|
7678
|
+
delegateAddress: resolved.payer.address
|
|
7679
|
+
},
|
|
7680
|
+
signPersonalMessage: async (message) => {
|
|
7681
|
+
const { signature } = await loadLocalKeypair().signPersonalMessage(message);
|
|
7682
|
+
return signature;
|
|
7683
|
+
},
|
|
7684
|
+
...deps.fetchImpl ? { fetchImpl: deps.fetchImpl } : {}
|
|
7685
|
+
});
|
|
7686
|
+
const listed = await reader.receipts();
|
|
7687
|
+
if (!listed.ok) {
|
|
7688
|
+
return closedError("RECEIPTS_UNAVAILABLE", "receipts could not be loaded");
|
|
7689
|
+
}
|
|
7690
|
+
const receiptsOut = listed.data.receipts;
|
|
7691
|
+
const filtered = args.challengeId && Array.isArray(receiptsOut) ? receiptsOut.filter((row) => {
|
|
7692
|
+
if (!row || typeof row !== "object") return false;
|
|
7693
|
+
const rec = row;
|
|
7694
|
+
return rec.challengeId === args.challengeId || rec.receipt?.challengeId === args.challengeId;
|
|
7695
|
+
}) : receiptsOut;
|
|
7696
|
+
return text({ count: Array.isArray(filtered) ? filtered.length : 0, receipts: filtered });
|
|
7697
|
+
}
|
|
7698
|
+
function parseLoginTarget(value) {
|
|
7699
|
+
if (value === void 0 || value === null || value === "") return void 0;
|
|
7700
|
+
if (typeof value !== "string") return { error: 'target must be "dev" or "local"' };
|
|
7701
|
+
const target = value.trim().toLowerCase();
|
|
7702
|
+
if (target === "dev" || target === "local") return target;
|
|
7703
|
+
return { error: 'target must be "dev" or "local"' };
|
|
6024
7704
|
}
|
|
6025
7705
|
async function suipayLogin(args, cfg, deps = {}) {
|
|
7706
|
+
const parsedTarget = parseLoginTarget(args.target);
|
|
7707
|
+
if (parsedTarget && typeof parsedTarget === "object" && "error" in parsedTarget) {
|
|
7708
|
+
return error({
|
|
7709
|
+
status: "failed",
|
|
7710
|
+
code: "INVALID_TARGET",
|
|
7711
|
+
detail: parsedTarget.error
|
|
7712
|
+
});
|
|
7713
|
+
}
|
|
7714
|
+
const target = parsedTarget;
|
|
6026
7715
|
const label = args.label?.trim() || cfg.label.trim();
|
|
6027
7716
|
const identity = await ensureLocalSigningKey(label ? { label } : {});
|
|
6028
|
-
const
|
|
6029
|
-
|
|
6030
|
-
const
|
|
6031
|
-
|
|
6032
|
-
|
|
6033
|
-
|
|
6034
|
-
|
|
7717
|
+
const local = loadLocalSigner();
|
|
7718
|
+
const stored = loadLocalSigningKeyRecord();
|
|
7719
|
+
const endpoints = target ? MCP_TARGET_PRESETS[target] : {
|
|
7720
|
+
consoleUrl: cfg.consoleUrl,
|
|
7721
|
+
gatewayUrl: stored?.gatewayUrl ?? cfg.gatewayUrl
|
|
7722
|
+
};
|
|
7723
|
+
if (local) {
|
|
7724
|
+
const live = await checkLiveDelegateGrant({
|
|
7725
|
+
gatewayUrl: endpoints.gatewayUrl,
|
|
7726
|
+
delegateAddress: local.address,
|
|
7727
|
+
signPersonalMessage: local.signPersonalMessage,
|
|
7728
|
+
...deps.fetchImpl ? { fetchImpl: deps.fetchImpl } : {}
|
|
7729
|
+
});
|
|
7730
|
+
if (live.kind === "ambiguous") {
|
|
7731
|
+
return error({
|
|
7732
|
+
status: "failed",
|
|
7733
|
+
code: "AMBIGUOUS_GRANT",
|
|
7734
|
+
detail: "multiple live grants name this key; revoke extras or pass a selector before paying"
|
|
7735
|
+
});
|
|
7736
|
+
}
|
|
7737
|
+
if (live.kind === "live") {
|
|
7738
|
+
const storedUrl = stored?.gatewayUrl;
|
|
7739
|
+
if (!storedUrl || storedUrl !== endpoints.gatewayUrl && (sameMcpGateway(storedUrl, endpoints.gatewayUrl) || target !== void 0)) {
|
|
7740
|
+
persistGatewayUrl(endpoints.gatewayUrl);
|
|
7741
|
+
}
|
|
7742
|
+
return text({
|
|
7743
|
+
status: "ok",
|
|
7744
|
+
reused: true,
|
|
7745
|
+
target: target ?? cfg.target,
|
|
7746
|
+
delegateAddress: identity.delegateAddress,
|
|
7747
|
+
gatewayUrl: loadLocalSigningKeyRecord()?.gatewayUrl ?? endpoints.gatewayUrl,
|
|
7748
|
+
grantObjectId: live.grantObjectId,
|
|
7749
|
+
message: "Existing spend-account grant is live for this key. No browser round-trip."
|
|
7750
|
+
});
|
|
7751
|
+
}
|
|
7752
|
+
if (live.kind === "unknown") {
|
|
7753
|
+
return error({
|
|
7754
|
+
status: "failed",
|
|
7755
|
+
code: "GRANT_STATUS_UNKNOWN",
|
|
7756
|
+
detail: "Could not verify the existing grant. Credentials were not modified. Retry later; do not start a new consent (that would mint a second grant)."
|
|
7757
|
+
});
|
|
7758
|
+
}
|
|
6035
7759
|
}
|
|
6036
|
-
|
|
6037
|
-
|
|
6038
|
-
|
|
6039
|
-
|
|
6040
|
-
|
|
7760
|
+
const started = await (deps.startStdioLogin ?? startStdioLogin)({
|
|
7761
|
+
consoleUrl: endpoints.consoleUrl,
|
|
7762
|
+
gatewayUrl: endpoints.gatewayUrl,
|
|
7763
|
+
identity,
|
|
7764
|
+
label,
|
|
7765
|
+
timeoutMs: LOGIN_BG_TIMEOUT_MS,
|
|
7766
|
+
openBrowser: false,
|
|
7767
|
+
onUrl: deps.onUrl
|
|
6041
7768
|
});
|
|
7769
|
+
const url = started.connectUrl;
|
|
7770
|
+
return text(
|
|
7771
|
+
[
|
|
7772
|
+
`## ACTION REQUIRED: User must click this URL to sign in`,
|
|
7773
|
+
``,
|
|
7774
|
+
`**Target:** ${target ?? cfg.target ?? "custom"} \xB7 ${endpoints.gatewayUrl}`,
|
|
7775
|
+
``,
|
|
7776
|
+
`**URL:** ${url}`,
|
|
7777
|
+
``,
|
|
7778
|
+
"```",
|
|
7779
|
+
url,
|
|
7780
|
+
"```",
|
|
7781
|
+
``,
|
|
7782
|
+
`[Click here to open SuiPay sign-in](${url})`,
|
|
7783
|
+
``,
|
|
7784
|
+
`**IMPORTANT for the assistant**: do NOT summarize or omit the URL above.`,
|
|
7785
|
+
`The user CANNOT proceed without seeing the exact URL. Surface it verbatim.`,
|
|
7786
|
+
``,
|
|
7787
|
+
`1. Open the URL in any browser`,
|
|
7788
|
+
`2. Connect the owner wallet and approve the spend-account grant (Access keys / policies)`,
|
|
7789
|
+
`3. Once Connected appears, retry the original request \u2014 credentials at \`~/.suipay/credentials.json\` are picked up on the next tool call`,
|
|
7790
|
+
``,
|
|
7791
|
+
`_The login link stays valid for 5 minutes. If it expires, call \`suipay_login\` again._`,
|
|
7792
|
+
``,
|
|
7793
|
+
JSON.stringify({
|
|
7794
|
+
status: "ok",
|
|
7795
|
+
target: target ?? cfg.target,
|
|
7796
|
+
delegateAddress: identity.delegateAddress,
|
|
7797
|
+
connectUrl: url,
|
|
7798
|
+
consoleUrl: endpoints.consoleUrl,
|
|
7799
|
+
gatewayUrl: endpoints.gatewayUrl
|
|
7800
|
+
})
|
|
7801
|
+
].join("\n")
|
|
7802
|
+
);
|
|
6042
7803
|
}
|
|
6043
7804
|
async function logout(args, _cfg, deps = {}) {
|
|
6044
7805
|
if (args.force) {
|
|
@@ -6064,7 +7825,7 @@ var TOOLS = [
|
|
|
6064
7825
|
},
|
|
6065
7826
|
{
|
|
6066
7827
|
name: "pay",
|
|
6067
|
-
description: "Pay a HTTP resource via delegate. On hosted /mcp the gateway PREPARES an unsigned transaction and returns it (prepare-only \u2014 it never holds the delegate key and cannot settle). Only a process holding the delegate key the grant names can complete settlement (e.g. local stdio MCP, or any self-hosted process with the matching key). Pass recipient and/or maxAmount to state who you intend to pay and what you will spend; both are checked before anything is signed or handed back. Local V1 allowance pay is retired.",
|
|
7828
|
+
description: "Pay a HTTP resource via delegate. On hosted /mcp the gateway PREPARES an unsigned transaction and returns it (prepare-only \u2014 it never holds the delegate key and cannot settle). Only a process holding the delegate key the grant names can complete settlement (e.g. local stdio MCP, or any self-hosted process with the matching key). Pass recipient and/or maxAmount to state who you intend to pay and what you will spend; both are checked before anything is signed or handed back. Local V1 allowance pay is retired. When this process settles a paid binary body, it writes the bytes under ~/.suipay/deliveries/ and names savedTo (absolute path) and savedToUrl (file://) first in the JSON. Hosted /mcp is prepare-only and does not write a buyer file. Always quote savedTo when present. If savedTo is null, quote saveError and do not call pay again. Do not assume the host rendered an image block. Never call pay again to display or save a delivery that already settled.",
|
|
6068
7829
|
inputSchema: {
|
|
6069
7830
|
type: "object",
|
|
6070
7831
|
properties: {
|
|
@@ -6078,6 +7839,14 @@ var TOOLS = [
|
|
|
6078
7839
|
type: "object",
|
|
6079
7840
|
description: "JSON request body for a POST. Sent as application/json and bound to the payment."
|
|
6080
7841
|
},
|
|
7842
|
+
file: {
|
|
7843
|
+
type: "string",
|
|
7844
|
+
description: "Absolute path under SUIPAY_MCP_FILE_ROOT (unset disables attach). Stdio POST only; requires fileField. Regular non-empty file whose decoded size fits the paid-body ceiling. Not a URL, FIFO, device, or path under ~/.suipay. Bytes fill json[fileField] as canonical base64."
|
|
7845
|
+
},
|
|
7846
|
+
fileField: {
|
|
7847
|
+
type: "string",
|
|
7848
|
+
description: "JSON key to set from file (e.g. contentBase64). Required with file. Pattern [A-Za-z][A-Za-z0-9_]{0,63}. Must not already be present on json."
|
|
7849
|
+
},
|
|
6081
7850
|
recipient: {
|
|
6082
7851
|
type: "string",
|
|
6083
7852
|
description: "Sui address you expect to be paid. The payment is refused, unsigned, if the prepared transaction pays anyone else."
|
|
@@ -6092,7 +7861,7 @@ var TOOLS = [
|
|
|
6092
7861
|
},
|
|
6093
7862
|
{
|
|
6094
7863
|
name: "discover",
|
|
6095
|
-
description: "Search SuiPay gateway resources.",
|
|
7864
|
+
description: "Search SuiPay gateway resources. After login, each row includes name, path, url, catalog price, and whether the current session can cover one call at that price (descriptive only \u2014 pay still enforces the grant).",
|
|
6096
7865
|
inputSchema: {
|
|
6097
7866
|
type: "object",
|
|
6098
7867
|
properties: {
|
|
@@ -6118,11 +7887,16 @@ var TOOLS = [
|
|
|
6118
7887
|
},
|
|
6119
7888
|
{
|
|
6120
7889
|
name: "suipay_login",
|
|
6121
|
-
description:
|
|
7890
|
+
description: 'Mint or reuse the local delegate key (version-2, mode 0600) and return a clickable console URL immediately so the owner can bind a spend-account grant. Does not wait for the wallet. Default target is Railway suipay-dev; pass target="local" (or start the binary with --local) for the laptop stack. Stdio only; hosted /mcp never holds a spendable key.',
|
|
6122
7891
|
inputSchema: {
|
|
6123
7892
|
type: "object",
|
|
6124
7893
|
properties: {
|
|
6125
7894
|
label: { type: "string", description: "Label shown in SuiPay console." },
|
|
7895
|
+
target: {
|
|
7896
|
+
type: "string",
|
|
7897
|
+
enum: ["dev", "local"],
|
|
7898
|
+
description: 'Where to bind the grant. Default is Railway suipay-dev. Pass "local" for localhost:3000 / 127.0.0.1:4340.'
|
|
7899
|
+
},
|
|
6126
7900
|
gatewayResourceUrl: {
|
|
6127
7901
|
type: "string",
|
|
6128
7902
|
description: "Gateway resource URL whose 402 defines recipient, asset, and package."
|
|
@@ -6178,8 +7952,9 @@ function toolError(value) {
|
|
|
6178
7952
|
isError: true
|
|
6179
7953
|
};
|
|
6180
7954
|
}
|
|
6181
|
-
function
|
|
7955
|
+
function createServer2(cfg, auth, trace, options = {}) {
|
|
6182
7956
|
const resolveAgentHeld = options.resolveAgentHeld ?? resolveAgentHeldSettlement;
|
|
7957
|
+
const transport = options.transport ?? "stdio";
|
|
6183
7958
|
const server = new Server(
|
|
6184
7959
|
{ name: "suipay", version: "1.0.0" },
|
|
6185
7960
|
{
|
|
@@ -6203,20 +7978,32 @@ function createServer(cfg, auth, trace, options = {}) {
|
|
|
6203
7978
|
}
|
|
6204
7979
|
switch (name) {
|
|
6205
7980
|
case "access_context":
|
|
6206
|
-
return await accessContext({ auth });
|
|
7981
|
+
return await accessContext({ auth }, cfg);
|
|
6207
7982
|
case "pay": {
|
|
6208
7983
|
const agentHeld = auth?.spendAccountPay ? resolveAgentHeld(cfg, auth.spendAccountPay) : null;
|
|
7984
|
+
const fileArgs = {
|
|
7985
|
+
...args.file !== void 0 ? { file: args.file } : {},
|
|
7986
|
+
...args.fileField !== void 0 ? { fileField: args.fileField } : {}
|
|
7987
|
+
};
|
|
7988
|
+
if (transport === "http" && hasLocalFileArgs(fileArgs)) {
|
|
7989
|
+
return closedPayError(
|
|
7990
|
+
"FILE_STDIO_ONLY",
|
|
7991
|
+
"file is stdio only; hosted /mcp does not read the buyer disk"
|
|
7992
|
+
);
|
|
7993
|
+
}
|
|
6209
7994
|
return await pay(
|
|
6210
7995
|
{
|
|
6211
7996
|
url: String(args.url ?? ""),
|
|
6212
7997
|
...args.method !== void 0 ? { method: args.method } : {},
|
|
6213
7998
|
...args.json !== void 0 ? { json: args.json } : {},
|
|
7999
|
+
...transport === "stdio" ? fileArgs : {},
|
|
6214
8000
|
...args.recipient !== void 0 ? { recipient: args.recipient } : {},
|
|
6215
8001
|
...args.maxAmount !== void 0 ? { maxAmount: args.maxAmount } : {}
|
|
6216
8002
|
},
|
|
6217
8003
|
cfg,
|
|
6218
8004
|
{
|
|
6219
8005
|
auth,
|
|
8006
|
+
transport,
|
|
6220
8007
|
...trace ? { trace } : {},
|
|
6221
8008
|
...agentHeld ? { agentHeld } : {}
|
|
6222
8009
|
}
|
|
@@ -6247,6 +8034,7 @@ function createServer(cfg, auth, trace, options = {}) {
|
|
|
6247
8034
|
return await suipayLogin(
|
|
6248
8035
|
{
|
|
6249
8036
|
label: typeof args.label === "string" ? args.label : void 0,
|
|
8037
|
+
target: typeof args.target === "string" ? args.target : void 0,
|
|
6250
8038
|
gatewayResourceUrl: typeof args.gatewayResourceUrl === "string" ? args.gatewayResourceUrl : void 0
|
|
6251
8039
|
},
|
|
6252
8040
|
cfg,
|
|
@@ -6304,6 +8092,11 @@ function canonicalMcpRequestId(id) {
|
|
|
6304
8092
|
function asRecord(value) {
|
|
6305
8093
|
return typeof value === "object" && value !== null && !Array.isArray(value) ? value : null;
|
|
6306
8094
|
}
|
|
8095
|
+
function redactPayFileArg(args) {
|
|
8096
|
+
const record = asRecord(args);
|
|
8097
|
+
if (!record || !("file" in record)) return args;
|
|
8098
|
+
return { ...record, file: "[redacted]" };
|
|
8099
|
+
}
|
|
6307
8100
|
function readEntry(message) {
|
|
6308
8101
|
const record = asRecord(message);
|
|
6309
8102
|
if (!record) return null;
|
|
@@ -6315,7 +8108,7 @@ function readEntry(message) {
|
|
|
6315
8108
|
jsonRpcId: typeof rawId === "string" || typeof rawId === "number" ? rawId : null,
|
|
6316
8109
|
requestId: canonicalMcpRequestId(rawId),
|
|
6317
8110
|
toolName: params && typeof params.name === "string" ? params.name : null,
|
|
6318
|
-
toolArguments: params ? params.arguments : void 0,
|
|
8111
|
+
toolArguments: params ? redactPayFileArg(params.arguments) : void 0,
|
|
6319
8112
|
clientName: clientInfo && typeof clientInfo.name === "string" ? clientInfo.name : null,
|
|
6320
8113
|
clientVersion: clientInfo && typeof clientInfo.version === "string" ? clientInfo.version : null,
|
|
6321
8114
|
protocolVersion: params && typeof params.protocolVersion === "string" ? params.protocolVersion : null
|
|
@@ -6745,7 +8538,10 @@ async function handleSuipayMcpHttpRequest(req, cfg, auth, trace, options) {
|
|
|
6745
8538
|
sessionIdGenerator: void 0,
|
|
6746
8539
|
enableJsonResponse: true
|
|
6747
8540
|
});
|
|
6748
|
-
const server =
|
|
8541
|
+
const server = createServer2(cfg, auth, hooks, {
|
|
8542
|
+
...options,
|
|
8543
|
+
transport: "http"
|
|
8544
|
+
});
|
|
6749
8545
|
await server.connect(transport);
|
|
6750
8546
|
return transport.handleRequest(request);
|
|
6751
8547
|
}
|
|
@@ -6754,10 +8550,15 @@ async function handleSuipayMcpHttpRequest(req, cfg, auth, trace, options) {
|
|
|
6754
8550
|
|
|
6755
8551
|
export {
|
|
6756
8552
|
resolveGrantTarget,
|
|
8553
|
+
MCP_TARGET_PRESETS,
|
|
8554
|
+
parseMcpTargetFlag,
|
|
8555
|
+
resolveMcpTarget,
|
|
8556
|
+
sameMcpGateway,
|
|
6757
8557
|
loadMcpConfig,
|
|
6758
8558
|
loadPaymentProfile,
|
|
6759
8559
|
ensureLocalSigningKey,
|
|
6760
8560
|
loadLocalSigner,
|
|
8561
|
+
loadLocalSigningKeyRecord,
|
|
6761
8562
|
resetAgentHeldProcessLedger,
|
|
6762
8563
|
resolveAgentHeldSettlement,
|
|
6763
8564
|
settlePreparedMcpPayment,
|
|
@@ -6768,7 +8569,7 @@ export {
|
|
|
6768
8569
|
receipts,
|
|
6769
8570
|
PACKAGE_NAME,
|
|
6770
8571
|
TOOLS,
|
|
6771
|
-
createServer,
|
|
8572
|
+
createServer2 as createServer,
|
|
6772
8573
|
canonicalMcpRequestId,
|
|
6773
8574
|
inspectMcpEnvelope,
|
|
6774
8575
|
traceMcpHttpRequest,
|