@0xinsider/mcp 2.1.0 → 2.1.3
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/README.md +25 -6
- package/dist/cli-lib.d.ts +1 -0
- package/dist/cli-lib.d.ts.map +1 -1
- package/dist/cli-lib.js +48 -8
- package/dist/cli-lib.js.map +1 -1
- package/dist/client.d.ts +33 -2
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +42 -4
- package/dist/client.js.map +1 -1
- package/dist/constants.d.ts +1 -1
- package/dist/constants.js +1 -1
- package/dist/product-cli/client.d.ts +3 -2
- package/dist/product-cli/client.d.ts.map +1 -1
- package/dist/product-cli/client.js.map +1 -1
- package/dist/sdk/client.d.ts +690 -133
- package/dist/sdk/client.d.ts.map +1 -1
- package/dist/sdk/client.js +699 -125
- package/dist/sdk/client.js.map +1 -1
- package/dist/sdk/errors.d.ts +42 -4
- package/dist/sdk/errors.d.ts.map +1 -1
- package/dist/sdk/errors.js +103 -2
- package/dist/sdk/errors.js.map +1 -1
- package/dist/sdk/index.d.ts +9 -8
- package/dist/sdk/index.d.ts.map +1 -1
- package/dist/sdk/index.js +6 -4
- package/dist/sdk/index.js.map +1 -1
- package/dist/sdk/pagination.d.ts +133 -15
- package/dist/sdk/pagination.d.ts.map +1 -1
- package/dist/sdk/pagination.js +132 -34
- package/dist/sdk/pagination.js.map +1 -1
- package/dist/sdk/retry.d.ts +68 -0
- package/dist/sdk/retry.d.ts.map +1 -0
- package/dist/sdk/retry.js +125 -0
- package/dist/sdk/retry.js.map +1 -0
- package/dist/sdk/schema.d.ts +1381 -439
- package/dist/sdk/schema.d.ts.map +1 -1
- package/dist/sdk/stream.d.ts +318 -8
- package/dist/sdk/stream.d.ts.map +1 -1
- package/dist/sdk/stream.js +503 -56
- package/dist/sdk/stream.js.map +1 -1
- package/dist/sdk/webhooks.d.ts +18 -3
- package/dist/sdk/webhooks.d.ts.map +1 -1
- package/dist/sdk/webhooks.js +13 -1
- package/dist/sdk/webhooks.js.map +1 -1
- package/dist/tools/explore-markets.d.ts.map +1 -1
- package/dist/tools/explore-markets.js +2 -5
- package/dist/tools/explore-markets.js.map +1 -1
- package/dist/tools/get-insider-radar.d.ts.map +1 -1
- package/dist/tools/get-insider-radar.js +3 -10
- package/dist/tools/get-insider-radar.js.map +1 -1
- package/dist/tools/get-large-positions.d.ts.map +1 -1
- package/dist/tools/get-large-positions.js +3 -10
- package/dist/tools/get-large-positions.js.map +1 -1
- package/dist/tools/get-leaderboard.d.ts.map +1 -1
- package/dist/tools/get-leaderboard.js +3 -10
- package/dist/tools/get-leaderboard.js.map +1 -1
- package/dist/tools/get-market-intel.d.ts.map +1 -1
- package/dist/tools/get-market-intel.js +3 -6
- package/dist/tools/get-market-intel.js.map +1 -1
- package/dist/tools/get-position-timeline.d.ts.map +1 -1
- package/dist/tools/get-position-timeline.js +3 -10
- package/dist/tools/get-position-timeline.js.map +1 -1
- package/dist/tools/get-positions.d.ts +2 -0
- package/dist/tools/get-positions.d.ts.map +1 -1
- package/dist/tools/get-positions.js +8 -11
- package/dist/tools/get-positions.js.map +1 -1
- package/dist/tools/get-trader-pnl.d.ts.map +1 -1
- package/dist/tools/get-trader-pnl.js +3 -6
- package/dist/tools/get-trader-pnl.js.map +1 -1
- package/dist/tools/get-trader.d.ts.map +1 -1
- package/dist/tools/get-trader.js +3 -6
- package/dist/tools/get-trader.js.map +1 -1
- package/dist/tools/get-trending-wallets.d.ts.map +1 -1
- package/dist/tools/get-trending-wallets.js +3 -10
- package/dist/tools/get-trending-wallets.js.map +1 -1
- package/dist/tools/get-whale-trades.d.ts.map +1 -1
- package/dist/tools/get-whale-trades.js +4 -11
- package/dist/tools/get-whale-trades.js.map +1 -1
- package/dist/tools/pick-of-the-day.d.ts.map +1 -1
- package/dist/tools/pick-of-the-day.js +5 -21
- package/dist/tools/pick-of-the-day.js.map +1 -1
- package/dist/tools/read-parity.d.ts.map +1 -1
- package/dist/tools/read-parity.js +36 -44
- package/dist/tools/read-parity.js.map +1 -1
- package/dist/tools/result.d.ts +31 -0
- package/dist/tools/result.d.ts.map +1 -0
- package/dist/tools/result.js +70 -0
- package/dist/tools/result.js.map +1 -0
- package/dist/tools/search-markets.d.ts.map +1 -1
- package/dist/tools/search-markets.js +16 -11
- package/dist/tools/search-markets.js.map +1 -1
- package/package.json +4 -4
package/dist/sdk/client.js
CHANGED
|
@@ -19,12 +19,17 @@
|
|
|
19
19
|
* - Extra typed convenience methods for list/candle/webhook-event/stream
|
|
20
20
|
* surfaces and a typed `Grade` enum.
|
|
21
21
|
*/
|
|
22
|
-
import { errorFromResponse, OxinsiderApiError, RequestTimeoutError, } from "./errors.js";
|
|
22
|
+
import { errorFromResponse, OxinsiderApiError, InvalidResponseError, RequestTimeoutError, } from "./errors.js";
|
|
23
|
+
import { RETRY_AFTER_CEILING_MS, retryAfterSeconds, sleepUnlessAborted, } from "./retry.js";
|
|
23
24
|
/**
|
|
24
|
-
* The full V1 operation table. One row per OpenAPI operation
|
|
25
|
-
*
|
|
26
|
-
* `
|
|
27
|
-
*
|
|
25
|
+
* The full V1 operation table. One row per OpenAPI operation with a
|
|
26
|
+
* documented 2xx, which since #16137 includes the `201`-only
|
|
27
|
+
* `registerAgent`; `kind` says which method reads its body. Kept in lockstep
|
|
28
|
+
* with `web/public/api/v1/openapi.json` by
|
|
29
|
+
* `scripts/check-sdk-openapi-drift.mjs`, which also checks every row's kind
|
|
30
|
+
* against the spec. Do not add a row without a matching spec operation, and
|
|
31
|
+
* do not remove a spec operation without removing its row. A redirect-only
|
|
32
|
+
* operation belongs in `REDIRECT_OPERATIONS` below, not here.
|
|
28
33
|
*/
|
|
29
34
|
export const API_CLIENT_OPERATIONS = [
|
|
30
35
|
{ method: "GET", path: "/api/v1/me", operationId: "getAccountIdentity", auth: "bearer" },
|
|
@@ -76,6 +81,12 @@ export const API_CLIENT_OPERATIONS = [
|
|
|
76
81
|
operationId: "getTraderPnl",
|
|
77
82
|
auth: "bearer",
|
|
78
83
|
},
|
|
84
|
+
{
|
|
85
|
+
method: "GET",
|
|
86
|
+
path: "/api/v1/trader/{address}/categories",
|
|
87
|
+
operationId: "getTraderCategoryRecords",
|
|
88
|
+
auth: "bearer",
|
|
89
|
+
},
|
|
79
90
|
// Pre-existing op-table drift (surfaced by test/drift.test.ts, #6915): these
|
|
80
91
|
// 200-returning openapi operations were never mirrored into the client table.
|
|
81
92
|
// Added so the table stays contract-complete against the checked-in spec. All
|
|
@@ -94,6 +105,7 @@ export const API_CLIENT_OPERATIONS = [
|
|
|
94
105
|
path: "/api/v1/trader/{address}/context.md",
|
|
95
106
|
operationId: "getTraderContextMarkdown",
|
|
96
107
|
auth: "bearer",
|
|
108
|
+
kind: "text",
|
|
97
109
|
},
|
|
98
110
|
{
|
|
99
111
|
method: "POST",
|
|
@@ -200,6 +212,12 @@ export const API_CLIENT_OPERATIONS = [
|
|
|
200
212
|
operationId: "getPlatforms",
|
|
201
213
|
auth: "none",
|
|
202
214
|
},
|
|
215
|
+
{
|
|
216
|
+
method: "GET",
|
|
217
|
+
path: "/api/v1/market/{condition_id}/holders",
|
|
218
|
+
operationId: "getMarketHolders",
|
|
219
|
+
auth: "bearer",
|
|
220
|
+
},
|
|
203
221
|
{
|
|
204
222
|
method: "GET",
|
|
205
223
|
path: "/api/v1/market/{condition_id}/intel",
|
|
@@ -223,6 +241,7 @@ export const API_CLIENT_OPERATIONS = [
|
|
|
223
241
|
path: "/api/v1/market/{condition_id}/context.md",
|
|
224
242
|
operationId: "getMarketContextMarkdown",
|
|
225
243
|
auth: "bearer",
|
|
244
|
+
kind: "text",
|
|
226
245
|
},
|
|
227
246
|
{
|
|
228
247
|
method: "GET",
|
|
@@ -253,6 +272,7 @@ export const API_CLIENT_OPERATIONS = [
|
|
|
253
272
|
path: "/api/v1/stream",
|
|
254
273
|
operationId: "getStream",
|
|
255
274
|
auth: "bearer",
|
|
275
|
+
kind: "sse",
|
|
256
276
|
},
|
|
257
277
|
{
|
|
258
278
|
method: "GET",
|
|
@@ -296,6 +316,24 @@ export const API_CLIENT_OPERATIONS = [
|
|
|
296
316
|
operationId: "rotateWebhookSecret",
|
|
297
317
|
auth: "bearer",
|
|
298
318
|
},
|
|
319
|
+
{
|
|
320
|
+
method: "POST",
|
|
321
|
+
path: "/api/v1/webhooks/{id}/rotate-secret/prepare",
|
|
322
|
+
operationId: "prepareWebhookSecret",
|
|
323
|
+
auth: "bearer",
|
|
324
|
+
},
|
|
325
|
+
{
|
|
326
|
+
method: "POST",
|
|
327
|
+
path: "/api/v1/webhooks/{id}/rotate-secret/activate",
|
|
328
|
+
operationId: "activateWebhookSecret",
|
|
329
|
+
auth: "bearer",
|
|
330
|
+
},
|
|
331
|
+
{
|
|
332
|
+
method: "POST",
|
|
333
|
+
path: "/api/v1/webhooks/{id}/rotate-secret/retire",
|
|
334
|
+
operationId: "retireWebhookSecret",
|
|
335
|
+
auth: "bearer",
|
|
336
|
+
},
|
|
299
337
|
{
|
|
300
338
|
method: "GET",
|
|
301
339
|
path: "/api/v1/webhooks/events",
|
|
@@ -325,12 +363,7 @@ export const API_CLIENT_OPERATIONS = [
|
|
|
325
363
|
path: "/api/v1/mcp",
|
|
326
364
|
operationId: "createMcpJsonRpcResponse",
|
|
327
365
|
auth: "bearer",
|
|
328
|
-
|
|
329
|
-
{
|
|
330
|
-
method: "GET",
|
|
331
|
-
path: "/api/v1/mcp",
|
|
332
|
-
operationId: "openMcpEventStream",
|
|
333
|
-
auth: "bearer",
|
|
366
|
+
kind: "jsonrpc",
|
|
334
367
|
},
|
|
335
368
|
{
|
|
336
369
|
method: "GET",
|
|
@@ -384,9 +417,100 @@ export const API_CLIENT_OPERATIONS = [
|
|
|
384
417
|
method: "GET",
|
|
385
418
|
path: "/api/v1/pick-of-the-day/ledger",
|
|
386
419
|
operationId: "getPickOfTheDayLedger",
|
|
420
|
+
// Keyless since #16459: the commitment ledger is published to be
|
|
421
|
+
// republished, so a client can read it with no key configured.
|
|
422
|
+
auth: "none",
|
|
423
|
+
},
|
|
424
|
+
{
|
|
425
|
+
method: "POST",
|
|
426
|
+
path: "/api/v1/agents/register",
|
|
427
|
+
operationId: "registerAgent",
|
|
428
|
+
// `security: []` in the document: minting a sandbox key is the one write
|
|
429
|
+
// that must work before a caller has any credential.
|
|
430
|
+
auth: "none",
|
|
431
|
+
},
|
|
432
|
+
];
|
|
433
|
+
/**
|
|
434
|
+
* Published operations whose success is a redirect, with the method that
|
|
435
|
+
* follows it (#16137). These have no 2xx body, so they are absent from
|
|
436
|
+
* `API_CLIENT_OPERATIONS` and from the generated `schema.ts`;
|
|
437
|
+
* `scripts/check-sdk-openapi-drift.mjs` requires every redirect-only spec
|
|
438
|
+
* operation to be declared here, so one cannot go missing in silence again.
|
|
439
|
+
*/
|
|
440
|
+
export const REDIRECT_OPERATIONS = [
|
|
441
|
+
{
|
|
442
|
+
method: "GET",
|
|
443
|
+
path: "/api/v1/trader/{address}/export/download",
|
|
444
|
+
operationId: "downloadTraderExport",
|
|
387
445
|
auth: "bearer",
|
|
446
|
+
status: 302,
|
|
447
|
+
handledBy: "getTraderExportDownloadUrl() resolves the Location; downloadTraderExport() then fetches the object with no Authorization header.",
|
|
448
|
+
},
|
|
449
|
+
{
|
|
450
|
+
method: "GET",
|
|
451
|
+
path: "/api/v1/openapi.json",
|
|
452
|
+
operationId: "redirectApiOpenapiSpec",
|
|
453
|
+
auth: "none",
|
|
454
|
+
status: 307,
|
|
455
|
+
handledBy: "No method: the document is public and static, so fetch the URL directly and let your runtime follow the redirect. This package ships the same contract as generated types in schema.ts.",
|
|
388
456
|
},
|
|
389
457
|
];
|
|
458
|
+
/**
|
|
459
|
+
* Published operations this client deliberately does not wrap, with the
|
|
460
|
+
* reason. The drift check requires every remaining spec operation to be
|
|
461
|
+
* accounted for here, so "unsupported" is always a stated decision.
|
|
462
|
+
*/
|
|
463
|
+
export const UNSUPPORTED_OPERATIONS = [
|
|
464
|
+
{
|
|
465
|
+
method: "GET",
|
|
466
|
+
path: "/api/v1/mcp",
|
|
467
|
+
operationId: "openMcpEventStream",
|
|
468
|
+
reason: "The MCP endpoint offers no server-to-client stream: GET answers 405 by design, so a client that reconnects to it would loop (#16354). Post JSON-RPC with mcp() instead.",
|
|
469
|
+
},
|
|
470
|
+
];
|
|
471
|
+
/**
|
|
472
|
+
* The writes the API replays under `Idempotency-Key`: a second request with
|
|
473
|
+
* the same key and the same body returns the first result instead of
|
|
474
|
+
* repeating the write. Source of truth: the operations declaring the
|
|
475
|
+
* `Idempotency-Key` header parameter in `web/public/api/v1/openapi.json`,
|
|
476
|
+
* pinned by `scripts/check-sdk-openapi-drift.mjs` (#16182). Only these are
|
|
477
|
+
* retried on a transport or 5xx failure, and only when a key is set; a key on
|
|
478
|
+
* any other operation is refused before the request, since the server would
|
|
479
|
+
* ignore it and a retry could repeat the side effect (`verifyWebhook` sends
|
|
480
|
+
* a challenge to your URL each time; `submitTraderExport` starts a job).
|
|
481
|
+
*/
|
|
482
|
+
export const IDEMPOTENT_WRITE_OPERATIONS = [
|
|
483
|
+
"createWebhook",
|
|
484
|
+
"updateWebhook",
|
|
485
|
+
"deleteWebhook",
|
|
486
|
+
"rotateWebhookSecret",
|
|
487
|
+
"prepareWebhookSecret",
|
|
488
|
+
"activateWebhookSecret",
|
|
489
|
+
"retireWebhookSecret",
|
|
490
|
+
"redeliverWebhookDelivery",
|
|
491
|
+
];
|
|
492
|
+
/**
|
|
493
|
+
* POST operations that read and never write (#16182): the batch lookups.
|
|
494
|
+
* `POST /api/v1/traders/batch` and `POST /api/v1/markets/intel/batch` resolve
|
|
495
|
+
* their inputs and store nothing (`backend/src/api_v1/handlers/batch.rs`), so
|
|
496
|
+
* a repeat cannot duplicate a side effect. Each attempt is one request
|
|
497
|
+
* against the account's quota and one reservation of batch item units, which
|
|
498
|
+
* is exactly what a retried GET costs, so they are retried like a GET. Every
|
|
499
|
+
* other POST, PATCH or DELETE is retried only as a keyed write above.
|
|
500
|
+
*/
|
|
501
|
+
export const READ_ONLY_POST_OPERATIONS = [
|
|
502
|
+
"batchGetTraders",
|
|
503
|
+
"batchGetMarketIntel",
|
|
504
|
+
];
|
|
505
|
+
const idempotentWrites = new Set(IDEMPOTENT_WRITE_OPERATIONS);
|
|
506
|
+
const readOnlyPosts = new Set(READ_ONLY_POST_OPERATIONS);
|
|
507
|
+
/** The retry class of an operation; see `RetryEligibility`. */
|
|
508
|
+
export function retryEligibility(operation) {
|
|
509
|
+
if (operation.method === "GET" || readOnlyPosts.has(operation.operationId)) {
|
|
510
|
+
return "read";
|
|
511
|
+
}
|
|
512
|
+
return idempotentWrites.has(operation.operationId) ? "keyed" : "never";
|
|
513
|
+
}
|
|
390
514
|
/**
|
|
391
515
|
* Trader skill grade. Source of truth:
|
|
392
516
|
* `components.schemas.Trader.properties.grade.enum` (S highest, F lowest).
|
|
@@ -394,17 +518,25 @@ export const API_CLIENT_OPERATIONS = [
|
|
|
394
518
|
export const GRADES = ["S", "A", "B", "C", "D", "F"];
|
|
395
519
|
/** Default production API base used when `baseUrl` is omitted. */
|
|
396
520
|
export const DEFAULT_BASE_URL = "https://api.0xinsider.com";
|
|
521
|
+
/**
|
|
522
|
+
* The sandbox server, the second `servers` entry of the OpenAPI document: no
|
|
523
|
+
* credential, no production data, every documented operation answered with
|
|
524
|
+
* its example or a deterministic sample, `?sandbox_status=<code>` for a
|
|
525
|
+
* documented error, and `X-Oxi-Sandbox: true` on every response. Streams and
|
|
526
|
+
* file downloads are not simulated there and answer 400.
|
|
527
|
+
*/
|
|
528
|
+
export const SANDBOX_BASE_URL = "https://0xinsider.com/sandbox";
|
|
529
|
+
/** A live secret key starts with this; the sandbox never needs one. */
|
|
530
|
+
const LIVE_KEY_PREFIX = "oxi_sk_live_";
|
|
397
531
|
/** Default per-request deadline; see `ApiClientOptions.timeoutMs`. */
|
|
398
532
|
export const DEFAULT_TIMEOUT_MS = 15_000;
|
|
399
533
|
/** Default retry budget; see `ApiClientOptions.maxRetries`. */
|
|
400
534
|
export const DEFAULT_MAX_RETRIES = 2;
|
|
401
535
|
/** Statuses a retry can fix: rate limited, or a gateway/availability failure. */
|
|
402
|
-
const RETRYABLE_STATUSES = new Set([429, 502, 503, 504]);
|
|
536
|
+
const RETRYABLE_STATUSES = new Set([408, 429, 502, 503, 504]);
|
|
403
537
|
const RETRY_BASE_DELAY_MS = 500;
|
|
404
538
|
const RETRY_MAX_BACKOFF_MS = 8_000;
|
|
405
539
|
const RETRY_JITTER_MS = 250;
|
|
406
|
-
/** A server-requested wait longer than this is the caller's to schedule. */
|
|
407
|
-
const RETRY_AFTER_CEILING_MS = 60_000;
|
|
408
540
|
function assertRetryCount(value, source) {
|
|
409
541
|
if (!Number.isInteger(value) || value < 0) {
|
|
410
542
|
throw new Error(`${source} must be a non-negative integer, got ${String(value)}`);
|
|
@@ -413,11 +545,14 @@ function assertRetryCount(value, source) {
|
|
|
413
545
|
}
|
|
414
546
|
/**
|
|
415
547
|
* Milliseconds to wait before retry `attempt` (0-based), or `null` when the
|
|
416
|
-
* server asked for a wait a retry loop should not hold.
|
|
548
|
+
* server asked for a wait a retry loop should not hold. The ceiling check
|
|
549
|
+
* runs on the server's number before jitter is added, so the value handed to
|
|
550
|
+
* the timer is at most `RETRY_AFTER_CEILING_MS + RETRY_JITTER_MS`, far inside
|
|
551
|
+
* the timer range (`MAX_TIMER_DELAY_MS`, `retry.ts`).
|
|
417
552
|
*/
|
|
418
553
|
function retryDelayMs(attempt, retryAfter) {
|
|
419
554
|
if (retryAfter !== null) {
|
|
420
|
-
const requested =
|
|
555
|
+
const requested = retryAfter * 1000;
|
|
421
556
|
if (requested > RETRY_AFTER_CEILING_MS)
|
|
422
557
|
return null;
|
|
423
558
|
return requested + Math.random() * RETRY_JITTER_MS;
|
|
@@ -425,24 +560,6 @@ function retryDelayMs(attempt, retryAfter) {
|
|
|
425
560
|
const ceiling = Math.min(RETRY_MAX_BACKOFF_MS, RETRY_BASE_DELAY_MS * 2 ** attempt);
|
|
426
561
|
return ceiling / 2 + Math.random() * (ceiling / 2);
|
|
427
562
|
}
|
|
428
|
-
/** Resolve after `ms`, or reject with the signal's reason the moment it aborts. */
|
|
429
|
-
function sleepUnlessAborted(ms, signal) {
|
|
430
|
-
return new Promise((resolve, reject) => {
|
|
431
|
-
if (signal?.aborted) {
|
|
432
|
-
reject(signal.reason);
|
|
433
|
-
return;
|
|
434
|
-
}
|
|
435
|
-
const onAbort = () => {
|
|
436
|
-
clearTimeout(timer);
|
|
437
|
-
reject(signal?.reason);
|
|
438
|
-
};
|
|
439
|
-
const timer = setTimeout(() => {
|
|
440
|
-
signal?.removeEventListener("abort", onAbort);
|
|
441
|
-
resolve();
|
|
442
|
-
}, ms);
|
|
443
|
-
signal?.addEventListener("abort", onAbort, { once: true });
|
|
444
|
-
});
|
|
445
|
-
}
|
|
446
563
|
function isLoopbackHost(hostname) {
|
|
447
564
|
const host = hostname.replace(/^\[|\]$/g, "").toLowerCase();
|
|
448
565
|
return (host === "localhost" ||
|
|
@@ -469,6 +586,28 @@ export function assertTrustedBaseUrl(baseUrl) {
|
|
|
469
586
|
return url;
|
|
470
587
|
throw new Error(`Refusing to send the API key to ${url.origin}: baseUrl must use https: (http: is accepted only for a loopback host such as localhost or 127.0.0.1).`);
|
|
471
588
|
}
|
|
589
|
+
/**
|
|
590
|
+
* The server URL as this client stores it: origin plus path, trailing slashes
|
|
591
|
+
* trimmed, and a trailing `/api/v1` removed once. Every operation path starts
|
|
592
|
+
* with `/api/v1/`, so a base that already ends in it would double the prefix;
|
|
593
|
+
* before #16138 such a base worked only because the path was discarded, and
|
|
594
|
+
* this keeps it working.
|
|
595
|
+
*/
|
|
596
|
+
function normalizeBaseUrl(base) {
|
|
597
|
+
const trimmed = base.toString().replace(/\/+$/, "");
|
|
598
|
+
return trimmed.replace(/\/api\/v1$/, "");
|
|
599
|
+
}
|
|
600
|
+
/**
|
|
601
|
+
* Join an `/api/v1/...` path onto the server URL, keeping the server's own
|
|
602
|
+
* path (`/sandbox`) exactly once. REST (`buildUrl`) and the SSE stream
|
|
603
|
+
* (`stream.ts`) both resolve through this, so the two cannot disagree about
|
|
604
|
+
* where a path-bearing base points (#16138: `new URL("/api/v1/...", base)`
|
|
605
|
+
* resolved against the origin and dropped `/sandbox`).
|
|
606
|
+
*/
|
|
607
|
+
export function resolveApiUrl(baseUrl, path) {
|
|
608
|
+
const suffix = path.startsWith("/") ? path : `/${path}`;
|
|
609
|
+
return new URL(`${baseUrl}${suffix}`);
|
|
610
|
+
}
|
|
472
611
|
/**
|
|
473
612
|
* One signal that aborts on the deadline OR when the caller's signal aborts.
|
|
474
613
|
* Uses native composition where available. The manual fallback removes both
|
|
@@ -518,6 +657,84 @@ function isTimeoutAbort(error) {
|
|
|
518
657
|
"name" in error &&
|
|
519
658
|
error.name === "TimeoutError");
|
|
520
659
|
}
|
|
660
|
+
/** Which method reads each kind of success body, named in the error that refuses the wrong one. */
|
|
661
|
+
const READER_FOR_KIND = {
|
|
662
|
+
envelope: "call() or list()",
|
|
663
|
+
text: "text()",
|
|
664
|
+
jsonrpc: "mcp()",
|
|
665
|
+
sse: "streamFeed(), streamFeedResilient() or consumeStreamCheckpointed()",
|
|
666
|
+
};
|
|
667
|
+
// --- MCP JSON-RPC (#16137) ---
|
|
668
|
+
/** The MCP methods `POST /api/v1/mcp` accepts, from the operation's request body. */
|
|
669
|
+
export const MCP_METHODS = [
|
|
670
|
+
"initialize",
|
|
671
|
+
"notifications/initialized",
|
|
672
|
+
"notifications/cancelled",
|
|
673
|
+
"ping",
|
|
674
|
+
"tools/list",
|
|
675
|
+
"tools/call",
|
|
676
|
+
];
|
|
677
|
+
function isMcpJsonRpcResponse(body) {
|
|
678
|
+
if (typeof body !== "object" || body === null)
|
|
679
|
+
return false;
|
|
680
|
+
const candidate = body;
|
|
681
|
+
return (candidate.jsonrpc === "2.0" &&
|
|
682
|
+
(typeof candidate.id === "string" ||
|
|
683
|
+
typeof candidate.id === "number" ||
|
|
684
|
+
candidate.id === null));
|
|
685
|
+
}
|
|
686
|
+
// --- Export download (#16137) ---
|
|
687
|
+
/** The `downloadTraderExport` row of `REDIRECT_OPERATIONS`, as a request target. */
|
|
688
|
+
const exportDownloadOperation = {
|
|
689
|
+
method: REDIRECT_OPERATIONS[0].method,
|
|
690
|
+
path: REDIRECT_OPERATIONS[0].path,
|
|
691
|
+
operationId: REDIRECT_OPERATIONS[0].operationId,
|
|
692
|
+
auth: REDIRECT_OPERATIONS[0].auth,
|
|
693
|
+
};
|
|
694
|
+
/**
|
|
695
|
+
* Read a presigned URL's own expiry, and refuse a destination that would
|
|
696
|
+
* downgrade the transport. The backend already validates the redirect
|
|
697
|
+
* against its expected R2 origin; this is the client-side half, so a
|
|
698
|
+
* redirect can never move a download onto plain `http:`.
|
|
699
|
+
*/
|
|
700
|
+
function presignedTarget(location) {
|
|
701
|
+
let url;
|
|
702
|
+
try {
|
|
703
|
+
url = new URL(location);
|
|
704
|
+
}
|
|
705
|
+
catch {
|
|
706
|
+
throw new InvalidResponseError(302, `The export download redirect is not an absolute URL: ${location}`, null);
|
|
707
|
+
}
|
|
708
|
+
if (url.protocol !== "https:" && !isLoopbackHost(url.hostname)) {
|
|
709
|
+
throw new InvalidResponseError(302, `Refusing to follow the export download redirect to ${url.origin}: it must use https: (http: is accepted only for a loopback host).`, null);
|
|
710
|
+
}
|
|
711
|
+
const signedAt = url.searchParams.get("X-Amz-Date");
|
|
712
|
+
const lifetime = url.searchParams.get("X-Amz-Expires");
|
|
713
|
+
const expiresAt = presignExpiry(signedAt, lifetime);
|
|
714
|
+
return { url: url.toString(), ...(expiresAt ? { expiresAt } : {}) };
|
|
715
|
+
}
|
|
716
|
+
/** `20260922T101500Z` plus `3600` seconds, as an ISO instant; `null` if either is unreadable. */
|
|
717
|
+
function presignExpiry(signedAt, lifetimeSeconds) {
|
|
718
|
+
if (!signedAt || !lifetimeSeconds)
|
|
719
|
+
return null;
|
|
720
|
+
const parsed = /^(\d{4})(\d{2})(\d{2})T(\d{2})(\d{2})(\d{2})Z$/.exec(signedAt);
|
|
721
|
+
const seconds = Number(lifetimeSeconds);
|
|
722
|
+
if (!parsed || !Number.isFinite(seconds))
|
|
723
|
+
return null;
|
|
724
|
+
const [, year, month, day, hour, minute, second] = parsed;
|
|
725
|
+
const signed = Date.UTC(Number(year), Number(month) - 1, Number(day), Number(hour), Number(minute), Number(second));
|
|
726
|
+
return new Date(signed + seconds * 1000).toISOString();
|
|
727
|
+
}
|
|
728
|
+
/** The `filename="..."` of a `Content-Disposition`, or `null`. */
|
|
729
|
+
function filenameFromDisposition(disposition) {
|
|
730
|
+
if (!disposition)
|
|
731
|
+
return null;
|
|
732
|
+
const quoted = /filename\*?=(?:UTF-8'')?"([^"]+)"/i.exec(disposition);
|
|
733
|
+
if (quoted)
|
|
734
|
+
return decodeURIComponent(quoted[1]);
|
|
735
|
+
const bare = /filename\*?=(?:UTF-8'')?([^;]+)/i.exec(disposition);
|
|
736
|
+
return bare ? decodeURIComponent(bare[1].trim()) : null;
|
|
737
|
+
}
|
|
521
738
|
/** `GET /api/v1/leaderboard` -> parameters -> strategy (`web/public/api/v1/openapi.json`). */
|
|
522
739
|
export const LEADERBOARD_STRATEGIES = [
|
|
523
740
|
"accumulator",
|
|
@@ -531,16 +748,48 @@ export const LEADERBOARD_STRATEGIES = [
|
|
|
531
748
|
"speculator",
|
|
532
749
|
"swing_trader",
|
|
533
750
|
];
|
|
751
|
+
/**
|
|
752
|
+
* Body-level `expand` values of `POST /api/v1/traders/batch`. The type is the
|
|
753
|
+
* operation's own body (`OperationBody["batchGetTraders"]["expand"]`); the
|
|
754
|
+
* const is the same list as a runtime value for a caller that iterates it.
|
|
755
|
+
*/
|
|
756
|
+
export const BATCH_TRADER_EXPANSIONS = [
|
|
757
|
+
"strategy",
|
|
758
|
+
"categories",
|
|
759
|
+
"quant_metrics",
|
|
760
|
+
"trust",
|
|
761
|
+
];
|
|
534
762
|
const operationsById = new Map(API_CLIENT_OPERATIONS.map((operation) => [operation.operationId, operation]));
|
|
535
763
|
export class OxinsiderApiClient {
|
|
536
764
|
baseUrl;
|
|
537
765
|
apiKey;
|
|
766
|
+
sandbox;
|
|
538
767
|
fetchImpl;
|
|
539
768
|
timeoutMs;
|
|
540
769
|
maxRetries;
|
|
770
|
+
/**
|
|
771
|
+
* A client for the sandbox server (#16138): `SANDBOX_BASE_URL`, no
|
|
772
|
+
* credential needed, example data only, never production data. The same
|
|
773
|
+
* as `new OxinsiderApiClient({ ...options, sandbox: true })`; pass a
|
|
774
|
+
* sandbox key (`oxi_sk_test_*`) as `apiKey` to have it checked.
|
|
775
|
+
*
|
|
776
|
+
* @example
|
|
777
|
+
* const sandbox = OxinsiderApiClient.sandbox();
|
|
778
|
+
* const board = await sandbox.listLeaderboard({ limit: 5 }); // no key
|
|
779
|
+
* board.meta?.sandbox; // true
|
|
780
|
+
*/
|
|
781
|
+
static sandbox(options = {}) {
|
|
782
|
+
return new OxinsiderApiClient({ ...options, sandbox: true });
|
|
783
|
+
}
|
|
541
784
|
constructor(options = {}) {
|
|
542
|
-
|
|
543
|
-
|
|
785
|
+
this.sandbox = options.sandbox === true;
|
|
786
|
+
const base = assertTrustedBaseUrl(options.baseUrl ?? (this.sandbox ? SANDBOX_BASE_URL : DEFAULT_BASE_URL));
|
|
787
|
+
this.baseUrl = normalizeBaseUrl(base);
|
|
788
|
+
if (this.sandbox && options.apiKey?.startsWith(LIVE_KEY_PREFIX)) {
|
|
789
|
+
// The sandbox needs no credential and ignores a live key, so the only
|
|
790
|
+
// effect of sending one is a secret on the wire for nothing.
|
|
791
|
+
throw new Error("Refusing to send a live API key (oxi_sk_live_*) to the sandbox: omit apiKey, or pass a sandbox key (oxi_sk_test_*) from POST /api/v1/agents/register.");
|
|
792
|
+
}
|
|
544
793
|
this.apiKey = options.apiKey;
|
|
545
794
|
this.timeoutMs =
|
|
546
795
|
options.timeoutMs === undefined ? DEFAULT_TIMEOUT_MS : options.timeoutMs;
|
|
@@ -552,29 +801,224 @@ export class OxinsiderApiClient {
|
|
|
552
801
|
// Bind so a passed-through `globalThis.fetch` keeps its receiver.
|
|
553
802
|
this.fetchImpl = fetchImpl.bind(globalThis);
|
|
554
803
|
}
|
|
804
|
+
// The implementation signature is what both overloads narrow; `unknown`
|
|
805
|
+
// here is the widest return both can refine, not a shape a caller sees.
|
|
806
|
+
async call(operationId, options = {}) {
|
|
807
|
+
const operation = this.resolveOperation(operationId, "envelope");
|
|
808
|
+
return this.request(operation, options, async (response) => {
|
|
809
|
+
if (response.status === 304) {
|
|
810
|
+
return notModifiedResponse(response);
|
|
811
|
+
}
|
|
812
|
+
const envelope = await parseJson(response);
|
|
813
|
+
if (isApiEnvelope(envelope)) {
|
|
814
|
+
const etag = response.headers.get("etag");
|
|
815
|
+
const sandbox = response.headers.get("x-oxi-sandbox") === "true";
|
|
816
|
+
const queryIgnored = response.headers.get("x-query-ignored");
|
|
817
|
+
const effectiveQuery = response.headers.get("x-effective-query");
|
|
818
|
+
// Only ADD to a meta the server actually sent. The contract makes
|
|
819
|
+
// `meta` required on every envelope, so synthesizing one from `?? {}`
|
|
820
|
+
// would hand a caller a `ResponseMeta` missing its required fields --
|
|
821
|
+
// which is what the hand-written type hid before #14278. `status` is
|
|
822
|
+
// always lifted (#16137): `201` and `202` are load-bearing on
|
|
823
|
+
// `registerAgent` and `submitTraderExport`, and a caller cannot read
|
|
824
|
+
// them off a body that looks identical to a 200.
|
|
825
|
+
if (envelope.meta) {
|
|
826
|
+
envelope.meta = {
|
|
827
|
+
...envelope.meta,
|
|
828
|
+
status: response.status,
|
|
829
|
+
...(etag ? { etag } : {}),
|
|
830
|
+
...(sandbox ? { sandbox: true } : {}),
|
|
831
|
+
...(queryIgnored ? { queryIgnored } : {}),
|
|
832
|
+
...(effectiveQuery ? { effectiveQuery } : {}),
|
|
833
|
+
};
|
|
834
|
+
}
|
|
835
|
+
return envelope;
|
|
836
|
+
}
|
|
837
|
+
throw new OxinsiderApiError(response.status, envelope);
|
|
838
|
+
});
|
|
839
|
+
}
|
|
840
|
+
async text(operationId, options = {}) {
|
|
841
|
+
const operation = this.resolveOperation(operationId, "text");
|
|
842
|
+
return this.request(operation, options, async (response) => response.text(), { accept: "text/markdown, text/plain;q=0.9, */*;q=0.1" });
|
|
843
|
+
}
|
|
555
844
|
/**
|
|
556
|
-
*
|
|
557
|
-
*
|
|
558
|
-
*
|
|
845
|
+
* Post one MCP JSON-RPC 2.0 message to `POST /api/v1/mcp` (#16137).
|
|
846
|
+
*
|
|
847
|
+
* The endpoint answers a JSON-RPC envelope, not the `{ object, data, meta }`
|
|
848
|
+
* envelope every other operation uses, so `call()` refuses it. A request
|
|
849
|
+
* (one carrying `id`) comes back as `status: 200` with `response` set; a
|
|
850
|
+
* supported notification (no `id`) comes back as `status: 202` with
|
|
851
|
+
* `response: null`, which is the empty body the server sends. A JSON-RPC
|
|
852
|
+
* `error` member is a protocol-level failure and is RETURNED, not thrown:
|
|
853
|
+
* only a non-2xx HTTP status throws, because an unknown tool is an answer
|
|
854
|
+
* and a revoked credential is not.
|
|
855
|
+
*
|
|
856
|
+
* The request is never retried: `tools/call` can have an effect, and the
|
|
857
|
+
* endpoint honours no `Idempotency-Key`.
|
|
858
|
+
*
|
|
859
|
+
* @example
|
|
860
|
+
* const listed = await client.mcp({ method: "tools/list", id: 1 });
|
|
861
|
+
* listed.response?.result;
|
|
559
862
|
*/
|
|
560
|
-
async
|
|
863
|
+
async mcp(request, options = {}) {
|
|
864
|
+
const operation = this.resolveOperation("createMcpJsonRpcResponse", "jsonrpc");
|
|
865
|
+
const { sessionId, protocolVersion, ...transportOptions } = options;
|
|
866
|
+
const headers = { ...options.headers };
|
|
867
|
+
if (sessionId !== undefined) {
|
|
868
|
+
headers["mcp-session-id"] = sessionId;
|
|
869
|
+
}
|
|
870
|
+
if (protocolVersion !== undefined) {
|
|
871
|
+
headers["mcp-protocol-version"] = protocolVersion;
|
|
872
|
+
}
|
|
873
|
+
const body = { jsonrpc: "2.0", ...request };
|
|
874
|
+
return this.request(operation, { ...transportOptions, headers, body, maxRetries: 0 }, async (response) => {
|
|
875
|
+
const sessionId = response.headers.get("mcp-session-id");
|
|
876
|
+
const result = {
|
|
877
|
+
status: response.status === 202 ? 202 : 200,
|
|
878
|
+
response: null,
|
|
879
|
+
...(sessionId ? { sessionId } : {}),
|
|
880
|
+
};
|
|
881
|
+
if (response.status === 202)
|
|
882
|
+
return result;
|
|
883
|
+
const parsed = await parseJson(response);
|
|
884
|
+
if (!isMcpJsonRpcResponse(parsed)) {
|
|
885
|
+
throw new InvalidResponseError(response.status, "POST /api/v1/mcp returned a body that is not a JSON-RPC 2.0 response: expected an object with jsonrpc \"2.0\" and an id.", parsed);
|
|
886
|
+
}
|
|
887
|
+
result.response = parsed;
|
|
888
|
+
return result;
|
|
889
|
+
});
|
|
890
|
+
}
|
|
891
|
+
/**
|
|
892
|
+
* Resolve the presigned object URL behind `GET /api/v1/trader/{address}/export/download`
|
|
893
|
+
* without downloading anything (#16137).
|
|
894
|
+
*
|
|
895
|
+
* The route answers `302` with a `Location`, which `call()` treats as an
|
|
896
|
+
* error. This reads the redirect target and stops there, so a caller can
|
|
897
|
+
* hand the URL to a download manager or a browser. The returned URL is a
|
|
898
|
+
* bearer credential in itself: anyone holding it can read the file until it
|
|
899
|
+
* expires, so do not log it.
|
|
900
|
+
*
|
|
901
|
+
* Browser caveat: `fetch` with `redirect: "manual"` yields an opaque
|
|
902
|
+
* redirect whose `Location` no script can read. The method says so rather
|
|
903
|
+
* than guessing a URL; run the download from a server runtime.
|
|
904
|
+
*/
|
|
905
|
+
async getTraderExportDownloadUrl(address, jobId, options = {}) {
|
|
906
|
+
const operation = exportDownloadOperation;
|
|
907
|
+
if (!this.apiKey && !this.sandbox) {
|
|
908
|
+
throw new Error("downloadTraderExport requires an API key (oxi_sk_*)");
|
|
909
|
+
}
|
|
910
|
+
return this.request(operation, { ...options, path: { address }, query: { job_id: jobId } }, async (response) => {
|
|
911
|
+
if (response.type === "opaqueredirect" || response.status === 0) {
|
|
912
|
+
throw new InvalidResponseError(302, "This runtime hides redirect targets: fetch with redirect: \"manual\" returned an opaque redirect, so the presigned download URL cannot be read. Run the export download from a server runtime (Node 18+, Deno, Bun, a Worker).", null);
|
|
913
|
+
}
|
|
914
|
+
const location = response.headers.get("location");
|
|
915
|
+
if (!location) {
|
|
916
|
+
throw new InvalidResponseError(response.status, "GET /api/v1/trader/{address}/export/download answered a redirect with no Location header.", null);
|
|
917
|
+
}
|
|
918
|
+
return presignedTarget(location);
|
|
919
|
+
}, {
|
|
920
|
+
// The 302 IS the success here, so it must not enter the error path.
|
|
921
|
+
isSuccess: (response) => response.status === 302 || response.type === "opaqueredirect",
|
|
922
|
+
redirect: "manual",
|
|
923
|
+
});
|
|
924
|
+
}
|
|
925
|
+
/**
|
|
926
|
+
* Download a finished export (#16137): resolve the `302`, then fetch the
|
|
927
|
+
* object store directly.
|
|
928
|
+
*
|
|
929
|
+
* The second request carries NO headers at all, so the live API key never
|
|
930
|
+
* reaches the object store; the presigned URL is its own credential. The
|
|
931
|
+
* body is not buffered -- read `response.body` as a stream and write it
|
|
932
|
+
* where it belongs.
|
|
933
|
+
*
|
|
934
|
+
* There is no default deadline on the object fetch, the way `streamFeed`
|
|
935
|
+
* has none: a multi-gigabyte export would fail the 15-second REST default
|
|
936
|
+
* halfway through. Pass `signal` to cancel it, or `downloadTimeoutMs` for
|
|
937
|
+
* a deadline of your own. `timeoutMs` still bounds the redirect request.
|
|
938
|
+
*
|
|
939
|
+
* @example
|
|
940
|
+
* const { response, filename } = await client.downloadTraderExport(address, jobId);
|
|
941
|
+
* await pipeline(Readable.fromWeb(response.body), createWriteStream(filename ?? "export.json"));
|
|
942
|
+
*/
|
|
943
|
+
async downloadTraderExport(address, jobId, options = {}) {
|
|
944
|
+
const target = await this.getTraderExportDownloadUrl(address, jobId, options);
|
|
945
|
+
const downloadSignal = composeRequestSignal(options.downloadTimeoutMs ?? null, options.signal);
|
|
946
|
+
let response;
|
|
947
|
+
try {
|
|
948
|
+
// No headers, and no credentials: the presigned URL authorizes itself,
|
|
949
|
+
// and forwarding the bearer would hand the live API key to the object
|
|
950
|
+
// store on every download.
|
|
951
|
+
response = await this.fetchImpl(target.url, {
|
|
952
|
+
method: "GET",
|
|
953
|
+
signal: downloadSignal.signal,
|
|
954
|
+
});
|
|
955
|
+
}
|
|
956
|
+
finally {
|
|
957
|
+
downloadSignal.dispose?.();
|
|
958
|
+
}
|
|
959
|
+
if (!response.ok) {
|
|
960
|
+
throw new InvalidResponseError(response.status, `The presigned export URL answered ${response.status}. A presigned link is valid for about an hour; request a fresh one with getTraderExportDownloadUrl.`, null);
|
|
961
|
+
}
|
|
962
|
+
const contentLength = response.headers.get("content-length");
|
|
963
|
+
return {
|
|
964
|
+
...target,
|
|
965
|
+
response,
|
|
966
|
+
contentLength: contentLength === null ? null : Number.parseInt(contentLength, 10),
|
|
967
|
+
contentType: response.headers.get("content-type"),
|
|
968
|
+
filename: filenameFromDisposition(response.headers.get("content-disposition")),
|
|
969
|
+
};
|
|
970
|
+
}
|
|
971
|
+
/**
|
|
972
|
+
* Look up an operation and refuse it when the caller reached for the wrong
|
|
973
|
+
* reader (#16137): `call()` on a Markdown route used to fail as a bad 200.
|
|
974
|
+
*/
|
|
975
|
+
resolveOperation(operationId, expected) {
|
|
561
976
|
const operation = operationsById.get(operationId);
|
|
562
977
|
if (!operation) {
|
|
563
978
|
throw new Error(`Unknown 0xinsider API operation: ${operationId}`);
|
|
564
979
|
}
|
|
565
|
-
|
|
980
|
+
const kind = operation.kind ?? "envelope";
|
|
981
|
+
if (kind !== expected) {
|
|
982
|
+
throw new Error(`${operationId} answers a "${kind}" body, not "${expected}": use ${READER_FOR_KIND[kind]}.`);
|
|
983
|
+
}
|
|
984
|
+
// The sandbox answers every operation without a credential; production
|
|
985
|
+
// does not, and the local check saves a round trip that can only be 401.
|
|
986
|
+
if (operation.auth === "bearer" && !this.apiKey && !this.sandbox) {
|
|
566
987
|
throw new Error(`${operationId} requires an API key (oxi_sk_*)`);
|
|
567
988
|
}
|
|
989
|
+
return operation;
|
|
990
|
+
}
|
|
991
|
+
/**
|
|
992
|
+
* One request pipeline for every transport: URL and headers, the retry
|
|
993
|
+
* policy, one deadline shared by every attempt, the typed error hierarchy,
|
|
994
|
+
* and cancellation held live through body consumption. `consume` runs
|
|
995
|
+
* inside that window, so a slow body is still covered by the deadline and
|
|
996
|
+
* by the caller's `signal`.
|
|
997
|
+
*/
|
|
998
|
+
async request(operation, options, consume, transport = {}) {
|
|
999
|
+
const operationId = operation.operationId;
|
|
568
1000
|
const timeoutMs = options.timeoutMs === undefined ? this.timeoutMs : options.timeoutMs;
|
|
569
1001
|
const maxRetries = options.maxRetries === undefined
|
|
570
1002
|
? this.maxRetries
|
|
571
1003
|
: assertRetryCount(options.maxRetries, "maxRetries");
|
|
572
1004
|
const url = this.buildUrl(operation, options);
|
|
573
|
-
const headers = this.buildHeaders(operation, options);
|
|
1005
|
+
const headers = this.buildHeaders(operation, options, transport.accept);
|
|
574
1006
|
const body = options.body === undefined ? undefined : JSON.stringify(options.body);
|
|
1007
|
+
const isSuccess = transport.isSuccess ??
|
|
1008
|
+
((response) => response.ok || response.status === 304);
|
|
1009
|
+
// A key the server does not read is a false promise of safety: the
|
|
1010
|
+
// request would be retried as though replayable while the route repeats
|
|
1011
|
+
// its effect (#16182). Refuse it here, before anything is sent.
|
|
1012
|
+
const eligibility = retryEligibility(operation);
|
|
1013
|
+
const keyed = headers.has("idempotency-key");
|
|
1014
|
+
if (keyed && eligibility !== "keyed") {
|
|
1015
|
+
throw new Error(`${operationId} does not honour Idempotency-Key; the API replays only ${IDEMPOTENT_WRITE_OPERATIONS.join(", ")}. Remove idempotencyKey: a retry of this request could repeat its side effect.`);
|
|
1016
|
+
}
|
|
575
1017
|
// Replaying a write without a key could repeat it (a second webhook
|
|
576
|
-
// endpoint, say), so only a
|
|
577
|
-
|
|
1018
|
+
// endpoint, say), so only a read or a keyed write is retried (#14281).
|
|
1019
|
+
// `headers` and `body` are built once, above, so every attempt carries
|
|
1020
|
+
// the same key and the same bytes.
|
|
1021
|
+
const retryable = eligibility === "read" || (eligibility === "keyed" && keyed);
|
|
578
1022
|
// One deadline for every attempt: a retry that cannot finish before it is
|
|
579
1023
|
// not started, so the caller sees the real failure, not a timeout.
|
|
580
1024
|
const deadlineAt = timeoutMs === null ? null : Date.now() + timeoutMs;
|
|
@@ -598,6 +1042,7 @@ export class OxinsiderApiClient {
|
|
|
598
1042
|
headers,
|
|
599
1043
|
body,
|
|
600
1044
|
signal: requestSignal.signal,
|
|
1045
|
+
...(transport.redirect ? { redirect: transport.redirect } : {}),
|
|
601
1046
|
});
|
|
602
1047
|
}
|
|
603
1048
|
catch (error) {
|
|
@@ -610,7 +1055,7 @@ export class OxinsiderApiClient {
|
|
|
610
1055
|
await sleepUnlessAborted(delay, requestSignal.signal);
|
|
611
1056
|
continue;
|
|
612
1057
|
}
|
|
613
|
-
if (response
|
|
1058
|
+
if (isSuccess(response))
|
|
614
1059
|
break;
|
|
615
1060
|
const errorBody = await parseJson(response);
|
|
616
1061
|
const retryAfter = retryAfterSeconds(response);
|
|
@@ -622,22 +1067,7 @@ export class OxinsiderApiClient {
|
|
|
622
1067
|
}
|
|
623
1068
|
await sleepUnlessAborted(delay, requestSignal.signal);
|
|
624
1069
|
}
|
|
625
|
-
|
|
626
|
-
return notModifiedResponse(response);
|
|
627
|
-
}
|
|
628
|
-
const envelope = await parseJson(response);
|
|
629
|
-
if (isApiEnvelope(envelope)) {
|
|
630
|
-
const etag = response.headers.get("etag");
|
|
631
|
-
// Only ADD to a meta the server actually sent. The contract makes
|
|
632
|
-
// `meta` required on every envelope, so synthesizing one from `?? {}`
|
|
633
|
-
// would hand a caller a `ResponseMeta` missing its required fields --
|
|
634
|
-
// which is what the hand-written type hid before #14278.
|
|
635
|
-
if (etag && envelope.meta) {
|
|
636
|
-
envelope.meta = { ...envelope.meta, etag };
|
|
637
|
-
}
|
|
638
|
-
return envelope;
|
|
639
|
-
}
|
|
640
|
-
throw new OxinsiderApiError(response.status, envelope);
|
|
1070
|
+
return await consume(response);
|
|
641
1071
|
}
|
|
642
1072
|
catch (error) {
|
|
643
1073
|
if (timeoutMs !== null &&
|
|
@@ -655,66 +1085,113 @@ export class OxinsiderApiClient {
|
|
|
655
1085
|
requestSignal.dispose?.();
|
|
656
1086
|
}
|
|
657
1087
|
}
|
|
658
|
-
/**
|
|
659
|
-
* Execute a Stripe-style list operation and return the list envelope.
|
|
660
|
-
* Use `paginate()` (in `pagination.ts`) to auto-follow `next_cursor`.
|
|
661
|
-
*/
|
|
662
1088
|
async list(operationId, options = {}) {
|
|
663
1089
|
const result = await this.call(operationId, options);
|
|
664
1090
|
if (isNotModified(result)) {
|
|
665
1091
|
throw new OxinsiderApiError(304, "list() received a 304 not_modified; use call() if you send If-None-Match on a list endpoint");
|
|
666
1092
|
}
|
|
667
1093
|
if (!isListEnvelope(result)) {
|
|
668
|
-
throw new
|
|
1094
|
+
throw new InvalidResponseError(200, `Operation ${operationId} did not return a list envelope: expected object "list", an array data, a boolean has_more and a string or absent next_cursor`, result);
|
|
669
1095
|
}
|
|
670
1096
|
return result;
|
|
671
1097
|
}
|
|
672
1098
|
// --- Typed convenience methods (key surfaces) ---
|
|
1099
|
+
//
|
|
1100
|
+
// Each takes the route's path parameters positionally, its documented
|
|
1101
|
+
// query as `params` (list reads) or `options.query` (single reads), and
|
|
1102
|
+
// the shared transport options: `signal`, `timeoutMs`, `maxRetries`,
|
|
1103
|
+
// `headers` (`If-None-Match` for a conditional read) and `idempotencyKey`
|
|
1104
|
+
// on a keyed write. The result is the operation's own envelope; no method
|
|
1105
|
+
// takes a type argument any more (#16136).
|
|
673
1106
|
getTrader(address, options = {}) {
|
|
674
1107
|
return this.call("getTrader", { ...options, path: { address } });
|
|
675
1108
|
}
|
|
676
1109
|
getTraderPnl(address, options = {}) {
|
|
677
1110
|
return this.call("getTraderPnl", { ...options, path: { address } });
|
|
678
1111
|
}
|
|
679
|
-
|
|
680
|
-
|
|
1112
|
+
/**
|
|
1113
|
+
* Fetch one wallet's win record per canonical category.
|
|
1114
|
+
*
|
|
1115
|
+
* Counts every settled market at any position size, so it can differ from
|
|
1116
|
+
* `category_strengths` on `getTrader`, which reads the floored calibration
|
|
1117
|
+
* sample. A category under `min_decided_for_win_rate` keeps its `wins` and
|
|
1118
|
+
* `decided` with `win_rate: null` and `status: "not_enough_data"`; a category
|
|
1119
|
+
* the wallet has no settled market in is absent, which means no record rather
|
|
1120
|
+
* than a 0% record. The `Esports` record carries `games`: the wallet's record
|
|
1121
|
+
* per esports title (`LoL`, `CS2`, `Dota 2`, ...) under the same rule, named
|
|
1122
|
+
* as the holder chips name them in `category_win_rate_game`. Pass
|
|
1123
|
+
* `params.category` to filter to one bucket; a filter that reaches Esports
|
|
1124
|
+
* returns its games too.
|
|
1125
|
+
*/
|
|
1126
|
+
getTraderCategoryRecords(address, params = {}, options = {}) {
|
|
1127
|
+
return this.call("getTraderCategoryRecords", {
|
|
681
1128
|
...options,
|
|
682
|
-
|
|
1129
|
+
path: { address },
|
|
1130
|
+
query: params,
|
|
683
1131
|
});
|
|
684
1132
|
}
|
|
685
|
-
|
|
686
|
-
|
|
1133
|
+
/**
|
|
1134
|
+
* Resolve 1 to 25 traders in one request (`POST /api/v1/traders/batch`).
|
|
1135
|
+
*
|
|
1136
|
+
* `traders` is sent as the request body's `traders` array, the field the
|
|
1137
|
+
* route requires: wallet addresses, usernames, `trd_` ids or integer trader
|
|
1138
|
+
* ids, resolved in input order. Until #16135 this method sent the array as
|
|
1139
|
+
* `identifiers`, a field the route does not declare, so every call was
|
|
1140
|
+
* refused for a missing `traders`. `options.expand` is sent as the body's
|
|
1141
|
+
* `expand` array and applies to every item.
|
|
1142
|
+
*
|
|
1143
|
+
* The response `data` keeps request order and one row per input, duplicates
|
|
1144
|
+
* included; a row is `status: "ok"` with `data`, or `status: "error"` with
|
|
1145
|
+
* the item's own `error` (an unknown identity is a per-item `not_found`, not
|
|
1146
|
+
* a request failure). `meta` is the batch's `BatchResponseMeta`:
|
|
1147
|
+
* `request_cost` and `rate_limit` follow the batch item quota, not the
|
|
1148
|
+
* per-request one.
|
|
1149
|
+
*/
|
|
1150
|
+
batchGetTraders(traders, options = {}) {
|
|
1151
|
+
const { expand, ...request } = options;
|
|
1152
|
+
return this.call("batchGetTraders", {
|
|
1153
|
+
...request,
|
|
1154
|
+
body: expand === undefined ? { traders } : { traders, expand },
|
|
1155
|
+
});
|
|
687
1156
|
}
|
|
688
|
-
|
|
689
|
-
|
|
1157
|
+
/**
|
|
1158
|
+
* Resolve up to 25 markets' intel in one request
|
|
1159
|
+
* (`POST /api/v1/markets/intel/batch`); `meta` is the batch's own.
|
|
1160
|
+
*/
|
|
1161
|
+
batchGetMarketIntel(body, options = {}) {
|
|
1162
|
+
return this.call("batchGetMarketIntel", { ...options, body });
|
|
1163
|
+
}
|
|
1164
|
+
listLeaderboard(params = {}, options = {}) {
|
|
1165
|
+
return this.list("listLeaderboard", { ...options, query: params });
|
|
1166
|
+
}
|
|
1167
|
+
listTrendingWallets(params = {}, options = {}) {
|
|
1168
|
+
return this.list("listTrendingWallets", { ...options, query: params });
|
|
690
1169
|
}
|
|
691
|
-
listWhaleTrades(params = {}) {
|
|
692
|
-
return this.list("listWhaleTrades", { query: params });
|
|
1170
|
+
listWhaleTrades(params = {}, options = {}) {
|
|
1171
|
+
return this.list("listWhaleTrades", { ...options, query: params });
|
|
693
1172
|
}
|
|
694
|
-
listWhaleTradeHistory(params = {}) {
|
|
695
|
-
return this.list("listWhaleTradeHistory", { query: params });
|
|
1173
|
+
listWhaleTradeHistory(params = {}, options = {}) {
|
|
1174
|
+
return this.list("listWhaleTradeHistory", { ...options, query: params });
|
|
696
1175
|
}
|
|
697
|
-
listPositions(params = {}) {
|
|
698
|
-
return this.list("listPositions", { query: params });
|
|
1176
|
+
listPositions(params = {}, options = {}) {
|
|
1177
|
+
return this.list("listPositions", { ...options, query: params });
|
|
699
1178
|
}
|
|
700
|
-
listLargePositions(params = {}) {
|
|
701
|
-
return this.list("listLargePositions", { query: params });
|
|
1179
|
+
listLargePositions(params = {}, options = {}) {
|
|
1180
|
+
return this.list("listLargePositions", { ...options, query: params });
|
|
702
1181
|
}
|
|
703
|
-
listSmartMoneyFlows(params = {}) {
|
|
704
|
-
return this.list("listSmartMoneyFlows", { query: params });
|
|
1182
|
+
listSmartMoneyFlows(params = {}, options = {}) {
|
|
1183
|
+
return this.list("listSmartMoneyFlows", { ...options, query: params });
|
|
705
1184
|
}
|
|
706
1185
|
/** Canonical alias of {@link listSmartMoneyFlows} (epic #6912). */
|
|
707
|
-
listSharpMoneyFlows(params = {}) {
|
|
708
|
-
return this.list("listSharpMoneyFlows", { query: params });
|
|
1186
|
+
listSharpMoneyFlows(params = {}, options = {}) {
|
|
1187
|
+
return this.list("listSharpMoneyFlows", { ...options, query: params });
|
|
709
1188
|
}
|
|
710
1189
|
/**
|
|
711
1190
|
* List the funded-primary sports signals with additive, shadow-only category
|
|
712
1191
|
* evidence. `category_skill` never changes membership, ordering, or sizing.
|
|
713
1192
|
*/
|
|
714
|
-
listSportsEdgeSignals(params = {}) {
|
|
715
|
-
return this.list("listSportsEdgeSignals", {
|
|
716
|
-
query: params,
|
|
717
|
-
});
|
|
1193
|
+
listSportsEdgeSignals(params = {}, options = {}) {
|
|
1194
|
+
return this.list("listSportsEdgeSignals", { ...options, query: params });
|
|
718
1195
|
}
|
|
719
1196
|
/**
|
|
720
1197
|
* Read an explicitly observation-only sports cohort and its accountable
|
|
@@ -726,7 +1203,10 @@ export class OxinsiderApiClient {
|
|
|
726
1203
|
* denominator view.
|
|
727
1204
|
*/
|
|
728
1205
|
async listSportsEdgeObservations(params, options = {}) {
|
|
729
|
-
const response = await this.list("listSportsEdgeObservations", {
|
|
1206
|
+
const response = await this.list("listSportsEdgeObservations", {
|
|
1207
|
+
...options,
|
|
1208
|
+
query: params,
|
|
1209
|
+
});
|
|
730
1210
|
return sportsEdgeObservationsResponse(response);
|
|
731
1211
|
}
|
|
732
1212
|
/**
|
|
@@ -739,23 +1219,25 @@ export class OxinsiderApiClient {
|
|
|
739
1219
|
* position remains covered.
|
|
740
1220
|
*/
|
|
741
1221
|
async listSportsEdgeObservationsConditional(params, options = {}) {
|
|
742
|
-
const response = await this.call("listSportsEdgeObservations", {
|
|
1222
|
+
const response = await this.call("listSportsEdgeObservations", {
|
|
1223
|
+
...options,
|
|
1224
|
+
query: params,
|
|
1225
|
+
});
|
|
743
1226
|
if (isNotModified(response)) {
|
|
744
1227
|
return response;
|
|
745
1228
|
}
|
|
746
|
-
if (!isListEnvelope(response)) {
|
|
747
|
-
throw new OxinsiderApiError(200, "listSportsEdgeObservations did not return a list envelope");
|
|
748
|
-
}
|
|
749
1229
|
return sportsEdgeObservationsResponse(response);
|
|
750
1230
|
}
|
|
751
1231
|
searchMarkets(q, options = {}) {
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
query: { q, ...options.query },
|
|
755
|
-
});
|
|
1232
|
+
const { query, ...request } = options;
|
|
1233
|
+
return this.list("searchMarkets", { ...request, query: { ...query, q } });
|
|
756
1234
|
}
|
|
757
|
-
|
|
758
|
-
|
|
1235
|
+
searchContent(q, options = {}) {
|
|
1236
|
+
const { query, ...request } = options;
|
|
1237
|
+
return this.list("searchContent", { ...request, query: { ...query, q } });
|
|
1238
|
+
}
|
|
1239
|
+
exploreMarkets(params = {}, options = {}) {
|
|
1240
|
+
return this.list("exploreMarkets", { ...options, query: params });
|
|
759
1241
|
}
|
|
760
1242
|
getMarketIntel(conditionId, options = {}) {
|
|
761
1243
|
return this.call("getMarketIntel", {
|
|
@@ -769,37 +1251,71 @@ export class OxinsiderApiClient {
|
|
|
769
1251
|
path: { condition_id: conditionId },
|
|
770
1252
|
});
|
|
771
1253
|
}
|
|
1254
|
+
/**
|
|
1255
|
+
* One page of a market's graded (S/A/B) holder roster from a complete
|
|
1256
|
+
* provider holder scan: the list a Pick of the Day shows, for any market.
|
|
1257
|
+
* `params.outcome` (`yes` | `no` | `all`), `params.min_grade` (`S` | `A` |
|
|
1258
|
+
* `B`), `params.limit` and `params.cursor` (`mh_` prefix). The page carries
|
|
1259
|
+
* the `market`, `scan` and roster `totals` beside `data`; `total` is the
|
|
1260
|
+
* count matching the filters across every page.
|
|
1261
|
+
*/
|
|
1262
|
+
getMarketHolders(conditionId, params = {}, options = {}) {
|
|
1263
|
+
return this.list("getMarketHolders", {
|
|
1264
|
+
...options,
|
|
1265
|
+
path: { condition_id: conditionId },
|
|
1266
|
+
query: params,
|
|
1267
|
+
});
|
|
1268
|
+
}
|
|
772
1269
|
/**
|
|
773
1270
|
* Fetch bucketed OHLC candles for a market's outcome tokens.
|
|
774
|
-
* Resolution defaults to the server default; pass `
|
|
775
|
-
* `"1d"` or `"1w"` to override.
|
|
1271
|
+
* Resolution defaults to the server default; pass `params.resolution` as
|
|
1272
|
+
* `"1d"` or `"1w"` to override. `params.from` is exclusive and `params.to`
|
|
1273
|
+
* is inclusive; when both are present, `from` must be less than or equal to
|
|
1274
|
+
* `to`.
|
|
776
1275
|
*/
|
|
777
|
-
getMarketCandles(conditionId, options = {}) {
|
|
1276
|
+
getMarketCandles(conditionId, params = {}, options = {}) {
|
|
778
1277
|
return this.call("getMarketCandles", {
|
|
779
1278
|
...options,
|
|
780
1279
|
path: { condition_id: conditionId },
|
|
1280
|
+
query: params,
|
|
781
1281
|
});
|
|
782
1282
|
}
|
|
783
1283
|
getInsiderRadarFlag(id, options = {}) {
|
|
784
1284
|
return this.call("getInsiderRadarFlag", { ...options, path: { id } });
|
|
785
1285
|
}
|
|
786
|
-
listInsiderRadar(params = {}) {
|
|
787
|
-
return this.list("listInsiderRadar", { query: params });
|
|
1286
|
+
listInsiderRadar(params = {}, options = {}) {
|
|
1287
|
+
return this.list("listInsiderRadar", { ...options, query: params });
|
|
1288
|
+
}
|
|
1289
|
+
getWhaleTrade(id, options = {}) {
|
|
1290
|
+
return this.call("getWhaleTrade", { ...options, path: { id } });
|
|
788
1291
|
}
|
|
789
1292
|
// --- Webhooks ---
|
|
790
1293
|
listWebhooks(options = {}) {
|
|
791
|
-
return this.
|
|
1294
|
+
return this.list("listWebhooks", options);
|
|
1295
|
+
}
|
|
1296
|
+
getWebhook(id, options = {}) {
|
|
1297
|
+
return this.call("getWebhook", { ...options, path: { id } });
|
|
1298
|
+
}
|
|
1299
|
+
createWebhook(body, options = {}) {
|
|
1300
|
+
return this.call("createWebhook", { ...options, body });
|
|
1301
|
+
}
|
|
1302
|
+
updateWebhook(id, body, options = {}) {
|
|
1303
|
+
return this.call("updateWebhook", { ...options, path: { id }, body });
|
|
1304
|
+
}
|
|
1305
|
+
deleteWebhook(id, options = {}) {
|
|
1306
|
+
return this.call("deleteWebhook", { ...options, path: { id } });
|
|
792
1307
|
}
|
|
793
1308
|
/**
|
|
794
1309
|
* Fetch the self-describing webhook event catalog (each entry's `id`,
|
|
795
1310
|
* description, payload shape, and active/dormant status).
|
|
796
1311
|
*/
|
|
797
1312
|
listWebhookEvents(options = {}) {
|
|
798
|
-
return this.
|
|
1313
|
+
return this.list("listWebhookEvents", options);
|
|
799
1314
|
}
|
|
800
1315
|
/** List delivery attempts for one webhook endpoint (Stripe-style list). */
|
|
801
|
-
listWebhookDeliveries(webhookId, params = {}) {
|
|
1316
|
+
listWebhookDeliveries(webhookId, params = {}, options = {}) {
|
|
802
1317
|
return this.list("listWebhookDeliveries", {
|
|
1318
|
+
...options,
|
|
803
1319
|
path: { id: webhookId },
|
|
804
1320
|
query: params,
|
|
805
1321
|
});
|
|
@@ -825,9 +1341,44 @@ export class OxinsiderApiClient {
|
|
|
825
1341
|
getApiDiscovery(options = {}) {
|
|
826
1342
|
return this.call("getApiDiscovery", options);
|
|
827
1343
|
}
|
|
1344
|
+
getPlatforms(options = {}) {
|
|
1345
|
+
return this.call("getPlatforms", options);
|
|
1346
|
+
}
|
|
828
1347
|
getAccountIdentity(options = {}) {
|
|
829
1348
|
return this.call("getAccountIdentity", options);
|
|
830
1349
|
}
|
|
1350
|
+
/**
|
|
1351
|
+
* One wallet's context as Markdown, ready to paste into a model prompt
|
|
1352
|
+
* (`GET /api/v1/trader/{address}/context.md`). The JSON form of the same
|
|
1353
|
+
* read is `getTraderContext`.
|
|
1354
|
+
*/
|
|
1355
|
+
getTraderContextMarkdown(address, options = {}) {
|
|
1356
|
+
return this.text("getTraderContextMarkdown", { ...options, path: { address } });
|
|
1357
|
+
}
|
|
1358
|
+
/**
|
|
1359
|
+
* One market's context as Markdown
|
|
1360
|
+
* (`GET /api/v1/market/{condition_id}/context.md`). The JSON form is
|
|
1361
|
+
* `getMarketIntel`.
|
|
1362
|
+
*/
|
|
1363
|
+
getMarketContextMarkdown(conditionId, options = {}) {
|
|
1364
|
+
return this.text("getMarketContextMarkdown", {
|
|
1365
|
+
...options,
|
|
1366
|
+
path: { condition_id: conditionId },
|
|
1367
|
+
});
|
|
1368
|
+
}
|
|
1369
|
+
/**
|
|
1370
|
+
* Mint a sandbox key (`POST /api/v1/agents/register`), the one write that
|
|
1371
|
+
* needs no credential. Answers `201`, which is why the SDK had no method
|
|
1372
|
+
* for it until #16137: the drift gate counted only operations with a
|
|
1373
|
+
* documented `200`. `meta.status` is `201`; `data.api_key` is the
|
|
1374
|
+
* `oxi_sk_test_*` key to pass to `OxinsiderApiClient.sandbox()`.
|
|
1375
|
+
*
|
|
1376
|
+
* Nothing is stored: the key cannot be listed or revoked and does not
|
|
1377
|
+
* expire. Register again for another one.
|
|
1378
|
+
*/
|
|
1379
|
+
registerAgent(options = {}) {
|
|
1380
|
+
return this.call("registerAgent", options);
|
|
1381
|
+
}
|
|
831
1382
|
getUsage(options = {}) {
|
|
832
1383
|
return this.call("getUsage", options);
|
|
833
1384
|
}
|
|
@@ -862,12 +1413,16 @@ export class OxinsiderApiClient {
|
|
|
862
1413
|
getApiKey() {
|
|
863
1414
|
return this.apiKey;
|
|
864
1415
|
}
|
|
1416
|
+
/** Whether this client was built in sandbox mode (#16138). */
|
|
1417
|
+
isSandbox() {
|
|
1418
|
+
return this.sandbox;
|
|
1419
|
+
}
|
|
865
1420
|
/** The resolved fetch implementation (used by the SSE stream consumer). */
|
|
866
1421
|
getFetch() {
|
|
867
1422
|
return this.fetchImpl;
|
|
868
1423
|
}
|
|
869
1424
|
buildUrl(operation, options) {
|
|
870
|
-
const url =
|
|
1425
|
+
const url = resolveApiUrl(this.baseUrl, interpolatePath(operation.path, options.path ?? {}));
|
|
871
1426
|
for (const [key, value] of Object.entries(options.query ?? {})) {
|
|
872
1427
|
if (value === null || value === undefined) {
|
|
873
1428
|
continue;
|
|
@@ -883,9 +1438,9 @@ export class OxinsiderApiClient {
|
|
|
883
1438
|
}
|
|
884
1439
|
return url.toString();
|
|
885
1440
|
}
|
|
886
|
-
buildHeaders(operation, options) {
|
|
1441
|
+
buildHeaders(operation, options, accept = "application/json") {
|
|
887
1442
|
const headers = new Headers(options.headers);
|
|
888
|
-
headers.set("accept",
|
|
1443
|
+
headers.set("accept", accept);
|
|
889
1444
|
if (options.body !== undefined && !headers.has("content-type")) {
|
|
890
1445
|
headers.set("content-type", "application/json");
|
|
891
1446
|
}
|
|
@@ -895,9 +1450,18 @@ export class OxinsiderApiClient {
|
|
|
895
1450
|
if (options.idempotencyKey !== undefined) {
|
|
896
1451
|
headers.set("idempotency-key", options.idempotencyKey);
|
|
897
1452
|
}
|
|
1453
|
+
if (options.strictQuery) {
|
|
1454
|
+
headers.set("x-query-validation", "strict");
|
|
1455
|
+
}
|
|
898
1456
|
return headers;
|
|
899
1457
|
}
|
|
900
1458
|
}
|
|
1459
|
+
/**
|
|
1460
|
+
* Narrow a `call()` result to the typed 304. Takes any envelope-shaped
|
|
1461
|
+
* value, so it works on an `OperationResult<K>` (whose `meta` is the
|
|
1462
|
+
* operation's own type) as well as the loose `ApiClientResponse<T>`
|
|
1463
|
+
* (#16136).
|
|
1464
|
+
*/
|
|
901
1465
|
export function isApiNotModifiedResponse(response) {
|
|
902
1466
|
return response.object === "not_modified";
|
|
903
1467
|
}
|
|
@@ -925,13 +1489,6 @@ async function parseJson(response) {
|
|
|
925
1489
|
return text;
|
|
926
1490
|
}
|
|
927
1491
|
}
|
|
928
|
-
function retryAfterSeconds(response) {
|
|
929
|
-
const header = response.headers.get("retry-after");
|
|
930
|
-
if (!header)
|
|
931
|
-
return null;
|
|
932
|
-
const seconds = Number(header);
|
|
933
|
-
return Number.isFinite(seconds) ? seconds : null;
|
|
934
|
-
}
|
|
935
1492
|
function notModifiedResponse(response) {
|
|
936
1493
|
const meta = { status: 304 };
|
|
937
1494
|
const etag = response.headers.get("etag");
|
|
@@ -942,6 +1499,14 @@ function notModifiedResponse(response) {
|
|
|
942
1499
|
if (requestId) {
|
|
943
1500
|
meta.request_id = requestId;
|
|
944
1501
|
}
|
|
1502
|
+
const queryIgnored = response.headers.get("x-query-ignored");
|
|
1503
|
+
if (queryIgnored) {
|
|
1504
|
+
meta.queryIgnored = queryIgnored;
|
|
1505
|
+
}
|
|
1506
|
+
const effectiveQuery = response.headers.get("x-effective-query");
|
|
1507
|
+
if (effectiveQuery) {
|
|
1508
|
+
meta.effectiveQuery = effectiveQuery;
|
|
1509
|
+
}
|
|
945
1510
|
return { object: "not_modified", data: null, meta };
|
|
946
1511
|
}
|
|
947
1512
|
function isApiEnvelope(body) {
|
|
@@ -950,14 +1515,23 @@ function isApiEnvelope(body) {
|
|
|
950
1515
|
"object" in body &&
|
|
951
1516
|
"data" in body);
|
|
952
1517
|
}
|
|
1518
|
+
/**
|
|
1519
|
+
* Whether `body` is a list envelope whose control fields the walker can act
|
|
1520
|
+
* on: `object: "list"`, an array `data`, a BOOLEAN `has_more`, and a
|
|
1521
|
+
* `next_cursor` that is a string, `null` or absent (#16246: the check used to
|
|
1522
|
+
* accept any `has_more` that was merely present, so a malformed value could
|
|
1523
|
+
* be coerced into "no more pages" or "more pages" by truthiness).
|
|
1524
|
+
*/
|
|
953
1525
|
export function isListEnvelope(body) {
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
1526
|
+
if (typeof body !== "object" || body === null)
|
|
1527
|
+
return false;
|
|
1528
|
+
const candidate = body;
|
|
1529
|
+
return (candidate.object === "list" &&
|
|
1530
|
+
Array.isArray(candidate.data) &&
|
|
1531
|
+
typeof candidate.has_more === "boolean" &&
|
|
1532
|
+
(candidate.next_cursor === undefined ||
|
|
1533
|
+
candidate.next_cursor === null ||
|
|
1534
|
+
typeof candidate.next_cursor === "string"));
|
|
961
1535
|
}
|
|
962
1536
|
function sportsEdgeObservationsResponse(response) {
|
|
963
1537
|
const candidate = response;
|