@routedock/routedock 0.3.0 → 0.4.0

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