~james/core

core/web/src/lib/api/delegation.ts -rw-r--r-- 3.8 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
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
import { ClientResponseError } from "@atcute/client";
import * as v from "@atcute/lexicons/validations";
import type { OAuthUserAgent } from "@atcute/oauth-browser-client";
import { hostForServiceDid, mintServiceAuth } from "#lib/auth/agent.ts";
import { buildUrl, toResponseError } from "#lib/api/_request.ts";
import { didOf, nsidOf } from "#lib/api/syntax.ts";
import { sentence } from "#lib/format.ts";
import {
	CREATE_DELEGATE_NSID,
	LIST_DELEGATES_ENDPOINT,
	LIST_DELEGATES_NSID,
	listDelegatesSchema
} from "#lib/api/tranquil.ts";
import type { Fetch } from "#lib/fetch.ts";

export { CREATE_DELEGATE_NSID, LIST_DELEGATES_NSID };

// The gate mints its own response, distinct from Tranquil's createAccount output.
const createdDelegateSchema = v.object({
	controllerDid: v.didString(),
	did: v.didString(),
	handle: v.handleString()
});

export type CreatedDelegate = v.InferInput<typeof createdDelegateSchema>;

export const GATE_CREATE_DELEGATE_NSID = nsidOf("org.tangled.delegation.createAccount");

export interface DelegationService {
	serviceUrl: string;
	serviceDid?: string;
	handleDomain?: string;
	fetch?: Fetch;
}

const GATE_ERRORS: Record<string, string> = {
	VerifiedEmailRequired:
		"Verify an email address in Settings → Emails before creating an organization.",
	UpstreamUnavailable:
		"Organization creation is temporarily unavailable. Try again in a few minutes.",
	HandleNotAvailable: "This organization name is taken. Choose another one.",
	InvalidHandle: "This organization name isn't allowed. Choose another one."
};

const gateError = (cause: ClientResponseError): string => {
	const known = GATE_ERRORS[cause.error];
	if (known) return known;
	if (cause.description) return sentence(cause.description);
	return "Unable to create the organization. Try again.";
};

export const provisionDelegate = async (
	agent: OAuthUserAgent,
	handle: string,
	service: DelegationService
): Promise<CreatedDelegate> => {
	if (!service.serviceUrl || !service.serviceDid)
		throw new Error("Organization creation isn't set up on this server.");
	let token: string;
	try {
		token = await mintServiceAuth(agent, {
			aud: didOf(service.serviceDid),
			lxm: CREATE_DELEGATE_NSID
		});
	} catch (cause) {
		if (cause instanceof ClientResponseError) {
			throw new Error(
				cause.status === 401 || cause.status === 403
					? "Sign in again to grant Tangled permission to create organizations."
					: "Unable to confirm your account with your PDS. Try again.",
				{ cause }
			);
		}
		throw cause;
	}
	const response = await (service.fetch ?? globalThis.fetch)(
		buildUrl(service.serviceUrl, GATE_CREATE_DELEGATE_NSID),
		{
			method: "POST",
			headers: { "content-type": "application/json", authorization: `Bearer ${token}` },
			body: JSON.stringify({ handle })
		}
	);
	if (!response.ok) {
		const cause = await toResponseError(response);
		throw new Error(gateError(cause), { cause });
	}
	const account = v.parse(createdDelegateSchema, await response.json());
	if (account.controllerDid !== agent.sub)
		throw new Error(
			"The organization was created under a different account than the one you chose. Sign out, sign back in, and try again."
		);
	return account;
};

// Remote controllers mint this token on their own PDS and list on Tranquil.
export const listDelegatedAccounts = async (
	agent: OAuthUserAgent,
	serviceDid: string,
	fetch: Fetch = globalThis.fetch
) => {
	const host = hostForServiceDid(serviceDid);
	if (!host) throw new Error("Organization accounts are not configured.");
	const token = await mintServiceAuth(agent, {
		aud: didOf(serviceDid),
		lxm: LIST_DELEGATES_NSID
	});
	const response = await fetch(buildUrl(`https://${host}`, LIST_DELEGATES_ENDPOINT), {
		headers: { authorization: `Bearer ${token}` }
	});
	if (!response.ok) throw await toResponseError(response);
	return v.parse(listDelegatesSchema, await response.json()).accounts;
};