@routedock/routedock 0.3.0 → 0.4.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 (58) hide show
  1. package/README.md +113 -12
  2. package/dist/{MppSessionHandler-1EL8QBAn.d.cts → MppSessionHandler-3QfCHI5x.d.ts} +40 -2
  3. package/dist/{MppSessionHandler-Bq4S-JTE.d.ts → MppSessionHandler-xu4Kxu6N.d.cts} +40 -2
  4. package/dist/{ProviderRegistry-CA_TdREG.d.ts → ProviderRegistry-D9KqyuH-.d.cts} +16 -36
  5. package/dist/{ProviderRegistry-D-0rqK0N.d.cts → ProviderRegistry-DVi2uzgk.d.ts} +16 -36
  6. package/dist/{RouteDockClient-go9aopfF.d.cts → RouteDockClient-6rmgkbYb.d.cts} +124 -142
  7. package/dist/{RouteDockClient-Bh86bVly.d.ts → RouteDockClient-CnAkxwrW.d.ts} +124 -142
  8. package/dist/{SessionReconciler-Bw-PoBVl.d.ts → SessionReconciler-Cce9MXO1.d.ts} +10 -3
  9. package/dist/{SessionReconciler-Cz2nuOlJ.d.cts → SessionReconciler-Dpe9qXdD.d.cts} +10 -3
  10. package/dist/{SessionStore-BaYiegP0.d.ts → SessionStore-DkLBxFQu.d.cts} +28 -3
  11. package/dist/{SessionStore-B4-Hi4H8.d.cts → SessionStore-_ZkoBzFY.d.ts} +28 -3
  12. package/dist/SpendStore-BUclV02i.d.ts +73 -0
  13. package/dist/SpendStore-s_dfcemj.d.cts +73 -0
  14. package/dist/client.cjs +767 -487
  15. package/dist/client.d.cts +80 -10
  16. package/dist/client.d.ts +80 -10
  17. package/dist/client.js +733 -468
  18. package/dist/index.cjs +1295 -590
  19. package/dist/index.d.cts +13 -10
  20. package/dist/index.d.ts +13 -10
  21. package/dist/index.js +1247 -559
  22. package/dist/logger-DVa7VT4c.d.cts +41 -0
  23. package/dist/logger-DVa7VT4c.d.ts +41 -0
  24. package/dist/network.cjs +57 -0
  25. package/dist/network.d.cts +72 -0
  26. package/dist/network.d.ts +72 -0
  27. package/dist/network.js +49 -0
  28. package/dist/node.cjs +166 -0
  29. package/dist/node.d.cts +77 -0
  30. package/dist/node.d.ts +77 -0
  31. package/dist/node.js +159 -0
  32. package/dist/provider/fastify.cjs +847 -62
  33. package/dist/provider/fastify.d.cts +10 -3
  34. package/dist/provider/fastify.d.ts +10 -3
  35. package/dist/provider/fastify.js +848 -63
  36. package/dist/provider/hono.cjs +936 -60
  37. package/dist/provider/hono.d.cts +23 -8
  38. package/dist/provider/hono.d.ts +23 -8
  39. package/dist/provider/hono.js +937 -61
  40. package/dist/provider.cjs +894 -70
  41. package/dist/provider.d.cts +9 -8
  42. package/dist/provider.d.ts +9 -8
  43. package/dist/provider.js +895 -72
  44. package/dist/react.cjs +736 -436
  45. package/dist/react.d.cts +7 -5
  46. package/dist/react.d.ts +7 -5
  47. package/dist/react.js +725 -425
  48. package/dist/{routedockMiddleware-57gUaEMf.d.ts → routedockMiddleware-Cq9HOJEa.d.ts} +9 -2
  49. package/dist/{routedockMiddleware-DlyLrxp7.d.cts → routedockMiddleware-CxsWkSKa.d.cts} +9 -2
  50. package/dist/schema.cjs +7 -0
  51. package/dist/schema.js +7 -0
  52. package/dist/{sign-AKH67sft.d.cts → sign-Bq08SeFq.d.ts} +2 -2
  53. package/dist/{sign-BEq-OsSq.d.ts → sign-DqGE0Plv.d.cts} +2 -2
  54. package/dist/testing.d.cts +4 -3
  55. package/dist/testing.d.ts +4 -3
  56. package/dist/{logger-D8_Q2ytp.d.cts → types-DXaS5Wip.d.cts} +85 -47
  57. package/dist/{logger-D8_Q2ytp.d.ts → types-DXaS5Wip.d.ts} +85 -47
  58. package/package.json +50 -4
package/dist/provider.js CHANGED
@@ -1,15 +1,16 @@
1
- import { Keypair, StrKey, Contract, nativeToScVal, TransactionBuilder, BASE_FEE } from '@stellar/stellar-sdk';
1
+ import { Keypair, StrKey, Networks, Contract, nativeToScVal, TransactionBuilder, BASE_FEE } from '@stellar/stellar-sdk';
2
+ import { usdcToStroops } from '@routedock/nulth-sdk';
3
+ import { Validator } from '@cfworker/json-schema';
2
4
  import { ExactStellarScheme } from '@x402/stellar/exact/facilitator';
3
5
  import { ExactStellarScheme as ExactStellarScheme$1 } from '@x402/stellar/exact/server';
4
6
  import { HTTPFacilitatorClient, x402ResourceServer } from '@x402/core/server';
5
7
  import { createEd25519Signer } from '@x402/stellar';
6
8
  import { encodePaymentRequiredHeader, decodePaymentSignatureHeader, encodePaymentResponseHeader } from '@x402/core/http';
7
- import { usdcToStroops } from '@routedock/nulth-sdk';
8
9
  import { stellar as stellar$1 } from '@stellar/mpp/charge/server';
9
10
  import { Request, Mppx } from 'mppx/server';
10
11
  import { Store, stellar, close } from '@stellar/mpp/channel/server';
11
12
  import { Errors } from 'mppx';
12
- import { createHash } from 'crypto';
13
+ import { sha256 as sha256$1 } from '@noble/hashes/sha2.js';
13
14
 
14
15
  // src/errors.ts
15
16
  var RouteDockError = class extends Error {
@@ -196,13 +197,618 @@ function isAssetEligible(manifest, assetIdentifier, mode, endpoint) {
196
197
  (asset) => asset.asset === assetIdentifier || asset.asset_contract === assetIdentifier
197
198
  );
198
199
  }
