@cmdoss/suipay-mcp 0.2.2-dev.8 → 0.2.2-dev.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -5,13 +5,13 @@ buyer-funded spend account. The buyer owns the account. The agent holds the
5
5
  delegate key and can spend only under the on-chain grant (recipient bind,
6
6
  per-payment cap, budget).
7
7
 
8
- Pin `@cmdoss/suipay-mcp@0.2.2` in MCP config. `1.0.1` remains on the
8
+ Pin `@cmdoss/suipay-mcp@0.2.2-dev.9` in MCP config. `1.0.1` remains on the
9
9
  registry as a historical release; do not install it for new agents.
10
10
 
11
11
  ## Install
12
12
 
13
13
  ```bash
14
- claude mcp add --scope user suipay -- npx -y @cmdoss/suipay-mcp@0.2.2
14
+ claude mcp add --scope user suipay -- npx -y @cmdoss/suipay-mcp@0.2.2-dev.9
15
15
  ```
16
16
 
17
17
  No wallet secret belongs in this command or MCP config. There is no
@@ -112,10 +112,10 @@ Default is Railway **suipay-dev**. `--local` / `target: "local"` is laptop-only.
112
112
 
113
113
  ```bash
114
114
  # Railway suipay-dev (default)
115
- npx @cmdoss/suipay-mcp@0.2.2
115
+ npx @cmdoss/suipay-mcp@0.2.2-dev.9
116
116
 
117
117
  # Laptop stack
118
- npx @cmdoss/suipay-mcp@0.2.2 --local
118
+ npx @cmdoss/suipay-mcp@0.2.2-dev.9 --local
119
119
  ```
120
120
 
121
121
  `SUIPAY_MCP_TARGET=local|dev` selects the same preset. Explicit
@@ -1,8 +1,8 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  main
4
- } from "../chunk-KW3ZECP6.js";
5
- import "../chunk-K4C64BXE.js";
4
+ } from "../chunk-LQOZBRH6.js";
5
+ import "../chunk-NDJ7BFI4.js";
6
6
 
7
7
  // src/bin/suipay.ts
8
8
  main().catch((err) => {
@@ -4,7 +4,7 @@ import {
4
4
  loadLocalSigningKeyRecord,
5
5
  loadMcpConfig,
6
6
  loadPaymentProfile
7
- } from "./chunk-K4C64BXE.js";
7
+ } from "./chunk-NDJ7BFI4.js";
8
8
 
9
9
  // src/index.ts
10
10
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
@@ -538,6 +538,32 @@ function createSpendAccountChain(options) {
538
538
  kindBytes: toBase64(await tx.build({ client, onlyTransactionKind: true }))
539
539
  };
540
540
  }
