# Cloudflare connection and publisher setup Flarebot has two independent Workers. `control-plane/index.ts` serves public onboarding, Cloudflare OAuth, account selection and protected authorization storage. `worker/index.ts` is the customer runtime. Its deployment release never includes the control plane, OAuth secrets or session/grant storage. The public Octane/Kumo screen does not connect to customer Agent transports or load private conversations, memory, model keys or agent content. Build the customer first (`pnpm build:release`), then run `pnpm build:control-plane`. The latter emits its own client/server outputs and Wrangler dry-run artifact under `dist/control-plane/`. Use `wrangler.control-plane.jsonc` only for the publisher Worker; never install this bundle in a customer's account. Builds and packaged tests must run sequentially. A fresh customer build can replace `dist` outputs, so rebuild the control plane after it. `pnpm test:oauth` requires both artifacts and Playwright Chromium. ## Publisher prerequisites Create a **Flarebot-owned** OAuth client using Cloudflare's [client registration](https://developers.cloudflare.com/fundamentals/oauth/create-an-oauth-client/). Never reuse Wrangler's OAuth client or developer credentials. Register the exact HTTPS callback `https:///auth/callback`, Authorization Code with PKCE S256, and the actual registered token authentication method: `none`, `client_secret_basic`, or `client_secret_post`. A confidential method requires `FLAREBOT_OAUTH_CLIENT_SECRET`; `none` rejects that binding. Private clients admit parent-account members; public onboarding additionally requires publisher domain verification and public promotion. The publisher must review the authenticated [OAuth scopes catalog](https://developers.cloudflare.com/api/resources/iam/subresources/oauth_scopes/methods/list/) (`GET https://api.cloudflare.com/client/v4/oauth/scopes`, official SDK `client.iam.oauthScopes.list()`). Catalog `id` values are authorization scope IDs; underlying resource strings in `scopes` are supporting evidence, not OAuth IDs. Retain the actual catalog name/category/resource strings in the nonsecret capability manifest. Use the smallest available scopes that cover every operation below. Do not convert Wrangler colon scopes into guessed dot IDs or silently request all permissions. | Flarebot capability | Required operation and catalog review | | ----------------------- | ------------------------------------------------------------------------------------------------------------------------------------------ | | `identity` | Verified UserInfo `sub` from the registered code flow. Check the client's normalized identity scopes and real UserInfo behavior. | | `account-selection` | Enumerate the grant's accounts. `account.read` is the documented Account Read example. | | `worker-upload` | Upload the Worker, SQLite Durable Object exports and native AI/BROWSER/LOADER binding declarations. API permission: Workers Scripts Write. | | `asset-upload` | Create the static asset upload session; bytes use its upload JWT. API permission: Workers Scripts Write. | | `workers-dev` | Enable the Worker endpoint and inspect account subdomain. API permission: Workers Scripts Write. | | `container-application` | Create/read/modify the Sandbox container application. Verify the catalog mapping for Workers Containers Write / Containers Write. | | `container-rollout` | Create and observe the Sandbox application rollout. Verify the exact catalog coverage. | | `model-gateway` | Read/create the default AI Gateway. Requires AI Gateway Write (`aig.write`); no custom-provider route is needed. | `workers-platform.read` and `workers-platform.write` are publicly documented examples, but public documentation does **not** establish that the write scope is the minimal complete scope for Scripts and Containers. No live catalog/client verification or deployment was performed for this implementation. There is no production default pretending that these examples cover the whole artifact. The workflow creates `default` before Worker upload on install and upgrade. OpenRouter uses AI Gateway's native `openrouter` provider, so Flarebot does not create or manage a custom-provider route. Enabling it still uses an ownership-checked, purpose-separated signed receipt and ensures the default gateway exists. The `model-gateway` capability and `aig.write` grant are still required for gateway management, not for forwarding inference. `POST /auth/start` also accepts a single `providerInstallationId` (32 hex digits) instead of `returnTo`. The encrypted OAuth transaction preserves only that ID; callback returns to the fixed local provider setup view. No URL, account or owner is accepted from the browser. `GET /api/installations/:id/providers/openrouter` checks ownership without enabling. Its exact-Origin-protected POST rechecks ownership, gateway-management scope and account membership, ensures the default gateway, then notifies the pinned customer runtime with a purpose-separated signed receipt. It returns the registered runtime's `/settings` URL only after acknowledgment. Provider keys never pass through these endpoints. Expired grants require reconnect; provider enablement is persisted in the runtime independently of grant lifetime. No zone/DNS route changes, registry image pushes, Cloudflare inference REST calls, Browser REST probes or extra customer OAuth-client administration are needed by this path. Merely declaring native bindings does not establish a need for extra AI/browser scopes; managing the default gateway requires `aig.write`. The existing public, pinned Sandbox image needs no registry push permission. Account listing proves account visibility, never deployment write permissions, product entitlement or container readiness. A real authorized full-artifact installation is still required to certify the reviewed mapping. Optional [custom-domain setup](custom-domains.md) adds separately reviewed `domain-zones` and `domain-routing` capabilities. They are not required for ordinary installation or sign-in. The domain flow accepts `domainInstallationId` instead of `providerInstallationId` and returns only to the fixed Flarebot domain setup view after OAuth. Never infer the actual OAuth scope IDs from those capability names. ## Configuration manifest Set `FLAREBOT_MODE=control-plane`, `FLAREBOT_ENV=production`, and a JSON string `FLAREBOT_CONTROL_PLANE`. Its schema version remains 1 with new explicit OAuth setup fields. The following is a **template**, deliberately not runnable: replace all catalog/client placeholders using the reviewed returned client configuration. `oauthScopes` and the manifest's scope IDs must be identical sets, and every requested scope must be allowed by registered client `scopeIds`. Additional registered permissions are not requested. Every requested scope must justify at least one required capability and all eight capabilities must be covered. Multiple capabilities may map to one scope. Do not add refresh/offline scopes; v0.1 reconnects for later privileged operations. ```json { "schemaVersion": 1, "publicOrigin": "https://control.example.com", "oauthClientId": "", "oauthRedirectUri": "https://control.example.com/auth/callback", "oauthTokenAuthMethod": "client_secret_basic", "oauthScopes": [""], "oauthCapabilities": { "schemaVersion": 1, "artifactVersion": "0.1.0-dev.1", "reviewedAt": "2026-09-06", "registeredClient": { "clientId": "", "redirectUri": "https://control.example.com/auth/callback", "tokenAuthMethod": "client_secret_basic", "scopeIds": [""], "userinfoVerified": true }, "scopes": [ { "id": "", "name": "", "category": "", "resourceScopes": [""], "capabilities": [""] } ] } } ``` Identity names are not inferred from OIDC discovery. Verify that the registered code-flow client actually supports UserInfo and its normalized requested scopes. The implementation does not consume an ID token or request implicit flow to force one. Its sole identity source is the `sub` in the authenticated HTTPS UserInfo response. It discards optional email/profile, refresh tokens and ID tokens. The manifest is a publisher attestation with strict shape/coverage checks; the parser cannot authenticate the catalog evidence or prove that a supplied mapping is least privilege. Keep the dated catalog review with publisher configuration, review it when changing the release, and test real grants. Missing setup returns `oauth_setup_required` or `oauth_capability_unavailable`, with publisher recovery copy, before redirecting a customer to Cloudflare. Every publisher version bump also requires reviewing the saved `FLAREBOT_CONTROL_PLANE.oauthCapabilities` and setting its `artifactVersion` to the new package version. `keep_vars` preserves the previous attestation; it does not make that attestation valid for the new release. Keep the registered client, scopes and secrets unchanged when the reviewed capabilities have not changed. After deployment, an unauthenticated `GET /api/connection` must return HTTP 401 with `not_connected`, not a publisher setup error. Verify that same-origin `POST /auth/start` returns HTTP 303 to Cloudflare's OAuth authorization endpoint; HTTP 200 from `/connect` alone only verifies the static page. Provision the native SQLite `AUTH_VAULT` binding from `wrangler.control-plane.jsonc`. Set independent server secrets: `FLAREBOT_SESSION_SECRET` (existing configuration contract, at least 32 bytes), `FLAREBOT_CREDENTIAL_ENCRYPTION_KEY` (32 random bytes, unpadded base64url), and the client secret only for a confidential registered client. Session cookies use random opaque references rather than self-contained claims; the retained session secret is not an encryption substitute. Never put secrets in the manifest, frontend build variables, installation metadata, Workflow inputs/results or logs. Rotating the credential encryption key invalidates extant protected records; coordinate key rotation with reconnects. Unreadable sessions are rejected and can be replaced by fresh OAuth or retired on logout. If an old grant reference or token can no longer be decrypted, Flarebot cannot remotely revoke it; its bounded vault alarm and Cloudflare token expiry remain in force. Users can revoke the application in Cloudflare immediately. Keep the key stable across normal upgrades. ## Browser and storage boundary `POST /auth/start` requires the exact configured Origin and an optional single `returnTo=/connect`. It creates independent 256-bit state, PKCE verifier and browser binding, stores a ten-minute encrypted transaction, and sends a Secure, HttpOnly, SameSite=Lax, Path=/ `__Host-flarebot-oauth` cookie. There are no arbitrary redirect/return destinations. One browser transaction is current at a time; starting a second flow replaces the binding cookie and invalidates the earlier browser flow. The earlier record expires automatically. `GET /auth/callback` checks the fixed callback origin/path, exact single state and code (or error), matching browser binding and previous session, optional issuer, and transaction expiry. It rejects mixed/duplicate/malformed parameters and atomically claims/deletes the transaction **before** code exchange. Errors and network failures never reopen state. Repeated callbacks cannot exchange again. An authorization error consumes its valid transaction too. All authorization, token, UserInfo, revoke and Accounts URLs are fixed to [Cloudflare's documented endpoints](https://developers.cloudflare.com/fundamentals/oauth/integrate-with-cloudflare/). Credential-bearing requests use `redirect: manual` with explicit non-2xx rejection, ten-second deadlines and bounded response bodies. Token error categories are sanitized; no raw response, query, token, OAuth code, verifier or credential enters logs. The token response's scope is checked against the required manifest; when omitted, OAuth's same-as-requested scope rule applies. No opaque token decoding substitutes for reported scope. No refresh credential is retained. Successful login creates a fresh eight-hour opaque identity session, deletes the previous session/grant, and attempts to revoke the previous grant. Credentials are retained for the lesser of the token lifetime and one hour. Identity sessions and grants use separate unguessable references and separate DO records. All record payloads are AES-GCM encrypted with associated data binding ciphertext to DO ID, record kind and expiry. Native transactions serialize state claims and session retirement; native alarms remove expired records. Grant records never share installation storage. `GET /api/connection` returns account IDs/names, selected account ID and grant expiry only to the authenticated session. It enumerates all granted account pages, bounded to 20 pages of 50 accounts, and never exposes principal/grant references or tokens. `POST /api/account` requires the session plus exact Origin, validates the small form body, fetches the grant's accounts again and rejects forged selection. The UI escapes account names and never stores tokens in browser storage. `POST /auth/disconnect` requires exact Origin, atomically retires the local session and removes its grant even after the session expires or publisher capability setup changes, then clears both cookies. It attempts remote revocation when setup allows; if unavailable, the UI explains that the user is locally signed out and can revoke Flarebot in Cloudflare authorized applications immediately. Credentials still expire at Cloudflare's token expiry. No unbounded retry queue is retained. Responses are `no-store`. Callback/API responses use `no-referrer`; the public page uses `same-origin` so browser form POSTs retain the Origin header required for CSRF checks without disclosing referrers to Cloudflare. SSR is public generic markup with only an ASSETS binding. Callback and credential request logging is disabled by the separate Wrangler observability configuration. Do not enable upstream logs that capture callback query strings or authorization headers. Recovery distinguishes revoked/expired authorization (reconnect), missing client capabilities (publisher fix), account denial/empty selection (choose/reconnect), and transient Cloudflare failure (retry). A fresh visitor sees ordinary connection onboarding. Product entitlement and actual deployment-write failures belong to the installer's checkpointed state, not an account-read preflight claim. ## Integration seams and remaining issues `authenticatedPrincipal(request, env)` in `control-plane/session.ts` resolves the real opaque session to `{subject, grantRef, selectedAccountId, expiresAt}` for trusted server callers. `selectedDeploymentGrant(request, env)` requires selection and revalidates current grant/account visibility, returning the principal and short-lived credential only within a trusted operation call stack. Both are server-only; no caller-selected principal/token endpoint exists. The [installation registry](installation-ownership.md) binds ownership metadata to this verified subject and stores no grant references or credentials. Every future install/update API must retain exact-Origin checks and look up ownership; authentication alone does not authorize an arbitrary record. FLA-9 must wire the real ownership-verified login bridge to customer runtime: customer-generated challenge, exact pinned issuer/audience/installation/owner, short expiry, one-time `jti`, pinned public signing key, atomic customer consume, and a real customer session cookie. FLA-8 intentionally issues no customer owner session or fake installation-ready link. Customer `createOwnerSession` remains a server-only seam until that bridge is implemented. Preserve customer session secrets across upgrades because BYOK encryption derives from them. ## Validation scope `tests/oauth.test.mjs` runs the actual Worker routing and native SQLite DOs with only an explicit fixed Cloudflare network fixture. It exercises PKCE/state replay races, cookie binding, expiry/alarms, rotation, restarts, error/redirect rejection, UserInfo identity, ciphertext/token separation, granted-account pagination and forgery, Origin checks, revoked/expired reconnect, public SSR, confidential methods, missing publisher setup, and real Chromium connect/select/disconnect rendering. The production bundle exports no fixture admin routes. Artifact assertions verify that OAuth secrets/modules stay outside customer and browser bundles. These local checks do not certify real consent, catalog minimality, public-client promotion, account entitlements, Worker uploads or Containers boot. Those require the real publisher/client setup and an authorized test customer installation.