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/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-
|
|
2
|
-
export { R as RankedEntry, b as ScorezillaError, c as ScorezillaErrorCode } from './errors-
|
|
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:
|
|
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 {
|
|
@@ -120,6 +126,15 @@ declare class Scorezilla {
|
|
|
120
126
|
/**
|
|
121
127
|
* Submit a score to a board. Signed end-to-end — the API verifies
|
|
122
128
|
* before any state change.
|
|
129
|
+
*
|
|
130
|
+
* **Behavioral note vs. the public-key client**: the server adapter
|
|
131
|
+
* does NOT perform local `metadata` validation. The public-key
|
|
132
|
+
* client (`scorezilla`) validates size + structure client-side and
|
|
133
|
+
* fails fast; the server adapter relies on the API to reject
|
|
134
|
+
* malformed metadata with `invalid_input`. Trade-off: smaller bundle
|
|
135
|
+
* + simpler server-side logic vs. one extra network round-trip on
|
|
136
|
+
* caller mistakes. If you want fast-fail behavior, validate metadata
|
|
137
|
+
* yourself before calling this method.
|
|
123
138
|
*/
|
|
124
139
|
submitScore(input: SubmitScoreInput): Promise<ApiSuccess<SubmitScoreResponse>>;
|
|
125
140
|
/** Fetch the top-N entries on a board. */
|
|
@@ -130,4 +145,4 @@ declare class Scorezilla {
|
|
|
130
145
|
getWindowAround(input: GetWindowAroundInput): Promise<ApiSuccess<WindowAroundResponse>>;
|
|
131
146
|
}
|
|
132
147
|
|
|
133
|
-
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
|
@@ -1,34 +1,46 @@
|
|
|
1
1
|
// src/config.ts
|
|
2
2
|
var DEFAULT_BASE_URL = "https://api.scorezilla.dev";
|
|
3
3
|
var SECRET_KEY_PREFIX = "sk_live_";
|
|
4
|
+
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]+$/;
|
|
4
5
|
function validateSecretKey(cfg) {
|
|
5
6
|
if (!cfg || typeof cfg !== "object") {
|
|
6
7
|
throw new Error("scorezilla/server: config must be an object with a secretKey field");
|
|
7
8
|
}
|
|
8
9
|
const sk = cfg.secretKey;
|
|
9
|
-
if (
|
|
10
|
-
throw new Error(
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
throw new Error("scorezilla/server: secretKey.id must be a non-empty string");
|
|
10
|
+
if (typeof sk !== "string") {
|
|
11
|
+
throw new Error(
|
|
12
|
+
`scorezilla/server: secretKey must be a single string of the shape ${SECRET_KEY_PREFIX}<keyId>_<random> (got: ${typeof sk})`
|
|
13
|
+
);
|
|
14
14
|
}
|
|
15
|
-
|
|
16
|
-
|
|
15
|
+
const match = SECRET_KEY_PATTERN.exec(sk);
|
|
16
|
+
if (!match) {
|
|
17
|
+
const shape = `string of length ${sk.length}`;
|
|
17
18
|
throw new Error(
|
|
18
|
-
`scorezilla/server: secretKey
|
|
19
|
+
`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.`
|
|
19
20
|
);
|
|
20
21
|
}
|
|
21
|
-
return { keyId:
|
|
22
|
+
return { keyId: match[1], secret: sk };
|
|
22
23
|
}
|
|
23
24
|
|
|
24
25
|
// src/hmac.ts
|
|
25
26
|
var enc = new TextEncoder();
|
|
26
27
|
var HMAC_AUTH_SCHEME = "Scorezilla-HMAC-SHA256";
|
|
27
|
-
|
|
28
|
+
var MIN_NONCE_LENGTH = 16;
|
|
29
|
+
var HMAC_SIGNING_VERSION_LATEST = 2;
|
|
30
|
+
async function buildSigningString(method, pathAndQuery, ts, nonce, body, host, version = HMAC_SIGNING_VERSION_LATEST) {
|
|
28
31
|
const bodyHash = await sha256Hex(body);
|
|
32
|
+
const upperMethod = method.toUpperCase();
|
|
33
|
+
if (version === 1) {
|
|
34
|
+
return `${ts}
|
|
35
|
+
${nonce}
|
|
36
|
+
${upperMethod}
|
|
37
|
+
${pathAndQuery}
|
|
38
|
+
${bodyHash}`;
|
|
39
|
+
}
|
|
29
40
|
return `${ts}
|
|
30
41
|
${nonce}
|
|
31
|
-
${
|
|
42
|
+
${upperMethod}
|
|
43
|
+
${host.toLowerCase()}
|
|
32
44
|
${pathAndQuery}
|
|
33
45
|
${bodyHash}`;
|
|
34
46
|
}
|
|
@@ -49,16 +61,27 @@ async function sha256Hex(message) {
|
|
|
49
61
|
}
|
|
50
62
|
async function buildHmacAuthHeader(args) {
|
|
51
63
|
const ts = args.nowSeconds ?? Math.floor(Date.now() / 1e3);
|
|
64
|
+
if (args.nonce !== void 0) {
|
|
65
|
+
if (typeof args.nonce !== "string" || args.nonce.length < MIN_NONCE_LENGTH) {
|
|
66
|
+
throw new Error(
|
|
67
|
+
`scorezilla: nonce must be a string of at least ${MIN_NONCE_LENGTH} characters; use the default (UUIDv4 via crypto.randomUUID) unless you have a specific reason.`
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
52
71
|
const nonce = args.nonce ?? generateNonce();
|
|
72
|
+
const version = args.version ?? HMAC_SIGNING_VERSION_LATEST;
|
|
53
73
|
const signingString = await buildSigningString(
|
|
54
74
|
args.method,
|
|
55
75
|
args.pathAndQuery,
|
|
56
76
|
ts,
|
|
57
77
|
nonce,
|
|
58
|
-
args.body
|
|
78
|
+
args.body,
|
|
79
|
+
args.host,
|
|
80
|
+
version
|
|
59
81
|
);
|
|
60
82
|
const signature = await hmacSha256B64u(args.secret, signingString);
|
|
61
|
-
|
|
83
|
+
const vParam = version === 1 ? "" : `, v=${version}`;
|
|
84
|
+
return `${HMAC_AUTH_SCHEME} keyId=${args.keyId}, ts=${ts}, nonce=${nonce}, signature=${signature}${vParam}`;
|
|
62
85
|
}
|
|
63
86
|
function generateNonce() {
|
|
64
87
|
const c = globalThis.crypto;
|
|
@@ -127,7 +150,9 @@ var ScorezillaError = class _ScorezillaError extends Error {
|
|
|
127
150
|
* {@link ScorezillaErrorCode}. For network errors, this is `'network_error'`;
|
|
128
151
|
* for aborts, `'aborted'`; for timeouts, `'timeout'`. */
|
|
129
152
|
code;
|
|
130
|
-
/** Sub-classifier — present on
|
|
153
|
+
/** Sub-classifier — present on:
|
|
154
|
+
* - `out_of_bounds`: `'below_min' | 'above_max'`
|
|
155
|
+
* - `usage_cap_exceeded`: `'over_cap' | 'suspended'`
|
|
131
156
|
* and possibly other codes in future minor releases. */
|
|
132
157
|
reason;
|
|
133
158
|
/** Seconds — present on `rate_limited`. Honored by the transport's retry
|
|
@@ -140,6 +165,20 @@ var ScorezillaError = class _ScorezillaError extends Error {
|
|
|
140
165
|
bound;
|
|
141
166
|
/** Which rate-limit layer fired on `rate_limited`. */
|
|
142
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;
|
|
143
182
|
/** The underlying cause (e.g., a `TypeError: fetch failed`) for
|
|
144
183
|
* network/abort/timeout paths. `undefined` when the error came from a
|
|
145
184
|
* successfully-parsed API error body. */
|
|
@@ -154,6 +193,11 @@ var ScorezillaError = class _ScorezillaError extends Error {
|
|
|
154
193
|
this.requestId = truncateField(init.requestId);
|
|
155
194
|
this.bound = init.bound;
|
|
156
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);
|
|
157
201
|
this.cause = init.cause;
|
|
158
202
|
Object.setPrototypeOf(this, _ScorezillaError.prototype);
|
|
159
203
|
if (typeof Error.captureStackTrace === "function") {
|
|
@@ -167,6 +211,22 @@ var ScorezillaError = class _ScorezillaError extends Error {
|
|
|
167
211
|
isRateLimited() {
|
|
168
212
|
return this.code === "rate_limited";
|
|
169
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
|
+
}
|
|
170
230
|
/** `true` when this error is a 401 / `unauthorized` (or 403 / `forbidden`). */
|
|
171
231
|
isAuth() {
|
|
172
232
|
return this.code === "unauthorized" || this.code === "forbidden";
|
|
@@ -179,13 +239,21 @@ var ScorezillaError = class _ScorezillaError extends Error {
|
|
|
179
239
|
isOutOfBounds() {
|
|
180
240
|
return this.code === "out_of_bounds";
|
|
181
241
|
}
|
|
182
|
-
/** `true` for
|
|
183
|
-
*
|
|
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. */
|
|
184
247
|
isTransient() {
|
|
185
|
-
if (this.
|
|
248
|
+
if (this.code === "network_error") return true;
|
|
186
249
|
if (this.status >= 500 && this.status < 600) return true;
|
|
187
250
|
return this.isRateLimited();
|
|
188
251
|
}
|
|
252
|
+
/** `true` when this error is a 409 / `conflict` (idempotency-key conflict
|
|
253
|
+
* on retry). */
|
|
254
|
+
isConflict() {
|
|
255
|
+
return this.code === "conflict";
|
|
256
|
+
}
|
|
189
257
|
// ─── Factory ─────────────────────────────────────────────────────────
|
|
190
258
|
/**
|
|
191
259
|
* Build a `ScorezillaError` from a fetch round-trip outcome.
|
|
@@ -206,6 +274,13 @@ var ScorezillaError = class _ScorezillaError extends Error {
|
|
|
206
274
|
retryAfter: body.retryAfter,
|
|
207
275
|
bound: body.bound,
|
|
208
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,
|
|
209
284
|
requestId,
|
|
210
285
|
cause
|
|
211
286
|
});
|
|
@@ -247,8 +322,10 @@ var ScorezillaError = class _ScorezillaError extends Error {
|
|
|
247
322
|
};
|
|
248
323
|
function codeForStatus(status) {
|
|
249
324
|
if (status === 401) return "unauthorized";
|
|
325
|
+
if (status === 402) return "usage_cap_exceeded";
|
|
250
326
|
if (status === 403) return "forbidden";
|
|
251
327
|
if (status === 404) return "not_found";
|
|
328
|
+
if (status === 409) return "conflict";
|
|
252
329
|
if (status === 422) return "out_of_bounds";
|
|
253
330
|
if (status === 429) return "rate_limited";
|
|
254
331
|
if (status >= 500) return "internal_error";
|
|
@@ -359,6 +436,7 @@ async function request(opts) {
|
|
|
359
436
|
}
|
|
360
437
|
const response = await fetchImpl(url, init);
|
|
361
438
|
if (response.ok) {
|
|
439
|
+
warnOnDeprecationOnce(response, opts.warnImpl);
|
|
362
440
|
return await parseJson(response);
|
|
363
441
|
}
|
|
364
442
|
const body = await safelyParseErrorBody(response);
|
|
@@ -470,6 +548,29 @@ function readRetryAfter(response) {
|
|
|
470
548
|
const n = Number(raw);
|
|
471
549
|
return Number.isFinite(n) && n >= 0 ? n : void 0;
|
|
472
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
|
+
}
|
|
473
574
|
function combineSignalsWithTimeout(caller, timeoutMs) {
|
|
474
575
|
const ctrl = new AbortController();
|
|
475
576
|
let didTimeOut = false;
|
|
@@ -527,27 +628,56 @@ var Scorezilla = class _Scorezilla {
|
|
|
527
628
|
* Mirrors the static on the public-key client so consumers can log
|
|
528
629
|
* the running SDK build the same way regardless of which surface
|
|
529
630
|
* they imported. */
|
|
530
|
-
static version = "0.
|
|
631
|
+
static version = "0.2.0";
|
|
531
632
|
#keyId;
|
|
532
633
|
#secret;
|
|
533
634
|
#baseUrl;
|
|
635
|
+
/** Host portion of `#baseUrl` (e.g. "api.scorezilla.dev"). Captured at
|
|
636
|
+
* construction so every signed request binds to this exact origin —
|
|
637
|
+
* see `buildSigningString` v=2 in `hmac.ts`. */
|
|
638
|
+
#host;
|
|
534
639
|
#fetchImpl;
|
|
535
640
|
#timeoutMs;
|
|
536
641
|
#maxRetries;
|
|
642
|
+
#sleepImpl;
|
|
643
|
+
#warnImpl;
|
|
537
644
|
#userAgent;
|
|
538
645
|
constructor(config) {
|
|
646
|
+
if (isRealBrowserEnvironment()) {
|
|
647
|
+
throw new Error(
|
|
648
|
+
"scorezilla/server: this adapter is server-only and must not run in browsers. Your bundler is shipping `scorezilla/server` into a browser bundle \u2014 check that it honors the `browser` export condition in package.json. Use the public-key client (`import { Scorezilla } from 'scorezilla'`) from browser code."
|
|
649
|
+
);
|
|
650
|
+
}
|
|
539
651
|
const resolved = validateSecretKey(config);
|
|
540
652
|
this.#keyId = resolved.keyId;
|
|
541
653
|
this.#secret = resolved.secret;
|
|
542
654
|
this.#baseUrl = (config.baseUrl ?? DEFAULT_BASE_URL).replace(/\/+$/, "");
|
|
655
|
+
try {
|
|
656
|
+
this.#host = new URL(this.#baseUrl).host;
|
|
657
|
+
} catch {
|
|
658
|
+
throw new Error(
|
|
659
|
+
`scorezilla/server: baseUrl must be a valid absolute URL (got: ${this.#baseUrl})`
|
|
660
|
+
);
|
|
661
|
+
}
|
|
543
662
|
this.#fetchImpl = config.fetch;
|
|
544
663
|
this.#timeoutMs = config.timeoutMs;
|
|
545
664
|
this.#maxRetries = config.maxRetries;
|
|
665
|
+
this.#sleepImpl = config.sleepImpl;
|
|
666
|
+
this.#warnImpl = config.warn;
|
|
546
667
|
this.#userAgent = config.userAgent ?? defaultUserAgent(_Scorezilla.version);
|
|
547
668
|
}
|
|
548
669
|
/**
|
|
549
670
|
* Submit a score to a board. Signed end-to-end — the API verifies
|
|
550
671
|
* before any state change.
|
|
672
|
+
*
|
|
673
|
+
* **Behavioral note vs. the public-key client**: the server adapter
|
|
674
|
+
* does NOT perform local `metadata` validation. The public-key
|
|
675
|
+
* client (`scorezilla`) validates size + structure client-side and
|
|
676
|
+
* fails fast; the server adapter relies on the API to reject
|
|
677
|
+
* malformed metadata with `invalid_input`. Trade-off: smaller bundle
|
|
678
|
+
* + simpler server-side logic vs. one extra network round-trip on
|
|
679
|
+
* caller mistakes. If you want fast-fail behavior, validate metadata
|
|
680
|
+
* yourself before calling this method.
|
|
551
681
|
*/
|
|
552
682
|
async submitScore(input) {
|
|
553
683
|
return this.#request({
|
|
@@ -558,7 +688,8 @@ var Scorezilla = class _Scorezilla {
|
|
|
558
688
|
playerId: input.playerId,
|
|
559
689
|
score: input.score,
|
|
560
690
|
...input.metadata !== void 0 ? { metadata: input.metadata } : {}
|
|
561
|
-
}
|
|
691
|
+
},
|
|
692
|
+
signal: input.signal
|
|
562
693
|
});
|
|
563
694
|
}
|
|
564
695
|
/** Fetch the top-N entries on a board. */
|
|
@@ -568,14 +699,16 @@ var Scorezilla = class _Scorezilla {
|
|
|
568
699
|
...input.top !== void 0 ? { top: input.top } : {},
|
|
569
700
|
...input.offset !== void 0 ? { offset: input.offset } : {}
|
|
570
701
|
}),
|
|
571
|
-
method: "GET"
|
|
702
|
+
method: "GET",
|
|
703
|
+
signal: input.signal
|
|
572
704
|
});
|
|
573
705
|
}
|
|
574
706
|
/** Look up a single player's rank on a board. */
|
|
575
707
|
async getPlayerRank(input) {
|
|
576
708
|
return this.#request({
|
|
577
709
|
path: getPlayerRankPath(input.boardId, input.playerId),
|
|
578
|
-
method: "GET"
|
|
710
|
+
method: "GET",
|
|
711
|
+
signal: input.signal
|
|
579
712
|
});
|
|
580
713
|
}
|
|
581
714
|
/** Fetch the slice of entries surrounding a player. */
|
|
@@ -585,7 +718,8 @@ var Scorezilla = class _Scorezilla {
|
|
|
585
718
|
...input.before !== void 0 ? { before: input.before } : {},
|
|
586
719
|
...input.after !== void 0 ? { after: input.after } : {}
|
|
587
720
|
}),
|
|
588
|
-
method: "GET"
|
|
721
|
+
method: "GET",
|
|
722
|
+
signal: input.signal
|
|
589
723
|
});
|
|
590
724
|
}
|
|
591
725
|
/**
|
|
@@ -611,18 +745,31 @@ var Scorezilla = class _Scorezilla {
|
|
|
611
745
|
secret: this.#secret,
|
|
612
746
|
method,
|
|
613
747
|
pathAndQuery,
|
|
748
|
+
host: this.#host,
|
|
614
749
|
body
|
|
615
750
|
})
|
|
616
751
|
};
|
|
617
752
|
if (opts.body !== void 0) requestOpts.body = opts.body;
|
|
753
|
+
if (opts.signal !== void 0) requestOpts.signal = opts.signal;
|
|
618
754
|
if (this.#fetchImpl !== void 0) requestOpts.fetchImpl = this.#fetchImpl;
|
|
755
|
+
if (this.#warnImpl !== void 0) requestOpts.warnImpl = this.#warnImpl;
|
|
619
756
|
if (this.#timeoutMs !== void 0) requestOpts.timeoutMs = this.#timeoutMs;
|
|
620
|
-
if (this.#maxRetries !== void 0) {
|
|
621
|
-
requestOpts.retry = {
|
|
757
|
+
if (this.#maxRetries !== void 0 || this.#sleepImpl !== void 0) {
|
|
758
|
+
requestOpts.retry = {
|
|
759
|
+
...this.#maxRetries !== void 0 ? { maxRetries: this.#maxRetries } : {},
|
|
760
|
+
...this.#sleepImpl !== void 0 ? { sleepImpl: this.#sleepImpl } : {}
|
|
761
|
+
};
|
|
622
762
|
}
|
|
623
763
|
return request(requestOpts);
|
|
624
764
|
}
|
|
625
765
|
};
|
|
766
|
+
function isRealBrowserEnvironment() {
|
|
767
|
+
const g = globalThis;
|
|
768
|
+
const hasBrowserGlobals = typeof g.window !== "undefined" && typeof g.document !== "undefined";
|
|
769
|
+
if (!hasBrowserGlobals) return false;
|
|
770
|
+
const hasNodeLikeHost = Boolean(g.process?.versions?.node) || typeof g.Deno !== "undefined" || typeof g.Bun !== "undefined";
|
|
771
|
+
return !hasNodeLikeHost;
|
|
772
|
+
}
|
|
626
773
|
|
|
627
774
|
export { Scorezilla, ScorezillaError };
|
|
628
775
|
//# sourceMappingURL=server.js.map
|