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/index.js CHANGED
@@ -30,6 +30,8 @@ function validateConfig(cfg) {
30
30
  fetch: cfg.fetch,
31
31
  timeoutMs: cfg.timeoutMs,
32
32
  maxRetries: cfg.maxRetries,
33
+ sleepImpl: cfg.sleepImpl,
34
+ warn: cfg.warn,
33
35
  userAgent: cfg.userAgent,
34
36
  auth
35
37
  };
@@ -43,24 +45,25 @@ function validatePublicKeyValue(pk) {
43
45
  }
44
46
  return pk;
45
47
  }
48
+ 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]+$/;
46
49
  function validateSecretKey(cfg) {
47
50
  if (!cfg || typeof cfg !== "object") {
48
51
  throw new Error("scorezilla/server: config must be an object with a secretKey field");
49
52
  }
50
53
  const sk = cfg.secretKey;
51
- if (!sk || typeof sk !== "object" || typeof sk.id !== "string" || typeof sk.secret !== "string") {
52
- throw new Error("scorezilla/server: secretKey must be an object with string `id` and `secret`");
53
- }
54
- if (sk.id.length === 0) {
55
- throw new Error("scorezilla/server: secretKey.id must be a non-empty string");
54
+ if (typeof sk !== "string") {
55
+ throw new Error(
56
+ `scorezilla/server: secretKey must be a single string of the shape ${SECRET_KEY_PREFIX}<keyId>_<random> (got: ${typeof sk})`
57
+ );
56
58
  }
57
- if (!sk.secret.startsWith(SECRET_KEY_PREFIX)) {
58
- const shape = `string of length ${sk.secret.length}`;
59
+ const match = SECRET_KEY_PATTERN.exec(sk);
60
+ if (!match) {
61
+ const shape = `string of length ${sk.length}`;
59
62
  throw new Error(
60
- `scorezilla/server: secretKey.secret must start with "${SECRET_KEY_PREFIX}" (live keys only) (got: ${shape})`
63
+ `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.`
61
64
  );
62
65
  }
63
- return { keyId: sk.id, secret: sk.secret };
66
+ return { keyId: match[1], secret: sk };
64
67
  }
65
68
 
66
69
  // src/paths.ts
@@ -115,7 +118,9 @@ var ScorezillaError = class _ScorezillaError extends Error {
115
118
  * {@link ScorezillaErrorCode}. For network errors, this is `'network_error'`;
116
119
  * for aborts, `'aborted'`; for timeouts, `'timeout'`. */
117
120
  code;
118
- /** Sub-classifier — present on `out_of_bounds` (`'below_min' | 'above_max'`)
121
+ /** Sub-classifier — present on:
122
+ * - `out_of_bounds`: `'below_min' | 'above_max'`
123
+ * - `usage_cap_exceeded`: `'over_cap' | 'suspended'`
119
124
  * and possibly other codes in future minor releases. */
120
125
  reason;
121
126
  /** Seconds — present on `rate_limited`. Honored by the transport's retry
@@ -128,6 +133,20 @@ var ScorezillaError = class _ScorezillaError extends Error {
128
133
  bound;
129
134
  /** Which rate-limit layer fired on `rate_limited`. */
130
135
  layer;
136
+ /** Tenant's billing tier — present on `usage_cap_exceeded`. */
137
+ tier;
138
+ /** The cap value crossed on `usage_cap_exceeded`. `0` indicates a
139
+ * suspended tenant. `undefined` on all other error codes. */
140
+ cap;
141
+ /** The post-increment submit count on `usage_cap_exceeded`. Always
142
+ * `> cap` when `reason === 'over_cap'`. */
143
+ count;
144
+ /** The period the count belongs to on `usage_cap_exceeded`, in `YYYY-MM`
145
+ * UTC form. */
146
+ period;
147
+ /** ISO-8601 timestamp of midnight UTC on the 1st of the next month —
148
+ * the counter's natural reset point on `usage_cap_exceeded`. */
149
+ resetsAt;
131
150
  /** The underlying cause (e.g., a `TypeError: fetch failed`) for
132
151
  * network/abort/timeout paths. `undefined` when the error came from a
133
152
  * successfully-parsed API error body. */
@@ -142,6 +161,11 @@ var ScorezillaError = class _ScorezillaError extends Error {
142
161
  this.requestId = truncateField(init.requestId);
143
162
  this.bound = init.bound;
144
163
  this.layer = truncateField(init.layer);
164
+ this.tier = truncateField(init.tier);
165
+ this.cap = init.cap;
166
+ this.count = init.count;
167
+ this.period = truncateField(init.period);
168
+ this.resetsAt = truncateField(init.resetsAt);
145
169
  this.cause = init.cause;
146
170
  Object.setPrototypeOf(this, _ScorezillaError.prototype);
147
171
  if (typeof Error.captureStackTrace === "function") {
@@ -155,6 +179,22 @@ var ScorezillaError = class _ScorezillaError extends Error {
155
179
  isRateLimited() {
156
180
  return this.code === "rate_limited";
157
181
  }
182
+ /**
183
+ * `true` when this error is a 402 / `usage_cap_exceeded`. The tenant
184
+ * has either hit their tier's monthly submit cap (`reason ===
185
+ * 'over_cap'`) or is suspended (`reason === 'suspended'`).
186
+ *
187
+ * Consumers SHOULD NOT auto-retry on this error — the cap doesn't lift
188
+ * until `resetsAt`. Surface to the developer with an upgrade prompt
189
+ * (over_cap) or contact-support message (suspended).
190
+ */
191
+ isUsageCapExceeded() {
192
+ return this.code === "usage_cap_exceeded";
193
+ }
194
+ /** `true` when this error is a 402 + reason 'suspended' (vs over-cap). */
195
+ isSuspended() {
196
+ return this.code === "usage_cap_exceeded" && this.reason === "suspended";
197
+ }
158
198
  /** `true` when this error is a 401 / `unauthorized` (or 403 / `forbidden`). */
159
199
  isAuth() {
160
200
  return this.code === "unauthorized" || this.code === "forbidden";
@@ -167,13 +207,21 @@ var ScorezillaError = class _ScorezillaError extends Error {
167
207
  isOutOfBounds() {
168
208
  return this.code === "out_of_bounds";
169
209
  }
170
- /** `true` for transient / retryable conditions: network errors, timeouts,
171
- * 5xx, and 429. The transport layer relies on this for its retry policy. */
210
+ /** `true` for the SDK's retryable conditions: pure network errors, 5xx, and
211
+ * 429. Deliberately excludes `timeout` and `aborted` those are caller-
212
+ * observable terminal states, not transient. Aligned with `shouldRetryError`
213
+ * in `retry.ts` so a consumer mirroring the SDK's retry policy gets the
214
+ * same answer the transport does. */
172
215
  isTransient() {
173
- if (this.status === STATUS_NETWORK_ERROR) return true;
216
+ if (this.code === "network_error") return true;
174
217
  if (this.status >= 500 && this.status < 600) return true;
175
218
  return this.isRateLimited();
176
219
  }
220
+ /** `true` when this error is a 409 / `conflict` (idempotency-key conflict
221
+ * on retry). */
222
+ isConflict() {
223
+ return this.code === "conflict";
224
+ }
177
225
  // ─── Factory ─────────────────────────────────────────────────────────
178
226
  /**
179
227
  * Build a `ScorezillaError` from a fetch round-trip outcome.
@@ -194,6 +242,13 @@ var ScorezillaError = class _ScorezillaError extends Error {
194
242
  retryAfter: body.retryAfter,
195
243
  bound: body.bound,
196
244
  layer: body.layer,
245
+ // Usage-cap fields from `ApiError` (populated by the server on
246
+ // 402 responses; undefined on other errors).
247
+ tier: body.tier,
248
+ cap: body.cap,
249
+ count: body.count,
250
+ period: body.period,
251
+ resetsAt: body.resetsAt,
197
252
  requestId,
198
253
  cause
199
254
  });
@@ -235,8 +290,10 @@ var ScorezillaError = class _ScorezillaError extends Error {
235
290
  };
236
291
  function codeForStatus(status) {
237
292
  if (status === 401) return "unauthorized";
293
+ if (status === 402) return "usage_cap_exceeded";
238
294
  if (status === 403) return "forbidden";
239
295
  if (status === 404) return "not_found";
296
+ if (status === 409) return "conflict";
240
297
  if (status === 422) return "out_of_bounds";
241
298
  if (status === 429) return "rate_limited";
242
299
  if (status >= 500) return "internal_error";
@@ -347,6 +404,7 @@ async function request(opts) {
347
404
  }
348
405
  const response = await fetchImpl(url, init);
349
406
  if (response.ok) {
407
+ warnOnDeprecationOnce(response, opts.warnImpl);
350
408
  return await parseJson(response);
351
409
  }
352
410
  const body = await safelyParseErrorBody(response);
@@ -458,6 +516,29 @@ function readRetryAfter(response) {
458
516
  const n = Number(raw);
459
517
  return Number.isFinite(n) && n >= 0 ? n : void 0;
460
518
  }
519
+ var seenDeprecations = /* @__PURE__ */ new Set();
520
+ function warnOnDeprecationOnce(response, warnImpl) {
521
+ const deprecation = response.headers.get("Deprecation");
522
+ const sunset = response.headers.get("Sunset");
523
+ if (!deprecation && !sunset) return;
524
+ const link = response.headers.get("Link") ?? "";
525
+ const key = `${deprecation ?? ""}|${sunset ?? ""}|${link}`;
526
+ if (seenDeprecations.has(key)) return;
527
+ seenDeprecations.add(key);
528
+ const detail = [];
529
+ if (deprecation === "true" || deprecation) detail.push(`Deprecation: ${deprecation}`);
530
+ if (sunset) detail.push(`Sunset: ${sunset}`);
531
+ if (link) {
532
+ const m = link.match(/<([^>]+)>/);
533
+ if (m) detail.push(`Docs: ${m[1]}`);
534
+ }
535
+ const message = `[scorezilla-sdk] API responded with deprecation signal: ${detail.join(" \xB7 ")}. Upgrade your SDK before the sunset date.`;
536
+ if (warnImpl) {
537
+ warnImpl(message);
538
+ } else {
539
+ console.warn(message);
540
+ }
541
+ }
461
542
  function combineSignalsWithTimeout(caller, timeoutMs) {
462
543
  const ctrl = new AbortController();
463
544
  let didTimeOut = false;
@@ -546,10 +627,11 @@ function validateMetadata(metadata) {
546
627
  `scorezilla: metadata exceeds ${METADATA_MAX_BYTES} bytes (got ${byteLength} bytes when JSON-stringified)`
547
628
  );
548
629
  }
630
+ return serialized;
549
631
  }
550
632
  var Scorezilla = class _Scorezilla {
551
633
  /** The package version, injected at build time from `package.json`. */
552
- static version = "0.1.0-next.1";
634
+ static version = "0.2.0";
553
635
  #config;
554
636
  #userAgent;
555
637
  #authHeader;
@@ -607,7 +689,8 @@ var Scorezilla = class _Scorezilla {
607
689
  return this.#request({
608
690
  path: submitScorePath(input.boardId),
609
691
  method: "POST",
610
- body
692
+ body,
693
+ signal: input.signal
611
694
  });
612
695
  }
613
696
  /**
@@ -631,7 +714,8 @@ var Scorezilla = class _Scorezilla {
631
714
  if (input.offset !== void 0) q.offset = input.offset;
632
715
  return this.#request({
633
716
  path: getLeaderboardPath(input.boardId, q),
634
- method: "GET"
717
+ method: "GET",
718
+ signal: input.signal
635
719
  });
636
720
  }
637
721
  /**
@@ -660,7 +744,8 @@ var Scorezilla = class _Scorezilla {
660
744
  async getPlayerRank(input) {
661
745
  return this.#request({
662
746
  path: getPlayerRankPath(input.boardId, input.playerId),
663
- method: "GET"
747
+ method: "GET",
748
+ signal: input.signal
664
749
  });
665
750
  }
666
751
  /**
@@ -685,7 +770,8 @@ var Scorezilla = class _Scorezilla {
685
770
  if (input.after !== void 0) q.after = input.after;
686
771
  return this.#request({
687
772
  path: getWindowAroundPath(input.boardId, input.playerId, q),
688
- method: "GET"
773
+ method: "GET",
774
+ signal: input.signal
689
775
  });
690
776
  }
691
777
  // ─── Internal ────────────────────────────────────────────────────────
@@ -712,10 +798,15 @@ var Scorezilla = class _Scorezilla {
712
798
  headers
713
799
  };
714
800
  if (opts.body !== void 0) requestOpts.body = opts.body;
801
+ if (opts.signal !== void 0) requestOpts.signal = opts.signal;
715
802
  if (this.#config.fetch !== void 0) requestOpts.fetchImpl = this.#config.fetch;
803
+ if (this.#config.warn !== void 0) requestOpts.warnImpl = this.#config.warn;
716
804
  if (this.#config.timeoutMs !== void 0) requestOpts.timeoutMs = this.#config.timeoutMs;
717
- if (this.#config.maxRetries !== void 0) {
718
- requestOpts.retry = { maxRetries: this.#config.maxRetries };
805
+ if (this.#config.maxRetries !== void 0 || this.#config.sleepImpl !== void 0) {
806
+ requestOpts.retry = {
807
+ ...this.#config.maxRetries !== void 0 ? { maxRetries: this.#config.maxRetries } : {},
808
+ ...this.#config.sleepImpl !== void 0 ? { sleepImpl: this.#config.sleepImpl } : {}
809
+ };
719
810
  }
720
811
  return request(requestOpts);
721
812
  }
@@ -725,7 +816,7 @@ function createClient(config) {
725
816
  }
726
817
 
727
818
  // src/index.ts
728
- var SDK_VERSION = "0.1.0-next.1";
819
+ var SDK_VERSION = "0.2.0";
729
820
 
730
821
  export { SDK_VERSION, Scorezilla, ScorezillaError, createClient, detectRuntime };
731
822
  //# sourceMappingURL=index.js.map