# Cloudflare Worker addon registry for Android TV apps

Use this reference when adding a server-driven addon registry to an Android streaming app, so addon metadata (URLs, descriptions, health, deprecation status) can be updated remotely without pushing a new APK.

## Worker shape

```text
worker/
  src/index.ts              # Cloudflare adapter: 5 routes, CORS
  src/core/models.ts        # AddonRegistryEntry, AddonPreset, health types
  src/core/registry.ts      # DEFAULT_ADDONS + DEFAULT_PRESETS (hardcoded fallback)
  src/core/manifest.ts      # fetchManifest(), applyManifestToEntry(), healthCheckAddon()
  wrangler.toml
  package.json
  tsconfig.json
```

## Endpoints

```
GET /v1/addons/registry   → { addons: [...], presets: [...], updatedAt }
GET /v1/addons/{id}/manifest → live or cached manifest.json
POST /v1/addons/{id}/health-check → tests manifest + stream endpoint, returns timing
GET /v1/addons/presets     → { presets: [...] }
GET /v1/addons/status      → { addons: [{id, name, health, lastChecked}] }
GET /v1/health             → { status: "ok", addonCount: N }
```

## Data model (TypeScript → Kotlin mapping)

| Worker (TS) | Android (Kotlin) |
|---|---|
| `AddonRegistryEntry` | `AddonRegistryEntry` (serializable data class) |
| `AddonPreset` | `AddonPreset` |
| `AddonHealthStatus` | `AddonHealthStatus` (enum) |
| `AddonHealthCheckResult` | `AddonHealthCheckResult` |
| `AddonStatusResponse` | `AddonStatusResponse` |

## Android sync pattern

1. `AddonRegistryRepository` fetches from `{baseUrl}/v1/addons/registry`
2. Merges local user enable/disable preferences (stored in SharedPreferences)
3. Caches locally for 1 hour (TTL)
4. Fallback: cached data, then hardcoded defaults
5. Key rule: **NEVER overwrite user enable/disable choices** — the worker is the source of truth for metadata, but the user is the source of truth for which addons are active.

## Health check flow

```kotlin
// Android calls worker health check (avoids direct addon calls from device)
repository.checkAddonHealth("torrentio") → AddonHealthCheckResult

// Worker tests:
// 1. HTTP GET {manifestUrl} → response time + status
// 2. If stream resource: GET {installUrl}/stream/movie/tt0111161.json → stream count
// 3. Returns: status, manifestMs, streamMs, streamCount, error
```

## Worker deployment

```bash
cd worker
npm install
npx wrangler deploy
```

## CORS pitfall

The worker must return `Access-Control-Allow-Origin: *` on every response (including error responses). The Android app makes cross-origin requests that fail without CORS headers.

## Never-log rules

- Worker logs must never output request query strings, headers, or full URLs
- Addon manifest URLs may contain tokens — sanitize before logging
- Health check error messages: include only HTTP status codes, not response bodies

## Verification gates

## Bundled fallback when worker is offline

When the worker URL is blank (not deployed, no `ADDON_REGISTRY_BASE_URL` in BuildConfig), the `AddonRegistryRepository` must NOT fail — it must return a hardcoded fallback registry with real addon data. User enable/disable preferences are applied on top of these defaults.

**Pattern — `buildFallbackRegistry()` in AddonRegistryRepository:**

```kotlin
suspend fun syncRegistry(): Result<AddonRegistryResponse> {
    if (baseUrl.isBlank()) {
        Log.d(TAG, "No worker URL configured — using fallback registry")
        return Result.success(buildFallbackRegistry())
    }
    // ... normal worker fetch
}

private fun buildFallbackRegistry(): AddonRegistryResponse {
    val enabledMap = getEnabledMap()  // user's local enable/disable choices

    return AddonRegistryResponse(
        addons = listOf(
            // System addons (always included):
            AddonRegistryEntry(
                id = "cinemeta", name = "Cinemeta",
                manifestUrl = "https://v3-cinemeta.strem.io/manifest.json",
                health = "online", enabledByDefault = true,
                category = "system",
                enabled = enabledMap["cinemeta"] ?: true,
            ),
            // Stream addons (real manifest URLs from live audit):
            AddonRegistryEntry(
                id = "torrentio", name = "Torrentio",
                manifestUrl = "https://torrentio.strem.fun/manifest.json",
                health = "online", enabledByDefault = true,
                category = "streams",
                enabled = enabledMap["torrentio"] ?: true,
            ),
            // ... more addons
        ),
        presets = listOf(
            AddonPreset("balanced", "Balanced", "Broad coverage", listOf("aiostreams", "comet", "mediafusion")),
            // ... more presets
        ),
        updatedAt = java.time.Instant.now().toString(),
    )
}
```

**Rules for the fallback:**
- Use **real, audited manifest URLs** (verify with curl before committing)
- Apply user's local enable/disable preferences via `getEnabledMap()`
- Never include deprecated or dead addons in the fallback
- Category field (`"system"` vs `"streams"`) enables sectioned UI display
- Preset addon ID lists must match the fallback addon IDs exactly
- The fallback is the FIRST source tried on cold install; it runs on every device until a worker URL is configured

**Worker → fallback transition is transparent:**
When a worker URL is later configured, `syncRegistry()` switches to the live endpoint automatically. User preferences persist in SharedPreferences across both modes — the `enabledMap` dictionary is source-agnostic.

```bash
# Worker\ncd worker && npx tsc --noEmit\n\n# Live smoke test\ncurl -fsS \"https://addon-registry.example.com/v1/health\"\ncurl -fsS \"https://addon-registry.example.com/v1/addons/registry\" | jq '.addons | length'\n\n# Android\n./gradlew :app:compileDebugKotlin :app:testDebugUnitTest :app:assembleDebug --console=plain\n```
