scorezilla 0.1.0-next.3 → 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 +153 -0
- package/README.md +13 -0
- package/dist/{errors-CUAQsaVS.d.cts → errors-B7hyC-C5.d.cts} +79 -5
- package/dist/{errors-CUAQsaVS.d.ts → errors-B7hyC-C5.d.ts} +79 -5
- package/dist/index.cjs +96 -10
- 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 +96 -10
- package/dist/index.js.map +1 -1
- package/dist/server.cjs +95 -9
- package/dist/server.cjs.map +1 -1
- package/dist/server.d.cts +19 -13
- package/dist/server.d.ts +19 -13
- package/dist/server.js +95 -9
- package/dist/server.js.map +1 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,158 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.2.0 — `scorezilla/server` HMAC adapter GA
|
|
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
|
+
- [`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.
|
|
26
|
+
|
|
27
|
+
Includes the v0.1.0 public-key client surface:
|
|
28
|
+
- `Scorezilla` class with `submitScore`, `getLeaderboard`, `getPlayerRank`, `getWindowAround`
|
|
29
|
+
- `ScorezillaError` for every failure path (network, timeout, abort, HTTP non-2xx)
|
|
30
|
+
- Universal runtime support (browsers, Node ≥ 20, Cloudflare Workers, Bun, Deno)
|
|
31
|
+
- Automatic retries with idempotency keys
|
|
32
|
+
- Dual ESM/CJS build with [arethetypeswrong](https://arethetypeswrong.github.io/)-clean exports map
|
|
33
|
+
- ~3.8 KB ESM gzipped
|
|
34
|
+
|
|
35
|
+
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.
|
|
36
|
+
|
|
37
|
+
- [#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).
|
|
38
|
+
|
|
39
|
+
The new `scorezilla/server` subpath ships an HMAC-SHA256 signing client for
|
|
40
|
+
game backends that need cheat-resistant submissions. The browser-side
|
|
41
|
+
public-key client (`scorezilla`) is unchanged.
|
|
42
|
+
|
|
43
|
+
```ts
|
|
44
|
+
import { Scorezilla } from 'scorezilla/server';
|
|
45
|
+
|
|
46
|
+
const sz = new Scorezilla({
|
|
47
|
+
secretKey: {
|
|
48
|
+
id: process.env.SCOREZILLA_KEY_ID,
|
|
49
|
+
secret: process.env.SCOREZILLA_KEY_SECRET, // never ship to a browser
|
|
50
|
+
},
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
await sz.submitScore({ boardId, playerId, score, metadata });
|
|
54
|
+
await sz.getLeaderboard({ boardId, top });
|
|
55
|
+
await sz.getPlayerRank({ boardId, playerId });
|
|
56
|
+
await sz.getWindowAround({ boardId, playerId, before, after });
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
Behavior:
|
|
60
|
+
- Each request is signed with HMAC-SHA256 over a canonical
|
|
61
|
+
`{ts}\n{nonce}\n{METHOD}\n{path?query}\n{sha256_hex(body)}` string and
|
|
62
|
+
delivered via `Authorization: Scorezilla-HMAC-SHA256 keyId=…, ts=…,
|
|
63
|
+
nonce=…, signature=…`. Matches the API's verifier byte-for-byte.
|
|
64
|
+
- `submitScore` posts to `/v1/secure/scores` (the boardId moves into the
|
|
65
|
+
body — the public-key endpoint at `/v1/boards/:id/scores` is unchanged).
|
|
66
|
+
- Read methods (`getLeaderboard` / `getPlayerRank` / `getWindowAround`)
|
|
67
|
+
hit the same paths as the public-key client.
|
|
68
|
+
- Every retry attempt regenerates `(ts, nonce)` so server-side replay
|
|
69
|
+
protection (10-minute nonce window) doesn't trip.
|
|
70
|
+
- Importing `scorezilla/server` from a browser bundle throws at module
|
|
71
|
+
evaluation — the `sk_live_*` secret never reaches client-side code.
|
|
72
|
+
|
|
73
|
+
Same `ScorezillaError` surface (`isRateLimited()`, `isAuth()`,
|
|
74
|
+
`isTransient()`, `code`, `requestId`, etc.) — typed catch patterns
|
|
75
|
+
written for v0.1 work unchanged.
|
|
76
|
+
|
|
77
|
+
Bundle: ~3.84 KB ESM gzipped, ~4.04 KB CJS gzipped — well under the
|
|
78
|
+
6 KB per-entry cap.
|
|
79
|
+
|
|
80
|
+
- [#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
|
|
81
|
+
change vs. v0.1.0-next.1; we're still in pre-release so this is permitted.
|
|
82
|
+
|
|
83
|
+
Before (v0.1.0-next.1):
|
|
84
|
+
|
|
85
|
+
```ts
|
|
86
|
+
new Scorezilla({
|
|
87
|
+
secretKey: { id: 'sk-id-uuid', secret: 'sk_live_xxxxx' },
|
|
88
|
+
});
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
After (v0.1.0-next.2):
|
|
92
|
+
|
|
93
|
+
```ts
|
|
94
|
+
new Scorezilla({
|
|
95
|
+
secretKey: 'sk_live_<keyId>_xxxxx', // one self-contained token
|
|
96
|
+
});
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
The keyId is embedded in the secret string itself; the SDK parses it
|
|
100
|
+
out via `/^sk_live_([0-9a-f]{8}-...)_/` before signing. The HMAC key
|
|
101
|
+
remains the WHOLE plaintext. Wire format on the Authorization header
|
|
102
|
+
is unchanged. API verification is unchanged.
|
|
103
|
+
|
|
104
|
+
Rationale: matches Stripe's design and the public-key client's
|
|
105
|
+
single-string shape. One value to copy from the dashboard, one to
|
|
106
|
+
manage in env config, one to pass into the constructor. Previously
|
|
107
|
+
users had to manage two distinct values (`id` AND `secret`) which
|
|
108
|
+
created confusion — they're functionally one credential.
|
|
109
|
+
|
|
110
|
+
**To upgrade**: issue (or rotate) a fresh secret key in the dashboard.
|
|
111
|
+
Old-format keys (no embedded keyId) are rejected by the SDK with a
|
|
112
|
+
clear error message pointing at the migration path.
|
|
113
|
+
|
|
114
|
+
- [#30](https://github.com/isco-tec/scorezilla-js/pull/30) [`39fc70f`](https://github.com/isco-tec/scorezilla-js/commit/39fc70fa3a38bce619e87aa9f6179e79f1ae45ff) Thanks [@isco-tec](https://github.com/isco-tec)! - Handle HTTP 402 `usage_cap_exceeded` + observe API deprecation headers.
|
|
115
|
+
|
|
116
|
+
**402 / `usage_cap_exceeded`**: when the server returns 402 (tenant hit
|
|
117
|
+
their monthly submission cap or is suspended), the SDK now surfaces a
|
|
118
|
+
typed `ScorezillaError` with the full cap context:
|
|
119
|
+
- `err.code === 'usage_cap_exceeded'`
|
|
120
|
+
- `err.reason === 'over_cap' | 'suspended'`
|
|
121
|
+
- `err.tier`, `err.cap`, `err.count`, `err.period`, `err.resetsAt`
|
|
122
|
+
- `err.isUsageCapExceeded()` predicate
|
|
123
|
+
- `err.isSuspended()` predicate (distinguishes the suspended sub-case)
|
|
124
|
+
- `err.isTransient() === false` — no auto-retry (the cap doesn't lift
|
|
125
|
+
until `resetsAt`)
|
|
126
|
+
|
|
127
|
+
**Deprecation signals**: when an API response carries an RFC 8594
|
|
128
|
+
`Sunset` header or IETF `Deprecation` header, the SDK now logs a
|
|
129
|
+
once-per-process `console.warn` so developers see deprecation
|
|
130
|
+
notices during dev without prod-loop spam. The warning includes the
|
|
131
|
+
sunset date and any `Link: rel="deprecation"` documentation URL.
|
|
132
|
+
|
|
133
|
+
No breaking changes. New fields are additive; existing error handling
|
|
134
|
+
keeps working unchanged.
|
|
135
|
+
|
|
136
|
+
### Patch Changes
|
|
137
|
+
|
|
138
|
+
- [#31](https://github.com/isco-tec/scorezilla-js/pull/31) [`9376a2d`](https://github.com/isco-tec/scorezilla-js/commit/9376a2d93d4edb42decca1bcf495b83fc013c309) Thanks [@isco-tec](https://github.com/isco-tec)! - **Error mapping, retry-policy alignment, `AbortSignal` plumbing, injectable warn, validator-return tightening.**
|
|
139
|
+
|
|
140
|
+
Bundled correctness + ergonomics fixes from the in-house improvement-phase review:
|
|
141
|
+
- `ScorezillaError.code` now resolves to `'conflict'` on HTTP 409 responses (previously fell through to `'invalid_input'`). The error type already listed `'conflict'` as a valid code; the gap was in the status→code mapper. A new `e.isConflict()` predicate joins the existing `isAuth() / isNotFound() / isRateLimited() / isOutOfBounds() / isUsageCapExceeded() / isTransient()` family.
|
|
142
|
+
- `e.isTransient()` no longer flags `timeout` or `aborted` as transient — those are caller-observable terminal states, not retryable. The predicate is now aligned with the transport's actual retry policy (`shouldRetryError` in `retry.ts`), so consumer code mirroring "if (e.isTransient()) retry()" no longer loops on timeouts.
|
|
143
|
+
- All four public methods on **both** the public-key client (`Scorezilla`) and the secret-key server adapter (`Scorezilla` from `scorezilla/server`) now accept an optional `signal?: AbortSignal` via a shared `CancellableInput` shape, and forward it through the transport. Unblocks request-cancellation propagation under frameworks that thread cancellation through the request lifecycle (Next.js route handlers, Hono, Express middleware, React effect cleanup).
|
|
144
|
+
- New `warn?: (...args: unknown[]) => void` field on `ScorezillaConfig`. Defaults to `console.warn`. Pass your logger to route SDK deprecation notices into your observability stack, or pass `() => {}` to suppress them. Used today only for the `Deprecation` / `Sunset` once-per-process warning emitted when the API signals an upcoming sunset — at million-integration scale, embedders shouldn't have to console-filter our messages.
|
|
145
|
+
- `validateMetadata` now returns the serialized JSON string it computed (previously declared `void` despite a comment that claimed otherwise). Public callers that re-stringify metadata can reuse the value and skip a duplicate `JSON.stringify` pass on the hot submit path.
|
|
146
|
+
|
|
147
|
+
No breaking changes for callers that didn't rely on the two buggy classifications, didn't need cancellation, and didn't read `validateMetadata`'s return value.
|
|
148
|
+
|
|
149
|
+
- [#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`:
|
|
150
|
+
- **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.
|
|
151
|
+
- **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)`).
|
|
152
|
+
- **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`.
|
|
153
|
+
|
|
154
|
+
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.
|
|
155
|
+
|
|
3
156
|
## 0.1.0-next.3
|
|
4
157
|
|
|
5
158
|
### Minor Changes
|
package/README.md
CHANGED
|
@@ -43,6 +43,19 @@ yarn add scorezilla
|
|
|
43
43
|
bun add scorezilla
|
|
44
44
|
```
|
|
45
45
|
|
|
46
|
+
## Get your keys
|
|
47
|
+
|
|
48
|
+
The Quickstart below needs two values: a `publicKey` and a `boardId`. Both come from the dashboard:
|
|
49
|
+
|
|
50
|
+
1. **Sign in** at [dashboard.scorezilla.dev](https://dashboard.scorezilla.dev) (magic-link email — no password).
|
|
51
|
+
2. **Open the Tutorial Game** that's created for you on first sign-in (or create a new game).
|
|
52
|
+
3. **Open Keys → Issue public key**. Copy the `pk_*` string (shown once — also visible in the keys list afterwards).
|
|
53
|
+
4. **Open Boards → New board** (or use the auto-created "High Scores"). Copy the `boardId` UUID.
|
|
54
|
+
|
|
55
|
+
You're now ready for the Quickstart below. **Public keys are safe to ship in client code** — they only authorize submits, not key rotation or board admin.
|
|
56
|
+
|
|
57
|
+
> Using AI to scaffold? Skip the manual steps: install the [Scorezilla MCP server](https://github.com/isco-tec/scorezilla-mcp) in Claude Code / Cursor and run `bootstrap_leaderboard` — the AI gets your keys + board id + a ready-to-paste integration snippet in one tool call.
|
|
58
|
+
|
|
46
59
|
## Quickstart
|
|
47
60
|
|
|
48
61
|
```ts
|
|
@@ -37,6 +37,13 @@ interface BaseConfig {
|
|
|
37
37
|
* this unset to use the default exponential backoff with jitter.
|
|
38
38
|
* @internal */
|
|
39
39
|
sleepImpl?: (ms: number, signal?: AbortSignal) => Promise<void>;
|
|
40
|
+
/** Inject a sink for SDK deprecation warnings. Defaults to
|
|
41
|
+
* `console.warn`. Pass your logger's `warn` to route SDK signals into
|
|
42
|
+
* the rest of your observability stack, or pass `() => {}` to
|
|
43
|
+
* suppress them. The SDK uses this ONLY for developer-visible
|
|
44
|
+
* deprecation notices triggered by `Deprecation` / `Sunset` response
|
|
45
|
+
* headers — never for runtime errors. */
|
|
46
|
+
warn?: (...args: unknown[]) => void;
|
|
40
47
|
}
|
|
41
48
|
/** Public-key auth: browser-safe path. The key is fingerprinted to a game
|
|
42
49
|
* on the server side via `pk_<gameSlug>_<base62>`. */
|
|
@@ -86,9 +93,22 @@ type ScorezillaConfig = PublicKeyConfig | SecretKeyConfig;
|
|
|
86
93
|
*
|
|
87
94
|
* @stable v0.1.0
|
|
88
95
|
*/
|
|
89
|
-
type ScorezillaErrorCode = 'unauthorized' | 'forbidden' | 'not_found' | 'invalid_input' | 'invalid_json' | 'out_of_bounds' | 'rate_limited' | 'conflict' | 'internal_error'
|
|
96
|
+
type ScorezillaErrorCode = 'unauthorized' | 'forbidden' | 'not_found' | 'invalid_input' | 'invalid_json' | 'out_of_bounds' | 'rate_limited' | 'conflict' | 'internal_error'
|
|
97
|
+
/** 402 Payment Required — tenant exceeded their monthly submission cap, OR
|
|
98
|
+
* the tenant is `'suspended'` (see {@link UsageCapReason}). The error body
|
|
99
|
+
* carries `tier`, `cap`, `count`, `period`, `resetsAt`. */
|
|
100
|
+
| 'usage_cap_exceeded' | (string & {});
|
|
90
101
|
/** Reason sub-classifier on `out_of_bounds` errors. Open union — see {@link ScorezillaErrorCode}. */
|
|
91
102
|
type OutOfBoundsReason = 'below_min' | 'above_max' | (string & {});
|
|
103
|
+
/** Reason sub-classifier on `usage_cap_exceeded` errors.
|
|
104
|
+
* - `'over_cap'` — tenant hit their tier's monthly submit limit
|
|
105
|
+
* - `'suspended'` — tenant's `billing_status` is `'suspended'`; every
|
|
106
|
+
* submit is rejected (cap is structurally 0)
|
|
107
|
+
* Open union for forward compatibility.
|
|
108
|
+
*/
|
|
109
|
+
type UsageCapReason = 'over_cap' | 'suspended' | (string & {});
|
|
110
|
+
/** Tier identifier mirrored from the server's `PlanConfig.key`. */
|
|
111
|
+
type BillingTier = 'free' | 'indie' | 'pro' | 'studio' | 'enterprise' | 'suspended' | (string & {});
|
|
92
112
|
/** Successful API response envelope. The `T` is the per-route payload. */
|
|
93
113
|
type ApiSuccess<T> = {
|
|
94
114
|
ok: true;
|
|
@@ -107,6 +127,21 @@ interface ApiError {
|
|
|
107
127
|
layer?: string;
|
|
108
128
|
/** The limit value that was crossed — present on `out_of_bounds`. */
|
|
109
129
|
bound?: number;
|
|
130
|
+
/** Tenant's tier at the time of rejection. */
|
|
131
|
+
tier?: BillingTier;
|
|
132
|
+
/** The cap value that was crossed (monthly submit limit). `0` for
|
|
133
|
+
* suspended tenants; `null` is never sent (enterprise has no cap and
|
|
134
|
+
* can never be over). */
|
|
135
|
+
cap?: number;
|
|
136
|
+
/** The post-increment submit count when the cap was crossed. Always
|
|
137
|
+
* > `cap` when `reason === 'over_cap'`. */
|
|
138
|
+
count?: number;
|
|
139
|
+
/** The period the count belongs to, in `YYYY-MM` UTC form. */
|
|
140
|
+
period?: string;
|
|
141
|
+
/** ISO-8601 timestamp of midnight UTC on the 1st of the next month —
|
|
142
|
+
* when the counter resets. Lets clients compute `Retry-After` without
|
|
143
|
+
* parsing dates server-side. */
|
|
144
|
+
resetsAt?: string;
|
|
110
145
|
}
|
|
111
146
|
/** Discriminated envelope: every API response is one of these two shapes. */
|
|
112
147
|
type ApiResponse<T> = ApiSuccess<T> | ApiError;
|
|
@@ -230,9 +265,11 @@ declare class ScorezillaError extends Error {
|
|
|
230
265
|
* {@link ScorezillaErrorCode}. For network errors, this is `'network_error'`;
|
|
231
266
|
* for aborts, `'aborted'`; for timeouts, `'timeout'`. */
|
|
232
267
|
readonly code: ScorezillaErrorCode;
|
|
233
|
-
/** Sub-classifier — present on
|
|
268
|
+
/** Sub-classifier — present on:
|
|
269
|
+
* - `out_of_bounds`: `'below_min' | 'above_max'`
|
|
270
|
+
* - `usage_cap_exceeded`: `'over_cap' | 'suspended'`
|
|
234
271
|
* and possibly other codes in future minor releases. */
|
|
235
|
-
readonly reason: OutOfBoundsReason | string | undefined;
|
|
272
|
+
readonly reason: OutOfBoundsReason | UsageCapReason | string | undefined;
|
|
236
273
|
/** Seconds — present on `rate_limited`. Honored by the transport's retry
|
|
237
274
|
* policy (Step 2.4). */
|
|
238
275
|
readonly retryAfter: number | undefined;
|
|
@@ -243,6 +280,20 @@ declare class ScorezillaError extends Error {
|
|
|
243
280
|
readonly bound: number | undefined;
|
|
244
281
|
/** Which rate-limit layer fired on `rate_limited`. */
|
|
245
282
|
readonly layer: string | undefined;
|
|
283
|
+
/** Tenant's billing tier — present on `usage_cap_exceeded`. */
|
|
284
|
+
readonly tier: BillingTier | undefined;
|
|
285
|
+
/** The cap value crossed on `usage_cap_exceeded`. `0` indicates a
|
|
286
|
+
* suspended tenant. `undefined` on all other error codes. */
|
|
287
|
+
readonly cap: number | undefined;
|
|
288
|
+
/** The post-increment submit count on `usage_cap_exceeded`. Always
|
|
289
|
+
* `> cap` when `reason === 'over_cap'`. */
|
|
290
|
+
readonly count: number | undefined;
|
|
291
|
+
/** The period the count belongs to on `usage_cap_exceeded`, in `YYYY-MM`
|
|
292
|
+
* UTC form. */
|
|
293
|
+
readonly period: string | undefined;
|
|
294
|
+
/** ISO-8601 timestamp of midnight UTC on the 1st of the next month —
|
|
295
|
+
* the counter's natural reset point on `usage_cap_exceeded`. */
|
|
296
|
+
readonly resetsAt: string | undefined;
|
|
246
297
|
/** The underlying cause (e.g., a `TypeError: fetch failed`) for
|
|
247
298
|
* network/abort/timeout paths. `undefined` when the error came from a
|
|
248
299
|
* successfully-parsed API error body. */
|
|
@@ -255,19 +306,42 @@ declare class ScorezillaError extends Error {
|
|
|
255
306
|
requestId?: string | undefined;
|
|
256
307
|
bound?: number | undefined;
|
|
257
308
|
layer?: string | undefined;
|
|
309
|
+
tier?: BillingTier | undefined;
|
|
310
|
+
cap?: number | undefined;
|
|
311
|
+
count?: number | undefined;
|
|
312
|
+
period?: string | undefined;
|
|
313
|
+
resetsAt?: string | undefined;
|
|
258
314
|
cause?: unknown;
|
|
259
315
|
});
|
|
260
316
|
/** `true` when this error is a 429 / `rate_limited`. */
|
|
261
317
|
isRateLimited(): boolean;
|
|
318
|
+
/**
|
|
319
|
+
* `true` when this error is a 402 / `usage_cap_exceeded`. The tenant
|
|
320
|
+
* has either hit their tier's monthly submit cap (`reason ===
|
|
321
|
+
* 'over_cap'`) or is suspended (`reason === 'suspended'`).
|
|
322
|
+
*
|
|
323
|
+
* Consumers SHOULD NOT auto-retry on this error — the cap doesn't lift
|
|
324
|
+
* until `resetsAt`. Surface to the developer with an upgrade prompt
|
|
325
|
+
* (over_cap) or contact-support message (suspended).
|
|
326
|
+
*/
|
|
327
|
+
isUsageCapExceeded(): boolean;
|
|
328
|
+
/** `true` when this error is a 402 + reason 'suspended' (vs over-cap). */
|
|
329
|
+
isSuspended(): boolean;
|
|
262
330
|
/** `true` when this error is a 401 / `unauthorized` (or 403 / `forbidden`). */
|
|
263
331
|
isAuth(): boolean;
|
|
264
332
|
/** `true` when this error is a 404 / `not_found`. */
|
|
265
333
|
isNotFound(): boolean;
|
|
266
334
|
/** `true` when this error is a 422 / `out_of_bounds` (score below/above board limit). */
|
|
267
335
|
isOutOfBounds(): boolean;
|
|
268
|
-
/** `true` for
|
|
269
|
-
*
|
|
336
|
+
/** `true` for the SDK's retryable conditions: pure network errors, 5xx, and
|
|
337
|
+
* 429. Deliberately excludes `timeout` and `aborted` — those are caller-
|
|
338
|
+
* observable terminal states, not transient. Aligned with `shouldRetryError`
|
|
339
|
+
* in `retry.ts` so a consumer mirroring the SDK's retry policy gets the
|
|
340
|
+
* same answer the transport does. */
|
|
270
341
|
isTransient(): boolean;
|
|
342
|
+
/** `true` when this error is a 409 / `conflict` (idempotency-key conflict
|
|
343
|
+
* on retry). */
|
|
344
|
+
isConflict(): boolean;
|
|
271
345
|
/**
|
|
272
346
|
* Build a `ScorezillaError` from a fetch round-trip outcome.
|
|
273
347
|
*
|
|
@@ -37,6 +37,13 @@ interface BaseConfig {
|
|
|
37
37
|
* this unset to use the default exponential backoff with jitter.
|
|
38
38
|
* @internal */
|
|
39
39
|
sleepImpl?: (ms: number, signal?: AbortSignal) => Promise<void>;
|
|
40
|
+
/** Inject a sink for SDK deprecation warnings. Defaults to
|
|
41
|
+
* `console.warn`. Pass your logger's `warn` to route SDK signals into
|
|
42
|
+
* the rest of your observability stack, or pass `() => {}` to
|
|
43
|
+
* suppress them. The SDK uses this ONLY for developer-visible
|
|
44
|
+
* deprecation notices triggered by `Deprecation` / `Sunset` response
|
|
45
|
+
* headers — never for runtime errors. */
|
|
46
|
+
warn?: (...args: unknown[]) => void;
|
|
40
47
|
}
|
|
41
48
|
/** Public-key auth: browser-safe path. The key is fingerprinted to a game
|
|
42
49
|
* on the server side via `pk_<gameSlug>_<base62>`. */
|
|
@@ -86,9 +93,22 @@ type ScorezillaConfig = PublicKeyConfig | SecretKeyConfig;
|
|
|
86
93
|
*
|
|
87
94
|
* @stable v0.1.0
|
|
88
95
|
*/
|
|
89
|
-
type ScorezillaErrorCode = 'unauthorized' | 'forbidden' | 'not_found' | 'invalid_input' | 'invalid_json' | 'out_of_bounds' | 'rate_limited' | 'conflict' | 'internal_error'
|
|
96
|
+
type ScorezillaErrorCode = 'unauthorized' | 'forbidden' | 'not_found' | 'invalid_input' | 'invalid_json' | 'out_of_bounds' | 'rate_limited' | 'conflict' | 'internal_error'
|
|
97
|
+
/** 402 Payment Required — tenant exceeded their monthly submission cap, OR
|
|
98
|
+
* the tenant is `'suspended'` (see {@link UsageCapReason}). The error body
|
|
99
|
+
* carries `tier`, `cap`, `count`, `period`, `resetsAt`. */
|
|
100
|
+
| 'usage_cap_exceeded' | (string & {});
|
|
90
101
|
/** Reason sub-classifier on `out_of_bounds` errors. Open union — see {@link ScorezillaErrorCode}. */
|
|
91
102
|
type OutOfBoundsReason = 'below_min' | 'above_max' | (string & {});
|
|
103
|
+
/** Reason sub-classifier on `usage_cap_exceeded` errors.
|
|
104
|
+
* - `'over_cap'` — tenant hit their tier's monthly submit limit
|
|
105
|
+
* - `'suspended'` — tenant's `billing_status` is `'suspended'`; every
|
|
106
|
+
* submit is rejected (cap is structurally 0)
|
|
107
|
+
* Open union for forward compatibility.
|
|
108
|
+
*/
|
|
109
|
+
type UsageCapReason = 'over_cap' | 'suspended' | (string & {});
|
|
110
|
+
/** Tier identifier mirrored from the server's `PlanConfig.key`. */
|
|
111
|
+
type BillingTier = 'free' | 'indie' | 'pro' | 'studio' | 'enterprise' | 'suspended' | (string & {});
|
|
92
112
|
/** Successful API response envelope. The `T` is the per-route payload. */
|
|
93
113
|
type ApiSuccess<T> = {
|
|
94
114
|
ok: true;
|
|
@@ -107,6 +127,21 @@ interface ApiError {
|
|
|
107
127
|
layer?: string;
|
|
108
128
|
/** The limit value that was crossed — present on `out_of_bounds`. */
|
|
109
129
|
bound?: number;
|
|
130
|
+
/** Tenant's tier at the time of rejection. */
|
|
131
|
+
tier?: BillingTier;
|
|
132
|
+
/** The cap value that was crossed (monthly submit limit). `0` for
|
|
133
|
+
* suspended tenants; `null` is never sent (enterprise has no cap and
|
|
134
|
+
* can never be over). */
|
|
135
|
+
cap?: number;
|
|
136
|
+
/** The post-increment submit count when the cap was crossed. Always
|
|
137
|
+
* > `cap` when `reason === 'over_cap'`. */
|
|
138
|
+
count?: number;
|
|
139
|
+
/** The period the count belongs to, in `YYYY-MM` UTC form. */
|
|
140
|
+
period?: string;
|
|
141
|
+
/** ISO-8601 timestamp of midnight UTC on the 1st of the next month —
|
|
142
|
+
* when the counter resets. Lets clients compute `Retry-After` without
|
|
143
|
+
* parsing dates server-side. */
|
|
144
|
+
resetsAt?: string;
|
|
110
145
|
}
|
|
111
146
|
/** Discriminated envelope: every API response is one of these two shapes. */
|
|
112
147
|
type ApiResponse<T> = ApiSuccess<T> | ApiError;
|
|
@@ -230,9 +265,11 @@ declare class ScorezillaError extends Error {
|
|
|
230
265
|
* {@link ScorezillaErrorCode}. For network errors, this is `'network_error'`;
|
|
231
266
|
* for aborts, `'aborted'`; for timeouts, `'timeout'`. */
|
|
232
267
|
readonly code: ScorezillaErrorCode;
|
|
233
|
-
/** Sub-classifier — present on
|
|
268
|
+
/** Sub-classifier — present on:
|
|
269
|
+
* - `out_of_bounds`: `'below_min' | 'above_max'`
|
|
270
|
+
* - `usage_cap_exceeded`: `'over_cap' | 'suspended'`
|
|
234
271
|
* and possibly other codes in future minor releases. */
|
|
235
|
-
readonly reason: OutOfBoundsReason | string | undefined;
|
|
272
|
+
readonly reason: OutOfBoundsReason | UsageCapReason | string | undefined;
|
|
236
273
|
/** Seconds — present on `rate_limited`. Honored by the transport's retry
|
|
237
274
|
* policy (Step 2.4). */
|
|
238
275
|
readonly retryAfter: number | undefined;
|
|
@@ -243,6 +280,20 @@ declare class ScorezillaError extends Error {
|
|
|
243
280
|
readonly bound: number | undefined;
|
|
244
281
|
/** Which rate-limit layer fired on `rate_limited`. */
|
|
245
282
|
readonly layer: string | undefined;
|
|
283
|
+
/** Tenant's billing tier — present on `usage_cap_exceeded`. */
|
|
284
|
+
readonly tier: BillingTier | undefined;
|
|
285
|
+
/** The cap value crossed on `usage_cap_exceeded`. `0` indicates a
|
|
286
|
+
* suspended tenant. `undefined` on all other error codes. */
|
|
287
|
+
readonly cap: number | undefined;
|
|
288
|
+
/** The post-increment submit count on `usage_cap_exceeded`. Always
|
|
289
|
+
* `> cap` when `reason === 'over_cap'`. */
|
|
290
|
+
readonly count: number | undefined;
|
|
291
|
+
/** The period the count belongs to on `usage_cap_exceeded`, in `YYYY-MM`
|
|
292
|
+
* UTC form. */
|
|
293
|
+
readonly period: string | undefined;
|
|
294
|
+
/** ISO-8601 timestamp of midnight UTC on the 1st of the next month —
|
|
295
|
+
* the counter's natural reset point on `usage_cap_exceeded`. */
|
|
296
|
+
readonly resetsAt: string | undefined;
|
|
246
297
|
/** The underlying cause (e.g., a `TypeError: fetch failed`) for
|
|
247
298
|
* network/abort/timeout paths. `undefined` when the error came from a
|
|
248
299
|
* successfully-parsed API error body. */
|
|
@@ -255,19 +306,42 @@ declare class ScorezillaError extends Error {
|
|
|
255
306
|
requestId?: string | undefined;
|
|
256
307
|
bound?: number | undefined;
|
|
257
308
|
layer?: string | undefined;
|
|
309
|
+
tier?: BillingTier | undefined;
|
|
310
|
+
cap?: number | undefined;
|
|
311
|
+
count?: number | undefined;
|
|
312
|
+
period?: string | undefined;
|
|
313
|
+
resetsAt?: string | undefined;
|
|
258
314
|
cause?: unknown;
|
|
259
315
|
});
|
|
260
316
|
/** `true` when this error is a 429 / `rate_limited`. */
|
|
261
317
|
isRateLimited(): boolean;
|
|
318
|
+
/**
|
|
319
|
+
* `true` when this error is a 402 / `usage_cap_exceeded`. The tenant
|
|
320
|
+
* has either hit their tier's monthly submit cap (`reason ===
|
|
321
|
+
* 'over_cap'`) or is suspended (`reason === 'suspended'`).
|
|
322
|
+
*
|
|
323
|
+
* Consumers SHOULD NOT auto-retry on this error — the cap doesn't lift
|
|
324
|
+
* until `resetsAt`. Surface to the developer with an upgrade prompt
|
|
325
|
+
* (over_cap) or contact-support message (suspended).
|
|
326
|
+
*/
|
|
327
|
+
isUsageCapExceeded(): boolean;
|
|
328
|
+
/** `true` when this error is a 402 + reason 'suspended' (vs over-cap). */
|
|
329
|
+
isSuspended(): boolean;
|
|
262
330
|
/** `true` when this error is a 401 / `unauthorized` (or 403 / `forbidden`). */
|
|
263
331
|
isAuth(): boolean;
|
|
264
332
|
/** `true` when this error is a 404 / `not_found`. */
|
|
265
333
|
isNotFound(): boolean;
|
|
266
334
|
/** `true` when this error is a 422 / `out_of_bounds` (score below/above board limit). */
|
|
267
335
|
isOutOfBounds(): boolean;
|
|
268
|
-
/** `true` for
|
|
269
|
-
*
|
|
336
|
+
/** `true` for the SDK's retryable conditions: pure network errors, 5xx, and
|
|
337
|
+
* 429. Deliberately excludes `timeout` and `aborted` — those are caller-
|
|
338
|
+
* observable terminal states, not transient. Aligned with `shouldRetryError`
|
|
339
|
+
* in `retry.ts` so a consumer mirroring the SDK's retry policy gets the
|
|
340
|
+
* same answer the transport does. */
|
|
270
341
|
isTransient(): boolean;
|
|
342
|
+
/** `true` when this error is a 409 / `conflict` (idempotency-key conflict
|
|
343
|
+
* on retry). */
|
|
344
|
+
isConflict(): boolean;
|
|
271
345
|
/**
|
|
272
346
|
* Build a `ScorezillaError` from a fetch round-trip outcome.
|
|
273
347
|
*
|