···11-import { docResolver, cleanHandle, handleResolver, handleAndPDSResolver } from './atprotoUtils.js'
22-import { AtpAgent } from '@atproto/api'
11+import {docResolver, cleanHandle, handleResolver, handleAndPDSResolver} from './atprotoUtils.js'
22+import {AtpAgent} from '@atproto/api'
3344function safeStatusUpdate(statusUpdateHandler, status) {
55- if (statusUpdateHandler) {
66- statusUpdateHandler(status)
77- }
55+ if (statusUpdateHandler) {
66+ statusUpdateHandler(status)
77+ }
88}
991010/**
···1212 * On pdsmoover.com this is the logic for the MOOver
1313 */
1414class Migrator {
1515- constructor() {
1616- /** @type {AtpAgent} */
1717- this.oldAgent = null
1818- /** @type {AtpAgent} */
1919- this.newAgent = null
2020- /** @type {[string]} */
2121- this.missingBlobs = []
2222- //State for reruns
2323- /** @type {boolean} */
2424- this.createNewAccount = true
2525- /** @type {boolean} */
2626- this.migrateRepo = true
2727- /** @type {boolean} */
2828- this.migrateBlobs = true
2929- /** @type {boolean} */
3030- this.migrateMissingBlobs = true
3131- /** @type {boolean} */
3232- this.migratePrefs = true
3333- /** @type {boolean} */
3434- this.migratePlcRecord = true
3535- /**
3636- * How many blobs have been uploaded to the new PDS in the current step
3737- @type {number} */
3838- this.uploadedBlobsCount = 0
3939- }
1515+ constructor() {
1616+ /** @type {AtpAgent} */
1717+ this.oldAgent = null
1818+ /** @type {AtpAgent} */
1919+ this.newAgent = null
2020+ /** @type {[string]} */
2121+ this.missingBlobs = []
2222+ //State for reruns
2323+ /** @type {boolean} */
2424+ this.createNewAccount = true
2525+ /** @type {boolean} */
2626+ this.migrateRepo = true
2727+ /** @type {boolean} */
2828+ this.migrateBlobs = true
2929+ /** @type {boolean} */
3030+ this.migrateMissingBlobs = true
3131+ /** @type {boolean} */
3232+ this.migratePrefs = true
3333+ /** @type {boolean} */
3434+ this.migratePlcRecord = true
3535+ /**
3636+ * How many blobs have been uploaded to the new PDS in the current step
3737+ @type {number} */
3838+ this.uploadedBlobsCount = 0
3939+ }
40404141- /**
4242- * Uploads blobs to the new PDS
4343- * @param {AtpAgent} oldAgent
4444- * @param {AtpAgent} newAgent
4545- * @param {string} usersDid
4646- * @param {[string]} cids
4747- * @param {number} totalBlobs
4848- * @param {function|null} statusUpdateHandler
4949- */
5050- async uploadBlobs(oldAgent, newAgent, usersDid, cids, totalBlobs, statusUpdateHandler) {
5151- for (const cid of cids) {
5252- try {
5353- const blobRes = await oldAgent.com.atproto.sync.getBlob({
5454- did: usersDid,
5555- cid,
5656- })
5757- await newAgent.com.atproto.repo.uploadBlob(blobRes.data, {
5858- encoding: blobRes.headers['content-type'],
5959- })
6060- this.uploadedBlobsCount++
6161- if (this.uploadedBlobsCount % 10 === 0) {
6262- safeStatusUpdate(
6363- statusUpdateHandler,
6464- `Migrating blobs: ${this.uploadedBlobsCount}/${totalBlobs}`,
6565- )
4141+ /**
4242+ * Uploads blobs to the new PDS
4343+ * @param {AtpAgent} oldAgent
4444+ * @param {AtpAgent} newAgent
4545+ * @param {string} usersDid
4646+ * @param {[string]} cids
4747+ * @param {number} totalBlobs
4848+ * @param {function|null} statusUpdateHandler
4949+ */
5050+ async uploadBlobs(oldAgent, newAgent, usersDid, cids, totalBlobs, statusUpdateHandler) {
5151+ for (const cid of cids) {
5252+ try {
5353+ const blobRes = await oldAgent.com.atproto.sync.getBlob({
5454+ did: usersDid,
5555+ cid,
5656+ })
5757+ await newAgent.com.atproto.repo.uploadBlob(blobRes.data, {
5858+ encoding: blobRes.headers['content-type'],
5959+ })
6060+ this.uploadedBlobsCount++
6161+ if (this.uploadedBlobsCount % 10 === 0) {
6262+ safeStatusUpdate(
6363+ statusUpdateHandler,
6464+ `Migrating blobs: ${this.uploadedBlobsCount}/${totalBlobs}`,
6565+ )
6666+ }
6767+ } catch (error) {
6868+ console.error(error)
6969+ }
6670 }
6767- } catch (error) {
6868- console.error(error)
6969- }
7071 }
7171- }
72727373- /**
7474- * This migrator is pretty cut and dry and makes a few assumptions
7575- * 1. You are using the same password between each account
7676- * 2. If this command fails for something like oauth 2fa code it throws an error and expects the same values when ran again.
7777- * 3. You can control which "actions" happen by setting the class variables to false.
7878- * 4. Each instance of the class is assumed to be for a single migration
7979- * @param {string} oldHandle - The handle you use on your old pds, something like alice.bsky.social
8080- * @param {string} password - Your password for your current login. Has to be your real password, no app password. When setting up a new account we reuse it as well for that account
8181- * @param {string} newPdsUrl - The new URL for your pds. Like https://coolnewpds.com
8282- * @param {string} newEmail - The email you want to use on the new pds (can be the same as the previous one as long as it's not already being used on the new pds)
8383- * @param {string} newHandle - The new handle you want, like alice.bsky.social, or if you already have a domain name set as a handle can use it myname.com.
8484- * @param {string|null} inviteCode - The invite code you got from the PDS you are migrating to. If null does not include one
8585- * @param {function|null} statusUpdateHandler - a function that takes a string used to update the UI. Like (status) => console.log(status)
8686- * @param {string|null} twoFactorCode - Optional, but needed if it fails with 2fa required
8787- * @param verificationCode - Optional verification captcha code for account creation if the PDS requires it
8888- */
8989- async migrate(
9090- oldHandle,
9191- password,
9292- newPdsUrl,
9393- newEmail,
9494- newHandle,
9595- inviteCode,
9696- statusUpdateHandler = null,
9797- twoFactorCode = null,
9898- verificationCode = null,
9999- ) {
100100- oldHandle = cleanHandle(oldHandle)
101101- let oldAgent
102102- let usersDid
103103- //If it's a bsky handle just go with the entryway and let it sort everything
104104- if (oldHandle.endsWith('.bsky.social')) {
105105- oldAgent = new AtpAgent({ service: 'https://bsky.social' })
106106- const publicAgent = new AtpAgent({
107107- service: 'https://public.api.bsky.app',
108108- })
109109- const resolveIdentityFromEntryway = await publicAgent.com.atproto.identity.resolveHandle({
110110- handle: oldHandle,
111111- })
112112- usersDid = resolveIdentityFromEntryway.data.did
113113- } else {
114114- //Resolves the did and finds the did document for the old PDS
115115- safeStatusUpdate(statusUpdateHandler, 'Resolving old PDS')
116116- let { usersDid: didFromLookUp, pds: oldPds } = await handleAndPDSResolver(oldHandle)
117117- usersDid = didFromLookUp
7373+ /**
7474+ * This migrator is pretty cut and dry and makes a few assumptions
7575+ * 1. You are using the same password between each account
7676+ * 2. If this command fails for something like oauth 2fa code it throws an error and expects the same values when ran again.
7777+ * 3. You can control which "actions" happen by setting the class variables to false.
7878+ * 4. Each instance of the class is assumed to be for a single migration
7979+ * @param {string} oldHandle - The handle you use on your old pds, something like alice.bsky.social
8080+ * @param {string} password - Your password for your current login. Has to be your real password, no app password. When setting up a new account we reuse it as well for that account
8181+ * @param {string} newPdsUrl - The new URL for your pds. Like https://coolnewpds.com
8282+ * @param {string} newEmail - The email you want to use on the new pds (can be the same as the previous one as long as it's not already being used on the new pds)
8383+ * @param {string} newHandle - The new handle you want, like alice.bsky.social, or if you already have a domain name set as a handle can use it myname.com.
8484+ * @param {string|null} inviteCode - The invite code you got from the PDS you are migrating to. If null does not include one
8585+ * @param {function|null} statusUpdateHandler - a function that takes a string used to update the UI. Like (status) => console.log(status)
8686+ * @param {string|null} twoFactorCode - Optional, but needed if it fails with 2fa required
8787+ * @param verificationCode - Optional verification captcha code for account creation if the PDS requires it
8888+ * @param {string|null} sourcePdsUrl - Optional URL to use as the source PDS instead of resolving from DID doc. Useful for moving from one PDS to another without changing the PDS hostname in the diddoc
8989+ */
9090+ async migrate(
9191+ oldHandle,
9292+ password,
9393+ newPdsUrl,
9494+ newEmail,
9595+ newHandle,
9696+ inviteCode,
9797+ statusUpdateHandler = null,
9898+ twoFactorCode = null,
9999+ verificationCode = null,
100100+ sourcePdsUrl = null,
101101+ ) {
102102+ oldHandle = cleanHandle(oldHandle)
103103+ let oldAgent
104104+ let usersDid
105105+ if (sourcePdsUrl) {
106106+ // Use the provided source PDS URL instead of resolving from DID doc
107107+ safeStatusUpdate(statusUpdateHandler, `Using provided source PDS: ${sourcePdsUrl}`)
108108+ console.log(`Using provided source PDS: ${sourcePdsUrl}`)
109109+ usersDid = await handleResolver.resolve(oldHandle)
110110+ oldAgent = new AtpAgent({service: sourcePdsUrl})
111111+ } else if (oldHandle.endsWith('.bsky.social')) {
112112+ //If it's a bsky handle just go with the entryway and let it sort everything
113113+ oldAgent = new AtpAgent({service: 'https://bsky.social'})
114114+ const publicAgent = new AtpAgent({
115115+ service: 'https://public.api.bsky.app',
116116+ })
117117+ const resolveIdentityFromEntryway = await publicAgent.com.atproto.identity.resolveHandle({
118118+ handle: oldHandle,
119119+ })
120120+ usersDid = resolveIdentityFromEntryway.data.did
121121+ } else {
122122+ //Resolves the did and finds the did document for the old PDS
123123+ safeStatusUpdate(statusUpdateHandler, 'Resolving old PDS')
124124+ let {usersDid: didFromLookUp, pds: oldPds} = await handleAndPDSResolver(oldHandle)
125125+ usersDid = didFromLookUp
118126119119- oldAgent = new AtpAgent({
120120- service: oldPds,
121121- })
122122- }
127127+ oldAgent = new AtpAgent({
128128+ service: oldPds,
129129+ })
130130+ }
123131124124- safeStatusUpdate(statusUpdateHandler, 'Logging you in to the old PDS')
125125- //Login to the old PDS
126126- if (twoFactorCode === null) {
127127- await oldAgent.login({ identifier: oldHandle, password })
128128- } else {
129129- await oldAgent.login({
130130- identifier: oldHandle,
131131- password: password,
132132- authFactorToken: twoFactorCode,
133133- })
134134- }
132132+ safeStatusUpdate(statusUpdateHandler, 'Logging you in to the old PDS')
133133+ //Login to the old PDS
134134+ if (twoFactorCode === null) {
135135+ await oldAgent.login({identifier: oldHandle, password})
136136+ } else {
137137+ await oldAgent.login({
138138+ identifier: oldHandle,
139139+ password: password,
140140+ authFactorToken: twoFactorCode,
141141+ })
142142+ }
135143136136- safeStatusUpdate(
137137- statusUpdateHandler,
138138- 'Checking that the new PDS is an actual PDS (if the url is wrong this takes a while to error out)',
139139- )
140140- const newAgent = new AtpAgent({ service: newPdsUrl })
141141- const newHostDesc = await newAgent.com.atproto.server.describeServer()
144144+ safeStatusUpdate(
145145+ statusUpdateHandler,
146146+ 'Checking that the new PDS is an actual PDS (if the url is wrong this takes a while to error out)',
147147+ )
148148+ console.log('New PDS URL:', newPdsUrl)
149149+ const newAgent = new AtpAgent({service: newPdsUrl})
150150+ const newHostDesc = await newAgent.com.atproto.server.describeServer()
142151143143- if (this.createNewAccount) {
144144- let needToCreateANewAccount = true
145145- //check to see if repo already exists
146146- try {
147147- // If successful at all means the repo is there
148148- const _ = await newAgent.com.atproto.sync.getRepoStatus({
149149- did: usersDid,
150150- })
151151- needToCreateANewAccount = false
152152- // Sets the migrate blobs flag to false so it moves on to just migrate missing blobs in the event of a retry
153153- this.migrateBlobs = false
154154- console.log('New check. Repo already exists, logging in')
155155- } catch (error) {
156156- //Should be good to cont, just logging in case we need it in the future for troubleshooting
157157- console.error('Expected Error on RepoStatus check.', error)
158158- }
152152+ if (this.createNewAccount) {
153153+ let needToCreateANewAccount = true
154154+ //check to see if repo already exists
155155+ try {
156156+ // If successful at all means the repo is there
157157+ const _ = await newAgent.com.atproto.sync.getRepoStatus({
158158+ did: usersDid,
159159+ })
160160+ needToCreateANewAccount = false
161161+ // Sets the migrate blobs flag to false so it moves on to just migrate missing blobs in the event of a retry
162162+ this.migrateBlobs = false
163163+ console.log('New check. Repo already exists, logging in')
164164+ } catch (error) {
165165+ //Should be good to cont, just logging in case we need it in the future for troubleshooting
166166+ console.error('Expected Error on RepoStatus check.', error)
167167+ }
159168160160- if (needToCreateANewAccount) {
161161- const newHostWebDid = newHostDesc.data.did
169169+ if (needToCreateANewAccount) {
170170+ const newHostWebDid = newHostDesc.data.did
162171163163- safeStatusUpdate(statusUpdateHandler, 'Creating a new account on the new PDS')
172172+ safeStatusUpdate(statusUpdateHandler, 'Creating a new account on the new PDS')
164173165165- const createAuthResp = await oldAgent.com.atproto.server.getServiceAuth({
166166- aud: newHostWebDid,
167167- lxm: 'com.atproto.server.createAccount',
168168- })
169169- const serviceJwt = createAuthResp.data.token
174174+ const createAuthResp = await oldAgent.com.atproto.server.getServiceAuth({
175175+ aud: newHostWebDid,
176176+ lxm: 'com.atproto.server.createAccount',
177177+ })
178178+ const serviceJwt = createAuthResp.data.token
170179171171- let createAccountRequest = {
172172- did: usersDid,
173173- handle: newHandle,
174174- email: newEmail,
175175- password: password,
176176- }
177177- if (inviteCode) {
178178- createAccountRequest.inviteCode = inviteCode
179179- }
180180- if (verificationCode) {
181181- createAccountRequest.verificationCode = verificationCode
182182- }
183183- try {
184184- const createNewAccount = await newAgent.com.atproto.server.createAccount(
185185- createAccountRequest,
186186- {
187187- headers: { authorization: `Bearer ${serviceJwt}` },
188188- encoding: 'application/json',
189189- },
190190- )
180180+ let createAccountRequest = {
181181+ did: usersDid,
182182+ handle: newHandle,
183183+ email: newEmail,
184184+ password: password,
185185+ }
186186+ if (inviteCode) {
187187+ createAccountRequest.inviteCode = inviteCode
188188+ }
189189+ if (verificationCode) {
190190+ createAccountRequest.verificationCode = verificationCode
191191+ }
192192+ try {
193193+ const createNewAccount = await newAgent.com.atproto.server.createAccount(
194194+ createAccountRequest,
195195+ {
196196+ headers: {authorization: `Bearer ${serviceJwt}`},
197197+ encoding: 'application/json',
198198+ },
199199+ )
191200192192- if (createNewAccount.data.did !== usersDid.toString()) {
193193- throw new Error('Did not create the new account with the same did as the old account')
194194- }
195195- } catch (error) {
196196- // Ideally should catch if the repo already exists, and if so silently log it and move along to the next step
197197- if (error?.error === 'AlreadyExists') {
198198- // Sets the migrate blobs flag to false so it moves on to just migrate missing blobs in the event of a retry
199199- this.migrateBlobs = false
200200- console.log('Repo already exists, logging in')
201201- } else {
202202- // Catches any other error and stops the migration process
203203- throw error
204204- }
201201+ if (createNewAccount.data.did !== usersDid.toString()) {
202202+ throw new Error('Did not create the new account with the same did as the old account')
203203+ }
204204+ } catch (error) {
205205+ // Ideally should catch if the repo already exists, and if so silently log it and move along to the next step
206206+ if (error?.error === 'AlreadyExists') {
207207+ // Sets the migrate blobs flag to false so it moves on to just migrate missing blobs in the event of a retry
208208+ this.migrateBlobs = false
209209+ console.log('Repo already exists, logging in')
210210+ } else {
211211+ // Catches any other error and stops the migration process
212212+ throw error
213213+ }
214214+ }
215215+ }
205216 }
206206- }
207207- }
208217209209- safeStatusUpdate(statusUpdateHandler, 'Logging in with the new account')
218218+ safeStatusUpdate(statusUpdateHandler, 'Logging in with the new account')
210219211211- await newAgent.login({
212212- identifier: usersDid,
213213- password: password,
214214- })
220220+ await newAgent.login({
221221+ identifier: usersDid,
222222+ password: password,
223223+ })
215224216216- if (this.migrateRepo) {
217217- safeStatusUpdate(statusUpdateHandler, 'Migrating your repo')
218218- const repoRes = await oldAgent.com.atproto.sync.getRepo({
219219- did: usersDid,
220220- })
221221- await newAgent.com.atproto.repo.importRepo(repoRes.data, {
222222- encoding: 'application/vnd.ipld.car',
223223- })
224224- }
225225+ if (this.migrateRepo) {
226226+ safeStatusUpdate(statusUpdateHandler, 'Migrating your repo')
227227+ const repoRes = await oldAgent.com.atproto.sync.getRepo({
228228+ did: usersDid,
229229+ })
230230+ await newAgent.com.atproto.repo.importRepo(repoRes.data, {
231231+ encoding: 'application/vnd.ipld.car',
232232+ })
233233+ }
225234226226- let newAccountStatus = await newAgent.com.atproto.server.checkAccountStatus()
227227-228228- if (this.migrateBlobs) {
229229- safeStatusUpdate(statusUpdateHandler, 'Migrating your blobs')
235235+ let newAccountStatus = await newAgent.com.atproto.server.checkAccountStatus()
230236231231- let blobCursor = undefined
232232- let uploadedBlobs = 0
233233- do {
234234- safeStatusUpdate(
235235- statusUpdateHandler,
236236- `Migrating blobs: ${uploadedBlobs}/${newAccountStatus.data.expectedBlobs}`,
237237- )
237237+ if (this.migrateBlobs) {
238238+ safeStatusUpdate(statusUpdateHandler, 'Migrating your blobs')
238239239239- const listedBlobs = await oldAgent.com.atproto.sync.listBlobs({
240240- did: usersDid,
241241- cursor: blobCursor,
242242- limit: 100,
243243- })
240240+ let blobCursor = undefined
241241+ let uploadedBlobs = 0
242242+ do {
243243+ safeStatusUpdate(
244244+ statusUpdateHandler,
245245+ `Migrating blobs: ${uploadedBlobs}/${newAccountStatus.data.expectedBlobs}`,
246246+ )
244247245245- await this.uploadBlobs(
246246- oldAgent,
247247- newAgent,
248248- usersDid,
249249- listedBlobs.data.cids,
250250- newAccountStatus.data.expectedBlobs,
251251- statusUpdateHandler,
252252- )
253253- blobCursor = listedBlobs.data.cursor
254254- } while (blobCursor)
255255- // Resets since this is a shared state with missing blobs job
256256- this.uploadedBlobsCount = 0
257257- }
248248+ const listedBlobs = await oldAgent.com.atproto.sync.listBlobs({
249249+ did: usersDid,
250250+ cursor: blobCursor,
251251+ limit: 100,
252252+ })
258253259259- if (this.migrateMissingBlobs) {
260260- newAccountStatus = await newAgent.com.atproto.server.checkAccountStatus()
261261- if (newAccountStatus.data.expectedBlobs !== newAccountStatus.data.importedBlobs) {
262262- let totalMissingBlobs =
263263- newAccountStatus.data.expectedBlobs - newAccountStatus.data.importedBlobs
264264- safeStatusUpdate(
265265- statusUpdateHandler,
266266- 'Looks like there are some missing blobs. Going to try and upload them now.',
267267- )
268268- //Probably should be shared between main blob uploader, but eh
269269- let missingBlobCursor = undefined
270270- let missingUploadedBlobs = 0
271271- do {
272272- safeStatusUpdate(
273273- statusUpdateHandler,
274274- `Migrating blobs: ${missingUploadedBlobs}/${totalMissingBlobs}`,
275275- )
254254+ await this.uploadBlobs(
255255+ oldAgent,
256256+ newAgent,
257257+ usersDid,
258258+ listedBlobs.data.cids,
259259+ newAccountStatus.data.expectedBlobs,
260260+ statusUpdateHandler,
261261+ )
262262+ blobCursor = listedBlobs.data.cursor
263263+ } while (blobCursor)
264264+ // Resets since this is a shared state with missing blobs job
265265+ this.uploadedBlobsCount = 0
266266+ }
276267277277- const missingBlobs = await newAgent.com.atproto.repo.listMissingBlobs({
278278- cursor: missingBlobCursor,
279279- limit: 100,
280280- })
268268+ if (this.migrateMissingBlobs) {
269269+ newAccountStatus = await newAgent.com.atproto.server.checkAccountStatus()
270270+ if (newAccountStatus.data.expectedBlobs !== newAccountStatus.data.importedBlobs) {
271271+ let totalMissingBlobs =
272272+ newAccountStatus.data.expectedBlobs - newAccountStatus.data.importedBlobs
273273+ safeStatusUpdate(
274274+ statusUpdateHandler,
275275+ 'Looks like there are some missing blobs. Going to try and upload them now.',
276276+ )
277277+ //Probably should be shared between main blob uploader, but eh
278278+ let missingBlobCursor = undefined
279279+ let missingUploadedBlobs = 0
280280+ do {
281281+ safeStatusUpdate(
282282+ statusUpdateHandler,
283283+ `Migrating blobs: ${missingUploadedBlobs}/${totalMissingBlobs}`,
284284+ )
281285282282- let missingCids = missingBlobs.data.blobs.map(blob => blob.cid)
283283- await this.uploadBlobs(
284284- oldAgent,
285285- newAgent,
286286- usersDid,
287287- missingCids,
288288- totalMissingBlobs,
289289- statusUpdateHandler,
290290- )
286286+ const missingBlobs = await newAgent.com.atproto.repo.listMissingBlobs({
287287+ cursor: missingBlobCursor,
288288+ limit: 100,
289289+ })
291290292292- missingBlobCursor = missingBlobs.data.cursor
293293- } while (missingBlobCursor)
294294- // Resets since this is a shared state with the migrate blobs job
295295- this.uploadedBlobsCount = 0
296296- }
297297- }
298298- if (this.migratePrefs) {
299299- const prefs = await oldAgent.app.bsky.actor.getPreferences()
300300- await newAgent.app.bsky.actor.putPreferences(prefs.data)
301301- }
291291+ let missingCids = missingBlobs.data.blobs.map(blob => blob.cid)
292292+ await this.uploadBlobs(
293293+ oldAgent,
294294+ newAgent,
295295+ usersDid,
296296+ missingCids,
297297+ totalMissingBlobs,
298298+ statusUpdateHandler,
299299+ )
302300303303- this.oldAgent = oldAgent
304304- this.newAgent = newAgent
301301+ missingBlobCursor = missingBlobs.data.cursor
302302+ } while (missingBlobCursor)
303303+ // Resets since this is a shared state with the migrate blobs job
304304+ this.uploadedBlobsCount = 0
305305+ }
306306+ }
307307+ if (this.migratePrefs) {
308308+ const prefs = await oldAgent.app.bsky.actor.getPreferences()
309309+ await newAgent.app.bsky.actor.putPreferences(prefs.data)
310310+ }
305311306306- if (this.migratePlcRecord) {
307307- await oldAgent.com.atproto.identity.requestPlcOperationSignature()
308308- safeStatusUpdate(
309309- statusUpdateHandler,
310310- 'Please check your email attached to your previous account for a PLC token',
311311- )
312312- }
313313- }
312312+ this.oldAgent = oldAgent
313313+ this.newAgent = newAgent
314314315315- /**
316316- * Sign and submits the PLC operation to officially migrate the account
317317- * @param {string} token - the PLC token sent in the email. If you're just wanting to run this rerun migrate with all the flags set as false except for migratePlcRecord
318318- * @param additionalRotationKeysToAdd {string[]} - additional rotation keys to add in addition to the ones provided by the new PDS.
319319- * @returns {Promise<void>}
320320- */
321321- async signPlcOperation(token, additionalRotationKeysToAdd = []) {
322322- const getDidCredentials =
323323- await this.newAgent.com.atproto.identity.getRecommendedDidCredentials()
324324- const pdsProvidedRotationKeys = getDidCredentials.data.rotationKeys ?? []
325325- // Prepend any additional rotation keys (e.g., user-added keys, newly created key) so they appear above the new PDS rotation key
326326- const rotationKeys = [...(additionalRotationKeysToAdd || []), ...pdsProvidedRotationKeys]
327327- if (!rotationKeys) {
328328- throw new Error('No rotation key provided from the new PDS')
329329- }
330330- const credentials = {
331331- ...getDidCredentials.data,
332332- rotationKeys: rotationKeys,
315315+ if (this.migratePlcRecord) {
316316+ await oldAgent.com.atproto.identity.requestPlcOperationSignature()
317317+ safeStatusUpdate(
318318+ statusUpdateHandler,
319319+ 'Please check your email attached to your previous account for a PLC token',
320320+ )
321321+ }
333322 }
334323335335- const plcOp = await this.oldAgent.com.atproto.identity.signPlcOperation({
336336- token: token,
337337- ...credentials,
338338- })
324324+ /**
325325+ * Sign and submits the PLC operation to officially migrate the account
326326+ * @param {string} token - the PLC token sent in the email. If you're just wanting to run this rerun migrate with all the flags set as false except for migratePlcRecord
327327+ * @param additionalRotationKeysToAdd {string[]} - additional rotation keys to add in addition to the ones provided by the new PDS.
328328+ * @returns {Promise<void>}
329329+ */
330330+ async signPlcOperation(token, additionalRotationKeysToAdd = []) {
331331+ const getDidCredentials =
332332+ await this.newAgent.com.atproto.identity.getRecommendedDidCredentials()
333333+ const pdsProvidedRotationKeys = getDidCredentials.data.rotationKeys ?? []
334334+ // Prepend any additional rotation keys (e.g., user-added keys, newly created key) so they appear above the new PDS rotation key
335335+ const rotationKeys = [...(additionalRotationKeysToAdd || []), ...pdsProvidedRotationKeys]
336336+ if (!rotationKeys) {
337337+ throw new Error('No rotation key provided from the new PDS')
338338+ }
339339+ const credentials = {
340340+ ...getDidCredentials.data,
341341+ rotationKeys: rotationKeys,
342342+ }
339343340340- await this.newAgent.com.atproto.identity.submitPlcOperation({
341341- operation: plcOp.data.operation,
342342- })
344344+ const plcOp = await this.oldAgent.com.atproto.identity.signPlcOperation({
345345+ token: token,
346346+ ...credentials,
347347+ })
343348344344- await this.newAgent.com.atproto.server.activateAccount()
345345- await this.oldAgent.com.atproto.server.deactivateAccount({})
346346- }
349349+ await this.newAgent.com.atproto.identity.submitPlcOperation({
350350+ operation: plcOp.data.operation,
351351+ })
347352348348- /**
349349- * Using this method assumes the Migrator class was constructed new and this was called.
350350- * Find the user's previous PDS from the PLC op logs,
351351- * logs in and deactivates their old account if it was found still active.
352352- *
353353- * @param oldHandle {string}
354354- * @param oldPassword {string}
355355- * @param {function|null} statusUpdateHandler - a function that takes a string used to update the UI.
356356- * Like (status) => console.log(status)
357357- * @param {string|null} twoFactorCode - Optional, but needed if it fails with 2fa required
358358- * @returns {Promise<void>}
359359- */
360360- async deactivateOldAccount(
361361- oldHandle,
362362- oldPassword,
363363- statusUpdateHandler = null,
364364- twoFactorCode = null,
365365- ) {
366366- //Leaving this logic that either sets the agent to bsky.social, or the PDS since it's what I found worked best for migrations.
367367- // handleAndPDSResolver should be able to handle it, but there have been edge cases and this was what worked best oldHandle = cleanHandle(oldHandle);
368368- let usersDid
369369- //If it's a bsky handle just go with the entryway and let it sort everything
370370- if (oldHandle.endsWith('.bsky.social')) {
371371- const publicAgent = new AtpAgent({
372372- service: 'https://public.api.bsky.app',
373373- })
374374- const resolveIdentityFromEntryway = await publicAgent.com.atproto.identity.resolveHandle({
375375- handle: oldHandle,
376376- })
377377- usersDid = resolveIdentityFromEntryway.data.did
378378- } else {
379379- //Resolves the did and finds the did document for the old PDS
380380- safeStatusUpdate(statusUpdateHandler, 'Resolving did from handle')
381381- usersDid = await handleResolver.resolve(oldHandle)
353353+ await this.newAgent.com.atproto.server.activateAccount()
354354+ await this.oldAgent.com.atproto.server.deactivateAccount({})
382355 }
383356384384- const didDoc = await docResolver.resolve(usersDid)
385385- let currentPds
386386- try {
387387- currentPds = didDoc.service.filter(s => s.type === 'AtprotoPersonalDataServer')[0]
388388- .serviceEndpoint
389389- } catch (error) {
390390- console.error(error)
391391- throw new Error('Could not find a PDS in the DID document.')
392392- }
357357+ /**
358358+ * Using this method assumes the Migrator class was constructed new and this was called.
359359+ * Find the user's previous PDS from the PLC op logs,
360360+ * logs in and deactivates their old account if it was found still active.
361361+ *
362362+ * @param oldHandle {string}
363363+ * @param oldPassword {string}
364364+ * @param {function|null} statusUpdateHandler - a function that takes a string used to update the UI.
365365+ * Like (status) => console.log(status)
366366+ * @param {string|null} twoFactorCode - Optional, but needed if it fails with 2fa required
367367+ * @returns {Promise<void>}
368368+ */
369369+ async deactivateOldAccount(
370370+ oldHandle,
371371+ oldPassword,
372372+ statusUpdateHandler = null,
373373+ twoFactorCode = null,
374374+ ) {
375375+ //Leaving this logic that either sets the agent to bsky.social, or the PDS since it's what I found worked best for migrations.
376376+ // handleAndPDSResolver should be able to handle it, but there have been edge cases and this was what worked best oldHandle = cleanHandle(oldHandle);
377377+ let usersDid
378378+ //If it's a bsky handle just go with the entryway and let it sort everything
379379+ if (oldHandle.endsWith('.bsky.social')) {
380380+ const publicAgent = new AtpAgent({
381381+ service: 'https://public.api.bsky.app',
382382+ })
383383+ const resolveIdentityFromEntryway = await publicAgent.com.atproto.identity.resolveHandle({
384384+ handle: oldHandle,
385385+ })
386386+ usersDid = resolveIdentityFromEntryway.data.did
387387+ } else {
388388+ //Resolves the did and finds the did document for the old PDS
389389+ safeStatusUpdate(statusUpdateHandler, 'Resolving did from handle')
390390+ usersDid = await handleResolver.resolve(oldHandle)
391391+ }
393392394394- const plcLogRequest = await fetch(`https://plc.directory/${usersDid}/log`)
395395- const plcLog = await plcLogRequest.json()
396396- let pdsBeforeCurrent = ''
397397- for (const log of plcLog) {
398398- try {
399399- const pds = log.services.atproto_pds.endpoint
400400- if (pds.toLowerCase() === currentPds.toLowerCase()) {
401401- console.log('Found the PDS before the current one')
402402- break
393393+ const didDoc = await docResolver.resolve(usersDid)
394394+ let currentPds
395395+ try {
396396+ currentPds = didDoc.service.filter(s => s.type === 'AtprotoPersonalDataServer')[0]
397397+ .serviceEndpoint
398398+ } catch (error) {
399399+ console.error(error)
400400+ throw new Error('Could not find a PDS in the DID document.')
403401 }
404404- pdsBeforeCurrent = pds
405405- } catch (e) {
406406- console.log(e)
407407- }
408408- }
409409- if (pdsBeforeCurrent === '') {
410410- throw new Error('Could not find the PDS before the current one')
411411- }
412402413413- let oldAgent = new AtpAgent({ service: pdsBeforeCurrent })
414414- safeStatusUpdate(statusUpdateHandler, `Logging you in to the old PDS: ${pdsBeforeCurrent}`)
415415- //Login to the old PDS
416416- if (twoFactorCode === null) {
417417- await oldAgent.login({ identifier: oldHandle, password: oldPassword })
418418- } else {
419419- await oldAgent.login({
420420- identifier: oldHandle,
421421- password: oldPassword,
422422- authFactorToken: twoFactorCode,
423423- })
424424- }
425425- safeStatusUpdate(statusUpdateHandler, "Checking this isn't your current PDS")
426426- if (pdsBeforeCurrent === currentPds) {
427427- throw new Error('This is your current PDS. Login to your old account username and password')
428428- }
403403+ const plcLogRequest = await fetch(`https://plc.directory/${usersDid}/log`)
404404+ const plcLog = await plcLogRequest.json()
405405+ let pdsBeforeCurrent = ''
406406+ for (const log of plcLog) {
407407+ try {
408408+ const pds = log.services.atproto_pds.endpoint
409409+ if (pds.toLowerCase() === currentPds.toLowerCase()) {
410410+ console.log('Found the PDS before the current one')
411411+ break
412412+ }
413413+ pdsBeforeCurrent = pds
414414+ } catch (e) {
415415+ console.log(e)
416416+ }
417417+ }
418418+ if (pdsBeforeCurrent === '') {
419419+ throw new Error('Could not find the PDS before the current one')
420420+ }
429421430430- let currentAccountStatus = await oldAgent.com.atproto.server.checkAccountStatus()
431431- if (!currentAccountStatus.data.activated) {
432432- safeStatusUpdate(statusUpdateHandler, 'All good. Your old account is not activated.')
422422+ let oldAgent = new AtpAgent({service: pdsBeforeCurrent})
423423+ safeStatusUpdate(statusUpdateHandler, `Logging you in to the old PDS: ${pdsBeforeCurrent}`)
424424+ //Login to the old PDS
425425+ if (twoFactorCode === null) {
426426+ await oldAgent.login({identifier: oldHandle, password: oldPassword})
427427+ } else {
428428+ await oldAgent.login({
429429+ identifier: oldHandle,
430430+ password: oldPassword,
431431+ authFactorToken: twoFactorCode,
432432+ })
433433+ }
434434+ safeStatusUpdate(statusUpdateHandler, "Checking this isn't your current PDS")
435435+ if (pdsBeforeCurrent === currentPds) {
436436+ throw new Error('This is your current PDS. Login to your old account username and password')
437437+ }
438438+439439+ let currentAccountStatus = await oldAgent.com.atproto.server.checkAccountStatus()
440440+ if (!currentAccountStatus.data.activated) {
441441+ safeStatusUpdate(statusUpdateHandler, 'All good. Your old account is not activated.')
442442+ }
443443+ safeStatusUpdate(statusUpdateHandler, 'Deactivating your OLD account')
444444+ await oldAgent.com.atproto.server.deactivateAccount({})
445445+ safeStatusUpdate(statusUpdateHandler, 'Successfully deactivated your OLD account')
433446 }
434434- safeStatusUpdate(statusUpdateHandler, 'Deactivating your OLD account')
435435- await oldAgent.com.atproto.server.deactivateAccount({})
436436- safeStatusUpdate(statusUpdateHandler, 'Successfully deactivated your OLD account')
437437- }
447447+448448+ /**
449449+ * Signs the logged-in user in this.newAgent for backups with PDS MOOver. This is usually called after migrate and signPlcOperation are successful
450450+ *
451451+ * @param {string} didWeb
452452+ * @returns {Promise<void>}
453453+ */
454454+ async signUpForBackupsFromMigration(didWeb = 'did:web:pdsmoover.com') {
455455+ //Manually grabbing the jwt and making a call with fetch cause for the life of me I could not figure out
456456+ //how you used @atproto/api to make a call for proxying
457457+ const url = `${this.newAgent.serviceUrl.origin}/xrpc/com.pdsmoover.backup.signUp`
438458439439- /**
440440- * Signs the logged-in user in this.newAgent for backups with PDS MOOver. This is usually called after migrate and signPlcOperation are successful
441441- *
442442- * @param {string} didWeb
443443- * @returns {Promise<void>}
444444- */
445445- async signUpForBackupsFromMigration(didWeb = 'did:web:pdsmoover.com') {
446446- //Manually grabbing the jwt and making a call with fetch cause for the life of me I could not figure out
447447- //how you used @atproto/api to make a call for proxying
448448- const url = `${this.newAgent.serviceUrl.origin}/xrpc/com.pdsmoover.backup.signUp`
459459+ const accessJwt = this.newAgent?.session?.accessJwt
460460+ if (!accessJwt) {
461461+ throw new Error('Missing access token for authorization')
462462+ }
449463450450- const accessJwt = this.newAgent?.session?.accessJwt
451451- if (!accessJwt) {
452452- throw new Error('Missing access token for authorization')
453453- }
464464+ const res = await fetch(url, {
465465+ method: 'POST',
466466+ headers: {
467467+ 'Authorization': `Bearer ${accessJwt}`,
468468+ 'Content-Type': 'application/json',
469469+ 'Accept': 'application/json',
470470+ 'atproto-proxy': `${didWeb}#repo_backup`,
471471+ },
472472+ body: JSON.stringify({}),
473473+ })
454474455455- const res = await fetch(url, {
456456- method: 'POST',
457457- headers: {
458458- 'Authorization': `Bearer ${accessJwt}`,
459459- 'Content-Type': 'application/json',
460460- 'Accept': 'application/json',
461461- 'atproto-proxy': `${didWeb}#repo_backup`,
462462- },
463463- body: JSON.stringify({}),
464464- })
475475+ if (!res.ok) {
476476+ let bodyText = ''
477477+ try {
478478+ bodyText = await res.text()
479479+ } catch {
480480+ }
481481+ throw new Error(
482482+ `Backup signup failed: ${res.status} ${res.statusText}${bodyText ? ` - ${bodyText}` : ''}`,
483483+ )
484484+ }
465485466466- if (!res.ok) {
467467- let bodyText = ''
468468- try {
469469- bodyText = await res.text()
470470- } catch {}
471471- throw new Error(
472472- `Backup signup failed: ${res.status} ${res.statusText}${bodyText ? ` - ${bodyText}` : ''}`,
473473- )
486486+ //No return the success is all that is needed, if there's an error it will throw
474487 }
475475-476476- //No return the success is all that is needed, if there's an error it will throw
477477- }
478488}
479489480480-export { Migrator }
490490+export {Migrator}
···5050 * @param {function|null} statusUpdateHandler - a function that takes a string used to update the UI. Like (status) => console.log(status)
5151 * @param {string|null} twoFactorCode - Optional, but needed if it fails with 2fa required
5252 * @param verificationCode - Optional verification captcha code for account creation if the PDS requires it
5353+ * @param {string|null} sourcePdsUrl - Optional URL to use as the source PDS instead of resolving from DID doc
5354 */
5454- migrate(oldHandle: string, password: string, newPdsUrl: string, newEmail: string, newHandle: string, inviteCode: string | null, statusUpdateHandler?: Function | null, twoFactorCode?: string | null, verificationCode?: any): Promise<void>;
5555+ migrate(oldHandle: string, password: string, newPdsUrl: string, newEmail: string, newHandle: string, inviteCode: string | null, statusUpdateHandler?: Function | null, twoFactorCode?: string | null, verificationCode?: any, sourcePdsUrl?: string | null): Promise<void>;
5556 /**
5657 * Sign and submits the PLC operation to officially migrate the account
5758 * @param {string} token - the PLC token sent in the email. If you're just wanting to run this rerun migrate with all the flags set as false except for migratePlcRecord