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;
};