Files
moeka-project/apps/ui-server-auth
ff7f64ace8 feat(server): add Steam OpenID sign-in and account linking plugin (#2226)
## Summary

Adds a self-contained better-auth plugin
(`server/apps/api/src/libs/auth-plugins/steam.ts`) implementing Steam
OpenID 2.0 sign-in, account linking, and callback verification via "dumb
mode".

Steam's web login is OpenID 2.0, not OAuth2/OIDC, so it cannot be
registered as a `socialProviders` entry, and better-auth has no plugin
hook for extending its OAuth2 endpoints with a non-OAuth2 protocol. The
plugin therefore adds the endpoints Steam's protocol needs: `POST
/sign-in/steam`, `POST /link/steam`, and `GET /steam/callback`.

- Callback verification uses OpenID "dumb mode"
(`openid.mode=check_authentication`): one extra round trip to Steam
instead of managing RSA association state.
- New sign-ups get a placeholder `<steamid64>@steam.placeholder.local`
with `emailVerified: true`, mirroring Apple Sign In's
`<sub>@apple.placeholder.local`.
- The plugin's request/query schemas use Zod; a `// NOTICE:` documents
that better-auth's OpenAPI generator is Zod-native. Steam verification
uses `ofetch`.
- Wires Steam into `apps/ui-server-auth` sign-in and profile "Connected
accounts", plus the shared `OAuthProvider` / `defaultSignInProviders` in
`packages/stage-ui`.
- Linking routes through `/link/steam` via the client's `$fetch`;
unlinking needs no special-casing (`/unlink-account` already takes a
free-form `providerId`).

No Steam Web API key is required for this browser-based flow.

We intentionally do not depend on community Steam packages (e.g.
`better-auth-steam`) or the still-open upstream draft
([better-auth#4877](https://github.com/better-auth/better-auth/pull/4877)).
Steam never returns an email, and we need sign-up that does not ask the
user for one plus first-class account linking; the available options
either require an email at sign-in, lack linking, or are abandoned /
blocked — shipping a small in-tree plugin is the safer auth dependency
for this requirement.

## Test plan

- [x] `pnpm exec vitest run
server/apps/api/src/libs/auth-plugins/steam.test.ts` — 6/6 passing
- [x] `pnpm -F @proj-airi/ui-server-auth exec vitest run` — 32/32
passing
- [x] `pnpm -F @proj-airi/stage-ui exec vitest run
src/libs/steam-auth-client.test.ts
src/composables/use-linked-accounts.test.ts` — 5/5 passing
- [x] `pnpm -F @proj-airi/api-server typecheck`
- [x] `pnpm -F @proj-airi/ui-server-auth typecheck`
- [x] `pnpm -F @proj-airi/stage-ui typecheck`

## Follow-ups

- Desktop Steam ticket sign-in (top of this stack): silent startup
ticket exchange for Steam builds; the server resolves or creates the
AIRI user for the verified SteamID before issuing an OIDC code.
- Steam persona name/avatar via `GetPlayerSummaries` inside the plugin,
if display names beyond `Steam User <id>` are wanted.

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
2026-08-05 23:57:55 +08:00
..

AIRI Server Auth UI

Auth UI for the hosted AIRI server. It is a Vue/Vite app deployed separately from server/apps/api and used for Better Auth sign-in, email verification, password reset, profile, and Electron OIDC callback relay flows.

Use When

  • Building user-facing auth pages backed by server /api/auth/* endpoints.
  • Updating login, sign-up, verification, reset-password, account profile, or Electron auth relay UX.
  • Deploying the auth surface to Cloudflare Workers Static Assets.

Do Not Use When

  • Building the main stage app sign-in callback pages that consume OIDC tokens.
  • Adding admin-only operational pages. Those belong in the standalone proj-airi admin repository.

Commands

pnpm -F @proj-airi/ui-server-auth dev
pnpm -F @proj-airi/ui-server-auth typecheck
pnpm -F @proj-airi/ui-server-auth build

Deployment

pnpm -F @proj-airi/ui-server-auth build writes to apps/ui-server-auth/dist. Vue Router owns /ui/*, while Vite assets are served from root /assets-v2/* so Cloudflare Pages can serve static files without rewriting nested asset paths. The versioned namespace moves existing clients away from previously poisoned /assets/* browser cache entries. Both namespaces use Cache-Control: public, no-cache, allowing browsers to retain files only when Pages revalidates them. public/_redirects scopes the SPA rewrite to /ui/*, and the top-level public/404.html keeps missing assets and other unknown paths as HTTP 404 responses.

The production GitHub Actions workflow deploys this app to the Cloudflare Pages project moeru-ai-airi-auth with separate auth-account credentials:

AUTH_CLOUDFLARE_ACCOUNT_ID=...
AUTH_CLOUDFLARE_API_TOKEN=...

apps/ui-server-auth/wrangler.toml remains available for Workers Static Assets deployments, but production CI uses Cloudflare Pages direct upload.

Production expects:

VITE_SERVER_URL=https://api.airi.build

The server redirects historical /auth/* URLs to AUTH_UI_URL, which defaults to https://accounts.airi.build/ui.

The server-dev workflow deploys a Cloudflare Pages branch build at https://server-dev.airi-server-auth.pages.dev/ui/ with VITE_SERVER_URL=https://airi-server-dev.up.railway.app. Set the server-dev API environment variable AUTH_UI_URL=https://server-dev.airi-server-auth.pages.dev/ui when the full dev auth redirect chain should stay on server-dev.