@routedock/routedock 0.1.1 → 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 +281 -19
  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/dist/provider.js CHANGED
@@ -1,46 +1,178 @@
1
- import { Keypair } from '@stellar/stellar-sdk';
1
+ import { Keypair, StrKey, Contract, nativeToScVal, TransactionBuilder, BASE_FEE } from '@stellar/stellar-sdk';
2
2
  import { ExactStellarScheme } from '@x402/stellar/exact/facilitator';
3
3
  import { ExactStellarScheme as ExactStellarScheme$1 } from '@x402/stellar/exact/server';
4
4
  import { HTTPFacilitatorClient, x402ResourceServer } from '@x402/core/server';
5
5
  import { createEd25519Signer } from '@x402/stellar';
6
6
  import { encodePaymentRequiredHeader, decodePaymentSignatureHeader, encodePaymentResponseHeader } from '@x402/core/http';
7
+ import { usdcToStroops } from '@routedock/nulth-sdk';
7
8
  import { stellar } from '@stellar/mpp/charge/server';
8
9
  import { Mppx, Request } from 'mppx/server';
9
10
  import { Store, stellar as stellar$1, close } from '@stellar/mpp/channel/server';
11
+ import { Errors } from 'mppx';
12
+ import { createHash } from 'crypto';
10
13
 
