Client side atproto account migrator in your web browser, along with services for backups and adversarial migrations.
pdsmoover.com
pds
atproto
migrations
moo
cow
1{%- import "partials/cow-header.askama.html" as cow -%}
2
3{% extends "layout.askama.html" %}
4
5{% block meta %}
6 <meta property="og:description" content="ATProto account migration tool"/>
7 <meta property="og:image" content="/halloween_moover.webp">
8{% endblock %}
9
10{% block content %}
11
12<script>
13 document.addEventListener('alpine:init', () => {
14 window.Alpine.data('moover', () => ({
15 handle: '',
16 password: '',
17 newPds: '',
18 newEmail: '',
19 newHandle: '',
20 inviteCode: null,
21 twoFactorCode: null,
22 confirmation: false,
23 showTwoFactorCodeInput: false,
24 error: null,
25 showStatusMessage: false,
26 askForPlcToken: false,
27
28 done: false,
29 //advance
30 showAdvance: false,
31 showAdvancedPlcOptions: false,
32 createNewAccount: true,
33 migrateRepo: true,
34 migrateBlobs: true,
35 migrateMissingBlobs: true,
36 migratePrefs: true,
37 migratePlcRecord: true,
38 //Plc signing
39 backupOptions: {
40 createANewRotationKey: false,
41 signupForBackups: false,
42 },
43 backupSignupMessage: null,
44 backupSignupError: null,
45 rotationKeys: ['', '', '', ''],
46 plcToken: null,
47 plcStatus: null,
48 newlyCreatedRotationKey: null, // { publicKey, privateKey } when generated
49 toggleAdvanceMenu() {
50 this.showAdvance = !this.showAdvance;
51 },
52 updateStatusHandler(status) {
53 console.log("Status update:", status);
54 document.getElementById("status-message").innerText = status;
55
56 },
57 async handleSubmit() {
58 this.error = null;
59 this.showStatusMessage = false;
60 console.log("Form data:", {
61 handle: this.handle,
62 password: this.password,
63 newPds: this.newPds,
64 newEmail: this.newEmail,
65 newHandle: this.newHandle,
66 inviteCode: this.inviteCode
67 })
68 try {
69
70 if (this.showTwoFactorCodeInput) {
71 if (this.twoFactorCode === null) {
72 this.error = 'Please enter the 2FA that was sent to your email.'
73 }
74 }
75 if (!this.confirmation) {
76 this.error = 'Please confirm that you understand the risks.'
77 }
78
79 window.Migrator.createNewAccount = this.createNewAccount;
80 window.Migrator.migrateRepo = this.migrateRepo;
81 window.Migrator.migrateBlobs = this.migrateBlobs;
82 window.Migrator.migrateMissingBlobs = this.migrateMissingBlobs;
83 window.Migrator.migratePrefs = this.migratePrefs;
84 window.Migrator.migratePlcRecord = this.migratePlcRecord;
85 this.updateStatusHandler('Starting migration...');
86 this.showStatusMessage = true;
87 await window.Migrator.migrate(
88 this.handle,
89 this.password,
90 this.newPds,
91 this.newEmail,
92 this.newHandle,
93 this.inviteCode,
94 this.updateStatusHandler,
95 this.twoFactorCode
96 );
97 if (this.migratePlcRecord) {
98 this.askForPlcToken = true;
99 } else {
100 this.updateStatusHandler('Migration of your repo is complete! But the PLC operation was not done so your old account is still the valid one.');
101 }
102 } catch (error) {
103 console.error(error.error, error.message);
104 if (error.error === 'AuthFactorTokenRequired') {
105 this.showTwoFactorCodeInput = true;
106 }
107 this.error = error.message;
108 }
109 },
110 async signPlcOperation() {
111 try {
112 this.plcStatus = 'Signing PLC operation...';
113 this.backupSignupMessage = null;
114 this.backupSignupError = null;
115 // Build additional rotation keys list (user-provided and/or newly created)
116 const additionalRotationKeysToAdd = [];
117 // Generate a new rotation key if requested
118 if (this.backupOptions.createANewRotationKey) {
119 window.handle = this.newHandle;
120 const created = await window.PlcOps.createANewSecp256k1();
121 this.newlyCreatedRotationKey = created; // { publicKey, privateKey }
122 // Reserve first slot for the newly created key (will appear above the PDS rotation key)
123 additionalRotationKeysToAdd.push(created.publicKey);
124 }
125 // Append any manually entered rotation keys (non-empty)
126 for (let i = 0; i < this.rotationKeys.length; i++) {
127 const k = (this.rotationKeys[i] || '').trim();
128 if (k) {
129 additionalRotationKeysToAdd.push(k);
130 }
131 }
132
133 await window.Migrator.signPlcOperation(this.plcToken, additionalRotationKeysToAdd);
134 this.plcStatus = 'PLC operation signed successfully! Your account has been MOOved to the new PDS.';
135 this.done = true;
136
137 if (this.backupOptions.signupForBackups) {
138 try {
139 this.backupSignupMessage = 'Signing you up for automated backups...';
140 let _ = await window.Migrator.signUpForBackupsFromMigration();
141 this.backupSignupMessage = 'Signed up for automated backups successfully.';
142 } catch (e) {
143 console.error(e);
144 this.backupSignupError = e?.message || 'Failed to sign you up for automated backups.';
145 }
146 }
147 } catch (error) {
148 this.error = error.message;
149 console.error(error);
150 }
151 }
152 }))
153 })
154</script>
155
156<div class="container" x-data="moover">
157 {% call cow::cow_header("PDS MOOver") %}
158
159 <a href="/info">Idk if I trust a cow to move my atproto account to a new PDS</a>
160 <br/>
161 <a href="https://blacksky.community/profile/did:plc:g7j6qok5us4hjqlwjxwrrkjm/post/3lw3hcuojck2u">Video guide for
162 joining blacksky.app</a>
163 <form x-show="!askForPlcToken" id="moover-form" @submit.prevent="await handleSubmit()">
164 <!-- First section: Login credentials -->
165 <div class="section">
166 <h2>Login for your current PDS</h2>
167 <div class="form-group">
168 <label for="handle">Old Handle:</label>
169 <input type="text" id="handle" name="handle" placeholder="alice.bsky.social" x-model="handle" required>
170 </div>
171
172 <div class="form-group">
173 <label for="password">Old Password (Will also be your new password):</label>
174 <input type="password" id="password" name="password" x-model="password" required>
175 </div>
176
177 <div x-show="showTwoFactorCodeInput" class="form-group">
178 <label for="two-factor-code">2FA from the email sent</label>
179 <input type="text" id="two-factor-code" name="two-factor-code" x-model="twoFactorCode">
180 <div class="error-message">Enter your 2fa code here</div>
181
182 </div>
183 </div>
184
185 <!-- Second section: New account details -->
186 <div class="section">
187 <h2>Setup for the new PDS</h2>
188 <div class="form-group">
189 <label for="new-pds">New PDS (URL):</label>
190 <input type="url" id="new-pds" name="new-pds" placeholder="https://coolnewpds.com" x-model="newPds"
191 required>
192 </div>
193
194 <div class="form-group">
195 <label for="new-email">New Email:</label>
196 <input type="email" id="new-email" name="new-email" placeholder="CanBeSameEmailAsTheOldPds@email.com"
197 x-model="newEmail" required>
198 </div>
199
200 <div class="form-group">
201 <label for="new-handle">New Handle:</label>
202 <input type="text" id="new-handle" name="new-handle"
203 placeholder="username.newpds.com or mycooldomain.com" x-model="newHandle" required>
204 </div>
205
206 <div class="form-group">
207 <label for="invite-code">Invite Code:</label>
208 <input type="text" id="invite-code" name="invite-code" placeholder="Invite code from your new PDS (Leave blank if you don't have one)"
209 x-model="inviteCode">
210 </div>
211 </div>
212 <div class="form-group">
213 <button type="button" @click="toggleAdvanceMenu()" id="advance" name="advance">Advance Options
214 </button>
215 </div>
216 <div x-show="showAdvance" class="section" style="padding-bottom: 10px; text-align: left">
217 <h3>Pick and choose which actions to run</h3>
218 <p>Useful if a migration failed and you want to have a bit more manual control</p>
219 <div class="form-control">
220 <label class="moove-checkbox-label">
221 <input type="checkbox" id="createNewAccount" name="createNewAccount" x-model="createNewAccount">
222 Create a New Account on the New PDS
223 </label>
224 </div>
225 <div class="form-control">
226 <label class="moove-checkbox-label">
227 <input type="checkbox" id="migrateRepo" name="migrateRepo" x-model="migrateRepo">
228 Migrate Repo
229 </label>
230 </div>
231 <div class="form-control">
232 <label class="moove-checkbox-label">
233 <input type="checkbox" id="migrateBlobs" name="migrateBlobs" x-model="migrateBlobs">
234 Migrate Blobs
235 </label>
236 </div>
237 <div class="form-control">
238 <label class="moove-checkbox-label">
239 <input type="checkbox" id="migrateMissingBlobs" name="migrateMissingBlobs"
240 x-model="migrateMissingBlobs">
241 Migrate Missing Blobs
242 </label>
243 </div>
244 <div class="form-control">
245 <label class="moove-checkbox-label">
246 <input type="checkbox" id="migratePrefs" name="migratePrefs" x-model="migratePrefs">
247 Migrate Prefs
248 </label>
249 </div>
250 <div class="form-control">
251 <label class="moove-checkbox-label">
252 <input type="checkbox" id="migratePlcRecord" name="migratePlcRecord" x-model="migratePlcRecord">
253 Migrate PLC Record
254 </label>
255 </div>
256
257 </div>
258
259 <p style="text-align: left">There are some risks that come with doing an account migration.
260 (Can view them
261 <a href="https://github.com/bluesky-social/pds/blob/main/ACCOUNT_MIGRATION.md#%EF%B8%8F-warning-%EF%B8%8F-%EF%B8%8F">here</a>)
262 and that the creator or host of this migration tool is not liable and will not be able to help you in the
263 event something goes wrong. I also have read over the <a href="/info">extended information from PDS MOOver about account
264 migrations.</a>
265 </p>
266 <div class="form-group">
267 <label for="confirmation" class="moove-checkbox-label">
268 <input x-model="confirmation" type="checkbox" id="confirmation" name="confirmation" required>
269 <span>I understand</span>
270 </label>
271 </div>
272 <div x-show="error" x-text="error" class="error-message"></div>
273 <div x-show="showStatusMessage" id="warning">*Please make sure to stay on this page during the MOOve for the
274 best result
275 </div>
276 <div x-show="showStatusMessage" id="status-message" class="status-message"></div>
277 <div>
278 <button type="submit">MOOve</button>
279 </div>
280 </form>
281
282 <div x-show="askForPlcToken" class="section">
283 <form @submit.prevent="await signPlcOperation()">
284 <div x-show="!done">
285 <h2>Please enter your PLC Token you received in an email</h2>
286 <div class="form-group">
287 <label for="plc-token">PLC Token:</label>
288 <input type="text" id="plc-token" name="plc-token" x-model="plcToken" required>
289 </div>
290 <p style="text-align: left">You can now select to add a new Rotation Key during migration and sign up for PDS MOOver's free backup service. With a Rotation Key and backups if your new PDS ever goes down you can recover your account and it's data.</p>
291 <div class="form-group">
292 <label for="rotation-key" class="moove-checkbox-label">
293 <input x-model="backupOptions.createANewRotationKey" type="checkbox" id="rotation-key" name="rotation-key">
294 <span>Create and add a new Rotation Key</span>
295 </label>
296 </div>
297 <div class="form-group">
298 <label for="backups-signup" class="moove-checkbox-label">
299 <input x-model="backupOptions.signupForBackups" type="checkbox" id="backups-signup" name="backups-signup" >
300 <span>Signup for automated account backups</span>
301 </label>
302 </div>
303 <div class="form-group">
304 <button type="button" id="plc-advance" @click="showAdvancedPlcOptions = !showAdvancedPlcOptions">Add Additional Rotation Keys</button>
305 </div>
306 <div x-show="showAdvancedPlcOptions" class="section" style="padding-bottom: 10px;">
307 <div style="text-align: left;">
308 You can pick up to 4 rotation keys to your PLC document. These will appear above your new PDS rotation key so you can recover your account in the event of an adversarial take over from a rogue PDS
309 </div>
310 <div class="form-group" style="margin-top: 10px;">
311 <label for="rotation-key-1">Rotation key 1</label>
312 <input type="text" id="rotation-key-1" name="rotation-key-1"
313 x-model="rotationKeys[0]"
314 :disabled="backupOptions.createANewRotationKey"
315 :placeholder="backupOptions.createANewRotationKey ? 'reserved for the newly created rotation key' : ''">
316 </div>
317 <div class="form-group">
318 <label for="rotation-key-2">Rotation key 2</label>
319 <input type="text" id="rotation-key-2" name="rotation-key-2" x-model="rotationKeys[1]">
320 </div>
321 <div class="form-group">
322 <label for="rotation-key-3">Rotation key 3</label>
323 <input type="text" id="rotation-key-3" name="rotation-key-3" x-model="rotationKeys[2]">
324 </div>
325 <div class="form-group">
326 <label for="rotation-key-4">Rotation key 4</label>
327 <input type="text" id="rotation-key-4" name="rotation-key-4" x-model="rotationKeys[3]">
328 </div>
329 </div>
330 </div>
331 <div x-show="error" x-text="error" class="error-message"></div>
332 <template x-if="done && backupOptions.createANewRotationKey && newlyCreatedRotationKey">
333 <div x-data="{newlyCreatedRotationKey: newlyCreatedRotationKey}">
334 {% include "partials/rotation_key_display.askama.html" %}
335 </div>
336 </template>
337 <div x-show="!done && plcStatus" x-text="plcStatus" class="status-message"></div>
338 <template x-if="backupOptions.signupForBackups">
339 <div>
340 <div x-show="backupSignupMessage" x-text="backupSignupMessage" class="status-message"></div>
341 <div x-show="backupSignupError" x-text="backupSignupError" class="error-message"></div>
342 </div>
343 </template>
344 <div x-show="done" class="status-message">Congratulations! You have MOOved to a new PDS! Remember to use
345 your new PDS URL under "Hosting provider" when logging in on Bluesky. Can find more detail information
346 <a href="/info#cant-login">here.</a></div>
347
348 <div>
349 <button x-show="!done" type="submit">Sign the papers</button>
350 </div>
351 </form>
352 </div>
353</div>
354
355{% endblock %}