~james/core

core/web/src/lib/api/orgMembers.ts -rw-r--r-- 3.9 KiB
452f17b7 — james chore(import): snapshot james.dad.town/core at f70c71c7696a 15 hours ago
                                                                                
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
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.`);
};