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