// Turning a failed space read into something a reader can act on. // // Three very different problems reach a handler as an exception, and until this // existed only one of them was translated — the rest surfaced as a 500, which // told a reader nothing and made "the author never added me", "sign in again" // and "your server cannot do this" look identical. import { InvalidRequestError } from "$hatk"; import { isNotAuthorized, SpaceCredentialError } from "@hatk/hatk/spaces"; import { getSpaceSupport, pdsEndpointFor } from "../helpers/spaceSupport.ts"; import { SpaceError } from "./client.ts"; type Db = { query: (sql: string, params?: unknown[]) => Promise; run: (sql: string, params?: unknown[]) => Promise; }; /** The status a thrown error carries, whether it is ours or hatk's ProxyError. */ function statusOf(err: unknown): number | null { const status = (err as { status?: unknown })?.status; return typeof status === "number" ? status : null; } /** * Rethrow a space failure as the closest thing to an explanation. * * Never returns — it either throws an InvalidRequestError naming the problem or * rethrows what it was given. */ export async function throwSpaceError(err: unknown, db: Db, viewerDid: string): Promise { const status = statusOf(err); // No session to trade for a credential. hatk drops the PDS session when a // scope is refused, so this is what a reader hits on the request after the // one that told them to authorize again — the browser still holds a viewer // cookie, and nothing behind it works. if (status === 401) { throw new InvalidRequestError("Sign in again to open this gallery", "SessionExpired"); } // The space's authority will not issue this reader a credential. hatk names // the refusal, which the authority answers with a 400. The reference alpha, // since 0.0.0-spaces-alpha-20261029171913, answers a refused reader as though // the space did not exist, so as not to disclose that it does: to the reader // the two are the same. if ( isNotAuthorized(err) || (err instanceof SpaceCredentialError && err.code === "SpaceNotFound") ) { throw new InvalidRequestError("Only members can see this", "NotAuthorized"); } // A PDS with no permissioned data answers the very first call — a delegation // token from the reader's own server — with a 404 for a method it does not // serve. Indistinguishable from "no such space" by status alone, so ask. if (status === 403 || status === 404) { const endpoint = await pdsEndpointFor(db, viewerDid); if (endpoint) { const support = await getSpaceSupport(db, endpoint); if (!support.supported) { throw new InvalidRequestError( "Private galleries aren't available for your account yet", "SpacesUnsupported", ); } } throw new InvalidRequestError("Only members can see this", "NotAuthorized"); } // A repo host that cannot be reached or resolved is a failure of ours to // report, not of the reader to fix. if (err instanceof SpaceError || err instanceof SpaceCredentialError) { throw new InvalidRequestError( "This couldn't be loaded right now. Try again in a moment", "UpstreamFailed", ); } throw err; }