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/identity.cjs
CHANGED
|
@@ -143,6 +143,94 @@ async function signInWithGoogle(params) {
|
|
|
143
143
|
return credential === null ? null : decodeSubFromIdToken(credential);
|
|
144
144
|
}
|
|
145
145
|
|
|
146
|
+
// src/identity/github.ts
|
|
147
|
+
var GITHUB_AUTHORIZE_URL = "https://github.com/login/oauth/authorize";
|
|
148
|
+
var GITHUB_MESSAGE_SOURCE = "scorezilla:github-oauth";
|
|
149
|
+
var POPUP_CLOSED_POLL_MS = 500;
|
|
150
|
+
var SIGN_IN_TIMEOUT_MS = 10 * 60 * 1e3;
|
|
151
|
+
var KNOWN_ERRORS = /* @__PURE__ */ new Set(["access_denied", "exchange_failed"]);
|
|
152
|
+
var ID_RE = /^\d{1,20}$/;
|
|
153
|
+
function randomState() {
|
|
154
|
+
const bytes = crypto.getRandomValues(new Uint8Array(16));
|
|
155
|
+
let out = "";
|
|
156
|
+
const alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789_-";
|
|
157
|
+
for (const b of bytes) out += alphabet[b & 63];
|
|
158
|
+
return out;
|
|
159
|
+
}
|
|
160
|
+
async function signInWithGitHub(params) {
|
|
161
|
+
const exchangeUrl = new URL(params.exchangeUrl, window.location.href);
|
|
162
|
+
const state = randomState();
|
|
163
|
+
const authorize = new URL(GITHUB_AUTHORIZE_URL);
|
|
164
|
+
authorize.searchParams.set("client_id", params.clientId);
|
|
165
|
+
authorize.searchParams.set("redirect_uri", exchangeUrl.toString());
|
|
166
|
+
authorize.searchParams.set("state", state);
|
|
167
|
+
const popup = window.open(
|
|
168
|
+
authorize.toString(),
|
|
169
|
+
"scorezilla-github-oauth",
|
|
170
|
+
"popup,width=600,height=700"
|
|
171
|
+
);
|
|
172
|
+
if (popup === null) {
|
|
173
|
+
throw new Error(
|
|
174
|
+
"useAuthProvider: the GitHub sign-in popup was blocked. Call useAuthProvider from a user gesture (e.g. a click handler), or allow popups for this site."
|
|
175
|
+
);
|
|
176
|
+
}
|
|
177
|
+
return new Promise((resolve, reject) => {
|
|
178
|
+
const pollTimer = setInterval(() => {
|
|
179
|
+
if (popup.closed) settle(() => resolve(null));
|
|
180
|
+
}, POPUP_CLOSED_POLL_MS);
|
|
181
|
+
const timeoutTimer = setTimeout(() => {
|
|
182
|
+
settle(
|
|
183
|
+
() => reject(
|
|
184
|
+
new Error(
|
|
185
|
+
"useAuthProvider: GitHub sign-in timed out. If this recurs, check that exchangeUrl is deployed and reachable."
|
|
186
|
+
)
|
|
187
|
+
)
|
|
188
|
+
);
|
|
189
|
+
}, SIGN_IN_TIMEOUT_MS);
|
|
190
|
+
const settle = (action) => {
|
|
191
|
+
window.removeEventListener("message", onMessage);
|
|
192
|
+
clearInterval(pollTimer);
|
|
193
|
+
clearTimeout(timeoutTimer);
|
|
194
|
+
if (!popup.closed) popup.close();
|
|
195
|
+
action();
|
|
196
|
+
};
|
|
197
|
+
const onMessage = (event) => {
|
|
198
|
+
if (event.origin !== exchangeUrl.origin) return;
|
|
199
|
+
const data = event.data;
|
|
200
|
+
if (data === null || typeof data !== "object") return;
|
|
201
|
+
if (data.source !== GITHUB_MESSAGE_SOURCE) return;
|
|
202
|
+
if (data.state !== state) return;
|
|
203
|
+
if (typeof data.error === "string" && data.error.length > 0) {
|
|
204
|
+
if (data.error === "access_denied") {
|
|
205
|
+
settle(() => resolve(null));
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
208
|
+
const safeError = KNOWN_ERRORS.has(data.error) ? data.error : "exchange_failed";
|
|
209
|
+
settle(
|
|
210
|
+
() => reject(
|
|
211
|
+
new Error(
|
|
212
|
+
`useAuthProvider: GitHub token exchange failed (${safeError}). Check the exchange endpoint logs and its GitHub OAuth app credentials.`
|
|
213
|
+
)
|
|
214
|
+
)
|
|
215
|
+
);
|
|
216
|
+
return;
|
|
217
|
+
}
|
|
218
|
+
if (typeof data.id === "string" && ID_RE.test(data.id)) {
|
|
219
|
+
settle(() => resolve(data.id));
|
|
220
|
+
return;
|
|
221
|
+
}
|
|
222
|
+
settle(
|
|
223
|
+
() => reject(
|
|
224
|
+
new Error(
|
|
225
|
+
"useAuthProvider: the GitHub exchange endpoint posted a malformed callback message (missing or non-numeric id). Is exchangeUrl pointing at createGitHubOAuthHandler (or an equivalent implementation)?"
|
|
226
|
+
)
|
|
227
|
+
)
|
|
228
|
+
);
|
|
229
|
+
};
|
|
230
|
+
window.addEventListener("message", onMessage);
|
|
231
|
+
});
|
|
232
|
+
}
|
|
233
|
+
|
|
146
234
|
// src/identity.ts
|
|
147
235
|
var isBrowser = () => typeof window !== "undefined";
|
|
148
236
|
function readPersisted(key) {
|
|
@@ -227,9 +315,7 @@ async function useAuthProvider(options) {
|
|
|
227
315
|
case "google":
|
|
228
316
|
return signInWithGoogleProvider(options);
|
|
229
317
|
case "github":
|
|
230
|
-
|
|
231
|
-
'useAuthProvider: the GitHub provider is not available yet. It ships in scorezilla@0.3.0-next.2 and will require a server-side token exchange (your backend or a Scorezilla Workers proxy), because GitHub OAuth cannot be completed securely in the browser alone. Until then use provider: "google", or drive your own GitHub OAuth flow and pass the resulting id to submitScore.'
|
|
232
|
-
);
|
|
318
|
+
return signInWithGitHubProvider(options);
|
|
233
319
|
default:
|
|
234
320
|
throw new TypeError(
|
|
235
321
|
`useAuthProvider: unknown provider ${JSON.stringify(
|
|
@@ -268,6 +354,32 @@ async function signInWithGoogleProvider(options) {
|
|
|
268
354
|
googleSignInInFlight.set(storageKey, run);
|
|
269
355
|
return run;
|
|
270
356
|
}
|
|
357
|
+
var githubSignInInFlight = /* @__PURE__ */ new Map();
|
|
358
|
+
async function signInWithGitHubProvider(options) {
|
|
359
|
+
const clientId = requireNonEmptyString("useAuthProvider", "clientId", options.clientId);
|
|
360
|
+
const exchangeUrl = requireNonEmptyString("useAuthProvider", "exchangeUrl", options.exchangeUrl);
|
|
361
|
+
const storageKey = requireNonEmptyString("useAuthProvider", "storageKey", options.storageKey);
|
|
362
|
+
const persisted = readPersisted(storageKey);
|
|
363
|
+
if (persisted !== null && persisted.length > 0) {
|
|
364
|
+
return makeAuthHandle(persisted, "github", storageKey, "restored");
|
|
365
|
+
}
|
|
366
|
+
if (!isBrowser()) {
|
|
367
|
+
throw new Error("useAuthProvider: GitHub sign-in requires a browser environment.");
|
|
368
|
+
}
|
|
369
|
+
const existing = githubSignInInFlight.get(storageKey);
|
|
370
|
+
if (existing) return existing;
|
|
371
|
+
const run = (async () => {
|
|
372
|
+
const userId = await signInWithGitHub({ clientId, exchangeUrl });
|
|
373
|
+
if (userId === null) return null;
|
|
374
|
+
const id = `github:${userId}`;
|
|
375
|
+
writePersisted(storageKey, id);
|
|
376
|
+
return makeAuthHandle(id, "github", storageKey, "signed-in");
|
|
377
|
+
})().finally(() => {
|
|
378
|
+
githubSignInInFlight.delete(storageKey);
|
|
379
|
+
});
|
|
380
|
+
githubSignInInFlight.set(storageKey, run);
|
|
381
|
+
return run;
|
|
382
|
+
}
|
|
271
383
|
function makeAuthHandle(id, provider, storageKey, source) {
|
|
272
384
|
return {
|
|
273
385
|
id,
|
|
@@ -275,7 +387,7 @@ function makeAuthHandle(id, provider, storageKey, source) {
|
|
|
275
387
|
source,
|
|
276
388
|
signOut: () => {
|
|
277
389
|
removePersisted(storageKey);
|
|
278
|
-
disableGoogleAutoSelect();
|
|
390
|
+
if (provider === "google") disableGoogleAutoSelect();
|
|
279
391
|
}
|
|
280
392
|
};
|
|
281
393
|
}
|
package/dist/identity.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/identity/google.ts","../src/identity.ts"],"names":[],"mappings":"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","file":"identity.cjs","sourcesContent":["/**\n * Google provider for `useAuthProvider` — wraps Google Identity Services\n * (GIS) \"One Tap\" to produce a stable, opaque player id from the signed-in\n * Google account's `sub` claim.\n *\n * This module lives behind `useAuthProvider` in `../identity`. Consumers who\n * only use the non-OAuth presets (`useAnonymousPlayer`, etc.) tree-shake it\n * out entirely — the package sets `sideEffects: false` and this module has no\n * top-level side effects, so a bundler drops it when `useAuthProvider` is\n * unused. A size-limit gate (`.size-limit.cjs`) keeps that boundary honest.\n *\n * **What's bundled vs. fetched.** The heavyweight GIS library itself is NOT\n * bundled; it's loaded at runtime from `accounts.google.com` via an injected\n * `<script>` the first time sign-in runs. This module is just the thin\n * loader + One Tap orchestration + a tiny JWT-payload decoder.\n *\n * **Identity, not authorization.** The derived id is used purely as the\n * opaque `playerId` for leaderboard attribution. Score submission is still\n * authorized by the public key or the HMAC secure path — this module never\n * sends the Google credential to the Scorezilla API. We therefore decode the\n * ID token's payload client-side (no signature verification) solely to read\n * `sub`; the token arrives directly from Google's library over TLS.\n *\n * **One Tap is an implementation detail.** v1 uses GIS \"One Tap\". Under\n * browser FedCM / third-party-cookie changes, One Tap availability and its\n * prompt-moment semantics can vary, so `signInWithGoogle` resolves `null`\n * (rather than throwing) whenever no credential is obtained — callers fall\n * back gracefully. The public contract (`sub` string or `null`) is\n * independent of the flow, leaving room to add a rendered-button fallback\n * later without an API change.\n *\n * @module scorezilla/identity/google\n * @since 0.3.0\n */\n\nconst GIS_SRC = 'https://accounts.google.com/gsi/client';\nconst GIS_LOAD_TIMEOUT_MS = 10_000;\n\ninterface GooglePromptNotification {\n readonly isNotDisplayed?: () => boolean;\n readonly isSkippedMoment?: () => boolean;\n readonly isDismissedMoment?: () => boolean;\n readonly getDismissedReason?: () => string;\n}\n\ninterface GoogleCredentialResponse {\n readonly credential: string;\n}\n\ninterface GoogleIdApi {\n initialize(config: {\n client_id: string;\n callback: (response: GoogleCredentialResponse) => void;\n auto_select?: boolean;\n cancel_on_tap_outside?: boolean;\n }): void;\n prompt(listener?: (notification: GooglePromptNotification) => void): void;\n disableAutoSelect(): void;\n}\n\ninterface GoogleGlobal {\n readonly google?: { readonly accounts?: { readonly id?: GoogleIdApi } };\n}\n\nexport interface GoogleSignInParams {\n readonly clientId: string;\n readonly autoSelect: boolean;\n}\n\n/** Google OAuth Web client IDs always end with this suffix. */\nconst GOOGLE_CLIENT_ID_SUFFIX = '.apps.googleusercontent.com';\n\n/**\n * True if `clientId` has the shape of a Google OAuth Web client ID. Used to\n * turn a typo'd id into an actionable error up front, rather than a silent\n * \"One Tap couldn't be shown\" → `null` further down.\n */\nexport function isGoogleClientId(clientId: string): boolean {\n return clientId.endsWith(GOOGLE_CLIENT_ID_SUFFIX);\n}\n\nfunction getGoogleIdApi(): GoogleIdApi | undefined {\n return (globalThis as GoogleGlobal).google?.accounts?.id;\n}\n\n// Dedupes concurrent / retry-after-failure calls so the GIS <script> is never\n// injected twice. Cleared once the load settles (success or failure), so a\n// later sign-in starts clean. This relies on GIS being a page-level global:\n// once loaded, `getGoogleIdApi()` sees it from any subsequent call, which is\n// what makes clearing-on-settle safe (the fast-path covers \"already loaded\").\nlet gisLoadInFlight: Promise<GoogleIdApi> | null = null;\n\n/**\n * Resolve the GIS `id` API, injecting the loader `<script>` on first use.\n * Resolves immediately if GIS is already present (returning visit within the\n * same page, or a host that preloads the script).\n *\n * **Host CSP.** The page must allow the GIS origin in its Content-Security-\n * Policy — at minimum `script-src https://accounts.google.com` (One Tap also\n * needs `frame-src`/`connect-src` for `https://accounts.google.com`). Without\n * it the browser blocks the script and sign-in rejects with the load error.\n */\nfunction loadGoogleIdentityServices(): Promise<GoogleIdApi> {\n const existing = getGoogleIdApi();\n if (existing) return Promise.resolve(existing);\n\n if (typeof document === 'undefined') {\n return Promise.reject(\n new Error('scorezilla/identity: Google sign-in requires a browser environment.'),\n );\n }\n\n if (gisLoadInFlight) return gisLoadInFlight;\n gisLoadInFlight = injectGisScript().finally(() => {\n gisLoadInFlight = null;\n });\n return gisLoadInFlight;\n}\n\nfunction injectGisScript(): Promise<GoogleIdApi> {\n return new Promise<GoogleIdApi>((resolve, reject) => {\n // Reuse a tag the host page may already have added; only create (and later\n // clean up) one of our own when none exists.\n const existingTag = document.querySelector<HTMLScriptElement>(`script[src=\"${GIS_SRC}\"]`);\n const script = existingTag ?? document.createElement('script');\n const isOurs = existingTag === null;\n\n const failWith = (message: string): void => {\n clearTimeout(timer);\n if (isOurs) script.remove();\n reject(new Error(message));\n };\n\n // `failWith` closes over `timer` but only runs in async callbacks, after\n // this `const` is initialized — so the forward reference is safe.\n const timer = setTimeout(() => {\n failWith('scorezilla/identity: timed out loading Google Identity Services.');\n }, GIS_LOAD_TIMEOUT_MS);\n\n script.addEventListener(\n 'load',\n () => {\n clearTimeout(timer);\n const api = getGoogleIdApi();\n if (api) {\n resolve(api);\n } else {\n failWith(\n 'scorezilla/identity: Google Identity Services loaded but ' +\n 'window.google.accounts.id is unavailable.',\n );\n }\n },\n { once: true },\n );\n\n script.addEventListener(\n 'error',\n () => failWith('scorezilla/identity: failed to load the Google Identity Services script.'),\n { once: true },\n );\n\n if (isOurs) {\n script.src = GIS_SRC;\n script.async = true;\n script.defer = true;\n document.head.appendChild(script);\n }\n });\n}\n\n/** True when a One Tap moment means \"no credential is coming\". */\nfunction isBlockedMoment(notification: GooglePromptNotification): boolean {\n try {\n if (notification.isNotDisplayed?.() === true) return true;\n if (notification.isSkippedMoment?.() === true) return true;\n if (notification.isDismissedMoment?.() === true) {\n // A dismissal with reason `credential_returned` is the SUCCESS path — the\n // credential callback already fired (or is about to). Don't treat it as a\n // \"no credential\" moment.\n return notification.getDismissedReason?.() !== 'credential_returned';\n }\n } catch {\n // Under FedCM these legacy moment-status methods are deprecated and can\n // throw. Treat that as \"no credential from this moment\" — the credential\n // callback still fires on success, so this only affects the no-sign-in\n // path, which resolves to a clean `null`.\n return true;\n }\n return false;\n}\n\n/**\n * Decode a Google ID token's payload (no signature verification) and return\n * its `sub` claim — the stable, unique-per-account Google subject identifier.\n */\nfunction decodeSubFromIdToken(idToken: string): string {\n const segments = idToken.split('.');\n const [, payloadSegment] = segments;\n if (segments.length !== 3 || !payloadSegment) {\n throw new Error('scorezilla/identity: malformed Google credential (expected a JWT).');\n }\n\n let payload: unknown;\n try {\n payload = base64UrlToJson(payloadSegment);\n } catch {\n throw new Error('scorezilla/identity: could not decode the Google credential payload.');\n }\n\n // Read ONLY `sub`, behind a typeof guard. NEVER read any other claim here\n // (email, email_verified, aud, …) and NEVER use this value for an\n // authorization decision: the payload is unverified (no signature check), so\n // any other claim would be attacker-forgeable. `sub` is safe as an opaque\n // attribution id only. Reading just `sub` also sidesteps prototype-pollution\n // — `JSON.parse` doesn't mutate prototypes and nothing here propagates other\n // keys.\n const sub = (payload as { sub?: unknown }).sub;\n if (typeof sub !== 'string' || sub.length === 0) {\n throw new Error('scorezilla/identity: Google credential is missing the \"sub\" claim.');\n }\n return sub;\n}\n\nfunction base64UrlToJson(segment: string): unknown {\n const base64 = segment.replace(/-/g, '+').replace(/_/g, '/');\n const padded = base64 + '='.repeat((4 - (base64.length % 4)) % 4);\n const binary = atob(padded);\n const bytes = Uint8Array.from(binary, (char) => char.charCodeAt(0));\n const json = new TextDecoder().decode(bytes);\n return JSON.parse(json);\n}\n\n/**\n * Best-effort: stop GIS auto-selecting this account on the next visit (the\n * counterpart to `auto_select`). No-op if GIS was never loaded — e.g. a return\n * visit that short-circuited on the persisted id and never injected the script.\n */\nexport function disableGoogleAutoSelect(): void {\n try {\n getGoogleIdApi()?.disableAutoSelect();\n } catch {\n // GIS not present / not loaded — nothing to disable.\n }\n}\n\n/**\n * Run the Google One Tap flow.\n *\n * - Resolves the account's `sub` claim on a successful sign-in.\n * - Resolves `null` when no credential is obtained — the user dismissed or\n * declined One Tap, or it couldn't be displayed (no Google session,\n * cookies/FedCM blocked, cooldown). \"Didn't sign in\" is not an error.\n * - **Throws** only on hard failures: the GIS script failing to load/timing\n * out, or a malformed/empty credential.\n */\nexport async function signInWithGoogle(params: GoogleSignInParams): Promise<string | null> {\n const api = await loadGoogleIdentityServices();\n\n const credential = await new Promise<string | null>((resolve, reject) => {\n // The credential callback and the prompt moment-listener settle the same\n // promise independently, and GIS does not guarantee their ordering. Guard\n // against either firing after the other so a late \"no credential\" moment\n // can't override an already-successful sign-in (and vice versa).\n let settled = false;\n const finish = (value: string | null): void => {\n if (settled) return;\n settled = true;\n resolve(value);\n };\n const fail = (message: string): void => {\n if (settled) return;\n settled = true;\n reject(new Error(message));\n };\n\n api.initialize({\n client_id: params.clientId,\n auto_select: params.autoSelect,\n cancel_on_tap_outside: false,\n callback: (response) => {\n if (response && typeof response.credential === 'string' && response.credential.length > 0) {\n finish(response.credential);\n } else {\n fail('scorezilla/identity: Google returned an empty credential.');\n }\n },\n });\n\n api.prompt((notification) => {\n // A blocking moment means no credential is coming for this attempt.\n if (isBlockedMoment(notification)) finish(null);\n });\n });\n\n return credential === null ? null : decodeSubFromIdToken(credential);\n}\n","/**\n * Identity preset helpers — opinionated, selectable ways for your game to\n * generate or fetch a `playerId` for score submission.\n *\n * Background: every Scorezilla score carries an opaque `playerId`. The SDK\n * doesn't care whether it's a UUID, a nickname, an email, or a server\n * session token. But _how_ your game decides on that value is a UX +\n * privacy decision the team should make explicitly. These presets are the\n * blessed patterns; pick one per integration.\n *\n * See ADR 0003 (MCP identity axis) for the design rationale:\n * https://github.com/isco-tec/scorezilla/blob/main/docs/adr/0003-mcp-identity-axis.md\n *\n * @module scorezilla/identity\n * @since 0.3.0\n */\n\nimport { disableGoogleAutoSelect, isGoogleClientId, signInWithGoogle } from './identity/google';\n\nexport interface AnonymousPlayerOptions {\n /** localStorage key under which the generated UUID is persisted. */\n readonly storageKey: string;\n}\n\nexport interface PromptedPlayerOptions {\n /** localStorage key under which the user-entered name is persisted. */\n readonly storageKey: string;\n /** Message shown in `window.prompt()` on first run. */\n readonly prompt: string;\n}\n\n/**\n * Identity handle returned by the storage-backed presets.\n *\n * `forget()` clears the persisted value from browser storage. It does\n * **not** delete server-side score history for this player — to fully\n * erase a player's data, call the admin \"delete player\" endpoint.\n */\nexport interface PlayerHandle {\n readonly id: string;\n readonly forget: () => void;\n}\n\n/**\n * Marker returned by `useServerAuthoritative()` to signal that the\n * game's backend (not the browser) owns the `playerId` via the\n * HMAC-signed secure path (`scorezilla/server`).\n */\nexport interface ServerAuthoritativeMarker {\n readonly source: 'server-authoritative';\n}\n\n/** OAuth providers selectable via {@link useAuthProvider}. */\nexport type AuthProvider = 'google' | 'github';\n\n/** Options for the Google provider (`provider: 'google'`). */\nexport interface GoogleAuthProviderOptions {\n readonly provider: 'google';\n /**\n * Your Google OAuth **client ID** (from the Google Cloud Console). The\n * helper never bundles Scorezilla-owned credentials — you bring your own so\n * revocation and consent stay under your control.\n */\n readonly clientId: string;\n /** localStorage key under which the derived player id is persisted. */\n readonly storageKey: string;\n /**\n * Let Google auto-select a returning account without an explicit tap\n * (GIS `auto_select`). Defaults to `false`.\n */\n readonly autoSelect?: boolean;\n}\n\n/**\n * Options for the GitHub provider (`provider: 'github'`).\n *\n * **Not available yet** — ships in `scorezilla@0.3.0-next.2`. GitHub OAuth\n * cannot be completed securely in the browser alone (the token exchange needs\n * a client secret and GitHub's token endpoint sends no CORS headers), so the\n * GitHub provider will require a server-side token exchange (your backend or\n * a Scorezilla Workers proxy). Calling it today rejects.\n *\n * @experimental The option fields below are provisional and will be finalized\n * (e.g. `clientId` becoming required, plus a token-exchange-endpoint field)\n * when the provider lands in `0.3.0-next.2` — before the `0.3.0` stable cut.\n */\nexport interface GitHubAuthProviderOptions {\n readonly provider: 'github';\n /** Reserved (provisional) — your GitHub OAuth app client ID. */\n readonly clientId?: string;\n /** Reserved (provisional) — localStorage key for the derived player id. */\n readonly storageKey?: string;\n}\n\n/** Discriminated union of {@link useAuthProvider} options, keyed on `provider`. */\nexport type AuthProviderOptions = GoogleAuthProviderOptions | GitHubAuthProviderOptions;\n\n/** How an {@link AuthPlayerHandle}'s `id` was obtained on this call. */\nexport type AuthIdSource = 'signed-in' | 'restored';\n\n/**\n * Handle returned by {@link useAuthProvider}. `id` is the opaque, stable\n * player id derived from the provider account (e.g. `google:<sub>`).\n * `signOut()` clears the persisted id and, where supported, disables the\n * provider's auto sign-in. It does **not** delete server-side score history.\n */\nexport interface AuthPlayerHandle {\n readonly id: string;\n readonly provider: AuthProvider;\n /**\n * `'signed-in'` when the id came from a fresh provider sign-in during this\n * call; `'restored'` when it was rehydrated from a prior session in\n * `localStorage` with no provider interaction. A `'restored'` id is **not**\n * a re-verified live session — see {@link useAuthProvider}.\n */\n readonly source: AuthIdSource;\n readonly signOut: () => void;\n}\n\nconst isBrowser = (): boolean => typeof window !== 'undefined';\n\nfunction readPersisted(key: string): string | null {\n if (!isBrowser()) return null;\n try {\n return window.localStorage.getItem(key);\n } catch {\n // Storage may throw in sandboxed iframes, privacy mode, or when the\n // user has disabled site data. Treat as \"missing\"; the caller will\n // mint or re-prompt.\n return null;\n }\n}\n\nfunction writePersisted(key: string, value: string): void {\n if (!isBrowser()) return;\n try {\n window.localStorage.setItem(key, value);\n } catch {\n // ignore; next call will re-mint or re-prompt\n }\n}\n\nfunction removePersisted(key: string): void {\n if (!isBrowser()) return;\n try {\n window.localStorage.removeItem(key);\n } catch {\n // ignore\n }\n}\n\nfunction mintUuid(): string {\n if (isBrowser() && typeof crypto !== 'undefined' && typeof crypto.randomUUID === 'function') {\n return crypto.randomUUID();\n }\n // Best-effort fallback: timestamp + random suffix. Not cryptographically\n // strong, but opaque enough for the identifier-only use case. The\n // browsers we target (Chrome 92+, Firefox 95+, Safari 15.4+) all have\n // crypto.randomUUID — this branch is reached only in non-browser\n // environments where useAnonymousPlayer shouldn't be called anyway.\n return `anon-${Date.now()}-${Math.random().toString(36).slice(2, 10)}`;\n}\n\nfunction requireNonEmptyString(fnName: string, field: string, value: unknown): string {\n if (typeof value !== 'string' || value.length === 0) {\n throw new TypeError(`${fnName}: options.${field} is required (non-empty string)`);\n }\n return value;\n}\n\nfunction requireStorageKey(fnName: string, options: { storageKey?: unknown } | undefined): string {\n return requireNonEmptyString(fnName, 'storageKey', options?.storageKey);\n}\n\n/**\n * Anonymous player identity. Generates an opaque UUID on first run and\n * persists it in `localStorage` so the same browser keeps the same ID\n * across page reloads.\n *\n * **Privacy.** Stores a randomly-generated UUID in browser localStorage;\n * the value is sent to the API on every score submission and persisted\n * indefinitely in the player's score-history rows. No PII is collected.\n * `forget()` removes the localStorage entry; for full server-side erasure\n * call the admin \"delete player\" endpoint.\n *\n * @example\n * ```ts\n * import { Scorezilla } from 'scorezilla';\n * import { useAnonymousPlayer } from 'scorezilla/identity';\n *\n * const player = useAnonymousPlayer({ storageKey: 'mygame:player' });\n * const sz = new Scorezilla({ publicKey: 'pk_…' });\n * await sz.submitScore({ boardId, playerId: player.id, score: 42 });\n * ```\n *\n * @since 0.3.0\n * @stability stable\n */\nexport function useAnonymousPlayer(options: AnonymousPlayerOptions): PlayerHandle {\n const storageKey = requireStorageKey('useAnonymousPlayer', options);\n let id = readPersisted(storageKey);\n if (id === null || id.length === 0) {\n id = mintUuid();\n writePersisted(storageKey, id);\n }\n return {\n id,\n forget: () => removePersisted(storageKey),\n };\n}\n\n/**\n * Prompted player identity. On first run shows a `window.prompt()` asking\n * the user for a name, then persists it in `localStorage` for subsequent\n * visits. Returns `null` if there is no browser (SSR), no `window.prompt`,\n * or if the user cancelled / entered an empty value.\n *\n * **Privacy.** The user-entered string is stored in browser localStorage,\n * transmitted to the API on every score submission, and persisted\n * indefinitely on the leaderboard. The persisted value is whatever the\n * user typed — sanitize at the UI layer if you care. `forget()` clears\n * local state but does NOT delete server-side history.\n *\n * **UX caveat.** `window.prompt()` blocks the main thread and looks\n * dated in modern apps. For a polished flow, build your own inline form\n * and pass the result to `submitScore` directly — the preset is here to\n * cover quick prototypes and jam-style integrations.\n *\n * @example\n * ```ts\n * import { Scorezilla } from 'scorezilla';\n * import { usePromptedPlayer } from 'scorezilla/identity';\n *\n * const player = usePromptedPlayer({\n * storageKey: 'mygame:player',\n * prompt: 'Enter a name for the leaderboard:',\n * });\n *\n * if (player) {\n * const sz = new Scorezilla({ publicKey: 'pk_…' });\n * await sz.submitScore({ boardId, playerId: player.id, score: 42 });\n * }\n * ```\n *\n * @since 0.3.0\n * @stability stable\n */\nexport function usePromptedPlayer(options: PromptedPlayerOptions): PlayerHandle | null {\n const storageKey = requireStorageKey('usePromptedPlayer', options);\n if (typeof options.prompt !== 'string' || options.prompt.length === 0) {\n throw new TypeError('usePromptedPlayer: options.prompt is required (non-empty string)');\n }\n\n let id = readPersisted(storageKey);\n if (id === null || id.length === 0) {\n if (!isBrowser() || typeof window.prompt !== 'function') {\n return null;\n }\n const entered = window.prompt(options.prompt);\n if (entered === null || entered.length === 0) {\n return null;\n }\n id = entered;\n writePersisted(storageKey, id);\n }\n return {\n id,\n forget: () => removePersisted(storageKey),\n };\n}\n\n/**\n * Server-authoritative identity marker. Signals that the game's backend\n * is responsible for the `playerId` via the HMAC-signed secure path\n * (`scorezilla/server`). The browser SDK does no identity work — the\n * server picks the value, signs the submission, and posts.\n *\n * The return value is a no-op marker; you don't pass it anywhere. It\n * exists so MCP-returned snippets can emit a single line that\n * unambiguously says \"this game uses the secure path; identity is\n * server-authoritative.\"\n *\n * @example\n * ```ts\n * // Client (no identity helper needed):\n * import { useServerAuthoritative } from 'scorezilla/identity';\n * useServerAuthoritative();\n *\n * // Server (where the real work happens):\n * import { Scorezilla } from 'scorezilla/server';\n * const sz = new Scorezilla({ secretKey: process.env.SCOREZILLA_SECRET_KEY! });\n * await sz.submitScore({ boardId, playerId: serverDerivedId, score });\n * ```\n *\n * @since 0.3.0\n * @stability stable\n */\nexport function useServerAuthoritative(): ServerAuthoritativeMarker {\n return { source: 'server-authoritative' };\n}\n\n/**\n * OAuth-backed player identity. Signs the player in with the chosen provider\n * and resolves a stable, opaque `playerId` derived from their account.\n *\n * Resolves to:\n * - an {@link AuthPlayerHandle} on success — `handle.source` distinguishes a\n * fresh sign-in (`'signed-in'`) from a `localStorage`-restored prior session\n * (`'restored'`); or\n * - `null` when the player **declines / dismisses** sign-in, or it can't be\n * shown (no provider session, blocked cookies). \"Didn't sign in\" is not an\n * error — fall back to another identity strategy.\n *\n * **Rejects** only on genuine failures: invalid arguments (`TypeError`), an\n * unavailable provider, or the provider flow breaking (script load failure,\n * malformed credential). Identity helpers throw plain `Error`/`TypeError` by\n * design — NOT `ScorezillaError` — so the `scorezilla/identity` subpath stays\n * dependency-free; don't `instanceof ScorezillaError` these.\n *\n * > Despite the `use*` name (shared with the other presets), this is a plain\n * > async function, **not a React hook** — rules-of-hooks don't apply. The\n * > `scorezilla/react` adapter exposes the React-bound surface separately.\n *\n * **Google** (stable since `0.3.0`) wraps Google Identity Services \"One Tap\".\n * The derived id is `google:<sub>`. It's persisted in `localStorage` under\n * `storageKey`, so returning visitors are recognized without signing in again\n * (`handle.source === 'restored'`); `signOut()` clears it. The host page's CSP\n * must allow `https://accounts.google.com` (`script-src`, plus `frame-src` /\n * `connect-src` for One Tap).\n *\n * **GitHub** ships in `0.3.0-next.2` and currently rejects — see\n * {@link GitHubAuthProviderOptions}.\n *\n * **Privacy.** Only the derived `sub`-based id is stored and transmitted (on\n * score submission) — never the Google credential, email, or profile. The id\n * is persisted in browser localStorage and stored indefinitely in the\n * player's score-history rows. `signOut()` clears local state; for full\n * server-side erasure call the admin \"delete player\" endpoint.\n *\n * **Bundle.** The Google provider is a separate module that tree-shakes out\n * entirely for consumers who don't call `useAuthProvider` (the package is\n * `sideEffects: false`; a size-limit gate verifies it). The Google Identity\n * Services library itself is never bundled — it's loaded at runtime from\n * `accounts.google.com` the first time sign-in runs.\n *\n * @example\n * ```ts\n * import { Scorezilla } from 'scorezilla';\n * import { useAuthProvider } from 'scorezilla/identity';\n *\n * const player = await useAuthProvider({\n * provider: 'google',\n * clientId: 'YOUR_GOOGLE_CLIENT_ID.apps.googleusercontent.com',\n * storageKey: 'mygame:player',\n * });\n *\n * if (player) {\n * const sz = new Scorezilla({ publicKey: 'pk_…' });\n * await sz.submitScore({ boardId, playerId: player.id, score: 42 });\n * }\n * ```\n *\n * @since 0.3.0\n * @stability stable (google) · preview (github)\n */\nexport async function useAuthProvider(\n options: AuthProviderOptions,\n): Promise<AuthPlayerHandle | null> {\n if (!options || typeof options !== 'object') {\n throw new TypeError('useAuthProvider: options is required ({ provider, … }).');\n }\n\n switch (options.provider) {\n case 'google':\n return signInWithGoogleProvider(options);\n case 'github':\n throw new Error(\n 'useAuthProvider: the GitHub provider is not available yet. It ships in ' +\n 'scorezilla@0.3.0-next.2 and will require a server-side token exchange ' +\n '(your backend or a Scorezilla Workers proxy), because GitHub OAuth cannot ' +\n 'be completed securely in the browser alone. Until then use provider: \"google\", ' +\n 'or drive your own GitHub OAuth flow and pass the resulting id to submitScore.',\n );\n default:\n throw new TypeError(\n `useAuthProvider: unknown provider ${JSON.stringify(\n (options as { provider?: unknown }).provider,\n )} (expected \"google\" or \"github\").`,\n );\n }\n}\n\n// Coalesces concurrent sign-ins for the same storageKey (e.g. React StrictMode\n// double-invoke, or two leaderboards on one page) so they share a single One\n// Tap rather than racing GIS's single global callback — which would otherwise\n// leave the losing call's promise unresolved. Entries clear when the sign-in\n// settles, so this never accumulates state.\nconst googleSignInInFlight = new Map<string, Promise<AuthPlayerHandle | null>>();\n\nasync function signInWithGoogleProvider(\n options: GoogleAuthProviderOptions,\n): Promise<AuthPlayerHandle | null> {\n const clientId = requireNonEmptyString('useAuthProvider', 'clientId', options.clientId);\n if (!isGoogleClientId(clientId)) {\n throw new TypeError(\n 'useAuthProvider: options.clientId must be a Google OAuth client ID ' +\n '(ending in \".apps.googleusercontent.com\"). Create one at ' +\n 'https://console.cloud.google.com/apis/credentials.',\n );\n }\n const storageKey = requireNonEmptyString('useAuthProvider', 'storageKey', options.storageKey);\n\n // Return visit: trust the persisted id without re-running sign-in. Like the\n // other presets, this value is whatever is in localStorage under storageKey\n // — it is NOT a freshly re-verified Google identity (hence source:\n // 'restored'). Consistent with ADR 0003 (playerId is opaque attribution,\n // never an auth credential; the secure path signs submissions server-side).\n // Call signOut() to force a fresh sign-in next time.\n const persisted = readPersisted(storageKey);\n if (persisted !== null && persisted.length > 0) {\n return makeAuthHandle(persisted, 'google', storageKey, 'restored');\n }\n\n if (!isBrowser()) {\n throw new Error('useAuthProvider: Google sign-in requires a browser environment.');\n }\n\n const existing = googleSignInInFlight.get(storageKey);\n if (existing) return existing;\n\n const run = (async (): Promise<AuthPlayerHandle | null> => {\n const sub = await signInWithGoogle({ clientId, autoSelect: options.autoSelect ?? false });\n if (sub === null) return null;\n const id = `google:${sub}`;\n writePersisted(storageKey, id);\n return makeAuthHandle(id, 'google', storageKey, 'signed-in');\n })().finally(() => {\n googleSignInInFlight.delete(storageKey);\n });\n\n googleSignInInFlight.set(storageKey, run);\n return run;\n}\n\nfunction makeAuthHandle(\n id: string,\n provider: AuthProvider,\n storageKey: string,\n source: AuthIdSource,\n): AuthPlayerHandle {\n return {\n id,\n provider,\n source,\n signOut: () => {\n removePersisted(storageKey);\n if (provider === 'google') disableGoogleAutoSelect();\n },\n };\n}\n"]}
|
|
1
|
+
{"version":3,"sources":["../src/identity/google.ts","../src/identity/github.ts","../src/identity.ts"],"names":[],"mappings":"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","file":"identity.cjs","sourcesContent":["/**\n * Google provider for `useAuthProvider` — wraps Google Identity Services\n * (GIS) \"One Tap\" to produce a stable, opaque player id from the signed-in\n * Google account's `sub` claim.\n *\n * This module lives behind `useAuthProvider` in `../identity`. Consumers who\n * only use the non-OAuth presets (`useAnonymousPlayer`, etc.) tree-shake it\n * out entirely — the package sets `sideEffects: false` and this module has no\n * top-level side effects, so a bundler drops it when `useAuthProvider` is\n * unused. A size-limit gate (`.size-limit.cjs`) keeps that boundary honest.\n *\n * **What's bundled vs. fetched.** The heavyweight GIS library itself is NOT\n * bundled; it's loaded at runtime from `accounts.google.com` via an injected\n * `<script>` the first time sign-in runs. This module is just the thin\n * loader + One Tap orchestration + a tiny JWT-payload decoder.\n *\n * **Identity, not authorization.** The derived id is used purely as the\n * opaque `playerId` for leaderboard attribution. Score submission is still\n * authorized by the public key or the HMAC secure path — this module never\n * sends the Google credential to the Scorezilla API. We therefore decode the\n * ID token's payload client-side (no signature verification) solely to read\n * `sub`; the token arrives directly from Google's library over TLS.\n *\n * **One Tap is an implementation detail.** v1 uses GIS \"One Tap\". Under\n * browser FedCM / third-party-cookie changes, One Tap availability and its\n * prompt-moment semantics can vary, so `signInWithGoogle` resolves `null`\n * (rather than throwing) whenever no credential is obtained — callers fall\n * back gracefully. The public contract (`sub` string or `null`) is\n * independent of the flow, leaving room to add a rendered-button fallback\n * later without an API change.\n *\n * @module scorezilla/identity/google\n * @since 0.3.0\n */\n\nconst GIS_SRC = 'https://accounts.google.com/gsi/client';\nconst GIS_LOAD_TIMEOUT_MS = 10_000;\n\ninterface GooglePromptNotification {\n readonly isNotDisplayed?: () => boolean;\n readonly isSkippedMoment?: () => boolean;\n readonly isDismissedMoment?: () => boolean;\n readonly getDismissedReason?: () => string;\n}\n\ninterface GoogleCredentialResponse {\n readonly credential: string;\n}\n\ninterface GoogleIdApi {\n initialize(config: {\n client_id: string;\n callback: (response: GoogleCredentialResponse) => void;\n auto_select?: boolean;\n cancel_on_tap_outside?: boolean;\n }): void;\n prompt(listener?: (notification: GooglePromptNotification) => void): void;\n disableAutoSelect(): void;\n}\n\ninterface GoogleGlobal {\n readonly google?: { readonly accounts?: { readonly id?: GoogleIdApi } };\n}\n\nexport interface GoogleSignInParams {\n readonly clientId: string;\n readonly autoSelect: boolean;\n}\n\n/** Google OAuth Web client IDs always end with this suffix. */\nconst GOOGLE_CLIENT_ID_SUFFIX = '.apps.googleusercontent.com';\n\n/**\n * True if `clientId` has the shape of a Google OAuth Web client ID. Used to\n * turn a typo'd id into an actionable error up front, rather than a silent\n * \"One Tap couldn't be shown\" → `null` further down.\n */\nexport function isGoogleClientId(clientId: string): boolean {\n return clientId.endsWith(GOOGLE_CLIENT_ID_SUFFIX);\n}\n\nfunction getGoogleIdApi(): GoogleIdApi | undefined {\n return (globalThis as GoogleGlobal).google?.accounts?.id;\n}\n\n// Dedupes concurrent / retry-after-failure calls so the GIS <script> is never\n// injected twice. Cleared once the load settles (success or failure), so a\n// later sign-in starts clean. This relies on GIS being a page-level global:\n// once loaded, `getGoogleIdApi()` sees it from any subsequent call, which is\n// what makes clearing-on-settle safe (the fast-path covers \"already loaded\").\nlet gisLoadInFlight: Promise<GoogleIdApi> | null = null;\n\n/**\n * Resolve the GIS `id` API, injecting the loader `<script>` on first use.\n * Resolves immediately if GIS is already present (returning visit within the\n * same page, or a host that preloads the script).\n *\n * **Host CSP.** The page must allow the GIS origin in its Content-Security-\n * Policy — at minimum `script-src https://accounts.google.com` (One Tap also\n * needs `frame-src`/`connect-src` for `https://accounts.google.com`). Without\n * it the browser blocks the script and sign-in rejects with the load error.\n */\nfunction loadGoogleIdentityServices(): Promise<GoogleIdApi> {\n const existing = getGoogleIdApi();\n if (existing) return Promise.resolve(existing);\n\n if (typeof document === 'undefined') {\n return Promise.reject(\n new Error('scorezilla/identity: Google sign-in requires a browser environment.'),\n );\n }\n\n if (gisLoadInFlight) return gisLoadInFlight;\n gisLoadInFlight = injectGisScript().finally(() => {\n gisLoadInFlight = null;\n });\n return gisLoadInFlight;\n}\n\nfunction injectGisScript(): Promise<GoogleIdApi> {\n return new Promise<GoogleIdApi>((resolve, reject) => {\n // Reuse a tag the host page may already have added; only create (and later\n // clean up) one of our own when none exists.\n const existingTag = document.querySelector<HTMLScriptElement>(`script[src=\"${GIS_SRC}\"]`);\n const script = existingTag ?? document.createElement('script');\n const isOurs = existingTag === null;\n\n const failWith = (message: string): void => {\n clearTimeout(timer);\n if (isOurs) script.remove();\n reject(new Error(message));\n };\n\n // `failWith` closes over `timer` but only runs in async callbacks, after\n // this `const` is initialized — so the forward reference is safe.\n const timer = setTimeout(() => {\n failWith('scorezilla/identity: timed out loading Google Identity Services.');\n }, GIS_LOAD_TIMEOUT_MS);\n\n script.addEventListener(\n 'load',\n () => {\n clearTimeout(timer);\n const api = getGoogleIdApi();\n if (api) {\n resolve(api);\n } else {\n failWith(\n 'scorezilla/identity: Google Identity Services loaded but ' +\n 'window.google.accounts.id is unavailable.',\n );\n }\n },\n { once: true },\n );\n\n script.addEventListener(\n 'error',\n () => failWith('scorezilla/identity: failed to load the Google Identity Services script.'),\n { once: true },\n );\n\n if (isOurs) {\n script.src = GIS_SRC;\n script.async = true;\n script.defer = true;\n document.head.appendChild(script);\n }\n });\n}\n\n/** True when a One Tap moment means \"no credential is coming\". */\nfunction isBlockedMoment(notification: GooglePromptNotification): boolean {\n try {\n if (notification.isNotDisplayed?.() === true) return true;\n if (notification.isSkippedMoment?.() === true) return true;\n if (notification.isDismissedMoment?.() === true) {\n // A dismissal with reason `credential_returned` is the SUCCESS path — the\n // credential callback already fired (or is about to). Don't treat it as a\n // \"no credential\" moment.\n return notification.getDismissedReason?.() !== 'credential_returned';\n }\n } catch {\n // Under FedCM these legacy moment-status methods are deprecated and can\n // throw. Treat that as \"no credential from this moment\" — the credential\n // callback still fires on success, so this only affects the no-sign-in\n // path, which resolves to a clean `null`.\n return true;\n }\n return false;\n}\n\n/**\n * Decode a Google ID token's payload (no signature verification) and return\n * its `sub` claim — the stable, unique-per-account Google subject identifier.\n */\nfunction decodeSubFromIdToken(idToken: string): string {\n const segments = idToken.split('.');\n const [, payloadSegment] = segments;\n if (segments.length !== 3 || !payloadSegment) {\n throw new Error('scorezilla/identity: malformed Google credential (expected a JWT).');\n }\n\n let payload: unknown;\n try {\n payload = base64UrlToJson(payloadSegment);\n } catch {\n throw new Error('scorezilla/identity: could not decode the Google credential payload.');\n }\n\n // Read ONLY `sub`, behind a typeof guard. NEVER read any other claim here\n // (email, email_verified, aud, …) and NEVER use this value for an\n // authorization decision: the payload is unverified (no signature check), so\n // any other claim would be attacker-forgeable. `sub` is safe as an opaque\n // attribution id only. Reading just `sub` also sidesteps prototype-pollution\n // — `JSON.parse` doesn't mutate prototypes and nothing here propagates other\n // keys.\n const sub = (payload as { sub?: unknown }).sub;\n if (typeof sub !== 'string' || sub.length === 0) {\n throw new Error('scorezilla/identity: Google credential is missing the \"sub\" claim.');\n }\n return sub;\n}\n\nfunction base64UrlToJson(segment: string): unknown {\n const base64 = segment.replace(/-/g, '+').replace(/_/g, '/');\n const padded = base64 + '='.repeat((4 - (base64.length % 4)) % 4);\n const binary = atob(padded);\n const bytes = Uint8Array.from(binary, (char) => char.charCodeAt(0));\n const json = new TextDecoder().decode(bytes);\n return JSON.parse(json);\n}\n\n/**\n * Best-effort: stop GIS auto-selecting this account on the next visit (the\n * counterpart to `auto_select`). No-op if GIS was never loaded — e.g. a return\n * visit that short-circuited on the persisted id and never injected the script.\n */\nexport function disableGoogleAutoSelect(): void {\n try {\n getGoogleIdApi()?.disableAutoSelect();\n } catch {\n // GIS not present / not loaded — nothing to disable.\n }\n}\n\n/**\n * Run the Google One Tap flow.\n *\n * - Resolves the account's `sub` claim on a successful sign-in.\n * - Resolves `null` when no credential is obtained — the user dismissed or\n * declined One Tap, or it couldn't be displayed (no Google session,\n * cookies/FedCM blocked, cooldown). \"Didn't sign in\" is not an error.\n * - **Throws** only on hard failures: the GIS script failing to load/timing\n * out, or a malformed/empty credential.\n */\nexport async function signInWithGoogle(params: GoogleSignInParams): Promise<string | null> {\n const api = await loadGoogleIdentityServices();\n\n const credential = await new Promise<string | null>((resolve, reject) => {\n // The credential callback and the prompt moment-listener settle the same\n // promise independently, and GIS does not guarantee their ordering. Guard\n // against either firing after the other so a late \"no credential\" moment\n // can't override an already-successful sign-in (and vice versa).\n let settled = false;\n const finish = (value: string | null): void => {\n if (settled) return;\n settled = true;\n resolve(value);\n };\n const fail = (message: string): void => {\n if (settled) return;\n settled = true;\n reject(new Error(message));\n };\n\n api.initialize({\n client_id: params.clientId,\n auto_select: params.autoSelect,\n cancel_on_tap_outside: false,\n callback: (response) => {\n if (response && typeof response.credential === 'string' && response.credential.length > 0) {\n finish(response.credential);\n } else {\n fail('scorezilla/identity: Google returned an empty credential.');\n }\n },\n });\n\n api.prompt((notification) => {\n // A blocking moment means no credential is coming for this attempt.\n if (isBlockedMoment(notification)) finish(null);\n });\n });\n\n return credential === null ? null : decodeSubFromIdToken(credential);\n}\n","/**\n * GitHub provider for `useAuthProvider` — popup-based OAuth web flow with a\n * developer-deployed token-exchange endpoint, producing a stable, opaque\n * player id (`github:<numeric user id>`).\n *\n * **Why an exchange endpoint at all.** GitHub's token endpoint requires the\n * OAuth app's client secret and sends no CORS headers — the exchange cannot\n * happen in the browser (ADR 0009 decision 6). The flow:\n *\n * 1. this module opens a popup to GitHub's authorize URL, with\n * `redirect_uri` pointing at the developer's `exchangeUrl` and a\n * crypto-random `state`;\n * 2. GitHub redirects the popup to `exchangeUrl?code=&state=`;\n * 3. the endpoint (`createGitHubOAuthHandler` in `scorezilla/server`, or\n * the developer's own ~30 lines) exchanges the code server-side,\n * fetches the user id, and serves a page that `postMessage`s\n * `{ source, state, id }` back to this window and closes the popup;\n * 4. this module validates the message **origin** (must match\n * `exchangeUrl`'s origin) and the **state** echo, then resolves.\n *\n * **Identity, not authorization.** Identical posture to the Google provider:\n * the derived id is opaque leaderboard attribution. The GitHub access token\n * never reaches this module — it lives and dies inside the exchange\n * endpoint. See the trust-boundary note on `useAuthProvider`.\n *\n * Tree-shaking: like `./google`, this module has no top-level side effects\n * and drops out of bundles that never select the GitHub provider.\n *\n * **COOP caveat.** The popup posts to `window.opener`; a game page served\n * with `Cross-Origin-Opener-Policy: same-origin` severs that link and the\n * sign-in cannot complete. Use `same-origin-allow-popups` if you set COOP.\n *\n * @module scorezilla/identity/github\n * @since 0.3.0\n */\n\nconst GITHUB_AUTHORIZE_URL = 'https://github.com/login/oauth/authorize';\n\n/** Marker the exchange endpoint's callback page must echo. Public contract\n * between `scorezilla/identity` and `createGitHubOAuthHandler`. */\nexport const GITHUB_MESSAGE_SOURCE = 'scorezilla:github-oauth';\n\n/** How often we check whether the player closed the popup unresolved. */\nconst POPUP_CLOSED_POLL_MS = 500;\n\n/**\n * Hard ceiling on an unresolved sign-in. Generous — a player hunting for\n * their 2FA device must not get cut off — but bounded, so an unreachable\n * exchange endpoint can't leak the `message` listener forever.\n */\nconst SIGN_IN_TIMEOUT_MS = 10 * 60 * 1000;\n\n/** The server half's only error vocabulary. Anything else arriving in a\n * callback message is clamped before it can reach an Error message. */\nconst KNOWN_ERRORS = new Set(['access_denied', 'exchange_failed']);\n\n/** Shape `createGitHubOAuthHandler` guarantees for `id` (a GitHub numeric\n * user id, stringified). Anything else is a malformed endpoint. */\nconst ID_RE = /^\\d{1,20}$/;\n\nexport interface GitHubSignInParams {\n readonly clientId: string;\n /** Absolute or page-relative URL of the deployed exchange endpoint. */\n readonly exchangeUrl: string;\n}\n\n/** Payload shape posted by the exchange endpoint's callback page. */\ninterface GitHubCallbackMessage {\n readonly source?: unknown;\n readonly state?: unknown;\n readonly id?: unknown;\n readonly error?: unknown;\n}\n\n/** Crypto-random, URL- and HTML-safe state (CSRF binding for the popup). */\nfunction randomState(): string {\n const bytes = crypto.getRandomValues(new Uint8Array(16));\n let out = '';\n const alphabet = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789_-';\n for (const b of bytes) out += alphabet[b & 63];\n return out;\n}\n\n/**\n * Run the popup flow. Resolves the GitHub numeric user id (as a string), or\n * `null` when the player declined (GitHub `access_denied`, or the popup was\n * closed before completing). Rejects on genuine failures: popup blocked, or\n * the exchange endpoint reporting an error.\n */\nexport async function signInWithGitHub(params: GitHubSignInParams): Promise<string | null> {\n // Resolve the exchange URL against the page so relative paths work; its\n // origin doubles as the ONLY origin we accept callback messages from.\n const exchangeUrl = new URL(params.exchangeUrl, window.location.href);\n const state = randomState();\n\n const authorize = new URL(GITHUB_AUTHORIZE_URL);\n authorize.searchParams.set('client_id', params.clientId);\n authorize.searchParams.set('redirect_uri', exchangeUrl.toString());\n authorize.searchParams.set('state', state);\n // No `scope` param: the default grant reads public profile info only —\n // all we need is the numeric user id.\n\n // NOTE: deliberately NO `noopener` in the feature string — the callback\n // page delivers the result via `window.opener.postMessage`, so the opener\n // link is load-bearing. The flip side (the callback page can postMessage\n // anything at us) is exactly what the origin pin + source marker + state\n // echo in `onMessage` below defend against — that validation is the sole\n // trust boundary between this window and the popup.\n const popup = window.open(\n authorize.toString(),\n 'scorezilla-github-oauth',\n 'popup,width=600,height=700',\n );\n if (popup === null) {\n throw new Error(\n 'useAuthProvider: the GitHub sign-in popup was blocked. Call ' +\n 'useAuthProvider from a user gesture (e.g. a click handler), or allow ' +\n 'popups for this site.',\n );\n }\n\n return new Promise<string | null>((resolve, reject) => {\n // Player closed the popup without completing sign-in → decline. (The\n // callbacks fire long after `settle` below is initialized — closures\n // capture the binding, not the order of declaration.)\n const pollTimer = setInterval(() => {\n if (popup.closed) settle(() => resolve(null));\n }, POPUP_CLOSED_POLL_MS);\n\n // Leak guard: an unreachable exchange endpoint (or a popup frozen on a\n // dead page) must not hold the message listener open forever.\n const timeoutTimer = setTimeout(() => {\n settle(() =>\n reject(\n new Error(\n 'useAuthProvider: GitHub sign-in timed out. If this recurs, check ' +\n 'that exchangeUrl is deployed and reachable.',\n ),\n ),\n );\n }, SIGN_IN_TIMEOUT_MS);\n\n const settle = (action: () => void): void => {\n window.removeEventListener('message', onMessage);\n clearInterval(pollTimer);\n clearTimeout(timeoutTimer);\n if (!popup.closed) popup.close();\n action();\n };\n\n const onMessage = (event: MessageEvent): void => {\n // Origin pin: only the exchange endpoint's origin may complete the\n // flow. Everything else is silently ignored (not an error — any page\n // can postMessage at us).\n if (event.origin !== exchangeUrl.origin) return;\n const data = event.data as GitHubCallbackMessage | null;\n if (data === null || typeof data !== 'object') return;\n if (data.source !== GITHUB_MESSAGE_SOURCE) return;\n // State echo: binds the callback to THIS sign-in attempt (CSRF /\n // replay). A mismatch is ignored, not fatal — a stale or forged\n // message must not be able to abort a legitimate flow.\n if (data.state !== state) return;\n\n if (typeof data.error === 'string' && data.error.length > 0) {\n if (data.error === 'access_denied') {\n // The player cancelled on GitHub's consent screen — a decline,\n // not a failure (ADR 0009 contract: resolve null).\n settle(() => resolve(null));\n return;\n }\n // Clamp to the handler's fixed vocabulary before the value can\n // reach an Error message — a buggy bespoke endpoint must not be\n // able to inject arbitrary text into error reporting.\n const safeError = KNOWN_ERRORS.has(data.error) ? data.error : 'exchange_failed';\n settle(() =>\n reject(\n new Error(\n `useAuthProvider: GitHub token exchange failed (${safeError}). ` +\n 'Check the exchange endpoint logs and its GitHub OAuth app credentials.',\n ),\n ),\n );\n return;\n }\n\n // `createGitHubOAuthHandler` only ever emits a stringified numeric\n // user id; enforce that here too so a buggy bespoke endpoint can't\n // smuggle an arbitrary string into the persisted player id.\n if (typeof data.id === 'string' && ID_RE.test(data.id)) {\n settle(() => resolve(data.id as string));\n return;\n }\n // Marker + state matched but no (valid) id or error: malformed endpoint.\n settle(() =>\n reject(\n new Error(\n 'useAuthProvider: the GitHub exchange endpoint posted a malformed ' +\n 'callback message (missing or non-numeric id). Is exchangeUrl ' +\n 'pointing at createGitHubOAuthHandler (or an equivalent implementation)?',\n ),\n ),\n );\n };\n\n window.addEventListener('message', onMessage);\n });\n}\n","/**\n * Identity preset helpers — opinionated, selectable ways for your game to\n * generate or fetch a `playerId` for score submission.\n *\n * Background: every Scorezilla score carries an opaque `playerId`. The SDK\n * doesn't care whether it's a UUID, a nickname, an email, or a server\n * session token. But _how_ your game decides on that value is a UX +\n * privacy decision the team should make explicitly. These presets are the\n * blessed patterns; pick one per integration.\n *\n * See ADR 0003 (MCP identity axis) for the design rationale:\n * https://github.com/isco-tec/scorezilla/blob/main/docs/adr/0003-mcp-identity-axis.md\n *\n * @module scorezilla/identity\n * @since 0.3.0\n */\n\nimport { disableGoogleAutoSelect, isGoogleClientId, signInWithGoogle } from './identity/google';\nimport { signInWithGitHub } from './identity/github';\n\nexport interface AnonymousPlayerOptions {\n /** localStorage key under which the generated UUID is persisted. */\n readonly storageKey: string;\n}\n\nexport interface PromptedPlayerOptions {\n /** localStorage key under which the user-entered name is persisted. */\n readonly storageKey: string;\n /** Message shown in `window.prompt()` on first run. */\n readonly prompt: string;\n}\n\n/**\n * Identity handle returned by the storage-backed presets.\n *\n * `forget()` clears the persisted value from browser storage. It does\n * **not** delete server-side score history for this player — to fully\n * erase a player's data, call the admin \"delete player\" endpoint.\n */\nexport interface PlayerHandle {\n readonly id: string;\n readonly forget: () => void;\n}\n\n/**\n * Marker returned by `useServerAuthoritative()` to signal that the\n * game's backend (not the browser) owns the `playerId` via the\n * HMAC-signed secure path (`scorezilla/server`).\n */\nexport interface ServerAuthoritativeMarker {\n readonly source: 'server-authoritative';\n}\n\n/** OAuth providers selectable via {@link useAuthProvider}. */\nexport type AuthProvider = 'google' | 'github';\n\n/** Options for the Google provider (`provider: 'google'`). */\nexport interface GoogleAuthProviderOptions {\n readonly provider: 'google';\n /**\n * Your Google OAuth **client ID** (from the Google Cloud Console). The\n * helper never bundles Scorezilla-owned credentials — you bring your own so\n * revocation and consent stay under your control.\n */\n readonly clientId: string;\n /** localStorage key under which the derived player id is persisted. */\n readonly storageKey: string;\n /**\n * Let Google auto-select a returning account without an explicit tap\n * (GIS `auto_select`). Defaults to `false`.\n */\n readonly autoSelect?: boolean;\n}\n\n/**\n * Options for the GitHub provider (`provider: 'github'`).\n *\n * GitHub OAuth cannot be completed in the browser alone — the token exchange\n * needs your OAuth app's client secret and GitHub's token endpoint sends no\n * CORS headers. You therefore deploy a tiny exchange endpoint and point\n * `exchangeUrl` at it. `createGitHubOAuthHandler` in `scorezilla/server` is\n * that endpoint, turnkey:\n *\n * ```ts\n * // Server — one line on any web runtime:\n * export const GET = createGitHubOAuthHandler({\n * clientId: process.env.GITHUB_CLIENT_ID!,\n * clientSecret: process.env.GITHUB_CLIENT_SECRET!,\n * allowedOrigin: 'https://mygame.example', // the GAME page's origin\n * });\n * ```\n *\n * Register the deployed `exchangeUrl` as the OAuth app's **callback URL** in\n * GitHub's settings — GitHub redirects the sign-in popup there.\n */\nexport interface GitHubAuthProviderOptions {\n readonly provider: 'github';\n /** Your GitHub OAuth app client ID (Settings → Developer settings). */\n readonly clientId: string;\n /**\n * URL of your deployed token-exchange endpoint (absolute, or relative to\n * the page). Its **origin** is the only origin the sign-in popup is\n * allowed to complete from.\n */\n readonly exchangeUrl: string;\n /** localStorage key under which the derived player id is persisted. */\n readonly storageKey: string;\n}\n\n/** Discriminated union of {@link useAuthProvider} options, keyed on `provider`. */\nexport type AuthProviderOptions = GoogleAuthProviderOptions | GitHubAuthProviderOptions;\n\n/** How an {@link AuthPlayerHandle}'s `id` was obtained on this call. */\nexport type AuthIdSource = 'signed-in' | 'restored';\n\n/**\n * Handle returned by {@link useAuthProvider}. `id` is the opaque, stable\n * player id derived from the provider account (e.g. `google:<sub>`).\n * `signOut()` clears the persisted id and, where supported, disables the\n * provider's auto sign-in. It does **not** delete server-side score history.\n *\n * The id is **client-asserted** on submission — see the trust-boundary note\n * on {@link useAuthProvider} before using it for ranking-sensitive boards.\n */\nexport interface AuthPlayerHandle {\n readonly id: string;\n readonly provider: AuthProvider;\n /**\n * `'signed-in'` when the id came from a fresh provider sign-in during this\n * call; `'restored'` when it was rehydrated from a prior session in\n * `localStorage` with no provider interaction. A `'restored'` id is **not**\n * a re-verified live session — see {@link useAuthProvider}.\n */\n readonly source: AuthIdSource;\n readonly signOut: () => void;\n}\n\nconst isBrowser = (): boolean => typeof window !== 'undefined';\n\nfunction readPersisted(key: string): string | null {\n if (!isBrowser()) return null;\n try {\n return window.localStorage.getItem(key);\n } catch {\n // Storage may throw in sandboxed iframes, privacy mode, or when the\n // user has disabled site data. Treat as \"missing\"; the caller will\n // mint or re-prompt.\n return null;\n }\n}\n\nfunction writePersisted(key: string, value: string): void {\n if (!isBrowser()) return;\n try {\n window.localStorage.setItem(key, value);\n } catch {\n // ignore; next call will re-mint or re-prompt\n }\n}\n\nfunction removePersisted(key: string): void {\n if (!isBrowser()) return;\n try {\n window.localStorage.removeItem(key);\n } catch {\n // ignore\n }\n}\n\nfunction mintUuid(): string {\n if (isBrowser() && typeof crypto !== 'undefined' && typeof crypto.randomUUID === 'function') {\n return crypto.randomUUID();\n }\n // Best-effort fallback: timestamp + random suffix. Not cryptographically\n // strong, but opaque enough for the identifier-only use case. The\n // browsers we target (Chrome 92+, Firefox 95+, Safari 15.4+) all have\n // crypto.randomUUID — this branch is reached only in non-browser\n // environments where useAnonymousPlayer shouldn't be called anyway.\n return `anon-${Date.now()}-${Math.random().toString(36).slice(2, 10)}`;\n}\n\nfunction requireNonEmptyString(fnName: string, field: string, value: unknown): string {\n if (typeof value !== 'string' || value.length === 0) {\n throw new TypeError(`${fnName}: options.${field} is required (non-empty string)`);\n }\n return value;\n}\n\nfunction requireStorageKey(fnName: string, options: { storageKey?: unknown } | undefined): string {\n return requireNonEmptyString(fnName, 'storageKey', options?.storageKey);\n}\n\n/**\n * Anonymous player identity. Generates an opaque UUID on first run and\n * persists it in `localStorage` so the same browser keeps the same ID\n * across page reloads.\n *\n * **Privacy.** Stores a randomly-generated UUID in browser localStorage;\n * the value is sent to the API on every score submission and persisted\n * indefinitely in the player's score-history rows. No PII is collected.\n * `forget()` removes the localStorage entry; for full server-side erasure\n * call the admin \"delete player\" endpoint.\n *\n * @example\n * ```ts\n * import { Scorezilla } from 'scorezilla';\n * import { useAnonymousPlayer } from 'scorezilla/identity';\n *\n * const player = useAnonymousPlayer({ storageKey: 'mygame:player' });\n * const sz = new Scorezilla({ publicKey: 'pk_…' });\n * await sz.submitScore({ boardId, playerId: player.id, score: 42 });\n * ```\n *\n * @since 0.3.0\n * @stability stable\n */\nexport function useAnonymousPlayer(options: AnonymousPlayerOptions): PlayerHandle {\n const storageKey = requireStorageKey('useAnonymousPlayer', options);\n let id = readPersisted(storageKey);\n if (id === null || id.length === 0) {\n id = mintUuid();\n writePersisted(storageKey, id);\n }\n return {\n id,\n forget: () => removePersisted(storageKey),\n };\n}\n\n/**\n * Prompted player identity. On first run shows a `window.prompt()` asking\n * the user for a name, then persists it in `localStorage` for subsequent\n * visits. Returns `null` if there is no browser (SSR), no `window.prompt`,\n * or if the user cancelled / entered an empty value.\n *\n * **Privacy.** The user-entered string is stored in browser localStorage,\n * transmitted to the API on every score submission, and persisted\n * indefinitely on the leaderboard. The persisted value is whatever the\n * user typed — sanitize at the UI layer if you care. `forget()` clears\n * local state but does NOT delete server-side history.\n *\n * **UX caveat.** `window.prompt()` blocks the main thread and looks\n * dated in modern apps. For a polished flow, build your own inline form\n * and pass the result to `submitScore` directly — the preset is here to\n * cover quick prototypes and jam-style integrations.\n *\n * @example\n * ```ts\n * import { Scorezilla } from 'scorezilla';\n * import { usePromptedPlayer } from 'scorezilla/identity';\n *\n * const player = usePromptedPlayer({\n * storageKey: 'mygame:player',\n * prompt: 'Enter a name for the leaderboard:',\n * });\n *\n * if (player) {\n * const sz = new Scorezilla({ publicKey: 'pk_…' });\n * await sz.submitScore({ boardId, playerId: player.id, score: 42 });\n * }\n * ```\n *\n * @since 0.3.0\n * @stability stable\n */\nexport function usePromptedPlayer(options: PromptedPlayerOptions): PlayerHandle | null {\n const storageKey = requireStorageKey('usePromptedPlayer', options);\n if (typeof options.prompt !== 'string' || options.prompt.length === 0) {\n throw new TypeError('usePromptedPlayer: options.prompt is required (non-empty string)');\n }\n\n let id = readPersisted(storageKey);\n if (id === null || id.length === 0) {\n if (!isBrowser() || typeof window.prompt !== 'function') {\n return null;\n }\n const entered = window.prompt(options.prompt);\n if (entered === null || entered.length === 0) {\n return null;\n }\n id = entered;\n writePersisted(storageKey, id);\n }\n return {\n id,\n forget: () => removePersisted(storageKey),\n };\n}\n\n/**\n * Server-authoritative identity marker. Signals that the game's backend\n * is responsible for the `playerId` via the HMAC-signed secure path\n * (`scorezilla/server`). The browser SDK does no identity work — the\n * server picks the value, signs the submission, and posts.\n *\n * The return value is a no-op marker; you don't pass it anywhere. It\n * exists so MCP-returned snippets can emit a single line that\n * unambiguously says \"this game uses the secure path; identity is\n * server-authoritative.\"\n *\n * @example\n * ```ts\n * // Client (no identity helper needed):\n * import { useServerAuthoritative } from 'scorezilla/identity';\n * useServerAuthoritative();\n *\n * // Server (where the real work happens):\n * import { Scorezilla } from 'scorezilla/server';\n * const sz = new Scorezilla({ secretKey: process.env.SCOREZILLA_SECRET_KEY! });\n * await sz.submitScore({ boardId, playerId: serverDerivedId, score });\n * ```\n *\n * @since 0.3.0\n * @stability stable\n */\nexport function useServerAuthoritative(): ServerAuthoritativeMarker {\n return { source: 'server-authoritative' };\n}\n\n/**\n * OAuth-backed player identity. Signs the player in with the chosen provider\n * and resolves a stable, opaque `playerId` derived from their account.\n *\n * **Trust boundary — client-authoritative by design (#213).** Signing in\n * proves the player's identity *to the browser*, not to the leaderboard: the\n * derived id is computed client-side and submitted with the public key, so a\n * submission carries exactly the same forgeability as any other public-key\n * write. OAuth here buys sign-in convenience and a stable, human-recognizable\n * id on casual / vanity boards — it is **not** anti-forgery. Competitive or\n * ranking-sensitive boards must submit through the secure path instead:\n * `createScoreSubmitHandler` in `scorezilla/server` with a server-verified\n * identity (built-in Supabase / Clerk / Auth0 / Firebase verifiers, or your\n * own session lookup) — see RECIPES.md (\"OAuth identity and the secure path\").\n *\n * Resolves to:\n * - an {@link AuthPlayerHandle} on success — `handle.source` distinguishes a\n * fresh sign-in (`'signed-in'`) from a `localStorage`-restored prior session\n * (`'restored'`); or\n * - `null` when the player **declines / dismisses** sign-in, or it can't be\n * shown (no provider session, blocked cookies). \"Didn't sign in\" is not an\n * error — fall back to another identity strategy.\n *\n * **Rejects** only on genuine failures: invalid arguments (`TypeError`), an\n * unavailable provider, or the provider flow breaking (script load failure,\n * malformed credential). Identity helpers throw plain `Error`/`TypeError` by\n * design — NOT `ScorezillaError` — so the `scorezilla/identity` subpath stays\n * dependency-free; don't `instanceof ScorezillaError` these.\n *\n * > Despite the `use*` name (shared with the other presets), this is a plain\n * > async function, **not a React hook** — rules-of-hooks don't apply. The\n * > `scorezilla/react` adapter exposes the React-bound surface separately.\n *\n * **Google** (stable since `0.3.0`) wraps Google Identity Services \"One Tap\".\n * The derived id is `google:<sub>`. It's persisted in `localStorage` under\n * `storageKey`, so returning visitors are recognized without signing in again\n * (`handle.source === 'restored'`); `signOut()` clears it. The host page's CSP\n * must allow `https://accounts.google.com` (`script-src`, plus `frame-src` /\n * `connect-src` for One Tap).\n *\n * **GitHub** (stable since `0.3.0`) runs a popup OAuth web flow against your\n * deployed token-exchange endpoint (`createGitHubOAuthHandler` in\n * `scorezilla/server` — GitHub's exchange needs your client secret, which\n * never belongs in a browser). The derived id is `github:<numeric user id>`.\n * Requires popups (call from a user gesture); a page setting\n * `Cross-Origin-Opener-Policy: same-origin` severs the popup link — use\n * `same-origin-allow-popups`. See {@link GitHubAuthProviderOptions}.\n *\n * **Privacy.** Only the derived provider-account id is stored and transmitted\n * (on score submission) — never the Google credential / GitHub access token,\n * email, or profile. The id is persisted in browser localStorage and stored\n * indefinitely in the player's score-history rows. `signOut()` clears local\n * state; for full server-side erasure call the admin \"delete player\" endpoint.\n *\n * **Bundle.** The Google provider is a separate module that tree-shakes out\n * entirely for consumers who don't call `useAuthProvider` (the package is\n * `sideEffects: false`; a size-limit gate verifies it). The Google Identity\n * Services library itself is never bundled — it's loaded at runtime from\n * `accounts.google.com` the first time sign-in runs.\n *\n * @example\n * ```ts\n * import { Scorezilla } from 'scorezilla';\n * import { useAuthProvider } from 'scorezilla/identity';\n *\n * const player = await useAuthProvider({\n * provider: 'google',\n * clientId: 'YOUR_GOOGLE_CLIENT_ID.apps.googleusercontent.com',\n * storageKey: 'mygame:player',\n * });\n *\n * if (player) {\n * const sz = new Scorezilla({ publicKey: 'pk_…' });\n * await sz.submitScore({ boardId, playerId: player.id, score: 42 });\n * }\n * ```\n *\n * @since 0.3.0\n * @stability stable (google) · preview (github)\n */\nexport async function useAuthProvider(\n options: AuthProviderOptions,\n): Promise<AuthPlayerHandle | null> {\n if (!options || typeof options !== 'object') {\n throw new TypeError('useAuthProvider: options is required ({ provider, … }).');\n }\n\n switch (options.provider) {\n case 'google':\n return signInWithGoogleProvider(options);\n case 'github':\n return signInWithGitHubProvider(options);\n default:\n throw new TypeError(\n `useAuthProvider: unknown provider ${JSON.stringify(\n (options as { provider?: unknown }).provider,\n )} (expected \"google\" or \"github\").`,\n );\n }\n}\n\n// Coalesces concurrent sign-ins for the same storageKey (e.g. React StrictMode\n// double-invoke, or two leaderboards on one page) so they share a single One\n// Tap rather than racing GIS's single global callback — which would otherwise\n// leave the losing call's promise unresolved. Entries clear when the sign-in\n// settles, so this never accumulates state.\nconst googleSignInInFlight = new Map<string, Promise<AuthPlayerHandle | null>>();\n\nasync function signInWithGoogleProvider(\n options: GoogleAuthProviderOptions,\n): Promise<AuthPlayerHandle | null> {\n const clientId = requireNonEmptyString('useAuthProvider', 'clientId', options.clientId);\n if (!isGoogleClientId(clientId)) {\n throw new TypeError(\n 'useAuthProvider: options.clientId must be a Google OAuth client ID ' +\n '(ending in \".apps.googleusercontent.com\"). Create one at ' +\n 'https://console.cloud.google.com/apis/credentials.',\n );\n }\n const storageKey = requireNonEmptyString('useAuthProvider', 'storageKey', options.storageKey);\n\n // Return visit: trust the persisted id without re-running sign-in. Like the\n // other presets, this value is whatever is in localStorage under storageKey\n // — it is NOT a freshly re-verified Google identity (hence source:\n // 'restored'). Consistent with ADR 0003 (playerId is opaque attribution,\n // never an auth credential; the secure path signs submissions server-side).\n // Call signOut() to force a fresh sign-in next time.\n const persisted = readPersisted(storageKey);\n if (persisted !== null && persisted.length > 0) {\n return makeAuthHandle(persisted, 'google', storageKey, 'restored');\n }\n\n if (!isBrowser()) {\n throw new Error('useAuthProvider: Google sign-in requires a browser environment.');\n }\n\n const existing = googleSignInInFlight.get(storageKey);\n if (existing) return existing;\n\n const run = (async (): Promise<AuthPlayerHandle | null> => {\n const sub = await signInWithGoogle({ clientId, autoSelect: options.autoSelect ?? false });\n if (sub === null) return null;\n const id = `google:${sub}`;\n writePersisted(storageKey, id);\n return makeAuthHandle(id, 'google', storageKey, 'signed-in');\n })().finally(() => {\n googleSignInInFlight.delete(storageKey);\n });\n\n googleSignInInFlight.set(storageKey, run);\n return run;\n}\n\n// Same coalescing rationale as googleSignInInFlight: concurrent calls for\n// one storageKey share a single popup instead of racing two (browsers may\n// focus-steal or block the second; the player should see exactly one).\nconst githubSignInInFlight = new Map<string, Promise<AuthPlayerHandle | null>>();\n\nasync function signInWithGitHubProvider(\n options: GitHubAuthProviderOptions,\n): Promise<AuthPlayerHandle | null> {\n const clientId = requireNonEmptyString('useAuthProvider', 'clientId', options.clientId);\n const exchangeUrl = requireNonEmptyString('useAuthProvider', 'exchangeUrl', options.exchangeUrl);\n const storageKey = requireNonEmptyString('useAuthProvider', 'storageKey', options.storageKey);\n\n // Return visit: same restored-id semantics (and the same trust caveats)\n // as the Google provider — see signInWithGoogleProvider above.\n const persisted = readPersisted(storageKey);\n if (persisted !== null && persisted.length > 0) {\n return makeAuthHandle(persisted, 'github', storageKey, 'restored');\n }\n\n if (!isBrowser()) {\n throw new Error('useAuthProvider: GitHub sign-in requires a browser environment.');\n }\n\n const existing = githubSignInInFlight.get(storageKey);\n if (existing) return existing;\n\n const run = (async (): Promise<AuthPlayerHandle | null> => {\n const userId = await signInWithGitHub({ clientId, exchangeUrl });\n if (userId === null) return null;\n const id = `github:${userId}`;\n writePersisted(storageKey, id);\n return makeAuthHandle(id, 'github', storageKey, 'signed-in');\n })().finally(() => {\n githubSignInInFlight.delete(storageKey);\n });\n\n githubSignInInFlight.set(storageKey, run);\n return run;\n}\n\nfunction makeAuthHandle(\n id: string,\n provider: AuthProvider,\n storageKey: string,\n source: AuthIdSource,\n): AuthPlayerHandle {\n return {\n id,\n provider,\n source,\n signOut: () => {\n removePersisted(storageKey);\n if (provider === 'google') disableGoogleAutoSelect();\n },\n };\n}\n"]}
|
package/dist/identity.d.cts
CHANGED
|
@@ -65,22 +65,36 @@ interface GoogleAuthProviderOptions {
|
|
|
65
65
|
/**
|
|
66
66
|
* Options for the GitHub provider (`provider: 'github'`).
|
|
67
67
|
*
|
|
68
|
-
*
|
|
69
|
-
*
|
|
70
|
-
*
|
|
71
|
-
*
|
|
72
|
-
*
|
|
73
|
-
*
|
|
74
|
-
*
|
|
75
|
-
*
|
|
76
|
-
*
|
|
68
|
+
* GitHub OAuth cannot be completed in the browser alone — the token exchange
|
|
69
|
+
* needs your OAuth app's client secret and GitHub's token endpoint sends no
|
|
70
|
+
* CORS headers. You therefore deploy a tiny exchange endpoint and point
|
|
71
|
+
* `exchangeUrl` at it. `createGitHubOAuthHandler` in `scorezilla/server` is
|
|
72
|
+
* that endpoint, turnkey:
|
|
73
|
+
*
|
|
74
|
+
* ```ts
|
|
75
|
+
* // Server — one line on any web runtime:
|
|
76
|
+
* export const GET = createGitHubOAuthHandler({
|
|
77
|
+
* clientId: process.env.GITHUB_CLIENT_ID!,
|
|
78
|
+
* clientSecret: process.env.GITHUB_CLIENT_SECRET!,
|
|
79
|
+
* allowedOrigin: 'https://mygame.example', // the GAME page's origin
|
|
80
|
+
* });
|
|
81
|
+
* ```
|
|
82
|
+
*
|
|
83
|
+
* Register the deployed `exchangeUrl` as the OAuth app's **callback URL** in
|
|
84
|
+
* GitHub's settings — GitHub redirects the sign-in popup there.
|
|
77
85
|
*/
|
|
78
86
|
interface GitHubAuthProviderOptions {
|
|
79
87
|
readonly provider: 'github';
|
|
80
|
-
/**
|
|
81
|
-
readonly clientId
|
|
82
|
-
/**
|
|
83
|
-
|
|
88
|
+
/** Your GitHub OAuth app client ID (Settings → Developer settings). */
|
|
89
|
+
readonly clientId: string;
|
|
90
|
+
/**
|
|
91
|
+
* URL of your deployed token-exchange endpoint (absolute, or relative to
|
|
92
|
+
* the page). Its **origin** is the only origin the sign-in popup is
|
|
93
|
+
* allowed to complete from.
|
|
94
|
+
*/
|
|
95
|
+
readonly exchangeUrl: string;
|
|
96
|
+
/** localStorage key under which the derived player id is persisted. */
|
|
97
|
+
readonly storageKey: string;
|
|
84
98
|
}
|
|
85
99
|
/** Discriminated union of {@link useAuthProvider} options, keyed on `provider`. */
|
|
86
100
|
type AuthProviderOptions = GoogleAuthProviderOptions | GitHubAuthProviderOptions;
|
|
@@ -91,6 +105,9 @@ type AuthIdSource = 'signed-in' | 'restored';
|
|
|
91
105
|
* player id derived from the provider account (e.g. `google:<sub>`).
|
|
92
106
|
* `signOut()` clears the persisted id and, where supported, disables the
|
|
93
107
|
* provider's auto sign-in. It does **not** delete server-side score history.
|
|
108
|
+
*
|
|
109
|
+
* The id is **client-asserted** on submission — see the trust-boundary note
|
|
110
|
+
* on {@link useAuthProvider} before using it for ranking-sensitive boards.
|
|
94
111
|
*/
|
|
95
112
|
interface AuthPlayerHandle {
|
|
96
113
|
readonly id: string;
|
|
@@ -197,6 +214,17 @@ declare function useServerAuthoritative(): ServerAuthoritativeMarker;
|
|
|
197
214
|
* OAuth-backed player identity. Signs the player in with the chosen provider
|
|
198
215
|
* and resolves a stable, opaque `playerId` derived from their account.
|
|
199
216
|
*
|
|
217
|
+
* **Trust boundary — client-authoritative by design (#213).** Signing in
|
|
218
|
+
* proves the player's identity *to the browser*, not to the leaderboard: the
|
|
219
|
+
* derived id is computed client-side and submitted with the public key, so a
|
|
220
|
+
* submission carries exactly the same forgeability as any other public-key
|
|
221
|
+
* write. OAuth here buys sign-in convenience and a stable, human-recognizable
|
|
222
|
+
* id on casual / vanity boards — it is **not** anti-forgery. Competitive or
|
|
223
|
+
* ranking-sensitive boards must submit through the secure path instead:
|
|
224
|
+
* `createScoreSubmitHandler` in `scorezilla/server` with a server-verified
|
|
225
|
+
* identity (built-in Supabase / Clerk / Auth0 / Firebase verifiers, or your
|
|
226
|
+
* own session lookup) — see RECIPES.md ("OAuth identity and the secure path").
|
|
227
|
+
*
|
|
200
228
|
* Resolves to:
|
|
201
229
|
* - an {@link AuthPlayerHandle} on success — `handle.source` distinguishes a
|
|
202
230
|
* fresh sign-in (`'signed-in'`) from a `localStorage`-restored prior session
|
|
@@ -222,14 +250,19 @@ declare function useServerAuthoritative(): ServerAuthoritativeMarker;
|
|
|
222
250
|
* must allow `https://accounts.google.com` (`script-src`, plus `frame-src` /
|
|
223
251
|
* `connect-src` for One Tap).
|
|
224
252
|
*
|
|
225
|
-
* **GitHub**
|
|
226
|
-
*
|
|
253
|
+
* **GitHub** (stable since `0.3.0`) runs a popup OAuth web flow against your
|
|
254
|
+
* deployed token-exchange endpoint (`createGitHubOAuthHandler` in
|
|
255
|
+
* `scorezilla/server` — GitHub's exchange needs your client secret, which
|
|
256
|
+
* never belongs in a browser). The derived id is `github:<numeric user id>`.
|
|
257
|
+
* Requires popups (call from a user gesture); a page setting
|
|
258
|
+
* `Cross-Origin-Opener-Policy: same-origin` severs the popup link — use
|
|
259
|
+
* `same-origin-allow-popups`. See {@link GitHubAuthProviderOptions}.
|
|
227
260
|
*
|
|
228
|
-
* **Privacy.** Only the derived
|
|
229
|
-
* score submission) — never the Google credential
|
|
230
|
-
* is persisted in browser localStorage and stored
|
|
231
|
-
* player's score-history rows. `signOut()` clears local
|
|
232
|
-
* server-side erasure call the admin "delete player" endpoint.
|
|
261
|
+
* **Privacy.** Only the derived provider-account id is stored and transmitted
|
|
262
|
+
* (on score submission) — never the Google credential / GitHub access token,
|
|
263
|
+
* email, or profile. The id is persisted in browser localStorage and stored
|
|
264
|
+
* indefinitely in the player's score-history rows. `signOut()` clears local
|
|
265
|
+
* state; for full server-side erasure call the admin "delete player" endpoint.
|
|
233
266
|
*
|
|
234
267
|
* **Bundle.** The Google provider is a separate module that tree-shakes out
|
|
235
268
|
* entirely for consumers who don't call `useAuthProvider` (the package is
|
package/dist/identity.d.ts
CHANGED
|
@@ -65,22 +65,36 @@ interface GoogleAuthProviderOptions {
|
|
|
65
65
|
/**
|
|
66
66
|
* Options for the GitHub provider (`provider: 'github'`).
|
|
67
67
|
*
|
|
68
|
-
*
|
|
69
|
-
*
|
|
70
|
-
*
|
|
71
|
-
*
|
|
72
|
-
*
|
|
73
|
-
*
|
|
74
|
-
*
|
|
75
|
-
*
|
|
76
|
-
*
|
|
68
|
+
* GitHub OAuth cannot be completed in the browser alone — the token exchange
|
|
69
|
+
* needs your OAuth app's client secret and GitHub's token endpoint sends no
|
|
70
|
+
* CORS headers. You therefore deploy a tiny exchange endpoint and point
|
|
71
|
+
* `exchangeUrl` at it. `createGitHubOAuthHandler` in `scorezilla/server` is
|
|
72
|
+
* that endpoint, turnkey:
|
|
73
|
+
*
|
|
74
|
+
* ```ts
|
|
75
|
+
* // Server — one line on any web runtime:
|
|
76
|
+
* export const GET = createGitHubOAuthHandler({
|
|
77
|
+
* clientId: process.env.GITHUB_CLIENT_ID!,
|
|
78
|
+
* clientSecret: process.env.GITHUB_CLIENT_SECRET!,
|
|
79
|
+
* allowedOrigin: 'https://mygame.example', // the GAME page's origin
|
|
80
|
+
* });
|
|
81
|
+
* ```
|
|
82
|
+
*
|
|
83
|
+
* Register the deployed `exchangeUrl` as the OAuth app's **callback URL** in
|
|
84
|
+
* GitHub's settings — GitHub redirects the sign-in popup there.
|
|
77
85
|
*/
|
|
78
86
|
interface GitHubAuthProviderOptions {
|
|
79
87
|
readonly provider: 'github';
|
|
80
|
-
/**
|
|
81
|
-
readonly clientId
|
|
82
|
-
/**
|
|
83
|
-
|
|
88
|
+
/** Your GitHub OAuth app client ID (Settings → Developer settings). */
|
|
89
|
+
readonly clientId: string;
|
|
90
|
+
/**
|
|
91
|
+
* URL of your deployed token-exchange endpoint (absolute, or relative to
|
|
92
|
+
* the page). Its **origin** is the only origin the sign-in popup is
|
|
93
|
+
* allowed to complete from.
|
|
94
|
+
*/
|
|
95
|
+
readonly exchangeUrl: string;
|
|
96
|
+
/** localStorage key under which the derived player id is persisted. */
|
|
97
|
+
readonly storageKey: string;
|
|
84
98
|
}
|
|
85
99
|
/** Discriminated union of {@link useAuthProvider} options, keyed on `provider`. */
|
|
86
100
|
type AuthProviderOptions = GoogleAuthProviderOptions | GitHubAuthProviderOptions;
|
|
@@ -91,6 +105,9 @@ type AuthIdSource = 'signed-in' | 'restored';
|
|
|
91
105
|
* player id derived from the provider account (e.g. `google:<sub>`).
|
|
92
106
|
* `signOut()` clears the persisted id and, where supported, disables the
|
|
93
107
|
* provider's auto sign-in. It does **not** delete server-side score history.
|
|
108
|
+
*
|
|
109
|
+
* The id is **client-asserted** on submission — see the trust-boundary note
|
|
110
|
+
* on {@link useAuthProvider} before using it for ranking-sensitive boards.
|
|
94
111
|
*/
|
|
95
112
|
interface AuthPlayerHandle {
|
|
96
113
|
readonly id: string;
|
|
@@ -197,6 +214,17 @@ declare function useServerAuthoritative(): ServerAuthoritativeMarker;
|
|
|
197
214
|
* OAuth-backed player identity. Signs the player in with the chosen provider
|
|
198
215
|
* and resolves a stable, opaque `playerId` derived from their account.
|
|
199
216
|
*
|
|
217
|
+
* **Trust boundary — client-authoritative by design (#213).** Signing in
|
|
218
|
+
* proves the player's identity *to the browser*, not to the leaderboard: the
|
|
219
|
+
* derived id is computed client-side and submitted with the public key, so a
|
|
220
|
+
* submission carries exactly the same forgeability as any other public-key
|
|
221
|
+
* write. OAuth here buys sign-in convenience and a stable, human-recognizable
|
|
222
|
+
* id on casual / vanity boards — it is **not** anti-forgery. Competitive or
|
|
223
|
+
* ranking-sensitive boards must submit through the secure path instead:
|
|
224
|
+
* `createScoreSubmitHandler` in `scorezilla/server` with a server-verified
|
|
225
|
+
* identity (built-in Supabase / Clerk / Auth0 / Firebase verifiers, or your
|
|
226
|
+
* own session lookup) — see RECIPES.md ("OAuth identity and the secure path").
|
|
227
|
+
*
|
|
200
228
|
* Resolves to:
|
|
201
229
|
* - an {@link AuthPlayerHandle} on success — `handle.source` distinguishes a
|
|
202
230
|
* fresh sign-in (`'signed-in'`) from a `localStorage`-restored prior session
|
|
@@ -222,14 +250,19 @@ declare function useServerAuthoritative(): ServerAuthoritativeMarker;
|
|
|
222
250
|
* must allow `https://accounts.google.com` (`script-src`, plus `frame-src` /
|
|
223
251
|
* `connect-src` for One Tap).
|
|
224
252
|
*
|
|
225
|
-
* **GitHub**
|
|
226
|
-
*
|
|
253
|
+
* **GitHub** (stable since `0.3.0`) runs a popup OAuth web flow against your
|
|
254
|
+
* deployed token-exchange endpoint (`createGitHubOAuthHandler` in
|
|
255
|
+
* `scorezilla/server` — GitHub's exchange needs your client secret, which
|
|
256
|
+
* never belongs in a browser). The derived id is `github:<numeric user id>`.
|
|
257
|
+
* Requires popups (call from a user gesture); a page setting
|
|
258
|
+
* `Cross-Origin-Opener-Policy: same-origin` severs the popup link — use
|
|
259
|
+
* `same-origin-allow-popups`. See {@link GitHubAuthProviderOptions}.
|
|
227
260
|
*
|
|
228
|
-
* **Privacy.** Only the derived
|
|
229
|
-
* score submission) — never the Google credential
|
|
230
|
-
* is persisted in browser localStorage and stored
|
|
231
|
-
* player's score-history rows. `signOut()` clears local
|
|
232
|
-
* server-side erasure call the admin "delete player" endpoint.
|
|
261
|
+
* **Privacy.** Only the derived provider-account id is stored and transmitted
|
|
262
|
+
* (on score submission) — never the Google credential / GitHub access token,
|
|
263
|
+
* email, or profile. The id is persisted in browser localStorage and stored
|
|
264
|
+
* indefinitely in the player's score-history rows. `signOut()` clears local
|
|
265
|
+
* state; for full server-side erasure call the admin "delete player" endpoint.
|
|
233
266
|
*
|
|
234
267
|
* **Bundle.** The Google provider is a separate module that tree-shakes out
|
|
235
268
|
* entirely for consumers who don't call `useAuthProvider` (the package is
|