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