diff --git a/tasks/v5.0.0-plan-03-oauth-foundation.md b/tasks/v5.0.0-plan-03-oauth-foundation.md new file mode 100644 index 0000000..c6362a5 --- /dev/null +++ b/tasks/v5.0.0-plan-03-oauth-foundation.md @@ -0,0 +1,204 @@ +# Plan 03: OAuth Foundation (Pillar 2 — Storage-Layer) + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:executing-plans. + +**Goal:** OAuth-Token-Speicher in SQLite + verschluesseltes Persistieren via Electron `safeStorage`. Keine eigentliche OAuth-Flow-Implementierung in Plan 03 — die kommt in einem Folgeplan (Plan 03b), nachdem die Flow-Entscheidung (Authorization Code mit PKCE via System-Browser + localhost-Redirect, oder Implicit via embedded BrowserWindow) bestaetigt ist. + +**Architecture:** Schema-Extension via additives `CREATE TABLE IF NOT EXISTS`. Crypto-Layer abstrahiert hinter `SecureStorage` Interface — Electron-safeStorage in Production, In-Memory in Tests. Token-CRUD ueber `token-store.ts` modul. Twitch ist der erste/einzige Provider in Plan 03 — Interface ist provider-agnostisch fuer spaeter. + +**Tech Stack:** Electron `safeStorage` (Win Credential Manager im Hintergrund), better-sqlite3, vitest. + +--- + +## Aenderung vs Goal.md + +Goal sagt "Device Code Flow". Twitch unterstuetzt das nicht — nur Authorization Code + PKCE, Implicit, oder Client Credentials. Plan 03 macht den Storage-Layer flow-agnostisch fertig. Konkrete Flow-Wahl + Implementation = Plan 03b (separat). + +--- + +## Out of Scope fuer Plan 03 + +- Eigentliche OAuth-Flow-Implementation (Browser-Window oder System-Browser + Loopback) +- Twitch Helix-Endpunkte mit Token (zB `/users/me`) — kommt mit Flow +- IPC-Handler `login/logout/whoami` — kommt mit Flow +- Renderer-UI fuer Multi-Account — kommt mit UI-Plan +- Sub-only-Stream-Aufnahme — kommt mit Live-Rec-Plan + +--- + +## File Structure + +**Neu:** +- `src/main/infra/secure-storage.ts` — `SecureStorage` interface + Electron-Impl + MemoryImpl +- `src/main/infra/secure-storage.test.ts` +- `src/main/domain/token-store.ts` — CRUD auf oauth_accounts +- `src/main/domain/token-store.test.ts` + +**Modifiziert:** +- `src/main/infra/schema-v5.ts` — neue Tabelle `oauth_accounts` +- `CLAUDE.md` +- `tasks/v5.0.0-roadmap.md` + +--- + +## Tasks + +### Task 1: Schema-Extension oauth_accounts + +In `schema-v5.ts` an das Ende der Tabellen-Deklarationen (vor `migrations_applied`) anhaengen: + +```sql +CREATE TABLE IF NOT EXISTS oauth_accounts ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + provider TEXT NOT NULL, + twitch_user_id TEXT, + login TEXT, + display_name TEXT, + encrypted_access_token TEXT NOT NULL, + encrypted_refresh_token TEXT, + expires_at INTEGER, + scopes_json TEXT, + is_default INTEGER NOT NULL DEFAULT 0, + created_at INTEGER NOT NULL DEFAULT (strftime('%s','now')), + updated_at INTEGER NOT NULL DEFAULT (strftime('%s','now')), + UNIQUE(provider, twitch_user_id) +); + +CREATE INDEX IF NOT EXISTS idx_oauth_provider ON oauth_accounts(provider); +CREATE INDEX IF NOT EXISTS idx_oauth_default ON oauth_accounts(is_default); +``` + +- [ ] Schema-Konstante erweitern +- [ ] db.test.ts: 1 Test adden — `oauth_accounts` Tabelle existiert nach openDatabase +- [ ] `npm run test:unit` gruen +- [ ] Commit: `feat(db): add oauth_accounts table to schema v5` + +--- + +### Task 2: secure-storage Modul + 4 Tests + +**Public Interface:** +```typescript +export interface SecureStorage { + isEncryptionAvailable(): boolean; + encrypt(plaintext: string): string; // base64-encoded ciphertext + decrypt(ciphertext: string): string; +} + +export class MemorySecureStorage implements SecureStorage { ... } // for tests +export function createElectronSecureStorage(): SecureStorage; // requires app.whenReady fired +``` + +**MemoryImpl:** verschluesselt nicht, gibt plaintext unveraendert zurueck. Liefert `isEncryptionAvailable() === false`. Markiert klar im Logger, dass kein Crypto greift — fuer Tests OK. + +**ElectronImpl:** wrappt `electron.safeStorage`. `isEncryptionAvailable()` ruft `safeStorage.isEncryptionAvailable()`. `encrypt()` ruft `safeStorage.encryptString(plaintext)` → Buffer → base64-string. `decrypt()` inverse. + +**Tests** (`secure-storage.test.ts`): +- MemoryImpl: `isEncryptionAvailable()` returns false +- MemoryImpl: `decrypt(encrypt('hello')) === 'hello'` +- MemoryImpl: roundtrip mit multi-byte (`'aeoeue-test'`) +- MemoryImpl: empty string roundtrip +- ElectronImpl: skip in vitest-env (Electron nicht verfuegbar), aber Existence-Check via `typeof createElectronSecureStorage === 'function'` + +- [ ] Tests + Modul schreiben +- [ ] `npm run test:unit` gruen (+5 tests) +- [ ] Commit: `feat(auth): SecureStorage interface + Memory + Electron impls (4 tests)` + +--- + +### Task 3: token-store Modul + 6 Tests + +**Public Surface:** +```typescript +export interface TokenRecord { + id: number; + provider: string; + twitchUserId: string | null; + login: string | null; + displayName: string | null; + expiresAt: number | null; + scopes: string[]; + isDefault: boolean; + createdAt: number; + updatedAt: number; +} + +export interface TokenWriteInput { + provider: string; + twitchUserId?: string; + login?: string; + displayName?: string; + accessToken: string; // plaintext, wird verschluesselt geschrieben + refreshToken?: string; + expiresAt?: number; + scopes?: string[]; + isDefault?: boolean; +} + +export interface TokenStore { + upsert(input: TokenWriteInput): TokenRecord; + list(provider?: string): TokenRecord[]; + getDefault(provider: string): TokenRecord | null; + setDefault(id: number): void; + getAccessToken(id: number): string; // entschluesselt + getRefreshToken(id: number): string | null; + delete(id: number): void; +} + +export function createTokenStore(db: DbHandle, storage: SecureStorage): TokenStore; +``` + +**Tests (mit MemorySecureStorage):** +1. `upsert` neuer Account → record returned mit id > 0 +2. `upsert` same provider + twitch_user_id → updated, kein neuer record +3. `list()` liefert alle accounts +4. `list('twitch')` filtert nach provider +5. `getDefault('twitch')` liefert isDefault=1, oder null +6. `setDefault(id)` setzt is_default=1 fuer id, 0 fuer alle anderen mit gleichem provider +7. `getAccessToken` entschluesselt korrekt +8. `delete` entfernt record + +(Mind. 6 Tests — gerne mehr.) + +- [ ] Tests + Modul schreiben +- [ ] `npm run test:unit` gruen (+6 tests) +- [ ] Commit: `feat(auth): token-store CRUD on oauth_accounts (encrypted, 6 tests)` + +--- + +### Task 4: Full verification + Version Bump 5.0.0-alpha.2 + +- [ ] `npm run test:e2e:release` Exit 0 (unit jetzt >= 117) +- [ ] `npm version 5.0.0-alpha.2 --no-git-tag-version` +- [ ] `npm run build` Exit 0 +- [ ] Commit: `release: 5.0.0-alpha.2 - OAuth foundation (storage layer)` +- [ ] Tag: `git tag v5.0.0-alpha.2` + +--- + +### Task 5: Docs + +CLAUDE.md unter Key Patterns adden: +> **Secrets**: OAuth-Token (Twitch + spaeter) landen verschluesselt via Electron `safeStorage` (Win Credential Manager) in `oauth_accounts` Tabelle der SQLite. Zugriff ueber `src/main/domain/token-store.ts`. Memory-Impl fuer Tests. + +Roadmap: Plan 03 → DONE, Plan 03b "OAuth Flow Implementation" als neuer NEXT-Eintrag dazu. + +- [ ] Updates + Commit: `docs: OAuth foundation pattern + roadmap` + +--- + +## Done-Definition Plan 03 + +1. `oauth_accounts` Tabelle in Schema +2. SecureStorage Interface + MemoryImpl + ElectronImpl +3. TokenStore CRUD getestet +4. `npm run test:e2e:release` Exit 0 +5. >= 11 neue unit-tests (1 schema + 4 secure-storage + 6 token-store) +6. Version 5.0.0-alpha.2 getaggt +7. CLAUDE.md + Roadmap aktualisiert + +--- + +## Execution Handoff + +Inline-Execution. Plan 03b (OAuth Flow Implementation) wird danach geschrieben.