scorezilla 0.1.0-next.0 → 0.1.0-next.3
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 +116 -0
- package/README.md +37 -4
- package/dist/errors-CUAQsaVS.d.cts +292 -0
- package/dist/errors-CUAQsaVS.d.ts +292 -0
- package/dist/index.cjs +89 -32
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +3 -283
- package/dist/index.d.ts +3 -283
- package/dist/index.js +89 -32
- package/dist/index.js.map +1 -1
- package/dist/server.cjs +688 -3
- package/dist/server.cjs.map +1 -1
- package/dist/server.d.cts +141 -1
- package/dist/server.d.ts +141 -1
- package/dist/server.js +687 -3
- package/dist/server.js.map +1 -1
- package/package.json +26 -21
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,121 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.1.0-next.3
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- [#25](https://github.com/isco-tec/scorezilla-js/pull/25) [`5f7025b`](https://github.com/isco-tec/scorezilla-js/commit/5f7025b7b06dded92b3e710316454eb8c891d053) Thanks [@isco-tec](https://github.com/isco-tec)! - **`scorezilla/server`: HMAC requests now bind to the target host (v=2).**
|
|
8
|
+
|
|
9
|
+
The canonical signing string includes the host of the request URL, so a signature minted against staging cannot be replayed against prod (or any other origin that happens to share key material). SigV4 has had this since day one; closing the gap before MCP locks the format.
|
|
10
|
+
|
|
11
|
+
Wire format change:
|
|
12
|
+
|
|
13
|
+
```
|
|
14
|
+
Authorization: Scorezilla-HMAC-SHA256 keyId=…, ts=…, nonce=…, signature=…, v=2
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
The `v=2` parameter is new. The canonical signing string now has six lines instead of five — `host` is inserted between `METHOD` and `pathAndQuery`, lowercased per RFC 9110 §4.2.4.
|
|
18
|
+
|
|
19
|
+
**Backward compatibility.** The API verifier still accepts v=1 (no `v=` field, no host binding) during the rollout window — your existing pre-next.3 SDK builds will keep working until v=1 is deprecated. The SDK itself, however, emits v=2 unconditionally from next.3 onwards. If you've forked or wrapped `buildHmacAuthHeader` / `buildSigningString`, you'll need to thread a `host` parameter through.
|
|
20
|
+
|
|
21
|
+
**For consumers of `Scorezilla` from `scorezilla/server`:** no API changes. The constructor derives `host` from `baseUrl` automatically. As an added safety net, an invalid `baseUrl` now throws at construction time rather than producing 401 mismatches at every request.
|
|
22
|
+
|
|
23
|
+
**For low-level consumers of `buildSigningString` / `buildHmacAuthHeader`:** a new `host` parameter is now required. The latter also accepts an optional `version: 1 | 2` for explicit backward-compat scenarios.
|
|
24
|
+
|
|
25
|
+
### Patch Changes
|
|
26
|
+
|
|
27
|
+
- [#28](https://github.com/isco-tec/scorezilla-js/pull/28) [`94b39d2`](https://github.com/isco-tec/scorezilla-js/commit/94b39d2e67ec7bba2681145560529f058acfc633) Thanks [@isco-tec](https://github.com/isco-tec)! - **Pre-release security hardening pass.** Three issues surfaced by a focused security review before the first public release of `scorezilla/server`:
|
|
28
|
+
- **HIGH — nonce injection.** `buildHmacAuthHeader` now requires injected nonces to be at least 16 characters. The default path (`crypto.randomUUID()`) is unaffected. Previously a misconfigured caller could pass `nonce: ''` and silently sign a header with no replay protection — the server has no minimum-length check, so the API would accept it. Now caught at SDK build-time with a clear error.
|
|
29
|
+
- **HIGH — server policy disclosure.** `HMAC_TIMESTAMP_WINDOW_SECONDS = 300` was previously exported as documentation of the API's clock-skew tolerance. Removed from the public API — publishing the server's replay-protection window in the SDK's types was unnecessary information disclosure and narrowed the pre-computation window for any future timestamp-forgery work. The SDK has no behavioral dependency on the value (it always emits a fresh `ts = floor(Date.now() / 1000)`).
|
|
30
|
+
- **MEDIUM — `EdgeRuntime` polyfill bypass.** The server adapter's runtime browser-guard previously trusted `globalThis.EdgeRuntime` as a "this is a server" signal. A browser extension or bundler misconfig setting that global would have bypassed the guard. Removed from the trusted set — the package's `exports.browser` condition remains the primary gate, and the runtime check now refuses to trust globals that can be polyfilled from a browser context. Real Vercel Edge runtimes still pass the guard because they don't have `window`/`document`.
|
|
31
|
+
|
|
32
|
+
No external-attack surface was exploitable; all three are self-inflicted/defense-in-depth issues caught before the first stable release. Tests added: 4 for nonce validation (rejects empty / too-short, accepts at-floor / default UUID), 1 for the EdgeRuntime polyfill scenario.
|
|
33
|
+
|
|
34
|
+
## 0.1.0-next.2
|
|
35
|
+
|
|
36
|
+
### Minor Changes
|
|
37
|
+
|
|
38
|
+
- [#23](https://github.com/isco-tec/scorezilla-js/pull/23) [`5163e31`](https://github.com/isco-tec/scorezilla-js/commit/5163e3130ac208d591b026d66870ce5a194f90b6) Thanks [@isco-tec](https://github.com/isco-tec)! - **`scorezilla/server` adopts a single-token secret-key format.** Breaking
|
|
39
|
+
change vs. v0.1.0-next.1; we're still in pre-release so this is permitted.
|
|
40
|
+
|
|
41
|
+
Before (v0.1.0-next.1):
|
|
42
|
+
|
|
43
|
+
```ts
|
|
44
|
+
new Scorezilla({
|
|
45
|
+
secretKey: { id: 'sk-id-uuid', secret: 'sk_live_xxxxx' },
|
|
46
|
+
});
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
After (v0.1.0-next.2):
|
|
50
|
+
|
|
51
|
+
```ts
|
|
52
|
+
new Scorezilla({
|
|
53
|
+
secretKey: 'sk_live_<keyId>_xxxxx', // one self-contained token
|
|
54
|
+
});
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
The keyId is embedded in the secret string itself; the SDK parses it
|
|
58
|
+
out via `/^sk_live_([0-9a-f]{8}-...)_/` before signing. The HMAC key
|
|
59
|
+
remains the WHOLE plaintext. Wire format on the Authorization header
|
|
60
|
+
is unchanged. API verification is unchanged.
|
|
61
|
+
|
|
62
|
+
Rationale: matches Stripe's design and the public-key client's
|
|
63
|
+
single-string shape. One value to copy from the dashboard, one to
|
|
64
|
+
manage in env config, one to pass into the constructor. Previously
|
|
65
|
+
users had to manage two distinct values (`id` AND `secret`) which
|
|
66
|
+
created confusion — they're functionally one credential.
|
|
67
|
+
|
|
68
|
+
**To upgrade**: issue (or rotate) a fresh secret key in the dashboard.
|
|
69
|
+
Old-format keys (no embedded keyId) are rejected by the SDK with a
|
|
70
|
+
clear error message pointing at the migration path.
|
|
71
|
+
|
|
72
|
+
## 0.1.0-next.1
|
|
73
|
+
|
|
74
|
+
### Minor Changes
|
|
75
|
+
|
|
76
|
+
- [#18](https://github.com/isco-tec/scorezilla-js/pull/18) [`a02b07a`](https://github.com/isco-tec/scorezilla-js/commit/a02b07a116a9d15c2928ad4f98a9cfa3a8dceffd) Thanks [@isco-tec](https://github.com/isco-tec)! - **v0.2.0 — HMAC server adapter (`scorezilla/server`).** Closes [#17](https://github.com/isco-tec/scorezilla-js/issues/17).
|
|
77
|
+
|
|
78
|
+
The new `scorezilla/server` subpath ships an HMAC-SHA256 signing client for
|
|
79
|
+
game backends that need cheat-resistant submissions. The browser-side
|
|
80
|
+
public-key client (`scorezilla`) is unchanged.
|
|
81
|
+
|
|
82
|
+
```ts
|
|
83
|
+
import { Scorezilla } from 'scorezilla/server';
|
|
84
|
+
|
|
85
|
+
const sz = new Scorezilla({
|
|
86
|
+
secretKey: {
|
|
87
|
+
id: process.env.SCOREZILLA_KEY_ID,
|
|
88
|
+
secret: process.env.SCOREZILLA_KEY_SECRET, // never ship to a browser
|
|
89
|
+
},
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
await sz.submitScore({ boardId, playerId, score, metadata });
|
|
93
|
+
await sz.getLeaderboard({ boardId, top });
|
|
94
|
+
await sz.getPlayerRank({ boardId, playerId });
|
|
95
|
+
await sz.getWindowAround({ boardId, playerId, before, after });
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
Behavior:
|
|
99
|
+
- Each request is signed with HMAC-SHA256 over a canonical
|
|
100
|
+
`{ts}\n{nonce}\n{METHOD}\n{path?query}\n{sha256_hex(body)}` string and
|
|
101
|
+
delivered via `Authorization: Scorezilla-HMAC-SHA256 keyId=…, ts=…,
|
|
102
|
+
nonce=…, signature=…`. Matches the API's verifier byte-for-byte.
|
|
103
|
+
- `submitScore` posts to `/v1/secure/scores` (the boardId moves into the
|
|
104
|
+
body — the public-key endpoint at `/v1/boards/:id/scores` is unchanged).
|
|
105
|
+
- Read methods (`getLeaderboard` / `getPlayerRank` / `getWindowAround`)
|
|
106
|
+
hit the same paths as the public-key client.
|
|
107
|
+
- Every retry attempt regenerates `(ts, nonce)` so server-side replay
|
|
108
|
+
protection (10-minute nonce window) doesn't trip.
|
|
109
|
+
- Importing `scorezilla/server` from a browser bundle throws at module
|
|
110
|
+
evaluation — the `sk_live_*` secret never reaches client-side code.
|
|
111
|
+
|
|
112
|
+
Same `ScorezillaError` surface (`isRateLimited()`, `isAuth()`,
|
|
113
|
+
`isTransient()`, `code`, `requestId`, etc.) — typed catch patterns
|
|
114
|
+
written for v0.1 work unchanged.
|
|
115
|
+
|
|
116
|
+
Bundle: ~3.84 KB ESM gzipped, ~4.04 KB CJS gzipped — well under the
|
|
117
|
+
6 KB per-entry cap.
|
|
118
|
+
|
|
3
119
|
## 0.1.0-next.0
|
|
4
120
|
|
|
5
121
|
### Minor Changes
|
package/README.md
CHANGED
|
@@ -18,10 +18,13 @@ AI-vibe-coded games.
|
|
|
18
18
|
- **Private.** No cookies, no `localStorage`, no fingerprinting beyond runtime
|
|
19
19
|
detection — see [COMPATIBILITY.md](./COMPATIBILITY.md).
|
|
20
20
|
|
|
21
|
-
> **Status:** v0.1.0 ships the **public-key client** (browser-safe).
|
|
22
|
-
> server adapter (`scorezilla/server`)
|
|
23
|
-
>
|
|
24
|
-
>
|
|
21
|
+
> **Status:** v0.1.0 ships the **public-key client** (browser-safe).
|
|
22
|
+
> v0.2.0 ships the **HMAC server adapter** (`scorezilla/server`) for
|
|
23
|
+
> game backends that need cheat-resistant submissions. React
|
|
24
|
+
> (`scorezilla/react`) lands in v0.3.0; Phaser (`scorezilla/phaser`) in
|
|
25
|
+
> v0.4.0. The first preview is on the `next` dist-tag — install with
|
|
26
|
+
> `npm install scorezilla@next`. See [CHANGELOG.md](./CHANGELOG.md) and
|
|
27
|
+
> [VERSIONING.md](./VERSIONING.md).
|
|
25
28
|
|
|
26
29
|
> **Commercial context.** Scorezilla is a hosted leaderboard service with free
|
|
27
30
|
> and paid tiers — see [scorezilla.dev/pricing](https://scorezilla.dev/pricing).
|
|
@@ -76,6 +79,36 @@ await sz.getWindowAround({ boardId, playerId, before?, after? });
|
|
|
76
79
|
See [**API.md**](./API.md) for the full reference, including every response
|
|
77
80
|
field, every error code, and advanced patterns.
|
|
78
81
|
|
|
82
|
+
## Server-side HMAC (`scorezilla/server`)
|
|
83
|
+
|
|
84
|
+
Public-key submissions are client-authoritative — anyone with your `pk_` can
|
|
85
|
+
submit any score from devtools. For games where ranking matters, sign each
|
|
86
|
+
submission server-side with a `sk_live_*` secret:
|
|
87
|
+
|
|
88
|
+
```ts
|
|
89
|
+
import { Scorezilla, ScorezillaError } from 'scorezilla/server';
|
|
90
|
+
|
|
91
|
+
// Single self-contained token from the dashboard. Format:
|
|
92
|
+
// sk_live_<keyId>_<random>
|
|
93
|
+
// The SDK parses the keyId out internally; you only manage one value.
|
|
94
|
+
const sz = new Scorezilla({
|
|
95
|
+
secretKey: process.env.SCOREZILLA_SECRET_KEY!, // never ship to a browser
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
await sz.submitScore({ boardId, playerId, score, metadata });
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
Same method shape as the public-key client — `submitScore`, `getLeaderboard`,
|
|
102
|
+
`getPlayerRank`, `getWindowAround`. The adapter signs every request with
|
|
103
|
+
**HMAC-SHA256** over a canonical string (method + path + ts + nonce +
|
|
104
|
+
sha256(body)), and the API verifies before any state change. Replay
|
|
105
|
+
protection is enforced server-side via a 10-minute nonce window.
|
|
106
|
+
|
|
107
|
+
The `scorezilla/server` subpath is server-only — importing it from the
|
|
108
|
+
browser throws at module evaluation. Use environment variables (or your
|
|
109
|
+
secret manager) to load the `sk_live_*` value; never embed it in a build
|
|
110
|
+
that ships to clients.
|
|
111
|
+
|
|
79
112
|
## Error handling
|
|
80
113
|
|
|
81
114
|
Every failure path — HTTP non-2xx, network error, timeout, abort, JSON parse
|
|
@@ -0,0 +1,292 @@
|
|
|
1
|
+
/** Minimal fetch shape — broader than `typeof fetch` so polyfills and
|
|
2
|
+
* test stubs (`vi.fn()`, `node-fetch`, etc.) typecheck cleanly. */
|
|
3
|
+
type FetchImpl = (input: RequestInfo | URL, init?: RequestInit) => Promise<Response>;
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* SDK configuration.
|
|
7
|
+
*
|
|
8
|
+
* The `ScorezillaConfig` is a TypeScript-level discriminated union of
|
|
9
|
+
* `PublicKeyConfig` and `SecretKeyConfig`. The mutual-exclusivity (you may
|
|
10
|
+
* pass `publicKey` OR `secretKey`, never both) is enforced at compile time:
|
|
11
|
+
* passing both fields fails type-checking before the runtime check fires.
|
|
12
|
+
*
|
|
13
|
+
* The runtime check in {@link validateConfig} is the second line of defense
|
|
14
|
+
* for consumers using plain JS or `as any` casts.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
/** Shared options across both auth modes. */
|
|
18
|
+
interface BaseConfig {
|
|
19
|
+
/** API base URL (no trailing slash required). Defaults to {@link DEFAULT_BASE_URL}. */
|
|
20
|
+
baseUrl?: string;
|
|
21
|
+
/** Custom fetch implementation — defaults to `globalThis.fetch`. Pass
|
|
22
|
+
* `node-fetch`, `undici`, or a mock here. The explicit signature
|
|
23
|
+
* (`(RequestInfo | URL, init?) => Promise<Response>`) is broader than
|
|
24
|
+
* `typeof fetch` so common polyfills typecheck cleanly. */
|
|
25
|
+
fetch?: FetchImpl;
|
|
26
|
+
/** Per-request timeout in milliseconds. Defaults to 30 s. */
|
|
27
|
+
timeoutMs?: number;
|
|
28
|
+
/** Maximum retry attempts on transient failures. Defaults to 2 (so the
|
|
29
|
+
* worst-case total request count is 3). */
|
|
30
|
+
maxRetries?: number;
|
|
31
|
+
/** Override the default `User-Agent` header (Node/Workers/Bun/Deno —
|
|
32
|
+
* browsers silently ignore the value). */
|
|
33
|
+
userAgent?: string;
|
|
34
|
+
/** Injectable sleep implementation for the retry loop's inter-attempt
|
|
35
|
+
* pause. Exists for tests that need deterministic, zero-delay retries
|
|
36
|
+
* rather than real wall-clock backoff. Production code should leave
|
|
37
|
+
* this unset to use the default exponential backoff with jitter.
|
|
38
|
+
* @internal */
|
|
39
|
+
sleepImpl?: (ms: number, signal?: AbortSignal) => Promise<void>;
|
|
40
|
+
}
|
|
41
|
+
/** Public-key auth: browser-safe path. The key is fingerprinted to a game
|
|
42
|
+
* on the server side via `pk_<gameSlug>_<base62>`. */
|
|
43
|
+
type PublicKeyConfig = BaseConfig & {
|
|
44
|
+
publicKey: string;
|
|
45
|
+
secretKey?: never;
|
|
46
|
+
};
|
|
47
|
+
/** Secret-key auth: server-side HMAC. A single self-contained token of the
|
|
48
|
+
* shape `sk_live_<keyId>_<random>`. The SDK parses the keyId out and uses
|
|
49
|
+
* the whole string as the HMAC key. One value to copy, one to manage —
|
|
50
|
+
* matches Stripe's design and the public-key client's single-string shape.
|
|
51
|
+
*
|
|
52
|
+
* Past versions of the SDK took `{ id, secret }` separately. That was an
|
|
53
|
+
* unnecessary cognitive tax — the id was always derivable from a properly-
|
|
54
|
+
* formatted secret. v0.1.0-next.2+ takes the single-string form. */
|
|
55
|
+
type SecretKeyConfig = BaseConfig & {
|
|
56
|
+
secretKey: string;
|
|
57
|
+
publicKey?: never;
|
|
58
|
+
};
|
|
59
|
+
/** The top-level config type. The union is open for additional auth modes
|
|
60
|
+
* in future major releases. */
|
|
61
|
+
type ScorezillaConfig = PublicKeyConfig | SecretKeyConfig;
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Wire types for the Scorezilla API at /v1.
|
|
65
|
+
*
|
|
66
|
+
* Mirrors the documented response shapes. TypeScript's structural typing
|
|
67
|
+
* means additional fields the server adds in a minor release won't break
|
|
68
|
+
* consumers — see VERSIONING.md for the full SemVer contract.
|
|
69
|
+
*
|
|
70
|
+
* No Zod or other runtime validators in v0.1.0 — keeps the bundle small.
|
|
71
|
+
* Narrow untrusted input with the {@link isApiSuccess} / {@link isApiError}
|
|
72
|
+
* type guards exported below.
|
|
73
|
+
*/
|
|
74
|
+
/**
|
|
75
|
+
* Machine-stable error codes returned by the API.
|
|
76
|
+
*
|
|
77
|
+
* Consumers MUST branch on this `code` rather than the human-readable
|
|
78
|
+
* `message` — message text is English-only and explicitly NOT part of the
|
|
79
|
+
* SemVer contract.
|
|
80
|
+
*
|
|
81
|
+
* The union is intentionally open (`| (string & {})`) so unknown future
|
|
82
|
+
* codes from a server-side minor release don't compile-error against the
|
|
83
|
+
* SDK. The trick preserves autocomplete on the known set while permitting
|
|
84
|
+
* arbitrary strings at runtime — see
|
|
85
|
+
* https://github.com/microsoft/TypeScript/issues/29729 for the pattern.
|
|
86
|
+
*
|
|
87
|
+
* @stable v0.1.0
|
|
88
|
+
*/
|
|
89
|
+
type ScorezillaErrorCode = 'unauthorized' | 'forbidden' | 'not_found' | 'invalid_input' | 'invalid_json' | 'out_of_bounds' | 'rate_limited' | 'conflict' | 'internal_error' | (string & {});
|
|
90
|
+
/** Reason sub-classifier on `out_of_bounds` errors. Open union — see {@link ScorezillaErrorCode}. */
|
|
91
|
+
type OutOfBoundsReason = 'below_min' | 'above_max' | (string & {});
|
|
92
|
+
/** Successful API response envelope. The `T` is the per-route payload. */
|
|
93
|
+
type ApiSuccess<T> = {
|
|
94
|
+
ok: true;
|
|
95
|
+
} & T;
|
|
96
|
+
/** Failure response envelope. The server returns this on every non-2xx response. */
|
|
97
|
+
interface ApiError {
|
|
98
|
+
ok: false;
|
|
99
|
+
error: ScorezillaErrorCode;
|
|
100
|
+
/** Human-readable, English only. Not machine-stable — branch on `error` and `reason`. */
|
|
101
|
+
message?: string;
|
|
102
|
+
/** Sub-classifier — used by `out_of_bounds` (`'below_min' | 'above_max'`). */
|
|
103
|
+
reason?: string;
|
|
104
|
+
/** Seconds — present on `rate_limited`. Also mirrored in the HTTP `Retry-After` header. */
|
|
105
|
+
retryAfter?: number;
|
|
106
|
+
/** Which rate-limit layer fired — present on `rate_limited`. */
|
|
107
|
+
layer?: string;
|
|
108
|
+
/** The limit value that was crossed — present on `out_of_bounds`. */
|
|
109
|
+
bound?: number;
|
|
110
|
+
}
|
|
111
|
+
/** Discriminated envelope: every API response is one of these two shapes. */
|
|
112
|
+
type ApiResponse<T> = ApiSuccess<T> | ApiError;
|
|
113
|
+
/**
|
|
114
|
+
* A single ranked entry on a leaderboard.
|
|
115
|
+
*
|
|
116
|
+
* Returned as an array on `leaderboard` and `window-around` responses, and
|
|
117
|
+
* inline on `playerRank` (without the `rank` wrapper — see
|
|
118
|
+
* {@link PlayerRankResponse}).
|
|
119
|
+
*/
|
|
120
|
+
interface RankedEntry {
|
|
121
|
+
/** 1-based rank. */
|
|
122
|
+
rank: number;
|
|
123
|
+
playerId: string;
|
|
124
|
+
score: number;
|
|
125
|
+
/** Milliseconds since epoch. */
|
|
126
|
+
submittedAt: number;
|
|
127
|
+
metadata?: Record<string, unknown>;
|
|
128
|
+
}
|
|
129
|
+
/** Payload from `POST /v1/boards/:boardId/scores`. */
|
|
130
|
+
interface SubmitScoreResponse {
|
|
131
|
+
boardId: string;
|
|
132
|
+
/** The key ID that authorized the submission. Useful for consumer-side audit. */
|
|
133
|
+
keyId: string;
|
|
134
|
+
/** 1-based rank after the submit settled. */
|
|
135
|
+
rank: number;
|
|
136
|
+
totalEntries: number;
|
|
137
|
+
isPersonalBest: boolean;
|
|
138
|
+
}
|
|
139
|
+
/** Payload from `GET /v1/boards/:boardId/leaderboard`. */
|
|
140
|
+
interface LeaderboardResponse {
|
|
141
|
+
boardId: string;
|
|
142
|
+
offset: number;
|
|
143
|
+
limit: number;
|
|
144
|
+
entries: RankedEntry[];
|
|
145
|
+
}
|
|
146
|
+
/** Payload from `GET /v1/boards/:boardId/players/:playerId/rank`. */
|
|
147
|
+
interface PlayerRankResponse {
|
|
148
|
+
boardId: string;
|
|
149
|
+
playerId: string;
|
|
150
|
+
rank: number;
|
|
151
|
+
score: number;
|
|
152
|
+
submittedAt: number;
|
|
153
|
+
totalEntries: number;
|
|
154
|
+
}
|
|
155
|
+
/** Payload from `GET /v1/boards/:boardId/players/:playerId/window`. */
|
|
156
|
+
interface WindowAroundResponse {
|
|
157
|
+
boardId: string;
|
|
158
|
+
playerId: string;
|
|
159
|
+
before: number;
|
|
160
|
+
after: number;
|
|
161
|
+
entries: RankedEntry[];
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* SDK error type.
|
|
166
|
+
*
|
|
167
|
+
* Every non-2xx API response is normalized into a `ScorezillaError` instance
|
|
168
|
+
* by the transport layer. Network failures and timeouts surface as the same
|
|
169
|
+
* class (with `status: 0`) so callers have a single error type to catch.
|
|
170
|
+
*
|
|
171
|
+
* **Invariant — consumers MUST branch on `code` (and optionally `reason`),
|
|
172
|
+
* never on `message`.** The English-language `message` is for operator
|
|
173
|
+
* logging only and is explicitly **not** part of the SemVer contract; a
|
|
174
|
+
* minor release MAY reword any message. Machine logic that depends on
|
|
175
|
+
* message text will break silently across upgrades.
|
|
176
|
+
*/
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Options for {@link ScorezillaError.from}.
|
|
180
|
+
*
|
|
181
|
+
* The fields mirror what's available after a fetch round-trip: the HTTP
|
|
182
|
+
* status, the parsed JSON body (if any), the request ID from
|
|
183
|
+
* `X-Request-Id`, and an optional `cause` for the underlying
|
|
184
|
+
* network/abort error.
|
|
185
|
+
*/
|
|
186
|
+
interface ScorezillaErrorFromInit {
|
|
187
|
+
status: number;
|
|
188
|
+
body?: ApiError | undefined;
|
|
189
|
+
requestId?: string | undefined;
|
|
190
|
+
cause?: unknown;
|
|
191
|
+
}
|
|
192
|
+
/**
|
|
193
|
+
* Thrown by the SDK for every failure path — non-2xx responses, network
|
|
194
|
+
* errors, aborts, and timeouts.
|
|
195
|
+
*
|
|
196
|
+
* Cross-realm `instanceof` is guaranteed: the class sets `Error.prototype`
|
|
197
|
+
* explicitly so checks survive iframe / worker boundaries.
|
|
198
|
+
*
|
|
199
|
+
* @example
|
|
200
|
+
* ```ts
|
|
201
|
+
* try {
|
|
202
|
+
* await sz.submitScore({ boardId, playerId, score });
|
|
203
|
+
* } catch (e) {
|
|
204
|
+
* if (!(e instanceof ScorezillaError)) throw e;
|
|
205
|
+
*
|
|
206
|
+
* if (e.isRateLimited()) {
|
|
207
|
+
* await sleep((e.retryAfter ?? 30) * 1000);
|
|
208
|
+
* return retry();
|
|
209
|
+
* }
|
|
210
|
+
* if (e.code === 'out_of_bounds') {
|
|
211
|
+
* console.warn(`Score crosses ${e.reason} bound (limit ${e.bound})`);
|
|
212
|
+
* return;
|
|
213
|
+
* }
|
|
214
|
+
* if (e.isAuth()) throw new Error('SDK misconfigured — bad publicKey');
|
|
215
|
+
*
|
|
216
|
+
* // Anything else: surface to your reporter with requestId for support.
|
|
217
|
+
* console.error(`Scorezilla ${e.code} (${e.status}) — request ${e.requestId}`);
|
|
218
|
+
* throw e;
|
|
219
|
+
* }
|
|
220
|
+
* ```
|
|
221
|
+
*
|
|
222
|
+
* @since 0.1.0
|
|
223
|
+
* @stability stable
|
|
224
|
+
*/
|
|
225
|
+
declare class ScorezillaError extends Error {
|
|
226
|
+
/** HTTP status of the response, or {@link STATUS_NETWORK_ERROR} (0) for
|
|
227
|
+
* network / abort / timeout. */
|
|
228
|
+
readonly status: number;
|
|
229
|
+
/** Machine-stable error code from the API. Open union — see
|
|
230
|
+
* {@link ScorezillaErrorCode}. For network errors, this is `'network_error'`;
|
|
231
|
+
* for aborts, `'aborted'`; for timeouts, `'timeout'`. */
|
|
232
|
+
readonly code: ScorezillaErrorCode;
|
|
233
|
+
/** Sub-classifier — present on `out_of_bounds` (`'below_min' | 'above_max'`)
|
|
234
|
+
* and possibly other codes in future minor releases. */
|
|
235
|
+
readonly reason: OutOfBoundsReason | string | undefined;
|
|
236
|
+
/** Seconds — present on `rate_limited`. Honored by the transport's retry
|
|
237
|
+
* policy (Step 2.4). */
|
|
238
|
+
readonly retryAfter: number | undefined;
|
|
239
|
+
/** Server-issued request ID, lifted from the `X-Request-Id` response
|
|
240
|
+
* header. Pass this to support when filing bugs. */
|
|
241
|
+
readonly requestId: string | undefined;
|
|
242
|
+
/** The bound value crossed on `out_of_bounds`. */
|
|
243
|
+
readonly bound: number | undefined;
|
|
244
|
+
/** Which rate-limit layer fired on `rate_limited`. */
|
|
245
|
+
readonly layer: string | undefined;
|
|
246
|
+
/** The underlying cause (e.g., a `TypeError: fetch failed`) for
|
|
247
|
+
* network/abort/timeout paths. `undefined` when the error came from a
|
|
248
|
+
* successfully-parsed API error body. */
|
|
249
|
+
readonly cause: unknown;
|
|
250
|
+
constructor(message: string, init: {
|
|
251
|
+
status: number;
|
|
252
|
+
code: ScorezillaErrorCode;
|
|
253
|
+
reason?: string | undefined;
|
|
254
|
+
retryAfter?: number | undefined;
|
|
255
|
+
requestId?: string | undefined;
|
|
256
|
+
bound?: number | undefined;
|
|
257
|
+
layer?: string | undefined;
|
|
258
|
+
cause?: unknown;
|
|
259
|
+
});
|
|
260
|
+
/** `true` when this error is a 429 / `rate_limited`. */
|
|
261
|
+
isRateLimited(): boolean;
|
|
262
|
+
/** `true` when this error is a 401 / `unauthorized` (or 403 / `forbidden`). */
|
|
263
|
+
isAuth(): boolean;
|
|
264
|
+
/** `true` when this error is a 404 / `not_found`. */
|
|
265
|
+
isNotFound(): boolean;
|
|
266
|
+
/** `true` when this error is a 422 / `out_of_bounds` (score below/above board limit). */
|
|
267
|
+
isOutOfBounds(): boolean;
|
|
268
|
+
/** `true` for transient / retryable conditions: network errors, timeouts,
|
|
269
|
+
* 5xx, and 429. The transport layer relies on this for its retry policy. */
|
|
270
|
+
isTransient(): boolean;
|
|
271
|
+
/**
|
|
272
|
+
* Build a `ScorezillaError` from a fetch round-trip outcome.
|
|
273
|
+
*
|
|
274
|
+
* Prefer this over `new ScorezillaError(...)` from the transport layer —
|
|
275
|
+
* it does the mapping from API response shape to error fields in one
|
|
276
|
+
* place, so future fields like `correlationId` get added once here.
|
|
277
|
+
*
|
|
278
|
+
* @param init - status, optional parsed body, optional requestId, optional cause
|
|
279
|
+
*/
|
|
280
|
+
static from(init: ScorezillaErrorFromInit): ScorezillaError;
|
|
281
|
+
/**
|
|
282
|
+
* Build a `ScorezillaError` for a transport-level failure (no HTTP
|
|
283
|
+
* response received): network error, abort, or timeout.
|
|
284
|
+
*/
|
|
285
|
+
static network(message: string, cause: unknown): ScorezillaError;
|
|
286
|
+
/** Build a `ScorezillaError` for an `AbortSignal`-triggered cancellation. */
|
|
287
|
+
static aborted(cause: unknown): ScorezillaError;
|
|
288
|
+
/** Build a `ScorezillaError` for a request that exceeded its timeout budget. */
|
|
289
|
+
static timeout(timeoutMs: number): ScorezillaError;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
export { type ApiSuccess as A, type BaseConfig as B, type LeaderboardResponse as L, type OutOfBoundsReason as O, type PlayerRankResponse as P, type RankedEntry as R, type SecretKeyConfig as S, type WindowAroundResponse as W, type SubmitScoreResponse as a, ScorezillaError as b, type ScorezillaErrorCode as c, type ScorezillaConfig as d, type ApiError as e, type ApiResponse as f, type PublicKeyConfig as g };
|