Client side atproto account migrator in your web browser, along with services for backups and adversarial migrations.

Deactivation helper

Changed files
+179 -3
public
src
-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};