scorezilla 0.1.0-next.1 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/server.d.ts CHANGED
@@ -1,5 +1,5 @@
1
- import { S as SecretKeyConfig, A as ApiSuccess, a as SubmitScoreResponse, L as LeaderboardResponse, P as PlayerRankResponse, W as WindowAroundResponse } from './errors-Bp9ZDYqm.js';
2
- export { R as RankedEntry, b as ScorezillaError, c as ScorezillaErrorCode } from './errors-Bp9ZDYqm.js';
1
+ import { S as SecretKeyConfig, A as ApiSuccess, a as SubmitScoreResponse, L as LeaderboardResponse, P as PlayerRankResponse, W as WindowAroundResponse } from './errors-B7hyC-C5.js';
2
+ export { R as RankedEntry, b as ScorezillaError, c as ScorezillaErrorCode } from './errors-B7hyC-C5.js';
3
3
 
4
4
  /**
5
5
  * `scorezilla/server` — HMAC-signed adapter for game backends (#17, v0.2.0).
@@ -20,11 +20,14 @@ export { R as RankedEntry, b as ScorezillaError, c as ScorezillaErrorCode } from
20
20
  * -----------------------------------------
21
21
  * The method shape is intentionally identical — `submitScore`,
22
22
  * `getLeaderboard`, `getPlayerRank`, `getWindowAround`. The only
23
- * difference at the call site is the constructor argument:
23
+ * difference at the call site is the constructor argument: a single
24
+ * `sk_live_<keyId>_<random>` token replaces the public key (Stripe-
25
+ * style single-token format; the keyId is embedded in the plaintext
26
+ * so consumers manage one value, not two):
24
27
  *
25
28
  * import { Scorezilla } from 'scorezilla/server';
26
29
  * const sz = new Scorezilla({
27
- * secretKey: { id: 'sk-id-abc', secret: 'sk_live_…' },
30
+ * secretKey: process.env.SCOREZILLA_SECRET_KEY!, // sk_live_<keyId>_<random>
28
31
  * });
29
32
  *
30
33
  * Behind the scenes:
@@ -43,8 +46,14 @@ export { R as RankedEntry, b as ScorezillaError, c as ScorezillaErrorCode } from
43
46
  * secret key MUST NEVER leave the server.
44
47
  */
45
48
 
49
+ /** Caller-cancellable common shape — server-side too. */
50
+ interface CancellableInput {
51
+ /** Optional `AbortSignal` to cancel mid-flight. The transport composes
52
+ * it with the per-attempt timeout, so aborting always wins. */
53
+ signal?: AbortSignal | undefined;
54
+ }
46
55
  /** Input for {@link Scorezilla.submitScore}. */
