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 +305 -0
- package/CHANGELOG.md +24 -0
- package/COMPATIBILITY.md +170 -0
- package/LICENSE +21 -0
- package/README.md +212 -0
- package/VERSIONING.md +119 -0
- package/dist/index.cjs +686 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +557 -0
- package/dist/index.d.ts +557 -0
- package/dist/index.js +680 -0
- package/dist/index.js.map +1 -0
- package/dist/phaser.cjs +6 -0
- package/dist/phaser.cjs.map +1 -0
- package/dist/phaser.d.cts +2 -0
- package/dist/phaser.d.ts +2 -0
- package/dist/phaser.js +4 -0
- package/dist/phaser.js.map +1 -0
- package/dist/react.cjs +6 -0
- package/dist/react.cjs.map +1 -0
- package/dist/react.d.cts +2 -0
- package/dist/react.d.ts +2 -0
- package/dist/react.js +4 -0
- package/dist/react.js.map +1 -0
- package/dist/server-browser-stub.cjs +8 -0
- package/dist/server-browser-stub.cjs.map +1 -0
- package/dist/server-browser-stub.d.cts +2 -0
- package/dist/server-browser-stub.d.ts +2 -0
- package/dist/server-browser-stub.js +6 -0
- package/dist/server-browser-stub.js.map +1 -0
- package/dist/server.cjs +8 -0
- package/dist/server.cjs.map +1 -0
- package/dist/server.d.cts +2 -0
- package/dist/server.d.ts +2 -0
- package/dist/server.js +6 -0
- package/dist/server.js.map +1 -0
- package/package.json +142 -0
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.
|
package/COMPATIBILITY.md
ADDED
|
@@ -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.
|