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.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 (
|
|
54
|
-
throw new Error(
|
|
55
|
-
|
|
56
|
-
|
|
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
|
-
|
|
60
|
-
|
|
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
|
|
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:
|
|
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
|
|
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
|
|
173
|
-
*
|
|
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.
|
|
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.
|
|
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 = {
|
|
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.
|
|
821
|
+
var SDK_VERSION = "0.2.0";
|
|
731
822
|
|
|
732
823
|
exports.SDK_VERSION = SDK_VERSION;
|
|
733
824
|
exports.Scorezilla = Scorezilla;
|