Graphical PDS migrator for AT Protocol
1import { getSessionAgent } from "../../../lib/sessions.ts";
2import { setCredentialSession } from "../../../lib/cred/sessions.ts";
3import { Agent } from "@atproto/api";
4import { define } from "../../../utils.ts";
5import { assertMigrationAllowed } from "../../../lib/migration-state.ts";
6
7/**
8 * Handle account creation
9 * First step of the migration process
10 * Body must contain:
11 * - service: The service URL of the new account
12 * - handle: The handle of the new account
13 * - password: The password of the new account
14 * - email: The email of the new account
15 * - invite: The invite code of the new account (optional depending on the PDS)
16 * @param ctx - The context object containing the request and response
17 * @returns A response object with the creation result
18 */
19export const handler = define.handlers({
20 async POST(ctx) {
21 const res = new Response();
22 try {
23 // Check if migrations are currently allowed
24 assertMigrationAllowed();
25
26 const body = await ctx.req.json();
27 const serviceUrl = body.service;
28 const newHandle = body.handle;
29 const newPassword = body.password;
30 const email = body.email;
31 const inviteCode = body.invite;
32
33 if (!serviceUrl || !newHandle || !newPassword || !email) {
34 return new Response(
35 "Missing params service, handle, password, or email",
36 { status: 400 },
37 );
38 }
39
40 const oldAgent = await getSessionAgent(ctx.req, res);
41 const newAgent = new Agent({ service: serviceUrl });
42
43 if (!oldAgent) return new Response("Unauthorized", { status: 401 });
44 if (!newAgent) {
45 return new Response("Could not create new agent", { status: 400 });
46 }
47
48 console.log("getting did");
49 const session = await oldAgent.com.atproto.server.getSession();
50 const accountDid = session.data.did;
51 console.log("got did");
52 const describeRes = await newAgent.com.atproto.server.describeServer();
53 const newServerDid = describeRes.data.did;
54 const inviteRequired = describeRes.data.inviteCodeRequired ?? false;
55
56 if (inviteRequired && !inviteCode) {
57 return new Response("Missing param invite code", { status: 400 });
58 }
59
60 const serviceJwtRes = await oldAgent.com.atproto.server.getServiceAuth({
61 aud: newServerDid,
62 lxm: "com.atproto.server.createAccount",
63 });
64 const serviceJwt = serviceJwtRes.data.token;
65
66 await newAgent.com.atproto.server.createAccount(
67 {
68 handle: newHandle,
69 email: email,
70 password: newPassword,
71 did: accountDid,
72 inviteCode: inviteCode ?? undefined,
73 },
74 {
75 headers: { authorization: `Bearer ${serviceJwt}` },
76 encoding: "application/json",
77 },
78 );
79
80 // Store the migration session
81 await setCredentialSession(ctx.req, res, {
82 did: accountDid,
83 handle: newHandle,
84 service: serviceUrl,
85 password: newPassword,
86 }, true);
87
88 return new Response(
89 JSON.stringify({
90 success: true,
91 message: "Account created successfully",
92 did: accountDid,
93 handle: newHandle,
94 }),
95 {
96 status: 200,
97 headers: {
98 "Content-Type": "application/json",
99 ...Object.fromEntries(res.headers), // Include session cookie headers
100 },
101 },
102 );
103 } catch (error) {
104 console.error("Create account error:", error);
105 return new Response(
106 JSON.stringify({
107 success: false,
108 message: error instanceof Error
109 ? error.message
110 : "Failed to create account",
111 }),
112 {
113 status: 400,
114 headers: { "Content-Type": "application/json" },
115 },
116 );
117 }
118 },
119});