import { Client, ClientResponseError, ok, simpleFetchHandler } from "@atcute/client";
import { mainSchema as createSessionSchema } from "@atcute/atproto/types/server/createSession";
import { mainSchema as deleteAccountSchema } from "@atcute/atproto/types/server/deleteAccount";
import { mainSchema as deleteSessionSchema } from "@atcute/atproto/types/server/deleteSession";
import { mainSchema as requestAccountDeleteSchema } from "@atcute/atproto/types/server/requestAccountDelete";
import { mainSchema as requestPasswordResetSchema } from "@atcute/atproto/types/server/requestPasswordReset";
import { mainSchema as resetPasswordSchema } from "@atcute/atproto/types/server/resetPassword";
import type { Did } from "@atcute/lexicons/syntax";
import type { OAuthUserAgent } from "@atcute/oauth-browser-client";
import { sentence } from "#lib/format.ts";
const pdsClient = (service: string, token?: string): Client =>
new Client({
handler: simpleFetchHandler({
service,
fetch: (token
? (input, init) => {
const headers = new Headers(init?.headers);
headers.set("authorization", `Bearer ${token}`);
return fetch(input, { ...init, headers });
}
: fetch) as typeof fetch
})
});
const accountError = (cause: ClientResponseError, fallback: string): string => {
if (cause.error === "AuthenticationRequired" || cause.error === "AuthenticationFailed") {
return "Password is incorrect.";
}
if (cause.error === "AuthFactorTokenRequired") {
return "Your account uses two-factor sign in, which this page does not support yet.";
}
if (cause.error === "InvalidToken" || cause.error === "ExpiredToken") {
return "The code is invalid or has expired. Send a new code and try again.";
}
if (cause.description) return sentence(cause.description);
return fallback;
};
const guard = async <T>(fallback: string, operation: () => Promise<T>): Promise<T> => {
try {
return await operation();
} catch (cause) {
if (cause instanceof ClientResponseError) {
throw new Error(accountError(cause, fallback), { cause });
}
throw cause;
}
};
const withPasswordSession = async <T>(
agent: OAuthUserAgent,
password: string,
operation: (client: Client, session: { email?: string }) => Promise<T>
): Promise<T> => {
const service = agent.session.info.aud;
const session = await ok(
pdsClient(service).call(createSessionSchema, {
input: { identifier: agent.sub, password }
})
);
try {
return await operation(pdsClient(service, session.accessJwt), session);
} finally {
await pdsClient(service, session.refreshJwt)
.call(deleteSessionSchema)
.catch(() => undefined);
}
};
export const requestAccountDeletion = (agent: OAuthUserAgent, password: string): Promise<void> =>
guard("Could not send a deletion code. Try again.", () =>
withPasswordSession(agent, password, async (client) => {
await ok(client.call(requestAccountDeleteSchema));
})
);
export const deleteAccount = (
agent: OAuthUserAgent,
input: { password: string; token: string }
): Promise<Did> =>
guard("Could not delete your account. Try again.", async () => {
const did = agent.sub;
await ok(
pdsClient(agent.session.info.aud).call(deleteAccountSchema, {
input: { did, password: input.password, token: input.token.trim() }
})
);
return did;
});
export const requestPasswordReset = (agent: OAuthUserAgent, password: string): Promise<void> =>
guard("Could not send a reset code. Try again.", () =>
withPasswordSession(agent, password, async (_client, { email }) => {
if (!email) throw new Error("Your account has no email address to send a code to.");
await ok(
pdsClient(agent.session.info.aud).call(requestPasswordResetSchema, {
input: { email }
})
);
})
);
export const resetPassword = (
agent: OAuthUserAgent,
input: { token: string; password: string }
): Promise<void> =>
guard("Could not change your password. Try again.", async () => {
await ok(
pdsClient(agent.session.info.aud).call(resetPasswordSchema, {
input: { token: input.token.trim(), password: input.password }
})
);
});