scorezilla 0.3.0-next.1 → 0.3.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/API.md +125 -7
- package/CHANGELOG.md +88 -0
- package/README.md +131 -0
- package/RECIPES.md +227 -0
- package/dist/{errors-B7hyC-C5.d.cts → errors-CWTmormh.d.cts} +1 -1
- package/dist/{errors-B7hyC-C5.d.ts → errors-CWTmormh.d.ts} +1 -1
- package/dist/identity.cjs +116 -4
- package/dist/identity.cjs.map +1 -1
- package/dist/identity.d.cts +53 -20
- package/dist/identity.d.ts +53 -20
- package/dist/identity.js +116 -4
- package/dist/identity.js.map +1 -1
- package/dist/index.cjs +2 -2
- package/dist/index.d.cts +2 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.js +2 -2
- package/dist/server.cjs +344 -1
- package/dist/server.cjs.map +1 -1
- package/dist/server.d.cts +301 -3
- package/dist/server.d.ts +301 -3
- package/dist/server.js +338 -2
- package/dist/server.js.map +1 -1
- package/package.json +14 -3
package/dist/server.d.cts
CHANGED
|
@@ -1,5 +1,187 @@
|
|
|
1
|
-
import {
|
|
2
|
-
export { R as RankedEntry,
|
|
1
|
+
import { F as FetchImpl, g as SecretKeyConfig, A as ApiSuccess, a as SubmitScoreResponse, L as LeaderboardResponse, P as PlayerRankResponse, W as WindowAroundResponse } from './errors-CWTmormh.cjs';
|
|
2
|
+
export { R as RankedEntry, e as ScorezillaError, f as ScorezillaErrorCode } from './errors-CWTmormh.cjs';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Built-in request verifiers for {@link createScoreSubmitHandler} (#211).
|
|
6
|
+
*
|
|
7
|
+
* These turn the common "verify a JWT, derive the player id from a claim"
|
|
8
|
+
* shape into a one-liner. They produce a function with the same signature as
|
|
9
|
+
* the handler's `verify` callback, so they drop straight in:
|
|
10
|
+
*
|
|
11
|
+
* ```ts
|
|
12
|
+
* import { createScoreSubmitHandler, verifySupabaseJwt } from 'scorezilla/server';
|
|
13
|
+
*
|
|
14
|
+
* createScoreSubmitHandler({
|
|
15
|
+
* secretKey, boardId,
|
|
16
|
+
* verify: verifySupabaseJwt({ supabaseUrl: process.env.SUPABASE_URL! }),
|
|
17
|
+
* });
|
|
18
|
+
* ```
|
|
19
|
+
*
|
|
20
|
+
* **`jose` is an optional peer dependency.** Only these verifiers use it, and
|
|
21
|
+
* only via a lazy `import('jose')` — so consumers who use the public-key
|
|
22
|
+
* client, the factory with their own `verify`, or a provider backend SDK never
|
|
23
|
+
* install or load it. If you call a built-in verifier without `jose` present,
|
|
24
|
+
* it throws a clear "install jose" error.
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
/** Same shape as the handler's `verify` callback. */
|
|
28
|
+
type RequestVerifier = (req: Request) => Promise<VerifiedIdentity | null>;
|
|
29
|
+
/** Options for the generic JWKS verifier {@link verifyJwt}. */
|
|
30
|
+
interface VerifyJwtOptions {
|
|
31
|
+
/** JWKS endpoint, e.g. `https://issuer/.well-known/jwks.json`. */
|
|
32
|
+
readonly jwksUrl: string | URL;
|
|
33
|
+
/** Expected `iss` claim — strongly recommended. */
|
|
34
|
+
readonly issuer?: string | string[];
|
|
35
|
+
/** Expected `aud` claim — strongly recommended. */
|
|
36
|
+
readonly audience?: string | string[];
|
|
37
|
+
/** Which claim becomes the `playerId`. Default `'sub'`. */
|
|
38
|
+
readonly claim?: string;
|
|
39
|
+
/** Custom `fetch` for retrieving the JWKS (proxy / self-host / testing). */
|
|
40
|
+
readonly fetch?: typeof fetch;
|
|
41
|
+
}
|
|
42
|
+
/** Options for the Supabase preset {@link verifySupabaseJwt}. */
|
|
43
|
+
interface VerifySupabaseJwtOptions {
|
|
44
|
+
/** Your Supabase project URL, e.g. `https://abcd.supabase.co`. */
|
|
45
|
+
readonly supabaseUrl: string;
|
|
46
|
+
/** Which claim becomes the `playerId`. Default `'sub'`. */
|
|
47
|
+
readonly claim?: string;
|
|
48
|
+
/** Custom `fetch` for retrieving the JWKS (proxy / self-host / testing). */
|
|
49
|
+
readonly fetch?: typeof fetch;
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Generic JWKS-backed JWT verifier. Verifies a `Bearer` token against the
|
|
53
|
+
* given JWKS and returns `{ playerId }` from the configured claim (default
|
|
54
|
+
* `sub`), or `null` if there's no token or verification fails.
|
|
55
|
+
*
|
|
56
|
+
* Covers the modern provider majority (Supabase, Clerk, Auth0, Firebase,
|
|
57
|
+
* WorkOS, …) — they differ only by `jwksUrl` / `issuer` / `audience`.
|
|
58
|
+
*
|
|
59
|
+
* @since 0.3.0
|
|
60
|
+
*/
|
|
61
|
+
declare function verifyJwt(options: VerifyJwtOptions): RequestVerifier;
|
|
62
|
+
/**
|
|
63
|
+
* Supabase preset over {@link verifyJwt}. Verifies a Supabase user JWT via the
|
|
64
|
+
* project's JWKS and derives the `playerId` from `sub`.
|
|
65
|
+
*
|
|
66
|
+
* ```ts
|
|
67
|
+
* verify: verifySupabaseJwt({ supabaseUrl: process.env.SUPABASE_URL! })
|
|
68
|
+
* ```
|
|
69
|
+
*
|
|
70
|
+
* @since 0.3.0
|
|
71
|
+
*/
|
|
72
|
+
declare function verifySupabaseJwt(options: VerifySupabaseJwtOptions): RequestVerifier;
|
|
73
|
+
/** Options for the Clerk preset {@link verifyClerkJwt}. */
|
|
74
|
+
interface VerifyClerkJwtOptions {
|
|
75
|
+
/**
|
|
76
|
+
* Your Clerk instance issuer (the token's `iss`), e.g.
|
|
77
|
+
* `https://clerk.your-app.com` or `https://<slug>.clerk.accounts.dev`. The
|
|
78
|
+
* JWKS URL is derived from it.
|
|
79
|
+
*/
|
|
80
|
+
readonly issuer: string;
|
|
81
|
+
/**
|
|
82
|
+
* Expected `aud`. Clerk session tokens have **no** `aud` by default, so leave
|
|
83
|
+
* this unset unless you added one via a custom JWT template.
|
|
84
|
+
*/
|
|
85
|
+
readonly audience?: string | string[];
|
|
86
|
+
/** Which claim becomes the `playerId`. Default `'sub'` (the Clerk user id). */
|
|
87
|
+
readonly claim?: string;
|
|
88
|
+
/** Custom `fetch` for retrieving the JWKS (proxy / self-host / testing). */
|
|
89
|
+
readonly fetch?: typeof fetch;
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Clerk preset over {@link verifyJwt}. Verifies a Clerk session JWT against the
|
|
93
|
+
* instance JWKS and derives the `playerId` from `sub` (the Clerk user id).
|
|
94
|
+
*
|
|
95
|
+
* @since 0.3.0
|
|
96
|
+
*/
|
|
97
|
+
declare function verifyClerkJwt(options: VerifyClerkJwtOptions): RequestVerifier;
|
|
98
|
+
/** Options for the Auth0 preset {@link verifyAuth0Jwt}. */
|
|
99
|
+
interface VerifyAuth0JwtOptions {
|
|
100
|
+
/** Your Auth0 domain, e.g. `your-tenant.us.auth0.com` (scheme optional). */
|
|
101
|
+
readonly domain: string;
|
|
102
|
+
/** Your API identifier — the access token's `aud`. */
|
|
103
|
+
readonly audience: string | string[];
|
|
104
|
+
/** Which claim becomes the `playerId`. Default `'sub'`. */
|
|
105
|
+
readonly claim?: string;
|
|
106
|
+
/** Custom `fetch` for retrieving the JWKS (proxy / self-host / testing). */
|
|
107
|
+
readonly fetch?: typeof fetch;
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* Auth0 preset over {@link verifyJwt}. Note Auth0's issuer carries a trailing
|
|
111
|
+
* slash (`https://<domain>/`) — the preset adds it for you.
|
|
112
|
+
*
|
|
113
|
+
* @since 0.3.0
|
|
114
|
+
*/
|
|
115
|
+
declare function verifyAuth0Jwt(options: VerifyAuth0JwtOptions): RequestVerifier;
|
|
116
|
+
/** Options for the Firebase preset {@link verifyFirebaseIdToken}. */
|
|
117
|
+
interface VerifyFirebaseIdTokenOptions {
|
|
118
|
+
/** Your Firebase project id — both the token's `aud` and the issuer suffix. */
|
|
119
|
+
readonly projectId: string;
|
|
120
|
+
/** Which claim becomes the `playerId`. Default `'sub'` (the Firebase uid). */
|
|
121
|
+
readonly claim?: string;
|
|
122
|
+
/** Custom `fetch` for retrieving the JWKS (proxy / self-host / testing). */
|
|
123
|
+
readonly fetch?: typeof fetch;
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* Firebase Authentication preset over {@link verifyJwt}. Verifies a Firebase ID
|
|
127
|
+
* token (`iss = https://securetoken.google.com/<projectId>`, `aud = projectId`)
|
|
128
|
+
* and derives the `playerId` from `sub` (the Firebase uid).
|
|
129
|
+
*
|
|
130
|
+
* @since 0.3.0
|
|
131
|
+
*/
|
|
132
|
+
declare function verifyFirebaseIdToken(options: VerifyFirebaseIdTokenOptions): RequestVerifier;
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* `createGitHubOAuthHandler` — the server-side callback leg of the GitHub
|
|
136
|
+
* identity provider (`useAuthProvider({ provider: 'github' })`, ADR 0009).
|
|
137
|
+
*
|
|
138
|
+
* GitHub redirects the sign-in popup here with `?code=&state=`. This handler:
|
|
139
|
+
* 1. validates the query strictly (formats are pinned — anything else is a
|
|
140
|
+
* 400, never reflected);
|
|
141
|
+
* 2. exchanges the code at GitHub's token endpoint — the client secret
|
|
142
|
+
* stays on this server, and the resulting access token is used for ONE
|
|
143
|
+
* `/user` lookup and then discarded;
|
|
144
|
+
* 3. responds with a tiny HTML page that `postMessage`s
|
|
145
|
+
* `{ source: 'scorezilla:github-oauth', state, id }` to `window.opener`,
|
|
146
|
+
* pinned to `allowedOrigin`, and closes the popup.
|
|
147
|
+
*
|
|
148
|
+
* The browser side (`scorezilla/identity`) accepts the message only when the
|
|
149
|
+
* origin matches its `exchangeUrl` and the `state` echoes the one it
|
|
150
|
+
* generated — this page is the only party that can complete a sign-in.
|
|
151
|
+
*
|
|
152
|
+
* Returns a standard `(Request) => Promise<Response>`: deploy it on any web
|
|
153
|
+
* runtime (a Next.js route handler, Hono, Deno, Bun, a Cloudflare Worker).
|
|
154
|
+
* Configure the deployed URL as the OAuth app's callback URL on GitHub.
|
|
155
|
+
*
|
|
156
|
+
* @since 0.3.0
|
|
157
|
+
*/
|
|
158
|
+
|
|
159
|
+
interface CreateGitHubOAuthHandlerConfig {
|
|
160
|
+
/** Your GitHub OAuth app client ID. */
|
|
161
|
+
readonly clientId: string;
|
|
162
|
+
/** Your GitHub OAuth app client secret. Server-only — never ships to a browser. */
|
|
163
|
+
readonly clientSecret: string;
|
|
164
|
+
/**
|
|
165
|
+
* The exact origin of the GAME page that opens the sign-in popup, e.g.
|
|
166
|
+
* `https://mygame.example`. Used as the `postMessage` target origin so the
|
|
167
|
+
* sign-in result can only be delivered to your page — never `*`.
|
|
168
|
+
*/
|
|
169
|
+
readonly allowedOrigin: string;
|
|
170
|
+
/** Inject a `fetch` (testing / custom transport). */
|
|
171
|
+
readonly fetch?: FetchImpl;
|
|
172
|
+
}
|
|
173
|
+
/** The web-standard request handler returned by {@link createGitHubOAuthHandler}. */
|
|
174
|
+
type GitHubOAuthHandler = (req: Request) => Promise<Response>;
|
|
175
|
+
/**
|
|
176
|
+
* Build the GitHub OAuth callback endpoint. See module docs for the flow;
|
|
177
|
+
* see `GitHubAuthProviderOptions` in `scorezilla/identity` for the matching
|
|
178
|
+
* client half.
|
|
179
|
+
*
|
|
180
|
+
* **Construction patterns.** In Node/Next, secrets are in `process.env`, so
|
|
181
|
+
* build the handler once at module scope. In Cloudflare Workers, secrets
|
|
182
|
+
* live on the per-request `env` binding, so build it inside `fetch`.
|
|
183
|
+
*/
|
|
184
|
+
declare function createGitHubOAuthHandler(config: CreateGitHubOAuthHandlerConfig): GitHubOAuthHandler;
|
|
3
185
|
|
|
4
186
|
/**
|
|
5
187
|
* `scorezilla/server` — HMAC-signed adapter for game backends (#17, v0.2.0).
|
|
@@ -144,5 +326,121 @@ declare class Scorezilla {
|
|
|
144
326
|
/** Fetch the slice of entries surrounding a player. */
|
|
145
327
|
getWindowAround(input: GetWindowAroundInput): Promise<ApiSuccess<WindowAroundResponse>>;
|
|
146
328
|
}
|
|
329
|
+
/** A value or a promise of it. */
|
|
330
|
+
type Awaitable<T> = T | Promise<T>;
|
|
331
|
+
/**
|
|
332
|
+
* The trusted identity produced by your `verify` callback. `playerId` is what
|
|
333
|
+
* gets submitted — derived from the *verified* request, never the request body.
|
|
334
|
+
*/
|
|
335
|
+
interface VerifiedIdentity {
|
|
336
|
+
/** Stable, trusted per-player id (e.g. your auth user id). Avoid PII. */
|
|
337
|
+
readonly playerId: string;
|
|
338
|
+
/** Optional trusted metadata merged into the submission (wins over the body). */
|
|
339
|
+
readonly metadata?: Record<string, unknown> | undefined;
|
|
340
|
+
}
|
|
341
|
+
/** The score payload extracted from the request body. */
|
|
342
|
+
interface ParsedScoreSubmission {
|
|
343
|
+
readonly score: number;
|
|
344
|
+
readonly metadata?: Record<string, unknown> | undefined;
|
|
345
|
+
}
|
|
346
|
+
/** Decision returned by an optional pre-verify rate-limit gate. */
|
|
347
|
+
interface RateLimitDecision {
|
|
348
|
+
readonly ok: boolean;
|
|
349
|
+
readonly retryAfterSeconds?: number | undefined;
|
|
350
|
+
}
|
|
351
|
+
/** CORS config for the handler. Omit entirely for same-origin endpoints. */
|
|
352
|
+
interface ScoreSubmitCorsOptions {
|
|
353
|
+
/**
|
|
354
|
+
* Allowed origin(s): an exact string, an array of strings, a predicate, or
|
|
355
|
+
* `true` to reflect any `Origin`. `false` disables reflection.
|
|
356
|
+
*/
|
|
357
|
+
readonly origin: string | readonly string[] | boolean | ((origin: string | null) => boolean);
|
|
358
|
+
/** Methods for the preflight. Default `['POST', 'OPTIONS']`. */
|
|
359
|
+
readonly methods?: readonly string[];
|
|
360
|
+
/** Request headers for the preflight. Default `['content-type', 'authorization']`. */
|
|
361
|
+
readonly headers?: readonly string[];
|
|
362
|
+
/** `Access-Control-Max-Age` seconds. Default `600`. */
|
|
363
|
+
readonly maxAgeSeconds?: number;
|
|
364
|
+
}
|
|
365
|
+
/** Configuration for {@link createScoreSubmitHandler}. */
|
|
366
|
+
interface CreateScoreSubmitHandlerConfig {
|
|
367
|
+
/** Your `sk_live_*` secret. Server-only — never ship to a browser. */
|
|
368
|
+
readonly secretKey: string;
|
|
369
|
+
/** The board UUID this handler submits to. */
|
|
370
|
+
readonly boardId: string;
|
|
371
|
+
/**
|
|
372
|
+
* Authenticate the request and return the **trusted** `playerId` (and any
|
|
373
|
+
* trusted metadata). Return `null` to reject (→ 401); throwing also rejects.
|
|
374
|
+
* Read identity from headers/cookies — the request **body** is reserved for
|
|
375
|
+
* the score payload (see `parseSubmission`). This callback is the universal
|
|
376
|
+
* seam: wire any auth (provider SDK, JWT verify, DB session lookup) here.
|
|
377
|
+
*/
|
|
378
|
+
readonly verify: (req: Request) => Awaitable<VerifiedIdentity | null>;
|
|
379
|
+
/**
|
|
380
|
+
* Extract the score (+ optional client metadata) from the request. Defaults
|
|
381
|
+
* to JSON `{ score: number, metadata?: object }`. Return `null` (or throw)
|
|
382
|
+
* to reject as a bad request (→ 400). Note: a `playerId` in the body is
|
|
383
|
+
* always ignored — identity comes only from `verify`.
|
|
384
|
+
*/
|
|
385
|
+
readonly parseSubmission?: (req: Request) => Awaitable<ParsedScoreSubmission | null>;
|
|
386
|
+
/**
|
|
387
|
+
* Optional gate that runs **before** `verify` (cheap defense — e.g. a per-IP
|
|
388
|
+
* rate limit, so unauthenticated spam can't drive auth/crypto work). Return
|
|
389
|
+
* `{ ok: false }` to reject with 429. Per-user limits belong inside `verify`.
|
|
390
|
+
*/
|
|
391
|
+
readonly rateLimit?: (req: Request) => Awaitable<RateLimitDecision>;
|
|
392
|
+
/** Optional CORS handling (OPTIONS preflight + reflected `Access-Control-Allow-Origin`). */
|
|
393
|
+
readonly cors?: ScoreSubmitCorsOptions;
|
|
394
|
+
/** Override the API base URL (self-host / testing). */
|
|
395
|
+
readonly baseUrl?: string;
|
|
396
|
+
/** Inject a `fetch` (testing / custom transport). */
|
|
397
|
+
readonly fetch?: FetchImpl;
|
|
398
|
+
/** Max transport retries (default SDK policy). Set `0` to disable. */
|
|
399
|
+
readonly maxRetries?: number;
|
|
400
|
+
/** Per-attempt timeout in ms. */
|
|
401
|
+
readonly timeoutMs?: number;
|
|
402
|
+
}
|
|
403
|
+
/** The web-standard request handler returned by {@link createScoreSubmitHandler}. */
|
|
404
|
+
type ScoreSubmitHandler = (req: Request) => Promise<Response>;
|
|
405
|
+
/**
|
|
406
|
+
* Build a turnkey, framework-agnostic secure score-submit endpoint.
|
|
407
|
+
*
|
|
408
|
+
* Returns a standard `(Request) => Promise<Response>` handler — drop it into a
|
|
409
|
+
* Cloudflare Worker, a Next.js route handler, Hono, Deno, Bun, or any runtime
|
|
410
|
+
* that speaks web `Request`/`Response`. You supply only what's app-specific:
|
|
411
|
+
* the `secretKey`, the `boardId`, and a `verify` callback that proves identity
|
|
412
|
+
* and returns the trusted `playerId`. The handler owns parsing/validation, the
|
|
413
|
+
* "playerId comes from `verify`, never the body" guarantee, signing via the
|
|
414
|
+
* HMAC server client, and error → HTTP mapping.
|
|
415
|
+
*
|
|
416
|
+
* **Construction patterns.** In Node/Next, secrets are in `process.env`, so
|
|
417
|
+
* build the handler once at module scope. In Cloudflare Workers, secrets live
|
|
418
|
+
* on the per-request `env` binding, so build it inside `fetch` (closing over
|
|
419
|
+
* `env`) — the construction is cheap (no I/O).
|
|
420
|
+
*
|
|
421
|
+
* @example Cloudflare Worker
|
|
422
|
+
* ```ts
|
|
423
|
+
* import { createScoreSubmitHandler } from 'scorezilla/server';
|
|
424
|
+
*
|
|
425
|
+
* export default {
|
|
426
|
+
* fetch(req, env) {
|
|
427
|
+
* const handler = createScoreSubmitHandler({
|
|
428
|
+
* secretKey: env.SCOREZILLA_SECRET_KEY,
|
|
429
|
+
* boardId: env.SCOREZILLA_BOARD_ID,
|
|
430
|
+
* verify: async (r) => {
|
|
431
|
+
* const user = await verifyMyAuth(r, env); // your auth: SDK / JWT / DB
|
|
432
|
+
* return user ? { playerId: user.id } : null;
|
|
433
|
+
* },
|
|
434
|
+
* cors: { origin: 'https://mygame.example' },
|
|
435
|
+
* });
|
|
436
|
+
* return handler(req);
|
|
437
|
+
* },
|
|
438
|
+
* };
|
|
439
|
+
* ```
|
|
440
|
+
*
|
|
441
|
+
* @since 0.3.0
|
|
442
|
+
* @stability stable
|
|
443
|
+
*/
|
|
444
|
+
declare function createScoreSubmitHandler(config: CreateScoreSubmitHandlerConfig): ScoreSubmitHandler;
|
|
147
445
|
|
|
148
|
-
export { ApiSuccess, type CancellableInput, type GetLeaderboardInput, type GetPlayerRankInput, type GetWindowAroundInput, LeaderboardResponse, PlayerRankResponse, Scorezilla, type SubmitScoreInput, SubmitScoreResponse, WindowAroundResponse };
|
|
446
|
+
export { ApiSuccess, type CancellableInput, type CreateGitHubOAuthHandlerConfig, type CreateScoreSubmitHandlerConfig, type GetLeaderboardInput, type GetPlayerRankInput, type GetWindowAroundInput, type GitHubOAuthHandler, LeaderboardResponse, type ParsedScoreSubmission, PlayerRankResponse, type RateLimitDecision, type RequestVerifier, type ScoreSubmitCorsOptions, type ScoreSubmitHandler, Scorezilla, type SubmitScoreInput, SubmitScoreResponse, type VerifiedIdentity, type VerifyAuth0JwtOptions, type VerifyClerkJwtOptions, type VerifyFirebaseIdTokenOptions, type VerifyJwtOptions, type VerifySupabaseJwtOptions, WindowAroundResponse, createGitHubOAuthHandler, createScoreSubmitHandler, verifyAuth0Jwt, verifyClerkJwt, verifyFirebaseIdToken, verifyJwt, verifySupabaseJwt };
|
package/dist/server.d.ts
CHANGED
|
@@ -1,5 +1,187 @@
|
|
|
1
|
-
import {
|
|
2
|
-
export { R as RankedEntry,
|
|
1
|
+
import { F as FetchImpl, g as SecretKeyConfig, A as ApiSuccess, a as SubmitScoreResponse, L as LeaderboardResponse, P as PlayerRankResponse, W as WindowAroundResponse } from './errors-CWTmormh.js';
|
|
2
|
+
export { R as RankedEntry, e as ScorezillaError, f as ScorezillaErrorCode } from './errors-CWTmormh.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Built-in request verifiers for {@link createScoreSubmitHandler} (#211).
|
|
6
|
+
*
|
|
7
|
+
* These turn the common "verify a JWT, derive the player id from a claim"
|
|
8
|
+
* shape into a one-liner. They produce a function with the same signature as
|
|
9
|
+
* the handler's `verify` callback, so they drop straight in:
|
|
10
|
+
*
|
|
11
|
+
* ```ts
|
|
12
|
+
* import { createScoreSubmitHandler, verifySupabaseJwt } from 'scorezilla/server';
|
|
13
|
+
*
|
|
14
|
+
* createScoreSubmitHandler({
|
|
15
|
+
* secretKey, boardId,
|
|
16
|
+
* verify: verifySupabaseJwt({ supabaseUrl: process.env.SUPABASE_URL! }),
|
|
17
|
+
* });
|
|
18
|
+
* ```
|
|
19
|
+
*
|
|
20
|
+
* **`jose` is an optional peer dependency.** Only these verifiers use it, and
|
|
21
|
+
* only via a lazy `import('jose')` — so consumers who use the public-key
|
|
22
|
+
* client, the factory with their own `verify`, or a provider backend SDK never
|
|
23
|
+
* install or load it. If you call a built-in verifier without `jose` present,
|
|
24
|
+
* it throws a clear "install jose" error.
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
/** Same shape as the handler's `verify` callback. */
|
|
28
|
+
type RequestVerifier = (req: Request) => Promise<VerifiedIdentity | null>;
|
|
29
|
+
/** Options for the generic JWKS verifier {@link verifyJwt}. */
|
|
30
|
+
interface VerifyJwtOptions {
|
|
31
|
+
/** JWKS endpoint, e.g. `https://issuer/.well-known/jwks.json`. */
|
|
32
|
+
readonly jwksUrl: string | URL;
|
|
33
|
+
/** Expected `iss` claim — strongly recommended. */
|
|
34
|
+
readonly issuer?: string | string[];
|
|
35
|
+
/** Expected `aud` claim — strongly recommended. */
|
|
36
|
+
readonly audience?: string | string[];
|
|
37
|
+
/** Which claim becomes the `playerId`. Default `'sub'`. */
|
|
38
|
+
readonly claim?: string;
|
|
39
|
+
/** Custom `fetch` for retrieving the JWKS (proxy / self-host / testing). */
|
|
40
|
+
readonly fetch?: typeof fetch;
|
|
41
|
+
}
|
|
42
|
+
/** Options for the Supabase preset {@link verifySupabaseJwt}. */
|
|
43
|
+
interface VerifySupabaseJwtOptions {
|
|
44
|
+
/** Your Supabase project URL, e.g. `https://abcd.supabase.co`. */
|
|
45
|
+
readonly supabaseUrl: string;
|
|
46
|
+
/** Which claim becomes the `playerId`. Default `'sub'`. */
|
|
47
|
+
readonly claim?: string;
|
|
48
|
+
/** Custom `fetch` for retrieving the JWKS (proxy / self-host / testing). */
|
|
49
|
+
readonly fetch?: typeof fetch;
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Generic JWKS-backed JWT verifier. Verifies a `Bearer` token against the
|
|
53
|
+
* given JWKS and returns `{ playerId }` from the configured claim (default
|
|
54
|
+
* `sub`), or `null` if there's no token or verification fails.
|
|
55
|
+
*
|
|
56
|
+
* Covers the modern provider majority (Supabase, Clerk, Auth0, Firebase,
|
|
57
|
+
* WorkOS, …) — they differ only by `jwksUrl` / `issuer` / `audience`.
|
|
58
|
+
*
|
|
59
|
+
* @since 0.3.0
|
|
60
|
+
*/
|
|
61
|
+
declare function verifyJwt(options: VerifyJwtOptions): RequestVerifier;
|
|
62
|
+
/**
|
|
63
|
+
* Supabase preset over {@link verifyJwt}. Verifies a Supabase user JWT via the
|
|
64
|
+
* project's JWKS and derives the `playerId` from `sub`.
|
|
65
|
+
*
|
|
66
|
+
* ```ts
|
|
67
|
+
* verify: verifySupabaseJwt({ supabaseUrl: process.env.SUPABASE_URL! })
|
|
68
|
+
* ```
|
|
69
|
+
*
|
|
70
|
+
* @since 0.3.0
|
|
71
|
+
*/
|
|
72
|
+
declare function verifySupabaseJwt(options: VerifySupabaseJwtOptions): RequestVerifier;
|
|
73
|
+
/** Options for the Clerk preset {@link verifyClerkJwt}. */
|
|
74
|
+
interface VerifyClerkJwtOptions {
|
|
75
|
+
/**
|
|
76
|
+
* Your Clerk instance issuer (the token's `iss`), e.g.
|
|
77
|
+
* `https://clerk.your-app.com` or `https://<slug>.clerk.accounts.dev`. The
|
|
78
|
+
* JWKS URL is derived from it.
|
|
79
|
+
*/
|
|
80
|
+
readonly issuer: string;
|
|
81
|
+
/**
|
|
82
|
+
* Expected `aud`. Clerk session tokens have **no** `aud` by default, so leave
|
|
83
|
+
* this unset unless you added one via a custom JWT template.
|
|
84
|
+
*/
|
|
85
|
+
readonly audience?: string | string[];
|
|
86
|
+
/** Which claim becomes the `playerId`. Default `'sub'` (the Clerk user id). */
|
|
87
|
+
readonly claim?: string;
|
|
88
|
+
/** Custom `fetch` for retrieving the JWKS (proxy / self-host / testing). */
|
|
89
|
+
readonly fetch?: typeof fetch;
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Clerk preset over {@link verifyJwt}. Verifies a Clerk session JWT against the
|
|
93
|
+
* instance JWKS and derives the `playerId` from `sub` (the Clerk user id).
|
|
94
|
+
*
|
|
95
|
+
* @since 0.3.0
|
|
96
|
+
*/
|
|
97
|
+
declare function verifyClerkJwt(options: VerifyClerkJwtOptions): RequestVerifier;
|
|
98
|
+
/** Options for the Auth0 preset {@link verifyAuth0Jwt}. */
|
|
99
|
+
interface VerifyAuth0JwtOptions {
|
|
100
|
+
/** Your Auth0 domain, e.g. `your-tenant.us.auth0.com` (scheme optional). */
|
|
101
|
+
readonly domain: string;
|
|
102
|
+
/** Your API identifier — the access token's `aud`. */
|
|
103
|
+
readonly audience: string | string[];
|
|
104
|
+
/** Which claim becomes the `playerId`. Default `'sub'`. */
|
|
105
|
+
readonly claim?: string;
|
|
106
|
+
/** Custom `fetch` for retrieving the JWKS (proxy / self-host / testing). */
|
|
107
|
+
readonly fetch?: typeof fetch;
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* Auth0 preset over {@link verifyJwt}. Note Auth0's issuer carries a trailing
|
|
111
|
+
* slash (`https://<domain>/`) — the preset adds it for you.
|
|
112
|
+
*
|
|
113
|
+
* @since 0.3.0
|
|
114
|
+
*/
|
|
115
|
+
declare function verifyAuth0Jwt(options: VerifyAuth0JwtOptions): RequestVerifier;
|
|
116
|
+
/** Options for the Firebase preset {@link verifyFirebaseIdToken}. */
|
|
117
|
+
interface VerifyFirebaseIdTokenOptions {
|
|
118
|
+
/** Your Firebase project id — both the token's `aud` and the issuer suffix. */
|
|
119
|
+
readonly projectId: string;
|
|
120
|
+
/** Which claim becomes the `playerId`. Default `'sub'` (the Firebase uid). */
|
|
121
|
+
readonly claim?: string;
|
|
122
|
+
/** Custom `fetch` for retrieving the JWKS (proxy / self-host / testing). */
|
|
123
|
+
readonly fetch?: typeof fetch;
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* Firebase Authentication preset over {@link verifyJwt}. Verifies a Firebase ID
|
|
127
|
+
* token (`iss = https://securetoken.google.com/<projectId>`, `aud = projectId`)
|
|
128
|
+
* and derives the `playerId` from `sub` (the Firebase uid).
|
|
129
|
+
*
|
|
130
|
+
* @since 0.3.0
|
|
131
|
+
*/
|
|
132
|
+
declare function verifyFirebaseIdToken(options: VerifyFirebaseIdTokenOptions): RequestVerifier;
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* `createGitHubOAuthHandler` — the server-side callback leg of the GitHub
|
|
136
|
+
* identity provider (`useAuthProvider({ provider: 'github' })`, ADR 0009).
|
|
137
|
+
*
|
|
138
|
+
* GitHub redirects the sign-in popup here with `?code=&state=`. This handler:
|
|
139
|
+
* 1. validates the query strictly (formats are pinned — anything else is a
|
|
140
|
+
* 400, never reflected);
|
|
141
|
+
* 2. exchanges the code at GitHub's token endpoint — the client secret
|
|
142
|
+
* stays on this server, and the resulting access token is used for ONE
|
|
143
|
+
* `/user` lookup and then discarded;
|
|
144
|
+
* 3. responds with a tiny HTML page that `postMessage`s
|
|
145
|
+
* `{ source: 'scorezilla:github-oauth', state, id }` to `window.opener`,
|
|
146
|
+
* pinned to `allowedOrigin`, and closes the popup.
|
|
147
|
+
*
|
|
148
|
+
* The browser side (`scorezilla/identity`) accepts the message only when the
|
|
149
|
+
* origin matches its `exchangeUrl` and the `state` echoes the one it
|
|
150
|
+
* generated — this page is the only party that can complete a sign-in.
|
|
151
|
+
*
|
|
152
|
+
* Returns a standard `(Request) => Promise<Response>`: deploy it on any web
|
|
153
|
+
* runtime (a Next.js route handler, Hono, Deno, Bun, a Cloudflare Worker).
|
|
154
|
+
* Configure the deployed URL as the OAuth app's callback URL on GitHub.
|
|
155
|
+
*
|
|
156
|
+
* @since 0.3.0
|
|
157
|
+
*/
|
|
158
|
+
|
|
159
|
+
interface CreateGitHubOAuthHandlerConfig {
|
|
160
|
+
/** Your GitHub OAuth app client ID. */
|
|
161
|
+
readonly clientId: string;
|
|
162
|
+
/** Your GitHub OAuth app client secret. Server-only — never ships to a browser. */
|
|
163
|
+
readonly clientSecret: string;
|
|
164
|
+
/**
|
|
165
|
+
* The exact origin of the GAME page that opens the sign-in popup, e.g.
|
|
166
|
+
* `https://mygame.example`. Used as the `postMessage` target origin so the
|
|
167
|
+
* sign-in result can only be delivered to your page — never `*`.
|
|
168
|
+
*/
|
|
169
|
+
readonly allowedOrigin: string;
|
|
170
|
+
/** Inject a `fetch` (testing / custom transport). */
|
|
171
|
+
readonly fetch?: FetchImpl;
|
|
172
|
+
}
|
|
173
|
+
/** The web-standard request handler returned by {@link createGitHubOAuthHandler}. */
|
|
174
|
+
type GitHubOAuthHandler = (req: Request) => Promise<Response>;
|
|
175
|
+
/**
|
|
176
|
+
* Build the GitHub OAuth callback endpoint. See module docs for the flow;
|
|
177
|
+
* see `GitHubAuthProviderOptions` in `scorezilla/identity` for the matching
|
|
178
|
+
* client half.
|
|
179
|
+
*
|
|
180
|
+
* **Construction patterns.** In Node/Next, secrets are in `process.env`, so
|
|
181
|
+
* build the handler once at module scope. In Cloudflare Workers, secrets
|
|
182
|
+
* live on the per-request `env` binding, so build it inside `fetch`.
|
|
183
|
+
*/
|
|
184
|
+
declare function createGitHubOAuthHandler(config: CreateGitHubOAuthHandlerConfig): GitHubOAuthHandler;
|
|
3
185
|
|
|
4
186
|
/**
|
|
5
187
|
* `scorezilla/server` — HMAC-signed adapter for game backends (#17, v0.2.0).
|
|
@@ -144,5 +326,121 @@ declare class Scorezilla {
|
|
|
144
326
|
/** Fetch the slice of entries surrounding a player. */
|
|
145
327
|
getWindowAround(input: GetWindowAroundInput): Promise<ApiSuccess<WindowAroundResponse>>;
|
|
146
328
|
}
|
|
329
|
+
/** A value or a promise of it. */
|
|
330
|
+
type Awaitable<T> = T | Promise<T>;
|
|
331
|
+
/**
|
|
332
|
+
* The trusted identity produced by your `verify` callback. `playerId` is what
|
|
333
|
+
* gets submitted — derived from the *verified* request, never the request body.
|
|
334
|
+
*/
|
|
335
|
+
interface VerifiedIdentity {
|
|
336
|
+
/** Stable, trusted per-player id (e.g. your auth user id). Avoid PII. */
|
|
337
|
+
readonly playerId: string;
|
|
338
|
+
/** Optional trusted metadata merged into the submission (wins over the body). */
|
|
339
|
+
readonly metadata?: Record<string, unknown> | undefined;
|
|
340
|
+
}
|
|
341
|
+
/** The score payload extracted from the request body. */
|
|
342
|
+
interface ParsedScoreSubmission {
|
|
343
|
+
readonly score: number;
|
|
344
|
+
readonly metadata?: Record<string, unknown> | undefined;
|
|
345
|
+
}
|
|
346
|
+
/** Decision returned by an optional pre-verify rate-limit gate. */
|
|
347
|
+
interface RateLimitDecision {
|
|
348
|
+
readonly ok: boolean;
|
|
349
|
+
readonly retryAfterSeconds?: number | undefined;
|
|
350
|
+
}
|
|
351
|
+
/** CORS config for the handler. Omit entirely for same-origin endpoints. */
|
|
352
|
+
interface ScoreSubmitCorsOptions {
|
|
353
|
+
/**
|
|
354
|
+
* Allowed origin(s): an exact string, an array of strings, a predicate, or
|
|
355
|
+
* `true` to reflect any `Origin`. `false` disables reflection.
|
|
356
|
+
*/
|
|
357
|
+
readonly origin: string | readonly string[] | boolean | ((origin: string | null) => boolean);
|
|
358
|
+
/** Methods for the preflight. Default `['POST', 'OPTIONS']`. */
|
|
359
|
+
readonly methods?: readonly string[];
|
|
360
|
+
/** Request headers for the preflight. Default `['content-type', 'authorization']`. */
|
|
361
|
+
readonly headers?: readonly string[];
|
|
362
|
+
/** `Access-Control-Max-Age` seconds. Default `600`. */
|
|
363
|
+
readonly maxAgeSeconds?: number;
|
|
364
|
+
}
|
|
365
|
+
/** Configuration for {@link createScoreSubmitHandler}. */
|
|
366
|
+
interface CreateScoreSubmitHandlerConfig {
|
|
367
|
+
/** Your `sk_live_*` secret. Server-only — never ship to a browser. */
|
|
368
|
+
readonly secretKey: string;
|
|
369
|
+
/** The board UUID this handler submits to. */
|
|
370
|
+
readonly boardId: string;
|
|
371
|
+
/**
|
|
372
|
+
* Authenticate the request and return the **trusted** `playerId` (and any
|
|
373
|
+
* trusted metadata). Return `null` to reject (→ 401); throwing also rejects.
|
|
374
|
+
* Read identity from headers/cookies — the request **body** is reserved for
|
|
375
|
+
* the score payload (see `parseSubmission`). This callback is the universal
|
|
376
|
+
* seam: wire any auth (provider SDK, JWT verify, DB session lookup) here.
|
|
377
|
+
*/
|
|
378
|
+
readonly verify: (req: Request) => Awaitable<VerifiedIdentity | null>;
|
|
379
|
+
/**
|
|
380
|
+
* Extract the score (+ optional client metadata) from the request. Defaults
|
|
381
|
+
* to JSON `{ score: number, metadata?: object }`. Return `null` (or throw)
|
|
382
|
+
* to reject as a bad request (→ 400). Note: a `playerId` in the body is
|
|
383
|
+
* always ignored — identity comes only from `verify`.
|
|
384
|
+
*/
|
|
385
|
+
readonly parseSubmission?: (req: Request) => Awaitable<ParsedScoreSubmission | null>;
|
|
386
|
+
/**
|
|
387
|
+
* Optional gate that runs **before** `verify` (cheap defense — e.g. a per-IP
|
|
388
|
+
* rate limit, so unauthenticated spam can't drive auth/crypto work). Return
|
|
389
|
+
* `{ ok: false }` to reject with 429. Per-user limits belong inside `verify`.
|
|
390
|
+
*/
|
|
391
|
+
readonly rateLimit?: (req: Request) => Awaitable<RateLimitDecision>;
|
|
392
|
+
/** Optional CORS handling (OPTIONS preflight + reflected `Access-Control-Allow-Origin`). */
|
|
393
|
+
readonly cors?: ScoreSubmitCorsOptions;
|
|
394
|
+
/** Override the API base URL (self-host / testing). */
|
|
395
|
+
readonly baseUrl?: string;
|
|
396
|
+
/** Inject a `fetch` (testing / custom transport). */
|
|
397
|
+
readonly fetch?: FetchImpl;
|
|
398
|
+
/** Max transport retries (default SDK policy). Set `0` to disable. */
|
|
399
|
+
readonly maxRetries?: number;
|
|
400
|
+
/** Per-attempt timeout in ms. */
|
|
401
|
+
readonly timeoutMs?: number;
|
|
402
|
+
}
|
|
403
|
+
/** The web-standard request handler returned by {@link createScoreSubmitHandler}. */
|
|
404
|
+
type ScoreSubmitHandler = (req: Request) => Promise<Response>;
|
|
405
|
+
/**
|
|
406
|
+
* Build a turnkey, framework-agnostic secure score-submit endpoint.
|
|
407
|
+
*
|
|
408
|
+
* Returns a standard `(Request) => Promise<Response>` handler — drop it into a
|
|
409
|
+
* Cloudflare Worker, a Next.js route handler, Hono, Deno, Bun, or any runtime
|
|
410
|
+
* that speaks web `Request`/`Response`. You supply only what's app-specific:
|
|
411
|
+
* the `secretKey`, the `boardId`, and a `verify` callback that proves identity
|
|
412
|
+
* and returns the trusted `playerId`. The handler owns parsing/validation, the
|
|
413
|
+
* "playerId comes from `verify`, never the body" guarantee, signing via the
|
|
414
|
+
* HMAC server client, and error → HTTP mapping.
|
|
415
|
+
*
|
|
416
|
+
* **Construction patterns.** In Node/Next, secrets are in `process.env`, so
|
|
417
|
+
* build the handler once at module scope. In Cloudflare Workers, secrets live
|
|
418
|
+
* on the per-request `env` binding, so build it inside `fetch` (closing over
|
|
419
|
+
* `env`) — the construction is cheap (no I/O).
|
|
420
|
+
*
|
|
421
|
+
* @example Cloudflare Worker
|
|
422
|
+
* ```ts
|
|
423
|
+
* import { createScoreSubmitHandler } from 'scorezilla/server';
|
|
424
|
+
*
|
|
425
|
+
* export default {
|
|
426
|
+
* fetch(req, env) {
|
|
427
|
+
* const handler = createScoreSubmitHandler({
|
|
428
|
+
* secretKey: env.SCOREZILLA_SECRET_KEY,
|
|
429
|
+
* boardId: env.SCOREZILLA_BOARD_ID,
|
|
430
|
+
* verify: async (r) => {
|
|
431
|
+
* const user = await verifyMyAuth(r, env); // your auth: SDK / JWT / DB
|
|
432
|
+
* return user ? { playerId: user.id } : null;
|
|
433
|
+
* },
|
|
434
|
+
* cors: { origin: 'https://mygame.example' },
|
|
435
|
+
* });
|
|
436
|
+
* return handler(req);
|
|
437
|
+
* },
|
|
438
|
+
* };
|
|
439
|
+
* ```
|
|
440
|
+
*
|
|
441
|
+
* @since 0.3.0
|
|
442
|
+
* @stability stable
|
|
443
|
+
*/
|
|
444
|
+
declare function createScoreSubmitHandler(config: CreateScoreSubmitHandlerConfig): ScoreSubmitHandler;
|
|
147
445
|
|
|
148
|
-
export { ApiSuccess, type CancellableInput, type GetLeaderboardInput, type GetPlayerRankInput, type GetWindowAroundInput, LeaderboardResponse, PlayerRankResponse, Scorezilla, type SubmitScoreInput, SubmitScoreResponse, WindowAroundResponse };
|
|
446
|
+
export { ApiSuccess, type CancellableInput, type CreateGitHubOAuthHandlerConfig, type CreateScoreSubmitHandlerConfig, type GetLeaderboardInput, type GetPlayerRankInput, type GetWindowAroundInput, type GitHubOAuthHandler, LeaderboardResponse, type ParsedScoreSubmission, PlayerRankResponse, type RateLimitDecision, type RequestVerifier, type ScoreSubmitCorsOptions, type ScoreSubmitHandler, Scorezilla, type SubmitScoreInput, SubmitScoreResponse, type VerifiedIdentity, type VerifyAuth0JwtOptions, type VerifyClerkJwtOptions, type VerifyFirebaseIdTokenOptions, type VerifyJwtOptions, type VerifySupabaseJwtOptions, WindowAroundResponse, createGitHubOAuthHandler, createScoreSubmitHandler, verifyAuth0Jwt, verifyClerkJwt, verifyFirebaseIdToken, verifyJwt, verifySupabaseJwt };
|