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