Skip to content

Wire full Better Auth flow with feature flags + callback normalizer - #268

Merged
khanhduyvt0101 merged 1 commit into
mainfrom
auth/full-flow
Apr 28, 2026
Merged

Wire full Better Auth flow with feature flags + callback normalizer#268
khanhduyvt0101 merged 1 commit into
mainfrom
auth/full-flow

Conversation

@khanhduyvt0101

Copy link
Copy Markdown
Collaborator

Summary

  • Wire magicLink + passkey Better Auth plugins (gated by env, off by default), plus Google + GitHub social providers, end-to-end on server and SPA.
  • Introduce a chatacademia-style callback normalizer: dual-storage callback survival across tabs, a new /auth-callback route, and a verify-email view for unverified users.
  • Replace isProduction with isLive / isLocal in world-util, add isMagicLinkEnabled / isPasskeyEnabled, and inject them into the SPA bundle via Vite per-key process.env defines.

What changed

Server (@openkitten/world-server)

  • auth.tsx: conditional magicLink() + passkey() plugin registration, env-gated socialProviders, defaultCookieAttributes scaled by isLive, ensureAuthCallback validates origin against worldURL before forwarding.
  • New passkey table + relations + migration 0002_conscious_maestro.sql (generated via @better-auth/cli generate and drizzle-kit generate).
  • New lib/social-providers.ts, lib/emails/magic-link.tsx.

SPA (@openkitten/world-spa)

  • auth-callback route + verify-email component cover the new flows.
  • lib/{normalize,produce,retrieve,consume}-callback.ts implement dual-storage callback survival with try/catch for Safari private / quota-exceeded.
  • lib/{authenticate,get-session,session-query-options}.ts wrap Better Auth UI's session query for clientLoader use.
  • auth-provider.tsx reads magicLink={isMagicLinkEnabled} / passkey={isPasskeyEnabled} from world-util; auth-client.ts adds magicLinkClient() + passkeyClient().
  • forgot-password.tsx passes redirectTo: ${worldURL}/auth/reset-password so reset emails land on the SPA. sign-up.tsx always routes through /auth-callback. authenticated.tsx layout uses the new authenticate() helper. auth.tsx route falls back to stored callback when no ?redirectTo= query is present.
  • vite.config.ts loads .env.local from project root and injects OPENKITTEN_* flags via per-key process.env.X defines.

Util (@openkitten/world-util)

  • New is-live.ts, is-local.ts, is-magic-link-enabled.ts, is-passkey-enabled.ts.

Env

  • .env.example documents every env var.

Test plan

  • bun --bun tsc --build clean
  • bun --bun biome check clean
  • bun run --workspaces --if-present test — 1517 tests pass across 9 workspaces
  • 100% coverage (statements/branches/functions/lines) on every changed package
  • Browser-verified /auth/sign-in renders all four methods (email+password, magic-link, passkey, Google, GitHub) with both flags enabled
  • Browser-verified flag toggle: setting OPENKITTEN_MAGIC_LINK_ENABLED= and OPENKITTEN_PASSKEY_ENABLED= empty hides their buttons and returns 404 from their endpoints
  • Magic-link end-to-end: token stored in Redis after signIn.magicLink API call → visited /auth/magic-link/verify?token=… → server set session cookie → 302 to /auth-callback → consumed stored callback /game → landed on /game
  • Passkey endpoints (generate-register-options, generate-authenticate-options, list-user-passkeys) return valid WebAuthn payloads with challenge + rpId
  • OAuth: /auth/sign-in/social for both google and github returns provider authorization URL with the dev client_id echoed back

Notes

  • Magic-link email send fails with connect ECONNREFUSED 127.0.0.1:587 until SMTP_HOST/USER/PASS are set — the plugin is correctly wired and stores the token in Redis even when the email fails. Production deployments need real SMTP credentials.
  • Full passkey enrollment requires a WebAuthn authenticator (hardware or CDP virtual). The endpoints are validated; full ceremony is left to integration testing.
  • serverURL and worldURL are localhost ports in dev (same-site). Production with separate hostnames will need crossSubDomainCookies config — out of scope for this PR.

🤖 Generated with Claude Code

- Add `magicLink` and `passkey` plugins server-side, gated by
  `OPENKITTEN_MAGIC_LINK_ENABLED` / `OPENKITTEN_PASSKEY_ENABLED`
  (default disabled). Wire matching client plugins on `auth-client.ts`
  and conditional UI in `auth-provider.tsx`.

- Add env-gated Google + GitHub social providers (`socialProviders.ts`)
  driven by `GOOGLE_CLIENT_ID/SECRET` and `GITHUB_CLIENT_ID/SECRET`.

- Introduce chatacademia-style callback storage (sessionStorage +
  localStorage) so OAuth and magic-link returns survive cross-tab and
  cross-window flows. New `lib/{normalize,produce,retrieve,consume}-callback`
  with try/catch hardening for Safari private mode and quota-exceeded.

- Add `/auth-callback` route that consumes the stored target and routes
  unverified users to the new `verify-email` view.

- Override Better Auth's `callbackURL` for verification + magic-link
  emails so they land on the SPA's `/auth-callback`, not the API.
  Validate origin to reject external redirects.

- Pass `redirectTo` to `requestPasswordReset` so reset emails point at
  the SPA's `/auth/reset-password` page.

- Replace `isProduction` with `isLive` / `isLocal` in world-util,
  derived from `OPENKITTEN_LOCAL`. Add `isMagicLinkEnabled` and
  `isPasskeyEnabled` flags. Vite injects them into the SPA bundle via
  per-key `process.env.X` defines.

- Add `passkey` table + relations via `@better-auth/cli generate`,
  migrated with drizzle-kit. Cookie attributes now scale `secure` with
  `isLive`.

- `.env.example` documents every env var. `.env.local` is dev-only.

100% test coverage maintained across all changed packages.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@khanhduyvt0101
khanhduyvt0101 enabled auto-merge (squash) April 28, 2026 17:47
@khanhduyvt0101
khanhduyvt0101 merged commit f8b556a into main Apr 28, 2026
1 check passed
@khanhduyvt0101
khanhduyvt0101 deleted the auth/full-flow branch April 28, 2026 17:50
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant