@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
@@ -1,6 +1,8 @@
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');
@@ -9,11 +11,10 @@ var http = require('@x402/core/http');
9
11
  var server$4 = require('@stellar/mpp/charge/server');
10
12
  var server$2 = require('@stellar/mpp/channel/server');
11
13
  var server$3 = require('mppx/server');
12
- var crypto$1 = require('crypto');
13
- var nulthSdk = require('@routedock/nulth-sdk');
14
+ var sha2_js = require('@noble/hashes/sha2.js');
14
15
  var mppx = require('mppx');
15
16
 
16
- // src/provider/hono.ts
17
+ // src/provider/internal/assertProviderConfig.ts
17
18
 
18
19
  // src/errors.ts
19
20
  var RouteDockError = class extends Error {
@@ -48,6 +49,658 @@ var RouteDockSignatureError = class extends RouteDockError {
48
49
  this.name = "RouteDockSignatureError";
49
50
  }
50
51
  };
52
+ var usdcToUnits = nulthSdk.usdcToStroops;
53
+
54
+ // src/schemas/routedock.schema.json
55
+ var routedock_schema_default = {
56
+ $schema: "http://json-schema.org/draft-07/schema#",
57
+ $id: "https://routedock.xyz/schemas/routedock.schema.json",
58
+ title: "RouteDock Manifest",
59
+ description: "Discovery manifest served at /.well-known/routedock.json on every RouteDock provider endpoint",
60
+ type: "object",
61
+ required: [
62
+ "routedock",
63
+ "name",
64
+ "description",
65
+ "modes",
66
+ "network",
67
+ "asset",
68
+ "asset_contract",
69
+ "payee",
70
+ "pricing",
71
+ "endpoints",
72
+ "tags"
73
+ ],
74
+ additionalProperties: false,
75
+ properties: {
76
+ routedock: {
77
+ type: "string",
78
+ pattern: "^[0-9]+\\.[0-9]+$",
79
+ description: "Manifest schema version, e.g. '1.0', '1.1', '2.0'."
80
+ },
81
+ min_client_version: {
82
+ type: "string",
83
+ pattern: "^[0-9]+\\.[0-9]+$",
84
+ description: "Minimum SDK client version (major.minor) required to use this manifest. Clients below this version must refuse to pay."
85
+ },
86
+ deprecated_modes: {
87
+ type: "array",
88
+ items: {
89
+ $ref: "#/definitions/PaymentMode"
90
+ },
91
+ uniqueItems: true,
92
+ description: "Payment modes still listed in 'modes' for backwards compatibility but deprecated for new integrations."
93
+ },
94
+ sunset_at: {
95
+ type: "string",
96
+ format: "date-time",
97
+ description: "ISO 8601 timestamp after which this manifest version is no longer supported by the provider."
98
+ },
99
+ name: {
100
+ type: "string",
101
+ minLength: 1,
102
+ maxLength: 120,
103
+ description: "Human-readable provider name"
104
+ },
105
+ description: {
106
+ type: "string",
107
+ minLength: 1,
108
+ maxLength: 500,
109
+ description: "Human-readable description of the provider's data or service"
110
+ },
111
+ modes: {
112
+ type: "array",
113
+ minItems: 1,
114
+ uniqueItems: true,
115
+ items: {
116
+ $ref: "#/definitions/PaymentMode"
117
+ },
118
+ description: "Payment modes supported by this provider"
119
+ },
120
+ network: {
121
+ type: "string",
122
+ enum: ["testnet", "mainnet"],
123
+ description: "Stellar network this provider operates on"
124
+ },
125
+ asset: {
126
+ type: "string",
127
+ description: "Asset ticker symbol (e.g. 'USDC')"
128
+ },
129
+ asset_contract: {
130
+ type: "string",
131
+ pattern: "^C[A-Z2-7]{55}$",
132
+ description: "Stellar Asset Contract (SAC) address for the payment asset"
133
+ },
134
+ assets: {
135
+ type: "array",
136
+ minItems: 1,
137
+ items: {
138
+ $ref: "#/definitions/AssetConfig"
139
+ },
140
+ 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."
141
+ },
142
+ payee: {
143
+ type: "string",
144
+ pattern: "^G[A-Z2-7]{55}$",
145
+ description: "Stellar address (G...) that receives payments"
146
+ },
147
+ pricing: {
148
+ type: "object",
149
+ description: "Pricing configuration per supported payment mode",
150
+ additionalProperties: false,
151
+ properties: {
152
+ x402: {
153
+ $ref: "#/definitions/PricingConfig"
154
+ },
155
+ "mpp-charge": {
156
+ $ref: "#/definitions/PricingConfig"
157
+ },
158
+ "mpp-session": {
159
+ $ref: "#/definitions/SessionPricingConfig"
160
+ },
161
+ "mpp-session-ws": {
162
+ $ref: "#/definitions/SessionPricingConfig",
163
+ 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."
164
+ }
165
+ },
166
+ minProperties: 1
167
+ },
168
+ endpoints: {
169
+ type: "object",
170
+ description: "Map of endpoint name to endpoint descriptors",
171
+ additionalProperties: {
172
+ $ref: "#/definitions/EndpointDescriptor"
173
+ },
174
+ minProperties: 1
175
+ },
176
+ sla: {
177
+ $ref: "#/definitions/SLAConfig"
178
+ },
179
+ tags: {
180
+ type: "array",
181
+ items: {
182
+ type: "string",
183
+ minLength: 1,
184
+ maxLength: 64
185
+ },
186
+ uniqueItems: true,
187
+ description: "Capability tags indexed with trigram search in the provider registry"
188
+ },
189
+ vault: {
190
+ $ref: "#/definitions/VaultMode"
191
+ },
192
+ nulth_account: {
193
+ type: "string",
194
+ description: "Optional Nulth smart account address associated with this provider"
195
+ },
196
+ capabilities: {
197
+ type: "object",
198
+ description: "Optional protocol features this provider supports",
199
+ properties: {
200
+ streaming: {
201
+ type: "array",
202
+ items: {
203
+ type: "string",
204
+ enum: ["sse", "websocket"]
205
+ },
206
+ description: "Supported streaming transports"
207
+ },
208
+ webhooks: {
209
+ type: "boolean",
210
+ description: "Whether webhook callbacks are supported"
211
+ },
212
+ idempotency_keys: {
213
+ type: "boolean",
214
+ description: "Whether idempotency keys are supported"
215
+ },
216
+ content_types: {
217
+ type: "array",
218
+ items: {
219
+ type: "string"
220
+ },
221
+ description: "Supported content types for request/response"
222
+ }
223
+ },
224
+ additionalProperties: false
225
+ },
226
+ regions: {
227
+ type: "array",
228
+ items: {
229
+ type: "string",
230
+ minLength: 2,
231
+ maxLength: 10
232
+ },
233
+ uniqueItems: true,
234
+ 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."
235
+ },
236
+ latency_hints: {
237
+ type: "object",
238
+ additionalProperties: {
239
+ type: "number",
240
+ minimum: 0
241
+ },
242
+ description: "Provider-declared p50 latency in milliseconds per region code. Keys must be a subset of 'regions'. Example: { 'IAD': 14, 'AMS': 22 }."
243
+ },
244
+ signature: {
245
+ type: "string",
246
+ 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."
247
+ },
248
+ signature_version: {
249
+ type: "string",
250
+ const: "2",
251
+ description: "Manifest signature format. Version 2 recursively includes every nested manifest field in the signed digest."
252
+ },
253
+ categories: {
254
+ type: "array",
255
+ items: {
256
+ type: "string",
257
+ minLength: 1,
258
+ maxLength: 128
259
+ },
260
+ uniqueItems: true,
261
+ description: "Optional hierarchical categories from a published taxonomy (e.g. data/price/crypto)"
262
+ }
263
+ },
264
+ definitions: {
265
+ VaultMode: {
266
+ type: "string",
267
+ enum: ["local-key", "agent-vault", "nulth"],
268
+ description: "Agent vault / custody mode for payments from this provider"
269
+ },
270
+ PaymentMode: {
271
+ type: "string",
272
+ enum: ["x402", "mpp-charge", "mpp-session", "mpp-session-ws"],
273
+ description: "A supported RouteDock payment mode"
274
+ },
275
+ AssetConfig: {
276
+ type: "object",
277
+ description: "Payment asset configuration with optional mode and endpoint scoping",
278
+ required: ["asset", "asset_contract"],
279
+ additionalProperties: false,
280
+ properties: {
281
+ asset: {
282
+ type: "string",
283
+ description: "Asset ticker symbol (e.g. 'USDC' or 'XLM')"
284
+ },
285
+ asset_contract: {
286
+ type: "string",
287
+ description: "Stellar Asset Contract (SAC) address for the payment asset"
288
+ },
289
+ modes: {
290
+ type: "array",
291
+ items: {
292
+ $ref: "#/definitions/PaymentMode"
293
+ },
294
+ uniqueItems: true,
295
+ description: "Optional: restrict this asset to specific payment modes. If omitted, available for all modes."
296
+ },
297
+ endpoints: {
298
+ type: "array",
299
+ items: {
300
+ type: "string"
301
+ },
302
+ uniqueItems: true,
303
+ description: "Optional: restrict this asset to specific endpoints (by name). If omitted, available for all endpoints."
304
+ }
305
+ }
306
+ },
307
+ PricingConfig: {
308
+ type: "object",
309
+ description: "Per-request pricing for x402 or mpp-charge modes",
310
+ required: ["amount", "per"],
311
+ additionalProperties: false,
312
+ properties: {
313
+ amount: {
314
+ type: "string",
315
+ pattern: "^[0-9]+(\\.[0-9]{1,7})?$",
316
+ description: "Cost per request in the payment asset (decimal string, e.g. '0.001')"
317
+ },
318
+ per: {
319
+ type: "string",
320
+ const: "request",
321
+ description: "Billing unit \u2014 always 'request' for non-session modes"
322
+ },
323
+ facilitator: {
324
+ type: "string",
325
+ description: "x402 facilitator URL (required for x402 mode). OpenZeppelin Channels endpoint."
326
+ },
327
+ payee: {
328
+ type: "string",
329
+ pattern: "^G[A-Z2-7]{55}$",
330
+ 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."
331
+ }
332
+ }
333
+ },
334
+ SessionPricingConfig: {
335
+ type: "object",
336
+ description: "Per-voucher pricing for mpp-session (one-way-channel) mode",
337
+ required: ["rate", "per", "channel_factory", "min_deposit", "refund_waiting_period_ledgers"],
338
+ additionalProperties: false,
339
+ properties: {
340
+ rate: {
341
+ type: "string",
342
+ pattern: "^[0-9]+(\\.[0-9]{1,7})?$",
343
+ description: "Cost per voucher in the payment asset (decimal string, e.g. '0.0001')"
344
+ },
345
+ per: {
346
+ type: "string",
347
+ const: "voucher",
348
+ description: "Billing unit \u2014 always 'voucher' for session mode"
349
+ },
350
+ channel_factory: {
351
+ type: "string",
352
+ pattern: "^C[A-Z2-7]{55}$",
353
+ description: "Stable Soroban factory address (C...) used to deploy a per-session channel"
354
+ },
355
+ min_deposit: {
356
+ type: "string",
357
+ pattern: "^[0-9]+(\\.[0-9]{1,7})?$",
358
+ description: "Minimum initial deposit to open a channel (decimal string)"
359
+ },
360
+ refund_waiting_period_ledgers: {
361
+ type: "integer",
362
+ minimum: 1,
363
+ description: "Ledgers the server has to settle before the funder can reclaim. Default: 17280 (~24h). Do not reduce in production."
364
+ }
365
+ }
366
+ },
367
+ SLAConfig: {
368
+ type: "object",
369
+ description: "Service Level Agreement for the provider",
370
+ required: ["uptime_30d_percent", "p95_latency_ms"],
371
+ additionalProperties: false,
372
+ properties: {
373
+ uptime_30d_percent: {
374
+ type: "number",
375
+ description: "Uptime percentage over the last 30 days"
376
+ },
377
+ p95_latency_ms: {
378
+ type: "number",
379
+ description: "P95 latency in milliseconds"
380
+ },
381
+ maintenance_windows: {
382
+ type: "array",
383
+ items: {
384
+ type: "object",
385
+ required: ["cron", "duration_minutes"],
386
+ additionalProperties: false,
387
+ properties: {
388
+ cron: {
389
+ type: "string",
390
+ description: "Cron expression for the maintenance window"
391
+ },
392
+ duration_minutes: {
393
+ type: "number",
394
+ description: "Duration of the maintenance window in minutes"
395
+ }
396
+ }
397
+ }
398
+ }
399
+ }
400
+ },
401
+ EndpointDescriptor: {
402
+ type: "object",
403
+ description: "Descriptor for a provider endpoint",
404
+ required: ["method", "path"],
405
+ additionalProperties: false,
406
+ properties: {
407
+ method: {
408
+ type: "string",
409
+ description: "HTTP method for the endpoint"
410
+ },
411
+ path: {
412
+ type: "string",
413
+ description: "Path for the endpoint"
414
+ },
415
+ deprecated: {
416
+ type: "boolean",
417
+ description: "Whether this endpoint is retained only for backwards compatibility"
418
+ },
419
+ sunset_at: {
420
+ type: "string",
421
+ format: "date-time",
422
+ description: "ISO 8601 timestamp after which callers should stop using this endpoint"
423
+ },
424
+ headers: {
425
+ type: "object",
426
+ additionalProperties: {
427
+ type: "string"
428
+ },
429
+ description: "Expected request headers"
430
+ },
431
+ request_schema: {
432
+ description: "JSON schema for the request body"
433
+ },
434
+ response_schema: {
435
+ description: "JSON schema for the response body"
436
+ },
437
+ rate_limit: {
438
+ type: "object",
439
+ required: ["requests", "window_seconds"],
440
+ additionalProperties: false,
441
+ properties: {
442
+ requests: {
443
+ type: "number"
444
+ },
445
+ window_seconds: {
446
+ type: "number"
447
+ }
448
+ }
449
+ }
450
+ }
451
+ }
452
+ }
453
+ };
454
+
455
+ // src/manifest/validate.ts
456
+ var validator = new jsonSchema.Validator(routedock_schema_default, "7");
457
+ function validateManifestSchema(manifest, source) {
458
+ const result = validator.validate(manifest);
459
+ if (!result.valid) {
460
+ const msgs = result.errors.map((err) => err.error).join("; ");
461
+ throw new RouteDockManifestError(`Invalid manifest${""}: ${msgs}`);
462
+ }
463
+ }
464
+ function assertManifestValid(manifest, baseUrl) {
465
+ const context = "";
466
+ if (manifest.latency_hints !== void 0) {
467
+ if (!Array.isArray(manifest.regions) || manifest.regions.length === 0) {
468
+ throw new RouteDockManifestError(
469
+ `Invalid manifest${context}: latency_hints defined without declaring regions`
470
+ );
471
+ }
472
+ const regionSet = new Set(manifest.regions);
473
+ for (const region of Object.keys(manifest.latency_hints)) {
474
+ if (!regionSet.has(region)) {
475
+ throw new RouteDockManifestError(
476
+ `Invalid manifest${context}: latency_hints key '${region}' is not declared in regions (${manifest.regions.join(", ")})`
477
+ );
478
+ }
479
+ }
480
+ }
481
+ if (manifest.assets !== void 0) {
482
+ if (!Array.isArray(manifest.assets) || manifest.assets.length === 0) {
483
+ throw new RouteDockManifestError(
484
+ `Invalid manifest${context}: assets must be a non-empty array when defined`
485
+ );
486
+ }
487
+ for (let i = 0; i < manifest.assets.length; i++) {
488
+ const a = manifest.assets[i];
489
+ if (!a || typeof a.asset !== "string" || typeof a.asset_contract !== "string") {
490
+ throw new RouteDockManifestError(
491
+ `Invalid manifest${context}: assets[${i}] must define asset and asset_contract strings`
492
+ );
493
+ }
494
+ }
495
+ const first = manifest.assets[0];
496
+ if (first.asset !== manifest.asset || first.asset_contract !== manifest.asset_contract) {
497
+ throw new RouteDockManifestError(
498
+ `Invalid manifest${context}: assets[0] (${first.asset}:${first.asset_contract}) must match root asset fields (${manifest.asset}:${manifest.asset_contract})`
499
+ );
500
+ }
501
+ }
502
+ assertPricingMatchesModes(manifest, context);
503
+ assertDeprecatedModesListed(manifest, context);
504
+ assertPricesParse(manifest, context);
505
+ }
506
+ function assertPricingMatchesModes(manifest, context) {
507
+ const pricing = manifest.pricing ?? {};
508
+ for (const mode of manifest.modes ?? []) {
509
+ if (pricing[mode] === void 0 || pricing[mode] === null) {
510
+ throw new RouteDockManifestError(
511
+ `Invalid manifest${context}: mode '${mode}' is listed in modes but pricing.${mode} is missing`
512
+ );
513
+ }
514
+ }
515
+ }
516
+ function assertDeprecatedModesListed(manifest, context) {
517
+ if (manifest.deprecated_modes === void 0) return;
518
+ const modes = new Set(manifest.modes ?? []);
519
+ for (const mode of manifest.deprecated_modes) {
520
+ if (!modes.has(mode)) {
521
+ throw new RouteDockManifestError(
522
+ `Invalid manifest${context}: deprecated_modes entry '${mode}' is not listed in modes`
523
+ );
524
+ }
525
+ }
526
+ }
527
+ var PRICE_FIELDS = {
528
+ x402: ["amount"],
529
+ "mpp-charge": ["amount"],
530
+ "mpp-session": ["rate", "min_deposit"],
531
+ "mpp-session-ws": ["rate", "min_deposit"]
532
+ };
533
+ function assertPricesParse(manifest, context) {
534
+ const pricing = manifest.pricing ?? {};
535
+ for (const [mode, fields] of Object.entries(PRICE_FIELDS)) {
536
+ const entry = pricing[mode];
537
+ if (entry === void 0 || entry === null) continue;
538
+ for (const field of fields) {
539
+ const value = entry[field];
540
+ try {
541
+ if (typeof value !== "string") {
542
+ throw new RangeError(`expected a decimal string, got ${typeof value}`);
543
+ }
544
+ nulthSdk.usdcToStroops(value);
545
+ } catch (err) {
546
+ const detail = err instanceof Error ? err.message : String(err);
547
+ throw new RouteDockManifestError(
548
+ `Invalid manifest${context}: pricing.${mode}.${field} ${JSON.stringify(value)} is not a valid amount (${detail})`,
549
+ { cause: err }
550
+ );
551
+ }
552
+ }
553
+ }
554
+ }
555
+
556
+ // src/provider/internal/assertProviderConfig.ts
557
+ var FLAT_MODES = ["x402", "mpp-charge"];
558
+ var SESSION_MODES = ["mpp-session", "mpp-session-ws"];
559
+ function sameAmount(adapter, label, optionValue, manifestValue) {
560
+ const parse = (where, value) => {
561
+ try {
562
+ return nulthSdk.usdcToStroops(value);
563
+ } catch {
564
+ throw new RouteDockManifestError(
565
+ `${adapter}: ${label} has an unparseable ${where} amount "${value}"`
566
+ );
567
+ }
568
+ };
569
+ return parse("option", optionValue) === parse("manifest", manifestValue);
570
+ }
571
+ function assertProviderConfig(opts, adapterName) {
572
+ const { manifest } = opts;
573
+ if (manifest === null || typeof manifest !== "object") {
574
+ throw new RouteDockManifestError(`${adapterName}: opts.manifest is required and must be an object`);
575
+ }
576
+ try {
577
+ validateManifestSchema(manifest);
578
+ assertManifestValid(manifest);
579
+ } catch (err) {
580
+ if (err instanceof RouteDockManifestError) {
581
+ throw new RouteDockManifestError(`${adapterName}: ${err.message}`, { cause: err });
582
+ }
583
+ throw err;
584
+ }
585
+ const served = new Set(opts.modes.filter((mode) => Boolean(opts.pricing[mode])));
586
+ const advertised = new Set(manifest.modes);
587
+ const notAdvertised = [...served].filter((mode) => !advertised.has(mode));
588
+ const notServed = [...advertised].filter((mode) => !served.has(mode));
589
+ if (notAdvertised.length > 0 || notServed.length > 0) {
590
+ const parts = [];
591
+ if (notAdvertised.length > 0) parts.push(`served but not in manifest.modes: ${notAdvertised.join(", ")}`);
592
+ if (notServed.length > 0) parts.push(`in manifest.modes but not served (missing from modes or pricing): ${notServed.join(", ")}`);
593
+ throw new RouteDockManifestError(`${adapterName}: modes do not match manifest.modes (${parts.join("; ")})`);
594
+ }
595
+ for (const mode of FLAT_MODES) {
596
+ if (!served.has(mode)) continue;
597
+ const optionValue = opts.pricing[mode];
598
+ const manifestValue = manifest.pricing[mode]?.amount;
599
+ if (manifestValue === void 0) {
600
+ throw new RouteDockManifestError(`${adapterName}: manifest.pricing.${mode}.amount is missing but ${mode} is served`);
601
+ }
602
+ if (!sameAmount(adapterName, `pricing.${mode}`, optionValue, manifestValue)) {
603
+ throw new RouteDockManifestError(
604
+ `${adapterName}: pricing.${mode} "${optionValue}" does not match manifest.pricing.${mode}.amount "${manifestValue}"`
605
+ );
606
+ }
607
+ }
608
+ for (const mode of SESSION_MODES) {
609
+ if (!served.has(mode)) continue;
610
+ const optionValue = opts.pricing[mode];
611
+ const manifestValue = manifest.pricing[mode];
612
+ if (!manifestValue) {
613
+ throw new RouteDockManifestError(`${adapterName}: manifest.pricing.${mode} is missing but ${mode} is served`);
614
+ }
615
+ if (!sameAmount(adapterName, `pricing.${mode}.rate`, optionValue.rate, manifestValue.rate)) {
616
+ throw new RouteDockManifestError(
617
+ `${adapterName}: pricing.${mode}.rate "${optionValue.rate}" does not match manifest.pricing.${mode}.rate "${manifestValue.rate}"`
618
+ );
619
+ }
620
+ if (optionValue.channelFactory !== manifestValue.channel_factory) {
621
+ throw new RouteDockManifestError(
622
+ `${adapterName}: pricing.${mode}.channelFactory "${optionValue.channelFactory}" does not match manifest.pricing.${mode}.channel_factory "${manifestValue.channel_factory}"`
623
+ );
624
+ }
625
+ }
626
+ if (opts.assetContract !== void 0 && opts.assetContract !== manifest.asset_contract) {
627
+ throw new RouteDockManifestError(
628
+ `${adapterName}: assetContract "${opts.assetContract}" does not match manifest.asset_contract "${manifest.asset_contract}"`
629
+ );
630
+ }
631
+ if (opts.asset !== void 0 && opts.asset !== manifest.asset) {
632
+ throw new RouteDockManifestError(
633
+ `${adapterName}: asset "${opts.asset}" does not match manifest.asset "${manifest.asset}"`
634
+ );
635
+ }
636
+ if (opts.network !== manifest.network) {
637
+ throw new RouteDockManifestError(
638
+ `${adapterName}: network "${opts.network}" does not match manifest.network "${manifest.network}"`
639
+ );
640
+ }
641
+ let signerPublicKey;
642
+ try {
643
+ signerPublicKey = stellarSdk.Keypair.fromSecret(opts.payeeSecretKey).publicKey();
644
+ } catch (err) {
645
+ throw new RouteDockManifestError(`${adapterName}: payeeSecretKey is not a valid Stellar secret key`, { cause: err });
646
+ }
647
+ if (signerPublicKey !== manifest.payee) {
648
+ throw new RouteDockManifestError(
649
+ `${adapterName}: payeeSecretKey belongs to ${signerPublicKey}, not manifest.payee ${manifest.payee}; clients would reject the signed manifest`
650
+ );
651
+ }
652
+ if (opts.payee !== manifest.payee) {
653
+ throw new RouteDockManifestError(
654
+ `${adapterName}: payee "${opts.payee}" does not match manifest.payee "${manifest.payee}"`
655
+ );
656
+ }
657
+ }
658
+
659
+ // src/internal/encoding.ts
660
+ var encoder = new TextEncoder();
661
+ var decoder = new TextDecoder();
662
+ function utf8ToBytes(text) {
663
+ return encoder.encode(text);
664
+ }
665
+ function bytesToUtf8(bytes) {
666
+ return decoder.decode(bytes);
667
+ }
668
+ function bytesToBase64(bytes) {
669
+ const chunk = 32768;
670
+ let binary = "";
671
+ for (let i = 0; i < bytes.length; i += chunk) {
672
+ binary += String.fromCharCode(...bytes.subarray(i, i + chunk));
673
+ }
674
+ return btoa(binary);
675
+ }
676
+ function base64ToBytes(b64) {
677
+ const end = b64.indexOf("=");
678
+ let clean = (end === -1 ? b64 : b64.slice(0, end)).replace(/-/g, "+").replace(/_/g, "/").replace(/[^A-Za-z0-9+/]/g, "");
679
+ if (clean.length % 4 === 1) clean = clean.slice(0, -1);
680
+ const binary = atob(clean);
681
+ const bytes = new Uint8Array(binary.length);
682
+ for (let i = 0; i < binary.length; i++) {
683
+ bytes[i] = binary.charCodeAt(i);
684
+ }
685
+ return bytes;
686
+ }
687
+ function base64ToUtf8(b64) {
688
+ return bytesToUtf8(base64ToBytes(b64));
689
+ }
690
+ function hexToBytes(hex) {
691
+ const clean = hex.startsWith("0x") ? hex.slice(2) : hex;
692
+ if (clean.length % 2 !== 0) {
693
+ throw new Error("hexToBytes: hex string must have an even length");
694
+ }
695
+ const bytes = new Uint8Array(clean.length / 2);
696
+ for (let i = 0; i < bytes.length; i++) {
697
+ bytes[i] = parseInt(clean.slice(i * 2, i * 2 + 2), 16);
698
+ }
699
+ return bytes;
700
+ }
701
+ function sha256(data) {
702
+ return sha2_js.sha256(typeof data === "string" ? utf8ToBytes(data) : data);
703
+ }
51
704
 
