The current contract for the DNS-Manager control plane (Cloudflare Worker dns-manager-api + D1 + KV HEALTH_KV + R2). Two surfaces, one database:
| Surface | Host | Auth | Used by |
|---|---|---|---|
| Public device API | dnsm.cbkr.xyz | WAF; provisioning/messages are HMAC-signed | the IPTV apps / TV boxes |
| Admin API + GUI | m7k2.cbkr.xyz | Cloudflare Access + JWT (RBAC) | operators only |
open = unauthenticated (WAF-gated),
HMAC = signed device request, JWT = operator bearer token,
ingest = scoped capture token (bookmarklet / webhook β neither JWT nor HMAC).
Endpoints marked (PR #NNN) are in an open pull request, not yet on main.
Retired: the legacy PHP panel (dns_list.php, dns_active.php) is gone β the Worker + D1 below is canonical.
One Cloudflare Worker (dns-manager-api) backs both surfaces; a Cloudflare Pages site (public/) serves the admin GUI. Everything runs on the free tier (~10 ms CPU/request, no native bcrypt, WAF Block/Skip/Challenge only).
| Layer | What |
|---|---|
Worker dns-manager-api | all request handling β public app surface, HMAC device endpoints, JWT admin API, Cron (portal health + expired-message sweep). |
D1 dns-manager-prod | system of record: portals, devices, playlists, messages, audit. SQLite β schema applied via database/*.sql migrations (tracked in the schema_migrations ledger; see /api/admin/migration-status). |
| KV | HEALTH_KV β cached portal-health probe (the only KV namespace; ~48 cron writes/day). Bundle F retired CONNECT_KV: Connect codes, abuse-jail, and the audit dead-letter queue moved to D1 (connect_codes/abuse_jail/audit_dlq); the replay-nonce, message poll-cache, and rate-limit counters moved to the Cache API. |
R2 dnsm-message-assets | banner/poster art, served publicly at /api/assets/<key>. |
| Cloudflare Access | Zero-Trust email login fronting m7k2.cbkr.xyz (the boxes can't do Access, so the public surface is WAF-only). |
| WAF | allow-lists the app paths on dnsm and edge-blocks /api/admin/* + /api/dns/* (returns 403 before the Worker runs). |
methodΒ·pathΒ·queryΒ·sha256(body)Β·tsΒ·nonceΒ·deviceIdΒ·keyId); Β±300 s timestamp window; single-use nonce (replay cache on the Cache API, per-PoP, fail-open); per-IP + per-device rate limits.token_version bumped on password change); RBAC admin/operator.device_id:key_id); JWT_SECRET/PROVISION_KEK/DNX_HMAC_KEYS are Cloudflare secrets.Pre-shipped apps re-pointed to https://dnsm.cbkr.xyz/api/. Do not rename these β the APKs append the path verbatim (shapes recovered in CONTRACTS.md). All open; no-store so GUI changes take effect on the next poll.
The Smarters resolver. The app posts a form body (m=gu, u=playlist, d, k, sc, pw, r, av, dt, do); the su field is load-bearing. Per-device routing is via the ?d= / ?device= query param (the worker routes by query, not the body field).
Helix bootstrap (home.php?action=appinfo is Helix's real config URL; action=update|getvpn are re-pointed apkloc/vpn resources that return {}). Each portal becomes domain1β¦domainN with all 12 Helix keys (one missing key makes Helix discard the whole response). ?d=/?device=<deviceId> serves that device's playlists.
Splash video β returns video/mp4 bytes (or a 302 to INTRO_VIDEO_URL); never JSON, never 404 (the app loads it in a VideoView).
Public R2 passthrough for message banner/poster art (minted by the admin asset upload). Key is pinned to the minted shape (no traversal).
Every request is HMAC-signed. Headers: X-DNX-Sign (HMAC-SHA256 of the canonical string), X-DNX-Ts (unix s, Β±300 s), X-DNX-Nonce (single-use), X-DNX-KeyId, X-DNX-DeviceId. The signed deviceId is authoritative. Per-IP + per-device rate limits.
HMAC-signed like provisioning. The box polls for messages (banner/poster/notification/ad) targeted at it (or broadcast) and acknowledges them. ad renders in the dashboard sidebar slot (1:9 art); it requires image_url, forces requires_ack off, and uses priority to pick the winning ad.
A box reporting configHash on poll drives the config-drift badge in the panel.
A keyboard-less box shows a PIN+QR; the user types creds on a phone at /connect; the box pulls them. The claimSecret (box-only) is required to claim β a leaked on-screen code alone can't retrieve creds. Codes live in D1 (connect_codes; single-use, in-band expiry). All open (no creds yet) but per-IP rate-limited via the Cache+D1 limiter.
HMAC-signed like provisioning. The app builds a password-encrypted JSON envelope (AES-256-GCM) locally and uploads the opaque base64 ciphertext for cross-device restore β the Worker never decrypts or inspects it. One latest backup per device (re-upload overwrites). Body deviceId must equal the signed device; payload capped at ~900 KB. Stored in D1 device_backups.
All JWT (Bearer) + Cloudflare Access. Token from /api/auth/login (HS256, 1 h TTL). RBAC: provisioning/messages require admin or operator.
| Method | Path | Purpose |
|---|---|---|
| GET | /api/dns/list | list portals (admin-only β returns inactive rows + descriptions) |
| POST | /api/dns/create | add a portal |
| PUT | /api/dns/update/:id | edit |
| POST | /api/dns/toggle/:id | activate / deactivate |
| DELETE | /api/dns/delete/:id | delete |
| GET/POST | /api/dns/health | cached / fresh portal health probe |
| GET | /api/logs | public-surface API access logs |
| Method | Path | Purpose |
|---|---|---|
| GET | /api/admin/devices?status=&q=&tag=&expiring=&provider=&limit=&offset= | paginated list β devices[], total, limit/offset, per-status counts. Filters: status, free-text q, tag, expiring=<days>|expired, provider=<id> |
| GET | /api/admin/devices/by-code/:code | find a pending device by its pairing code |
| GET | /api/admin/device-codes/:code | support lookup (#180) β resolve a box by the human-dictatable 9-char code a user reads out (Crockford, public, identifier-only) β {device_id, status, β¦}. The box binds it via the public POST /api/provision/register-code. The device list + detail also include device_code per row; the Devices Info modal shows it as a Support code (#184) |
| GET | /api/admin/devices/:id | one device |
| GET | /api/admin/devices/:id/events | per-device telemetry timeline |
| PATCH | /api/admin/devices/:id | edit identity (device_name / device_number / owner_name / notes / tags / provider_account_id) and clear/set the geo_flagged review flag (0/1) |
| POST | /api/admin/devices/:id/assign | assign source (xtream/m3u) + creds + curation profile + helix flags + optional epg_url (m3u XMLTV guide) |
| POST | /api/admin/devices/:id/clone-from | {source_device_id} β restore/clone: copy the source device's active playlists onto this one, re-encrypting Xtream creds under this device's key. Append-only. The Restore button for a box that reappeared under a new device_id (reinstall / debugβsigned) |
| POST | /api/admin/devices/:id/rotate-code | issue a fresh pairing code |
| POST | /api/admin/devices/:id/revoke | revoke |
| POST | /api/admin/devices/:id/expiry | set / extend / clear this device's subscription expiry (renewal reactivates an expired box) |
| DELETE | /api/admin/devices/:id | hard delete |
| POST | /api/admin/devices/bulk-tags | add/remove tags across many devices |
| POST | /api/admin/devices/bulk-expiry | set/extend expiry across many devices |
| GET | /api/admin/devices/export.csv | CSV export of the fleet (formula-injection neutralized) |
| POST | /api/admin/devices/import | CSV import of device identity/metadata |
| POST | /api/admin/sources/import (M3) | bulk-import m3u OR xtream sources from CSV. device_id + EITHER source_url (m3u) OR server/portal_url + username + password (xtream); optional source_type (else inferred), display_name, epg_url, priority, tags. Each row appends a playlist (a device_id may repeat); xtream creds are AES-GCM encrypted per row (needs PROVISION_KEK; xtream rows skip if unset). β {imported, skipped[]} |
| POST | /api/admin/devices/categories/preview (PR #120) | fetch + classify provider categories for the content filter |
| POST | /api/admin/source/preflight (M2) | validate + auto-detect a source URL: {url} β {detected_type, is_hls, reachable, status} (xtream/m3u, .m3u8βHLS; fail-soft reachability probe, advisory only β never assigns) |
| GET/POST | /api/admin/devices/:id/playlists | list / add a playlist |
| PUT/DELETE | /api/admin/devices/:id/playlists/:pid | edit / remove |
| PATCH | /api/admin/devices/:id/playlists/:pid/priority | reorder (priority only) |
Ring a config change (helix render-flags or an m3u source) to a pilot tag cohort, watch adoption via the existing config-drift (configHash) signal, then promote to the full cohort or roll back to a per-device snapshot. The ring only controls which devices get a change and when β never the device-facing response shape.
| Method | Path | Purpose |
|---|---|---|
| GET/POST | /api/admin/rollouts | list / create a draft β {description?, change_spec:{op:"set_flags"|"set_source_m3u", β¦}, target:{tag}, pilot:{tag}} β previews {id, ring_state:"draft", counts:{target,pilot}} (applies nothing) |
| GET | /api/admin/rollouts/:id | status + per-ring adoption metrics (synced / drifted / awaiting) |
| POST | /api/admin/rollouts/:id/apply-pilot | snapshot + apply the change to the pilot ring only (draftβpilot) |
| POST | /api/admin/rollouts/:id/promote | snapshot + apply to the rest of the target cohort (pilotβpromoted) |
| POST | /api/admin/rollouts/:id/rollback | restore every applied device's pre-change snapshot (βrolled_back) |
State transitions are compare-and-swap guarded (a double-submit returns 409). Cohorts are capped at 500 and applied in chunked D1 batches. Encrypted creds travel verbatim in the snapshot, so rollback never needs the KEK.
| POST | /api/admin/failover/run | run the auto-failover sweep now β {reverted, failed_over} (the same sweep a */30 Cron runs). When a device reports its primary source failed (device_playlists.status='error', the box's own report β never the datacenter probe), it's failed over to its next device-reported-healthy priority source, recorded as an auto-failover rollout (so it's auditable + rollback-able on the rail above), and auto-reverted once the original recovers. Anti-flap cooldown FAILOVER_COOLDOWN_S (default 3600s). |
Register a reseller panel, then auto-provision a device: the Worker calls the panel's admin API to create a line and assigns the resulting Xtream creds to the device. The panel API key is AES-GCM encrypted at rest (never returned/logged). XUI.one plain-api_key mode.
| Method | Path | Purpose |
|---|---|---|
| GET/POST | /api/admin/resellers | list / create β {label, type:"xui", admin_base (β¦:9000), access_code, api_key, stream_base}; key encrypted on save, returned only as has_key |
| PATCH/DELETE | /api/admin/resellers/:id | edit / delete (changing access_code requires re-supplying api_key β it re-anchors the key's AAD) |
| POST | /api/admin/resellers/:id/test | connectivity check β decrypts the key + calls get_packages β {ok, packages} (fail-soft, never 500s) |
| POST | /api/admin/devices/:id/provision-from-reseller | {reseller_id, package?, duration?, max_connections?, bouquets?} β calls create_line β assigns the new Xtream line to the device (creds encrypted). Returns {server, username} β never the password. Needs the panel's :9000 cert valid (Workers reject self-signed) + a stream_base for building the client URL. |
For reseller panels with no API. A scoped ingest token lets an operator bookmarklet (or a reseller webhook) post a captured line into a staging Inbox; the Worker auto-detects m3u/Xtream, encrypts creds, and dedups. The operator then assigns each inbox item to a device. Generate the token + bookmarklet/webhook URL on the Inbox page.
| Method | Path | Auth | Purpose |
|---|---|---|---|
| POST | /api/ingest/source | ingest | public + CORS β the bookmarklet posts {raw|type,server,username,password|url, label} (Bearer ingest token, scope ingest). Auto-detect + encrypt + dedup β Inbox |
| POST | /api/ingest/webhook/:secret | ingest | public β a reseller "line created" callback; secret in an X-Webhook-Secret header (preferred β kept out of logs) or the path (scope webhook). JSON or form body |
| GET | /api/admin/inbox | JWT | list pending captures (creds shown only as has_creds) |
| POST | /api/admin/inbox/:id/assign | JWT | {device_id} β append the captured source to that device (creds re-encrypted under its key) |
| POST | /api/admin/inbox/:id/discard | JWT | drop a captured line |
| GET/POST | /api/admin/ingest-tokens | JWT | list / mint a scoped token {label, scope:"ingest"|"webhook"} β the plaintext is shown once |
| DELETE | /api/admin/ingest-tokens/:id | JWT | revoke a token (its bookmarklet/webhook stops working) |
| Method | Path | Purpose |
|---|---|---|
| POST | /api/admin/messages | create β {target_device_id?, type, title, body, image_url, priority, requires_ack, action, expires_at} (no target = broadcast; image must be https; type:"ad" requires image_url, forces requires_ack off, priority Β±1000) |
| GET | /api/admin/messages?device=&all=1&limit= | list with ack counts |
| POST | /api/admin/messages/asset | upload banner/poster/ad art to R2 β public /api/assets/<key> URL (also surfaced as a click-to-copy chip in the composer) |
| GET | /api/admin/messages/assets | list uploaded R2 art β {assets:[{key,url,size,uploaded}], truncated}, newest-first (the Messages page βAsset libraryβ) |
| DELETE | /api/admin/messages/:id | soft-delete (active=0; preserves ack history) |
| Method | Path | Purpose |
|---|---|---|
| GET | /api/admin/providers | list provider accounts + live device counts (total / currently-serving, for oversell) |
| POST | /api/admin/providers | create a provider account |
| PATCH | /api/admin/providers/:id | edit a provider account |
| DELETE | /api/admin/providers/:id | delete a provider account |
| GET | /api/admin/ledger | list ledger entries |
| POST | /api/admin/ledger | record a ledger entry |
| DELETE | /api/admin/ledger/:id | delete a ledger entry |
| Method | Path | Purpose |
|---|---|---|
| GET | /api/admin/dashboard | fleet summary counts for the Dashboard |
| GET | /api/admin/triage | triage cards β expiring-soon / expired / offline-active / oversold / geo-flagged |
| GET | /api/admin/jail | currently abuse-jailed IPs (D1) |
| DELETE | /api/admin/jail/:ip | release a jailed IP (clears jail + fail counter) |
| GET | /api/admin/connect/active | live Connect codes (masked) |
| POST | /api/admin/connect/:code/revoke | revoke a Connect code |
| GET | /api/admin/provision/logs?device=&event=&limit= | provisioning audit log |
| GET | /api/admin/migration-status | applied-migrations ledger + schema-drift checks |
| GET | /api/admin/audit-dlq?limit= | failed-audit dead-letter queue (D1) |
| GET | /api/admin/backups?device_id=&limit= | device backups β metadata only (size, versions, timestamps); never the encrypted payload |
The panel pages map to these flows. Sign in via Cloudflare Access at m7k2.cbkr.xyz (first login forces a password change; the panel calls /api/auth/login for the JWT).
/connect on a phone and types the playlist details; the box pulls them. The operator watches Live Connect codes.Why it's needed: a box's device_id is generated and stored by the app, so a reinstall β or switching between a debug and a signed build β makes it reappear as a brand-new device with no config. Restore copies the old device's playlists onto the new one so you don't re-enter anything.
POST /api/admin/devices/:id/clone-from {source_device_id} reads the source's active playlists and decrypts each Xtream credential under the source's key, re-encrypting it under the new device's key (creds never leave the Worker in the clear), then appends them β non-destructive, existing config preserved. The new device is marked assigned.On the Inbox page, generate an ingest token (shown once) and copy the bookmarklet into your browser. On a reseller panel's "line created" page, select the m3u URL / Xtream creds and click the bookmarklet β the line lands in the Inbox (creds encrypted, m3u/Xtream auto-detected, deduped). Or generate a Webhook URL and paste it into a panel that can POST callbacks. Then Assign each inbox item to a device (creds re-encrypted for it), or Discard it.
In the assign form, choose Default β English regions and Preview categories: the panel fetches the provider's live categories and classifies each keep/hide (US/CA/Caribbean/UK/AU/NZ + 24/7-English + Canada-French kept). Flip any row; tick Lock to make hides PIN-protected. The result is pushed as the device's CurationProfile β no app change.
Messages β compose a banner/poster/notification/ad (Sidebar), broadcast or target one device, optionally require acknowledgement, set an expiry, and upload art to R2. The live TV preview shows how it renders. Sent messages show ack counts; deactivate to stop delivery (ack history kept).
Dashboard β CRUD the DNS entries (portals), set active/priority. Check Health probes each portal from the edge (best-effort badge; a Cron re-probes active portals every 30 min). Portals double as the assign-form presets.
For a risky config change (a new provider URL, a render-flag flip) across a segment: tag a small pilot cohort, create a rollout targeting the segment tag with that pilot tag, apply-pilot, then watch the rollout's adoption metrics (synced / drifted / awaiting) as the pilot boxes re-poll. If it's healthy, promote to the rest; if not, roll back β every applied device is restored from its pre-change snapshot. Lowers the blast radius of a bad edit from the whole segment to the pilot. Drive it from the Rollouts page.
A box reports per-portal outcome after it tries each source. When it reports its primary source failed and has a healthy lower-priority source, a */30 Cron (or POST /api/admin/failover/run) automatically promotes the healthy source β recorded as an auto-failover rollout you can see and roll back β and auto-reverts when the original recovers. The trigger is the box's own report, not the edge health probe (which has datacenter-IP false-negatives and stays an operator badge signal only). An anti-flap cooldown stops a flapping portal from bouncing a device.
last_seen) and a config-drift badge (β synced / β stale) once a box reports its configHash.migration-status health snapshot, the audit dead-letter queue, and the public-surface API access Logs page.admin and operator both reach the provisioning/messages admin API; the distinction is reserved for future least-privilege splits. All admin mutations write a provision_audit row (actor + IP/geo).
| Table | Holds |
|---|---|
users | operator accounts β PBKDF2 hash, role, token_version, must_change_password. |
dns_entries | portals/servers β title, dns_url (host), port, protocol, active, priority, description. |
devices | provisioned boxes β pair_code, status, AES-GCM creds, source_type/url/portal, profile_json (curation), display_name + device_name/number/owner/notes, tags_json, provider_account_id, geo_flagged/geo_flagged_at (country-change review flag), last_seen + edge telemetry (ip/country/ISP), config_hash/reported_config_hash (drift), expires_at. |
device_playlists | multi-playlist per device β source/creds/profile/helix-flags, priority, is_active, load/activation counts, device-reported status (active/error β drives auto-failover). |
rollouts Β· rollout_targets | canary / staged-rollout engine (#160) β a staged change + its cohorts and lifecycle state (draftβpilotβpromotedβrolled_back), and one row per applied device carrying the pre-change snapshot (prev_config_json) for rollback. Auto-failover (#161) records its actions here as auto-failover rollouts (reverted_at tracks the anti-flap cooldown). |
reseller_panels | registered reseller panels for auto-provision (XUI.one) β label, admin API base + access code, AES-GCM-encrypted API key, client stream base. Never returns the key (only has_key). |
inbound_sources Β· ingest_tokens | no-API ingest β captured reseller lines staged in the Inbox (type/portal/source, AES-GCM-encrypted xtream creds, via, dedup_hash, status pendingβassigned/discarded); and the scoped capture tokens (SHA-256-hashed, scope ingest|webhook). A partial unique index dedups only among pending captures. |
messages Β· message_acks | push messages (type/title/body/image/priority/requires_ack/target/expiry/active) + per-device acks. |
provider_accounts | wholesale lines a device draws from β label + metadata; LEFT-JOIN device counts surface oversell. |
ledger_entries | per-device cost ledger entries (renewal/subscription bookkeeping). |
rl_jailed | durable rate-limit jail rows (Bundle E) β written only on a limit breach; cron-swept on expiry. |
connect_codes | 3DNX Connect activation codes (Bundle F3, was KV) β single-use, in-band expiry, atomic claim; cron-swept. |
abuse_jail | abuse-jailed IPs (Bundle F4, was KV) β write-once-on-jail, in-band expiry, fail-open; cron-swept. |
audit_dlq | failed-audit dead-letter queue (Bundle F5, was KV) β durable retry sink for lost provision_audit rows; cron-swept. |
provision_audit | non-secret event log β actor, device, event, masked detail, IP/country/ISP/colo. |
api_logs | public-surface access log β device id, IP, endpoint, method, response code, UA. |
sessions | admin session rows (legacy/auxiliary to the signed JWT). |
schema_migrations | applied-migration ledger (feeds /api/admin/migration-status). |
device_backups | device-scoped encrypted backup store β opaque base64 payload + metadata, one latest per device (never decrypted server-side). |
Pushed in a device's provision/playlist config (stored as profile_json); the box applies it client-side. The panel's "Default β English regions" preset (PR #120) computes hide_categories from the provider's real category list so US/CA/Caribbean/UK/AU/NZ + 24/7-English + Canada-French survive.