scorezilla 0.3.0-next.0 → 0.3.0-next.2
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 +77 -7
- package/CHANGELOG.md +110 -0
- package/README.md +77 -0
- package/RECIPES.md +186 -0
- package/dist/{errors-B7hyC-C5.d.cts → errors-CtXMAHtJ.d.cts} +1 -1
- package/dist/{errors-B7hyC-C5.d.ts → errors-CtXMAHtJ.d.ts} +1 -1
- package/dist/identity.cjs +209 -8
- package/dist/identity.cjs.map +1 -1
- package/dist/identity.d.cts +122 -11
- package/dist/identity.d.ts +122 -11
- package/dist/identity.js +209 -8
- 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 +215 -1
- package/dist/server.cjs.map +1 -1
- package/dist/server.d.cts +249 -3
- package/dist/server.d.ts +249 -3
- package/dist/server.js +210 -2
- package/dist/server.js.map +1 -1
- package/package.json +12 -2
package/API.md
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
# Scorezilla SDK — API Reference
|
|
2
2
|
|
|
3
|
-
> **Status:**
|
|
4
|
-
> (`scorezilla/server`
|
|
5
|
-
>
|
|
6
|
-
> [VERSIONING.md](./VERSIONING.md) for the
|
|
7
|
-
> guarantees.
|
|
3
|
+
> **Status:** The public-key client and the HMAC server adapter
|
|
4
|
+
> (`scorezilla/server` — incl. `createScoreSubmitHandler` + JWT verifiers) are
|
|
5
|
+
> stable; the `scorezilla/identity` presets ship in 0.3.0. See
|
|
6
|
+
> [CHANGELOG.md](./CHANGELOG.md) and [VERSIONING.md](./VERSIONING.md) for the
|
|
7
|
+
> release timeline and stability guarantees.
|
|
8
8
|
|
|
9
9
|
## Quick start
|
|
10
10
|
|
|
@@ -50,8 +50,9 @@ type ScorezillaConfig = PublicKeyConfig /* | SecretKeyConfig — v0.2.0 */;
|
|
|
50
50
|
|
|
51
51
|
- `publicKey`: `pk_<game-slug>_<base62-suffix>`. Issued by the operator
|
|
52
52
|
dashboard. Safe to embed in browser code.
|
|
53
|
-
- `secretKey
|
|
54
|
-
|
|
53
|
+
- `secretKey`: a single `sk_live_<keyId>_<random>` token (the keyId is embedded,
|
|
54
|
+
so you manage one value). Server-side only — used by the `scorezilla/server`
|
|
55
|
+
adapter to HMAC-sign the secure path. Never embed it in client code.
|
|
55
56
|
|
|
56
57
|
### Mutual exclusivity
|
|
57
58
|
|
|
@@ -255,6 +256,75 @@ try {
|
|
|
255
256
|
}
|
|
256
257
|
```
|
|
257
258
|
|
|
259
|
+
## Server adapter (`scorezilla/server`)
|
|
260
|
+
|
|
261
|
+
The public-key client is client-authoritative — any player can submit any score
|
|
262
|
+
from devtools. For ranking-sensitive boards, sign submissions server-side with a
|
|
263
|
+
`sk_live_*` secret. Full recipes are in the
|
|
264
|
+
[README](./README.md#server-side-hmac-scorezillaserver); the surface:
|
|
265
|
+
|
|
266
|
+
### `Scorezilla` (server client)
|
|
267
|
+
|
|
268
|
+
```ts
|
|
269
|
+
import { Scorezilla } from 'scorezilla/server';
|
|
270
|
+
|
|
271
|
+
const sz = new Scorezilla({
|
|
272
|
+
secretKey: process.env.SCOREZILLA_SECRET_KEY!, // sk_live_<keyId>_<random>
|
|
273
|
+
});
|
|
274
|
+
await sz.submitScore({ boardId, playerId, score, metadata });
|
|
275
|
+
```
|
|
276
|
+
|
|
277
|
+
Same methods as the public-key client (`submitScore`, `getLeaderboard`,
|
|
278
|
+
`getPlayerRank`, `getWindowAround`); each request is HMAC-SHA256 signed and
|
|
279
|
+
verified server-side. Server-only — importing it in a browser throws.
|
|
280
|
+
|
|
281
|
+
### `createScoreSubmitHandler(config)`
|
|
282
|
+
|
|
283
|
+
A framework-agnostic factory returning a `(Request) => Promise<Response>`
|
|
284
|
+
handler (Cloudflare Workers, Next route handlers, Hono, Deno, Bun). You supply
|
|
285
|
+
your auth via `verify`; it owns parsing/validation, signing, and error → HTTP
|
|
286
|
+
mapping.
|
|
287
|
+
|
|
288
|
+
```ts
|
|
289
|
+
import { createScoreSubmitHandler, verifySupabaseJwt } from 'scorezilla/server';
|
|
290
|
+
|
|
291
|
+
export const POST = createScoreSubmitHandler({
|
|
292
|
+
secretKey: process.env.SCOREZILLA_SECRET_KEY!,
|
|
293
|
+
boardId: process.env.SCOREZILLA_BOARD_ID!,
|
|
294
|
+
verify: verifySupabaseJwt({ supabaseUrl: process.env.SUPABASE_URL! }),
|
|
295
|
+
});
|
|
296
|
+
```
|
|
297
|
+
|
|
298
|
+
| Option | Type | Notes |
|
|
299
|
+
| ---------------------------------------------------- | ------------------------------------------------ | ----------------------------------------------------------------------------------- |
|
|
300
|
+
| `secretKey` | `string` | `sk_live_*`. Required. |
|
|
301
|
+
| `boardId` | `string` | Required. |
|
|
302
|
+
| `verify` | `(req) => { playerId, metadata? } \| null` | Required. The submitted `playerId` always comes from here — never the request body. |
|
|
303
|
+
| `parseSubmission?` | `(req) => { score, metadata? } \| null` | Defaults to JSON `{ score, metadata? }`. |
|
|
304
|
+
| `rateLimit?` | `(req) => { ok, retryAfterSeconds? }` | Runs **before** `verify`. |
|
|
305
|
+
| `cors?` | `{ origin, methods?, headers?, maxAgeSeconds? }` | OPTIONS preflight + reflected `Access-Control-Allow-Origin`. |
|
|
306
|
+
| `baseUrl?` · `fetch?` · `maxRetries?` · `timeoutMs?` | | Pass-through to the server client. |
|
|
307
|
+
|
|
308
|
+
### JWT verifiers
|
|
309
|
+
|
|
310
|
+
Built-in `verify` helpers — each returns `(req) => Promise<{ playerId } | null>`.
|
|
311
|
+
They require the optional peer dependency `jose` (loaded lazily; only consumers
|
|
312
|
+
of a verifier install it).
|
|
313
|
+
|
|
314
|
+
```ts
|
|
315
|
+
import {
|
|
316
|
+
verifyJwt, // generic JWKS: { jwksUrl, issuer, audience, claim? }
|
|
317
|
+
verifySupabaseJwt, // { supabaseUrl }
|
|
318
|
+
verifyClerkJwt, // { issuer }
|
|
319
|
+
verifyAuth0Jwt, // { domain, audience }
|
|
320
|
+
verifyFirebaseIdToken, // { projectId }
|
|
321
|
+
} from 'scorezilla/server';
|
|
322
|
+
```
|
|
323
|
+
|
|
324
|
+
For non-JWKS auth (Auth.js JWE sessions, opaque sessions, provider backend
|
|
325
|
+
SDKs), write your own `verify` — anything returning `{ playerId }` works. See
|
|
326
|
+
[RECIPES.md](./RECIPES.md) for worked recipes.
|
|
327
|
+
|
|
258
328
|
## Advanced
|
|
259
329
|
|
|
260
330
|
### Custom fetch / polyfills
|
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,115 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.3.0-next.2
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- [#41](https://github.com/isco-tec/scorezilla-js/pull/41) [`e48a5a2`](https://github.com/isco-tec/scorezilla-js/commit/e48a5a2f09cd0f098e8466b51586bd4108bb5678) Thanks [@isco-tec](https://github.com/isco-tec)! - feat(server): `createScoreSubmitHandler()` — turnkey secure score submissions
|
|
8
|
+
|
|
9
|
+
A framework-agnostic factory in `scorezilla/server` that collapses the secure
|
|
10
|
+
(HMAC-signed) submission path from ~150 lines of boilerplate into a few. It
|
|
11
|
+
returns a standard `(Request) => Promise<Response>` handler — drop it into a
|
|
12
|
+
Cloudflare Worker, a Next.js route handler, Hono, Deno, or Bun.
|
|
13
|
+
|
|
14
|
+
```ts
|
|
15
|
+
import { createScoreSubmitHandler } from 'scorezilla/server';
|
|
16
|
+
|
|
17
|
+
export const POST = createScoreSubmitHandler({
|
|
18
|
+
secretKey: process.env.SCOREZILLA_SECRET_KEY!,
|
|
19
|
+
boardId: process.env.SCOREZILLA_BOARD_ID!,
|
|
20
|
+
verify: async (req) => {
|
|
21
|
+
// your auth — any provider; return the trusted playerId
|
|
22
|
+
const user = await myAuth(req);
|
|
23
|
+
return user ? { playerId: user.id } : null;
|
|
24
|
+
},
|
|
25
|
+
});
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
- The submitted `playerId` always comes from `verify` (the verified request),
|
|
29
|
+
never the request body — so ranking-sensitive boards aren't subject to the
|
|
30
|
+
client-authoritative submission of the public-key path.
|
|
31
|
+
- Owns body parsing/validation, HMAC signing, and `ScorezillaError` → HTTP
|
|
32
|
+
status mapping. Optional `cors` (OPTIONS preflight + reflected origin) and a
|
|
33
|
+
pre-verify `rateLimit` gate.
|
|
34
|
+
- Works with **any** auth via the `verify` callback (Supabase / Clerk / Auth0 /
|
|
35
|
+
Firebase JWTs, Lucia / opaque sessions, or a provider backend SDK). First-class
|
|
36
|
+
one-line verifiers (`verifySupabaseJwt`, `verifyJwt`) follow.
|
|
37
|
+
|
|
38
|
+
- [#42](https://github.com/isco-tec/scorezilla-js/pull/42) [`7ca5976`](https://github.com/isco-tec/scorezilla-js/commit/7ca5976857cfff44cc3a3c155181cd9f6276aea0) Thanks [@isco-tec](https://github.com/isco-tec)! - feat(server): built-in `verifyJwt` + `verifySupabaseJwt` for `createScoreSubmitHandler`
|
|
39
|
+
|
|
40
|
+
Turn the common "verify a JWT, derive the player id" step into a one-liner.
|
|
41
|
+
Both return a `verify` function you drop straight into `createScoreSubmitHandler`.
|
|
42
|
+
|
|
43
|
+
```ts
|
|
44
|
+
import { createScoreSubmitHandler, verifySupabaseJwt } from 'scorezilla/server';
|
|
45
|
+
|
|
46
|
+
export const POST = createScoreSubmitHandler({
|
|
47
|
+
secretKey: process.env.SCOREZILLA_SECRET_KEY!,
|
|
48
|
+
boardId: process.env.SCOREZILLA_BOARD_ID!,
|
|
49
|
+
verify: verifySupabaseJwt({ supabaseUrl: process.env.SUPABASE_URL! }),
|
|
50
|
+
});
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
- `verifyJwt({ jwksUrl, issuer, audience, claim? })` — generic JWKS verifier,
|
|
54
|
+
plus first-class presets for the popular providers: `verifySupabaseJwt({
|
|
55
|
+
supabaseUrl })`, `verifyClerkJwt({ issuer })`, `verifyAuth0Jwt({ domain,
|
|
56
|
+
audience })`, and `verifyFirebaseIdToken({ projectId })`.
|
|
57
|
+
- **`jose` is an optional peer dependency**, loaded lazily via dynamic
|
|
58
|
+
`import()` — consumers who use the public-key client, the factory with their
|
|
59
|
+
own `verify`, or a provider backend SDK never install or load it.
|
|
60
|
+
|
|
61
|
+
## 0.3.0-next.1
|
|
62
|
+
|
|
63
|
+
### Minor Changes
|
|
64
|
+
|
|
65
|
+
- [#39](https://github.com/isco-tec/scorezilla-js/pull/39) [`608137f`](https://github.com/isco-tec/scorezilla-js/commit/608137f2a880fd3b9031cde8de765a5262d6c334) Thanks [@isco-tec](https://github.com/isco-tec)! - feat(identity): ship the Google provider for `useAuthProvider`
|
|
66
|
+
|
|
67
|
+
`useAuthProvider({ provider: 'google', clientId, storageKey })` is now
|
|
68
|
+
implemented and **stable**. It wraps Google Identity Services ("One Tap"),
|
|
69
|
+
derives a stable, opaque player id from the account's `sub` claim
|
|
70
|
+
(`google:<sub>`), and persists it in `localStorage` so returning visitors are
|
|
71
|
+
recognized without signing in again.
|
|
72
|
+
|
|
73
|
+
```ts
|
|
74
|
+
import { Scorezilla } from 'scorezilla';
|
|
75
|
+
import { useAuthProvider } from 'scorezilla/identity';
|
|
76
|
+
|
|
77
|
+
const player = await useAuthProvider({
|
|
78
|
+
provider: 'google',
|
|
79
|
+
clientId: 'YOUR_CLIENT_ID.apps.googleusercontent.com',
|
|
80
|
+
storageKey: 'mygame:player',
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
if (player) {
|
|
84
|
+
const sz = new Scorezilla({ publicKey: 'pk_…' });
|
|
85
|
+
await sz.submitScore({ boardId, playerId: player.id, score: 42 });
|
|
86
|
+
// player.signOut() clears the persisted id and disables Google auto-select.
|
|
87
|
+
}
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
- **Resolves `null` when the player declines** or One Tap can't be shown — a
|
|
91
|
+
dismissed sign-in is not an error. It **rejects** only on genuine failures
|
|
92
|
+
(invalid args, script load failure, malformed credential).
|
|
93
|
+
- **`handle.source`** is `'signed-in'` for a fresh sign-in or `'restored'` when
|
|
94
|
+
the id was rehydrated from `localStorage` (a restored id is not a re-verified
|
|
95
|
+
live session).
|
|
96
|
+
- **Bring your own client ID.** The SDK never bundles Scorezilla-owned OAuth
|
|
97
|
+
credentials, so revocation and consent stay under your control.
|
|
98
|
+
- **Privacy.** Only the derived `sub`-based id is stored and transmitted on
|
|
99
|
+
score submission — never the Google credential, email, or profile.
|
|
100
|
+
- **Bundle.** The Google provider tree-shakes out for consumers who don't call
|
|
101
|
+
`useAuthProvider`; the Google Identity Services library is loaded at runtime
|
|
102
|
+
from `accounts.google.com`, never bundled.
|
|
103
|
+
- `useAuthProvider` is now async (replacing the `0.3.0-next.0` preview stub that
|
|
104
|
+
threw synchronously). Despite the `use*` name it is **not** a React hook.
|
|
105
|
+
Identity errors are plain `Error`/`TypeError` (not `ScorezillaError`), keeping
|
|
106
|
+
the `scorezilla/identity` subpath dependency-free. The host page's CSP must
|
|
107
|
+
allow `https://accounts.google.com`.
|
|
108
|
+
- The **GitHub** provider is not available yet — it ships in a follow-up and
|
|
109
|
+
will require a server-side token exchange (your backend or a Scorezilla
|
|
110
|
+
Workers proxy). Calling `useAuthProvider({ provider: 'github' })` rejects
|
|
111
|
+
with guidance until then.
|
|
112
|
+
|
|
3
113
|
## 0.3.0-next.0
|
|
4
114
|
|
|
5
115
|
### Minor Changes
|
package/README.md
CHANGED
|
@@ -122,6 +122,83 @@ browser throws at module evaluation. Use environment variables (or your
|
|
|
122
122
|
secret manager) to load the `sk_live_*` value; never embed it in a build
|
|
123
123
|
that ships to clients.
|
|
124
124
|
|
|
125
|
+
### Turnkey endpoint: `createScoreSubmitHandler`
|
|
126
|
+
|
|
127
|
+
Wiring the secure path by hand means verifying your auth, deriving the player
|
|
128
|
+
id from it, validating the body, signing, and mapping errors.
|
|
129
|
+
`createScoreSubmitHandler` does all of that — you supply only your auth. It
|
|
130
|
+
returns a standard `(Request) => Promise<Response>`, so it drops into a
|
|
131
|
+
Cloudflare Worker, a Next.js route handler, Hono, Deno, or Bun.
|
|
132
|
+
|
|
133
|
+
```ts
|
|
134
|
+
import { createScoreSubmitHandler } from 'scorezilla/server';
|
|
135
|
+
|
|
136
|
+
export const POST = createScoreSubmitHandler({
|
|
137
|
+
secretKey: process.env.SCOREZILLA_SECRET_KEY!,
|
|
138
|
+
boardId: process.env.SCOREZILLA_BOARD_ID!,
|
|
139
|
+
// The one app-specific bit: prove identity, return the TRUSTED playerId.
|
|
140
|
+
// The submitted playerId comes from here — never from the request body.
|
|
141
|
+
verify: async (req) => {
|
|
142
|
+
const user = await myAuth(req);
|
|
143
|
+
return user ? { playerId: user.id } : null;
|
|
144
|
+
},
|
|
145
|
+
cors: { origin: 'https://mygame.example' }, // omit for same-origin
|
|
146
|
+
});
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
Your client just POSTs `{ score, metadata? }` (plus its auth header) to the
|
|
150
|
+
endpoint; the handler signs and forwards it.
|
|
151
|
+
|
|
152
|
+
**Supabase? One line.** Built-in verifiers do the JWKS verification for you.
|
|
153
|
+
(`jose` is an optional peer dependency, loaded only when you use a built-in
|
|
154
|
+
verifier — `npm i jose`.)
|
|
155
|
+
|
|
156
|
+
```ts
|
|
157
|
+
import { createScoreSubmitHandler, verifySupabaseJwt } from 'scorezilla/server';
|
|
158
|
+
|
|
159
|
+
export const POST = createScoreSubmitHandler({
|
|
160
|
+
secretKey: process.env.SCOREZILLA_SECRET_KEY!,
|
|
161
|
+
boardId: process.env.SCOREZILLA_BOARD_ID!,
|
|
162
|
+
verify: verifySupabaseJwt({ supabaseUrl: process.env.SUPABASE_URL! }),
|
|
163
|
+
});
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
**Clerk, Auth0, and Firebase have presets too:**
|
|
167
|
+
|
|
168
|
+
```ts
|
|
169
|
+
import { verifyClerkJwt, verifyAuth0Jwt, verifyFirebaseIdToken } from 'scorezilla/server';
|
|
170
|
+
|
|
171
|
+
verify: verifyClerkJwt({ issuer: 'https://clerk.your-app.com' });
|
|
172
|
+
verify: verifyAuth0Jwt({ domain: 'you.us.auth0.com', audience: 'your-api' });
|
|
173
|
+
verify: verifyFirebaseIdToken({ projectId: 'your-firebase-project' });
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
**Any other JWKS provider** uses the generic `verifyJwt`:
|
|
177
|
+
|
|
178
|
+
```ts
|
|
179
|
+
import { verifyJwt } from 'scorezilla/server';
|
|
180
|
+
|
|
181
|
+
verify: verifyJwt({
|
|
182
|
+
jwksUrl: 'https://your-issuer/.well-known/jwks.json',
|
|
183
|
+
issuer: 'https://your-issuer',
|
|
184
|
+
audience: 'your-api', // claim: 'sub' (default) → playerId
|
|
185
|
+
});
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
**Anything else** — the `verify` callback is the universal seam. For
|
|
189
|
+
Auth.js/NextAuth (encrypted JWE sessions), Better Auth, opaque session
|
|
190
|
+
cookies, or a provider backend SDK, anything that returns `{ playerId }`
|
|
191
|
+
works:
|
|
192
|
+
|
|
193
|
+
```ts
|
|
194
|
+
verify: async (req) => {
|
|
195
|
+
const session = await validateSession(req); // your DB / SDK
|
|
196
|
+
return session ? { playerId: session.userId } : null;
|
|
197
|
+
};
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
Worked recipes for each of those live in [RECIPES.md](./RECIPES.md).
|
|
201
|
+
|
|
125
202
|
## Error handling
|
|
126
203
|
|
|
127
204
|
Every failure path — HTTP non-2xx, network error, timeout, abort, JSON parse
|
package/RECIPES.md
ADDED
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
# Auth recipes — `createScoreSubmitHandler`
|
|
2
|
+
|
|
3
|
+
The built-in verifiers ([README](./README.md#turnkey-endpoint-createscoresubmithandler))
|
|
4
|
+
cover auth systems that issue **JWKS-verifiable bearer tokens** — Supabase,
|
|
5
|
+
Clerk, Auth0, Firebase, and any other provider via the generic `verifyJwt`.
|
|
6
|
+
|
|
7
|
+
This doc covers everything else: sessions that **cannot** be verified against a
|
|
8
|
+
public key set — encrypted session cookies (Auth.js), database-backed sessions
|
|
9
|
+
(Better Auth, roll-your-own), and provider backend SDKs. They all plug into the
|
|
10
|
+
same seam: the `verify` callback.
|
|
11
|
+
|
|
12
|
+
## The `verify` contract
|
|
13
|
+
|
|
14
|
+
```ts
|
|
15
|
+
verify: (req: Request) => Promise<{ playerId: string; metadata?: object } | null>;
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
- Return `{ playerId }` on success. The submitted `playerId` **always** comes
|
|
19
|
+
from `verify` — a `playerId` in the request body is ignored.
|
|
20
|
+
- Return `null` to reject (→ `401 unauthorized`). Throwing also rejects with
|
|
21
|
+
401; the error message is never surfaced to the client (it may carry token
|
|
22
|
+
internals), so prefer returning `null` explicitly.
|
|
23
|
+
- Read identity from **headers/cookies only**. The request body is reserved for
|
|
24
|
+
the score payload (`parseSubmission` consumes it).
|
|
25
|
+
- Optional `metadata` you return is trusted and **wins over** body metadata on
|
|
26
|
+
key conflicts — use it for server-verified fields like a display name.
|
|
27
|
+
- The optional `rateLimit` gate runs **before** `verify`, so unauthenticated
|
|
28
|
+
spam can't drive your auth/crypto work. Per-user limits belong inside
|
|
29
|
+
`verify` (after you know who the user is).
|
|
30
|
+
|
|
31
|
+
## Auth.js / NextAuth — encrypted JWE sessions
|
|
32
|
+
|
|
33
|
+
Auth.js does not expose a JWKS: its session cookie is a **JWE** — encrypted
|
|
34
|
+
with a key derived from `AUTH_SECRET` — so only your server can read it. Two
|
|
35
|
+
ways to wire it:
|
|
36
|
+
|
|
37
|
+
### In a Next.js App Router route handler
|
|
38
|
+
|
|
39
|
+
Call your Auth.js instance's `auth()` inside `verify` — it reads the session
|
|
40
|
+
from the ambient request context:
|
|
41
|
+
|
|
42
|
+
```ts
|
|
43
|
+
// app/api/submit-score/route.ts
|
|
44
|
+
import { createScoreSubmitHandler } from 'scorezilla/server';
|
|
45
|
+
import { auth } from '@/auth'; // your Auth.js config
|
|
46
|
+
|
|
47
|
+
export const POST = createScoreSubmitHandler({
|
|
48
|
+
secretKey: process.env.SCOREZILLA_SECRET_KEY!,
|
|
49
|
+
boardId: process.env.SCOREZILLA_BOARD_ID!,
|
|
50
|
+
verify: async () => {
|
|
51
|
+
const session = await auth();
|
|
52
|
+
return session?.user?.id ? { playerId: session.user.id } : null;
|
|
53
|
+
},
|
|
54
|
+
});
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
> **Note:** `session.user.id` is not populated by default with the JWT session
|
|
58
|
+
> strategy — expose it in your Auth.js config's `session` callback
|
|
59
|
+
> (`session.user.id = token.sub`), or use the `getToken` recipe below, where
|
|
60
|
+
> `sub` is always present.
|
|
61
|
+
|
|
62
|
+
### Anywhere else (framework-agnostic)
|
|
63
|
+
|
|
64
|
+
`getToken` decrypts the session cookie (or `Authorization` header) directly
|
|
65
|
+
from the `Request` — no Next context needed, so it works in the handler's
|
|
66
|
+
`verify` on any runtime:
|
|
67
|
+
|
|
68
|
+
```ts
|
|
69
|
+
import { createScoreSubmitHandler } from 'scorezilla/server';
|
|
70
|
+
import { getToken } from 'next-auth/jwt'; // or '@auth/core/jwt' outside Next
|
|
71
|
+
|
|
72
|
+
export const handler = createScoreSubmitHandler({
|
|
73
|
+
secretKey: process.env.SCOREZILLA_SECRET_KEY!,
|
|
74
|
+
boardId: process.env.SCOREZILLA_BOARD_ID!,
|
|
75
|
+
verify: async (req) => {
|
|
76
|
+
const token = await getToken({
|
|
77
|
+
req,
|
|
78
|
+
secret: process.env.AUTH_SECRET!,
|
|
79
|
+
// The cookie name carries a __Secure- prefix on https. Auth.js infers
|
|
80
|
+
// this from AUTH_URL; set it explicitly where AUTH_URL isn't available.
|
|
81
|
+
secureCookie: true,
|
|
82
|
+
});
|
|
83
|
+
return token?.sub ? { playerId: token.sub } : null;
|
|
84
|
+
},
|
|
85
|
+
});
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
NextAuth **v4** uses a different default cookie name — add
|
|
89
|
+
`cookieName: 'next-auth.session-token'` (or its `__Secure-` variant) to the
|
|
90
|
+
`getToken` call.
|
|
91
|
+
|
|
92
|
+
## Better Auth — database-backed sessions
|
|
93
|
+
|
|
94
|
+
Better Auth sessions are opaque tokens looked up server-side. Its server API
|
|
95
|
+
takes the request headers directly:
|
|
96
|
+
|
|
97
|
+
```ts
|
|
98
|
+
import { createScoreSubmitHandler } from 'scorezilla/server';
|
|
99
|
+
import { auth } from './auth'; // your Better Auth instance
|
|
100
|
+
|
|
101
|
+
export const handler = createScoreSubmitHandler({
|
|
102
|
+
secretKey: process.env.SCOREZILLA_SECRET_KEY!,
|
|
103
|
+
boardId: process.env.SCOREZILLA_BOARD_ID!,
|
|
104
|
+
verify: async (req) => {
|
|
105
|
+
const session = await auth.api.getSession({ headers: req.headers });
|
|
106
|
+
return session ? { playerId: session.user.id } : null;
|
|
107
|
+
},
|
|
108
|
+
});
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
## Any opaque session cookie
|
|
112
|
+
|
|
113
|
+
For roll-your-own sessions (or anything storing a session id in a cookie and
|
|
114
|
+
the session itself in a DB/KV/Redis), `verify` is a cookie parse + store
|
|
115
|
+
lookup:
|
|
116
|
+
|
|
117
|
+
```ts
|
|
118
|
+
const SESSION_COOKIE = 'sid';
|
|
119
|
+
|
|
120
|
+
verify: async (req) => {
|
|
121
|
+
const cookies = req.headers.get('cookie') ?? '';
|
|
122
|
+
const match = new RegExp(`(?:^|;\\s*)${SESSION_COOKIE}=([^;]+)`).exec(cookies);
|
|
123
|
+
if (!match) return null;
|
|
124
|
+
|
|
125
|
+
const session = await sessionStore.get(match[1]); // your DB / KV / Redis
|
|
126
|
+
if (!session || session.expiresAt < now()) return null;
|
|
127
|
+
|
|
128
|
+
return { playerId: session.userId };
|
|
129
|
+
},
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
Encrypted-cookie systems (e.g. iron-session) are the same shape — replace the
|
|
133
|
+
store lookup with the library's unseal/decrypt call and read the user id from
|
|
134
|
+
the decrypted payload.
|
|
135
|
+
|
|
136
|
+
## Provider backend SDKs
|
|
137
|
+
|
|
138
|
+
Already shipping a provider's admin SDK? Wrap it instead of re-verifying
|
|
139
|
+
tokens yourself. For example, Firebase Admin (alternative to the
|
|
140
|
+
`verifyFirebaseIdToken` JWKS preset):
|
|
141
|
+
|
|
142
|
+
```ts
|
|
143
|
+
import { getAuth } from 'firebase-admin/auth';
|
|
144
|
+
|
|
145
|
+
verify: async (req) => {
|
|
146
|
+
const token = req.headers.get('authorization')?.replace(/^Bearer\s+/i, '');
|
|
147
|
+
if (!token) return null;
|
|
148
|
+
try {
|
|
149
|
+
const decoded = await getAuth().verifyIdToken(token);
|
|
150
|
+
return { playerId: decoded.uid };
|
|
151
|
+
} catch {
|
|
152
|
+
return null; // bad signature / expired / wrong project
|
|
153
|
+
}
|
|
154
|
+
},
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
The same pattern fits any backend SDK that authenticates a request or token
|
|
158
|
+
and hands back a stable user id.
|
|
159
|
+
|
|
160
|
+
## Returning trusted metadata
|
|
161
|
+
|
|
162
|
+
Anything your session already proves can ride along as trusted metadata — it
|
|
163
|
+
overrides whatever the client put in the body for the same keys:
|
|
164
|
+
|
|
165
|
+
```ts
|
|
166
|
+
verify: async (req) => {
|
|
167
|
+
const session = await auth.api.getSession({ headers: req.headers });
|
|
168
|
+
if (!session) return null;
|
|
169
|
+
return {
|
|
170
|
+
playerId: session.user.id,
|
|
171
|
+
metadata: { displayName: session.user.name }, // server-verified, wins over body
|
|
172
|
+
};
|
|
173
|
+
},
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
## Hardening checklist
|
|
177
|
+
|
|
178
|
+
- **Never** derive `playerId` from the request body or an unverified header —
|
|
179
|
+
only from the verified session/token. (The handler enforces the body half of
|
|
180
|
+
this for you.)
|
|
181
|
+
- Use a stable, non-PII user id as `playerId` (auth user id, not an email).
|
|
182
|
+
- Add a cheap per-IP `rateLimit` gate; do per-user limits inside `verify`.
|
|
183
|
+
- Keep secrets (`SCOREZILLA_SECRET_KEY`, `AUTH_SECRET`, session-store
|
|
184
|
+
credentials) in env vars or a secret manager — never in client builds.
|
|
185
|
+
- Set `cors` only if the game is served from a different origin than the
|
|
186
|
+
endpoint; omit it for same-origin.
|
|
@@ -363,4 +363,4 @@ declare class ScorezillaError extends Error {
|
|
|
363
363
|
static timeout(timeoutMs: number): ScorezillaError;
|
|
364
364
|
}
|
|
365
365
|
|
|
366
|
-
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 };
|
|
366
|
+
export { type ApiSuccess as A, type BaseConfig as B, type FetchImpl as F, 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 };
|
|
@@ -363,4 +363,4 @@ declare class ScorezillaError extends Error {
|
|
|
363
363
|
static timeout(timeoutMs: number): ScorezillaError;
|
|
364
364
|
}
|
|
365
365
|
|
|
366
|
-
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 };
|
|
366
|
+
export { type ApiSuccess as A, type BaseConfig as B, type FetchImpl as F, 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 };
|