~james/core

core/web/src/lib/api/orgCreate.ts -rw-r--r-- 2.9 KiB
452f17b7 — james chore(import): snapshot james.dad.town/core at f70c71c7696a 15 hours ago
                                                                                
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
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: [] };
};