# CF Worker OAuth / API Key Auth Flow for Android TV

## Overview

When an Android TV app needs debrid service authentication (Real-Debrid, Premiumize, TorBox, AllDebrid, etc.), the user cannot type API keys or OAuth codes on a TV remote. The solution is a **Cloudflare Worker with Durable Objects** that mediates a phone-based QR flow.

## Architecture

```
Android TV               CF Worker + DO              Phone Browser
    │                          │                          │
    │  POST /api/debrid/auth-sessions                     │
    │  { providerId: "realdebrid" }                       │
    │─────────────────────────►│                          │
    │◄─── { code, authUrl } ──│                          │
    │                          │                          │
    │ Show QR(code)            │                          │
    │ (user scans with phone)  │─────────────────────────►│
    │                          │  GET /debrid/auth/{code}  │
    │                          │◄─────────────────────────│
    │                          │  Serve HTML form          │
    │                          │─────────────────────────►│
    │                          │  POST apiKey / OAuth      │
    │                          │◄─────────────────────────│
    │                          │                          │
    │  GET /api/debrid/auth-sessions/{code}               │
    │  (poll every 3s)         │                          │
    │─────────────────────────►│                          │
    │◄── { configuredApiKey }──│                          │
    │                          │                          │
    │ Use API key/Token → connect debrid service          │
```

## Key: DurableObject state flow

The `AuthSessionDO` stores session state in Durable Object storage under key `"auth"`:

```typescript
// Normal key-form auth (TorBox, AllDebrid, Debrider, Offcloud)
{
  code: "abc123",
  service: "torbox",
  authType: "apikey",
  status: "pending",        // pending → completed (or cancelled/expired)
  apiKey: "user_api_key",   // ← STORED when user fills form
  token: "user_api_key",    // ← ALSO stored for consistency
  createdAt: 1700000000000,
  expiresAt: 1700000000000,
}

// OAuth auth (Real-Debrid, Premiumize)
{
  code: "abc123",
  service: "realdebrid",
  authType: "oauth",
  status: "active",         // active → completed (or cancelled/expired)
  deviceCode: "device_code_from_provider",
  userCode: "UA-1234",
  verificationUrl: "https://real-debrid.com/device",
  interval: 5,
  token: "oauth_access_token", // ← SET by handleOAuthPoll() when user approves
  pollCount: 0,
  createdAt: 1700000000000,
  expiresAt: 1700000000000,
}
```

## CRITICAL: The `configuredApiKey` gap

The worker's `handleAuthStatus()` is the endpoint the Android app polls (every 3s). It MUST return the API key/token when status is `"completed"`. The DO stores it as `apiKey` (key auth) or `token` (OAuth), but the Android client expects field name `configuredApiKey`.

**WRONG — returns status but no API key:**
```typescript
return json({
  code: s.code, service: s.service, authType: s.authType, status: s.status, userCode: s.userCode
  // ← missing configuredApiKey — polling loops forever
});
```

**RIGHT — includes the API key on completed sessions:**
```typescript
return json({
  code: s.code, service: s.service, authType: s.authType, status: s.status,
  userCode: s.userCode,
  configuredApiKey: s.apiKey || s.token || null,  // ← CARRY THIS THROUGH
  error: s.error || null,
});
```

This fix must be applied to:
- `handleAuthStatus()` — the main polling endpoint (GET /api/debrid/auth-sessions/{code})
- `handleAuthSubmit()` — the form submission response (POST /debrid/auth/submit/{code})
- `handleOAuthPoll()` — the OAuth poll inside the DO (GET /debrid/auth/poll inside the DO)

## Provider ID normalization

The Android app sends `provider.name.lowercase()` where `provider` is a `DebridProvider` enum. The enum names contain underscores (`REAL_DEBRID`, `ALL_DEBRID`, `TOR_BOX`) which the worker does NOT expect.

**Fix on Android side (in the ViewModel):**
```kotlin
val result = debridAuthApiClient.createSession(
    provider.name.lowercase().replace("_", "")
    // "real_debrid" → "realdebrid", "tor_box" → "torbox"
)
```

**Fix on Worker side (in handleApiAuthCreate) — makes it tolerant of both formats:**
```typescript
const providerId = (body.providerId || "").replace(/_/g, "").toLowerCase();
// "real_debrid" → "realdebrid", "Real_Debrid" → "realdebrid"
```

## The 6-route verification suite

After deploying auth-related CF Worker changes, always verify all 6 routes:

```bash
WORKER="https://your-worker-domain.tw24fr.workers.dev"

# 1. Auth create (OAuth)
curl -s -w "\nHTTP %{http_code}\n" "$WORKER/api/debrid/auth-sessions" \
  -X POST -H "Content-Type: application/json" -d '{"providerId":"realdebrid"}'

# 2. Auth create (API key form)
curl -s -w "\nHTTP %{http_code}\n" "$WORKER/api/debrid/auth-sessions" \
  -X POST -H "Content-Type: application/json" -d '{"providerId":"torbox"}'

# 3. Auth poll
SESSION=$(curl -s "$WORKER/api/debrid/auth-sessions" \
  -X POST -H "Content-Type: application/json" -d '{"providerId":"torbox"}')
CODE=$(echo "$SESSION" | python3 -c "import sys,json;print(json.load(sys.stdin)['code'])")
curl -s "$WORKER/api/debrid/auth-sessions/$CODE"

# 4. Auth cancel
curl -s -X POST "$WORKER/api/debrid/auth-sessions/$CODE/cancel"

# 5. Addon URL session
curl -s -w "\nHTTP %{http_code}\n" "$WORKER/api/addons/url-sessions" \
  -X POST -H "Content-Type: application/json" -d '{}'

# 6. Config session
curl -s -w "\nHTTP %{http_code}\n" "$WORKER/api/addons/config-sessions" \
  -X POST -H "Content-Type: application/json" \
  -d '{"addonId":"test","addonName":"Test","baseManifestUrl":"https://test.com/manifest.json"}'
```

## Merging auth + config routes on one worker

When auth and config routes live on separate Workers but the Android app points to one URL:

1. Add the second DO namespace to `wrangler.toml`:
   ```toml
   [[durable_objects.bindings]]
   name = "AUTH_SESSIONS"
   class_name = "AuthSessionDO"

   [[migrations]]
   tag = "v2"
   new_sqlite_classes = ["AuthSessionDO"]
   ```

2. Export both DO classes from `index.ts`:
   ```typescript
   export { ConfigSessionDO, AuthSessionDO };
   ```

3. Add both namespaces to the `Env` interface:
   ```typescript
   export interface Env {
     ADDON_CONFIG_SESSIONS: DurableObjectNamespace;
     AUTH_SESSIONS: DurableObjectNamespace;
     PUBLIC_HOST: string;
     APP_NAME?: string;
   }
   ```

4. Copy all route handlers before the final 404 return

5. Ensure `PUBLIC_HOST` is set to the worker's own URL in `wrangler.toml` — form URLs generated by the worker use this host

## `DebridAuthSessionResponse` data class

```kotlin
@Serializable
data class DebridAuthSessionResponse(
    val code: String,
    val status: String = "waiting",
    val providerId: String? = null,      // Worker returns "service", not "providerId"
    val configuredApiKey: String? = null, // ← Worker MUST return this on completion
    val error: String? = null,
)
```

Note: `ignoreUnknownKeys = true` must be set on the `Json` instance since the Worker returns extra fields (`service`, `authType`, `userCode`, etc.) that aren't in the DTO.
