this repo has no description
1import type { 2 InboundMigrationState, 3 InboundStep, 4 MigrationProgress, 5 OutboundMigrationState, 6 OutboundStep, 7 ServerDescription, 8 StoredMigrationState, 9} from "./types"; 10import { 11 AtprotoClient, 12 createLocalClient, 13 resolvePdsUrl, 14} from "./atproto-client"; 15import { 16 clearMigrationState, 17 loadMigrationState, 18 saveMigrationState, 19 updateProgress, 20 updateStep, 21} from "./storage"; 22 23function migrationLog(stage: string, data?: Record<string, unknown>) { 24 const timestamp = new Date().toISOString(); 25 const msg = `[MIGRATION ${timestamp}] ${stage}`; 26 if (data) { 27 console.log(msg, JSON.stringify(data, null, 2)); 28 } else { 29 console.log(msg); 30 } 31} 32 33function createInitialProgress(): MigrationProgress { 34 return { 35 repoExported: false, 36 repoImported: false, 37 blobsTotal: 0, 38 blobsMigrated: 0, 39 blobsFailed: [], 40 prefsMigrated: false, 41 plcSigned: false, 42 activated: false, 43 deactivated: false, 44 currentOperation: "", 45 }; 46} 47 48export function createInboundMigrationFlow() { 49 let state = $state<InboundMigrationState>({ 50 direction: "inbound", 51 step: "welcome", 52 sourcePdsUrl: "", 53 sourceDid: "", 54 sourceHandle: "", 55 targetHandle: "", 56 targetEmail: "", 57 targetPassword: "", 58 inviteCode: "", 59 sourceAccessToken: null, 60 sourceRefreshToken: null, 61 serviceAuthToken: null, 62 emailVerifyToken: "", 63 plcToken: "", 64 progress: createInitialProgress(), 65 error: null, 66 requires2FA: false, 67 twoFactorCode: "", 68 targetVerificationMethod: null, 69 }); 70 71 let sourceClient: AtprotoClient | null = null; 72 let localClient: AtprotoClient | null = null; 73 let localServerInfo: ServerDescription | null = null; 74 75 function setStep(step: InboundStep) { 76 state.step = step; 77 state.error = null; 78 saveMigrationState(state); 79 updateStep(step); 80 } 81 82 function setError(error: string) { 83 state.error = error; 84 saveMigrationState(state); 85 } 86 87 function setProgress(updates: Partial<MigrationProgress>) { 88 state.progress = { ...state.progress, ...updates }; 89 updateProgress(updates); 90 } 91 92 async function loadLocalServerInfo(): Promise<ServerDescription> { 93 if (!localClient) { 94 localClient = createLocalClient(); 95 } 96 if (!localServerInfo) { 97 localServerInfo = await localClient.describeServer(); 98 } 99 return localServerInfo; 100 } 101 102 async function resolveSourcePds(handle: string): Promise<void> { 103 try { 104 const { did, pdsUrl } = await resolvePdsUrl(handle); 105 state.sourcePdsUrl = pdsUrl; 106 state.sourceDid = did; 107 state.sourceHandle = handle; 108 sourceClient = new AtprotoClient(pdsUrl); 109 } catch (e) { 110 throw new Error(`Could not resolve handle: ${(e as Error).message}`); 111 } 112 } 113 114 async function loginToSource( 115 handle: string, 116 password: string, 117 twoFactorCode?: string, 118 ): Promise<void> { 119 migrationLog("loginToSource START", { handle, has2FA: !!twoFactorCode }); 120 121 if (!state.sourcePdsUrl) { 122 await resolveSourcePds(handle); 123 } 124 125 if (!sourceClient) { 126 sourceClient = new AtprotoClient(state.sourcePdsUrl); 127 } 128 129 try { 130 migrationLog("loginToSource: Calling createSession on OLD PDS", { 131 pdsUrl: state.sourcePdsUrl, 132 }); 133 const session = await sourceClient.login(handle, password, twoFactorCode); 134 migrationLog("loginToSource SUCCESS", { 135 did: session.did, 136 handle: session.handle, 137 pdsUrl: state.sourcePdsUrl, 138 }); 139 state.sourceAccessToken = session.accessJwt; 140 state.sourceRefreshToken = session.refreshJwt; 141 state.sourceDid = session.did; 142 state.sourceHandle = session.handle; 143 state.requires2FA = false; 144 saveMigrationState(state); 145 } catch (e) { 146 const err = e as Error & { error?: string }; 147 migrationLog("loginToSource FAILED", { 148 error: err.message, 149 errorCode: err.error, 150 }); 151 if (err.error === "AuthFactorTokenRequired") { 152 state.requires2FA = true; 153 throw new Error( 154 "Two-factor authentication required. Please enter the code sent to your email.", 155 ); 156 } 157 throw e; 158 } 159 } 160 161 async function checkHandleAvailability(handle: string): Promise<boolean> { 162 if (!localClient) { 163 localClient = createLocalClient(); 164 } 165 try { 166 await localClient.resolveHandle(handle); 167 return false; 168 } catch { 169 return true; 170 } 171 } 172 173 async function authenticateToLocal( 174 email: string, 175 password: string, 176 ): Promise<void> { 177 if (!localClient) { 178 localClient = createLocalClient(); 179 } 180 await localClient.loginDeactivated(email, password); 181 } 182 183 async function startMigration(): Promise<void> { 184 migrationLog("startMigration START", { 185 sourceDid: state.sourceDid, 186 sourceHandle: state.sourceHandle, 187 targetHandle: state.targetHandle, 188 sourcePdsUrl: state.sourcePdsUrl, 189 }); 190 191 if (!sourceClient || !state.sourceAccessToken) { 192 migrationLog("startMigration ERROR: Not logged in to source PDS"); 193 throw new Error("Not logged in to source PDS"); 194 } 195 196 if (!localClient) { 197 localClient = createLocalClient(); 198 } 199 200 setStep("migrating"); 201 setProgress({ currentOperation: "Getting service auth token..." }); 202 203 try { 204 migrationLog("startMigration: Loading local server info"); 205 const serverInfo = await loadLocalServerInfo(); 206 migrationLog("startMigration: Got server info", { 207 serverDid: serverInfo.did, 208 }); 209 210 migrationLog("startMigration: Getting service auth token from OLD PDS"); 211 const { token } = await sourceClient.getServiceAuth( 212 serverInfo.did, 213 "com.atproto.server.createAccount", 214 ); 215 migrationLog("startMigration: Got service auth token"); 216 state.serviceAuthToken = token; 217 218 setProgress({ currentOperation: "Creating account on new PDS..." }); 219 220 const accountParams = { 221 did: state.sourceDid, 222 handle: state.targetHandle, 223 email: state.targetEmail, 224 password: state.targetPassword, 225 inviteCode: state.inviteCode || undefined, 226 }; 227 228 migrationLog("startMigration: Creating account on NEW PDS", { 229 did: accountParams.did, 230 handle: accountParams.handle, 231 }); 232 const session = await localClient.createAccount(accountParams, token); 233 migrationLog("startMigration: Account created on NEW PDS", { 234 did: session.did, 235 }); 236 localClient.setAccessToken(session.accessJwt); 237 238 setProgress({ currentOperation: "Exporting repository..." }); 239 migrationLog("startMigration: Exporting repo from OLD PDS"); 240 const exportStart = Date.now(); 241 const car = await sourceClient.getRepo(state.sourceDid); 242 migrationLog("startMigration: Repo exported", { 243 durationMs: Date.now() - exportStart, 244 sizeBytes: car.byteLength, 245 }); 246 setProgress({ 247 repoExported: true, 248 currentOperation: "Importing repository...", 249 }); 250 251 migrationLog("startMigration: Importing repo to NEW PDS"); 252 const importStart = Date.now(); 253 await localClient.importRepo(car); 254 migrationLog("startMigration: Repo imported", { 255 durationMs: Date.now() - importStart, 256 }); 257 setProgress({ 258 repoImported: true, 259 currentOperation: "Counting blobs...", 260 }); 261 262 const accountStatus = await localClient.checkAccountStatus(); 263 migrationLog("startMigration: Account status", { 264 expectedBlobs: accountStatus.expectedBlobs, 265 importedBlobs: accountStatus.importedBlobs, 266 }); 267 setProgress({ 268 blobsTotal: accountStatus.expectedBlobs, 269 currentOperation: "Migrating blobs...", 270 }); 271 272 await migrateBlobs(); 273 274 setProgress({ currentOperation: "Migrating preferences..." }); 275 await migratePreferences(); 276 277 migrationLog( 278 "startMigration: Initial migration complete, waiting for email verification", 279 ); 280 setStep("email-verify"); 281 } catch (e) { 282 const err = e as Error & { error?: string; status?: number }; 283 const message = err.message || err.error || 284 `Unknown error (status ${err.status || "unknown"})`; 285 migrationLog("startMigration FAILED", { 286 error: message, 287 errorCode: err.error, 288 status: err.status, 289 stack: err.stack, 290 }); 291 setError(message); 292 setStep("error"); 293 } 294 } 295 296 async function migrateBlobs(): Promise<void> { 297 if (!sourceClient || !localClient) return; 298 299 let cursor: string | undefined; 300 let migrated = 0; 301 302 do { 303 const { blobs, cursor: nextCursor } = await localClient.listMissingBlobs( 304 cursor, 305 100, 306 ); 307 308 for (const blob of blobs) { 309 try { 310 setProgress({ 311 currentOperation: `Migrating blob ${ 312 migrated + 1 313 }/${state.progress.blobsTotal}...`, 314 }); 315 316 const blobData = await sourceClient.getBlob( 317 state.sourceDid, 318 blob.cid, 319 ); 320 await localClient.uploadBlob(blobData, "application/octet-stream"); 321 migrated++; 322 setProgress({ blobsMigrated: migrated }); 323 } catch (e) { 324 state.progress.blobsFailed.push(blob.cid); 325 } 326 } 327 328 cursor = nextCursor; 329 } while (cursor); 330 } 331 332 async function migratePreferences(): Promise<void> { 333 if (!sourceClient || !localClient) return; 334 335 try { 336 const prefs = await sourceClient.getPreferences(); 337 await localClient.putPreferences(prefs); 338 setProgress({ prefsMigrated: true }); 339 } catch { 340 } 341 } 342 343 async function submitEmailVerifyToken( 344 token: string, 345 localPassword?: string, 346 ): Promise<void> { 347 if (!localClient) { 348 localClient = createLocalClient(); 349 } 350 351 state.emailVerifyToken = token; 352 setError(null); 353 354 try { 355 await localClient.verifyToken(token, state.targetEmail); 356 357 if (!sourceClient) { 358 setStep("source-login"); 359 setError( 360 "Email verified! Please log in to your old account again to complete the migration.", 361 ); 362 return; 363 } 364 365 if (localPassword) { 366 setProgress({ currentOperation: "Authenticating to new PDS..." }); 367 await localClient.loginDeactivated(state.targetEmail, localPassword); 368 } 369 370 if (!localClient.getAccessToken()) { 371 setError("Email verified! Please enter your password to continue."); 372 return; 373 } 374 375 if (state.sourceDid.startsWith("did:web:")) { 376 const credentials = await localClient.getRecommendedDidCredentials(); 377 state.targetVerificationMethod = 378 credentials.verificationMethods?.atproto || null; 379 setStep("did-web-update"); 380 } else { 381 setProgress({ currentOperation: "Requesting PLC operation token..." }); 382 await sourceClient.requestPlcOperationSignature(); 383 setStep("plc-token"); 384 } 385 } catch (e) { 386 const err = e as Error & { error?: string; status?: number }; 387 const message = err.message || err.error || 388 `Unknown error (status ${err.status || "unknown"})`; 389 setError(message); 390 } 391 } 392 393 async function resendEmailVerification(): Promise<void> { 394 if (!localClient) { 395 localClient = createLocalClient(); 396 } 397 await localClient.resendMigrationVerification(); 398 } 399 400 let checkingEmailVerification = false; 401 402 async function checkEmailVerifiedAndProceed(): Promise<boolean> { 403 if (checkingEmailVerification) return false; 404 if (!sourceClient || !localClient) return false; 405 406 checkingEmailVerification = true; 407 try { 408 await localClient.loginDeactivated( 409 state.targetEmail, 410 state.targetPassword, 411 ); 412 if (state.sourceDid.startsWith("did:web:")) { 413 const credentials = await localClient.getRecommendedDidCredentials(); 414 state.targetVerificationMethod = 415 credentials.verificationMethods?.atproto || null; 416 setStep("did-web-update"); 417 } else { 418 await sourceClient.requestPlcOperationSignature(); 419 setStep("plc-token"); 420 } 421 return true; 422 } catch (e) { 423 const err = e as Error & { error?: string }; 424 if (err.error === "AccountNotVerified") { 425 return false; 426 } 427 return false; 428 } finally { 429 checkingEmailVerification = false; 430 } 431 } 432 433 async function submitPlcToken(token: string): Promise<void> { 434 migrationLog("submitPlcToken START", { 435 sourceDid: state.sourceDid, 436 sourceHandle: state.sourceHandle, 437 targetHandle: state.targetHandle, 438 sourcePdsUrl: state.sourcePdsUrl, 439 }); 440 441 if (!sourceClient || !localClient) { 442 migrationLog("submitPlcToken ERROR: Not connected to PDSes", { 443 hasSourceClient: !!sourceClient, 444 hasLocalClient: !!localClient, 445 }); 446 throw new Error("Not connected to PDSes"); 447 } 448 449 state.plcToken = token; 450 setStep("finalizing"); 451 setProgress({ currentOperation: "Signing PLC operation..." }); 452 453 try { 454 migrationLog("Step 1: Getting recommended DID credentials from NEW PDS"); 455 const credentials = await localClient.getRecommendedDidCredentials(); 456 migrationLog("Step 1 COMPLETE: Got credentials", { 457 rotationKeys: credentials.rotationKeys, 458 alsoKnownAs: credentials.alsoKnownAs, 459 verificationMethods: credentials.verificationMethods, 460 services: credentials.services, 461 }); 462 463 migrationLog("Step 2: Signing PLC operation on OLD PDS", { 464 sourcePdsUrl: state.sourcePdsUrl, 465 }); 466 const signStart = Date.now(); 467 const { operation } = await sourceClient.signPlcOperation({ 468 token, 469 ...credentials, 470 }); 471 migrationLog("Step 2 COMPLETE: PLC operation signed", { 472 durationMs: Date.now() - signStart, 473 operationType: operation.type, 474 operationPrev: operation.prev, 475 }); 476 477 setProgress({ 478 plcSigned: true, 479 currentOperation: "Submitting PLC operation...", 480 }); 481 migrationLog("Step 3: Submitting PLC operation to NEW PDS"); 482 const submitStart = Date.now(); 483 await localClient.submitPlcOperation(operation); 484 migrationLog("Step 3 COMPLETE: PLC operation submitted", { 485 durationMs: Date.now() - submitStart, 486 }); 487 488 setProgress({ 489 currentOperation: "Activating account (waiting for DID propagation)...", 490 }); 491 migrationLog("Step 4: Activating account on NEW PDS"); 492 const activateStart = Date.now(); 493 await localClient.activateAccount(); 494 migrationLog("Step 4 COMPLETE: Account activated on NEW PDS", { 495 durationMs: Date.now() - activateStart, 496 }); 497 setProgress({ activated: true }); 498 499 setProgress({ currentOperation: "Deactivating old account..." }); 500 migrationLog("Step 5: Deactivating account on OLD PDS", { 501 sourcePdsUrl: state.sourcePdsUrl, 502 }); 503 const deactivateStart = Date.now(); 504 try { 505 await sourceClient.deactivateAccount(); 506 migrationLog("Step 5 COMPLETE: Account deactivated on OLD PDS", { 507 durationMs: Date.now() - deactivateStart, 508 success: true, 509 }); 510 setProgress({ deactivated: true }); 511 } catch (deactivateErr) { 512 const err = deactivateErr as Error & { 513 error?: string; 514 status?: number; 515 }; 516 migrationLog("Step 5 FAILED: Could not deactivate on OLD PDS", { 517 durationMs: Date.now() - deactivateStart, 518 error: err.message, 519 errorCode: err.error, 520 status: err.status, 521 }); 522 } 523 524 migrationLog("submitPlcToken SUCCESS: Migration complete", { 525 sourceDid: state.sourceDid, 526 newHandle: state.targetHandle, 527 }); 528 setStep("success"); 529 clearMigrationState(); 530 } catch (e) { 531 const err = e as Error & { error?: string; status?: number }; 532 const message = err.message || err.error || 533 `Unknown error (status ${err.status || "unknown"})`; 534 migrationLog("submitPlcToken FAILED", { 535 error: message, 536 errorCode: err.error, 537 status: err.status, 538 stack: err.stack, 539 }); 540 state.step = "plc-token"; 541 state.error = message; 542 saveMigrationState(state); 543 } 544 } 545 546 async function requestPlcToken(): Promise<void> { 547 if (!sourceClient) { 548 throw new Error("Not connected to source PDS"); 549 } 550 setProgress({ currentOperation: "Requesting PLC operation token..." }); 551 await sourceClient.requestPlcOperationSignature(); 552 } 553 554 async function resendPlcToken(): Promise<void> { 555 if (!sourceClient) { 556 throw new Error("Not connected to source PDS"); 557 } 558 await sourceClient.requestPlcOperationSignature(); 559 } 560 561 async function completeDidWebMigration(): Promise<void> { 562 migrationLog("completeDidWebMigration START", { 563 sourceDid: state.sourceDid, 564 sourceHandle: state.sourceHandle, 565 targetHandle: state.targetHandle, 566 }); 567 568 if (!sourceClient || !localClient) { 569 migrationLog("completeDidWebMigration ERROR: Not connected to PDSes"); 570 throw new Error("Not connected to PDSes"); 571 } 572 573 setStep("finalizing"); 574 setProgress({ currentOperation: "Activating account..." }); 575 576 try { 577 migrationLog("Activating account on NEW PDS"); 578 const activateStart = Date.now(); 579 await localClient.activateAccount(); 580 migrationLog("Account activated", { durationMs: Date.now() - activateStart }); 581 setProgress({ activated: true }); 582 583 setProgress({ currentOperation: "Deactivating old account..." }); 584 migrationLog("Deactivating account on OLD PDS"); 585 const deactivateStart = Date.now(); 586 try { 587 await sourceClient.deactivateAccount(); 588 migrationLog("Account deactivated on OLD PDS", { 589 durationMs: Date.now() - deactivateStart, 590 }); 591 setProgress({ deactivated: true }); 592 } catch (deactivateErr) { 593 const err = deactivateErr as Error & { error?: string }; 594 migrationLog("Could not deactivate on OLD PDS", { error: err.message }); 595 } 596 597 migrationLog("completeDidWebMigration SUCCESS"); 598 setStep("success"); 599 clearMigrationState(); 600 } catch (e) { 601 const err = e as Error & { error?: string; status?: number }; 602 const message = err.message || err.error || 603 `Unknown error (status ${err.status || "unknown"})`; 604 migrationLog("completeDidWebMigration FAILED", { error: message }); 605 setError(message); 606 setStep("did-web-update"); 607 } 608 } 609 610 function reset(): void { 611 state = { 612 direction: "inbound", 613 step: "welcome", 614 sourcePdsUrl: "", 615 sourceDid: "", 616 sourceHandle: "", 617 targetHandle: "", 618 targetEmail: "", 619 targetPassword: "", 620 inviteCode: "", 621 sourceAccessToken: null, 622 sourceRefreshToken: null, 623 serviceAuthToken: null, 624 emailVerifyToken: "", 625 plcToken: "", 626 progress: createInitialProgress(), 627 error: null, 628 requires2FA: false, 629 twoFactorCode: "", 630 targetVerificationMethod: null, 631 }; 632 sourceClient = null; 633 clearMigrationState(); 634 } 635 636 async function resumeFromState(stored: StoredMigrationState): Promise<void> { 637 if (stored.direction !== "inbound") return; 638 639 state.sourcePdsUrl = stored.sourcePdsUrl; 640 state.sourceDid = stored.sourceDid; 641 state.sourceHandle = stored.sourceHandle; 642 state.targetHandle = stored.targetHandle; 643 state.targetEmail = stored.targetEmail; 644 state.progress = { 645 ...createInitialProgress(), 646 ...stored.progress, 647 }; 648 649 state.step = "source-login"; 650 } 651 652 function getLocalSession(): 653 | { accessJwt: string; did: string; handle: string } 654 | null { 655 if (!localClient) return null; 656 const token = localClient.getAccessToken(); 657 if (!token) return null; 658 return { 659 accessJwt: token, 660 did: state.sourceDid, 661 handle: state.targetHandle, 662 }; 663 } 664 665 return { 666 get state() { 667 return state; 668 }, 669 setStep, 670 setError, 671 loadLocalServerInfo, 672 loginToSource, 673 authenticateToLocal, 674 checkHandleAvailability, 675 startMigration, 676 submitEmailVerifyToken, 677 resendEmailVerification, 678 checkEmailVerifiedAndProceed, 679 requestPlcToken, 680 submitPlcToken, 681 resendPlcToken, 682 completeDidWebMigration, 683 reset, 684 resumeFromState, 685 getLocalSession, 686 687 updateField<K extends keyof InboundMigrationState>( 688 field: K, 689 value: InboundMigrationState[K], 690 ) { 691 state[field] = value; 692 }, 693 }; 694} 695 696export function createOutboundMigrationFlow() { 697 let state = $state<OutboundMigrationState>({ 698 direction: "outbound", 699 step: "welcome", 700 localDid: "", 701 localHandle: "", 702 targetPdsUrl: "", 703 targetPdsDid: "", 704 targetHandle: "", 705 targetEmail: "", 706 targetPassword: "", 707 inviteCode: "", 708 targetAccessToken: null, 709 targetRefreshToken: null, 710 serviceAuthToken: null, 711 plcToken: "", 712 progress: createInitialProgress(), 713 error: null, 714 targetServerInfo: null, 715 }); 716 717 let localClient: AtprotoClient | null = null; 718 let targetClient: AtprotoClient | null = null; 719 720 function setStep(step: OutboundStep) { 721 state.step = step; 722 state.error = null; 723 saveMigrationState(state); 724 updateStep(step); 725 } 726 727 function setError(error: string) { 728 state.error = error; 729 saveMigrationState(state); 730 } 731 732 function setProgress(updates: Partial<MigrationProgress>) { 733 state.progress = { ...state.progress, ...updates }; 734 updateProgress(updates); 735 } 736 737 async function validateTargetPds(url: string): Promise<ServerDescription> { 738 const normalizedUrl = url.replace(/\/$/, ""); 739 targetClient = new AtprotoClient(normalizedUrl); 740 741 try { 742 const serverInfo = await targetClient.describeServer(); 743 state.targetPdsUrl = normalizedUrl; 744 state.targetPdsDid = serverInfo.did; 745 state.targetServerInfo = serverInfo; 746 return serverInfo; 747 } catch (e) { 748 throw new Error(`Could not connect to PDS: ${(e as Error).message}`); 749 } 750 } 751 752 function initLocalClient( 753 accessToken: string, 754 did?: string, 755 handle?: string, 756 ): void { 757 localClient = createLocalClient(); 758 localClient.setAccessToken(accessToken); 759 if (did) { 760 state.localDid = did; 761 } 762 if (handle) { 763 state.localHandle = handle; 764 } 765 } 766 767 async function startMigration(currentDid: string): Promise<void> { 768 if (!localClient || !targetClient) { 769 throw new Error("Not connected to PDSes"); 770 } 771 772 setStep("migrating"); 773 setProgress({ currentOperation: "Getting service auth token..." }); 774 775 try { 776 const { token } = await localClient.getServiceAuth( 777 state.targetPdsDid, 778 "com.atproto.server.createAccount", 779 ); 780 state.serviceAuthToken = token; 781 782 setProgress({ currentOperation: "Creating account on new PDS..." }); 783 784 const accountParams = { 785 did: currentDid, 786 handle: state.targetHandle, 787 email: state.targetEmail, 788 password: state.targetPassword, 789 inviteCode: state.inviteCode || undefined, 790 }; 791 792 const session = await targetClient.createAccount(accountParams, token); 793 state.targetAccessToken = session.accessJwt; 794 state.targetRefreshToken = session.refreshJwt; 795 targetClient.setAccessToken(session.accessJwt); 796 797 setProgress({ currentOperation: "Exporting repository..." }); 798 799 const car = await localClient.getRepo(currentDid); 800 setProgress({ 801 repoExported: true, 802 currentOperation: "Importing repository...", 803 }); 804 805 await targetClient.importRepo(car); 806 setProgress({ 807 repoImported: true, 808 currentOperation: "Counting blobs...", 809 }); 810 811 const accountStatus = await targetClient.checkAccountStatus(); 812 setProgress({ 813 blobsTotal: accountStatus.expectedBlobs, 814 currentOperation: "Migrating blobs...", 815 }); 816 817 await migrateBlobs(currentDid); 818 819 setProgress({ currentOperation: "Migrating preferences..." }); 820 await migratePreferences(); 821 822 setProgress({ currentOperation: "Requesting PLC operation token..." }); 823 await localClient.requestPlcOperationSignature(); 824 825 setStep("plc-token"); 826 } catch (e) { 827 const err = e as Error & { error?: string; status?: number }; 828 const message = err.message || err.error || 829 `Unknown error (status ${err.status || "unknown"})`; 830 setError(message); 831 setStep("error"); 832 } 833 } 834 835 async function migrateBlobs(did: string): Promise<void> { 836 if (!localClient || !targetClient) return; 837 838 let cursor: string | undefined; 839 let migrated = 0; 840 841 do { 842 const { blobs, cursor: nextCursor } = await targetClient.listMissingBlobs( 843 cursor, 844 100, 845 ); 846 847 for (const blob of blobs) { 848 try { 849 setProgress({ 850 currentOperation: `Migrating blob ${ 851 migrated + 1 852 }/${state.progress.blobsTotal}...`, 853 }); 854 855 const blobData = await localClient.getBlob(did, blob.cid); 856 await targetClient.uploadBlob(blobData, "application/octet-stream"); 857 migrated++; 858 setProgress({ blobsMigrated: migrated }); 859 } catch (e) { 860 state.progress.blobsFailed.push(blob.cid); 861 } 862 } 863 864 cursor = nextCursor; 865 } while (cursor); 866 } 867 868 async function migratePreferences(): Promise<void> { 869 if (!localClient || !targetClient) return; 870 871 try { 872 const prefs = await localClient.getPreferences(); 873 await targetClient.putPreferences(prefs); 874 setProgress({ prefsMigrated: true }); 875 } catch { 876 } 877 } 878 879 async function submitPlcToken(token: string): Promise<void> { 880 if (!localClient || !targetClient) { 881 throw new Error("Not connected to PDSes"); 882 } 883 884 state.plcToken = token; 885 setStep("finalizing"); 886 setProgress({ currentOperation: "Signing PLC operation..." }); 887 888 try { 889 const credentials = await targetClient.getRecommendedDidCredentials(); 890 891 const { operation } = await localClient.signPlcOperation({ 892 token, 893 ...credentials, 894 }); 895 896 setProgress({ 897 plcSigned: true, 898 currentOperation: "Submitting PLC operation...", 899 }); 900 901 await targetClient.submitPlcOperation(operation); 902 903 setProgress({ currentOperation: "Activating account on new PDS..." }); 904 await targetClient.activateAccount(); 905 setProgress({ activated: true }); 906 907 setProgress({ currentOperation: "Deactivating old account..." }); 908 try { 909 await localClient.deactivateAccount(state.targetPdsUrl); 910 setProgress({ deactivated: true }); 911 } catch { 912 } 913 914 setStep("success"); 915 clearMigrationState(); 916 } catch (e) { 917 const err = e as Error & { error?: string; status?: number }; 918 const message = err.message || err.error || 919 `Unknown error (status ${err.status || "unknown"})`; 920 setError(message); 921 setStep("plc-token"); 922 } 923 } 924 925 async function resendPlcToken(): Promise<void> { 926 if (!localClient) { 927 throw new Error("Not connected to local PDS"); 928 } 929 await localClient.requestPlcOperationSignature(); 930 } 931 932 function reset(): void { 933 state = { 934 direction: "outbound", 935 step: "welcome", 936 localDid: "", 937 localHandle: "", 938 targetPdsUrl: "", 939 targetPdsDid: "", 940 targetHandle: "", 941 targetEmail: "", 942 targetPassword: "", 943 inviteCode: "", 944 targetAccessToken: null, 945 targetRefreshToken: null, 946 serviceAuthToken: null, 947 plcToken: "", 948 progress: createInitialProgress(), 949 error: null, 950 targetServerInfo: null, 951 }; 952 localClient = null; 953 targetClient = null; 954 clearMigrationState(); 955 } 956 957 return { 958 get state() { 959 return state; 960 }, 961 setStep, 962 setError, 963 validateTargetPds, 964 initLocalClient, 965 startMigration, 966 submitPlcToken, 967 resendPlcToken, 968 reset, 969 970 updateField<K extends keyof OutboundMigrationState>( 971 field: K, 972 value: OutboundMigrationState[K], 973 ) { 974 state[field] = value; 975 }, 976 }; 977} 978 979export type InboundMigrationFlow = ReturnType< 980 typeof createInboundMigrationFlow 981>; 982export type OutboundMigrationFlow = ReturnType< 983 typeof createOutboundMigrationFlow 984>;