## Summary
- Add the `config_kv` schema and Drizzle migration `0020`.
- Keep the ConfigKV schema, cache store, and invalidation contract in
the Resource API.
- Read ConfigKV through a five-minute Redis cache with PostgreSQL
fallback.
- Reload Router and TTS voice state through `configkv:invalidate`.
- Keep Auth rate limits fixed at 20 requests per 60 seconds.
## Stack
- Depends on #2294 for the Redis test implementation.
- This PR adds ConfigKV-specific cache-aside and Pub/Sub tests on top of
that implementation.
## Deployment
Run migration `0020` before this runtime reaches production traffic.
Then freeze ConfigKV writes. Audit and backfill the data with
[proj-airi/backend#2](https://github.com/proj-airi/backend/pull/2).
Merge
[proj-airi/backend#4](https://github.com/proj-airi/backend/pull/4)
first, so
the fixed Auth rate-limit keys are skipped.
Keep writes frozen until the hashes match and two API instances pass the
Pub/Sub reload check. This PR does not run production DDL or data
migration.
## Verification
- `pnpm exec vitest run <ConfigKV cache store, sync subscriber, and Auth
rate-limit tests>` (12 tests passed)
- `pnpm -F @proj-airi/api-server typecheck`
- `git diff --check`
See #2294 for its frozen-install, ESLint, and 73-test verification.
## Visual changes
None. This PR changes backend persistence and rate-limit wiring only.
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit
- **New Features**
- Added centralized configuration storage with validation, caching,
refresh, and automatic synchronization across services.
- Configuration updates now refresh related language-model and
text-to-speech settings automatically.
- **Bug Fixes**
- Improved recovery after service reconnects by clearing stale
configuration and reloading current values.
- Invalid or unavailable configuration data now produces clearer
service-unavailable responses.
- **Changes**
- Authentication rate limiting now uses a consistent limit of 20
requests per minute per client.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
---------
Signed-off-by: RainbowBird <git@luoling.moe>
Signed-off-by: RainbowBird <rbxin2003@outlook.com>
## Summary
- Replace Redis command fakes with `ioredis-mock` in API tests.
- Run the production Lua scripts through `EVAL`.
- Keep Redis behavior tests on the same command and Pub/Sub
implementation used by production code.
## Stack
- This PR is the base for #2289.
- It replaces #2291 as the merge-to-`main` unit. #2291 merged into the
old
ConfigKV branch before the stack could be reordered.
## Tests
- `pnpm install --frozen-lockfile --offline --ignore-scripts`
- `pnpm exec vitest run <6 changed API test files>` (73 tests passed)
- `pnpm exec eslint <7 changed API TypeScript files>`
- `git diff --check`
## Visual changes
None. This PR changes test infrastructure only.
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit
* **Tests**
* Added isolated in-memory Redis support for automated testing.
* Updated billing, Stripe, flux, concurrency, and user-deletion tests to
use a shared Redis test setup.
* Improved verification of Redis operations while preserving existing
test coverage and expected outcomes.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
---------
Signed-off-by: RainbowBird <rbxin2003@outlook.com>
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
## 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>