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<SignupError> => {
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<void> => {
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<void> => {
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<string, unknown>)[key] === "string"
);
export const completeSignup = async (
serviceUrl: string,
input: CompleteSignupInput,
fetchFn: Fetch = fetch
): Promise<CompletedSignup> => {
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 };
};