···1212 instance
1313- [MissingBlobs](./lib/missingBlobs.js) - Finds missing blobs on your old PDS and uploads them to your new PDS
1414- [PlcOps](./lib/plc-ops.js) - Helpers for manual PCL operations
1515-- [Restore](./lib/restore.js) - Handles a recovery and restores the at proto from the backup1515+- [Restore](./lib/restore.js) - Handles a recovery and restores the at proto from the backup
···11+{
22+ "id": "blue.microcosm.identity.resolveMiniDoc",
33+ "defs": {
44+ "main": {
55+ "type": "query",
66+ "output": {
77+ "schema": {
88+ "type": "object",
99+ "required": ["did", "handle", "pds", "signing_key"],
1010+ "properties": {
1111+ "did": {
1212+ "type": "string",
1313+ "format": "did",
1414+ "description": "DID, bi-directionally verified if a handle was provided in the query."
1515+ },
1616+ "pds": {
1717+ "type": "string",
1818+ "format": "uri",
1919+ "description": "The identity's PDS URL"
2020+ },
2121+ "handle": {
2222+ "type": "string",
2323+ "format": "handle",
2424+ "description": "The validated handle of the account or `handle.invalid` if the handle\ndid not bi-directionally match the DID document."
2525+ },
2626+ "signing_key": {
2727+ "type": "string",
2828+ "description": "The atproto signing key publicKeyMultibase\n\nLegacy key encoding not supported. the key is returned directly; `id`,\n`type`, and `controller` are omitted."
2929+ }
3030+ }
3131+ },
3232+ "encoding": "application/json"
3333+ },
3434+ "parameters": {
3535+ "type": "params",
3636+ "required": ["identifier"],
3737+ "properties": {
3838+ "identifier": {
3939+ "type": "string",
4040+ "format": "at-identifier",
4141+ "description": "Handle or DID to resolve"
4242+ }
4343+ }
4444+ },
4545+ "description": "Like [com.atproto.identity.resolveIdentity](https://docs.bsky.app/docs/api/com-atproto-identity-resolve-identity) but instead of the full `didDoc` it returns an atproto-relevant subset."
4646+ }
4747+ },
4848+ "$type": "com.atproto.lexicon.schema",
4949+ "lexicon": 1
5050+}
+47-50
packages/moover/lib/atprotoUtils.js
···11import {
22- CompositeDidDocumentResolver, CompositeHandleResolver,
33- DohJsonHandleResolver,
44- PlcDidDocumentResolver, WebDidDocumentResolver,
55- WellKnownHandleResolver
66-} from '@atcute/identity-resolver';
22+ CompositeDidDocumentResolver,
33+ CompositeHandleResolver,
44+ DohJsonHandleResolver,
55+ PlcDidDocumentResolver,
66+ WebDidDocumentResolver,
77+ WellKnownHandleResolver,
88+} from '@atcute/identity-resolver'
79810const handleResolver = new CompositeHandleResolver({
99- strategy: 'race',
1010- methods: {
1111- dns: new DohJsonHandleResolver({
1212- dohUrl: 'https://mozilla.cloudflare-dns.com/dns-query',
1313- }),
1414- http: new WellKnownHandleResolver(),
1515- },
1616-});
1111+ strategy: 'race',
1212+ methods: {
1313+ dns: new DohJsonHandleResolver({
1414+ dohUrl: 'https://mozilla.cloudflare-dns.com/dns-query',
1515+ }),
1616+ http: new WellKnownHandleResolver(),
1717+ },
1818+})
17191820const docResolver = new CompositeDidDocumentResolver({
1919- methods: {
2020- plc: new PlcDidDocumentResolver(),
2121- web: new WebDidDocumentResolver(),
2222- },
2323-});
2121+ methods: {
2222+ plc: new PlcDidDocumentResolver(),
2323+ web: new WebDidDocumentResolver(),
2424+ },
2525+})
24262527/**
2628 * Cleans the handle of @ and some other unicode characters that used to show up when copied from the profile
2729 * @param handle {string}
2830 * @returns {string}
2931 */
3030-const cleanHandle = (handle) =>
3131- handle.replace('@', '').trim().replace(
3232- /[\u202A\u202C\u200E\u200F\u2066-\u2069]/g,
3333- '',
3434- );
3535-3232+const cleanHandle = handle =>
3333+ handle
3434+ .replace('@', '')
3535+ .trim()
3636+ .replace(/[\u202A\u202C\u200E\u200F\u2066-\u2069]/g, '')
36373738/**
3839 * Convince helper to resolve a handle to a did and then find the PDS url from the did document.
···4142 * @returns {Promise<{usersDid: string, pds: string}>}
4243 */
4344async function handleAndPDSResolver(handle) {
4444- let usersDid = null;
4545- if (handle.startsWith('did:')) {
4646- usersDid = handle;
4747- } else {
4848- const cleanedHandle = cleanHandle(handle);
4949- usersDid = await handleResolver.resolve(cleanedHandle);
5050- }
5151- const didDoc = await docResolver.resolve(usersDid);
4545+ let usersDid = null
4646+ if (handle.startsWith('did:')) {
4747+ usersDid = handle
4848+ } else {
4949+ const cleanedHandle = cleanHandle(handle)
5050+ usersDid = await handleResolver.resolve(cleanedHandle)
5151+ }
5252+ const didDoc = await docResolver.resolve(usersDid)
52535353- let pds;
5454- try {
5555- pds = didDoc.service?.filter((s) =>
5656- s.type === 'AtprotoPersonalDataServer'
5757- )[0].serviceEndpoint;
5858- } catch (error) {
5959- throw new Error('Could not find a PDS in the DID document.');
6060- }
6161- return {usersDid, pds};
5454+ let pds
5555+ try {
5656+ pds = didDoc.service?.filter(s => s.type === 'AtprotoPersonalDataServer')[0].serviceEndpoint
5757+ } catch (error) {
5858+ throw new Error('Could not find a PDS in the DID document.')
5959+ }
6060+ return { usersDid, pds }
6261}
6363-64626563/**
6664 * Fetches the DID Web from the .well-known/did.json endpoint of the server.
···6967 * @returns {Promise<*>}
7068 */
7169async function fetchPDSMooverDIDWeb(baseUrl) {
7272- const response = await fetch(`${baseUrl}/.well-known/did.json`);
7373- if (!response.ok) {
7474- throw new Error(`Failed to fetch DID document: ${response.status}`);
7575- }
7676- const didDoc = await response.json();
7777- return didDoc.id;
7070+ const response = await fetch(`${baseUrl}/.well-known/did.json`)
7171+ if (!response.ok) {
7272+ throw new Error(`Failed to fetch DID document: ${response.status}`)
7373+ }
7474+ const didDoc = await response.json()
7575+ return didDoc.id
7876}
79778080-8181-export {handleResolver, docResolver, cleanHandle, handleAndPDSResolver, fetchPDSMooverDIDWeb};
7878+export { handleResolver, docResolver, cleanHandle, handleAndPDSResolver, fetchPDSMooverDIDWeb }
+221-225
packages/moover/lib/backup.js
···11-import {Client, CredentialManager, ok} from '@atcute/client';
22-import {handleAndPDSResolver} from './atprotoUtils.js';
11+import { Client, CredentialManager, ok } from '@atcute/client'
22+import { handleAndPDSResolver } from './atprotoUtils.js'
33//Shows as unused, but is used in the return types
44-import {ComPdsmooverBackupDescribeServer} from '@pds-moover/lexicons';
44+import { ComPdsmooverBackupDescribeServer } from '@pds-moover/lexicons'
5566/**
77 * JSDoc type-only import to avoid runtime import errors in the browser.
88 * @typedef {import('@atcute/lexicons').InferXRPCBodyOutput} InferXRPCBodyOutput
99 */
1010-11101211/**
1312 * Logic to sign up and manage backups for pdsmoover.com (or your own selfhosted instance)
1413 */
1514class BackupService {
1515+ /**
1616+ *
1717+ * @param backupDidWeb {string} - The did:web for the xrpc service for backups, defaults to did:web:pdsmoover.com
1818+ */
1919+ constructor(backupDidWeb = 'did:web:pdsmoover.com') {
1620 /**
1721 *
1818- * @param backupDidWeb {string} - The did:web for the xrpc service for backups, defaults to did:web:pdsmoover.com
2222+ * @type {Client}
1923 */
2020- constructor(backupDidWeb = 'did:web:pdsmoover.com') {
2121- /**
2222- *
2323- * @type {Client}
2424- */
2525- this.atCuteClient = null;
2626- /**
2727- *
2828- * @type {CredentialManager}
2929- */
3030- this.atCuteCredentialManager = null;
3131-3232- /**
3333- * The did:web for the xrpc service for backups, defaults to pdsmoover.com
3434- * @type {string}
3535- */
3636- this.backupDidWeb = backupDidWeb;
3737- }
3838-3939-2424+ this.atCuteClient = null
4025 /**
4141- * Logs in and returns the backup status.
4242- * To use the rest of the BackupService, it is assumed that this has ran first,
4343- * and the user has successfully signed up. A successful login is a returned null if the user has not signed up.
4444- * or the backup status if they are
4526 *
4646- * If the server requires 2FA,
4747- * it will throw with error.error === 'AuthFactorTokenRequired'.
4848- * @param identifier {string} handle or did
4949- * @param password {string}
5050- * @param {function|null} onStatus - a function that takes a string used to update the UI.
5151- * Like (status) => console.log(status)
5252- * @param twoFactorCode {string|null}
5353- *
5454- * @returns {Promise<InferXRPCBodyOutput<ComPdsmooverBackupDescribeServer.mainSchema['output']>|null>}
2727+ * @type {CredentialManager}
5528 */
5656- async loginAndStatus(identifier, password, onStatus = null, twoFactorCode = null) {
5757- let {pds} = await handleAndPDSResolver(identifier);
2929+ this.atCuteCredentialManager = null
58303131+ /**
3232+ * The did:web for the xrpc service for backups, defaults to pdsmoover.com
3333+ * @type {string}
3434+ */
3535+ this.backupDidWeb = backupDidWeb
3636+ }
59376060- const manager = new CredentialManager({
6161- service: pds
6262- });
3838+ /**
3939+ * Logs in and returns the backup status.
4040+ * To use the rest of the BackupService, it is assumed that this has ran first,
4141+ * and the user has successfully signed up. A successful login is a returned null if the user has not signed up.
4242+ * or the backup status if they are
4343+ *
4444+ * If the server requires 2FA,
4545+ * it will throw with error.error === 'AuthFactorTokenRequired'.
4646+ * @param identifier {string} handle or did
4747+ * @param password {string}
4848+ * @param {function|null} onStatus - a function that takes a string used to update the UI.
4949+ * Like (status) => console.log(status)
5050+ * @param twoFactorCode {string|null}
5151+ *
5252+ * @returns {Promise<InferXRPCBodyOutput<ComPdsmooverBackupDescribeServer.mainSchema['output']>|null>}
5353+ */
5454+ async loginAndStatus(identifier, password, onStatus = null, twoFactorCode = null) {
5555+ let { pds } = await handleAndPDSResolver(identifier)
63565757+ const manager = new CredentialManager({
5858+ service: pds,
5959+ })
64606565- const rpc = new Client({
6666- handler: manager,
6767- proxy: {
6868- did: this.backupDidWeb,
6969- serviceId: '#repo_backup'
7070- }
7171- });
6161+ const rpc = new Client({
6262+ handler: manager,
6363+ proxy: {
6464+ did: this.backupDidWeb,
6565+ serviceId: '#repo_backup',
6666+ },
6767+ })
72687373- try {
7474- if (onStatus) onStatus('Signing in…');
6969+ try {
7070+ if (onStatus) onStatus('Signing in…')
75717676- let loginInput = {
7777- identifier,
7878- password,
7272+ let loginInput = {
7373+ identifier,
7474+ password,
7575+ }
7676+ if (twoFactorCode) {
7777+ loginInput.code = twoFactorCode
7878+ }
7979+ await manager.login(loginInput)
79808080- };
8181- if (twoFactorCode) {
8282- loginInput.code = twoFactorCode;
8383- }
8484- await manager.login(loginInput);
8585-8686-8787- // Make the client/manager available regardless of repo status so we can sign up if needed.
8888- this.atCuteClient = rpc;
8989- this.atCuteCredentialManager = manager;
9090-9191- if (onStatus) onStatus('Checking backup status');
9292- const result = await rpc.get('com.pdsmoover.backup.getRepoStatus', {
9393- params: {
9494- did: manager.session.did.toString()
9595- }
9696- });
9797- if (result.ok) {
9898- return result.data;
9999- } else {
100100- switch (result.data.error) {
101101- case 'RepoNotFound':
102102- return null;
103103- default:
104104- throw result.data.error;
105105- }
8181+ // Make the client/manager available regardless of repo status so we can sign up if needed.
8282+ this.atCuteClient = rpc
8383+ this.atCuteCredentialManager = manager
10684107107- }
108108- } catch (err) {
109109- throw err;
8585+ if (onStatus) onStatus('Checking backup status')
8686+ const result = await rpc.get('com.pdsmoover.backup.getRepoStatus', {
8787+ params: {
8888+ did: manager.session.did.toString(),
8989+ },
9090+ })
9191+ if (result.ok) {
9292+ return result.data
9393+ } else {
9494+ switch (result.data.error) {
9595+ case 'RepoNotFound':
9696+ return null
9797+ default:
9898+ throw result.data.error
11099 }
100100+ }
101101+ } catch (err) {
102102+ throw err
111103 }
104104+ }
112105113113- /**
114114- * Signs the user up for backups with the service
115115- * @param onStatus
116116- * @returns {Promise<void>}
117117- */
118118- async signUp(onStatus = null) {
119119- if (!this.atCuteClient || !this.atCuteCredentialManager) {
120120- throw new Error('Not signed in');
121121- }
122122- if (onStatus) onStatus('Creating backup registration…');
123123- await ok(
124124- this.atCuteClient.post('com.pdsmoover.backup.signUp', {
125125- as: null,
126126- })
127127- );
128128- if (onStatus) onStatus('Backup registration complete');
129129- //No return if successful
106106+ /**
107107+ * Signs the user up for backups with the service
108108+ * @param onStatus
109109+ * @returns {Promise<void>}
110110+ */
111111+ async signUp(onStatus = null) {
112112+ if (!this.atCuteClient || !this.atCuteCredentialManager) {
113113+ throw new Error('Not signed in')
130114 }
115115+ if (onStatus) onStatus('Creating backup registration…')
116116+ await ok(
117117+ this.atCuteClient.post('com.pdsmoover.backup.signUp', {
118118+ as: null,
119119+ }),
120120+ )
121121+ if (onStatus) onStatus('Backup registration complete')
122122+ //No return if successful
123123+ }
131124132132- /**
133133- * Requests a PLC token to be sent to the user's email, needed to add a new rotation key
134134- * @returns {Promise<void>}
135135- */
136136- async requestAPlcToken() {
137137- if (!this.atCuteClient || !this.atCuteCredentialManager) {
138138- throw new Error('Not signed in');
139139- }
140140- const rpc = new Client({
141141- handler: this.atCuteCredentialManager,
142142- });
143143-144144- let response = await rpc.post('com.atproto.identity.requestPlcOperationSignature', {
145145- as: null,
146146- });
147147- if (!response.ok) {
148148- throw new Error(response.data?.message || 'Failed to request PLC token');
149149- }
125125+ /**
126126+ * Requests a PLC token to be sent to the user's email, needed to add a new rotation key
127127+ * @returns {Promise<void>}
128128+ */
129129+ async requestAPlcToken() {
130130+ if (!this.atCuteClient || !this.atCuteCredentialManager) {
131131+ throw new Error('Not signed in')
150132 }
151151-152152- /**
153153- * Adds a new rotation to the users did document. Assumes you are already signed in.
154154- *
155155- * WARNING: This will overwrite any existing rotation keys with the new one at the top, and the PDS key as the second one
156156- * @param plcToken {string} - PLC token from the user's email that was sent from requestAPlcToken
157157- * @param rotationKey {string} - The new rotation key to add to the user's did document
158158- * @returns {Promise<void>}
159159- */
160160- async addANewRotationKey(plcToken, rotationKey) {
161161- if (!this.atCuteClient || !this.atCuteCredentialManager) {
162162- throw new Error('Not signed in');
163163- }
164164-165165-166166- const rpc = new Client({
167167- handler: this.atCuteCredentialManager,
168168- });
133133+ const rpc = new Client({
134134+ handler: this.atCuteCredentialManager,
135135+ })
169136170170- let getDidCredentials = await rpc.get('com.atproto.identity.getRecommendedDidCredentials');
137137+ let response = await rpc.post('com.atproto.identity.requestPlcOperationSignature', {
138138+ as: null,
139139+ })
140140+ if (!response.ok) {
141141+ throw new Error(response.data?.message || 'Failed to request PLC token')
142142+ }
143143+ }
171144172172- if (getDidCredentials.ok) {
173173- const pdsProvidedRotationKeys = getDidCredentials.data.rotationKeys ?? [];
174174- const updatedRotationKeys = [rotationKey, ...pdsProvidedRotationKeys];
145145+ /**
146146+ * Adds a new rotation to the users did document. Assumes you are already signed in.
147147+ *
148148+ * WARNING: This will overwrite any existing rotation keys with the new one at the top, and the PDS key as the second one
149149+ * @param plcToken {string} - PLC token from the user's email that was sent from requestAPlcToken
150150+ * @param rotationKey {string} - The new rotation key to add to the user's did document
151151+ * @returns {Promise<void>}
152152+ */
153153+ async addANewRotationKey(plcToken, rotationKey) {
154154+ if (!this.atCuteClient || !this.atCuteCredentialManager) {
155155+ throw new Error('Not signed in')
156156+ }
175157176176- const credentials = {
177177- ...getDidCredentials.data,
178178- rotationKeys: updatedRotationKeys,
179179- };
158158+ const rpc = new Client({
159159+ handler: this.atCuteCredentialManager,
160160+ })
180161181181- const signDocRes = await rpc.post('com.atproto.identity.signPlcOperation', {
182182- input: {
183183- token: plcToken,
184184- ...credentials,
185185- }
186186- });
162162+ let getDidCredentials = await rpc.get('com.atproto.identity.getRecommendedDidCredentials')
187163188188- if (signDocRes.ok) {
189189- const submitDocRes = await rpc.post('com.atproto.identity.submitPlcOperation', {
190190- input: signDocRes.data,
191191- as: null,
192192- });
164164+ if (getDidCredentials.ok) {
165165+ const pdsProvidedRotationKeys = getDidCredentials.data.rotationKeys ?? []
166166+ const updatedRotationKeys = [rotationKey, ...pdsProvidedRotationKeys]
193167194194- if (!submitDocRes.ok) {
195195- throw new Error(submitDocRes.data?.message || 'Failed to submit PLC operation');
196196- }
168168+ const credentials = {
169169+ ...getDidCredentials.data,
170170+ rotationKeys: updatedRotationKeys,
171171+ }
197172198198- } else {
199199- throw new Error(signDocRes.data?.message || 'Failed to sign PLC operation');
200200- }
173173+ const signDocRes = await rpc.post('com.atproto.identity.signPlcOperation', {
174174+ input: {
175175+ token: plcToken,
176176+ ...credentials,
177177+ },
178178+ })
201179180180+ if (signDocRes.ok) {
181181+ const submitDocRes = await rpc.post('com.atproto.identity.submitPlcOperation', {
182182+ input: signDocRes.data,
183183+ as: null,
184184+ })
202185203203- } else {
204204- throw new Error(getDidCredentials.data?.message || 'Failed to get status');
186186+ if (!submitDocRes.ok) {
187187+ throw new Error(submitDocRes.data?.message || 'Failed to submit PLC operation')
205188 }
189189+ } else {
190190+ throw new Error(signDocRes.data?.message || 'Failed to sign PLC operation')
191191+ }
192192+ } else {
193193+ throw new Error(getDidCredentials.data?.message || 'Failed to get status')
206194 }
195195+ }
207196197197+ /**
198198+ *
199199+ * Gets the current status of the user's backup repository.
200200+ *
201201+ * @param onStatus {function|null} - a function that takes a string used to update the UI.
202202+ * @returns {Promise<InferXRPCBodyOutput<ComPdsmooverBackupDescribeServer.mainSchema['output']>>}
203203+ */
204204+ async getUsersRepoStatus(onStatus = null) {
205205+ if (!this.atCuteClient || !this.atCuteCredentialManager) {
206206+ throw new Error('Not signed in')
207207+ }
208208+ if (onStatus) onStatus('Refreshing backup status…')
209209+ const result = await this.atCuteClient.get('com.pdsmoover.backup.getRepoStatus', {
210210+ params: { did: this.atCuteCredentialManager.session.did.toString() },
211211+ })
212212+ if (result.ok) {
213213+ return result.data
214214+ } else {
215215+ throw new Error(result.data?.message || 'Failed to get status')
216216+ }
217217+ }
208218209209- /**
210210- *
211211- * Gets the current status of the user's backup repository.
212212- *
213213- * @param onStatus {function|null} - a function that takes a string used to update the UI.
214214- * @returns {Promise<InferXRPCBodyOutput<ComPdsmooverBackupDescribeServer.mainSchema['output']>>}
215215- */
216216- async getUsersRepoStatus(onStatus = null) {
217217- if (!this.atCuteClient || !this.atCuteCredentialManager) {
218218- throw new Error('Not signed in');
219219- }
220220- if (onStatus) onStatus('Refreshing backup status…');
221221- const result = await this.atCuteClient.get('com.pdsmoover.backup.getRepoStatus', {
222222- params: {did: this.atCuteCredentialManager.session.did.toString()}
223223- });
224224- if (result.ok) {
225225- return result.data;
226226- } else {
227227- throw new Error(result.data?.message || 'Failed to get status');
219219+ /**
220220+ * Requests a backup to be run immediately for the signed-in user. Usually does, depend on the server's backup queue
221221+ * @param onStatus
222222+ * @returns {Promise<boolean>}
223223+ */
224224+ async runBackupNow(onStatus = null) {
225225+ if (!this.atCuteClient || !this.atCuteCredentialManager) {
226226+ throw new Error('Not signed in')
227227+ }
228228+ if (onStatus) onStatus('Requesting backup…')
229229+ const res = await this.atCuteClient.post('com.pdsmoover.backup.requestBackup', {
230230+ as: null,
231231+ data: {},
232232+ })
233233+ if (res.ok) {
234234+ if (onStatus) onStatus('Backup requested.')
235235+ return true
236236+ } else {
237237+ const err = res.data
238238+ if (err?.error === 'Timeout') {
239239+ throw {
240240+ error: 'Timeout',
241241+ message: err?.message || 'Please wait a few minutes before requesting again.',
228242 }
243243+ }
244244+ throw new Error(err?.message || 'Failed to request backup')
229245 }
246246+ }
230247231231- /**
232232- * Requests a backup to be run immediately for the signed-in user. Usually does, depend on the server's backup queue
233233- * @param onStatus
234234- * @returns {Promise<boolean>}
235235- */
236236- async runBackupNow(onStatus = null) {
237237- if (!this.atCuteClient || !this.atCuteCredentialManager) {
238238- throw new Error('Not signed in');
239239- }
240240- if (onStatus) onStatus('Requesting backup…');
241241- const res = await this.atCuteClient.post('com.pdsmoover.backup.requestBackup', {as: null, data: {}});
242242- if (res.ok) {
243243- if (onStatus) onStatus('Backup requested.');
244244- return true;
245245- } else {
246246- const err = res.data;
247247- if (err?.error === 'Timeout') {
248248- throw {error: 'Timeout', message: err?.message || 'Please wait a few minutes before requesting again.'};
249249- }
250250- throw new Error(err?.message || 'Failed to request backup');
251251- }
248248+ /**
249249+ * Remove (delete) the signed-in user's backup repository. this also deletes all the user's backup data.
250250+ * @param onStatus
251251+ * @returns {Promise<boolean>}
252252+ */
253253+ async removeRepo(onStatus = null) {
254254+ if (!this.atCuteClient || !this.atCuteCredentialManager) {
255255+ throw new Error('Not signed in')
252256 }
253253-254254- /**
255255- * Remove (delete) the signed-in user's backup repository. this also deletes all the user's backup data.
256256- * @param onStatus
257257- * @returns {Promise<boolean>}
258258- */
259259- async removeRepo(onStatus = null) {
260260- if (!this.atCuteClient || !this.atCuteCredentialManager) {
261261- throw new Error('Not signed in');
262262- }
263263- if (onStatus) onStatus('Deleting backup repository…');
264264- const res = await this.atCuteClient.post('com.pdsmoover.backup.removeRepo', {as: null, data: {}});
265265- if (res.ok) {
266266- if (onStatus) onStatus('Backup repository deleted.');
267267- return true;
268268- } else {
269269- const err = res.data;
270270- throw new Error(err?.message || 'Failed to delete backup repository');
271271- }
257257+ if (onStatus) onStatus('Deleting backup repository…')
258258+ const res = await this.atCuteClient.post('com.pdsmoover.backup.removeRepo', {
259259+ as: null,
260260+ data: {},
261261+ })
262262+ if (res.ok) {
263263+ if (onStatus) onStatus('Backup repository deleted.')
264264+ return true
265265+ } else {
266266+ const err = res.data
267267+ throw new Error(err?.message || 'Failed to delete backup repository')
272268 }
269269+ }
273270}
274271275275-276276-export {BackupService};
272272+export { BackupService }
+5
packages/moover/lib/lexicons/blue.ts
···11+/*
22+ * THIS FILE WAS GENERATED BY "@atproto/lex". DO NOT EDIT.
33+ */
44+55+export * as microcosm from './blue/microcosm.js'
+5
packages/moover/lib/lexicons/blue/microcosm.ts
···11+/*
22+ * THIS FILE WAS GENERATED BY "@atproto/lex". DO NOT EDIT.
33+ */
44+55+export * as identity from './microcosm/identity.js'
···11+/*
22+ * THIS FILE WAS GENERATED BY "@atproto/lex". DO NOT EDIT.
33+ */
44+55+export * from './resolveMiniDoc.defs.js'
66+export * as $defs from './resolveMiniDoc.defs.js'
+7-16
packages/moover/lib/main.js
···11-import {Migrator} from './pdsmoover.js';
22-import {MissingBlobs} from './missingBlobs.js';
33-import {BackupService} from './backup.js';
44-import {PlcOps} from './plc-ops.js';
55-import {Restore} from './restore.js';
66-import {handleAndPDSResolver} from './atprotoUtils.js';
11+import { Migrator } from './pdsmoover.js'
22+import { MissingBlobs } from './missingBlobs.js'
33+import { BackupService } from './backup.js'
44+import { PlcOps } from './plc-ops.js'
55+import { Restore } from './restore.js'
66+import { handleAndPDSResolver } from './atprotoUtils.js'
7788-export {
99- Migrator,
1010- MissingBlobs,
1111- BackupService,
1212- PlcOps,
1313- Restore,
1414- handleAndPDSResolver,
1515-1616-}
1717-88+export { Migrator, MissingBlobs, BackupService, PlcOps, Restore, handleAndPDSResolver }
+162-166
packages/moover/lib/missingBlobs.js
···11-import {AtpAgent} from '@atproto/api';
22-import {handleAndPDSResolver} from './atprotoUtils.js';
33-11+import { AtpAgent } from '@atproto/api'
22+import { handleAndPDSResolver } from './atprotoUtils.js'
4354/**
65 * Class to help find missing blobs from the did's previous PDS and import them into the current PDS
76 */
87class MissingBlobs {
99-1010- constructor() {
1111- /**
1212- * The user's current PDS agent
1313- * @type {AtpAgent}
1414- */
1515- this.currentPdsAgent = null;
1616- /**
1717- * The user's old PDS agent
1818- * @type {AtpAgent}
1919- */
2020- this.oldPdsAgent = null;
2121- /**
2222- * the user's did
2323- * @type {string|null}
2424- */
2525- this.did = null;
2626- /**
2727- * The user's current PDS url
2828- * @type {null}
2929- */
3030- this.currentPdsUrl = null;
3131- /**
3232- * A list of the missing cids blobs from the old PDS. In this case if a retry upload fails it gets put in this array for the ui
3333- * @type {string[]}
3434- */
3535- this.missingBlobs = [];
3636-3737- }
3838-88+ constructor() {
399 /**
4040- * Logs the user into the current PDS and gets the account status
4141- * @param handle {string}
4242- * @param password {string}
4343- * @param twoFactorCode {string|null}
4444- * @returns {Promise<{accountStatus: OutputSchema, missingBlobsCount: number}>}
1010+ * The user's current PDS agent
1111+ * @type {AtpAgent}
4512 */
4646- async currentAgentLogin(
4747- handle,
4848- password,
4949- twoFactorCode = null,
5050- ) {
5151- let {usersDid, pds} = await handleAndPDSResolver(handle);
5252- this.did = usersDid;
5353- this.currentPdsUrl = pds;
5454- const agent = new AtpAgent({
5555- service: pds,
5656- });
5757-5858- if (twoFactorCode === null) {
5959- await agent.login({identifier: usersDid, password});
6060- } else {
6161- await agent.login({identifier: usersDid, password: password, authFactorToken: twoFactorCode});
6262- }
1313+ this.currentPdsAgent = null
1414+ /**
1515+ * The user's old PDS agent
1616+ * @type {AtpAgent}
1717+ */
1818+ this.oldPdsAgent = null
1919+ /**
2020+ * the user's did
2121+ * @type {string|null}
2222+ */
2323+ this.did = null
2424+ /**
2525+ * The user's current PDS url
2626+ * @type {null}
2727+ */
2828+ this.currentPdsUrl = null
2929+ /**
3030+ * A list of the missing cids blobs from the old PDS. In this case if a retry upload fails it gets put in this array for the ui
3131+ * @type {string[]}
3232+ */
3333+ this.missingBlobs = []
3434+ }
63356464- this.currentPdsAgent = agent;
3636+ /**
3737+ * Logs the user into the current PDS and gets the account status
3838+ * @param handle {string}
3939+ * @param password {string}
4040+ * @param twoFactorCode {string|null}
4141+ * @returns {Promise<{accountStatus: OutputSchema, missingBlobsCount: number}>}
4242+ */
4343+ async currentAgentLogin(handle, password, twoFactorCode = null) {
4444+ let { usersDid, pds } = await handleAndPDSResolver(handle)
4545+ this.did = usersDid
4646+ this.currentPdsUrl = pds
4747+ const agent = new AtpAgent({
4848+ service: pds,
4949+ })
65506666- const result = await agent.com.atproto.server.checkAccountStatus();
6767- const missingBlobs = await this.currentPdsAgent.com.atproto.repo.listMissingBlobs({
6868- limit: 10,
6969- });
7070- return {accountStatus: result.data, missingBlobsCount: missingBlobs.data.blobs.length};
5151+ if (twoFactorCode === null) {
5252+ await agent.login({ identifier: usersDid, password })
5353+ } else {
5454+ await agent.login({
5555+ identifier: usersDid,
5656+ password: password,
5757+ authFactorToken: twoFactorCode,
5858+ })
7159 }
72607373- /**
7474- * Logs into the old PDS and gets the account status.
7575- * Does not need a handle
7676- * since it is assumed the user has already logged in with the current PDS and we are using their did
7777- * @param password {string}
7878- * @param twoFactorCode {string|null}
7979- * @param pdsUrl {string|null} - If you know the url of the old PDS you can pass it in here. If not it will be guessed at from plc ops
8080- * @returns {Promise<void>}
8181- */
8282- async oldAgentLogin(
8383- password,
8484- twoFactorCode = null,
8585- pdsUrl = null,
8686- ) {
8787- let oldPds = null;
6161+ this.currentPdsAgent = agent
88628989- if (pdsUrl === null) {
9090- const response = await fetch(`https://plc.directory/${this.did}/log`);
9191- let auditLog = await response.json();
9292- auditLog = auditLog.reverse();
9393- let debugCount = 0;
9494- for (const entry of auditLog) {
9595- console.log(`Loop: ${debugCount++}`);
9696- console.log(entry);
9797- if (entry.services) {
9898- if (entry.services.atproto_pds) {
9999- if (entry.services.atproto_pds.type === 'AtprotoPersonalDataServer') {
100100- const pds = entry.services.atproto_pds.endpoint;
101101- console.log(`Found PDS: ${pds}`);
102102- if (pds.toLowerCase() !== this.currentPdsUrl.toLowerCase()) {
103103- oldPds = pds;
104104- break;
105105- }
106106- }
107107- }
108108- }
109109- }
110110- if (oldPds === null) {
111111- throw new Error('Could not find your old PDS');
112112- }
113113- } else {
114114- oldPds = pdsUrl;
115115- }
6363+ const result = await agent.com.atproto.server.checkAccountStatus()
6464+ const missingBlobs = await this.currentPdsAgent.com.atproto.repo.listMissingBlobs({
6565+ limit: 10,
6666+ })
6767+ return { accountStatus: result.data, missingBlobsCount: missingBlobs.data.blobs.length }
6868+ }
11669117117- const agent = new AtpAgent({
118118- service: oldPds,
119119- });
7070+ /**
7171+ * Logs into the old PDS and gets the account status.
7272+ * Does not need a handle
7373+ * since it is assumed the user has already logged in with the current PDS and we are using their did
7474+ * @param password {string}
7575+ * @param twoFactorCode {string|null}
7676+ * @param pdsUrl {string|null} - If you know the url of the old PDS you can pass it in here. If not it will be guessed at from plc ops
7777+ * @returns {Promise<void>}
7878+ */
7979+ async oldAgentLogin(password, twoFactorCode = null, pdsUrl = null) {
8080+ let oldPds = null
12081121121- if (twoFactorCode === null) {
122122- await agent.login({identifier: this.did, password});
123123- } else {
124124- await agent.login({identifier: this.did, password: password, authFactorToken: twoFactorCode});
8282+ if (pdsUrl === null) {
8383+ const response = await fetch(`https://plc.directory/${this.did}/log`)
8484+ let auditLog = await response.json()
8585+ auditLog = auditLog.reverse()
8686+ let debugCount = 0
8787+ for (const entry of auditLog) {
8888+ console.log(`Loop: ${debugCount++}`)
8989+ console.log(entry)
9090+ if (entry.services) {
9191+ if (entry.services.atproto_pds) {
9292+ if (entry.services.atproto_pds.type === 'AtprotoPersonalDataServer') {
9393+ const pds = entry.services.atproto_pds.endpoint
9494+ console.log(`Found PDS: ${pds}`)
9595+ if (pds.toLowerCase() !== this.currentPdsUrl.toLowerCase()) {
9696+ oldPds = pds
9797+ break
9898+ }
9999+ }
100100+ }
125101 }
126126- this.oldPdsAgent = agent;
102102+ }
103103+ if (oldPds === null) {
104104+ throw new Error('Could not find your old PDS')
105105+ }
106106+ } else {
107107+ oldPds = pdsUrl
127108 }
128109129129- /**
130130- * Gets the missing blobs from the old PDS and uploads them to the current PDS
131131- * @param statusUpdateHandler {function} - A function to update the status of the migration. This is useful for showing the user the progress of the migration
132132- * @returns {Promise<{accountStatus: OutputSchema, missingBlobsCount: number}>}
133133- */
134134- async migrateMissingBlobs(statusUpdateHandler) {
135135- if (this.currentPdsAgent === null) {
136136- throw new Error('Current PDS agent is not set');
137137- }
138138- if (this.oldPdsAgent === null) {
139139- throw new Error('Old PDS agent is not set');
140140- }
141141- statusUpdateHandler('Starting to import blobs...');
110110+ const agent = new AtpAgent({
111111+ service: oldPds,
112112+ })
142113143143- let totalMissingBlobs = 0;
144144- let missingBlobCursor = undefined;
145145- let missingUploadedBlobs = 0;
146146-147147- do {
148148-149149- const missingBlobs = await this.currentPdsAgent.com.atproto.repo.listMissingBlobs({
150150- cursor: missingBlobCursor,
151151- limit: 1000,
152152- });
153153- totalMissingBlobs += missingBlobs.data.blobs.length;
154154-155155- for (const recordBlob of missingBlobs.data.blobs) {
156156- try {
114114+ if (twoFactorCode === null) {
115115+ await agent.login({ identifier: this.did, password })
116116+ } else {
117117+ await agent.login({
118118+ identifier: this.did,
119119+ password: password,
120120+ authFactorToken: twoFactorCode,
121121+ })
122122+ }
123123+ this.oldPdsAgent = agent
124124+ }
157125158158- const blobRes = await this.oldPdsAgent.com.atproto.sync.getBlob({
159159- did: this.did,
160160- cid: recordBlob.cid,
161161- });
162162- let result = await this.currentPdsAgent.com.atproto.repo.uploadBlob(blobRes.data, {
163163- encoding: blobRes.headers['content-type'],
164164- });
126126+ /**
127127+ * Gets the missing blobs from the old PDS and uploads them to the current PDS
128128+ * @param statusUpdateHandler {function} - A function to update the status of the migration. This is useful for showing the user the progress of the migration
129129+ * @returns {Promise<{accountStatus: OutputSchema, missingBlobsCount: number}>}
130130+ */
131131+ async migrateMissingBlobs(statusUpdateHandler) {
132132+ if (this.currentPdsAgent === null) {
133133+ throw new Error('Current PDS agent is not set')
134134+ }
135135+ if (this.oldPdsAgent === null) {
136136+ throw new Error('Old PDS agent is not set')
137137+ }
138138+ statusUpdateHandler('Starting to import blobs...')
165139166166- if (result.status === 429) {
167167- statusUpdateHandler(`You are being rate limited. Will need to try again later to get the rest of the blobs. Migrated blobs: ${missingUploadedBlobs}/${totalMissingBlobs}`);
168168- }
140140+ let totalMissingBlobs = 0
141141+ let missingBlobCursor = undefined
142142+ let missingUploadedBlobs = 0
169143170170- if (missingUploadedBlobs % 2 === 0) {
171171- statusUpdateHandler(`Migrating blobs: ${missingUploadedBlobs}/${totalMissingBlobs} (The total may increase as we find more)`);
172172- }
173173- missingUploadedBlobs++;
174174- } catch (error) {
175175- console.error(error);
176176- this.missingBlobs.push(recordBlob.cid);
177177- }
178178- }
179179- missingBlobCursor = missingBlobs.data.cursor;
180180- } while (missingBlobCursor);
144144+ do {
145145+ const missingBlobs = await this.currentPdsAgent.com.atproto.repo.listMissingBlobs({
146146+ cursor: missingBlobCursor,
147147+ limit: 1000,
148148+ })
149149+ totalMissingBlobs += missingBlobs.data.blobs.length
181150182182- const accountStatus = await this.currentPdsAgent.com.atproto.server.checkAccountStatus();
183183- const missingBlobs = await this.currentPdsAgent.com.atproto.repo.listMissingBlobs({
184184- limit: 10,
185185- });
186186- return {accountStatus: accountStatus.data, missingBlobsCount: missingBlobs.data.blobs.length};
151151+ for (const recordBlob of missingBlobs.data.blobs) {
152152+ try {
153153+ const blobRes = await this.oldPdsAgent.com.atproto.sync.getBlob({
154154+ did: this.did,
155155+ cid: recordBlob.cid,
156156+ })
157157+ let result = await this.currentPdsAgent.com.atproto.repo.uploadBlob(blobRes.data, {
158158+ encoding: blobRes.headers['content-type'],
159159+ })
187160161161+ if (result.status === 429) {
162162+ statusUpdateHandler(
163163+ `You are being rate limited. Will need to try again later to get the rest of the blobs. Migrated blobs: ${missingUploadedBlobs}/${totalMissingBlobs}`,
164164+ )
165165+ }
188166189189- }
167167+ if (missingUploadedBlobs % 2 === 0) {
168168+ statusUpdateHandler(
169169+ `Migrating blobs: ${missingUploadedBlobs}/${totalMissingBlobs} (The total may increase as we find more)`,
170170+ )
171171+ }
172172+ missingUploadedBlobs++
173173+ } catch (error) {
174174+ console.error(error)
175175+ this.missingBlobs.push(recordBlob.cid)
176176+ }
177177+ }
178178+ missingBlobCursor = missingBlobs.data.cursor
179179+ } while (missingBlobCursor)
190180181181+ const accountStatus = await this.currentPdsAgent.com.atproto.server.checkAccountStatus()
182182+ const missingBlobs = await this.currentPdsAgent.com.atproto.repo.listMissingBlobs({
183183+ limit: 10,
184184+ })
185185+ return { accountStatus: accountStatus.data, missingBlobsCount: missingBlobs.data.blobs.length }
186186+ }
191187}
192188193193-export {MissingBlobs};
189189+export { MissingBlobs }
+392-341
packages/moover/lib/pdsmoover.js
···11-import {docResolver, cleanHandle, handleResolver} from './atprotoUtils.js';
22-import {AtpAgent} from '@atproto/api';
33-11+import { docResolver, cleanHandle, handleResolver } from './atprotoUtils.js'
22+import { AtpAgent } from '@atproto/api'
4354function safeStatusUpdate(statusUpdateHandler, status) {
66- if (statusUpdateHandler) {
77- statusUpdateHandler(status);
88- }
55+ if (statusUpdateHandler) {
66+ statusUpdateHandler(status)
77+ }
98}
1091110/**
···1312 * On pdsmoover.com this is the logic for the MOOver
1413 */
1514class Migrator {
1616- constructor() {
1717- /** @type {AtpAgent} */
1818- this.oldAgent = null;
1919- /** @type {AtpAgent} */
2020- this.newAgent = null;
2121- /** @type {[string]} */
2222- this.missingBlobs = [];
2323- //State for reruns
2424- /** @type {boolean} */
2525- this.createNewAccount = true;
2626- /** @type {boolean} */
2727- this.migrateRepo = true;
2828- /** @type {boolean} */
2929- this.migrateBlobs = true;
3030- /** @type {boolean} */
3131- this.migrateMissingBlobs = true;
3232- /** @type {boolean} */
3333- this.migratePrefs = true;
3434- /** @type {boolean} */
3535- this.migratePlcRecord = true;
3636- }
1515+ constructor() {
1616+ /** @type {AtpAgent} */
1717+ this.oldAgent = null
1818+ /** @type {AtpAgent} */
1919+ this.newAgent = null
2020+ /** @type {[string]} */
2121+ this.missingBlobs = []
2222+ //State for reruns
2323+ /** @type {boolean} */
2424+ this.createNewAccount = true
2525+ /** @type {boolean} */
2626+ this.migrateRepo = true
2727+ /** @type {boolean} */
2828+ this.migrateBlobs = true
2929+ /** @type {boolean} */
3030+ this.migrateMissingBlobs = true
3131+ /** @type {boolean} */
3232+ this.migratePrefs = true
3333+ /** @type {boolean} */
3434+ this.migratePlcRecord = true
3535+ }
37363838- /**
3939- * This migrator is pretty cut and dry and makes a few assumptions
4040- * 1. You are using the same password between each account
4141- * 2. If this command fails for something like oauth 2fa code it throws an error and expects the same values when ran again.
4242- * 3. You can control which "actions" happen by setting the class variables to false.
4343- * 4. Each instance of the class is assumed to be for a single migration
4444- * @param {string} oldHandle - The handle you use on your old pds, something like alice.bsky.social
4545- * @param {string} password - Your password for your current login. Has to be your real password, no app password. When setting up a new account we reuse it as well for that account
4646- * @param {string} newPdsUrl - The new URL for your pds. Like https://coolnewpds.com
4747- * @param {string} newEmail - The email you want to use on the new pds (can be the same as the previous one as long as it's not already being used on the new pds)
4848- * @param {string} newHandle - The new handle you want, like alice.bsky.social, or if you already have a domain name set as a handle can use it myname.com.
4949- * @param {string|null} inviteCode - The invite code you got from the PDS you are migrating to. If null does not include one
5050- * @param {function|null} statusUpdateHandler - a function that takes a string used to update the UI. Like (status) => console.log(status)
5151- * @param {string|null} twoFactorCode - Optional, but needed if it fails with 2fa required
5252- * @param verificationCode - Optional verification captcha code for account creation if the PDS requires it
5353- */
5454- async migrate(oldHandle, password, newPdsUrl, newEmail, newHandle, inviteCode, statusUpdateHandler = null, twoFactorCode = null, verificationCode = null) {
5555- //Leaving this logic that either sets the agent to bsky.social, or the PDS since it's what I found worked best for migrations.
5656- // handleAndPDSResolver should be able to handle it, but there have been edge cases and this was what worked best
5757- oldHandle = cleanHandle(oldHandle);
5858- let oldAgent;
5959- let usersDid;
6060- //If it's a bsky handle just go with the entryway and let it sort everything
6161- if (oldHandle.endsWith('.bsky.social')) {
6262- oldAgent = new AtpAgent({service: 'https://bsky.social'});
6363- const publicAgent = new AtpAgent({service: 'https://public.api.bsky.app'});
6464- const resolveIdentityFromEntryway = await publicAgent.com.atproto.identity.resolveHandle({handle: oldHandle});
6565- usersDid = resolveIdentityFromEntryway.data.did;
3737+ /**
3838+ * This migrator is pretty cut and dry and makes a few assumptions
3939+ * 1. You are using the same password between each account
4040+ * 2. If this command fails for something like oauth 2fa code it throws an error and expects the same values when ran again.
4141+ * 3. You can control which "actions" happen by setting the class variables to false.
4242+ * 4. Each instance of the class is assumed to be for a single migration
4343+ * @param {string} oldHandle - The handle you use on your old pds, something like alice.bsky.social
4444+ * @param {string} password - Your password for your current login. Has to be your real password, no app password. When setting up a new account we reuse it as well for that account
4545+ * @param {string} newPdsUrl - The new URL for your pds. Like https://coolnewpds.com
4646+ * @param {string} newEmail - The email you want to use on the new pds (can be the same as the previous one as long as it's not already being used on the new pds)
4747+ * @param {string} newHandle - The new handle you want, like alice.bsky.social, or if you already have a domain name set as a handle can use it myname.com.
4848+ * @param {string|null} inviteCode - The invite code you got from the PDS you are migrating to. If null does not include one
4949+ * @param {function|null} statusUpdateHandler - a function that takes a string used to update the UI. Like (status) => console.log(status)
5050+ * @param {string|null} twoFactorCode - Optional, but needed if it fails with 2fa required
5151+ * @param verificationCode - Optional verification captcha code for account creation if the PDS requires it
5252+ */
5353+ async migrate(
5454+ oldHandle,
5555+ password,
5656+ newPdsUrl,
5757+ newEmail,
5858+ newHandle,
5959+ inviteCode,
6060+ statusUpdateHandler = null,
6161+ twoFactorCode = null,
6262+ verificationCode = null,
6363+ ) {
6464+ //Leaving this logic that either sets the agent to bsky.social, or the PDS since it's what I found worked best for migrations.
6565+ // handleAndPDSResolver should be able to handle it, but there have been edge cases and this was what worked best
6666+ oldHandle = cleanHandle(oldHandle)
6767+ let oldAgent
6868+ let usersDid
6969+ //If it's a bsky handle just go with the entryway and let it sort everything
7070+ if (oldHandle.endsWith('.bsky.social')) {
7171+ oldAgent = new AtpAgent({ service: 'https://bsky.social' })
7272+ const publicAgent = new AtpAgent({
7373+ service: 'https://public.api.bsky.app',
7474+ })
7575+ const resolveIdentityFromEntryway = await publicAgent.com.atproto.identity.resolveHandle({
7676+ handle: oldHandle,
7777+ })
7878+ usersDid = resolveIdentityFromEntryway.data.did
7979+ } else {
8080+ //Resolves the did and finds the did document for the old PDS
8181+ safeStatusUpdate(statusUpdateHandler, 'Resolving old PDS')
8282+ usersDid = await handleResolver.resolve(oldHandle)
8383+ const didDoc = await docResolver.resolve(usersDid)
8484+ safeStatusUpdate(
8585+ statusUpdateHandler,
8686+ 'Resolving did document and finding your current PDS URL',
8787+ )
66886767- } else {
6868- //Resolves the did and finds the did document for the old PDS
6969- safeStatusUpdate(statusUpdateHandler, 'Resolving old PDS');
7070- usersDid = await handleResolver.resolve(oldHandle);
7171- const didDoc = await docResolver.resolve(usersDid);
7272- safeStatusUpdate(statusUpdateHandler, 'Resolving did document and finding your current PDS URL');
8989+ let oldPds
9090+ try {
9191+ oldPds = didDoc.service.filter(s => s.type === 'AtprotoPersonalDataServer')[0]
9292+ .serviceEndpoint
9393+ } catch (error) {
9494+ console.error(error)
9595+ throw new Error('Could not find a PDS in the DID document.')
9696+ }
73977474- let oldPds;
7575- try {
7676- oldPds = didDoc.service.filter(s => s.type === 'AtprotoPersonalDataServer')[0].serviceEndpoint;
7777- } catch (error) {
7878- console.error(error);
7979- throw new Error('Could not find a PDS in the DID document.');
8080- }
9898+ oldAgent = new AtpAgent({
9999+ service: oldPds,
100100+ })
101101+ }
811028282- oldAgent = new AtpAgent({
8383- service: oldPds,
8484- });
103103+ safeStatusUpdate(statusUpdateHandler, 'Logging you in to the old PDS')
104104+ //Login to the old PDS
105105+ if (twoFactorCode === null) {
106106+ await oldAgent.login({ identifier: oldHandle, password })
107107+ } else {
108108+ await oldAgent.login({
109109+ identifier: oldHandle,
110110+ password: password,
111111+ authFactorToken: twoFactorCode,
112112+ })
113113+ }
851148686- }
115115+ safeStatusUpdate(
116116+ statusUpdateHandler,
117117+ 'Checking that the new PDS is an actual PDS (if the url is wrong this takes a while to error out)',
118118+ )
119119+ const newAgent = new AtpAgent({ service: newPdsUrl })
120120+ const newHostDesc = await newAgent.com.atproto.server.describeServer()
121121+ if (this.createNewAccount) {
122122+ const newHostWebDid = newHostDesc.data.did
871238888- safeStatusUpdate(statusUpdateHandler, 'Logging you in to the old PDS');
8989- //Login to the old PDS
9090- if (twoFactorCode === null) {
9191- await oldAgent.login({identifier: oldHandle, password});
9292- } else {
9393- await oldAgent.login({identifier: oldHandle, password: password, authFactorToken: twoFactorCode});
9494- }
124124+ safeStatusUpdate(statusUpdateHandler, 'Creating a new account on the new PDS')
951259696- safeStatusUpdate(statusUpdateHandler, 'Checking that the new PDS is an actual PDS (if the url is wrong this takes a while to error out)');
9797- const newAgent = new AtpAgent({service: newPdsUrl});
9898- const newHostDesc = await newAgent.com.atproto.server.describeServer();
9999- if (this.createNewAccount) {
100100- const newHostWebDid = newHostDesc.data.did;
126126+ const createAuthResp = await oldAgent.com.atproto.server.getServiceAuth({
127127+ aud: newHostWebDid,
128128+ lxm: 'com.atproto.server.createAccount',
129129+ })
130130+ const serviceJwt = createAuthResp.data.token
101131102102- safeStatusUpdate(statusUpdateHandler, 'Creating a new account on the new PDS');
132132+ let createAccountRequest = {
133133+ did: usersDid,
134134+ handle: newHandle,
135135+ email: newEmail,
136136+ password: password,
137137+ }
138138+ if (inviteCode) {
139139+ createAccountRequest.inviteCode = inviteCode
140140+ }
141141+ if (verificationCode) {
142142+ createAccountRequest.verificationCode = verificationCode
143143+ }
144144+ const createNewAccount = await newAgent.com.atproto.server.createAccount(
145145+ createAccountRequest,
146146+ {
147147+ headers: { authorization: `Bearer ${serviceJwt}` },
148148+ encoding: 'application/json',
149149+ },
150150+ )
103151104104- const createAuthResp = await oldAgent.com.atproto.server.getServiceAuth({
105105- aud: newHostWebDid,
106106- lxm: 'com.atproto.server.createAccount',
107107- });
108108- const serviceJwt = createAuthResp.data.token;
152152+ if (createNewAccount.data.did !== usersDid.toString()) {
153153+ throw new Error('Did not create the new account with the same did as the old account')
154154+ }
155155+ }
156156+ safeStatusUpdate(statusUpdateHandler, 'Logging in with the new account')
109157110110- let createAccountRequest = {
111111- did: usersDid,
112112- handle: newHandle,
113113- email: newEmail,
114114- password: password,
115115- };
116116- if (inviteCode) {
117117- createAccountRequest.inviteCode = inviteCode;
118118- }
119119- if (verificationCode) {
120120- createAccountRequest.verificationCode = verificationCode;
121121- }
122122- const createNewAccount = await newAgent.com.atproto.server.createAccount(
123123- createAccountRequest,
124124- {
125125- headers: {authorization: `Bearer ${serviceJwt}`},
126126- encoding: 'application/json',
127127- });
158158+ await newAgent.login({
159159+ identifier: usersDid,
160160+ password: password,
161161+ })
128162129129- if (createNewAccount.data.did !== usersDid.toString()) {
130130- throw new Error('Did not create the new account with the same did as the old account');
131131- }
132132- }
133133- safeStatusUpdate(statusUpdateHandler, 'Logging in with the new account');
163163+ if (this.migrateRepo) {
164164+ safeStatusUpdate(statusUpdateHandler, 'Migrating your repo')
165165+ const repoRes = await oldAgent.com.atproto.sync.getRepo({
166166+ did: usersDid,
167167+ })
168168+ await newAgent.com.atproto.repo.importRepo(repoRes.data, {
169169+ encoding: 'application/vnd.ipld.car',
170170+ })
171171+ }
134172135135- await newAgent.login({
136136- identifier: usersDid,
137137- password: password,
138138- });
173173+ let newAccountStatus = await newAgent.com.atproto.server.checkAccountStatus()
139174140140- if (this.migrateRepo) {
141141- safeStatusUpdate(statusUpdateHandler, 'Migrating your repo');
142142- const repoRes = await oldAgent.com.atproto.sync.getRepo({did: usersDid});
143143- await newAgent.com.atproto.repo.importRepo(repoRes.data, {
144144- encoding: 'application/vnd.ipld.car',
145145- });
146146- }
175175+ if (this.migrateBlobs) {
176176+ safeStatusUpdate(statusUpdateHandler, 'Migrating your blobs')
147177148148- let newAccountStatus = await newAgent.com.atproto.server.checkAccountStatus();
178178+ let blobCursor = undefined
179179+ let uploadedBlobs = 0
180180+ do {
181181+ safeStatusUpdate(
182182+ statusUpdateHandler,
183183+ `Migrating blobs: ${uploadedBlobs}/${newAccountStatus.data.expectedBlobs}`,
184184+ )
149185150150- if (this.migrateBlobs) {
151151- safeStatusUpdate(statusUpdateHandler, 'Migrating your blobs');
186186+ const listedBlobs = await oldAgent.com.atproto.sync.listBlobs({
187187+ did: usersDid,
188188+ cursor: blobCursor,
189189+ limit: 100,
190190+ })
152191153153- let blobCursor = undefined;
154154- let uploadedBlobs = 0;
155155- do {
156156- safeStatusUpdate(statusUpdateHandler, `Migrating blobs: ${uploadedBlobs}/${newAccountStatus.data.expectedBlobs}`);
157157-158158- const listedBlobs = await oldAgent.com.atproto.sync.listBlobs({
159159- did: usersDid,
160160- cursor: blobCursor,
161161- limit: 100,
162162- });
163163-164164- for (const cid of listedBlobs.data.cids) {
165165- try {
166166- const blobRes = await oldAgent.com.atproto.sync.getBlob({
167167- did: usersDid,
168168- cid,
169169- });
170170- await newAgent.com.atproto.repo.uploadBlob(blobRes.data, {
171171- encoding: blobRes.headers['content-type'],
172172- });
173173- uploadedBlobs++;
174174- if (uploadedBlobs % 10 === 0) {
175175- safeStatusUpdate(statusUpdateHandler, `Migrating blobs: ${uploadedBlobs}/${newAccountStatus.data.expectedBlobs}`);
176176- }
177177- } catch (error) {
178178- console.error(error);
179179- }
180180- }
181181- blobCursor = listedBlobs.data.cursor;
182182- } while (blobCursor);
192192+ for (const cid of listedBlobs.data.cids) {
193193+ try {
194194+ const blobRes = await oldAgent.com.atproto.sync.getBlob({
195195+ did: usersDid,
196196+ cid,
197197+ })
198198+ await newAgent.com.atproto.repo.uploadBlob(blobRes.data, {
199199+ encoding: blobRes.headers['content-type'],
200200+ })
201201+ uploadedBlobs++
202202+ if (uploadedBlobs % 10 === 0) {
203203+ safeStatusUpdate(
204204+ statusUpdateHandler,
205205+ `Migrating blobs: ${uploadedBlobs}/${newAccountStatus.data.expectedBlobs}`,
206206+ )
207207+ }
208208+ } catch (error) {
209209+ console.error(error)
210210+ }
183211 }
212212+ blobCursor = listedBlobs.data.cursor
213213+ } while (blobCursor)
214214+ }
184215185185- if (this.migrateMissingBlobs) {
186186- newAccountStatus = await newAgent.com.atproto.server.checkAccountStatus();
187187- if (newAccountStatus.data.expectedBlobs !== newAccountStatus.data.importedBlobs) {
188188- let totalMissingBlobs = newAccountStatus.data.expectedBlobs - newAccountStatus.data.importedBlobs;
189189- safeStatusUpdate(statusUpdateHandler, 'Looks like there are some missing blobs. Going to try and upload them now.');
190190- //Probably should be shared between main blob uploader, but eh
191191- let missingBlobCursor = undefined;
192192- let missingUploadedBlobs = 0;
193193- do {
194194- safeStatusUpdate(statusUpdateHandler, `Migrating blobs: ${missingUploadedBlobs}/${totalMissingBlobs}`);
216216+ if (this.migrateMissingBlobs) {
217217+ newAccountStatus = await newAgent.com.atproto.server.checkAccountStatus()
218218+ if (newAccountStatus.data.expectedBlobs !== newAccountStatus.data.importedBlobs) {
219219+ let totalMissingBlobs =
220220+ newAccountStatus.data.expectedBlobs - newAccountStatus.data.importedBlobs
221221+ safeStatusUpdate(
222222+ statusUpdateHandler,
223223+ 'Looks like there are some missing blobs. Going to try and upload them now.',
224224+ )
225225+ //Probably should be shared between main blob uploader, but eh
226226+ let missingBlobCursor = undefined
227227+ let missingUploadedBlobs = 0
228228+ do {
229229+ safeStatusUpdate(
230230+ statusUpdateHandler,
231231+ `Migrating blobs: ${missingUploadedBlobs}/${totalMissingBlobs}`,
232232+ )
195233196196- const missingBlobs = await newAgent.com.atproto.repo.listMissingBlobs({
197197- cursor: missingBlobCursor,
198198- limit: 100,
199199- });
200200-201201- for (const recordBlob of missingBlobs.data.blobs) {
202202- try {
203203-204204- const blobRes = await oldAgent.com.atproto.sync.getBlob({
205205- did: usersDid,
206206- cid: recordBlob.cid,
207207- });
208208- await newAgent.com.atproto.repo.uploadBlob(blobRes.data, {
209209- encoding: blobRes.headers['content-type'],
210210- });
211211- if (missingUploadedBlobs % 10 === 0) {
212212- safeStatusUpdate(statusUpdateHandler, `Migrating blobs: ${missingUploadedBlobs}/${totalMissingBlobs}`);
213213- }
214214- missingUploadedBlobs++;
215215- } catch (error) {
216216- //TODO silently logging prob should list them so user can manually download
217217- console.error(error);
218218- this.missingBlobs.push(recordBlob.cid);
219219- }
220220- }
221221- missingBlobCursor = missingBlobs.data.cursor;
222222- } while (missingBlobCursor);
234234+ const missingBlobs = await newAgent.com.atproto.repo.listMissingBlobs({
235235+ cursor: missingBlobCursor,
236236+ limit: 100,
237237+ })
223238239239+ for (const recordBlob of missingBlobs.data.blobs) {
240240+ try {
241241+ const blobRes = await oldAgent.com.atproto.sync.getBlob({
242242+ did: usersDid,
243243+ cid: recordBlob.cid,
244244+ })
245245+ await newAgent.com.atproto.repo.uploadBlob(blobRes.data, {
246246+ encoding: blobRes.headers['content-type'],
247247+ })
248248+ if (missingUploadedBlobs % 10 === 0) {
249249+ safeStatusUpdate(
250250+ statusUpdateHandler,
251251+ `Migrating blobs: ${missingUploadedBlobs}/${totalMissingBlobs}`,
252252+ )
253253+ }
254254+ missingUploadedBlobs++
255255+ } catch (error) {
256256+ //TODO silently logging prob should list them so user can manually download
257257+ console.error(error)
258258+ this.missingBlobs.push(recordBlob.cid)
224259 }
225225- }
226226- if (this.migratePrefs) {
227227- const prefs = await oldAgent.app.bsky.actor.getPreferences();
228228- await newAgent.app.bsky.actor.putPreferences(prefs.data);
229229- }
260260+ }
261261+ missingBlobCursor = missingBlobs.data.cursor
262262+ } while (missingBlobCursor)
263263+ }
264264+ }
265265+ if (this.migratePrefs) {
266266+ const prefs = await oldAgent.app.bsky.actor.getPreferences()
267267+ await newAgent.app.bsky.actor.putPreferences(prefs.data)
268268+ }
230269231231- this.oldAgent = oldAgent;
232232- this.newAgent = newAgent;
270270+ this.oldAgent = oldAgent
271271+ this.newAgent = newAgent
233272234234- if (this.migratePlcRecord) {
235235- await oldAgent.com.atproto.identity.requestPlcOperationSignature();
236236- safeStatusUpdate(statusUpdateHandler, 'Please check your email for a PLC token');
237237- }
273273+ if (this.migratePlcRecord) {
274274+ await oldAgent.com.atproto.identity.requestPlcOperationSignature()
275275+ safeStatusUpdate(statusUpdateHandler, 'Please check your email for a PLC token')
238276 }
277277+ }
239278240240- /**
241241- * Sign and submits the PLC operation to officially migrate the account
242242- * @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
243243- * @param additionalRotationKeysToAdd {string[]} - additional rotation keys to add in addition to the ones provided by the new PDS.
244244- * @returns {Promise<void>}
245245- */
246246- async signPlcOperation(token, additionalRotationKeysToAdd = []) {
247247- const getDidCredentials =
248248- await this.newAgent.com.atproto.identity.getRecommendedDidCredentials();
249249- const pdsProvidedRotationKeys = getDidCredentials.data.rotationKeys ?? [];
250250- // Prepend any additional rotation keys (e.g., user-added keys, newly created key) so they appear above the new PDS rotation key
251251- const rotationKeys = [...(additionalRotationKeysToAdd || []), ...pdsProvidedRotationKeys];
252252- if (!rotationKeys) {
253253- throw new Error('No rotation key provided from the new PDS');
254254- }
255255- const credentials = {
256256- ...getDidCredentials.data,
257257- rotationKeys: rotationKeys,
258258- };
279279+ /**
280280+ * Sign and submits the PLC operation to officially migrate the account
281281+ * @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
282282+ * @param additionalRotationKeysToAdd {string[]} - additional rotation keys to add in addition to the ones provided by the new PDS.
283283+ * @returns {Promise<void>}
284284+ */
285285+ async signPlcOperation(token, additionalRotationKeysToAdd = []) {
286286+ const getDidCredentials =
287287+ await this.newAgent.com.atproto.identity.getRecommendedDidCredentials()
288288+ const pdsProvidedRotationKeys = getDidCredentials.data.rotationKeys ?? []
289289+ // Prepend any additional rotation keys (e.g., user-added keys, newly created key) so they appear above the new PDS rotation key
290290+ const rotationKeys = [...(additionalRotationKeysToAdd || []), ...pdsProvidedRotationKeys]
291291+ if (!rotationKeys) {
292292+ throw new Error('No rotation key provided from the new PDS')
293293+ }
294294+ const credentials = {
295295+ ...getDidCredentials.data,
296296+ rotationKeys: rotationKeys,
297297+ }
259298299299+ const plcOp = await this.oldAgent.com.atproto.identity.signPlcOperation({
300300+ token: token,
301301+ ...credentials,
302302+ })
260303261261- const plcOp = await this.oldAgent.com.atproto.identity.signPlcOperation({
262262- token: token,
263263- ...credentials,
264264- });
304304+ await this.newAgent.com.atproto.identity.submitPlcOperation({
305305+ operation: plcOp.data.operation,
306306+ })
265307266266- await this.newAgent.com.atproto.identity.submitPlcOperation({
267267- operation: plcOp.data.operation,
268268- });
308308+ await this.newAgent.com.atproto.server.activateAccount()
309309+ await this.oldAgent.com.atproto.server.deactivateAccount({})
310310+ }
269311270270- await this.newAgent.com.atproto.server.activateAccount();
271271- await this.oldAgent.com.atproto.server.deactivateAccount({});
312312+ /**
313313+ * Using this method assumes the Migrator class was constructed new and this was called.
314314+ * Find the user's previous PDS from the PLC op logs,
315315+ * logs in and deactivates their old account if it was found still active.
316316+ *
317317+ * @param oldHandle {string}
318318+ * @param oldPassword {string}
319319+ * @param {function|null} statusUpdateHandler - a function that takes a string used to update the UI.
320320+ * Like (status) => console.log(status)
321321+ * @param {string|null} twoFactorCode - Optional, but needed if it fails with 2fa required
322322+ * @returns {Promise<void>}
323323+ */
324324+ async deactivateOldAccount(
325325+ oldHandle,
326326+ oldPassword,
327327+ statusUpdateHandler = null,
328328+ twoFactorCode = null,
329329+ ) {
330330+ //Leaving this logic that either sets the agent to bsky.social, or the PDS since it's what I found worked best for migrations.
331331+ // handleAndPDSResolver should be able to handle it, but there have been edge cases and this was what worked best oldHandle = cleanHandle(oldHandle);
332332+ let usersDid
333333+ //If it's a bsky handle just go with the entryway and let it sort everything
334334+ if (oldHandle.endsWith('.bsky.social')) {
335335+ const publicAgent = new AtpAgent({
336336+ service: 'https://public.api.bsky.app',
337337+ })
338338+ const resolveIdentityFromEntryway = await publicAgent.com.atproto.identity.resolveHandle({
339339+ handle: oldHandle,
340340+ })
341341+ usersDid = resolveIdentityFromEntryway.data.did
342342+ } else {
343343+ //Resolves the did and finds the did document for the old PDS
344344+ safeStatusUpdate(statusUpdateHandler, 'Resolving did from handle')
345345+ usersDid = await handleResolver.resolve(oldHandle)
272346 }
273347274274- /**
275275- * Using this method assumes the Migrator class was constructed new and this was called.
276276- * Find the user's previous PDS from the PLC op logs,
277277- * logs in and deactivates their old account if it was found still active.
278278- *
279279- * @param oldHandle {string}
280280- * @param oldPassword {string}
281281- * @param {function|null} statusUpdateHandler - a function that takes a string used to update the UI.
282282- * Like (status) => console.log(status)
283283- * @param {string|null} twoFactorCode - Optional, but needed if it fails with 2fa required
284284- * @returns {Promise<void>}
285285- */
286286- async deactivateOldAccount(oldHandle, oldPassword, statusUpdateHandler = null, twoFactorCode = null) {
287287- //Leaving this logic that either sets the agent to bsky.social, or the PDS since it's what I found worked best for migrations.
288288- // handleAndPDSResolver should be able to handle it, but there have been edge cases and this was what worked best oldHandle = cleanHandle(oldHandle);
289289- let usersDid;
290290- //If it's a bsky handle just go with the entryway and let it sort everything
291291- if (oldHandle.endsWith('.bsky.social')) {
292292- const publicAgent = new AtpAgent({service: 'https://public.api.bsky.app'});
293293- const resolveIdentityFromEntryway = await publicAgent.com.atproto.identity.resolveHandle({handle: oldHandle});
294294- usersDid = resolveIdentityFromEntryway.data.did;
295295- } else {
296296- //Resolves the did and finds the did document for the old PDS
297297- safeStatusUpdate(statusUpdateHandler, 'Resolving did from handle');
298298- usersDid = await handleResolver.resolve(oldHandle);
299299- }
348348+ const didDoc = await docResolver.resolve(usersDid)
349349+ let currentPds
350350+ try {
351351+ currentPds = didDoc.service.filter(s => s.type === 'AtprotoPersonalDataServer')[0]
352352+ .serviceEndpoint
353353+ } catch (error) {
354354+ console.error(error)
355355+ throw new Error('Could not find a PDS in the DID document.')
356356+ }
300357301301- const didDoc = await docResolver.resolve(usersDid);
302302- let currentPds;
303303- try {
304304- currentPds = didDoc.service.filter(s => s.type === 'AtprotoPersonalDataServer')[0].serviceEndpoint;
305305- } catch (error) {
306306- console.error(error);
307307- throw new Error('Could not find a PDS in the DID document.');
358358+ const plcLogRequest = await fetch(`https://plc.directory/${usersDid}/log`)
359359+ const plcLog = await plcLogRequest.json()
360360+ let pdsBeforeCurrent = ''
361361+ for (const log of plcLog) {
362362+ try {
363363+ const pds = log.services.atproto_pds.endpoint
364364+ if (pds.toLowerCase() === currentPds.toLowerCase()) {
365365+ console.log('Found the PDS before the current one')
366366+ break
308367 }
368368+ pdsBeforeCurrent = pds
369369+ } catch (e) {
370370+ console.log(e)
371371+ }
372372+ }
373373+ if (pdsBeforeCurrent === '') {
374374+ throw new Error('Could not find the PDS before the current one')
375375+ }
309376310310- const plcLogRequest = await fetch(`https://plc.directory/${usersDid}/log`);
311311- const plcLog = await plcLogRequest.json();
312312- let pdsBeforeCurrent = '';
313313- for (const log of plcLog) {
314314- try {
315315- const pds = log.services.atproto_pds.endpoint;
316316- if (pds.toLowerCase() === currentPds.toLowerCase()) {
317317- console.log('Found the PDS before the current one');
318318- break;
319319- }
320320- pdsBeforeCurrent = pds;
321321- } catch (e) {
322322- console.log(e);
323323- }
324324- }
325325- if (pdsBeforeCurrent === '') {
326326- throw new Error('Could not find the PDS before the current one');
327327- }
377377+ let oldAgent = new AtpAgent({ service: pdsBeforeCurrent })
378378+ safeStatusUpdate(statusUpdateHandler, `Logging you in to the old PDS: ${pdsBeforeCurrent}`)
379379+ //Login to the old PDS
380380+ if (twoFactorCode === null) {
381381+ await oldAgent.login({ identifier: oldHandle, password: oldPassword })
382382+ } else {
383383+ await oldAgent.login({
384384+ identifier: oldHandle,
385385+ password: oldPassword,
386386+ authFactorToken: twoFactorCode,
387387+ })
388388+ }
389389+ safeStatusUpdate(statusUpdateHandler, "Checking this isn't your current PDS")
390390+ if (pdsBeforeCurrent === currentPds) {
391391+ throw new Error('This is your current PDS. Login to your old account username and password')
392392+ }
328393329329- let oldAgent = new AtpAgent({service: pdsBeforeCurrent});
330330- safeStatusUpdate(statusUpdateHandler, `Logging you in to the old PDS: ${pdsBeforeCurrent}`);
331331- //Login to the old PDS
332332- if (twoFactorCode === null) {
333333- await oldAgent.login({identifier: oldHandle, password: oldPassword});
334334- } else {
335335- await oldAgent.login({identifier: oldHandle, password: oldPassword, authFactorToken: twoFactorCode});
336336- }
337337- safeStatusUpdate(statusUpdateHandler, 'Checking this isn\'t your current PDS');
338338- if (pdsBeforeCurrent === currentPds) {
339339- throw new Error('This is your current PDS. Login to your old account username and password');
340340- }
341341-342342- let currentAccountStatus = await oldAgent.com.atproto.server.checkAccountStatus();
343343- if (!currentAccountStatus.data.activated) {
344344- safeStatusUpdate(statusUpdateHandler, 'All good. Your old account is not activated.');
345345- }
346346- safeStatusUpdate(statusUpdateHandler, 'Deactivating your OLD account');
347347- await oldAgent.com.atproto.server.deactivateAccount({});
348348- safeStatusUpdate(statusUpdateHandler, 'Successfully deactivated your OLD account');
394394+ let currentAccountStatus = await oldAgent.com.atproto.server.checkAccountStatus()
395395+ if (!currentAccountStatus.data.activated) {
396396+ safeStatusUpdate(statusUpdateHandler, 'All good. Your old account is not activated.')
349397 }
398398+ safeStatusUpdate(statusUpdateHandler, 'Deactivating your OLD account')
399399+ await oldAgent.com.atproto.server.deactivateAccount({})
400400+ safeStatusUpdate(statusUpdateHandler, 'Successfully deactivated your OLD account')
401401+ }
350402351351- /**
352352- * Signs the logged-in user in this.newAgent for backups with PDS MOOver. This is usually called after migrate and signPlcOperation are successful
353353- *
354354- * @param {string} didWeb
355355- * @returns {Promise<void>}
356356- */
357357- async signUpForBackupsFromMigration(didWeb = 'did:web:pdsmoover.com') {
403403+ /**
404404+ * Signs the logged-in user in this.newAgent for backups with PDS MOOver. This is usually called after migrate and signPlcOperation are successful
405405+ *
406406+ * @param {string} didWeb
407407+ * @returns {Promise<void>}
408408+ */
409409+ async signUpForBackupsFromMigration(didWeb = 'did:web:pdsmoover.com') {
410410+ //Manually grabbing the jwt and making a call with fetch cause for the life of me I could not figure out
411411+ //how you used @atproto/api to make a call for proxying
412412+ const url = `${this.newAgent.serviceUrl.origin}/xrpc/com.pdsmoover.backup.signUp`
358413359359- //Manually grabbing the jwt and making a call with fetch cause for the life of me I could not figure out
360360- //how you used @atproto/api to make a call for proxying
361361- const url = `${this.newAgent.serviceUrl.origin}/xrpc/com.pdsmoover.backup.signUp`;
414414+ const accessJwt = this.newAgent?.session?.accessJwt
415415+ if (!accessJwt) {
416416+ throw new Error('Missing access token for authorization')
417417+ }
362418363363- const accessJwt = this.newAgent?.session?.accessJwt;
364364- if (!accessJwt) {
365365- throw new Error('Missing access token for authorization');
366366- }
419419+ const res = await fetch(url, {
420420+ method: 'POST',
421421+ headers: {
422422+ 'Authorization': `Bearer ${accessJwt}`,
423423+ 'Content-Type': 'application/json',
424424+ 'Accept': 'application/json',
425425+ 'atproto-proxy': `${didWeb}#repo_backup`,
426426+ },
427427+ body: JSON.stringify({}),
428428+ })
367429368368- const res = await fetch(url, {
369369- method: 'POST',
370370- headers: {
371371- 'Authorization': `Bearer ${accessJwt}`,
372372- 'Content-Type': 'application/json',
373373- 'Accept': 'application/json',
374374- 'atproto-proxy': `${didWeb}#repo_backup`,
375375- },
376376- body: JSON.stringify({}),
377377- });
430430+ if (!res.ok) {
431431+ let bodyText = ''
432432+ try {
433433+ bodyText = await res.text()
434434+ } catch {}
435435+ throw new Error(
436436+ `Backup signup failed: ${res.status} ${res.statusText}${bodyText ? ` - ${bodyText}` : ''}`,
437437+ )
438438+ }
378439379379- if (!res.ok) {
380380- let bodyText = '';
381381- try {
382382- bodyText = await res.text();
383383- } catch {
384384- }
385385- throw new Error(`Backup signup failed: ${res.status} ${res.statusText}${bodyText ? ` - ${bodyText}` : ''}`);
386386- }
387387-388388- //No return the success is all that is needed, if there's an error it will throw
389389- }
440440+ //No return the success is all that is needed, if there's an error it will throw
441441+ }
390442}
391443392392-export {Migrator};
393393-444444+export { Migrator }
+242-239
packages/moover/lib/plc-ops.js
···88 * @typedef {import('@atcute/did-plc').IndexedEntry} IndexedEntry
99 */
10101111-import {defs, normalizeOp} from '@atcute/did-plc';
1212-import {P256PrivateKey, parsePrivateMultikey, Secp256k1PrivateKey, Secp256k1PrivateKeyExportable} from '@atcute/crypto';
1313-import * as CBOR from '@atcute/cbor';
1414-import {fromBase16, toBase64Url} from '@atcute/multibase';
1515-1111+import { defs, normalizeOp } from '@atcute/did-plc'
1212+import {
1313+ P256PrivateKey,
1414+ parsePrivateMultikey,
1515+ Secp256k1PrivateKey,
1616+ Secp256k1PrivateKeyExportable,
1717+} from '@atcute/crypto'
1818+import * as CBOR from '@atcute/cbor'
1919+import { fromBase16, toBase64Url } from '@atcute/multibase'
16201721// Helper to base64url-encode JSON
1818-const jsonToB64Url = (obj) => {
1919- const enc = new TextEncoder();
2020- const json = JSON.stringify(obj);
2121- return toBase64Url(enc.encode(json));
2222-};
2222+const jsonToB64Url = obj => {
2323+ const enc = new TextEncoder()
2424+ const json = JSON.stringify(obj)
2525+ return toBase64Url(enc.encode(json))
2626+}
23272428/**
2529 * Class to help with various PLC operations
2630 */
2731class PlcOps {
3232+ /**
3333+ *
3434+ * @param plcDirectoryUrl {string} - The url of the plc directory, defaults to https://plc.directory
3535+ */
3636+ constructor(plcDirectoryUrl = 'https://plc.directory') {
2837 /**
2929- *
3030- * @param plcDirectoryUrl {string} - The url of the plc directory, defaults to https://plc.directory
3838+ * The url of the plc directory
3939+ * @type {string}
3140 */
3232- constructor(plcDirectoryUrl = 'https://plc.directory') {
3333- /**
3434- * The url of the plc directory
3535- * @type {string}
3636- */
3737- this.plcDirectoryUrl = plcDirectoryUrl;
3838- }
4141+ this.plcDirectoryUrl = plcDirectoryUrl
4242+ }
39434040- /**
4141- * Gets the current rotation keys for a user via their last PlC operation
4242- * @param did
4343- * @returns {Promise<string[]>}
4444- */
4545- async getCurrentRotationKeysForUser(did) {
4646- const logs = await this.getPlcAuditLogs(did);
4747- const {lastOperation} = this.getLastPlcOp(logs);
4848- return lastOperation.rotationKeys || [];
4949- }
4444+ /**
4545+ * Gets the current rotation keys for a user via their last PlC operation
4646+ * @param did
4747+ * @returns {Promise<string[]>}
4848+ */
4949+ async getCurrentRotationKeysForUser(did) {
5050+ const logs = await this.getPlcAuditLogs(did)
5151+ const { lastOperation } = this.getLastPlcOp(logs)
5252+ return lastOperation.rotationKeys || []
5353+ }
5454+5555+ /**
5656+ * Gets the last PlC operation for a user from the plc directory
5757+ * @param did
5858+ * @returns {Promise<{lastOperation: Operation, base: any}>}
5959+ */
6060+ async getLastPlcOpFromPlc(did) {
6161+ const logs = await this.getPlcAuditLogs(did)
6262+ return this.getLastPlcOp(logs)
6363+ }
50645151- /**
5252- * Gets the last PlC operation for a user from the plc directory
5353- * @param did
5454- * @returns {Promise<{lastOperation: Operation, base: any}>}
5555- */
5656- async getLastPlcOpFromPlc(did) {
5757- const logs = await this.getPlcAuditLogs(did);
5858- return this.getLastPlcOp(logs);
5959- }
6565+ /**
6666+ *
6767+ * @param logs {IndexedEntryLog}
6868+ * @returns {{lastOperation: Operation, base: IndexedEntry}}
6969+ */
7070+ getLastPlcOp(logs) {
7171+ const lastOp = logs.at(-1)
7272+ return { lastOperation: normalizeOp(lastOp.operation), base: lastOp }
7373+ }
60746161- /**
6262- *
6363- * @param logs {IndexedEntryLog}
6464- * @returns {{lastOperation: Operation, base: IndexedEntry}}
6565- */
6666- getLastPlcOp(logs) {
6767- const lastOp = logs.at(-1);
6868- return {lastOperation: normalizeOp(lastOp.operation), base: lastOp};
7575+ /**
7676+ * Gets the plc audit logs for a user from the plc directory
7777+ * @param did
7878+ * @returns {Promise<IndexedEntryLog>}
7979+ */
8080+ async getPlcAuditLogs(did) {
8181+ const response = await fetch(`${this.plcDirectoryUrl}/${did}/log/audit`)
8282+ if (!response.ok) {
8383+ throw new Error(`got response ${response.status}`)
6984 }
70858686+ const json = await response.json()
8787+ return defs.indexedEntryLog.parse(json)
8888+ }
71897272- /**
7373- * Gets the plc audit logs for a user from the plc directory
7474- * @param did
7575- * @returns {Promise<IndexedEntryLog>}
7676- */
7777- async getPlcAuditLogs(did) {
7878- const response = await fetch(`${this.plcDirectoryUrl}/${did}/log/audit`);
7979- if (!response.ok) {
8080- throw new Error(`got response ${response.status}`);
8181- }
8282-8383- const json = await response.json();
8484- return defs.indexedEntryLog.parse(json);
9090+ /**
9191+ * Creates a new secp256k1 key that can be used for either rotation or verification key
9292+ * @returns {Promise<{privateKey: string, publicKey: `did:key:${string}`}>}
9393+ */
9494+ async createANewSecp256k1() {
9595+ let keypair = await Secp256k1PrivateKeyExportable.createKeypair()
9696+ let publicKey = await keypair.exportPublicKey('did')
9797+ let privateKey = await keypair.exportPrivateKey('multikey')
9898+ return {
9999+ privateKey,
100100+ publicKey,
85101 }
102102+ }
861038787- /**
8888- * Creates a new secp256k1 key that can be used for either rotation or verification key
8989- * @returns {Promise<{privateKey: string, publicKey: `did:key:${string}`}>}
9090- */
9191- async createANewSecp256k1() {
9292- let keypair = await Secp256k1PrivateKeyExportable.createKeypair();
9393- let publicKey = await keypair.exportPublicKey('did');
9494- let privateKey = await keypair.exportPrivateKey('multikey');
9595- return {
9696- privateKey,
9797- publicKey
9898- };
104104+ /**
105105+ * Signs a new operation with the provided signing key, and information and submits it to the plc directory
106106+ * @param did {string} - The user's did
107107+ * @param signingRotationKey { P256PrivateKey|Secp256k1PrivateKey} - The keypair to sign the op with
108108+ * @param alsoKnownAs {string[]}
109109+ * @param rotationKeys {string[]}
110110+ * @param pds {string}
111111+ * @param verificationKey {string} - The public verification key
112112+ * @param prev {string} - The previous valid operation's cid.
113113+ * @returns {Promise<void>}
114114+ */
115115+ async signAndPublishNewOp(
116116+ did,
117117+ signingRotationKey,
118118+ alsoKnownAs,
119119+ rotationKeys,
120120+ pds,
121121+ verificationKey,
122122+ prev,
123123+ ) {
124124+ const rotationKeysToUse = [...new Set(rotationKeys)]
125125+ if (!rotationKeysToUse) {
126126+ throw new Error('No rotation keys were found to be added to the PLC')
99127 }
100128101101-102102- /**
103103- * Signs a new operation with the provided signing key, and information and submits it to the plc directory
104104- * @param did {string} - The user's did
105105- * @param signingRotationKey { P256PrivateKey|Secp256k1PrivateKey} - The keypair to sign the op with
106106- * @param alsoKnownAs {string[]}
107107- * @param rotationKeys {string[]}
108108- * @param pds {string}
109109- * @param verificationKey {string} - The public verification key
110110- * @param prev {string} - The previous valid operation's cid.
111111- * @returns {Promise<void>}
112112- */
113113- async signAndPublishNewOp(did, signingRotationKey, alsoKnownAs, rotationKeys, pds, verificationKey, prev) {
114114-115115- const rotationKeysToUse = [...new Set(rotationKeys)];
116116- if (!rotationKeysToUse) {
117117- throw new Error('No rotation keys were found to be added to the PLC');
118118- }
119119-120120- if (rotationKeysToUse.length > 5) {
121121- throw new Error('You can only add up to 5 rotation keys to the PLC');
122122- }
123123-124124- const operation = {
125125- type: 'plc_operation',
126126- prev,
127127- alsoKnownAs,
128128- rotationKeys: rotationKeysToUse,
129129- services: {
130130- atproto_pds: {
131131- type: 'AtprotoPersonalDataServer',
132132- endpoint: pds
133133- }
134134- },
135135- verificationMethods: {
136136- atproto: verificationKey
137137- }
138138- };
139139- const opBytes = CBOR.encode(operation);
140140- const sigBytes = await signingRotationKey.sign(opBytes);
129129+ if (rotationKeysToUse.length > 5) {
130130+ throw new Error('You can only add up to 5 rotation keys to the PLC')
131131+ }
141132142142- const signature = toBase64Url(sigBytes);
133133+ const operation = {
134134+ type: 'plc_operation',
135135+ prev,
136136+ alsoKnownAs,
137137+ rotationKeys: rotationKeysToUse,
138138+ services: {
139139+ atproto_pds: {
140140+ type: 'AtprotoPersonalDataServer',
141141+ endpoint: pds,
142142+ },
143143+ },
144144+ verificationMethods: {
145145+ atproto: verificationKey,
146146+ },
147147+ }
148148+ const opBytes = CBOR.encode(operation)
149149+ const sigBytes = await signingRotationKey.sign(opBytes)
143150144144- const signedOperation = {
145145- ...operation,
146146- sig: signature,
147147- };
151151+ const signature = toBase64Url(sigBytes)
148152149149- await this.pushPlcOperation(did, signedOperation);
153153+ const signedOperation = {
154154+ ...operation,
155155+ sig: signature,
150156 }
151157152152- /**
153153- * Takes a multi or hex based private key and returns a keypair
154154- * @param privateKeyString {string}
155155- * @param type {string} - secp256k1 or p256, needed if the private key is hex based, can be assumed if it's a multikey
156156- * @returns {Promise<{type: string, didPublicKey: `did:key:${string}`, keypair: P256PrivateKey|Secp256k1PrivateKey}>}
157157- */
158158- async getKeyPair(privateKeyString, type = 'secp256k1') {
159159- const HEX_REGEX = /^[0-9a-f]+$/i;
160160- const MULTIKEY_REGEX = /^z[a-km-zA-HJ-NP-Z1-9]+$/;
161161- let keypair = undefined;
158158+ await this.pushPlcOperation(did, signedOperation)
159159+ }
162160163163- if (HEX_REGEX.test(privateKeyString)) {
164164- const privateKeyBytes = fromBase16(privateKeyString);
161161+ /**
162162+ * Takes a multi or hex based private key and returns a keypair
163163+ * @param privateKeyString {string}
164164+ * @param type {string} - secp256k1 or p256, needed if the private key is hex based, can be assumed if it's a multikey
165165+ * @returns {Promise<{type: string, didPublicKey: `did:key:${string}`, keypair: P256PrivateKey|Secp256k1PrivateKey}>}
166166+ */
167167+ async getKeyPair(privateKeyString, type = 'secp256k1') {
168168+ const HEX_REGEX = /^[0-9a-f]+$/i
169169+ const MULTIKEY_REGEX = /^z[a-km-zA-HJ-NP-Z1-9]+$/
170170+ let keypair = undefined
165171166166- switch (type) {
167167- case 'p256': {
168168- keypair = await P256PrivateKey.importRaw(privateKeyBytes);
169169- break;
170170- }
171171- case 'secp256k1': {
172172- keypair = await Secp256k1PrivateKey.importRaw(privateKeyBytes);
173173- break;
174174- }
175175- default: {
176176- throw new Error(`unsupported "${type}" type`);
177177- }
178178- }
179179- } else if (MULTIKEY_REGEX.test(privateKeyString)) {
172172+ if (HEX_REGEX.test(privateKeyString)) {
173173+ const privateKeyBytes = fromBase16(privateKeyString)
180174181181- const match = parsePrivateMultikey(privateKeyString);
182182- const privateKeyBytes = match.privateKeyBytes;
175175+ switch (type) {
176176+ case 'p256': {
177177+ keypair = await P256PrivateKey.importRaw(privateKeyBytes)
178178+ break
179179+ }
180180+ case 'secp256k1': {
181181+ keypair = await Secp256k1PrivateKey.importRaw(privateKeyBytes)
182182+ break
183183+ }
184184+ default: {
185185+ throw new Error(`unsupported "${type}" type`)
186186+ }
187187+ }
188188+ } else if (MULTIKEY_REGEX.test(privateKeyString)) {
189189+ const match = parsePrivateMultikey(privateKeyString)
190190+ const privateKeyBytes = match.privateKeyBytes
183191184184- switch (match.type) {
185185- case 'p256': {
186186- keypair = await P256PrivateKey.importRaw(privateKeyBytes);
187187- console.log(keypair);
188188- break;
189189- }
190190- case 'secp256k1': {
191191- keypair = await Secp256k1PrivateKey.importRaw(privateKeyBytes);
192192- break;
193193- }
194194- default: {
195195- throw new Error(`unsupported "${type}" type`);
196196- }
197197- }
198198- } else {
199199- throw new Error('unknown input format');
192192+ switch (match.type) {
193193+ case 'p256': {
194194+ keypair = await P256PrivateKey.importRaw(privateKeyBytes)
195195+ console.log(keypair)
196196+ break
200197 }
201201- return {
202202- type: 'private_key',
203203- didPublicKey: await keypair.exportPublicKey('did'),
204204- keypair: keypair,
205205- };
198198+ case 'secp256k1': {
199199+ keypair = await Secp256k1PrivateKey.importRaw(privateKeyBytes)
200200+ break
201201+ }
202202+ default: {
203203+ throw new Error(`unsupported "${type}" type`)
204204+ }
205205+ }
206206+ } else {
207207+ throw new Error('unknown input format')
206208 }
207207-208208- /**
209209- * Submits a new operation to the plc directory
210210- * @param did {string} - The user's did
211211- * @param operation
212212- * @returns {Promise<void>}
213213- */
214214- async pushPlcOperation(did, operation) {
215215- const response = await fetch(`${this.plcDirectoryUrl}/${did}`, {
216216- method: 'post',
217217- headers: {
218218- 'content-type': 'application/json',
219219- },
220220- body: JSON.stringify(operation),
221221- });
209209+ return {
210210+ type: 'private_key',
211211+ didPublicKey: await keypair.exportPublicKey('did'),
212212+ keypair: keypair,
213213+ }
214214+ }
222215223223- const headers = response.headers;
224224- if (!response.ok) {
225225- const type = headers.get('content-type');
216216+ /**
217217+ * Submits a new operation to the plc directory
218218+ * @param did {string} - The user's did
219219+ * @param operation
220220+ * @returns {Promise<void>}
221221+ */
222222+ async pushPlcOperation(did, operation) {
223223+ const response = await fetch(`${this.plcDirectoryUrl}/${did}`, {
224224+ method: 'post',
225225+ headers: {
226226+ 'content-type': 'application/json',
227227+ },
228228+ body: JSON.stringify(operation),
229229+ })
226230227227- if (type?.includes('application/json')) {
228228- const json = await response.json();
229229- if (typeof json === 'object' && json !== null && typeof json.message === 'string') {
230230- throw new Error(json.message);
231231- }
232232- }
231231+ const headers = response.headers
232232+ if (!response.ok) {
233233+ const type = headers.get('content-type')
233234234234- throw new Error(`got http ${response.status} from plc`);
235235+ if (type?.includes('application/json')) {
236236+ const json = await response.json()
237237+ if (typeof json === 'object' && json !== null && typeof json.message === 'string') {
238238+ throw new Error(json.message)
235239 }
236236- };
237237-238238-239239- /**
240240- * Creates a new service auth token for a user. This is what is used to create a new account on a PDS for your did
241241- *
242242- * @param iss The user's did
243243- * @param aud The did:web, if it's a PDS it's usually from /xrpc/com.atproto.server.describeServer
244244- * @param keypair The keypair to sign with only supporting ES256K atm
245245- * @param lxm The lxm which is usually com.atproto.server.createAccount for creating a new account
246246- * @returns {Promise<string>}
247247- */
248248- async createANewServiceAuthToken(iss, aud, keypair, lxm) {
249249-250250-251251- // Compute iat/exp defaults (60s window like reference: MINUTE/1e3)
252252- const iat = Math.floor(Date.now() / 1e3);
253253- const exp = iat + 60;
254254-255255- // Generate a 16-byte hex jti
256256- const jti = (() => {
257257- const bytes = new Uint8Array(16);
258258- // crypto in browser or node; fall back safely
259259- (globalThis.crypto || window.crypto).getRandomValues(bytes);
260260- return Array.from(bytes).map(b => b.toString(16).padStart(2, '0')).join('');
261261- })();
240240+ }
262241242242+ throw new Error(`got http ${response.status} from plc`)
243243+ }
244244+ }
263245264264- // Build header and payload (omit undefined fields)
265265- // Just defaulting to ES256K since p256 was not importing on firefox
266266- const header = {typ: 'JWT', alg: 'ES256K'};
267267- const payload = {};
268268- payload.iat = iat;
269269- payload.iss = iss;
270270- payload.aud = aud;
271271- payload.exp = exp;
272272- payload.lxm = lxm;
273273- payload.jti = jti;
246246+ /**
247247+ * Creates a new service auth token for a user. This is what is used to create a new account on a PDS for your did
248248+ *
249249+ * @param iss The user's did
250250+ * @param aud The did:web, if it's a PDS it's usually from /xrpc/com.atproto.server.describeServer
251251+ * @param keypair The keypair to sign with only supporting ES256K atm
252252+ * @param lxm The lxm which is usually com.atproto.server.createAccount for creating a new account
253253+ * @returns {Promise<string>}
254254+ */
255255+ async createANewServiceAuthToken(iss, aud, keypair, lxm) {
256256+ // Compute iat/exp defaults (60s window like reference: MINUTE/1e3)
257257+ const iat = Math.floor(Date.now() / 1e3)
258258+ const exp = iat + 60
274259275275- const headerB64 = jsonToB64Url(header);
276276- const payloadB64 = jsonToB64Url(payload);
277277- const toSignStr = `${headerB64}.${payloadB64}`;
260260+ // Generate a 16-byte hex jti
261261+ const jti = (() => {
262262+ const bytes = new Uint8Array(16)
263263+ // crypto in browser or node; fall back safely
264264+ ;(globalThis.crypto || window.crypto).getRandomValues(bytes)
265265+ return Array.from(bytes)
266266+ .map(b => b.toString(16).padStart(2, '0'))
267267+ .join('')
268268+ })()
278269279279- // Sign
280280- const toSignBytes = new TextEncoder().encode(toSignStr);
281281- const sigBytes = await keypair.sign(toSignBytes);
270270+ // Build header and payload (omit undefined fields)
271271+ // Just defaulting to ES256K since p256 was not importing on firefox
272272+ const header = { typ: 'JWT', alg: 'ES256K' }
273273+ const payload = {}
274274+ payload.iat = iat
275275+ payload.iss = iss
276276+ payload.aud = aud
277277+ payload.exp = exp
278278+ payload.lxm = lxm
279279+ payload.jti = jti
282280283283- // Return compact JWS
284284- const sigB64 = toBase64Url(sigBytes);
285285- return `${toSignStr}.${sigB64}`;
286286- }
281281+ const headerB64 = jsonToB64Url(header)
282282+ const payloadB64 = jsonToB64Url(payload)
283283+ const toSignStr = `${headerB64}.${payloadB64}`
287284285285+ // Sign
286286+ const toSignBytes = new TextEncoder().encode(toSignStr)
287287+ const sigBytes = await keypair.sign(toSignBytes)
288288289289+ // Return compact JWS
290290+ const sigB64 = toBase64Url(sigBytes)
291291+ return `${toSignStr}.${sigB64}`
292292+ }
289293}
290294291291-292292-export {PlcOps};295295+export { PlcOps }
+283-281
packages/moover/lib/restore.js
···11/**
22 * @typedef {import('@atcute/did-plc').Operation} Operation
33 */
44-import {P256PrivateKey, Secp256k1PrivateKey} from '@atcute/crypto';
55-import {handleAndPDSResolver} from './atprotoUtils.js';
66-import {PlcOps} from './plc-ops.js';
77-import {normalizeOp} from '@atcute/did-plc';
88-import {AtpAgent} from '@atproto/api';
99-import {Secp256k1PrivateKeyExportable} from '@atcute/crypto';
1010-import * as CBOR from '@atcute/cbor';
1111-import {toBase64Url} from '@atcute/multibase';
44+import { P256PrivateKey, Secp256k1PrivateKey } from '@atcute/crypto'
55+import { handleAndPDSResolver } from './atprotoUtils.js'
66+import { PlcOps } from './plc-ops.js'
77+import { normalizeOp } from '@atcute/did-plc'
88+import { AtpAgent } from '@atproto/api'
99+import { Secp256k1PrivateKeyExportable } from '@atcute/crypto'
1010+import * as CBOR from '@atcute/cbor'
1111+import { toBase64Url } from '@atcute/multibase'
12121313class Restore {
1414+ /**
1515+ *
1616+ * @param pdsMooverInstance {string} - The url of the pds moover instance to restore from. Defaults to https://pdsmover.com
1717+ */
1818+ constructor(pdsMooverInstance = 'https://pdsmover.com') {
1919+ /**
2020+ * If you want to use a different plc directory create your own instance of the plc ops class and pass it in here
2121+ * @type {PlcOps} */
2222+ this.plcOps = new PlcOps()
14231524 /**
1616- *
1717- * @param pdsMooverInstance {string} - The url of the pds moover instance to restore from. Defaults to https://pdsmover.com
2525+ * This is the base url for the pds moover instance used to restore the files from a backup.
2626+ * @type {string}
1827 */
1919- constructor(pdsMooverInstance = 'https://pdsmover.com') {
2020- /**
2121- * If you want to use a different plc directory create your own instance of the plc ops class and pass it in here
2222- * @type {PlcOps} */
2323- this.plcOps = new PlcOps();
2424-2525- /**
2626- * This is the base url for the pds moover instance used to restore the files from a backup.
2727- * @type {string}
2828- */
2929- this.pdsMooverInstance = pdsMooverInstance
3030-3131- /**
3232- * To keep it simple, only uses secp256k for the temp verification key that is used to create the new account on the new PDS
3333- * and is temporarily assigned to the user's account on PLC
3434- * @type {null|Secp256k1PrivateKeyExportable}
3535- */
3636- this.tempVerificationKeypair = null;
3737-3838- /** @type {AtpAgent} */
3939- this.atpAgent = null;
4040-4141- /**
4242- * The keypair that is used to sign the plc operation
4343- * @type {null|{type: string, didPublicKey: `did:key:${string}`, keypair: P256PrivateKey|Secp256k1PrivateKey}}
4444- */
4545- this.recoveryRotationKeyPair = null;
4646-4747- /**
4848- * If this is true we are just restoring the repo and blobs. Ideally for rerunning a restore process after account recovery
4949- * @type {boolean}
5050- */
5151- this.RestoreFromBackup = true;
5252-5353- /**
5454- * If set to true then it will do the account recovery. Writes a temp key to the did doc,
5555- * create a new account on the new pds, and then submit a new plc op for the pds to have control (finishes the migration, can always restore the backup later)
5656- * @type {boolean}
5757- */
5858- this.AccountRecovery = true;
5959- }
2828+ this.pdsMooverInstance = pdsMooverInstance
60296130 /**
6262- * Recovers an account with the users rotation key and restores the repo from a PDS MOOver backup
6363- * This method can fail, and the account was still recovered, it's best to check the PLC logs to see where an account stands before reruns
6464- * @param rotationKey {string} - The users private rotation key, can be a multi key or hex key
6565- * @param rotationKeyType {string} - The type of the key, secp256k1 or p256. Required if the key is in hex format, defaults to secp256k1
6666- * @param currentHandleOrDid {string} - The users current handle or did, if they don't have a DNS record it will have to be their did for success
6767- * @param newPDS {string} - The new PDS url, like https://coolnewpds.com
6868- * @param newHandle {string} - Can be the users DNS handle if it is already setup with their did, if not it's bob.mypds.com
6969- * @param newPassword {string} - The new password for the new account
7070- * @param newEmail {string} - The new email for the new account
7171- * @param inviteCode {string|null} - The invite code for the new PDS if it requires one
7272- * @param cidToRestoreTo {string|null} - The cid of the plc op to restore to, used mostly to revert a fraudulent plc op. Want to give it the last valid operations cid
7373- * @param onStatus {function|null} - A function that takes a string used to update the UI. Like (status) => console.log(status)
7474- * @returns {Promise<void>} If there is a failure during restoring the back up (after the status Success! Restoring your repo...) then your account is most likely
7575- * recovered and future runs need to have the RestoreFromBackup flag set to true and AccountRecovery set to false.
3131+ * To keep it simple, only uses secp256k for the temp verification key that is used to create the new account on the new PDS
3232+ * and is temporarily assigned to the user's account on PLC
3333+ * @type {null|Secp256k1PrivateKeyExportable}
7634 */
7777- async recover(
7878- rotationKey,
7979- rotationKeyType = 'secp256k1',
8080- currentHandleOrDid,
8181- newPDS,
8282- newHandle,
8383- newPassword,
8484- newEmail,
8585- inviteCode,
8686- cidToRestoreTo = null,
8787- onStatus = null) {
8888-8989- if (onStatus) onStatus('Resolving your handle...');
9090-9191- let {usersDid} = await handleAndPDSResolver(currentHandleOrDid);
9292-9393- if (onStatus) onStatus('Checking that the new PDS is an actual PDS (if the url is wrong, this takes a while to error out)');
9494- this.atpAgent = new AtpAgent({service: newPDS});
9595- const newHostDesc = await this.atpAgent.com.atproto.server.describeServer();
9696-9797-9898- //Check to see if the user already has a repo on the new PDS, if they do no reason to try and restore via the plc operations
9999- try {
100100- await this.atpAgent.com.atproto.repo.describeRepo({repo: usersDid.toString()});
101101- //If we got this far and there is a repo on the new PDS with the users did, we can just move on and restore the files.
102102- //We do not want to mess with the plc ops if we dont have to
103103- this.AccountRecovery = false;
3535+ this.tempVerificationKeypair = null
10436105105- } catch (error) {
106106- console.error(error);
107107- let parsedError = error.error;
108108- if (parsedError === 'RepoDeactivated') {
109109- //Ideally should mean they already have a repo on the new PDS and we just need to restore the files
110110- this.AccountRecovery = false;
111111- }
112112- //This is the error we want to see, anything else throw
113113- if (parsedError !== 'RepoNotFound') {
114114- throw error;
115115- }
116116- }
117117-118118- //We need to double check that the new handle has not been taken, if it has we need to throw an error
119119- //We care a bit more because we do not want any unnecessary plc ops to be created
120120- try {
121121- let resolveHandle = await this.atpAgent.com.atproto.identity.resolveHandle({handle: newHandle});
122122- if (resolveHandle.data.did === usersDid.toString()) {
123123- //This was originally setting the AccountRecovery to false, which works if it is resolved via .well-known, but not dns
124124- //The idea was to check and see if the handle has been taken. just leaving for now since it does that check and if the user owns the handle
125125- //their did should be set anyhow
126126-127127- } else {
128128- //There is a repo with that name and it's not the users did,
129129- throw new Error('The new handle is already taken, please select a different handle');
130130- }
131131- } catch (error) {
132132- // Going to silently log this and just assume the handle has not been taken.
133133- console.error(error);
134134- if (error.message.startsWith('The new handle')) {
135135- //it's not our custom error, so we can just throw it
136136- throw error;
137137- }
3737+ /** @type {AtpAgent} */
3838+ this.atpAgent = null
13839139139- }
4040+ /**
4141+ * The keypair that is used to sign the plc operation
4242+ * @type {null|{type: string, didPublicKey: `did:key:${string}`, keypair: P256PrivateKey|Secp256k1PrivateKey}}
4343+ */
4444+ this.recoveryRotationKeyPair = null
14045141141- if (this.AccountRecovery) {
4646+ /**
4747+ * If this is true we are just restoring the repo and blobs. Ideally for rerunning a restore process after account recovery
4848+ * @type {boolean}
4949+ */
5050+ this.RestoreFromBackup = true
14251143143- if (onStatus) onStatus('Validating your private rotation key is in the correct format...');
5252+ /**
5353+ * If set to true then it will do the account recovery. Writes a temp key to the did doc,
5454+ * create a new account on the new pds, and then submit a new plc op for the pds to have control (finishes the migration, can always restore the backup later)
5555+ * @type {boolean}
5656+ */
5757+ this.AccountRecovery = true
5858+ }
14459145145- this.recoveryRotationKeyPair = await this.plcOps.getKeyPair(rotationKey, rotationKeyType);
6060+ /**
6161+ * Recovers an account with the users rotation key and restores the repo from a PDS MOOver backup
6262+ * This method can fail, and the account was still recovered, it's best to check the PLC logs to see where an account stands before reruns
6363+ * @param rotationKey {string} - The users private rotation key, can be a multi key or hex key
6464+ * @param rotationKeyType {string} - The type of the key, secp256k1 or p256. Required if the key is in hex format, defaults to secp256k1
6565+ * @param currentHandleOrDid {string} - The users current handle or did, if they don't have a DNS record it will have to be their did for success
6666+ * @param newPDS {string} - The new PDS url, like https://coolnewpds.com
6767+ * @param newHandle {string} - Can be the users DNS handle if it is already setup with their did, if not it's bob.mypds.com
6868+ * @param newPassword {string} - The new password for the new account
6969+ * @param newEmail {string} - The new email for the new account
7070+ * @param inviteCode {string|null} - The invite code for the new PDS if it requires one
7171+ * @param cidToRestoreTo {string|null} - The cid of the plc op to restore to, used mostly to revert a fraudulent plc op. Want to give it the last valid operations cid
7272+ * @param onStatus {function|null} - A function that takes a string used to update the UI. Like (status) => console.log(status)
7373+ * @returns {Promise<void>} If there is a failure during restoring the back up (after the status Success! Restoring your repo...) then your account is most likely
7474+ * recovered and future runs need to have the RestoreFromBackup flag set to true and AccountRecovery set to false.
7575+ */
7676+ async recover(
7777+ rotationKey,
7878+ rotationKeyType = 'secp256k1',
7979+ currentHandleOrDid,
8080+ newPDS,
8181+ newHandle,
8282+ newPassword,
8383+ newEmail,
8484+ inviteCode,
8585+ cidToRestoreTo = null,
8686+ onStatus = null,
8787+ ) {
8888+ if (onStatus) onStatus('Resolving your handle...')
146899090+ let { usersDid } = await handleAndPDSResolver(currentHandleOrDid)
14791148148- if (onStatus) onStatus('Resolving PlC operation logs...');
9292+ if (onStatus)
9393+ onStatus(
9494+ 'Checking that the new PDS is an actual PDS (if the url is wrong, this takes a while to error out)',
9595+ )
9696+ this.atpAgent = new AtpAgent({ service: newPDS })
9797+ const newHostDesc = await this.atpAgent.com.atproto.server.describeServer()
14998150150- /** @type {Operation} */
151151- let baseOpForSigning = null;
152152- let opPrevCid = null;
9999+ //Check to see if the user already has a repo on the new PDS, if they do no reason to try and restore via the plc operations
100100+ try {
101101+ await this.atpAgent.com.atproto.repo.describeRepo({ repo: usersDid.toString() })
102102+ //If we got this far and there is a repo on the new PDS with the users did, we can just move on and restore the files.
103103+ //We do not want to mess with the plc ops if we dont have to
104104+ this.AccountRecovery = false
105105+ } catch (error) {
106106+ console.error(error)
107107+ let parsedError = error.error
108108+ if (parsedError === 'RepoDeactivated') {
109109+ //Ideally should mean they already have a repo on the new PDS and we just need to restore the files
110110+ this.AccountRecovery = false
111111+ }
112112+ //This is the error we want to see, anything else throw
113113+ if (parsedError !== 'RepoNotFound') {
114114+ throw error
115115+ }
116116+ }
153117154154- //This is for reversals against a rogue plc op and you want to restore to a specific cid in the audit log
155155- if (cidToRestoreTo) {
156156- let auditLogs = await this.plcOps.getPlcAuditLogs(usersDid);
157157- for (const log of auditLogs) {
158158- if (log.cid === cidToRestoreTo) {
159159- baseOpForSigning = normalizeOp(log.operation);
160160- opPrevCid = log.cid;
161161- break;
162162- }
163163- }
164164- if (!baseOpForSigning) {
165165- throw new Error('Could not find the cid in the audit logs');
166166- }
167167- } else {
168168- let {lastOperation, base} = await this.plcOps.getLastPlcOpFromPlc(usersDid);
169169- opPrevCid = base.cid;
170170- baseOpForSigning = lastOperation;
171171- }
118118+ //We need to double check that the new handle has not been taken, if it has we need to throw an error
119119+ //We care a bit more because we do not want any unnecessary plc ops to be created
120120+ try {
121121+ let resolveHandle = await this.atpAgent.com.atproto.identity.resolveHandle({
122122+ handle: newHandle,
123123+ })
124124+ if (resolveHandle.data.did === usersDid.toString()) {
125125+ //This was originally setting the AccountRecovery to false, which works if it is resolved via .well-known, but not dns
126126+ //The idea was to check and see if the handle has been taken. just leaving for now since it does that check and if the user owns the handle
127127+ //their did should be set anyhow
128128+ } else {
129129+ //There is a repo with that name and it's not the users did,
130130+ throw new Error('The new handle is already taken, please select a different handle')
131131+ }
132132+ } catch (error) {
133133+ // Going to silently log this and just assume the handle has not been taken.
134134+ console.error(error)
135135+ if (error.message.startsWith('The new handle')) {
136136+ //it's not our custom error, so we can just throw it
137137+ throw error
138138+ }
139139+ }
172140173173- if (onStatus) onStatus('Preparing to switch to a temp atproto key...');
174174- if (this.tempVerificationKeypair == null) {
175175- if (onStatus) onStatus('Creating a new temp atproto key...');
176176- this.tempVerificationKeypair = await Secp256k1PrivateKeyExportable.createKeypair();
177177- }
178178- //Just defaulting to the user's recovery key for now. Advance cases will be something else
179179- //Maybe just a new ui to edit the PLC doc in a limited capacity, but sinc ethis is a temp plc op i don't think it's needed
180180- let tempRotationKeys = [this.recoveryRotationKeyPair.didPublicKey];
141141+ if (this.AccountRecovery) {
142142+ if (onStatus) onStatus('Validating your private rotation key is in the correct format...')
181143182182- if (onStatus) onStatus('Modifying the PLC OP for recovery...');
183183- //A temp plc op for control of the atproto key to create a serviceAuth and new account on the new PDS
184184- await this.plcOps.signAndPublishNewOp(
185185- usersDid,
186186- this.recoveryRotationKeyPair.keypair,
187187- baseOpForSigning.alsoKnownAs,
188188- tempRotationKeys,
189189- newPDS,
190190- await this.tempVerificationKeypair.exportPublicKey('did'),
191191- opPrevCid);
144144+ this.recoveryRotationKeyPair = await this.plcOps.getKeyPair(rotationKey, rotationKeyType)
192145146146+ if (onStatus) onStatus('Resolving PlC operation logs...')
193147194194- if (onStatus) onStatus('Creating your new account on the new PDS...');
195195- let serviceAuthToken = await this.plcOps.createANewServiceAuthToken(usersDid, newHostDesc.data.did, this.tempVerificationKeypair, 'com.atproto.server.createAccount');
148148+ /** @type {Operation} */
149149+ let baseOpForSigning = null
150150+ let opPrevCid = null
196151197197- let createAccountRequest = {
198198- did: usersDid,
199199- handle: newHandle,
200200- email: newEmail,
201201- password: newPassword,
202202- };
203203- if (inviteCode) {
204204- createAccountRequest.inviteCode = inviteCode;
205205- }
206206- const _ = await this.atpAgent.com.atproto.server.createAccount(
207207- createAccountRequest,
208208- {
209209- headers: {authorization: `Bearer ${serviceAuthToken}`},
210210- encoding: 'application/json',
211211- });
152152+ //This is for reversals against a rogue plc op and you want to restore to a specific cid in the audit log
153153+ if (cidToRestoreTo) {
154154+ let auditLogs = await this.plcOps.getPlcAuditLogs(usersDid)
155155+ for (const log of auditLogs) {
156156+ if (log.cid === cidToRestoreTo) {
157157+ baseOpForSigning = normalizeOp(log.operation)
158158+ opPrevCid = log.cid
159159+ break
160160+ }
212161 }
213213-214214- await this.atpAgent.login({
215215- identifier: usersDid,
216216- password: newPassword,
217217- });
218218-219219- if (this.AccountRecovery) {
220220- //Moving the user offically to the new PDS
221221- if (onStatus) onStatus('Signing the papers...');
222222- let {base} = await this.plcOps.getLastPlcOpFromPlc(usersDid);
223223- await this.signRestorePlcOperation(usersDid, [this.recoveryRotationKeyPair.didPublicKey], base.cid);
162162+ if (!baseOpForSigning) {
163163+ throw new Error('Could not find the cid in the audit logs')
224164 }
165165+ } else {
166166+ let { lastOperation, base } = await this.plcOps.getLastPlcOpFromPlc(usersDid)
167167+ opPrevCid = base.cid
168168+ baseOpForSigning = lastOperation
169169+ }
225170226226- if (this.RestoreFromBackup) {
227227- if (onStatus) onStatus('Success! Restoring your repo...');
228228- const pdsMoover = new AtpAgent({service: this.pdsMooverInstance});
229229- const repoRes = await pdsMoover.com.atproto.sync.getRepo({did: usersDid});
230230- await this.atpAgent.com.atproto.repo.importRepo(repoRes.data, {
231231- encoding: 'application/vnd.ipld.car',
232232- });
171171+ if (onStatus) onStatus('Preparing to switch to a temp atproto key...')
172172+ if (this.tempVerificationKeypair == null) {
173173+ if (onStatus) onStatus('Creating a new temp atproto key...')
174174+ this.tempVerificationKeypair = await Secp256k1PrivateKeyExportable.createKeypair()
175175+ }
176176+ //Just defaulting to the user's recovery key for now. Advance cases will be something else
177177+ //Maybe just a new ui to edit the PLC doc in a limited capacity, but sinc ethis is a temp plc op i don't think it's needed
178178+ let tempRotationKeys = [this.recoveryRotationKeyPair.didPublicKey]
233179234234- if (onStatus) onStatus('Restoring your blobs...');
180180+ if (onStatus) onStatus('Modifying the PLC OP for recovery...')
181181+ //A temp plc op for control of the atproto key to create a serviceAuth and new account on the new PDS
182182+ await this.plcOps.signAndPublishNewOp(
183183+ usersDid,
184184+ this.recoveryRotationKeyPair.keypair,
185185+ baseOpForSigning.alsoKnownAs,
186186+ tempRotationKeys,
187187+ newPDS,
188188+ await this.tempVerificationKeypair.exportPublicKey('did'),
189189+ opPrevCid,
190190+ )
235191236236- //Using the missing endpoint to findout what's missing then the PDS MOOver endpoint to restore
237237- let totalMissingBlobs = 0;
238238- let missingBlobCursor = undefined;
239239- let missingUploadedBlobs = 0;
192192+ if (onStatus) onStatus('Creating your new account on the new PDS...')
193193+ let serviceAuthToken = await this.plcOps.createANewServiceAuthToken(
194194+ usersDid,
195195+ newHostDesc.data.did,
196196+ this.tempVerificationKeypair,
197197+ 'com.atproto.server.createAccount',
198198+ )
240199241241- do {
242242-243243- const missingBlobs = await this.atpAgent.com.atproto.repo.listMissingBlobs({
244244- cursor: missingBlobCursor,
245245- limit: 1000,
246246- });
247247- totalMissingBlobs += missingBlobs.data.blobs.length;
248248-249249- for (const recordBlob of missingBlobs.data.blobs) {
250250- try {
200200+ let createAccountRequest = {
201201+ did: usersDid,
202202+ handle: newHandle,
203203+ email: newEmail,
204204+ password: newPassword,
205205+ }
206206+ if (inviteCode) {
207207+ createAccountRequest.inviteCode = inviteCode
208208+ }
209209+ const _ = await this.atpAgent.com.atproto.server.createAccount(createAccountRequest, {
210210+ headers: { authorization: `Bearer ${serviceAuthToken}` },
211211+ encoding: 'application/json',
212212+ })
213213+ }
251214252252- const blobRes = await pdsMoover.com.atproto.sync.getBlob({
253253- did: usersDid,
254254- cid: recordBlob.cid,
255255- });
256256- let result = await this.atpAgent.com.atproto.repo.uploadBlob(blobRes.data, {
257257- encoding: blobRes.headers['content-type'],
258258- });
215215+ await this.atpAgent.login({
216216+ identifier: usersDid,
217217+ password: newPassword,
218218+ })
259219220220+ if (this.AccountRecovery) {
221221+ //Moving the user offically to the new PDS
222222+ if (onStatus) onStatus('Signing the papers...')
223223+ let { base } = await this.plcOps.getLastPlcOpFromPlc(usersDid)
224224+ await this.signRestorePlcOperation(
225225+ usersDid,
226226+ [this.recoveryRotationKeyPair.didPublicKey],
227227+ base.cid,
228228+ )
229229+ }
260230261261- if (missingUploadedBlobs % 2 === 0) {
262262- if (onStatus) onStatus(`Migrating blobs: ${missingUploadedBlobs}/${totalMissingBlobs} (The total may increase as we find more)`);
263263- }
264264- missingUploadedBlobs++;
265265- } catch (error) {
266266- console.error(error);
267267- }
268268- }
269269- missingBlobCursor = missingBlobs.data.cursor;
270270- } while (missingBlobCursor);
271271- }
272272- const accountStatus = await this.atpAgent.com.atproto.server.checkAccountStatus();
273273- if (!accountStatus.data.activated) {
274274- if (onStatus) onStatus('Activating your account...');
275275- await this.atpAgent.com.atproto.server.activateAccount();
276276- }
231231+ if (this.RestoreFromBackup) {
232232+ if (onStatus) onStatus('Success! Restoring your repo...')
233233+ const pdsMoover = new AtpAgent({ service: this.pdsMooverInstance })
234234+ const repoRes = await pdsMoover.com.atproto.sync.getRepo({ did: usersDid })
235235+ await this.atpAgent.com.atproto.repo.importRepo(repoRes.data, {
236236+ encoding: 'application/vnd.ipld.car',
237237+ })
277238278278- }
239239+ if (onStatus) onStatus('Restoring your blobs...')
279240241241+ //Using the missing endpoint to findout what's missing then the PDS MOOver endpoint to restore
242242+ let totalMissingBlobs = 0
243243+ let missingBlobCursor = undefined
244244+ let missingUploadedBlobs = 0
280245281281- /**
282282- * This method signs the plc operation over to the new PDS and activates the account
283283- * Assumes you have already created a new account during the recovery process and logged in
284284- * Uses the recommended did doc from the PDS as a base and adds the users rotation key to the rotation keys array
285285- *
286286- * @param usersDid
287287- * @param additionalRotationKeysToAdd
288288- * @param prevCid
289289- * @returns {Promise<void>}
290290- */
291291- async signRestorePlcOperation(usersDid, additionalRotationKeysToAdd = [], prevCid) {
292292- const getDidCredentials =
293293- await this.atpAgent.com.atproto.identity.getRecommendedDidCredentials();
246246+ do {
247247+ const missingBlobs = await this.atpAgent.com.atproto.repo.listMissingBlobs({
248248+ cursor: missingBlobCursor,
249249+ limit: 1000,
250250+ })
251251+ totalMissingBlobs += missingBlobs.data.blobs.length
294252295295- const pdsProvidedRotationKeys = getDidCredentials.data.rotationKeys ?? [];
296296- //Puts the provided rotation keys above the pds pro
297297- const rotationKeys = [...new Set([...(additionalRotationKeysToAdd || []), ...pdsProvidedRotationKeys])];
298298- if (!rotationKeys) {
299299- throw new Error('No rotation keys were found to be added to the PLC');
300300- }
253253+ for (const recordBlob of missingBlobs.data.blobs) {
254254+ try {
255255+ const blobRes = await pdsMoover.com.atproto.sync.getBlob({
256256+ did: usersDid,
257257+ cid: recordBlob.cid,
258258+ })
259259+ let result = await this.atpAgent.com.atproto.repo.uploadBlob(blobRes.data, {
260260+ encoding: blobRes.headers['content-type'],
261261+ })
301262302302- if (rotationKeys.length > 5) {
303303- throw new Error('You can only add up to 5 rotation keys to the PLC');
263263+ if (missingUploadedBlobs % 2 === 0) {
264264+ if (onStatus)
265265+ onStatus(
266266+ `Migrating blobs: ${missingUploadedBlobs}/${totalMissingBlobs} (The total may increase as we find more)`,
267267+ )
268268+ }
269269+ missingUploadedBlobs++
270270+ } catch (error) {
271271+ console.error(error)
272272+ }
304273 }
274274+ missingBlobCursor = missingBlobs.data.cursor
275275+ } while (missingBlobCursor)
276276+ }
277277+ const accountStatus = await this.atpAgent.com.atproto.server.checkAccountStatus()
278278+ if (!accountStatus.data.activated) {
279279+ if (onStatus) onStatus('Activating your account...')
280280+ await this.atpAgent.com.atproto.server.activateAccount()
281281+ }
282282+ }
305283284284+ /**
285285+ * This method signs the plc operation over to the new PDS and activates the account
286286+ * Assumes you have already created a new account during the recovery process and logged in
287287+ * Uses the recommended did doc from the PDS as a base and adds the users rotation key to the rotation keys array
288288+ *
289289+ * @param usersDid
290290+ * @param additionalRotationKeysToAdd
291291+ * @param prevCid
292292+ * @returns {Promise<void>}
293293+ */
294294+ async signRestorePlcOperation(usersDid, additionalRotationKeysToAdd = [], prevCid) {
295295+ const getDidCredentials =
296296+ await this.atpAgent.com.atproto.identity.getRecommendedDidCredentials()
306297307307- const plcOpToSubmit = {
308308- type: 'plc_operation',
309309- ...getDidCredentials.data,
310310- prev: prevCid,
311311- rotationKeys: rotationKeys,
312312- };
313313-298298+ const pdsProvidedRotationKeys = getDidCredentials.data.rotationKeys ?? []
299299+ //Puts the provided rotation keys above the pds pro
300300+ const rotationKeys = [
301301+ ...new Set([...(additionalRotationKeysToAdd || []), ...pdsProvidedRotationKeys]),
302302+ ]
303303+ if (!rotationKeys) {
304304+ throw new Error('No rotation keys were found to be added to the PLC')
305305+ }
314306315315- const opBytes = CBOR.encode(plcOpToSubmit);
316316- const sigBytes = await this.recoveryRotationKeyPair.keypair.sign(opBytes);
307307+ if (rotationKeys.length > 5) {
308308+ throw new Error('You can only add up to 5 rotation keys to the PLC')
309309+ }
317310318318- const signature = toBase64Url(sigBytes);
311311+ const plcOpToSubmit = {
312312+ type: 'plc_operation',
313313+ ...getDidCredentials.data,
314314+ prev: prevCid,
315315+ rotationKeys: rotationKeys,
316316+ }
319317320320- const signedOperation = {
321321- ...plcOpToSubmit,
322322- sig: signature,
323323- };
318318+ const opBytes = CBOR.encode(plcOpToSubmit)
319319+ const sigBytes = await this.recoveryRotationKeyPair.keypair.sign(opBytes)
324320325325- await this.plcOps.pushPlcOperation(usersDid, signedOperation);
326326- await this.atpAgent.com.atproto.server.activateAccount();
321321+ const signature = toBase64Url(sigBytes)
327322323323+ const signedOperation = {
324324+ ...plcOpToSubmit,
325325+ sig: signature,
328326 }
327327+328328+ await this.plcOps.pushPlcOperation(usersDid, signedOperation)
329329+ await this.atpAgent.com.atproto.server.activateAccount()
330330+ }
329331}
330332331331-export {Restore};333333+export { Restore }
···11-export const handleResolver: CompositeHandleResolver;
22-export const docResolver: CompositeDidDocumentResolver<"plc" | "web">;
11+export const handleResolver: CompositeHandleResolver
22+export const docResolver: CompositeDidDocumentResolver<'plc' | 'web'>
33/**
44 * Cleans the handle of @ and some other unicode characters that used to show up when copied from the profile
55 * @param handle {string}
66 * @returns {string}
77 */
88-export function cleanHandle(handle: string): string;
88+export function cleanHandle(handle: string): string
99/**
1010 * Convince helper to resolve a handle to a did and then find the PDS url from the did document.
1111 *
···1313 * @returns {Promise<{usersDid: string, pds: string}>}
1414 */
1515export function handleAndPDSResolver(handle: any): Promise<{
1616- usersDid: string;
1717- pds: string;
1818-}>;
1616+ usersDid: string
1717+ pds: string
1818+}>
1919/**
2020 * Fetches the DID Web from the .well-known/did.json endpoint of the server.
2121 * Legacy and was helpful if the web ui and server are on the same domain, not as useful now
2222 * @param baseUrl
2323 * @returns {Promise<*>}
2424 */
2525-export function fetchPDSMooverDIDWeb(baseUrl: any): Promise<any>;
2626-import { CompositeHandleResolver } from '@atcute/identity-resolver';
2727-import { CompositeDidDocumentResolver } from '@atcute/identity-resolver';
2828-//# sourceMappingURL=atprotoUtils.d.ts.map2525+export function fetchPDSMooverDIDWeb(baseUrl: any): Promise<any>
2626+import { CompositeHandleResolver } from '@atcute/identity-resolver'
2727+import { CompositeDidDocumentResolver } from '@atcute/identity-resolver'
2828+//# sourceMappingURL=atprotoUtils.d.ts.map
+89-82
packages/moover/types/backup.d.ts
···11/**
22 * JSDoc type-only import to avoid runtime import errors in the browser.
33 */
44-export type InferXRPCBodyOutput = any;
44+export type InferXRPCBodyOutput = any
55/**
66 * JSDoc type-only import to avoid runtime import errors in the browser.
77 * @typedef {import('@atcute/lexicons').InferXRPCBodyOutput} InferXRPCBodyOutput
···1010 * Logic to sign up and manage backups for pdsmoover.com (or your own selfhosted instance)
1111 */
1212export class BackupService {
1313- /**
1414- *
1515- * @param backupDidWeb {string} - The did:web for the xrpc service for backups, defaults to did:web:pdsmoover.com
1616- */
1717- constructor(backupDidWeb?: string);
1818- /**
1919- *
2020- * @type {Client}
2121- */
2222- atCuteClient: Client;
2323- /**
2424- *
2525- * @type {CredentialManager}
2626- */
2727- atCuteCredentialManager: CredentialManager;
2828- /**
2929- * The did:web for the xrpc service for backups, defaults to pdsmoover.com
3030- * @type {string}
3131- */
3232- backupDidWeb: string;
3333- /**
3434- * Logs in and returns the backup status.
3535- * To use the rest of the BackupService, it is assumed that this has ran first,
3636- * and the user has successfully signed up. A successful login is a returned null if the user has not signed up.
3737- * or the backup status if they are
3838- *
3939- * If the server requires 2FA,
4040- * it will throw with error.error === 'AuthFactorTokenRequired'.
4141- * @param identifier {string} handle or did
4242- * @param password {string}
4343- * @param {function|null} onStatus - a function that takes a string used to update the UI.
4444- * Like (status) => console.log(status)
4545- * @param twoFactorCode {string|null}
4646- *
4747- * @returns {Promise<InferXRPCBodyOutput<ComPdsmooverBackupDescribeServer.mainSchema['output']>|null>}
4848- */
4949- loginAndStatus(identifier: string, password: string, onStatus?: Function | null, twoFactorCode?: string | null): Promise<InferXRPCBodyOutput<ComPdsmooverBackupDescribeServer.mainSchema["output"]> | null>;
5050- /**
5151- * Signs the user up for backups with the service
5252- * @param onStatus
5353- * @returns {Promise<void>}
5454- */
5555- signUp(onStatus?: any): Promise<void>;
5656- /**
5757- * Requests a PLC token to be sent to the user's email, needed to add a new rotation key
5858- * @returns {Promise<void>}
5959- */
6060- requestAPlcToken(): Promise<void>;
6161- /**
6262- * Adds a new rotation to the users did document. Assumes you are already signed in.
6363- *
6464- * WARNING: This will overwrite any existing rotation keys with the new one at the top, and the PDS key as the second one
6565- * @param plcToken {string} - PLC token from the user's email that was sent from requestAPlcToken
6666- * @param rotationKey {string} - The new rotation key to add to the user's did document
6767- * @returns {Promise<void>}
6868- */
6969- addANewRotationKey(plcToken: string, rotationKey: string): Promise<void>;
7070- /**
7171- *
7272- * Gets the current status of the user's backup repository.
7373- *
7474- * @param onStatus {function|null} - a function that takes a string used to update the UI.
7575- * @returns {Promise<InferXRPCBodyOutput<ComPdsmooverBackupDescribeServer.mainSchema['output']>>}
7676- */
7777- getUsersRepoStatus(onStatus?: Function | null): Promise<InferXRPCBodyOutput<ComPdsmooverBackupDescribeServer.mainSchema["output"]>>;
7878- /**
7979- * Requests a backup to be run immediately for the signed-in user. Usually does, depend on the server's backup queue
8080- * @param onStatus
8181- * @returns {Promise<boolean>}
8282- */
8383- runBackupNow(onStatus?: any): Promise<boolean>;
8484- /**
8585- * Remove (delete) the signed-in user's backup repository. this also deletes all the user's backup data.
8686- * @param onStatus
8787- * @returns {Promise<boolean>}
8888- */
8989- removeRepo(onStatus?: any): Promise<boolean>;
1313+ /**
1414+ *
1515+ * @param backupDidWeb {string} - The did:web for the xrpc service for backups, defaults to did:web:pdsmoover.com
1616+ */
1717+ constructor(backupDidWeb?: string)
1818+ /**
1919+ *
2020+ * @type {Client}
2121+ */
2222+ atCuteClient: Client
2323+ /**
2424+ *
2525+ * @type {CredentialManager}
2626+ */
2727+ atCuteCredentialManager: CredentialManager
2828+ /**
2929+ * The did:web for the xrpc service for backups, defaults to pdsmoover.com
3030+ * @type {string}
3131+ */
3232+ backupDidWeb: string
3333+ /**
3434+ * Logs in and returns the backup status.
3535+ * To use the rest of the BackupService, it is assumed that this has ran first,
3636+ * and the user has successfully signed up. A successful login is a returned null if the user has not signed up.
3737+ * or the backup status if they are
3838+ *
3939+ * If the server requires 2FA,
4040+ * it will throw with error.error === 'AuthFactorTokenRequired'.
4141+ * @param identifier {string} handle or did
4242+ * @param password {string}
4343+ * @param {function|null} onStatus - a function that takes a string used to update the UI.
4444+ * Like (status) => console.log(status)
4545+ * @param twoFactorCode {string|null}
4646+ *
4747+ * @returns {Promise<InferXRPCBodyOutput<ComPdsmooverBackupDescribeServer.mainSchema['output']>|null>}
4848+ */
4949+ loginAndStatus(
5050+ identifier: string,
5151+ password: string,
5252+ onStatus?: Function | null,
5353+ twoFactorCode?: string | null,
5454+ ): Promise<InferXRPCBodyOutput<ComPdsmooverBackupDescribeServer.mainSchema['output']> | null>
5555+ /**
5656+ * Signs the user up for backups with the service
5757+ * @param onStatus
5858+ * @returns {Promise<void>}
5959+ */
6060+ signUp(onStatus?: any): Promise<void>
6161+ /**
6262+ * Requests a PLC token to be sent to the user's email, needed to add a new rotation key
6363+ * @returns {Promise<void>}
6464+ */
6565+ requestAPlcToken(): Promise<void>
6666+ /**
6767+ * Adds a new rotation to the users did document. Assumes you are already signed in.
6868+ *
6969+ * WARNING: This will overwrite any existing rotation keys with the new one at the top, and the PDS key as the second one
7070+ * @param plcToken {string} - PLC token from the user's email that was sent from requestAPlcToken
7171+ * @param rotationKey {string} - The new rotation key to add to the user's did document
7272+ * @returns {Promise<void>}
7373+ */
7474+ addANewRotationKey(plcToken: string, rotationKey: string): Promise<void>
7575+ /**
7676+ *
7777+ * Gets the current status of the user's backup repository.
7878+ *
7979+ * @param onStatus {function|null} - a function that takes a string used to update the UI.
8080+ * @returns {Promise<InferXRPCBodyOutput<ComPdsmooverBackupDescribeServer.mainSchema['output']>>}
8181+ */
8282+ getUsersRepoStatus(
8383+ onStatus?: Function | null,
8484+ ): Promise<InferXRPCBodyOutput<ComPdsmooverBackupDescribeServer.mainSchema['output']>>
8585+ /**
8686+ * Requests a backup to be run immediately for the signed-in user. Usually does, depend on the server's backup queue
8787+ * @param onStatus
8888+ * @returns {Promise<boolean>}
8989+ */
9090+ runBackupNow(onStatus?: any): Promise<boolean>
9191+ /**
9292+ * Remove (delete) the signed-in user's backup repository. this also deletes all the user's backup data.
9393+ * @param onStatus
9494+ * @returns {Promise<boolean>}
9595+ */
9696+ removeRepo(onStatus?: any): Promise<boolean>
9097}
9191-import { Client } from '@atcute/client';
9292-import { CredentialManager } from '@atcute/client';
9393-import { ComPdsmooverBackupDescribeServer } from '@pds-moover/lexicons';
9494-//# sourceMappingURL=backup.d.ts.map9898+import { Client } from '@atcute/client'
9999+import { CredentialManager } from '@atcute/client'
100100+import { ComPdsmooverBackupDescribeServer } from '@pds-moover/lexicons'
101101+//# sourceMappingURL=backup.d.ts.map
+8-8
packages/moover/types/main.d.ts
···11-import { Migrator } from './pdsmoover.js';
22-import { MissingBlobs } from './missingBlobs.js';
33-import { BackupService } from './backup.js';
44-import { PlcOps } from './plc-ops.js';
55-import { Restore } from './restore.js';
66-import { handleAndPDSResolver } from './atprotoUtils.js';
77-export { Migrator, MissingBlobs, BackupService, PlcOps, Restore, handleAndPDSResolver };
88-//# sourceMappingURL=main.d.ts.map11+import { Migrator } from './pdsmoover.js'
22+import { MissingBlobs } from './missingBlobs.js'
33+import { BackupService } from './backup.js'
44+import { PlcOps } from './plc-ops.js'
55+import { Restore } from './restore.js'
66+import { handleAndPDSResolver } from './atprotoUtils.js'
77+export { Migrator, MissingBlobs, BackupService, PlcOps, Restore, handleAndPDSResolver }
88+//# sourceMappingURL=main.d.ts.map
+65-57
packages/moover/types/missingBlobs.d.ts
···22 * Class to help find missing blobs from the did's previous PDS and import them into the current PDS
33 */
44export class MissingBlobs {
55- /**
66- * The user's current PDS agent
77- * @type {AtpAgent}
88- */
99- currentPdsAgent: AtpAgent;
1010- /**
1111- * The user's old PDS agent
1212- * @type {AtpAgent}
1313- */
1414- oldPdsAgent: AtpAgent;
1515- /**
1616- * the user's did
1717- * @type {string|null}
1818- */
1919- did: string | null;
2020- /**
2121- * The user's current PDS url
2222- * @type {null}
2323- */
2424- currentPdsUrl: any;
2525- /**
2626- * A list of the missing cids blobs from the old PDS. In this case if a retry upload fails it gets put in this array for the ui
2727- * @type {string[]}
2828- */
2929- missingBlobs: string[];
3030- /**
3131- * Logs the user into the current PDS and gets the account status
3232- * @param handle {string}
3333- * @param password {string}
3434- * @param twoFactorCode {string|null}
3535- * @returns {Promise<{accountStatus: OutputSchema, missingBlobsCount: number}>}
3636- */
3737- currentAgentLogin(handle: string, password: string, twoFactorCode?: string | null): Promise<{
3838- accountStatus: OutputSchema;
3939- missingBlobsCount: number;
4040- }>;
4141- /**
4242- * Logs into the old PDS and gets the account status.
4343- * Does not need a handle
4444- * since it is assumed the user has already logged in with the current PDS and we are using their did
4545- * @param password {string}
4646- * @param twoFactorCode {string|null}
4747- * @param pdsUrl {string|null} - If you know the url of the old PDS you can pass it in here. If not it will be guessed at from plc ops
4848- * @returns {Promise<void>}
4949- */
5050- oldAgentLogin(password: string, twoFactorCode?: string | null, pdsUrl?: string | null): Promise<void>;
5151- /**
5252- * Gets the missing blobs from the old PDS and uploads them to the current PDS
5353- * @param statusUpdateHandler {function} - A function to update the status of the migration. This is useful for showing the user the progress of the migration
5454- * @returns {Promise<{accountStatus: OutputSchema, missingBlobsCount: number}>}
5555- */
5656- migrateMissingBlobs(statusUpdateHandler: Function): Promise<{
5757- accountStatus: OutputSchema;
5858- missingBlobsCount: number;
5959- }>;
55+ /**
66+ * The user's current PDS agent
77+ * @type {AtpAgent}
88+ */
99+ currentPdsAgent: AtpAgent
1010+ /**
1111+ * The user's old PDS agent
1212+ * @type {AtpAgent}
1313+ */
1414+ oldPdsAgent: AtpAgent
1515+ /**
1616+ * the user's did
1717+ * @type {string|null}
1818+ */
1919+ did: string | null
2020+ /**
2121+ * The user's current PDS url
2222+ * @type {null}
2323+ */
2424+ currentPdsUrl: any
2525+ /**
2626+ * A list of the missing cids blobs from the old PDS. In this case if a retry upload fails it gets put in this array for the ui
2727+ * @type {string[]}
2828+ */
2929+ missingBlobs: string[]
3030+ /**
3131+ * Logs the user into the current PDS and gets the account status
3232+ * @param handle {string}
3333+ * @param password {string}
3434+ * @param twoFactorCode {string|null}
3535+ * @returns {Promise<{accountStatus: OutputSchema, missingBlobsCount: number}>}
3636+ */
3737+ currentAgentLogin(
3838+ handle: string,
3939+ password: string,
4040+ twoFactorCode?: string | null,
4141+ ): Promise<{
4242+ accountStatus: OutputSchema
4343+ missingBlobsCount: number
4444+ }>
4545+ /**
4646+ * Logs into the old PDS and gets the account status.
4747+ * Does not need a handle
4848+ * since it is assumed the user has already logged in with the current PDS and we are using their did
4949+ * @param password {string}
5050+ * @param twoFactorCode {string|null}
5151+ * @param pdsUrl {string|null} - If you know the url of the old PDS you can pass it in here. If not it will be guessed at from plc ops
5252+ * @returns {Promise<void>}
5353+ */
5454+ oldAgentLogin(
5555+ password: string,
5656+ twoFactorCode?: string | null,
5757+ pdsUrl?: string | null,
5858+ ): Promise<void>
5959+ /**
6060+ * Gets the missing blobs from the old PDS and uploads them to the current PDS
6161+ * @param statusUpdateHandler {function} - A function to update the status of the migration. This is useful for showing the user the progress of the migration
6262+ * @returns {Promise<{accountStatus: OutputSchema, missingBlobsCount: number}>}
6363+ */
6464+ migrateMissingBlobs(statusUpdateHandler: Function): Promise<{
6565+ accountStatus: OutputSchema
6666+ missingBlobsCount: number
6767+ }>
6068}
6161-import { AtpAgent } from '@atproto/api';
6262-//# sourceMappingURL=missingBlobs.d.ts.map6969+import { AtpAgent } from '@atproto/api'
7070+//# sourceMappingURL=missingBlobs.d.ts.map
+79-64
packages/moover/types/pdsmoover.d.ts
···33 * On pdsmoover.com this is the logic for the MOOver
44 */
55export class Migrator {
66- /** @type {AtpAgent} */
77- oldAgent: AtpAgent;
88- /** @type {AtpAgent} */
99- newAgent: AtpAgent;
1010- /** @type {[string]} */
1111- missingBlobs: [string];
1212- /** @type {boolean} */
1313- createNewAccount: boolean;
1414- /** @type {boolean} */
1515- migrateRepo: boolean;
1616- /** @type {boolean} */
1717- migrateBlobs: boolean;
1818- /** @type {boolean} */
1919- migrateMissingBlobs: boolean;
2020- /** @type {boolean} */
2121- migratePrefs: boolean;
2222- /** @type {boolean} */
2323- migratePlcRecord: boolean;
2424- /**
2525- * This migrator is pretty cut and dry and makes a few assumptions
2626- * 1. You are using the same password between each account
2727- * 2. If this command fails for something like oauth 2fa code it throws an error and expects the same values when ran again.
2828- * 3. You can control which "actions" happen by setting the class variables to false.
2929- * 4. Each instance of the class is assumed to be for a single migration
3030- * @param {string} oldHandle - The handle you use on your old pds, something like alice.bsky.social
3131- * @param {string} password - Your password for your current login. Has to be your real password, no app password. When setting up a new account we reuse it as well for that account
3232- * @param {string} newPdsUrl - The new URL for your pds. Like https://coolnewpds.com
3333- * @param {string} newEmail - The email you want to use on the new pds (can be the same as the previous one as long as it's not already being used on the new pds)
3434- * @param {string} newHandle - The new handle you want, like alice.bsky.social, or if you already have a domain name set as a handle can use it myname.com.
3535- * @param {string|null} inviteCode - The invite code you got from the PDS you are migrating to. If null does not include one
3636- * @param {function|null} statusUpdateHandler - a function that takes a string used to update the UI. Like (status) => console.log(status)
3737- * @param {string|null} twoFactorCode - Optional, but needed if it fails with 2fa required
3838- * @param verificationCode - Optional verification captcha code for account creation if the PDS requires it
3939- */
4040- migrate(oldHandle: string, password: string, newPdsUrl: string, newEmail: string, newHandle: string, inviteCode: string | null, statusUpdateHandler?: Function | null, twoFactorCode?: string | null, verificationCode?: any): Promise<void>;
4141- /**
4242- * Sign and submits the PLC operation to officially migrate the account
4343- * @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
4444- * @param additionalRotationKeysToAdd {string[]} - additional rotation keys to add in addition to the ones provided by the new PDS.
4545- * @returns {Promise<void>}
4646- */
4747- signPlcOperation(token: string, additionalRotationKeysToAdd?: string[]): Promise<void>;
4848- /**
4949- * Using this method assumes the Migrator class was constructed new and this was called.
5050- * Find the user's previous PDS from the PLC op logs,
5151- * logs in and deactivates their old account if it was found still active.
5252- *
5353- * @param oldHandle {string}
5454- * @param oldPassword {string}
5555- * @param {function|null} statusUpdateHandler - a function that takes a string used to update the UI.
5656- * Like (status) => console.log(status)
5757- * @param {string|null} twoFactorCode - Optional, but needed if it fails with 2fa required
5858- * @returns {Promise<void>}
5959- */
6060- deactivateOldAccount(oldHandle: string, oldPassword: string, statusUpdateHandler?: Function | null, twoFactorCode?: string | null): Promise<void>;
6161- /**
6262- * Signs the logged-in user in this.newAgent for backups with PDS MOOver. This is usually called after migrate and signPlcOperation are successful
6363- *
6464- * @param {string} didWeb
6565- * @returns {Promise<void>}
6666- */
6767- signUpForBackupsFromMigration(didWeb?: string): Promise<void>;
66+ /** @type {AtpAgent} */
77+ oldAgent: AtpAgent
88+ /** @type {AtpAgent} */
99+ newAgent: AtpAgent
1010+ /** @type {[string]} */
1111+ missingBlobs: [string]
1212+ /** @type {boolean} */
1313+ createNewAccount: boolean
1414+ /** @type {boolean} */
1515+ migrateRepo: boolean
1616+ /** @type {boolean} */
1717+ migrateBlobs: boolean
1818+ /** @type {boolean} */
1919+ migrateMissingBlobs: boolean
2020+ /** @type {boolean} */
2121+ migratePrefs: boolean
2222+ /** @type {boolean} */
2323+ migratePlcRecord: boolean
2424+ /**
2525+ * This migrator is pretty cut and dry and makes a few assumptions
2626+ * 1. You are using the same password between each account
2727+ * 2. If this command fails for something like oauth 2fa code it throws an error and expects the same values when ran again.
2828+ * 3. You can control which "actions" happen by setting the class variables to false.
2929+ * 4. Each instance of the class is assumed to be for a single migration
3030+ * @param {string} oldHandle - The handle you use on your old pds, something like alice.bsky.social
3131+ * @param {string} password - Your password for your current login. Has to be your real password, no app password. When setting up a new account we reuse it as well for that account
3232+ * @param {string} newPdsUrl - The new URL for your pds. Like https://coolnewpds.com
3333+ * @param {string} newEmail - The email you want to use on the new pds (can be the same as the previous one as long as it's not already being used on the new pds)
3434+ * @param {string} newHandle - The new handle you want, like alice.bsky.social, or if you already have a domain name set as a handle can use it myname.com.
3535+ * @param {string|null} inviteCode - The invite code you got from the PDS you are migrating to. If null does not include one
3636+ * @param {function|null} statusUpdateHandler - a function that takes a string used to update the UI. Like (status) => console.log(status)
3737+ * @param {string|null} twoFactorCode - Optional, but needed if it fails with 2fa required
3838+ * @param verificationCode - Optional verification captcha code for account creation if the PDS requires it
3939+ */
4040+ migrate(
4141+ oldHandle: string,
4242+ password: string,
4343+ newPdsUrl: string,
4444+ newEmail: string,
4545+ newHandle: string,
4646+ inviteCode: string | null,
4747+ statusUpdateHandler?: Function | null,
4848+ twoFactorCode?: string | null,
4949+ verificationCode?: any,
5050+ ): Promise<void>
5151+ /**
5252+ * Sign and submits the PLC operation to officially migrate the account
5353+ * @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
5454+ * @param additionalRotationKeysToAdd {string[]} - additional rotation keys to add in addition to the ones provided by the new PDS.
5555+ * @returns {Promise<void>}
5656+ */
5757+ signPlcOperation(token: string, additionalRotationKeysToAdd?: string[]): Promise<void>
5858+ /**
5959+ * Using this method assumes the Migrator class was constructed new and this was called.
6060+ * Find the user's previous PDS from the PLC op logs,
6161+ * logs in and deactivates their old account if it was found still active.
6262+ *
6363+ * @param oldHandle {string}
6464+ * @param oldPassword {string}
6565+ * @param {function|null} statusUpdateHandler - a function that takes a string used to update the UI.
6666+ * Like (status) => console.log(status)
6767+ * @param {string|null} twoFactorCode - Optional, but needed if it fails with 2fa required
6868+ * @returns {Promise<void>}
6969+ */
7070+ deactivateOldAccount(
7171+ oldHandle: string,
7272+ oldPassword: string,
7373+ statusUpdateHandler?: Function | null,
7474+ twoFactorCode?: string | null,
7575+ ): Promise<void>
7676+ /**
7777+ * Signs the logged-in user in this.newAgent for backups with PDS MOOver. This is usually called after migrate and signPlcOperation are successful
7878+ *
7979+ * @param {string} didWeb
8080+ * @returns {Promise<void>}
8181+ */
8282+ signUpForBackupsFromMigration(didWeb?: string): Promise<void>
6883}
6969-import { AtpAgent } from '@atproto/api';
7070-//# sourceMappingURL=pdsmoover.d.ts.map8484+import { AtpAgent } from '@atproto/api'
8585+//# sourceMappingURL=pdsmoover.d.ts.map
+109-98
packages/moover/types/plc-ops.d.ts
···11/**
22 * JSDoc type-only import to avoid runtime import errors in the browser.
33 */
44-export type defs = typeof defs;
44+export type defs = typeof defs
55/**
66 * JSDoc type-only import to avoid runtime import errors in the browser.
77 */
88-export type normalizeOp = any;
88+export type normalizeOp = any
99/**
1010 * JSDoc type-only import to avoid runtime import errors in the browser.
1111 */
1212-export type Operation = import("@atcute/did-plc").Operation;
1212+export type Operation = import('@atcute/did-plc').Operation
1313/**
1414 * JSDoc type-only import to avoid runtime import errors in the browser.
1515 */
1616-export type CompatibleOperation = import("@atcute/did-plc").CompatibleOperation;
1616+export type CompatibleOperation = import('@atcute/did-plc').CompatibleOperation
1717/**
1818 * JSDoc type-only import to avoid runtime import errors in the browser.
1919 */
2020-export type IndexedEntryLog = import("@atcute/did-plc").IndexedEntryLog;
2020+export type IndexedEntryLog = import('@atcute/did-plc').IndexedEntryLog
2121/**
2222 * JSDoc type-only import to avoid runtime import errors in the browser.
2323 */
2424-export type IndexedEntry = import("@atcute/did-plc").IndexedEntry;
2424+export type IndexedEntry = import('@atcute/did-plc').IndexedEntry
2525/**
2626 * Class to help with various PLC operations
2727 */
2828export class PlcOps {
2929- /**
3030- *
3131- * @param plcDirectoryUrl {string} - The url of the plc directory, defaults to https://plc.directory
3232- */
3333- constructor(plcDirectoryUrl?: string);
3434- /**
3535- * The url of the plc directory
3636- * @type {string}
3737- */
3838- plcDirectoryUrl: string;
3939- /**
4040- * Gets the current rotation keys for a user via their last PlC operation
4141- * @param did
4242- * @returns {Promise<string[]>}
4343- */
4444- getCurrentRotationKeysForUser(did: any): Promise<string[]>;
4545- /**
4646- * Gets the last PlC operation for a user from the plc directory
4747- * @param did
4848- * @returns {Promise<{lastOperation: Operation, base: any}>}
4949- */
5050- getLastPlcOpFromPlc(did: any): Promise<{
5151- lastOperation: Operation;
5252- base: any;
5353- }>;
5454- /**
5555- *
5656- * @param logs {IndexedEntryLog}
5757- * @returns {{lastOperation: Operation, base: IndexedEntry}}
5858- */
5959- getLastPlcOp(logs: IndexedEntryLog): {
6060- lastOperation: Operation;
6161- base: IndexedEntry;
6262- };
6363- /**
6464- * Gets the plc audit logs for a user from the plc directory
6565- * @param did
6666- * @returns {Promise<IndexedEntryLog>}
6767- */
6868- getPlcAuditLogs(did: any): Promise<IndexedEntryLog>;
6969- /**
7070- * Creates a new secp256k1 key that can be used for either rotation or verification key
7171- * @returns {Promise<{privateKey: string, publicKey: `did:key:${string}`}>}
7272- */
7373- createANewSecp256k1(): Promise<{
7474- privateKey: string;
7575- publicKey: `did:key:${string}`;
7676- }>;
7777- /**
7878- * Signs a new operation with the provided signing key, and information and submits it to the plc directory
7979- * @param did {string} - The user's did
8080- * @param signingRotationKey { P256PrivateKey|Secp256k1PrivateKey} - The keypair to sign the op with
8181- * @param alsoKnownAs {string[]}
8282- * @param rotationKeys {string[]}
8383- * @param pds {string}
8484- * @param verificationKey {string} - The public verification key
8585- * @param prev {string} - The previous valid operation's cid.
8686- * @returns {Promise<void>}
8787- */
8888- signAndPublishNewOp(did: string, signingRotationKey: P256PrivateKey | Secp256k1PrivateKey, alsoKnownAs: string[], rotationKeys: string[], pds: string, verificationKey: string, prev: string): Promise<void>;
8989- /**
9090- * Takes a multi or hex based private key and returns a keypair
9191- * @param privateKeyString {string}
9292- * @param type {string} - secp256k1 or p256, needed if the private key is hex based, can be assumed if it's a multikey
9393- * @returns {Promise<{type: string, didPublicKey: `did:key:${string}`, keypair: P256PrivateKey|Secp256k1PrivateKey}>}
9494- */
9595- getKeyPair(privateKeyString: string, type?: string): Promise<{
9696- type: string;
9797- didPublicKey: `did:key:${string}`;
9898- keypair: P256PrivateKey | Secp256k1PrivateKey;
9999- }>;
100100- /**
101101- * Submits a new operation to the plc directory
102102- * @param did {string} - The user's did
103103- * @param operation
104104- * @returns {Promise<void>}
105105- */
106106- pushPlcOperation(did: string, operation: any): Promise<void>;
107107- /**
108108- * Creates a new service auth token for a user. This is what is used to create a new account on a PDS for your did
109109- *
110110- * @param iss The user's did
111111- * @param aud The did:web, if it's a PDS it's usually from /xrpc/com.atproto.server.describeServer
112112- * @param keypair The keypair to sign with only supporting ES256K atm
113113- * @param lxm The lxm which is usually com.atproto.server.createAccount for creating a new account
114114- * @returns {Promise<string>}
115115- */
116116- createANewServiceAuthToken(iss: any, aud: any, keypair: any, lxm: any): Promise<string>;
2929+ /**
3030+ *
3131+ * @param plcDirectoryUrl {string} - The url of the plc directory, defaults to https://plc.directory
3232+ */
3333+ constructor(plcDirectoryUrl?: string)
3434+ /**
3535+ * The url of the plc directory
3636+ * @type {string}
3737+ */
3838+ plcDirectoryUrl: string
3939+ /**
4040+ * Gets the current rotation keys for a user via their last PlC operation
4141+ * @param did
4242+ * @returns {Promise<string[]>}
4343+ */
4444+ getCurrentRotationKeysForUser(did: any): Promise<string[]>
4545+ /**
4646+ * Gets the last PlC operation for a user from the plc directory
4747+ * @param did
4848+ * @returns {Promise<{lastOperation: Operation, base: any}>}
4949+ */
5050+ getLastPlcOpFromPlc(did: any): Promise<{
5151+ lastOperation: Operation
5252+ base: any
5353+ }>
5454+ /**
5555+ *
5656+ * @param logs {IndexedEntryLog}
5757+ * @returns {{lastOperation: Operation, base: IndexedEntry}}
5858+ */
5959+ getLastPlcOp(logs: IndexedEntryLog): {
6060+ lastOperation: Operation
6161+ base: IndexedEntry
6262+ }
6363+ /**
6464+ * Gets the plc audit logs for a user from the plc directory
6565+ * @param did
6666+ * @returns {Promise<IndexedEntryLog>}
6767+ */
6868+ getPlcAuditLogs(did: any): Promise<IndexedEntryLog>
6969+ /**
7070+ * Creates a new secp256k1 key that can be used for either rotation or verification key
7171+ * @returns {Promise<{privateKey: string, publicKey: `did:key:${string}`}>}
7272+ */
7373+ createANewSecp256k1(): Promise<{
7474+ privateKey: string
7575+ publicKey: `did:key:${string}`
7676+ }>
7777+ /**
7878+ * Signs a new operation with the provided signing key, and information and submits it to the plc directory
7979+ * @param did {string} - The user's did
8080+ * @param signingRotationKey { P256PrivateKey|Secp256k1PrivateKey} - The keypair to sign the op with
8181+ * @param alsoKnownAs {string[]}
8282+ * @param rotationKeys {string[]}
8383+ * @param pds {string}
8484+ * @param verificationKey {string} - The public verification key
8585+ * @param prev {string} - The previous valid operation's cid.
8686+ * @returns {Promise<void>}
8787+ */
8888+ signAndPublishNewOp(
8989+ did: string,
9090+ signingRotationKey: P256PrivateKey | Secp256k1PrivateKey,
9191+ alsoKnownAs: string[],
9292+ rotationKeys: string[],
9393+ pds: string,
9494+ verificationKey: string,
9595+ prev: string,
9696+ ): Promise<void>
9797+ /**
9898+ * Takes a multi or hex based private key and returns a keypair
9999+ * @param privateKeyString {string}
100100+ * @param type {string} - secp256k1 or p256, needed if the private key is hex based, can be assumed if it's a multikey
101101+ * @returns {Promise<{type: string, didPublicKey: `did:key:${string}`, keypair: P256PrivateKey|Secp256k1PrivateKey}>}
102102+ */
103103+ getKeyPair(
104104+ privateKeyString: string,
105105+ type?: string,
106106+ ): Promise<{
107107+ type: string
108108+ didPublicKey: `did:key:${string}`
109109+ keypair: P256PrivateKey | Secp256k1PrivateKey
110110+ }>
111111+ /**
112112+ * Submits a new operation to the plc directory
113113+ * @param did {string} - The user's did
114114+ * @param operation
115115+ * @returns {Promise<void>}
116116+ */
117117+ pushPlcOperation(did: string, operation: any): Promise<void>
118118+ /**
119119+ * Creates a new service auth token for a user. This is what is used to create a new account on a PDS for your did
120120+ *
121121+ * @param iss The user's did
122122+ * @param aud The did:web, if it's a PDS it's usually from /xrpc/com.atproto.server.describeServer
123123+ * @param keypair The keypair to sign with only supporting ES256K atm
124124+ * @param lxm The lxm which is usually com.atproto.server.createAccount for creating a new account
125125+ * @returns {Promise<string>}
126126+ */
127127+ createANewServiceAuthToken(iss: any, aud: any, keypair: any, lxm: any): Promise<string>
117128}
118118-import { defs } from '@atcute/did-plc';
119119-import { P256PrivateKey } from '@atcute/crypto';
120120-import { Secp256k1PrivateKey } from '@atcute/crypto';
121121-//# sourceMappingURL=plc-ops.d.ts.map129129+import { defs } from '@atcute/did-plc'
130130+import { P256PrivateKey } from '@atcute/crypto'
131131+import { Secp256k1PrivateKey } from '@atcute/crypto'
132132+//# sourceMappingURL=plc-ops.d.ts.map
+92-77
packages/moover/types/restore.d.ts
···11-export type Operation = import("@atcute/did-plc").Operation;
11+export type Operation = import('@atcute/did-plc').Operation
22export class Restore {
33- /**
44- *
55- * @param pdsMooverInstance {string} - The url of the pds moover instance to restore from. Defaults to https://pdsmover.com
66- */
77- constructor(pdsMooverInstance?: string);
88- /**
99- * If you want to use a different plc directory create your own instance of the plc ops class and pass it in here
1010- * @type {PlcOps} */
1111- plcOps: PlcOps;
1212- /**
1313- * This is the base url for the pds moover instance used to restore the files from a backup.
1414- * @type {string}
1515- */
1616- pdsMooverInstance: string;
1717- /**
1818- * To keep it simple, only uses secp256k for the temp verification key that is used to create the new account on the new PDS
1919- * and is temporarily assigned to the user's account on PLC
2020- * @type {null|Secp256k1PrivateKeyExportable}
2121- */
2222- tempVerificationKeypair: null | Secp256k1PrivateKeyExportable;
2323- /** @type {AtpAgent} */
2424- atpAgent: AtpAgent;
2525- /**
2626- * The keypair that is used to sign the plc operation
2727- * @type {null|{type: string, didPublicKey: `did:key:${string}`, keypair: P256PrivateKey|Secp256k1PrivateKey}}
2828- */
2929- recoveryRotationKeyPair: null | {
3030- type: string;
3131- didPublicKey: `did:key:${string}`;
3232- keypair: P256PrivateKey | Secp256k1PrivateKey;
3333- };
3434- /**
3535- * If this is true we are just restoring the repo and blobs. Ideally for rerunning a restore process after account recovery
3636- * @type {boolean}
3737- */
3838- RestoreFromBackup: boolean;
3939- /**
4040- * If set to true then it will do the account recovery. Writes a temp key to the did doc,
4141- * create a new account on the new pds, and then submit a new plc op for the pds to have control (finishes the migration, can always restore the backup later)
4242- * @type {boolean}
4343- */
4444- AccountRecovery: boolean;
4545- /**
4646- * Recovers an account with the users rotation key and restores the repo from a PDS MOOver backup
4747- * This method can fail, and the account was still recovered, it's best to check the PLC logs to see where an account stands before reruns
4848- * @param rotationKey {string} - The users private rotation key, can be a multi key or hex key
4949- * @param rotationKeyType {string} - The type of the key, secp256k1 or p256. Required if the key is in hex format, defaults to secp256k1
5050- * @param currentHandleOrDid {string} - The users current handle or did, if they don't have a DNS record it will have to be their did for success
5151- * @param newPDS {string} - The new PDS url, like https://coolnewpds.com
5252- * @param newHandle {string} - Can be the users DNS handle if it is already setup with their did, if not it's bob.mypds.com
5353- * @param newPassword {string} - The new password for the new account
5454- * @param newEmail {string} - The new email for the new account
5555- * @param inviteCode {string|null} - The invite code for the new PDS if it requires one
5656- * @param cidToRestoreTo {string|null} - The cid of the plc op to restore to, used mostly to revert a fraudulent plc op. Want to give it the last valid operations cid
5757- * @param onStatus {function|null} - A function that takes a string used to update the UI. Like (status) => console.log(status)
5858- * @returns {Promise<void>} If there is a failure during restoring the back up (after the status Success! Restoring your repo...) then your account is most likely
5959- * recovered and future runs need to have the RestoreFromBackup flag set to true and AccountRecovery set to false.
6060- */
6161- recover(rotationKey: string, rotationKeyType: string, currentHandleOrDid: string, newPDS: string, newHandle: string, newPassword: string, newEmail: string, inviteCode: string | null, cidToRestoreTo?: string | null, onStatus?: Function | null): Promise<void>;
6262- /**
6363- * This method signs the plc operation over to the new PDS and activates the account
6464- * Assumes you have already created a new account during the recovery process and logged in
6565- * Uses the recommended did doc from the PDS as a base and adds the users rotation key to the rotation keys array
6666- *
6767- * @param usersDid
6868- * @param additionalRotationKeysToAdd
6969- * @param prevCid
7070- * @returns {Promise<void>}
7171- */
7272- signRestorePlcOperation(usersDid: any, additionalRotationKeysToAdd: any[], prevCid: any): Promise<void>;
33+ /**
44+ *
55+ * @param pdsMooverInstance {string} - The url of the pds moover instance to restore from. Defaults to https://pdsmover.com
66+ */
77+ constructor(pdsMooverInstance?: string)
88+ /**
99+ * If you want to use a different plc directory create your own instance of the plc ops class and pass it in here
1010+ * @type {PlcOps} */
1111+ plcOps: PlcOps
1212+ /**
1313+ * This is the base url for the pds moover instance used to restore the files from a backup.
1414+ * @type {string}
1515+ */
1616+ pdsMooverInstance: string
1717+ /**
1818+ * To keep it simple, only uses secp256k for the temp verification key that is used to create the new account on the new PDS
1919+ * and is temporarily assigned to the user's account on PLC
2020+ * @type {null|Secp256k1PrivateKeyExportable}
2121+ */
2222+ tempVerificationKeypair: null | Secp256k1PrivateKeyExportable
2323+ /** @type {AtpAgent} */
2424+ atpAgent: AtpAgent
2525+ /**
2626+ * The keypair that is used to sign the plc operation
2727+ * @type {null|{type: string, didPublicKey: `did:key:${string}`, keypair: P256PrivateKey|Secp256k1PrivateKey}}
2828+ */
2929+ recoveryRotationKeyPair: null | {
3030+ type: string
3131+ didPublicKey: `did:key:${string}`
3232+ keypair: P256PrivateKey | Secp256k1PrivateKey
3333+ }
3434+ /**
3535+ * If this is true we are just restoring the repo and blobs. Ideally for rerunning a restore process after account recovery
3636+ * @type {boolean}
3737+ */
3838+ RestoreFromBackup: boolean
3939+ /**
4040+ * If set to true then it will do the account recovery. Writes a temp key to the did doc,
4141+ * create a new account on the new pds, and then submit a new plc op for the pds to have control (finishes the migration, can always restore the backup later)
4242+ * @type {boolean}
4343+ */
4444+ AccountRecovery: boolean
4545+ /**
4646+ * Recovers an account with the users rotation key and restores the repo from a PDS MOOver backup
4747+ * This method can fail, and the account was still recovered, it's best to check the PLC logs to see where an account stands before reruns
4848+ * @param rotationKey {string} - The users private rotation key, can be a multi key or hex key
4949+ * @param rotationKeyType {string} - The type of the key, secp256k1 or p256. Required if the key is in hex format, defaults to secp256k1
5050+ * @param currentHandleOrDid {string} - The users current handle or did, if they don't have a DNS record it will have to be their did for success
5151+ * @param newPDS {string} - The new PDS url, like https://coolnewpds.com
5252+ * @param newHandle {string} - Can be the users DNS handle if it is already setup with their did, if not it's bob.mypds.com
5353+ * @param newPassword {string} - The new password for the new account
5454+ * @param newEmail {string} - The new email for the new account
5555+ * @param inviteCode {string|null} - The invite code for the new PDS if it requires one
5656+ * @param cidToRestoreTo {string|null} - The cid of the plc op to restore to, used mostly to revert a fraudulent plc op. Want to give it the last valid operations cid
5757+ * @param onStatus {function|null} - A function that takes a string used to update the UI. Like (status) => console.log(status)
5858+ * @returns {Promise<void>} If there is a failure during restoring the back up (after the status Success! Restoring your repo...) then your account is most likely
5959+ * recovered and future runs need to have the RestoreFromBackup flag set to true and AccountRecovery set to false.
6060+ */
6161+ recover(
6262+ rotationKey: string,
6363+ rotationKeyType: string,
6464+ currentHandleOrDid: string,
6565+ newPDS: string,
6666+ newHandle: string,
6767+ newPassword: string,
6868+ newEmail: string,
6969+ inviteCode: string | null,
7070+ cidToRestoreTo?: string | null,
7171+ onStatus?: Function | null,
7272+ ): Promise<void>
7373+ /**
7474+ * This method signs the plc operation over to the new PDS and activates the account
7575+ * Assumes you have already created a new account during the recovery process and logged in
7676+ * Uses the recommended did doc from the PDS as a base and adds the users rotation key to the rotation keys array
7777+ *
7878+ * @param usersDid
7979+ * @param additionalRotationKeysToAdd
8080+ * @param prevCid
8181+ * @returns {Promise<void>}
8282+ */
8383+ signRestorePlcOperation(
8484+ usersDid: any,
8585+ additionalRotationKeysToAdd: any[],
8686+ prevCid: any,
8787+ ): Promise<void>
7388}
7474-import { PlcOps } from './plc-ops.js';
7575-import { Secp256k1PrivateKeyExportable } from '@atcute/crypto';
7676-import { AtpAgent } from '@atproto/api';
7777-import { P256PrivateKey } from '@atcute/crypto';
7878-import { Secp256k1PrivateKey } from '@atcute/crypto';
7979-//# sourceMappingURL=restore.d.ts.map8989+import { PlcOps } from './plc-ops.js'
9090+import { Secp256k1PrivateKeyExportable } from '@atcute/crypto'
9191+import { AtpAgent } from '@atproto/api'
9292+import { P256PrivateKey } from '@atcute/crypto'
9393+import { Secp256k1PrivateKey } from '@atcute/crypto'
9494+//# sourceMappingURL=restore.d.ts.map
+23-23
packages/moover/vite.config.js
···11-import {dirname, resolve} from 'node:path'
22-import {fileURLToPath} from 'node:url'
33-import {defineConfig} from 'vite'
11+import { dirname, resolve } from 'node:path'
22+import { fileURLToPath } from 'node:url'
33+import { defineConfig } from 'vite'
4455const __dirname = dirname(fileURLToPath(import.meta.url))
6677export default defineConfig({
88- build: {
99- lib: {
1010- entry: resolve(__dirname, 'lib/main.js'),
1111- name: '@pds-moover/moover',
1212- // the proper extensions will be added
1313- fileName: 'pds-moover',
1414- },
1515- rollupOptions: {
1616- // // make sure to externalize deps that shouldn't be bundled
1717- // // into your library
1818- // external: ['vue'],
1919- // output: {
2020- // // Provide global variables to use in the UMD build
2121- // // for externalized deps
2222- // globals: {
2323- // vue: 'Vue',
2424- // },
2525- // },
2626- },
88+ build: {
99+ lib: {
1010+ entry: resolve(__dirname, 'lib/main.js'),
1111+ name: '@pds-moover/moover',
1212+ // the proper extensions will be added
1313+ fileName: 'pds-moover',
1414+ },
1515+ rollupOptions: {
1616+ // // make sure to externalize deps that shouldn't be bundled
1717+ // // into your library
1818+ // external: ['vue'],
1919+ // output: {
2020+ // // Provide global variables to use in the UMD build
2121+ // // for externalized deps
2222+ // globals: {
2323+ // vue: 'Vue',
2424+ // },
2525+ // },
2726 },
2828-})2727+ },
2828+})