Something went wrong. Try again.
Monorepo for Tangled
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191import type { Did } from "@atcute/lexicons/syntax";import type { OAuthUserAgent } from "@atcute/oauth-browser-client";import { getOrCreateDelegatedSession } from "$lib/api/actAs";import { uploadProfileAvatar, putProfile } from "$lib/api/profile";import type { ProfileRecord } from "$lib/api/records";import { isValidSubdomain, SUBDOMAIN_MAX_LENGTH, SUBDOMAIN_MIN_LENGTH } from "$lib/subdomain";import { addController, createDelegatedAccount, getScopePresets, listControlledAccounts} from "$lib/api/tranquil";
// TODO: change thisexport const ORG_HANDLE_SUFFIX = ".tranquil.tngl.boltless.dev";
export interface OrgMember { did: Did; handle: string;}
export interface OrgProfileInput { description?: string; website?: string; avatarFile?: File;}
export interface OrgCreationInput extends OrgProfileInput { handle: string; members: OrgMember[];}
export interface AddOrgMembersInput { ownerDid: Did; members: OrgMember[];}
export interface OrgAccount { did: Did; handle: string;}
export interface CreatedOrg extends OrgAccount { warnings: string[];}
const validateOrgHandle = (raw: string): string => { const handle = raw.trim().toLowerCase(); if (handle.length < SUBDOMAIN_MIN_LENGTH || handle.length > SUBDOMAIN_MAX_LENGTH) { throw new Error( `Organization handle must be ${SUBDOMAIN_MIN_LENGTH}-${SUBDOMAIN_MAX_LENGTH} characters.` ); } if (!isValidSubdomain(handle)) { throw new Error( "Organization handle can only contain lowercase letters, digits, and hyphens, and cannot start or end with a hyphen." ); } return handle;};
type ProfileLink = NonNullable<ProfileRecord["links"]>[number];
const validateWebsite = (raw: string | undefined): ProfileLink | undefined => { const website = raw?.trim(); if (!website) return undefined; let url: URL; try { url = new URL(website); } catch { throw new Error("Website must be a valid URL."); } if (url.protocol !== "http:" && url.protocol !== "https:") { throw new Error("Website must use http or https."); } return website as ProfileLink;};
export const createOrgAccount = async ( agent: OAuthUserAgent, rawHandle: string): Promise<OrgAccount> => { const handle = validateOrgHandle(rawHandle); const fullHandle = `${handle}${ORG_HANDLE_SUFFIX}`;
const presets = await getScopePresets(agent); const ownerPreset = presets.find((preset) => preset.name === "owner"); if (!ownerPreset) { throw new Error('tranquil has no "owner" scope preset available.'); } const controlled = await listControlledAccounts(agent); const existing = controlled.find((account) => account.handle === fullHandle); if (existing) { throw new Error(`The organization handle ${fullHandle} is already in use.`); } const account = await createDelegatedAccount(agent, { handle: fullHandle, controllerScopes: ownerPreset.scopes }); return { did: account.did, handle: fullHandle };};
export const writeOrgProfile = async ( orgAgent: OAuthUserAgent, input: OrgProfileInput): Promise<void> => { const description = input.description?.trim() || undefined; const website = validateWebsite(input.website); const avatar = input.avatarFile ? await uploadProfileAvatar(orgAgent, input.avatarFile) : undefined;
const record: ProfileRecord = { $type: "sh.tangled.actor.profile", bluesky: false, isOrganization: true, ...(avatar ? { avatar } : {}), ...(description ? { description } : {}), ...(website ? { links: [website] } : {}) }; await putProfile(orgAgent, record);};
export const addOrgMembers = async ( orgAgent: OAuthUserAgent, input: AddOrgMembersInput): Promise<string[]> => { const members = input.members.filter((member) => member.did !== input.ownerDid); if (members.length === 0) return [];
const presets = await getScopePresets(orgAgent); const memberPreset = presets.find((preset) => preset.name === "editor"); if (!memberPreset) { throw new Error('tranquil has no "editor" scope preset available.'); }
const results = await Promise.allSettled( members.map((member) => addController(orgAgent, { controllerDid: member.did, grantedScopes: memberPreset.scopes }) ) ); return results.flatMap((result, i) => { if (result.status === "fulfilled") return []; return [`Unable to add ${members[i].handle} as a member. Try again later.`]; });};
export const createOrg = async ( controllerAgent: OAuthUserAgent, input: OrgCreationInput): Promise<CreatedOrg> => { // Validate the website before creating an account we cannot roll back. validateWebsite(input.website); const org = await createOrgAccount(controllerAgent, input.handle);
let orgAgent: OAuthUserAgent; try { orgAgent = await getOrCreateDelegatedSession(controllerAgent, org.did); } catch { return { ...org, warnings: [ "Unable to authorize the organization, so its profile and members were not saved. Try again later." ] }; }
const warnings: string[] = []; try { await writeOrgProfile(orgAgent, input); } catch { warnings.push("Unable to save the organization's profile. Try again later."); }
try { warnings.push( ...(await addOrgMembers(orgAgent, { ownerDid: controllerAgent.sub, members: input.members })) ); } catch { warnings.push("Unable to add members. Try again later."); }
return { ...org, warnings };};