scorezilla 0.1.0-next.3 → 0.3.0-next.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-CUAQsaVS.js';
2
- export { R as RankedEntry, b as ScorezillaError, c as ScorezillaErrorCode } from './errors-CUAQsaVS.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 {
@@ -139,4 +145,4 @@ declare class Scorezilla {
139
145
  getWindowAround(input: GetWindowAroundInput): Promise<ApiSuccess<WindowAroundResponse>>;
140
146
  }
141
147
 
142
- 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
@@ -150,7 +150,9 @@ var ScorezillaError = class _ScorezillaError extends Error {
150
150
  * {@link ScorezillaErrorCode}. For network errors, this is `'network_error'`;
151
151
  * for aborts, `'aborted'`; for timeouts, `'timeout'`. */
152
152
  code;
153
- /** 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'`
154
156
  * and possibly other codes in future minor releases. */
155
157
  reason;
156
158
  /** Seconds — present on `rate_limited`. Honored by the transport's retry
@@ -163,6 +165,20 @@ var ScorezillaError = class _ScorezillaError extends Error {
163
165
  bound;
164
166
  /** Which rate-limit layer fired on `rate_limited`. */
165
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;
166
182
  /** The underlying cause (e.g., a `TypeError: fetch failed`) for
167
183
  * network/abort/timeout paths. `undefined` when the error came from a
168
184
  * successfully-parsed API error body. */
@@ -177,6 +193,11 @@ var ScorezillaError = class _ScorezillaError extends Error {
177
193
  this.requestId = truncateField(init.requestId);
178
194
  this.bound = init.bound;
179
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);
180
201
  this.cause = init.cause;
181
202
  Object.setPrototypeOf(this, _ScorezillaError.prototype);
182
203
  if (typeof Error.captureStackTrace === "function") {
@@ -190,6 +211,22 @@ var ScorezillaError = class _ScorezillaError extends Error {
190
211
  isRateLimited() {
191
212
  return this.code === "rate_limited";
192
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
+ }
193
230
  /** `true` when this error is a 401 / `unauthorized` (or 403 / `forbidden`). */
194
231
  isAuth() {
195
232
  return this.code === "unauthorized" || this.code === "forbidden";
@@ -202,13 +239,21 @@ var ScorezillaError = class _ScorezillaError extends Error {
202
239
  isOutOfBounds() {
203
240
  return this.code === "out_of_bounds";
204
241
  }
205
- /** `true` for transient / retryable conditions: network errors, timeouts,
206
- * 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. */
207
247
  isTransient() {
208
- if (this.status === STATUS_NETWORK_ERROR) return true;
248
+ if (this.code === "network_error") return true;
209
249
  if (this.status >= 500 && this.status < 600) return true;
210
250
  return this.isRateLimited();
211
251
  }
252
+ /** `true` when this error is a 409 / `conflict` (idempotency-key conflict
253
+ * on retry). */
254
+ isConflict() {
255
+ return this.code === "conflict";
256
+ }
212
257
  // ─── Factory ─────────────────────────────────────────────────────────
213
258
  /**
214
259
  * Build a `ScorezillaError` from a fetch round-trip outcome.
@@ -229,6 +274,13 @@ var ScorezillaError = class _ScorezillaError extends Error {
229
274
  retryAfter: body.retryAfter,
230
275
  bound: body.bound,
231
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,
232
284
  requestId,
233
285
  cause
234
286
  });
@@ -270,8 +322,10 @@ var ScorezillaError = class _ScorezillaError extends Error {
270
322
  };
271
323
  function codeForStatus(status) {
272
324
  if (status === 401) return "unauthorized";
325
+ if (status === 402) return "usage_cap_exceeded";
273
326
  if (status === 403) return "forbidden";
274
327
  if (status === 404) return "not_found";
328
+ if (status === 409) return "conflict";
275
329
  if (status === 422) return "out_of_bounds";
276
330
  if (status === 429) return "rate_limited";
277
331
  if (status >= 500) return "internal_error";
@@ -382,6 +436,7 @@ async function request(opts) {
382
436
  }
383
437
  const response = await fetchImpl(url, init);
384
438
  if (response.ok) {
439
+ warnOnDeprecationOnce(response, opts.warnImpl);
385
440
  return await parseJson(response);
386
441
  }
387
442
  const body = await safelyParseErrorBody(response);
@@ -493,6 +548,29 @@ function readRetryAfter(response) {
493
548
  const n = Number(raw);
494
549
  return Number.isFinite(n) && n >= 0 ? n : void 0;
495
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
+ }
496
574
  function combineSignalsWithTimeout(caller, timeoutMs) {
497
575
  const ctrl = new AbortController();
498
576
  let didTimeOut = false;
@@ -550,7 +628,7 @@ var Scorezilla = class _Scorezilla {
550
628
  * Mirrors the static on the public-key client so consumers can log
551
629
  * the running SDK build the same way regardless of which surface
552
630
  * they imported. */
553
- static version = "0.1.0-next.3";
631
+ static version = "0.3.0-next.0";
554
632
  #keyId;
555
633
  #secret;
556
634
  #baseUrl;
@@ -562,6 +640,7 @@ var Scorezilla = class _Scorezilla {
562
640
  #timeoutMs;
563
641
  #maxRetries;
564
642
  #sleepImpl;
643
+ #warnImpl;
565
644
  #userAgent;
566
645
  constructor(config) {
567
646
  if (isRealBrowserEnvironment()) {
@@ -584,6 +663,7 @@ var Scorezilla = class _Scorezilla {
584
663
  this.#timeoutMs = config.timeoutMs;
585
664
  this.#maxRetries = config.maxRetries;
586
665
  this.#sleepImpl = config.sleepImpl;
666
+ this.#warnImpl = config.warn;
587
667
  this.#userAgent = config.userAgent ?? defaultUserAgent(_Scorezilla.version);
588
668
  }
589
669
  /**
@@ -608,7 +688,8 @@ var Scorezilla = class _Scorezilla {
608
688
  playerId: input.playerId,
609
689
  score: input.score,
610
690
  ...input.metadata !== void 0 ? { metadata: input.metadata } : {}
611
- }
691
+ },
692
+ signal: input.signal
612
693
  });
613
694
  }
614
695
  /** Fetch the top-N entries on a board. */
@@ -618,14 +699,16 @@ var Scorezilla = class _Scorezilla {
618
699
  ...input.top !== void 0 ? { top: input.top } : {},
619
700
  ...input.offset !== void 0 ? { offset: input.offset } : {}
620
701
  }),
621
- method: "GET"
702
+ method: "GET",
703
+ signal: input.signal
622
704
  });
623
705
  }
624
706
  /** Look up a single player's rank on a board. */
625
707
  async getPlayerRank(input) {
626
708
  return this.#request({
627
709
  path: getPlayerRankPath(input.boardId, input.playerId),
628
- method: "GET"
710
+ method: "GET",
711
+ signal: input.signal
629
712
  });
630
713
  }
631
714
  /** Fetch the slice of entries surrounding a player. */
@@ -635,7 +718,8 @@ var Scorezilla = class _Scorezilla {
635
718
  ...input.before !== void 0 ? { before: input.before } : {},
636
719
  ...input.after !== void 0 ? { after: input.after } : {}
637
720
  }),
638
- method: "GET"
721
+ method: "GET",
722
+ signal: input.signal
639
723
  });
640
724
  }
641
725
  /**
@@ -666,7 +750,9 @@ var Scorezilla = class _Scorezilla {
666
750
  })
667
751
  };
668
752
  if (opts.body !== void 0) requestOpts.body = opts.body;
753
+ if (opts.signal !== void 0) requestOpts.signal = opts.signal;
669
754
  if (this.#fetchImpl !== void 0) requestOpts.fetchImpl = this.#fetchImpl;
755
+ if (this.#warnImpl !== void 0) requestOpts.warnImpl = this.#warnImpl;
670
756
  if (this.#timeoutMs !== void 0) requestOpts.timeoutMs = this.#timeoutMs;
671
757
  if (this.#maxRetries !== void 0 || this.#sleepImpl !== void 0) {
672
758
  requestOpts.retry = {