Something went wrong. Try again.
Monorepo for Tangled forked from tangled.org/core
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103import { isHandle, type ActorIdentifier, type Did } from "@atcute/lexicons/syntax";import { deleteStoredSession, type OAuthUserAgent } from "@atcute/oauth-browser-client";import { getOrCreateDelegatedSession } from "#lib/api/actAs.ts";import { cached, invalidate } from "#lib/api/cache.ts";import type { BobbinContext, XrpcRequestInit } from "#lib/api/client.ts";import { listDelegatedAccounts } from "#lib/api/delegation.ts";import { PROFILE_COLLECTION } from "#lib/api/profile.ts";import { getProfiles, type ProfileRecord } from "#lib/api/records.ts";import { removeController } from "#lib/api/tranquil.ts";import { didFromUri } from "#lib/api/uri.ts";import { forgetOrgDid } from "#lib/auth/accounts.ts";
export type OrganizationRole = "owner" | "member";
export interface UserOrganization { did: Did; handle: ActorIdentifier; 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<Map<string, ProfileRecord>> => { 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<UserOrganization[]> => { 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<string, ProfileRecord>() ); return accounts .filter((account) => profiles.get(account.did)?.isOrganization !== false) .map((account) => ({ did: account.did, handle: isHandle(account.handle) ? account.handle : account.did, role: organizationRole(account.grantedScopes), joinedAt: account.grantedAt }));};
export const viewerControlsOrganization = async ( agent: OAuthUserAgent, serviceDid: string, orgDid: string): Promise<boolean> => { const accounts = await controlledAccounts(agent, serviceDid); return accounts.some((account) => account.did === orgDid);};
export const viewerOwnsOrganization = async ( agent: OAuthUserAgent, serviceDid: string, orgDid: string): Promise<boolean> => { const accounts = await controlledAccounts(agent, serviceDid); return accounts.some( (account) => account.did === orgDid && organizationRole(account.grantedScopes) === "owner" );};
export const leaveOrganization = async ( controllerAgent: OAuthUserAgent, orgDid: Did, serviceDid: string): Promise<void> => { 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);};