@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/LICENSE +21 -0
- package/README.md +674 -0
- package/dist/client.d.ts +1652 -0
- package/dist/client.d.ts.map +1 -0
- package/dist/client.js +2127 -0
- package/dist/client.js.map +1 -0
- package/dist/data-quality.d.ts +74 -0
- package/dist/data-quality.d.ts.map +1 -0
- package/dist/data-quality.js +68 -0
- package/dist/data-quality.js.map +1 -0
- package/dist/errors.d.ts +400 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +700 -0
- package/dist/errors.js.map +1 -0
- package/dist/index.d.ts +65 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +33 -0
- package/dist/index.js.map +1 -0
- package/dist/pagination.d.ts +196 -0
- package/dist/pagination.d.ts.map +1 -0
- package/dist/pagination.js +208 -0
- package/dist/pagination.js.map +1 -0
- package/dist/provenance.d.ts +8 -0
- package/dist/provenance.d.ts.map +1 -0
- package/dist/provenance.js +15 -0
- package/dist/provenance.js.map +1 -0
- package/dist/retry.d.ts +68 -0
- package/dist/retry.d.ts.map +1 -0
- package/dist/retry.js +125 -0
- package/dist/retry.js.map +1 -0
- package/dist/schema.d.ts +4910 -0
- package/dist/schema.d.ts.map +1 -0
- package/dist/schema.js +7 -0
- package/dist/schema.js.map +1 -0
- package/dist/stream.d.ts +509 -0
- package/dist/stream.d.ts.map +1 -0
- package/dist/stream.js +932 -0
- package/dist/stream.js.map +1 -0
- package/dist/webhooks.d.ts +314 -0
- package/dist/webhooks.d.ts.map +1 -0
- package/dist/webhooks.js +153 -0
- package/dist/webhooks.js.map +1 -0
- package/package.json +64 -0
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
|