47
- interface SubmitScoreInput {
56
+ interface SubmitScoreInput extends CancellableInput {
48
57
  /** UUID-typed board identifier — issued by the operator dashboard. */
49
58
  boardId: string;
50
59
  /** Stable per-player identifier (UUID, your account ID, etc.). Avoid PII. */
@@ -56,7 +65,7 @@ interface SubmitScoreInput {
56
65
  metadata?: Record<string, unknown> | undefined;
57
66
  }
58
67
  /** Input for {@link Scorezilla.getLeaderboard}. */
59
- interface GetLeaderboardInput {
68
+ interface GetLeaderboardInput extends CancellableInput {
60
69
  boardId: string;
61
70
  /** Number of entries (API caps at 1000; default 100). */
62
71
  top?: number | undefined;
@@ -64,12 +73,12 @@ interface GetLeaderboardInput {
64
73
  offset?: number | undefined;
65
74
  }
66
75
  /** Input for {@link Scorezilla.getPlayerRank}. */
67
- interface GetPlayerRankInput {
76
+ interface GetPlayerRankInput extends CancellableInput {
68
77
  boardId: string;
69
78
  playerId: string;
70
79
  }
71
80
  /** Input for {@link Scorezilla.getWindowAround}. */
72
- interface GetWindowAroundInput {
81
+ interface GetWindowAroundInput extends CancellableInput {
73
82
  boardId: string;
74
83
  playerId: string;
75
84
  /** Entries strictly above the player (API caps at 100; default 5). */
@@ -90,10 +99,7 @@ interface GetWindowAroundInput {
90
99
  * import { Scorezilla, ScorezillaError } from 'scorezilla/server';
91
100
  *
92
101
  * const sz = new Scorezilla({
93
- * secretKey: {
94
- * id: process.env.SCOREZILLA_KEY_ID!,
95
- * secret: process.env.SCOREZILLA_KEY_SECRET!,
96
- * },
102
+ * secretKey: process.env.SCOREZILLA_SECRET_KEY!, // sk_live_<keyId>_<random>
97
103
  * });
98
104
  *
99
105
  * try {
@@ -120,6 +126,15 @@ declare class Scorezilla {
120
126
  /**
121
127
  * Submit a score to a board. Signed end-to-end — the API verifies
122
128
  * before any state change.
129
+ *
130
+ * **Behavioral note vs. the public-key client**: the server adapter
131
+ * does NOT perform local `metadata` validation. The public-key
132
+ * client (`scorezilla`) validates size + structure client-side and
133
+ * fails fast; the server adapter relies on the API to reject
134
+ * malformed metadata with `invalid_input`. Trade-off: smaller bundle
135
+ * + simpler server-side logic vs. one extra network round-trip on
136
+ * caller mistakes. If you want fast-fail behavior, validate metadata
137
+ * yourself before calling this method.
123
138
  */
124
139
  submitScore(input: SubmitScoreInput): Promise<ApiSuccess<SubmitScoreResponse>>;
125
140
  /** Fetch the top-N entries on a board. */
@@ -130,4 +145,4 @@ declare class Scorezilla {
130
145
  getWindowAround(input: GetWindowAroundInput): Promise<ApiSuccess<WindowAroundResponse>>;
131
146
  }
132
147
 
133
- export { ApiSuccess, type GetLeaderboardInput, type GetPlayerRankInput, type GetWindowAroundInput, LeaderboardResponse, PlayerRankResponse, Scorezilla, type SubmitScoreInput, SubmitScoreResponse, WindowAroundResponse };
148
+ export { ApiSuccess, type CancellableInput, type GetLeaderboardInput, type GetPlayerRankInput, type GetWindowAroundInput, LeaderboardResponse, PlayerRankResponse, Scorezilla, type SubmitScoreInput, SubmitScoreResponse, WindowAroundResponse };
package/dist/server.js CHANGED
@@ -1,34 +1,46 @@
1
1
  // src/config.ts
2
2
  var DEFAULT_BASE_URL = "https://api.scorezilla.dev";
3
3
  var SECRET_KEY_PREFIX = "sk_live_";
4
+ 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]+$/;
4
5
  function validateSecretKey(cfg) {
5
6
  if (!cfg || typeof cfg !== "object") {
6
7
  throw new Error("scorezilla/server: config must be an object with a secretKey field");
7
8
  }
8
9
  const sk = cfg.secretKey;
9
- if (!sk || typeof sk !== "object" || typeof sk.id !== "string" || typeof sk.secret !== "string") {
10
- throw new Error("scorezilla/server: secretKey must be an object with string `id` and `secret`");
11
- }
12
- if (sk.id.length === 0) {
13
- throw new Error("scorezilla/server: secretKey.id must be a non-empty string");
10
+ if (typeof sk !== "string") {
11
+ throw new Error(
12
+ `scorezilla/server: secretKey must be a single string of the shape ${SECRET_KEY_PREFIX}<keyId>_<random> (got: ${typeof sk})`
13
+ );
14
14
  }
15
- if (!sk.secret.startsWith(SECRET_KEY_PREFIX)) {
16
- const shape = `string of length ${sk.secret.length}`;
15
+ const match = SECRET_KEY_PATTERN.exec(sk);
16
+ if (!match) {
17
+ const shape = `string of length ${sk.length}`;
17
18
  throw new Error(
18
- `scorezilla/server: secretKey.secret must start with "${SECRET_KEY_PREFIX}" (live keys only) (got: ${shape})`
19
+ `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.`
19
20
  );
20
21
  }
21
- return { keyId: sk.id, secret: sk.secret };
22
+ return { keyId: match[1], secret: sk };
22
23
  }
23
24
 
24
25
  // src/hmac.ts
25
26
  var enc = new TextEncoder();
26
27
  var HMAC_AUTH_SCHEME = "Scorezilla-HMAC-SHA256";
27
- async function buildSigningString(method, pathAndQuery, ts, nonce, body) {
28
+ var MIN_NONCE_LENGTH = 16;
29
+ var HMAC_SIGNING_VERSION_LATEST = 2;
30
+ async function buildSigningString(method, pathAndQuery, ts, nonce, body, host, version = HMAC_SIGNING_VERSION_LATEST) {
28
31
  const bodyHash = await sha256Hex(body);
32
+ const upperMethod = method.toUpperCase();
33
+ if (version === 1) {
34
+ return `${ts}
35
+ ${nonce}
36
+ ${upperMethod}
37
+ ${pathAndQuery}
38
+ ${bodyHash}`;
39
+ }
29
40
  return `${ts}
30
41
  ${nonce}
31
- ${method.toUpperCase()}
42
+ ${upperMethod}
43
+ ${host.toLowerCase()}
32
44
  ${pathAndQuery}
33
45
  ${bodyHash}`;
34
46
  }
@@ -49,16 +61,27 @@ async function sha256Hex(message) {
49
61
  }
50
62
  async function buildHmacAuthHeader(args) {
51
63
  const ts = args.nowSeconds ?? Math.floor(Date.now() / 1e3);
64
+ if (args.nonce !== void 0) {
65
+ if (typeof args.nonce !== "string" || args.nonce.length < MIN_NONCE_LENGTH) {
66
+ throw new Error(
67
+ `scorezilla: nonce must be a string of at least ${MIN_NONCE_LENGTH} characters; use the default (UUIDv4 via crypto.randomUUID) unless you have a specific reason.`
68
+ );
69
+ }
70
+ }
52
71
  const nonce = args.nonce ?? generateNonce();
72
+ const version = args.version ?? HMAC_SIGNING_VERSION_LATEST;
53
73
  const signingString = await buildSigningString(
54
74
  args.method,
55
75
  args.pathAndQuery,
56
76
  ts,
57
77
  nonce,
58
- args.body
78
+ args.body,
79
+ args.host,
80
+ version
59
81
  );
60
82
  const signature = await hmacSha256B64u(args.secret, signingString);
61
- return `${HMAC_AUTH_SCHEME} keyId=${args.keyId}, ts=${ts}, nonce=${nonce}, signature=${signature}`;
83
+ const vParam = version === 1 ? "" : `, v=${version}`;
84
+ return `${HMAC_AUTH_SCHEME} keyId=${args.keyId}, ts=${ts}, nonce=${nonce}, signature=${signature}${vParam}`;
62
85
  }
63
86
  function generateNonce() {
64
87
  const c = globalThis.crypto;
@@ -127,7 +150,9 @@ var ScorezillaError = class _ScorezillaError extends Error {
127
150
  * {@link ScorezillaErrorCode}. For network errors, this is `'network_error'`;
128
151
  * for aborts, `'aborted'`; for timeouts, `'timeout'`. */
129
152
  code;
130
- /** Sub-classifier — present on `out_of_bounds` (`'below_min' | 'above_max'`)
153
+ /** Sub-classifier — present on:
154
+ * - `out_of_bounds`: `'below_min' | 'above_max'`
155
+ * - `usage_cap_exceeded`: `'over_cap' | 'suspended'`
131
156
  * and possibly other codes in future minor releases. */
132
157
  reason;
133
158
  /** Seconds — present on `rate_limited`. Honored by the transport's retry
@@ -140,6 +165,20 @@ var ScorezillaError = class _ScorezillaError extends Error {
140
165
  bound;
141
166
  /** Which rate-limit layer fired on `rate_limited`. */
142
167
  layer;
168
+ /** Tenant's billing tier — present on `usage_cap_exceeded`. */
169
+ tier;
170
+ /** The cap value crossed on `usage_cap_exceeded`. `0` indicates a
171
+ * suspended tenant. `undefined` on all other error codes. */
172
+ cap;
173
+ /** The post-increment submit count on `usage_cap_exceeded`. Always
174
+ * `> cap` when `reason === 'over_cap'`. */
175
+ count;
176
+ /** The period the count belongs to on `usage_cap_exceeded`, in `YYYY-MM`
177
+ * UTC form. */
178
+ period;
179
+ /** ISO-8601 timestamp of midnight UTC on the 1st of the next month —
180
+ * the counter's natural reset point on `usage_cap_exceeded`. */
181
+ resetsAt;
143
182
  /** The underlying cause (e.g., a `TypeError: fetch failed`) for
144
183
  * network/abort/timeout paths. `undefined` when the error came from a
145
184
  * successfully-parsed API error body. */
@@ -154,6 +193,11 @@ var ScorezillaError = class _ScorezillaError extends Error {
154
193
  this.requestId = truncateField(init.requestId);
155
194
  this.bound = init.bound;
156
195
  this.layer = truncateField(init.layer);
196
+ this.tier = truncateField(init.tier);
197
+ this.cap = init.cap;
198
+ this.count = init.count;
199
+ this.period = truncateField(init.period);
200
+ this.resetsAt = truncateField(init.resetsAt);
157
201
  this.cause = init.cause;
158
202
  Object.setPrototypeOf(this, _ScorezillaError.prototype);
159
203
  if (typeof Error.captureStackTrace === "function") {
@@ -167,6 +211,22 @@ var ScorezillaError = class _ScorezillaError extends Error {
167
211
  isRateLimited() {
168
212
  return this.code === "rate_limited";
169
213
  }
214
+ /**
215
+ * `true` when this error is a 402 / `usage_cap_exceeded`. The tenant
216
+ * has either hit their tier's monthly submit cap (`reason ===
217
+ * 'over_cap'`) or is suspended (`reason === 'suspended'`).
218
+ *
219
+ * Consumers SHOULD NOT auto-retry on this error — the cap doesn't lift
220
+ * until `resetsAt`. Surface to the developer with an upgrade prompt
221
+ * (over_cap) or contact-support message (suspended).
222
+ */
223
+ isUsageCapExceeded() {
224
+ return this.code === "usage_cap_exceeded";
225
+ }
226
+ /** `true` when this error is a 402 + reason 'suspended' (vs over-cap). */
227
+ isSuspended() {
228
+ return this.code === "usage_cap_exceeded" && this.reason === "suspended";
229
+ }
170
230
  /** `true` when this error is a 401 / `unauthorized` (or 403 / `forbidden`). */
171
231
  isAuth() {
172
232
  return this.code === "unauthorized" || this.code === "forbidden";
@@ -179,13 +239,21 @@ var ScorezillaError = class _ScorezillaError extends Error {
179
239
  isOutOfBounds() {
180
240
  return this.code === "out_of_bounds";
181
241
  }
182
- /** `true` for transient / retryable conditions: network errors, timeouts,
183
- * 5xx, and 429. The transport layer relies on this for its retry policy. */
242
+ /** `true` for the SDK's retryable conditions: pure network errors, 5xx, and
243
+ * 429. Deliberately excludes `timeout` and `aborted` those are caller-
244
+ * observable terminal states, not transient. Aligned with `shouldRetryError`
245
+ * in `retry.ts` so a consumer mirroring the SDK's retry policy gets the
246
+ * same answer the transport does. */
184
247
  isTransient() {
185
- if (this.status === STATUS_NETWORK_ERROR) return true;
248
+ if (this.code === "network_error") return true;
186
249
  if (this.status >= 500 && this.status < 600) return true;
187
250
  return this.isRateLimited();
188
251
  }
252
+ /** `true` when this error is a 409 / `conflict` (idempotency-key conflict
253
+ * on retry). */
254
+ isConflict() {
255
+ return this.code === "conflict";
256
+ }
189
257
  // ─── Factory ─────────────────────────────────────────────────────────
190
258
  /**
191
259
  * Build a `ScorezillaError` from a fetch round-trip outcome.
@@ -206,6 +274,13 @@ var ScorezillaError = class _ScorezillaError extends Error {
206
274
  retryAfter: body.retryAfter,
207
275
  bound: body.bound,
208
276
  layer: body.layer,
277
+ // Usage-cap fields from `ApiError` (populated by the server on
278
+ // 402 responses; undefined on other errors).
279
+ tier: body.tier,
280
+ cap: body.cap,
281
+ count: body.count,
282
+ period: body.period,
283
+ resetsAt: body.resetsAt,
209
284
  requestId,
210
285
  cause
211
286
  });
@@ -247,8 +322,10 @@ var ScorezillaError = class _ScorezillaError extends Error {
247
322
  };
248
323
  function codeForStatus(status) {
249
324
  if (status === 401) return "unauthorized";
325
+ if (status === 402) return "usage_cap_exceeded";
250
326
  if (status === 403) return "forbidden";
251
327
  if (status === 404) return "not_found";
328
+ if (status === 409) return "conflict";
252
329
  if (status === 422) return "out_of_bounds";
253
330
  if (status === 429) return "rate_limited";
254
331
  if (status >= 500) return "internal_error";
@@ -359,6 +436,7 @@ async function request(opts) {
359
436
  }
360
437
  const response = await fetchImpl(url, init);
361
438
  if (response.ok) {
439
+ warnOnDeprecationOnce(response, opts.warnImpl);
362
440
  return await parseJson(response);
363
441
  }
364
442
  const body = await safelyParseErrorBody(response);
@@ -470,6 +548,29 @@ function readRetryAfter(response) {
470
548
  const n = Number(raw);
471
549
  return Number.isFinite(n) && n >= 0 ? n : void 0;
472
550
  }
551
+ var seenDeprecations = /* @__PURE__ */ new Set();
552
+ function warnOnDeprecationOnce(response, warnImpl) {
553
+ const deprecation = response.headers.get("Deprecation");
554
+ const sunset = response.headers.get("Sunset");
555
+ if (!deprecation && !sunset) return;
556
+ const link = response.headers.get("Link") ?? "";
557
+ const key = `${deprecation ?? ""}|${sunset ?? ""}|${link}`;
558
+ if (seenDeprecations.has(key)) return;
559
+ seenDeprecations.add(key);
560
+ const detail = [];
561
+ if (deprecation === "true" || deprecation) detail.push(`Deprecation: ${deprecation}`);
562
+ if (sunset) detail.push(`Sunset: ${sunset}`);
563
+ if (link) {
564
+ const m = link.match(/<([^>]+)>/);
565
+ if (m) detail.push(`Docs: ${m[1]}`);
566
+ }
567
+ const message = `[scorezilla-sdk] API responded with deprecation signal: ${detail.join(" \xB7 ")}. Upgrade your SDK before the sunset date.`;
568
+ if (warnImpl) {
569
+ warnImpl(message);
570
+ } else {
571
+ console.warn(message);
572
+ }
573
+ }
473
574
  function combineSignalsWithTimeout(caller, timeoutMs) {
474
575
  const ctrl = new AbortController();
475
576
  let didTimeOut = false;
@@ -527,27 +628,56 @@ var Scorezilla = class _Scorezilla {
527
628
  * Mirrors the static on the public-key client so consumers can log
528
629
  * the running SDK build the same way regardless of which surface
529
630
  * they imported. */
530
- static version = "0.1.0-next.1";
631
+ static version = "0.2.0";
531
632
  #keyId;
532
633
  #secret;
533
634
  #baseUrl;
635
+ /** Host portion of `#baseUrl` (e.g. "api.scorezilla.dev"). Captured at
636
+ * construction so every signed request binds to this exact origin —
637
+ * see `buildSigningString` v=2 in `hmac.ts`. */
638
+ #host;
534
639
  #fetchImpl;
535
640
  #timeoutMs;
536
641
  #maxRetries;
642
+ #sleepImpl;
643
+ #warnImpl;
537
644
  #userAgent;
538
645
  constructor(config) {
646
+ if (isRealBrowserEnvironment()) {
647
+ throw new Error(
648
+ "scorezilla/server: this adapter is server-only and must not run in browsers. Your bundler is shipping `scorezilla/server` into a browser bundle \u2014 check that it honors the `browser` export condition in package.json. Use the public-key client (`import { Scorezilla } from 'scorezilla'`) from browser code."
649
+ );
650
+ }
539
651
  const resolved = validateSecretKey(config);
540
652
  this.#keyId = resolved.keyId;
541
653
  this.#secret = resolved.secret;
542
654
  this.#baseUrl = (config.baseUrl ?? DEFAULT_BASE_URL).replace(/\/+$/, "");
655
+ try {
656
+ this.#host = new URL(this.#baseUrl).host;
657
+ } catch {
658
+ throw new Error(
659
+ `scorezilla/server: baseUrl must be a valid absolute URL (got: ${this.#baseUrl})`
660
+ );
661
+ }
543
662
  this.#fetchImpl = config.fetch;
544
663
  this.#timeoutMs = config.timeoutMs;
545
664
  this.#maxRetries = config.maxRetries;
665
+ this.#sleepImpl = config.sleepImpl;
666
+ this.#warnImpl = config.warn;
546
667
  this.#userAgent = config.userAgent ?? defaultUserAgent(_Scorezilla.version);
547
668
  }
548
669
  /**
549
670
  * Submit a score to a board. Signed end-to-end — the API verifies
550
671
  * before any state change.
672
+ *
673
+ * **Behavioral note vs. the public-key client**: the server adapter
674
+ * does NOT perform local `metadata` validation. The public-key
675
+ * client (`scorezilla`) validates size + structure client-side and
676
+ * fails fast; the server adapter relies on the API to reject
677
+ * malformed metadata with `invalid_input`. Trade-off: smaller bundle
678
+ * + simpler server-side logic vs. one extra network round-trip on
679
+ * caller mistakes. If you want fast-fail behavior, validate metadata
680
+ * yourself before calling this method.
551
681
  */
552
682
  async submitScore(input) {
553
683
  return this.#request({
@@ -558,7 +688,8 @@ var Scorezilla = class _Scorezilla {
558
688
  playerId: input.playerId,
559
689
  score: input.score,
560
690
  ...input.metadata !== void 0 ? { metadata: input.metadata } : {}
561
- }
691
+ },
692
+ signal: input.signal
562
693
  });
563
694
  }
564
695
  /** Fetch the top-N entries on a board. */
@@ -568,14 +699,16 @@ var Scorezilla = class _Scorezilla {
568
699
  ...input.top !== void 0 ? { top: input.top } : {},
569
700
  ...input.offset !== void 0 ? { offset: input.offset } : {}
570
701
  }),
571
- method: "GET"
702
+ method: "GET",
703
+ signal: input.signal
572
704
  });
573
705
  }
574
706
  /** Look up a single player's rank on a board. */
575
707
  async getPlayerRank(input) {
576
708
  return this.#request({
577
709
  path: getPlayerRankPath(input.boardId, input.playerId),
578
- method: "GET"
710
+ method: "GET",
711
+ signal: input.signal
579
712
  });
580
713
  }
581
714
  /** Fetch the slice of entries surrounding a player. */
@@ -585,7 +718,8 @@ var Scorezilla = class _Scorezilla {
585
718
  ...input.before !== void 0 ? { before: input.before } : {},
586
719
  ...input.after !== void 0 ? { after: input.after } : {}
587
720
  }),
588
- method: "GET"
721
+ method: "GET",
722
+ signal: input.signal
589
723
  });
590
724
  }
591
725
  /**
@@ -611,18 +745,31 @@ var Scorezilla = class _Scorezilla {
611
745
  secret: this.#secret,
612
746
  method,
613
747
  pathAndQuery,
748
+ host: this.#host,
614
749
  body
615
750
  })
616
751
  };
617
752
  if (opts.body !== void 0) requestOpts.body = opts.body;
753
+ if (opts.signal !== void 0) requestOpts.signal = opts.signal;
618
754
  if (this.#fetchImpl !== void 0) requestOpts.fetchImpl = this.#fetchImpl;
755
+ if (this.#warnImpl !== void 0) requestOpts.warnImpl = this.#warnImpl;
619
756
  if (this.#timeoutMs !== void 0) requestOpts.timeoutMs = this.#timeoutMs;
620
- if (this.#maxRetries !== void 0) {
621
- requestOpts.retry = { maxRetries: this.#maxRetries };
757
+ if (this.#maxRetries !== void 0 || this.#sleepImpl !== void 0) {
758
+ requestOpts.retry = {
759
+ ...this.#maxRetries !== void 0 ? { maxRetries: this.#maxRetries } : {},
760
+ ...this.#sleepImpl !== void 0 ? { sleepImpl: this.#sleepImpl } : {}
761
+ };
622
762
  }
623
763
  return request(requestOpts);
624
764
  }
625
765
  };
766
+ function isRealBrowserEnvironment() {
767
+ const g = globalThis;
768
+ const hasBrowserGlobals = typeof g.window !== "undefined" && typeof g.document !== "undefined";
769
+ if (!hasBrowserGlobals) return false;
770
+ const hasNodeLikeHost = Boolean(g.process?.versions?.node) || typeof g.Deno !== "undefined" || typeof g.Bun !== "undefined";
771
+ return !hasNodeLikeHost;
772
+ }
626
773
 
627
774
  export { Scorezilla, ScorezillaError };
628
775
  //# sourceMappingURL=server.js.map