import {Client, CredentialManager, ok} from '@atcute/client'; import {fetchPDSMooverDIDWeb, handleAndPDSResolver} from './atprotoUtils.js'; class BackupService { constructor() { /** * * @type {Client} */ this.atCuteClient = null; /** * * @type {CredentialManager} */ this.atCuteCredentialManager = null; } // Perform backup sign-up using user credentials. If the server requires 2FA, // it will throw with error.error === 'AuthFactorTokenRequired'. async loginAndStatus(identifier, password, onStatus = null, twoFactorCode = null) { let {pds} = await handleAndPDSResolver(identifier); const manager = new CredentialManager({ service: pds }); //TODO prob should be a function param, but eh const didWeb = await fetchPDSMooverDIDWeb(window.location.origin); const rpc = new Client({ handler: manager, proxy: { did: didWeb, serviceId: '#repo_backup' } }); try { if (onStatus) onStatus('Signing in…'); let loginInput = { identifier, password, }; if (twoFactorCode) { loginInput.code = twoFactorCode; } await manager.login(loginInput); // Make the client/manager available regardless of repo status so we can sign up if needed. this.atCuteClient = rpc; this.atCuteCredentialManager = manager; if (onStatus) onStatus('Checking backup status'); const result = await rpc.get('com.pdsmoover.backup.getRepoStatus', { params: { did: manager.session.did.toString() } }); if (result.ok) { return result.data; } else { switch (result.data.error) { case 'RepoNotFound': console.log('Repo not found'); return null; default: throw result.data.error; } } } catch (err) { throw err; } } // Sign up for backups. Optionally create a new recovery key on signup. async signUp(onStatus = null) { if (!this.atCuteClient || !this.atCuteCredentialManager) { throw new Error('Not signed in'); } if (onStatus) onStatus('Creating backup registration…'); const data = await ok( this.atCuteClient.post('com.pdsmoover.backup.signUp', { as: null, }) ); if (onStatus) onStatus('Backup registration complete'); return data; } async requestAPlcToken() { if (!this.atCuteClient || !this.atCuteCredentialManager) { throw new Error('Not signed in'); } const rpc = new Client({ handler: this.atCuteCredentialManager, }); let response = await rpc.post('com.atproto.identity.requestPlcOperationSignature', { as: null, }); if (!response.ok) { throw new Error(response.data?.message || 'Failed to request PLC token'); } } async addANewRotationKey(plcToken, rotationKey) { if (!this.atCuteClient || !this.atCuteCredentialManager) { throw new Error('Not signed in'); } const rpc = new Client({ handler: this.atCuteCredentialManager, }); let getDidCredentials = await rpc.get('com.atproto.identity.getRecommendedDidCredentials'); if (getDidCredentials.ok) { const pdsProvidedRotationKeys = getDidCredentials.data.rotationKeys ?? []; const updatedRotationKeys = [rotationKey, ...pdsProvidedRotationKeys]; const credentials = { ...getDidCredentials.data, rotationKeys: updatedRotationKeys, }; const signDocRes = await rpc.post('com.atproto.identity.signPlcOperation', { input: { token: plcToken, ...credentials, } }); if (signDocRes.ok) { const submitDocRes = await rpc.post('com.atproto.identity.submitPlcOperation', { input: signDocRes.data, as: null, }); if (!submitDocRes.ok) { throw new Error(submitDocRes.data?.message || 'Failed to submit PLC operation'); } } else { throw new Error(signDocRes.data?.message || 'Failed to sign PLC operation'); } } else { throw new Error(getDidCredentials.data?.message || 'Failed to get status'); } } async getUsersRepoStatus(onStatus = null) { if (!this.atCuteClient || !this.atCuteCredentialManager) { throw new Error('Not signed in'); } if (onStatus) onStatus('Refreshing backup status…'); const result = await this.atCuteClient.get('com.pdsmoover.backup.getRepoStatus', { params: {did: this.atCuteCredentialManager.session.did.toString()} }); if (result.ok) { return result.data; } else { throw new Error(result.data?.message || 'Failed to get status'); } } async runBackupNow(onStatus = null) { if (!this.atCuteClient || !this.atCuteCredentialManager) { throw new Error('Not signed in'); } if (onStatus) onStatus('Requesting backup…'); const res = await this.atCuteClient.post('com.pdsmoover.backup.requestBackup', {as: null, data: {}}); if (res.ok) { if (onStatus) onStatus('Backup requested.'); // After requesting, refresh status to reflect any immediate changes try { await this.refreshStatus(onStatus); } catch (_) { /* ignore */ } return true; } else { const err = res.data; if (err?.error === 'Timeout') { throw {error: 'Timeout', message: err?.message || 'Please wait a few minutes before requesting again.'}; } throw new Error(err?.message || 'Failed to request backup'); } } // Remove (delete) the user's backup repository async removeRepo(onStatus = null) { if (!this.atCuteClient || !this.atCuteCredentialManager) { throw new Error('Not signed in'); } if (onStatus) onStatus('Deleting backup repository…'); const res = await this.atCuteClient.post('com.pdsmoover.backup.removeRepo', {as: null, data: {}}); if (res.ok) { if (onStatus) onStatus('Backup repository deleted.'); return true; } else { const err = res.data; throw new Error(err?.message || 'Failed to delete backup repository'); } } } export {BackupService};