Client side atproto account migrator in your web browser, along with services for backups and adversarial migrations.
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};