205 lines
7.3 KiB
Markdown
205 lines
7.3 KiB
Markdown
# 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.
|