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
+357 -192
packages
web-ui
+1 -1
justfile
··· 25 docker buildx build \ 26 --platform linux/arm64,linux/amd64 \ 27 --tag fatfingers23/moover_ui:latest \ 28 - --tag fatfingers23/moover_ui:0.0.3 \ 29 --file Dockerfiles/web-ui.Dockerfile \ 30 --push .
··· 25 docker buildx build \ 26 --platform linux/arm64,linux/amd64 \ 27 --tag fatfingers23/moover_ui:latest \ 28 + --tag fatfingers23/moover_ui:0.0.4 \ 29 --file Dockerfiles/web-ui.Dockerfile \ 30 --push .
+5 -1
packages/moover/lib/pdsmoover.js
··· 49 * @param {string|null} inviteCode - The invite code you got from the PDS you are migrating to. If null does not include one 50 * @param {function|null} statusUpdateHandler - a function that takes a string used to update the UI. Like (status) => console.log(status) 51 * @param {string|null} twoFactorCode - Optional, but needed if it fails with 2fa required 52 */ 53 - async migrate(oldHandle, password, newPdsUrl, newEmail, newHandle, inviteCode, statusUpdateHandler = null, twoFactorCode = null) { 54 //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 // handleAndPDSResolver should be able to handle it, but there have been edge cases and this was what worked best 56 oldHandle = cleanHandle(oldHandle); ··· 115 if (inviteCode) { 116 createAccountRequest.inviteCode = inviteCode; 117 } 118 const createNewAccount = await newAgent.com.atproto.server.createAccount( 119 createAccountRequest, 120 {
··· 49 * @param {string|null} inviteCode - The invite code you got from the PDS you are migrating to. If null does not include one 50 * @param {function|null} statusUpdateHandler - a function that takes a string used to update the UI. Like (status) => console.log(status) 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 53 */ 54 + async migrate(oldHandle, password, newPdsUrl, newEmail, newHandle, inviteCode, statusUpdateHandler = null, twoFactorCode = null, verificationCode = null) { 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. 56 // handleAndPDSResolver should be able to handle it, but there have been edge cases and this was what worked best 57 oldHandle = cleanHandle(oldHandle); ··· 116 if (inviteCode) { 117 createAccountRequest.inviteCode = inviteCode; 118 } 119 + if (verificationCode) { 120 + createAccountRequest.verificationCode = verificationCode; 121 + } 122 const createNewAccount = await newAgent.com.atproto.server.createAccount( 123 createAccountRequest, 124 {
+1 -1
packages/moover/package.json
··· 1 { 2 "name": "@pds-moover/moover", 3 - "version": "1.0.4", 4 "description": "Utilities for ATProto PDS migrations and recovery", 5 "repository": { 6 "type": "git",
··· 1 { 2 "name": "@pds-moover/moover", 3 + "version": "1.0.5", 4 "description": "Utilities for ATProto PDS migrations and recovery", 5 "repository": { 6 "type": "git",
+2 -1
packages/moover/types/pdsmoover.d.ts
··· 35 * @param {string|null} inviteCode - The invite code you got from the PDS you are migrating to. If null does not include one 36 * @param {function|null} statusUpdateHandler - a function that takes a string used to update the UI. Like (status) => console.log(status) 37 * @param {string|null} twoFactorCode - Optional, but needed if it fails with 2fa required 38 */ 39 - migrate(oldHandle: string, password: string, newPdsUrl: string, newEmail: string, newHandle: string, inviteCode: string | null, statusUpdateHandler?: Function | null, twoFactorCode?: string | null): Promise<void>; 40 /** 41 * Sign and submits the PLC operation to officially migrate the account 42 * @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
··· 35 * @param {string|null} inviteCode - The invite code you got from the PDS you are migrating to. If null does not include one 36 * @param {function|null} statusUpdateHandler - a function that takes a string used to update the UI. Like (status) => console.log(status) 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 39 */ 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>; 41 /** 42 * Sign and submits the PLC operation to officially migrate the account 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 "@atcute/client": "^4.0.5", 18 "@atcute/lexicons": "^1.2.2", 19 "@pds-moover/lexicons": "^1.0.1", 20 - "@pds-moover/moover": "^1.0.4" 21 }, 22 "devDependencies": { 23 "@eslint/compat": "^1.4.0",
··· 17 "@atcute/client": "^4.0.5", 18 "@atcute/lexicons": "^1.2.2", 19 "@pds-moover/lexicons": "^1.0.1", 20 + "@pds-moover/moover": "^1.0.5" 21 }, 22 "devDependencies": { 23 "@eslint/compat": "^1.4.0",
+18 -18
web-ui/pnpm-lock.yaml
··· 21 specifier: ^1.0.1 22 version: 1.0.1 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)) 26 devDependencies: 27 '@eslint/compat': 28 specifier: ^1.4.0 ··· 79 '@atcute/atproto@3.1.9': 80 resolution: {integrity: sha512-DyWwHCTdR4hY2BPNbLXgVmm7lI+fceOwWbE4LXbGvbvVtSn+ejSVFaAv01Ra3kWDha0whsOmbJL8JP0QPpf1+w==} 81 82 - '@atcute/cbor@2.2.7': 83 - resolution: {integrity: sha512-/mwAF0gnokOphceZqFq3uzMGdd8sbw5y6bxF8CRutRkCCUcpjjpJc5fkLwhxyGgOveF3mZuHE6p7t/+IAqb7Aw==} 84 85 '@atcute/cid@2.2.6': 86 resolution: {integrity: sha512-bTAHHbJ24p+E//V4KCS4xdmd39o211jJswvqQOevj7vk+5IYcgDLx1ryZWZ1sEPOo9x875li/kj5gpKL14RDwQ==} ··· 111 '@atcute/uint8array@1.0.5': 112 resolution: {integrity: sha512-XLWWxoR2HNl2qU+FCr0rp1APwJXci7HnzbOQLxK55OaMNBXZ19+xNC5ii4QCsThsDxa4JS/JTzuiQLziITWf2Q==} 113 114 - '@atcute/util-fetch@1.0.3': 115 - resolution: {integrity: sha512-f8zzTb/xlKIwv2OQ31DhShPUNCmIIleX6p7qIXwWwEUjX6x8skUtpdISSjnImq01LXpltGV5y8yhV4/Mlb7CRQ==} 116 117 '@atproto/api@0.16.11': 118 resolution: {integrity: sha512-1dhfQNHiclb102RW+Ea8Nft5olfqU0Ev/vlQaSX6mWNo1aP5zT+sPODJ8+BTUOYk3vcuvL7QMkqA/rLYy2PMyw==} ··· 386 '@pds-moover/lexicons@1.0.1': 387 resolution: {integrity: sha512-fv5b/DtHM7FEo/JklyF9gdK0ainlb6mWjWrBe6cmSAeg9G/4O2jBlQUOqfOAICY9gOcrCpkOrk9PHgGw//JQ2A==} 388 389 - '@pds-moover/moover@1.0.4': 390 - resolution: {integrity: sha512-VR4pMB9fMUEV0QCxLDaxfreaRGyxZR54UfOk5TLRYEcp2zmaLKFTI3NTcpvIFiVREpy6EzK+RNkwhxUD16OaVA==} 391 392 '@polka/url@1.0.0-next.29': 393 resolution: {integrity: sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww==} ··· 677 ajv@6.12.6: 678 resolution: {integrity: sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==} 679 680 - alpinejs@3.15.1: 681 - resolution: {integrity: sha512-HLO1TtiE92VajFHtLLPK8BWaK1YepV/uj31UrfoGnQ00lyFOJZ+oVY3F0DghPAwvg8sLU79pmjGQSytERa2gEg==} 682 683 ansi-styles@4.3.0: 684 resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} ··· 1352 dependencies: 1353 '@atcute/lexicons': 1.2.2 1354 1355 - '@atcute/cbor@2.2.7': 1356 dependencies: 1357 '@atcute/cid': 2.2.6 1358 '@atcute/multibase': 1.1.6 ··· 1376 1377 '@atcute/did-plc@0.1.7': 1378 dependencies: 1379 - '@atcute/cbor': 2.2.7 1380 '@atcute/cid': 2.2.6 1381 '@atcute/crypto': 2.2.6 1382 '@atcute/identity': 1.1.1 ··· 1389 dependencies: 1390 '@atcute/identity': 1.1.1 1391 '@atcute/lexicons': 1.2.2 1392 - '@atcute/util-fetch': 1.0.3 1393 '@badrap/valita': 0.4.6 1394 1395 '@atcute/identity@1.1.1': ··· 1408 1409 '@atcute/uint8array@1.0.5': {} 1410 1411 - '@atcute/util-fetch@1.0.3': 1412 dependencies: 1413 '@badrap/valita': 0.4.6 1414 ··· 1626 '@atproto/lexicon': 0.5.1 1627 '@atproto/xrpc': 0.7.5 1628 1629 - '@pds-moover/moover@1.0.4(@atcute/identity@1.1.1)(vite@7.1.12(@types/node@22.19.0))': 1630 dependencies: 1631 - '@atcute/cbor': 2.2.7 1632 '@atcute/client': 4.0.5 1633 '@atcute/crypto': 2.2.6 1634 '@atcute/did-plc': 0.1.7 ··· 1637 '@atcute/multibase': 1.1.6 1638 '@atproto/api': 0.16.11 1639 '@pds-moover/lexicons': 1.0.1 1640 - alpinejs: 3.15.1 1641 vite-plugin-full-reload: 1.2.0 1642 vite-rs-plugin: 1.0.1(vite@7.1.12(@types/node@22.19.0)) 1643 transitivePeerDependencies: ··· 1930 json-schema-traverse: 0.4.1 1931 uri-js: 4.4.1 1932 1933 - alpinejs@3.15.1: 1934 dependencies: 1935 '@vue/reactivity': 3.1.5 1936
··· 21 specifier: ^1.0.1 22 version: 1.0.1 23 '@pds-moover/moover': 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 devDependencies: 27 '@eslint/compat': 28 specifier: ^1.4.0 ··· 79 '@atcute/atproto@3.1.9': 80 resolution: {integrity: sha512-DyWwHCTdR4hY2BPNbLXgVmm7lI+fceOwWbE4LXbGvbvVtSn+ejSVFaAv01Ra3kWDha0whsOmbJL8JP0QPpf1+w==} 81 82 + '@atcute/cbor@2.2.8': 83 + resolution: {integrity: sha512-UzOAN9BuN6JCXgn0ryV8qZuRJUDrNqrbLd6EFM8jc6RYssjRyGRxNy6RZ1NU/07Hd8Tq/0pz8+nQiMu5Zai5uw==} 84 85 '@atcute/cid@2.2.6': 86 resolution: {integrity: sha512-bTAHHbJ24p+E//V4KCS4xdmd39o211jJswvqQOevj7vk+5IYcgDLx1ryZWZ1sEPOo9x875li/kj5gpKL14RDwQ==} ··· 111 '@atcute/uint8array@1.0.5': 112 resolution: {integrity: sha512-XLWWxoR2HNl2qU+FCr0rp1APwJXci7HnzbOQLxK55OaMNBXZ19+xNC5ii4QCsThsDxa4JS/JTzuiQLziITWf2Q==} 113 114 + '@atcute/util-fetch@1.0.4': 115 + resolution: {integrity: sha512-sIU9Qk0dE8PLEXSfhy+gIJV+HpiiknMytCI2SqLlqd0vgZUtEKI/EQfP+23LHWvP+CLCzVDOa6cpH045OlmNBg==} 116 117 '@atproto/api@0.16.11': 118 resolution: {integrity: sha512-1dhfQNHiclb102RW+Ea8Nft5olfqU0Ev/vlQaSX6mWNo1aP5zT+sPODJ8+BTUOYk3vcuvL7QMkqA/rLYy2PMyw==} ··· 386 '@pds-moover/lexicons@1.0.1': 387 resolution: {integrity: sha512-fv5b/DtHM7FEo/JklyF9gdK0ainlb6mWjWrBe6cmSAeg9G/4O2jBlQUOqfOAICY9gOcrCpkOrk9PHgGw//JQ2A==} 388 389 + '@pds-moover/moover@1.0.5': 390 + resolution: {integrity: sha512-do8Itd1mrH/446KYJf+velZqsA45ldJCPrEV10eD3nhFJyhf4KBEuseSHIhV0+KIZGU06HvmofBO+v8EZx9ToA==} 391 392 '@polka/url@1.0.0-next.29': 393 resolution: {integrity: sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww==} ··· 677 ajv@6.12.6: 678 resolution: {integrity: sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==} 679 680 + alpinejs@3.15.2: 681 + resolution: {integrity: sha512-2kYF2aG+DTFkE6p0rHG5XmN4VEb6sO9b02aOdU4+i8QN6rL0DbRZQiypDE1gBcGO65yDcqMz5KKYUYgMUxgNkw==} 682 683 ansi-styles@4.3.0: 684 resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} ··· 1352 dependencies: 1353 '@atcute/lexicons': 1.2.2 1354 1355 + '@atcute/cbor@2.2.8': 1356 dependencies: 1357 '@atcute/cid': 2.2.6 1358 '@atcute/multibase': 1.1.6 ··· 1376 1377 '@atcute/did-plc@0.1.7': 1378 dependencies: 1379 + '@atcute/cbor': 2.2.8 1380 '@atcute/cid': 2.2.6 1381 '@atcute/crypto': 2.2.6 1382 '@atcute/identity': 1.1.1 ··· 1389 dependencies: 1390 '@atcute/identity': 1.1.1 1391 '@atcute/lexicons': 1.2.2 1392 + '@atcute/util-fetch': 1.0.4 1393 '@badrap/valita': 0.4.6 1394 1395 '@atcute/identity@1.1.1': ··· 1408 1409 '@atcute/uint8array@1.0.5': {} 1410 1411 + '@atcute/util-fetch@1.0.4': 1412 dependencies: 1413 '@badrap/valita': 0.4.6 1414 ··· 1626 '@atproto/lexicon': 0.5.1 1627 '@atproto/xrpc': 0.7.5 1628 1629 + '@pds-moover/moover@1.0.5(@atcute/identity@1.1.1)(vite@7.1.12(@types/node@22.19.0))': 1630 dependencies: 1631 + '@atcute/cbor': 2.2.8 1632 '@atcute/client': 4.0.5 1633 '@atcute/crypto': 2.2.6 1634 '@atcute/did-plc': 0.1.7 ··· 1637 '@atcute/multibase': 1.1.6 1638 '@atproto/api': 0.16.11 1639 '@pds-moover/lexicons': 1.0.1 1640 + alpinejs: 3.15.2 1641 vite-plugin-full-reload: 1.2.0 1642 vite-rs-plugin: 1.0.1(vite@7.1.12(@types/node@22.19.0)) 1643 transitivePeerDependencies: ··· 1930 json-schema-traverse: 0.4.1 1931 uri-js: 4.4.1 1932 1933 + alpinejs@3.15.2: 1934 dependencies: 1935 '@vue/reactivity': 3.1.5 1936
+212 -168
web-ui/src/routes/moover/[[pds]]/+page.svelte
··· 4 import {resolve} from '$app/paths'; 5 import {Migrator} from '@pds-moover/moover'; 6 import SignThePapers from './SignThePapers.svelte'; 7 8 let {data} = $props(); 9 ··· 43 newHandle: '', 44 inviteCode: null, 45 twoFactorCode: null, 46 confirmation: false, 47 // Acceptance of provider policies (when required by selected PDS) 48 acceptPolicies: false, ··· 63 let showStatusMessage = $state(false); 64 let askForPlcToken = $state(false); 65 let disableSubmit = $state(false); 66 67 let errorMessage: null | string = $state(null); 68 let statusMessage: null | string = $state(null); ··· 72 const tosUrl = $derived(selectedPds?.links?.termsOfService); 73 const requiresAccept = $derived(!!(privacyUrl || tosUrl)); 74 75 const updateStatusHandler = (status: string) => { 76 statusMessage = status; 77 } 78 79 async function submitMoove(event: SubmitEvent & { currentTarget: EventTarget & HTMLFormElement }) { 80 event.preventDefault(); 81 disableSubmit = true; ··· 105 } 106 } 107 108 try { 109 110 if (showTwoFactorCodeInput) { ··· 127 128 updateStatusHandler('Starting migration...'); 129 showStatusMessage = true; 130 await migrator.migrate( 131 formData.handle, 132 formData.password, ··· 136 formData.inviteCode, 137 updateStatusHandler, 138 formData.twoFactorCode, 139 ); 140 if (migrator.migratePlcRecord) { 141 //I don't think disable submit is needed, but you never know. ··· 170 <br/> 171 <a href="https://blacksky.community/profile/did:plc:g7j6qok5us4hjqlwjxwrrkjm/post/3lw3hcuojck2u">Video guide for 172 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> 183 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> 194 195 </div> 196 - {/if} 197 - </div> 198 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 <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}> 207 </div> 208 - {/if} 209 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}> 215 </div> 216 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} 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> 236 </div> 237 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 </div> 245 {/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 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> 324 I have read and accept 325 326 </span> 327 - </label> 328 </div> 329 </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} 359 360 - <div> 361 - <button disabled={disableSubmit} 362 - type="submit">{selectedPds ? `MOOve to ${cleanSelectedPds}` : 'MOOve'}</button> 363 - </div> 364 - </form> 365 366 {:else} 367 <SignThePapers migrator={migrator} newHandle={newHandle}/> 368 {/if}
··· 4 import {resolve} from '$app/paths'; 5 import {Migrator} from '@pds-moover/moover'; 6 import SignThePapers from './SignThePapers.svelte'; 7 + import Captcha from './Captcha.svelte'; 8 9 let {data} = $props(); 10 ··· 44 newHandle: '', 45 inviteCode: null, 46 twoFactorCode: null, 47 + verificationCode: null, 48 confirmation: false, 49 // Acceptance of provider policies (when required by selected PDS) 50 acceptPolicies: false, ··· 65 let showStatusMessage = $state(false); 66 let askForPlcToken = $state(false); 67 let disableSubmit = $state(false); 68 + let showCaptcha = $state(false); 69 70 let errorMessage: null | string = $state(null); 71 let statusMessage: null | string = $state(null); ··· 75 const tosUrl = $derived(selectedPds?.links?.termsOfService); 76 const requiresAccept = $derived(!!(privacyUrl || tosUrl)); 77 78 + // Check if phone verification is required 79 + const captchaVerificationRequired = $derived(selectedPds?.phoneVerificationRequired === true); 80 + 81 const updateStatusHandler = (status: string) => { 82 statusMessage = status; 83 } 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 + 97 async function submitMoove(event: SubmitEvent & { currentTarget: EventTarget & HTMLFormElement }) { 98 event.preventDefault(); 99 disableSubmit = true; ··· 123 } 124 } 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() { 136 try { 137 138 if (showTwoFactorCodeInput) { ··· 155 156 updateStatusHandler('Starting migration...'); 157 showStatusMessage = true; 158 + 159 await migrator.migrate( 160 formData.handle, 161 formData.password, ··· 165 formData.inviteCode, 166 updateStatusHandler, 167 formData.twoFactorCode, 168 + formData.verificationCode, 169 ); 170 if (migrator.migratePlcRecord) { 171 //I don't think disable submit is needed, but you never know. ··· 200 <br/> 201 <a href="https://blacksky.community/profile/did:plc:g7j6qok5us4hjqlwjxwrrkjm/post/3lw3hcuojck2u">Video guide for 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> 221 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} 235 </div> 236 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} 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}> 253 </div> 254 255 + 256 <div class="form-group"> 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> 274 </div> 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} 284 </div> 285 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> 363 I have read and accept 364 365 </span> 366 + </label> 367 + </div> 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> 390 </div> 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 + 402 + 403 + <div> 404 + <button disabled={disableSubmit} 405 + type="submit">{selectedPds ? `MOOve to ${cleanSelectedPds}` : 'MOOve'}</button> 406 + </div> 407 408 + </form> 409 + {/if} 410 {:else} 411 <SignThePapers migrator={migrator} newHandle={newHandle}/> 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>