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