@cmdoss/suipay-mcp 0.2.2 → 0.3.0-dev.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.ts CHANGED
@@ -1,19 +1,9 @@
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';
1
+ import { S as SpendAccountSigningRequest, P as PaidHttpRequest, D as DelegatedKeySigner, G as GetObjectJson, a as DelegatedPayerSession, b as DelegatedPayerConfig, c as DelegatedPayerDeps, d as SpendIntent, e as DelegatedPayResult, f as DelegationScope, C as CodeFor, N as NextAction, g as GrantPolicyBinding, h as PaymentAttemptLedger, i as DelegatedPayerLog, j as PreparedSettlementTerms, X as X402PayProgress, O as Offer, M as McpConfig, k as McpAuthContext, l as McpTraceHooks, m as SpendAccountPayDeps, n as createReader } from './http-BCFfQFIg.js';
2
+ export { A as AgentHeldResolver, o as AgentHeldSettlement, p as MCP_TARGET_PRESETS, q as McpEnvelope, r as McpEnvelopeEntry, s as McpServerOptions, t as McpTarget, u as McpTraceContext, v as McpTraceRuntime, w as PACKAGE_NAME, x as PreparedPayment, y as SpendAccountPrepareResult, z as SpendAccountPrepareSession, T as TOOLS, B as buildSpendAccountPayKind, E as canonicalMcpRequestId, F as createServer, H as handleSuipayMcpHttpRequest, I as inspectMcpEnvelope, J as loadMcpConfig, K as parseMcpTargetFlag, L as prepareSpendAccountPayment, Q as resetAgentHeldProcessLedger, R as resolveAgentHeldSettlement, U as resolveGrantTarget, V as resolveMcpTarget, W as sameMcpGateway, Y as traceMcpHttpRequest, Z as verifySpendAccountPayReconstructs } from './http-BCFfQFIg.js';
3
+ import { SuiGrpcClient } from '@mysten/sui/grpc';
4
+ import { Ed25519Keypair } from '@mysten/sui/keypairs/ed25519';
5
+ import { CallToolResult } from '@modelcontextprotocol/sdk/types.js';
3
6
  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
7
 
18
8
  /** Holds a key and signs transaction bytes. Never leaves the payer. */
