+ Probe spaces never touch your public repo or existing spaces, and
+ every check deletes what it created — a failed cleanup can be
+ removed with{" "}
+ pds spaces tooling.
+ The session is signed out automatically when the run finishes.
+
diff --git a/apps/check/src/SpacesConformanceView.tsx b/apps/check/src/SpacesConformanceView.tsx
new file mode 100644
index 00000000..04379e98
--- /dev/null
+++ b/apps/check/src/SpacesConformanceView.tsx
@@ -0,0 +1,297 @@
+/**
+ * Runner #3: the browser adapter for the spaces conformance suite.
+ *
+ * Signed in (the landing-page flow), the run carries the operator, blob
+ * and delegation capabilities: probe spaces, record writes, applyWrites
+ * atomicity, blob isolation and the operator's own delegation → credential
+ * → read flow all execute for real against the user's PDS, authenticated
+ * through the DPoP-bound OAuth session. Results stream into the table as
+ * each check lands.
+ *
+ * The checks a browser genuinely cannot run — harness-held foreign
+ * identities, the alpha crypto libs — are not shown at all: a row (or a
+ * count) implies a way they could have run here, and there isn't one.
+ * They are covered by the CLI and vitest runners.
+ *
+ * Without a session it degrades to the anonymous slice (discovery and
+ * unauthenticated request shapes) against any host the user names.
+ */
+
+import { createSignal, For, onMount, Show } from "solid-js";
+import {
+ browserCatalog,
+ filterCatalog,
+ operatorChecks,
+ runChecks,
+ type CheckContext,
+ type CheckResult,
+ type RunReport,
+} from "@getcirrus/space-conformance";
+import type { OAuthUserAgent } from "@atcute/oauth-browser-client";
+import { getAgent, signOut, signedInDid } from "./lib/oauth";
+
+function normalizeOrigin(input: string): string {
+ const trimmed = input.trim().replace(/\/+$/, "");
+ if (/^https?:\/\//i.test(trimmed)) return trimmed;
+ return `https://${trimmed}`;
+}
+
+async function resolveDid(origin: string): Promise {
+ const res = await fetch(`${origin}/.well-known/did.json`);
+ if (!res.ok) throw new Error(`did.json returned ${res.status}`);
+ const doc = (await res.json()) as { id?: string };
+ if (!doc.id) throw new Error("did.json has no id");
+ return doc.id;
+}
+
+/**
+ * A fetch that routes same-origin requests through the OAuth agent (which
+ * signs each one with the session's DPoP key) and everything else through
+ * the plain window fetch. The conformance context keeps its bare `fetch`
+ * for the requests that must stay anonymous.
+ */
+function agentFetch(agent: OAuthUserAgent, origin: string): typeof fetch {
+ return (input, init) => {
+ const url = new URL(
+ typeof input === "string"
+ ? input
+ : input instanceof URL
+ ? input.toString()
+ : input.url,
+ );
+ if (url.origin === origin) {
+ return agent.handle(url.pathname + url.search, init);
+ }
+ return fetch(input, init);
+ };
+}
+
+const STATUS_STYLE: Record = {
+ pass: "text-pass",
+ fail: "text-fail",
+ error: "text-fail",
+ skipped: "text-faint",
+};
+const STATUS_ICON: Record = {
+ pass: "✓",
+ fail: "✗",
+ error: "!",
+ skipped: "–",
+};
+
+export function SpacesConformanceView(props: { onExit: () => void }) {
+ const [target, setTarget] = createSignal("");
+ const [running, setRunning] = createSignal(false);
+ const [error, setError] = createSignal(null);
+ const [results, setResults] = createSignal([]);
+ const [report, setReport] = createSignal(null);
+ const [authedRun, setAuthedRun] = createSignal(false);
+
+ async function runCatalog(
+ context: Omit,
+ capabilities: Parameters[1],
+ ) {
+ setRunning(true);
+ setError(null);
+ setResults([]);
+ setReport(null);
+ try {
+ const filtered = filterCatalog(browserCatalog, capabilities);
+ const result = await runChecks({
+ catalog: filtered,
+ context,
+ suiteVersion: "web",
+ alphaBuild: "0.0.0-spaces-alpha-20260818163953",
+ // Stream rows into the table as checks land. Skipped results
+ // are checks this transport can never run — not listing them
+ // avoids implying they could have; the footer counts them.
+ onResult: (result) => {
+ if (result.status !== "skipped") {
+ setResults((prev) => [...prev, result]);
+ }
+ },
+ });
+ setReport(result);
+ } catch (err) {
+ setError(err instanceof Error ? err.message : String(err));
+ } finally {
+ setRunning(false);
+ }
+ }
+
+ async function runAuthed(agent: OAuthUserAgent) {
+ const origin = normalizeOrigin(agent.session.info.aud);
+ const did = agent.session.info.sub;
+ setAuthedRun(true);
+ setTarget(origin);
+ await runCatalog(
+ {
+ target: { origin, did },
+ fetch: window.fetch.bind(window),
+ operator: {
+ oauth: true,
+ fetch: agentFetch(agent, origin),
+ },
+ },
+ {
+ capabilities: ["operator", "pds-blobs", "pds-delegation"],
+ destructive: true,
+ },
+ );
+ // Sessions are ephemeral — sign out once the run has finished, the
+ // same convention as the write tests.
+ if (signedInDid()) void signOut();
+ }
+
+ async function onRunAnonymous(event: Event) {
+ event.preventDefault();
+ const raw = target().trim();
+ if (!raw) return;
+ setAuthedRun(false);
+ try {
+ const origin = normalizeOrigin(raw);
+ const did = await resolveDid(origin);
+ await runCatalog(
+ {
+ target: { origin, did, implementation: origin },
+ fetch: window.fetch.bind(window),
+ },
+ { capabilities: [] },
+ );
+ } catch (err) {
+ setError(err instanceof Error ? err.message : String(err));
+ }
+ }
+
+ onMount(() => {
+ // Arriving from the landing flow the user has just authorized; run
+ // immediately against their own PDS. With no session this is the
+ // anonymous surface and waits for a target instead.
+ void getAgent().then((agent) => {
+ if (agent) void runAuthed(agent);
+ });
+ });
+
+ // In an anonymous run, the operator checks were skipped but WOULD run
+ // with a sign-in — worth a pointer. (The alpha-lib/identity checks are
+ // deliberately not mentioned at all: they never run in a browser.)
+ const operatorIds = new Set(operatorChecks.map((c) => c.id));
+ const signInWouldRun = () =>
+ (report()?.results ?? []).filter(
+ (r) => r.status === "skipped" && operatorIds.has(r.id),
+ ).length;
+
+ return (
+
+
+
+ spaces conformance (alpha)
+
+
+
+
SPACES CONFORMANCE
+
+
+ Discovery and request-shape checks against a live spaces host.
+ Sign in from the landing page to run the write, blob and
+ credential checks against your own PDS.
+ >
+ }
+ >
+ Running the operator checks against{" "}
+ {target()}: probe spaces, record
+ writes, blob isolation and the delegation → credential → read flow.
+
+
+ {signInWouldRun()} more checks need an authenticated session —
+ start from SPACES CONFORMANCE on the landing page to run them
+ against your own PDS.
+