Real sign-in: Google, Apple, account deletion
2026-04-28
Identity is now separate from authentication. A new player_identities table stores (provider, providerUserId, email, emailVerified) with a unique constraint on (provider, providerUserId); sessions stores SHA-256+pepper hashes of 32-byte random tokens with a 30-day rolling expiry. POST /auth/google and POST /auth/apple verify the platform ID token (google-auth-library / jose against Apple JWKS), find-or-create the identity, and mint a session — the raw token is returned once at sign-in and never again. The authenticate middleware resolves Bearer tokens against the sessions table, refreshes lastUsedAt + expiresAt, and tags Sentry with the user. The legacy 'player-id' header path is gone from the API entirely.
Title screen drops Continue + New Game in favor of a single PLAY button that opens a SignInModal with Continue with Google / Continue with Apple and a Terms & Privacy link. Web sign-in works in the browser via Google Identity Services and Sign in with Apple JS. Native iOS/Android use expo-apple-authentication and @react-native-google-signin/google-signin — these need a dev client build (Expo Go can't load the native modules), so `npx expo run:ios` is now the way to test sign-in on a simulator. The expo-secure-store wrapper holds session token + playerId on device; api/client.ts auto-attaches Authorization: Bearer on every request.
Account deletion (App Store guideline 5.1.1(v) + GDPR Art. 17) wipes events across all 14 detail tables — including the previously-missed EndCutsceneEvent and CraftSpellEvent — plus notifications, sessions, and player_identities in one transaction. For Apple-linked accounts, the server signs a client_secret JWT with the .p8 key and posts to appleid.apple.com/auth/revoke so Apple drops the credential too; the client also calls GoogleSignin.revokeAccess() to drop the OAuth grant on the device. While making the API safe to ship sign-in publicly, /player routes also gained a token-bucket rate limiter that returns 429 with a Retry-After header — the app's ApiClientError carries a RATE_LIMITED code, and the retry loop honours the server-issued Retry-After before falling back to exponential backoff with jitter.