@routedock/routedock 0.1.2 → 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.
Files changed (74) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +227 -4
  3. package/dist/MppSessionHandler-B4TyD7Xg.d.cts +121 -0
  4. package/dist/MppSessionHandler-CLB5SzgN.d.ts +121 -0
  5. package/dist/ProviderRegistry-BJ9oJLVz.d.cts +142 -0
  6. package/dist/ProviderRegistry-C50nyhaF.d.ts +142 -0
  7. package/dist/RouteDockClient-B1roCa8h.d.cts +455 -0
  8. package/dist/RouteDockClient-BbkqjhMG.d.ts +455 -0
  9. package/dist/SessionReconciler-Dscw43F3.d.ts +125 -0
  10. package/dist/SessionReconciler-bi0z2qtN.d.cts +125 -0
  11. package/dist/SessionStore-BpK8vv0L.d.cts +111 -0
  12. package/dist/SessionStore-DvkZv8-E.d.ts +111 -0
  13. package/dist/client.cjs +2419 -202
  14. package/dist/client.d.cts +50 -81
  15. package/dist/client.d.ts +50 -81
  16. package/dist/client.js +2373 -203
  17. package/dist/index.cjs +3080 -298
  18. package/dist/index.d.cts +12 -3
  19. package/dist/index.d.ts +12 -3
  20. package/dist/index.js +3026 -298
  21. package/dist/provider/fastify.cjs +882 -0
  22. package/dist/provider/fastify.d.cts +35 -0
  23. package/dist/provider/fastify.d.ts +35 -0
  24. package/dist/provider/fastify.js +880 -0
  25. package/dist/provider/hono.cjs +948 -0
  26. package/dist/provider/hono.d.cts +99 -0
  27. package/dist/provider/hono.d.ts +99 -0
  28. package/dist/provider/hono.js +938 -0
  29. package/dist/provider.cjs +827 -97
  30. package/dist/provider.d.cts +32 -25
  31. package/dist/provider.d.ts +32 -25
  32. package/dist/provider.js +810 -99
  33. package/dist/react.cjs +2641 -0
  34. package/dist/react.d.cts +107 -0
  35. package/dist/react.d.ts +107 -0
  36. package/dist/react.js +2634 -0
  37. package/dist/routedockMiddleware-BlDsHuPl.d.ts +59 -0
  38. package/dist/routedockMiddleware-D97A5ptQ.d.cts +59 -0
  39. package/dist/schema.cjs +364 -0
  40. package/dist/schema.d.cts +3 -0
  41. package/dist/schema.d.ts +3 -0
  42. package/dist/schema.js +359 -0
  43. package/dist/sign-CUf3rhTx.d.cts +53 -0
  44. package/dist/sign-Dpj2Mp7k.d.ts +53 -0
  45. package/dist/testing.cjs +57 -0
  46. package/dist/testing.d.cts +95 -0
  47. package/dist/testing.d.ts +95 -0
  48. package/dist/testing.js +54 -0
  49. package/dist/types-DmhmfLzP.d.cts +410 -0
  50. package/dist/types-DmhmfLzP.d.ts +410 -0
  51. package/package.json +143 -23
  52. package/PUBLISH_CHECKLIST.md +0 -70
  53. package/dist/SessionStore-CCVO0PHM.d.cts +0 -152
  54. package/dist/SessionStore-CCVO0PHM.d.ts +0 -152
  55. package/dist/routedockMiddleware-B7WTB3LT.d.cts +0 -53
  56. package/dist/routedockMiddleware-DRgdrycG.d.ts +0 -53
  57. package/src/client/ModeRouter.ts +0 -104
  58. package/src/client/MppChargeClient.ts +0 -44
  59. package/src/client/MppSessionClient.ts +0 -144
  60. package/src/client/RouteDockClient.ts +0 -124
  61. package/src/client/index.ts +0 -8
  62. package/src/client/x402Client.ts +0 -84
  63. package/src/index.ts +0 -9
  64. package/src/provider/MppChargeHandler.ts +0 -90
  65. package/src/provider/MppSessionHandler.ts +0 -157
  66. package/src/provider/index.ts +0 -7
  67. package/src/provider/routedockMiddleware.ts +0 -154
  68. package/src/provider/x402Handler.ts +0 -164
  69. package/src/schemas/routedock.schema.json +0 -159
  70. package/src/store/SessionStore.ts +0 -90
  71. package/src/tests/smoke.test.ts +0 -228
  72. package/src/types.ts +0 -165
  73. package/tsconfig.json +0 -14
  74. package/tsup.config.ts +0 -15
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 RouteDock
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,17 +1,17 @@
1
- # @routedock/routedock
1
+ # @routedock/sdk
2
2
 
