0
2
// Share an existing private gallery with another account.
// POST /xrpc/social.grain.unspecced.addSpaceMember
//
// The space's authority holds the member list and consults it when minting a
// credential, so adding someone here is what actually grants them the gallery.
// The invite row written alongside is only so they can find it without a link.
import { defineProcedure, InvalidRequestError } from "$hatk";
import { resolveActor } from "../helpers/resolveActor.ts";
import { addGalleryReader, parseSpaceUri } from "../spaces/client.ts";
import { throwSpaceError } from "../spaces/errors.ts";
export default defineProcedure("social.grain.unspecced.addSpaceMember", async (ctx) => {
const { ok, db, viewer, pds, input } = ctx;
if (!viewer) throw new InvalidRequestError("Authentication required");
const { space, actor } = input;
let authority: string;
try {
({ authority } = parseSpaceUri(space));
} catch {
throw new InvalidRequestError(`Not a space uri: ${space}`);
}
if (authority !== viewer.did) {
throw new InvalidRequestError("Only the gallery's author can add members", "NotAuthorized");
}
// A handle is what a person knows; the space stores a DID. Resolved through
// the index first, then the viewer's PDS, which can resolve a handle grain
// has never indexed.
const did = await resolveActor(db, pds, actor);
if (!did) {
throw new InvalidRequestError(`Could not find ${actor}`, "ActorNotFound");
}
if (did === viewer.did) {
throw new InvalidRequestError("You already have access to your own gallery");
}
try {
await addGalleryReader(pds, space, did);
} catch (err) {
return throwSpaceError(err, db, viewer.did);
}
await db.run(
`INSERT INTO _space_invites (space, member_did, author_did, created_at)
VALUES ($1, $2, $3, $4)
ON CONFLICT (space, member_did) DO NOTHING`,
[space, did, viewer.did, new Date().toISOString()],
);
return ok({ did });
});