@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.
@@ -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 loadMcpConfig(env = process.env) {
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 gatewayUrl = env.SUIPAY_GATEWAY_URL ?? "http://localhost:4340";
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
- consoleUrl: env.SUIPAY_CONSOLE_URL ?? "http://localhost:3000",
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
- return 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";
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 writeCredentialsFile(payload) {
4486
- const dir = credsDir();
4487
- mkdirSync(dir, { recursive: true, mode: 448 });
4690
+ function fsyncDir(dir) {
4488
4691
  try {
4489
- chmodSync(dir, 448);
4692
+ const fd = openSync(dir, "r");
4693
+ try {
4694
+ fsyncSync(fd);
4695
+ } finally {
4696
+ closeSync(fd);
4697
+ }
4490
4698
  } catch {
4491
4699
  }
4492
- const path = credsPath();
4493
- const tmp = join2(dir, `.credentials.${process.pid}.tmp`);
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
- let fd;
4740
+ const tmp = writeTmpCredentials(dir, payload);
4741
+ fsyncDir(dir);
4516
4742
  try {
4517
- fd = openSync(path, "wx", 384);
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
- writeSync(fd, JSON.stringify(payload, null, 2));
4524
- fsyncSync(fd);
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 normalizeSuiAddress10 } from "@mysten/sui/utils";
5179
+ import { isValidSuiAddress, normalizeSuiAddress as normalizeSuiAddress12 } from "@mysten/sui/utils";
4844
5180
 
4845
- // ../sdk/dist/agent-sdk/manifest.js
4846
- var MANIFEST_ERROR_CODES = {
4847
- MALFORMED_MANIFEST: "MALFORMED_MANIFEST",
4848
- MISSING_NETWORK: "MISSING_NETWORK",
4849
- MISSING_PACKAGE: "MISSING_PACKAGE",
4850
- MISSING_DELEGATE: "MISSING_DELEGATE",
4851
- NO_GRANTS: "NO_GRANTS",
4852
- MISSING_COIN_TYPE: "MISSING_COIN_TYPE",
4853
- MISSING_POOL_OBJECT: "MISSING_POOL_OBJECT",
4854
- MISSING_GRANT_OBJECT: "MISSING_GRANT_OBJECT",
4855
- MISSING_POLICY_BINDING: "MISSING_POLICY_BINDING",
4856
- INVALID_ACCESS_MODE: "INVALID_ACCESS_MODE",
4857
- GRANT_NOT_FOUND: "GRANT_NOT_FOUND",
4858
- AMBIGUOUS_GRANT: "AMBIGUOUS_GRANT",
4859
- /**
4860
- * The chain does not agree that this grant belongs to this delegate. The
4861
- * gateway said it did; an unsigned `getObject` says otherwise, and the chain
4862
- * wins (ADR-0016 §3).
4863
- */
4864
- GRANT_NOT_VERIFIED: "GRANT_NOT_VERIFIED",
4865
- /**
4866
- * The on-chain check could not run to a verdict — the fullnode read was
4867
- * unreachable or returned nothing. "Could not verify" is not "verified and it
4868
- * failed": both refuse, but only one is a security event.
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
- return null;
4900
- }
4901
- function normalizePolicyBinding(raw) {
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
- // src/tools.ts
5276
- import open from "open";
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
- // src/settle.ts
5279
- async function settlePreparedMcpPayment(args) {
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 verifyPreparedSettlementKind({
5282
- kindBytes: args.signingRequest.unsignedKindBytes,
5283
- expected: {
5284
- packageId: args.signingRequest.packageId,
5285
- delegateAddress: args.expected.delegateAddress,
5286
- amount: args.expected.amount,
5287
- recipient: args.expected.recipient,
5288
- accessMode: args.signingRequest.submit.moveCallTarget.endsWith(
5289
- "settle_open_recipient_payment"
5290
- ) ? "open" : "scoped"
5291
- },
5292
- getObjectJson: args.getObjectJson
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 (err) {
5295
- if (err instanceof PreparedVerificationUnavailableError) {
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.PREPARED_VERIFY_UNAVAILABLE,
5300
- detail: err.message
6032
+ code: DELEGATED_PAY_ERROR_CODES.UNSUPPORTED_REQUEST,
6033
+ detail: "request cannot be paid for"
5301
6034
  };
5302
6035
  }
5303
- return {
5304
- status: "failed",
5305
- paid: false,
5306
- code: DELEGATED_PAY_ERROR_CODES.TX_MISMATCH,
5307
- detail: err instanceof Error ? err.message : "prepared transaction mismatch"
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
- const prepared = args.signingRequest.resource;
5311
- if (args.request.url !== prepared.url || args.request.method !== prepared.method) {
5312
- return {
5313
- status: "failed",
5314
- paid: false,
5315
- code: DELEGATED_PAY_ERROR_CODES.UNSUPPORTED_REQUEST,
5316
- detail: "prepared resource request cannot be replayed"
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 payDelegatedSpendAccount({
5320
- url: args.signingRequest.resource.url,
5321
- request: args.request,
5322
- session: args.session,
5323
- cfg: args.cfg,
5324
- deps: args.deps,
5325
- preparedTerms: {
5326
- recipient: args.expected.recipient,
5327
- amount: args.expected.amount,
5328
- asset: args.signingRequest.coinType,
5329
- targetHash: args.signingRequest.targetHash
5330
- },
5331
- ...args.intent ? { intent: args.intent } : {}
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 Transaction5 } from "@mysten/sui/transactions";
6349
+ import { Transaction as Transaction6 } from "@mysten/sui/transactions";
5337
6350
  import {
5338
- fromBase64 as fromBase645,
5339
- normalizeSuiAddress as normalizeSuiAddress9,
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 = `${normalizeSuiAddress9(auth.packageId)}::${auth.module}::${auth.payFunction}`;
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 Transaction5();
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 = Transaction5.from(fromBase645(bytes)).getData();
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 || normalizeSuiAddress9(data.sender) !== normalizeSuiAddress9(expected.delegate)) {
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 = `${normalizeSuiAddress9(auth.packageId)}::${auth.module}::${auth.payFunction}`;
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 = `${normalizeSuiAddress9(String(cmd.MoveCall.package))}::${cmd.MoveCall.module}::${cmd.MoveCall.function}`;
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 = Transaction5.fromKind(fromBase645(expected.kindBytes)).getData();
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: normalizeSuiAddress9(authority.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/tools.ts
5676
- function payerTrace(deps) {
5677
- const hooks = deps.trace;
5678
- const buyerAccountId = deps.auth?.buyerAccountId;
5679
- if (!hooks || !buyerAccountId) return null;
5680
- return {
5681
- sink: hooks.sink,
5682
- traceId: hooks.context.traceId,
5683
- requestId: hooks.context.requestId,
5684
- buyerAccountId,
5685
- connectionId: hooks.runtime.connectionId,
5686
- ...hooks.runtime.secret ? { secret: hooks.runtime.secret } : {},
5687
- ...hooks.runtime.now ? { now: hooks.runtime.now } : {}
5688
- };
5689
- }
5690
- var AUTH_REQUIRED = "No SuiPay credentials - run suipay_login to create a local delegate key, then bind a spend-account grant in the console.";
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
- type: "text",
5695
- text: typeof value === "string" ? value : JSON.stringify(value, null, 2)
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
- var MAX_MCP_IMAGE_BYTES = 5 * 1024 * 1024;
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
- { type: "text", text: JSON.stringify({ ...result, body: shape }, null, 2) }
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 = normalizeSuiAddress10(raw);
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 = normalizeSuiAddress10(prepared.recipient);
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(args);
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 settled.status === "ok" ? paidResult(settled) : error(settled);
7381
+ return settlePayResult(settled);
5874
7382
  }
5875
7383
  if (result.status === "prepared") return preparedResult(result);
5876
- return result.status === "ok" ? paidResult(result) : error(result);
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, local });
7399
+ return payStdioWithLocalKey({ request, intent, cfg, deps });
5892
7400
  }
5893
- function snapshotFromBinding(binding, delegateAddress) {
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
- async function payStdioWithLocalKey(args) {
5908
- const { request, intent, cfg, deps, local } = args;
5909
- const resolved = await resolvePayerSession({
5910
- gatewayUrl: cfg.gatewayUrl,
5911
- delegateAddress: local.address,
5912
- signPersonalMessage: local.signPersonalMessage,
5913
- signer: {
5914
- address: local.address,
5915
- signTransaction: local.signTransaction,
5916
- signPersonalMessage: local.signPersonalMessage
5917
- },
5918
- ...deps.fetchImpl ? { fetchImpl: deps.fetchImpl } : {}
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 (!resolved.ok) {
5921
- return closedPayError(resolved.error.code, resolved.error.detail);
7435
+ if (!deployment.ok) {
7436
+ return {
7437
+ kind: "unknown",
7438
+ detail: "gateway deployment identity could not be verified"
7439
+ };
5922
7440
  }
5923
- const snapshot = snapshotFromBinding(resolved.value.binding, local.address);
5924
- const packageId = resolved.value.session.packageId;
5925
- const result = await prepareSpendAccountPayment({
5926
- url: request.url,
5927
- request,
5928
- session: {
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
- cfg,
5934
- deps: {
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 (result.status === "prepared") {
5940
- const violation = intentViolation(intent, result.signingRequest);
5941
- if (violation) {
5942
- return error({
5943
- status: "failed",
5944
- paid: false,
5945
- code: "POLICY_REJECTED",
5946
- detail: violation
5947
- });
5948
- }
5949
- const agentHeld = deps.agentHeld ?? settlementFromLocalSigner(cfg, local, packageId);
5950
- const grantCeiling = resolved.value.binding.maxPerPayment;
5951
- const maxAmount = intent.maxAmount !== void 0 ? intent.maxAmount.toString() : grantCeiling != null && grantCeiling !== "" ? grantCeiling : null;
5952
- if (maxAmount === null) {
5953
- return error({
5954
- status: "failed",
5955
- paid: false,
5956
- code: "POLICY_REJECTED",
5957
- detail: "stdio pay requires maxAmount or a grant maxPerPayment; refusing to default the ceiling to the offer"
5958
- });
5959
- }
5960
- const spendIntent = {
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: resolved.value.binding.coinType,
7483
+ asset: payer.grant.coinType,
5963
7484
  ...intent.recipient ? { recipient: intent.recipient } : {}
5964
- };
5965
- const settled = await settlePreparedMcpPayment({
5966
- signingRequest: result.signingRequest,
5967
- request,
5968
- expected: {
5969
- amount: result.signingRequest.amount,
5970
- recipient: intent.recipient ?? result.signingRequest.recipient,
5971
- delegateAddress: local.address
5972
- },
5973
- signer: local,
5974
- getObjectJson: agentHeld.getObjectJson,
5975
- session: {
5976
- ...resolved.value.session,
5977
- signer: local
5978
- },
5979
- cfg: { gatewayUrl: cfg.gatewayUrl },
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
- return settled.status === "ok" ? paidResult(settled) : error(settled);
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 result.status === "ok" ? paidResult(result) : error(result);
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
- const url = new URL("/v1/services", cfg.gatewayUrl);
5992
- if (args.query) url.searchParams.set("q", args.query);
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
- return text(deps.auth.access);
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, _cfg, deps = {}) {
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 (!profile(deps)) return error(AUTH_REQUIRED);
6021
- return error(
6022
- "receipts is available on remote OAuth (/mcp) sessions only; the local allowance profile has no settlement feed access"
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 connect = new URL("/connect/agent", cfg.consoleUrl);
6029
- connect.searchParams.set("delegateAddress", identity.delegateAddress);
6030
- const url = connect.toString();
6031
- deps.onUrl?.(url);
6032
- try {
6033
- await open(url);
6034
- } catch {
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
- return text({
6037
- status: "ok",
6038
- delegateAddress: identity.delegateAddress,
6039
- connectUrl: url,
6040
- message: "Open the console to bind a spend-account grant to this delegate address. The private key never leaves this process."
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: "Mint or reuse the local delegate key (version-2, mode 0600) and open the console with the public address only so the owner can bind a spend-account grant. Stdio only; hosted /mcp never holds a spendable key.",
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 createServer(cfg, auth, trace, options = {}) {
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 = createServer(cfg, auth, hooks, options);
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,