import { ClientResponseError } from "@atcute/client"; import type { Did } from "@atcute/lexicons/syntax"; import type { OAuthUserAgent } from "@atcute/oauth-browser-client"; import { getOrCreateDelegatedSession } from "#lib/api/actAs.ts"; import { provisionDelegate, type DelegationService } from "#lib/api/delegation.ts"; import { ensureProfile, PROFILE_COLLECTION } from "#lib/api/profile.ts"; import type { ProfileRecord } from "#lib/api/records.ts"; import { isValidSubdomain, SUBDOMAIN_MAX_LENGTH, SUBDOMAIN_MIN_LENGTH } from "#lib/subdomain.ts"; export const ORG_PROFILE_MARKER: ProfileRecord = { $type: PROFILE_COLLECTION, isOrganization: true }; export interface OrgAccount { did: Did; handle: string; } export interface CreatedOrg extends OrgAccount { warnings: string[]; } const validateOrgHandle = (raw: string, handleDomain: string | undefined): string => { if (!handleDomain) throw new Error("Organization creation isn't set up on this server."); const label = raw.trim().toLowerCase(); if (label.length < SUBDOMAIN_MIN_LENGTH || label.length > SUBDOMAIN_MAX_LENGTH) { throw new Error( `Use ${SUBDOMAIN_MIN_LENGTH}–${SUBDOMAIN_MAX_LENGTH} characters for the organization name.` ); } if (!isValidSubdomain(label)) { throw new Error( "Use only lowercase letters, digits, and hyphens in the organization name, and start and end it with a letter or digit." ); } return `${label}.${handleDomain}`; }; export const createOrgAccount = async ( agent: OAuthUserAgent, rawHandle: string, service: DelegationService ): Promise => { const handle = validateOrgHandle(rawHandle, service.handleDomain); const account = await provisionDelegate(agent, handle, service); return { did: account.did, handle: account.handle }; }; const SETUP_FAILURES = { "organization session": "Unable to sign in to the organization account", "profile write": "Unable to save the organization's profile" } as const; const detailOf = (cause: unknown): string => { const detail = cause instanceof ClientResponseError ? (cause.description ?? cause.error) : cause instanceof Error ? cause.message : String(cause); return detail.trim().replace(/\.$/, ""); }; export const createOrg = async ( controllerAgent: OAuthUserAgent, rawHandle: string, service: DelegationService, existingOrg?: OrgAccount ): Promise => { const org = existingOrg ?? (await createOrgAccount(controllerAgent, rawHandle, service)); let step: keyof typeof SETUP_FAILURES = "organization session"; try { const orgAgent = await getOrCreateDelegatedSession(controllerAgent, org.did).catch(() => getOrCreateDelegatedSession(controllerAgent, org.did) ); step = "profile write"; await ensureProfile(orgAgent, ORG_PROFILE_MARKER); } catch (cause) { console.error(`organization setup failed at ${step}`, org.did, cause); return { ...org, warnings: [`${SETUP_FAILURES[step]} (${detailOf(cause)}).`] }; } return { ...org, warnings: [] }; };