3
3
  Unified payment execution layer for autonomous agents on Stellar. One SDK for x402, MPP charge, and MPP session — mode selected automatically from the provider's `routedock.json` manifest.
4
4
 
5
5
  ## Install
6
6
 
7
7
  ```bash
8
- npm install @routedock/routedock
8
+ npm install @routedock/sdk
9
9
  ```
10
10
 
11
11
  ## Agent Usage
12
12
 
13
13
  ```ts
14
- import { RouteDockClient } from '@routedock/routedock/client'
14
+ import { RouteDockClient } from '@routedock/sdk/client'
15
15
  import { Keypair } from '@stellar/stellar-sdk'
16
16
 
17
17
  const client = new RouteDockClient({
@@ -35,11 +35,40 @@ for await (const update of session.stream()) {
35
35
  await session.close() // triggers on-chain settlement
36
36
  ```
37
37
 
38
+ ### Durable spend cap
39
+
40
+ The `spendCap` is enforced via a pluggable `SpendStore` (`read()` / `write()`).
41
+ By default the client uses an in-memory store that resets on every process
42
+ restart — and logs a startup warning — so the cap is **not** durable. For
43
+ production, inject a persistent implementation so a crash or restart can't reset
44
+ the accumulator and bypass the cap:
45
+
46
+ ```ts
47
+ import { RouteDockClient, type SpendStore, type DailySpend } from '@routedock/sdk/client'
48
+
49
+ const redisSpendStore: SpendStore = {
50
+ async read(): Promise<DailySpend | null> {
51
+ const raw = await redis.get('routedock:dailySpend')
52
+ return raw ? (JSON.parse(raw) as DailySpend) : null
53
+ },
54
+ async write(state: DailySpend): Promise<void> {
55
+ await redis.set('routedock:dailySpend', JSON.stringify(state))
56
+ },
57
+ }
58
+
59
+ const client = new RouteDockClient({
60
+ wallet: Keypair.fromSecret(process.env.AGENT_SECRET),
61
+ network: 'mainnet',
62
+ spendCap: { daily: '1.00', asset: 'USDC' },
63
+ spendStore: redisSpendStore,
64
+ })
65
+ ```
66
+
38
67
  ## Provider Usage
39
68
 