11
- // src/types.ts
14
+ // src/errors.ts
12
15
  var RouteDockError = class extends Error {
13
- constructor(message) {
14
- super(message);
16
+ code;
17
+ retryable;
18
+ retryAfterMs;
19
+ constructor(message, code, retryable, options = {}) {
20
+ super(message, { cause: options.cause });
15
21
  this.name = "RouteDockError";
22
+ this.code = code;
23
+ this.retryable = retryable;
24
+ if (options.retryAfterMs !== void 0) {
25
+ this.retryAfterMs = options.retryAfterMs;
26
+ }
16
27
  }
17
28
  };
18
29
  var RouteDockManifestError = class extends RouteDockError {
19
- constructor(message) {
20
- super(message);
30
+ constructor(message, options = {}) {
31
+ super(message, "MANIFEST", false, options);
21
32
  this.name = "RouteDockManifestError";
22
33
  }
23
34
  };
24
35
  var RouteDockNoSupportedModeError = class extends RouteDockError {
25
- constructor(message) {
26
- super(message);
36
+ constructor(message, options = {}) {
37
+ super(message, "NO_SUPPORTED_MODE", false, options);
27
38
  this.name = "RouteDockNoSupportedModeError";
28
39
  }
29
40
  };
30
- var RouteDockSessionError = class extends RouteDockError {
31
- constructor(message) {
32
- super(message);
33
- this.name = "RouteDockSessionError";
41
+ var RouteDockClientVersionError = class extends RouteDockError {
42
+ constructor(message, options = {}) {
43
+ super(message, "CLIENT_VERSION_TOO_OLD", false, options);
44
+ this.name = "RouteDockClientVersionError";
45
+ }
46
+ };
47
+ var RouteDockTrustlineError = class extends RouteDockError {
48
+ asset;
49
+ issuer;
50
+ remediation;
51
+ constructor(asset, issuer, remediation, options = {}) {
52
+ super(
53
+ `Missing trustline for ${asset} (issuer: ${issuer}). ${remediation}`,
54
+ "TRUSTLINE",
55
+ false,
56
+ options
57
+ );
58
+ this.name = "RouteDockTrustlineError";
59
+ this.asset = asset;
60
+ this.issuer = issuer;
61
+ this.remediation = remediation;
62
+ }
63
+ };
64
+ var RouteDockFacilitatorError = class extends RouteDockError {
65
+ status;
66
+ constructor(message, status, options = {}) {
67
+ super(message, "FACILITATOR", true, options);
68
+ this.name = "RouteDockFacilitatorError";
69
+ this.status = status;
70
+ }
71
+ };
72
+ var RouteDockNetworkError = class extends RouteDockError {
73
+ constructor(message, options = {}) {
74
+ super(message, "NETWORK", true, options);
75
+ this.name = "RouteDockNetworkError";
76
+ }
77
+ };
78
+ var RouteDockSignatureError = class extends RouteDockError {
79
+ constructor(message, options = {}) {
80
+ super(message, "SIGNATURE", false, options);
81
+ this.name = "RouteDockSignatureError";
82
+ }
83
+ };
84
+ var RouteDockVoucherMonotonicityError = class extends RouteDockError {
85
+ constructor(message, options = {}) {
86
+ super(message, "VOUCHER_MONOTONICITY", false, options);
87
+ this.name = "RouteDockVoucherMonotonicityError";
34
88
  }
35
89
  };
36
- var RouteDockPolicyRejectedError = class extends RouteDockError {
90
+ var RouteDockPolicyRejectError = class extends RouteDockError {
37
91
  reason;
38
- constructor(reason) {
39
- super(`Payment rejected by policy: ${reason}`);
40
- this.name = "RouteDockPolicyRejectedError";
92
+ constructor(reason, options = {}) {
93
+ super(`Payment rejected by policy: ${reason}`, "POLICY_REJECT", false, options);
94
+ this.name = "RouteDockPolicyRejectError";
41
95
  this.reason = reason;
42
96
  }
43
97
  };
98
+ var RouteDockChannelStateError = class extends RouteDockError {
99
+ constructor(message, options = {}) {
100
+ super(message, "CHANNEL_STATE", false, options);
101
+ this.name = "RouteDockChannelStateError";
102
+ }
103
+ };
104
+ var RouteDockDisputeError = class extends RouteDockError {
105
+ constructor(message, options = {}) {
106
+ super(message, "DISPUTE", false, options);
107
+ this.name = "RouteDockDisputeError";
108
+ }
109
+ };
110
+ var RouteDockRefundWindowError = class extends RouteDockError {
111
+ constructor(message, options = {}) {
112
+ super(message, "REFUND_WINDOW", false, options);
113
+ this.name = "RouteDockRefundWindowError";
114
+ }
115
+ };
116
+ var RouteDockPolicyRejectedError = RouteDockPolicyRejectError;
117
+ var RouteDockSessionError = RouteDockVoucherMonotonicityError;
118
+
119
+ // src/types.ts
120
+ var DEFAULT_MAX_SESSION_DURATION_MS = 36e5;
121
+
122
+ // src/provider/payee.ts
123
+ function resolvePayee(manifest, mode) {
124
+ const override = manifest.pricing[mode]?.payee;
125
+ return override ?? manifest.payee;
126
+ }
127
+ var usdcToUnits = usdcToStroops;
128
+ function extractPayerAddress(key) {
129
+ if (typeof key !== "string") return null;
130
+ return StrKey.isValidEd25519PublicKey(key) || StrKey.isValidMed25519PublicKey(key) ? key : null;
131
+ }
132
+
133
+ // src/provider/SeenTxStore.ts
134
+ var InMemorySeenTxStore = class {
135
+ map = /* @__PURE__ */ new Map();
136
+ order = [];
137
+ constructor(maxEntriesOrOptions = 1e4) {
138
+ const options = typeof maxEntriesOrOptions === "number" ? { maxEntries: maxEntriesOrOptions } : maxEntriesOrOptions;
139
+ this.maxEntries = options.maxEntries ?? 1e4;
140
+ if (options.warn !== false) {
141
+ const isServerless = typeof globalThis.navigator !== "undefined" && globalThis.navigator.userAgent === "Cloudflare-Workers";
142
+ if (isServerless) {
143
+ console.warn(
144
+ "[RouteDock] Using in-memory SeenTxStore: settlement deduplication is NOT durable and resets on every isolate restart. Supply a SupabaseSeenTxStore (or other durable SeenTxStore) for production safety."
145
+ );
146
+ }
147
+ }
148
+ }
149
+ maxEntries;
150
+ get(key) {
151
+ return this.map.get(key);
152
+ }
153
+ set(key, record) {
154
+ if (!this.map.has(key)) {
155
+ this.order.push(key);
156
+ if (this.order.length > this.maxEntries) {
157
+ const evicted = this.order.shift();
158
+ if (evicted !== void 0) this.map.delete(evicted);
159
+ }
160
+ }
161
+ this.map.set(key, record);
162
+ }
163
+ };
164
+ async function sha256Hex(input) {
165
+ const buf = new TextEncoder().encode(input);
166
+ const digest = await crypto.subtle.digest("SHA-256", buf);
167
+ return Array.from(new Uint8Array(digest)).map((b) => b.toString(16).padStart(2, "0")).join("");
168
+ }
169
+ async function paymentIdempotencyKey(getHeader) {
170
+ const material = getHeader("payment-signature") ?? getHeader("x-payment") ?? getHeader("authorization");
171
+ if (!material) return null;
172
+ return sha256Hex(material);
173
+ }
174
+
175
+ // src/provider/x402Handler.ts
44
176
  var CAIP2 = {
45
177
  testnet: "stellar:testnet",
46
178
  mainnet: "stellar:pubnet"
@@ -50,6 +182,7 @@ function createX402Handler(opts) {
50
182
  const caip2 = CAIP2[opts.network];
51
183
  const payeeKeypair = Keypair.fromSecret(opts.payeeSecretKey);
52
184
  const signer = createEd25519Signer(opts.payeeSecretKey, caip2);
185
+ const seenTxStore = opts.seenTxStore ?? new InMemorySeenTxStore();
53
186
  const useOzFacilitator = opts.network === "mainnet" && opts.facilitatorApiKey;
54
187
  const localFacilitator = new ExactStellarScheme([signer], {
55
188
  areFeesSponsored: true
@@ -68,13 +201,14 @@ function createX402Handler(opts) {
68
201
  ozServer = new x402ResourceServer(facilitator);
69
202
  ozServer.register(caip2, new ExactStellarScheme$1());
70
203
  }
71
- const amountInBaseUnits = String(Math.round(parseFloat(opts.amount) * 1e7));
204
+ const amountInBaseUnits = String(usdcToUnits(opts.amount));
205
+ const payTo = resolvePayee(opts.manifest, "x402");
72
206
  const requirements = {
73
207
  scheme: "exact",
74
208
  network: caip2,
75
209
  asset: opts.assetContract,
76
210
  amount: amountInBaseUnits,
77
- payTo: opts.manifest.payee,
211
+ payTo,
78
212
  maxTimeoutSeconds: 60,
79
213
  extra: {
80
214
  areFeesSponsored: true,
@@ -108,8 +242,31 @@ function createX402Handler(opts) {
108
242
  }
109
243
  return;
110
244
  }
245
+ const idempotencyKey = await paymentIdempotencyKey((name) => {
246
+ const v = req.headers[name.toLowerCase()];
247
+ return Array.isArray(v) ? v[0] : v;
248
+ });
249
+ if (idempotencyKey) {
250
+ const cached = await seenTxStore.get(idempotencyKey);
251
+ if (cached) {
252
+ if (cached.headers) {
253
+ for (const [k, val] of Object.entries(cached.headers)) {
254
+ res.setHeader(k, val);
255
+ }
256
+ }
257
+ next();
258
+ return;
259
+ }
260
+ }
111
261
  const payload = decodePaymentSignatureHeader(paymentHeader);
112
262
  let txHash = null;
263
+ let payerAddress = null;
264
+ try {
265
+ const creds = payload.authorization?.credentials;
266
+ const key = Array.isArray(creds) ? creds[0]?.publicKey : void 0;
267
+ payerAddress = extractPayerAddress(key);
268
+ } catch {
269
+ }
113
270
  if (ozServer) {
114
271
  const settleResult = await ozServer.settlePayment(payload, requirements);
115
272
  txHash = settleResult.transaction ?? null;
@@ -147,8 +304,19 @@ function createX402Handler(opts) {
147
304
  );
148
305
  }
149
306
  }
307
+ if (idempotencyKey) {
308
+ const headers = {};
309
+ const paymentResponse = res.getHeader("X-Payment-Response");
310
+ if (typeof paymentResponse === "string") {
311
+ headers["X-Payment-Response"] = paymentResponse;
312
+ }
313
+ await seenTxStore.set(idempotencyKey, { txHash, headers });
314
+ }
150
315
  if (txHash && opts.onSettled) {
151
- await opts.onSettled(txHash, opts.amount, "x402");
316
+ Promise.resolve().then(() => opts.onSettled(txHash, opts.amount, "x402", payerAddress)).catch((err) => {
317
+ console.error("[x402] onSettled callback error:", err);
318
+ opts.onCallbackError?.(err, "onSettled");
319
+ });
152
320
  }
153
321
  next();
154
322
  } catch (err) {
@@ -164,11 +332,13 @@ var MPP_NETWORK = {
164
332
  function createMppChargeHandler(opts) {
165
333
  const networkId = MPP_NETWORK[opts.network];
166
334
  const amountHumanReadable = opts.amount;
335
+ const recipient = resolvePayee(opts.manifest, "mpp-charge");
336
+ const seenTxStore = opts.seenTxStore ?? new InMemorySeenTxStore();
167
337
  const mppx = Mppx.create({
168
338
  secretKey: opts.payeeSecretKey,
169
339
  methods: [
170
340
  stellar.charge({
171
- recipient: opts.manifest.payee,
341
+ recipient,
172
342
  currency: opts.assetContract,
173
343
  network: networkId,
174
344
  feePayer: { envelopeSigner: opts.payeeSecretKey }
@@ -177,12 +347,43 @@ function createMppChargeHandler(opts) {
177
347
  });
178
348
  return async (req, res, next) => {
179
349
  try {
350
+ let payerAddress = null;
351
+ try {
352
+ const authHeader = req.headers["authorization"];
353
+ if (typeof authHeader === "string" && authHeader.startsWith("Payment ")) {
354
+ const credPart = authHeader.replace(/^Payment\s+/, "").split(",").find((p) => p.trim().startsWith("credential="));
355
+ if (credPart) {
356
+ const b64 = credPart.split("=").slice(1).join("=").replace(/^"|"$/g, "");
357
+ const credJson = Buffer.from(b64, "base64").toString("utf8");
358
+ const cred = JSON.parse(credJson);
359
+ const key = cred.sender ?? cred.payload?.sender ?? cred.payload?.from;
360
+ payerAddress = extractPayerAddress(key);
361
+ }
362
+ }
363
+ } catch {
364
+ }
365
+ const idempotencyKey = await paymentIdempotencyKey((name) => {
366
+ const v = req.headers[name.toLowerCase()];
367
+ return Array.isArray(v) ? v[0] : v;
368
+ });
369
+ if (idempotencyKey) {
370
+ const cached = await seenTxStore.get(idempotencyKey);
371
+ if (cached) {
372
+ if (cached.headers) {
373
+ for (const [k, val] of Object.entries(cached.headers)) {
374
+ res.setHeader(k, val);
375
+ }
376
+ }
377
+ next();
378
+ return;
379
+ }
380
+ }
180
381
  const fetchReq = Request.fromNodeListener(req, res);
181
382
  const handler = mppx["stellar/charge"];
182
383
  const result = await handler({
183
384
  amount: amountHumanReadable,
184
385
  currency: opts.assetContract,
185
- recipient: opts.manifest.payee,
386
+ recipient,
186
387
  description: opts.manifest.name
187
388
  })(fetchReq);
188
389
  if (result.status === 402) {
@@ -193,25 +394,130 @@ function createMppChargeHandler(opts) {
193
394
  return;
194
395
  }
195
396
  const receipt = result.withReceipt(new Response(""));
196
- receipt.headers.forEach((v, k) => res.setHeader(k, v));
397
+ const receiptHeaders = {};
398
+ receipt.headers.forEach((v, k) => {
399
+ res.setHeader(k, v);
400
+ receiptHeaders[k] = v;
401
+ });
197
402
  const receiptHeader = receipt.headers.get("payment-receipt");
198
- if (receiptHeader && opts.onSettled) {
403
+ let reference;
404
+ if (receiptHeader) {
199
405
  try {
200
406
  const parsed = JSON.parse(
201
407
  Buffer.from(receiptHeader, "base64").toString("utf8")
202
408
  );
203
- if (parsed.reference) {
204
- await opts.onSettled(parsed.reference, opts.amount, "mpp-charge");
205
- }
409
+ reference = parsed.reference;
206
410
  } catch {
207
411
  }
208
412
  }
413
+ if (idempotencyKey) {
414
+ await seenTxStore.set(idempotencyKey, {
415
+ txHash: reference ?? null,
416
+ headers: receiptHeaders
417
+ });
418
+ }
419
+ if (reference && opts.onSettled) {
420
+ Promise.resolve().then(() => opts.onSettled(reference, opts.amount, "mpp-charge", payerAddress)).catch((err) => {
421
+ console.error("[mpp-charge] onSettled callback error:", err);
422
+ opts.onCallbackError?.(err, "onSettled");
423
+ });
424
+ }
209
425
  next();
210
426
  } catch (err) {
211
427
  next(err);
212
428
  }
213
429
  };
214
430
  }
431
+ function resolveVaultSettlementAddresses(payer, payee) {
432
+ if (!payer) return null;
433
+ return { payer, payee };
434
+ }
435
+ function buildSessionSettlementTransaction(source, vaultContractId, channelId, addresses, amount, voucherCount, networkPassphrase) {
436
+ const vault = new Contract(vaultContractId);
437
+ const op = vault.call(
438
+ "record_session_settlement",
439
+ nativeToScVal(channelId, { type: "address" }),
440
+ nativeToScVal(addresses.payer, { type: "address" }),
441
+ nativeToScVal(addresses.payee, { type: "address" }),
442
+ nativeToScVal(amount, { type: "i128" }),
443
+ nativeToScVal(voucherCount, { type: "u32" })
444
+ );
445
+ return new TransactionBuilder(source, {
446
+ fee: BASE_FEE,
447
+ networkPassphrase
448
+ }).addOperation(op).setTimeout(30).build();
449
+ }
450
+ function channelAuthorizer(envelopeSigner) {
451
+ return { envelopeSigner };
452
+ }
453
+ function formatMppError(error) {
454
+ if (error instanceof Error) return error.message;
455
+ if (typeof error === "string") return error;
456
+ try {
457
+ const serialized = JSON.stringify(error);
458
+ return serialized === void 0 ? String(error) : serialized;
459
+ } catch {
460
+ return String(error);
461
+ }
462
+ }
463
+ function withTypedChannelErrors(method) {
464
+ return {
465
+ ...method,
466
+ async verify(parameters) {
467
+ try {
468
+ return await method.verify(parameters);
469
+ } catch (error) {
470
+ throw mapChannelError(error);
471
+ }
472
+ }
473
+ };
474
+ }
475
+ function onVerifiedCredential(method, commit) {
476
+ return {
477
+ ...method,
478
+ async verify(parameters) {
479
+ const receipt = await method.verify(parameters);
480
+ await commit(parameters.credential);
481
+ return receipt;
482
+ }
483
+ };
484
+ }
485
+ function mapChannelError(error) {
486
+ const message = formatMppError(error);
487
+ const details = error && typeof error === "object" && "details" in error ? error.details : void 0;
488
+ const reason = details && Object.keys(details).length > 0 ? `${message} (${JSON.stringify(details)})` : message;
489
+ if (/signer mismatch|authori[sz]er|unauthori[sz]ed/i.test(message)) {
490
+ return new Errors.SignerMismatchError({ reason });
491
+ }
492
+ if (/already used|replay|challenge/i.test(message)) {
493
+ return new Errors.InvalidChallengeError({ reason });
494
+ }
495
+ if (/channel has been closed|channel is closed|finalized/i.test(message)) {
496
+ return new Errors.ChannelClosedError({ reason });
497
+ }
498
+ if (/exceeds channel balance|exceeds.*deposit/i.test(message)) {
499
+ return new Errors.AmountExceedsDepositError({ reason });
500
+ }
501
+ if (/channel not found|no channel/i.test(message)) {
502
+ return new Errors.ChannelNotFoundError({ reason });
503
+ }
504
+ if (/must be greater|does not cover|below.*minimum/i.test(message)) {
505
+ return new Errors.DeltaTooSmallError({ reason });
506
+ }
507
+ if (/signature/i.test(message)) {
508
+ return new Errors.InvalidSignatureError({ reason });
509
+ }
510
+ return new Errors.VerificationFailedError({ reason });
511
+ }
512
+
513
+ // src/provider/MppSessionHandler.ts
514
+ function isVoucherStoreValue(value) {
515
+ if (typeof value !== "object" || value === null) {
516
+ return false;
517
+ }
518
+ const candidate = value;
519
+ return typeof candidate.amount === "string" && /^\d+$/.test(candidate.amount);
520
+ }
215
521
  var MPP_NETWORK2 = {
216
522
  testnet: "stellar:testnet",
217
523
  mainnet: "stellar:pubnet"
@@ -220,67 +526,202 @@ function createMppSessionHandler(opts) {
220
526
  const networkId = MPP_NETWORK2[opts.network];
221
527
  const rateHuman = opts.rate;
222
528
  const payeeKeypair = Keypair.fromSecret(opts.payeeSecretKey);
223
- const cumulativeKey = `stellar:channel:cumulative:${opts.channelContract}`;
529
+ const voucherRecordKey = `routedock:session:voucher:${opts.channelFactory}`;
224
530
  const innerStore = Store.memory();
225
- let lastCumulativeAmount = 0n;
531
+ let record = null;
226
532
  let voucherCount = 0;
227
533
  let sessionOpened = false;
534
+ let idleTimer = null;
535
+ let settledCleanly = false;
536
+ let closeListenerArmed = false;
537
+ function clearIdleTimer() {
538
+ if (idleTimer) {
539
+ clearTimeout(idleTimer);
540
+ idleTimer = null;
541
+ }
542
+ }
543
+ function armIdleTimer() {
544
+ if (!opts.idleTimeoutMs) return;
545
+ clearIdleTimer();
546
+ idleTimer = setTimeout(() => {
547
+ void flagOrphan("idle-timeout");
548
+ }, opts.idleTimeoutMs);
549
+ if (typeof idleTimer.unref === "function") idleTimer.unref();
550
+ }
551
+ async function loadPersistedRecord() {
552
+ if (record) return record;
553
+ try {
554
+ const stored = await innerStore.get(voucherRecordKey);
555
+ if (!stored || typeof stored.amount !== "string" || typeof stored.signature !== "string") {
556
+ return null;
557
+ }
558
+ record = { amount: BigInt(stored.amount), signature: stored.signature, payer: stored.payer ?? null };
559
+ } catch {
560
+ return null;
561
+ }
562
+ return record;
563
+ }
564
+ async function flagOrphan(reason) {
565
+ await loadPersistedRecord();
566
+ if (!sessionOpened || settledCleanly) return;
567
+ sessionOpened = false;
568
+ closeListenerArmed = false;
569
+ clearIdleTimer();
570
+ const cumulativeAmount = record ? (Number(record.amount) / 1e7).toFixed(7) : "0.0000000";
571
+ if (opts.onOrphaned) {
572
+ try {
573
+ await opts.onOrphaned(opts.channelFactory, {
574
+ cumulativeAmount,
575
+ lastSignature: record?.signature ?? "",
576
+ voucherCount,
577
+ reason
578
+ });
579
+ } catch (err) {
580
+ console.error("[mpp-session] onOrphaned handler failed:", err);
581
+ }
582
+ }
583
+ }
228
584
  const wrappedStore = {
229
585
  async get(key) {
230
586
  return innerStore.get(key);
231
587
  },
232
588
  async put(key, value) {
233
- await innerStore.put(key, value);
234
- if (key === cumulativeKey && value && typeof value === "object" && "amount" in value) {
235
- lastCumulativeAmount = BigInt(value.amount);
236
- voucherCount++;
237
- if (!sessionOpened) {
238
- sessionOpened = true;
239
- if (opts.onSessionOpen) {
240
- await opts.onSessionOpen(opts.channelContract);
241
- }
242
- }
243
- if (opts.onVoucher) {
244
- const humanAmount = (Number(lastCumulativeAmount) / 1e7).toFixed(7);
245
- await opts.onVoucher(voucherCount, humanAmount);
246
- }
247
- }
589
+ return innerStore.put(key, value);
248
590
  },
249
591
  async delete(key) {
250
592
  return innerStore.delete(key);
593
+ },
594
+ async update(key, fn) {
595
+ const storeWithUpdate = innerStore;
596
+ if (typeof storeWithUpdate.update === "function") {
597
+ return storeWithUpdate.update(key, fn);
598
+ }
599
+ throw new Error("Store does not support atomic update operations");
251
600
  }
252
601
  };
602
+ async function commit(credential) {
603
+ await loadPersistedRecord();
604
+ const payload = credential.payload;
605
+ if (typeof payload?.amount !== "string" || typeof payload.signature !== "string") return;
606
+ let amount;
607
+ try {
608
+ amount = BigInt(payload.amount);
609
+ } catch {
610
+ return;
611
+ }
612
+ if (record && amount < record.amount) return;
613
+ const payer = record?.payer ?? extractPayerAddress(credential.source);
614
+ record = { amount, signature: payload.signature, payer };
615
+ await innerStore.put(voucherRecordKey, {
616
+ amount: record.amount.toString(),
617
+ signature: record.signature,
618
+ payer: record.payer
619
+ });
620
+ voucherCount++;
621
+ settledCleanly = false;
622
+ armIdleTimer();
623
+ if (!sessionOpened) {
624
+ sessionOpened = true;
625
+ if (opts.onSessionOpen) {
626
+ Promise.resolve().then(() => opts.onSessionOpen(opts.channelFactory, record.payer)).catch((err) => {
627
+ console.error("[mpp-session] onSessionOpen callback error:", err);
628
+ opts.onCallbackError?.(err, "onSessionOpen");
629
+ });
630
+ }
631
+ }
632
+ if (opts.onVoucher) {
633
+ const humanAmount = (Number(record.amount) / 1e7).toFixed(7);
634
+ Promise.resolve().then(() => opts.onVoucher(opts.channelFactory, voucherCount, humanAmount, record.signature)).catch((err) => {
635
+ console.error("[mpp-session] onVoucher callback error:", err);
636
+ opts.onCallbackError?.(err, "onVoucher");
637
+ });
638
+ }
639
+ }
253
640
  const mppx = Mppx.create({
254
641
  secretKey: opts.payeeSecretKey,
255
642
  methods: [
256
- stellar$1.channel({
257
- channel: opts.channelContract,
258
- commitmentKey: opts.commitmentPublicKey,
259
- network: networkId,
260
- store: wrappedStore,
261
- sourceAccount: payeeKeypair.publicKey(),
262
- feePayer: { envelopeSigner: payeeKeypair }
263
- })
643
+ withTypedChannelErrors(
644
+ onVerifiedCredential(
645
+ stellar$1.channel({
646
+ channel: opts.channelFactory,
647
+ commitmentKey: opts.commitmentPublicKey,
648
+ network: networkId,
649
+ store: wrappedStore,
650
+ sourceAccount: payeeKeypair.publicKey(),
651
+ feePayer: channelAuthorizer(payeeKeypair)
652
+ }),
653
+ commit
654
+ )
655
+ )
264
656
  ]
265
657
  });
266
- let lastSignatureHex = "";
267
658
  return async (req, res, next) => {
268
659
  try {
269
660
  if (req.method === "DELETE") {
661
+ await loadPersistedRecord();
270
662
  const body = req.body;
271
- const closeAmount = body?.amount ? BigInt(body.amount) : lastCumulativeAmount;
272
- const closeSig = body?.signature ?? lastSignatureHex;
663
+ const bodyAmount = body?.amount ? BigInt(body.amount) : 0n;
664
+ const recordAmount = record?.amount ?? 0n;
665
+ let closeAmount;
666
+ let closeSig;
667
+ if (bodyAmount > recordAmount) {
668
+ closeAmount = bodyAmount;
669
+ closeSig = body?.signature ?? record?.signature ?? "";
670
+ } else {
671
+ closeAmount = recordAmount;
672
+ closeSig = record?.signature ?? "";
673
+ }
273
674
  if (closeAmount > 0n && closeSig) {
675
+ const closePayer = record?.payer ?? null;
274
676
  const closeTxHash = await close({
275
- channel: opts.channelContract,
677
+ channel: opts.channelFactory,
276
678
  amount: closeAmount,
277
679
  signature: Buffer.from(closeSig, "hex"),
278
- feePayer: { envelopeSigner: payeeKeypair },
680
+ feePayer: channelAuthorizer(payeeKeypair),
279
681
  network: networkId
280
682
  });
683
+ settledCleanly = true;
684
+ clearIdleTimer();
281
685
  if (opts.onSettled) {
282
- const totalPaid = (Number(lastCumulativeAmount) / 1e7).toFixed(7);
283
- await opts.onSettled(closeTxHash, totalPaid, "mpp-session");
686
+ const totalPaid = (Number(closeAmount) / 1e7).toFixed(7);
687
+ Promise.resolve().then(() => opts.onSettled(closeTxHash, totalPaid, "mpp-session", closePayer)).catch((err) => {
688
+ console.error("[mpp-session] onSettled callback error:", err);
689
+ opts.onCallbackError?.(err, "onSettled");
690
+ });
691
+ }
692
+ const vaultContract = process.env.AGENT_VAULT_CONTRACT;
693
+ if (vaultContract) {
694
+ const settlementAddresses = resolveVaultSettlementAddresses(
695
+ closePayer,
696
+ payeeKeypair.publicKey()
697
+ );
698
+ if (!settlementAddresses) {
699
+ console.error("[mpp-session] skipped session_settled vault record: payer address unavailable");
700
+ } else {
701
+ try {
702
+ const { Networks } = await import('@stellar/stellar-sdk');
703
+ const { Server } = await import('@stellar/stellar-sdk/rpc');
704
+ const networkPassphrase = opts.network === "mainnet" ? Networks.PUBLIC : Networks.TESTNET;
705
+ const rpcUrl = opts.network === "mainnet" ? "https://mainnet.sorobanrpc.com" : "https://soroban-testnet.stellar.org";
706
+ const server = new Server(rpcUrl);
707
+ const payeePublicKey = payeeKeypair.publicKey();
708
+ const sourceAccount = await server.getAccount(payeePublicKey);
709
+ const tx = buildSessionSettlementTransaction(
710
+ sourceAccount,
711
+ vaultContract,
712
+ opts.channelFactory,
713
+ settlementAddresses,
714
+ closeAmount,
715
+ voucherCount,
716
+ networkPassphrase
717
+ );
718
+ const preparedTx = await server.prepareTransaction(tx);
719
+ preparedTx.sign(payeeKeypair);
720
+ await server.sendTransaction(preparedTx);
721
+ } catch (recordErr) {
722
+ console.error("[mpp-session] failed to record session_settled on vault:", recordErr);
723
+ }
724
+ }
284
725
  }
285
726
  res.json({ closeTxHash });
286
727
  } else {
@@ -288,23 +729,12 @@ function createMppSessionHandler(opts) {
288
729
  }
289
730
  sessionOpened = false;
290
731
  voucherCount = 0;
291
- lastCumulativeAmount = 0n;
732
+ record = null;
733
+ await innerStore.delete(voucherRecordKey);
734
+ closeListenerArmed = false;
735
+ clearIdleTimer();
292
736
  return;
293
737
  }
294
- const authHeader = req.headers["authorization"];
295
- if (typeof authHeader === "string" && authHeader.startsWith("Payment ")) {
296
- try {
297
- const credB64 = authHeader.replace(/^Payment\s+/, "").split(",").find((p) => p.trim().startsWith("credential="));
298
- if (credB64) {
299
- const credJson = Buffer.from(credB64.split("=").slice(1).join("=").replace(/^"|"$/g, ""), "base64").toString("utf8");
300
- const cred = JSON.parse(credJson);
301
- if (cred.payload?.signature) {
302
- lastSignatureHex = cred.payload.signature;
303
- }
304
- }
305
- } catch {
306
- }
307
- }
308
738
  const fetchReq = Request.fromNodeListener(req, res);
309
739
  const result = await mppx.channel({
310
740
  amount: rateHuman,
@@ -318,28 +748,100 @@ function createMppSessionHandler(opts) {
318
748
  res.send(body);
319
749
  return;
320
750
  }
751
+ if (!closeListenerArmed) {
752
+ closeListenerArmed = true;
753
+ req.on("close", () => {
754
+ void flagOrphan("connection-closed");
755
+ });
756
+ }
321
757
  next();
322
758
  } catch (err) {
323
759
  next(err);
324
760
  }
325
761
  };
326
762
  }
763
+ var MANIFEST_SIGNATURE_VERSION = "2";
764
+ function canonicalize(value) {
765
+ if (Array.isArray(value)) {
766
+ return value.map(canonicalize);
767
+ }
768
+ if (value !== null && typeof value === "object") {
769
+ return Object.fromEntries(
770
+ Object.keys(value).sort().map((key) => [key, canonicalize(value[key])])
771
+ );
772
+ }
773
+ return value;
774
+ }
775
+ function manifestDigest(manifest) {
776
+ const { signature: _omit, ...rest } = manifest;
777
+ const canonical = JSON.stringify(canonicalize(rest));
778
+ return createHash("sha256").update(canonical, "utf8").digest();
779
+ }
780
+ function signManifest(manifest, payeeSecretKey) {
781
+ const keypair = Keypair.fromSecret(payeeSecretKey);
782
+ const versionedManifest = {
783
+ ...manifest,
784
+ signature_version: MANIFEST_SIGNATURE_VERSION
785
+ };
786
+ const digest = manifestDigest(versionedManifest);
787
+ const sig = keypair.sign(digest);
788
+ return { ...versionedManifest, signature: Buffer.from(sig).toString("base64") };
789
+ }
790
+ function verifyManifestSignature(manifest, expectedPayee) {
791
+ if (!manifest.signature) {
792
+ throw new RouteDockSignatureError(
793
+ "Manifest is missing a signature field \u2014 cannot verify payee authenticity"
794
+ );
795
+ }
796
+ if (manifest.signature_version !== MANIFEST_SIGNATURE_VERSION) {
797
+ throw new RouteDockSignatureError(
798
+ `Manifest signature version must be ${MANIFEST_SIGNATURE_VERSION}; legacy signatures are not safe because nested fields were excluded from their digest`
799
+ );
800
+ }
801
+ const keypair = Keypair.fromPublicKey(manifest.payee);
802
+ const digest = manifestDigest(manifest);
803
+ let sigBytes;
804
+ try {
805
+ sigBytes = Buffer.from(manifest.signature, "base64");
806
+ } catch {
807
+ throw new RouteDockSignatureError("Manifest signature is not valid base64");
808
+ }
809
+ let valid = false;
810
+ try {
811
+ valid = keypair.verify(digest, sigBytes);
812
+ } catch {
813
+ throw new RouteDockSignatureError(
814
+ `Manifest signature verification failed for payee ${manifest.payee}`
815
+ );
816
+ }
817
+ if (!valid) {
818
+ throw new RouteDockSignatureError(
819
+ `Manifest signature verification failed for payee ${manifest.payee}`
820
+ );
821
+ }
822
+ }
327
823
 
328
824
  // src/provider/routedockMiddleware.ts
825
+ var MPP_MODES = ["mpp-session", "mpp-charge"];
329
826
  function routedock(opts) {
330
- const handlers = [];
827
+ const startTime = Date.now();
828
+ const signedManifest = signManifest(opts.manifest, opts.payeeSecretKey);
829
+ const handlerMap = /* @__PURE__ */ new Map();
331
830
  if (opts.modes.includes("x402")) {
332
831
  const x402Price = opts.pricing.x402;
333
832
  if (x402Price) {
334
- handlers.push(
833
+ handlerMap.set(
834
+ "x402",
335
835
  createX402Handler({
336
836
  payeeSecretKey: opts.payeeSecretKey,
337
837
  network: opts.network,
338
838
  amount: x402Price,
339
839
  assetContract: opts.assetContract,
340
840
  ...opts.facilitatorApiKey ? { facilitatorApiKey: opts.facilitatorApiKey } : {},
341
- manifest: opts.manifest,
342
- ...opts.onSettled ? { onSettled: opts.onSettled } : {}
841
+ manifest: signedManifest,
842
+ ...opts.onSettled ? { onSettled: opts.onSettled } : {},
843
+ ...opts.onCallbackError ? { onCallbackError: opts.onCallbackError } : {},
844
+ ...opts.seenTxStore ? { seenTxStore: opts.seenTxStore } : {}
343
845
  })
344
846
  );
345
847
  }
@@ -347,14 +849,17 @@ function routedock(opts) {
347
849
  if (opts.modes.includes("mpp-charge")) {
348
850
  const chargePrice = opts.pricing["mpp-charge"];
349
851
  if (chargePrice) {
350
- handlers.push(
852
+ handlerMap.set(
853
+ "mpp-charge",
351
854
  createMppChargeHandler({
352
855
  payeeSecretKey: opts.payeeSecretKey,
353
856
  network: opts.network,
354
857
  amount: chargePrice,
355
858
  assetContract: opts.assetContract,
356
- manifest: opts.manifest,
357
- ...opts.onSettled ? { onSettled: opts.onSettled } : {}
859
+ manifest: signedManifest,
860
+ ...opts.onSettled ? { onSettled: opts.onSettled } : {},
861
+ ...opts.onCallbackError ? { onCallbackError: opts.onCallbackError } : {},
862
+ ...opts.seenTxStore ? { seenTxStore: opts.seenTxStore } : {}
358
863
  })
359
864
  );
360
865
  }
@@ -365,39 +870,52 @@ function routedock(opts) {
365
870
  if (!opts.commitmentPublicKey) {
366
871
  throw new Error("routedock: mpp-session mode requires commitmentPublicKey");
367
872
  }
368
- handlers.push(
873
+ handlerMap.set(
874
+ "mpp-session",
369
875
  createMppSessionHandler({
370
876
  payeeSecretKey: opts.payeeSecretKey,
371
877
  network: opts.network,
372
- channelContract: sessionPricing.channelContract,
878
+ channelFactory: sessionPricing.channelFactory,
373
879
  rate: sessionPricing.rate,
374
880
  assetContract: opts.assetContract,
375
- manifest: opts.manifest,
881
+ manifest: signedManifest,
376
882
  commitmentPublicKey: opts.commitmentPublicKey,
377
883
  ...opts.onSettled ? { onSettled: opts.onSettled } : {},
378
884
  ...opts.onSessionOpen ? { onSessionOpen: opts.onSessionOpen } : {},
379
- ...opts.onVoucher ? { onVoucher: opts.onVoucher } : {}
885
+ ...opts.onVoucher ? { onVoucher: opts.onVoucher } : {},
886
+ ...opts.onCallbackError ? { onCallbackError: opts.onCallbackError } : {},
887
+ ...opts.onOrphaned ? { onOrphaned: opts.onOrphaned } : {},
888
+ ...opts.idleTimeoutMs != null ? { idleTimeoutMs: opts.idleTimeoutMs } : {}
380
889
  })
381
890
  );
382
891
  }
383
892
  }
893
+ const defaultMode = MPP_MODES.find((mode) => handlerMap.has(mode)) ?? (handlerMap.has("x402") ? "x402" : void 0);
384
894
  return (req, res, next) => {
385
895
  if (req.path === "/.well-known/routedock.json") {
386
- res.json(opts.manifest);
896
+ res.json(signedManifest);
897
+ return;
898
+ }
899
+ if (req.path === "/health") {
900
+ res.json({
901
+ status: "ok",
902
+ routedock: opts.manifest.routedock,
903
+ network: opts.network,
904
+ payee: opts.payee,
905
+ modesActive: opts.modes,
906
+ uptime: Math.floor((Date.now() - startTime) / 1e3)
907
+ });
387
908
  return;
388
909
  }
389
- if (handlers.length === 0) {
910
+ if (handlerMap.size === 0) {
390
911
  next();
391
912
  return;
392
913
  }
393
914
  const hasX402Header = !!(req.headers["payment-signature"] || req.headers["x-payment"]);
394
915
  const prefersX402 = req.headers["x-preferred-mode"] === "x402";
395
- let handler;
396
- if (hasX402Header || prefersX402) {
397
- handler = handlers[0];
398
- } else {
399
- handler = handlers[handlers.length - 1];
400
- }
916
+ const x402Handler = handlerMap.get("x402");
917
+ const defaultHandler = defaultMode ? handlerMap.get(defaultMode) : void 0;
918
+ const handler = hasX402Header || prefersX402 ? x402Handler ?? defaultHandler : defaultHandler;
401
919
  if (!handler) {
402
920
  next();
403
921
  return;
@@ -410,6 +928,148 @@ function routedock(opts) {
410
928
  })).then(() => next()).catch((err) => next(err));
411
929
  };
412
930
  }
931
+ var MPP_NETWORK3 = {
932
+ testnet: "stellar:testnet",
933
+ mainnet: "stellar:pubnet"
934
+ };
935
+ function decimalToStroops(amountStr) {
936
+ return usdcToStroops(amountStr);
937
+ }
938
+ async function reconcileAbandonedSessions(opts) {
939
+ const stats = {
940
+ orphanedCount: 0,
941
+ recoveredCount: 0,
942
+ skippedCount: 0,
943
+ failedCount: 0,
944
+ errors: []
945
+ };
946
+ const payeeKeypair = Keypair.fromSecret(opts.payeeSecretKey);
947
+ const networkId = MPP_NETWORK3[opts.network];
948
+ try {
949
+ const { data: closingSessions, error } = await opts.supabase.from("sessions").select("channel_id, channel_contract, cumulative_amount, last_signature, settlement_tx_hash").eq("status", "closing").eq("network", opts.network).eq("payee", payeeKeypair.publicKey()).is("settlement_tx_hash", null).not("last_signature", "is", null).order("updated_at", { ascending: true }).limit(100);
950
+ if (error) {
951
+ throw new Error(`Failed to query sessions: ${error.message}`);
952
+ }
953
+ stats.orphanedCount = closingSessions?.length ?? 0;
954
+ if (!closingSessions || closingSessions.length === 0) {
955
+ return stats;
956
+ }
957
+ for (const session of closingSessions) {
958
+ try {
959
+ const {
960
+ channel_id: sessionId,
961
+ channel_contract: channelContract,
962
+ cumulative_amount,
963
+ last_signature
964
+ } = session;
965
+ const channelId = channelContract || sessionId;
966
+ if (!channelId || !cumulative_amount || !last_signature) {
967
+ stats.skippedCount++;
968
+ continue;
969
+ }
970
+ const closeAmount = decimalToStroops(cumulative_amount);
971
+ const closeSig = Buffer.from(last_signature, "hex");
972
+ const closeFn = opts.channelClose ?? (await import('@stellar/mpp/channel/server')).close;
973
+ const closeTxHash = await closeFn({
974
+ channel: channelId,
975
+ amount: closeAmount,
976
+ signature: closeSig,
977
+ feePayer: channelAuthorizer(payeeKeypair),
978
+ network: networkId
979
+ });
980
+ const totalPaid = (Number(closeAmount) / 1e7).toFixed(7);
981
+ const { error: updateError } = await opts.supabase.from("sessions").update({
982
+ status: "closed",
983
+ settlement_tx_hash: closeTxHash,
984
+ updated_at: (/* @__PURE__ */ new Date()).toISOString()
985
+ }).eq("channel_id", sessionId);
986
+ if (updateError) {
987
+ throw new Error(`Failed to update session: ${updateError.message}`);
988
+ }
989
+ stats.recoveredCount++;
990
+ if (opts.onRecovered) {
991
+ await opts.onRecovered(channelId, closeTxHash, totalPaid);
992
+ }
993
+ } catch (err) {
994
+ stats.failedCount++;
995
+ stats.errors.push({
996
+ channelId: session.channel_id,
997
+ reason: formatMppError(err)
998
+ });
999
+ }
1000
+ }
1001
+ return stats;
1002
+ } catch (err) {
1003
+ throw new Error(
1004
+ `Session reconciliation failed: ${formatMppError(err)}`
1005
+ );
1006
+ }
1007
+ }
1008
+ async function runStartupReconciliation(opts, logger) {
1009
+ const log = logger || console.log;
1010
+ log("[SessionReconciler] Running startup reconciliation...");
1011
+ try {
1012
+ const stats = await reconcileAbandonedSessions(opts);
1013
+ if (stats.orphanedCount === 0) {
1014
+ log("[SessionReconciler] No orphaned sessions found");
1015
+ return;
1016
+ }
1017
+ log(
1018
+ `[SessionReconciler] Found ${stats.orphanedCount} orphaned session(s). Recovered: ${stats.recoveredCount}, Skipped: ${stats.skippedCount}, Failed: ${stats.failedCount}`
1019
+ );
1020
+ if (stats.errors.length > 0) {
1021
+ log("[SessionReconciler] Reconciliation errors:");
1022
+ for (const err of stats.errors) {
1023
+ log(` - ${err.channelId}: ${err.reason}`);
1024
+ }
1025
+ }
1026
+ } catch (err) {
1027
+ log(`[SessionReconciler] Startup reconciliation failed: ${formatMppError(err)}`);
1028
+ }
1029
+ }
1030
+
1031
+ // src/provider/registerProvider.ts
1032
+ async function registerProvider(optionsOrSupabase, manifestArg, baseUrlArg, verifiedArg) {
1033
+ let supabase;
1034
+ let manifest;
1035
+ let baseUrl;
1036
+ let verified;
1037
+ if ("supabase" in optionsOrSupabase) {
1038
+ supabase = optionsOrSupabase.supabase;
1039
+ manifest = optionsOrSupabase.manifest;
1040
+ baseUrl = optionsOrSupabase.baseUrl;
1041
+ verified = optionsOrSupabase.verified ?? true;
1042
+ } else {
1043
+ supabase = optionsOrSupabase;
1044
+ if (!manifestArg || !baseUrlArg) {
1045
+ throw new RouteDockManifestError("manifest and baseUrl are required for registerProvider");
1046
+ }
1047
+ manifest = manifestArg;
1048
+ baseUrl = baseUrlArg;
1049
+ verified = verifiedArg ?? true;
1050
+ }
1051
+ verifyManifestSignature(manifest);
1052
+ const normalizedBaseUrl = baseUrl.replace(/\/$/, "");
1053
+ const { error } = await supabase.from("providers").upsert(
1054
+ {
1055
+ name: manifest.name,
1056
+ description: manifest.description ?? null,
1057
+ base_url: normalizedBaseUrl,
1058
+ modes: manifest.modes,
1059
+ tags: manifest.tags ?? [],
1060
+ categories: manifest.categories ?? [],
1061
+ network: manifest.network,
1062
+ payee: manifest.payee,
1063
+ manifest,
1064
+ verified,
1065
+ registered_at: (/* @__PURE__ */ new Date()).toISOString()
1066
+ },
1067
+ { onConflict: "base_url" }
1068
+ );
1069
+ if (error) {
1070
+ throw new Error(`Failed to register provider in Supabase: ${error.message}`);
1071
+ }
1072
+ }
413
1073
 
414
1074
  // src/store/SessionStore.ts
415
1075
  var SupabaseSessionStore = class {
@@ -419,12 +1079,16 @@ var SupabaseSessionStore = class {
419
1079
  supabase;
420
1080
  async get(channelId) {
421
1081
  const { data, error } = await this.supabase.from("sessions").select("*").eq("channel_id", channelId).maybeSingle();
422
- if (error) throw new RouteDockSessionError(`SessionStore.get failed: ${error.message}`);
1082
+ if (error) {
1083
+ throw new RouteDockNetworkError(`SessionStore.get failed: ${error.message}`);
1084
+ }
423
1085
  if (!data) return null;
424
1086
  return {
425
1087
  channel_id: data.channel_id,
426
1088
  payee: data.payee,
427
1089
  payer: data.payer,
1090
+ channel_contract: data.channel_contract,
1091
+ network: data.network,
428
1092
  cumulative_amount: String(data.cumulative_amount),
429
1093
  last_signature: data.last_signature,
430
1094
  status: data.status,
@@ -439,7 +1103,7 @@ var SupabaseSessionStore = class {
439
1103
  const prev = parseFloat(existing.cumulative_amount);
440
1104
  const next = parseFloat(state.cumulative_amount);
441
1105
  if (next <= prev) {
442
- throw new RouteDockSessionError(
1106
+ throw new RouteDockVoucherMonotonicityError(
443
1107
  `cumulative_amount must be strictly increasing: ${next} <= ${prev}`
444
1108
  );
445
1109
  }
@@ -449,6 +1113,8 @@ var SupabaseSessionStore = class {
449
1113
  channel_id: state.channel_id,
450
1114
  payee: state.payee,
451
1115
  payer: state.payer,
1116
+ channel_contract: state.channel_contract,
1117
+ network: state.network,
452
1118
  cumulative_amount: state.cumulative_amount,
453
1119
  last_signature: state.last_signature,
454
1120
  status: state.status,
@@ -460,17 +1126,62 @@ var SupabaseSessionStore = class {
460
1126
  );
461
1127
  if (error) {
462
1128
  if (error.message.includes("strictly increasing")) {
463
- throw new RouteDockSessionError(
1129
+ throw new RouteDockVoucherMonotonicityError(
464
1130
  `cumulative_amount must be strictly increasing (DB): ${error.message}`
465
1131
  );
466
1132
  }
467
- throw new RouteDockSessionError(`SessionStore.upsert failed: ${error.message}`);
1133
+ throw new RouteDockNetworkError(`SessionStore.upsert failed: ${error.message}`);
1134
+ }
1135
+ }
1136
+ async setStatus(channelId, status, settlementTxHash) {
1137
+ const payload = {
1138
+ status,
1139
+ updated_at: (/* @__PURE__ */ new Date()).toISOString()
1140
+ };
1141
+ if (settlementTxHash !== void 0) {
1142
+ payload.settlement_tx_hash = settlementTxHash;
1143
+ }
1144
+ const { error } = await this.supabase.from("sessions").update(payload).eq("channel_id", channelId);
1145
+ if (error) {
1146
+ throw new RouteDockNetworkError(`SessionStore.setStatus failed: ${error.message}`);
468
1147
  }
469
1148
  }
470
1149
  async close(channelId) {
471
- const { error } = await this.supabase.from("sessions").update({ status: "closed", updated_at: (/* @__PURE__ */ new Date()).toISOString() }).eq("channel_id", channelId);
472
- if (error) throw new RouteDockSessionError(`SessionStore.close failed: ${error.message}`);
1150
+ await this.setStatus(channelId, "closed");
1151
+ }
1152
+ };
1153
+
1154
+ // src/store/SupabaseSeenTxStore.ts
1155
+ var SupabaseSeenTxStore = class {
1156
+ constructor(supabase) {
1157
+ this.supabase = supabase;
1158
+ }
1159
+ supabase;
1160
+ async get(key) {
1161
+ const { data, error } = await this.supabase.from("settlements").select("tx_hash, headers").eq("key", key).maybeSingle();
1162
+ if (error) {
1163
+ throw new RouteDockNetworkError(`SupabaseSeenTxStore.get failed: ${error.message}`);
1164
+ }
1165
+ if (!data) return void 0;
1166
+ const headers = data.headers;
1167
+ return {
1168
+ txHash: data.tx_hash ?? null,
1169
+ ...headers ? { headers } : {}
1170
+ };
1171
+ }
1172
+ async set(key, record) {
1173
+ const { error } = await this.supabase.from("settlements").upsert(
1174
+ {
1175
+ key,
1176
+ tx_hash: record.txHash,
1177
+ headers: record.headers ?? null
1178
+ },
1179
+ { onConflict: "key" }
1180
+ );
1181
+ if (error) {
1182
+ throw new RouteDockNetworkError(`SupabaseSeenTxStore.set failed: ${error.message}`);
1183
+ }
473
1184
  }
474
1185
  };
475
1186
 
476
- export { RouteDockError, RouteDockManifestError, RouteDockNoSupportedModeError, RouteDockPolicyRejectedError, RouteDockSessionError, SupabaseSessionStore, createMppChargeHandler, createMppSessionHandler, createX402Handler, routedock };
1187
+ export { DEFAULT_MAX_SESSION_DURATION_MS, InMemorySeenTxStore, RouteDockChannelStateError, RouteDockClientVersionError, RouteDockDisputeError, RouteDockError, RouteDockFacilitatorError, RouteDockManifestError, RouteDockNetworkError, RouteDockNoSupportedModeError, RouteDockPolicyRejectError, RouteDockPolicyRejectedError, RouteDockRefundWindowError, RouteDockSessionError, RouteDockSignatureError, RouteDockTrustlineError, RouteDockVoucherMonotonicityError, SupabaseSeenTxStore, SupabaseSessionStore, createMppChargeHandler, createMppSessionHandler, createX402Handler, decimalToStroops, isVoucherStoreValue, paymentIdempotencyKey, reconcileAbandonedSessions, registerProvider, routedock, runStartupReconciliation };