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.
Never reuse Wrangler's OAuth client or developer credentials. Register the exact
HTTPS callback https://<publisher-origin>/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
(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 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.
{
"schemaVersion": 1,
"publicOrigin": "https://control.example.com",
"oauthClientId": "<registered-flarebot-client-id>",
"oauthRedirectUri": "https://control.example.com/auth/callback",
"oauthTokenAuthMethod": "client_secret_basic",
"oauthScopes": ["<exact-reviewed-scope-ids>"],
"oauthCapabilities": {
"schemaVersion": 1,
"artifactVersion": "0.1.0-dev.1",
"reviewedAt": "2026-09-06",
"registeredClient": {
"clientId": "<registered-flarebot-client-id>",
"redirectUri": "https://control.example.com/auth/callback",
"tokenAuthMethod": "client_secret_basic",
"scopeIds": ["<exact-normalized-registered-scope-ids>"],
"userinfoVerified": true
},
"scopes": [
{
"id": "<actual-catalog-id-or-verified-identity-scope>",
"name": "<actual-catalog-name>",
"category": "<actual-category-if-present>",
"resourceScopes": ["<actual-underlying-resource-scopes>"],
"capabilities": ["<covered-capability-names-from-table>"]
}
]
}
}
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.
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 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.