xytara 3.1.13 → 3.1.15
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.
- package/.env.example +38 -4
- package/BSV_TERANODE_PLATFORM_DISCONTINUITY_RUNBOOK.md +126 -126
- package/REAL_PAYMENT_SETUP.md +14 -7
- package/RELEASE_NOTES.md +9 -4
- package/fixtures/catalog.js +22 -0
- package/lib/a2a_rpc.js +287 -280
- package/lib/account_auth.js +585 -585
- package/lib/agent_identity.js +162 -162
- package/lib/asyncapi_contract.js +3 -3
- package/lib/bolt_adapter.js +187 -187
- package/lib/bolt_payment_front.js +224 -224
- package/lib/buyer_event_log.js +215 -0
- package/lib/buyer_safety_policy.js +132 -0
- package/lib/checkout_processor_adapter.js +217 -217
- package/lib/command_flow.js +911 -722
- package/lib/commerce_artifacts.js +591 -591
- package/lib/commerce_bridge.js +274 -274
- package/lib/commerce_cases.js +399 -399
- package/lib/commerce_client.js +7694 -7694
- package/lib/commerce_quotes.js +29 -0
- package/lib/commerce_runtime.js +322 -322
- package/lib/execution_dedupe.js +86 -86
- package/lib/external_credit_bridge.js +142 -142
- package/lib/http_transport.js +99 -81
- package/lib/l402_adapter.js +241 -241
- package/lib/l402_payment_front.js +265 -265
- package/lib/mpp_adapter.js +237 -237
- package/lib/openapi_contract.js +4 -0
- package/lib/payment_config.js +257 -253
- package/lib/payment_verification.js +357 -357
- package/lib/provider_payout_execution.js +155 -155
- package/lib/provider_registry.js +852 -842
- package/lib/rate_limiter.js +81 -81
- package/lib/runtime_state_store.js +495 -369
- package/lib/settlement_bsv_live.js +600 -600
- package/lib/settlement_bsv_teranode.js +117 -117
- package/lib/stripe_mpp_payment_front.js +258 -258
- package/lib/x402_facilitator.js +198 -191
- package/lib/x402_lane_contract.js +5 -3
- package/lib/x402_settlement_recovery.js +175 -0
- package/package.json +9 -3
- package/scripts/buyer_events_daily_report.js +79 -0
- package/scripts/state_backup_offsite.js +196 -0
- package/scripts/verify_a2a_rpc.js +131 -105
- package/scripts/verify_agent_identity.js +153 -153
- package/scripts/verify_all.js +1 -1
- package/scripts/verify_bolt_adapter.js +128 -128
- package/scripts/verify_buyer_event_log.js +123 -0
- package/scripts/verify_buyer_safety.js +141 -0
- package/scripts/verify_cdp_facilitator.js +110 -110
- package/scripts/verify_checkout_processor.js +109 -109
- package/scripts/verify_commerce_bridge.js +177 -177
- package/scripts/verify_execution_dedupe.js +189 -189
- package/scripts/verify_execution_surfacing.js +76 -76
- package/scripts/verify_final_activation.js +191 -191
- package/scripts/verify_l402_adapter.js +86 -86
- package/scripts/verify_lifecycle_spend_binding.js +88 -0
- package/scripts/verify_mpp_adapter.js +128 -128
- package/scripts/verify_production_readiness.js +268 -268
- package/scripts/verify_rate_limiter.js +52 -52
- package/scripts/verify_refund_credit.js +106 -106
- package/scripts/verify_release_candidate.js +81 -81
- package/scripts/verify_runtime_state_guard.js +117 -0
- package/scripts/verify_service.js +15421 -15421
- package/scripts/verify_verify_execute_settle.js +209 -208
- package/scripts/verify_x402_facilitator.js +307 -286
- package/scripts/verify_x402_settlement_recovery.js +153 -0
- package/server.js +12242 -11974
package/lib/command_flow.js
CHANGED
|
@@ -1,722 +1,911 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
|
|
3
|
-
const {
|
|
4
|
-
createCommandResult,
|
|
5
|
-
createQuote,
|
|
6
|
-
hydrateQuoteStates
|
|
7
|
-
} = require("./commerce_runtime");
|
|
8
|
-
const { consumeAccountCredits, buildUsageMeterPreview, previewCreditSpend } = require("./commerce_economics");
|
|
9
|
-
const { resolveSpendCredential, evaluateAuthorityBindingAction } = require("./account_auth");
|
|
10
|
-
const { validateIntegrationSelection } = require("../integrations/dispatch");
|
|
11
|
-
const { markPaymentAccepted, settleDeferredPayment, verifyPaymentAcceptance } = require("./payment_verification");
|
|
12
|
-
const { executeExternalTarget, resolveExecutionTarget } = require("./external_execution_runtime");
|
|
13
|
-
const {
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
}
|
|
38
|
-
if (v.verification_mode === "
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
function
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
return
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
: null
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
||
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
//
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
}
|
|
260
|
-
|
|
261
|
-
//
|
|
262
|
-
//
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
//
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
}
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
}
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
const
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
const
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
const
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
:
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
if (
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
payload
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const {
|
|
4
|
+
createCommandResult,
|
|
5
|
+
createQuote,
|
|
6
|
+
hydrateQuoteStates
|
|
7
|
+
} = require("./commerce_runtime");
|
|
8
|
+
const { consumeAccountCredits, buildUsageMeterPreview, previewCreditSpend } = require("./commerce_economics");
|
|
9
|
+
const { resolveSpendCredential, evaluateAuthorityBindingAction } = require("./account_auth");
|
|
10
|
+
const { validateIntegrationSelection } = require("../integrations/dispatch");
|
|
11
|
+
const { markPaymentAccepted, settleDeferredPayment, verifyPaymentAcceptance } = require("./payment_verification");
|
|
12
|
+
const { executeExternalTarget, loadExecutionTargetRegistry, resolveExecutionTarget } = require("./external_execution_runtime");
|
|
13
|
+
const {
|
|
14
|
+
STATE_RESTORE_PENDING_RESPONSE,
|
|
15
|
+
creditSpendPolicyMode,
|
|
16
|
+
creditUnitPriceAtomic,
|
|
17
|
+
isStateRestorePending,
|
|
18
|
+
paidExecutionPolicyMode
|
|
19
|
+
} = require("./buyer_safety_policy");
|
|
20
|
+
const { externalExecutionBudgetMs } = require("./external_execution_policy");
|
|
21
|
+
const { encodePaymentResponse } = require("./x402_facilitator");
|
|
22
|
+
const {
|
|
23
|
+
authorizationRefFromVerification,
|
|
24
|
+
confirmAuthorizationUsed,
|
|
25
|
+
rememberReplay,
|
|
26
|
+
resolveReplay
|
|
27
|
+
} = require("./x402_settlement_recovery");
|
|
28
|
+
const { hashCanonical, issueDeliveryReceipt, loadDeliveryReceiptSigners } = require("./delivery_receipt");
|
|
29
|
+
|
|
30
|
+
const DELIVERY_RECEIPT_ISSUER = "xytara-runtime";
|
|
31
|
+
const DELIVERY_RECEIPT_RETENTION = 5000;
|
|
32
|
+
|
|
33
|
+
// The payment leg of a delivery receipt: on-chain facts for x402, the spend record for credits,
|
|
34
|
+
// and the verification mode for any other lane. Never the buyer's signature or secrets.
|
|
35
|
+
function deliveryReceiptPayment(verification, quote) {
|
|
36
|
+
const v = verification && typeof verification === "object" ? verification : {};
|
|
37
|
+
const paymentPayload = v.payment_payload && typeof v.payment_payload === "object" ? v.payment_payload : {};
|
|
38
|
+
if (v.verification_mode === "x402_facilitator") {
|
|
39
|
+
const settlement = v.settlement && typeof v.settlement === "object" ? v.settlement : null;
|
|
40
|
+
return {
|
|
41
|
+
protocol: "x402",
|
|
42
|
+
scheme: "exact",
|
|
43
|
+
network: paymentPayload.x402_network || (settlement && settlement.network) || null,
|
|
44
|
+
asset: paymentPayload.x402_asset || null,
|
|
45
|
+
amount: paymentPayload.x402_amount !== undefined && paymentPayload.x402_amount !== null ? String(paymentPayload.x402_amount) : null,
|
|
46
|
+
payer: paymentPayload.x402_payer || (settlement && settlement.payer) || null,
|
|
47
|
+
pay_to: paymentPayload.x402_pay_to || null,
|
|
48
|
+
settlement_tx: settlement ? (settlement.transaction || settlement.txid || null) : null,
|
|
49
|
+
settled: Boolean(settlement && settlement.success !== false && (settlement.transaction || settlement.txid))
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
if (v.verification_mode === "account_credits" || paymentPayload.credit_spend_id) {
|
|
53
|
+
return {
|
|
54
|
+
protocol: "account_credits",
|
|
55
|
+
credit_spend_id: paymentPayload.credit_spend_id || null,
|
|
56
|
+
quoted_amount_minor: quote && Number.isFinite(Number(quote.amount_minor)) ? Number(quote.amount_minor) : null,
|
|
57
|
+
currency: (quote && quote.currency) || null
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
return {
|
|
61
|
+
protocol: v.verification_mode || "unspecified",
|
|
62
|
+
quoted_amount_minor: quote && Number.isFinite(Number(quote.amount_minor)) ? Number(quote.amount_minor) : null,
|
|
63
|
+
currency: (quote && quote.currency) || null
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Issues the signed delivery receipt for a completed paid call, attaches it to the response, and
|
|
68
|
+
// keeps it retrievable by id (bounded). Fail-safe: a receipt problem never breaks a completed call.
|
|
69
|
+
function attachDeliveryReceipt(state, result, payload, quote, verification, externalExecutionOptions) {
|
|
70
|
+
try {
|
|
71
|
+
if (!result || !result.receipt) return;
|
|
72
|
+
const execution = externalExecutionOptions && externalExecutionOptions.externalExecution
|
|
73
|
+
? externalExecutionOptions.externalExecution
|
|
74
|
+
: null;
|
|
75
|
+
const deliverables = result.command && Array.isArray(result.command.deliverables) ? result.command.deliverables : [];
|
|
76
|
+
const outputs = deliverables.map((deliverable) => (deliverable && deliverable.output_payload !== undefined ? deliverable.output_payload : null));
|
|
77
|
+
const body = payload && payload.body && typeof payload.body === "object" && !Array.isArray(payload.body) ? payload.body : {};
|
|
78
|
+
const taskRef = (payload && payload.task_ref)
|
|
79
|
+
|| (Array.isArray(payload && payload.tasks) && payload.tasks[0] && payload.tasks[0].task_ref)
|
|
80
|
+
|| null;
|
|
81
|
+
const { signers } = loadDeliveryReceiptSigners("XYTARA_DELIVERY_RECEIPT");
|
|
82
|
+
const deliveryReceipt = issueDeliveryReceipt({
|
|
83
|
+
receipt_id: `drcpt_${result.receipt.receipt_id.replace(/^rcpt_/, "")}`,
|
|
84
|
+
issuer: DELIVERY_RECEIPT_ISSUER,
|
|
85
|
+
call: {
|
|
86
|
+
quote_id: quote && quote.quote_id ? quote.quote_id : null,
|
|
87
|
+
transaction_id: result.transaction ? result.transaction.transaction_id : null,
|
|
88
|
+
runtime_receipt_id: result.receipt.receipt_id,
|
|
89
|
+
task_ref: taskRef,
|
|
90
|
+
tool_name: typeof body.tool_name === "string" ? body.tool_name : null
|
|
91
|
+
},
|
|
92
|
+
payment: deliveryReceiptPayment(verification, quote),
|
|
93
|
+
delivery: {
|
|
94
|
+
status: "completed",
|
|
95
|
+
provider_id: execution && execution.registered_provider_id ? execution.registered_provider_id : null,
|
|
96
|
+
routing: execution
|
|
97
|
+
? (execution.registered_provider_id ? "open_provider_registry" : "operator_env_target")
|
|
98
|
+
: "runtime_native",
|
|
99
|
+
execution_target_id: execution && execution.target_id ? execution.target_id : null,
|
|
100
|
+
request_sha256: hashCanonical(body),
|
|
101
|
+
request_hash_input: "the request `body` object (e.g. { tool_name, arguments })",
|
|
102
|
+
output_sha256: hashCanonical(outputs.length === 1 ? outputs[0] : outputs),
|
|
103
|
+
output_hash_input: outputs.length === 1
|
|
104
|
+
? "command.deliverables[0].output_payload (null when the task returns no payload)"
|
|
105
|
+
: "the array of command.deliverables[*].output_payload",
|
|
106
|
+
completed_at: (result.receipt.timing && result.receipt.timing.completed_at_iso) || result.receipt.created_at_iso || null
|
|
107
|
+
}
|
|
108
|
+
}, signers);
|
|
109
|
+
result.delivery_receipt = deliveryReceipt;
|
|
110
|
+
if (!(state.deliveryReceipts instanceof Map)) state.deliveryReceipts = new Map();
|
|
111
|
+
state.deliveryReceipts.set(deliveryReceipt.receipt_id, deliveryReceipt);
|
|
112
|
+
while (state.deliveryReceipts.size > DELIVERY_RECEIPT_RETENTION) {
|
|
113
|
+
state.deliveryReceipts.delete(state.deliveryReceipts.keys().next().value);
|
|
114
|
+
}
|
|
115
|
+
} catch (_) { /* non-fatal: never break a completed, possibly settled call */ }
|
|
116
|
+
}
|
|
117
|
+
const { resolveRegisteredTargets, hasActiveMatchingProvider, recordProviderHealth, recordProviderRouting, recordProviderSettlement } = require("./provider_registry");
|
|
118
|
+
const { claimDeferredExecution, releaseDeferredExecution } = require("./execution_dedupe");
|
|
119
|
+
|
|
120
|
+
function hasPaymentHeaders(headers, requiredHeaders) {
|
|
121
|
+
return requiredHeaders.every((headerName) => headers[headerName]);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function parseBearerToken(headers) {
|
|
125
|
+
const authorization = headers && typeof headers.authorization === "string"
|
|
126
|
+
? headers.authorization.trim()
|
|
127
|
+
: "";
|
|
128
|
+
const match = authorization.match(/^Bearer\s+(.+)$/i);
|
|
129
|
+
return match ? match[1].trim() : null;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// Extract an agent-identity proof (for key-bound spend credentials) from request
|
|
133
|
+
// headers: X-Agent-Signature (base64 ed25519 sig) + X-Agent-Timestamp (ms).
|
|
134
|
+
function extractAgentProof(headers) {
|
|
135
|
+
const h = headers || {};
|
|
136
|
+
return {
|
|
137
|
+
signature: typeof h["x-agent-signature"] === "string" ? h["x-agent-signature"] : null,
|
|
138
|
+
timestamp: typeof h["x-agent-timestamp"] === "string" ? h["x-agent-timestamp"] : null
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function buildCreditCommitPayload(payload, quote, requiredUnits) {
|
|
143
|
+
return {
|
|
144
|
+
account_id: quote.account_id,
|
|
145
|
+
agent_id: payload.agent_id || null,
|
|
146
|
+
budget_id: payload.budget_id || null,
|
|
147
|
+
pack_id: payload.pack_id || null,
|
|
148
|
+
entitlement_id: payload.entitlement_id || null,
|
|
149
|
+
quote_id: quote.quote_id,
|
|
150
|
+
task_ref: payload.task_ref || null,
|
|
151
|
+
workflow_ref: payload.workflow_ref || null,
|
|
152
|
+
units: requiredUnits,
|
|
153
|
+
reason: "credit_first_execution"
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function buildCreditRejectionResponse(creditDecision, quote) {
|
|
158
|
+
const failureReason = creditDecision && creditDecision.credit_spend_preview
|
|
159
|
+
? creditDecision.credit_spend_preview.failure_reason
|
|
160
|
+
: null;
|
|
161
|
+
const reason = failureReason
|
|
162
|
+
|| (creditDecision && creditDecision.policy_guard && creditDecision.policy_guard.reason)
|
|
163
|
+
|| (creditDecision && creditDecision.entitlement_guard && creditDecision.entitlement_guard.reason)
|
|
164
|
+
|| (creditDecision && creditDecision.reason)
|
|
165
|
+
|| "credit_spend_not_allowed";
|
|
166
|
+
|
|
167
|
+
if (reason === "insufficient_credits") {
|
|
168
|
+
const preview = creditDecision && creditDecision.credit_spend_preview ? creditDecision.credit_spend_preview : {};
|
|
169
|
+
return {
|
|
170
|
+
status: 402,
|
|
171
|
+
payload: {
|
|
172
|
+
ok: false,
|
|
173
|
+
reason: "insufficient_credits",
|
|
174
|
+
insufficient_credits: {
|
|
175
|
+
account_id: quote.account_id,
|
|
176
|
+
required_units: preview.required_units,
|
|
177
|
+
available_units: preview.available_units,
|
|
178
|
+
recommended_next_surfaces: {
|
|
179
|
+
credits_topup: "/x402/credits/topup",
|
|
180
|
+
direct_payment: "/v1/payment-quotes"
|
|
181
|
+
}
|
|
182
|
+
},
|
|
183
|
+
quote
|
|
184
|
+
}
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
return {
|
|
189
|
+
status: 403,
|
|
190
|
+
payload: {
|
|
191
|
+
ok: false,
|
|
192
|
+
reason,
|
|
193
|
+
account_id: quote.account_id,
|
|
194
|
+
policy_guard: creditDecision && creditDecision.policy_guard ? creditDecision.policy_guard : null,
|
|
195
|
+
entitlement_guard: creditDecision && creditDecision.entitlement_guard ? creditDecision.entitlement_guard : null,
|
|
196
|
+
quote
|
|
197
|
+
}
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// What a provider is allowed to see of the buyer's payment context: the verification outcome and
|
|
202
|
+
// scalar payment facts only. NEVER the signed x402 authorization / deferred-settlement material
|
|
203
|
+
// (that would hand a third-party provider the buyer's signature before settlement), and never
|
|
204
|
+
// signature-, secret- or raw-looking keys or nested objects from any lane's payload.
|
|
205
|
+
const SENSITIVE_CONTEXT_KEY = /signature|secret|private|authorization|raw|base64|header|token/i;
|
|
206
|
+
function sanitizeAuthorizationContextForProvider(context) {
|
|
207
|
+
if (!context || typeof context !== "object") return null;
|
|
208
|
+
const payload = context.payment_payload && typeof context.payment_payload === "object" ? context.payment_payload : null;
|
|
209
|
+
const safePayload = payload
|
|
210
|
+
? Object.fromEntries(Object.entries(payload).filter(([key, value]) => !SENSITIVE_CONTEXT_KEY.test(key) && (value === null || typeof value !== "object")))
|
|
211
|
+
: null;
|
|
212
|
+
return {
|
|
213
|
+
ok: context.ok === true,
|
|
214
|
+
verification_mode: context.verification_mode || null,
|
|
215
|
+
wallet_id: context.wallet_id || null,
|
|
216
|
+
age_seconds: Number.isFinite(Number(context.age_seconds)) ? Number(context.age_seconds) : null,
|
|
217
|
+
payment_payload: safePayload
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// The execution deadline: the configured budget, but never so late that the buyer's signed x402
|
|
222
|
+
// authorization expires before we can settle it (keep 20 s for the settle call).
|
|
223
|
+
function executionDeadlineMs(authorizationContext) {
|
|
224
|
+
const budgetDeadline = Date.now() + externalExecutionBudgetMs();
|
|
225
|
+
const deferred = authorizationContext && authorizationContext.deferred_settlement;
|
|
226
|
+
const signed = deferred && deferred.authorization && deferred.authorization.paymentPayload
|
|
227
|
+
&& deferred.authorization.paymentPayload.payload && deferred.authorization.paymentPayload.payload.authorization;
|
|
228
|
+
const validBeforeSeconds = signed ? Number(signed.validBefore) : NaN;
|
|
229
|
+
if (!Number.isFinite(validBeforeSeconds) || validBeforeSeconds <= 0) return budgetDeadline;
|
|
230
|
+
return Math.min(budgetDeadline, validBeforeSeconds * 1000 - 20000);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
async function buildExternalExecutionOptions(state, payload, quote, authorizationContext) {
|
|
234
|
+
// One wall-clock budget for the whole call (every retry of every candidate), inside the x402
|
|
235
|
+
// authorization window — see external_execution_policy.js.
|
|
236
|
+
const executionOptions = { deadline_ms: executionDeadlineMs(authorizationContext) };
|
|
237
|
+
const forwardedContext = sanitizeAuthorizationContextForProvider(authorizationContext);
|
|
238
|
+
// Operator-pinned env target resolves first and is used as-is (an explicit single target,
|
|
239
|
+
// no failover). Empty env + empty registry -> {} (unchanged behavior).
|
|
240
|
+
const envTarget = resolveExecutionTarget(payload, quote);
|
|
241
|
+
if (envTarget) {
|
|
242
|
+
const execution = await executeExternalTarget(envTarget, payload, quote, forwardedContext, executionOptions);
|
|
243
|
+
if (!execution || !execution.ok) {
|
|
244
|
+
return { error: execution || { ok: false, status: 502, reason: "external_execution_failed" } };
|
|
245
|
+
}
|
|
246
|
+
return { externalExecution: execution };
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// Open-network path: try health-ranked registered providers (healthy first, then half-open)
|
|
250
|
+
// in order, FAILING OVER to the next when one is down — so a single unhealthy/throttled
|
|
251
|
+
// provider doesn't fail the buyer when another serves the same capability. Each attempt
|
|
252
|
+
// updates the provider's health; accounting is wrapped so it can never break execution.
|
|
253
|
+
const candidates = resolveRegisteredTargets(state, payload, quote);
|
|
254
|
+
if (candidates.length < 1) {
|
|
255
|
+
// No HEALTHY candidate. If providers DO serve this capability but are all in cooldown,
|
|
256
|
+
// fail closed (503) so the buyer is NOT charged for undelivered work — rather than falling
|
|
257
|
+
// through to the synthetic no-op. If nothing serves it, keep the legacy local path ({}).
|
|
258
|
+
if (hasActiveMatchingProvider(state, payload, quote)) {
|
|
259
|
+
return { error: { ok: false, status: 503, reason: "no_healthy_provider" } };
|
|
260
|
+
}
|
|
261
|
+
// Defense in depth behind assessPaidExecutability: on a hosted deploy a call with no real
|
|
262
|
+
// execution target must never reach settlement with a synthetic result.
|
|
263
|
+
if (paidExecutionPolicyMode() === "strict") {
|
|
264
|
+
return { error: { ok: false, status: 501, reason: "capability_not_executable" } };
|
|
265
|
+
}
|
|
266
|
+
return {};
|
|
267
|
+
}
|
|
268
|
+
let lastError = null;
|
|
269
|
+
for (const target of candidates) {
|
|
270
|
+
if (Date.now() >= executionOptions.deadline_ms) {
|
|
271
|
+
// Budget spent on earlier candidates: stop here rather than start a call that cannot settle.
|
|
272
|
+
lastError = lastError || { ok: false, status: 504, reason: "external_execution_budget_exhausted" };
|
|
273
|
+
break;
|
|
274
|
+
}
|
|
275
|
+
const execution = await executeExternalTarget(target, payload, quote, forwardedContext, executionOptions);
|
|
276
|
+
try { recordProviderRouting(state, target.registered_provider_id); } catch (_) { /* non-fatal */ }
|
|
277
|
+
if (execution && execution.ok) {
|
|
278
|
+
try { recordProviderHealth(state, target.registered_provider_id, { ok: true }); } catch (_) { /* non-fatal */ }
|
|
279
|
+
execution.registered_provider_id = target.registered_provider_id;
|
|
280
|
+
return { externalExecution: execution };
|
|
281
|
+
}
|
|
282
|
+
try { recordProviderHealth(state, target.registered_provider_id, { ok: false }); } catch (_) { /* non-fatal */ }
|
|
283
|
+
lastError = execution || { ok: false, status: 502, reason: "external_execution_failed" };
|
|
284
|
+
// Fail over to the NEXT provider ONLY when this one provably did NOT process the request
|
|
285
|
+
// (rejected / unreachable). Never on an ambiguous failure (timeout, reset, 5xx) — that
|
|
286
|
+
// could make a side-effectful provider execute twice. Health still marks the failure.
|
|
287
|
+
if (!(execution && execution.failover_safe)) break;
|
|
288
|
+
}
|
|
289
|
+
return { error: lastError };
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
// Assets the x402 lane can settle in, with their on-chain decimals (needed to turn settled base
|
|
293
|
+
// units into the ledger's cents). Anything else cannot be reconciled and must not accrue on the quote.
|
|
294
|
+
const X402_ASSET_DECIMALS = new Map([
|
|
295
|
+
["0x833589fcd6edb6e08f4c7c32d4f71b54bda02913", 6], // USDC, Base mainnet (eip155:8453)
|
|
296
|
+
["0x036cbd53842c5426634e7929541ec2318f3dcf7e", 6] // USDC, Base Sepolia (eip155:84532)
|
|
297
|
+
]);
|
|
298
|
+
|
|
299
|
+
// The amount a provider's revenue share is computed from. The quote is what we ASKED for; on the
|
|
300
|
+
// x402 lane the facilitator settles exactly the advertised `accepts` amount, which may differ from
|
|
301
|
+
// the catalog quote — so accrue on min(quote, settled) and never on money that was not collected.
|
|
302
|
+
// Other lanes bind the paid amount to the quote at verification time (local_signed) or charge the
|
|
303
|
+
// quote directly (credits / hosted checkout), so the quote is the settled amount there.
|
|
304
|
+
function reconcileProviderSettlementAmount(quote, verification) {
|
|
305
|
+
const quoted = quote && Number.isFinite(Number(quote.amount_minor)) ? Math.max(0, Math.round(Number(quote.amount_minor))) : 0;
|
|
306
|
+
const mode = verification && typeof verification.verification_mode === "string" ? verification.verification_mode : null;
|
|
307
|
+
// Credit spends accrue on what the consumed units were actually bought for (units x unit
|
|
308
|
+
// price; USDC 6 decimals -> cents), never on the catalog quote.
|
|
309
|
+
const creditPayload = (verification && verification.payment_payload) || {};
|
|
310
|
+
const creditUnits = Number(creditPayload.credit_units);
|
|
311
|
+
const unitPriceAtomic = Number(creditPayload.credit_unit_price_atomic);
|
|
312
|
+
if (Number.isFinite(creditUnits) && creditUnits > 0 && Number.isFinite(unitPriceAtomic) && unitPriceAtomic > 0) {
|
|
313
|
+
const settledCents = Math.floor((creditUnits * unitPriceAtomic) / 10000);
|
|
314
|
+
return { amount_minor: Math.min(quoted, settledCents), quoted_minor: quoted, settled_minor: settledCents, settlement_basis: "credits_unit_value_min_quote", reconciled: true };
|
|
315
|
+
}
|
|
316
|
+
if (mode !== "x402_facilitator") {
|
|
317
|
+
return { amount_minor: quoted, quoted_minor: quoted, settled_minor: null, settlement_basis: mode ? `${mode}_quote` : "quote", reconciled: true };
|
|
318
|
+
}
|
|
319
|
+
const paymentPayload = (verification && verification.payment_payload) || {};
|
|
320
|
+
const decimals = X402_ASSET_DECIMALS.get(String(paymentPayload.x402_asset || "").toLowerCase());
|
|
321
|
+
const baseUnits = /^\d{1,30}$/.test(String(paymentPayload.x402_amount)) ? Number(paymentPayload.x402_amount) : NaN;
|
|
322
|
+
const centsCurrency = /^USD(_CENTS)?$/i.test(String((quote && quote.currency) || "USD_CENTS"));
|
|
323
|
+
if (decimals === undefined || !Number.isFinite(baseUnits) || !centsCurrency) {
|
|
324
|
+
return { amount_minor: 0, quoted_minor: quoted, settled_minor: null, settlement_basis: "x402_unreconciled_asset", reconciled: false };
|
|
325
|
+
}
|
|
326
|
+
const settledCents = Math.floor(baseUnits / Math.pow(10, decimals - 2));
|
|
327
|
+
return { amount_minor: Math.min(quoted, settledCents), quoted_minor: quoted, settled_minor: settledCents, settlement_basis: "x402_settled_min_quote", reconciled: true };
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
// Records the provider revenue-share split for a settled call routed to a registered
|
|
331
|
+
// provider. Fail-safe: metering must NEVER break or reverse a settled/charged response.
|
|
332
|
+
function attributeProviderSettlement(state, externalExecutionOptions, payload, quote, extra, verification) {
|
|
333
|
+
const execution = externalExecutionOptions && externalExecutionOptions.externalExecution;
|
|
334
|
+
const providerId = execution && execution.registered_provider_id;
|
|
335
|
+
if (!providerId) return;
|
|
336
|
+
try {
|
|
337
|
+
const taskRef = (payload && payload.task_ref)
|
|
338
|
+
|| (Array.isArray(payload && payload.tasks) && payload.tasks[0] && payload.tasks[0].task_ref)
|
|
339
|
+
|| null;
|
|
340
|
+
const reconciled = reconcileProviderSettlementAmount(quote, verification);
|
|
341
|
+
recordProviderSettlement(state, {
|
|
342
|
+
provider_id: providerId,
|
|
343
|
+
amount_minor: reconciled.amount_minor,
|
|
344
|
+
quoted_minor: reconciled.quoted_minor,
|
|
345
|
+
settled_minor: reconciled.settled_minor,
|
|
346
|
+
settlement_basis: reconciled.settlement_basis,
|
|
347
|
+
reconciled: reconciled.reconciled,
|
|
348
|
+
currency: quote && quote.currency ? quote.currency : null,
|
|
349
|
+
transaction_id: (extra && extra.transaction_id) || (quote && quote.quote_id) || null,
|
|
350
|
+
settlement_ref: (extra && extra.settlement_ref) || null,
|
|
351
|
+
task_ref: taskRef
|
|
352
|
+
});
|
|
353
|
+
} catch (_) { /* non-fatal: a settled call is never undone by an accounting error */ }
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
// When a buyer is quoted for a routed task, wake the provider(s) that will serve it: the buyer
|
|
357
|
+
// spends a few seconds signing the payment, which is usually enough for a sleeping free-tier host
|
|
358
|
+
// to come up. Fire-and-forget, at most once a minute per provider, never blocks the response.
|
|
359
|
+
const providerWarmedAt = new Map();
|
|
360
|
+
function warmProvidersForCall(state, payload, quote) {
|
|
361
|
+
try {
|
|
362
|
+
const now = Date.now();
|
|
363
|
+
for (const target of resolveRegisteredTargets(state, payload, quote).slice(0, 3)) {
|
|
364
|
+
const last = providerWarmedAt.get(target.registered_provider_id) || 0;
|
|
365
|
+
if (now - last < 60000) continue;
|
|
366
|
+
providerWarmedAt.set(target.registered_provider_id, now);
|
|
367
|
+
const origin = new URL(target.endpoint_url).origin;
|
|
368
|
+
fetch(`${origin}/health`, { method: "GET", signal: AbortSignal.timeout(3000), redirect: "error" }).catch(() => {});
|
|
369
|
+
}
|
|
370
|
+
} catch (_) { /* best effort */ }
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
// Task refs that resolve to a real execution target right now: operator-pinned env targets plus
|
|
374
|
+
// active open-network providers. This is the set a hosted deploy will accept payment for.
|
|
375
|
+
function listExecutableTaskRefs(state) {
|
|
376
|
+
const refs = new Set();
|
|
377
|
+
try {
|
|
378
|
+
for (const target of loadExecutionTargetRegistry()) {
|
|
379
|
+
if (target && target.enabled && target.task_ref) refs.add(target.task_ref);
|
|
380
|
+
}
|
|
381
|
+
} catch (_) { /* a malformed env registry simply contributes nothing */ }
|
|
382
|
+
const registry = state && state.providerRegistry;
|
|
383
|
+
if (registry && typeof registry.values === "function") {
|
|
384
|
+
for (const provider of registry.values()) {
|
|
385
|
+
if (provider && provider.status === "active" && Array.isArray(provider.task_refs)) {
|
|
386
|
+
provider.task_refs.forEach((ref) => refs.add(ref));
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
return Array.from(refs).sort();
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
// Refuse BEFORE quoting/charging any task that nothing can actually execute (strict policy), so
|
|
394
|
+
// a buyer never pays for a synthetic "completed" result. Permissive (local) keeps legacy behavior.
|
|
395
|
+
function assessPaidExecutability(state, payload) {
|
|
396
|
+
if (paidExecutionPolicyMode() !== "strict") return { ok: true };
|
|
397
|
+
const body = payload && typeof payload === "object" ? payload : {};
|
|
398
|
+
const taskRefs = Array.isArray(body.tasks) && body.tasks.length > 0
|
|
399
|
+
? body.tasks.map((task) => String((task && task.task_ref) || "").trim())
|
|
400
|
+
: [String(body.task_ref || "").trim()];
|
|
401
|
+
const executable = listExecutableTaskRefs(state);
|
|
402
|
+
if (taskRefs.some((ref) => !ref)) {
|
|
403
|
+
return {
|
|
404
|
+
ok: false,
|
|
405
|
+
status: 400,
|
|
406
|
+
payload: { ok: false, reason: "task_ref_required", detail: "Name the task to run in task_ref.", executable_task_refs: executable }
|
|
407
|
+
};
|
|
408
|
+
}
|
|
409
|
+
const probeQuote = { integration_ids: Array.isArray(body.integration_ids) ? body.integration_ids : [] };
|
|
410
|
+
const runnable = (ref) => {
|
|
411
|
+
const single = { ...body, task_ref: ref, tasks: undefined };
|
|
412
|
+
return Boolean(resolveExecutionTarget(single, probeQuote)) || hasActiveMatchingProvider(state, single, probeQuote);
|
|
413
|
+
};
|
|
414
|
+
const missing = taskRefs.filter((ref) => !runnable(ref));
|
|
415
|
+
if (missing.length > 0) {
|
|
416
|
+
return {
|
|
417
|
+
ok: false,
|
|
418
|
+
status: 501,
|
|
419
|
+
payload: {
|
|
420
|
+
ok: false,
|
|
421
|
+
reason: "capability_not_executable",
|
|
422
|
+
detail: "Nothing can execute " + missing.join(", ") + " right now, so no payment is requested. Choose one of executable_task_refs, or call the MCP tools directly at https://xytara-mcp-executor.onrender.com/mcp.",
|
|
423
|
+
task_refs: missing,
|
|
424
|
+
executable_task_refs: executable
|
|
425
|
+
}
|
|
426
|
+
};
|
|
427
|
+
}
|
|
428
|
+
return { ok: true };
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
async function executeCommandRequest(state, payload, headers, options) {
|
|
432
|
+
const opts = options || {};
|
|
433
|
+
const requiredHeaders = Array.isArray(opts.requiredHeaders) && opts.requiredHeaders.length > 0
|
|
434
|
+
? opts.requiredHeaders
|
|
435
|
+
: ["x-wallet-id", "x-payment", "x-payment-signature"];
|
|
436
|
+
const challengePayload = typeof opts.challengePayload === "function"
|
|
437
|
+
? opts.challengePayload
|
|
438
|
+
: (quote) => ({ ok: false, quote });
|
|
439
|
+
|
|
440
|
+
hydrateQuoteStates(state);
|
|
441
|
+
const validation = validateIntegrationSelection(payload);
|
|
442
|
+
if (!validation.ok) {
|
|
443
|
+
return {
|
|
444
|
+
status: 400,
|
|
445
|
+
payload: {
|
|
446
|
+
ok: false,
|
|
447
|
+
reason: validation.reason,
|
|
448
|
+
errors: validation.errors,
|
|
449
|
+
selection: validation.selection
|
|
450
|
+
}
|
|
451
|
+
};
|
|
452
|
+
}
|
|
453
|
+
const executability = assessPaidExecutability(state, payload);
|
|
454
|
+
if (!executability.ok) {
|
|
455
|
+
return { status: executability.status, payload: executability.payload };
|
|
456
|
+
}
|
|
457
|
+
const prefersCredits = payload && payload.payment_preference === "credits_first";
|
|
458
|
+
if (prefersCredits && isStateRestorePending()) {
|
|
459
|
+
return { status: 503, payload: { ...STATE_RESTORE_PENDING_RESPONSE }, headers: { "Retry-After": "30" } };
|
|
460
|
+
}
|
|
461
|
+
if (!hasPaymentHeaders(headers || {}, requiredHeaders)) {
|
|
462
|
+
const bearerToken = parseBearerToken(headers);
|
|
463
|
+
const delegatedSpendRequested = typeof bearerToken === "string" && bearerToken.startsWith("xytara_spend_");
|
|
464
|
+
const delegatedSpend = delegatedSpendRequested
|
|
465
|
+
? resolveSpendCredential(state, bearerToken, extractAgentProof(headers), {
|
|
466
|
+
account_id: payload && payload.account_id,
|
|
467
|
+
task_ref: payload && payload.task_ref,
|
|
468
|
+
units: payload && payload.quoted_units,
|
|
469
|
+
// Strict binding is scoped to this execute path (where a binding is supplied); the
|
|
470
|
+
// lifecycle paths pass no binding and are intentionally unaffected.
|
|
471
|
+
require: String(process.env.XYTARA_REQUIRE_SPEND_PROOF_BINDING || "").trim() === "1"
|
|
472
|
+
})
|
|
473
|
+
: { ok: false, reason: "spend_credential_not_requested" };
|
|
474
|
+
if (delegatedSpendRequested && !delegatedSpend.ok) {
|
|
475
|
+
return {
|
|
476
|
+
status: 403,
|
|
477
|
+
payload: {
|
|
478
|
+
ok: false,
|
|
479
|
+
reason: delegatedSpend.reason
|
|
480
|
+
}
|
|
481
|
+
};
|
|
482
|
+
}
|
|
483
|
+
if (delegatedSpend.ok) {
|
|
484
|
+
// Pin the command to the credential's own account — a delegated spend token must
|
|
485
|
+
// never be aimed at another account by supplying a different account_id.
|
|
486
|
+
if (payload && typeof payload.account_id === "string" && payload.account_id && payload.account_id !== delegatedSpend.credential.account_id) {
|
|
487
|
+
return { status: 403, payload: { ok: false, reason: "delegated_account_mismatch" } };
|
|
488
|
+
}
|
|
489
|
+
payload = {
|
|
490
|
+
...payload,
|
|
491
|
+
account_id: delegatedSpend.credential.account_id,
|
|
492
|
+
agent_id: payload && payload.agent_id ? payload.agent_id : delegatedSpend.credential.agent_id,
|
|
493
|
+
budget_id: payload && payload.budget_id ? payload.budget_id : delegatedSpend.credential.budget_id,
|
|
494
|
+
pack_id: delegatedSpend.credential.pack_id || (payload && payload.pack_id) || null,
|
|
495
|
+
entitlement_id: delegatedSpend.credential.entitlement_id || (payload && payload.entitlement_id) || null
|
|
496
|
+
};
|
|
497
|
+
}
|
|
498
|
+
if (prefersCredits && !delegatedSpend.ok && creditSpendPolicyMode() === "strict") {
|
|
499
|
+
return {
|
|
500
|
+
status: 401,
|
|
501
|
+
payload: {
|
|
502
|
+
ok: false,
|
|
503
|
+
reason: "credit_spend_credential_required",
|
|
504
|
+
detail: "Spending credits needs the account's spend credential: send it as `Authorization: Bearer xytara_spend_...`. A self-serve top-up (POST /x402/credits/topup) returns one for the paying address; or pay per call with x402 instead (omit payment_preference).",
|
|
505
|
+
credits_topup: "/x402/credits/topup"
|
|
506
|
+
}
|
|
507
|
+
};
|
|
508
|
+
}
|
|
509
|
+
const quote = createQuote(state, payload);
|
|
510
|
+
if (prefersCredits) {
|
|
511
|
+
const creditPayload = {
|
|
512
|
+
...payload,
|
|
513
|
+
account_id: quote.account_id,
|
|
514
|
+
quoted_units: Number.isFinite(Number(payload.quoted_units)) ? Number(payload.quoted_units) : undefined
|
|
515
|
+
};
|
|
516
|
+
const meteringPreview = buildUsageMeterPreview(state, creditPayload);
|
|
517
|
+
const requiredUnits = meteringPreview.usage_units;
|
|
518
|
+
if (delegatedSpend.ok && delegatedSpend.binding) {
|
|
519
|
+
const authorityDecision = evaluateAuthorityBindingAction(delegatedSpend.binding, "delegated_credit_spend", requiredUnits);
|
|
520
|
+
if (!authorityDecision.ok) {
|
|
521
|
+
return {
|
|
522
|
+
status: 403,
|
|
523
|
+
payload: {
|
|
524
|
+
ok: false,
|
|
525
|
+
reason: authorityDecision.reason,
|
|
526
|
+
binding_id: authorityDecision.binding_id || null,
|
|
527
|
+
required_scope: authorityDecision.required_scope || null,
|
|
528
|
+
authority_scope: authorityDecision.authority_scope || [],
|
|
529
|
+
required_consequence_family: authorityDecision.required_consequence_family || null,
|
|
530
|
+
allowed_consequence_families: authorityDecision.allowed_consequence_families || [],
|
|
531
|
+
max_units: authorityDecision.max_units || null,
|
|
532
|
+
attempted_units: authorityDecision.attempted_units || requiredUnits
|
|
533
|
+
}
|
|
534
|
+
};
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
const creditDecision = previewCreditSpend(state, creditPayload);
|
|
539
|
+
if (!creditDecision.ok || !creditDecision.credit_spend_preview || creditDecision.credit_spend_preview.executable_now !== true) {
|
|
540
|
+
return buildCreditRejectionResponse(creditDecision, quote);
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
const preCommitAuthorizationContext = delegatedSpend.ok
|
|
544
|
+
? delegatedSpend.authorization_context
|
|
545
|
+
: {
|
|
546
|
+
ok: true,
|
|
547
|
+
verification_mode: "account_credits",
|
|
548
|
+
wallet_id: quote.account_id,
|
|
549
|
+
age_seconds: 0,
|
|
550
|
+
payment_payload: {
|
|
551
|
+
credit_spend_status: "precommit"
|
|
552
|
+
}
|
|
553
|
+
};
|
|
554
|
+
const externalExecutionOptions = await buildExternalExecutionOptions(state, payload, quote, preCommitAuthorizationContext);
|
|
555
|
+
if (externalExecutionOptions.error) {
|
|
556
|
+
return {
|
|
557
|
+
status: externalExecutionOptions.error.status || 502,
|
|
558
|
+
payload: {
|
|
559
|
+
ok: false,
|
|
560
|
+
reason: externalExecutionOptions.error.reason || "external_execution_failed",
|
|
561
|
+
details: externalExecutionOptions.error.details || null,
|
|
562
|
+
credit_spend_status: "not_committed",
|
|
563
|
+
credit_spend_preview: creditDecision.credit_spend_preview,
|
|
564
|
+
quote
|
|
565
|
+
}
|
|
566
|
+
};
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
const creditSpend = consumeAccountCredits(state, buildCreditCommitPayload(payload, quote, requiredUnits));
|
|
570
|
+
if (creditSpend.ok) {
|
|
571
|
+
const paymentVerification = {
|
|
572
|
+
...(delegatedSpend.ok
|
|
573
|
+
? delegatedSpend.authorization_context
|
|
574
|
+
: {
|
|
575
|
+
ok: true,
|
|
576
|
+
verification_mode: "account_credits",
|
|
577
|
+
wallet_id: quote.account_id,
|
|
578
|
+
age_seconds: 0,
|
|
579
|
+
payment_payload: {}
|
|
580
|
+
}),
|
|
581
|
+
payment_payload: {
|
|
582
|
+
...((delegatedSpend.ok && delegatedSpend.authorization_context && delegatedSpend.authorization_context.payment_payload) || {}),
|
|
583
|
+
credit_spend_id: creditSpend.credit_spend.credit_spend_id,
|
|
584
|
+
credit_units: requiredUnits,
|
|
585
|
+
credit_unit_price_atomic: creditUnitPriceAtomic()
|
|
586
|
+
}
|
|
587
|
+
};
|
|
588
|
+
attributeProviderSettlement(state, externalExecutionOptions, payload, quote, {
|
|
589
|
+
transaction_id: creditSpend.credit_spend.credit_spend_id
|
|
590
|
+
}, paymentVerification);
|
|
591
|
+
const creditResult = createCommandResult(state, {
|
|
592
|
+
...payload,
|
|
593
|
+
payment_preference: "credits_first"
|
|
594
|
+
}, quote, {
|
|
595
|
+
paymentVerification,
|
|
596
|
+
creditSpend: creditSpend.credit_spend,
|
|
597
|
+
skipPaymentLedger: true,
|
|
598
|
+
externalExecution: externalExecutionOptions.externalExecution || null
|
|
599
|
+
});
|
|
600
|
+
attachDeliveryReceipt(state, creditResult, payload, quote, paymentVerification, externalExecutionOptions);
|
|
601
|
+
return {
|
|
602
|
+
status: 200,
|
|
603
|
+
payload: creditResult
|
|
604
|
+
};
|
|
605
|
+
}
|
|
606
|
+
if (creditSpend.reason !== "insufficient_credits") {
|
|
607
|
+
return {
|
|
608
|
+
status: 403,
|
|
609
|
+
payload: {
|
|
610
|
+
ok: false,
|
|
611
|
+
reason: creditSpend.reason,
|
|
612
|
+
account_id: quote.account_id,
|
|
613
|
+
provider_execution_completed_but_credit_commit_failed: Boolean(externalExecutionOptions.externalExecution),
|
|
614
|
+
policy_guard: {
|
|
615
|
+
agent_id: creditSpend.agent_id || null,
|
|
616
|
+
budget_id: creditSpend.budget_id || null,
|
|
617
|
+
task_domain: creditSpend.task_domain || null,
|
|
618
|
+
policy_ref: creditSpend.policy_ref || null,
|
|
619
|
+
max_reserve_units: creditSpend.max_reserve_units || null,
|
|
620
|
+
allowed_job_domains: creditSpend.allowed_job_domains || null,
|
|
621
|
+
budget_status: creditSpend.budget_status || null,
|
|
622
|
+
agent_status: creditSpend.agent_status || null
|
|
623
|
+
},
|
|
624
|
+
quote
|
|
625
|
+
}
|
|
626
|
+
};
|
|
627
|
+
}
|
|
628
|
+
return {
|
|
629
|
+
status: 402,
|
|
630
|
+
payload: {
|
|
631
|
+
ok: false,
|
|
632
|
+
reason: "insufficient_credits",
|
|
633
|
+
provider_execution_completed_but_credit_commit_failed: Boolean(externalExecutionOptions.externalExecution),
|
|
634
|
+
insufficient_credits: {
|
|
635
|
+
account_id: quote.account_id,
|
|
636
|
+
required_units: creditSpend.required_units,
|
|
637
|
+
available_units: creditSpend.available_units,
|
|
638
|
+
recommended_next_surfaces: {
|
|
639
|
+
credits_topup: "/x402/credits/topup",
|
|
640
|
+
direct_payment: "/v1/payment-quotes"
|
|
641
|
+
}
|
|
642
|
+
},
|
|
643
|
+
quote
|
|
644
|
+
}
|
|
645
|
+
};
|
|
646
|
+
}
|
|
647
|
+
warmProvidersForCall(state, payload, quote);
|
|
648
|
+
return {
|
|
649
|
+
status: 402,
|
|
650
|
+
payload: await challengePayload(quote)
|
|
651
|
+
};
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
const quote = payload.quote_id && state.quotes.get(payload.quote_id)
|
|
655
|
+
? state.quotes.get(payload.quote_id)
|
|
656
|
+
: createQuote(state, payload);
|
|
657
|
+
|
|
658
|
+
if (quote && quote.status === "expired") {
|
|
659
|
+
return {
|
|
660
|
+
status: 409,
|
|
661
|
+
payload: {
|
|
662
|
+
ok: false,
|
|
663
|
+
reason: "quote_expired",
|
|
664
|
+
quote
|
|
665
|
+
}
|
|
666
|
+
};
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
if (quote && (quote.status === "executed" || quote.status === "accepted")) {
|
|
670
|
+
return {
|
|
671
|
+
status: 409,
|
|
672
|
+
payload: {
|
|
673
|
+
ok: false,
|
|
674
|
+
reason: "quote_already_used",
|
|
675
|
+
quote
|
|
676
|
+
}
|
|
677
|
+
};
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
// A resent x402 payment we already settled (or were still confirming) gets its stored result
|
|
681
|
+
// instead of a re-execution or a confusing "nonce already used" failure.
|
|
682
|
+
const requestDigest = hashCanonical({
|
|
683
|
+
task_ref: (payload && payload.task_ref) || null,
|
|
684
|
+
tasks: (payload && payload.tasks) || null,
|
|
685
|
+
body: (payload && payload.body) || null
|
|
686
|
+
});
|
|
687
|
+
if (requiredHeaders.includes("payment-signature")) {
|
|
688
|
+
const replay = await resolveReplay(state, headers || {}, { request_digest: requestDigest });
|
|
689
|
+
if (replay) return replay;
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
// Verify-execute-settle: for the x402 facilitator path, settlement is DEFERRED
|
|
693
|
+
// until execution succeeds, so a failed call never settles/charges. Other paths
|
|
694
|
+
// (native/credits, and the external rails L402/BOLT/MPP whose funds already moved
|
|
695
|
+
// before the proof) have nothing to defer and are unaffected.
|
|
696
|
+
const paymentVerification = await verifyPaymentAcceptance(state, headers || {}, requiredHeaders, quote, {
|
|
697
|
+
payment_protocol: opts.paymentProtocol || (payload && payload.payment_protocol) || null,
|
|
698
|
+
authorization_scheme: opts.authorizationScheme || null,
|
|
699
|
+
resource_url: opts.resourceUrl || null,
|
|
700
|
+
deferSettlement: true
|
|
701
|
+
});
|
|
702
|
+
if (!paymentVerification.ok) {
|
|
703
|
+
return {
|
|
704
|
+
status: paymentVerification.status || 403,
|
|
705
|
+
payload: {
|
|
706
|
+
ok: false,
|
|
707
|
+
reason: paymentVerification.reason,
|
|
708
|
+
...(paymentVerification.detail ? { detail: paymentVerification.detail } : {}),
|
|
709
|
+
quote
|
|
710
|
+
}
|
|
711
|
+
};
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
markPaymentAccepted(state, paymentVerification);
|
|
715
|
+
const settlementDeferred = Boolean(paymentVerification.deferred_settlement);
|
|
716
|
+
|
|
717
|
+
// Reserve an execution slot so the same signed x402 authorization cannot execute
|
|
718
|
+
// twice concurrently (the deferred settle no longer fronts the on-chain nonce as an
|
|
719
|
+
// implicit pre-execution guard). No-op for non-deferred lanes; fails open if the
|
|
720
|
+
// store is unavailable. The slot is released in the finally below, whatever happens.
|
|
721
|
+
const executionClaim = claimDeferredExecution(state, paymentVerification);
|
|
722
|
+
if (!executionClaim.ok) {
|
|
723
|
+
return {
|
|
724
|
+
status: executionClaim.status || 409,
|
|
725
|
+
payload: {
|
|
726
|
+
ok: false,
|
|
727
|
+
reason: executionClaim.reason || "payment_authorization_in_progress",
|
|
728
|
+
...(executionClaim.detail ? { detail: executionClaim.detail } : {}),
|
|
729
|
+
payment_settled: false,
|
|
730
|
+
quote
|
|
731
|
+
}
|
|
732
|
+
};
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
try {
|
|
736
|
+
const externalExecutionOptions = await buildExternalExecutionOptions(state, payload, quote, paymentVerification);
|
|
737
|
+
if (externalExecutionOptions.error) {
|
|
738
|
+
// Execution failed. Report the money state by what actually happened, not by lane:
|
|
739
|
+
// - deferred x402: we never settled, so the caller is NOT charged.
|
|
740
|
+
// - a concrete receipt is in hand (an x402 settlement already captured): the caller
|
|
741
|
+
// WAS charged, so surface the receipt and a refund path.
|
|
742
|
+
// - everything else (native/local_signed, presence, and the access-token rails
|
|
743
|
+
// whose proof we only validated): no receipt was produced on this request, so we
|
|
744
|
+
// do not claim a charge — a bare retry hint, never a false refund pointer.
|
|
745
|
+
const hasReceipt = Boolean(paymentVerification.settlement || paymentVerification.payment_response_base64);
|
|
746
|
+
const failureStatus = externalExecutionOptions.error.status || 502;
|
|
747
|
+
// A provider outage/timeout is not the buyer's input: say so, and say when to retry.
|
|
748
|
+
const providerUnavailable = failureStatus === 502 || failureStatus === 503 || failureStatus === 504;
|
|
749
|
+
const retryHint = providerUnavailable ? "the provider did not respond in time — retry in a few seconds" : "retry with corrected input";
|
|
750
|
+
return {
|
|
751
|
+
status: failureStatus,
|
|
752
|
+
...(providerUnavailable ? { headers: { "Retry-After": "5" } } : {}),
|
|
753
|
+
payload: {
|
|
754
|
+
ok: false,
|
|
755
|
+
reason: externalExecutionOptions.error.reason || "external_execution_failed",
|
|
756
|
+
details: externalExecutionOptions.error.details || null,
|
|
757
|
+
payment_settled: hasReceipt,
|
|
758
|
+
...(paymentVerification.settlement ? { settlement: paymentVerification.settlement } : {}),
|
|
759
|
+
remediation: settlementDeferred
|
|
760
|
+
? `execution failed before settlement; you were not charged — ${retryHint}`
|
|
761
|
+
: hasReceipt
|
|
762
|
+
? `payment was accepted but execution failed; ${retryHint}, or request a refund with your payment proof`
|
|
763
|
+
: `execution failed; ${retryHint}`,
|
|
764
|
+
quote
|
|
765
|
+
},
|
|
766
|
+
...(hasReceipt && paymentVerification.payment_response_base64
|
|
767
|
+
? { paymentResponse: paymentVerification.payment_response_base64 }
|
|
768
|
+
: {})
|
|
769
|
+
};
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
// Execution succeeded — settle the deferred payment now (no-op for non-deferred).
|
|
773
|
+
const finalizePaid = (settleResult) => {
|
|
774
|
+
// settleDeferredPayment is a no-op for non-deferred paths (returns {ok:true} with no
|
|
775
|
+
// settlement) — in that case keep the verification untouched; only when a deferred
|
|
776
|
+
// x402 settlement actually ran do we fold its receipt in and drop the deferred marker.
|
|
777
|
+
const settlementOccurred = Boolean(settleResult.settlement || settleResult.payment_response_base64);
|
|
778
|
+
const settledVerification = settlementOccurred
|
|
779
|
+
? { ...paymentVerification, settlement: settleResult.settlement, payment_response_base64: settleResult.payment_response_base64, deferred_settlement: undefined }
|
|
780
|
+
: paymentVerification;
|
|
781
|
+
|
|
782
|
+
attributeProviderSettlement(state, externalExecutionOptions, payload, quote, {
|
|
783
|
+
settlement_ref: (settledVerification.settlement && (settledVerification.settlement.transaction || settledVerification.settlement.txid)) || null
|
|
784
|
+
}, settledVerification);
|
|
785
|
+
const paidResult = createCommandResult(state, payload, quote, {
|
|
786
|
+
paymentVerification: settledVerification,
|
|
787
|
+
externalExecution: externalExecutionOptions.externalExecution || null
|
|
788
|
+
});
|
|
789
|
+
attachDeliveryReceipt(state, paidResult, payload, quote, settledVerification, externalExecutionOptions);
|
|
790
|
+
return {
|
|
791
|
+
status: 200,
|
|
792
|
+
payload: paidResult,
|
|
793
|
+
...(settledVerification.payment_response_base64
|
|
794
|
+
? { paymentResponse: settledVerification.payment_response_base64 }
|
|
795
|
+
: {})
|
|
796
|
+
};
|
|
797
|
+
};
|
|
798
|
+
|
|
799
|
+
let settleResult = await settleDeferredPayment(paymentVerification);
|
|
800
|
+
const authorizationRef = settlementDeferred ? authorizationRefFromVerification(paymentVerification) : null;
|
|
801
|
+
if (!settleResult.ok && authorizationRef && settleResult.reason === "facilitator_unreachable") {
|
|
802
|
+
// The facilitator timed out / dropped: it may still have broadcast the transfer. Ask the chain.
|
|
803
|
+
const used = await confirmAuthorizationUsed(authorizationRef);
|
|
804
|
+
if (used === true) {
|
|
805
|
+
const settlement = { success: true, transaction: null, network: authorizationRef.network, payer: authorizationRef.authorizer, recovered_from: "authorization_state" };
|
|
806
|
+
settleResult = { ok: true, settlement, payment_response_base64: encodePaymentResponse(settlement) };
|
|
807
|
+
} else {
|
|
808
|
+
// Not confirmed (yet). Keep the finished result under this payment so resending the SAME
|
|
809
|
+
// signature delivers it once the transfer shows up on-chain — never ask for a new payment.
|
|
810
|
+
rememberReplay(state, authorizationRef, {
|
|
811
|
+
status: "unconfirmed",
|
|
812
|
+
request_digest: requestDigest,
|
|
813
|
+
finalize: async (settlement) => finalizePaid({ ok: true, settlement, payment_response_base64: encodePaymentResponse(settlement) })
|
|
814
|
+
});
|
|
815
|
+
return {
|
|
816
|
+
status: 502,
|
|
817
|
+
payload: {
|
|
818
|
+
ok: false,
|
|
819
|
+
reason: used === false ? "settlement_not_confirmed" : "settlement_outcome_unknown",
|
|
820
|
+
detail: "The payment processor did not answer in time. Your work is done and held for 15 minutes: if this payment shows up on-chain, resend the SAME payment signature to receive the result; if it never settles you are not charged. Do not sign a new payment for this request.",
|
|
821
|
+
payment_settled: used === false ? false : null,
|
|
822
|
+
money_moved: used !== false,
|
|
823
|
+
quote
|
|
824
|
+
}
|
|
825
|
+
};
|
|
826
|
+
}
|
|
827
|
+
}
|
|
828
|
+
if (!settleResult.ok) {
|
|
829
|
+
return {
|
|
830
|
+
status: settleResult.status || 402,
|
|
831
|
+
payload: {
|
|
832
|
+
ok: false,
|
|
833
|
+
reason: settleResult.reason || "settlement_failed",
|
|
834
|
+
...(settleResult.detail ? { detail: settleResult.detail } : {}),
|
|
835
|
+
payment_settled: false,
|
|
836
|
+
remediation: "the work executed but the payment was declined at settlement; you were not charged — retry with a valid payment",
|
|
837
|
+
quote
|
|
838
|
+
}
|
|
839
|
+
};
|
|
840
|
+
}
|
|
841
|
+
const paidResponse = finalizePaid(settleResult);
|
|
842
|
+
if (authorizationRef) rememberReplay(state, authorizationRef, { status: "settled", request_digest: requestDigest, response: paidResponse });
|
|
843
|
+
return paidResponse;
|
|
844
|
+
} finally {
|
|
845
|
+
releaseDeferredExecution(state, executionClaim.key);
|
|
846
|
+
}
|
|
847
|
+
}
|
|
848
|
+
|
|
849
|
+
async function executeAuthorizedCommandRequest(state, payload, authorizationContext) {
|
|
850
|
+
const validation = validateIntegrationSelection(payload);
|
|
851
|
+
if (!validation.ok) {
|
|
852
|
+
return {
|
|
853
|
+
status: 400,
|
|
854
|
+
payload: {
|
|
855
|
+
ok: false,
|
|
856
|
+
reason: validation.reason,
|
|
857
|
+
errors: validation.errors,
|
|
858
|
+
selection: validation.selection
|
|
859
|
+
}
|
|
860
|
+
};
|
|
861
|
+
}
|
|
862
|
+
|
|
863
|
+
hydrateQuoteStates(state);
|
|
864
|
+
|
|
865
|
+
const quote = createQuote(state, payload);
|
|
866
|
+
const paymentVerification = {
|
|
867
|
+
ok: true,
|
|
868
|
+
verification_mode: authorizationContext && authorizationContext.verification_mode
|
|
869
|
+
? authorizationContext.verification_mode
|
|
870
|
+
: "hosted_checkout",
|
|
871
|
+
wallet_id: authorizationContext && authorizationContext.wallet_id
|
|
872
|
+
? authorizationContext.wallet_id
|
|
873
|
+
: "hosted_checkout",
|
|
874
|
+
age_seconds: 0,
|
|
875
|
+
payment_payload: authorizationContext && authorizationContext.payment_payload
|
|
876
|
+
? authorizationContext.payment_payload
|
|
877
|
+
: null
|
|
878
|
+
};
|
|
879
|
+
const externalExecutionOptions = await buildExternalExecutionOptions(state, payload, quote, paymentVerification);
|
|
880
|
+
if (externalExecutionOptions.error) {
|
|
881
|
+
return {
|
|
882
|
+
status: externalExecutionOptions.error.status || 502,
|
|
883
|
+
payload: {
|
|
884
|
+
ok: false,
|
|
885
|
+
reason: externalExecutionOptions.error.reason || "external_execution_failed",
|
|
886
|
+
details: externalExecutionOptions.error.details || null
|
|
887
|
+
}
|
|
888
|
+
};
|
|
889
|
+
}
|
|
890
|
+
|
|
891
|
+
attributeProviderSettlement(state, externalExecutionOptions, payload, quote, undefined, paymentVerification);
|
|
892
|
+
const authorizedResult = createCommandResult(state, payload, quote, {
|
|
893
|
+
paymentVerification,
|
|
894
|
+
externalExecution: externalExecutionOptions.externalExecution || null
|
|
895
|
+
});
|
|
896
|
+
attachDeliveryReceipt(state, authorizedResult, payload, quote, paymentVerification, externalExecutionOptions);
|
|
897
|
+
return {
|
|
898
|
+
status: 200,
|
|
899
|
+
payload: authorizedResult
|
|
900
|
+
};
|
|
901
|
+
}
|
|
902
|
+
|
|
903
|
+
module.exports = {
|
|
904
|
+
executeAuthorizedCommandRequest,
|
|
905
|
+
executeCommandRequest,
|
|
906
|
+
listExecutableTaskRefs,
|
|
907
|
+
assessPaidExecutability,
|
|
908
|
+
buildExternalExecutionOptions,
|
|
909
|
+
reconcileProviderSettlementAmount,
|
|
910
|
+
sanitizeAuthorizationContextForProvider
|
|
911
|
+
};
|