~ knotless Search/
↑↓ move↵ open⇧↵ all results

grain.social/grain

photography-focused photo sharing platform

grain.social
1 collaborator
0
2
80 lines · 3.2 KB Raw History
// 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<unknown[]>;
  run: (sql: string, params?: unknown[]) => Promise<void>;
};

/** 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<never> {
  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;
}