scorezilla 0.1.0-next.0 → 0.1.0-next.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.
@@ -0,0 +1,292 @@
1
+ /** Minimal fetch shape — broader than `typeof fetch` so polyfills and
2
+ * test stubs (`vi.fn()`, `node-fetch`, etc.) typecheck cleanly. */
3
+ type FetchImpl = (input: RequestInfo | URL, init?: RequestInit) => Promise<Response>;
4
+
5
+ /**
6
+ * SDK configuration.
7
+ *
8
+ * The `ScorezillaConfig` is a TypeScript-level discriminated union of
9
+ * `PublicKeyConfig` and `SecretKeyConfig`. The mutual-exclusivity (you may
10
+ * pass `publicKey` OR `secretKey`, never both) is enforced at compile time:
11
+ * passing both fields fails type-checking before the runtime check fires.
12
+ *
13
+ * The runtime check in {@link validateConfig} is the second line of defense
14
+ * for consumers using plain JS or `as any` casts.
15
+ */
16
+
17
+ /** Shared options across both auth modes. */
18
+ interface BaseConfig {
19
+ /** API base URL (no trailing slash required). Defaults to {@link DEFAULT_BASE_URL}. */
20
+ baseUrl?: string;
21
+ /** Custom fetch implementation — defaults to `globalThis.fetch`. Pass
22
+ * `node-fetch`, `undici`, or a mock here. The explicit signature
23
+ * (`(RequestInfo | URL, init?) => Promise<Response>`) is broader than
24
+ * `typeof fetch` so common polyfills typecheck cleanly. */
25
+ fetch?: FetchImpl;
26
+ /** Per-request timeout in milliseconds. Defaults to 30 s. */
27
+ timeoutMs?: number;
28
+ /** Maximum retry attempts on transient failures. Defaults to 2 (so the
29
+ * worst-case total request count is 3). */
30
+ maxRetries?: number;
31
+ /** Override the default `User-Agent` header (Node/Workers/Bun/Deno —
32
+ * browsers silently ignore the value). */
33
+ userAgent?: string;
34
+ /** Injectable sleep implementation for the retry loop's inter-attempt
35
+ * pause. Exists for tests that need deterministic, zero-delay retries
36
+ * rather than real wall-clock backoff. Production code should leave
37
+ * this unset to use the default exponential backoff with jitter.
38
+ * @internal */
39
+ sleepImpl?: (ms: number, signal?: AbortSignal) => Promise<void>;
40
+ }
41
+ /** Public-key auth: browser-safe path. The key is fingerprinted to a game
42
+ * on the server side via `pk_<gameSlug>_<base62>`. */
43
+ type PublicKeyConfig = BaseConfig & {
44
+ publicKey: string;
45
+ secretKey?: never;
46
+ };
47
+ /** Secret-key auth: server-side HMAC. A single self-contained token of the
48
+ * shape `sk_live_<keyId>_<random>`. The SDK parses the keyId out and uses
49
+ * the whole string as the HMAC key. One value to copy, one to manage —
50
+ * matches Stripe's design and the public-key client's single-string shape.
51
+ *
52
+ * Past versions of the SDK took `{ id, secret }` separately. That was an
53
+ * unnecessary cognitive tax — the id was always derivable from a properly-
54
+ * formatted secret. v0.1.0-next.2+ takes the single-string form. */
55
+ type SecretKeyConfig = BaseConfig & {
56
+ secretKey: string;
57
+ publicKey?: never;
58
+ };
59
+ /** The top-level config type. The union is open for additional auth modes
60
+ * in future major releases. */
61
+ type ScorezillaConfig = PublicKeyConfig | SecretKeyConfig;
62
+
63
+ /**
64
+ * Wire types for the Scorezilla API at /v1.
65
+ *
66
+ * Mirrors the documented response shapes. TypeScript's structural typing
67
+ * means additional fields the server adds in a minor release won't break
68
+ * consumers — see VERSIONING.md for the full SemVer contract.
69
+ *
70
+ * No Zod or other runtime validators in v0.1.0 — keeps the bundle small.
71
+ * Narrow untrusted input with the {@link isApiSuccess} / {@link isApiError}
72
+ * type guards exported below.
73
+ */
74
+ /**
75
+ * Machine-stable error codes returned by the API.
76
+ *
77
+ * Consumers MUST branch on this `code` rather than the human-readable
78
+ * `message` — message text is English-only and explicitly NOT part of the
79
+ * SemVer contract.
80
+ *
81
+ * The union is intentionally open (`| (string & {})`) so unknown future
82
+ * codes from a server-side minor release don't compile-error against the
83
+ * SDK. The trick preserves autocomplete on the known set while permitting
84
+ * arbitrary strings at runtime — see
85
+ * https://github.com/microsoft/TypeScript/issues/29729 for the pattern.
86
+ *
87
+ * @stable v0.1.0
88
+ */
89
+ type ScorezillaErrorCode = 'unauthorized' | 'forbidden' | 'not_found' | 'invalid_input' | 'invalid_json' | 'out_of_bounds' | 'rate_limited' | 'conflict' | 'internal_error' | (string & {});
90
+ /** Reason sub-classifier on `out_of_bounds` errors. Open union — see {@link ScorezillaErrorCode}. */
91
+ type OutOfBoundsReason = 'below_min' | 'above_max' | (string & {});
92
+ /** Successful API response envelope. The `T` is the per-route payload. */
93
+ type ApiSuccess<T> = {
94
+ ok: true;
95
+ } & T;
96
+ /** Failure response envelope. The server returns this on every non-2xx response. */
97
+ interface ApiError {
98
+ ok: false;
99
+ error: ScorezillaErrorCode;
100
+ /** Human-readable, English only. Not machine-stable — branch on `error` and `reason`. */
101
+ message?: string;
102
+ /** Sub-classifier — used by `out_of_bounds` (`'below_min' | 'above_max'`). */
103
+ reason?: string;
104
+ /** Seconds — present on `rate_limited`. Also mirrored in the HTTP `Retry-After` header. */
105
+ retryAfter?: number;
106
+ /** Which rate-limit layer fired — present on `rate_limited`. */
107
+ layer?: string;
108
+ /** The limit value that was crossed — present on `out_of_bounds`. */
109
+ bound?: number;
110
+ }
111
+ /** Discriminated envelope: every API response is one of these two shapes. */
112
+ type ApiResponse<T> = ApiSuccess<T> | ApiError;
113
+ /**
114
+ * A single ranked entry on a leaderboard.
115
+ *
116
+ * Returned as an array on `leaderboard` and `window-around` responses, and
117
+ * inline on `playerRank` (without the `rank` wrapper — see
118
+ * {@link PlayerRankResponse}).
119
+ */
120
+ interface RankedEntry {
121
+ /** 1-based rank. */
122
+ rank: number;
123
+ playerId: string;
124
+ score: number;
125
+ /** Milliseconds since epoch. */
126
+ submittedAt: number;
127
+ metadata?: Record<string, unknown>;
128
+ }
129
+ /** Payload from `POST /v1/boards/:boardId/scores`. */
130
+ interface SubmitScoreResponse {
131
+ boardId: string;
132
+ /** The key ID that authorized the submission. Useful for consumer-side audit. */
133
+ keyId: string;
134
+ /** 1-based rank after the submit settled. */
135
+ rank: number;
136
+ totalEntries: number;
137
+ isPersonalBest: boolean;
138
+ }
139
+ /** Payload from `GET /v1/boards/:boardId/leaderboard`. */
140
+ interface LeaderboardResponse {
141
+ boardId: string;
142
+ offset: number;
143
+ limit: number;
144
+ entries: RankedEntry[];
145
+ }
146
+ /** Payload from `GET /v1/boards/:boardId/players/:playerId/rank`. */
147
+ interface PlayerRankResponse {
148
+ boardId: string;
149
+ playerId: string;
150
+ rank: number;
151
+ score: number;
152
+ submittedAt: number;
153
+ totalEntries: number;
154
+ }
155
+ /** Payload from `GET /v1/boards/:boardId/players/:playerId/window`. */
156
+ interface WindowAroundResponse {
157
+ boardId: string;
158
+ playerId: string;
159
+ before: number;
160
+ after: number;
161
+ entries: RankedEntry[];
162
+ }
163
+
164
+ /**
165
+ * SDK error type.
166
+ *
167
+ * Every non-2xx API response is normalized into a `ScorezillaError` instance
168
+ * by the transport layer. Network failures and timeouts surface as the same
169
+ * class (with `status: 0`) so callers have a single error type to catch.
170
+ *
171
+ * **Invariant — consumers MUST branch on `code` (and optionally `reason`),
172
+ * never on `message`.** The English-language `message` is for operator
173
+ * logging only and is explicitly **not** part of the SemVer contract; a
174
+ * minor release MAY reword any message. Machine logic that depends on
175
+ * message text will break silently across upgrades.
176
+ */
177
+
178
+ /**
179
+ * Options for {@link ScorezillaError.from}.
180
+ *
181
+ * The fields mirror what's available after a fetch round-trip: the HTTP
182
+ * status, the parsed JSON body (if any), the request ID from
183
+ * `X-Request-Id`, and an optional `cause` for the underlying
184
+ * network/abort error.
185
+ */
186
+ interface ScorezillaErrorFromInit {
187
+ status: number;
188
+ body?: ApiError | undefined;
189
+ requestId?: string | undefined;
190
+ cause?: unknown;
191
+ }
192
+ /**
193
+ * Thrown by the SDK for every failure path — non-2xx responses, network
194
+ * errors, aborts, and timeouts.
195
+ *
196
+ * Cross-realm `instanceof` is guaranteed: the class sets `Error.prototype`
197
+ * explicitly so checks survive iframe / worker boundaries.
198
+ *
199
+ * @example
200
+ * ```ts
201
+ * try {
202
+ * await sz.submitScore({ boardId, playerId, score });
203
+ * } catch (e) {
204
+ * if (!(e instanceof ScorezillaError)) throw e;
205
+ *
206
+ * if (e.isRateLimited()) {
207
+ * await sleep((e.retryAfter ?? 30) * 1000);
208
+ * return retry();
209
+ * }
210
+ * if (e.code === 'out_of_bounds') {
211
+ * console.warn(`Score crosses ${e.reason} bound (limit ${e.bound})`);
212
+ * return;
213
+ * }
214
+ * if (e.isAuth()) throw new Error('SDK misconfigured — bad publicKey');
215
+ *
216
+ * // Anything else: surface to your reporter with requestId for support.
217
+ * console.error(`Scorezilla ${e.code} (${e.status}) — request ${e.requestId}`);
218
+ * throw e;
219
+ * }
220
+ * ```
221
+ *
222
+ * @since 0.1.0
223
+ * @stability stable
224
+ */
225
+ declare class ScorezillaError extends Error {
226
+ /** HTTP status of the response, or {@link STATUS_NETWORK_ERROR} (0) for
227
+ * network / abort / timeout. */
228
+ readonly status: number;
229
+ /** Machine-stable error code from the API. Open union — see
230
+ * {@link ScorezillaErrorCode}. For network errors, this is `'network_error'`;
231
+ * for aborts, `'aborted'`; for timeouts, `'timeout'`. */
232
+ readonly code: ScorezillaErrorCode;
233
+ /** Sub-classifier — present on `out_of_bounds` (`'below_min' | 'above_max'`)
234
+ * and possibly other codes in future minor releases. */
235
+ readonly reason: OutOfBoundsReason | string | undefined;
236
+ /** Seconds — present on `rate_limited`. Honored by the transport's retry
237
+ * policy (Step 2.4). */
238
+ readonly retryAfter: number | undefined;
239
+ /** Server-issued request ID, lifted from the `X-Request-Id` response
240
+ * header. Pass this to support when filing bugs. */
241
+ readonly requestId: string | undefined;
242
+ /** The bound value crossed on `out_of_bounds`. */
243
+ readonly bound: number | undefined;
244
+ /** Which rate-limit layer fired on `rate_limited`. */
245
+ readonly layer: string | undefined;
246
+ /** The underlying cause (e.g., a `TypeError: fetch failed`) for
247
+ * network/abort/timeout paths. `undefined` when the error came from a
248
+ * successfully-parsed API error body. */
249
+ readonly cause: unknown;
250
+ constructor(message: string, init: {
251
+ status: number;
252
+ code: ScorezillaErrorCode;
253
+ reason?: string | undefined;
254
+ retryAfter?: number | undefined;
255
+ requestId?: string | undefined;
256
+ bound?: number | undefined;
257
+ layer?: string | undefined;
258
+ cause?: unknown;
259
+ });
260
+ /** `true` when this error is a 429 / `rate_limited`. */
261
+ isRateLimited(): boolean;
262
+ /** `true` when this error is a 401 / `unauthorized` (or 403 / `forbidden`). */
263
+ isAuth(): boolean;
264
+ /** `true` when this error is a 404 / `not_found`. */
265
+ isNotFound(): boolean;
266
+ /** `true` when this error is a 422 / `out_of_bounds` (score below/above board limit). */
267
+ isOutOfBounds(): boolean;
268
+ /** `true` for transient / retryable conditions: network errors, timeouts,
269
+ * 5xx, and 429. The transport layer relies on this for its retry policy. */
270
+ isTransient(): boolean;
271
+ /**
272
+ * Build a `ScorezillaError` from a fetch round-trip outcome.
273
+ *
274
+ * Prefer this over `new ScorezillaError(...)` from the transport layer —
275
+ * it does the mapping from API response shape to error fields in one
276
+ * place, so future fields like `correlationId` get added once here.
277
+ *
278
+ * @param init - status, optional parsed body, optional requestId, optional cause
279
+ */
280
+ static from(init: ScorezillaErrorFromInit): ScorezillaError;
281
+ /**
282
+ * Build a `ScorezillaError` for a transport-level failure (no HTTP
283
+ * response received): network error, abort, or timeout.
284
+ */
285
+ static network(message: string, cause: unknown): ScorezillaError;
286
+ /** Build a `ScorezillaError` for an `AbortSignal`-triggered cancellation. */
287
+ static aborted(cause: unknown): ScorezillaError;
288
+ /** Build a `ScorezillaError` for a request that exceeded its timeout budget. */
289
+ static timeout(timeoutMs: number): ScorezillaError;
290
+ }
291
+
292
+ export { type ApiSuccess as A, type BaseConfig as B, type LeaderboardResponse as L, type OutOfBoundsReason as O, type PlayerRankResponse as P, type RankedEntry as R, type SecretKeyConfig as S, type WindowAroundResponse as W, type SubmitScoreResponse as a, ScorezillaError as b, type ScorezillaErrorCode as c, type ScorezillaConfig as d, type ApiError as e, type ApiResponse as f, type PublicKeyConfig as g };
package/dist/index.cjs CHANGED
@@ -18,24 +18,10 @@ function validateConfig(cfg) {
18
18
  }
