@cmdoss/suipay-mcp 0.2.0

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/dist/http.d.ts ADDED
@@ -0,0 +1,3 @@
1
+ export { q as handleSuipayMcpHttpRequest } from './http-BnIjiwcR.js';
2
+ import '@modelcontextprotocol/sdk/server/index.js';
3
+ import '@mysten/sui/keypairs/ed25519';
package/dist/http.js ADDED
@@ -0,0 +1,7 @@
1
+ import {
2
+ handleSuipayMcpHttpRequest
3
+ } from "./chunk-XZQOOY66.js";
4
+ import "./chunk-A3JIIDYT.js";
5
+ export {
6
+ handleSuipayMcpHttpRequest
7
+ };
@@ -0,0 +1,324 @@
1
+ import { D as Dialect, G as GrantSnapshot, M as McpConfig, T as TraceSink, a as DelegatedKeySigner, b as GetObjectJson, c as DelegatedPayerSession, d as DelegatedPayerConfig, e as DelegatedPayerDeps, S as SpendIntent, f as DelegatedPayResult } from './http-BnIjiwcR.js';
2
+ export { A as AgentHeldResolver, g as AgentHeldSettlement, h as McpEnvelope, i as McpEnvelopeEntry, j as McpServerOptions, k as McpTraceContext, l as McpTraceHooks, m as McpTraceRuntime, P as PACKAGE_NAME, n as TOOLS, o as canonicalMcpRequestId, p as createServer, q as handleSuipayMcpHttpRequest, r as inspectMcpEnvelope, s as loadMcpConfig, t as resetAgentHeldProcessLedger, u as resolveAgentHeldSettlement, v as resolveGrantTarget, w as traceMcpHttpRequest } from './http-BnIjiwcR.js';
3
+ import '@modelcontextprotocol/sdk/server/index.js';
4
+ import '@mysten/sui/keypairs/ed25519';
5
+
6
+ type PaidHttpMethod = 'GET' | 'POST';
7
+ interface PaidHttpRequest {
8
+ url: string;
9
+ method: PaidHttpMethod;
10
+ /** Exact wire bytes. Empty for GET. */
11
+ body: Uint8Array;
12
+ /** Normalized media type, or null when no body is sent. */
13
+ contentType: string | null;
14
+ /** `hashRequestBody(body)` — the single canonical body hash. */
15
+ bodyHash: string;
16
+ }
17
+
18
+ /** Holds a key and signs transaction bytes. Never leaves the payer. */
19
+ interface Wallet {
20
+ readonly address: string;
21
+ signTransaction(txBytes: string): Promise<string>;
22
+ }
23
+
24
+ interface SuiPayCredentials {
25
+ /** 64-hex Ed25519 seed. NEVER logged. */
26
+ delegatePrivateKey: string;
27
+ delegatePublicKeyHex: string;
28
+ delegateAddress: string;
29
+ ownerAddress: string;
30
+ allowanceId: string;
31
+ packageId: string;
32
+ coinType: string;
33
+ /** Allowance recipient = the resource's payTo. */
34
+ recipient: string;
35
+ gatewayUrl: string;
36
+ network: string;
37
+ maxPerPayment: string;
38
+ label?: string;
39
+ createdAt: string;
40
+ version: 1;
41
+ }
42
+ /** Public-only view. Never includes the seed. */
43
+ interface LocalSigningKeyIdentity {
44
+ delegateAddress: string;
45
+ publicKeyHex: string;
46
+ }
47
+ type SignerSource = 'secret-key' | 'credentials' | 'mnemonic';
48
+ interface PaymentProfile {
49
+ creds: SuiPayCredentials;
50
+ wallet: Wallet;
51
+ source: SignerSource;
52
+ }
53
+ /**
54
+ * Load a historical V1 allowance payment profile, if present.
55
+ * ADR-0010: not for new settlement. Callers that pay must refuse this profile
56
+ * and point operators at shared_pool OAuth / delegated-signer flows.
57
+ */
58
+ declare function loadPaymentProfile(): PaymentProfile | null;
59
+ /**
60
+ * First authorization generates a local keypair and persists it at 0600.
61
+ * Re-authorization reuses the stored key so a second key cannot orphan the grant.
62
+ * Returns only the public identity.
63
+ */
64
+ declare function ensureLocalSigningKey(opts?: {
65
+ label?: string;
66
+ }): Promise<LocalSigningKeyIdentity>;
67
+ /**
68
+ * The persisted agent-held delegate signer, or null when this process holds no
69
+ * key. Reads only — it never mints.
70
+ *
71
+ * `ensureLocalSigningKey` is the provisioning entry point and mints on first
72
+ * authorization, which is right there and wrong here: `pay` runs on hosts that
73
+ * are not the agent (the gateway runs the same tool code), and minting on a read
74
+ * path would write key material onto a machine that must hold none, then quietly
75
+ * fail to match any grant. Absent key → no settlement, by construction.
76
+ */
77
+ declare function loadLocalSigner(): {
78
+ address: string;
79
+ signTransaction: (txBytes: string) => Promise<string>;
80
+ signPersonalMessage: (message: Uint8Array) => Promise<string>;
81
+ } | null;
82
+
83
+ /**
84
+ * Shared pay-request helpers.
85
+ *
86
+ * The V1 allowance sponsored payer (ADR-0010) is retired; only the
87
+ * rail-neutral request surface the SpendAccount payer depends on survives here:
88
+ * the `PayResult` shape every payer returns and `paidRequestInit`, which builds
89
+ * the one RequestInit replayed byte for byte on the unpaid and the paid call.
90
+ */
91
+
92
+ type PayResult = {
93
+ status: 'ok' | 'failed' | 'ambiguous';
94
+ /**
95
+ * `true` — settlement confirmed. `false` — money did not leave the
96
+ * account. `'unknown'` — execute/finality did not resolve; a retry can
97
+ * pay twice.
98
+ */
99
+ paid: boolean | 'unknown';
100
+ body?: unknown;
101
+ receipt?: unknown;
102
+ code?: string;
103
+ detail?: string;
104
+ txDigest?: string;
105
+ };
106
+
107
+ /**
108
+ * SpendAccount PREPARE-only client (non-custodial MCP).
109
+ *
110
+ * The remote MCP `pay` tool runs inside the gateway, which holds NO key that can
111
+ * spend. So this module does not sign or submit: it PREPARES a payment and hands
112
+ * back an unsigned `spend_account::settle_policy_payment` transaction plus everything the client
113
+ * needs. The client — which holds the delegate key — sponsors, signs locally,
114
+ * and submits through the same personal-message challenge → local-sign → submit flow the
115
+ * REST/SDK delegate path uses. Only signatures ever reach the gateway; the
116
+ * private key is never placed in an HTTP body, a bearer, or any header.
117
+ *
118
+ * Flow: 402 Offer → request-binding → package/network SSOT →
119
+ * resolveGrantTarget(snapshot, offer.targetHash) (pause/revoke gate) → cap/coin
120
+ * checks → assertLive (fresh authority) → build unsigned kind bytes → return the
121
+ * signing request. There is deliberately no sponsor call, no decrypt, no
122
+ * signature, and no forwarded bearer here.
123
+ *
124
+ * See docs/design/mcp-non-custodial-signing.md and ADR-0013 §3.
125
+ */
126
+
127
+ /**
128
+ * Read/prepare-only session for the non-custodial MCP pay tool. It carries the
129
+ * immutable grant snapshot and an optional prepare-time live gate — and no key,
130
+ * by construction. There is no `decryptDelegateSeed` and no `sponsorAuthorization`.
131
+ */
132
+ interface SpendAccountPrepareSession {
133
+ snapshot: GrantSnapshot;
134
+ /** SpendAccount package id (V2). */
135
+ packageId: string;
136
+ /**
137
+ * Re-assert the grant is still live (not paused/revoked/expired) against fresh
138
+ * canonical state, invoked immediately before a signing request is handed back.
139
+ * Throw to abort. Optional; when absent, only the request-time snapshot gates
140
+ * the prepare (on-chain `spend_account::settle_policy_payment` remains the final backstop for any
141
+ * spend the client later submits).
142
+ */
143
+ assertLive?: () => Promise<void>;
144
+ }
145
+ interface SpendAccountPayDeps {
146
+ fetchImpl?: typeof fetch;
147
+ /** Build only-transaction-kind bytes for spend_account::settle_policy_payment. */
148
+ buildPayKind?: (input: {
149
+ delegate: string;
150
+ coinType: string;
151
+ poolObjectId: string;
152
+ grantObjectId: string;
153
+ policyId: number | string;
154
+ targetHash: string;
155
+ amount: string;
156
+ paymentIdHash: Uint8Array;
157
+ termsHash: Uint8Array;
158
+ recipient?: string;
159
+ accessMode?: 'scoped' | 'open';
160
+ }) => Promise<{
161
+ kindBytes: string;
162
+ }>;
163
+ /**
164
+ * Live trace for this prepare. Purely observational: emission records what the
165
+ * canonical read decisions already returned — an event is written after a check
166
+ * passes and can never be the thing that lets the next step run.
167
+ */
168
+ trace?: SpendAccountPayTrace;
169
+ }
170
+ interface SpendAccountPayTrace {
171
+ sink: TraceSink;
172
+ traceId: string;
173
+ requestId: string | null;
174
+ buyerAccountId: string;
175
+ connectionId: string;
176
+ /** HMAC secret for signed cross-request context. Absent disables signing. */
177
+ secret?: string | null;
178
+ /** Injected clock, for tests. */
179
+ now?: () => number;
180
+ }
181
+ /** The unsigned payment the client signs locally and submits. */
182
+ interface SpendAccountSigningRequest {
183
+ /**
184
+ * Unsigned `spend_account::settle_policy_payment` transaction-kind bytes (base64), sender set to
185
+ * `delegateAddress`. The client sponsors these for gas, signs with the delegate
186
+ * key, and submits — the gateway never receives the key.
187
+ */
188
+ unsignedKindBytes: string;
189
+ delegateAddress: string;
190
+ packageId: string;
191
+ poolObjectId: string;
192
+ grantObjectId: string;
193
+ /** On-chain policy id the pay is bound to. */
194
+ policyId: number | string;
195
+ coinType: string;
196
+ targetHash: string;
197
+ amount: string;
198
+ /** Hex (no 0x) of the payment-id hash bound into the transaction. */
199
+ paymentIdHashHex: string;
200
+ /** Hex (no 0x) of the terms hash bound into the transaction. */
201
+ termsHashHex: string;
202
+ challengeId: string;
203
+ /** The exact paid request to replay after the client submits. */
204
+ resource: {
205
+ url: string;
206
+ method: string;
207
+ };
208
+ dialect: Dialect;
209
+ /** Recipient the agent requested (offer.payTo). */
210
+ recipient: string;
211
+ /** Where the client (holding the key) sponsors and submits. */
212
+ submit: {
213
+ sponsorUrl: string;
214
+ executeUrl: string;
215
+ moveCallTarget: string;
216
+ };
217
+ /**
218
+ * Non-secret wallet/console handoff for hosts that cannot run a local signer:
219
+ * the delegate key signs and submits out of band there. Parameterized only by
220
+ * public Connection-manifest fields — never any secret.
221
+ */
222
+ handoffUrl: string;
223
+ /** Restated hard rule for the agent host. */
224
+ note: string;
225
+ }
226
+ interface PreparedPayment {
227
+ status: 'prepared';
228
+ paid: false;
229
+ signingRequest: SpendAccountSigningRequest;
230
+ }
231
+ /** Prepared signing request, or a canonical failure (never a spend). */
232
+ type SpendAccountPrepareResult = PreparedPayment | PayResult;
233
+ /** Default kind builder (no chain client — pure PTB). */
234
+ declare function buildSpendAccountPayKind(input: {
235
+ packageId: string;
236
+ delegate: string;
237
+ coinType: string;
238
+ poolObjectId: string;
239
+ grantObjectId: string;
240
+ policyId: number | string;
241
+ targetHash: string;
242
+ amount: string;
243
+ paymentIdHash: Uint8Array;
244
+ termsHash: Uint8Array;
245
+ }): Promise<{
246
+ kindBytes: string;
247
+ }>;
248
+ /**
249
+ * Refuse blind signing: a client validating a sponsored transaction must be able
250
+ * to prove the sponsored bytes reconstruct the intended spend_account::settle_policy_payment. Kept
251
+ * here (and exported) so both the client signer and its tests share one check.
252
+ */
253
+ declare function verifySpendAccountPayReconstructs(bytes: string, expected: {
254
+ kindBytes: string;
255
+ packageId: string;
256
+ delegate: string;
257
+ poolObjectId: string;
258
+ grantObjectId: string;
259
+ amount: string;
260
+ }): void;
261
+ /**
262
+ * Prepare a shared_pool payment WITHOUT signing. Reads the 402 offer, binds it,
263
+ * gates on the immutable grant snapshot + a fresh authority check, builds the
264
+ * unsigned transaction, and returns a signing request. The client holding the
265
+ * delegate key signs locally and submits. This function never decrypts, signs,
266
+ * sponsors, or submits.
267
+ */
268
+ declare function prepareSpendAccountPayment(args: {
269
+ url: string;
270
+ /** The one normalized paid request, replayed byte for byte after signing. */
271
+ request?: PaidHttpRequest;
272
+ session: SpendAccountPrepareSession;
273
+ cfg: McpConfig;
274
+ deps?: SpendAccountPayDeps;
275
+ /** Preferred dialect when the 402 advertises both (default mpp). */
276
+ preferredDialect?: Dialect;
277
+ }): Promise<SpendAccountPrepareResult>;
278
+
279
+ /**
280
+ * Client-side completion of an MCP prepared payment.
281
+ *
282
+ * Verifies the server-prepared kind (ADR-0016 §3) then settles through
283
+ * `payDelegatedSpendAccount` — the same path as direct signer mode. This module
284
+ * does not implement a second submitter.
285
+ *
286
+ * Two hops read a 402, and only one of them was verified. The prepare hop's kind
287
+ * is checked here; the payer then reads its own offer and builds the bytes it
288
+ * signs from that. So the verified terms are carried down as `preparedTerms` and
289
+ * re-asserted against the payer's offer before it signs. Without that carry, the
290
+ * two hops can price different recipients and nothing would notice.
291
+ */
292
+
293
+ declare function settlePreparedMcpPayment(args: {
294
+ signingRequest: SpendAccountSigningRequest;
295
+ /**
296
+ * What this payment is supposed to move, sourced from the caller's intent —
297
+ * never re-read from the object being verified. Comparing a prepared kind
298
+ * against fields copied out of the same prepare response proves only that the
299
+ * response agrees with itself.
300
+ */
301
+ expected: {
302
+ amount: string;
303
+ recipient: string;
304
+ delegateAddress: string;
305
+ };
306
+ signer: DelegatedKeySigner;
307
+ getObjectJson: GetObjectJson;
308
+ session: DelegatedPayerSession;
309
+ cfg: DelegatedPayerConfig;
310
+ deps: DelegatedPayerDeps;
311
+ /**
312
+ * Agent-declared spend bound. Required when `deps.offerSecret` is absent
313
+ * (stdio local key): the HMAC is a gateway secret the agent must not need.
314
+ */
315
+ intent?: SpendIntent;
316
+ }): Promise<DelegatedPayResult>;
317
+
318
+ declare function loadBootProfile(load?: typeof loadPaymentProfile): {
319
+ profile: ReturnType<typeof loadPaymentProfile>;
320
+ unreadable: boolean;
321
+ };
322
+ declare function main(): Promise<void>;
323
+
324
+ export { type PreparedPayment, type SpendAccountPrepareResult, type SpendAccountPrepareSession, type SpendAccountSigningRequest, buildSpendAccountPayKind, ensureLocalSigningKey, loadBootProfile, loadLocalSigner, main, prepareSpendAccountPayment, settlePreparedMcpPayment, verifySpendAccountPayReconstructs };
package/dist/index.js ADDED
@@ -0,0 +1,45 @@
1
+ import {
2
+ loadBootProfile,
3
+ main
4
+ } from "./chunk-TPW6S2K4.js";
5
+ import {
6
+ PACKAGE_NAME,
7
+ TOOLS,
8
+ buildSpendAccountPayKind,
9
+ canonicalMcpRequestId,
10
+ createServer,
11
+ ensureLocalSigningKey,
12
+ handleSuipayMcpHttpRequest,
13
+ inspectMcpEnvelope,
14
+ loadLocalSigner,
15
+ loadMcpConfig,
16
+ prepareSpendAccountPayment,
17
+ resetAgentHeldProcessLedger,
18
+ resolveAgentHeldSettlement,
19
+ resolveGrantTarget,
20
+ settlePreparedMcpPayment,
21
+ traceMcpHttpRequest,
22
+ verifySpendAccountPayReconstructs
23
+ } from "./chunk-XZQOOY66.js";
24
+ import "./chunk-A3JIIDYT.js";
25
+ export {
26
+ PACKAGE_NAME,
27
+ TOOLS,
28
+ buildSpendAccountPayKind,
29
+ canonicalMcpRequestId,
30
+ createServer,
31
+ ensureLocalSigningKey,
32
+ handleSuipayMcpHttpRequest,
33
+ inspectMcpEnvelope,
34
+ loadBootProfile,
35
+ loadLocalSigner,
36
+ loadMcpConfig,
37
+ main,
38
+ prepareSpendAccountPayment,
39
+ resetAgentHeldProcessLedger,
40
+ resolveAgentHeldSettlement,
41
+ resolveGrantTarget,
42
+ settlePreparedMcpPayment,
43
+ traceMcpHttpRequest,
44
+ verifySpendAccountPayReconstructs
45
+ };
@@ -0,0 +1,284 @@
1
+ import {
2
+ SPEND_ACCOUNT_DEBIT_FUNCTIONS
3
+ } from "./chunk-A3JIIDYT.js";
4
+
5
+ // ../../src/gateway/sponsor/personal-message.ts
6
+ import { createHash, randomBytes } from "crypto";
7
+ import { bcs } from "@mysten/sui/bcs";
8
+ import {
9
+ isValidPersonalMessageSignature,
10
+ verifyPersonalMessageSignature
11
+ } from "@mysten/sui/verify";
12
+ import {
13
+ fromBase64,
14
+ normalizeSuiAddress,
15
+ toBase64
16
+ } from "@mysten/sui/utils";
17
+ import { Transaction } from "@mysten/sui/transactions";
18
+ var SPONSOR_PERSONAL_MESSAGE_DOMAIN = "suipay:sponsor-personal-message:v1";
19
+ var SPONSOR_PERSONAL_MESSAGE_DEFAULT_TTL_MS = 6e4;
20
+ var SPONSOR_PERSONAL_MESSAGE_DEFAULT_GAS_BUDGET_MIST = 50000000n;
21
+ function encodeSponsorPersonalMessagePayload(payload) {
22
+ if (payload.domain !== SPONSOR_PERSONAL_MESSAGE_DOMAIN) {
23
+ throw new Error(`domain must be ${SPONSOR_PERSONAL_MESSAGE_DOMAIN}`);
24
+ }
25
+ if (payload.txKindHash.length !== 32) {
26
+ throw new Error("txKindHash must be 32 bytes");
27
+ }
28
+ if (!payload.nonce.trim()) throw new Error("nonce is required");
29
+ if (!Number.isFinite(payload.expiryMs) || payload.expiryMs <= 0) {
30
+ throw new Error("expiryMs must be a positive finite number");
31
+ }
32
+ if (!payload.requestId.trim()) throw new Error("requestId is required");
33
+ if (!payload.network.trim()) throw new Error("network is required");
34
+ const parts = [];
35
+ const push = (u) => parts.push(u);
36
+ push(
37
+ bcs.vector(bcs.u8()).serialize([...Buffer.from(payload.domain, "utf8")]).toBytes()
38
+ );
39
+ push(
40
+ bcs.vector(bcs.u8()).serialize([...Buffer.from(payload.nonce, "utf8")]).toBytes()
41
+ );
42
+ push(bcs.u64().serialize(BigInt(payload.expiryMs)).toBytes());
43
+ push(bcs.Address.serialize(normalizeSuiAddress(payload.delegate)).toBytes());
44
+ push(bcs.Address.serialize(normalizeSuiAddress(payload.poolId)).toBytes());
45
+ push(bcs.Address.serialize(normalizeSuiAddress(payload.grantId)).toBytes());
46
+ push(
47
+ bcs.vector(bcs.u8()).serialize([...Buffer.from(payload.network, "utf8")]).toBytes()
48
+ );
49
+ push(bcs.Address.serialize(normalizeSuiAddress(payload.packageId)).toBytes());
50
+ push(
51
+ bcs.vector(bcs.u8()).serialize([...Buffer.from(payload.requestId, "utf8")]).toBytes()
52
+ );
53
+ push(bcs.vector(bcs.u8()).serialize([...payload.txKindHash]).toBytes());
54
+ return Buffer.concat(parts.map((p) => Buffer.from(p)));
55
+ }
56
+ function hashTransactionKindBytes(kindBytes) {
57
+ const raw = typeof kindBytes === "string" ? fromBase64(kindBytes) : kindBytes;
58
+ return createHash("sha256").update(raw).digest();
59
+ }
60
+ function txKindHashHex(kindBytes) {
61
+ return Buffer.from(hashTransactionKindBytes(kindBytes)).toString("hex");
62
+ }
63
+ function buildSponsorPersonalMessagePayload(input) {
64
+ return {
65
+ domain: SPONSOR_PERSONAL_MESSAGE_DOMAIN,
66
+ nonce: input.nonce,
67
+ expiryMs: input.expiryMs,
68
+ delegate: normalizeSuiAddress(input.delegate),
69
+ poolId: normalizeSuiAddress(input.poolId),
70
+ grantId: normalizeSuiAddress(input.grantId),
71
+ network: input.network,
72
+ packageId: normalizeSuiAddress(input.packageId),
73
+ requestId: input.requestId,
74
+ txKindHash: hashTransactionKindBytes(input.transactionKindBytes)
75
+ };
76
+ }
77
+ async function verifySponsorPersonalMessageSignature(input) {
78
+ try {
79
+ const expected = normalizeSuiAddress(input.expectedDelegate);
80
+ const encoded = encodeSponsorPersonalMessagePayload(input.payload);
81
+ const addressMatches = normalizeSuiAddress(input.payload.delegate) === expected;
82
+ if (!addressMatches) {
83
+ return { ok: false, reason: "payload_delegate_mismatch" };
84
+ }
85
+ try {
86
+ const publicKey = await verifyPersonalMessageSignature(
87
+ encoded,
88
+ input.signature,
89
+ { address: expected }
90
+ );
91
+ const derived = publicKey.toSuiAddress();
92
+ if (normalizeSuiAddress(derived) !== expected) {
93
+ return { ok: false, reason: "address_derivation_mismatch" };
94
+ }
95
+ return { ok: true, address: expected };
96
+ } catch {
97
+ const ok = await isValidPersonalMessageSignature(
98
+ encoded,
99
+ input.signature,
100
+ { address: expected }
101
+ );
102
+ if (!ok) return { ok: false, reason: "bad_signature" };
103
+ return { ok: true, address: expected };
104
+ }
105
+ } catch {
106
+ return { ok: false, reason: "bad_signature" };
107
+ }
108
+ }
109
+ function assertPersonalMessageKindIsPermittedPay(input) {
110
+ const allowed = new Set(
111
+ input.allowedFunctions ?? SPEND_ACCOUNT_DEBIT_FUNCTIONS
112
+ );
113
+ let data;
114
+ try {
115
+ data = Transaction.fromKind(
116
+ fromBase64(input.transactionKindBytes)
117
+ ).getData();
118
+ } catch {
119
+ return { ok: false, reason: "kind_invalid" };
120
+ }
121
+ if (JSON.stringify(data.commands).includes('"GasCoin"')) {
122
+ return { ok: false, reason: "kind_gas_coin" };
123
+ }
124
+ const commands = data.commands ?? [];
125
+ if (commands.length !== 1) {
126
+ return { ok: false, reason: "kind_extra_commands" };
127
+ }
128
+ const cmd = commands[0];
129
+ if (cmd.$kind !== "MoveCall") {
130
+ return { ok: false, reason: "kind_not_move_call" };
131
+ }
132
+ const move = cmd.MoveCall;
133
+ if (!move) return { ok: false, reason: "kind_missing_move_call" };
134
+ const pkg = normalizeSuiAddress(String(move.package));
135
+ const expectedPkg = normalizeSuiAddress(input.packageId);
136
+ if (pkg !== expectedPkg) {
137
+ return { ok: false, reason: "kind_package_mismatch" };
138
+ }
139
+ if (String(move.module) !== "spend_account") {
140
+ return { ok: false, reason: "kind_module_not_shared_pool" };
141
+ }
142
+ const fn = String(move.function);
143
+ if (!allowed.has(fn)) {
144
+ return { ok: false, reason: "kind_function_not_permitted" };
145
+ }
146
+ const args = move.arguments ?? [];
147
+ if (args.length < 2) {
148
+ return { ok: false, reason: "kind_args_incomplete" };
149
+ }
150
+ const poolObj = objectIdFromArg(args[0], data.inputs);
151
+ const grantObj = objectIdFromArg(args[1], data.inputs);
152
+ if (!poolObj || !grantObj) {
153
+ return { ok: false, reason: "kind_object_unresolved" };
154
+ }
155
+ if (normalizeSuiAddress(poolObj) !== normalizeSuiAddress(input.poolObjectId)) {
156
+ return { ok: false, reason: "kind_pool_mismatch" };
157
+ }
158
+ if (normalizeSuiAddress(grantObj) !== normalizeSuiAddress(input.grantObjectId)) {
159
+ return { ok: false, reason: "kind_grant_mismatch" };
160
+ }
161
+ return { ok: true, functionName: String(move.function) };
162
+ }
163
+ function assertPersonalMessageDeploymentBinding(input) {
164
+ const wantNet = input.expectedNetwork.trim().toLowerCase();
165
+ const gotNet = input.network.trim().toLowerCase();
166
+ if (!wantNet || gotNet !== wantNet) {
167
+ return { ok: false, reason: "network_mismatch" };
168
+ }
169
+ try {
170
+ if (normalizeSuiAddress(input.packageId) !== normalizeSuiAddress(input.expectedPackageId)) {
171
+ return { ok: false, reason: "package_mismatch" };
172
+ }
173
+ } catch {
174
+ return { ok: false, reason: "package_mismatch" };
175
+ }
176
+ return { ok: true };
177
+ }
178
+ function publicSponsorGrantDenyReason(reason) {
179
+ const r = reason.toLowerCase();
180
+ if (r.includes("expired")) return "grant_expired";
181
+ if (r.includes("revok")) return "grant_revoked";
182
+ if (r.includes("paus")) return "grant_paused";
183
+ if (r.includes("not active") || r.includes("not_active")) {
184
+ return "grant_not_active";
185
+ }
186
+ if (r.includes("no active") || r.includes("not found") || r.includes("missing")) {
187
+ return "grant_not_found";
188
+ }
189
+ return "grant_not_authorized";
190
+ }
191
+ function objectIdFromArg(arg, inputs) {
192
+ if (!arg || typeof arg !== "object") return null;
193
+ const a = arg;
194
+ if (a.$kind !== "Input" || !Number.isInteger(a.Input)) return null;
195
+ const input = inputs?.[Number(a.Input)];
196
+ if (!input || typeof input !== "object") return null;
197
+ const value = input;
198
+ if (value.$kind !== "Object" || !value.Object) return null;
199
+ const object = value.Object;
200
+ const id = object.$kind === "SharedObject" ? object.SharedObject?.objectId : object.$kind === "ImmOrOwnedObject" ? object.ImmOrOwnedObject?.objectId : object.$kind === "Receiving" ? object.Receiving?.objectId : void 0;
201
+ return typeof id === "string" ? id : null;
202
+ }
203
+ function createResolveSponsorGrant(store) {
204
+ return async (query) => {
205
+ const found = await store.findActiveGrantForPersonalMessage({
206
+ grantObjectId: query.grantObjectId,
207
+ poolObjectId: query.poolObjectId,
208
+ delegateAddress: query.delegateAddress
209
+ });
210
+ if (!found) {
211
+ return { ok: false, reason: "grant_not_found" };
212
+ }
213
+ const expiresAtMs = Number(found.grant.expiresAtMs);
214
+ if (Number.isFinite(expiresAtMs) && expiresAtMs > 0 && expiresAtMs <= query.nowMs) {
215
+ return { ok: false, reason: "grant_expired" };
216
+ }
217
+ if (found.grant.status === "revoked") {
218
+ return { ok: false, reason: "grant_revoked" };
219
+ }
220
+ if (found.grant.status !== "active") {
221
+ return { ok: false, reason: "grant_not_active" };
222
+ }
223
+ if (found.aggregateStatus === "paused" || found.aggregateStatus === "pausing") {
224
+ return { ok: false, reason: "grant_paused" };
225
+ }
226
+ if (found.aggregateStatus === "revoking" || found.aggregateStatus === "partially_revoked" || found.aggregateStatus === "revoked") {
227
+ return { ok: false, reason: "grant_revoked" };
228
+ }
229
+ if (!found.grant.grantObjectId) {
230
+ return { ok: false, reason: "grant_not_found" };
231
+ }
232
+ return {
233
+ ok: true,
234
+ grant: {
235
+ delegateAddress: normalizeSuiAddress(found.agentAddress),
236
+ poolObjectId: normalizeSuiAddress(found.grant.poolObjectId),
237
+ grantObjectId: normalizeSuiAddress(found.grant.grantObjectId),
238
+ expiresAtMs: Number.isFinite(expiresAtMs) ? expiresAtMs : 0,
239
+ status: "active"
240
+ }
241
+ };
242
+ };
243
+ }
244
+ function mintSponsorChallengeNonce() {
245
+ return randomBytes(24).toString("base64url");
246
+ }
247
+ function mintSponsorExecuteToken() {
248
+ return randomBytes(24).toString("base64url");
249
+ }
250
+ async function signSponsorPersonalMessagePayload(input) {
251
+ const encoded = encodeSponsorPersonalMessagePayload(input.payload);
252
+ return input.signPersonalMessage(encoded);
253
+ }
254
+ function bytesToHex(bytes) {
255
+ return Buffer.from(bytes).toString("hex");
256
+ }
257
+ function hexToBytes32(hex) {
258
+ const h = hex.replace(/^0x/i, "").toLowerCase();
259
+ if (!/^[0-9a-f]{64}$/.test(h)) {
260
+ throw new Error(`expected 32-byte hex, got ${hex}`);
261
+ }
262
+ return Buffer.from(h, "hex");
263
+ }
264
+ export {
265
+ SPONSOR_PERSONAL_MESSAGE_DEFAULT_GAS_BUDGET_MIST,
266
+ SPONSOR_PERSONAL_MESSAGE_DEFAULT_TTL_MS,
267
+ SPONSOR_PERSONAL_MESSAGE_DOMAIN,
268
+ assertPersonalMessageDeploymentBinding,
269
+ assertPersonalMessageKindIsPermittedPay,
270
+ buildSponsorPersonalMessagePayload,
271
+ bytesToHex,
272
+ createResolveSponsorGrant,
273
+ encodeSponsorPersonalMessagePayload,
274
+ fromBase64,
275
+ hashTransactionKindBytes,
276
+ hexToBytes32,
277
+ mintSponsorChallengeNonce,
278
+ mintSponsorExecuteToken,
279
+ publicSponsorGrantDenyReason,
280
+ signSponsorPersonalMessagePayload,
281
+ toBase64,
282
+ txKindHashHex,
283
+ verifySponsorPersonalMessageSignature
284
+ };