···599599600600 // Union path: when the caller is authenticated, fold in records from
601601 // spaces they're a member of. Anonymous callers just get public results.
602602+ //
603603+ // The caller authenticates with service-auth (`Authorization: Bearer
604604+ // <jwt>`). Their member-of space list comes from one of:
605605+ // 1. `X-Membership-Manifest` header — signed list issued by some
606606+ // authority asserting `sub` is in these spaces. The manifest's
607607+ // `sub` MUST match the JWT's issuer (DID) — the manifest is not
608608+ // a bearer token. Preferred for multi-authority deployments.
609609+ // 2. Local listSpaces — appview asks its own authority adapter
610610+ // `listSpaces({ memberDid: jwt.issuer })`. Works when the appview
611611+ // operator IS the authority.
602612 let spaceUris: string[] | undefined;
603613 const hasAuthHeader = !!c.req.header("Authorization");
614614+ const manifestHeader = c.req.header("X-Membership-Manifest");
604615 if (spacesCtx) {
605616 const nsid = new URL(c.req.url).pathname.match(/\/xrpc\/([^?]+)/)?.[1] as Nsid | null;
606617 const auth = await verifyServiceAuthRequest(spacesCtx.verifier, c.req.raw, nsid);
607618 if (auth) {
608608- const { spaces } = await spacesCtx.adapter.listSpaces({
609609- memberDid: auth.issuer,
610610- limit: 200,
611611- });
612612- spaceUris = spaces.map((s) => s.uri);
619619+ if (manifestHeader && spacesCtx.manifestVerifier) {
620620+ const verified = await spacesCtx.manifestVerifier.verify(manifestHeader);
621621+ if (!verified.ok) {
622622+ return c.json(
623623+ { error: "AuthRequired", reason: verified.reason, message: "invalid membership manifest" },
624624+ 401
625625+ );
626626+ }
627627+ if (verified.claims.sub !== auth.issuer) {
628628+ return c.json(
629629+ { error: "Forbidden", reason: "manifest-sub-mismatch", message: "manifest sub does not match caller" },
630630+ 403
631631+ );
632632+ }
633633+ spaceUris = verified.claims.spaces;
634634+ } else {
635635+ const { spaces } = await spacesCtx.adapter.listSpaces({
636636+ memberDid: auth.issuer,
637637+ limit: 200,
638638+ });
639639+ spaceUris = spaces.map((s) => s.uri);
640640+ }
613641 } else if (hasAuthHeader) {
614642 // Had an auth header but it was invalid — reject rather than
615643 // silently downgrading to public results.