19
19
  let auth;
20
20
  if (hasPublic) {
21
- const pk = cfg.publicKey;
22
- if (typeof pk !== "string" || !PUBLIC_KEY_PATTERN.test(pk)) {
23
- throw new Error(
24
- `scorezilla: publicKey must match ${PUBLIC_KEY_PATTERN.toString()} (got: ${typeof pk === "string" ? pk.slice(0, 12) + "\u2026" : typeof pk})`
25
- );
26
- }
27
- auth = { kind: "public", key: pk };
21
+ auth = { kind: "public", key: validatePublicKeyValue(cfg.publicKey) };
28
22
  } else {
29
- const sk = cfg.secretKey;
30
- if (!sk || typeof sk !== "object" || typeof sk.id !== "string" || typeof sk.secret !== "string") {
31
- throw new Error("scorezilla: secretKey must be an object with string `id` and `secret`");
32
- }
33
- if (!sk.secret.startsWith(SECRET_KEY_PREFIX)) {
34
- throw new Error(
35
- `scorezilla: secretKey.secret must start with "${SECRET_KEY_PREFIX}" (live keys only)`
36
- );
37
- }
38
- auth = { kind: "secret", keyId: sk.id, secret: sk.secret };
23
+ const resolved = validateSecretKey(cfg);
24
+ auth = { kind: "secret", keyId: resolved.keyId, secret: resolved.secret };
39
25
  }
