d3ro-voice/docs/map/07-api-server.md
Yun Chan e87ce63440 docs: record Wave 3 surface consolidation and fold NAS-only whisper into compose
- docs/REFACTOR_WAVE3_REPORT.md and the Wave 3 policy: canonical map,
  production changes, verification and remaining external steps.
- Gap backlog: GAP-BILL-01 resolved; new GAP-BILL-02 (Payple renewal never ran,
  Payple client key never set), GAP-WEB-01 (tunnel host for /app), GAP-OPS-01
  (NAS compose/.env drift), GAP-CI-01, GAP-I18N-02, GAP-TEAM-02.
- design.md: hero loop decision (numbers taken from the app capsule), pricing
  mismatch closed; feature catalog SHELL-11 updated.
- docs/map, release guide and mobile release docs no longer describe the
  deleted wwwroot, binaries, Dockerfile.admin, NAS site copy or .github CI.
- docker-compose.nas.yml gains the d3ro-whisper service that only existed in
  the NAS copy, so the repository file is the complete definition.
- refactor-wave skill: Wave 3 index and lessons P10-P12.
2026-09-26 16:02:44 +09:00

106 lines
6.5 KiB
Markdown

# 07 — API Server (.NET) Map
> Surface: `apps/api-server` (`D3ROVoice.Api`), tests in `apps/api-server.Tests`
> Stack: ASP.NET Core 10 + EF Core + SQLite
> Role: AI proxy (LLM/STT) + admin back-office backend for the .NET identity side
---
## 1. Composition root (`Program.cs`)
- **DI:** `AddControllers`, `AddHttpClient` (proxies), Swagger (`v1`), rate limiter policy `auth` (fixed window 10/min per IP, 429), `AddDbContext<AppDbContext>` (SQLite; `DATA_DIR`/`DB_PATH`), scoped `IAuthService`, `ILlmProxyService`, `ISttProxyService`, `IAdminOperationService`.
- **Auth:** JWT Bearer HS256; startup hard-fails unless `JWT_SECRET` (≥32 bytes), `JWT_ISSUER`, `JWT_AUDIENCE` are set. Zero clock skew.
- **Policies:** `ManagerOrAbove` (manager/admin/superadmin), `AdminOrAbove` (admin/superadmin), `SuperAdminOnly` (superadmin). Role normalization strips `_`/`-`, lowercases.
- **CORS/Hosts:** strict origin validation (`CORS_ALLOWED_ORIGINS`), `ALLOWED_HOSTS` required outside Development.
- **Startup DB init:** `EnsureCreated()`, raw `CREATE TABLE IF NOT EXISTS` for admin operation/audit tables, legacy SHA-256 password lockdown (`IsActive=false`, `Role="LegacyDisabled"`), idempotent env admin provisioning (`ADMIN_EMAIL`/`ADMIN_PASSWORD`, only when no active user), default LLM/STT endpoints seeded.
- **Middleware order:** Swagger (dev) → CORS → rate limiter → authentication → authorization. The API no longer serves static files: `wwwroot` (stale site build, download/invite pages, `.well-known` copy, legacy embedded admin SPA, 1.0.0 binaries) was deleted 2026-09-26 (Wave 3, cd9d199), and `UseStaticFiles`/the mobile-asset-block fallbacks were removed from `Program.cs` along with it. The Next.js admin (`apps/admin`) is the only admin UI.
- **Health:** `GET /health`, `GET /api/health` → `{status, service, version, uptimeSeconds, database, timestamp}`.
---
## 2. Controllers & routes
### `AuthController` (`/api/auth`)
| Route | Auth | Behavior |
|---|---|---|
| `POST /register` | anonymous + `X-D3RO-Bootstrap-Token` | Rate-limited, ≤16KB; requires `ADMIN_BOOTSTRAP_TOKEN`; fixed-time compare; `409` if registered |
| `POST /login` | anonymous | Rate-limited; returns JWT; `401` invalid |
| `GET /me` | `[Authorize]` | `UserInfoDto` or `404` |
### `LlmController` (`/api/llm`, class `[Authorize]`)
| Route | Behavior |
|---|---|
| `POST /generate` | Requires prompt; `GenerateAsync` |
| `POST /chat` | `ChatAsync` (last message = prompt) |
### `SttController` (`/api/stt`, class `[Authorize]`)
| Route | Auth | Behavior |
|---|---|---|
| `POST /transcribe` | any auth | **Always `410 Gone` `stt_edge_gateway_required`** — user transcription is Edge-only |
| `POST /internal/transcribe` | anonymous + `X-D3RO-STT-Gateway-Token` | multipart, ≤26MB; requires `D3RO_API_TOKEN`; fixed-time compare; provider orchestration; error map `503/400/502` |
| `GET /providers` | ManagerOrAbove | List STT endpoints |
| `POST /test?endpointId=` | ManagerOrAbove | Test endpoint |
### `AdminController` (`/api/admin`, class `ManagerOrAbove`)
All mutations flow through `ExecuteAdminMutationAsync` (idempotent + audited).
| Route | Effective policy |
|---|---|
| `GET /stats` | Manager+ |
| `GET /users` | Manager+ |
| `GET/POST/PUT/DELETE /endpoints[/{id}]` | Manager+ read; Admin+ write |
| `GET /stt-endpoints`, `/stt-endpoints/{id}` | Manager+ |
| `POST/PUT/DELETE /stt-endpoints[/{id}]`, `POST .../set-default` | Admin+ |
| `POST /stt-endpoints/{id}/test` | Manager+ |
| `POST /stt-endpoints/test-direct` | Admin+ |
| `GET /stt-usage`, `GET /usage` | Manager+ |
| `POST /license-audit` | **SuperAdminOnly** |
---
## 3. Services
| Service | Purpose |
|---|---|
| `AuthService` | Register (one-time SuperAdmin bootstrap only, `PasswordHasher<User>`, serialized), login (timing-safe dummy verify, inactive rejection, rehash), JWT gen (8h, claims id/email/role) |
| `LlmProxyService` | Resolve endpoint by model → fallback → Mock echo; OpenAI-style POST; parse content+usage; token cost → `ApiUsageLog`; errors → `ServerErrorLog` + fallback text |
| `SttProxyService` (~1182 lines) | Provider adapters (groq/openai/custom, deepgram, google, assemblyai, azure, local-sidecar), candidate resolution + fallback, content-type/duration detection, `SttUsageLog` cost, synthetic-tone endpoint test, CRUD with exclusive default, usage report |
| `AdminOperationService` | Validates actor/idempotency key/memo, SHA-256 request hash, `Serializable` transaction, idempotent replay, before/after audit entry |
---
## 4. Data (`Data/AppDbContext.cs`)
| DbSet / table | Key fields |
|---|---|
| `Users` | Id, unique Email, PasswordHash, Role (default "User"), IsActive, CreatedAt, LastLoginAt |
| `ModelEndpoints` | Id, unique ModelId, ModelName, Provider, EndpointUrl, ApiKey, per-1k costs, IsActive |
| `UsageLogs` | UserId/Email, model, tokens, cost, duration, status |
| `ErrorLogs` | ErrorType, Message, StackTrace, Endpoint |
| `SttProviderEndpoints` | Name, ProviderType, URL, ApiKey, ModelId, Method, Language, Prompt, Temperature, per-minute/second cost, IsDefault, FallbackPriority, ExtraHeadersJson |
| `SttUsageLogs` | User, endpoint, provider, duration, cost, latency, status, transcript preview |
| `AdminOperationRequests` | ActorEmail + IdempotencyKey (unique), Operation, RequestHash, ResponseJson |
| `AdminAuditEntries` | ActorEmail, Action, TargetType/Id, BeforeJson, AfterJson, Memo, IdempotencyKey |
DTOs (`Dtos/Dtos.cs`): auth, license audit, LLM, admin/model endpoints, STT (transcribe, endpoints, test, usage reports).
---
## 5. Tests (`apps/api-server.Tests/`, xUnit)
`AdminAuthorizationE2ETests`, `AdminOperationServiceTests`, `AuthBootstrapControllerTests`, `AuthSecurityTests`, `SttControllerSecurityTests`, `SttFailClosedTests`, `SttGatewayAuthorizationE2ETests` (26/26 per SSOT).
---
## 6. API server status summary
- Auth, login, JWT, role policies, rate limiting, CORS, host restrictions: **implemented**.
- LLM proxy: **implemented** (with local Mock echo fallback for standalone testing).
- STT proxy: **implemented** for internal gateway; public `/transcribe` intentionally `410` (Edge-only by design).
- Admin API: users, endpoints (LLM + STT), usage, license audit: **implemented** with idempotency + audit.
- Known intentional states (not bugs):
- Legacy SHA-256 users force-disabled.
- No hardcoded/seeded admin credentials.
- No static file serving — `wwwroot` and its mobile/legacy release asset block were deleted 2026-09-26 (Wave 3, cd9d199); downloads/releases are served only from `site/`.
- `LlmProxyService` Mock fallback.
- This backend holds a **separate identity** from Supabase; see `11-gap-backlog.md` `ID-01`.