Client side atproto account migrator in your web browser, along with services for backups and adversarial migrations. pdsmoover.com
pds atproto migrations moo cow
129
fork

Configure Feed

Select the types of activity you want to include in your feed.

Deactivation helper

+179 -3
-2
public/info.html
··· 8 8 <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no"/> 9 9 <title>PDS MOOver Info</title> 10 10 <link rel="stylesheet" href="/style.css"> 11 - <script src="https://unpkg.com/alpinejs" defer></script> 12 - 13 11 </head> 14 12 <body> 15 13 <div class="container">
+106
public/turnoff.html
··· 1 + <!doctype html> 2 + <html lang="en"> 3 + <head> 4 + <meta charset="UTF-8"/> 5 + <link rel="icon" type="image/webp" href="/moo.webp"/> 6 + <meta property="og:description" content="ATProto account migration tool"/> 7 + <meta property="og:image" content="/moo.webp"> 8 + <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no"/> 9 + <title>PDS MOOver - Turn OFF</title> 10 + <link rel="stylesheet" href="/style.css"> 11 + <script src="https://unpkg.com/alpinejs" defer></script> 12 + 13 + <script type="module"> 14 + import {Migrator} from './src/main.js'; 15 + 16 + window.Migrator = new Migrator(); 17 + </script> 18 + 19 + <script> 20 + document.addEventListener('alpine:init', () => { 21 + 22 + Alpine.data('moover', () => ({ 23 + oldHandle: '', 24 + oldPassword: '', 25 + twoFactorCode: '', 26 + showTwoFactorCodeInput: false, 27 + error: null, 28 + showStatusMessage: false, 29 + updateStatusHandler(status) { 30 + console.log("Status update:", status); 31 + document.getElementById("status-message").innerText = status; 32 + }, 33 + async handleSubmit() { 34 + this.error = null; 35 + this.showStatusMessage = false; 36 + 37 + try { 38 + 39 + if (this.showTwoFactorCodeInput) { 40 + if (this.twoFactorCode === null) { 41 + this.error = 'Please enter the 2FA that was sent to your email.' 42 + } 43 + } 44 + 45 + this.showStatusMessage = true; 46 + await window.Migrator.deactivateOldAccount( 47 + this.oldHandle, 48 + this.oldPassword, 49 + this.updateStatusHandler, 50 + this.twoFactorCode); 51 + } catch (error) { 52 + console.error(error.error, error.message); 53 + if (error.error === 'AuthFactorTokenRequired') { 54 + this.showTwoFactorCodeInput = true; 55 + } 56 + this.error = error.message; 57 + } 58 + }, 59 + })) 60 + }) 61 + </script> 62 + </head> 63 + <body> 64 + <div class="container" x-data="moover"> 65 + <h1>PDS MOOver</h1> 66 + 67 + <div class="cow-image"> 68 + <img src="/moo.webp" alt="Cartoon milk cow" style="max-width: 100%; max-height: 100%; object-fit: contain;"> 69 + </div> 70 + <div class="made-by-blur">Made by <a href="https://bsky.app/profile/baileytownsend.dev">@baileytownsend.dev</a> 71 + </div> 72 + <p>Use this page to make sure your old account is deactivated</p> 73 + <form id="moover-form" @submit.prevent="await handleSubmit()"> 74 + <!-- First section: Login credentials --> 75 + <div class="section"> 76 + <h2>Login for your old PDS</h2> 77 + <div class="form-group"> 78 + <label for="handle">Old Handle:</label> 79 + <input type="text" id="handle" name="handle" placeholder="alice.bsky.social" x-model="oldHandle" 80 + required> 81 + </div> 82 + 83 + <div class="form-group"> 84 + <label for="password">Old Password:</label> 85 + <input type="password" id="password" name="password" x-model="oldPassword" required> 86 + </div> 87 + 88 + <div x-show="showTwoFactorCodeInput" class="form-group"> 89 + <label for="two-factor-code">2FA from the email sent</label> 90 + <input type="text" id="two-factor-code" name="two-factor-code" x-model="twoFactorCode"> 91 + <div class="error-message">Enter your 2fa code here</div> 92 + 93 + </div> 94 + </div> 95 + 96 + <div x-show="error" x-text="error" class="error-message"></div> 97 + <div x-show="showStatusMessage" id="status-message" class="status-message"></div> 98 + <div> 99 + <button type="submit">Turn it off</button> 100 + </div> 101 + </form> 102 + </div> 103 + 104 + 105 + </body> 106 + </html>
+73 -1
src/pdsmoover.js
··· 237 237 } 238 238 } 239 239 240 + /** 241 + * Sign and submits the PLC operation to officially migrate the account 242 + * @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 243 + * @returns {Promise<void>} 244 + */ 240 245 async signPlcOperation(token) { 241 246 const getDidCredentials = 242 247 await this.newAgent.com.atproto.identity.getRecommendedDidCredentials(); ··· 262 267 await this.newAgent.com.atproto.server.activateAccount(); 263 268 await this.oldAgent.com.atproto.server.deactivateAccount({}); 264 269 } 270 + 271 + // Quick and dirty copy and paste of the above to get a fix out to help people without breaking or introducing any bugs to the migration service...hopefully 272 + async deactivateOldAccount(oldHandle, oldPassword, statusUpdateHandler = null, twoFactorCode = null) { 273 + //Copying the handle from bsky website adds some random unicodes on 274 + oldHandle = oldHandle.replace('@', '').trim().replace(/[\u202A\u202C\u200E\u200F\u2066-\u2069]/g, ''); 275 + let usersDid; 276 + //If it's a bsky handle just go with the entryway and let it sort everything 277 + if (oldHandle.endsWith('.bsky.social')) { 278 + const publicAgent = new AtpAgent({service: 'https://public.api.bsky.app'}); 279 + const resolveIdentityFromEntryway = await publicAgent.com.atproto.identity.resolveHandle({handle: oldHandle}); 280 + usersDid = resolveIdentityFromEntryway.data.did; 281 + } else { 282 + //Resolves the did and finds the did document for the old PDS 283 + safeStatusUpdate(statusUpdateHandler, 'Resolving did from handle'); 284 + usersDid = await handleResolver.resolve(oldHandle); 285 + } 286 + 287 + const didDoc = await docResolver.resolve(usersDid); 288 + let currentPds; 289 + try { 290 + currentPds = didDoc.service.filter(s => s.type === 'AtprotoPersonalDataServer')[0].serviceEndpoint; 291 + } catch (error) { 292 + console.error(error); 293 + throw new Error('Could not find a PDS in the DID document.'); 294 + } 295 + 296 + const plcLogRequest = await fetch(`https://plc.directory/${usersDid}/log`); 297 + const plcLog = await plcLogRequest.json(); 298 + let pdsBeforeCurrent = ''; 299 + for (const log of plcLog) { 300 + try { 301 + const pds = log.services.atproto_pds.endpoint; 302 + console.log(pds); 303 + if (pds.toLowerCase() === currentPds.toLowerCase()) { 304 + console.log('Found the PDS before the current one'); 305 + break; 306 + } 307 + pdsBeforeCurrent = pds; 308 + } catch (e) { 309 + console.log(e); 310 + } 311 + } 312 + if (pdsBeforeCurrent === '') { 313 + throw new Error('Could not find the PDS before the current one'); 314 + } 315 + 316 + let oldAgent = new AtpAgent({service: pdsBeforeCurrent}); 317 + safeStatusUpdate(statusUpdateHandler, `Logging you in to the old PDS: ${pdsBeforeCurrent}`); 318 + //Login to the old PDS 319 + if (twoFactorCode === null) { 320 + await oldAgent.login({identifier: oldHandle, password: oldPassword}); 321 + } else { 322 + await oldAgent.login({identifier: oldHandle, password: oldPassword, authFactorToken: twoFactorCode}); 323 + } 324 + safeStatusUpdate(statusUpdateHandler, 'Checking this isn\'t your current PDS'); 325 + if (pdsBeforeCurrent === currentPds) { 326 + throw new Error('This is your current PDS. Login to your old account username and password'); 327 + } 328 + 329 + let currentAccountStatus = await oldAgent.com.atproto.server.checkAccountStatus(); 330 + if (!currentAccountStatus.data.activated) { 331 + safeStatusUpdate(statusUpdateHandler, 'All good. Your old account is not activated.'); 332 + } 333 + safeStatusUpdate(statusUpdateHandler, 'Deactivating your OLD account'); 334 + await oldAgent.com.atproto.server.deactivateAccount({}); 335 + safeStatusUpdate(statusUpdateHandler, 'Successfully deactivated your OLD account'); 336 + } 265 337 } 266 338 267 - export {Migrator}; 339 + export {Migrator};