40
26
  const baseUrlRaw = cfg.baseUrl ?? DEFAULT_BASE_URL;
41
27
  if (typeof baseUrlRaw !== "string" || baseUrlRaw.length === 0) {
@@ -46,10 +32,40 @@ function validateConfig(cfg) {
46
32
  fetch: cfg.fetch,
47
33
  timeoutMs: cfg.timeoutMs,
48
34
  maxRetries: cfg.maxRetries,
35
+ sleepImpl: cfg.sleepImpl,
49
36
  userAgent: cfg.userAgent,
50
37
  auth
51
38
  };
52
39
  }
40
+ function validatePublicKeyValue(pk) {
41
+ if (typeof pk !== "string" || !PUBLIC_KEY_PATTERN.test(pk)) {
42
+ const shape = typeof pk === "string" ? `string of length ${pk.length}` : typeof pk;
43
+ throw new Error(
44
+ `scorezilla: publicKey must match ${PUBLIC_KEY_PATTERN.toString()} (got: ${shape})`
45
+ );
46
+ }
47
+ return pk;
48
+ }
49
+ var SECRET_KEY_PATTERN = /^sk_live_([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})_[A-Za-z0-9]+$/;
50
+ function validateSecretKey(cfg) {
51
+ if (!cfg || typeof cfg !== "object") {
52
+ throw new Error("scorezilla/server: config must be an object with a secretKey field");
53
+ }
54
+ const sk = cfg.secretKey;
55
+ if (typeof sk !== "string") {
56
+ throw new Error(
57
+ `scorezilla/server: secretKey must be a single string of the shape ${SECRET_KEY_PREFIX}<keyId>_<random> (got: ${typeof sk})`
58
+ );
59
+ }
60
+ const match = SECRET_KEY_PATTERN.exec(sk);
61
+ if (!match) {
62
+ const shape = `string of length ${sk.length}`;
63
+ throw new Error(
64
+ `scorezilla/server: secretKey must match ${SECRET_KEY_PATTERN.toString()} (got: ${shape}). v0.1.0-next.2 switched to a single-token format \u2014 if you have a pre-next.2 pair, issue a fresh key in the dashboard to upgrade.`
65
+ );
66
+ }
67
+ return { keyId: match[1], secret: sk };
68
+ }
53
69
 
