Client side atproto account migrator in your web browser, along with services for backups and adversarial migrations. pdsmoover.com
pds atproto migrations moo cow
at next 216 lines 6.2 kB view raw
1import {Client, CredentialManager, ok} from '@atcute/client'; 2import {fetchPDSMooverDIDWeb, handleAndPDSResolver} from './atprotoUtils.js'; 3 4 5class BackupService { 6 constructor() { 7 /** 8 * 9 * @type {Client} 10 */ 11 this.atCuteClient = null; 12 /** 13 * 14 * @type {CredentialManager} 15 */ 16 this.atCuteCredentialManager = null; 17 } 18 19 // Perform backup sign-up using user credentials. If the server requires 2FA, 20 // it will throw with error.error === 'AuthFactorTokenRequired'. 21 async loginAndStatus(identifier, password, onStatus = null, twoFactorCode = null) { 22 let {pds} = await handleAndPDSResolver(identifier); 23 24 25 const manager = new CredentialManager({ 26 service: pds 27 }); 28 29 //TODO prob should be a function param, but eh 30 const didWeb = await fetchPDSMooverDIDWeb(window.location.origin); 31 32 const rpc = new Client({ 33 handler: manager, 34 proxy: { 35 did: didWeb, 36 serviceId: '#repo_backup' 37 } 38 }); 39 40 try { 41 if (onStatus) onStatus('Signing in…'); 42 43 let loginInput = { 44 identifier, 45 password, 46 47 }; 48 if (twoFactorCode) { 49 loginInput.code = twoFactorCode; 50 } 51 await manager.login(loginInput); 52 53 54 // Make the client/manager available regardless of repo status so we can sign up if needed. 55 this.atCuteClient = rpc; 56 this.atCuteCredentialManager = manager; 57 58 if (onStatus) onStatus('Checking backup status'); 59 const result = await rpc.get('com.pdsmoover.backup.getRepoStatus', { 60 params: { 61 did: manager.session.did.toString() 62 } 63 }); 64 if (result.ok) { 65 return result.data; 66 } else { 67 switch (result.data.error) { 68 case 'RepoNotFound': 69 console.log('Repo not found'); 70 return null; 71 default: 72 throw result.data.error; 73 } 74 75 } 76 } catch (err) { 77 throw err; 78 } 79 } 80 81 // Sign up for backups. Optionally create a new recovery key on signup. 82 async signUp(onStatus = null) { 83 if (!this.atCuteClient || !this.atCuteCredentialManager) { 84 throw new Error('Not signed in'); 85 } 86 if (onStatus) onStatus('Creating backup registration…'); 87 const data = await ok( 88 this.atCuteClient.post('com.pdsmoover.backup.signUp', { 89 as: null, 90 }) 91 ); 92 if (onStatus) onStatus('Backup registration complete'); 93 return data; 94 } 95 96 async requestAPlcToken() { 97 if (!this.atCuteClient || !this.atCuteCredentialManager) { 98 throw new Error('Not signed in'); 99 } 100 const rpc = new Client({ 101 handler: this.atCuteCredentialManager, 102 }); 103 104 let response = await rpc.post('com.atproto.identity.requestPlcOperationSignature', { 105 as: null, 106 }); 107 if (!response.ok) { 108 throw new Error(response.data?.message || 'Failed to request PLC token'); 109 } 110 } 111 112 async addANewRotationKey(plcToken, rotationKey) { 113 if (!this.atCuteClient || !this.atCuteCredentialManager) { 114 throw new Error('Not signed in'); 115 } 116 117 118 const rpc = new Client({ 119 handler: this.atCuteCredentialManager, 120 }); 121 122 let getDidCredentials = await rpc.get('com.atproto.identity.getRecommendedDidCredentials'); 123 124 if (getDidCredentials.ok) { 125 const pdsProvidedRotationKeys = getDidCredentials.data.rotationKeys ?? []; 126 const updatedRotationKeys = [rotationKey, ...pdsProvidedRotationKeys]; 127 128 const credentials = { 129 ...getDidCredentials.data, 130 rotationKeys: updatedRotationKeys, 131 }; 132 133 const signDocRes = await rpc.post('com.atproto.identity.signPlcOperation', { 134 input: { 135 token: plcToken, 136 ...credentials, 137 } 138 }); 139 140 if (signDocRes.ok) { 141 const submitDocRes = await rpc.post('com.atproto.identity.submitPlcOperation', { 142 input: signDocRes.data, 143 as: null, 144 }); 145 146 if (!submitDocRes.ok) { 147 throw new Error(submitDocRes.data?.message || 'Failed to submit PLC operation'); 148 } 149 150 } else { 151 throw new Error(signDocRes.data?.message || 'Failed to sign PLC operation'); 152 } 153 154 155 } else { 156 throw new Error(getDidCredentials.data?.message || 'Failed to get status'); 157 } 158 } 159 160 async getUsersRepoStatus(onStatus = null) { 161 if (!this.atCuteClient || !this.atCuteCredentialManager) { 162 throw new Error('Not signed in'); 163 } 164 if (onStatus) onStatus('Refreshing backup status…'); 165 const result = await this.atCuteClient.get('com.pdsmoover.backup.getRepoStatus', { 166 params: {did: this.atCuteCredentialManager.session.did.toString()} 167 }); 168 if (result.ok) { 169 return result.data; 170 } else { 171 throw new Error(result.data?.message || 'Failed to get status'); 172 } 173 } 174 175 async runBackupNow(onStatus = null) { 176 if (!this.atCuteClient || !this.atCuteCredentialManager) { 177 throw new Error('Not signed in'); 178 } 179 if (onStatus) onStatus('Requesting backup…'); 180 const res = await this.atCuteClient.post('com.pdsmoover.backup.requestBackup', {as: null, data: {}}); 181 if (res.ok) { 182 if (onStatus) onStatus('Backup requested.'); 183 // After requesting, refresh status to reflect any immediate changes 184 try { 185 await this.refreshStatus(onStatus); 186 } catch (_) { /* ignore */ 187 } 188 return true; 189 } else { 190 const err = res.data; 191 if (err?.error === 'Timeout') { 192 throw {error: 'Timeout', message: err?.message || 'Please wait a few minutes before requesting again.'}; 193 } 194 throw new Error(err?.message || 'Failed to request backup'); 195 } 196 } 197 198 // Remove (delete) the user's backup repository 199 async removeRepo(onStatus = null) { 200 if (!this.atCuteClient || !this.atCuteCredentialManager) { 201 throw new Error('Not signed in'); 202 } 203 if (onStatus) onStatus('Deleting backup repository…'); 204 const res = await this.atCuteClient.post('com.pdsmoover.backup.removeRepo', {as: null, data: {}}); 205 if (res.ok) { 206 if (onStatus) onStatus('Backup repository deleted.'); 207 return true; 208 } else { 209 const err = res.data; 210 throw new Error(err?.message || 'Failed to delete backup repository'); 211 } 212 } 213} 214 215 216export {BackupService};