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

Compare changes

Choose any two refs to compare.

Changed files
+355 -190
packages
web-ui
+1 -1
justfile
··· 25 25 docker buildx build \ 26 26 --platform linux/arm64,linux/amd64 \ 27 27 --tag fatfingers23/moover_ui:latest \ 28 - --tag fatfingers23/moover_ui:0.0.3 \ 28 + --tag fatfingers23/moover_ui:0.0.4 \ 29 29 --file Dockerfiles/web-ui.Dockerfile \ 30 30 --push .
+5 -1
packages/moover/lib/pdsmoover.js
··· 49 49 * @param {string|null} inviteCode - The invite code you got from the PDS you are migrating to. If null does not include one 50 50 * @param {function|null} statusUpdateHandler - a function that takes a string used to update the UI. Like (status) => console.log(status) 51 51 * @param {string|null} twoFactorCode - Optional, but needed if it fails with 2fa required 52 + * @param verificationCode - Optional verification captcha code for account creation if the PDS requires it 52 53 */ 53 - async migrate(oldHandle, password, newPdsUrl, newEmail, newHandle, inviteCode, statusUpdateHandler = null, twoFactorCode = null) { 54 + async migrate(oldHandle, password, newPdsUrl, newEmail, newHandle, inviteCode, statusUpdateHandler = null, twoFactorCode = null, verificationCode = null) { 54 55 //Leaving this logic that either sets the agent to bsky.social, or the PDS since it's what I found worked best for migrations. 55 56 // handleAndPDSResolver should be able to handle it, but there have been edge cases and this was what worked best 56 57 oldHandle = cleanHandle(oldHandle); ··· 114 115 }; 115 116 if (inviteCode) { 116 117 createAccountRequest.inviteCode = inviteCode; 118 + } 119 + if (verificationCode) { 120 + createAccountRequest.verificationCode = verificationCode; 117 121 } 118 122 const createNewAccount = await newAgent.com.atproto.server.createAccount( 119 123 createAccountRequest,
+1 -1
packages/moover/package.json
··· 1 1 { 2 2 "name": "@pds-moover/moover", 3 - "version": "1.0.4", 3 + "version": "1.0.5", 4 4 "description": "Utilities for ATProto PDS migrations and recovery", 5 5 "repository": { 6 6 "type": "git",
+2 -1
packages/moover/types/pdsmoover.d.ts
··· 35 35 * @param {string|null} inviteCode - The invite code you got from the PDS you are migrating to. If null does not include one 36 36 * @param {function|null} statusUpdateHandler - a function that takes a string used to update the UI. Like (status) => console.log(status) 37 37 * @param {string|null} twoFactorCode - Optional, but needed if it fails with 2fa required 38 + * @param verificationCode - Optional verification captcha code for account creation if the PDS requires it 38 39 */ 39 - migrate(oldHandle: string, password: string, newPdsUrl: string, newEmail: string, newHandle: string, inviteCode: string | null, statusUpdateHandler?: Function | null, twoFactorCode?: string | null): Promise<void>; 40 + migrate(oldHandle: string, password: string, newPdsUrl: string, newEmail: string, newHandle: string, inviteCode: string | null, statusUpdateHandler?: Function | null, twoFactorCode?: string | null, verificationCode?: any): Promise<void>; 40 41 /** 41 42 * Sign and submits the PLC operation to officially migrate the account 42 43 * @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
+1 -1
packages/moover/types/pdsmoover.d.ts.map
··· 1 - {"version":3,"file":"pdsmoover.d.ts","sourceRoot":"","sources":["../lib/pdsmoover.js"],"names":[],"mappings":"AAUA;;;GAGG;AACH;IAEQ,uBAAuB;IACvB,UADW,QAAQ,CACC;IACpB,uBAAuB;IACvB,UADW,QAAQ,CACC;IACpB,uBAAuB;IACvB,cADW,CAAC,MAAM,CAAC,CACG;IAEtB,sBAAsB;IACtB,kBADW,OAAO,CACU;IAC5B,sBAAsB;IACtB,aADW,OAAO,CACK;IACvB,sBAAsB;IACtB,cADW,OAAO,CACM;IACxB,sBAAsB;IACtB,qBADW,OAAO,CACa;IAC/B,sBAAsB;IACtB,cADW,OAAO,CACM;IACxB,sBAAsB;IACtB,kBADW,OAAO,CACU;IAGhC;;;;;;;;;;;;;;OAcG;IACH,mBATW,MAAM,YACN,MAAM,aACN,MAAM,YACN,MAAM,aACN,MAAM,cACN,MAAM,GAAC,IAAI,wBACX,WAAS,IAAI,kBACb,MAAM,GAAC,IAAI,iBAuLrB;IAED;;;;;OAKG;IACH,wBAJW,MAAM,gCACsB,MAAM,EAAE,GAClC,OAAO,CAAC,IAAI,CAAC,CA4BzB;IAED;;;;;;;;;;;OAWG;IACH,gCAPqB,MAAM,eACJ,MAAM,wBAClB,WAAS,IAAI,kBAEb,MAAM,GAAC,IAAI,GACT,OAAO,CAAC,IAAI,CAAC,CAiEzB;IAED;;;;;OAKG;IACH,uCAHW,MAAM,GACJ,OAAO,CAAC,IAAI,CAAC,CAkCzB;CACJ;yBAhYsB,cAAc"} 1 + {"version":3,"file":"pdsmoover.d.ts","sourceRoot":"","sources":["../lib/pdsmoover.js"],"names":[],"mappings":"AAUA;;;GAGG;AACH;IAEQ,uBAAuB;IACvB,UADW,QAAQ,CACC;IACpB,uBAAuB;IACvB,UADW,QAAQ,CACC;IACpB,uBAAuB;IACvB,cADW,CAAC,MAAM,CAAC,CACG;IAEtB,sBAAsB;IACtB,kBADW,OAAO,CACU;IAC5B,sBAAsB;IACtB,aADW,OAAO,CACK;IACvB,sBAAsB;IACtB,cADW,OAAO,CACM;IACxB,sBAAsB;IACtB,qBADW,OAAO,CACa;IAC/B,sBAAsB;IACtB,cADW,OAAO,CACM;IACxB,sBAAsB;IACtB,kBADW,OAAO,CACU;IAGhC;;;;;;;;;;;;;;;OAeG;IACH,mBAVW,MAAM,YACN,MAAM,aACN,MAAM,YACN,MAAM,aACN,MAAM,cACN,MAAM,GAAC,IAAI,wBACX,WAAS,IAAI,kBACb,MAAM,GAAC,IAAI,yCA2LrB;IAED;;;;;OAKG;IACH,wBAJW,MAAM,gCACsB,MAAM,EAAE,GAClC,OAAO,CAAC,IAAI,CAAC,CA4BzB;IAED;;;;;;;;;;;OAWG;IACH,gCAPqB,MAAM,eACJ,MAAM,wBAClB,WAAS,IAAI,kBAEb,MAAM,GAAC,IAAI,GACT,OAAO,CAAC,IAAI,CAAC,CAiEzB;IAED;;;;;OAKG;IACH,uCAHW,MAAM,GACJ,OAAO,CAAC,IAAI,CAAC,CAkCzB;CACJ;yBApYsB,cAAc"}
+1 -1
web-ui/package.json
··· 17 17 "@atcute/client": "^4.0.5", 18 18 "@atcute/lexicons": "^1.2.2", 19 19 "@pds-moover/lexicons": "^1.0.1", 20 - "@pds-moover/moover": "^1.0.4" 20 + "@pds-moover/moover": "^1.0.5" 21 21 }, 22 22 "devDependencies": { 23 23 "@eslint/compat": "^1.4.0",
+18 -18
web-ui/pnpm-lock.yaml
··· 21 21 specifier: ^1.0.1 22 22 version: 1.0.1 23 23 '@pds-moover/moover': 24 - specifier: ^1.0.4 25 - version: 1.0.4(@atcute/identity@1.1.1)(vite@7.1.12(@types/node@22.19.0)) 24 + specifier: ^1.0.5 25 + version: 1.0.5(@atcute/identity@1.1.1)(vite@7.1.12(@types/node@22.19.0)) 26 26 devDependencies: 27 27 '@eslint/compat': 28 28 specifier: ^1.4.0 ··· 79 79 '@atcute/atproto@3.1.9': 80 80 resolution: {integrity: sha512-DyWwHCTdR4hY2BPNbLXgVmm7lI+fceOwWbE4LXbGvbvVtSn+ejSVFaAv01Ra3kWDha0whsOmbJL8JP0QPpf1+w==} 81 81 82 - '@atcute/cbor@2.2.7': 83 - resolution: {integrity: sha512-/mwAF0gnokOphceZqFq3uzMGdd8sbw5y6bxF8CRutRkCCUcpjjpJc5fkLwhxyGgOveF3mZuHE6p7t/+IAqb7Aw==} 82 + '@atcute/cbor@2.2.8': 83 + resolution: {integrity: sha512-UzOAN9BuN6JCXgn0ryV8qZuRJUDrNqrbLd6EFM8jc6RYssjRyGRxNy6RZ1NU/07Hd8Tq/0pz8+nQiMu5Zai5uw==} 84 84 85 85 '@atcute/cid@2.2.6': 86 86 resolution: {integrity: sha512-bTAHHbJ24p+E//V4KCS4xdmd39o211jJswvqQOevj7vk+5IYcgDLx1ryZWZ1sEPOo9x875li/kj5gpKL14RDwQ==} ··· 111 111 '@atcute/uint8array@1.0.5': 112 112 resolution: {integrity: sha512-XLWWxoR2HNl2qU+FCr0rp1APwJXci7HnzbOQLxK55OaMNBXZ19+xNC5ii4QCsThsDxa4JS/JTzuiQLziITWf2Q==} 113 113 114 - '@atcute/util-fetch@1.0.3': 115 - resolution: {integrity: sha512-f8zzTb/xlKIwv2OQ31DhShPUNCmIIleX6p7qIXwWwEUjX6x8skUtpdISSjnImq01LXpltGV5y8yhV4/Mlb7CRQ==} 114 + '@atcute/util-fetch@1.0.4': 115 + resolution: {integrity: sha512-sIU9Qk0dE8PLEXSfhy+gIJV+HpiiknMytCI2SqLlqd0vgZUtEKI/EQfP+23LHWvP+CLCzVDOa6cpH045OlmNBg==} 116 116 117 117 '@atproto/api@0.16.11': 118 118 resolution: {integrity: sha512-1dhfQNHiclb102RW+Ea8Nft5olfqU0Ev/vlQaSX6mWNo1aP5zT+sPODJ8+BTUOYk3vcuvL7QMkqA/rLYy2PMyw==} ··· 386 386 '@pds-moover/lexicons@1.0.1': 387 387 resolution: {integrity: sha512-fv5b/DtHM7FEo/JklyF9gdK0ainlb6mWjWrBe6cmSAeg9G/4O2jBlQUOqfOAICY9gOcrCpkOrk9PHgGw//JQ2A==} 388 388 389 - '@pds-moover/moover@1.0.4': 390 - resolution: {integrity: sha512-VR4pMB9fMUEV0QCxLDaxfreaRGyxZR54UfOk5TLRYEcp2zmaLKFTI3NTcpvIFiVREpy6EzK+RNkwhxUD16OaVA==} 389 + '@pds-moover/moover@1.0.5': 390 + resolution: {integrity: sha512-do8Itd1mrH/446KYJf+velZqsA45ldJCPrEV10eD3nhFJyhf4KBEuseSHIhV0+KIZGU06HvmofBO+v8EZx9ToA==} 391 391 392 392 '@polka/url@1.0.0-next.29': 393 393 resolution: {integrity: sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww==} ··· 677 677 ajv@6.12.6: 678 678 resolution: {integrity: sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==} 679 679 680 - alpinejs@3.15.1: 681 - resolution: {integrity: sha512-HLO1TtiE92VajFHtLLPK8BWaK1YepV/uj31UrfoGnQ00lyFOJZ+oVY3F0DghPAwvg8sLU79pmjGQSytERa2gEg==} 680 + alpinejs@3.15.2: 681 + resolution: {integrity: sha512-2kYF2aG+DTFkE6p0rHG5XmN4VEb6sO9b02aOdU4+i8QN6rL0DbRZQiypDE1gBcGO65yDcqMz5KKYUYgMUxgNkw==} 682 682 683 683 ansi-styles@4.3.0: 684 684 resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} ··· 1352 1352 dependencies: 1353 1353 '@atcute/lexicons': 1.2.2 1354 1354 1355 - '@atcute/cbor@2.2.7': 1355 + '@atcute/cbor@2.2.8': 1356 1356 dependencies: 1357 1357 '@atcute/cid': 2.2.6 1358 1358 '@atcute/multibase': 1.1.6 ··· 1376 1376 1377 1377 '@atcute/did-plc@0.1.7': 1378 1378 dependencies: 1379 - '@atcute/cbor': 2.2.7 1379 + '@atcute/cbor': 2.2.8 1380 1380 '@atcute/cid': 2.2.6 1381 1381 '@atcute/crypto': 2.2.6 1382 1382 '@atcute/identity': 1.1.1 ··· 1389 1389 dependencies: 1390 1390 '@atcute/identity': 1.1.1 1391 1391 '@atcute/lexicons': 1.2.2 1392 - '@atcute/util-fetch': 1.0.3 1392 + '@atcute/util-fetch': 1.0.4 1393 1393 '@badrap/valita': 0.4.6 1394 1394 1395 1395 '@atcute/identity@1.1.1': ··· 1408 1408 1409 1409 '@atcute/uint8array@1.0.5': {} 1410 1410 1411 - '@atcute/util-fetch@1.0.3': 1411 + '@atcute/util-fetch@1.0.4': 1412 1412 dependencies: 1413 1413 '@badrap/valita': 0.4.6 1414 1414 ··· 1626 1626 '@atproto/lexicon': 0.5.1 1627 1627 '@atproto/xrpc': 0.7.5 1628 1628 1629 - '@pds-moover/moover@1.0.4(@atcute/identity@1.1.1)(vite@7.1.12(@types/node@22.19.0))': 1629 + '@pds-moover/moover@1.0.5(@atcute/identity@1.1.1)(vite@7.1.12(@types/node@22.19.0))': 1630 1630 dependencies: 1631 - '@atcute/cbor': 2.2.7 1631 + '@atcute/cbor': 2.2.8 1632 1632 '@atcute/client': 4.0.5 1633 1633 '@atcute/crypto': 2.2.6 1634 1634 '@atcute/did-plc': 0.1.7 ··· 1637 1637 '@atcute/multibase': 1.1.6 1638 1638 '@atproto/api': 0.16.11 1639 1639 '@pds-moover/lexicons': 1.0.1 1640 - alpinejs: 3.15.1 1640 + alpinejs: 3.15.2 1641 1641 vite-plugin-full-reload: 1.2.0 1642 1642 vite-rs-plugin: 1.0.1(vite@7.1.12(@types/node@22.19.0)) 1643 1643 transitivePeerDependencies: ··· 1930 1930 json-schema-traverse: 0.4.1 1931 1931 uri-js: 4.4.1 1932 1932 1933 - alpinejs@3.15.1: 1933 + alpinejs@3.15.2: 1934 1934 dependencies: 1935 1935 '@vue/reactivity': 3.1.5 1936 1936
+210 -166
web-ui/src/routes/moover/[[pds]]/+page.svelte
··· 4 4 import {resolve} from '$app/paths'; 5 5 import {Migrator} from '@pds-moover/moover'; 6 6 import SignThePapers from './SignThePapers.svelte'; 7 + import Captcha from './Captcha.svelte'; 7 8 8 9 let {data} = $props(); 9 10 ··· 43 44 newHandle: '', 44 45 inviteCode: null, 45 46 twoFactorCode: null, 47 + verificationCode: null, 46 48 confirmation: false, 47 49 // Acceptance of provider policies (when required by selected PDS) 48 50 acceptPolicies: false, ··· 63 65 let showStatusMessage = $state(false); 64 66 let askForPlcToken = $state(false); 65 67 let disableSubmit = $state(false); 68 + let showCaptcha = $state(false); 66 69 67 70 let errorMessage: null | string = $state(null); 68 71 let statusMessage: null | string = $state(null); ··· 71 74 const privacyUrl = $derived(selectedPds?.links?.privacyPolicy); 72 75 const tosUrl = $derived(selectedPds?.links?.termsOfService); 73 76 const requiresAccept = $derived(!!(privacyUrl || tosUrl)); 77 + 78 + // Check if phone verification is required 79 + const captchaVerificationRequired = $derived(selectedPds?.phoneVerificationRequired === true); 74 80 75 81 const updateStatusHandler = (status: string) => { 76 82 statusMessage = status; 77 83 } 78 84 85 + function handleCaptchaSuccess(code: string) { 86 + formData.verificationCode = code; 87 + showCaptcha = false; 88 + // Continue with the migration 89 + performMigration(); 90 + } 91 + 92 + function handleCaptchaError(error: string) { 93 + errorMessage = `Verification failed: ${error}`; 94 + disableSubmit = false; 95 + } 96 + 79 97 async function submitMoove(event: SubmitEvent & { currentTarget: EventTarget & HTMLFormElement }) { 80 98 event.preventDefault(); 81 99 disableSubmit = true; ··· 105 123 } 106 124 } 107 125 126 + if (captchaVerificationRequired && formData.createNewAccount && !formData.verificationCode) { 127 + showCaptcha = true; 128 + return; 129 + } 130 + 131 + // Continue with migration 132 + await performMigration(); 133 + } 134 + 135 + async function performMigration() { 108 136 try { 109 137 110 138 if (showTwoFactorCodeInput) { ··· 127 155 128 156 updateStatusHandler('Starting migration...'); 129 157 showStatusMessage = true; 158 + 130 159 await migrator.migrate( 131 160 formData.handle, 132 161 formData.password, ··· 136 165 formData.inviteCode, 137 166 updateStatusHandler, 138 167 formData.twoFactorCode, 168 + formData.verificationCode, 139 169 ); 140 170 if (migrator.migratePlcRecord) { 141 171 //I don't think disable submit is needed, but you never know. ··· 170 200 <br/> 171 201 <a href="https://blacksky.community/profile/did:plc:g7j6qok5us4hjqlwjxwrrkjm/post/3lw3hcuojck2u">Video guide for 172 202 joining blacksky.app</a> 173 - 174 - <form id="moover-form" onsubmit={submitMoove}> 175 - <!-- First section: Login credentials --> 176 - <div class="section"> 177 - <h2>Login for your current PDS</h2> 178 - <div class="form-group"> 179 - <label for="handle">Old Handle:</label> 180 - <input type="text" id="handle" name="handle" placeholder="alice.bsky.social" required 181 - bind:value={formData.handle}> 182 - </div> 203 + {#if showCaptcha} 204 + <Captcha 205 + pdsUrl={formData.newPds} 206 + handle={newHandle} 207 + onSuccess={handleCaptchaSuccess} 208 + onError={handleCaptchaError} 209 + /> 210 + {:else} 183 211 184 - <div class="form-group"> 185 - <label for="password">Old Password (Will also be your new password):</label> 186 - <input type="password" id="password" name="password" required bind:value={formData.password}> 187 - </div> 188 - {#if showTwoFactorCodeInput} 212 + <form id="moover-form" onsubmit={submitMoove}> 213 + <!-- First section: Login credentials --> 214 + <div class="section"> 215 + <h2>Login for your current PDS</h2> 189 216 <div class="form-group"> 190 - <label for="two-factor-code">2FA from the email sent</label> 191 - <input type="text" id="two-factor-code" name="twoFactorCode" 192 - bind:value={formData.twoFactorCode}> 193 - <div class="error-message">Enter your 2fa code here</div> 194 - 217 + <label for="handle">Old Handle:</label> 218 + <input type="text" id="handle" name="handle" placeholder="alice.bsky.social" required 219 + bind:value={formData.handle}> 195 220 </div> 196 - {/if} 197 - </div> 198 221 199 - <!-- Second section: New account details --> 200 - <div class="section"> 201 - <h2>{selectedPds ? `Setup for ${cleanSelectedPds}` : 'Setup for the new PDS'}</h2> 202 - {#if !selectedPds} 203 222 <div class="form-group"> 204 - <label for="new-pds">New PDS (URL):</label> 205 - <input type="url" id="new-pds" name="newPds" placeholder="https://coolnewpds.com" 206 - required bind:value={formData.newPds}> 223 + <label for="password">Old Password (Will also be your new password):</label> 224 + <input type="password" id="password" name="password" required bind:value={formData.password}> 207 225 </div> 208 - {/if} 226 + {#if showTwoFactorCodeInput} 227 + <div class="form-group"> 228 + <label for="two-factor-code">2FA from the email sent</label> 229 + <input type="text" id="two-factor-code" name="twoFactorCode" 230 + bind:value={formData.twoFactorCode}> 231 + <div class="error-message">Enter your 2fa code here</div> 209 232 210 - <div class="form-group"> 211 - <label for="new-email">New Email:</label> 212 - <input type="email" id="new-email" name="newEmail" 213 - placeholder="CanBeSameEmailAsTheOldPds@email.com" 214 - required bind:value={formData.newEmail}> 233 + </div> 234 + {/if} 215 235 </div> 216 236 217 - 218 - <div class="form-group"> 219 - <label for="new-handle">New Handle:</label> 220 - <div class={selectedPds ? 'input-group' : ''}> 221 - <input type="text" id="new-handle" name="newHandle" 222 - placeholder="{handlePlaceHolder}" 223 - required 224 - bind:value={formData.newHandle}> 225 - {#if selectedPds} 237 + <!-- Second section: New account details --> 238 + <div class="section"> 239 + <h2>{selectedPds ? `Setup for ${cleanSelectedPds}` : 'Setup for the new PDS'}</h2> 240 + {#if !selectedPds} 241 + <div class="form-group"> 242 + <label for="new-pds">New PDS (URL):</label> 243 + <input type="url" id="new-pds" name="newPds" placeholder="https://coolnewpds.com" 244 + required bind:value={formData.newPds}> 245 + </div> 246 + {/if} 226 247 227 - <select bind:value={selectedDomain} class="domain-select"> 228 - {#each selectedPds?.availableUserDomains as domain (domain)} 229 - <option value={domain}>{domain}</option> 230 - {/each} 231 - <option value="custom">I have my own domain setup</option> 248 + <div class="form-group"> 249 + <label for="new-email">New Email:</label> 250 + <input type="email" id="new-email" name="newEmail" 251 + placeholder="CanBeSameEmailAsTheOldPds@email.com" 252 + required bind:value={formData.newEmail}> 253 + </div> 232 254 233 - </select> 234 - {/if} 235 - </div> 236 - </div> 237 255 238 - {#if !selectedPds || selectedPds.inviteCodeRequired !== false} 239 256 <div class="form-group"> 240 - <label for="invite-code">Invite Code:</label> 241 - <input type="text" id="invite-code" name="inviteCode" 242 - placeholder="Invite code from your new PDS (Leave blank if you don't have one)" 243 - bind:value={formData.inviteCode}> 244 - </div> 245 - {/if} 246 - </div> 257 + <label for="new-handle">New Handle:</label> 258 + <div class={selectedPds ? 'input-group' : ''}> 259 + <input type="text" id="new-handle" name="newHandle" 260 + placeholder="{handlePlaceHolder}" 261 + required 262 + bind:value={formData.newHandle}> 263 + {#if selectedPds} 247 264 265 + <select bind:value={selectedDomain} class="domain-select"> 266 + {#each selectedPds?.availableUserDomains as domain (domain)} 267 + <option value={domain}>{domain}</option> 268 + {/each} 269 + <option value="custom">I have my own domain setup</option> 248 270 249 - <div class="form-group"> 250 - <button type="button" onclick={() => showAdvance = !showAdvance} id="advance" name="advance">Advance 251 - Options 252 - </button> 253 - </div> 254 - {#if showAdvance} 255 - <div class="section" style="padding-bottom: 10px; text-align: left"> 256 - <h3>Pick and choose which actions to run</h3> 257 - <p>Useful if a migration failed and you want to have a bit more manual control</p> 258 - <div class="form-control"> 259 - <label class="moove-checkbox-label"> 260 - <input type="checkbox" id="createNewAccount" name="createNewAccount" 261 - bind:checked={formData.createNewAccount}> 262 - Create a New Account on the New PDS 263 - </label> 264 - </div> 265 - <div class="form-control"> 266 - <label class="moove-checkbox-label"> 267 - <input bind:checked={formData.migrateRepo} type="checkbox" id="migrateRepo" 268 - name="migrateRepo"> 269 - Migrate Repo 270 - </label> 271 - </div> 272 - <div class="form-control"> 273 - <label class="moove-checkbox-label"> 274 - <input bind:checked={formData.migrateBlobs} type="checkbox" id="migrateBlobs" 275 - name="migrateBlobs"> 276 - Migrate Blobs 277 - </label> 278 - </div> 279 - <div class="form-control"> 280 - <label class="moove-checkbox-label"> 281 - <input bind:checked={formData.migrateMissingBlobs} type="checkbox" id="migrateMissingBlobs" 282 - name="migrateMissingBlobs"> 283 - Migrate Missing Blobs 284 - </label> 285 - </div> 286 - <div class="form-control"> 287 - <label class="moove-checkbox-label"> 288 - <input bind:checked={formData.migratePrefs} type="checkbox" id="migratePrefs" 289 - name="migratePrefs"> 290 - Migrate Prefs 291 - </label> 292 - </div> 293 - <div class="form-control"> 294 - <label class="moove-checkbox-label"> 295 - <input bind:checked={formData.migratePlcRecord} type="checkbox" id="migratePlcRecord" 296 - name="migratePlcRecord"> 297 - Migrate PLC Record 298 - </label> 271 + </select> 272 + {/if} 273 + </div> 299 274 </div> 300 275 276 + {#if !selectedPds || selectedPds.inviteCodeRequired !== false} 277 + <div class="form-group"> 278 + <label for="invite-code">Invite Code:</label> 279 + <input type="text" id="invite-code" name="inviteCode" 280 + placeholder="Invite code from your new PDS (Leave blank if you don't have one)" 281 + bind:value={formData.inviteCode}> 282 + </div> 283 + {/if} 301 284 </div> 302 - {/if} 303 285 304 - {#if requiresAccept} 305 - <div class="section" style="text-align: left"> 306 - <h3>Provider policies</h3> 307 - <p> 308 - To migrate to {cleanSelectedPds}, you must review and accept: 309 - </p> 310 - <ul> 311 - {#if privacyUrl} 312 - <li><a href={privacyUrl} target="_blank" rel="noopener noreferrer">Privacy 313 - Policy</a></li> 314 - {/if} 315 - {#if tosUrl} 316 - <li><a href={tosUrl} target="_blank" rel="noopener noreferrer">Terms of Service</a></li> 317 - {/if} 318 - </ul> 319 - <div class="form-group"> 320 - <label for="accept-policies" class="moove-checkbox-label"> 321 - <input bind:checked={formData.acceptPolicies} type="checkbox" id="accept-policies" 322 - name="acceptPolicies" required> 323 - <span> 286 + 287 + <div class="form-group"> 288 + <button type="button" onclick={() => showAdvance = !showAdvance} id="advance" name="advance">Advance 289 + Options 290 + </button> 291 + </div> 292 + {#if showAdvance} 293 + <div class="section" style="padding-bottom: 10px; text-align: left"> 294 + <h3>Pick and choose which actions to run</h3> 295 + <p>Useful if a migration failed and you want to have a bit more manual control</p> 296 + <div class="form-control"> 297 + <label class="moove-checkbox-label"> 298 + <input type="checkbox" id="createNewAccount" name="createNewAccount" 299 + bind:checked={formData.createNewAccount}> 300 + Create a New Account on the New PDS 301 + </label> 302 + </div> 303 + <div class="form-control"> 304 + <label class="moove-checkbox-label"> 305 + <input bind:checked={formData.migrateRepo} type="checkbox" id="migrateRepo" 306 + name="migrateRepo"> 307 + Migrate Repo 308 + </label> 309 + </div> 310 + <div class="form-control"> 311 + <label class="moove-checkbox-label"> 312 + <input bind:checked={formData.migrateBlobs} type="checkbox" id="migrateBlobs" 313 + name="migrateBlobs"> 314 + Migrate Blobs 315 + </label> 316 + </div> 317 + <div class="form-control"> 318 + <label class="moove-checkbox-label"> 319 + <input bind:checked={formData.migrateMissingBlobs} type="checkbox" 320 + id="migrateMissingBlobs" 321 + name="migrateMissingBlobs"> 322 + Migrate Missing Blobs 323 + </label> 324 + </div> 325 + <div class="form-control"> 326 + <label class="moove-checkbox-label"> 327 + <input bind:checked={formData.migratePrefs} type="checkbox" id="migratePrefs" 328 + name="migratePrefs"> 329 + Migrate Prefs 330 + </label> 331 + </div> 332 + <div class="form-control"> 333 + <label class="moove-checkbox-label"> 334 + <input bind:checked={formData.migratePlcRecord} type="checkbox" id="migratePlcRecord" 335 + name="migratePlcRecord"> 336 + Migrate PLC Record 337 + </label> 338 + </div> 339 + 340 + </div> 341 + {/if} 342 + 343 + {#if requiresAccept} 344 + <div class="section" style="text-align: left"> 345 + <h3>Provider policies</h3> 346 + <p> 347 + To migrate to {cleanSelectedPds}, you must review and accept: 348 + </p> 349 + <ul> 350 + {#if privacyUrl} 351 + <li><a href={privacyUrl} target="_blank" rel="noopener noreferrer">Privacy 352 + Policy</a></li> 353 + {/if} 354 + {#if tosUrl} 355 + <li><a href={tosUrl} target="_blank" rel="noopener noreferrer">Terms of Service</a></li> 356 + {/if} 357 + </ul> 358 + <div class="form-group"> 359 + <label for="accept-policies" class="moove-checkbox-label"> 360 + <input bind:checked={formData.acceptPolicies} type="checkbox" id="accept-policies" 361 + name="acceptPolicies" required> 362 + <span> 324 363 I have read and accept 325 364 326 365 </span> 327 - </label> 366 + </label> 367 + </div> 328 368 </div> 369 + {/if} 370 + <p style="text-align: left">There are some risks that come with doing an account migration. 371 + (Can view them 372 + <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>) 373 + and that the creator or host of this migration tool is not liable and will not be able to help you 374 + in 375 + the 376 + event something goes wrong. I also have read over the <a href={resolve('/info')}>extended 377 + information 378 + from 379 + PDS MOOver 380 + about account 381 + migrations.</a> 382 + </p> 383 + <div class="form-group"> 384 + <label for="confirmation" class="moove-checkbox-label"> 385 + <input bind:checked={formData.confirmation} type="checkbox" id="confirmation" 386 + name="confirmation" 387 + required> 388 + <span>I understand</span> 389 + </label> 329 390 </div> 330 - {/if} 331 - <p style="text-align: left">There are some risks that come with doing an account migration. 332 - (Can view them 333 - <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>) 334 - and that the creator or host of this migration tool is not liable and will not be able to help you in 335 - the 336 - event something goes wrong. I also have read over the <a href={resolve('/info')}>extended information 337 - from 338 - PDS MOOver 339 - about account 340 - migrations.</a> 341 - </p> 342 - <div class="form-group"> 343 - <label for="confirmation" class="moove-checkbox-label"> 344 - <input bind:checked={formData.confirmation} type="checkbox" id="confirmation" name="confirmation" 345 - required> 346 - <span>I understand</span> 347 - </label> 348 - </div> 349 - {#if errorMessage !== null} 350 - <div class="error-message">{errorMessage}</div> 351 - {/if} 391 + {#if errorMessage !== null} 392 + <div class="error-message">{errorMessage}</div> 393 + {/if} 394 + 395 + {#if showStatusMessage} 396 + <div id="warning">*Please make sure to stay on this page during the MOOve for the 397 + best result 398 + </div> 399 + <div id="status-message" class="status-message">{statusMessage}</div> 400 + {/if} 401 + 352 402 353 - {#if showStatusMessage} 354 - <div id="warning">*Please make sure to stay on this page during the MOOve for the 355 - best result 403 + <div> 404 + <button disabled={disableSubmit} 405 + type="submit">{selectedPds ? `MOOve to ${cleanSelectedPds}` : 'MOOve'}</button> 356 406 </div> 357 - <div id="status-message" class="status-message">{statusMessage}</div> 358 - {/if} 359 407 360 - <div> 361 - <button disabled={disableSubmit} 362 - type="submit">{selectedPds ? `MOOve to ${cleanSelectedPds}` : 'MOOve'}</button> 363 - </div> 364 - </form> 365 - 408 + </form> 409 + {/if} 366 410 {:else} 367 411 <SignThePapers migrator={migrator} newHandle={newHandle}/> 368 412 {/if}
+116
web-ui/src/routes/moover/[[pds]]/Captcha.svelte
··· 1 + <script lang="ts"> 2 + import {onMount} from 'svelte'; 3 + 4 + interface CaptchaProps { 5 + pdsUrl: string; 6 + handle: string; 7 + onSuccess: (code: string) => void; 8 + onError?: (error: string) => void; 9 + } 10 + 11 + let {pdsUrl, handle, onSuccess, onError}: CaptchaProps = $props(); 12 + 13 + function generateState(): string { 14 + const array = new Uint8Array(32); 15 + crypto.getRandomValues(array); 16 + return Array.from(array, byte => byte.toString(16).padStart(2, '0')).join(''); 17 + } 18 + 19 + let captcha_state = $state(generateState()); 20 + let iframeRef: HTMLIFrameElement | null = $state(null); 21 + let isLoading = $state(true); 22 + 23 + 24 + const gateUrl = $derived( 25 + `${pdsUrl}/gate/signup?state=${encodeURIComponent(captcha_state)}&handle=${encodeURIComponent(handle)}&redirect_url=${encodeURIComponent(window.location.origin)}`, 26 + ); 27 + 28 + // Monitor iframe for URL changes 29 + function checkIframeUrl() { 30 + if (!iframeRef) return; 31 + 32 + try { 33 + const iframeUrl = new URL(iframeRef.contentWindow?.location.href ?? ''); 34 + 35 + // Check if the iframe has been redirected with code and state parameters 36 + // This indicates the captcha was completed 37 + const urlState = iframeUrl.searchParams.get('state'); 38 + const code = iframeUrl.searchParams.get('code'); 39 + 40 + // Only process if we have at least a state parameter (indicates redirect happened) 41 + if (urlState) { 42 + 43 + // Verify state matches 44 + if (urlState !== captcha_state) { 45 + const stateError = 'State mismatch - possible security issue'; 46 + onError?.(stateError); 47 + return; 48 + } 49 + if (!code) { 50 + const codeError = 'No code returned from captcha'; 51 + onError?.(codeError); 52 + return; 53 + 54 + } 55 + 56 + onSuccess(code); 57 + } 58 + } catch { 59 + /* empty */ 60 + } 61 + } 62 + 63 + onMount(() => { 64 + // Poll for URL changes 65 + const interval = setInterval(checkIframeUrl, 100); 66 + 67 + return () => clearInterval(interval); 68 + }); 69 + </script> 70 + 71 + <div class="iframe-wrapper"> 72 + <iframe 73 + bind:this={iframeRef} 74 + src={gateUrl} 75 + title="Captcha Verification" 76 + onload={() => isLoading = false} 77 + ></iframe> 78 + {#if isLoading} 79 + <div class="loading-overlay"> 80 + <p>Loading verification...</p> 81 + </div> 82 + {/if} 83 + </div> 84 + 85 + <style> 86 + 87 + .iframe-wrapper { 88 + position: relative; 89 + width: 100%; 90 + height: 500px; 91 + background: white; 92 + border: 1px solid #ddd; 93 + border-radius: 4px; 94 + overflow: hidden; 95 + } 96 + 97 + iframe { 98 + width: 100%; 99 + height: 100%; 100 + border: none; 101 + } 102 + 103 + .loading-overlay { 104 + position: absolute; 105 + top: 0; 106 + left: 0; 107 + right: 0; 108 + bottom: 0; 109 + background: rgba(255, 255, 255, 0.9); 110 + display: flex; 111 + align-items: center; 112 + justify-content: center; 113 + color: #666; 114 + } 115 + 116 + </style>