19
9
  interface Wallet {
@@ -53,14 +43,9 @@ interface PaymentProfile {
53
43
  /**
54
44
  * Load a historical V1 allowance payment profile, if present.
55
45
  * ADR-0010: not for new settlement. Callers that pay must refuse this profile
56
- * and point operators at shared_pool OAuth / delegated-signer flows.
46
+ * and point operators at delegate OAuth / delegated-signer flows.
57
47
  */
58
48
  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
49
  declare function ensureLocalSigningKey(opts?: {
65
50
  label?: string;
66
51
  }): Promise<LocalSigningKeyIdentity>;
@@ -81,239 +66,519 @@ declare function loadLocalSigner(): {
81
66
  } | null;
82
67
 
83
68
  /**
84
- * Shared pay-request helpers.
69
+ * Client-side completion of an MCP prepared payment.
85
70
  *
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.
71
+ * Verifies the server-prepared kind (ADR-0016 §3) then settles through
72
+ * `payDelegatedSpendAccount` — the same path as direct signer mode. This module
73
+ * does not implement a second submitter.
74
+ *
75
+ * Two hops read a 402, and only one of them was verified. The prepare hop's kind
76
+ * is checked here; the payer then reads its own offer and builds the bytes it
77
+ * signs from that. So the verified terms are carried down as `preparedTerms` and
78
+ * re-asserted against the payer's offer before it signs. Without that carry, the
79
+ * two hops can price different recipients and nothing would notice.
90
80
  */
91
81
 
92
- type PayResult = {
93
- status: 'ok' | 'failed' | 'ambiguous';
82
+ declare function settlePreparedMcpPayment(args: {
83
+ signingRequest: SpendAccountSigningRequest;
94
84
  /**
95
- * `true` — settlement confirmed. `false` — money did not leave the
96
- * account. `'unknown'` — execute/finality did not resolve; a retry can
97
- * pay twice.
85
+ * The same paid request that minted the prepare hop. Reconstructing from
86
+ * url+method alone drops a POST body and the settlement hop then prices
87
+ * (or 400s) a different request than the one the tool was called with.
98
88
  */
99
- paid: boolean | 'unknown';
100
- body?: unknown;
101
- receipt?: unknown;
102
- code?: string;
103
- detail?: string;
104
- txDigest?: string;
105
- };
89
+ request: PaidHttpRequest;
90
+ /**
91
+ * What this payment is supposed to move, sourced from the caller's intent —
92
+ * never re-read from the object being verified. Comparing a prepared kind
93
+ * against fields copied out of the same prepare response proves only that the
94
+ * response agrees with itself.
95
+ */
96
+ expected: {
97
+ amount: string;
98
+ recipient: string;
99
+ delegateAddress: string;
100
+ };
101
+ signer: DelegatedKeySigner;
102
+ getObjectJson: GetObjectJson;
103
+ session: DelegatedPayerSession;
104
+ cfg: DelegatedPayerConfig;
105
+ deps: DelegatedPayerDeps;
106
+ /**
107
+ * Agent-declared spend bound. Required when `deps.offerSecret` is absent
108
+ * (stdio local key): the HMAC is a gateway secret the agent must not need.
109
+ */
110
+ intent?: SpendIntent;
111
+ }): Promise<DelegatedPayResult>;
106
112
 
107
113
  /**
108
- * SpendAccount PREPARE-only client (non-custodial MCP).
114
+ * The executable settlement a 402 carries so the agent needs no chain access.
109
115
  *
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.
116
+ * ADR-0016 §2 says "the server prepares, the agent verifies, the agent signs".
117
+ * ADR-0015 shipped only the first half: the prepare path emitted a placeholder
118
+ * `poolInitialSharedVersion: '1'`, so its bytes described a payment instead of
119
+ * executing one, and an agent still had to make two full-node reads to build
120
+ * something submittable. These bytes are executable — the gateway resolves the
121
+ * real shared-object initial versions before it builds them.
117
122
  *
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
+ * This travels ALONGSIDE the offer on the 402 body, never inside it, exactly as
124
+ * `requestSchema` does (`src/gateway/app.ts`). The offer digest is a
125
+ * consensus-relevant wire shape whose fields go on chain via `terms_hash`;
126
+ * adding to it is a protocol change. A prepared settlement authorizes nothing
127
+ * on its own — it is unsigned bytes, worthless until the delegate's key signs
128
+ * them — so it has no business inside a signed digest.
123
129
  *
124
- * See docs/design/mcp-non-custodial-signing.md and ADR-0013 §3.
130
+ * Every field here exists to let the agent REBUILD the same transaction kind
131
+ * offline and compare it byte for byte. Nothing here is trusted: see
132
+ * `verifyPreparedPayment` in `src/agent-sdk/verify-prepared-settlement.ts`
133
+ * for what an agent must check before it signs, and why what it can check from
134
+ * the 402 alone is sufficient.
125
135
  */
126
136
 
137
+ /** Current prepared-settlement wire version. Bumped on any shape change. */
138
+ declare const PREPARED_SETTLEMENT_VERSION: 1;
127
139
  /**
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`.
140
+ * An unsigned, executable settlement transaction plus everything needed to
141
+ * reconstruct it offline.
142
+ *
143
+ * `transactionBytes` is the full sponsored `TransactionData` — gas is already
144
+ * attached by the gateway's sponsor, so this is the ONE thing the agent signs.
145
+ * `transactionKindBytes` is the pre-sponsor programmable-transaction kind: the
146
+ * Move call and nothing else, which is what the agent rebuilds and compares.
131
147
  */
132
- interface SpendAccountPrepareSession {
133
- snapshot: GrantSnapshot;
134
- /** SpendAccount package id (V2). */
135
- packageId: string;
148
+ interface PreparedSettlement {
149
+ version: typeof PREPARED_SETTLEMENT_VERSION;
150
+ /** Base64 sponsored `TransactionData`. The agent signs exactly these bytes. */
151
+ transactionBytes: string;
152
+ /** Base64 pre-sponsor transaction kind (the Move call baseline). */
153
+ transactionKindBytes: string;
136
154
  /**
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).
155
+ * The address that must sign. Move asserts `ctx.sender() == grant.delegate`,
156
+ * so an agent that checks this equals its own address has the chain enforce
157
+ * that the named grant is one it may draw on — no `getObject` required.
142
158
  */
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 {
159
+ sender: string;
160
+ /** Deployed `suipay` package the call targets (normalized). */
161
+ packageId: string;
162
+ /** Fully-qualified `package::module::function` the call invokes. */
163
+ moveTarget: string;
183
164
  /**
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.
165
+ * Delegation scope: `open` pays an arbitrary recipient, `scoped` an
166
+ * allowlist. This is the SCOPE discriminator (which pay function) — distinct
167
+ * from the x402 eligibility (`SpendAccountGrantKind`) and the budget cap. The
168
+ * `'open'`/`'scoped'` value strings are a frozen wire shape (parsed by
169
+ * `asPreparedSettlement`); the type only names the concept.
187
170
  */
188
- unsignedKindBytes: string;
189
- delegateAddress: string;
190
- packageId: string;
171
+ accessMode: DelegationScope;
172
+ /** Shared `Account` the payment debits. */
191
173
  poolObjectId: string;
174
+ /** Shared `Grant` naming the delegate. */
192
175
  grantObjectId: string;
193
- /** On-chain policy id the pay is bound to. */
194
- policyId: number | string;
176
+ /** Real initial shared versions — the gap ADR-0015's placeholder `'1'` left. */
177
+ poolInitialSharedVersion: string;
178
+ grantInitialSharedVersion: string;
179
+ clockInitialSharedVersion: string;
180
+ /** On-chain policy id; `'0'` for an open grant. Decimal string (u64). */
181
+ policyId: string;
182
+ /** Coin type the call spends. Must equal the offer's asset. */
195
183
  coinType: string;
196
- targetHash: string;
184
+ /** Atomic amount the call moves. Must equal the offer's amount. */
197
185
  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). */
186
+ /** Recipient the call pays. Must equal the offer's payTo. */
210
187
  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
188
  /**
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.
189
+ * Fresh lineage (ADR-0043 Option B): settlement is MANDATORY-guarded, so the
190
+ * prepared call always consumes the offer's single-use exclusion marker before
191
+ * funds move. These four let the agent rebuild the SAME guarded kind offline
192
+ * and compare it byte for byte. The `ExclusionDirectory` the offer committed
193
+ * to (from its `registryIdentity`), read-only.
221
194
  */
222
- handoffUrl: string;
223
- /** Restated hard rule for the agent host. */
224
- note: string;
195
+ exclusionDirectoryId: string;
196
+ /** The per-recipient `RecipientRegistry` the marker is claimed in (mutable). */
197
+ recipientRegistryId: string;
198
+ /** Real initial shared version of the exclusion directory. */
199
+ exclusionDirectoryInitialSharedVersion: string;
200
+ /** Real initial shared version of the recipient registry. */
201
+ recipientRegistryInitialSharedVersion: string;
202
+ /** Signed expiry (epoch-ms, decimal string) bound into the guarded settle. */
203
+ expiresAtMs: string;
204
+ /** Absolute URL to POST `{challengeId, signature, body}` to. */
205
+ payUrl: string;
225
206
  }
226
- interface PreparedPayment {
227
- status: 'prepared';
228
- paid: false;
229
- signingRequest: SpendAccountSigningRequest;
207
+
208
+ /**
209
+ * Connection-manifest → SDK session bridge.
210
+ *
211
+ * The dashboard hands an agent a non-secret "Connection manifest" after it
212
+ * authorizes a delegate key. Historically that manifest could NOT be passed to
213
+ * the SDK: it named the signer `agentAddress` (not `delegateAddress`), the asset
214
+ * by symbol (`"USDC"`, not the full `coinType`), and its policies by off-chain
215
+ * UUID — while the payer needs the full coin type and each policy's NUMERIC
216
+ * on-chain id + target hashes (`GrantPolicyBinding`). This module closes that
217
+ * mismatch two ways:
218
+ *
219
+ * 1. {@link AgentConnectionManifest} — the SDK-consumable shape a corrected
220
+ * console emits directly (delegate address, full coin type, numeric policy
221
+ * ids + target hashes). {@link manifestToSession} turns it, plus an agent
222
+ * key, into a ready `DelegatedPayerSession` — no protocol reverse-engineering
223
+ * and no on-chain lookups.
224
+ *
225
+ * 2. {@link dashboardManifestToAgentManifest} — a bridge from the legacy
226
+ * console shape, resolving asset→coinType and (for scoped grants) UUID→
227
+ * numeric policy binding through injected resolvers.
228
+ *
229
+ * Every failure is a typed, caller-safe error (never a thrown internal): a
230
+ * malformed or legacy manifest that cannot be resolved fails loudly, not
231
+ * silently.
232
+ */
233
+
234
+ /**
235
+ * A manifest or session that cannot be paid from. Nothing was signed, so the
236
+ * money state is always `not_charged`. Branch on `code`; `reason` names the
237
+ * internal check for logs.
238
+ */
239
+ interface ManifestError {
240
+ code: CodeFor<'sdk'>;
241
+ detail: string;
242
+ paymentState: 'not_charged';
243
+ retryable: boolean;
244
+ nextAction: NextAction;
230
245
  }
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;
246
+ type ManifestResult<T> = {
247
+ ok: true;
248
+ value: T;
249
+ } | {
250
+ ok: false;
251
+ error: ManifestError;
252
+ };
253
+ /**
254
+ * A manifest policy binding. `onChainPolicyId` is always a validated u64 DECIMAL
255
+ * STRING — never a JS number — so `BigInt(onChainPolicyId)` on the money path
256
+ * (`spend_account::settle_policy`) is exact. A `u64` policy id can exceed `2^53`, past which
257
+ * a JS number loses precision and would bind the WRONG on-chain policy; carrying
258
+ * the string preserves every id exactly.
259
+ */
260
+ type AgentPolicyBinding = Omit<GrantPolicyBinding, 'onChainPolicyId'> & {
261
+ onChainPolicyId: string;
262
+ };
263
+ /** One grant's SDK-consumable binding: everything the payer needs to settle. */
264
+ interface AgentSessionBinding {
265
+ /** Display asset symbol (informational; `coinType` is authoritative). */
266
+ asset: string;
267
+ /** Full coin struct type, e.g. `0x..::usdc::USDC`. Required for pay. */
237
268
  coinType: string;
238
269
  poolObjectId: string;
239
270
  grantObjectId: string;
240
- policyId: number | string;
241
- targetHash: string;
242
- amount: string;
243
- paymentIdHash: Uint8Array;
244
- termsHash: Uint8Array;
245
- }): Promise<{
246
- kindBytes: string;
247
- }>;
271
+ /** `open` = capped, no allowlist; `scoped` = policy-bound. */
272
+ accessMode: 'scoped' | 'open';
273
+ /** Grant-level session cap (atomic); null/undefined = unlimited. */
274
+ sessionCap?: string | null;
275
+ /** Grant-level per-payment ceiling (atomic); null/undefined = unlimited. */
276
+ maxPerPayment?: string | null;
277
+ /** On-chain policy ids (u64 strings) + target hashes. Empty for open grants. */
278
+ policies: AgentPolicyBinding[];
279
+ }
280
+ /** Which grant to drive when a manifest carries several. */
281
+ interface SessionSelector {
282
+ grantObjectId?: string;
283
+ asset?: string;
284
+ coinType?: string;
285
+ }
286
+ /** @deprecated Renamed to {@link AgentSessionBinding}. */
287
+ type AgentGrantBinding = AgentSessionBinding;
288
+ /** @deprecated Renamed to {@link SessionSelector}. */
289
+ type GrantSelector = SessionSelector;
290
+
248
291
  /**
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.
292
+ * Key + gateway URL → a ready delegated-pay session.
293
+ *
294
+ * This module is the point of `docs/decisions/ADR-0016-delegate-key-sole-agent-input.md`
295
+ * §1. Before it, an agent needed a connection manifest — network, package id,
296
+ * pool id, grant id, caps, expiry — copied out of the console at authorize time
297
+ * and liable to go stale the moment the owner tightened a cap or the operator
298
+ * rotated a package. After it, the agent is handed one secret and one URL, and
299
+ * everything else is resolved from the chain's own projection at startup.
300
+ *
301
+ * The order is forced by the protocol. A bootstrap signature binds `network` and
302
+ * `packageId`, which the gateway reconstructs from its own config, so the agent
303
+ * must know them before it can sign anything that will verify. It learns them
304
+ * from the gateway's public deployment constants — the same two values every 402
305
+ * offer already carries in the clear — and only then signs.
306
+ *
307
+ * Nothing here spends. It resolves authority and hands back a session; the
308
+ * payment is still built and signed later, on this machine, by
309
+ * `payDelegatedSpendAccount` with a key that never leaves the process.
252
310
  */
253
- declare function verifySpendAccountPayReconstructs(bytes: string, expected: {
254
- kindBytes: string;
311
+
312
+ /** The gateway's public deployment constants. */
313
+ interface GatewayDeployment {
314
+ network: string;
255
315
  packageId: string;
256
- delegate: string;
257
- poolObjectId: string;
258
- grantObjectId: string;
259
- amount: string;
260
- }): void;
316
+ /**
317
+ * The canonical `ExclusionDirectory` object id, present only while the gateway
318
+ * mints guarded offers. Absent means every offer is unguarded and no registry
319
+ * ever needs resolving.
320
+ *
321
+ * It enables the resolver; it is never resolved against. A guarded settle
322
+ * resolves the directory the OFFER committed to under the HMAC, so a gateway
323
+ * cannot point a payer at a directory of its choosing by answering here.
324
+ */
325
+ exclusionDirectoryObjectId?: string;
326
+ }
327
+
261
328
  /**
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.
329
+ * A delegate key and a gateway URL, and the agent can pay.
330
+ *
331
+ * `resolvePayerSession` already turned a key into a pay *session*
332
+ * (ADR-0016 §1). What it did not do was assemble the rest of what
333
+ * `payDelegatedSpendAccount` demands, so a real agent still had to supply four
334
+ * things its key does not carry: the gateway's own HMAC secret, a fullnode
335
+ * connection to confirm settlement, a second fullnode read for live
336
+ * shared-object versions, and a personal-message signer for the sponsor's
337
+ * personal-message challenge, separate from the transaction signer. This module is
338
+ * where those four go.
339
+ *
340
+ * Where each one went, and why that is safe:
341
+ *
342
+ * - **The gateway's HMAC secret is gone, not relocated.** It authenticated the
343
+ * offer's issuer to the agent — but the issuer is the same gateway the agent
344
+ * is paying, reached over TLS at an origin the payer pins, so a gateway that
345
+ * wanted to overcharge never needed to forge its own offer. What replaces it
346
+ * is a bound the gateway cannot see: the agent's own {@link SpendIntent}.
347
+ * - **Both fullnode reads stay with the AGENT**, against a public Sui fullnode
348
+ * derived from the network the gateway reports. They are not proxied through
349
+ * the gateway, because their whole value is that they are independent of it:
350
+ * an agent that asked the gateway whether the gateway's transaction settled
351
+ * would be verifying nothing. The gateway supplies no input here it could
352
+ * not already supply; what it never gains is the ability to answer for the
353
+ * chain.
354
+ * - **The personal-message challenge signer is the same key**, taken from the one
355
+ * keypair the agent already holds. It was never a second credential, only a
356
+ * second callback the caller had to wire.
357
+ *
358
+ * The key never leaves this process, and the gateway never gains the ability to
359
+ * spend: every payment is still built here, checked here against the chain's
360
+ * own view of the grant, and signed here.
267
361
  */
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
362
 
363
+ interface PayerOptions {
364
+ /** Gateway origin, e.g. `https://gateway.suipay.dev`. */
365
+ gatewayUrl: string;
366
+ /**
367
+ * The delegate key. Used for the bootstrap signature, the sponsor
368
+ * personal-message challenge, and the payment signature — one key, three uses, none
369
+ * of which leave this process.
370
+ */
371
+ keypair: Ed25519Keypair;
372
+ /** Which grant to drive when the delegate holds several. */
373
+ select?: GrantSelector;
374
+ /** Injectable fetch (tests / custom transports). Defaults to global `fetch`. */
375
+ fetchImpl?: typeof fetch;
376
+ /**
377
+ * Sui fullnode for the agent's own on-chain reads. Defaults to the public
378
+ * fullnode for the network the gateway reports. An operator running a private
379
+ * network sets it; an agent does not need to.
380
+ */
381
+ fullnodeUrl?: string;
382
+ /**
383
+ * A chain transport to read through, for a caller that already has one (or a
384
+ * test). The seam is the CLIENT, not the individual reads: an option to
385
+ * supply `getSettledTx` directly would let a caller point settlement
386
+ * confirmation at something that is not the chain.
387
+ */
388
+ client?: SuiGrpcClient;
389
+ /**
390
+ * Skip deployment discovery by supplying the constants directly. Same escape
391
+ * hatch `resolvePayerSession` offers.
392
+ */
393
+ deployment?: GatewayDeployment;
394
+ /**
395
+ * Durable payment-attempt ledger. Defaults to an in-process one, which is
396
+ * exactly-once for the lifetime of THIS process. An agent that must survive a
397
+ * restart mid-payment supplies a durable one.
398
+ */
399
+ paymentLedger?: PaymentAttemptLedger;
400
+ /**
401
+ * The gateway's HMAC secret, when the caller legitimately holds one (an
402
+ * operator running its own gateway). An external agent leaves it unset and
403
+ * relies on its spend intent; see {@link SpendIntent}.
404
+ */
405
+ offerSecret?: string;
406
+ /**
407
+ * The canonical `ExclusionDirectory` object id, so this payer can build a
408
+ * GUARDED delegate settle for a guarded offer out of the box.
409
+ *
410
+ * Optional: `createPayer` otherwise takes it from `GET /v1/delegate/deployment`,
411
+ * which discovery already calls, so an agent holding only a key and a gateway
412
+ * URL needs no out-of-band value to pay a guarded offer. Pass it to pin the
413
+ * behaviour against a gateway that does not publish one.
414
+ *
415
+ * When present from either source, `createPayer` constructs the
416
+ * `resolveExclusionRegistry` dependency bound to this payer's own gRPC reader
417
+ * (the same client that reads the grant and confirms settlement), which
418
+ * resolves the per-recipient `RecipientRegistry` from the directory's
419
+ * dynamic-field binding at settle-build time. When absent from BOTH, a guarded
420
+ * offer FAILS CLOSED (BUILD_FAILED) — the payer never builds an unguarded
421
+ * settle, which would reopen the cross-rail double-settle hole and be rejected
422
+ * by the gateway's self-settled verifier anyway. An unguarded offer never
423
+ * resolves a registry, so it is unaffected either way.
424
+ */
425
+ exclusionDirectoryObjectId?: string;
426
+ /** Server-side log port for internal failure detail. Never reaches a result. */
427
+ log?: DelegatedPayerLog;
428
+ }
429
+ /** Result of `payer.pay`. Same shape as the internal settlement result. */
430
+ type PayResult = DelegatedPayResult;
431
+
432
+ /** One paid request. `intent` is what this agent says it is buying. */
433
+ interface PayInput {
434
+ method?: string;
435
+ body?: string | Uint8Array;
436
+ contentType?: string;
437
+ /**
438
+ * Required. Without it nothing bounds what the offer may charge, and the
439
+ * payer refuses — the whole point of the key-only path is that the ceiling
440
+ * comes from the agent, not from a secret it should not hold.
441
+ */
442
+ intent: SpendIntent;
443
+ /**
444
+ * Terms the caller has ALREADY verified, which the offer this payer reads
445
+ * must price exactly or nothing is signed.
446
+ *
447
+ * Distinct from {@link SpendIntent}, and not a replacement for it: `intent`
448
+ * is a ceiling — it refuses an offer that charges MORE — while this is an
449
+ * equality. A caller that showed a person a specific price, or checked a
450
+ * server-prepared kind, is not agreeing to "that or less from anyone in the
451
+ * catalogue"; it is agreeing to one exact payment. The payer fetches its own
452
+ * offer immediately before signing, so without this that final offer is only
453
+ * ceiling-bounded and could legitimately differ from the one the caller
454
+ * verified.
455
+ *
456
+ * Optional, so every existing caller keeps ceiling semantics unchanged.
457
+ */
458
+ preparedTerms?: PreparedSettlementTerms;
459
+ /**
460
+ * Which dialect to pay in. Default `'mpp'`, unchanged: the sponsor executes
461
+ * the pay and the payer presents a finalized proof. `'x402'` signs the pay
462
+ * with the offer's pinned sponsor gas and presents it as `PAYMENT-SIGNATURE`;
463
+ * the gateway co-signs and submits (ADR-0045). Before reserving anything,
464
+ * x402 refuses an open-recipient grant and an offer without the pins.
465
+ */
466
+ dialect?: 'mpp' | 'x402';
467
+ /**
468
+ * x402 only: each step as it happens (built, signed, then presented and
469
+ * answered per presentation). Called synchronously; a throw is logged and
470
+ * never affects the payment.
471
+ */
472
+ onX402Progress?: (event: X402PayProgress) => void;
473
+ }
474
+ interface Payer {
475
+ /** The delegate's own address, derived from the key it holds. */
476
+ address: string;
477
+ /** Deployment constants the gateway published and the bootstrap bound. */
478
+ deployment: GatewayDeployment;
479
+ /** The grant this agent will pay from, as resolved and then verified on chain. */
480
+ grant: AgentGrantBinding;
481
+ /** The assembled session; exposed so a caller can inspect what it will sign with. */
482
+ session: DelegatedPayerSession;
483
+ /** Pay for one resource and return its response. */
484
+ pay(url: string, input: PayInput): Promise<PayResult>;
485
+ /**
486
+ * Verify a 402 prepared settlement against the grant this agent resolved.
487
+ * Always pins {@link Payer.grant} — a settlement drawn on another
488
+ * buyer's grant is refused. Callers with no resolved grant still use
489
+ * {@link verifyPreparedPayment} unpinned.
490
+ */
491
+ verifySettlement(prepared: unknown, offer: Offer): Promise<PreparedSettlement>;
492
+ }
279
493
  /**
280
- * Client-side completion of an MCP prepared payment.
494
+ * Create a payer from its key and a gateway URL.
281
495
  *
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.
496
+ * The call sequence, all of it derivable from those two inputs:
497
+ * 1. `GET /v1/delegate/deployment` — network, package id, and the exclusion
498
+ * directory id when guarded minting is on. All in the clear.
499
+ * 2. `GET /v1/delegate/grants`, signed by the key — the grants naming it.
500
+ * 3. an unsigned `getObject` on the resolved grant, against a public
501
+ * fullnode — the agent's own check that the grant is its own.
285
502
  *
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.
503
+ * Typed failure, never a throw: an agent that cannot verify its own authority
504
+ * must refuse to sign, not proceed on the gateway's word.
291
505
  */
506
+ declare function createPayer(options: PayerOptions): Promise<ManifestResult<Payer>>;
292
507
 
293
- declare function settlePreparedMcpPayment(args: {
294
- signingRequest: SpendAccountSigningRequest;
508
+ interface StdioLoginOptions {
509
+ consoleUrl: string;
510
+ gatewayUrl: string;
511
+ identity: {
512
+ delegateAddress: string;
513
+ publicKeyHex: string;
514
+ };
515
+ label?: string;
516
+ timeoutMs?: number;
517
+ openBrowser?: boolean;
518
+ onUrl?: (connectUrl: string) => void;
519
+ }
520
+ interface StartedStdioLogin {
521
+ connectUrl: string;
522
+ port: number;
523
+ connectState: string;
524
+ done: Promise<void>;
525
+ close: () => void;
526
+ }
527
+ /**
528
+ * Bind the loopback listener and return the console URL immediately.
529
+ * The wallet callback is awaited on `done`, not on the MCP tool call.
530
+ */
531
+ declare function startStdioLogin(opts: StdioLoginOptions): Promise<StartedStdioLogin>;
532
+
533
+ type ToolResult = CallToolResult;
534
+
535
+ interface ToolDeps {
536
+ fetchImpl?: typeof fetch;
537
+ profileLoader?: () => PaymentProfile | null;
538
+ /** Remote HTTP auth (grant + scope + optional shared-pool pay session). */
539
+ auth?: McpAuthContext;
540
+ /** Wire transport. Hosted HTTP never reads a buyer path. */
541
+ transport?: 'stdio' | 'http';
295
542
  /**
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.
543
+ * Proxy-resolved live trace for this MCP request. Observation only: it never
544
+ * changes which rail runs, what is paid, or whether a payment is allowed.
300
545
  */
301
- expected: {
302
- amount: string;
303
- recipient: string;
304
- delegateAddress: string;
546
+ trace?: McpTraceHooks;
547
+ /**
548
+ * Agent-held delegate key. Present only in the process that stores the
549
+ * key — never on the gateway. When set after a server-side prepare, `pay`
550
+ * verifies the prepared kind and settles through `payDelegatedSpendAccount`.
551
+ */
552
+ agentHeld?: {
553
+ signer: DelegatedKeySigner;
554
+ getObjectJson: GetObjectJson;
555
+ payerDeps: DelegatedPayerDeps;
305
556
  };
306
- signer: DelegatedKeySigner;
307
- getObjectJson: GetObjectJson;
308
- session: DelegatedPayerSession;
309
- cfg: DelegatedPayerConfig;
310
- deps: DelegatedPayerDeps;
557
+ /** Override the prepare-time kind builder (tests / offline). */
558
+ buildPayKind?: SpendAccountPayDeps['buildPayKind'];
559
+ /** Override the exclusion-registry resolver (tests / offline). */
560
+ resolveExclusionRegistry?: SpendAccountPayDeps['resolveExclusionRegistry'];
311
561
  /**
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.
562
+ * Stdio login surfaces the console URL (MCP progress notification). The URL
563
+ * carries the delegate address only — never the seed.
314
564
  */
315
- intent?: SpendIntent;
316
- }): Promise<DelegatedPayResult>;
565
+ onUrl?: (url: string) => void;
566
+ createPayer?: typeof createPayer;
567
+ createReader?: typeof createReader;
568
+ startStdioLogin?: typeof startStdioLogin;
569
+ }
570
+ declare function pay(args: {
571
+ url: string;
572
+ method?: unknown;
573
+ json?: unknown;
574
+ file?: unknown;
575
+ fileField?: unknown;
576
+ recipient?: unknown;
577
+ maxAmount?: unknown;
578
+ }, cfg: McpConfig, deps?: ToolDeps): Promise<ToolResult>;
579
+ declare function receipts(args: {
580
+ challengeId?: string;
581
+ }, cfg: McpConfig, deps?: ToolDeps): Promise<ToolResult>;
317
582
 
318
583
  declare function loadBootProfile(load?: typeof loadPaymentProfile): {
319
584
  profile: ReturnType<typeof loadPaymentProfile>;
@@ -321,4 +586,4 @@ declare function loadBootProfile(load?: typeof loadPaymentProfile): {
321
586
  };
322
587
  declare function main(): Promise<void>;
323
588
 
324
- export { type PreparedPayment, type SpendAccountPrepareResult, type SpendAccountPrepareSession, type SpendAccountSigningRequest, buildSpendAccountPayKind, ensureLocalSigningKey, loadBootProfile, loadLocalSigner, main, prepareSpendAccountPayment, settlePreparedMcpPayment, verifySpendAccountPayReconstructs };
589
+ export { McpAuthContext, McpConfig, McpTraceHooks, SpendAccountSigningRequest, ensureLocalSigningKey, loadBootProfile, loadLocalSigner, main, pay, receipts, settlePreparedMcpPayment };