Something went wrong. Try again.
This repository has no description
Something went wrong. Try again.
13 kB · 394 lines
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395import { ControlPlaneLayout } from "./ControlPlaneLayout.tsx";import { useEffect, useRef, useState } from "octane";import { Button } from "octane-kumo/components/button";import { LayerCard } from "octane-kumo/components/layer-card";import { messages, type ErrorCode } from "../errors.ts";import { useInstallationStatus } from "./installation-status.tsx";import { InstallationStatus } from "./InstallationStatus.tsx";import { ProviderSetup } from "./ProviderSetup.tsx";import { DomainSetup } from "./DomainSetup.tsx";import { UpdateInstallation } from "./UpdateInstallation.tsx";import { PublisherQueryProvider, usePublisherSession,} from "./PublisherQueryProvider";import { usePublisherIdentityQuery } from "./queries/use-publisher-identity-query";import { useConnectionQuery, connectionOptions,} from "./queries/use-connection-query";import { PublisherReadError } from "./publisher-request";
export function Onboarding() { return ( <PublisherQueryProvider> <PublisherApp /> </PublisherQueryProvider> );}function PublisherApp() { const [callback, setCallback] = useState<{ error: string | null; notice: string | null; }>({ error: null, notice: null }); const [updateInstallationId, setUpdateInstallationId] = useState< string | null | undefined >(undefined); const [providerInstallationId, setProviderInstallationId] = useState< string | null | undefined >(undefined); const [domainInstallationId, setDomainInstallationId] = useState< string | null | undefined >(undefined); useEffect(() => { const params = new URLSearchParams(window.location.search); setCallback({ error: params.get("error"), notice: params.get("notice") }); if (params.has("error") || params.has("notice")) window.history.replaceState(null, "", "/connect"); const installationId = params.get("installationId"); const updateId = params.get("updateInstallation"); setUpdateInstallationId( updateId && /^[a-f0-9]{32}$/.test(updateId) ? updateId : null, ); setProviderInstallationId( params.get("enableProvider") === "openrouter" && installationId && /^[a-f0-9]{32}$/.test(installationId) ? installationId : null, ); const domainId = params.get("configureDomain"); setDomainInstallationId( domainId && /^[a-f0-9]{32}$/.test(domainId) ? domainId : null, ); }, []); const session = usePublisherSession(); const identity = usePublisherIdentityQuery( updateInstallationId ?? domainInstallationId ?? providerInstallationId ?? null, updateInstallationId !== undefined, ); const owner = session.getSnapshot(); const ownerKey = String(owner.epoch); if (updateInstallationId) return ( <UpdateInstallation key={ownerKey} identityError={identity.error} installationId={updateInstallationId} /> ); if (providerInstallationId) return ( <ControlPlaneLayout title="Provider setup" description="Enable an optional model provider for your Flarebot installation." page="Provider setup" > <ProviderSetup key={ownerKey} identity={identity} installationId={providerInstallationId} /> </ControlPlaneLayout> ); if (domainInstallationId) return ( <ControlPlaneLayout title="Custom domain setup" description="Choose an optional address for your Flarebot installation." page="Custom domain" > <DomainSetup key={ownerKey} identity={identity} installationId={domainInstallationId} /> </ControlPlaneLayout> ); return ( <StandardOnboarding key={ownerKey} identity={identity} callback={callback} deferred={ providerInstallationId === undefined || updateInstallationId === undefined || domainInstallationId === undefined } /> );}
function StandardOnboarding({ deferred, identity, callback,}: { deferred: boolean; callback: { error: string | null; notice: string | null }; identity: ReturnType<typeof usePublisherIdentityQuery>;}) { const session = usePublisherSession(); const installations = useInstallationStatus(identity); const [busy, setBusy] = useState(false); const [commandError, setError] = useState<ErrorCode | null>(null); const [notice, setNotice] = useState(""); const generation = useRef(0); const epoch = session.getSnapshot().epoch; const query = useConnectionQuery({ enabled: !deferred && !busy, bootstrap: true, }); const connection = query.error instanceof PublisherReadError && query.error.status === 401 ? null : (query.data ?? null); const loading = !deferred && !session.getSnapshot().signedOut && !query.data && (identity.isFetching || query.isFetching); const message = (value: unknown): ErrorCode => typeof value === "string" && Object.hasOwn(messages, value) ? (value as ErrorCode) : "temporarily_unavailable"; const error = commandError ?? (query.error instanceof PublisherReadError ? query.error.code === "not_connected" ? null : message(query.error.code) : query.error ? "temporarily_unavailable" : connection && !connection.accounts.length ? "no_accounts" : null); const load = () => { setError(null); if (!session.getSnapshot().ownerSubject) void identity.refetch(); void query.refetch(); }; useEffect(() => { if (deferred) return; const callbackError = callback.error; if (callbackError) { setError(message(callbackError)); } if (callback.notice === "revocation_pending") setNotice( "You are signed out of Flarebot. Cloudflare could not confirm revocation; remove Flarebot from your Cloudflare authorized applications to revoke access immediately.", ); return () => { generation.current++; }; }, [deferred, callback]); async function select(accountId: string) { if ( busy || loading || !navigator.onLine || !session.getSnapshot().ownerSubject ) return; const version = ++generation.current; setBusy(true); setError(null); setNotice(""); try { await session.client.cancelQueries({ queryKey: connectionOptions(session).queryKey, }); if ( version !== generation.current || session.getSnapshot().epoch !== epoch ) return; const response = await fetch("/api/account", { method: "POST", headers: { "Content-Type": "application/x-www-form-urlencoded" }, body: new URLSearchParams({ accountId }).toString(), signal: AbortSignal.timeout(15_000), }); const data = await response.json(); if ( version !== generation.current || session.getSnapshot().epoch !== epoch ) return; if (!response.ok) { setError(message(data.error)); if (data.error === "reauthorization_required") void session.client.invalidateQueries({ queryKey: connectionOptions(session).queryKey, }); } else { if (data.ownerSubject !== session.getSnapshot().ownerSubject) { session.identify(data.ownerSubject); return; } await session.client.cancelQueries({ queryKey: connectionOptions(session).queryKey, }); if ( version !== generation.current || session.getSnapshot().epoch !== epoch ) return; session.client.setQueryData( connectionOptions(session).queryKey, (current) => current ? { ...current, selectedAccountId: data.selectedAccountId } : current, ); setNotice("Account selected. New installations will use this account."); } } catch { if ( version === generation.current && session.getSnapshot().epoch === epoch ) setError("temporarily_unavailable"); } finally { if ( version === generation.current && session.getSnapshot().epoch === epoch ) setBusy(false); } } const setupRequired = error === "oauth_setup_required" || error === "oauth_capability_unavailable"; return ( <ControlPlaneLayout title="Your personal Flarebot" description="Run your personal Flarebot in your own Cloudflare account. Your conversations and agent data stay in that account." > <section className="control-section" id="cloudflare-account" aria-labelledby="account-heading" tabIndex={-1} > <div className="intro"> <h2 id="account-heading">Cloudflare account</h2> <p>Choose where your agent runs and manage your connection.</p> </div> <LayerCard className="connect-card"> <div className="intro"> <h3>{connection ? "Choose an account" : "Authorize Cloudflare"}</h3> <p> {connection ? "Select an account available with your authorization. Access is checked again when you select it." : "Connect to Cloudflare to authorize deployment and choose where Flarebot will run."} </p> </div> {loading && <p role="status">Checking your connection…</p>} {error && <p role="alert">{messages[error]}</p>} {notice && <p role="status">{notice}</p>} {connection && ( <ul className="account-list" aria-label="Cloudflare accounts"> {connection.accounts.map((account) => ( <li key={account.id} data-selected={connection.selectedAccountId === account.id} > <div> <p className="account-name">{account.name}</p> <p className="account-id">{account.id}</p> </div> <Button variant={ connection.selectedAccountId === account.id ? "secondary" : "primary" } disabled={busy || loading || installations.busy} onClick={() => void select(account.id)} aria-label={`Select ${account.name}`} > {connection.selectedAccountId === account.id ? "Selected" : "Select account"} </Button> </li> ))} </ul> )} <div className="actions"> {!loading && !setupRequired && ( <form action="/auth/start" method="post"> <Button type="submit" variant={connection ? "secondary" : "primary"} disabled={busy || installations.busy} > {connection ? "Reconnect Cloudflare" : "Connect Cloudflare"} </Button> </form> )} {!loading && (error === "temporarily_unavailable" || error === "account_denied" || setupRequired) && ( <Button variant="secondary" disabled={busy || loading} onClick={() => void load()} > Try again </Button> )} {connection && ( <form action="/auth/disconnect" method="post" onSubmit={() => installations.forget()} > <Button type="submit" variant="secondary" disabled={busy}> Disconnect </Button> </form> )} </div> </LayerCard> </section> <InstallationStatus status={installations} accountId={connection?.selectedAccountId ?? null} accounts={connection?.accounts ?? []} disabled={busy || loading} /> <section className="control-section" id="permissions" aria-labelledby="permissions-heading" tabIndex={-1} > <div className="intro"> <h2 id="permissions-heading">Permissions and access</h2> <p>Understand what your Cloudflare connection allows.</p> </div> <LayerCard className="connect-card permissions"> <div className="intro"> <h3>What you authorize</h3> </div> <p> Account selection and deployment of Flarebot’s Worker, static assets, workers.dev address, and Sandbox container. Cloudflare shows the exact publisher-reviewed permissions before you authorize. </p> <p> Deployment authorization is short-lived. You reconnect for later installation or update operations. Selecting an account does not deploy resources. </p> </LayerCard> </section> </ControlPlaneLayout> );}