scorezilla 0.1.0-next.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/API.md ADDED
@@ -0,0 +1,305 @@
1
+ # Scorezilla SDK — API Reference
2
+
3
+ > **Status:** v0.1.0 (public-key client). The HMAC server adapter
4
+ > (`scorezilla/server`) ships in v0.2.0; React adapter in v0.3.0; Phaser in
5
+ > v0.4.0. See [CHANGELOG.md](./CHANGELOG.md) and
6
+ > [VERSIONING.md](./VERSIONING.md) for the release timeline and stability
7
+ > guarantees.
8
+
9
+ ## Quick start
10
+
11
+ ```ts
12
+ import { Scorezilla } from 'scorezilla';
13
+
14
+ const sz = new Scorezilla({ publicKey: 'pk_mygame_aBcDeF…' });
15
+
16
+ const r = await sz.submitScore({
17
+ boardId: 'board-uuid',
18
+ playerId: 'player-uuid',
19
+ score: 9001,
20
+ });
21
+ console.log(r.rank, r.isPersonalBest);
22
+ ```
23
+
24
+ ## Configuration
25
+
26
+ The constructor takes a `ScorezillaConfig`. Pass `publicKey` for client-side
27
+ use; the SDK throws if you pass `secretKey` (use the `scorezilla/server` adapter
28
+ from v0.2.0).
29
+
30
+ ```ts
31
+ interface BaseConfig {
32
+ /** API base URL. Defaults to `https://api.scorezilla.dev`. */
33
+ baseUrl?: string;
34
+ /** Custom fetch (node-fetch, undici, mock). Defaults to globalThis.fetch. */
35
+ fetch?: (input: RequestInfo | URL, init?: RequestInit) => Promise<Response>;
36
+ /** Per-request timeout in ms. Default 30 000. */
37
+ timeoutMs?: number;
38
+ /** Maximum retry attempts on transient failures. Default 2. */
39
+ maxRetries?: number;
40
+ /** Override the default User-Agent header. */
41
+ userAgent?: string;
42
+ }
43
+
44
+ type PublicKeyConfig = BaseConfig & { publicKey: string; secretKey?: never };
45
+
46
+ type ScorezillaConfig = PublicKeyConfig /* | SecretKeyConfig — v0.2.0 */;
47
+ ```
48
+
49
+ ### Key format
50
+
51
+ - `publicKey`: `pk_<game-slug>_<base62-suffix>`. Issued by the operator
52
+ dashboard. Safe to embed in browser code.
53
+ - `secretKey` (v0.2.0+): `{ id: string, secret: 'sk_live_…' }`. Server-side
54
+ only. Used to compute HMAC signatures for the secure path.
55
+
56
+ ### Mutual exclusivity
57
+
58
+ Passing both `publicKey` and `secretKey` is a TypeScript error. Passing neither
59
+ throws at runtime.
60
+
61
+ ## Methods
62
+
63
+ All methods are `async` and return parsed, typed responses on success. Every
64
+ failure path — HTTP non-2xx, network error, timeout, abort, JSON parse error —
65
+ throws a single error type, [`ScorezillaError`](#errors).
66
+
67
+ ### `submitScore`
68
+
69
+ `POST /v1/boards/:boardId/scores`. Submits a score for a player. The API
70
+ authoritatively decides if the new score is a personal best.
71
+
72
+ ```ts
73
+ interface SubmitScoreInput {
74
+ boardId: string;
75
+ playerId: string; // ← The only accepted key (no `player` alias).
76
+ score: number; // Finite. NaN / Infinity rejected as invalid_input.
77
+ metadata?: Record<string, unknown>; // ≤ 4 KB UTF-8, JSON-serializable.
78
+ }
79
+
80
+ interface SubmitScoreResponse {
81
+ ok: true;
82
+ boardId: string;
83
+ keyId: string; // The publicKey ID that authorized the submission.
84
+ rank: number; // 1-based, after the submit settles.
85
+ totalEntries: number;
86
+ isPersonalBest: boolean;
87
+ }
88
+
89
+ const r = await sz.submitScore({
90
+ boardId: 'board-uuid',
91
+ playerId: 'alice',
92
+ score: 9001,
93
+ metadata: { level: 'hard', completionMs: 27_400 },
94
+ });
95
+ if (r.isPersonalBest) {
96
+ console.log(`New PB! Rank ${r.rank} of ${r.totalEntries}`);
97
+ }
98
+ ```
99
+
100
+ #### Metadata constraints
101
+
102
+ - Must be a **plain object** (arrays / primitives / null rejected).
103
+ - Values must be JSON-serializable: no `function`, `symbol`, or `bigint`.
104
+ - No circular references.
105
+ - ≤ **4096 UTF-8 bytes** when JSON-stringified (the byte count is what matters —
106
+ emoji weigh ~4 bytes each).
107
+
108
+ Violations throw a plain `Error` (not `ScorezillaError`) before any network call
109
+ — these are caller bugs, not API failures.
110
+
111
+ #### Errors
112
+
113
+ | Code | Status | Meaning |
114
+ | -------------------------------- | ------ | ------------------------------------------------------------------------------------------------------------------------------- |
115
+ | `unauthorized` | 401 | Invalid `publicKey`. |
116
+ | `forbidden` | 403 | Key is not bound to this `boardId`. |
117
+ | `not_found` | 404 | Board doesn't exist. |
118
+ | `out_of_bounds` | 422 | Score outside the board's `[minScore, maxScore]`. `error.reason` is `'below_min'` or `'above_max'`; `error.bound` is the limit. |
119
+ | `rate_limited` | 429 | Throttled. `error.retryAfter` (seconds), `error.layer`. |
120
+ | `invalid_input` / `invalid_json` | 400 | Malformed body. |
121
+ | `network_error` | 0 | Could not reach the API. |
122
+ | `timeout` | 0 | Request exceeded `timeoutMs`. |
123
+ | `aborted` | 0 | Caller-provided `AbortSignal` fired. |
124
+
125
+ ### `getLeaderboard`
126
+
127
+ `GET /v1/boards/:boardId/leaderboard?top=&offset=`.
128
+
129
+ ```ts
130
+ interface GetLeaderboardInput {
131
+ boardId: string;
132
+ top?: number; // API caps at 1000; default 100.
133
+ offset?: number; // API caps at 1_000_000; default 0.
134
+ }
135
+
136
+ interface LeaderboardResponse {
137
+ ok: true;
138
+ boardId: string;
139
+ offset: number;
140
+ limit: number;
141
+ entries: RankedEntry[];
142
+ }
143
+
144
+ interface RankedEntry {
145
+ rank: number; // 1-based.
146
+ playerId: string;
147
+ score: number;
148
+ submittedAt: number; // Milliseconds since epoch.
149
+ metadata?: Record<string, unknown>;
150
+ }
151
+
152
+ const { entries } = await sz.getLeaderboard({ boardId, top: 25 });
153
+ for (const e of entries) console.log(`${e.rank}. ${e.playerId}: ${e.score}`);
154
+ ```
155
+
156
+ ### `getPlayerRank`
157
+
158
+ `GET /v1/boards/:boardId/players/:playerId/rank`. Returns 404 (`not_found`) if
159
+ the player has no submission yet.
160
+
161
+ ```ts
162
+ interface GetPlayerRankInput {
163
+ boardId: string;
164
+ playerId: string;
165
+ }
166
+
167
+ interface PlayerRankResponse {
168
+ ok: true;
169
+ boardId: string;
170
+ playerId: string;
171
+ rank: number;
172
+ score: number;
173
+ submittedAt: number;
174
+ totalEntries: number;
175
+ }
176
+ ```
177
+
178
+ ### `getWindowAround`
179
+
180
+ `GET /v1/boards/:boardId/players/:playerId/window?before=&after=`. Returns the
181
+ slice of entries surrounding a player.
182
+
183
+ ```ts
184
+ interface GetWindowAroundInput {
185
+ boardId: string;
186
+ playerId: string;
187
+ before?: number; // API caps at 100; default 5.
188
+ after?: number; // API caps at 100; default 5.
189
+ }
190
+
191
+ interface WindowAroundResponse {
192
+ ok: true;
193
+ boardId: string;
194
+ playerId: string;
195
+ before: number;
196
+ after: number;
197
+ entries: RankedEntry[];
198
+ }
199
+ ```
200
+
201
+ ## Errors
202
+
203
+ The SDK throws a single error type, `ScorezillaError`, for every failure mode.
204
+
205
+ ```ts
206
+ class ScorezillaError extends Error {
207
+ /** HTTP status of the response, or 0 for network/abort/timeout. */
208
+ readonly status: number;
209
+ /** Machine-stable code — see the table above. */
210
+ readonly code: ScorezillaErrorCode;
211
+ /** Sub-classifier — present on out_of_bounds (`below_min`/`above_max`). */
212
+ readonly reason: string | undefined;
213
+ /** Seconds — present on rate_limited (also Retry-After header). */
214
+ readonly retryAfter: number | undefined;
215
+ /** Server-issued request ID for support tickets. */
216
+ readonly requestId: string | undefined;
217
+ /** Bound that was crossed — present on out_of_bounds. */
218
+ readonly bound: number | undefined;
219
+ /** Which rate-limit layer fired — present on rate_limited. */
220
+ readonly layer: string | undefined;
221
+ /** Underlying cause (TypeError, DOMException, …) for transport failures. */
222
+ readonly cause: unknown;
223
+
224
+ isRateLimited(): boolean;
225
+ isAuth(): boolean; // unauthorized OR forbidden
226
+ isNotFound(): boolean;
227
+ isOutOfBounds(): boolean;
228
+ isTransient(): boolean; // network_error, timeout, 5xx, 429
229
+ }
230
+ ```
231
+
232
+ ### Branching pattern
233
+
234
+ **Always branch on `code`** — `message` is English-only and NOT part of the
235
+ SemVer contract (a minor release may reword any text).
236
+
237
+ ```ts
238
+ try {
239
+ await sz.submitScore({ boardId, playerId, score });
240
+ } catch (e) {
241
+ if (!(e instanceof ScorezillaError)) throw e;
242
+
243
+ if (e.isRateLimited()) {
244
+ await sleep(e.retryAfter! * 1000);
245
+ return retry();
246
+ }
247
+ if (e.code === 'out_of_bounds') {
248
+ console.warn(`Score outside ${e.reason} bound (limit ${e.bound})`);
249
+ return;
250
+ }
251
+ if (e.isAuth()) {
252
+ throw new Error('SDK is misconfigured — bad publicKey');
253
+ }
254
+ throw e;
255
+ }
256
+ ```
257
+
258
+ ## Advanced
259
+
260
+ ### Custom fetch / polyfills
261
+
262
+ ```ts
263
+ import fetch from 'node-fetch';
264
+ const sz = new Scorezilla({ publicKey, fetch });
265
+ ```
266
+
267
+ The signature is intentionally broader than `typeof fetch` so `node-fetch`,
268
+ `undici`, `vi.fn()`, and other polyfills typecheck cleanly.
269
+
270
+ ### AbortController
271
+
272
+ Every method accepts the SDK's internal abort signal. To cancel from your own
273
+ code, configure a global `signal` on construction — TODO in v0.2.x; for v0.1.0,
274
+ set a short `timeoutMs` per construction.
275
+
276
+ ### Idempotency keys
277
+
278
+ Every `POST` (i.e., `submitScore`) gets an automatic `Idempotency-Key` header
279
+ (UUID v4). The same key is reused across the SDK's internal retry attempts, so
280
+ server-side dedup (when added) is safe by default.
281
+
282
+ To control idempotency across your own retry loop, pass a fixed
283
+ `Idempotency-Key` via the `headers` field of `RequestOptions` — TODO public on
284
+ the client class in a follow-up release.
285
+
286
+ ### Custom User-Agent
287
+
288
+ ```ts
289
+ const sz = new Scorezilla({
290
+ publicKey,
291
+ userAgent: 'my-game/2.0',
292
+ });
293
+ ```
294
+
295
+ Note: browsers silently ignore the `User-Agent` header per the Fetch spec. The
296
+ SDK also sets `X-Scorezilla-Client` (which browsers do honor) to the same value
297
+ for cross-runtime telemetry parity.
298
+
299
+ ## See also
300
+
301
+ - [README.md](./README.md) — install + quickstart
302
+ - [VERSIONING.md](./VERSIONING.md) — SemVer contract + deprecation policy
303
+ - [CHANGELOG.md](./CHANGELOG.md) — release history
304
+ - [Scorezilla operator dashboard](https://dashboard.scorezilla.dev) — manage
305
+ games + keys
package/CHANGELOG.md ADDED
@@ -0,0 +1,24 @@
1
+ # Changelog
2
+
3
+ ## 0.1.0-next.0
4
+
5
+ ### Minor Changes
6
+
7
+ - [`239642a`](https://github.com/isco-tec/scorezilla-js/commit/239642aaf73f643c2f51d806170d3ced31a3fe68) Thanks [@isco-tec](https://github.com/isco-tec)! - **Initial public release candidate.** First publish of the `scorezilla` SDK to npm.
8
+
9
+ Includes the v0.1.0 public-key client surface:
10
+ - `Scorezilla` class with `submitScore`, `getLeaderboard`, `getPlayerRank`, `getWindowAround`
11
+ - `ScorezillaError` for every failure path (network, timeout, abort, HTTP non-2xx)
12
+ - Universal runtime support (browsers, Node ≥ 20, Cloudflare Workers, Bun, Deno)
13
+ - Automatic retries with idempotency keys
14
+ - Dual ESM/CJS build with [arethetypeswrong](https://arethetypeswrong.github.io/)-clean exports map
15
+ - ~3.8 KB ESM gzipped
16
+
17
+ This RC publishes under the `next` npm dist-tag — install with `npm install scorezilla@next` to try it out. The HMAC server adapter, React hooks, and Phaser plugin land in subsequent v0.2.0, v0.3.0, and v0.4.0 releases.
18
+
19
+ All notable changes to `scorezilla` are documented here.
20
+
21
+ This project follows [Semantic Versioning](https://semver.org/). Releases are
22
+ managed by [Changesets](https://github.com/changesets/changesets) — see
23
+ [VERSIONING.md](./VERSIONING.md) for the SemVer contract, deprecation policy,
24
+ and the 0.x → 1.0 exit criteria.
@@ -0,0 +1,170 @@
1
+ # Compatibility
2
+
3
+ Where the Scorezilla SDK runs, what it needs, what it sends, and what it does
4
+ not.
5
+
6
+ ## Runtime support
7
+
8
+ The SDK targets ES2022 + the standard `fetch` / `AbortController` /
9
+ `crypto.randomUUID` web platform APIs. Anywhere those are available, the SDK
10
+ works.
11
+
12
+ | Runtime | Status | Notes |
13
+ | ---------------------- | ------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
14
+ | **Node** | ≥ 20 (hard) | Native `fetch` since Node 18.0; `crypto.randomUUID` since Node 14.17. We require Node 20 to inherit the modern Web Crypto profile. |
15
+ | **Browsers** | All evergreen | Native `fetch` since 2017; `crypto.randomUUID` since Safari 15.4, Firefox 95, Chrome 92. **Safari 17.0–17.3** is supported despite lacking `AbortSignal.any` — the SDK composes signals manually. |
16
+ | **Cloudflare Workers** | Supported | Detected via `navigator.userAgent === 'Cloudflare-Workers'`. No Node-only API used. |
17
+ | **Bun** | ≥ 1.0 | Bun ≥ 1.0 ships compatible fetch + WebCrypto. CI runs Bun as a best-effort matrix until v0.2.0 (see VERSIONING.md). |
18
+ | **Deno** | ≥ 1.40 | Native fetch + Web Crypto. |
19
+ | **React Native** | Unverified | The standard `fetch` polyfill ships with RN ≥ 0.60, but `crypto.randomUUID` typically requires `react-native-get-random-values`. Not part of the v0.1.0 CI matrix. |
20
+
21
+ ### Minimum API version
22
+
23
+ The SDK talks to `/v1/*` only. The base URL defaults to
24
+ `https://api.scorezilla.dev`; pass `baseUrl` to override (e.g. for a local
25
+ `wrangler dev` instance during development).
26
+
27
+ The SDK does not negotiate API versions — it speaks `/v1` literally. When `/v2`
28
+ ships, a major-bumped SDK will accept a new `apiVersion: '2'` knob.
29
+
30
+ ## TypeScript
31
+
32
+ ### Compiler version
33
+
34
+ TypeScript ≥ 4.7 (for the modern `node16`/`bundler` `moduleResolution`). The
35
+ SDK's published types include a `typesVersions` fallback for ≤ 4.6's `node10`
36
+ resolver, so `import { Scorezilla } from 'scorezilla'` resolves cleanly under
37
+ both algorithms.
38
+
39
+ ### `exactOptionalPropertyTypes`
40
+
41
+ The SDK is built with `exactOptionalPropertyTypes: true`. The PUBLIC input types
42
+ use `?: T | undefined` (with explicit `| undefined`) on optional fields
43
+ specifically so callers under the same setting can pass a maybe-undefined
44
+ variable without a workaround:
45
+
46
+ ```ts
47
+ // Works under exactOptionalPropertyTypes: true thanks to `| undefined`.
48
+ const meta: Record<string, unknown> | undefined = computeMeta();
49
+ await sz.submitScore({ boardId, playerId, score, metadata: meta });
50
+ ```
51
+
52
+ If you're consuming the SDK from a project that does NOT use
53
+ `exactOptionalPropertyTypes`, this is fully transparent — the union collapses to
54
+ just `T?` for you.
55
+
56
+ ### The spread workaround (for the rare strict consumer)
57
+
58
+ If you find yourself with a value typed `T` (not `T | undefined`) inside a
59
+ strict object you want to spread into a Scorezilla input, use object spread
60
+ rather than property assignment:
61
+
62
+ ```ts
63
+ // Don't:
64
+ const input: SubmitScoreInput = {
65
+ boardId,
66
+ playerId,
67
+ score,
68
+ metadata: someStrictMeta as Record<string, unknown>, // cast
69
+ };
70
+
71
+ // Do:
72
+ const input: SubmitScoreInput = {
73
+ boardId,
74
+ playerId,
75
+ score,
76
+ ...(someStrictMeta ? { metadata: someStrictMeta } : {}),
77
+ };
78
+ ```
79
+
80
+ The conditional spread keeps the `metadata` key absent (rather than
81
+ present-with-`undefined`) when the value isn't ready — matching what the strict
82
+ type system expects.
83
+
84
+ ## Custom fetch / polyfills
85
+
86
+ `cfg.fetch` accepts any function with the shape
87
+ `(input: RequestInfo | URL, init?: RequestInit) => Promise<Response>`. This is
88
+ intentionally broader than `typeof fetch` so common polyfills and test stubs
89
+ typecheck cleanly:
90
+
91
+ ```ts
92
+ import fetch from 'node-fetch';
93
+ import { Scorezilla } from 'scorezilla';
94
+ const sz = new Scorezilla({ publicKey, fetch });
95
+ ```
96
+
97
+ Polyfill compatibility:
98
+
99
+ - [`node-fetch`](https://www.npmjs.com/package/node-fetch) ≥ 3 — typechecks
100
+ against the SDK's `FetchImpl`; functional smoke-test in the SDK's own unit
101
+ suite.
102
+ - [`undici`](https://www.npmjs.com/package/undici) — typechecks; not exercised
103
+ in CI but its native fetch matches the contract.
104
+ - `vi.fn()` (Vitest) — used throughout the SDK's own 171-test suite.
105
+ - `jest.fn()` — same shape as `vi.fn()`, expected to work but not currently
106
+ exercised in CI. File an issue if you hit a mismatch.
107
+
108
+ ## Bundle sizes
109
+
110
+ | Bundle | Format | Gzipped |
111
+ | ---------------- | ------ | ------- |
112
+ | `dist/index.js` | ESM | ~3.8 KB |
113
+ | `dist/index.cjs` | CJS | ~4.0 KB |
114
+
115
+ These numbers come from `size-limit` simulating a consumer's bundler. Source
116
+ maps ship alongside but aren't counted against the budget. The CI gate caps the
117
+ bundle at 6 KB gzipped.
118
+
119
+ ## What the SDK does NOT do
120
+
121
+ The SDK is deliberately minimal in side-effects and identifiers.
122
+
123
+ ### No fingerprinting
124
+
125
+ `detectRuntime()` probes only `globalThis.Bun`, `globalThis.Deno`,
126
+ `globalThis.process.versions.node`, `globalThis.document`, and a single specific
127
+ string check on `navigator.userAgent` (`'Cloudflare-Workers'`). No other
128
+ browser, hardware, locale, or timezone signal is read.
129
+
130
+ ### No cookies, no localStorage, no IndexedDB
131
+
132
+ The SDK does not set cookies, write to `localStorage` / `sessionStorage` /
133
+ `IndexedDB`, or use any persistent client storage. The `playerId` you pass is
134
+ the only identifier the SDK transmits; it never generates or stores one on your
135
+ behalf.
136
+
137
+ ### No analytics calls
138
+
139
+ The SDK sends `POST` and `GET` requests only to the configured `baseUrl`. It
140
+ does not phone home, report errors externally, or fetch from any other origin.
141
+
142
+ ### No console writes during normal operation
143
+
144
+ The SDK throws `ScorezillaError` for failures rather than logging. The sole
145
+ intentional `console.warn` site is the deprecation-warning machinery described
146
+ in [VERSIONING.md](./VERSIONING.md), which fires at most once per deprecated
147
+ symbol per process and can be suppressed.
148
+
149
+ ### No automatic retries on caller errors
150
+
151
+ 4xx responses (except 429) are surfaced verbatim. The retry loop only fires on
152
+ 5xx, 429, and network-level errors — exactly the conditions where re-attempt is
153
+ idempotent and reasonable.
154
+
155
+ ## Privacy invariants summary
156
+
157
+ For your `<privacy-policy>` if you embed the SDK:
158
+
159
+ > The Scorezilla SDK transmits the player identifier and metadata you provide,
160
+ > with the API requests you instruct it to send. It does not read browser
161
+ > fingerprinting signals beyond a single runtime-detection probe; does not write
162
+ > cookies, local storage, or any other persistent data; and does not contact any
163
+ > origin besides the one you configure via `baseUrl`.
164
+
165
+ ## See also
166
+
167
+ - [README.md](./README.md)
168
+ - [API.md](./API.md)
169
+ - [VERSIONING.md](./VERSIONING.md)
170
+ - [CHANGELOG.md](./CHANGELOG.md)
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 isco-tec
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.