Something went wrong. Try again.
Monorepo for Tangled
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100import type { Did } from "@atcute/lexicons/syntax";import { deleteStoredSession, type OAuthUserAgent } from "@atcute/oauth-browser-client";import { getOrCreateDelegatedSession } from "$lib/api/actAs";import type { BobbinContext, XrpcRequestInit } from "$lib/api/client";import { REPO_COUNT } from "$lib/api/descriptors";import { countOf, enrich, target } from "$lib/api/enrich";import type { ProfileRecord, RecordList } from "$lib/api/records";import { listControlledAccounts, removeController } from "$lib/api/tranquil";import { didFromUri } from "$lib/api/uri";import { forgetOrgDid } from "$lib/auth/accounts";
export type OrganizationRole = "owner" | "member";
export interface UserOrganization { did: Did; handle: string; description?: string; repos: number; role: OrganizationRole; joinedAt: string;}
export const roleForScopes = (grantedScopes: string): OrganizationRole => grantedScopes .split(/\s+/) .some((scope) => scope.startsWith("account:") && scope.includes("action=manage")) ? "owner" : "member";
const profileUriFor = (did: Did): string => `at://${did}/sh.tangled.actor.profile/self`;
interface OrganizationProfiles { profiles: Map<string, ProfileRecord>; repos: Map<string, number>;}
const emptyProfiles: OrganizationProfiles = { profiles: new Map(), repos: new Map() };
const fetchOrganizationProfiles = async ( ctx: BobbinContext, dids: readonly Did[], init?: XrpcRequestInit): Promise<OrganizationProfiles> => { const enriched = await enrich<RecordList<ProfileRecord>>( ctx, { xrpc: "sh.tangled.actor.getProfiles", params: { actors: dids.map(profileUriFor) }, enrich: [target(REPO_COUNT, ["items[].uri"])] }, init ); const profiles = new Map<string, ProfileRecord>(); for (const item of enriched.output.items) { profiles.set(didFromUri(item.uri), item.value); } const repos = new Map(dids.map((did) => [did, countOf(enriched.data, did, REPO_COUNT)])); return { profiles, repos };};
export const listUserOrganizations = async ( agent: OAuthUserAgent, ctx: BobbinContext, init?: XrpcRequestInit): Promise<UserOrganization[]> => { const accounts = await listControlledAccounts(agent, init); if (accounts.length === 0) return []; const dids = accounts.map((account) => account.did); // the profile record carries the description, the appview carries the repo // count; neither is essential, so the list still renders if bobbin is down const { profiles, repos } = await fetchOrganizationProfiles(ctx, dids, init).catch( () => emptyProfiles ); return accounts .filter((account) => profiles.get(account.did)?.isOrganization !== false) .map((account) => { const profile = profiles.get(account.did); return { did: account.did, handle: account.handle ?? account.did, description: profile?.description, repos: repos.get(account.did) ?? 0, role: roleForScopes(account.grantedScopes), joinedAt: account.grantedAt }; });};
export const leaveOrganization = async ( controllerAgent: OAuthUserAgent, orgDid: Did): Promise<void> => { const orgAgent = await getOrCreateDelegatedSession(controllerAgent, orgDid); await removeController(orgAgent, controllerAgent.sub as Did); // the grant is gone, so the org session is dead whether or not the pds // accepts the revocation await orgAgent.signOut().catch(() => deleteStoredSession(orgDid)); forgetOrgDid(orgDid);};