Client side atproto account migrator in your web browser, along with services for backups and adversarial migrations.

Compare changes

Choose any two refs to compare.

Changed files
+357 -192
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); ··· 115 116 if (inviteCode) { 116 117 createAccountRequest.inviteCode = inviteCode; 117 118 } 119 + if (verificationCode) { 120 + createAccountRequest.verificationCode = verificationCode; 121 + } 118 122 const createNewAccount = await newAgent.com.atproto.server.createAccount( 119 123 createAccountRequest, 120 124 {
+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
+212 -168
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); ··· 72 75 const tosUrl = $derived(selectedPds?.links?.termsOfService); 73 76 const requiresAccept = $derived(!!(privacyUrl || tosUrl)); 74 77 78 + // Check if phone verification is required 79 + const captchaVerificationRequired = $derived(selectedPds?.phoneVerificationRequired === true); 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> 203 + {#if showCaptcha} 204 + <Captcha 205 + pdsUrl={formData.newPds} 206 + handle={newHandle} 207 + onSuccess={handleCaptchaSuccess} 208 + onError={handleCaptchaError} 209 + /> 210 + {:else} 211 + 212 + <form id="moover-form" onsubmit={submitMoove}> 213 + <!-- First section: Login credentials --> 214 + <div class="section"> 215 + <h2>Login for your current PDS</h2> 216 + <div class="form-group"> 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}> 220 + </div> 173 221 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}> 222 + <div class="form-group"> 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}> 225 + </div> 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> 232 + 233 + </div> 234 + {/if} 182 235 </div> 183 236 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} 189 - <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> 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} 194 247 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}> 195 253 </div> 196 - {/if} 197 - </div> 198 254 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} 255 + 203 256 <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}> 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} 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> 270 + 271 + </select> 272 + {/if} 273 + </div> 207 274 </div> 208 - {/if} 209 275 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}> 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} 215 284 </div> 216 285 217 286 218 287 <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} 226 - 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> 232 - 233 - </select> 234 - {/if} 235 - </div> 288 + <button type="button" onclick={() => showAdvance = !showAdvance} id="advance" name="advance">Advance 289 + Options 290 + </button> 236 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> 237 339 238 - {#if !selectedPds || selectedPds.inviteCodeRequired !== false} 239 - <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 340 </div> 245 341 {/if} 246 - </div> 247 - 248 - 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> 299 - </div> 300 342 301 - </div> 302 - {/if} 303 - 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> 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} 352 - 353 - {#if showStatusMessage} 354 - <div id="warning">*Please make sure to stay on this page during the MOOve for the 355 - best result 356 - </div> 357 - <div id="status-message" class="status-message">{statusMessage}</div> 358 - {/if} 391 + {#if errorMessage !== null} 392 + <div class="error-message">{errorMessage}</div> 393 + {/if} 359 394 360 - <div> 361 - <button disabled={disableSubmit} 362 - type="submit">{selectedPds ? `MOOve to ${cleanSelectedPds}` : 'MOOve'}</button> 363 - </div> 364 - </form> 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 + 402 + 403 + <div> 404 + <button disabled={disableSubmit} 405 + type="submit">{selectedPds ? `MOOve to ${cleanSelectedPds}` : 'MOOve'}</button> 406 + </div> 365 407 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>