200
+ var usdcToUnits = usdcToStroops;
201
+
202
+ // src/schemas/routedock.schema.json
203
+ var routedock_schema_default = {
204
+ $schema: "http://json-schema.org/draft-07/schema#",
205
+ $id: "https://routedock.xyz/schemas/routedock.schema.json",
206
+ title: "RouteDock Manifest",
207
+ description: "Discovery manifest served at /.well-known/routedock.json on every RouteDock provider endpoint",
208
+ type: "object",
209
+ required: [
210
+ "routedock",
211
+ "name",
212
+ "description",
213
+ "modes",
214
+ "network",
215
+ "asset",
216
+ "asset_contract",
217
+ "payee",
218
+ "pricing",
219
+ "endpoints",
220
+ "tags"
221
+ ],
222
+ additionalProperties: false,
223
+ properties: {
224
+ routedock: {
225
+ type: "string",
226
+ pattern: "^[0-9]+\\.[0-9]+$",
227
+ description: "Manifest schema version, e.g. '1.0', '1.1', '2.0'."
228
+ },
229
+ min_client_version: {
230
+ type: "string",
231
+ pattern: "^[0-9]+\\.[0-9]+$",
232
+ description: "Minimum SDK client version (major.minor) required to use this manifest. Clients below this version must refuse to pay."
233
+ },
234
+ deprecated_modes: {
235
+ type: "array",
236
+ items: {
237
+ $ref: "#/definitions/PaymentMode"
238
+ },
239
+ uniqueItems: true,
240
+ description: "Payment modes still listed in 'modes' for backwards compatibility but deprecated for new integrations."
241
+ },
242
+ sunset_at: {
243
+ type: "string",
244
+ format: "date-time",
245
+ description: "ISO 8601 timestamp after which this manifest version is no longer supported by the provider."
246
+ },
247
+ name: {
248
+ type: "string",
249
+ minLength: 1,
250
+ maxLength: 120,
251
+ description: "Human-readable provider name"
252
+ },
253
+ description: {
254
+ type: "string",
255
+ minLength: 1,
256
+ maxLength: 500,
257
+ description: "Human-readable description of the provider's data or service"
258
+ },
259
+ modes: {
260
+ type: "array",
261
+ minItems: 1,
262
+ uniqueItems: true,
263
+ items: {
264
+ $ref: "#/definitions/PaymentMode"
265
+ },
266
+ description: "Payment modes supported by this provider"
267
+ },
268
+ network: {
269
+ type: "string",
270
+ enum: ["testnet", "mainnet"],
271
+ description: "Stellar network this provider operates on"
272
+ },
273
+ asset: {
274
+ type: "string",
275
+ description: "Asset ticker symbol (e.g. 'USDC')"
276
+ },
277
+ asset_contract: {
278
+ type: "string",
279
+ pattern: "^C[A-Z2-7]{55}$",
280
+ description: "Stellar Asset Contract (SAC) address for the payment asset"
281
+ },
282
+ assets: {
283
+ type: "array",
284
+ minItems: 1,
285
+ items: {
286
+ $ref: "#/definitions/AssetConfig"
287
+ },
288
+ description: "Payment assets accepted by this provider. Each asset can be scoped to specific modes and endpoints. When present, assets[0] must match the root-level asset and asset_contract."
289
+ },
290
+ payee: {
291
+ type: "string",
292
+ pattern: "^G[A-Z2-7]{55}$",
293
+ description: "Stellar address (G...) that receives payments"
294
+ },
295
+ pricing: {
296
+ type: "object",
297
+ description: "Pricing configuration per supported payment mode",
298
+ additionalProperties: false,
299
+ properties: {
300
+ x402: {
301
+ $ref: "#/definitions/PricingConfig"
302
+ },
303
+ "mpp-charge": {
304
+ $ref: "#/definitions/PricingConfig"
305
+ },
306
+ "mpp-session": {
307
+ $ref: "#/definitions/SessionPricingConfig"
308
+ },
309
+ "mpp-session-ws": {
310
+ $ref: "#/definitions/SessionPricingConfig",
311
+ description: "WebSocket transport variant of mpp-session: channel opened and first voucher negotiated over HTTP, then the connection upgrades to WebSocket for push-based streaming."
312
+ }
313
+ },
314
+ minProperties: 1
315
+ },
316
+ endpoints: {
317
+ type: "object",
318
+ description: "Map of endpoint name to endpoint descriptors",
319
+ additionalProperties: {
320
+ $ref: "#/definitions/EndpointDescriptor"
321
+ },
322
+ minProperties: 1
323
+ },
324
+ sla: {
325
+ $ref: "#/definitions/SLAConfig"
326
+ },
327
+ tags: {
328
+ type: "array",
329
+ items: {
330
+ type: "string",
331
+ minLength: 1,
332
+ maxLength: 64
333
+ },
334
+ uniqueItems: true,
335
+ description: "Capability tags indexed with trigram search in the provider registry"
336
+ },
337
+ vault: {
338
+ $ref: "#/definitions/VaultMode"
339
+ },
340
+ nulth_account: {
341
+ type: "string",
342
+ description: "Optional Nulth smart account address associated with this provider"
343
+ },
344
+ capabilities: {
345
+ type: "object",
346
+ description: "Optional protocol features this provider supports",
347
+ properties: {
348
+ streaming: {
349
+ type: "array",
350
+ items: {
351
+ type: "string",
352
+ enum: ["sse", "websocket"]
353
+ },
354
+ description: "Supported streaming transports"
355
+ },
356
+ webhooks: {
357
+ type: "boolean",
358
+ description: "Whether webhook callbacks are supported"
359
+ },
360
+ idempotency_keys: {
361
+ type: "boolean",
362
+ description: "Whether idempotency keys are supported"
363
+ },
364
+ content_types: {
365
+ type: "array",
366
+ items: {
367
+ type: "string"
368
+ },
369
+ description: "Supported content types for request/response"
370
+ }
371
+ },
372
+ additionalProperties: false
373
+ },
374
+ regions: {
375
+ type: "array",
376
+ items: {
377
+ type: "string",
378
+ minLength: 2,
379
+ maxLength: 10
380
+ },
381
+ uniqueItems: true,
382
+ description: "IATA airport codes or UN/LOCODE identifiers for regions where this provider has infrastructure (e.g. ['IAD', 'AMS']). Used by agents to select the nearest provider."
383
+ },
384
+ latency_hints: {
385
+ type: "object",
386
+ additionalProperties: {
387
+ type: "number",
388
+ minimum: 0
389
+ },
390
+ description: "Provider-declared p50 latency in milliseconds per region code. Keys must be a subset of 'regions'. Example: { 'IAD': 14, 'AMS': 22 }."
391
+ },
392
+ signature: {
393
+ type: "string",
394
+ description: "Base64 Ed25519 signature (payee keypair) over the SHA-256 hash of this manifest with the signature field omitted. Clients must verify before trusting any routing field."
395
+ },
396
+ signature_version: {
397
+ type: "string",
398
+ const: "2",
399
+ description: "Manifest signature format. Version 2 recursively includes every nested manifest field in the signed digest."
400
+ },
401
+ categories: {
402
+ type: "array",
403
+ items: {
404
+ type: "string",
405
+ minLength: 1,
406
+ maxLength: 128
407
+ },
408
+ uniqueItems: true,
409
+ description: "Optional hierarchical categories from a published taxonomy (e.g. data/price/crypto)"
410
+ }
411
+ },
412
+ definitions: {
413
+ VaultMode: {
414
+ type: "string",
415
+ enum: ["local-key", "agent-vault", "nulth"],
416
+ description: "Agent vault / custody mode for payments from this provider"
417
+ },
418
+ PaymentMode: {
419
+ type: "string",
420
+ enum: ["x402", "mpp-charge", "mpp-session", "mpp-session-ws"],
421
+ description: "A supported RouteDock payment mode"
422
+ },
423
+ AssetConfig: {
424
+ type: "object",
425
+ description: "Payment asset configuration with optional mode and endpoint scoping",
426
+ required: ["asset", "asset_contract"],
427
+ additionalProperties: false,
428
+ properties: {
429
+ asset: {
430
+ type: "string",
431
+ description: "Asset ticker symbol (e.g. 'USDC' or 'XLM')"
432
+ },
433
+ asset_contract: {
434
+ type: "string",
435
+ description: "Stellar Asset Contract (SAC) address for the payment asset"
436
+ },
437
+ modes: {
438
+ type: "array",
439
+ items: {
440
+ $ref: "#/definitions/PaymentMode"
441
+ },
442
+ uniqueItems: true,
443
+ description: "Optional: restrict this asset to specific payment modes. If omitted, available for all modes."
444
+ },
445
+ endpoints: {
446
+ type: "array",
447
+ items: {
448
+ type: "string"
449
+ },
450
+ uniqueItems: true,
451
+ description: "Optional: restrict this asset to specific endpoints (by name). If omitted, available for all endpoints."
452
+ }
453
+ }
454
+ },
455
+ PricingConfig: {
456
+ type: "object",
457
+ description: "Per-request pricing for x402 or mpp-charge modes",
458
+ required: ["amount", "per"],
459
+ additionalProperties: false,
460
+ properties: {
461
+ amount: {
462
+ type: "string",
463
+ pattern: "^[0-9]+(\\.[0-9]{1,7})?$",
464
+ description: "Cost per request in the payment asset (decimal string, e.g. '0.001')"
465
+ },
466
+ per: {
467
+ type: "string",
468
+ const: "request",
469
+ description: "Billing unit \u2014 always 'request' for non-session modes"
470
+ },
471
+ facilitator: {
472
+ type: "string",
473
+ description: "x402 facilitator URL (required for x402 mode). OpenZeppelin Channels endpoint."
474
+ },
475
+ payee: {
476
+ type: "string",
477
+ pattern: "^G[A-Z2-7]{55}$",
478
+ description: "Optional per-mode payee override (Stellar G... address). Directs settlements for this mode to a separate account for treasury separation. Falls back to the top-level 'payee' when omitted."
479
+ }
480
+ }
481
+ },
482
+ SessionPricingConfig: {
483
+ type: "object",
484
+ description: "Per-voucher pricing for mpp-session (one-way-channel) mode",
485
+ required: ["rate", "per", "channel_factory", "min_deposit", "refund_waiting_period_ledgers"],
486
+ additionalProperties: false,
487
+ properties: {
488
+ rate: {
489
+ type: "string",
490
+ pattern: "^[0-9]+(\\.[0-9]{1,7})?$",
491
+ description: "Cost per voucher in the payment asset (decimal string, e.g. '0.0001')"
492
+ },
493
+ per: {
494
+ type: "string",
495
+ const: "voucher",
496
+ description: "Billing unit \u2014 always 'voucher' for session mode"
497
+ },
498
+ channel_factory: {
499
+ type: "string",
500
+ pattern: "^C[A-Z2-7]{55}$",
501
+ description: "Stable Soroban factory address (C...) used to deploy a per-session channel"
502
+ },
503
+ min_deposit: {
504
+ type: "string",
505
+ pattern: "^[0-9]+(\\.[0-9]{1,7})?$",
506
+ description: "Minimum initial deposit to open a channel (decimal string)"
507
+ },
508
+ refund_waiting_period_ledgers: {
509
+ type: "integer",
510
+ minimum: 1,
511
+ description: "Ledgers the server has to settle before the funder can reclaim. Default: 17280 (~24h). Do not reduce in production."
512
+ }
513
+ }
514
+ },
515
+ SLAConfig: {
516
+ type: "object",
517
+ description: "Service Level Agreement for the provider",
518
+ required: ["uptime_30d_percent", "p95_latency_ms"],
519
+ additionalProperties: false,
520
+ properties: {
521
+ uptime_30d_percent: {
522
+ type: "number",
523
+ description: "Uptime percentage over the last 30 days"
524
+ },
525
+ p95_latency_ms: {
526
+ type: "number",
527
+ description: "P95 latency in milliseconds"
528
+ },
529
+ maintenance_windows: {
530
+ type: "array",
531
+ items: {
532
+ type: "object",
533
+ required: ["cron", "duration_minutes"],
534
+ additionalProperties: false,
535
+ properties: {
536
+ cron: {
537
+ type: "string",
538
+ description: "Cron expression for the maintenance window"
539
+ },
540
+ duration_minutes: {
541
+ type: "number",
542
+ description: "Duration of the maintenance window in minutes"
543
+ }
544
+ }
545
+ }
546
+ }
547
+ }
548
+ },
549
+ EndpointDescriptor: {
550
+ type: "object",
551
+ description: "Descriptor for a provider endpoint",
552
+ required: ["method", "path"],
553
+ additionalProperties: false,
554
+ properties: {
555
+ method: {
556
+ type: "string",
557
+ description: "HTTP method for the endpoint"
558
+ },
559
+ path: {
560
+ type: "string",
561
+ description: "Path for the endpoint"
562
+ },
563
+ deprecated: {
564
+ type: "boolean",
565
+ description: "Whether this endpoint is retained only for backwards compatibility"
566
+ },
567
+ sunset_at: {
568
+ type: "string",
569
+ format: "date-time",
570
+ description: "ISO 8601 timestamp after which callers should stop using this endpoint"
571
+ },
572
+ headers: {
573
+ type: "object",
574
+ additionalProperties: {
575
+ type: "string"
576
+ },
577
+ description: "Expected request headers"
578
+ },
579
+ request_schema: {
580
+ description: "JSON schema for the request body"
581
+ },
582
+ response_schema: {
583
+ description: "JSON schema for the response body"
584
+ },
585
+ rate_limit: {
586
+ type: "object",
587
+ required: ["requests", "window_seconds"],
588
+ additionalProperties: false,
589
+ properties: {
590
+ requests: {
591
+ type: "number"
592
+ },
593
+ window_seconds: {
594
+ type: "number"
595
+ }
596
+ }
597
+ }
598
+ }
599
+ }
600
+ }
601
+ };
602
+
603
+ // src/manifest/validate.ts
604
+ var validator = new Validator(routedock_schema_default, "7");
605
+ function validateManifestSchema(manifest, source) {
606
+ const result = validator.validate(manifest);
607
+ if (!result.valid) {
608
+ const msgs = result.errors.map((err) => err.error).join("; ");
609
+ throw new RouteDockManifestError(`Invalid manifest${""}: ${msgs}`);
610
+ }
611
+ }
612
+ function assertManifestValid(manifest, baseUrl) {
613
+ const context = "";
614
+ if (manifest.latency_hints !== void 0) {
615
+ if (!Array.isArray(manifest.regions) || manifest.regions.length === 0) {
616
+ throw new RouteDockManifestError(
617
+ `Invalid manifest${context}: latency_hints defined without declaring regions`
618
+ );
619
+ }
620
+ const regionSet = new Set(manifest.regions);
621
+ for (const region of Object.keys(manifest.latency_hints)) {
622
+ if (!regionSet.has(region)) {
623
+ throw new RouteDockManifestError(
624
+ `Invalid manifest${context}: latency_hints key '${region}' is not declared in regions (${manifest.regions.join(", ")})`
625
+ );
626
+ }
627
+ }
628
+ }
629
+ if (manifest.assets !== void 0) {
630
+ if (!Array.isArray(manifest.assets) || manifest.assets.length === 0) {
631
+ throw new RouteDockManifestError(
632
+ `Invalid manifest${context}: assets must be a non-empty array when defined`
633
+ );
634
+ }
635
+ for (let i = 0; i < manifest.assets.length; i++) {
636
+ const a = manifest.assets[i];
637
+ if (!a || typeof a.asset !== "string" || typeof a.asset_contract !== "string") {
638
+ throw new RouteDockManifestError(
639
+ `Invalid manifest${context}: assets[${i}] must define asset and asset_contract strings`
640
+ );
641
+ }
642
+ }
643
+ const first = manifest.assets[0];
644
+ if (first.asset !== manifest.asset || first.asset_contract !== manifest.asset_contract) {
645
+ throw new RouteDockManifestError(
646
+ `Invalid manifest${context}: assets[0] (${first.asset}:${first.asset_contract}) must match root asset fields (${manifest.asset}:${manifest.asset_contract})`
647
+ );
648
+ }
649
+ }
650
+ assertPricingMatchesModes(manifest, context);
651
+ assertDeprecatedModesListed(manifest, context);
652
+ assertPricesParse(manifest, context);
653
+ }
654
+ function assertPricingMatchesModes(manifest, context) {
655
+ const pricing = manifest.pricing ?? {};
656
+ for (const mode of manifest.modes ?? []) {
657
+ if (pricing[mode] === void 0 || pricing[mode] === null) {
658
+ throw new RouteDockManifestError(
659
+ `Invalid manifest${context}: mode '${mode}' is listed in modes but pricing.${mode} is missing`
660
+ );
661
+ }
662
+ }
663
+ }
664
+ function assertDeprecatedModesListed(manifest, context) {
665
+ if (manifest.deprecated_modes === void 0) return;
666
+ const modes = new Set(manifest.modes ?? []);
667
+ for (const mode of manifest.deprecated_modes) {
668
+ if (!modes.has(mode)) {
669
+ throw new RouteDockManifestError(
670
+ `Invalid manifest${context}: deprecated_modes entry '${mode}' is not listed in modes`
671
+ );
672
+ }
673
+ }
674
+ }
675
+ var PRICE_FIELDS = {
676
+ x402: ["amount"],
677
+ "mpp-charge": ["amount"],
678
+ "mpp-session": ["rate", "min_deposit"],
679
+ "mpp-session-ws": ["rate", "min_deposit"]
680
+ };
681
+ function assertPricesParse(manifest, context) {
682
+ const pricing = manifest.pricing ?? {};
683
+ for (const [mode, fields] of Object.entries(PRICE_FIELDS)) {
684
+ const entry = pricing[mode];
685
+ if (entry === void 0 || entry === null) continue;
686
+ for (const field of fields) {
687
+ const value = entry[field];
688
+ try {
689
+ if (typeof value !== "string") {
690
+ throw new RangeError(`expected a decimal string, got ${typeof value}`);
691
+ }
692
+ usdcToStroops(value);
693
+ } catch (err) {
694
+ const detail = err instanceof Error ? err.message : String(err);
695
+ throw new RouteDockManifestError(
696
+ `Invalid manifest${context}: pricing.${mode}.${field} ${JSON.stringify(value)} is not a valid amount (${detail})`,
697
+ { cause: err }
698
+ );
699
+ }
700
+ }
701
+ }
702
+ }
703
+
704
+ // src/provider/internal/assertProviderConfig.ts
705
+ var FLAT_MODES = ["x402", "mpp-charge"];
706
+ var SESSION_MODES = ["mpp-session", "mpp-session-ws"];
707
+ function sameAmount(adapter, label, optionValue, manifestValue) {
708
+ const parse = (where, value) => {
709
+ try {
710
+ return usdcToStroops(value);
711
+ } catch {
712
+ throw new RouteDockManifestError(
713
+ `${adapter}: ${label} has an unparseable ${where} amount "${value}"`
714
+ );
715
+ }
716
+ };
717
+ return parse("option", optionValue) === parse("manifest", manifestValue);
718
+ }
719
+ function assertProviderConfig(opts, adapterName) {
720
+ const { manifest } = opts;
721
+ if (manifest === null || typeof manifest !== "object") {
722
+ throw new RouteDockManifestError(`${adapterName}: opts.manifest is required and must be an object`);
723
+ }
724
+ try {
725
+ validateManifestSchema(manifest);
726
+ assertManifestValid(manifest);
727
+ } catch (err) {
728
+ if (err instanceof RouteDockManifestError) {
729
+ throw new RouteDockManifestError(`${adapterName}: ${err.message}`, { cause: err });
730
+ }
731
+ throw err;
732
+ }
733
+ const served = new Set(opts.modes.filter((mode) => Boolean(opts.pricing[mode])));
734
+ const advertised = new Set(manifest.modes);
735
+ const notAdvertised = [...served].filter((mode) => !advertised.has(mode));
736
+ const notServed = [...advertised].filter((mode) => !served.has(mode));
737
+ if (notAdvertised.length > 0 || notServed.length > 0) {
738
+ const parts = [];
739
+ if (notAdvertised.length > 0) parts.push(`served but not in manifest.modes: ${notAdvertised.join(", ")}`);
740
+ if (notServed.length > 0) parts.push(`in manifest.modes but not served (missing from modes or pricing): ${notServed.join(", ")}`);
741
+ throw new RouteDockManifestError(`${adapterName}: modes do not match manifest.modes (${parts.join("; ")})`);
742
+ }
743
+ for (const mode of FLAT_MODES) {
744
+ if (!served.has(mode)) continue;
745
+ const optionValue = opts.pricing[mode];
746
+ const manifestValue = manifest.pricing[mode]?.amount;
747
+ if (manifestValue === void 0) {
748
+ throw new RouteDockManifestError(`${adapterName}: manifest.pricing.${mode}.amount is missing but ${mode} is served`);
749
+ }
750
+ if (!sameAmount(adapterName, `pricing.${mode}`, optionValue, manifestValue)) {
751
+ throw new RouteDockManifestError(
752
+ `${adapterName}: pricing.${mode} "${optionValue}" does not match manifest.pricing.${mode}.amount "${manifestValue}"`
753
+ );
754
+ }
755
+ }
756
+ for (const mode of SESSION_MODES) {
757
+ if (!served.has(mode)) continue;
758
+ const optionValue = opts.pricing[mode];
759
+ const manifestValue = manifest.pricing[mode];
760
+ if (!manifestValue) {
761
+ throw new RouteDockManifestError(`${adapterName}: manifest.pricing.${mode} is missing but ${mode} is served`);
762
+ }
763
+ if (!sameAmount(adapterName, `pricing.${mode}.rate`, optionValue.rate, manifestValue.rate)) {
764
+ throw new RouteDockManifestError(
765
+ `${adapterName}: pricing.${mode}.rate "${optionValue.rate}" does not match manifest.pricing.${mode}.rate "${manifestValue.rate}"`
766
+ );
767
+ }
768
+ if (optionValue.channelFactory !== manifestValue.channel_factory) {
769
+ throw new RouteDockManifestError(
770
+ `${adapterName}: pricing.${mode}.channelFactory "${optionValue.channelFactory}" does not match manifest.pricing.${mode}.channel_factory "${manifestValue.channel_factory}"`
771
+ );
772
+ }
773
+ }
774
+ if (opts.assetContract !== void 0 && opts.assetContract !== manifest.asset_contract) {
775
+ throw new RouteDockManifestError(
776
+ `${adapterName}: assetContract "${opts.assetContract}" does not match manifest.asset_contract "${manifest.asset_contract}"`
777
+ );
778
+ }
779
+ if (opts.asset !== void 0 && opts.asset !== manifest.asset) {
780
+ throw new RouteDockManifestError(
781
+ `${adapterName}: asset "${opts.asset}" does not match manifest.asset "${manifest.asset}"`
782
+ );
783
+ }
784
+ if (opts.network !== manifest.network) {
785
+ throw new RouteDockManifestError(
786
+ `${adapterName}: network "${opts.network}" does not match manifest.network "${manifest.network}"`
787
+ );
788
+ }
789
+ let signerPublicKey;
790
+ try {
791
+ signerPublicKey = Keypair.fromSecret(opts.payeeSecretKey).publicKey();
792
+ } catch (err) {
793
+ throw new RouteDockManifestError(`${adapterName}: payeeSecretKey is not a valid Stellar secret key`, { cause: err });
794
+ }
795
+ if (signerPublicKey !== manifest.payee) {
796
+ throw new RouteDockManifestError(
797
+ `${adapterName}: payeeSecretKey belongs to ${signerPublicKey}, not manifest.payee ${manifest.payee}; clients would reject the signed manifest`
798
+ );
799
+ }
800
+ if (opts.payee !== manifest.payee) {
801
+ throw new RouteDockManifestError(
802
+ `${adapterName}: payee "${opts.payee}" does not match manifest.payee "${manifest.payee}"`
803
+ );
804
+ }
805
+ }
199
806
 