54
70
  // src/paths.ts
55
71
  function encodeSegment(value, label) {
@@ -89,6 +105,12 @@ function truncateMessage(raw) {
89
105
  const sliceEnd = Math.max(0, MESSAGE_MAX_CHARS - TRUNCATION_SUFFIX.length);
90
106
  return raw.slice(0, sliceEnd) + TRUNCATION_SUFFIX;
91
107
  }
108
+ function truncateField(raw) {
109
+ if (typeof raw !== "string") return void 0;
110
+ if (raw.length <= MESSAGE_MAX_CHARS) return raw;
111
+ const sliceEnd = Math.max(0, MESSAGE_MAX_CHARS - TRUNCATION_SUFFIX.length);
112
+ return raw.slice(0, sliceEnd) + TRUNCATION_SUFFIX;
113
+ }
92
114
  var ScorezillaError = class _ScorezillaError extends Error {
93
115
  /** HTTP status of the response, or {@link STATUS_NETWORK_ERROR} (0) for
94
116
  * network / abort / timeout. */
@@ -119,11 +141,11 @@ var ScorezillaError = class _ScorezillaError extends Error {
119
141
  this.name = "ScorezillaError";
120
142
  this.status = init.status;
121
143
  this.code = init.code;
122
- this.reason = init.reason;
144
+ this.reason = truncateField(init.reason);
123
145
  this.retryAfter = init.retryAfter;
124
- this.requestId = init.requestId;
146
+ this.requestId = truncateField(init.requestId);
125
147
  this.bound = init.bound;
126
- this.layer = init.layer;
148
+ this.layer = truncateField(init.layer);
127
149
  this.cause = init.cause;
128
150
  Object.setPrototypeOf(this, _ScorezillaError.prototype);
129
151
  if (typeof Error.captureStackTrace === "function") {
@@ -253,8 +275,9 @@ function shouldRetryError(err) {
253
275
  function generateIdempotencyKey() {
254
276
  const c = globalThis.crypto;
255
277
  if (!c || typeof c.randomUUID !== "function") {
256
- throw new Error(
257
- "scorezilla: globalThis.crypto.randomUUID is unavailable. The SDK requires Node \u2265 20 or a modern browser. Check your runtime."
278
+ throw new ScorezillaError(
279
+ "scorezilla: globalThis.crypto.randomUUID is unavailable. The SDK requires Node \u2265 20 or a modern browser. Check your runtime.",
280
+ { status: 0, code: "internal_error" }
258
281
  );
259
282
  }
260
283
  return c.randomUUID();
@@ -309,16 +332,24 @@ async function request(opts) {
309
332
  for (let attempt = 0; attempt <= maxRetries; attempt++) {
310
333
  const combined = combineSignalsWithTimeout(opts.signal, timeoutMs);
311
334
  try {
335
+ const bodyString = opts.body !== void 0 ? JSON.stringify(opts.body) : "";
336
+ const perAttemptHeaders = { ...opts.headers ?? {} };
337
+ if (opts.signRequest) {
338
+ perAttemptHeaders.Authorization = await opts.signRequest({
339
+ method: opts.method,
340
+ pathAndQuery: opts.path,
341
+ body: bodyString
342
+ });
343
+ }
312
344
  const init = {
313
345
  method: opts.method,
314
- headers: buildHeaders(opts, idempotencyKey),
346
+ headers: buildHeaders({ ...opts, headers: perAttemptHeaders }, idempotencyKey),
315
347
  signal: combined.signal
316
348
  };
317
349
  if (opts.body !== void 0) {
318
- init.body = JSON.stringify(opts.body);
350
+ init.body = bodyString;
319
351
  }
320
352
  const response = await fetchImpl(url, init);
321
- combined.cleanup();
322
353
  if (response.ok) {
323
354
  return await parseJson(response);
324
355
  }
@@ -337,7 +368,6 @@ async function request(opts) {
337
368
  }
338
369
  throw err;
339
370
  } catch (caught) {
340
- combined.cleanup();
341
371
  if (caught instanceof ScorezillaError) {
342
372
  if (shouldRetryError(caught) && attempt < maxRetries) {
343
373
  const delay = nextDelay(attempt, void 0, random);
@@ -355,6 +385,8 @@ async function request(opts) {
355
385
  continue;
356
386
  }
357
387
  throw mapped;
388
+ } finally {
389
+ combined.cleanup();
358
390
  }
359
391
  }
360
392
  throw lastError ?? new ScorezillaError("Request failed after retries", {
@@ -380,16 +412,38 @@ function buildHeaders(opts, idempotencyKey) {
380
412
  return headers;
381
413
  }
382
414
  async function parseJson(response) {
415
+ const requestId = response.headers.get("X-Request-Id") ?? void 0;
416
+ let parsed;
383
417
  try {
384
- return await response.json();
418
+ parsed = await response.json();
385
419
  } catch (cause) {
386
420
  throw new ScorezillaError("Response body was not valid JSON", {
387
421
  status: response.status,
388
422
  code: "invalid_json",
389
- requestId: response.headers.get("X-Request-Id") ?? void 0,
423
+ requestId,
390
424
  cause
391
425
  });
392
426
  }
427
+ if (parsed === null || typeof parsed !== "object") {
428
+ const observed = parsed === null ? "null" : typeof parsed;
429
+ throw new ScorezillaError(`Response body was not a JSON object (got ${observed})`, {
430
+ status: response.status,
431
+ code: "invalid_json",
432
+ requestId
433
+ });
434
+ }
435
+ const okField = parsed.ok;
436
+ if (okField !== true) {
437
+ throw new ScorezillaError(
438
+ `Response body on a 2xx is missing the \`ok: true\` discriminator (got ok=${String(okField)})`,
439
+ {
440
+ status: response.status,
441
+ code: "invalid_json",
442
+ requestId
443
+ }
444
+ );
445
+ }
446
+ return parsed;
393
447
  }
394
448
  async function safelyParseErrorBody(response) {
395
449
  try {
@@ -499,7 +553,7 @@ function validateMetadata(metadata) {
499
553
  }
500
554
  var Scorezilla = class _Scorezilla {
501
555
  /** The package version, injected at build time from `package.json`. */
502
- static version = "0.1.0-next.0";
556
+ static version = "0.1.0-next.3";
503
557
  #config;
504
558
  #userAgent;
505
559
  #authHeader;
@@ -664,8 +718,11 @@ var Scorezilla = class _Scorezilla {
664
718
  if (opts.body !== void 0) requestOpts.body = opts.body;
665
719
  if (this.#config.fetch !== void 0) requestOpts.fetchImpl = this.#config.fetch;
666
720
  if (this.#config.timeoutMs !== void 0) requestOpts.timeoutMs = this.#config.timeoutMs;
667
- if (this.#config.maxRetries !== void 0) {
668
- requestOpts.retry = { maxRetries: this.#config.maxRetries };
721
+ if (this.#config.maxRetries !== void 0 || this.#config.sleepImpl !== void 0) {
722
+ requestOpts.retry = {
723
+ ...this.#config.maxRetries !== void 0 ? { maxRetries: this.#config.maxRetries } : {},
724
+ ...this.#config.sleepImpl !== void 0 ? { sleepImpl: this.#config.sleepImpl } : {}
725
+ };
669
726
  }
670
727
  return request(requestOpts);
671
728
  }
@@ -675,7 +732,7 @@ function createClient(config) {
675
732
  }
676
733
 
677
734
  // src/index.ts
678
- var SDK_VERSION = "0.1.0-next.0";
735
+ var SDK_VERSION = "0.1.0-next.3";
679
736
 
680
737
  exports.SDK_VERSION = SDK_VERSION;
681
738
  exports.Scorezilla = Scorezilla;