import { Client, type DidString } from '@atproto/lex'; import { PasswordSession } from '@atproto/lex-password-session'; import type { SessionData } from '@atproto/lex-password-session'; import type { RateLimitInfo } from './blobTransfer.js'; type StatusHandler = ((status: string) => void) | null; /** How the caller wants to react to a blob-upload 429 during migration. */ export type RateLimitDecision = 'continue' | 'stop'; /** Passed to onRateLimitDecision so the UI can explain the pause. */ export interface RateLimitPauseInfo { side: 'source' | 'destination'; uploaded: number; remaining: number; resetAt?: Date; /** True when this is a deliberate early stop (headroom), not an actual 429. */ headroom?: boolean; } /** * Handles normal PDS Migrations between two PDSs that are both up. * On pdsmoover.com this is the logic for the MOOver */ declare class Migrator { /** The logged-in session on the source PDS, set by loginPreflight() or migrate() */ oldSession: PasswordSession | null; /** The logged-in session on the destination PDS */ newSession: PasswordSession | null; /** Authenticated client for the source PDS */ oldClient: Client | null; /** Authenticated client for the destination PDS */ newClient: Client | null; usersDid: DidString | null; /** * Whether the account already exists on the destination PDS, set by loginPreflight(). * Null until a preflight has run; migrate() checks for itself when so. */ destinationExists: boolean | null; missingBlobs: string[]; createNewAccount: boolean; migrateRepo: boolean; migrateBlobs: boolean; migrateMissingBlobs: boolean; migratePrefs: boolean; migratePlcRecord: boolean; /** * Called when the new PDS starts rate limiting (HTTP 429) blob uploads. Return * 'continue' to skip the remaining blob phases and still finish prefs/PLC, or * 'stop' to abort with a BlobRateLimitError. When null (headless callers), a * 429 throws BlobRateLimitError. */ onRateLimitDecision: ((info: RateLimitPauseInfo) => Promise) | null; /** * Called whenever a source ('old') or destination ('new') login session is created or * refreshed (with its serializable SessionData), or deleted/invalidated (with null). Lets * the caller persist the session so a mid-migration page refresh can be resumed via * restoreSessions(). When null (the default), no persistence happens. */ onSessionUpdate: ((side: 'old' | 'new', data: SessionData | null) => void) | null; /** Blobs left unmigrated after the user chose 'continue' on a rate limit. */ skippedBlobCount: number; /** Details of the rate limit that paused the migration, if one happened. */ rateLimitInfo: RateLimitInfo | null; /** * Attempts the source and (when it already exists) destination logins up front and reports * which ones still need an emailed 2FA code. A PDS that uses email 2FA (e.g. bsky.social) * only sends the code after a token-less login attempt, so this is also what triggers the * emails: call it once without codes to make the PDSs send them, show an input for each * flagged login, then call it again with the codes before migrate(). Successful logins are * kept on the instance and reused by migrate() so each single-use code is only spent once. * * @returns which logins still need a 2FA code entered */ loginPreflight({ oldHandle, password, twoFactorCode, newPdsUrl, newPassword, newTwoFactorCode, sourcePdsUrl, statusUpdateHandler, }: { /** The handle on the current PDS, like alice.bsky.social */ oldHandle: string; /** Password for the current PDS login */ password: string; /** 2FA code for the current PDS, when it asked for one */ twoFactorCode?: string | null; /** The URL of the destination PDS, like https://coolnewpds.com */ newPdsUrl: string; /** Password for the destination account (bsky.social move-backs: the password from before you left). Falls back to password */ newPassword?: string | null; /** 2FA code for the destination account, when it asked for one */ newTwoFactorCode?: string | null; /** Optional override of the source PDS URL instead of resolving from the DID doc */ sourcePdsUrl?: string | null; /** a function that takes a string used to update the UI */ statusUpdateHandler?: StatusHandler; }): Promise<{ needsSourceToken: boolean; needsDestinationToken: boolean; }>; /** * Rebuilds the source and destination sessions from previously persisted SessionData * (see onSessionUpdate) so a migration can be resumed after a page refresh without * re-entering credentials or triggering another 2FA email. Each session is refreshed via * PasswordSession.resume(), which throws only when the tokens are definitively invalid — * the caller should clear its cache and fall back to the login form in that case. Leaves * the migrator in the same state loginPreflight() would for an existing destination * account (destinationExists = true), so a following migrate() takes the already-exists * path (skips account creation, runs the missing-blob catch-up). */ restoreSessions(oldData: SessionData, newData: SessionData): Promise; /** * This migrator is pretty cut and dry and makes a few assumptions * 1. You are using the same password between each account, unless newPassword is provided * 2. If this command fails for something like oauth 2fa code it throws an error and expects the same values when ran again. * For PDSs with email 2FA, run loginPreflight() first (with the same values) so every needed code is emailed and collected up front; migrate() reuses the sessions it made. * 3. You can control which "actions" happen by setting the class variables to false. * 4. Each instance of the class is assumed to be for a single migration * @param oldHandle - The handle you use on your old pds, something like alice.bsky.social * @param 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 * @param newPdsUrl - The new URL for your pds. Like https://coolnewpds.com * @param 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) * @param 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. * @param inviteCode - The invite code you got from the PDS you are migrating to. If null does not include one * @param statusUpdateHandler - a function that takes a string used to update the UI. Like (status) => console.log(status) * @param twoFactorCode - Optional, but needed if it fails with 2fa required * @param verificationCode - Optional verification captcha code for account creation if the PDS requires it * @param sourcePdsUrl - Optional URL to use as the source PDS instead of resolving from DID doc. Useful for moving from one PDS to another without changing the PDS hostname in the diddoc * @param newPassword - Optional password for the account on the new PDS. For bsky.social move-backs this is the password the account had when you left. Falls back to password when null/empty * @param newTwoFactorCode - Optional 2FA token for the destination-account login. For bsky.social move-backs this is emailed by bsky.social and is a different token than twoFactorCode (which is for the source PDS). Does NOT fall back to twoFactorCode */ migrate(oldHandle: string, password: string, newPdsUrl: string, newEmail: string, newHandle: string, inviteCode: string | null, statusUpdateHandler?: StatusHandler, twoFactorCode?: string | null, verificationCode?: string | null, sourcePdsUrl?: string | null, newPassword?: string | null, newTwoFactorCode?: string | null): Promise; /** * Reacts to a rate-limited blob transfer. Uploaded/remaining counts are derived from * `status` (a checkAccountStatus the caller already fetched right before the * transferBlobs call that produced `result`) plus `result.uploaded`, so this no longer * makes its own checkAccountStatus round-trip — that extra call was the source of a long * UI stall right after a 429. Asks onRateLimitDecision (if set) how to proceed; a * missing handler or a 'stop' decision throws a BlobRateLimitError, while 'continue' * records the skipped-blob count + rate-limit info and returns true so the caller skips * the remaining blob phases (prefs/PLC still run). * * @param status - checkAccountStatus fetched by the caller immediately before the * transferBlobs call that produced `result`. * @returns whether the remaining blob phases should be skipped. */ private handleBlobRateLimit; /** * Sign and submits the PLC operation to officially migrate the account * @param 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 * @param additionalRotationKeysToAdd - additional rotation keys to add in addition to the ones provided by the new PDS. */ signPlcOperation(token: string, additionalRotationKeysToAdd?: string[]): Promise; /** * Using this method assumes the Migrator class was constructed new and this was called. * Find the user's previous PDS from the PLC op logs, * logs in and deactivates their old account if it was found still active. * * @param oldHandle * @param oldPassword * @param statusUpdateHandler - a function that takes a string used to update the UI. * Like (status) => console.log(status) * @param twoFactorCode - Optional, but needed if it fails with 2fa required */ deactivateOldAccount(oldHandle: string, oldPassword: string, statusUpdateHandler?: StatusHandler, twoFactorCode?: string | null): Promise; /** * Signs the logged-in user in this.newSession up for backups with PDS MOOver. This is usually called after migrate and signPlcOperation are successful */ signUpForBackupsFromMigration(didWeb?: DidString): Promise; } export { Migrator }; //# sourceMappingURL=pdsmoover.d.ts.map