Something went wrong. Try again.
This repository has no description
Something went wrong. Try again.
18 kB · 546 lines
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547import { sameRelease } from "../installation-metadata.ts";import { useEffect, useRef, useState } from "octane";import { useMutation } from "@octanejs/tanstack-query";import { usePublisherSession } from "./PublisherQueryProvider";import { useInstallationsQuery, installationsOptions,} from "./queries/use-installations-query";import { useInstallationQuery, installationOptions,} from "./queries/use-installation-query";import type { usePublisherIdentityQuery } from "./queries/use-publisher-identity-query";import { PublisherReadError } from "./publisher-request";import type { Installation, ReleaseIdentity,} from "../installation-metadata.ts";
export const installationMessages = { recovery_required: "Installation was interrupted. Recover installation checks the existing resources before continuing.", artifact_unavailable: "This pre-release target is no longer available. Contact the publisher for manual recovery. Your existing resources and data have not been reset.", setup_required: "The publisher needs to finish installation setup. Refresh after the setup has been corrected.", reauthorization_required: "Reconnect Cloudflare and select this installation’s account to continue. Your existing installation is retained.", account_denied: "Select the account shown for this installation. Reconnect Cloudflare if it is not available.", resource_conflict: "An existing Cloudflare resource does not match this installation. It has been preserved. Ask your account administrator to resolve the conflict before retrying.", deployment_failed: "Flarebot could not finish deploying. Retry this installation to continue with its existing resources.", health_failed: "Flarebot did not pass its readiness checks. Retry this installation to check and repair it.", temporarily_unavailable: "Flarebot could not be reached. Check your connection and try again. Your saved request and last available status are retained.", installation_conflict: "This installation changed. Refresh its status before continuing.", installation_not_found: "This installation is no longer available to your signed-in account.", invalid_metadata: "This installation request could not be verified. Refresh the page and try again.", forbidden: "This request could not be verified. Reload Flarebot and try again.",} as const;type Failure = keyof typeof installationMessages;type Intent = { ownerSubject: string; accountId: string; installationId: string | null; action: "reserve" | "start" | "recover" | "upgrade"; requestId: string; target?: ReleaseIdentity | null;};interface CommandState { selectedId: string | null; cursor: string | null; busy: boolean; error: Failure | null; pending: Intent | null;}const initial: CommandState = { selectedId: null, cursor: null, busy: false, error: null, pending: null,};const storageKey = "flarebot-installation-intent";const id = (value: unknown): value is string => typeof value === "string" && /^[a-f0-9]{32}$/.test(value);const safeError = (value: unknown): Failure => typeof value === "string" && Object.hasOwn(installationMessages, value) ? (value as Failure) : "temporarily_unavailable";function saved() { try { const value = JSON.parse(sessionStorage.getItem(storageKey) ?? "null"); if ( !value || typeof value.ownerSubject !== "string" || !(value.selectedId === null || id(value.selectedId)) ) return null; const p = value.pending; if ( p && (p.ownerSubject !== value.ownerSubject || !id(p.accountId) || !id(p.requestId) || !["reserve", "start", "recover", "upgrade"].includes(p.action) || (p.action === "upgrade" && (!p.target || typeof p.target.version !== "string" || !/^[a-f0-9]{64}$/.test(p.target.artifactDigest) || !/^(?:[a-f0-9]{40}|[a-f0-9]{64})$/.test( p.target.sourceRevision, ))) || (p.action === "reserve" ? p.installationId !== null : !id(p.installationId))) ) return null; return value as { ownerSubject: string; selectedId: string | null; pending: Intent | null; }; } catch { return null; }}
// Browser intent is nonsecret and never grants authority. Every POST still// resolves its owner and selected account from the server's authenticated session.export function useInstallationStatus( identity: ReturnType<typeof usePublisherIdentityQuery>,) { const cache = usePublisherSession(); const owner = cache.getSnapshot(); const [state, setState] = useState<CommandState>(() => { const restored = typeof window !== "undefined" ? saved() : null; return restored?.ownerSubject === owner.ownerSubject ? { ...initial, selectedId: restored.selectedId, pending: restored.pending, } : initial; }); const commands = useRef(state); const generation = useRef(0); const controller = useRef<AbortController | null>(null); const alive = useRef(false); const publish = (changes: Partial<CommandState>) => { commands.current = { ...commands.current, ...changes }; if (alive.current) setState(commands.current); }; const pagination = useMutation({ mutationFn: async (cursor: string | null) => { const version = generation.current; await cache.client.fetchQuery({ ...installationsOptions(cache, cursor), staleTime: 0, }); if ( alive.current && version === generation.current && cache.getSnapshot().epoch === owner.epoch ) publish({ cursor }); }, }); const listKey = installationsOptions(cache, state.cursor).queryKey; const cached = cache.client.getQueryData(listKey); const cachedSelected = cached?.installations.find( (item) => item.installationId === state.selectedId, ) ?? cache.client.getQueryData( installationOptions(cache, state.selectedId ?? "").queryKey, )?.installation ?? cached?.installations[0]; const list = useInstallationsQuery( state.cursor, state.busy || pagination.isPending, ["installing", "updating"].includes(cachedSelected?.status ?? ""), ); const wanted = state.selectedId ?? list.data?.installations[0]?.installationId ?? ""; const listed = list.data?.installations.find( (item) => item.installationId === wanted, ); const detail = useInstallationQuery({ id: wanted, enabled: !state.busy && !!wanted && !listed, interval: ["installing", "updating"].includes(cachedSelected?.status ?? "") ? 2000 : 30_000, }); function snapshot() { const command = commands.current; const page = cache.client.getQueryData( installationsOptions(cache, command.cursor).queryKey, ); const selectedId = command.selectedId ?? page?.installations[0]?.installationId; const selected = page?.installations.find((item) => item.installationId === selectedId) ?? cache.client.getQueryData( installationOptions(cache, selectedId ?? "").queryKey, )?.installation ?? null; return { ...command, ownerSubject: cache.getSnapshot().ownerSubject, selected, latestRelease: page?.latestRelease ?? null, }; } const confirm = (installation: Installation) => { void cache.client.cancelQueries({ queryKey: cache.key("installations") }); void cache.client.cancelQueries({ queryKey: cache.key("installation", installation.installationId), }); cache.client.setQueryData( installationsOptions(cache, commands.current.cursor).queryKey, (page) => page ? { ...page, installations: [ installation, ...page.installations.filter( (item) => item.installationId !== installation.installationId, ), ], } : page, ); cache.client.setQueryData( installationOptions(cache, installation.installationId).queryKey, { installation, latestRelease: snapshot().latestRelease, upgradeFrom: list.data?.upgradeFrom ?? [], }, ); publish({ selectedId: installation.installationId }); }; const persist = () => { const s = snapshot(); try { if (s.ownerSubject) sessionStorage.setItem( storageKey, JSON.stringify({ ownerSubject: s.ownerSubject, selectedId: s.selectedId ?? s.selected?.installationId ?? null, pending: s.pending, }), ); } catch { /* Native request replay remains valid if browser storage is unavailable. */ } }; const cancel = () => { generation.current++; controller.current?.abort(); }; const request = async ( path: string, options: RequestInit = {}, timeoutMs = 15_000, ) => { const response = await fetch(path, { credentials: "same-origin", cache: "no-store", ...options, signal: AbortSignal.any([ controller.current!.signal, AbortSignal.timeout(timeoutMs), ]), }); const data = await response.json(); return { response, data }; }; async function refresh( cursor = commands.current.cursor, selectedId?: string, ) { if (commands.current.busy || pagination.isPending) return; pagination.reset(); publish({ ...(selectedId ? { selectedId } : {}), error: null }); if (!cache.getSnapshot().ownerSubject) { await identity.refetch(); return; } if (cursor !== commands.current.cursor) { pagination.mutate(cursor); return; } await Promise.all([ list.refetch(), ...(!listed && wanted ? [detail.refetch()] : []), ]); } const choose = (record: Installation) => { if (commands.current.busy || pagination.isPending) return; publish({ selectedId: record.installationId, error: null }); persist(); }; async function run( accountId: string, action: Intent["action"], record: Pick<Installation, "installationId" | "accountId"> | null = null, ) { const s = snapshot(); const epoch = cache.getSnapshot().epoch; if ( !navigator.onLine || pagination.isPending || s.busy || !s.ownerSubject || (record && record.accountId !== accountId) ) return; const pending = s.pending; if ( pending && (pending.ownerSubject !== s.ownerSubject || pending.accountId !== accountId) ) { publish({ error: "account_denied" }); return; } if ( pending && (pending.action !== action || pending.installationId !== (record?.installationId ?? null)) ) { publish({ error: "installation_conflict" }); return; } cancel(); const version = generation.current; controller.current = new AbortController(); let intent: Intent = pending ?? { ownerSubject: s.ownerSubject, accountId, installationId: record?.installationId ?? null, action, target: action === "upgrade" ? s.selected?.status === "failed" ? s.selected.desiredRelease : s.latestRelease : null, requestId: crypto.randomUUID().replaceAll("-", ""), }; publish({ busy: true, error: null, pending: intent }); await cache.client.cancelQueries({ queryKey: cache.key() }); if (version !== generation.current || cache.getSnapshot().epoch !== epoch) return; persist(); let reconcile = false; try { while (true) { const path = intent.action === "reserve" ? "/api/installations" : `/api/installations/${intent.installationId}/${intent.action}`; // Upgrade/recovery can verify deployed code with Cloudflare before // acknowledging the command. Keep polling quick without aborting that // valid preflight at the status-read deadline. Explicit cancellation and // the frozen replay intent still apply if this longer deadline expires. const { response, data } = await request( path, { method: "POST", headers: { "Content-Type": "application/x-www-form-urlencoded" }, body: new URLSearchParams({ requestId: intent.requestId, ...(intent.action === "upgrade" ? { target: JSON.stringify(intent.target) } : {}), }).toString(), }, 120_000, ); if ( version !== generation.current || cache.getSnapshot().epoch !== epoch ) return; if (!response.ok) { const code = safeError(data.error); reconcile = [ "installation_conflict", "installation_not_found", "invalid_metadata", "recovery_required", ].includes(code); throw code; } const installation: Installation = data.installation; if (installation.ownerSubject !== intent.ownerSubject) { // A successful response confirms an identity change in another tab. // Clear the previous owner's view before the next identity-only read. try { sessionStorage.removeItem(storageKey); } catch { /* Storage may be disabled. */ } cache.identify(installation.ownerSubject); return; } if (installation.accountId !== intent.accountId) { // A different tab may have changed the server's selected account. // Reservation is confirmed but deployment has not been authorized here. confirm(installation); publish({ pending: null }); persist(); throw "account_denied"; } confirm(installation); if (intent.action !== "reserve" || installation.status !== "reserved") { publish({ pending: null }); persist(); break; } intent = { ...intent, installationId: installation.installationId, action: "start", requestId: crypto.randomUUID().replaceAll("-", ""), }; publish({ pending: intent }); persist(); } } catch (error) { if ( version === generation.current && cache.getSnapshot().epoch === epoch ) { const code = safeError(error); publish({ error: code, ...(reconcile ? { pending: null } : {}) }); if (reconcile) persist(); } } finally { if ( version === generation.current && cache.getSnapshot().epoch === epoch ) { publish({ busy: false }); if (reconcile) void refresh(); } } } const forget = () => { // Keep the form mounted until its native disconnect POST navigates away. cancel(); cache.clearIntents(); }; async function continuePending(accountId: string) { const pending = snapshot().pending; if (!pending || snapshot().busy) return; // Replay the frozen target, independently of the currently viewed page. // Its owner/account are still verified by the production POST route. await run( accountId, pending.action, pending.installationId ? { installationId: pending.installationId, accountId: pending.accountId, } : null, ); } async function recover(accountId: string, record: Installation) { const s = snapshot(); if ( s.busy || !["installing", "updating"].includes(record.status) || record.ownerSubject !== s.ownerSubject || accountId !== record.accountId || (s.pending && (s.pending.installationId !== record.installationId || s.pending.ownerSubject !== s.ownerSubject)) ) return; // This is an explicit owner recovery action, never a polling side effect. // The server terminates/reconciles the old attempt before starting a new one. publish({ pending: null, error: null }); persist(); await run(accountId, "recover", record); } useEffect(() => { alive.current = true; return () => { alive.current = false; cancel(); }; }, []); const selected = listed ?? detail.data?.installation ?? null; useEffect(() => { const pending = commands.current.pending; if ( !listed && detail.error instanceof PublisherReadError && detail.error.status === 404 && list.data?.installations[0] ) publish({ selectedId: list.data.installations[0].installationId }); if ( pending && pending.installationId === selected?.installationId && selected.status === "ready" && (pending.action !== "upgrade" || sameRelease(pending.target ?? null, selected.installedRelease)) ) publish({ pending: null }); persist(); }, [selected, state.pending, detail.error, list.data]); const readError = pagination.error ?? list.error ?? (!listed ? detail.error : null) ?? identity.error; return { ...state, ownerSubject: owner.ownerSubject, signedOut: owner.signedOut, installations: list.data?.installations ?? [], latestRelease: list.data?.latestRelease ?? null, upgradeFrom: list.data?.upgradeFrom ?? [], selected, nextCursor: list.data?.nextCursor ?? null, loading: pagination.isPending || identity.isFetching || list.isFetching || (!listed && detail.isFetching), refreshedAt: list.dataUpdatedAt || null, error: state.error ?? (readError && !( readError instanceof PublisherReadError && [401, 404].includes(readError.status) ) ? safeError( readError instanceof PublisherReadError ? readError.code : null, ) : null), refresh, choose, run, continuePending, recover, forget, };}