Something went wrong. Try again.
This repository has no description
Something went wrong. Try again.
15 kB · 448 lines
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449import { useEffect, useRef, useState } from "octane";import { Button } from "octane-kumo/components/button";import { LayerCard } from "octane-kumo/components/layer-card";
import { validOrigin } from "./domain-data";import { usePublisherSession } from "./PublisherQueryProvider";import { useInstallationQuery } from "./queries/use-installation-query";import { useDomainQuery, domainOptions } from "./queries/use-domain-query";import { useDomainZonesQuery } from "./queries/use-domain-zones-query";import type { usePublisherIdentityQuery } from "./queries/use-publisher-identity-query";import { PublisherReadError } from "./publisher-request";
const errors: Record<string, string> = { resource_conflict: "That hostname conflicts with an existing Cloudflare resource. Remove the conflict or choose another hostname, then retry.", reauthorization_required: "Reconnect Cloudflare to continue domain setup.", temporarily_unavailable: "Domain setup is temporarily unavailable. Try again.", setup_required: "Custom-domain authorization is not available yet. The publisher must review the required Cloudflare OAuth scopes.", account_denied: "Your Cloudflare account does not allow this domain operation.", invalid_hostname: "Enter a valid hostname within the selected zone.", https_pending: "Cloudflare is still preparing HTTPS for this hostname. Try again shortly.", attachment_outcome_unknown: "Cloudflare has not confirmed the attachment outcome. Retry connection to check the same hostname. It cannot be marked removed while a write may be pending; your workers.dev address remains available.",};
function id() { return Array.from(crypto.getRandomValues(new Uint8Array(16)), (byte) => byte.toString(16).padStart(2, "0"), ).join("");}async function json(response: Response) { try { return (await response.json()) as Record<string, any>; } catch { return {}; }}
export function DomainSetup({ installationId, identity,}: { installationId: string; identity: ReturnType<typeof usePublisherIdentityQuery>;}) { const endpoint = `/api/installations/${installationId}/domain`; const [zoneId, setZoneId] = useState(""); const [hostname, setHostname] = useState(""); const [busy, setBusy] = useState(false); const [commandReconnect, setReconnect] = useState(false); const [confirmRemove, setConfirmRemove] = useState(false); const [commandError, setError] = useState(""); const mounted = useRef(true); const request = useRef<{ path: "" | "/remove" | "/retry"; values: Record<string, string>; requestId: string; ownerSubject: string | null; } | null>(null); const replayKey = `flarebot-domain-request:${installationId}`; const [uncertain, setUncertain] = useState(false);
const cache = usePublisherSession(); const epoch = cache.getSnapshot().epoch; const installation = useInstallationQuery({ id: installationId }); const accountId = installation.data?.installation.accountId ?? ""; const query = useDomainQuery(installationId, accountId, busy); const details = query.data ?? null; const zonesQuery = useDomainZonesQuery( installationId, accountId, !!details && (!details.domain || details.domain.status === "removed") && !busy, ); const zones = zonesQuery.data ?? null; const readError = identity.error ?? installation.error ?? query.error ?? zonesQuery.error; const reconnect = commandReconnect || cache.getSnapshot().signedOut || (readError instanceof PublisherReadError && readError.status === 401) || details?.domain?.errorCode === "reauthorization_required"; const error = commandError || (readError && !reconnect ? readError instanceof PublisherReadError ? (errors[readError.code] ?? "Could not load domain setup. Try again.") : "Could not load domain setup. Try again." : ""); const loading = !details && (installation.isFetching || query.isFetching); const load = () => { setError(""); setReconnect(false); if (!cache.getSnapshot().ownerSubject) { void identity.refetch(); return; } if (accountId) void query.refetch(); if (!accountId || installation.error) void installation.refetch(); if (details && (!details.domain || details.domain.status === "removed")) void zonesQuery.refetch(); }; useEffect(() => { if (zones?.length) setZoneId((current) => current || zones[0].id); }, [zones]);
function clearRequest() { request.current = null; setUncertain(false); try { sessionStorage.removeItem(replayKey); } catch { /* Storage may be disabled. */ } }
function showError( code: unknown, fallback = "Could not load domain setup. Try again.", ) { setError( typeof code === "string" && errors[code] ? errors[code] : fallback, ); } useEffect(() => { mounted.current = true; try { const saved = JSON.parse(sessionStorage.getItem(replayKey) ?? "null"); if ( saved && !!cache.getSnapshot().ownerSubject && (saved.ownerSubject === undefined || saved.ownerSubject === cache.getSnapshot().ownerSubject) && /^[a-f0-9]{32}$/.test(saved.requestId) && ["", "/remove", "/retry"].includes(saved.path) && saved.values && typeof saved.values === "object" ) { request.current = saved; setUncertain(true); } } catch { /* A missing local replay never grants authorization. */ } return () => { mounted.current = false; }; }, [installationId]); async function submit( path: "" | "/remove" | "/retry", values: Record<string, string>, ) { if (busy || !navigator.onLine || !cache.getSnapshot().ownerSubject) return; const pending = request.current ?? { path, values, requestId: id(), ownerSubject: cache.getSnapshot().ownerSubject, }; request.current = pending; try { sessionStorage.setItem(replayKey, JSON.stringify(pending)); } catch { /* In-memory retries still retain the request. */ } setBusy(true); setError(""); try { await cache.client.cancelQueries({ queryKey: domainOptions(cache, installationId, accountId).queryKey, }); if (!mounted.current || cache.getSnapshot().epoch !== epoch) return; const response = await fetch(`${endpoint}${pending.path}`, { method: "POST", credentials: "same-origin", headers: { "Content-Type": "application/x-www-form-urlencoded" }, body: new URLSearchParams({ requestId: pending.requestId, ...pending.values, }).toString(), signal: AbortSignal.timeout(30_000), }); const data = await json(response); if (!mounted.current || cache.getSnapshot().epoch !== epoch) return; if (response.status === 401) { clearRequest(); return setReconnect(true); } if (!response.ok) { if ([400, 403, 404, 409].includes(response.status)) clearRequest(); else setUncertain(true); return showError( data.error, "The request may have been received. Retry to safely confirm it.", ); } if (!data.domain || !Number.isSafeInteger(data.domain.revision)) throw new Error("invalid response"); clearRequest(); setConfirmRemove(false); await cache.client.cancelQueries({ queryKey: domainOptions(cache, installationId, accountId).queryKey, }); if (!mounted.current || cache.getSnapshot().epoch !== epoch) return; cache.client.setQueryData( domainOptions(cache, installationId, accountId).queryKey, (current) => current && (!current.domain || data.domain.revision >= current.domain.revision) ? { ...current, domain: data.domain } : current, ); } catch { if (!mounted.current || cache.getSnapshot().epoch !== epoch) return; setUncertain(true); showError( null, "The request may have been received. Retry to safely confirm it.", ); } finally { if (mounted.current && cache.getSnapshot().epoch === epoch) { setBusy(false); void cache.client.invalidateQueries({ queryKey: domainOptions(cache, installationId, accountId).queryKey, }); } } }
const domain = details?.domain; const configuring = !domain || domain.status === "removed"; const returnUrl = details ? new URL("/settings", details.runtimeOrigin).href : "#"; return ( <LayerCard className="connect-card"> <div className="intro"> <h2>Choose your address</h2> <p> Add an optional custom domain. Your workers.dev address always remains available. </p> </div> {loading && <p role="status">Checking domain setup…</p>} {error && <p role="alert">{error}</p>} {uncertain && ( <div className="actions"> <Button disabled={busy} onClick={() => void submit("", {})}> Confirm previous request </Button> <p> The previous result is uncertain. Confirm it before starting another operation. </p> <Button variant="secondary" disabled={busy} onClick={() => { clearRequest(); void load(); }} > Check saved status instead </Button> </div> )} {domain?.status === "connecting" && ( <p role="status"> {domain.domainId ? "Waiting for HTTPS on" : "Connecting"}{" "} {domain.hostname}… You can leave this page; setup will continue. </p> )} {domain?.status === "removing" && ( <p role="status"> Removing {domain.hostname}… Your workers.dev address remains available. </p> )} {domain && ["connecting", "removing"].includes(domain.status) && !uncertain && ( <details> <summary>Setup interrupted?</summary> <p> Restart the saved operation if it is no longer making progress. Existing resources will be checked, not replaced. </p> <Button disabled={busy} onClick={() => void submit("/retry", {})}> Restart saved operation </Button> </details> )} {domain?.status === "active" && ( <> <p> <strong>{domain.hostname}</strong> is active. </p> {domain.origin && validOrigin(domain.origin) && ( <a className="open-flarebot" href={domain.origin}> Open Flarebot at {domain.hostname} </a> )} <p>You may need to sign in again at your new address.</p> </> )} {domain?.status === "failed" && ( <> <p role="alert"> {errors[domain.errorCode ?? ""] ?? "Domain setup failed. Retry the same operation."} </p> <Button disabled={busy || uncertain || reconnect} onClick={() => void submit("/retry", {})} > {busy ? "Retrying…" : `Retry ${domain.action === "remove" ? "removal" : "connection"}`} </Button> </> )} {configuring && zones === null && !error && ( <p role="status">Loading active zones…</p> )} {configuring && zones?.length === 0 && ( <p> No active Cloudflare zones were found. This first version requires an existing zone; add a domain from your registrar to Cloudflare, or register one with Cloudflare, then reconnect. </p> )} {configuring && !!zones?.length && ( <form className="domain-form" onSubmit={(event) => { event.preventDefault(); void submit("", { zoneId, hostname: hostname.trim().toLowerCase(), }); }} > <label htmlFor="domain-zone">Cloudflare zone</label> <select id="domain-zone" value={zoneId} onChange={(event) => setZoneId((event.currentTarget as HTMLSelectElement).value) } disabled={busy || uncertain} > {zones.map((zone) => ( <option key={zone.id} value={zone.id}> {zone.name} </option> ))} </select> <label htmlFor="domain-hostname">Hostname</label> <input id="domain-hostname" value={hostname} onInput={(event) => setHostname((event.currentTarget as HTMLInputElement).value) } placeholder={`bot.${zones.find((zone) => zone.id === zoneId)?.name ?? zones[0].name}`} required disabled={busy || uncertain} /> <div className="actions"> <Button type="submit" disabled={busy || uncertain || !hostname.trim()} > {busy ? "Connecting…" : "Connect domain"} </Button> </div> </form> )} {(domain?.status === "active" || domain?.status === "failed") && !confirmRemove && ( <Button variant="secondary" disabled={uncertain} onClick={() => setConfirmRemove(true)} > {domain.status === "active" ? "Remove custom domain" : "Cancel domain setup"} </Button> )} {(domain?.status === "active" || domain?.status === "failed") && confirmRemove && ( <div> <p> Remove {domain.hostname}? Flarebot will continue at its workers.dev address. </p> <div className="actions"> <Button disabled={busy} onClick={() => void submit("/remove", {})} > Confirm removal </Button> <Button variant="secondary" disabled={busy} onClick={() => setConfirmRemove(false)} > Cancel </Button> </div> </div> )} {reconnect && ( <> <p role="alert"> Reconnect Cloudflare to continue. Cloudflare may redirect you to review authorization. </p> <form action="/auth/start" method="post"> <input type="hidden" name="domainInstallationId" value={installationId} /> <Button type="submit">Reconnect Cloudflare</Button> </form> </> )} {error && !uncertain && ( <Button variant="secondary" onClick={() => void load()}> Refresh domain status </Button> )} {details && ( <a className="open-flarebot" href={returnUrl}> Skip for now — use workers.dev </a> )} </LayerCard> );}