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

Configure Feed

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

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>