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<OrgAccount> => {
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<CreatedOrg> => {
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: [] };
};