Graphical PDS migrator for AT Protocol
1import { Agent } from "npm:@atproto/api";
2import { getIronSession, SessionOptions } from "npm:iron-session";
3import { createSessionOptions, CredentialSession } from "../types.ts";
4
5let migrationSessionOptions: SessionOptions;
6let credentialSessionOptions: SessionOptions;
7
8/**
9 * Get the session options for the given request.
10 * @param isMigration - Whether to get the migration session options
11 * @returns The session options
12 */
13async function getOptions(isMigration: boolean) {
14 if (isMigration) {
15 if (!migrationSessionOptions) {
16 migrationSessionOptions = await createSessionOptions("migration_sid");
17 }
18 return migrationSessionOptions;
19 }
20
21 if (!credentialSessionOptions) {
22 credentialSessionOptions = await createSessionOptions("cred_sid");
23 }
24 return credentialSessionOptions;
25}
26
27/**
28 * Get the credential session for the given request.
29 * @param req - The request object
30 * @param res - The response object
31 * @param isMigration - Whether to get the migration session
32 * @returns The credential session
33 */
34export async function getCredentialSession(
35 req: Request,
36 res: Response = new Response(),
37 isMigration: boolean = false,
38) {
39 const options = await getOptions(isMigration);
40 return getIronSession<CredentialSession>(req, res, options);
41}
42
43/**
44 * Get the credential agent for the given request.
45 * @param req - The request object
46 * @param res - The response object
47 * @param isMigration - Whether to get the migration session
48 * @returns The credential agent
49 */
50export async function getCredentialAgent(
51 req: Request,
52 res: Response = new Response(),
53 isMigration: boolean = false,
54) {
55 const session = await getCredentialSession(req, res, isMigration);
56 if (
57 !session.did ||
58 !session.service ||
59 !session.handle ||
60 !session.password
61 ) {
62 return null;
63 }
64
65 try {
66 console.log("Creating agent with service:", session.service);
67 const agent = new Agent({ service: session.service });
68
69 // Attempt to restore session by creating a new one
70 try {
71 console.log("Attempting to create session for:", session.handle);
72 const sessionRes = await agent.com.atproto.server.createSession({
73 identifier: session.handle,
74 password: session.password,
75 });
76 console.log("Session created successfully:", !!sessionRes);
77
78 // Set the auth tokens in the agent
79 agent.setHeader("Authorization", `Bearer ${sessionRes.data.accessJwt}`);
80
81 return agent;
82 } catch (err) {
83 // Session creation failed, clear the session
84 console.error("Failed to create session:", err);
85 session.destroy();
86 return null;
87 }
88 } catch (err) {
89 console.warn("Failed to create migration agent:", err);
90 session.destroy();
91 return null;
92 }
93}
94
95/**
96 * Set the credential session for the given request.
97 * @param req - The request object
98 * @param res - The response object
99 * @param data - The credential session data
100 * @param isMigration - Whether to set the migration session
101 * @returns The credential session
102 */
103export async function setCredentialSession(
104 req: Request,
105 res: Response,
106 data: CredentialSession,
107 isMigration: boolean = false,
108) {
109 const session = await getCredentialSession(req, res, isMigration);
110 session.did = data.did;
111 session.handle = data.handle;
112 session.service = data.service;
113 session.password = data.password;
114 await session.save();
115 return session;
116}
117
118/**
119 * Get the credential session agent for the given request.
120 * @param req - The request object
121 * @param res - The response object
122 * @param isMigration - Whether to get the migration session
123 * @returns The credential session agent
124 */
125export async function getCredentialSessionAgent(
126 req: Request,
127 res: Response = new Response(),
128 isMigration: boolean = false,
129) {
130 const session = await getCredentialSession(req, res, isMigration);
131
132 console.log("Session state:", {
133 hasDid: !!session.did,
134 hasService: !!session.service,
135 hasHandle: !!session.handle,
136 hasPassword: !!session.password,
137 hasAccessJwt: !!session.accessJwt,
138 service: session.service,
139 handle: session.handle,
140 });
141
142 if (
143 !session.did ||
144 !session.service ||
145 !session.handle ||
146 !session.password
147 ) {
148 console.log("Missing required session fields");
149 return null;
150 }
151
152 try {
153 console.log("Creating agent with service:", session.service);
154 const agent = new Agent({ service: session.service });
155
156 // If we have a stored JWT, try to use it first
157 if (session.accessJwt) {
158 console.log("Found stored JWT, attempting to use it");
159 agent.setHeader("Authorization", `Bearer ${session.accessJwt}`);
160 try {
161 // Verify the token is still valid
162 const sessionInfo = await agent.com.atproto.server.getSession();
163 console.log("Stored JWT is valid, session info:", {
164 did: sessionInfo.data.did,
165 handle: sessionInfo.data.handle,
166 });
167 return agent;
168 } catch (err) {
169 // Token expired or invalid, continue to create new session
170 const message = err instanceof Error ? err.message : String(err);
171 console.log("Stored token invalid or expired:", message);
172 }
173 }
174
175 // Create new session if no token or token expired
176 try {
177 console.log("Attempting to create session for:", session.handle);
178 const sessionRes = await agent.com.atproto.server.createSession({
179 identifier: session.handle,
180 password: session.password,
181 });
182 console.log("Session created successfully:", {
183 did: sessionRes.data.did,
184 handle: sessionRes.data.handle,
185 hasAccessJwt: !!sessionRes.data.accessJwt,
186 });
187
188 // Store the new token
189 session.accessJwt = sessionRes.data.accessJwt;
190 await session.save();
191 console.log("Saved new access token to session");
192
193 // Set the auth tokens in the agent
194 agent.setHeader("Authorization", `Bearer ${sessionRes.data.accessJwt}`);
195
196 // Verify the agent is properly authenticated
197 try {
198 await agent.com.atproto.server.getSession();
199 return agent;
200 } catch (err) {
201 console.error("Failed to verify agent authentication:", err);
202 session.destroy();
203 return null;
204 }
205 } catch (err) {
206 // Session creation failed, clear the session
207 const message = err instanceof Error ? err.message : String(err);
208 console.error("Failed to create session:", message);
209 session.destroy();
210 return null;
211 }
212 } catch (err) {
213 const message = err instanceof Error ? err.message : String(err);
214 console.warn("Failed to create migration agent:", message);
215 session.destroy();
216 return null;
217 }
218}