200
807
  // src/internal/payee.ts
201
808
  function resolvePayee(manifest, mode) {
202
809
  const override = manifest.pricing[mode]?.payee;
203
810
  return override ?? manifest.payee;
204
811
  }
205
- var usdcToUnits = usdcToStroops;
206
812
  function extractPayerAddress(key) {
207
813
  if (typeof key !== "string") return null;
208
814
  return StrKey.isValidEd25519PublicKey(key) || StrKey.isValidMed25519PublicKey(key) ? key : null;
@@ -227,6 +833,18 @@ function resolveLogger(logger, fallback = consoleLogger) {
227
833
  // src/provider/SeenTxStore.ts
228
834
  var SETTLEMENT_REPLAY_WINDOW_MS = 6e4;
229
835
  var MAX_SETTLEMENT_REPLAYS = 1;
836
+ async function recordSettlement(store, key, record, ctx) {
837
+ try {
838
+ await store.set(key, record);
839
+ } catch (err) {
840
+ ctx.logger("error", `${ctx.label} seenTxStore.set failed after settlement`, { error: err });
841
+ try {
842
+ ctx.onCallbackError?.(err, "seenTxStore.set");
843
+ } catch (cbErr) {
844
+ ctx.logger("error", `${ctx.label} onCallbackError threw`, { error: cbErr });
845
+ }
846
+ }
847
+ }
230
848
  async function checkSettlementReplay(store, key, now = Date.now()) {
231
849
  const record = await store.get(key);
232
850
  if (!record) return { kind: "miss" };
@@ -294,7 +912,7 @@ async function paymentIdempotencyKey(getHeader, scope) {
294
912
  );
295
913
  }
296
914
 
297
- // src/provider/settleResult.ts
915
+ // src/internal/settleResult.ts
298
916
  function readSettleResult(result) {
299
917
  const settle = result;
300
918
  const failed = (reason) => reason ? { ok: false, reason } : { ok: false };
@@ -444,10 +1062,10 @@ function createX402Handler(opts) {
444
1062
  requirements
445
1063
  );
446
1064
  if (!verifyResult.isValid) {
447
- res.status(401).json({
448
- error: "Payment verification failed",
449
- reason: verifyResult.invalidReason
450
- });
1065
+ await respondPaymentRequired(
1066
+ "Payment verification failed",
1067
+ verifyResult.invalidReason
1068
+ );
451
1069
  return;
452
1070
  }
453
1071
  const settleResult = await localFacilitator.settle(
@@ -473,7 +1091,12 @@ function createX402Handler(opts) {
473
1091
  if (typeof paymentResponse === "string") {
474
1092
  headers["X-Payment-Response"] = paymentResponse;
475
1093
  }
476
- await seenTxStore.set(idempotencyKey, { txHash, headers, createdAt: Date.now() });
1094
+ await recordSettlement(
1095
+ seenTxStore,
1096
+ idempotencyKey,
1097
+ { txHash, headers, createdAt: Date.now() },
1098
+ { logger, label: "[x402]", onCallbackError: opts.onCallbackError }
1099
+ );
477
1100
  }
478
1101
  if (txHash && opts.onSettled) {
479
1102
  Promise.resolve().then(() => opts.onSettled(txHash, opts.amount, "x402", payerAddress)).catch((err) => {
@@ -492,6 +1115,51 @@ function createX402Handler(opts) {
492
1115
  }
493
1116
  };
494
1117
  }
1118
+
1119
+ // src/internal/encoding.ts
1120
+ var encoder = new TextEncoder();
1121
+ var decoder = new TextDecoder();
1122
+ function utf8ToBytes(text) {
1123
+ return encoder.encode(text);
1124
+ }
1125
+ function bytesToUtf8(bytes) {
1126
+ return decoder.decode(bytes);
1127
+ }
1128
+ function bytesToBase64(bytes) {
1129
+ const chunk = 32768;
1130
+ let binary = "";
1131
+ for (let i = 0; i < bytes.length; i += chunk) {
1132
+ binary += String.fromCharCode(...bytes.subarray(i, i + chunk));
1133
+ }
1134
+ return btoa(binary);
1135
+ }
1136
+ function base64ToBytes(b64) {
1137
+ const end = b64.indexOf("=");
1138
+ let clean = (end === -1 ? b64 : b64.slice(0, end)).replace(/-/g, "+").replace(/_/g, "/").replace(/[^A-Za-z0-9+/]/g, "");
1139
+ if (clean.length % 4 === 1) clean = clean.slice(0, -1);
1140
+ const binary = atob(clean);
1141
+ const bytes = new Uint8Array(binary.length);
1142
+ for (let i = 0; i < binary.length; i++) {
1143
+ bytes[i] = binary.charCodeAt(i);
1144
+ }
1145
+ return bytes;
1146
+ }
1147
+ function base64ToUtf8(b64) {
1148
+ return bytesToUtf8(base64ToBytes(b64));
1149
+ }
1150
+ function hexToBytes(hex) {
1151
+ const clean = hex.startsWith("0x") ? hex.slice(2) : hex;
1152
+ if (clean.length % 2 !== 0) {
1153
+ throw new Error("hexToBytes: hex string must have an even length");
1154
+ }
1155
+ const bytes = new Uint8Array(clean.length / 2);
1156
+ for (let i = 0; i < bytes.length; i++) {
1157
+ bytes[i] = parseInt(clean.slice(i * 2, i * 2 + 2), 16);
1158
+ }
1159
+ return bytes;
1160
+ }
1161
+
1162
+ // src/provider/MppChargeHandler.ts
495
1163
  var MPP_NETWORK = {
496
1164
  testnet: "stellar:testnet",
497
1165
  mainnet: "stellar:pubnet"
@@ -538,7 +1206,7 @@ function createMppChargeHandler(opts) {
538
1206
  const credPart = authHeader.replace(/^Payment\s+/, "").split(",").find((p) => p.trim().startsWith("credential="));
539
1207
  if (credPart) {
540
1208
  const b64 = credPart.split("=").slice(1).join("=").replace(/^"|"$/g, "");
541
- const credJson = Buffer.from(b64, "base64").toString("utf8");
1209
+ const credJson = base64ToUtf8(b64);
542
1210
  const cred = JSON.parse(credJson);
543
1211
  const key = cred.sender ?? cred.payload?.sender ?? cred.payload?.from;
544
1212
  payerAddress = extractPayerAddress(key);
@@ -600,18 +1268,19 @@ function createMppChargeHandler(opts) {
600
1268
  if (receiptHeader) {
601
1269
  try {
602
1270
  const parsed = JSON.parse(
603
- Buffer.from(receiptHeader, "base64").toString("utf8")
1271
+ base64ToUtf8(receiptHeader)
604
1272
  );
605
1273
  reference = parsed.reference;
606
1274
  } catch {
607
1275
  }
608
1276
  }
609
1277
  if (idempotencyKey) {
610
- await seenTxStore.set(idempotencyKey, {
611
- txHash: reference ?? null,
612
- headers: receiptHeaders,
613
- createdAt: Date.now()
614
- });
1278
+ await recordSettlement(
1279
+ seenTxStore,
1280
+ idempotencyKey,
1281
+ { txHash: reference ?? null, headers: receiptHeaders, createdAt: Date.now() },
1282
+ { logger, label: "[mpp-charge]", onCallbackError: opts.onCallbackError }
1283
+ );
615
1284
  }
616
1285
  if (reference && opts.onSettled) {
617
1286
  Promise.resolve().then(() => opts.onSettled(reference, opts.amount, "mpp-charge", payerAddress)).catch((err) => {
@@ -625,6 +1294,14 @@ function createMppChargeHandler(opts) {
625
1294
  }
626
1295
  };
627
1296
  }
1297
+
1298
+ // src/provider/internal/vaultRpc.ts
1299
+ async function createSorobanRpcServer(rpcUrl) {
1300
+ const { Server } = await import('@stellar/stellar-sdk/rpc');
1301
+ return new Server(rpcUrl);
1302
+ }
1303
+
1304
+ // src/provider/internal/vaultSettlement.ts
628
1305
  function resolveVaultSettlementAddresses(payer, payee) {
629
1306
  if (!payer) return null;
630
1307
  return { payer, payee };
@@ -644,6 +1321,111 @@ function buildSessionSettlementTransaction(source, vaultContractId, channelId, a
644
1321
  networkPassphrase
645
1322
  }).addOperation(op).setTimeout(30).build();
646
1323
  }
1324
+ var DEFAULT_RPC_URLS = {
1325
+ testnet: "https://soroban-testnet.stellar.org",
1326
+ mainnet: "https://mainnet.sorobanrpc.com"
1327
+ };
1328
+ var CONFIRM_TIMEOUT_MS = 12e4;
1329
+ var CONFIRM_POLL_MS = 1e3;
1330
+ function delay(ms) {
1331
+ return new Promise((resolve) => setTimeout(resolve, ms));
1332
+ }
1333
+ function describeSendError(sent) {
1334
+ const errorResult = sent.errorResult;
1335
+ try {
1336
+ const name = errorResult?.result?.().switch?.().name;
1337
+ if (name) return `sendTransaction returned ERROR: ${name}`;
1338
+ } catch {
1339
+ }
1340
+ return `sendTransaction returned status ${sent.status}`;
1341
+ }
1342
+ async function waitForConfirmation(server, txHash) {
1343
+ const deadline = Date.now() + CONFIRM_TIMEOUT_MS;
1344
+ for (; ; ) {
1345
+ const tx = await server.getTransaction(txHash);
1346
+ if (tx.status === "SUCCESS") return "success";
1347
+ if (tx.status === "FAILED") return "failed";
1348
+ if (Date.now() >= deadline) return "timeout";
1349
+ await delay(CONFIRM_POLL_MS);
1350
+ }
1351
+ }
1352
+ async function recordSessionSettlement(request, { logger, onCallbackError }) {
1353
+ const reportFailure = (fields) => {
1354
+ logger("error", "[vault] failed to record session_settled", {
1355
+ channelId: request.channelId,
1356
+ ...fields.txHash ? { txHash: fields.txHash } : {},
1357
+ reason: fields.reason,
1358
+ ...fields.error !== void 0 ? { error: fields.error } : {}
1359
+ });
1360
+ if (onCallbackError) {
1361
+ const err = fields.error ?? new Error(`session_settled vault record failed: ${fields.reason}`);
1362
+ onCallbackError(err, "vaultSettlement");
1363
+ }
1364
+ return { ok: false, reason: fields.reason, ...fields.txHash ? { txHash: fields.txHash } : {} };
1365
+ };
1366
+ try {
1367
+ const payeeKeypair = Keypair.fromSecret(request.payeeSecretKey);
1368
+ const addresses = resolveVaultSettlementAddresses(request.payer, payeeKeypair.publicKey());
1369
+ if (!addresses) {
1370
+ logger("warn", "[vault] skipped session_settled record: payer address unavailable", {
1371
+ channelId: request.channelId
1372
+ });
1373
+ return { ok: false, reason: "payer address unavailable" };
1374
+ }
1375
+ const networkPassphrase = request.network === "mainnet" ? Networks.PUBLIC : Networks.TESTNET;
1376
+ const rpcUrl = request.config.rpcUrl ?? DEFAULT_RPC_URLS[request.network];
1377
+ const server = await createSorobanRpcServer(rpcUrl);
1378
+ const sourceAccount = await server.getAccount(payeeKeypair.publicKey());
1379
+ const tx = buildSessionSettlementTransaction(
1380
+ sourceAccount,
1381
+ request.config.contract,
1382
+ request.channelId,
1383
+ addresses,
1384
+ request.amount,
1385
+ request.voucherCount,
1386
+ networkPassphrase
1387
+ );
1388
+ const preparedTx = await server.prepareTransaction(tx);
1389
+ preparedTx.sign(payeeKeypair);
1390
+ const sent = await server.sendTransaction(preparedTx);
1391
+ if (sent.status === "ERROR") {
1392
+ return reportFailure({
1393
+ txHash: sent.hash || preparedTx.hash().toString("hex"),
1394
+ reason: describeSendError(sent)
1395
+ });
1396
+ }
1397
+ const txHash = sent.hash || preparedTx.hash().toString("hex");
1398
+ const confirmation = await waitForConfirmation(server, txHash);
1399
+ if (confirmation === "success") {
1400
+ logger("debug", "[vault] recorded session_settled", {
1401
+ channelId: request.channelId,
1402
+ txHash
1403
+ });
1404
+ return { ok: true, txHash };
1405
+ }
1406
+ return reportFailure({
1407
+ txHash,
1408
+ reason: confirmation === "failed" ? "transaction failed on-chain" : `confirmation timed out after ${CONFIRM_TIMEOUT_MS}ms`
1409
+ });
1410
+ } catch (err) {
1411
+ const reason = err instanceof Error ? err.message : String(err);
1412
+ return reportFailure({ reason, error: err });
1413
+ }
1414
+ }
1415
+
1416
+ // src/internal/vaultConfig.ts
1417
+ function resolveVaultSettlementConfig(option, logger) {
1418
+ if (option) return option;
1419
+ if (typeof process === "undefined") return void 0;
1420
+ const fromEnv = process.env.AGENT_VAULT_CONTRACT ?? process.env.AGENT_VAULT_CONTRACT_ID;
1421
+ if (!fromEnv) return void 0;
1422
+ logger(
1423
+ "warn",
1424
+ "[vault] configuring the vault contract via the AGENT_VAULT_CONTRACT/AGENT_VAULT_CONTRACT_ID environment variables is deprecated and will be removed in the next minor release \u2014 pass the vaultSettlement option instead",
1425
+ {}
1426
+ );
1427
+ return { contract: fromEnv };
1428
+ }
647
1429
  function channelAuthorizer(envelopeSigner) {
648
1430
  return { envelopeSigner };
649
1431
  }
@@ -729,6 +1511,7 @@ function createMppSessionHandler(opts) {
729
1511
  let record = null;
730
1512
  let voucherCount = 0;
731
1513
  let sessionOpened = false;
1514
+ let orphanFlagged = false;
732
1515
  let idleTimer = null;
733
1516
  let settledCleanly = false;
734
1517
  let closeListenerArmed = false;
@@ -754,6 +1537,14 @@ function createMppSessionHandler(opts) {
754
1537
  return null;
755
1538
  }
756
1539
  record = { amount: BigInt(stored.amount), signature: stored.signature, payer: stored.payer ?? null };
1540
+ if (typeof stored.voucherCount === "number") {
1541
+ voucherCount = stored.voucherCount;
1542
+ }
1543
+ if (typeof stored.sessionOpened === "boolean") {
1544
+ sessionOpened = stored.sessionOpened;
1545
+ } else if (record !== null) {
1546
+ sessionOpened = true;
1547
+ }
757
1548
  } catch {
758
1549
  return null;
759
1550
  }
@@ -761,8 +1552,8 @@ function createMppSessionHandler(opts) {
761
1552
  }
762
1553
  async function flagOrphan(reason) {
763
1554
  await loadPersistedRecord();
764
- if (!sessionOpened || settledCleanly) return;
765
- sessionOpened = false;
1555
+ if (!sessionOpened || settledCleanly || orphanFlagged) return;
1556
+ orphanFlagged = true;
766
1557
  closeListenerArmed = false;
767
1558
  clearIdleTimer();
768
1559
  const cumulativeAmount = record ? (Number(record.amount) / 1e7).toFixed(7) : "0.0000000";
@@ -810,16 +1601,20 @@ function createMppSessionHandler(opts) {
810
1601
  if (record && amount < record.amount) return;
811
1602
  const payer = record?.payer ?? extractPayerAddress(credential.source);
812
1603
  record = { amount, signature: payload.signature, payer };
1604
+ voucherCount++;
1605
+ settledCleanly = false;
1606
+ orphanFlagged = false;
1607
+ armIdleTimer();
1608
+ const wasOpened = sessionOpened;
1609
+ sessionOpened = true;
813
1610
  await innerStore.put(voucherRecordKey, {
814
1611
  amount: record.amount.toString(),
815
1612
  signature: record.signature,
816
- payer: record.payer
1613
+ payer: record.payer,
1614
+ voucherCount,
1615
+ sessionOpened: true
817
1616
  });
818
- voucherCount++;
819
- settledCleanly = false;
820
- armIdleTimer();
821
- if (!sessionOpened) {
822
- sessionOpened = true;
1617
+ if (!wasOpened) {
823
1618
  if (opts.onSessionOpen) {
824
1619
  Promise.resolve().then(() => opts.onSessionOpen(opts.channelFactory, record.payer)).catch((err) => {
825
1620
  logger("error", "[mpp-session] onSessionOpen callback error", { error: err });
@@ -871,10 +1666,11 @@ function createMppSessionHandler(opts) {
871
1666
  }
872
1667
  if (closeAmount > 0n && closeSig) {
873
1668
  const closePayer = record?.payer ?? null;
1669
+ const vouchersClosed = voucherCount;
874
1670
  const closeTxHash = await close({
875
1671
  channel: opts.channelFactory,
876
1672
  amount: closeAmount,
877
- signature: Buffer.from(closeSig, "hex"),
1673
+ signature: hexToBytes(closeSig),
878
1674
  feePayer: channelAuthorizer(payeeKeypair),
879
1675
  network: networkId
880
1676
  });
@@ -887,46 +1683,28 @@ function createMppSessionHandler(opts) {
887
1683
  opts.onCallbackError?.(err, "onSettled");
888
1684
  });
889
1685
  }
890
- const vaultContract = process.env.AGENT_VAULT_CONTRACT;
891
- if (vaultContract) {
892
- const settlementAddresses = resolveVaultSettlementAddresses(
893
- closePayer,
894
- payeeKeypair.publicKey()
1686
+ res.json({ closeTxHash });
1687
+ const vaultConfig = resolveVaultSettlementConfig(opts.vaultSettlement, logger);
1688
+ if (vaultConfig) {
1689
+ await recordSessionSettlement(
1690
+ {
1691
+ config: vaultConfig,
1692
+ network: opts.network,
1693
+ payeeSecretKey: opts.payeeSecretKey,
1694
+ channelId: opts.channelFactory,
1695
+ payer: closePayer,
1696
+ amount: closeAmount,
1697
+ voucherCount: vouchersClosed
1698
+ },
1699
+ { logger, onCallbackError: opts.onCallbackError }
895
1700
  );
896
- if (!settlementAddresses) {
897
- logger("warn", "[mpp-session] skipped session_settled vault record: payer address unavailable");
898
- } else {
899
- try {
900
- const { Networks } = await import('@stellar/stellar-sdk');
901
- const { Server } = await import('@stellar/stellar-sdk/rpc');
902
- const networkPassphrase = opts.network === "mainnet" ? Networks.PUBLIC : Networks.TESTNET;
903
- const rpcUrl = opts.network === "mainnet" ? "https://mainnet.sorobanrpc.com" : "https://soroban-testnet.stellar.org";
904
- const server = new Server(rpcUrl);
905
- const payeePublicKey = payeeKeypair.publicKey();
906
- const sourceAccount = await server.getAccount(payeePublicKey);
907
- const tx = buildSessionSettlementTransaction(
908
- sourceAccount,
909
- vaultContract,
910
- opts.channelFactory,
911
- settlementAddresses,
912
- closeAmount,
913
- voucherCount,
914
- networkPassphrase
915
- );
916
- const preparedTx = await server.prepareTransaction(tx);
917
- preparedTx.sign(payeeKeypair);
918
- await server.sendTransaction(preparedTx);
919
- } catch (recordErr) {
920
- logger("error", "[mpp-session] failed to record session_settled on vault", { error: recordErr });
921
- }
922
- }
923
1701
  }
924
- res.json({ closeTxHash });
925
1702
  } else {
926
1703
  res.json({ closeTxHash: null, message: "no vouchers received" });
927
1704
  }
928
1705
  sessionOpened = false;
929
1706
  voucherCount = 0;
1707
+ orphanFlagged = false;
930
1708
  record = null;
931
1709
  await innerStore.delete(voucherRecordKey);
932
1710
  closeListenerArmed = false;
@@ -958,6 +1736,11 @@ function createMppSessionHandler(opts) {
958
1736
  }
959
1737
  };
960
1738
  }
1739
+ function sha256(data) {
1740
+ return sha256$1(typeof data === "string" ? utf8ToBytes(data) : data);
1741
+ }
1742
+
1743
+ // src/manifest/sign.ts
961
1744
  var MANIFEST_SIGNATURE_VERSION = "2";
962
1745
  function canonicalize(value) {
963
1746
  if (Array.isArray(value)) {
@@ -973,7 +1756,7 @@ function canonicalize(value) {
973
1756
  function manifestDigest(manifest) {
974
1757
  const { signature: _omit, ...rest } = manifest;
975
1758
  const canonical = JSON.stringify(canonicalize(rest));
976
- return createHash("sha256").update(canonical, "utf8").digest();
1759
+ return sha256(utf8ToBytes(canonical));
977
1760
  }
978
1761
  function signManifest(manifest, payeeSecretKey) {
979
1762
  const keypair = Keypair.fromSecret(payeeSecretKey);
@@ -983,7 +1766,7 @@ function signManifest(manifest, payeeSecretKey) {
983
1766
  };
984
1767
  const digest = manifestDigest(versionedManifest);
985
1768
  const sig = keypair.sign(digest);
986
- return { ...versionedManifest, signature: Buffer.from(sig).toString("base64") };
1769
+ return { ...versionedManifest, signature: bytesToBase64(sig) };
987
1770
  }
988
1771
  function verifyManifestSignature(manifest, expectedPayee) {
989
1772
  if (!manifest.signature) {
@@ -996,11 +1779,19 @@ function verifyManifestSignature(manifest, expectedPayee) {
996
1779
  `Manifest signature version must be ${MANIFEST_SIGNATURE_VERSION}; legacy signatures are not safe because nested fields were excluded from their digest`
997
1780
  );
998
1781
  }
999
- const keypair = Keypair.fromPublicKey(manifest.payee);
1782
+ let keypair;
1783
+ try {
1784
+ keypair = Keypair.fromPublicKey(manifest.payee);
1785
+ } catch (err) {
1786
+ throw new RouteDockSignatureError(
1787
+ `Manifest payee ${manifest.payee} is not a valid Stellar public key`,
1788
+ { cause: err }
1789
+ );
1790
+ }
1000
1791
  const digest = manifestDigest(manifest);
1001
1792
  let sigBytes;
1002
1793
  try {
1003
- sigBytes = Buffer.from(manifest.signature, "base64");
1794
+ sigBytes = base64ToBytes(manifest.signature);
1004
1795
  } catch {
1005
1796
  throw new RouteDockSignatureError("Manifest signature is not valid base64");
1006
1797
  }
@@ -1023,6 +1814,7 @@ function verifyManifestSignature(manifest, expectedPayee) {
1023
1814
  var MPP_MODES = ["mpp-session", "mpp-charge"];
1024
1815
  function routedock(opts) {
1025
1816
  const startTime = Date.now();
1817
+ assertProviderConfig(opts, "routedock");
1026
1818
  const signedManifest = signManifest(opts.manifest, opts.payeeSecretKey);
1027
1819
  const handlerMap = /* @__PURE__ */ new Map();
1028
1820
  if (opts.modes.includes("x402")) {
@@ -1086,7 +1878,8 @@ function routedock(opts) {
1086
1878
  ...opts.onCallbackError ? { onCallbackError: opts.onCallbackError } : {},
1087
1879
  ...opts.onOrphaned ? { onOrphaned: opts.onOrphaned } : {},
1088
1880
  ...opts.idleTimeoutMs != null ? { idleTimeoutMs: opts.idleTimeoutMs } : {},
1089
- ...opts.logger ? { logger: opts.logger } : {}
1881
+ ...opts.logger ? { logger: opts.logger } : {},
1882
+ ...opts.vaultSettlement ? { vaultSettlement: opts.vaultSettlement } : {}
1090
1883
  })
1091
1884
  );
1092
1885
  }
@@ -1133,6 +1926,8 @@ var MPP_NETWORK3 = {
1133
1926
  testnet: "stellar:testnet",
1134
1927
  mainnet: "stellar:pubnet"
1135
1928
  };
1929
+ var DEFAULT_MAX_RECONCILE_ATTEMPTS = 5;
1930
+ var ALREADY_CLOSED_PATTERN = /channel has been closed|channel is closed|already closed|finalized/i;
1136
1931
  function decimalToStroops(amountStr) {
1137
1932
  return usdcToStroops(amountStr);
1138
1933
  }
@@ -1142,12 +1937,15 @@ async function reconcileAbandonedSessions(opts) {
1142
1937
  recoveredCount: 0,
1143
1938
  skippedCount: 0,
1144
1939
  failedCount: 0,
1940
+ alreadyClosedCount: 0,
1941
+ terminalCount: 0,
1145
1942
  errors: []
1146
1943
  };
1944
+ const maxAttempts = opts.maxReconcileAttempts ?? DEFAULT_MAX_RECONCILE_ATTEMPTS;
1147
1945
  const payeeKeypair = Keypair.fromSecret(opts.payeeSecretKey);
1148
1946
  const networkId = MPP_NETWORK3[opts.network];
1149
1947
  try {
1150
- 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);
1948
+ const { data: closingSessions, error } = await opts.supabase.from("sessions").select("channel_id, channel_contract, cumulative_amount, last_signature, settlement_tx_hash, reconcile_attempts").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);
1151
1949
  if (error) {
1152
1950
  throw new Error(`Failed to query sessions: ${error.message}`);
1153
1951
  }
@@ -1169,7 +1967,7 @@ async function reconcileAbandonedSessions(opts) {
1169
1967
  continue;
1170
1968
  }
1171
1969
  const closeAmount = decimalToStroops(cumulative_amount);
1172
- const closeSig = Buffer.from(last_signature, "hex");
1970
+ const closeSig = hexToBytes(last_signature);
1173
1971
  const closeFn = opts.channelClose ?? (await import('@stellar/mpp/channel/server')).close;
1174
1972
  const closeTxHash = await closeFn({
1175
1973
  channel: channelId,
@@ -1192,11 +1990,36 @@ async function reconcileAbandonedSessions(opts) {
1192
1990
  await opts.onRecovered(channelId, closeTxHash, totalPaid);
1193
1991
  }
1194
1992
  } catch (err) {
1993
+ const reason = formatMppError(err);
1994
+ if (ALREADY_CLOSED_PATTERN.test(reason)) {
1995
+ const { error: closeError } = await opts.supabase.from("sessions").update({
1996
+ status: "closed",
1997
+ last_reconcile_error: `channel already closed on-chain: ${reason}`,
1998
+ updated_at: (/* @__PURE__ */ new Date()).toISOString()
1999
+ }).eq("channel_id", session.channel_id);
2000
+ if (!closeError) {
2001
+ stats.alreadyClosedCount++;
2002
+ continue;
2003
+ }
2004
+ }
1195
2005
  stats.failedCount++;
1196
- stats.errors.push({
1197
- channelId: session.channel_id,
1198
- reason: formatMppError(err)
1199
- });
2006
+ stats.errors.push({ channelId: session.channel_id, reason });
2007
+ const attempts = (Number(session.reconcile_attempts) || 0) + 1;
2008
+ const terminal = attempts >= maxAttempts;
2009
+ const { error: bookkeepingError } = await opts.supabase.from("sessions").update({
2010
+ reconcile_attempts: attempts,
2011
+ last_reconcile_error: reason,
2012
+ updated_at: (/* @__PURE__ */ new Date()).toISOString(),
2013
+ ...terminal ? { status: "failed" } : {}
2014
+ }).eq("channel_id", session.channel_id);
2015
+ if (bookkeepingError) {
2016
+ stats.errors.push({
2017
+ channelId: session.channel_id,
2018
+ reason: `Failed to record reconcile failure: ${bookkeepingError.message}`
2019
+ });
2020
+ } else if (terminal) {
2021
+ stats.terminalCount++;
2022
+ }
1200
2023
  }
1201
2024
  }
1202
2025
  return stats;
@@ -1217,7 +2040,7 @@ async function runStartupReconciliation(opts, logger) {
1217
2040
  }
1218
2041
  log(
1219
2042
  "info",
1220
- `[SessionReconciler] Found ${stats.orphanedCount} orphaned session(s). Recovered: ${stats.recoveredCount}, Skipped: ${stats.skippedCount}, Failed: ${stats.failedCount}`
2043
+ `[SessionReconciler] Found ${stats.orphanedCount} orphaned session(s). Recovered: ${stats.recoveredCount}, Skipped: ${stats.skippedCount}, Failed: ${stats.failedCount}, Already closed: ${stats.alreadyClosedCount}, Moved to failed: ${stats.terminalCount}`
1221
2044
  );
1222
2045
  if (stats.errors.length > 0) {
1223
2046
  log("warn", "[SessionReconciler] Reconciliation errors:");
@@ -1404,4 +2227,4 @@ var SupabaseSeenTxStore = class {
1404
2227
  }
1405
2228
  };
1406
2229
 
1407
- export { DEFAULT_MAX_SESSION_DURATION_MS, InMemorySeenTxStore, MAX_SETTLEMENT_REPLAYS, RouteDockChannelStateError, RouteDockClientVersionError, RouteDockDisputeError, RouteDockError, RouteDockFacilitatorError, RouteDockManifestError, RouteDockNetworkError, RouteDockNoSupportedModeError, RouteDockPolicyRejectError, RouteDockPolicyRejectedError, RouteDockRefundWindowError, RouteDockSessionError, RouteDockSignatureError, RouteDockTrustlineError, RouteDockVoucherMonotonicityError, SETTLEMENT_REPLAY_WINDOW_MS, SupabaseSeenTxStore, SupabaseSessionStore, checkSettlementReplay, createMppChargeHandler, createMppSessionHandler, createX402Handler, decimalToStroops, getEligibleAssets, isAssetEligible, isVoucherStoreValue, normalizeManifestAssets, paymentIdempotencyKey, reconcileAbandonedSessions, registerProvider, resolveAssetContract, routedock, runStartupReconciliation, selectAsset };
2230
+ export { DEFAULT_MAX_SESSION_DURATION_MS, InMemorySeenTxStore, MAX_SETTLEMENT_REPLAYS, RouteDockChannelStateError, RouteDockClientVersionError, RouteDockDisputeError, RouteDockError, RouteDockFacilitatorError, RouteDockManifestError, RouteDockNetworkError, RouteDockNoSupportedModeError, RouteDockPolicyRejectError, RouteDockPolicyRejectedError, RouteDockRefundWindowError, RouteDockSessionError, RouteDockSignatureError, RouteDockTrustlineError, RouteDockVoucherMonotonicityError, SETTLEMENT_REPLAY_WINDOW_MS, SupabaseSeenTxStore, SupabaseSessionStore, checkSettlementReplay, createMppChargeHandler, createMppSessionHandler, createX402Handler, decimalToStroops, getEligibleAssets, isAssetEligible, isVoucherStoreValue, normalizeManifestAssets, paymentIdempotencyKey, reconcileAbandonedSessions, recordSettlement, registerProvider, resolveAssetContract, routedock, runStartupReconciliation, selectAsset };