Something went wrong. Try again.
Client side atproto account migrator in your web browser, along with services for backups and adversarial migrations. pdsmoover.com
pds atproto migrations moo cow
Something went wrong. Try again.
6.2 kB · 137 lines
Svelte
at oauth
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138<script lang="ts"> import {untrack} from 'svelte'; import {handleAndPDSResolver} from '$lib/moover' import type {RotationKeyType} from '$lib/types'; import Button from '$lib/components/ui/Button.svelte';
let {handle, did = '', rotationKey}: { handle: string, /** The account's DID, when the caller already knows it. Lets the download skip a lookup. */ did?: string, rotationKey: RotationKeyType } = $props();
let showPrivateKey = $state(false); let saveForm: HTMLFormElement | undefined = $state();
/** What the password manager and the downloaded file label the key by. */ const accountLabel = $derived(handle || did);
const copyToClipboard = async (text: string) => { try { await navigator.clipboard.writeText(text); alert('Copied to clipboard'); } catch (e) { console.error(e); alert('Failed to copy to clipboard'); } }
//PasswordCredential is Chromium-only and not in the TS DOM types type PasswordCredentialCtor = new (data: { id: string; password: string; name?: string }) => Credential;
const saveToPasswordManager = async () => { try { const win = window as Window & { PasswordCredential?: PasswordCredentialCtor }; if (win.PasswordCredential) { //Chromium: ask the browser directly to store the credential const credential = new win.PasswordCredential({ id: accountLabel, password: rotationKey.privateKey, name: `Rotation key for ${accountLabel}` }); await navigator.credentials.store(credential); } else { //Safari/Firefox and extension password managers watch for a submitted //form with username + password fields. The submit is prevented so the //page never navigates. saveForm?.requestSubmit(); } } catch (e) { console.error(e); } }
const downloadNewRotationKey = async (rotationKey: RotationKeyType) => { if (!rotationKey) return; //try and find the did to add to the file as well let didText = ''; let fileDid = did; if (!fileDid) { try { const {usersDid} = await handleAndPDSResolver(handle); fileDid = usersDid; } catch (e) { //sliently log. Rather the user have their rotation key than not. a did can always be found other ways if needed console.error(e); } } if (fileDid) didText = `DID: ${fileDid}\n`;
const content = `This is a did:plc rotation key for your atproto account. With the private key you can recover your identity if your PDS ever disappears or turns against you (see https://atproto.com/guides/account-recovery).\n\nKEEP IN A SECURE LOCATION\n\n${didText}PublicKey: ${rotationKey.publicKey}\nPrivateKey: ${rotationKey.privateKey}\n`; const blob = new Blob([content], {type: 'text/plain'}); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url;
a.download = `${accountLabel}-rotation-key.txt`; document.body.appendChild(a); a.click(); document.body.removeChild(a); URL.revokeObjectURL(url); }
// A freshly generated key is shown exactly once, so start a download of it straight away. // Browsers may refuse a download that no click started; the button below is the fallback. $effect(() => { void rotationKey.publicKey; // re-run only when a different key is shown untrack(() => downloadNewRotationKey(rotationKey)) .catch((e) => console.error('Automatic rotation key download failed', e)); });</script>
<div class="mt-4 border-2 border-[#f39c12] p-4"> <h3 class="text-[#d35400]">Important: Save Your New Rotation Key Now</h3> <p class="font-bold text-[#c0392b]"> Warning: This is the only time we will show you your private rotation key. Save it in a secure place. If you lose it, you may not be able to recover your account in the event of a PDS failure or hijack. </p> <p class="text-[0.9em]">A key file download should have started. If it did not, use the buttons below.</p> <form method="get" action="#" bind:this={saveForm} onsubmit={(e) => e.preventDefault()}> <!-- Kept for password managers that detect a submitted username + password form. Not shown. --> <input type="text" name="username" autocomplete="username" value={accountLabel} readonly class="sr-only" tabindex="-1" aria-hidden="true"/> <p class="text-[0.9em] opacity-80"> Account: <span class="font-semibold break-all">{accountLabel}</span> {#if handle && did} <span class="break-all opacity-70">({did})</span> {/if} </p> <div class="mb-3.75 text-left"> <label for="rotation-key-private" class="mb-1.25 block">New Rotation Key (Private - keep secret)</label> <div class="flex items-center gap-2"> {#if rotationKey} <input id="rotation-key-private" type={showPrivateKey ? 'text' : 'password'} name="password" autocomplete="new-password" class="w-full min-w-0 flex-1 box-border" value={rotationKey.privateKey} readonly/> {/if} <Button onclick={() => showPrivateKey = !showPrivateKey}> {showPrivateKey ? 'Hide' : 'Show'} </Button> <Button onclick={async () => await copyToClipboard(rotationKey.privateKey)}>Copy </Button> </div> </div> <div class="mb-3.75 flex flex-wrap gap-2 text-left"> <Button onclick={saveToPasswordManager}>Save in Password Manager</Button> <Button onclick={async () => await downloadNewRotationKey(rotationKey)}>Download Key File </Button> </div> </form></div>