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 { didOf } from "#lib/api/syntax.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 => { 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 => { 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 => { const repos = await collect(ctx, "org.tangled.repo.listRepos", { subject: orgDid, limit: 100 }); const found = await Promise.all( repos.map(async (item): Promise => { const repoDid = didOf(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 => { 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 => { 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.`); };