Twitch-VOD-Manager/tasks/v5.0.0-plan-03-oauth-foundation.md
2026-05-11 22:10:32 +02:00

7.3 KiB

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.tsSecureStorage 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:

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:

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:

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.