52
705
  // src/manifest/sign.ts
53
706
  var MANIFEST_SIGNATURE_VERSION = "2";
@@ -65,7 +718,7 @@ function canonicalize(value) {
65
718
  function manifestDigest(manifest) {
66
719
  const { signature: _omit, ...rest } = manifest;
67
720
  const canonical = JSON.stringify(canonicalize(rest));
68
- return crypto$1.createHash("sha256").update(canonical, "utf8").digest();
721
+ return sha256(utf8ToBytes(canonical));
69
722
  }
70
723
  function signManifest(manifest, payeeSecretKey) {
71
724
  const keypair = stellarSdk.Keypair.fromSecret(payeeSecretKey);
@@ -75,7 +728,7 @@ function signManifest(manifest, payeeSecretKey) {
75
728
  };
76
729
  const digest = manifestDigest(versionedManifest);
77
730
  const sig = keypair.sign(digest);
78
- return { ...versionedManifest, signature: Buffer.from(sig).toString("base64") };
731
+ return { ...versionedManifest, signature: bytesToBase64(sig) };
79
732
  }
80
733
  function verifyManifestSignature(manifest, expectedPayee) {
81
734
  if (!manifest.signature) {
@@ -88,11 +741,19 @@ function verifyManifestSignature(manifest, expectedPayee) {
88
741
  `Manifest signature version must be ${MANIFEST_SIGNATURE_VERSION}; legacy signatures are not safe because nested fields were excluded from their digest`
89
742
  );
90
743
  }
91
- const keypair = stellarSdk.Keypair.fromPublicKey(manifest.payee);
744
+ let keypair;
745
+ try {
746
+ keypair = stellarSdk.Keypair.fromPublicKey(manifest.payee);
747
+ } catch (err) {
748
+ throw new RouteDockSignatureError(
749
+ `Manifest payee ${manifest.payee} is not a valid Stellar public key`,
750
+ { cause: err }
751
+ );
752
+ }
92
753
  const digest = manifestDigest(manifest);
93
754
  let sigBytes;
94
755
  try {
95
- sigBytes = Buffer.from(manifest.signature, "base64");
756
+ sigBytes = base64ToBytes(manifest.signature);
96
757
  } catch {
97
758
  throw new RouteDockSignatureError("Manifest signature is not valid base64");
98
759
  }
@@ -116,7 +777,6 @@ function resolvePayee(manifest, mode) {
116
777
  const override = manifest.pricing[mode]?.payee;
117
778
  return override ?? manifest.payee;
118
779
  }
119
- var usdcToUnits = nulthSdk.usdcToStroops;
120
780
  function extractPayerAddress(key) {
121
781
  if (typeof key !== "string") return null;
122
782
  return stellarSdk.StrKey.isValidEd25519PublicKey(key) || stellarSdk.StrKey.isValidMed25519PublicKey(key) ? key : null;
@@ -200,30 +860,21 @@ function mapChannelError(error) {
200
860
  return new mppx.Errors.VerificationFailedError({ reason });
201
861
  }
202
862
 
203
- // src/provider/encoding.ts
204
- function base64ToUtf8(b64) {
205
- const binary = atob(b64);
206
- const bytes = new Uint8Array(binary.length);
207
- for (let i = 0; i < binary.length; i++) {
208
- bytes[i] = binary.charCodeAt(i);
209
- }
210
- return new TextDecoder().decode(bytes);
211
- }
212
- function hexToBytes(hex) {
213
- const clean = hex.startsWith("0x") ? hex.slice(2) : hex;
214
- if (clean.length % 2 !== 0) {
215
- throw new Error("hexToBytes: hex string must have an even length");
216
- }
217
- const bytes = new Uint8Array(clean.length / 2);
218
- for (let i = 0; i < bytes.length; i++) {
219
- bytes[i] = parseInt(clean.slice(i * 2, i * 2 + 2), 16);
220
- }
221
- return bytes;
222
- }
223
-
224
863
  // src/provider/SeenTxStore.ts
225
864
  var SETTLEMENT_REPLAY_WINDOW_MS = 6e4;
226
865
  var MAX_SETTLEMENT_REPLAYS = 1;
866
+ async function recordSettlement(store, key, record, ctx) {
867
+ try {
868
+ await store.set(key, record);
869
+ } catch (err) {
870
+ ctx.logger("error", `${ctx.label} seenTxStore.set failed after settlement`, { error: err });
871
+ try {
872
+ ctx.onCallbackError?.(err, "seenTxStore.set");
873
+ } catch (cbErr) {
874
+ ctx.logger("error", `${ctx.label} onCallbackError threw`, { error: cbErr });
875
+ }
876
+ }
877
+ }
227
878
  async function checkSettlementReplay(store, key, now = Date.now()) {
228
879
  const record = await store.get(key);
229
880
  if (!record) return { kind: "miss" };
@@ -363,7 +1014,7 @@ function selectAsset(manifest, mode, endpoint) {
363
1014
  return eligible[0];
364
1015
  }
365
1016
 
366
- // src/provider/settleResult.ts
1017
+ // src/internal/settleResult.ts
367
1018
  function readSettleResult(result) {
368
1019
  const settle = result;
369
1020
  const failed = (reason) => reason ? { ok: false, reason } : { ok: false };
@@ -374,6 +1025,138 @@ function readSettleResult(result) {
374
1025
  return { ok: true, txHash: settle.transaction };
375
1026
  }
376
1027
 
1028
+ // src/provider/internal/vaultRpc.ts
1029
+ async function createSorobanRpcServer(rpcUrl) {
1030
+ const { Server } = await import('@stellar/stellar-sdk/rpc');
1031
+ return new Server(rpcUrl);
1032
+ }
1033
+
1034
+ // src/provider/internal/vaultSettlement.ts
1035
+ function resolveVaultSettlementAddresses(payer, payee) {
1036
+ if (!payer) return null;
1037
+ return { payer, payee };
1038
+ }
1039
+ function buildSessionSettlementTransaction(source, vaultContractId, channelId, addresses, amount, voucherCount, networkPassphrase) {
1040
+ const vault = new stellarSdk.Contract(vaultContractId);
1041
+ const op = vault.call(
1042
+ "record_session_settlement",
1043
+ stellarSdk.nativeToScVal(channelId, { type: "address" }),
1044
+ stellarSdk.nativeToScVal(addresses.payer, { type: "address" }),
1045
+ stellarSdk.nativeToScVal(addresses.payee, { type: "address" }),
1046
+ stellarSdk.nativeToScVal(amount, { type: "i128" }),
1047
+ stellarSdk.nativeToScVal(voucherCount, { type: "u32" })
1048
+ );
1049
+ return new stellarSdk.TransactionBuilder(source, {
1050
+ fee: stellarSdk.BASE_FEE,
1051
+ networkPassphrase
1052
+ }).addOperation(op).setTimeout(30).build();
1053
+ }
1054
+ var DEFAULT_RPC_URLS = {
1055
+ testnet: "https://soroban-testnet.stellar.org",
1056
+ mainnet: "https://mainnet.sorobanrpc.com"
1057
+ };
1058
+ var CONFIRM_TIMEOUT_MS = 12e4;
1059
+ var CONFIRM_POLL_MS = 1e3;
1060
+ function delay(ms) {
1061
+ return new Promise((resolve) => setTimeout(resolve, ms));
1062
+ }
1063
+ function describeSendError(sent) {
1064
+ const errorResult = sent.errorResult;
1065
+ try {
1066
+ const name = errorResult?.result?.().switch?.().name;
1067
+ if (name) return `sendTransaction returned ERROR: ${name}`;
1068
+ } catch {
1069
+ }
1070
+ return `sendTransaction returned status ${sent.status}`;
1071
+ }
1072
+ async function waitForConfirmation(server, txHash) {
1073
+ const deadline = Date.now() + CONFIRM_TIMEOUT_MS;
1074
+ for (; ; ) {
1075
+ const tx = await server.getTransaction(txHash);
1076
+ if (tx.status === "SUCCESS") return "success";
1077
+ if (tx.status === "FAILED") return "failed";
1078
+ if (Date.now() >= deadline) return "timeout";
1079
+ await delay(CONFIRM_POLL_MS);
1080
+ }
1081
+ }
1082
+ async function recordSessionSettlement(request, { logger, onCallbackError }) {
1083
+ const reportFailure = (fields) => {
1084
+ logger("error", "[vault] failed to record session_settled", {
1085
+ channelId: request.channelId,
1086
+ ...fields.txHash ? { txHash: fields.txHash } : {},
1087
+ reason: fields.reason,
1088
+ ...fields.error !== void 0 ? { error: fields.error } : {}
1089
+ });
1090
+ if (onCallbackError) {
1091
+ const err = fields.error ?? new Error(`session_settled vault record failed: ${fields.reason}`);
1092
+ onCallbackError(err, "vaultSettlement");
1093
+ }
1094
+ return { ok: false, reason: fields.reason, ...fields.txHash ? { txHash: fields.txHash } : {} };
1095
+ };
1096
+ try {
1097
+ const payeeKeypair = stellarSdk.Keypair.fromSecret(request.payeeSecretKey);
1098
+ const addresses = resolveVaultSettlementAddresses(request.payer, payeeKeypair.publicKey());
1099
+ if (!addresses) {
1100
+ logger("warn", "[vault] skipped session_settled record: payer address unavailable", {
1101
+ channelId: request.channelId
1102
+ });
1103
+ return { ok: false, reason: "payer address unavailable" };
1104
+ }
1105
+ const networkPassphrase = request.network === "mainnet" ? stellarSdk.Networks.PUBLIC : stellarSdk.Networks.TESTNET;
1106
+ const rpcUrl = request.config.rpcUrl ?? DEFAULT_RPC_URLS[request.network];
1107
+ const server = await createSorobanRpcServer(rpcUrl);
1108
+ const sourceAccount = await server.getAccount(payeeKeypair.publicKey());
1109
+ const tx = buildSessionSettlementTransaction(
1110
+ sourceAccount,
1111
+ request.config.contract,
1112
+ request.channelId,
1113
+ addresses,
1114
+ request.amount,
1115
+ request.voucherCount,
1116
+ networkPassphrase
1117
+ );
1118
+ const preparedTx = await server.prepareTransaction(tx);
1119
+ preparedTx.sign(payeeKeypair);
1120
+ const sent = await server.sendTransaction(preparedTx);
1121
+ if (sent.status === "ERROR") {
1122
+ return reportFailure({
1123
+ txHash: sent.hash || preparedTx.hash().toString("hex"),
1124
+ reason: describeSendError(sent)
1125
+ });
1126
+ }
1127
+ const txHash = sent.hash || preparedTx.hash().toString("hex");
1128
+ const confirmation = await waitForConfirmation(server, txHash);
1129
+ if (confirmation === "success") {
1130
+ logger("debug", "[vault] recorded session_settled", {
1131
+ channelId: request.channelId,
1132
+ txHash
1133
+ });
1134
+ return { ok: true, txHash };
1135
+ }
1136
+ return reportFailure({
1137
+ txHash,
1138
+ reason: confirmation === "failed" ? "transaction failed on-chain" : `confirmation timed out after ${CONFIRM_TIMEOUT_MS}ms`
1139
+ });
1140
+ } catch (err) {
1141
+ const reason = err instanceof Error ? err.message : String(err);
1142
+ return reportFailure({ reason, error: err });
1143
+ }
1144
+ }
1145
+
1146
+ // src/internal/vaultConfig.ts
1147
+ function resolveVaultSettlementConfig(option, logger) {
1148
+ if (option) return option;
1149
+ if (typeof process === "undefined") return void 0;
1150
+ const fromEnv = process.env.AGENT_VAULT_CONTRACT ?? process.env.AGENT_VAULT_CONTRACT_ID;
1151
+ if (!fromEnv) return void 0;
1152
+ logger(
1153
+ "warn",
1154
+ "[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",
1155
+ {}
1156
+ );
1157
+ return { contract: fromEnv };
1158
+ }
1159
+
377
1160
  // src/provider/registerProvider.ts
378
1161
  async function registerProvider(optionsOrSupabase, manifestArg, baseUrlArg, verifiedArg) {
379
1162
  let supabase;
@@ -471,6 +1254,8 @@ var MPP_NETWORK = {
471
1254
  testnet: "stellar:testnet",
472
1255
  mainnet: "stellar:pubnet"
473
1256
  };
1257
+ var DEFAULT_MAX_RECONCILE_ATTEMPTS = 5;
1258
+ var ALREADY_CLOSED_PATTERN = /channel has been closed|channel is closed|already closed|finalized/i;
474
1259
  function decimalToStroops(amountStr) {
475
1260
  return nulthSdk.usdcToStroops(amountStr);
476
1261
  }
@@ -480,12 +1265,15 @@ async function reconcileAbandonedSessions(opts) {
480
1265
  recoveredCount: 0,
481
1266
  skippedCount: 0,
482
1267
  failedCount: 0,
1268
+ alreadyClosedCount: 0,
1269
+ terminalCount: 0,
483
1270
  errors: []
484
1271
  };
1272
+ const maxAttempts = opts.maxReconcileAttempts ?? DEFAULT_MAX_RECONCILE_ATTEMPTS;
485
1273
  const payeeKeypair = stellarSdk.Keypair.fromSecret(opts.payeeSecretKey);
486
1274
  const networkId = MPP_NETWORK[opts.network];
487
1275
  try {
488
- 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);
1276
+ 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);
489
1277
  if (error) {
490
1278
  throw new Error(`Failed to query sessions: ${error.message}`);
491
1279
  }
@@ -507,7 +1295,7 @@ async function reconcileAbandonedSessions(opts) {
507
1295
  continue;
508
1296
  }
509
1297
  const closeAmount = decimalToStroops(cumulative_amount);
510
- const closeSig = Buffer.from(last_signature, "hex");
1298
+ const closeSig = hexToBytes(last_signature);
511
1299
  const closeFn = opts.channelClose ?? (await import('@stellar/mpp/channel/server')).close;
512
1300
  const closeTxHash = await closeFn({
513
1301
  channel: channelId,
@@ -530,11 +1318,36 @@ async function reconcileAbandonedSessions(opts) {
530
1318
  await opts.onRecovered(channelId, closeTxHash, totalPaid);
531
1319
  }
532
1320
  } catch (err) {
1321
+ const reason = formatMppError(err);
1322
+ if (ALREADY_CLOSED_PATTERN.test(reason)) {
1323
+ const { error: closeError } = await opts.supabase.from("sessions").update({
1324
+ status: "closed",
1325
+ last_reconcile_error: `channel already closed on-chain: ${reason}`,
1326
+ updated_at: (/* @__PURE__ */ new Date()).toISOString()
1327
+ }).eq("channel_id", session.channel_id);
1328
+ if (!closeError) {
1329
+ stats.alreadyClosedCount++;
1330
+ continue;
1331
+ }
1332
+ }
533
1333
  stats.failedCount++;
534
- stats.errors.push({
535
- channelId: session.channel_id,
536
- reason: formatMppError(err)
537
- });
1334
+ stats.errors.push({ channelId: session.channel_id, reason });
1335
+ const attempts = (Number(session.reconcile_attempts) || 0) + 1;
1336
+ const terminal = attempts >= maxAttempts;
1337
+ const { error: bookkeepingError } = await opts.supabase.from("sessions").update({
1338
+ reconcile_attempts: attempts,
1339
+ last_reconcile_error: reason,
1340
+ updated_at: (/* @__PURE__ */ new Date()).toISOString(),
1341
+ ...terminal ? { status: "failed" } : {}
1342
+ }).eq("channel_id", session.channel_id);
1343
+ if (bookkeepingError) {
1344
+ stats.errors.push({
1345
+ channelId: session.channel_id,
1346
+ reason: `Failed to record reconcile failure: ${bookkeepingError.message}`
1347
+ });
1348
+ } else if (terminal) {
1349
+ stats.terminalCount++;
1350
+ }
538
1351
  }
539
1352
  }
540
1353
  return stats;
@@ -555,7 +1368,7 @@ async function runStartupReconciliation(opts, logger) {
555
1368
  }
556
1369
  log(
557
1370
  "info",
558
- `[SessionReconciler] Found ${stats.orphanedCount} orphaned session(s). Recovered: ${stats.recoveredCount}, Skipped: ${stats.skippedCount}, Failed: ${stats.failedCount}`
1371
+ `[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}`
559
1372
  );
560
1373
  if (stats.errors.length > 0) {
561
1374
  log("warn", "[SessionReconciler] Reconciliation errors:");
@@ -697,12 +1510,9 @@ function createX402HonoHandler(opts) {
697
1510
  requirements
698
1511
  );
699
1512
  if (!verifyResult.isValid) {
700
- return c.json(
701
- {
702
- error: "Payment verification failed",
703
- reason: verifyResult.invalidReason
704
- },
705
- 401
1513
+ return respondPaymentRequired(
1514
+ "Payment verification failed",
1515
+ verifyResult.invalidReason
706
1516
  );
707
1517
  }
708
1518
  const settleResult = await localFacilitator.settle(
@@ -722,7 +1532,12 @@ function createX402HonoHandler(opts) {
722
1532
  if (idempotencyKey) {
723
1533
  const headers = {};
724
1534
  if (paymentResponseHeader) headers["X-Payment-Response"] = paymentResponseHeader;
725
- await seenTxStore.set(idempotencyKey, { txHash, headers, createdAt: Date.now() });
1535
+ await recordSettlement(
1536
+ seenTxStore,
1537
+ idempotencyKey,
1538
+ { txHash, headers, createdAt: Date.now() },
1539
+ { logger, label: "[x402]", onCallbackError: opts.onCallbackError }
1540
+ );
726
1541
  }
727
1542
  if (txHash && opts.onSettled) {
728
1543
  Promise.resolve().then(() => opts.onSettled(txHash, x402Price, "x402", payerAddress)).catch((err) => {
@@ -845,11 +1660,12 @@ function createMppChargeHonoHandler(opts) {
845
1660
  }
846
1661
  }
847
1662
  if (idempotencyKey) {
848
- await seenTxStore.set(idempotencyKey, {
849
- txHash: reference ?? null,
850
- headers: receiptHeaders,
851
- createdAt: Date.now()
852
- });
1663
+ await recordSettlement(
1664
+ seenTxStore,
1665
+ idempotencyKey,
1666
+ { txHash: reference ?? null, headers: receiptHeaders, createdAt: Date.now() },
1667
+ { logger, label: "[mpp-charge]", onCallbackError: opts.onCallbackError }
1668
+ );
853
1669
  }
854
1670
  if (reference && opts.onSettled) {
855
1671
  Promise.resolve().then(() => opts.onSettled(reference, chargePrice, "mpp-charge", payerAddress)).catch((err) => {
@@ -859,7 +1675,11 @@ function createMppChargeHonoHandler(opts) {
859
1675
  }
860
1676
  await next();
861
1677
  } catch (err) {
862
- throw err;
1678
+ if (err instanceof RouteDockManifestError) {
1679
+ throw err;
1680
+ }
1681
+ logger("error", "[mpp-charge] Settlement error", { error: err });
1682
+ return c.json({ error: "Payment settlement failed" }, 500);
863
1683
  }
864
1684
  };
865
1685
  }
@@ -873,9 +1693,11 @@ function createMppSessionHandlerState(opts, sessionPricing, reportMode) {
873
1693
  const logger = resolveLogger(opts.logger);
874
1694
  const voucherRecordKey = `routedock:session:voucher:${sessionPricing.channelFactory}`;
875
1695
  const innerStore = opts.sessionStore ?? server$2.Store.memory();
1696
+ const cumulativeKey = `stellar:channel:cumulative:${sessionPricing.channelFactory}`;
876
1697
  let record = null;
877
1698
  let voucherCount = 0;
878
1699
  let sessionOpened = false;
1700
+ let orphanFlagged = false;
879
1701
  let idleTimer = null;
880
1702
  let settledCleanly = false;
881
1703
  let abortListenerArmed = false;
@@ -902,15 +1724,32 @@ function createMppSessionHandlerState(opts, sessionPricing, reportMode) {
902
1724
  return null;
903
1725
  }
904
1726
  record = { amount: BigInt(stored.amount), signature: stored.signature, payer: stored.payer ?? null };
1727
+ if (typeof stored.voucherCount === "number") {
1728
+ voucherCount = stored.voucherCount;
1729
+ }
1730
+ if (typeof stored.sessionOpened === "boolean") {
1731
+ sessionOpened = stored.sessionOpened;
1732
+ } else if (record !== null) {
1733
+ sessionOpened = true;
1734
+ }
905
1735
  } catch {
906
1736
  return null;
907
1737
  }
908
1738
  return record;
909
1739
  }
1740
+ async function readStoredCumulative() {
1741
+ try {
1742
+ const raw = await innerStore.get(cumulativeKey);
1743
+ const amount = typeof raw === "object" && raw !== null ? raw.amount : void 0;
1744
+ return typeof amount === "string" && /^\d+$/.test(amount) ? BigInt(amount) : null;
1745
+ } catch {
1746
+ return null;
1747
+ }
1748
+ }
910
1749
  async function flagOrphan(reason) {
911
1750
  await loadPersistedRecord();
912
- if (!sessionOpened || settledCleanly) return;
913
- sessionOpened = false;
1751
+ if (!sessionOpened || settledCleanly || orphanFlagged) return;
1752
+ orphanFlagged = true;
914
1753
  abortListenerArmed = false;
915
1754
  clearIdleTimer();
916
1755
  const cumulativeAmount = record ? (Number(record.amount) / 1e7).toFixed(7) : "0.0000000";
@@ -958,16 +1797,20 @@ function createMppSessionHandlerState(opts, sessionPricing, reportMode) {
958
1797
  if (record && amount < record.amount) return;
959
1798
  const payer = record?.payer ?? extractPayerAddress(credential.source);
960
1799
  record = { amount, signature: payload.signature, payer };
1800
+ voucherCount++;
1801
+ settledCleanly = false;
1802
+ orphanFlagged = false;
1803
+ armIdleTimer();
1804
+ const wasOpened = sessionOpened;
1805
+ sessionOpened = true;
961
1806
  await innerStore.put(voucherRecordKey, {
962
1807
  amount: record.amount.toString(),
963
1808
  signature: record.signature,
964
- payer: record.payer
1809
+ payer: record.payer,
1810
+ voucherCount,
1811
+ sessionOpened: true
965
1812
  });
966
- voucherCount++;
967
- settledCleanly = false;
968
- armIdleTimer();
969
- if (!sessionOpened) {
970
- sessionOpened = true;
1813
+ if (!wasOpened) {
971
1814
  if (opts.onSessionOpen) {
972
1815
  Promise.resolve().then(() => opts.onSessionOpen(sessionPricing.channelFactory, record.payer)).catch((err) => {
973
1816
  logger("error", "[mpp-session] onSessionOpen callback error", { error: err });
@@ -1022,6 +1865,16 @@ function createMppSessionHandlerState(opts, sessionPricing, reportMode) {
1022
1865
  closeSig = record?.signature || (bodyAmount === recordAmount ? body?.signature ?? "" : "");
1023
1866
  }
1024
1867
  if (closeAmount > 0n && closeSig) {
1868
+ const storedCumulative = await readStoredCumulative();
1869
+ if (storedCumulative !== null && closeAmount < storedCumulative) {
1870
+ return c.json(
1871
+ {
1872
+ closeTxHash: null,
1873
+ error: "close amount below the highest verified voucher"
1874
+ },
1875
+ 409
1876
+ );
1877
+ }
1025
1878
  const closeTxHash = await server$2.close({
1026
1879
  channel: sessionPricing.channelFactory,
1027
1880
  amount: closeAmount,
@@ -1039,8 +1892,25 @@ function createMppSessionHandlerState(opts, sessionPricing, reportMode) {
1039
1892
  opts.onCallbackError?.(err, "onSettled");
1040
1893
  });
1041
1894
  }
1895
+ const vaultConfig = resolveVaultSettlementConfig(opts.vaultSettlement, logger);
1896
+ if (vaultConfig) {
1897
+ const settlement = recordSessionSettlement(
1898
+ {
1899
+ config: vaultConfig,
1900
+ network: opts.network,
1901
+ payeeSecretKey: opts.payeeSecretKey,
1902
+ channelId: sessionPricing.channelFactory,
1903
+ payer: closePayer,
1904
+ amount: closeAmount,
1905
+ voucherCount
1906
+ },
1907
+ { logger, onCallbackError: opts.onCallbackError }
1908
+ );
1909
+ if (opts.waitUntil) opts.waitUntil(settlement);
1910
+ }
1042
1911
  sessionOpened = false;
1043
1912
  voucherCount = 0;
1913
+ orphanFlagged = false;
1044
1914
  record = null;
1045
1915
  await innerStore.delete(voucherRecordKey);
1046
1916
  abortListenerArmed = false;
@@ -1065,10 +1935,11 @@ function createMppSessionHandlerState(opts, sessionPricing, reportMode) {
1065
1935
  }
1066
1936
  function createMppSessionHonoHandler(opts, config, state) {
1067
1937
  const sessionPricing = opts.pricing[config.mode];
1938
+ const logger = resolveLogger(opts.logger);
1068
1939
  return async (c, next) => {
1069
1940
  try {
1070
1941
  if (c.req.method === "DELETE") {
1071
- return state.handleDelete(c);
1942
+ return await state.handleDelete(c);
1072
1943
  }
1073
1944
  const result = await state.mppx.channel({
1074
1945
  amount: sessionPricing.rate,
@@ -1090,7 +1961,11 @@ function createMppSessionHonoHandler(opts, config, state) {
1090
1961
  }
1091
1962
  await next();
1092
1963
  } catch (err) {
1093
- throw err;
1964
+ if (err instanceof RouteDockManifestError) {
1965
+ throw err;
1966
+ }
1967
+ logger("error", `[${config.mode}] Session error`, { error: err });
1968
+ return c.json({ error: "Session payment failed" }, 500);
1094
1969
  }
1095
1970
  };
1096
1971
  }
@@ -1099,6 +1974,7 @@ function isWebSocketUpgradeRequest(c) {
1099
1974
  }
1100
1975
  function routedockHono(opts) {
1101
1976
  const handlers = [];
1977
+ assertProviderConfig(opts, "routedockHono");
1102
1978
  const signedManifest = signManifest(opts.manifest, opts.payeeSecretKey);
1103
1979
  if (opts.modes.includes("x402") && opts.pricing.x402) {
1104
1980
  handlers.push(createX402HonoHandler({ ...opts, manifest: signedManifest }));