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

Autofill PDS if trusted

+91 -17
+14 -6
web-ui/src/routes/moover/[[pds]]/+page.server.ts
··· 5 5 6 6 export const load: PageServerLoad = async ({params}) => { 7 7 8 + const allowedPds = env.PDS_AUTOFILL.split(',').sort(); 9 + 10 + const defaultResponse = { 11 + pdsOptions: null, 12 + intinalDomain: null, 13 + allowedPds: allowedPds 14 + }; 15 + 8 16 if (!params.pds) { 9 - return {pdsOptions: null, intinalDomain: null}; 17 + return defaultResponse; 10 18 } 11 19 12 - const allowedPds = env.PDS_AUTOFILL.split(','); 13 20 if (!allowedPds.includes(params.pds.toLowerCase())) { 14 21 console.error('PDS not allowed', params.pds); 15 - return {pdsOptions: null, intinalDomain: null}; 22 + return defaultResponse; 16 23 } 17 24 18 25 try { ··· 21 28 const {ok, data} = await rpc.get('com.atproto.server.describeServer', {}) 22 29 if (!ok) { 23 30 console.error('Failed to describe the PDS server', data); 24 - return {pds: null}; 31 + return {pds: null, allowedPds}; 25 32 } 26 33 return { 27 34 pdsOptions: data, 28 - intinalDomain: data?.availableUserDomains[0] ?? '' 35 + intinalDomain: data?.availableUserDomains[0] ?? '', 36 + allowedPds: allowedPds 29 37 }; 30 38 } catch (e) { 31 39 console.error('Failed to describe the PDS server', e); 32 - return {pdsOptions: null, intinalDomain: null}; 40 + return defaultResponse; 33 41 } 34 42 };
+66 -6
web-ui/src/routes/moover/[[pds]]/+page.svelte
··· 5 5 import {Migrator} from '@pds-moover/moover'; 6 6 import SignThePapers from './SignThePapers.svelte'; 7 7 import Captcha from '$lib/components/Captcha.svelte'; 8 + import {Client, simpleFetchHandler} from '@atcute/client'; 9 + import type {} from '@atcute/atproto'; 8 10 9 11 10 12 let {data} = $props(); 11 13 12 - let selectedPds = $derived(data.pdsOptions); 14 + let pdsOverride = $state<null | typeof data.pdsOptions>(null); 15 + let selectedPds = $derived(pdsOverride ?? data.pdsOptions); 13 16 let cleanSelectedPds = $derived(selectedPds?.did.replace('did:web:', '')); 14 17 //Kept as a "global" state to handle logic of passing the full handle that is used to SignThePapers 15 18 let newHandle = $state(''); ··· 19 22 let handlePlaceHolder = $derived( 20 23 selectedPds ? `username${selectedDomain === 'custom' ? '' : `${selectedPds?.availableUserDomains[0]}`} or mydomain.com` : 'username.newpds.com or mycooldomain.com') 21 24 25 + 26 + function extractHostname(url: string): string | null { 27 + try { 28 + return new URL(url).hostname; 29 + } catch { 30 + return url; 31 + } 32 + } 33 + 34 + // Watch the newPds input and auto-fetch describeServer for allowed PDS hosts 35 + $effect(() => { 36 + const possiblePdsUrl = formData.newPds; 37 + // Only run when no PDS was already resolved via URL param 38 + if (data.pdsOptions) return; 39 + 40 + const hostname = extractHostname(possiblePdsUrl); 41 + if (!hostname || !data.allowedPds.includes(hostname.toLowerCase())) return; 42 + const pdsUrl = `https://${hostname}`; 43 + const handler = simpleFetchHandler({service: pdsUrl}); 44 + const rpc = new Client({handler}); 45 + rpc.get('com.atproto.server.describeServer', {}).then((res) => { 46 + if (!res.ok) return; 47 + pdsOverride = res.data; 48 + selectedDomain = res.data?.availableUserDomains?.[0] ?? ''; 49 + }).catch((e) => { 50 + console.error('Failed to describe PDS', e); 51 + }); 52 + }); 22 53 23 54 $effect(() => { 24 55 if (!selectedPds) return; ··· 197 228 </svelte:head> 198 229 199 230 <div class="container"> 200 - <MooHeader title="PDS MOOver"/> 231 + <MooHeader title="PDS MOOver"/> 201 232 {#if !migrationInProgress} 202 233 <a href={resolve('/info')}>Idk if I trust a cow to move my atproto account to a new PDS</a> 203 234 <br/> ··· 239 270 240 271 <!-- Second section: New account details --> 241 272 <div class="section"> 242 - <h2>{selectedPds ? `Setup for ${cleanSelectedPds}` : 'Setup for the new PDS'}</h2> 273 + <h2> 274 + {#if selectedPds} 275 + Setup for <span style="text-decoration: underline">{cleanSelectedPds}</span> 276 + {:else} 277 + Setup for the new PDS 278 + {/if} 279 + {#if !data.pdsOptions && selectedPds} 280 + <button type="button" class="change-pds-btn" 281 + onclick={() => { pdsOverride = null; selectedDomain = null; formData.newPds = ''; }}> 282 + Change 283 + </button> 284 + {/if} 285 + </h2> 243 286 {#if !selectedPds} 244 287 <div class="form-group"> 245 288 <label for="new-pds">New PDS (URL):</label> 246 289 <input type="url" id="new-pds" name="newPds" placeholder="https://coolnewpds.com" 247 - required bind:value={formData.newPds}> 290 + required bind:value={formData.newPds} list="allowed-pds-list"> 291 + <datalist id="allowed-pds-list"> 292 + {#each data.allowedPds as pds (pds)} 293 + <option value="{pds}"></option> 294 + {/each} 295 + </datalist> 248 296 </div> 249 297 {/if} 250 298 ··· 408 456 {#if errorMessage !== null} 409 457 <div class="error-message">{errorMessage}</div> 410 458 411 - <div id="status-message" class="status-message">A error has occurred. Please take a screenshot of this screen for support. You can also retry by refreshing the page and entering the same information as before, it will not harm your account.</div> 459 + <div id="status-message" class="status-message">A error has occurred. Please take a screenshot of 460 + this screen for support. You can also retry by refreshing the page and entering the same 461 + information as before, it will not harm your account. 462 + </div> 412 463 413 464 {/if} 414 465 ··· 421 472 422 473 423 474 {#if askForPlcToken} 424 - <SignThePapers migrator={migrator} newHandle={newHandle}/> 475 + <SignThePapers migrator={migrator} newHandle={newHandle} newPdsUrl={formData.newPds}/> 425 476 {/if} 426 477 427 478 428 479 </div> 480 + 481 + <style> 482 + .change-pds-btn { 483 + font-size: 0.7em; 484 + padding: 0.4em 0.12em; 485 + vertical-align: middle; 486 + font-weight: 400; 487 + } 488 + </style>
+11 -5
web-ui/src/routes/moover/[[pds]]/SignThePapers.svelte
··· 5 5 import type {RotationKeyType} from '$lib/types'; 6 6 import {env} from '$env/dynamic/public'; 7 7 8 - let {migrator, newHandle}: { migrator: Migrator, newHandle: string } = $props(); 8 + let {migrator, newHandle, newPdsUrl}: { migrator: Migrator, newHandle: string, newPdsUrl: string } = $props(); 9 + 10 + let newPds = $derived(newPdsUrl.replace('https://', '')); 9 11 10 12 //UI State 11 13 let errorMessage: null | string = $state(null); ··· 80 82 <form onsubmit="{signPlcOperation}"> 81 83 {#if !done} 82 84 <div> 83 - <h2>Please check your email attached to your previous account for a PLC token to enter below</h2> 85 + <h2>MOOving to <span style="text-decoration: underline">{newPds}</span></h2> 86 + <p>Please check your email attached to your previous account for a PLC token to enter below</p> 84 87 <div class="form-group"> 85 88 <label for="plc-token">PLC Token:</label> 86 89 <input type="text" id="plc-token" name="plc-token" bind:value={plcToken} required> 87 90 </div> 88 91 <p style="text-align: left"> 89 - Please check the boxes below if you would like to add a Rotation Key to your account and to sign up for PDS MOOver's free backup service. 92 + Please check the boxes below if you would like to add a Rotation Key to your account and to sign up 93 + for PDS MOOver's free backup service. 90 94 With a Rotation Key and backups if your new PDS ever goes down 91 95 you can recover your account and it's data. This is not required but highly recommended.</p> 92 96 <div class="form-group"> ··· 165 169 {/if} 166 170 167 171 {#if done} 168 - <div class="status-message">Congratulations! You have MOOved to a new PDS! Remember to use 169 - your new PDS URL under "Hosting provider" when logging in on Bluesky. If you cannot login or see "Your account is deactivated" please follow the directions here 172 + <div class="status-message">Congratulations! You have MOOved to <strong>{newPdsUrl}</strong>! Remember to 173 + use 174 + your new PDS URL under "Hosting provider" when logging in on Bluesky. If you cannot login or see "Your 175 + account is deactivated" please follow the directions 170 176 <a href={resolve('/info#cant-login')}>here.</a></div> 171 177 {:else } 172 178 <div>