@0xinsider/mcp 2.0.1 → 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 +27 -7
- package/dist/catalog.generated.d.ts +4 -0
- package/dist/catalog.generated.d.ts.map +1 -1
- package/dist/catalog.generated.js +5 -0
- package/dist/catalog.generated.js.map +1 -1
- 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 +36 -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 +705 -131
- package/dist/sdk/client.d.ts.map +1 -1
- package/dist/sdk/client.js +719 -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 +1536 -430
- 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/server.d.ts.map +1 -1
- package/dist/server.js +2 -1
- package/dist/server.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 +1 -0
- package/dist/tools/read-parity.d.ts.map +1 -1
- package/dist/tools/read-parity.js +69 -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",
|
|
@@ -380,7 +413,104 @@ export const API_CLIENT_OPERATIONS = [
|
|
|
380
413
|
operationId: "getPickOfTheDayArchive",
|
|
381
414
|
auth: "bearer",
|
|
382
415
|
},
|
|
416
|
+
{
|
|
417
|
+
method: "GET",
|
|
418
|
+
path: "/api/v1/pick-of-the-day/ledger",
|
|
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",
|
|
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.",
|
|
456
|
+
},
|
|
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",
|
|
383
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
|
+
}
|
|
384
514
|
/**
|
|
385
515
|
* Trader skill grade. Source of truth:
|
|
386
516
|
* `components.schemas.Trader.properties.grade.enum` (S highest, F lowest).
|
|
@@ -388,17 +518,25 @@ export const API_CLIENT_OPERATIONS = [
|
|
|
388
518
|
export const GRADES = ["S", "A", "B", "C", "D", "F"];
|
|
389
519
|
/** Default production API base used when `baseUrl` is omitted. */
|
|
390
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_";
|
|
391
531
|
/** Default per-request deadline; see `ApiClientOptions.timeoutMs`. */
|
|
392
532
|
export const DEFAULT_TIMEOUT_MS = 15_000;
|
|
393
533
|
/** Default retry budget; see `ApiClientOptions.maxRetries`. */
|
|
394
534
|
export const DEFAULT_MAX_RETRIES = 2;
|
|
395
535
|
/** Statuses a retry can fix: rate limited, or a gateway/availability failure. */
|
|
396
|
-
const RETRYABLE_STATUSES = new Set([429, 502, 503, 504]);
|
|
536
|
+
const RETRYABLE_STATUSES = new Set([408, 429, 502, 503, 504]);
|
|
397
537
|
const RETRY_BASE_DELAY_MS = 500;
|
|
398
538
|
const RETRY_MAX_BACKOFF_MS = 8_000;
|
|
399
539
|
const RETRY_JITTER_MS = 250;
|
|
400
|
-
/** A server-requested wait longer than this is the caller's to schedule. */
|
|
401
|
-
const RETRY_AFTER_CEILING_MS = 60_000;
|
|
402
540
|
function assertRetryCount(value, source) {
|
|
403
541
|
if (!Number.isInteger(value) || value < 0) {
|
|
404
542
|
throw new Error(`${source} must be a non-negative integer, got ${String(value)}`);
|
|
@@ -407,11 +545,14 @@ function assertRetryCount(value, source) {
|
|
|
407
545
|
}
|
|
408
546
|
/**
|
|
409
547
|
* Milliseconds to wait before retry `attempt` (0-based), or `null` when the
|
|
410
|
-
* 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`).
|
|
411
552
|
*/
|
|
412
553
|
function retryDelayMs(attempt, retryAfter) {
|
|
413
554
|
if (retryAfter !== null) {
|
|
414
|
-
const requested =
|
|
555
|
+
const requested = retryAfter * 1000;
|
|
415
556
|
if (requested > RETRY_AFTER_CEILING_MS)
|
|
416
557
|
return null;
|
|
417
558
|
return requested + Math.random() * RETRY_JITTER_MS;
|
|
@@ -419,24 +560,6 @@ function retryDelayMs(attempt, retryAfter) {
|
|
|
419
560
|
const ceiling = Math.min(RETRY_MAX_BACKOFF_MS, RETRY_BASE_DELAY_MS * 2 ** attempt);
|
|
420
561
|
return ceiling / 2 + Math.random() * (ceiling / 2);
|
|
421
562
|
}
|
|
422
|
-
/** Resolve after `ms`, or reject with the signal's reason the moment it aborts. */
|
|
423
|
-
function sleepUnlessAborted(ms, signal) {
|
|
424
|
-
return new Promise((resolve, reject) => {
|
|
425
|
-
if (signal?.aborted) {
|
|
426
|
-
reject(signal.reason);
|
|
427
|
-
return;
|
|
428
|
-
}
|
|
429
|
-
const onAbort = () => {
|
|
430
|
-
clearTimeout(timer);
|
|
431
|
-
reject(signal?.reason);
|
|
432
|
-
};
|
|
433
|
-
const timer = setTimeout(() => {
|
|
434
|
-
signal?.removeEventListener("abort", onAbort);
|
|
435
|
-
resolve();
|
|
436
|
-
}, ms);
|
|
437
|
-
signal?.addEventListener("abort", onAbort, { once: true });
|
|
438
|
-
});
|
|
439
|
-
}
|
|
440
563
|
function isLoopbackHost(hostname) {
|
|
441
564
|
const host = hostname.replace(/^\[|\]$/g, "").toLowerCase();
|
|
442
565
|
return (host === "localhost" ||
|
|
@@ -463,6 +586,28 @@ export function assertTrustedBaseUrl(baseUrl) {
|
|
|
463
586
|
return url;
|
|
464
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).`);
|
|
465
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
|
+
}
|
|
466
611
|
/**
|
|
467
612
|
* One signal that aborts on the deadline OR when the caller's signal aborts.
|
|
468
613
|
* Uses native composition where available. The manual fallback removes both
|
|
@@ -512,6 +657,84 @@ function isTimeoutAbort(error) {
|
|
|
512
657
|
"name" in error &&
|
|
513
658
|
error.name === "TimeoutError");
|
|
514
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
|
+
}
|
|
515
738
|
/** `GET /api/v1/leaderboard` -> parameters -> strategy (`web/public/api/v1/openapi.json`). */
|
|
516
739
|
export const LEADERBOARD_STRATEGIES = [
|
|
517
740
|
"accumulator",
|
|
@@ -525,16 +748,48 @@ export const LEADERBOARD_STRATEGIES = [
|
|
|
525
748
|
"speculator",
|
|
526
749
|
"swing_trader",
|
|
527
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
|
+
];
|
|
528
762
|
const operationsById = new Map(API_CLIENT_OPERATIONS.map((operation) => [operation.operationId, operation]));
|
|
529
763
|
export class OxinsiderApiClient {
|
|
530
764
|
baseUrl;
|
|
531
765
|
apiKey;
|
|
766
|
+
sandbox;
|
|
532
767
|
fetchImpl;
|
|
533
768
|
timeoutMs;
|
|
534
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
|
+
}
|
|
535
784
|
constructor(options = {}) {
|
|
536
|
-
|
|
537
|
-
|
|
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
|
+
}
|
|
538
793
|
this.apiKey = options.apiKey;
|
|
539
794
|
this.timeoutMs =
|
|
540
795
|
options.timeoutMs === undefined ? DEFAULT_TIMEOUT_MS : options.timeoutMs;
|
|
@@ -546,29 +801,224 @@ export class OxinsiderApiClient {
|
|
|
546
801
|
// Bind so a passed-through `globalThis.fetch` keeps its receiver.
|
|
547
802
|
this.fetchImpl = fetchImpl.bind(globalThis);
|
|
548
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
|
+
}
|
|
549
844
|
/**
|
|
550
|
-
*
|
|
551
|
-
*
|
|
552
|
-
*
|
|
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;
|
|
553
862
|
*/
|
|
554
|
-
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) {
|
|
555
976
|
const operation = operationsById.get(operationId);
|
|
556
977
|
if (!operation) {
|
|
557
978
|
throw new Error(`Unknown 0xinsider API operation: ${operationId}`);
|
|
558
979
|
}
|
|
559
|
-
|
|
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) {
|
|
560
987
|
throw new Error(`${operationId} requires an API key (oxi_sk_*)`);
|
|
561
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;
|
|
562
1000
|
const timeoutMs = options.timeoutMs === undefined ? this.timeoutMs : options.timeoutMs;
|
|
563
1001
|
const maxRetries = options.maxRetries === undefined
|
|
564
1002
|
? this.maxRetries
|
|
565
1003
|
: assertRetryCount(options.maxRetries, "maxRetries");
|
|
566
1004
|
const url = this.buildUrl(operation, options);
|
|
567
|
-
const headers = this.buildHeaders(operation, options);
|
|
1005
|
+
const headers = this.buildHeaders(operation, options, transport.accept);
|
|
568
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
|
+
}
|
|
569
1017
|
// Replaying a write without a key could repeat it (a second webhook
|
|
570
|
-
// endpoint, say), so only a
|
|
571
|
-
|
|
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);
|
|
572
1022
|
// One deadline for every attempt: a retry that cannot finish before it is
|
|
573
1023
|
// not started, so the caller sees the real failure, not a timeout.
|
|
574
1024
|
const deadlineAt = timeoutMs === null ? null : Date.now() + timeoutMs;
|
|
@@ -592,6 +1042,7 @@ export class OxinsiderApiClient {
|
|
|
592
1042
|
headers,
|
|
593
1043
|
body,
|
|
594
1044
|
signal: requestSignal.signal,
|
|
1045
|
+
...(transport.redirect ? { redirect: transport.redirect } : {}),
|
|
595
1046
|
});
|
|
596
1047
|
}
|
|
597
1048
|
catch (error) {
|
|
@@ -604,7 +1055,7 @@ export class OxinsiderApiClient {
|
|
|
604
1055
|
await sleepUnlessAborted(delay, requestSignal.signal);
|
|
605
1056
|
continue;
|
|
606
1057
|
}
|
|
607
|
-
if (response
|
|
1058
|
+
if (isSuccess(response))
|
|
608
1059
|
break;
|
|
609
1060
|
const errorBody = await parseJson(response);
|
|
610
1061
|
const retryAfter = retryAfterSeconds(response);
|
|
@@ -616,22 +1067,7 @@ export class OxinsiderApiClient {
|
|
|
616
1067
|
}
|
|
617
1068
|
await sleepUnlessAborted(delay, requestSignal.signal);
|
|
618
1069
|
}
|
|
619
|
-
|
|
620
|
-
return notModifiedResponse(response);
|
|
621
|
-
}
|
|
622
|
-
const envelope = await parseJson(response);
|
|
623
|
-
if (isApiEnvelope(envelope)) {
|
|
624
|
-
const etag = response.headers.get("etag");
|
|
625
|
-
// Only ADD to a meta the server actually sent. The contract makes
|
|
626
|
-
// `meta` required on every envelope, so synthesizing one from `?? {}`
|
|
627
|
-
// would hand a caller a `ResponseMeta` missing its required fields --
|
|
628
|
-
// which is what the hand-written type hid before #14278.
|
|
629
|
-
if (etag && envelope.meta) {
|
|
630
|
-
envelope.meta = { ...envelope.meta, etag };
|
|
631
|
-
}
|
|
632
|
-
return envelope;
|
|
633
|
-
}
|
|
634
|
-
throw new OxinsiderApiError(response.status, envelope);
|
|
1070
|
+
return await consume(response);
|
|
635
1071
|
}
|
|
636
1072
|
catch (error) {
|
|
637
1073
|
if (timeoutMs !== null &&
|
|
@@ -649,66 +1085,113 @@ export class OxinsiderApiClient {
|
|
|
649
1085
|
requestSignal.dispose?.();
|
|
650
1086
|
}
|
|
651
1087
|
}
|
|
652
|
-
/**
|
|
653
|
-
* Execute a Stripe-style list operation and return the list envelope.
|
|
654
|
-
* Use `paginate()` (in `pagination.ts`) to auto-follow `next_cursor`.
|
|
655
|
-
*/
|
|
656
1088
|
async list(operationId, options = {}) {
|
|
657
1089
|
const result = await this.call(operationId, options);
|
|
658
1090
|
if (isNotModified(result)) {
|
|
659
1091
|
throw new OxinsiderApiError(304, "list() received a 304 not_modified; use call() if you send If-None-Match on a list endpoint");
|
|
660
1092
|
}
|
|
661
1093
|
if (!isListEnvelope(result)) {
|
|
662
|
-
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);
|
|
663
1095
|
}
|
|
664
1096
|
return result;
|
|
665
1097
|
}
|
|
666
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).
|
|
667
1106
|
getTrader(address, options = {}) {
|
|
668
1107
|
return this.call("getTrader", { ...options, path: { address } });
|
|
669
1108
|
}
|
|
670
1109
|
getTraderPnl(address, options = {}) {
|
|
671
1110
|
return this.call("getTraderPnl", { ...options, path: { address } });
|
|
672
1111
|
}
|
|
673
|
-
|
|
674
|
-
|
|
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", {
|
|
675
1128
|
...options,
|
|
676
|
-
|
|
1129
|
+
path: { address },
|
|
1130
|
+
query: params,
|
|
677
1131
|
});
|
|
678
1132
|
}
|
|
679
|
-
|
|
680
|
-
|
|
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
|
+
});
|
|
1156
|
+
}
|
|
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 });
|
|
681
1166
|
}
|
|
682
|
-
listTrendingWallets(params = {}) {
|
|
683
|
-
return this.list("listTrendingWallets", { query: params });
|
|
1167
|
+
listTrendingWallets(params = {}, options = {}) {
|
|
1168
|
+
return this.list("listTrendingWallets", { ...options, query: params });
|
|
684
1169
|
}
|
|
685
|
-
listWhaleTrades(params = {}) {
|
|
686
|
-
return this.list("listWhaleTrades", { query: params });
|
|
1170
|
+
listWhaleTrades(params = {}, options = {}) {
|
|
1171
|
+
return this.list("listWhaleTrades", { ...options, query: params });
|
|
687
1172
|
}
|
|
688
|
-
listWhaleTradeHistory(params = {}) {
|
|
689
|
-
return this.list("listWhaleTradeHistory", { query: params });
|
|
1173
|
+
listWhaleTradeHistory(params = {}, options = {}) {
|
|
1174
|
+
return this.list("listWhaleTradeHistory", { ...options, query: params });
|
|
690
1175
|
}
|
|
691
|
-
listPositions(params = {}) {
|
|
692
|
-
return this.list("listPositions", { query: params });
|
|
1176
|
+
listPositions(params = {}, options = {}) {
|
|
1177
|
+
return this.list("listPositions", { ...options, query: params });
|
|
693
1178
|
}
|
|
694
|
-
listLargePositions(params = {}) {
|
|
695
|
-
return this.list("listLargePositions", { query: params });
|
|
1179
|
+
listLargePositions(params = {}, options = {}) {
|
|
1180
|
+
return this.list("listLargePositions", { ...options, query: params });
|
|
696
1181
|
}
|
|
697
|
-
listSmartMoneyFlows(params = {}) {
|
|
698
|
-
return this.list("listSmartMoneyFlows", { query: params });
|
|
1182
|
+
listSmartMoneyFlows(params = {}, options = {}) {
|
|
1183
|
+
return this.list("listSmartMoneyFlows", { ...options, query: params });
|
|
699
1184
|
}
|
|
700
1185
|
/** Canonical alias of {@link listSmartMoneyFlows} (epic #6912). */
|
|
701
|
-
listSharpMoneyFlows(params = {}) {
|
|
702
|
-
return this.list("listSharpMoneyFlows", { query: params });
|
|
1186
|
+
listSharpMoneyFlows(params = {}, options = {}) {
|
|
1187
|
+
return this.list("listSharpMoneyFlows", { ...options, query: params });
|
|
703
1188
|
}
|
|
704
1189
|
/**
|
|
705
1190
|
* List the funded-primary sports signals with additive, shadow-only category
|
|
706
1191
|
* evidence. `category_skill` never changes membership, ordering, or sizing.
|
|
707
1192
|
*/
|
|
708
|
-
listSportsEdgeSignals(params = {}) {
|
|
709
|
-
return this.list("listSportsEdgeSignals", {
|
|
710
|
-
query: params,
|
|
711
|
-
});
|
|
1193
|
+
listSportsEdgeSignals(params = {}, options = {}) {
|
|
1194
|
+
return this.list("listSportsEdgeSignals", { ...options, query: params });
|
|
712
1195
|
}
|
|
713
1196
|
/**
|
|
714
1197
|
* Read an explicitly observation-only sports cohort and its accountable
|
|
@@ -720,7 +1203,10 @@ export class OxinsiderApiClient {
|
|
|
720
1203
|
* denominator view.
|
|
721
1204
|
*/
|
|
722
1205
|
async listSportsEdgeObservations(params, options = {}) {
|
|
723
|
-
const response = await this.list("listSportsEdgeObservations", {
|
|
1206
|
+
const response = await this.list("listSportsEdgeObservations", {
|
|
1207
|
+
...options,
|
|
1208
|
+
query: params,
|
|
1209
|
+
});
|
|
724
1210
|
return sportsEdgeObservationsResponse(response);
|
|
725
1211
|
}
|
|
726
1212
|
/**
|
|
@@ -733,23 +1219,25 @@ export class OxinsiderApiClient {
|
|
|
733
1219
|
* position remains covered.
|
|
734
1220
|
*/
|
|
735
1221
|
async listSportsEdgeObservationsConditional(params, options = {}) {
|
|
736
|
-
const response = await this.call("listSportsEdgeObservations", {
|
|
1222
|
+
const response = await this.call("listSportsEdgeObservations", {
|
|
1223
|
+
...options,
|
|
1224
|
+
query: params,
|
|
1225
|
+
});
|
|
737
1226
|
if (isNotModified(response)) {
|
|
738
1227
|
return response;
|
|
739
1228
|
}
|
|
740
|
-
if (!isListEnvelope(response)) {
|
|
741
|
-
throw new OxinsiderApiError(200, "listSportsEdgeObservations did not return a list envelope");
|
|
742
|
-
}
|
|
743
1229
|
return sportsEdgeObservationsResponse(response);
|
|
744
1230
|
}
|
|
745
1231
|
searchMarkets(q, options = {}) {
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
1232
|
+
const { query, ...request } = options;
|
|
1233
|
+
return this.list("searchMarkets", { ...request, query: { ...query, q } });
|
|
1234
|
+
}
|
|
1235
|
+
searchContent(q, options = {}) {
|
|
1236
|
+
const { query, ...request } = options;
|
|
1237
|
+
return this.list("searchContent", { ...request, query: { ...query, q } });
|
|
750
1238
|
}
|
|
751
|
-
exploreMarkets(params = {}) {
|
|
752
|
-
return this.list("exploreMarkets", { query: params });
|
|
1239
|
+
exploreMarkets(params = {}, options = {}) {
|
|
1240
|
+
return this.list("exploreMarkets", { ...options, query: params });
|
|
753
1241
|
}
|
|
754
1242
|
getMarketIntel(conditionId, options = {}) {
|
|
755
1243
|
return this.call("getMarketIntel", {
|
|
@@ -763,37 +1251,71 @@ export class OxinsiderApiClient {
|
|
|
763
1251
|
path: { condition_id: conditionId },
|
|
764
1252
|
});
|
|
765
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
|
+
}
|
|
766
1269
|
/**
|
|
767
1270
|
* Fetch bucketed OHLC candles for a market's outcome tokens.
|
|
768
|
-
* Resolution defaults to the server default; pass `
|
|
769
|
-
* `"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`.
|
|
770
1275
|
*/
|
|
771
|
-
getMarketCandles(conditionId, options = {}) {
|
|
1276
|
+
getMarketCandles(conditionId, params = {}, options = {}) {
|
|
772
1277
|
return this.call("getMarketCandles", {
|
|
773
1278
|
...options,
|
|
774
1279
|
path: { condition_id: conditionId },
|
|
1280
|
+
query: params,
|
|
775
1281
|
});
|
|
776
1282
|
}
|
|
777
1283
|
getInsiderRadarFlag(id, options = {}) {
|
|
778
1284
|
return this.call("getInsiderRadarFlag", { ...options, path: { id } });
|
|
779
1285
|
}
|
|
780
|
-
listInsiderRadar(params = {}) {
|
|
781
|
-
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 } });
|
|
782
1291
|
}
|
|
783
1292
|
// --- Webhooks ---
|
|
784
1293
|
listWebhooks(options = {}) {
|
|
785
|
-
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 } });
|
|
786
1307
|
}
|
|
787
1308
|
/**
|
|
788
1309
|
* Fetch the self-describing webhook event catalog (each entry's `id`,
|
|
789
1310
|
* description, payload shape, and active/dormant status).
|
|
790
1311
|
*/
|
|
791
1312
|
listWebhookEvents(options = {}) {
|
|
792
|
-
return this.
|
|
1313
|
+
return this.list("listWebhookEvents", options);
|
|
793
1314
|
}
|
|
794
1315
|
/** List delivery attempts for one webhook endpoint (Stripe-style list). */
|
|
795
|
-
listWebhookDeliveries(webhookId, params = {}) {
|
|
1316
|
+
listWebhookDeliveries(webhookId, params = {}, options = {}) {
|
|
796
1317
|
return this.list("listWebhookDeliveries", {
|
|
1318
|
+
...options,
|
|
797
1319
|
path: { id: webhookId },
|
|
798
1320
|
query: params,
|
|
799
1321
|
});
|
|
@@ -819,9 +1341,44 @@ export class OxinsiderApiClient {
|
|
|
819
1341
|
getApiDiscovery(options = {}) {
|
|
820
1342
|
return this.call("getApiDiscovery", options);
|
|
821
1343
|
}
|
|
1344
|
+
getPlatforms(options = {}) {
|
|
1345
|
+
return this.call("getPlatforms", options);
|
|
1346
|
+
}
|
|
822
1347
|
getAccountIdentity(options = {}) {
|
|
823
1348
|
return this.call("getAccountIdentity", options);
|
|
824
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
|
+
}
|
|
825
1382
|
getUsage(options = {}) {
|
|
826
1383
|
return this.call("getUsage", options);
|
|
827
1384
|
}
|
|
@@ -833,6 +1390,20 @@ export class OxinsiderApiClient {
|
|
|
833
1390
|
getPickOfTheDayArchive(options = {}) {
|
|
834
1391
|
return this.call("getPickOfTheDayArchive", options);
|
|
835
1392
|
}
|
|
1393
|
+
/**
|
|
1394
|
+
* Fetch the Pick of the Day commitment ledger (single-object envelope).
|
|
1395
|
+
*
|
|
1396
|
+
* Every entry is `sealed` (a live pick: the hash, no side and no price),
|
|
1397
|
+
* `opened` (a settled pick: the nonce and the exact hashed payload) or
|
|
1398
|
+
* `uncommitted` (no commitment; once settled, its unhashed side and price
|
|
1399
|
+
* under `payload`). Verify an opened entry by
|
|
1400
|
+
* appending the hex-decoded `commitment_nonce` to the `payload` bytes as
|
|
1401
|
+
* received and hashing with sha256; do not reserialize the payload, since it
|
|
1402
|
+
* is served byte for byte as it was hashed.
|
|
1403
|
+
*/
|
|
1404
|
+
getPickOfTheDayLedger(options = {}) {
|
|
1405
|
+
return this.call("getPickOfTheDayLedger", options);
|
|
1406
|
+
}
|
|
836
1407
|
// --- Internals ---
|
|
837
1408
|
/** Resolve the base URL (used by the SSE stream consumer). */
|
|
838
1409
|
getBaseUrl() {
|
|
@@ -842,12 +1413,16 @@ export class OxinsiderApiClient {
|
|
|
842
1413
|
getApiKey() {
|
|
843
1414
|
return this.apiKey;
|
|
844
1415
|
}
|
|
1416
|
+
/** Whether this client was built in sandbox mode (#16138). */
|
|
1417
|
+
isSandbox() {
|
|
1418
|
+
return this.sandbox;
|
|
1419
|
+
}
|
|
845
1420
|
/** The resolved fetch implementation (used by the SSE stream consumer). */
|
|
846
1421
|
getFetch() {
|
|
847
1422
|
return this.fetchImpl;
|
|
848
1423
|
}
|
|
849
1424
|
buildUrl(operation, options) {
|
|
850
|
-
const url =
|
|
1425
|
+
const url = resolveApiUrl(this.baseUrl, interpolatePath(operation.path, options.path ?? {}));
|
|
851
1426
|
for (const [key, value] of Object.entries(options.query ?? {})) {
|
|
852
1427
|
if (value === null || value === undefined) {
|
|
853
1428
|
continue;
|
|
@@ -863,9 +1438,9 @@ export class OxinsiderApiClient {
|
|
|
863
1438
|
}
|
|
864
1439
|
return url.toString();
|
|
865
1440
|
}
|
|
866
|
-
buildHeaders(operation, options) {
|
|
1441
|
+
buildHeaders(operation, options, accept = "application/json") {
|
|
867
1442
|
const headers = new Headers(options.headers);
|
|
868
|
-
headers.set("accept",
|
|
1443
|
+
headers.set("accept", accept);
|
|
869
1444
|
if (options.body !== undefined && !headers.has("content-type")) {
|
|
870
1445
|
headers.set("content-type", "application/json");
|
|
871
1446
|
}
|
|
@@ -875,9 +1450,18 @@ export class OxinsiderApiClient {
|
|
|
875
1450
|
if (options.idempotencyKey !== undefined) {
|
|
876
1451
|
headers.set("idempotency-key", options.idempotencyKey);
|
|
877
1452
|
}
|
|
1453
|
+
if (options.strictQuery) {
|
|
1454
|
+
headers.set("x-query-validation", "strict");
|
|
1455
|
+
}
|
|
878
1456
|
return headers;
|
|
879
1457
|
}
|
|
880
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
|
+
*/
|
|
881
1465
|
export function isApiNotModifiedResponse(response) {
|
|
882
1466
|
return response.object === "not_modified";
|
|
883
1467
|
}
|
|
@@ -905,13 +1489,6 @@ async function parseJson(response) {
|
|
|
905
1489
|
return text;
|
|
906
1490
|
}
|
|
907
1491
|
}
|
|
908
|
-
function retryAfterSeconds(response) {
|
|
909
|
-
const header = response.headers.get("retry-after");
|
|
910
|
-
if (!header)
|
|
911
|
-
return null;
|
|
912
|
-
const seconds = Number(header);
|
|
913
|
-
return Number.isFinite(seconds) ? seconds : null;
|
|
914
|
-
}
|
|
915
1492
|
function notModifiedResponse(response) {
|
|
916
1493
|
const meta = { status: 304 };
|
|
917
1494
|
const etag = response.headers.get("etag");
|
|
@@ -922,6 +1499,14 @@ function notModifiedResponse(response) {
|
|
|
922
1499
|
if (requestId) {
|
|
923
1500
|
meta.request_id = requestId;
|
|
924
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
|
+
}
|
|
925
1510
|
return { object: "not_modified", data: null, meta };
|
|
926
1511
|
}
|
|
927
1512
|
function isApiEnvelope(body) {
|
|
@@ -930,14 +1515,23 @@ function isApiEnvelope(body) {
|
|
|
930
1515
|
"object" in body &&
|
|
931
1516
|
"data" in body);
|
|
932
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
|
+
*/
|
|
933
1525
|
export function isListEnvelope(body) {
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
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"));
|
|
941
1535
|
}
|
|
942
1536
|
function sportsEdgeObservationsResponse(response) {
|
|
943
1537
|
const candidate = response;
|