Skip to content

Instantly share code, notes, and snippets.

@haileyok
Last active October 20, 2024 19:29
Show Gist options
  • Save haileyok/67bede50387c3dc57448e716c0a9be6e to your computer and use it in GitHub Desktop.
Save haileyok/67bede50387c3dc57448e716c0a9be6e to your computer and use it in GitHub Desktop.
Migrate PDS script
import AtpAgent from '@atproto/api'
import { Secp256k1Keypair } from '@atproto/crypto'
import * as ui8 from 'uint8arrays'
const OLD_PDS_URL = 'https://bsky.social'
const NEW_PDS_URL = 'https://pds.haileyok.com'
const CURRENT_HANDLE = 'haileyok.com'
const CURRENT_PASSWORD = ''
const NEW_HANDLE = 'newphone.pds.haileyok.com'
const NEW_ACCOUNT_EMAIL = ''
const NEW_ACCOUNT_PASSWORD = ''
const NEW_PDS_INVITE_CODE = ''
const TOKEN = '' // Use `getEmail` first, and set this to the token that you receive.
// You probably need to bump your blob size if you're a real poster.
// Add to /pds/pds.env
// PDS_BLOB_UPLOAD_LIMIT=1000000000
// Save the private key that's output below. If you trust your PDS opsec more, add the new key
// to the end (push).
// If you trust your own password manager more, put it in front [newKey, ...keys]
const getEmail = async () => {
const oldAgent = new AtpAgent.AtpAgent({ service: OLD_PDS_URL })
await oldAgent.login({
identifier: CURRENT_HANDLE,
password: CURRENT_PASSWORD,
})
await oldAgent.com.atproto.identity.requestPlcOperationSignature()
}
const migrateAccount = async () => {
const oldAgent = new AtpAgent.AtpAgent({ service: OLD_PDS_URL })
const newAgent = new AtpAgent.AtpAgent({ service: NEW_PDS_URL })
await oldAgent.login({
identifier: CURRENT_HANDLE,
password: CURRENT_PASSWORD,
})
const accountDid = oldAgent.session?.did
if (!accountDid) {
throw new Error('Could not get DID for old account')
}
// Create account
// ------------------
const describeRes = await newAgent.api.com.atproto.server.describeServer()
const newServerDid = describeRes.data.did
const serviceJwtRes = await oldAgent.com.atproto.server.getServiceAuth({
aud: newServerDid,
})
const serviceJwt = serviceJwtRes.data.token
await newAgent.api.com.atproto.server.createAccount(
{
handle: NEW_HANDLE,
email: NEW_ACCOUNT_EMAIL,
password: NEW_ACCOUNT_PASSWORD,
did: accountDid,
inviteCode: NEW_PDS_INVITE_CODE,
},
{
headers: { authorization: `Bearer ${serviceJwt}` },
encoding: 'application/json',
},
)
await newAgent.login({
identifier: NEW_HANDLE,
password: NEW_ACCOUNT_PASSWORD,
})
// Migrate Data
// ------------------
const repoRes = await oldAgent.com.atproto.sync.getRepo({ did: accountDid })
await newAgent.com.atproto.repo.importRepo(repoRes.data, {
encoding: 'application/vnd.ipld.car',
})
let blobCursor = undefined
do {
const listedBlobs = await oldAgent.com.atproto.sync.listBlobs({
did: accountDid,
cursor: blobCursor,
})
for (const cid of listedBlobs.data.cids) {
const blobRes = await oldAgent.com.atproto.sync.getBlob({
did: accountDid,
cid,
})
await newAgent.com.atproto.repo.uploadBlob(blobRes.data, {
encoding: blobRes.headers['content-type'],
})
}
blobCursor = listedBlobs.data.cursor
} while (blobCursor)
const prefs = await oldAgent.api.app.bsky.actor.getPreferences()
await newAgent.api.app.bsky.actor.putPreferences(prefs.data)
// Migrate Identity
// ------------------
const getDidCredentials =
await newAgent.com.atproto.identity.getRecommendedDidCredentials()
console.log(JSON.stringify(getDidCredentials))
// @NOTE, this token will need to come from the email from the previous step
const keypair = await Secp256k1Keypair.create({ exportable: true })
const privateKey = await keypair.export()
const rotationKey = keypair.did()
console.log('SAVE THIS PRIVATE KEY!!!')
console.log('rotation key: ', rotationKey)
console.log('private key: ', ui8.toString(privateKey, 'hex'))
if(getDidCredentials.data.rotationKeys == null) {
console.log("Nope")
return
}
getDidCredentials.data.rotationKeys = [rotationKey, ...getDidCredentials.data.rotationKeys]
const plcOp = await oldAgent.com.atproto.identity.signPlcOperation({
token: TOKEN,
...getDidCredentials.data,
})
await newAgent.com.atproto.identity.submitPlcOperation({
operation: plcOp.data.operation,
})
// Finalize Migration
// ------------------
await newAgent.com.atproto.server.activateAccount()
await oldAgent.com.atproto.server.deactivateAccount({})
}
// STEP ONE IS HERE
getEmail()
// STEP TWO IS HERE. COMMENT OUT THE ABOVE AND UNCOMMENT THIS ONCE YOU HAVE YOUR CODE
// migrateAccount()
@mozzius
Copy link

mozzius commented Mar 21, 2024

I don't think line 112 is supposed to be there lol

@haileyok
Copy link
Author

haileyok commented Apr 2, 2024

fixed it!

@lukeacl
Copy link

lukeacl commented Oct 19, 2024

Needs a couple of adjustments.

I changed import AtpAgent from '@atproto/api' to import { AtpAgent } from '@atproto/api' then adjusted all of the constructor references from new AtpAgent.AtpAgent( to new AtpAgent(.

I removed all the .api parts of calls, for example oldAgent.api.app.bsky.actor.getPreferences() changed to oldAgent.app.bsky.actor.getPreferences().

The last one is oldAgent.com.atproto.server.getServiceAuth apparently needs lxm provided with value com.atproto.server.createAccount, see error: XRPCError: missing jwt lexicon method ("lxm"). must match: com.atproto.server.createAccount.

Thanks for the script! It worked really well on a test account, working up the courage to throw the main account off the edge. 😮‍💨

@haileyok
Copy link
Author

oh yea, there's been a lot of changes to the api package since i made this...should probably update it lol

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment