import type { Did } from "@atcute/lexicons/syntax"; import { deleteStoredSession, type OAuthUserAgent } from "@atcute/oauth-browser-client"; import { getOrCreateDelegatedSession } from "$lib/api/actAs"; import { cached, invalidate } from "$lib/api/cache"; import type { BobbinContext, XrpcRequestInit } from "$lib/api/client"; import { listDelegatedAccounts } from "$lib/api/delegation"; import { PROFILE_COLLECTION } from "$lib/api/profile"; import { getProfiles, type ProfileRecord } from "$lib/api/records"; import { 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; role: OrganizationRole; joinedAt: string; } const MANAGE_ACCOUNT_SCOPE = "account:*?action=manage"; export const organizationRole = (grantedScopes: string): OrganizationRole => grantedScopes.split(" ").includes(MANAGE_ACCOUNT_SCOPE) ? "owner" : "member"; const controlledKey = (agent: OAuthUserAgent, serviceDid: string) => `controlled:${agent.sub}:${serviceDid}`; const controlledAccounts = (agent: OAuthUserAgent, serviceDid: string) => cached(controlledKey(agent, serviceDid), 30_000, () => listDelegatedAccounts(agent, serviceDid) ); const fetchOrganizationProfiles = async ( ctx: BobbinContext, dids: readonly Did[], init?: XrpcRequestInit ): Promise> => { const list = await getProfiles( ctx, dids.map((did) => `at://${did}/${PROFILE_COLLECTION}/self`), init ); return new Map(list.items.map((item) => [didFromUri(item.uri), item.value])); }; export const listUserOrganizations = async ( agent: OAuthUserAgent, ctx: BobbinContext, serviceDid: string, init?: XrpcRequestInit ): Promise => { const accounts = await controlledAccounts(agent, serviceDid); if (accounts.length === 0) return []; const dids = accounts.map((account) => account.did); const profiles = await fetchOrganizationProfiles(ctx, dids, init).catch( () => new Map() ); return accounts .filter((account) => profiles.get(account.did)?.isOrganization !== false) .map((account) => ({ did: account.did, handle: account.handle ?? account.did, role: organizationRole(account.grantedScopes), joinedAt: account.grantedAt })); }; export const viewerControlsOrganization = async ( agent: OAuthUserAgent, serviceDid: string, orgDid: string ): Promise => { const accounts = await controlledAccounts(agent, serviceDid); return accounts.some((account) => account.did === orgDid); }; export const leaveOrganization = async ( controllerAgent: OAuthUserAgent, orgDid: Did, serviceDid: string ): Promise => { const orgAgent = await getOrCreateDelegatedSession(controllerAgent, orgDid); await removeController(orgAgent, controllerAgent.sub as Did); invalidate(controlledKey(controllerAgent, serviceDid)); // 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); };