@0xinsider/sdk 0.14.0-bootstrap.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/errors.js ADDED
@@ -0,0 +1,700 @@
1
+ /**
2
+ * Typed error hierarchy for the 0xinsider API V1 error envelope.
3
+ *
4
+ * The V1 contract returns a stable error shape on every non-2xx response
5
+ * (`web/public/api/v1/openapi.json` -> components.schemas.ApiError):
6
+ *
7
+ * {
8
+ * "object": "error",
9
+ * "error": { "code": <ApiErrorCode>, "message": string, "doc_url"?, "param"? },
10
+ * "meta": { "request_id": string, "cached": boolean, "cost": number, ... }
11
+ * }
12
+ *
13
+ * Some upstream/proxy failures return a flat body `{ code, message }` without
14
+ * the `object: "error"` wrapper; both shapes are normalized here.
15
+ *
16
+ * The canonical `code` set is the `ApiError.error.code` enum in the OpenAPI
17
+ * spec. The client maps each error to the most specific subclass -- dispatching on `reason` first (it is strictly more specific), then `code` so callers can
18
+ * `instanceof` the specific failure, and falls back to the base
19
+ * `OxinsiderApiError` for unknown codes or non-JSON bodies.
20
+ */
21
+ /**
22
+ * Canonical error codes the API can return. Source of truth:
23
+ * `components.schemas.ApiError.properties.error.properties.code.enum`.
24
+ */
25
+ export const API_ERROR_CODES = [
26
+ "bad_request",
27
+ "invalid_api_key",
28
+ "subscription_required",
29
+ "forbidden",
30
+ "not_found",
31
+ "account_locked",
32
+ "rate_limited",
33
+ "rate_limit_unavailable",
34
+ "internal_error",
35
+ // 403 (#13689): an OAuth token that lacks the scope this route needs.
36
+ "insufficient_scope",
37
+ // 408 (#16146): the handler did not answer inside the server's 30-second
38
+ // timeout. Dispatched to ServerTimeoutError below.
39
+ "request_timeout",
40
+ ];
41
+ /**
42
+ * The specific, actionable cause behind a `code`. Source of truth:
43
+ * `components.schemas.ApiError.properties.error.properties.reason.enum`.
44
+ *
45
+ * Additive (#7209): `code` keeps its published values, so this never changes what
46
+ * an existing `code` means. It tells you what to DO, where the code alone could
47
+ * not: `pick_not_released` is "not YET" (sleep until `retryAt`), not "does not
48
+ * exist"; `unknown_endpoint` is a wrong PATH (do not retry); `trader_not_tracked`
49
+ * is a wallet we do not capture (stop asking for it); `cursor_expired` means
50
+ * re-request page 1; `read_model_warming` is endpoint-local unavailability,
51
+ * not rate limiting. Retry only that route after the supplied interval; do not
52
+ * infer dependency health from the reason.
53
+ */
54
+ export const API_ERROR_REASONS = [
55
+ "cursor_expired",
56
+ "unknown_endpoint",
57
+ "pick_not_released",
58
+ "trader_not_tracked",
59
+ "read_model_warming",
60
+ "database_unavailable",
61
+ "request_accounting_unavailable",
62
+ // 409 conflict causes (code `bad_request` at HTTP 409): the same mutation is
63
+ // still running / a webhook endpoint is mid-delivery. Dispatched to
64
+ // IdempotencyInProgressError / WebhookDeliveryInProgressError below, both
65
+ // refining BadRequestError to match the wire code.
66
+ "idempotency_in_progress",
67
+ "webhook_delivery_in_progress",
68
+ // Staged webhook rotation state: prepare before activate, and retire the
69
+ // bounded overlap before starting another staged rotation.
70
+ "webhook_secret_rotation_not_prepared",
71
+ "webhook_secret_rotation_overlap_active",
72
+ // 401 `invalid_api_key` refinement (#13959): a sandbox key sent to the live
73
+ // API. Dispatched to SandboxApiKeyError below.
74
+ "sandbox_api_key",
75
+ // 401 `invalid_api_key` refinement (#14123): the key was sent as a `?token=`
76
+ // query parameter, which no route reads. Dispatched to ApiKeyInQueryError.
77
+ "api_key_in_query",
78
+ // 402 `subscription_required` refinement (#14283): the account's Pro
79
+ // subscription has lapsed. Permanent until a person reactivates; the
80
+ // message names the URL. Carried on SubscriptionRequiredError.reason.
81
+ "subscription_inactive",
82
+ // 429 `rate_limited` refinement (#16111): the account has used the requests
83
+ // Pro includes for the UTC calendar month. `retry_at` is the first of next
84
+ // month; pay as you go is turned on at https://0xinsider.com/developers.
85
+ "monthly_quota_exceeded",
86
+ // 400 `bad_request` refinements (#16146): the request never reached a
87
+ // handler because a query parameter, a path segment or the JSON body did
88
+ // not parse or did not fit the route's schema; `param` names the field
89
+ // when the server named one. Fix the request; never retry it as sent.
90
+ "invalid_query",
91
+ // 400 `bad_request` refinement (#16189): strict query validation rejected a
92
+ // name the operation does not publish before the handler ran.
93
+ "unknown_query_parameter",
94
+ "invalid_path",
95
+ "invalid_body",
96
+ // 415, 413 and 405 under `bad_request` (#16146): send the body as
97
+ // `Content-Type: application/json`; keep it under 1 MiB; use a method the
98
+ // path serves (`Allow` names them). All three arrive as BadRequestError
99
+ // with `reason` set.
100
+ "unsupported_media_type",
101
+ "payload_too_large",
102
+ "method_not_allowed",
103
+ // 429 `rate_limited` refinements (#16380): the per-address budget shared by
104
+ // every caller behind one IP (`ip_rate_limited`; the RateLimit-* headers
105
+ // describe that bucket, not the key's), and an address cooldown after
106
+ // sustained over-limit traffic (`ip_throttled`; Retry-After is minutes to
107
+ // days and an earlier retry extends it). Both arrive as RateLimitedError
108
+ // with `reason` set; sleep `retryAfterSeconds` either way.
109
+ "ip_rate_limited",
110
+ "ip_throttled",
111
+ // 410 `not_found` refinement (#16181): the export job finished but its
112
+ // retention window has passed and the file is retired. Arrives as
113
+ // NotFoundError with `status` 410 and `reason` set; submit a new export,
114
+ // polling or retrying the download cannot succeed.
115
+ "export_expired",
116
+ // 409 `bad_request` refinement (#16964): an opt-in trader freshness ceiling
117
+ // could not be met by the stored body. The response carries the requested
118
+ // ceiling and the measured clock when one exists; retrying does not refresh it.
119
+ "freshness_ceiling_unsatisfied",
120
+ ];
121
+ /**
122
+ * The SDK's deadline (`timeoutMs`) expired during the request, body read, or
123
+ * retry backoff. Caller cancellation preserves the caller's exact reason.
124
+ * Not an `OxinsiderApiError`: there is no status, body, or request id.
125
+ */
126
+ export class RequestTimeoutError extends Error {
127
+ operationId;
128
+ timeoutMs;
129
+ constructor(operationId, timeoutMs) {
130
+ super(`0xinsider API ${operationId} did not answer within ${String(timeoutMs)} ms`);
131
+ this.name = "RequestTimeoutError";
132
+ this.operationId = operationId;
133
+ this.timeoutMs = timeoutMs;
134
+ }
135
+ }
136
+ /**
137
+ * An owned JSON/text response failed while its body was being read.
138
+ * Headers may have arrived successfully; no body, URL or credential is copied.
139
+ * The native read failure is retained as cause. Client deadlines and caller
140
+ * abort reasons take precedence, and this failure does not trigger a retry.
141
+ */
142
+ export class ResponseBodyReadError extends Error {
143
+ responseStatus;
144
+ phase = "response_body";
145
+ constructor(responseStatus, cause) {
146
+ super("0xinsider API response body could not be read", { cause });
147
+ this.responseStatus = responseStatus;
148
+ this.name = "ResponseBodyReadError";
149
+ }
150
+ }
151
+ /**
152
+ * The export object was read, but its streamed bytes did not match the
153
+ * immutable manifest returned by the status route. The partial file must be
154
+ * discarded before requesting a fresh download.
155
+ */
156
+ export class ExportIntegrityError extends Error {
157
+ jobId;
158
+ expectedSha256;
159
+ actualSha256;
160
+ expectedSizeBytes;
161
+ actualSizeBytes;
162
+ cause;
163
+ constructor(jobId, expectedSha256, actualSha256, expectedSizeBytes, actualSizeBytes, cause) {
164
+ const message = actualSha256 === null
165
+ ? `Export job ${String(jobId)} could not be verified: the download ended before its SHA-256 could be checked after ${String(actualSizeBytes)} of ${String(expectedSizeBytes)} bytes. Discard the partial file and request a fresh download.`
166
+ : `Export job ${String(jobId)} failed integrity verification: expected ${String(expectedSizeBytes)} bytes with SHA-256 ${expectedSha256}, received ${String(actualSizeBytes)} bytes with SHA-256 ${actualSha256}. Discard the partial file and request a fresh download.`;
167
+ super(message);
168
+ this.name = "ExportIntegrityError";
169
+ this.jobId = jobId;
170
+ this.expectedSha256 = expectedSha256;
171
+ this.actualSha256 = actualSha256;
172
+ this.expectedSizeBytes = expectedSizeBytes;
173
+ this.actualSizeBytes = actualSizeBytes;
174
+ this.cause = cause;
175
+ }
176
+ }
177
+ /** `value` when it is one of `API_ERROR_REASONS`, otherwise `null` (#16547). */
178
+ function knownApiErrorReason(value) {
179
+ return typeof value === "string" &&
180
+ API_ERROR_REASONS.includes(value)
181
+ ? value
182
+ : null;
183
+ }
184
+ function extractFreshnessFailure(error) {
185
+ const value = error?.freshness;
186
+ return value && typeof value === "object" ? value : null;
187
+ }
188
+ /**
189
+ * Base class for every error thrown by the SDK on a non-2xx API response.
190
+ * Subclasses below specialize by documented `code`.
191
+ */
192
+ export class OxinsiderApiError extends Error {
193
+ /** HTTP status code of the failing response. */
194
+ status;
195
+ /** Documented error code, when the body carried one. */
196
+ code;
197
+ /**
198
+ * The specific, actionable cause behind `code`, or `null` when the response carried
199
+ * no reason the SDK recognizes (#7209).
200
+ *
201
+ * Declared on the BASE class, not only on the five reason subclasses, so the thing an
202
+ * SDK user actually writes type-checks:
203
+ *
204
+ * catch (e) { if (e instanceof OxinsiderApiError && e.reason === "pick_not_released") ... }
205
+ *
206
+ * With `reason` living only on the subclasses, that read was a type error, and the only
207
+ * way to branch on the cause was an `instanceof` ladder over all five -- which defeats
208
+ * the point of shipping a reason at all. The subclasses narrow this to their own literal.
209
+ *
210
+ * The raw wire value always stays on `error.reason`, so a reason NEWER than this SDK is
211
+ * never destroyed -- it is simply not typed yet, and reads as `null` here.
212
+ *
213
+ * Read from the wire for EVERY class (#16547): a `SubscriptionRequiredError` carries
214
+ * `subscription_inactive`, a `RateLimitedError` carries `monthly_quota_exceeded`,
215
+ * `ip_rate_limited` or `ip_throttled`, and a `BadRequestError` carries `invalid_body`,
216
+ * `payload_too_large` and the rest. Until then only the subclasses that pin a literal
217
+ * carried a value and every other known reason read `null`.
218
+ */
219
+ reason;
220
+ /**
221
+ * When retrying can first succeed, or `null` when the error is terminal.
222
+ *
223
+ * On the BASE class for exactly the reason `reason` is, and the backend says so in its
224
+ * own contract (`ApiErrorBody::retry_at`): the field is "generic on purpose ... so
225
+ * clients key on ONE field across the whole API rather than a per-endpoint zoo". With
226
+ * `retryAt` living only on the three retryable subclasses,
227
+ *
228
+ * catch (e) { if (e instanceof OxinsiderApiError && e.retryAt) scheduleRetry(e.retryAt) }
229
+ *
230
+ * was a type error -- the same defect the `reason` hoist above exists to fix, left in
231
+ * place one field over.
232
+ *
233
+ * Parsed from the wire on every error, so it is present whenever the API sends it and
234
+ * `null` when it does not. A terminal error (`cursor_expired`, `trader_not_tracked`)
235
+ * has no `retry_at`, and `null` is the honest answer: do not retry this.
236
+ *
237
+ * DO NOT BLOCK A WORKER THREAD ON IT. On `pick_not_released` this can be 13-14 hours
238
+ * out (a full day on a skipped day). Schedule the retry; do not sleep.
239
+ */
240
+ retryAt;
241
+ /** Parsed `error` object (`{ code, message, doc_url?, param? }`), or null. */
242
+ error;
243
+ /** Response `meta` (`request_id`, `cost`, ...), or null. */
244
+ meta;
245
+ /** Body `meta.request_id`, or the received `X-Request-ID` when absent. */
246
+ requestId;
247
+ /** Parsed `Retry-After` guidance, independent of whether this request can be retried. */
248
+ retryAfterSeconds;
249
+ /** Raw, unparsed body for debugging. */
250
+ body;
251
+ constructor(status, body, transport = {}) {
252
+ const error = extractApiErrorBody(body);
253
+ const meta = extractApiErrorMeta(body);
254
+ const message = error?.message ??
255
+ `0xinsider API request failed with status ${String(status)}`;
256
+ super(message);
257
+ this.name = "OxinsiderApiError";
258
+ this.status = status;
259
+ this.code = error?.code;
260
+ this.reason = knownApiErrorReason(error?.reason);
261
+ this.error = error;
262
+ this.meta = meta;
263
+ const headerRequestId = transport.requestId;
264
+ this.requestId = meta?.request_id ??
265
+ (headerRequestId?.trim() ? headerRequestId : undefined);
266
+ this.retryAfterSeconds = transport.retryAfterSeconds ?? null;
267
+ this.body = body;
268
+ // Parsed once, here, for EVERY error. The retryable subclasses no longer parse it
269
+ // themselves -- two parsers for one wire field is how they drift.
270
+ this.retryAt = parseRetryAt(body);
271
+ }
272
+ }
273
+ /** 400 - the request was malformed; see `param` for the offending field. */
274
+ export class BadRequestError extends OxinsiderApiError {
275
+ code = "bad_request";
276
+ constructor(status, body, transport = {}) {
277
+ super(status, body, transport);
278
+ this.name = "BadRequestError";
279
+ }
280
+ }
281
+ /**
282
+ * 409 `reason: freshness_ceiling_unsatisfied` - a trader response did not
283
+ * satisfy the caller's opt-in whole-response age ceiling. The server does not
284
+ * refresh or bypass its normal read, so the caller may use the details to decide
285
+ * whether to accept the body or try again later.
286
+ */
287
+ export class FreshnessCeilingUnsatisfiedError extends BadRequestError {
288
+ reason = "freshness_ceiling_unsatisfied";
289
+ freshness;
290
+ constructor(status, body, transport = {}) {
291
+ super(status, body, transport);
292
+ this.name = "FreshnessCeilingUnsatisfiedError";
293
+ this.freshness = extractFreshnessFailure(this.error);
294
+ }
295
+ }
296
+ /**
297
+ * 400 `reason: unknown_query_parameter` - strict validation rejected a query
298
+ * name the operation does not publish. Remove or correct the name, or leave
299
+ * `strictQuery` unset while migrating a caller.
300
+ */
301
+ export class UnknownQueryParameterError extends BadRequestError {
302
+ reason = "unknown_query_parameter";
303
+ constructor(status, body, transport = {}) {
304
+ super(status, body, transport);
305
+ this.name = "UnknownQueryParameterError";
306
+ }
307
+ }
308
+ /** 401 - the `oxi_sk_*` API key is missing, malformed, or revoked. */
309
+ export class InvalidApiKeyError extends OxinsiderApiError {
310
+ code = "invalid_api_key";
311
+ constructor(status, body, transport = {}) {
312
+ super(status, body, transport);
313
+ this.name = "InvalidApiKeyError";
314
+ }
315
+ }
316
+ /**
317
+ * 401 `reason: sandbox_api_key` - the credential is a sandbox key (`oxi_sk_test_`)
318
+ * from `POST /api/v1/agents/register` (#13959). Only the sandbox server accepts
319
+ * it: call `https://0xinsider.com/sandbox/api/v1` with it, or use a live key or
320
+ * OAuth access token. Retrying here will not help.
321
+ */
322
+ export class SandboxApiKeyError extends InvalidApiKeyError {
323
+ reason = "sandbox_api_key";
324
+ constructor(status, body, transport = {}) {
325
+ super(status, body, transport);
326
+ this.name = "SandboxApiKeyError";
327
+ }
328
+ }
329
+ /**
330
+ * 401 `reason: api_key_in_query` - the key was sent as a `?token=` query
331
+ * parameter (#14123). No route reads a key from the URL, so the key itself was
332
+ * not checked: resend it as `Authorization: Bearer`. This client always sends
333
+ * the header, so seeing this means a URL was built by hand.
334
+ */
335
+ export class ApiKeyInQueryError extends InvalidApiKeyError {
336
+ reason = "api_key_in_query";
337
+ constructor(status, body, transport = {}) {
338
+ super(status, body, transport);
339
+ this.name = "ApiKeyInQueryError";
340
+ }
341
+ }
342
+ /**
343
+ * 402 - the key is valid but the account has no active Pro subscription.
344
+ *
345
+ * Permanent until a person reactivates (`reason: subscription_inactive`,
346
+ * #14283): there is no `Retry-After`, and a retry on a schedule never clears
347
+ * it. Stop the loop and surface `reactivationUrl` to your own user. The key
348
+ * and its settings are unchanged and work again the moment Pro is active;
349
+ * the owner is emailed once per lapse.
350
+ */
351
+ export class SubscriptionRequiredError extends OxinsiderApiError {
352
+ code = "subscription_required";
353
+ /** Where the account reactivates Pro. */
354
+ reactivationUrl = "https://0xinsider.com/billing";
355
+ constructor(status, body, transport = {}) {
356
+ super(status, body, transport);
357
+ this.name = "SubscriptionRequiredError";
358
+ }
359
+ }
360
+ /** 403 - the key is authenticated but not allowed to access this resource. */
361
+ export class ForbiddenError extends OxinsiderApiError {
362
+ code = "forbidden";
363
+ constructor(status, body, transport = {}) {
364
+ super(status, body, transport);
365
+ this.name = "ForbiddenError";
366
+ }
367
+ }
368
+ /** 404 - the requested resource does not exist. */
369
+ export class NotFoundError extends OxinsiderApiError {
370
+ code = "not_found";
371
+ constructor(status, body, transport = {}) {
372
+ super(status, body, transport);
373
+ this.name = "NotFoundError";
374
+ }
375
+ }
376
+ /** 403 - the account is locked; contact support. */
377
+ export class AccountLockedError extends OxinsiderApiError {
378
+ code = "account_locked";
379
+ constructor(status, body, transport = {}) {
380
+ super(status, body, transport);
381
+ this.name = "AccountLockedError";
382
+ }
383
+ }
384
+ /**
385
+ * 429 - the per-key rate limit was exceeded. `retryAfterSeconds` is read from
386
+ * the standard `Retry-After` response header when present.
387
+ */
388
+ export class RateLimitedError extends OxinsiderApiError {
389
+ code = "rate_limited";
390
+ constructor(status, body, retryAfterSeconds, transport = {}) {
391
+ super(status, body, { ...transport, retryAfterSeconds });
392
+ this.name = "RateLimitedError";
393
+ }
394
+ }
395
+ /**
396
+ * 408 `request_timeout` (#16146) - the server did not finish the request
397
+ * inside its 30-second timeout. Distinct from `RequestTimeoutError`, which is
398
+ * this client's own deadline with no response at all.
399
+ *
400
+ * `retryAfterSeconds` is set only on a safe method (GET, HEAD): the server
401
+ * gives a mutation no hint, because it may have completed. Check its state
402
+ * before repeating it, and reuse its Idempotency-Key.
403
+ */
404
+ export class ServerTimeoutError extends OxinsiderApiError {
405
+ code = "request_timeout";
406
+ constructor(status, body, retryAfterSeconds, transport = {}) {
407
+ super(status, body, { ...transport, retryAfterSeconds });
408
+ this.name = "ServerTimeoutError";
409
+ }
410
+ }
411
+ /** 503 - the rate-limit/admission backend was briefly unavailable; retry. */
412
+ export class RateLimitUnavailableError extends OxinsiderApiError {
413
+ code = "rate_limit_unavailable";
414
+ constructor(status, body, retryAfterSeconds, transport = {}) {
415
+ super(status, body, { ...transport, retryAfterSeconds });
416
+ this.name = "RateLimitUnavailableError";
417
+ }
418
+ }
419
+ /**
420
+ * 404 `reason: pick_not_released` - no Pick of the Day is published for the
421
+ * current product day (#7209).
422
+ *
423
+ * This is NOT "the endpoint is broken" and NOT "the resource does not exist".
424
+ * Each selected pick releases about an hour before its own provider kickoff, inside
425
+ * the daily operating window, 11:00 UTC to 23:00 America/New_York (#7226, #7709,
426
+ * #16207) -- there is no fixed publish clock time. The product day rolls at midnight America/New_York, so the
427
+ * endpoint legitimately 404s from that roll until the day's release (a span that
428
+ * varies with the pick's kickoff), and for the full product day on a skipped day.
429
+ *
430
+ * DO NOT POLL. Sleep for `retryAfterSeconds` (or until `retryAt`) and request
431
+ * once. Blind polling through this window was 91.8% of all logged v1 API errors.
432
+ */
433
+ export class PickNotReleasedError extends NotFoundError {
434
+ reason = "pick_not_released";
435
+ constructor(status, body, retryAfterSeconds, transport = {}) {
436
+ super(status, body, { ...transport, retryAfterSeconds });
437
+ this.name = "PickNotReleasedError";
438
+ }
439
+ }
440
+ /**
441
+ * 503 `reason: read_model_warming` - endpoint-local read-model unavailability (#7209).
442
+ *
443
+ * This is not rate limiting. Retry only this route after the supplied interval;
444
+ * the exact cause is endpoint-specific, so do not infer dependency health from
445
+ * this reason.
446
+ */
447
+ export class ReadModelWarmingError extends RateLimitUnavailableError {
448
+ reason = "read_model_warming";
449
+ constructor(status, body, retryAfterSeconds, transport = {}) {
450
+ super(status, body, retryAfterSeconds, transport);
451
+ this.name = "ReadModelWarmingError";
452
+ }
453
+ }
454
+ /**
455
+ * 503 `reason: database_unavailable` - the API's database or its connection
456
+ * pool is temporarily unreachable (#10737). A connection-class failure, not a
457
+ * query fault, and not rate limiting: back off for `retryAfterSeconds`, then
458
+ * retry the same request.
459
+ */
460
+ export class DatabaseUnavailableError extends RateLimitUnavailableError {
461
+ reason = "database_unavailable";
462
+ constructor(status, body, retryAfterSeconds, transport = {}) {
463
+ super(status, body, retryAfterSeconds, transport);
464
+ this.name = "DatabaseUnavailableError";
465
+ }
466
+ }
467
+ /** 503 - accounting capacity was unavailable before the handler ran. */
468
+ export class RequestAccountingUnavailableError extends RateLimitUnavailableError {
469
+ reason = "request_accounting_unavailable";
470
+ constructor(status, body, retryAfterSeconds, transport = {}) {
471
+ super(status, body, retryAfterSeconds, transport);
472
+ this.name = "RequestAccountingUnavailableError";
473
+ }
474
+ }
475
+ /**
476
+ * 400 `reason: cursor_expired` - the pagination cursor was invalidated by an
477
+ * upstream data change (#7209). Recovery is mechanical: re-request the first page
478
+ * and continue. This is NOT a bad parameter -- do not stop retrying.
479
+ */
480
+ export class CursorExpiredError extends BadRequestError {
481
+ reason = "cursor_expired";
482
+ constructor(status, body, transport = {}) {
483
+ super(status, body, transport);
484
+ this.name = "CursorExpiredError";
485
+ }
486
+ }
487
+ /**
488
+ * 409 `reason: idempotency_in_progress` - the same idempotent mutation is still
489
+ * running. Retain the exact `Idempotency-Key` and body and retry shortly; do NOT
490
+ * mint a new mutation. The wire carries code `bad_request` at HTTP 409, so this
491
+ * refines `BadRequestError` -- a consumer catching `BadRequestError` still matches.
492
+ */
493
+ export class IdempotencyInProgressError extends BadRequestError {
494
+ reason = "idempotency_in_progress";
495
+ constructor(status, body, transport = {}) {
496
+ super(status, body, transport);
497
+ this.name = "IdempotencyInProgressError";
498
+ }
499
+ }
500
+ /**
501
+ * 409 `reason: webhook_delivery_in_progress` - a webhook endpoint's URL and
502
+ * signing secret are frozen while a delivery is in flight. Retry the
503
+ * configuration change after that delivery completes. The wire carries code
504
+ * `bad_request` at HTTP 409, so this refines `BadRequestError`.
505
+ */
506
+ export class WebhookDeliveryInProgressError extends BadRequestError {
507
+ reason = "webhook_delivery_in_progress";
508
+ constructor(status, body, transport = {}) {
509
+ super(status, body, transport);
510
+ this.name = "WebhookDeliveryInProgressError";
511
+ }
512
+ }
513
+ /**
514
+ * 404 `reason: unknown_endpoint` - the PATH is not a route on this API (#7209).
515
+ * Read `GET /api/v1` for the route index. Retrying will not help.
516
+ */
517
+ export class UnknownEndpointError extends NotFoundError {
518
+ reason = "unknown_endpoint";
519
+ constructor(status, body, transport = {}) {
520
+ super(status, body, transport);
521
+ this.name = "UnknownEndpointError";
522
+ }
523
+ }
524
+ /**
525
+ * 404 `reason: trader_not_tracked` - the wallet is real and the URL is right, but
526
+ * the trader is outside the HOT/WARM sync tiers so we do not capture this data
527
+ * for them (#7209). Stop asking for this wallet; do not hunt for a different URL.
528
+ */
529
+ export class TraderNotTrackedError extends NotFoundError {
530
+ reason = "trader_not_tracked";
531
+ constructor(status, body, transport = {}) {
532
+ super(status, body, transport);
533
+ this.name = "TraderNotTrackedError";
534
+ }
535
+ }
536
+ /** Parse the RFC3339 `error.retry_at` off an error envelope, if present. */
537
+ function parseRetryAt(body) {
538
+ const raw = extractApiErrorBody(body)?.retry_at;
539
+ if (typeof raw !== "string")
540
+ return null;
541
+ const parsed = new Date(raw);
542
+ return Number.isNaN(parsed.getTime()) ? null : parsed;
543
+ }
544
+ /**
545
+ * A 2xx response whose body does not match the contract this client relies
546
+ * on (#16246): a list operation answering without a list envelope, or with a
547
+ * `has_more` that is not a boolean or a `next_cursor` that is not a string.
548
+ * `code` is `invalid_response`, a client-side code that the API never sends;
549
+ * `received` is the body as parsed. Not retried: the same request would
550
+ * return the same body. Report it with `requestId` when the body carried one.
551
+ */
552
+ export class InvalidResponseError extends OxinsiderApiError {
553
+ code = "invalid_response";
554
+ /** The response body as received, for diagnosis. */
555
+ received;
556
+ constructor(status, message, received) {
557
+ super(status, {
558
+ object: "error",
559
+ error: { code: "invalid_response", message },
560
+ ...(typeof received === "object" &&
561
+ received !== null &&
562
+ "meta" in received
563
+ ? { meta: received.meta }
564
+ : {}),
565
+ });
566
+ this.name = "InvalidResponseError";
567
+ this.received = received;
568
+ }
569
+ }
570
+ /** 5xx - an unexpected server-side error; retry only when the request is safe to replay. */
571
+ export class InternalServerError extends OxinsiderApiError {
572
+ code = "internal_error";
573
+ constructor(status, body, transport = {}) {
574
+ super(status, body, transport);
575
+ this.name = "InternalServerError";
576
+ }
577
+ }
578
+ /**
579
+ * Construct the most specific `OxinsiderApiError` subclass for a failed
580
+ * response. Dispatch is by `error.reason` FIRST -- strictly more specific than the code,
581
+ * because the reason names the CAUSE where the code names only the class -- then by
582
+ * `error.code`, then a status-code heuristic, falling back to the base class for anything
583
+ * unrecognized so an unknown code is never silently dropped.
584
+ */
585
+ export function errorFromResponse(status, body, retryAfterSeconds = null, transport = {}) {
586
+ const metadata = { ...transport, retryAfterSeconds };
587
+ const parsed = extractApiErrorBody(body);
588
+ const code = parsed?.code;
589
+ // `reason` first: it is strictly more specific than `code` (#7209), and it is
590
+ // what carries the actionable distinction -- "not yet" vs "does not exist",
591
+ // "warming" vs "rate limiter down". Falling through to `code` here is what made
592
+ // the SDK re-assert the very lie the backend removed.
593
+ switch (parsed?.reason) {
594
+ case "freshness_ceiling_unsatisfied":
595
+ return new FreshnessCeilingUnsatisfiedError(status, body, metadata);
596
+ case "pick_not_released":
597
+ return new PickNotReleasedError(status, body, retryAfterSeconds, metadata);
598
+ case "read_model_warming":
599
+ return new ReadModelWarmingError(status, body, retryAfterSeconds, metadata);
600
+ case "request_accounting_unavailable":
601
+ return new RequestAccountingUnavailableError(status, body, retryAfterSeconds, metadata);
602
+ case "database_unavailable":
603
+ return new DatabaseUnavailableError(status, body, retryAfterSeconds, metadata);
604
+ case "cursor_expired":
605
+ return new CursorExpiredError(status, body, metadata);
606
+ case "unknown_endpoint":
607
+ return new UnknownEndpointError(status, body, metadata);
608
+ case "trader_not_tracked":
609
+ return new TraderNotTrackedError(status, body, metadata);
610
+ case "idempotency_in_progress":
611
+ return new IdempotencyInProgressError(status, body, metadata);
612
+ case "webhook_delivery_in_progress":
613
+ return new WebhookDeliveryInProgressError(status, body, metadata);
614
+ case "sandbox_api_key":
615
+ return new SandboxApiKeyError(status, body, metadata);
616
+ case "api_key_in_query":
617
+ return new ApiKeyInQueryError(status, body, metadata);
618
+ case "unknown_query_parameter":
619
+ return new UnknownQueryParameterError(status, body, metadata);
620
+ default:
621
+ break;
622
+ }
623
+ switch (code) {
624
+ case "bad_request":
625
+ return new BadRequestError(status, body, metadata);
626
+ case "invalid_api_key":
627
+ return new InvalidApiKeyError(status, body, metadata);
628
+ case "subscription_required":
629
+ return new SubscriptionRequiredError(status, body, metadata);
630
+ case "forbidden":
631
+ return new ForbiddenError(status, body, metadata);
632
+ case "not_found":
633
+ return new NotFoundError(status, body, metadata);
634
+ case "account_locked":
635
+ return new AccountLockedError(status, body, metadata);
636
+ case "rate_limited":
637
+ return new RateLimitedError(status, body, retryAfterSeconds, metadata);
638
+ case "rate_limit_unavailable":
639
+ return new RateLimitUnavailableError(status, body, retryAfterSeconds, metadata);
640
+ case "request_timeout":
641
+ return new ServerTimeoutError(status, body, retryAfterSeconds, metadata);
642
+ case "internal_error":
643
+ return new InternalServerError(status, body, metadata);
644
+ default:
645
+ break;
646
+ }
647
+ // No (or unknown) error code: dispatch on the HTTP status so common
648
+ // failures still arrive as the expected subclass.
649
+ switch (status) {
650
+ case 400:
651
+ return new BadRequestError(status, body, metadata);
652
+ case 401:
653
+ return new InvalidApiKeyError(status, body, metadata);
654
+ case 402:
655
+ return new SubscriptionRequiredError(status, body, metadata);
656
+ case 404:
657
+ return new NotFoundError(status, body, metadata);
658
+ case 408:
659
+ return new ServerTimeoutError(status, body, retryAfterSeconds, metadata);
660
+ case 429:
661
+ return new RateLimitedError(status, body, retryAfterSeconds, metadata);
662
+ case 503:
663
+ return new RateLimitUnavailableError(status, body, retryAfterSeconds, metadata);
664
+ default:
665
+ if (status >= 500) {
666
+ return new InternalServerError(status, body, metadata);
667
+ }
668
+ return new OxinsiderApiError(status, body, metadata);
669
+ }
670
+ }
671
+ /** Pull the `error` object out of an envelope or a flat error body. */
672
+ export function extractApiErrorBody(body) {
673
+ if (!body || typeof body !== "object")
674
+ return null;
675
+ if ("object" in body &&
676
+ body.object === "error" &&
677
+ "error" in body &&
678
+ isApiErrorBody(body.error)) {
679
+ return body.error;
680
+ }
681
+ if (isApiErrorBody(body) && typeof body.code === "string") {
682
+ return body;
683
+ }
684
+ return null;
685
+ }
686
+ function extractApiErrorMeta(body) {
687
+ if (!body || typeof body !== "object")
688
+ return null;
689
+ if ("meta" in body) {
690
+ const meta = body.meta;
691
+ if (meta && typeof meta === "object") {
692
+ return meta;
693
+ }
694
+ }
695
+ return null;
696
+ }
697
+ function isApiErrorBody(body) {
698
+ return typeof body === "object" && body !== null;
699
+ }
700
+ //# sourceMappingURL=errors.js.map