40
69
  ```ts
41
70
  import express from 'express'
42
- import { routedock } from '@routedock/routedock/provider'
71
+ import { routedock } from '@routedock/sdk/provider'
43
72
 
44
73
  const app = express()
45
74
 
@@ -107,6 +136,175 @@ app.use('/stream', routedock({
107
136
  | `onVoucher(index, cumulativeAmount)` | Each verified ed25519 commitment | No |
108
137
  | `onSettled(txHash, amount, mode)` | Channel close transaction confirmed | Yes |
109
138
 
139
+ ## React Integration
140
+
141
+ > **Warning:** These hooks create a RouteDockClient in the browser, so the wallet secret and any commitmentSecret are readable by anyone who loads the page.
142
+ >
143
+ > Next.js inlines every `NEXT_PUBLIC_*` variable into the client bundle at build time, so never put a Stellar secret in one.
144
+ >
145
+ > Only use these hooks with a dedicated, low-balance testnet key, and spendCap does not protect a key that has leaked.
146
+ >
147
+ > For production or mainnet, keep RouteDockClient on the server (a route handler or server action) and read the key from a non-public variable such as `AGENT_SECRET`, as the Agent Usage section does.
148
+
149
+ `@routedock/sdk/react` provides hooks for client construction, payments, sessions, and live tx log subscription. Wrap your app with `RouteDockProvider`:
150
+
151
+ ```tsx
152
+ import { RouteDockProvider, useRouteDockClient } from '@routedock/sdk/react'
153
+ import { createClient } from '@supabase/supabase-js'
154
+
155
+ // Dedicated throwaway testnet key. Never a funded or mainnet key.
156
+ const THROWAWAY_TESTNET_SECRET = 'S...'
157
+
158
+ function App({ children }: { children: React.ReactNode }) {
159
+ const client = useRouteDockClient({
160
+ wallet: THROWAWAY_TESTNET_SECRET,
161
+ network: 'testnet',
162
+ spendCap: { daily: '1.00', asset: 'USDC' },
163
+ })
164
+ const supabase = createClient(
165
+ process.env.NEXT_PUBLIC_SUPABASE_URL!,
166
+ process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
167
+ )
168
+ return (
169
+ <RouteDockProvider client={client} supabase={supabase}>
170
+ {children}
171
+ </RouteDockProvider>
172
+ )
173
+ }
174
+ ```
175
+
176
+ ### Paying from the server
177
+
178
+ ```ts
179
+ // app/api/price/route.ts (runs on the server only)
180
+ import { RouteDockClient } from '@routedock/sdk/client'
181
+
182
+ const client = new RouteDockClient({
183
+ wallet: process.env.AGENT_SECRET!,
184
+ network: 'testnet',
185
+ spendCap: { daily: '1.00', asset: 'USDC' },
186
+ })
187
+
188
+ export async function POST() {
189
+ const result = await client.pay('https://provider.example.com/price')
190
+ return Response.json({ mode: result.mode, txHash: result.txHash, data: result.data })
191
+ }
192
+ ```
193
+
194
+ ### `usePay(url, options?)`
195
+
196
+ ```tsx
197
+ import { usePay } from '@routedock/sdk/react'
198
+
199
+ function PriceButton() {
200
+ const { pay, result, loading, error } = usePay('https://provider.example.com/price')
201
+ if (error) return <span>Error: {error.message}</span>
202
+ return (
203
+ <button onClick={pay} disabled={loading}>
204
+ {loading ? 'Paying…' : result ? `Paid: ${result.txHash}` : 'Pay 0.001 USDC'}
205
+ </button>
206
+ )
207
+ }
208
+ ```
209
+
210
+ ### `useSession(url)`
211
+
212
+ ```tsx
213
+ import { useEffect } from 'react'
214
+ import { useSession } from '@routedock/sdk/react'
215
+
216
+ function StreamingFeed() {
217
+ const { open, close, status, vouchers, cumulative } = useSession(
218
+ 'https://provider.example.com/stream/orderbook',
219
+ )
220
+ useEffect(() => { void open() }, [])
221
+ return (
222
+ <div>
223
+ <p>Status: {status} — vouchers: {vouchers} — paid: {cumulative} USDC</p>
224
+ <button onClick={close} disabled={status !== 'open'}>Close & settle</button>
225
+ </div>
226
+ )
227
+ }
228
+ ```
229
+
230
+ The hook automatically fires `session.close()` in the background on unmount when status is `open` (best-effort settlement).
231
+
232
+ ### `useTxLog(filter?)`
233
+
234
+ ```tsx
235
+ import { useTxLog } from '@routedock/sdk/react'
236
+
237
+ function ActivityFeed({ channelId }: { channelId: string }) {
238
+ const txLog = useTxLog({ channelId, limit: 25 })
239
+ return (
240
+ <ul>
241
+ {txLog.map((row) => (
242
+ <li key={row.id}>{row.mode} — {row.amount} USDC — {row.tx_hash}</li>
243
+ ))}
244
+ </ul>
245
+ )
246
+ }
247
+ ```
248
+
249
+ | Hook | Returns | Requires from `RouteDockProvider` |
250
+ |---|---|---|
251
+ | `useRouteDockClient(config)` | Memoized `RouteDockClient` | — (creator hook) |
252
+ | `usePay(url, options?)` | `{ pay, result, loading, error }` | `client` |
253
+ | `useSession(url)` | `{ session, status, vouchers, cumulative, error, open, close }` | `client` (with `commitmentSecret`) |
254
+ | `useTxLog(filter?)` | `TxLogRow[]` (newest first) | `supabase` |
255
+
256
+ React is a peer dependency — install `react@^18` (or `^19`) in your app.
257
+
258
+ ---
259
+
260
+ ## Error Handling
261
+
262
+ All SDK failures extend `RouteDockError` with a stable `code`, `retryable` flag, and optional `cause`. Transient failures (network timeouts, facilitator 5xx, Horizon RPC errors) are retried automatically with exponential backoff unless you disable retries.
263
+
264
+ ```ts
265
+ import {
266
+ RouteDockClient,
267
+ RouteDockError,
268
+ RouteDockFacilitatorError,
269
+ RouteDockManifestError,
270
+ RouteDockPolicyRejectError,
271
+ } from '@routedock/sdk/client'
272
+
273
+ const client = new RouteDockClient({
274
+ wallet: process.env.AGENT_SECRET!,
275
+ network: 'testnet',
276
+ retryPolicy: { maxAttempts: 4, baseDelayMs: 250 },
277
+ })
278
+
279
+ try {
280
+ const result = await client.pay('https://provider.example.com/price')
281
+ console.log(result.mode, result.txHash)
282
+ } catch (err) {
283
+ if (err instanceof RouteDockPolicyRejectError) {
284
+ console.error('Spend cap exceeded:', err.reason)
285
+ } else if (err instanceof RouteDockFacilitatorError) {
286
+ console.error(`Facilitator HTTP ${err.status} (retryable=${err.retryable})`)
287
+ } else if (err instanceof RouteDockManifestError) {
288
+ console.error('Manifest or config problem:', err.message)
289
+ } else if (err instanceof RouteDockError) {
290
+ console.error(`[${err.code}] ${err.message} (retryable=${err.retryable})`)
291
+ } else {
292
+ throw err
293
+ }
294
+ }
295
+ ```
296
+
297
+ | Error | `retryable` | Typical cause |
298
+ |---|---|---|
299
+ | `RouteDockManifestError` | no | Invalid manifest, missing pricing fields |
300
+ | `RouteDockNoSupportedModeError` | no | Provider has no compatible payment mode |
301
+ | `RouteDockFacilitatorError` | yes | Facilitator/provider 429 or 5xx |
302
+ | `RouteDockNetworkError` | yes | Timeouts, connection failures |
303
+ | `RouteDockSignatureError` | no | Signing or commitment failure |
304
+ | `RouteDockVoucherMonotonicityError` | no | Non-increasing voucher amount |
305
+ | `RouteDockPolicyRejectError` | no | Local spend cap exceeded |
306
+ | `RouteDockChannelStateError` | no | Channel simulate/close invariant violation |
307
+
110
308
  ## Mode Selection Logic
111
309
 
112
310
  The `ModeRouter` uses this decision tree (deterministic, no randomness):
@@ -118,6 +316,31 @@ The `ModeRouter` uses this decision tree (deterministic, no randomness):
118
316
 
119
317
  Override with `{ forceMode: 'x402' }` to bypass auto-selection.
120
318
 
319
+ ## Dispute Resolution
320
+
321
+ For `mpp-session` mode, the session handle exposes three methods to handle server unavailability or crashes:
322
+
323
+ ```ts
324
+ const session = await client.openSession('https://provider.example.com/stream')
325
+
326
+ // Check the current dispute status (open, in-refund-window, refundable, settled)
327
+ const status = await session.getDisputeStatus()
328
+
329
+ // If server is unresponsive, request a refund (starts the refund window)
330
+ const refundTxHash = await session.requestRefund()
331
+
332
+ // Server-side: settle with the latest signed voucher before refund window expires
333
+ const settleTxHash = await session.settleWithLatestVoucher()
334
+ ```
335
+
336
+ | Method | Purpose | Requires | Returns |
337
+ |--------|---------|----------|---------|
338
+ | `getDisputeStatus()` | Query channel state | None | `'open' \| 'in-refund-window' \| 'refundable' \| 'settled'` |
339
+ | `requestRefund()` | Initiate refund process | Signed agent keypair | Transaction hash (string) |
340
+ | `settleWithLatestVoucher()` | Server counter-settle | Latest voucher signature | Transaction hash (string) |
341
+
342
+ Raises: `RouteDockDisputeError`, `RouteDockChannelStateError`, `RouteDockRefundWindowError`
343
+
121
344
  ## Security
122
345
 
123
346
  The `one-way-channel` Soroban contract wrapped by `MppSessionClient` is **unaudited** (`stellar-experimental/one-way-channel`). Safe defaults are enforced: 17280 ledger refund window, monotonic cumulative enforcement at application, database, and contract layers. Production mainnet use should await a formal audit.
@@ -0,0 +1,121 @@
1
+ import { RequestHandler } from 'express';
2
+ import { Store } from 'mppx';
3
+ import { R as RouteDockManifest } from './types-DmhmfLzP.cjs';
4
+
5
+ /**
6
+ * Idempotency store for payment settlement.
7
+ *
8
+ * `x402Handler` and `MppChargeHandler` settle on every request carrying a
9
+ * payment header. An agent that retries after a post-settlement network
10
+ * timeout resends the *same* signed payment, which would settle a second time
11
+ * and invoke `onSettled` twice — double-counting in billing.
12
+ *
13
+ * Before settling, a handler derives an idempotency key from the inbound
14
+ * payment header(s) and checks this store. On a hit it replays the cached
15
+ * settlement response and skips both the on-chain settle and `onSettled`.
16
+ *
17
+ * The default {@link InMemorySeenTxStore} is per-handler and per-process. For
18
+ * multi-instance deployments, supply a shared implementation backed by Redis,
19
+ * Supabase, etc.
20
+ */
21
+ /** Cached outcome of a settlement, replayed on a duplicate payment. */
22
+ interface SettlementRecord {
23
+ /** On-chain transaction hash (or settlement reference), if known. */
24
+ txHash: string | null;
25
+ /** Response headers to re-apply on a replay (e.g. `X-Payment-Response`). */
26
+ headers?: Record<string, string>;
27
+ }
28
+ interface SeenTxStore {
29
+ /** Returns the cached settlement for a key, or `undefined` if unseen. */
30
+ get(key: string): Promise<SettlementRecord | undefined> | SettlementRecord | undefined;
31
+ /** Records the settlement outcome for a key. */
32
+ set(key: string, record: SettlementRecord): Promise<void> | void;
33
+ }
34
+ interface InMemorySeenTxStoreOptions {
35
+ /** Maximum number of entries before FIFO eviction. Defaults to 10,000. */
36
+ maxEntries?: number;
37
+ /** Log a startup warning about non-durability. Defaults to true. */
38
+ warn?: boolean;
39
+ }
40
+ /**
41
+ * Default in-memory {@link SeenTxStore}. Bounded by `maxEntries` with FIFO
42
+ * eviction so a long-running process cannot grow without limit.
43
+ *
44
+ * **Not durable.** On serverless/edge runtimes each request may land on a
45
+ * fresh isolate with an empty cache, which causes duplicate settlements.
46
+ * Supply a persistent implementation (Supabase, Redis, …) in production.
47
+ */
48
+ declare class InMemorySeenTxStore implements SeenTxStore {
49
+ private readonly map;
50
+ private readonly order;
51
+ constructor(maxEntriesOrOptions?: number | InMemorySeenTxStoreOptions);
52
+ private readonly maxEntries;
53
+ get(key: string): SettlementRecord | undefined;
54
+ set(key: string, record: SettlementRecord): void;
55
+ }
56
+ /**
57
+ * Derive a collision-resistant idempotency key from the inbound
58
+ * payment-bearing headers. Uses SHA-256 instead of 32-bit FNV-1a
59
+ * (which has a 2^32 keyspace — birthday collisions become likely
60
+ * well before the 10_000-entry default store is full).
61
+ *
62
+ * x402 clients send the signed payment in `payment-signature` / `x-payment`;
63
+ * mppx clients send it in `authorization` (the `Payment` scheme). A retry
64
+ * resends byte-identical headers, so hashing the first present one yields a
65
+ * stable key. Returns `null` when no payment header is present (nothing to
66
+ * dedupe — e.g. the initial 402 challenge request).
67
+ */
68
+ declare function paymentIdempotencyKey(getHeader: (name: string) => string | undefined): Promise<string | null>;
69
+
70
+ /** Store shape extended with optional atomic update operation. */
71
+ type ChannelStore = Store.Store & {
72
+ update?: (key: string, fn: (prev: unknown) => unknown) => Promise<unknown>;
73
+ };
74
+ /** Expected payload shape for voucher cumulative amount entries. */
75
+ interface VoucherStoreValue {
76
+ amount: string;
77
+ [key: string]: unknown;
78
+ }
79
+ /** Validates that a stored store value is an object containing a numeric string amount. */
80
+ declare function isVoucherStoreValue(value: unknown): value is VoucherStoreValue;
81
+ type Network = 'testnet' | 'mainnet';
82
+ /** Why a live session was flagged as orphaned (never cleanly closed). */
83
+ type OrphanReason = 'connection-closed' | 'idle-timeout';
84
+ /** Latest signed state of an orphaned session, enough for the reconciler to settle. */
85
+ interface OrphanedSessionInfo {
86
+ /** Cumulative amount of the highest voucher seen (human-readable, 7 dp). */
87
+ cumulativeAmount: string;
88
+ /** Hex-encoded signature of the highest voucher seen. */
89
+ lastSignature: string;
90
+ /** Number of vouchers received before teardown. */
91
+ voucherCount: number;
92
+ /** What triggered the orphan flag. */
93
+ reason: OrphanReason;
94
+ }
95
+ interface MppSessionHandlerOptions {
96
+ payeeSecretKey: string;
97
+ network: Network;
98
+ channelFactory: string;
99
+ rate: string;
100
+ assetContract: string;
101
+ manifest: RouteDockManifest;
102
+ commitmentPublicKey: string;
103
+ onSettled?: (txHash: string, totalPaid: string, mode: string, payer: string | null) => Promise<void>;
104
+ onSessionOpen?: (channelId: string, payer: string | null) => Promise<void>;
105
+ onVoucher?: (channelId: string, voucherIndex: number, cumulativeAmount: string, signature: string) => Promise<void>;
106
+ onCallbackError?: (err: unknown, cb: string) => void;
107
+ /**
108
+ * Called when the client connection drops mid-session or the session goes
109
+ * idle, before a clean close. Persist the session as `closing` so the
110
+ * SessionReconciler can settle it with the latest signed voucher.
111
+ */
112
+ onOrphaned?: (channelId: string, info: OrphanedSessionInfo) => Promise<void>;
113
+ /**
114
+ * Flag the session orphaned after this many milliseconds with no voucher
115
+ * activity. Disabled when unset.
116
+ */
117
+ idleTimeoutMs?: number;
118
+ }
119
+ declare function createMppSessionHandler(opts: MppSessionHandlerOptions): RequestHandler;
120
+
121
+ export { type ChannelStore as C, InMemorySeenTxStore as I, type MppSessionHandlerOptions as M, type OrphanedSessionInfo as O, type SeenTxStore as S, type VoucherStoreValue as V, type SettlementRecord as a, type InMemorySeenTxStoreOptions as b, type OrphanReason as c, createMppSessionHandler as d, isVoucherStoreValue as i, paymentIdempotencyKey as p };
@@ -0,0 +1,121 @@
1
+ import { RequestHandler } from 'express';
2
+ import { Store } from 'mppx';
3
+ import { R as RouteDockManifest } from './types-DmhmfLzP.js';
4
+
5
+ /**
6
+ * Idempotency store for payment settlement.
7
+ *
8
+ * `x402Handler` and `MppChargeHandler` settle on every request carrying a
9
+ * payment header. An agent that retries after a post-settlement network
10
+ * timeout resends the *same* signed payment, which would settle a second time
11
+ * and invoke `onSettled` twice — double-counting in billing.
12
+ *
13
+ * Before settling, a handler derives an idempotency key from the inbound
14
+ * payment header(s) and checks this store. On a hit it replays the cached
15
+ * settlement response and skips both the on-chain settle and `onSettled`.
16
+ *
17
+ * The default {@link InMemorySeenTxStore} is per-handler and per-process. For
18
+ * multi-instance deployments, supply a shared implementation backed by Redis,
19
+ * Supabase, etc.
20
+ */
21
+ /** Cached outcome of a settlement, replayed on a duplicate payment. */
22
+ interface SettlementRecord {
23
+ /** On-chain transaction hash (or settlement reference), if known. */
24
+ txHash: string | null;
25
+ /** Response headers to re-apply on a replay (e.g. `X-Payment-Response`). */
26
+ headers?: Record<string, string>;
27
+ }
28
+ interface SeenTxStore {
29
+ /** Returns the cached settlement for a key, or `undefined` if unseen. */
30
+ get(key: string): Promise<SettlementRecord | undefined> | SettlementRecord | undefined;
31
+ /** Records the settlement outcome for a key. */
32
+ set(key: string, record: SettlementRecord): Promise<void> | void;
33
+ }
34
+ interface InMemorySeenTxStoreOptions {
35
+ /** Maximum number of entries before FIFO eviction. Defaults to 10,000. */
36
+ maxEntries?: number;
37
+ /** Log a startup warning about non-durability. Defaults to true. */
38
+ warn?: boolean;
39
+ }
40
+ /**
41
+ * Default in-memory {@link SeenTxStore}. Bounded by `maxEntries` with FIFO
42
+ * eviction so a long-running process cannot grow without limit.
43
+ *
44
+ * **Not durable.** On serverless/edge runtimes each request may land on a
45
+ * fresh isolate with an empty cache, which causes duplicate settlements.
46
+ * Supply a persistent implementation (Supabase, Redis, …) in production.
47
+ */
48
+ declare class InMemorySeenTxStore implements SeenTxStore {
49
+ private readonly map;
50
+ private readonly order;
51
+ constructor(maxEntriesOrOptions?: number | InMemorySeenTxStoreOptions);
52
+ private readonly maxEntries;
53
+ get(key: string): SettlementRecord | undefined;
54
+ set(key: string, record: SettlementRecord): void;
55
+ }
56
+ /**
57
+ * Derive a collision-resistant idempotency key from the inbound
58
+ * payment-bearing headers. Uses SHA-256 instead of 32-bit FNV-1a
59
+ * (which has a 2^32 keyspace — birthday collisions become likely
60
+ * well before the 10_000-entry default store is full).
61
+ *
62
+ * x402 clients send the signed payment in `payment-signature` / `x-payment`;
63
+ * mppx clients send it in `authorization` (the `Payment` scheme). A retry
64
+ * resends byte-identical headers, so hashing the first present one yields a
65
+ * stable key. Returns `null` when no payment header is present (nothing to
66
+ * dedupe — e.g. the initial 402 challenge request).
67
+ */
68
+ declare function paymentIdempotencyKey(getHeader: (name: string) => string | undefined): Promise<string | null>;
69
+
70
+ /** Store shape extended with optional atomic update operation. */
71
+ type ChannelStore = Store.Store & {
72
+ update?: (key: string, fn: (prev: unknown) => unknown) => Promise<unknown>;
73
+ };
74
+ /** Expected payload shape for voucher cumulative amount entries. */
75
+ interface VoucherStoreValue {
76
+ amount: string;
77
+ [key: string]: unknown;
78
+ }
79
+ /** Validates that a stored store value is an object containing a numeric string amount. */
80
+ declare function isVoucherStoreValue(value: unknown): value is VoucherStoreValue;
81
+ type Network = 'testnet' | 'mainnet';
82
+ /** Why a live session was flagged as orphaned (never cleanly closed). */
83
+ type OrphanReason = 'connection-closed' | 'idle-timeout';
84
+ /** Latest signed state of an orphaned session, enough for the reconciler to settle. */
85
+ interface OrphanedSessionInfo {
86
+ /** Cumulative amount of the highest voucher seen (human-readable, 7 dp). */
87
+ cumulativeAmount: string;
88
+ /** Hex-encoded signature of the highest voucher seen. */
89
+ lastSignature: string;
90
+ /** Number of vouchers received before teardown. */
91
+ voucherCount: number;
92
+ /** What triggered the orphan flag. */
93
+ reason: OrphanReason;
94
+ }
95
+ interface MppSessionHandlerOptions {
96
+ payeeSecretKey: string;
97
+ network: Network;
98
+ channelFactory: string;
99
+ rate: string;
100
+ assetContract: string;
101
+ manifest: RouteDockManifest;
102
+ commitmentPublicKey: string;
103
+ onSettled?: (txHash: string, totalPaid: string, mode: string, payer: string | null) => Promise<void>;
104
+ onSessionOpen?: (channelId: string, payer: string | null) => Promise<void>;
105
+ onVoucher?: (channelId: string, voucherIndex: number, cumulativeAmount: string, signature: string) => Promise<void>;
106
+ onCallbackError?: (err: unknown, cb: string) => void;
107
+ /**
108
+ * Called when the client connection drops mid-session or the session goes
109
+ * idle, before a clean close. Persist the session as `closing` so the
110
+ * SessionReconciler can settle it with the latest signed voucher.
111
+ */
112
+ onOrphaned?: (channelId: string, info: OrphanedSessionInfo) => Promise<void>;
113
+ /**
114
+ * Flag the session orphaned after this many milliseconds with no voucher
115
+ * activity. Disabled when unset.
116
+ */
117
+ idleTimeoutMs?: number;
118
+ }
119
+ declare function createMppSessionHandler(opts: MppSessionHandlerOptions): RequestHandler;
120
+
121
+ export { type ChannelStore as C, InMemorySeenTxStore as I, type MppSessionHandlerOptions as M, type OrphanedSessionInfo as O, type SeenTxStore as S, type VoucherStoreValue as V, type SettlementRecord as a, type InMemorySeenTxStoreOptions as b, type OrphanReason as c, createMppSessionHandler as d, isVoucherStoreValue as i, paymentIdempotencyKey as p };
@@ -0,0 +1,142 @@
1
+ import { usdcToStroops } from '@routedock/nulth-sdk';
2
+ import { m as SpendStore, a as DailySpend, R as RetryPolicy } from './RouteDockClient-B1roCa8h.cjs';
3
+ import { ClientStellarSigner } from '@x402/stellar';
4
+ import { R as RouteDockManifest, c as PaymentResult } from './types-DmhmfLzP.cjs';
5
+ import { Keypair } from '@stellar/stellar-sdk';
6
+ import { SupabaseClient } from '@supabase/supabase-js';
7
+
8
+ /**
9
+ * USDC conversion — canonical implementation lives in @routedock/nulth-sdk and is
10
+ * re-exported here so SDK consumers (client, provider) share one converter.
11
+ *
12
+ * @routedock/routedock already depends on @routedock/nulth-sdk, so re-exporting
13
+ * (rather than defining a parallel helper) keeps a single source of truth.
14
+ */
15
+
16
+ /**
17
+ * @deprecated Use {@link usdcToStroops} — the same validated bigint converter.
18
+ * Kept as a thin alias so existing call sites keep working during migration.
19
+ */
20
+ declare const usdcToUnits: typeof usdcToStroops;
21
+
22
+ /**
23
+ * File-backed durable {@link SpendStore}.
24
+ *
25
+ * Persists the daily spend accumulator to a JSON file on disk, preserving
26
+ * cumulative spending totals across process restarts, crashes, and redeploys.
27
+ *
28
+ * ### Error semantics
29
+ * - File absent (ENOENT): returns null — legitimately no spend recorded yet.
30
+ * - File present but unreadable or malformed: throws — a safety control must
31
+ * fail closed, not assume nothing was spent.
32
+ *
33
+ * ### Write atomicity
34
+ * Writes go to `<filePath>.tmp` first, then `fs.rename` onto the target.
35
+ * `rename` is atomic within a filesystem, so there is no window where the
36
+ * target file is truncated but not yet fully written.
37
+ */
38
+ declare class FileSpendStore implements SpendStore {
39
+ readonly filePath: string;
40
+ constructor(filePath: string);
41
+ read(): Promise<DailySpend | null>;
42
+ write(state: DailySpend): Promise<void>;
43
+ }
44
+
45
+ type Network = 'testnet' | 'mainnet';
46
+ declare class X402Client {
47
+ private readonly network;
48
+ private readonly retryPolicy?;
49
+ private readonly httpClient;
50
+ private readonly signer;
51
+ constructor(secretKeyOrSigner: string | ClientStellarSigner, network: Network, retryPolicy?: RetryPolicy | undefined);
52
+ /** Replace signer (e.g. swap to Nulth ZK account payer before pay) */
53
+ withSigner(signer: ClientStellarSigner): X402Client;
54
+ pay(url: string, manifest: RouteDockManifest): Promise<PaymentResult>;
55
+ }
56
+
57
+ declare class MppChargeClient {
58
+ private readonly keypair;
59
+ private readonly network;
60
+ private readonly retryPolicy?;
61
+ constructor(keypair: Keypair, network: 'testnet' | 'mainnet', retryPolicy?: RetryPolicy | undefined);
62
+ pay(url: string, manifest: RouteDockManifest): Promise<PaymentResult>;
63
+ }
64
+
65
+ interface OnChainProviderInfo {
66
+ account: string;
67
+ endpoint: string;
68
+ tags: string[];
69
+ }
70
+ declare class OnChainRegistry {
71
+ private readonly horizon;
72
+ private readonly knownAccounts;
73
+ private readonly timeoutMs;
74
+ constructor(options: {
75
+ horizonUrl: string;
76
+ knownAccounts: string[];
77
+ timeoutMs?: number;
78
+ });
79
+ /**
80
+ * stellar-sdk 14.6.1 has no way to cancel the underlying Horizon request, so
81
+ * we stop waiting on it instead: the promise returned here rejects once
82
+ * `timeoutMs` elapses, even though the request may still be in flight. The
83
+ * timer is always cleared, including on the error path.
84
+ */
85
+ private loadAccountWithTimeout;
86
+ listProviders(): Promise<OnChainProviderInfo[]>;
87
+ }
88
+
89
+ interface ProviderRecord {
90
+ name: string;
91
+ description: string | undefined;
92
+ base_url: string;
93
+ modes: string[];
94
+ tags: string[];
95
+ network: string;
96
+ payee: string;
97
+ source: 'supabase' | 'onchain';
98
+ /**
99
+ * True only when the provider was verified out-of-band — for on-chain
100
+ * records the payee is the account that published the endpoint, for
101
+ * supabase records only rows flagged `verified` are returned.
102
+ */
103
+ verified: boolean;
104
+ }
105
+ interface ProviderRegistryConfig {
106
+ supabase?: SupabaseClient;
107
+ onChain: {
108
+ horizonUrl: string;
109
+ knownAccounts: string[];
110
+ /**
111
+ * Optional explicit network override. When omitted it is derived from the
112
+ * Horizon URL: URLs containing `testnet` map to 'testnet', everything else
113
+ * defaults to 'mainnet'.
114
+ */
115
+ network?: 'testnet' | 'mainnet';
116
+ /**
117
+ * Per-account timeout for on-chain account loads, in milliseconds.
118
+ * Defaults to 10000 ms.
119
+ */
120
+ timeoutMs?: number;
121
+ };
122
+ }
123
+ declare class ProviderRegistry {
124
+ private readonly supabase;
125
+ private readonly onChain;
126
+ private readonly network;
127
+ constructor(config: ProviderRegistryConfig);
128
+ listProviders(): Promise<ProviderRecord[]>;
129
+ private trySupabase;
130
+ private tryOnChain;
131
+ /**
132
+ * Fetch a provider's manifest bound to the record's payee. The record's
133
+ * `payee` (from supabase `verified` rows or the on-chain registry account)
134
+ * is the out-of-band trust anchor: the fetched manifest's `payee` must
135
+ * equal it, otherwise `RouteDockSignatureError` is thrown. This closes the
136
+ * gap where the registry never cross-checked the on-chain payee against
137
+ * the manifest payee.
138
+ */
139
+ fetchProviderManifest(record: ProviderRecord): Promise<RouteDockManifest>;
140
+ }
141
+
142
+ export { FileSpendStore as F, MppChargeClient as M, type OnChainProviderInfo as O, type ProviderRecord as P, X402Client as X, OnChainRegistry as a, ProviderRegistry as b, type ProviderRegistryConfig as c, usdcToUnits as u };