// The page: sign-in, the checks, and a router that draws every address from // the one index.html. infra/policy-site/viewer-request.js serves index.html // at each address src/routes.ts answers. import { loadChecks, type Checks } from "./checks.ts"; import { byId, errorText, h } from "./dom.ts"; import { loadFlow, redirectFor, saveFlow, type FlowState } from "./flow.ts"; import { startLattice } from "./lattice.ts"; import { Repo } from "./pds.ts"; import { recommended } from "./recommended.ts"; import { parseRoute, returnPath, type Route } from "./routes.ts"; import { pdsUrl, resume, signIn, signOut, type OAuthSession } from "./session.ts"; import { Store } from "./store.ts"; import { PolicySyntax } from "./ui/policy-syntax.ts"; import { bindingsView } from "./views/bindings.ts"; import { rolloutsView } from "./views/rollouts.ts"; import { simulatorView } from "./views/simulator.ts"; import { unboundPolicies, unboundView } from "./views/unbound.ts"; import type { Page } from "./views/page.ts"; import { policiesView } from "./views/policies.ts"; import { notFound, recommendedList, recommendedPage } from "./views/recommended.ts"; import { tray } from "./views/tray.ts"; import { stepAnnouncement, wizardView } from "./views/wizard.ts"; import { copy } from "./copy.ts"; function showError(error: unknown): void { const el = byId("page-status"); el.textContent = errorText(error); el.hidden = false; } /** Resolves once the browser has a moment with nothing else to do. */ function idle(): Promise { return new Promise((resolve) => { if ("requestIdleCallback" in window) requestIdleCallback(() => resolve(), { timeout: 2000 }); else setTimeout(resolve, 200); }); } /** A sign-in form whose sign-in comes back to `path`. */ function signInForm(path: string): HTMLElement { const input = h("input", { attrs: { name: "identity", autocomplete: "username", autocapitalize: "none", spellcheck: "false", required: "" }, }); const error = h("p", { class: "status error", attrs: { role: "alert" }, hidden: true }); const label = h("label", {}, "Handle or DID"); input.id = `sign-in-input-${Math.random().toString(36).slice(2)}`; label.setAttribute("for", input.id); return h( "form", { class: "sign-in", on: { submit: async (event) => { event.preventDefault(); error.hidden = true; try { await signIn(input.value, path); } catch (caught) { error.textContent = errorText(caught); error.hidden = false; } }, }, }, label, input, h("button", { class: "primary", attrs: { type: "submit" } }, "Sign in"), h("p", { class: "hint" }, copy.signInHint), error, ); } function showAccount(session: OAuthSession, store: Store): void { byId("account").hidden = false; byId("account-did").textContent = session.did; store.repo.describe().then( ({ handle, handleIsCorrect }) => (byId("account-handle").textContent = handleIsCorrect ? handle : `${handle} (handleIsCorrect: false)`), (error: unknown) => (byId("account-handle").textContent = `describeRepo: ${errorText(error)}`), ); byId("sign-out").addEventListener("click", async () => { try { await signOut(session); } finally { location.reload(); } }); } /** Which tab a route belongs to. */ function tabOf(route: Route): string | null { if (route.view === "policies") return "policies"; if (route.view === "unbound") return "unbound"; if (route.view === "rollouts") return "rollouts"; if (route.view === "simulate") return "simulate"; if (route.view === "bindings") return "bindings"; if (route.view === "recommended" || route.view === "recommended-policy") return "recommended"; if (route.view === "flow") { const { kind } = route.flow.entry; if (kind === "recommended") return "recommended"; if (kind === "new-binding" || kind === "binding") return "bindings"; if (kind === "new-policy" || kind === "policy") return "policies"; } return null; } async function main(): Promise { const checksLoading = loadChecks(); // The background waits for the checks to settle and the page to go idle, // and nothing waits for it. Without it the page has a plain ground. void checksLoading .catch(() => undefined) .then(idle) .then(() => startLattice(byId("lattice"))) .catch((error: unknown) => console.warn(`background lattice: ${errorText(error)}`)); let session: OAuthSession | undefined; try { const resumed = await resume(); session = resumed.session; const back = returnPath(resumed.state); if (back) history.replaceState(null, "", back); } catch (error) { showError(error); } let checks: Checks; try { checks = await checksLoading; } catch (error) { // Every judgment of a record is the checks'; without them the page shows // the catalog and nothing it would write. showError(`The policy checks did not load: ${errorText(error)}`); return; } let store: Store | null = null; if (session) { try { store = new Store(new Repo(session.did, await pdsUrl(session), session), checks, sessionStorage); showAccount(session, store); await store.reload(); void store.directory.load(store.servers); } catch (error) { showError(error); } } start(checks, store); } let stopRouter: (() => void) | undefined; /** * Draws the current address, and every address the page moves to after. * Starting again replaces the router already running. */ export function start(checks: Checks, store: Store | null): void { stopRouter?.(); let unsubscribe: (() => void) | undefined; let flow: FlowState | null = loadFlow(sessionStorage); const view = byId("view"); const trayEl = byId("tray"); // How many policies apply to nobody, wherever the operator looks first. const drawBadge = () => { const tab = document.querySelector('#tabs a[data-tab="unbound"]'); const badge = tab?.querySelector("span"); if (!tab || !badge) return; const count = store ? unboundPolicies(store).length : 0; badge.textContent = String(count); badge.hidden = count === 0; if (count) tab.setAttribute("aria-label", `Unbound: ${count} ${count === 1 ? "policy" : "policies"}`); else tab.removeAttribute("aria-label"); }; const drawTray = () => { const body = tray(page); trayEl.hidden = body === null; trayEl.replaceChildren(...(body ? [body] : [])); }; const page: Page = { checks, syntax: new PolicySyntax(checks.grammars), catalog: recommended, store, flow: () => flow, setFlow: (state) => { flow = state; saveFlow(sessionStorage, state); }, navigate: (path, options = {}) => { if (options.replace) history.replaceState(null, "", path); else history.pushState(null, "", path); draw(); }, signIn: signInForm, announce: (text) => { byId("announce").textContent = text; }, redrawOnChange: () => { unsubscribe?.(); unsubscribe = store?.subscribe(() => { drawTray(); drawBadge(); redraw(); }); }, }; let redraw = () => {}; const draw = () => { unsubscribe?.(); unsubscribe = store?.subscribe(() => { drawTray(); drawBadge(); }); const route = parseRoute(location.pathname); if (route.view === "flow") { const to = redirectFor(route.flow, route.step, flow); if (to) { history.replaceState(null, "", to); draw(); return; } } const tab = tabOf(route); for (const link of document.querySelectorAll("#tabs a[data-tab]")) { if (link.dataset.tab === tab) link.setAttribute("aria-current", "page"); else link.removeAttribute("aria-current"); } const render = (): HTMLElement => { switch (route.view) { case "policies": return policiesView(page); case "unbound": return unboundView(page); case "rollouts": return rolloutsView(page); case "simulate": return simulatorView(page, route.target); case "bindings": return bindingsView(page); case "recommended": return recommendedList(page); case "recommended-policy": return recommendedPage(page, route.name); case "flow": return wizardView(page, route.flow, route.step); case "not-found": return notFound(); } }; redraw = () => view.replaceChildren(render()); redraw(); drawTray(); drawBadge(); window.scrollTo(0, 0); view.querySelector("h2")?.focus({ preventScroll: true }); if (route.view === "flow") page.announce(stepAnnouncement(route.flow, route.step)); }; // Links within the page move without reloading it. const follow = (event: MouseEvent) => { const anchor = (event.target as Element | null)?.closest?.("a[href]"); if (!(anchor instanceof HTMLAnchorElement) || event.defaultPrevented) return; if (event.button !== 0 || event.metaKey || event.ctrlKey || event.shiftKey || event.altKey) return; const url = new URL(anchor.href); if (url.origin !== location.origin || anchor.target || parseRoute(url.pathname).view === "not-found") return; event.preventDefault(); page.navigate(url.pathname); }; document.addEventListener("click", follow); window.addEventListener("popstate", draw); stopRouter = () => { unsubscribe?.(); document.removeEventListener("click", follow); window.removeEventListener("popstate", draw); }; draw(); } void main().catch(showError);