import { isDid, type Did } from "@atcute/lexicons/syntax"; import { buildUrl, toResponseError } from "#lib/api/_request.ts"; import { nsidOf } from "#lib/api/syntax.ts"; import type { AuthorizationResponse } from "#lib/auth.svelte.ts"; import { sentence } from "#lib/format.ts"; import type { Fetch } from "#lib/fetch.ts"; const BEGIN_SIGNUP = nsidOf("org.tangled.temp.account.beginSignup"); const RESEND_SIGNUP = nsidOf("org.tangled.temp.account.resendSignup"); const GET_SIGNUP = nsidOf("org.tangled.temp.account.getSignup"); const COMPLETE_SIGNUP = nsidOf("org.tangled.temp.account.completeSignup"); const SERVER_FAILURE = "Unable to reach Tangled. Try again in a moment."; export class SignupError extends Error { constructor( message: string, readonly code?: string ) { super(message); } } const signupError = async (response: Response, fallback: string): Promise => { if (response.status >= 500) return new SignupError(SERVER_FAILURE); const err = await toResponseError(response); // "XRPCError" is toResponseError's sentinel for non-JSON bodies, not a service code const code = err.error && err.error !== "XRPCError" ? err.error : undefined; if (err.description) return new SignupError(sentence(err.description), code); return new SignupError(code ?? fallback, code); }; // open endpoints: no service auth, the turnstile challenge guards beginSignup export const beginSignup = async ( serviceUrl: string, email: string, turnstileToken: string, fetchFn: Fetch = fetch ): Promise => { const response = await fetchFn(buildUrl(serviceUrl, BEGIN_SIGNUP), { method: "POST", headers: { "content-type": "application/json", accept: "application/json" }, body: JSON.stringify({ email, turnstileToken }) }); if (!response.ok) { throw await signupError( response, `Unable to send the link (HTTP ${response.status}). Try again in a moment.` ); } }; export const resendSignup = async ( serviceUrl: string, email: string, fetchFn: Fetch = fetch ): Promise => { const response = await fetchFn(buildUrl(serviceUrl, RESEND_SIGNUP), { method: "POST", headers: { "content-type": "application/json", accept: "application/json" }, body: JSON.stringify({ email }) }); if (!response.ok) { throw await signupError( response, `Unable to resend the email (HTTP ${response.status}). Try again in a moment.` ); } }; export const getSignup = async ( serviceUrl: string, token: string, fetchFn: Fetch = fetch ): Promise<{ email: string; pds?: string }> => { const response = await fetchFn(buildUrl(serviceUrl, GET_SIGNUP, { token }), { headers: { accept: "application/json" } }); if (!response.ok) { throw await signupError(response, `Unable to check the link (HTTP ${response.status}).`); } const body = (await response.json().catch(() => null)) as { email?: unknown; pds?: unknown; } | null; if (typeof body?.email !== "string") { throw new SignupError("Unable to check the link. Try again in a moment."); } // a deliberi without the field leaves the page to sign in through the pds return typeof body.pds === "string" ? { email: body.email, pds: body.pds } : { email: body.email }; }; export interface CompleteSignupInput { token: string; username: string; password: string; requestUri?: string; } export interface CompletedSignup { did: Did; handle: string; authorization?: AuthorizationResponse; } const isAuthorization = (value: unknown): value is AuthorizationResponse => !!value && typeof value === "object" && ["code", "state", "iss"].every( (key) => typeof (value as Record)[key] === "string" ); export const completeSignup = async ( serviceUrl: string, input: CompleteSignupInput, fetchFn: Fetch = fetch ): Promise => { const response = await fetchFn(buildUrl(serviceUrl, COMPLETE_SIGNUP), { method: "POST", headers: { "content-type": "application/json", accept: "application/json" }, body: JSON.stringify(input) }); if (!response.ok) { throw await signupError( response, `Unable to create the account (HTTP ${response.status}). Try again in a moment.` ); } // a successful signup must return both fields; anything else is a broken // response, and the pages must not render "Account created" for it const body = (await response.json().catch(() => null)) as { did?: unknown; handle?: unknown; authorization?: unknown; } | null; if (!isDid(body?.did) || typeof body.handle !== "string") { throw new Error("Unable to create the account. Try again in a moment."); } const { did, handle, authorization } = body; return isAuthorization(authorization) ? { did, handle, authorization } : { did, handle }; };