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
|
@@ -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 };
|
package/dist/index.cjs
CHANGED
|
@@ -18,24 +18,10 @@ function validateConfig(cfg) {
|
|
|
18
18
|
}
|
|
19
19
|
let auth;
|
|
20
20
|
if (hasPublic) {
|
|
21
|
-
|
|
22
|
-
if (typeof pk !== "string" || !PUBLIC_KEY_PATTERN.test(pk)) {
|
|
23
|
-
throw new Error(
|
|
24
|
-
`scorezilla: publicKey must match ${PUBLIC_KEY_PATTERN.toString()} (got: ${typeof pk === "string" ? pk.slice(0, 12) + "\u2026" : typeof pk})`
|
|
25
|
-
);
|
|
26
|
-
}
|
|
27
|
-
auth = { kind: "public", key: pk };
|
|
21
|
+
auth = { kind: "public", key: validatePublicKeyValue(cfg.publicKey) };
|
|
28
22
|
} else {
|
|
29
|
-
const
|
|
30
|
-
|
|
31
|
-
throw new Error("scorezilla: secretKey must be an object with string `id` and `secret`");
|
|
32
|
-
}
|
|
33
|
-
if (!sk.secret.startsWith(SECRET_KEY_PREFIX)) {
|
|
34
|
-
throw new Error(
|
|
35
|
-
`scorezilla: secretKey.secret must start with "${SECRET_KEY_PREFIX}" (live keys only)`
|
|
36
|
-
);
|
|
37
|
-
}
|
|
38
|
-
auth = { kind: "secret", keyId: sk.id, secret: sk.secret };
|
|
23
|
+
const resolved = validateSecretKey(cfg);
|
|
24
|
+
auth = { kind: "secret", keyId: resolved.keyId, secret: resolved.secret };
|
|
39
25
|
}
|
|
40
26
|
const baseUrlRaw = cfg.baseUrl ?? DEFAULT_BASE_URL;
|
|
41
27
|
if (typeof baseUrlRaw !== "string" || baseUrlRaw.length === 0) {
|
|
@@ -46,10 +32,40 @@ function validateConfig(cfg) {
|
|
|
46
32
|
fetch: cfg.fetch,
|
|
47
33
|
timeoutMs: cfg.timeoutMs,
|
|
48
34
|
maxRetries: cfg.maxRetries,
|
|
35
|
+
sleepImpl: cfg.sleepImpl,
|
|
49
36
|
userAgent: cfg.userAgent,
|
|
50
37
|
auth
|
|
51
38
|
};
|
|
52
39
|
}
|
|
40
|
+
function validatePublicKeyValue(pk) {
|
|
41
|
+
if (typeof pk !== "string" || !PUBLIC_KEY_PATTERN.test(pk)) {
|
|
42
|
+
const shape = typeof pk === "string" ? `string of length ${pk.length}` : typeof pk;
|
|
43
|
+
throw new Error(
|
|
44
|
+
`scorezilla: publicKey must match ${PUBLIC_KEY_PATTERN.toString()} (got: ${shape})`
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
return pk;
|
|
48
|
+
}
|
|
49
|
+
var SECRET_KEY_PATTERN = /^sk_live_([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})_[A-Za-z0-9]+$/;
|
|
50
|
+
function validateSecretKey(cfg) {
|
|
51
|
+
if (!cfg || typeof cfg !== "object") {
|
|
52
|
+
throw new Error("scorezilla/server: config must be an object with a secretKey field");
|
|
53
|
+
}
|
|
54
|
+
const sk = cfg.secretKey;
|
|
55
|
+
if (typeof sk !== "string") {
|
|
56
|
+
throw new Error(
|
|
57
|
+
`scorezilla/server: secretKey must be a single string of the shape ${SECRET_KEY_PREFIX}<keyId>_<random> (got: ${typeof sk})`
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
const match = SECRET_KEY_PATTERN.exec(sk);
|
|
61
|
+
if (!match) {
|
|
62
|
+
const shape = `string of length ${sk.length}`;
|
|
63
|
+
throw new Error(
|
|
64
|
+
`scorezilla/server: secretKey must match ${SECRET_KEY_PATTERN.toString()} (got: ${shape}). v0.1.0-next.2 switched to a single-token format \u2014 if you have a pre-next.2 pair, issue a fresh key in the dashboard to upgrade.`
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
return { keyId: match[1], secret: sk };
|
|
68
|
+
}
|
|
53
69
|
|
|
54
70
|
// src/paths.ts
|
|
55
71
|
function encodeSegment(value, label) {
|
|
@@ -89,6 +105,12 @@ function truncateMessage(raw) {
|
|
|
89
105
|
const sliceEnd = Math.max(0, MESSAGE_MAX_CHARS - TRUNCATION_SUFFIX.length);
|
|
90
106
|
return raw.slice(0, sliceEnd) + TRUNCATION_SUFFIX;
|
|
91
107
|
}
|
|
108
|
+
function truncateField(raw) {
|
|
109
|
+
if (typeof raw !== "string") return void 0;
|
|
110
|
+
if (raw.length <= MESSAGE_MAX_CHARS) return raw;
|
|
111
|
+
const sliceEnd = Math.max(0, MESSAGE_MAX_CHARS - TRUNCATION_SUFFIX.length);
|
|
112
|
+
return raw.slice(0, sliceEnd) + TRUNCATION_SUFFIX;
|
|
113
|
+
}
|
|
92
114
|
var ScorezillaError = class _ScorezillaError extends Error {
|
|
93
115
|
/** HTTP status of the response, or {@link STATUS_NETWORK_ERROR} (0) for
|
|
94
116
|
* network / abort / timeout. */
|
|
@@ -119,11 +141,11 @@ var ScorezillaError = class _ScorezillaError extends Error {
|
|
|
119
141
|
this.name = "ScorezillaError";
|
|
120
142
|
this.status = init.status;
|
|
121
143
|
this.code = init.code;
|
|
122
|
-
this.reason = init.reason;
|
|
144
|
+
this.reason = truncateField(init.reason);
|
|
123
145
|
this.retryAfter = init.retryAfter;
|
|
124
|
-
this.requestId = init.requestId;
|
|
146
|
+
this.requestId = truncateField(init.requestId);
|
|
125
147
|
this.bound = init.bound;
|
|
126
|
-
this.layer = init.layer;
|
|
148
|
+
this.layer = truncateField(init.layer);
|
|
127
149
|
this.cause = init.cause;
|
|
128
150
|
Object.setPrototypeOf(this, _ScorezillaError.prototype);
|
|
129
151
|
if (typeof Error.captureStackTrace === "function") {
|
|
@@ -253,8 +275,9 @@ function shouldRetryError(err) {
|
|
|
253
275
|
function generateIdempotencyKey() {
|
|
254
276
|
const c = globalThis.crypto;
|
|
255
277
|
if (!c || typeof c.randomUUID !== "function") {
|
|
256
|
-
throw new
|
|
257
|
-
"scorezilla: globalThis.crypto.randomUUID is unavailable. The SDK requires Node \u2265 20 or a modern browser. Check your runtime."
|
|
278
|
+
throw new ScorezillaError(
|
|
279
|
+
"scorezilla: globalThis.crypto.randomUUID is unavailable. The SDK requires Node \u2265 20 or a modern browser. Check your runtime.",
|
|
280
|
+
{ status: 0, code: "internal_error" }
|
|
258
281
|
);
|
|
259
282
|
}
|
|
260
283
|
return c.randomUUID();
|
|
@@ -309,16 +332,24 @@ async function request(opts) {
|
|
|
309
332
|
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
|
310
333
|
const combined = combineSignalsWithTimeout(opts.signal, timeoutMs);
|
|
311
334
|
try {
|
|
335
|
+
const bodyString = opts.body !== void 0 ? JSON.stringify(opts.body) : "";
|
|
336
|
+
const perAttemptHeaders = { ...opts.headers ?? {} };
|
|
337
|
+
if (opts.signRequest) {
|
|
338
|
+
perAttemptHeaders.Authorization = await opts.signRequest({
|
|
339
|
+
method: opts.method,
|
|
340
|
+
pathAndQuery: opts.path,
|
|
341
|
+
body: bodyString
|
|
342
|
+
});
|
|
343
|
+
}
|
|
312
344
|
const init = {
|
|
313
345
|
method: opts.method,
|
|
314
|
-
headers: buildHeaders(opts, idempotencyKey),
|
|
346
|
+
headers: buildHeaders({ ...opts, headers: perAttemptHeaders }, idempotencyKey),
|
|
315
347
|
signal: combined.signal
|
|
316
348
|
};
|
|
317
349
|
if (opts.body !== void 0) {
|
|
318
|
-
init.body =
|
|
350
|
+
init.body = bodyString;
|
|
319
351
|
}
|
|
320
352
|
const response = await fetchImpl(url, init);
|
|
321
|
-
combined.cleanup();
|
|
322
353
|
if (response.ok) {
|
|
323
354
|
return await parseJson(response);
|
|
324
355
|
}
|
|
@@ -337,7 +368,6 @@ async function request(opts) {
|
|
|
337
368
|
}
|
|
338
369
|
throw err;
|
|
339
370
|
} catch (caught) {
|
|
340
|
-
combined.cleanup();
|
|
341
371
|
if (caught instanceof ScorezillaError) {
|
|
342
372
|
if (shouldRetryError(caught) && attempt < maxRetries) {
|
|
343
373
|
const delay = nextDelay(attempt, void 0, random);
|
|
@@ -355,6 +385,8 @@ async function request(opts) {
|
|
|
355
385
|
continue;
|
|
356
386
|
}
|
|
357
387
|
throw mapped;
|
|
388
|
+
} finally {
|
|
389
|
+
combined.cleanup();
|
|
358
390
|
}
|
|
359
391
|
}
|
|
360
392
|
throw lastError ?? new ScorezillaError("Request failed after retries", {
|
|
@@ -380,16 +412,38 @@ function buildHeaders(opts, idempotencyKey) {
|
|
|
380
412
|
return headers;
|
|
381
413
|
}
|
|
382
414
|
async function parseJson(response) {
|
|
415
|
+
const requestId = response.headers.get("X-Request-Id") ?? void 0;
|
|
416
|
+
let parsed;
|
|
383
417
|
try {
|
|
384
|
-
|
|
418
|
+
parsed = await response.json();
|
|
385
419
|
} catch (cause) {
|
|
386
420
|
throw new ScorezillaError("Response body was not valid JSON", {
|
|
387
421
|
status: response.status,
|
|
388
422
|
code: "invalid_json",
|
|
389
|
-
requestId
|
|
423
|
+
requestId,
|
|
390
424
|
cause
|
|
391
425
|
});
|
|
392
426
|
}
|
|
427
|
+
if (parsed === null || typeof parsed !== "object") {
|
|
428
|
+
const observed = parsed === null ? "null" : typeof parsed;
|
|
429
|
+
throw new ScorezillaError(`Response body was not a JSON object (got ${observed})`, {
|
|
430
|
+
status: response.status,
|
|
431
|
+
code: "invalid_json",
|
|
432
|
+
requestId
|
|
433
|
+
});
|
|
434
|
+
}
|
|
435
|
+
const okField = parsed.ok;
|
|
436
|
+
if (okField !== true) {
|
|
437
|
+
throw new ScorezillaError(
|
|
438
|
+
`Response body on a 2xx is missing the \`ok: true\` discriminator (got ok=${String(okField)})`,
|
|
439
|
+
{
|
|
440
|
+
status: response.status,
|
|
441
|
+
code: "invalid_json",
|
|
442
|
+
requestId
|
|
443
|
+
}
|
|
444
|
+
);
|
|
445
|
+
}
|
|
446
|
+
return parsed;
|
|
393
447
|
}
|
|
394
448
|
async function safelyParseErrorBody(response) {
|
|
395
449
|
try {
|
|
@@ -499,7 +553,7 @@ function validateMetadata(metadata) {
|
|
|
499
553
|
}
|
|
500
554
|
var Scorezilla = class _Scorezilla {
|
|
501
555
|
/** The package version, injected at build time from `package.json`. */
|
|
502
|
-
static version = "0.1.0-next.
|
|
556
|
+
static version = "0.1.0-next.3";
|
|
503
557
|
#config;
|
|
504
558
|
#userAgent;
|
|
505
559
|
#authHeader;
|
|
@@ -664,8 +718,11 @@ var Scorezilla = class _Scorezilla {
|
|
|
664
718
|
if (opts.body !== void 0) requestOpts.body = opts.body;
|
|
665
719
|
if (this.#config.fetch !== void 0) requestOpts.fetchImpl = this.#config.fetch;
|
|
666
720
|
if (this.#config.timeoutMs !== void 0) requestOpts.timeoutMs = this.#config.timeoutMs;
|
|
667
|
-
if (this.#config.maxRetries !== void 0) {
|
|
668
|
-
requestOpts.retry = {
|
|
721
|
+
if (this.#config.maxRetries !== void 0 || this.#config.sleepImpl !== void 0) {
|
|
722
|
+
requestOpts.retry = {
|
|
723
|
+
...this.#config.maxRetries !== void 0 ? { maxRetries: this.#config.maxRetries } : {},
|
|
724
|
+
...this.#config.sleepImpl !== void 0 ? { sleepImpl: this.#config.sleepImpl } : {}
|
|
725
|
+
};
|
|
669
726
|
}
|
|
670
727
|
return request(requestOpts);
|
|
671
728
|
}
|
|
@@ -675,7 +732,7 @@ function createClient(config) {
|
|
|
675
732
|
}
|
|
676
733
|
|
|
677
734
|
// src/index.ts
|
|
678
|
-
var SDK_VERSION = "0.1.0-next.
|
|
735
|
+
var SDK_VERSION = "0.1.0-next.3";
|
|
679
736
|
|
|
680
737
|
exports.SDK_VERSION = SDK_VERSION;
|
|
681
738
|
exports.Scorezilla = Scorezilla;
|