···11+# PDS MOOver
22+33+
44+55+Use at your own risk, I will not host till I have a big more validation and retry logic in place.
66+AND it will still be very much so use at your own Risk.
···11-import './style.css'
22-import javascriptLogo from './javascript.svg'
33-import viteLogo from '/vite.svg'
44-import { setupCounter } from './counter.js'
11+//Just reimporting and re exporting for the web app
22+import {Migrator} from './pdsmoover.js';
5366-document.querySelector('#app').innerHTML = `
77- <div>
88- <a href="https://vite.dev" target="_blank">
99- <img src="${viteLogo}" class="logo" alt="Vite logo" />
1010- </a>
1111- <a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript" target="_blank">
1212- <img src="${javascriptLogo}" class="logo vanilla" alt="JavaScript logo" />
1313- </a>
1414- <h1>Hello Vite!</h1>
1515- <div class="card">
1616- <button id="counter" type="button"></button>
1717- </div>
1818- <p class="read-the-docs">
1919- Click on the Vite logo to learn more
2020- </p>
2121- </div>
2222-`
2342424-setupCounter(document.querySelector('#counter'))
55+export {Migrator};
+190
src/pdsmoover.js
···11+import {
22+ CompositeHandleResolver,
33+ DohJsonHandleResolver,
44+ WellKnownHandleResolver,
55+ CompositeDidDocumentResolver,
66+ PlcDidDocumentResolver,
77+ WebDidDocumentResolver
88+} from '@atcute/identity-resolver';
99+import {AtpAgent} from '@atproto/api';
1010+1111+const handleResolver = new CompositeHandleResolver({
1212+ strategy: 'race',
1313+ methods: {
1414+ dns: new DohJsonHandleResolver({dohUrl: 'https://mozilla.cloudflare-dns.com/dns-query'}),
1515+ http: new WellKnownHandleResolver(),
1616+ },
1717+});
1818+1919+const docResolver = new CompositeDidDocumentResolver({
2020+ methods: {
2121+ plc: new PlcDidDocumentResolver(),
2222+ web: new WebDidDocumentResolver(),
2323+ },
2424+});
2525+2626+2727+function safeStatusUpdate(statusUpdateHandler, status) {
2828+ if (statusUpdateHandler) {
2929+ statusUpdateHandler(status);
3030+ }
3131+}
3232+3333+3434+class Migrator {
3535+ constructor() {
3636+ this.oldAgent = null;
3737+ this.newAgent = null;
3838+ }
3939+4040+ /**
4141+ * This migrator is pretty cut and dry and makes a few assumptions
4242+ * 1. You are using the same password between each account
4343+ * 2. If this command fails for something like oauth 2fa code it throws an error and expects the same values when ran again.
4444+ * @param {string} oldHandle - The handle you use on your old pds, something like alice.bsky.social
4545+ * @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
4646+ * @param {string} newPdsUrl - The new URL for your pds. Like https://coolnewpds.com
4747+ * @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)
4848+ * @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.
4949+ * @param {string} inviteCode - The invite code you got from the PDS you are migrating to
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+ */
5353+ async migrate(oldHandle, password, newPdsUrl, newEmail, newHandle, inviteCode, statusUpdateHandler = null, twoFactorCode = null) {
5454+ //Resolves the did and finds the did document for the old PDS
5555+ safeStatusUpdate(statusUpdateHandler, 'Resolving old PDS');
5656+ const usersDid = await handleResolver.resolve(oldHandle);
5757+ const didDoc = await docResolver.resolve(usersDid);
5858+ safeStatusUpdate(statusUpdateHandler, 'Resolving did document and finding your current PDS URL');
5959+6060+ let oldPds;
6161+ try {
6262+ oldPds = didDoc.service.filter(s => s.type === 'AtprotoPersonalDataServer')[0].serviceEndpoint;
6363+ } catch (error) {
6464+ console.error(error);
6565+ throw new Error('Could not find a PDS in the DID document.');
6666+ }
6767+6868+ const oldAgent = new AtpAgent({
6969+ service: oldPds,
7070+ });
7171+7272+ safeStatusUpdate(statusUpdateHandler, 'Logging you in to the old PDS');
7373+ //Login to the old PDS
7474+ if (twoFactorCode === null) {
7575+ await oldAgent.login({identifier: oldHandle, password});
7676+ } else {
7777+ await oldAgent.login({identifier: oldHandle, password: password, authFactorToken: twoFactorCode});
7878+ }
7979+8080+ safeStatusUpdate(statusUpdateHandler, 'Checking that the new PDS is an actual PDS');
8181+ const newAgent = new AtpAgent({service: newPdsUrl});
8282+ const newHostDesc = await newAgent.com.atproto.server.describeServer();
8383+ const newHostWebDid = newHostDesc.data.did;
8484+8585+ safeStatusUpdate(statusUpdateHandler, 'Creating a new account on the new PDS');
8686+8787+ const createAuthResp = await oldAgent.com.atproto.server.getServiceAuth({
8888+ aud: newHostWebDid,
8989+ lxm: 'com.atproto.server.createAccount',
9090+ });
9191+ const serviceJwt = createAuthResp.data.token;
9292+9393+ const createNewAccount = await newAgent.com.atproto.server.createAccount({
9494+ did: usersDid,
9595+ handle: newHandle,
9696+ email: newEmail,
9797+ password: password,
9898+ inviteCode: inviteCode,
9999+ },
100100+ {
101101+ headers: {authorization: `Bearer ${serviceJwt}`},
102102+ encoding: 'application/json',
103103+ });
104104+105105+ if (createNewAccount.data.did !== usersDid.toString()) {
106106+ throw new Error('Did not create the new account with the same did as the old account');
107107+ }
108108+109109+ safeStatusUpdate(statusUpdateHandler, 'Logging in with the new account');
110110+111111+ await newAgent.login({
112112+ identifier: usersDid,
113113+ password,
114114+ });
115115+116116+ safeStatusUpdate(statusUpdateHandler, 'Migrating your repo');
117117+ const repoRes = await oldAgent.com.atproto.sync.getRepo({did: usersDid});
118118+ await newAgent.com.atproto.repo.importRepo(repoRes.data, {
119119+ encoding: 'application/vnd.ipld.car',
120120+ });
121121+122122+ safeStatusUpdate(statusUpdateHandler, 'Migrating your blobs');
123123+124124+ let blobCursor = undefined;
125125+ let uploadedBlobs = 0;
126126+ do {
127127+ safeStatusUpdate(statusUpdateHandler, `Migrating blobs, ${uploadedBlobs}/${uploadedBlobs + 100}`);
128128+ uploadedBlobs += 100;
129129+ const listedBlobs = await oldAgent.com.atproto.sync.listBlobs({
130130+ did: usersDid,
131131+ cursor: blobCursor,
132132+ limit: 100,
133133+ });
134134+ for (const cid of listedBlobs.data.cids) {
135135+ try {
136136+ //TODO may move the status update here but would have it only update like every 10
137137+ const blobRes = await oldAgent.com.atproto.sync.getBlob({
138138+ did: usersDid,
139139+ cid,
140140+ });
141141+ await newAgent.com.atproto.repo.uploadBlob(blobRes.data, {
142142+ encoding: blobRes.headers['content-type'],
143143+ });
144144+ } catch (error) {
145145+ //TODO silently logging for now will do a missing blobs later
146146+ console.error(error);
147147+ }
148148+ }
149149+ blobCursor = listedBlobs.data.cursor;
150150+ } while (blobCursor);
151151+152152+ //TODO NEED to do some checking on the missing blobs here
153153+154154+ const prefs = await oldAgent.app.bsky.actor.getPreferences();
155155+ await newAgent.app.bsky.actor.putPreferences(prefs.data);
156156+ this.oldAgent = oldAgent;
157157+ this.newAgent = newAgent;
158158+ await oldAgent.com.atproto.identity.requestPlcOperationSignature();
159159+ safeStatusUpdate(statusUpdateHandler, 'Please check your email for a PLC token');
160160+161161+ }
162162+163163+ async signPlcOperation(token) {
164164+ const getDidCredentials =
165165+ await this.newAgent.com.atproto.identity.getRecommendedDidCredentials();
166166+ const rotationKeys = getDidCredentials.data.rotationKeys ?? [];
167167+ if (!rotationKeys) {
168168+ throw new Error('No rotation key provided from the new PDS');
169169+ }
170170+ const credentials = {
171171+ ...getDidCredentials.data,
172172+ rotationKeys: rotationKeys,
173173+ };
174174+175175+176176+ const plcOp = await this.oldAgent.com.atproto.identity.signPlcOperation({
177177+ token: token,
178178+ ...credentials,
179179+ });
180180+181181+ await this.newAgent.com.atproto.identity.submitPlcOperation({
182182+ operation: plcOp.data.operation,
183183+ });
184184+185185+ await this.newAgent.com.atproto.server.activateAccount();
186186+ await this.oldAgent.com.atproto.server.deactivateAccount({});
187187+ }
188188+}
189189+190190+export {Migrator};