541
+ function appendCreatePolicyGrant(tx, input) {
542
+ if (!input.onChainPolicyIds.length) {
543
+ throw new Error("create_grant requires at least one on-chain policy id");
544
+ }
545
+ if (!input.maxPerPayment || BigInt(input.maxPerPayment) <= 0n) {
546
+ throw new Error("create_grant requires a non-zero max_per_payment");
547
+ }
548
+ if (!input.sessionCap || BigInt(input.sessionCap) <= 0n) {
549
+ throw new Error("create_grant requires a non-zero session_cap");
550
+ }
551
+ const expires = input.expiresAtMs === void 0 || input.expiresAtMs === null ? 0n : BigInt(input.expiresAtMs);
552
+ tx.moveCall({
553
+ target: target("create_policy_grant"),
554
+ typeArguments: [input.coinType],
555
+ arguments: [
556
+ tx.object(input.poolObjectId),
557
+ tx.pure.address(input.delegate),
558
+ // The unified Grant<T> takes Option<u64> caps. This builder rejects
559
+ // absent or zero caps above, so both are always Some here.
560
+ tx.pure.option("u64", BigInt(input.sessionCap)),
561
+ tx.pure.option("u64", BigInt(input.maxPerPayment)),
562
+ tx.pure.u64(expires),
563
+ tx.pure.vector("u64", input.onChainPolicyIds.map((id) => BigInt(id)))
564
+ ]
565
+ });
566
+ }
541
567
  async function fundingCoin(tx, owner, coinType, amount) {
542
568
  if (!isSuiCoinType(coinType)) {
543
569
  return coinWithBalance({ type: coinType, balance: amount });
@@ -737,32 +763,23 @@ function createSpendAccountChain(options) {
737
763
  return kindBytes(tx);
738
764
  },
739
765
  async buildCreateGrantKind(input) {
740
- if (!input.onChainPolicyIds.length) {
741
- throw new Error("create_grant requires at least one on-chain policy id");
742
- }
743
- if (!input.maxPerPayment || BigInt(input.maxPerPayment) <= 0n) {
744
- throw new Error("create_grant requires a non-zero max_per_payment");
766
+ const tx = new Transaction2();
767
+ tx.setSender(input.owner);
768
+ appendCreatePolicyGrant(tx, input);
769
+ return kindBytes(tx);
770
+ },
771
+ async buildCreateGrantBatchKind(inputs) {
772
+ if (!inputs.length) {
773
+ throw new Error("create_grant batch requires at least one grant");
745
774
  }
746
- if (!input.sessionCap || BigInt(input.sessionCap) <= 0n) {
747
- throw new Error("create_grant requires a non-zero session_cap");
775
+ const owner = inputs[0].owner;
776
+ if (inputs.some((input) => input.owner !== owner)) {
777
+ throw new Error("create_grant batch requires one owner");
748
778
  }
749
779
  const tx = new Transaction2();
750
- tx.setSender(input.owner);
751
- const expires = input.expiresAtMs === void 0 || input.expiresAtMs === null ? 0n : BigInt(input.expiresAtMs);
752
- tx.moveCall({
753
- target: target("create_policy_grant"),
754
- typeArguments: [input.coinType],
755
- arguments: [
756
- tx.object(input.poolObjectId),
757
- tx.pure.address(input.delegate),
758
- // The unified Grant<T> takes Option<u64> caps. This builder rejects
759
- // absent or zero caps above, so both are always Some here.
760
- tx.pure.option("u64", BigInt(input.sessionCap)),
761
- tx.pure.option("u64", BigInt(input.maxPerPayment)),
762
- tx.pure.u64(expires),
763
- tx.pure.vector("u64", input.onChainPolicyIds.map((id) => BigInt(id)))
764
- ]
765
- });
780
+ tx.setSender(owner);
781
+ for (const input of inputs)
782
+ appendCreatePolicyGrant(tx, input);
766
783
  return kindBytes(tx);
767
784
  },
768
785
  async buildCreateSpendGrantKind(input) {
@@ -5563,7 +5580,7 @@ var MCP_TARGET_PRESETS = {
5563
5580
  gatewayUrl: "https://gateway-dev-ba84.up.railway.app"
5564
5581
  },
5565
5582
  local: {
5566
- consoleUrl: "http://localhost:3000",
5583
+ consoleUrl: "https://suipay-dev.up.railway.app",
5567
5584
  gatewayUrl: "http://127.0.0.1:4340"
5568
5585
  }
5569
5586
  };
@@ -5996,9 +6013,29 @@ function recordFromKeypair(kp, opts = {}, existing, operatorSecret) {
5996
6013
  };
5997
6014
  }
5998
6015
  var ENV_KEY_MISMATCH = "SUIPAY_MCP_KEY does not match the stored delegate key - refusing to overwrite";
6016
+ function signingMaterial(parsed) {
6017
+ if (!parsed || typeof parsed !== "object") return null;
6018
+ if (isLocalSigningKeyRecord(parsed)) return parsed;
6019
+ const c = parsed;
6020
+ if (typeof c.delegatePrivateKey !== "string" || !HEX64.test(c.delegatePrivateKey) || typeof c.delegatePublicKeyHex !== "string" || !HEX64.test(c.delegatePublicKeyHex) || typeof c.delegateAddress !== "string" || !ADDR.test(c.delegateAddress)) {
6021
+ return null;
6022
+ }
6023
+ const label = typeof c.label === "string" ? c.label.trim() : "";
6024
+ return {
6025
+ version: 2,
6026
+ delegatePrivateKey: c.delegatePrivateKey,
6027
+ delegatePublicKeyHex: c.delegatePublicKeyHex,
6028
+ delegateAddress: c.delegateAddress,
6029
+ createdAt: typeof c.createdAt === "string" && c.createdAt ? c.createdAt : (/* @__PURE__ */ new Date(0)).toISOString(),
6030
+ ...isHttpUrl(c.gatewayUrl) ? { gatewayUrl: c.gatewayUrl } : {},
6031
+ ...label ? { label } : {}
6032
+ };
6033
+ }
5999
6034
  function existingRecordForEnv(parsed) {
6000
6035
  if (parsed === null) return null;
6001
- if (isLocalSigningKeyRecord(parsed) || isValid(parsed)) return parsed;
6036
+ if (isValid(parsed)) return parsed;
6037
+ const material = signingMaterial(parsed);
6038
+ if (material) return material;
6002
6039
  throw new Error(
6003
6040
  `credentials file at ${credsPath()} failed schema validation - refusing to continue`
6004
6041
  );
@@ -6036,16 +6073,25 @@ async function ensureLocalSigningKey(opts = {}) {
6036
6073
  }
6037
6074
  const parsed = readCredentialsJson();
6038
6075
  if (parsed !== null) {
6039
- if (isLocalSigningKeyRecord(parsed)) {
6040
- return identityFromStored(
6041
- parsed.delegatePrivateKey,
6042
- parsed.delegatePublicKeyHex,
6043
- parsed.delegateAddress
6044
- );
6045
- }
6046
- if (isValid(parsed)) {
6076
+ if (isValid(parsed) && !isLocalSigningKeyRecord(parsed)) {
6047
6077
  return rewriteV1ToLocalSigningKey(parsed, opts);
6048
6078
  }
6079
+ const material = signingMaterial(parsed);
6080
+ if (material) {
6081
+ const identity = identityFromStored(
6082
+ material.delegatePrivateKey,
6083
+ material.delegatePublicKeyHex,
6084
+ material.delegateAddress
6085
+ );
6086
+ if (!isLocalSigningKeyRecord(parsed)) {
6087
+ const label = opts.label?.trim() || material.label;
6088
+ writeCredentialsFile({
6089
+ ...material,
6090
+ ...label ? { label } : {}
6091
+ });
6092
+ }
6093
+ return identity;
6094
+ }
6049
6095
  throw new Error(`credentials file at ${credsPath()} failed schema validation - refusing to continue`);
6050
6096
  }
6051
6097
  const kp = await generateKeypair();
@@ -6136,6 +6182,32 @@ function loadLocalKeypair() {
6136
6182
  Uint8Array.from(Buffer.from(parsed.delegatePrivateKey, "hex"))
6137
6183
  );
6138
6184
  }
6185
+ function replaceLocalSigningKey(input) {
6186
+ identityFromStored(
6187
+ input.delegatePrivateKey,
6188
+ input.delegatePublicKeyHex,
6189
+ input.delegateAddress
6190
+ );
6191
+ const parsed = readCredentialsJson();
6192
+ let gatewayUrl;
6193
+ let label;
6194
+ if (parsed && typeof parsed === "object") {
6195
+ const record = parsed;
6196
+ if (isHttpUrl(record.gatewayUrl)) gatewayUrl = record.gatewayUrl;
6197
+ if (typeof record.label === "string" && record.label.trim()) {
6198
+ label = record.label.trim();
6199
+ }
6200
+ }
6201
+ writeCredentialsFile({
6202
+ version: 2,
6203
+ delegatePrivateKey: input.delegatePrivateKey,
6204
+ delegatePublicKeyHex: input.delegatePublicKeyHex,
6205
+ delegateAddress: input.delegateAddress,
6206
+ createdAt: (/* @__PURE__ */ new Date()).toISOString(),
6207
+ ...gatewayUrl ? { gatewayUrl } : {},
6208
+ ...label ? { label } : {}
6209
+ });
6210
+ }
6139
6211
  function persistGatewayUrl(gatewayUrl) {
6140
6212
  if (!isHttpUrl(gatewayUrl)) {
6141
6213
  throw new Error("gatewayUrl must be an http(s) origin");
@@ -7146,7 +7218,7 @@ var SECRET_KEY_SET = new Set(FORBIDDEN_SECRET_KEYS.map((k) => k.toLowerCase()));
7146
7218
 
7147
7219
  // ../sdk/dist/agent-sdk/delegate-agent.js
7148
7220
  function resolveExclusionDirectoryId(explicit, discovered) {
7149
- return explicit ?? discovered;
7221
+ return explicit?.trim() ? explicit : discovered;
7150
7222
  }
7151
7223
  function createExclusionRegistryResolver(client, exclusionDirectoryObjectId) {
7152
7224
  if (!exclusionDirectoryObjectId || !exclusionDirectoryObjectId.trim())
@@ -7216,7 +7288,15 @@ async function createPayer(options) {
7216
7288
  const { session, binding } = resolved.value;
7217
7289
  const deployment = {
7218
7290
  network: session.network,
7219
- packageId: session.packageId
7291
+ packageId: session.packageId,
7292
+ // What the GATEWAY published, not what this payer settled on: a caller
7293
+ // override belongs to the payer's configuration, not to the deployment it
7294
+ // is describing. Dropping this left the field permanently undefined on the
7295
+ // one instance a consumer can reach, so anything asking the payer whether
7296
+ // guarded settles are available got "no" while it was doing them.
7297
+ ...resolved.value.deployment.exclusionDirectoryObjectId ? {
7298
+ exclusionDirectoryObjectId: resolved.value.deployment.exclusionDirectoryObjectId
7299
+ } : {}
7220
7300
  };
7221
7301
  const exclusionDirectoryObjectId = resolveExclusionDirectoryId(options.exclusionDirectoryObjectId, resolved.value.deployment.exclusionDirectoryObjectId);
7222
7302
  let network;
@@ -7336,6 +7416,7 @@ async function createPayer(options) {
7336
7416
  import { createServer } from "http";
7337
7417
  import { randomBytes as randomBytes3, timingSafeEqual as timingSafeEqual2 } from "crypto";
7338
7418
  import open from "open";
7419
+ import { Ed25519Keypair as Ed25519Keypair3 } from "@mysten/sui/keypairs/ed25519";
7339
7420
  var HEX642 = /^[0-9a-f]{64}$/;
7340
7421
  var PUBHEX = /^[0-9a-fA-F]{64}$/;
7341
7422
  function isPreflight(obj) {
@@ -7343,6 +7424,11 @@ function isPreflight(obj) {
7343
7424
  const o = obj;
7344
7425
  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;
7345
7426
  }
7427
+ function isAdoptKey(obj) {
7428
+ if (!obj || typeof obj !== "object") return false;
7429
+ const o = obj;
7430
+ return typeof o.state === "string" && HEX642.test(o.state) && typeof o.delegatePrivateKey === "string" && HEX642.test(o.delegatePrivateKey) && typeof o.delegatePublicKeyHex === "string" && PUBHEX.test(o.delegatePublicKeyHex) && typeof o.delegateAddress === "string" && /^0x[0-9a-fA-F]{64}$/.test(o.delegateAddress);
7431
+ }
7346
7432
  function isCallback(obj) {
7347
7433
  if (!obj || typeof obj !== "object") return false;
7348
7434
  const o = obj;
@@ -7467,7 +7553,7 @@ async function startStdioLogin(opts) {
7467
7553
  res.writeHead(code, { "content-type": type, ...SECURITY_HEADERS, ...extra });
7468
7554
  res.end(body);
7469
7555
  };
7470
- const okPath = url.pathname === "/preflight" || url.pathname === "/callback";
7556
+ const okPath = url.pathname === "/preflight" || url.pathname === "/callback" || url.pathname === "/key";
7471
7557
  if (req.method === "OPTIONS") {
7472
7558
  const extra = { ...cors };
7473
7559
  if (okHost && okOrigin && okPath && req.headers["access-control-request-private-network"] === "true") {
@@ -7538,6 +7624,55 @@ async function startStdioLogin(opts) {
7538
7624
  );
7539
7625
  return;
7540
7626
  }
7627
+ if (url.pathname === "/key") {
7628
+ if (!preflightVerified) {
7629
+ res.writeHead(428, {
7630
+ "content-type": "application/json",
7631
+ ...cors,
7632
+ ...SECURITY_HEADERS
7633
+ });
7634
+ res.end(JSON.stringify({ ok: false, error: "preflight required" }));
7635
+ return;
7636
+ }
7637
+ if (!isAdoptKey(parsed) || !stateEquals(parsed.state, connectState)) {
7638
+ res.writeHead(400, {
7639
+ "content-type": "application/json",
7640
+ ...cors,
7641
+ ...SECURITY_HEADERS
7642
+ });
7643
+ res.end(JSON.stringify({ ok: false, error: "invalid key payload" }));
7644
+ return;
7645
+ }
7646
+ try {
7647
+ const kp = Ed25519Keypair3.fromSecretKey(
7648
+ Uint8Array.from(Buffer.from(parsed.delegatePrivateKey, "hex"))
7649
+ );
7650
+ const derived = kp.getPublicKey().toSuiAddress();
7651
+ if (derived.toLowerCase() !== parsed.delegateAddress.toLowerCase()) {
7652
+ throw new Error("address mismatch");
7653
+ }
7654
+ replaceLocalSigningKey({
7655
+ delegatePrivateKey: parsed.delegatePrivateKey,
7656
+ delegatePublicKeyHex: parsed.delegatePublicKeyHex,
7657
+ delegateAddress: derived
7658
+ });
7659
+ } catch {
7660
+ res.writeHead(400, {
7661
+ "content-type": "application/json",
7662
+ ...cors,
7663
+ ...SECURITY_HEADERS
7664
+ });
7665
+ res.end(JSON.stringify({ ok: false, error: "key rejected" }));
7666
+ return;
7667
+ }
7668
+ res.writeHead(200, {
7669
+ "content-type": "application/json",
7670
+ ...cors,
7671
+ ...SECURITY_HEADERS
7672
+ });
7673
+ res.end(JSON.stringify({ ok: true }));
7674
+ return;
7675
+ }
7541
7676
  if (!preflightVerified) {
7542
7677
  res.writeHead(428, {
7543
7678
  "content-type": "text/html",
@@ -9099,10 +9234,15 @@ async function suipayLogin(args, cfg, deps = {}) {
9099
9234
  });
9100
9235
  }
9101
9236
  if (live.kind === "unknown") {
9102
- return error({
9103
- status: "failed",
9237
+ return text({
9238
+ status: "ok",
9239
+ reused: true,
9240
+ grantStatus: "unknown",
9104
9241
  code: "GRANT_STATUS_UNKNOWN",
9105
- 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)."
9242
+ target: target ?? cfg.target,
9243
+ delegateAddress: identity.delegateAddress,
9244
+ gatewayUrl: endpoints.gatewayUrl,
9245
+ message: "Credentials are already saved for this agent. The existing grant could not be checked just now, so no new sign-in was started and the key file was left unchanged. Retry suipay_login in a moment."
9106
9246
  });
9107
9247
  }
9108
9248
  }
package/dist/http.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  handleSuipayMcpHttpRequest
3
- } from "./chunk-K4C64BXE.js";
3
+ } from "./chunk-NDJ7BFI4.js";
4
4
  export {
5
5
  handleSuipayMcpHttpRequest
6
6
  };
package/dist/index.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  loadBootProfile,
3
3
  main
4
- } from "./chunk-KW3ZECP6.js";
4
+ } from "./chunk-LQOZBRH6.js";
5
5
  import {
6
6
  MCP_TARGET_PRESETS,
7
7
  PACKAGE_NAME,
@@ -26,7 +26,7 @@ import {
26
26
  settlePreparedMcpPayment,
27
27
  traceMcpHttpRequest,
28
28
  verifySpendAccountPayReconstructs
29
- } from "./chunk-K4C64BXE.js";
29
+ } from "./chunk-NDJ7BFI4.js";
30
30
  export {
31
31
  MCP_TARGET_PRESETS,
32
32
  PACKAGE_NAME,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cmdoss/suipay-mcp",
3
- "version": "0.2.2-dev.8",
3
+ "version": "0.2.2-dev.9",
4
4
  "type": "module",
5
5
  "description": "SuiPay MCP — pay MPP-gated APIs from a spend-account grant with an agent-held delegate key.",
6
6
  "main": "./dist/index.js",
@@ -20,7 +20,7 @@
20
20
  ],
21
21
  "publishConfig": {
22
22
  "access": "public",
23
- "provenance": true
23
+ "provenance": false
24
24
  },
25
25
  "engines": {
26
26
  "node": ">=20"
@@ -36,7 +36,9 @@
36
36
  "@types/node": "^26",
37
37
  "tsup": "^8.0.0",
38
38
  "typescript": "^6.0.3",
39
- "vitest": "^4.1.10"
39
+ "vitest": "^4.1.10",
40
+ "@cmdoss/suipay-core": "0.1.0",
41
+ "@cmdoss/suipay-sdk": "0.1.0"
40
42
  },
41
43
  "scripts": {
42
44
  "build": "tsup",