@hyperscale0/sdk 1.0.338 → 1.0.339

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/client.js CHANGED
@@ -6,7 +6,8 @@ const routes = {
6
6
  "pathParams": [
7
7
  "accountId"
8
8
  ],
9
- "idempotency": "not_required"
9
+ "idempotency": "not_required",
10
+ "takesProductId": true
10
11
  },
11
12
  "account.close": {
12
13
  "method": "POST",
@@ -20,7 +21,8 @@ const routes = {
20
21
  "method": "POST",
21
22
  "path": "/v1/accounts",
22
23
  "pathParams": [],
23
- "idempotency": "required"
24
+ "idempotency": "required",
25
+ "takesProductId": true
24
26
  },
25
27
  "account.freeze": {
26
28
  "method": "POST",
@@ -36,7 +38,9 @@ const routes = {
36
38
  "pathParams": [
37
39
  "accountId"
38
40
  ],
39
- "idempotency": "not_required"
41
+ "idempotency": "not_required",
42
+ "takesProductId": true,
43
+ "collection": "items"
40
44
  },
41
45
  "account.hold.retrieve": {
42
46
  "method": "GET",
@@ -45,13 +49,16 @@ const routes = {
45
49
  "accountId",
46
50
  "transferId"
47
51
  ],
48
- "idempotency": "not_required"
52
+ "idempotency": "not_required",
53
+ "takesProductId": true
49
54
  },
50
55
  "account.list": {
51
56
  "method": "GET",
52
57
  "path": "/v1/accounts",
53
58
  "pathParams": [],
54
- "idempotency": "not_required"
59
+ "idempotency": "not_required",
60
+ "takesProductId": true,
61
+ "collection": "items"
55
62
  },
56
63
  "account.retrieve": {
57
64
  "method": "GET",
@@ -59,7 +66,8 @@ const routes = {
59
66
  "pathParams": [
60
67
  "accountId"
61
68
  ],
62
- "idempotency": "not_required"
69
+ "idempotency": "not_required",
70
+ "takesProductId": true
63
71
  },
64
72
  "account.statement.export": {
65
73
  "method": "GET",
@@ -68,7 +76,8 @@ const routes = {
68
76
  "accountId",
69
77
  "period"
70
78
  ],
71
- "idempotency": "not_required"
79
+ "idempotency": "not_required",
80
+ "takesProductId": true
72
81
  },
73
82
  "account.statement.retrieve": {
74
83
  "method": "GET",
@@ -77,7 +86,9 @@ const routes = {
77
86
  "accountId",
78
87
  "period"
79
88
  ],
80
- "idempotency": "not_required"
89
+ "idempotency": "not_required",
90
+ "takesProductId": true,
91
+ "collection": "lines"
81
92
  },
82
93
  "account.unfreeze": {
83
94
  "method": "POST",
@@ -91,7 +102,9 @@ const routes = {
91
102
  "method": "GET",
92
103
  "path": "/v1/activity",
93
104
  "pathParams": [],
94
- "idempotency": "not_required"
105
+ "idempotency": "not_required",
106
+ "takesProductId": true,
107
+ "collection": "items"
95
108
  },
96
109
  "activity.retrieve": {
97
110
  "method": "GET",
@@ -99,13 +112,16 @@ const routes = {
99
112
  "pathParams": [
100
113
  "activityId"
101
114
  ],
102
- "idempotency": "not_required"
115
+ "idempotency": "not_required",
116
+ "takesProductId": true
103
117
  },
104
118
  "audit_event.list": {
105
119
  "method": "GET",
106
120
  "path": "/v1/audit-events",
107
121
  "pathParams": [],
108
- "idempotency": "not_required"
122
+ "idempotency": "not_required",
123
+ "takesProductId": true,
124
+ "collection": "items"
109
125
  },
110
126
  "audit_event.retrieve": {
111
127
  "method": "GET",
@@ -113,13 +129,16 @@ const routes = {
113
129
  "pathParams": [
114
130
  "auditEventId"
115
131
  ],
116
- "idempotency": "not_required"
132
+ "idempotency": "not_required",
133
+ "takesProductId": true
117
134
  },
118
135
  "consent.list": {
119
136
  "method": "GET",
120
137
  "path": "/v1/consents",
121
138
  "pathParams": [],
122
- "idempotency": "not_required"
139
+ "idempotency": "not_required",
140
+ "takesProductId": true,
141
+ "collection": "items"
123
142
  },
124
143
  "consent.retrieve": {
125
144
  "method": "GET",
@@ -127,7 +146,8 @@ const routes = {
127
146
  "pathParams": [
128
147
  "consentId"
129
148
  ],
130
- "idempotency": "not_required"
149
+ "idempotency": "not_required",
150
+ "takesProductId": true
131
151
  },
132
152
  "consent.revoke": {
133
153
  "method": "POST",
@@ -135,7 +155,8 @@ const routes = {
135
155
  "pathParams": [
136
156
  "consentId"
137
157
  ],
138
- "idempotency": "required"
158
+ "idempotency": "required",
159
+ "takesProductId": true
139
160
  },
140
161
  "customer_access.close": {
141
162
  "method": "POST",
@@ -149,7 +170,9 @@ const routes = {
149
170
  "method": "GET",
150
171
  "path": "/v1/customer-access",
151
172
  "pathParams": [],
152
- "idempotency": "not_required"
173
+ "idempotency": "not_required",
174
+ "takesProductId": true,
175
+ "collection": "items"
153
176
  },
154
177
  "customer_access.reactivate": {
155
178
  "method": "POST",
@@ -165,7 +188,8 @@ const routes = {
165
188
  "pathParams": [
166
189
  "customerAccessId"
167
190
  ],
168
- "idempotency": "not_required"
191
+ "idempotency": "not_required",
192
+ "takesProductId": true
169
193
  },
170
194
  "customer_access.suspend": {
171
195
  "method": "POST",
@@ -179,19 +203,23 @@ const routes = {
179
203
  "method": "POST",
180
204
  "path": "/v1/customers",
181
205
  "pathParams": [],
182
- "idempotency": "required"
206
+ "idempotency": "required",
207
+ "takesProductId": true
183
208
  },
184
209
  "customer.list": {
185
210
  "method": "GET",
186
211
  "path": "/v1/customers",
187
212
  "pathParams": [],
188
- "idempotency": "not_required"
213
+ "idempotency": "not_required",
214
+ "takesProductId": true,
215
+ "collection": "items"
189
216
  },
190
217
  "customer.login": {
191
218
  "method": "POST",
192
219
  "path": "/v1/customer-identity/login",
193
220
  "pathParams": [],
194
- "idempotency": "required"
221
+ "idempotency": "required",
222
+ "takesProductId": true
195
223
  },
196
224
  "customer.logout": {
197
225
  "method": "POST",
@@ -213,7 +241,8 @@ const routes = {
213
241
  "pathParams": [
214
242
  "customerId"
215
243
  ],
216
- "idempotency": "not_required"
244
+ "idempotency": "not_required",
245
+ "takesProductId": true
217
246
  },
218
247
  "customer.session.revoke": {
219
248
  "method": "POST",
@@ -227,27 +256,16 @@ const routes = {
227
256
  "method": "POST",
228
257
  "path": "/v1/customer-identity/signup",
229
258
  "pathParams": [],
230
- "idempotency": "required"
231
- },
232
- "developer.log.list": {
233
- "method": "GET",
234
- "path": "/v1/developer-logs",
235
- "pathParams": [],
236
- "idempotency": "not_required"
237
- },
238
- "developer.log.retrieve": {
239
- "method": "GET",
240
- "path": "/v1/developer-logs/{operationId}",
241
- "pathParams": [
242
- "operationId"
243
- ],
244
- "idempotency": "not_required"
259
+ "idempotency": "required",
260
+ "takesProductId": true
245
261
  },
246
262
  "event.list": {
247
263
  "method": "GET",
248
264
  "path": "/v1/events",
249
265
  "pathParams": [],
250
- "idempotency": "not_required"
266
+ "idempotency": "not_required",
267
+ "takesProductId": true,
268
+ "collection": "items"
251
269
  },
252
270
  "event.retrieve": {
253
271
  "method": "GET",
@@ -255,19 +273,23 @@ const routes = {
255
273
  "pathParams": [
256
274
  "eventId"
257
275
  ],
258
- "idempotency": "not_required"
276
+ "idempotency": "not_required",
277
+ "takesProductId": true
259
278
  },
260
279
  "extension.list": {
261
280
  "method": "GET",
262
281
  "path": "/v1/extensions",
263
282
  "pathParams": [],
264
- "idempotency": "not_required"
283
+ "idempotency": "not_required",
284
+ "collection": "items"
265
285
  },
266
286
  "internal_transfer.list": {
267
287
  "method": "GET",
268
288
  "path": "/v1/internal-transfers",
269
289
  "pathParams": [],
270
- "idempotency": "not_required"
290
+ "idempotency": "not_required",
291
+ "takesProductId": true,
292
+ "collection": "items"
271
293
  },
272
294
  "internal_transfer.retrieve": {
273
295
  "method": "GET",
@@ -275,13 +297,16 @@ const routes = {
275
297
  "pathParams": [
276
298
  "transferId"
277
299
  ],
278
- "idempotency": "not_required"
300
+ "idempotency": "not_required",
301
+ "takesProductId": true
279
302
  },
280
303
  "operation.list": {
281
304
  "method": "GET",
282
305
  "path": "/v1/operations",
283
306
  "pathParams": [],
284
- "idempotency": "not_required"
307
+ "idempotency": "not_required",
308
+ "takesProductId": true,
309
+ "collection": "items"
285
310
  },
286
311
  "operation.retrieve": {
287
312
  "method": "GET",
@@ -289,7 +314,8 @@ const routes = {
289
314
  "pathParams": [
290
315
  "operationId"
291
316
  ],
292
- "idempotency": "not_required"
317
+ "idempotency": "not_required",
318
+ "takesProductId": true
293
319
  },
294
320
  "payment_reminder.delivery.retrieve": {
295
321
  "method": "GET",
@@ -298,7 +324,8 @@ const routes = {
298
324
  "productId",
299
325
  "operationId"
300
326
  ],
301
- "idempotency": "not_required"
327
+ "idempotency": "not_required",
328
+ "takesProductId": true
302
329
  },
303
330
  "product.actions.list": {
304
331
  "method": "GET",
@@ -306,7 +333,8 @@ const routes = {
306
333
  "pathParams": [
307
334
  "productId"
308
335
  ],
309
- "idempotency": "not_required"
336
+ "idempotency": "not_required",
337
+ "takesProductId": true
310
338
  },
311
339
  "product.activation.retrieve": {
312
340
  "method": "GET",
@@ -314,7 +342,8 @@ const routes = {
314
342
  "pathParams": [
315
343
  "productId"
316
344
  ],
317
- "idempotency": "not_required"
345
+ "idempotency": "not_required",
346
+ "takesProductId": true
318
347
  },
319
348
  "product.blocks.summary": {
320
349
  "method": "GET",
@@ -322,7 +351,18 @@ const routes = {
322
351
  "pathParams": [
323
352
  "productId"
324
353
  ],
325
- "idempotency": "not_required"
354
+ "idempotency": "not_required",
355
+ "takesProductId": true,
356
+ "collection": "items"
357
+ },
358
+ "product.books.retrieve": {
359
+ "method": "GET",
360
+ "path": "/v1/products/{productId}/books",
361
+ "pathParams": [
362
+ "productId"
363
+ ],
364
+ "idempotency": "not_required",
365
+ "takesProductId": true
326
366
  },
327
367
  "product.connections.retrieve": {
328
368
  "method": "GET",
@@ -330,7 +370,8 @@ const routes = {
330
370
  "pathParams": [
331
371
  "productId"
332
372
  ],
333
- "idempotency": "not_required"
373
+ "idempotency": "not_required",
374
+ "takesProductId": true
334
375
  },
335
376
  "product.earn.rate.list": {
336
377
  "method": "GET",
@@ -338,7 +379,9 @@ const routes = {
338
379
  "pathParams": [
339
380
  "productId"
340
381
  ],
341
- "idempotency": "not_required"
382
+ "idempotency": "not_required",
383
+ "takesProductId": true,
384
+ "collection": "items"
342
385
  },
343
386
  "product.earnings.settle": {
344
387
  "method": "POST",
@@ -346,7 +389,8 @@ const routes = {
346
389
  "pathParams": [
347
390
  "productId"
348
391
  ],
349
- "idempotency": "required"
392
+ "idempotency": "required",
393
+ "takesProductId": true
350
394
  },
351
395
  "product.economic_snapshot.retrieve": {
352
396
  "method": "GET",
@@ -354,7 +398,8 @@ const routes = {
354
398
  "pathParams": [
355
399
  "productId"
356
400
  ],
357
- "idempotency": "not_required"
401
+ "idempotency": "not_required",
402
+ "takesProductId": true
358
403
  },
359
404
  "product.guidance.retrieve": {
360
405
  "method": "GET",
@@ -362,7 +407,8 @@ const routes = {
362
407
  "pathParams": [
363
408
  "productId"
364
409
  ],
365
- "idempotency": "not_required"
410
+ "idempotency": "not_required",
411
+ "takesProductId": true
366
412
  },
367
413
  "product.instruments.list": {
368
414
  "method": "GET",
@@ -370,13 +416,8 @@ const routes = {
370
416
  "pathParams": [
371
417
  "productId"
372
418
  ],
373
- "idempotency": "not_required"
374
- },
375
- "product.list": {
376
- "method": "GET",
377
- "path": "/v1/products",
378
- "pathParams": [],
379
- "idempotency": "not_required"
419
+ "idempotency": "not_required",
420
+ "takesProductId": true
380
421
  },
381
422
  "product.margin.retrieve": {
382
423
  "method": "GET",
@@ -384,7 +425,8 @@ const routes = {
384
425
  "pathParams": [
385
426
  "productId"
386
427
  ],
387
- "idempotency": "not_required"
428
+ "idempotency": "not_required",
429
+ "takesProductId": true
388
430
  },
389
431
  "product.objects.actions": {
390
432
  "method": "GET",
@@ -394,7 +436,9 @@ const routes = {
394
436
  "kind",
395
437
  "objectId"
396
438
  ],
397
- "idempotency": "not_required"
439
+ "idempotency": "not_required",
440
+ "takesProductId": true,
441
+ "collection": "actions"
398
442
  },
399
443
  "product.objects.agreements.evidence.list": {
400
444
  "method": "GET",
@@ -405,7 +449,9 @@ const routes = {
405
449
  "objectId",
406
450
  "instanceId"
407
451
  ],
408
- "idempotency": "not_required"
452
+ "idempotency": "not_required",
453
+ "takesProductId": true,
454
+ "collection": "items"
409
455
  },
410
456
  "product.objects.agreements.list": {
411
457
  "method": "GET",
@@ -415,7 +461,9 @@ const routes = {
415
461
  "kind",
416
462
  "objectId"
417
463
  ],
418
- "idempotency": "not_required"
464
+ "idempotency": "not_required",
465
+ "takesProductId": true,
466
+ "collection": "items"
419
467
  },
420
468
  "product.objects.create": {
421
469
  "method": "POST",
@@ -424,7 +472,8 @@ const routes = {
424
472
  "productId",
425
473
  "kind"
426
474
  ],
427
- "idempotency": "required"
475
+ "idempotency": "required",
476
+ "takesProductId": true
428
477
  },
429
478
  "product.objects.discover": {
430
479
  "method": "GET",
@@ -432,7 +481,8 @@ const routes = {
432
481
  "pathParams": [
433
482
  "productId"
434
483
  ],
435
- "idempotency": "not_required"
484
+ "idempotency": "not_required",
485
+ "takesProductId": true
436
486
  },
437
487
  "product.objects.execute": {
438
488
  "method": "POST",
@@ -443,7 +493,8 @@ const routes = {
443
493
  "objectId",
444
494
  "action"
445
495
  ],
446
- "idempotency": "required"
496
+ "idempotency": "required",
497
+ "takesProductId": true
447
498
  },
448
499
  "product.objects.list": {
449
500
  "method": "GET",
@@ -452,7 +503,9 @@ const routes = {
452
503
  "productId",
453
504
  "kind"
454
505
  ],
455
- "idempotency": "not_required"
506
+ "idempotency": "not_required",
507
+ "takesProductId": true,
508
+ "collection": "items"
456
509
  },
457
510
  "product.objects.preview": {
458
511
  "method": "POST",
@@ -463,7 +516,8 @@ const routes = {
463
516
  "objectId",
464
517
  "action"
465
518
  ],
466
- "idempotency": "not_required"
519
+ "idempotency": "not_required",
520
+ "takesProductId": true
467
521
  },
468
522
  "product.objects.retrieve": {
469
523
  "method": "GET",
@@ -473,7 +527,8 @@ const routes = {
473
527
  "kind",
474
528
  "objectId"
475
529
  ],
476
- "idempotency": "not_required"
530
+ "idempotency": "not_required",
531
+ "takesProductId": true
477
532
  },
478
533
  "product.objects.timeline.list": {
479
534
  "method": "GET",
@@ -483,7 +538,9 @@ const routes = {
483
538
  "kind",
484
539
  "objectId"
485
540
  ],
486
- "idempotency": "not_required"
541
+ "idempotency": "not_required",
542
+ "takesProductId": true,
543
+ "collection": "items"
487
544
  },
488
545
  "product.operation_set.retrieve": {
489
546
  "method": "GET",
@@ -491,15 +548,8 @@ const routes = {
491
548
  "pathParams": [
492
549
  "productId"
493
550
  ],
494
- "idempotency": "not_required"
495
- },
496
- "product.retrieve": {
497
- "method": "GET",
498
- "path": "/v1/products/{productId}",
499
- "pathParams": [
500
- "productId"
501
- ],
502
- "idempotency": "not_required"
551
+ "idempotency": "not_required",
552
+ "takesProductId": true
503
553
  },
504
554
  "product.schedule.list": {
505
555
  "method": "GET",
@@ -507,7 +557,9 @@ const routes = {
507
557
  "pathParams": [
508
558
  "productId"
509
559
  ],
510
- "idempotency": "not_required"
560
+ "idempotency": "not_required",
561
+ "takesProductId": true,
562
+ "collection": "items"
511
563
  },
512
564
  "product.treasury.retrieve": {
513
565
  "method": "GET",
@@ -515,7 +567,8 @@ const routes = {
515
567
  "pathParams": [
516
568
  "productId"
517
569
  ],
518
- "idempotency": "not_required"
570
+ "idempotency": "not_required",
571
+ "takesProductId": true
519
572
  },
520
573
  "product.work.list": {
521
574
  "method": "GET",
@@ -523,13 +576,17 @@ const routes = {
523
576
  "pathParams": [
524
577
  "productId"
525
578
  ],
526
- "idempotency": "not_required"
579
+ "idempotency": "not_required",
580
+ "takesProductId": true,
581
+ "collection": "items"
527
582
  },
528
583
  "receipt.list": {
529
584
  "method": "GET",
530
585
  "path": "/v1/receipts",
531
586
  "pathParams": [],
532
- "idempotency": "not_required"
587
+ "idempotency": "not_required",
588
+ "takesProductId": true,
589
+ "collection": "items"
533
590
  },
534
591
  "receipt.retrieve": {
535
592
  "method": "GET",
@@ -537,7 +594,8 @@ const routes = {
537
594
  "pathParams": [
538
595
  "receiptId"
539
596
  ],
540
- "idempotency": "not_required"
597
+ "idempotency": "not_required",
598
+ "takesProductId": true
541
599
  },
542
600
  "sandbox.account.fund": {
543
601
  "method": "POST",
@@ -549,7 +607,8 @@ const routes = {
549
607
  "method": "POST",
550
608
  "path": "/v1/sandbox/clock/advance",
551
609
  "pathParams": [],
552
- "idempotency": "required"
610
+ "idempotency": "required",
611
+ "takesProductId": true
553
612
  },
554
613
  "sandbox.clock.retrieve": {
555
614
  "method": "GET",
@@ -557,7 +616,8 @@ const routes = {
557
616
  "pathParams": [
558
617
  "productId"
559
618
  ],
560
- "idempotency": "not_required"
619
+ "idempotency": "not_required",
620
+ "takesProductId": true
561
621
  },
562
622
  "sandbox.customer_access.activate": {
563
623
  "method": "POST",
@@ -573,17 +633,13 @@ const routes = {
573
633
  "pathParams": [],
574
634
  "idempotency": "required"
575
635
  },
576
- "transfer_return.create": {
577
- "method": "POST",
578
- "path": "/v1/transfer-returns",
579
- "pathParams": [],
580
- "idempotency": "required"
581
- },
582
636
  "usage_record.list": {
583
637
  "method": "GET",
584
638
  "path": "/v1/usage-records",
585
639
  "pathParams": [],
586
- "idempotency": "not_required"
640
+ "idempotency": "not_required",
641
+ "takesProductId": true,
642
+ "collection": "items"
587
643
  },
588
644
  "usage_record.retrieve": {
589
645
  "method": "GET",
@@ -591,13 +647,8 @@ const routes = {
591
647
  "pathParams": [
592
648
  "usageRecordId"
593
649
  ],
594
- "idempotency": "not_required"
595
- },
596
- "usage.statement": {
597
- "method": "GET",
598
- "path": "/v1/usage/statement",
599
- "pathParams": [],
600
- "idempotency": "not_required"
650
+ "idempotency": "not_required",
651
+ "takesProductId": true
601
652
  },
602
653
  "webhook.delivery.list": {
603
654
  "method": "GET",
@@ -605,7 +656,8 @@ const routes = {
605
656
  "pathParams": [
606
657
  "webhookEndpointId"
607
658
  ],
608
- "idempotency": "not_required"
659
+ "idempotency": "not_required",
660
+ "collection": "items"
609
661
  },
610
662
  "webhook.delivery.resend": {
611
663
  "method": "POST",
@@ -620,7 +672,8 @@ const routes = {
620
672
  "method": "POST",
621
673
  "path": "/v1/webhook-endpoints",
622
674
  "pathParams": [],
623
- "idempotency": "required"
675
+ "idempotency": "required",
676
+ "takesProductId": true
624
677
  },
625
678
  "webhook.endpoint.delete": {
626
679
  "method": "POST",
@@ -650,7 +703,9 @@ const routes = {
650
703
  "method": "GET",
651
704
  "path": "/v1/webhook-endpoints",
652
705
  "pathParams": [],
653
- "idempotency": "not_required"
706
+ "idempotency": "not_required",
707
+ "takesProductId": true,
708
+ "collection": "items"
654
709
  },
655
710
  "webhook.endpoint.recover": {
656
711
  "method": "POST",
@@ -690,16 +745,52 @@ const routes = {
690
745
  "pathParams": [
691
746
  "productId"
692
747
  ],
693
- "idempotency": "not_required"
748
+ "idempotency": "not_required",
749
+ "takesProductId": true
694
750
  }
695
751
  };
696
752
 
753
+ /** A refusal from Hyperscale, carrying the server's own code and message. */
754
+ export class HyperscaleError extends Error {
755
+ constructor({ status, code, message, details, requestId }) {
756
+ super(message);
757
+ this.name = "HyperscaleError";
758
+ this.code = code;
759
+ if (status !== undefined) this.status = status;
760
+ if (details !== undefined) this.details = details;
761
+ if (requestId) this.requestId = requestId;
762
+ if (typeof details?.nextStep === "string") this.nextStep = details.nextStep;
763
+ if (typeof details?.shortfall === "string")
764
+ this.shortfall = details.shortfall;
765
+ }
766
+ }
767
+
768
+ /** No readable answer arrived, so the request may or may not have run. */
769
+ export class HyperscaleConnectionError extends HyperscaleError {
770
+ constructor(reason, { status, idempotencyKey, cause } = {}) {
771
+ super({
772
+ status,
773
+ code: "connection_error",
774
+ message: `${reason}. The outcome is unknown: retry with the same idempotency key.`,
775
+ });
776
+ this.name = "HyperscaleConnectionError";
777
+ if (idempotencyKey) this.idempotencyKey = idempotencyKey;
778
+ if (cause) this.cause = cause;
779
+ }
780
+ }
781
+
782
+ export const isHyperscaleError = (error) => error instanceof HyperscaleError;
783
+
784
+ const retryStatuses = new Set([408, 429, 500, 502, 503, 504]);
785
+
697
786
  export function createClient({
698
787
  apiKey,
699
788
  sessionToken,
700
789
  productId,
701
790
  baseUrl = "https://hyperscale0.ai",
702
791
  environment = "sandbox",
792
+ timeout = 30_000,
793
+ maxRetries = 2,
703
794
  turnstileToken,
704
795
  }) {
705
796
  if (!apiKey === !sessionToken || !productId)
@@ -707,7 +798,7 @@ export function createClient({
707
798
  const credential = sessionToken ?? apiKey;
708
799
  const origin = new URL(baseUrl);
709
800
  if (
710
- origin.origin !== baseUrl ||
801
+ origin.origin !== baseUrl.replace(/\/+$/, "") ||
711
802
  (origin.protocol !== "https:" &&
712
803
  !["localhost", "127.0.0.1", "[::1]"].includes(origin.hostname))
713
804
  ) {
@@ -717,104 +808,253 @@ export function createClient({
717
808
  }
718
809
  if (!["sandbox", "live"].includes(environment))
719
810
  throw new Error("environment must be sandbox or live");
720
- async function request(path, input, idempotencyKey, method, extraHeaders) {
721
- const httpMethod = method ?? (input === undefined ? "GET" : "POST");
722
- const response = await fetch(new URL(path, origin), {
723
- method: httpMethod,
724
- headers: {
725
- authorization: `Bearer ${credential}`,
726
- "x-hyperscale-environment": environment,
727
- ...(input === undefined ? {} : { "content-type": "application/json" }),
728
- ...(idempotencyKey ? { "idempotency-key": idempotencyKey } : {}),
729
- ...extraHeaders,
730
- },
731
- ...(input === undefined ? {} : { body: JSON.stringify(input) }),
732
- redirect: "error",
733
- signal: AbortSignal.timeout(30_000),
734
- });
735
- const result = await response.json();
736
- if (!response.ok) {
737
- const error = new Error(
738
- `Hyperscale request failed with HTTP ${response.status}`,
811
+
812
+ /** One HTTP exchange: `{ result }`, or `{ error, retryAfter }`. */
813
+ async function attempt(method, path, body, idempotencyKey, headers) {
814
+ let response;
815
+ let text;
816
+ try {
817
+ response = await fetch(new URL(path, origin), {
818
+ method,
819
+ headers: {
820
+ accept: "application/json",
821
+ authorization: `Bearer ${credential}`,
822
+ "x-hyperscale-environment": environment,
823
+ ...(body === undefined ? {} : { "content-type": "application/json" }),
824
+ ...(idempotencyKey ? { "idempotency-key": idempotencyKey } : {}),
825
+ ...headers,
826
+ },
827
+ ...(body === undefined ? {} : { body: JSON.stringify(body) }),
828
+ redirect: "error",
829
+ signal: AbortSignal.timeout(timeout),
830
+ });
831
+ text = await response.text();
832
+ } catch (cause) {
833
+ const reason =
834
+ cause?.name === "TimeoutError"
835
+ ? `No answer from ${origin.origin} within ${timeout} ms`
836
+ : `Could not reach ${origin.origin}: ${cause?.message ?? cause}`;
837
+ return {
838
+ error: new HyperscaleConnectionError(reason, {
839
+ status: response?.status,
840
+ idempotencyKey,
841
+ cause,
842
+ }),
843
+ };
844
+ }
845
+ let result;
846
+ try {
847
+ result = text === "" && response.ok ? undefined : JSON.parse(text);
848
+ } catch {
849
+ const reason = `HTTP ${response.status} from ${origin.origin} was not a Hyperscale answer`;
850
+ return {
851
+ error: new HyperscaleConnectionError(reason, {
852
+ status: response.status,
853
+ idempotencyKey,
854
+ }),
855
+ };
856
+ }
857
+ if (response.ok) return { result };
858
+ return {
859
+ error: new HyperscaleError({
860
+ status: response.status,
861
+ code: result?.error?.code ?? "internal_error",
862
+ message: result?.error?.message ?? `HTTP ${response.status}`,
863
+ details: result?.error?.details,
864
+ requestId: result?.requestId ?? response.headers.get("x-request-id"),
865
+ }),
866
+ retryAfter: Number(response.headers.get("retry-after")) || 0,
867
+ };
868
+ }
869
+
870
+ /** Reads retry, and writes retry only under an idempotency key. */
871
+ async function request(method, path, body, idempotencyKey, headers) {
872
+ for (let retries = 0; ; retries += 1) {
873
+ const {
874
+ result,
875
+ error,
876
+ retryAfter = 0,
877
+ } = await attempt(method, path, body, idempotencyKey, headers);
878
+ if (!error) return result;
879
+ if (
880
+ retries >= maxRetries ||
881
+ (method !== "GET" && !idempotencyKey) ||
882
+ !(
883
+ error instanceof HyperscaleConnectionError ||
884
+ retryStatuses.has(error.status)
885
+ )
886
+ )
887
+ throw error;
888
+ const backoff =
889
+ Math.min(500 * 2 ** retries, 8_000) * (0.5 + Math.random() / 2);
890
+ await new Promise((resolve) =>
891
+ setTimeout(resolve, Math.max(backoff, Math.min(retryAfter, 10) * 1000)),
739
892
  );
740
- error.status = response.status;
741
- error.code = result?.error?.code;
742
- error.details = result?.error?.details;
743
- error.requestId = response.headers.get("x-request-id");
744
- throw error;
745
893
  }
746
- return result;
747
894
  }
748
- const productPath = `/v1/products/${encodeURIComponent(productId)}`;
749
- const actionPath = sessionToken
750
- ? `${productPath}/operations`
751
- : "/v1/operations";
752
- return {
753
- reminderDelivery: (operationId) =>
754
- request(
755
- `${productPath}/payment-reminders/${encodeURIComponent(operationId)}`,
756
- ),
757
- discover: () => request(`${productPath}/objects`),
758
- call: (name, input = {}, options = {}) => {
759
- const route = routes[name];
760
- if (!route) throw new Error(`Unknown operation: ${name}`);
761
- const remaining = { ...input };
762
- const path = route.path.replace(/\{([^}]+)\}/g, (_, param) => {
763
- let value = remaining[param];
764
- if (value === undefined && param === "productId") {
765
- value = productId;
766
- }
767
- if (value === undefined || value === null) {
768
- throw new Error(`${name} requires ${param} in input`);
769
- }
770
- delete remaining[param];
771
- return encodeURIComponent(String(value));
772
- });
773
- const extraHeaders = { ...(options.headers ?? {}) };
774
- const turnstile = options.turnstileToken ?? turnstileToken;
775
- if (turnstile) {
776
- extraHeaders["x-turnstile-token"] ??= turnstile;
777
- }
778
- if (route.method === "GET") {
779
- const params = new URLSearchParams();
780
- for (const [key, value] of Object.entries(remaining)) {
781
- if (value === undefined || value === null) continue;
782
- params.set(
783
- key,
784
- typeof value === "string" ? value : JSON.stringify(value),
785
- );
786
- }
787
- const query = params.toString();
788
- const targetPath = query ? `${path}?${query}` : path;
789
- return request(
790
- targetPath,
791
- undefined,
792
- options.idempotencyKey,
793
- route.method,
794
- extraHeaders,
795
- );
796
- }
797
- return request(
798
- path,
799
- remaining,
800
- options.idempotencyKey,
801
- route.method,
802
- extraHeaders,
895
+
896
+ // A Product key's own operations (key create's allowedOperationIds) are
897
+ // listed by id and name in the Product's operation set.
898
+ let operationSet;
899
+ async function productOperationName(name) {
900
+ if (sessionToken) return name;
901
+ operationSet ??= call("product.operation_set.retrieve", {}).catch(
902
+ (error) => {
903
+ operationSet = undefined;
904
+ throw error;
905
+ },
906
+ );
907
+ const { operations } = await operationSet;
908
+ const operation = operations.find(
909
+ (entry) => entry.operationName === name || entry.operationId === name,
910
+ );
911
+ if (!operation)
912
+ throw new Error(
913
+ `Unknown operation ${name}. This Product's own operations are listed by product.operation_set.retrieve.`,
803
914
  );
804
- },
805
- instruments: () => request(`${productPath}/instruments`),
806
- actions: (instrument) =>
807
- request(
808
- `${productPath}/actions${instrument ? `?instrument=${encodeURIComponent(instrument)}` : ""}`,
809
- ),
810
- act: (name, input, { idempotencyKey } = {}) => {
811
- if (!idempotencyKey)
812
- throw new Error("act requires an idempotencyKey for safe retries");
915
+ return operation.operationName;
916
+ }
917
+
918
+ async function call(name, input = {}, options = {}) {
919
+ const route = routes[name];
920
+ const headers = { ...options.headers };
921
+ const turnstile = options.turnstileToken ?? turnstileToken;
922
+ if (turnstile) headers["x-turnstile-token"] ??= turnstile;
923
+ if (!route) {
924
+ const operation = await productOperationName(name);
925
+ const path = sessionToken
926
+ ? `/v1/products/${encodeURIComponent(productId)}/operations`
927
+ : "/v1/operations";
928
+ // Reads ignore the key; writes need one.
813
929
  return request(
814
- `${actionPath}/${encodeURIComponent(name)}`,
930
+ "POST",
931
+ `${path}/${encodeURIComponent(operation)}`,
815
932
  input,
816
- idempotencyKey,
933
+ options.idempotencyKey ?? crypto.randomUUID(),
934
+ headers,
935
+ );
936
+ }
937
+ const remaining =
938
+ route.takesProductId && input.productId === undefined
939
+ ? { ...input, productId }
940
+ : { ...input };
941
+ const path = route.path.replace(/\{([^}]+)\}/g, (_, param) => {
942
+ const value = remaining[param];
943
+ if (value === undefined || value === null)
944
+ throw new Error(`${name} requires ${param} in input`);
945
+ delete remaining[param];
946
+ return encodeURIComponent(String(value));
947
+ });
948
+ const idempotencyKey =
949
+ options.idempotencyKey ??
950
+ (route.idempotency === "required" ? crypto.randomUUID() : undefined);
951
+ if (route.method !== "GET")
952
+ return request(route.method, path, remaining, idempotencyKey, headers);
953
+ const query = new URLSearchParams();
954
+ for (const [key, value] of Object.entries(remaining))
955
+ if (value !== undefined && value !== null)
956
+ query.set(
957
+ key,
958
+ typeof value === "string" ? value : JSON.stringify(value),
959
+ );
960
+ const search = query.toString();
961
+ return request(
962
+ "GET",
963
+ search ? `${path}?${search}` : path,
964
+ undefined,
965
+ idempotencyKey,
966
+ headers,
967
+ );
968
+ }
969
+
970
+ async function* paginate(name, input = {}, options) {
971
+ const collection = routes[name]?.collection;
972
+ if (!collection) throw new Error(`${name} is not a cursor list`);
973
+ const seen = new Set();
974
+ let page = await call(name, input, options);
975
+ yield* page[collection];
976
+ while (page.nextCursor) {
977
+ if (seen.has(page.nextCursor))
978
+ throw new Error(`${name} repeated cursor ${page.nextCursor}`);
979
+ seen.add(page.nextCursor);
980
+ page = await call(name, { ...input, cursor: page.nextCursor }, options);
981
+ yield* page[collection];
982
+ }
983
+ }
984
+
985
+ // What run sent under a key whose outcome is unknown, so a retry with that
986
+ // key resends it even if the first attempt moved the object on.
987
+ const sent = new Map();
988
+ async function run(kind, objectId, action, options = {}) {
989
+ const { onBehalfOf, fields, inputs, instanceId } = options;
990
+ const idempotencyKey = options.idempotencyKey ?? crypto.randomUUID();
991
+ let body = sent.get(idempotencyKey);
992
+ if (!body) {
993
+ const read = { kind, objectId, ...(onBehalfOf ? { onBehalfOf } : {}) };
994
+ let page = await call("product.objects.actions", read);
995
+ const { revision } = page;
996
+ const actions = [...page.actions];
997
+ while (page.nextCursor) {
998
+ page = await call("product.objects.actions", {
999
+ ...read,
1000
+ cursor: page.nextCursor,
1001
+ });
1002
+ if (page.revision !== revision)
1003
+ throw new HyperscaleError({
1004
+ code: "state_conflict",
1005
+ message: `${kind} ${objectId} changed while its actions were read. Run it again.`,
1006
+ });
1007
+ actions.push(...page.actions);
1008
+ }
1009
+ const matches = actions.filter(
1010
+ (entry) =>
1011
+ entry.name === action &&
1012
+ (!instanceId || entry.target.instanceId === instanceId),
817
1013
  );
818
- },
1014
+ if (matches.length !== 1)
1015
+ throw new HyperscaleError({
1016
+ code: matches.length ? "invalid_request" : "not_found",
1017
+ message: matches.length
1018
+ ? `${action} runs on several agreements of ${kind} ${objectId}. Pass instanceId: ${matches.map((entry) => entry.target.instanceId).join(", ")}.`
1019
+ : `${kind} ${objectId} has no action ${action}. Its actions: ${[...new Set(actions.map((entry) => entry.name))].join(", ") || "none"}.`,
1020
+ });
1021
+ const [selected] = matches;
1022
+ const { availability } = selected;
1023
+ if (availability.status === "unavailable")
1024
+ throw new HyperscaleError({
1025
+ code: availability.blockers[0]?.code ?? "state_conflict",
1026
+ message: availability.blockers
1027
+ .map((blocker) => blocker.reason)
1028
+ .join(" "),
1029
+ details: { blockers: availability.blockers },
1030
+ });
1031
+ body = {
1032
+ kind,
1033
+ objectId,
1034
+ action,
1035
+ expectedRevision: revision,
1036
+ productBuildId: selected.productBuildId,
1037
+ digest: selected.digest,
1038
+ target: selected.target,
1039
+ ...(fields ? { fields } : {}),
1040
+ ...(inputs ? { input: inputs } : {}),
1041
+ ...(onBehalfOf ? { onBehalfOf } : {}),
1042
+ };
1043
+ }
1044
+ sent.delete(idempotencyKey);
1045
+ try {
1046
+ return await call("product.objects.execute", body, { idempotencyKey });
1047
+ } catch (error) {
1048
+ if (error instanceof HyperscaleConnectionError)
1049
+ sent.set(idempotencyKey, body);
1050
+ throw error;
1051
+ }
1052
+ }
1053
+
1054
+ return {
1055
+ discover: () => call("product.objects.discover", {}),
1056
+ call,
1057
+ paginate,
1058
+ objects: { run },
819
1059
  };
820
1060
  }