import type { Did } from "@atcute/lexicons/syntax";
import type { OAuthUserAgent } from "@atcute/oauth-browser-client";
import { getOrCreateDelegatedSession } from "#lib/api/actAs.ts";
import type { BobbinContext } from "#lib/api/client.ts";
import { organizationRole, type OrganizationRole } from "#lib/api/organizations.ts";
import { collect } from "#lib/api/pagination.ts";
import { getRepoByRepoDid } from "#lib/api/records.ts";
import { addCollaborator, removeCollaborator } from "#lib/api/repoSettings.ts";
import { didFromUri } from "#lib/api/uri.ts";
import {
addController,
getScopePresets,
listControllers,
removeController
} from "#lib/api/tranquil.ts";
export interface OrganizationMember {
did: Did;
handle?: string;
role: OrganizationRole;
grantedAt: string;
}
export const listOrganizationMembers = async (
controllerAgent: OAuthUserAgent,
orgDid: Did
): Promise<OrganizationMember[]> => {
const orgAgent = await getOrCreateDelegatedSession(controllerAgent, orgDid);
const controllers = await listControllers(orgAgent);
return controllers
.filter((controller) => controller.isActive)
.map((controller) => ({
did: controller.did,
handle: controller.handle,
role: organizationRole(controller.grantedScopes),
grantedAt: controller.grantedAt
}));
};
export const inviteOrganizationMembers = async (
controllerAgent: OAuthUserAgent,
orgDid: Did,
dids: readonly Did[]
): Promise<void> => {
const orgAgent = await getOrCreateDelegatedSession(controllerAgent, orgDid);
const editor = (await getScopePresets(orgAgent)).find((preset) => preset.name === "editor");
if (!editor) throw new Error("The organization's PDS does not offer an editor role.");
for (const did of dids) {
await addController(orgAgent, { controllerDid: did, grantedScopes: editor.scopes });
}
};
export interface MemberCollaboration {
repoDid: Did;
name: string;
knot: string;
}
export const listMemberCollaborations = async (
ctx: BobbinContext,
orgDid: Did,
memberDid: Did
): Promise<MemberCollaboration[]> => {
const repos = await collect(ctx, "org.tangled.repo.listRepos", { subject: orgDid, limit: 100 });
const found = await Promise.all(
repos.map(async (item): Promise<MemberCollaboration | null> => {
const repoDid = didFromUri(item.uri);
const collaborators = await collect(ctx, "org.tangled.repo.listCollaborators", {
subject: repoDid,
limit: 100
});
if (!collaborators.some((collaborator) => collaborator.subject === memberDid))
return null;
const repo = await getRepoByRepoDid(ctx, repoDid);
if (!repo.knot) return null;
return { repoDid, name: repo.value.declaration.name, knot: repo.knot };
})
);
return found.filter((collaboration) => collaboration !== null);
};
// remove the member first, then each grant; a failed grant comes back to the caller
export const removeOrganizationMember = async (
controllerAgent: OAuthUserAgent,
orgDid: Did,
memberDid: Did,
collaborations: readonly MemberCollaboration[] = []
): Promise<MemberCollaboration[]> => {
const orgAgent = await getOrCreateDelegatedSession(controllerAgent, orgDid);
await removeController(orgAgent, memberDid);
const failed: MemberCollaboration[] = [];
for (const collaboration of collaborations) {
await removeCollaborator(
orgAgent,
collaboration.knot,
collaboration.repoDid,
memberDid
).catch(() => failed.push(collaboration));
}
return failed;
};
export const addMembersAsCollaborators = async (
orgAgent: OAuthUserAgent,
knot: string,
repoDid: Did
): Promise<void> => {
const controllers = await listControllers(orgAgent);
const results = await Promise.allSettled(
controllers
.filter((controller) => controller.isActive && controller.did !== orgAgent.sub)
.map((controller) => addCollaborator(orgAgent, knot, repoDid, controller.did))
);
const failed = results.filter((result) => result.status === "rejected").length;
if (failed > 0)
throw new Error(`Could not add ${failed} organization member(s) as collaborators.`);
};