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/CHANGELOG.md +222 -0
- package/README.md +17 -4
- package/dist/{errors-Bp9ZDYqm.d.cts → errors-B7hyC-C5.d.cts} +94 -12
- package/dist/{errors-Bp9ZDYqm.d.ts → errors-B7hyC-C5.d.ts} +94 -12
- package/dist/index.cjs +112 -21
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +18 -6
- package/dist/index.d.ts +18 -6
- package/dist/index.js +112 -21
- package/dist/index.js.map +1 -1
- package/dist/server.cjs +171 -24
- package/dist/server.cjs.map +1 -1
- package/dist/server.d.cts +28 -13
- package/dist/server.d.ts +28 -13
- package/dist/server.js +171 -24
- package/dist/server.js.map +1 -1
- package/package.json +5 -2
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 (
|
|
52
|
-
throw new Error(
|
|
53
|
-
|
|
54
|
-
|
|
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
|
-
|
|
58
|
-
|
|
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
|
|
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:
|
|
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
|
|
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
|
|
171
|
-
*
|
|
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.
|
|
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.
|
|
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 = {
|
|
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.
|
|
819
|
+
var SDK_VERSION = "0.2.0";
|
|
729
820
|
|
|
730
821
|
export { SDK_VERSION, Scorezilla, ScorezillaError, createClient, detectRuntime };
|
|
731
822
|
//# sourceMappingURL=index.js.map
|