## Summary
Steam OpenID returns only a SteamID64. On the first Steam sign-up, Auth
reads `GetPlayerSummaries` and stores the persona name and the https
avatar URL. An empty `STEAM_PUBLISHER_KEY`, or a failed profile read,
still creates the user with the placeholder name `Steam User
{SteamID64}` and an empty image. A later sign-in and an account link do
not read or replace the profile. This PR is independent.
## Architecture and behavior
```mermaid
sequenceDiagram
participant Browser
participant Auth
participant SteamOpenID
participant SteamWebAPI
Browser->>Auth: GET /steam/callback
Auth->>SteamOpenID: check_authentication
alt existing Steam account or link
Auth->>Browser: session or link redirect
else first signup
Auth->>SteamWebAPI: GetPlayerSummaries
Auth->>Auth: createOAuthUser name and image
Auth->>Browser: session redirect
end
```
Auth owns the read. It uses the publisher key only as the `key` query
parameter, and it does not store the key or the raw response.
`personaname` is trimmed into `user.name`. An https `avatarfull` URL is
stored in `user.image`. Auth does not download the avatar file.
## Boundaries and risks
| Invariant or boundary | Failure mode | Protection | Evidence or gap |
| --- | --- | --- | --- |
| First sign-up stores the public persona name and https avatar URL |
Empty name, non-https avatar, or a player that does not match the
SteamID64 | Trim the name, accept only https, ignore a missing player,
keep the placeholder name | Plugin test for a successful first callback
|
| Sign-up still completes when the profile read fails | Timeout, HTTP
error, or invalid body | Catch the failure, log it without the request
URL, create the placeholder user | Plugin test for a failed profile
request |
| The publisher key stays out of logs, the browser, and stored user data
| ofetch includes the request URL in the error text | Replace the Steam
Web API URL and the key before logging | Plugin test asserts the log
omits the key and `api.steampowered.com` |
| A later sign-in or a link does not overwrite name or image | A second
callback would refresh a name the user changed | Skip
`GetPlayerSummaries` unless Auth is creating the Steam user | Plugin
tests for a second sign-in and a link |
| Existing users are not backfilled | Accounts created before this
change keep the placeholder name | No migration and no read on an
existing account | Not verified against production data |
## Verification
```text
pnpm -F @proj-airi/auth-server exec vitest run src/tests/steam.test.ts src/tests/env.test.ts
```
14 tests passed.
A local first Steam sign-up stored a persona name and an
`https://avatars.steamstatic.com/..._full.jpg` avatar URL. The
placeholder email and the Steam account link were also present.
## Rollout and follow-up
Set `STEAM_PUBLISHER_KEY` on the Auth service. For `pnpm dev:backend`,
set it in `server/apps/api/.env.local`. An empty value keeps the
previous placeholder behavior. No database migration is required.
Made with [Cursor](https://cursor.com)
---------
Co-authored-by: Cursor <cursoragent@cursor.com>
AIRI Auth Server
Standalone authentication and identity application for Project AIRI.
Responsibilities
- Better Auth session, social login, magic-link, password, and OIDC flows.
/api/auth/*,/auth/*, and authentication discovery endpoints.- Auth-owned Redis configuration, transactional email, and auth telemetry.
- Calling the resource API over the deployment's private network before deleting business data.
Code layout
The runtime is intentionally flat. Its main boundaries are:
auth.ts: Better Auth configuration and identity lifecycle hooks.routes.ts: the complete public Auth HTTP surface and request authentication.server.ts: dependency composition, health checks, and process lifecycle.resource-api.ts: the single private Auth-to-resource-API boundary.rate-limit.tsandotel.ts: cross-route operational policies.email.tsandoidc-jwt-bearer.ts: substantial external integration modules.
Small shared contracts stay beside those boundaries (db.ts, env.ts,
error.ts, and origin.ts). Tests are collected under src/tests; Better
Auth schema-generation wiring is isolated under src/tooling.
Run locally
pnpm -F @proj-airi/auth-server dev
The service reads .env.local from this directory. PUBLIC_URL is the public issuer origin presented through Caddy; RESOURCE_SERVER_URL is the private resource API used for internal calls.
To run PostgreSQL, Redis, the resource API, and Auth together from the repository root:
pnpm dev:backend
server/docker-compose.yaml exposes only the local Caddy gateway on http://localhost:6112; API and Auth
stay on its private network. The internal /internal/* boundary has no
application token, and Caddy rejects that path at the public edge.
Native Google sign-in
Set AUTH_GOOGLE_NATIVE_CLIENT_IDS to a comma-separated list of additional Google OAuth client IDs.
For Android Credential Manager, include the Web client ID passed as serverClientId.
- For the standalone Auth process, set the variable in
server/apps/auth/.env.local. - For
pnpm dev:backend, set it inserver/apps/api/.env.local. Compose loadsserver/apps/api/.envand.env.localinto the Auth container, in that order. It does not loadserver/apps/auth/.env.local. Runpnpm dev:backendagain after edits so Compose recreates the container with the updated values. - For Railway, set it in the Auth service variables for the target environment.
AUTH_GOOGLE_NATIVE_CLIENT_IDS=123456789-native.apps.googleusercontent.com
The original AUTH_GOOGLE_CLIENT_ID stays first in the provider configuration.
Browser authorization still uses that client and AUTH_GOOGLE_CLIENT_SECRET.
Native ID tokens can use any configured audience. Better Auth checks the token signature, issuer, expiry, and supplied nonce.
Omit the new variable to keep the existing configuration. No database migration is required.
Google ID token sign-in can create an account without a Google access or refresh token. Account deletion continues when neither token is stored, because AIRI has no Google API credential to revoke. This does not revoke consent in the user's Google Account. If either token is stored, Auth must complete its existing revocation policy before it deletes AIRI data.
Railway
Deploy this as the Auth Railway service. Keep the service Root Directory at
the repository root because the Dockerfile copies workspace manifests and
server/packages/auth-shared. The project-level Infrastructure as Code file
is proj-airi/airi-railway/.railway/railway.ts. It owns the Dockerfile, start
command, /readyz healthcheck, and watch patterns.
Set PUBLIC_URL to this service's canonical public issuer URL, and make the
Resource API's AUTH_SERVER_URL exactly the same value. Set
RESOURCE_SERVER_URL from the Resource API's Railway private domain; do not
run shared database migrations from Auth. See
server/README.md for the complete
service contract.
Do not use it for
- Product APIs, billing, model routing, chat, or WebSocket business state.
- Importing modules from
server/apps/api. - Running the shared database migration history during normal process startup.
Auth tables and principal contracts live in @proj-airi/auth-shared. Drizzle reads shared migration files during API startup. The API remains the migration owner.