Graphical PDS migrator for AT Protocol

fix migration logout bug, add did check to every step and verification

+35 -9
.zed/settings.json
··· 1 + // Folder-specific settings 2 + // 3 + // For a full list of overridable settings, and general information on folder-specific settings, 4 + // see the documentation: https://zed.dev/docs/configuring-zed#settings-files 1 5 { 6 + "lsp": { 7 + "deno": { 8 + "settings": { 9 + "deno": { 10 + "enable": true, 11 + "cacheOnSave": true, 12 + "suggest": { 13 + "imports": { 14 + "autoDiscover": true 15 + } 16 + } 17 + } 18 + } 19 + } 20 + }, 2 21 "languages": { 22 + "JavaScript": { 23 + "language_servers": [ 24 + "deno", 25 + "!vtsls", 26 + "!eslint", 27 + "..." 28 + ] 29 + }, 3 30 "TypeScript": { 4 31 "language_servers": [ 5 - "wakatime", 6 32 "deno", 7 33 "!typescript-language-server", 8 34 "!vtsls", 9 - "!eslint" 10 - ], 11 - "formatter": "language_server" 35 + "!eslint", 36 + "..." 37 + ] 12 38 }, 13 39 "TSX": { 14 40 "language_servers": [ 15 - "wakatime", 16 41 "deno", 17 42 "!typescript-language-server", 18 43 "!vtsls", 19 - "!eslint" 20 - ], 21 - "formatter": "language_server" 44 + "!eslint", 45 + "..." 46 + ] 22 47 } 23 - } 48 + }, 49 + "formatter": "language_server" 24 50 }
+155 -66
islands/MigrationProgress.tsx
··· 40 40 */ 41 41 export default function MigrationProgress(props: MigrationProgressProps) { 42 42 const [token, setToken] = useState(""); 43 - const [migrationState, setMigrationState] = useState<MigrationStateInfo | null>(null); 43 + const [migrationState, setMigrationState] = useState< 44 + MigrationStateInfo | null 45 + >(null); 44 46 45 47 const [steps, setSteps] = useState<MigrationStep[]>([ 46 48 { name: "Create Account", status: "pending" }, ··· 139 141 const getStepDisplayName = (step: MigrationStep, index: number) => { 140 142 if (step.status === "completed") { 141 143 switch (index) { 142 - case 0: return "Account Created"; 143 - case 1: return "Data Migrated"; 144 - case 2: return "Identity Migrated"; 145 - case 3: return "Migration Finalized"; 144 + case 0: 145 + return "Account Created"; 146 + case 1: 147 + return "Data Migrated"; 148 + case 2: 149 + return "Identity Migrated"; 150 + case 3: 151 + return "Migration Finalized"; 146 152 } 147 153 } 148 154 149 155 if (step.status === "in-progress") { 150 156 switch (index) { 151 - case 0: return "Creating your new account..."; 152 - case 1: return "Migrating your data..."; 153 - case 2: return step.name === "Enter the token sent to your email to complete identity migration" 154 - ? step.name 155 - : "Migrating your identity..."; 156 - case 3: return "Finalizing migration..."; 157 + case 0: 158 + return "Creating your new account..."; 159 + case 1: 160 + return "Migrating your data..."; 161 + case 2: 162 + return step.name === 163 + "Enter the token sent to your email to complete identity migration" 164 + ? step.name 165 + : "Migrating your identity..."; 166 + case 3: 167 + return "Finalizing migration..."; 157 168 } 158 169 } 159 170 160 171 if (step.status === "verifying") { 161 172 switch (index) { 162 - case 0: return "Verifying account creation..."; 163 - case 1: return "Verifying data migration..."; 164 - case 2: return "Verifying identity migration..."; 165 - case 3: return "Verifying migration completion..."; 173 + case 0: 174 + return "Verifying account creation..."; 175 + case 1: 176 + return "Verifying data migration..."; 177 + case 2: 178 + return "Verifying identity migration..."; 179 + case 3: 180 + return "Verifying migration completion..."; 166 181 } 167 182 } 168 183 ··· 268 283 console.error("Blob migration: Error response:", json); 269 284 throw new Error(json.message || "Failed to migrate blobs"); 270 285 } catch { 271 - console.error("Blob migration: Non-JSON error response:", blobsText); 286 + console.error( 287 + "Blob migration: Non-JSON error response:", 288 + blobsText, 289 + ); 272 290 throw new Error(blobsText || "Failed to migrate blobs"); 273 291 } 274 292 } ··· 290 308 console.error("Preferences migration: Error response:", json); 291 309 throw new Error(json.message || "Failed to migrate preferences"); 292 310 } catch { 293 - console.error("Preferences migration: Non-JSON error response:", prefsText); 311 + console.error( 312 + "Preferences migration: Non-JSON error response:", 313 + prefsText, 314 + ); 294 315 throw new Error(prefsText || "Failed to migrate preferences"); 295 316 } 296 317 } ··· 329 350 if (!requestRes.ok) { 330 351 try { 331 352 const json = JSON.parse(requestText); 332 - throw new Error(json.message || "Failed to request identity migration"); 353 + throw new Error( 354 + json.message || "Failed to request identity migration", 355 + ); 333 356 } catch { 334 - throw new Error(requestText || "Failed to request identity migration"); 357 + throw new Error( 358 + requestText || "Failed to request identity migration", 359 + ); 335 360 } 336 361 } 337 362 ··· 345 370 console.log("Identity migration requested successfully"); 346 371 347 372 // Update step name to prompt for token 348 - setSteps(prevSteps => 373 + setSteps((prevSteps) => 349 374 prevSteps.map((step, i) => 350 375 i === 2 351 - ? { ...step, name: "Enter the token sent to your email to complete identity migration" } 376 + ? { 377 + ...step, 378 + name: 379 + "Enter the token sent to your email to complete identity migration", 380 + } 352 381 : step 353 382 ) 354 383 ); ··· 389 418 if (!identityRes.ok) { 390 419 try { 391 420 const json = JSON.parse(identityData); 392 - throw new Error(json.message || "Failed to complete identity migration"); 421 + throw new Error( 422 + json.message || "Failed to complete identity migration", 423 + ); 393 424 } catch { 394 - throw new Error(identityData || "Failed to complete identity migration"); 425 + throw new Error( 426 + identityData || "Failed to complete identity migration", 427 + ); 395 428 } 396 429 } 397 430 ··· 404 437 } catch { 405 438 throw new Error("Invalid response from server"); 406 439 } 407 - 408 440 409 441 updateStepStatus(2, "verifying"); 410 442 const verified = await verifyStep(2); ··· 554 586 updateStepStatus(stepNum, "completed"); 555 587 return true; 556 588 } else { 557 - console.log(`Verification: Step ${stepNum + 1} is not ready:`, data.reason); 589 + console.log( 590 + `Verification: Step ${stepNum + 1} is not ready:`, 591 + data.reason, 592 + ); 558 593 const statusDetails = { 559 594 activated: data.activated, 560 595 validDid: data.validDid, ··· 565 600 indexedRecords: data.indexedRecords, 566 601 privateStateValues: data.privateStateValues, 567 602 expectedBlobs: data.expectedBlobs, 568 - importedBlobs: data.importedBlobs 603 + importedBlobs: data.importedBlobs, 569 604 }; 570 - console.log(`Verification: Step ${stepNum + 1} status details:`, statusDetails); 571 - const errorMessage = `${data.reason || "Verification failed"}\nStatus details: ${JSON.stringify(statusDetails, null, 2)}`; 605 + console.log( 606 + `Verification: Step ${stepNum + 1} status details:`, 607 + statusDetails, 608 + ); 609 + const errorMessage = `${ 610 + data.reason || "Verification failed" 611 + }\nStatus details: ${JSON.stringify(statusDetails, null, 2)}`; 572 612 updateStepStatus(stepNum, "error", errorMessage); 573 613 return false; 574 614 } 575 615 } catch (e) { 576 616 console.error(`Verification: Error in step ${stepNum + 1}:`, e); 577 - updateStepStatus(stepNum, "error", e instanceof Error ? e.message : String(e)); 617 + updateStepStatus( 618 + stepNum, 619 + "error", 620 + e instanceof Error ? e.message : String(e), 621 + ); 578 622 return false; 579 623 } 580 624 }; ··· 583 627 <div class="space-y-8"> 584 628 {/* Migration state alert */} 585 629 {migrationState && !migrationState.allowMigration && ( 586 - <div class={`p-4 rounded-lg border ${ 587 - migrationState.state === "maintenance" 588 - ? "bg-yellow-50 border-yellow-200 text-yellow-800 dark:bg-yellow-900/20 dark:border-yellow-800 dark:text-yellow-200" 589 - : "bg-red-50 border-red-200 text-red-800 dark:bg-red-900/20 dark:border-red-800 dark:text-red-200" 590 - }`}> 630 + <div 631 + class={`p-4 rounded-lg border ${ 632 + migrationState.state === "maintenance" 633 + ? "bg-yellow-50 border-yellow-200 text-yellow-800 dark:bg-yellow-900/20 dark:border-yellow-800 dark:text-yellow-200" 634 + : "bg-red-50 border-red-200 text-red-800 dark:bg-red-900/20 dark:border-red-800 dark:text-red-200" 635 + }`} 636 + > 591 637 <div class="flex items-center"> 592 - <div class={`mr-3 ${ 593 - migrationState.state === "maintenance" ? "text-yellow-600 dark:text-yellow-400" : "text-red-600 dark:text-red-400" 594 - }`}> 638 + <div 639 + class={`mr-3 ${ 640 + migrationState.state === "maintenance" 641 + ? "text-yellow-600 dark:text-yellow-400" 642 + : "text-red-600 dark:text-red-400" 643 + }`} 644 + > 595 645 {migrationState.state === "maintenance" ? "⚠️" : "🚫"} 596 646 </div> 597 647 <div> 598 648 <h3 class="font-semibold mb-1"> 599 - {migrationState.state === "maintenance" ? "Maintenance Mode" : "Service Unavailable"} 649 + {migrationState.state === "maintenance" 650 + ? "Maintenance Mode" 651 + : "Service Unavailable"} 600 652 </h3> 601 653 <p class="text-sm">{migrationState.message}</p> 602 654 </div> ··· 635 687 </p> 636 688 )} 637 689 {index === 2 && step.status === "in-progress" && 638 - step.name === "Enter the token sent to your email to complete identity migration" && ( 690 + step.name === 691 + "Enter the token sent to your email to complete identity migration" && 692 + ( 639 693 <div class="mt-4 space-y-4"> 640 694 <p class="text-sm text-blue-800 dark:text-blue-200"> 641 - Please check your email for the migration token and enter it below: 695 + Please check your email for the migration token and enter 696 + it below: 642 697 </p> 643 698 <div class="flex space-x-2"> 644 699 <input ··· 657 712 </button> 658 713 </div> 659 714 </div> 660 - ) 661 - } 715 + )} 662 716 </div> 663 717 </div> 664 718 ))} ··· 666 720 667 721 {steps[3].status === "completed" && ( 668 722 <div class="p-4 bg-green-50 dark:bg-green-900 rounded-lg border-2 border-green-200 dark:border-green-800"> 669 - <p class="text-sm text-green-800 dark:text-green-200"> 670 - Migration completed successfully! You can now close this page. 723 + <p class="text-sm text-green-800 dark:text-green-200 pb-2"> 724 + Migration completed successfully! Sign out to finish the process and 725 + return home.<br /> 726 + Please consider donating to Airport to support server and 727 + development costs. 671 728 </p> 672 - <button 673 - type="button" 674 - onClick={async () => { 675 - try { 676 - const response = await fetch("/api/logout", { 677 - method: "POST", 678 - credentials: "include", 679 - }); 680 - if (!response.ok) { 681 - throw new Error("Logout failed"); 729 + <div class="flex space-x-4"> 730 + <button 731 + type="button" 732 + onClick={async () => { 733 + try { 734 + const response = await fetch("/api/logout", { 735 + method: "POST", 736 + credentials: "include", 737 + }); 738 + if (!response.ok) { 739 + throw new Error("Logout failed"); 740 + } 741 + globalThis.location.href = "/"; 742 + } catch (error) { 743 + console.error("Failed to logout:", error); 682 744 } 683 - globalThis.location.href = "/"; 684 - } catch (error) { 685 - console.error("Failed to logout:", error); 686 - } 687 - }} 688 - class="mt-4 mr-4 px-4 py-2 bg-green-600 hover:bg-green-700 text-white rounded-md transition-colors duration-200" 689 - > 690 - Sign Out 691 - </button> 692 - <a href="https://ko-fi.com/knotbin" target="_blank" class="mt-4 px-4 py-2 bg-green-600 hover:bg-green-700 text-white rounded-md transition-colors duration-200"> 693 - Donate 694 - </a> 745 + }} 746 + class="px-4 py-2 bg-green-600 hover:bg-green-700 text-white rounded-md transition-colors duration-200 flex items-center space-x-2" 747 + > 748 + <svg 749 + class="w-5 h-5" 750 + fill="none" 751 + stroke="currentColor" 752 + viewBox="0 0 24 24" 753 + > 754 + <path 755 + stroke-linecap="round" 756 + stroke-linejoin="round" 757 + stroke-width="2" 758 + d="M17 16l4-4m0 0l-4-4m4 4H7m6 4v1a3 3 0 01-3 3H6a3 3 0 01-3-3V7a3 3 0 013-3h4a3 3 0 013 3v1" 759 + /> 760 + </svg> 761 + <span>Sign Out</span> 762 + </button> 763 + <a 764 + href="https://ko-fi.com/knotbin" 765 + target="_blank" 766 + class="px-4 py-2 bg-green-600 hover:bg-green-700 text-white rounded-md transition-colors duration-200 flex items-center space-x-2" 767 + > 768 + <svg 769 + class="w-5 h-5" 770 + fill="none" 771 + stroke="currentColor" 772 + viewBox="0 0 24 24" 773 + > 774 + <path 775 + stroke-linecap="round" 776 + stroke-linejoin="round" 777 + stroke-width="2" 778 + d="M4.318 6.318a4.5 4.5 0 000 6.364L12 20.364l7.682-7.682a4.5 4.5 0 00-6.364-6.364L12 7.636l-1.318-1.318a4.5 4.5 0 00-6.364 0z" 779 + /> 780 + </svg> 781 + <span>Support Us</span> 782 + </a> 783 + </div> 695 784 </div> 696 785 )} 697 786 </div>
+7
lib/check-dids.ts
··· 1 + import { getSession } from "./sessions.ts"; 2 + 3 + export async function checkDidsMatch(req: Request): Promise<boolean> { 4 + const oldSession = await getSession(req, undefined, false); 5 + const newSession = await getSession(req, undefined, true); 6 + return oldSession.did === newSession.did; 7 + }
+1 -1
lib/migration-state.ts
··· 28 28 case "maintenance": 29 29 return { 30 30 state: "maintenance", 31 - message: "Migration services are temporarily unavailable for scheduled maintenance. Please try again later.", 31 + message: "Migration services are temporarily unavailable for maintenance. Please try again later.", 32 32 allowMigration: false, 33 33 }; 34 34
+31 -10
lib/sessions.ts
··· 1 1 import { Agent } from "npm:@atproto/api"; 2 - import { OauthSession, CredentialSession } from "./types.ts"; 3 - import { getCredentialSession, getCredentialSessionAgent } from "./cred/sessions.ts"; 2 + import { CredentialSession, OauthSession } from "./types.ts"; 3 + import { 4 + getCredentialSession, 5 + getCredentialSessionAgent, 6 + } from "./cred/sessions.ts"; 4 7 import { getOauthSession, getOauthSessionAgent } from "./oauth/sessions.ts"; 5 8 import { IronSession } from "npm:iron-session"; 6 9 ··· 14 17 export async function getSession( 15 18 req: Request, 16 19 res: Response = new Response(), 17 - isMigration: boolean = false 20 + isMigration: boolean = false, 18 21 ): Promise<IronSession<OauthSession | CredentialSession>> { 19 22 if (isMigration) { 20 23 return await getCredentialSession(req, res, true); ··· 23 26 const credentialSession = await getCredentialSession(req, res); 24 27 25 28 if (oauthSession.did) { 26 - console.log("Oauth session found") 29 + console.log("Oauth session found"); 27 30 return oauthSession; 28 31 } 29 32 if (credentialSession.did) { ··· 43 46 export async function getSessionAgent( 44 47 req: Request, 45 48 res: Response = new Response(), 46 - isMigration: boolean = false 49 + isMigration: boolean = false, 47 50 ): Promise<Agent | null> { 48 51 if (isMigration) { 49 52 return await getCredentialSessionAgent(req, res, isMigration); 50 53 } 51 54 52 55 const oauthAgent = await getOauthSessionAgent(req); 53 - const credentialAgent = await getCredentialSessionAgent(req, res, isMigration); 56 + const credentialAgent = await getCredentialSessionAgent( 57 + req, 58 + res, 59 + isMigration, 60 + ); 54 61 55 62 if (oauthAgent) { 56 63 return oauthAgent; ··· 66 73 /** 67 74 * Destroy all sessions for the given request. 68 75 * @param req - The request object 76 + * @param res - The response object 69 77 */ 70 - export async function destroyAllSessions(req: Request) { 71 - const oauthSession = await getOauthSession(req); 72 - const credentialSession = await getCredentialSession(req); 73 - const migrationSession = await getCredentialSession(req, new Response(), true); 78 + export async function destroyAllSessions( 79 + req: Request, 80 + res?: Response, 81 + ): Promise<Response> { 82 + const response = res || new Response(); 83 + const oauthSession = await getOauthSession(req, response); 84 + const credentialSession = await getCredentialSession(req, res); 85 + const migrationSession = await getCredentialSession( 86 + req, 87 + res, 88 + true, 89 + ); 74 90 75 91 if (oauthSession.did) { 76 92 oauthSession.destroy(); ··· 79 95 credentialSession.destroy(); 80 96 } 81 97 if (migrationSession.did) { 98 + console.log("DESTROYING MIGRATION SESSION", migrationSession); 82 99 migrationSession.destroy(); 100 + } else { 101 + console.log("MIGRATION SESSION NOT FOUND", migrationSession); 83 102 } 103 + 104 + return response; 84 105 }
+1 -1
lib/storage.ts
··· 3 3 NodeSavedSessionStore, 4 4 NodeSavedState, 5 5 NodeSavedStateStore, 6 - } from "jsr:@bigmoves/atproto-oauth-client"; 6 + } from "@bigmoves/atproto-oauth-client"; 7 7 8 8 /** 9 9 * The state store for sessions.
+4 -4
routes/api/logout.ts
··· 1 - import { getSession, destroyAllSessions } from "../../lib/sessions.ts"; 1 + import { destroyAllSessions, getSession } from "../../lib/sessions.ts"; 2 2 import { oauthClient } from "../../lib/oauth/client.ts"; 3 3 import { define } from "../../utils.ts"; 4 4 ··· 13 13 if (session.did) { 14 14 // Try to revoke both types of sessions - the one that doesn't exist will just no-op 15 15 await Promise.all([ 16 - oauthClient.revoke(session.did).catch(console.error) 16 + oauthClient.revoke(session.did).catch(console.error), 17 17 ]); 18 18 // Then destroy the iron session 19 19 session.destroy(); 20 20 } 21 21 22 22 // Destroy all sessions including migration session 23 - await destroyAllSessions(req); 23 + const result = await destroyAllSessions(req, response); 24 24 25 - return response; 25 + return result; 26 26 } catch (error: unknown) { 27 27 const err = error instanceof Error ? error : new Error(String(error)); 28 28 console.error("Logout failed:", err.message);
+2 -2
routes/api/migrate/create.ts
··· 45 45 return new Response("Could not create new agent", { status: 400 }); 46 46 } 47 47 48 - console.log("getting did") 48 + console.log("getting did"); 49 49 const session = await oldAgent.com.atproto.server.getSession(); 50 50 const accountDid = session.data.did; 51 - console.log("got did") 51 + console.log("got did"); 52 52 const describeRes = await newAgent.com.atproto.server.describeServer(); 53 53 const newServerDid = describeRes.data.did; 54 54 const inviteRequired = describeRes.data.inviteCodeRequired ?? false;
+178 -44
routes/api/migrate/data/blobs.ts
··· 1 1 import { getSessionAgent } from "../../../../lib/sessions.ts"; 2 + import { checkDidsMatch } from "../../../../lib/check-dids.ts"; 2 3 import { define } from "../../../../utils.ts"; 3 4 import { assertMigrationAllowed } from "../../../../lib/migration-state.ts"; 4 5 ··· 41 42 ); 42 43 } 43 44 45 + // Verify DIDs match between sessions 46 + const didsMatch = await checkDidsMatch(ctx.req); 47 + if (!didsMatch) { 48 + return new Response( 49 + JSON.stringify({ 50 + success: false, 51 + message: "Invalid state, original and target DIDs do not match", 52 + }), 53 + { 54 + status: 400, 55 + headers: { "Content-Type": "application/json" }, 56 + }, 57 + ); 58 + } 59 + 44 60 // Migrate blobs 45 61 const migrationLogs: string[] = []; 46 62 const migratedBlobs: string[] = []; ··· 52 68 53 69 const startTime = Date.now(); 54 70 console.log(`[${new Date().toISOString()}] Starting blob migration...`); 55 - migrationLogs.push(`[${new Date().toISOString()}] Starting blob migration...`); 71 + migrationLogs.push( 72 + `[${new Date().toISOString()}] Starting blob migration...`, 73 + ); 56 74 57 75 // First count total blobs 58 76 console.log(`[${new Date().toISOString()}] Starting blob count...`); 59 - migrationLogs.push(`[${new Date().toISOString()}] Starting blob count...`); 77 + migrationLogs.push( 78 + `[${new Date().toISOString()}] Starting blob count...`, 79 + ); 60 80 61 81 const session = await oldAgent.com.atproto.server.getSession(); 62 82 const accountDid = session.data.did; 63 83 64 84 do { 65 85 const pageStartTime = Date.now(); 66 - console.log(`[${new Date().toISOString()}] Counting blobs on page ${pageCount + 1}...`); 67 - migrationLogs.push(`[${new Date().toISOString()}] Counting blobs on page ${pageCount + 1}...`); 86 + console.log( 87 + `[${new Date().toISOString()}] Counting blobs on page ${ 88 + pageCount + 1 89 + }...`, 90 + ); 91 + migrationLogs.push( 92 + `[${new Date().toISOString()}] Counting blobs on page ${ 93 + pageCount + 1 94 + }...`, 95 + ); 68 96 const listedBlobs = await oldAgent.com.atproto.sync.listBlobs({ 69 97 did: accountDid, 70 98 cursor: blobCursor, ··· 74 102 totalBlobs += newBlobs; 75 103 const pageTime = Date.now() - pageStartTime; 76 104 77 - console.log(`[${new Date().toISOString()}] Found ${newBlobs} blobs on page ${pageCount + 1} in ${pageTime/1000} seconds, total so far: ${totalBlobs}`); 78 - migrationLogs.push(`[${new Date().toISOString()}] Found ${newBlobs} blobs on page ${pageCount + 1} in ${pageTime/1000} seconds, total so far: ${totalBlobs}`); 105 + console.log( 106 + `[${new Date().toISOString()}] Found ${newBlobs} blobs on page ${ 107 + pageCount + 1 108 + } in ${pageTime / 1000} seconds, total so far: ${totalBlobs}`, 109 + ); 110 + migrationLogs.push( 111 + `[${new Date().toISOString()}] Found ${newBlobs} blobs on page ${ 112 + pageCount + 1 113 + } in ${pageTime / 1000} seconds, total so far: ${totalBlobs}`, 114 + ); 79 115 80 116 pageCount++; 81 117 blobCursor = listedBlobs.data.cursor; 82 118 } while (blobCursor); 83 119 84 - console.log(`[${new Date().toISOString()}] Total blobs to migrate: ${totalBlobs}`); 85 - migrationLogs.push(`[${new Date().toISOString()}] Total blobs to migrate: ${totalBlobs}`); 120 + console.log( 121 + `[${new Date().toISOString()}] Total blobs to migrate: ${totalBlobs}`, 122 + ); 123 + migrationLogs.push( 124 + `[${new Date().toISOString()}] Total blobs to migrate: ${totalBlobs}`, 125 + ); 86 126 87 127 // Reset cursor for actual migration 88 128 blobCursor = undefined; ··· 91 131 92 132 do { 93 133 const pageStartTime = Date.now(); 94 - console.log(`[${new Date().toISOString()}] Fetching blob list page ${pageCount + 1}...`); 95 - migrationLogs.push(`[${new Date().toISOString()}] Fetching blob list page ${pageCount + 1}...`); 134 + console.log( 135 + `[${new Date().toISOString()}] Fetching blob list page ${ 136 + pageCount + 1 137 + }...`, 138 + ); 139 + migrationLogs.push( 140 + `[${new Date().toISOString()}] Fetching blob list page ${ 141 + pageCount + 1 142 + }...`, 143 + ); 96 144 97 145 const listedBlobs = await oldAgent.com.atproto.sync.listBlobs({ 98 146 did: accountDid, ··· 100 148 }); 101 149 102 150 const pageTime = Date.now() - pageStartTime; 103 - console.log(`[${new Date().toISOString()}] Found ${listedBlobs.data.cids.length} blobs on page ${pageCount + 1} in ${pageTime/1000} seconds`); 104 - migrationLogs.push(`[${new Date().toISOString()}] Found ${listedBlobs.data.cids.length} blobs on page ${pageCount + 1} in ${pageTime/1000} seconds`); 151 + console.log( 152 + `[${ 153 + new Date().toISOString() 154 + }] Found ${listedBlobs.data.cids.length} blobs on page ${ 155 + pageCount + 1 156 + } in ${pageTime / 1000} seconds`, 157 + ); 158 + migrationLogs.push( 159 + `[${ 160 + new Date().toISOString() 161 + }] Found ${listedBlobs.data.cids.length} blobs on page ${ 162 + pageCount + 1 163 + } in ${pageTime / 1000} seconds`, 164 + ); 105 165 106 166 blobCursor = listedBlobs.data.cursor; 107 167 108 168 for (const cid of listedBlobs.data.cids) { 109 169 try { 110 170 const blobStartTime = Date.now(); 111 - console.log(`[${new Date().toISOString()}] Starting migration for blob ${cid} (${processedBlobs + 1} of ${totalBlobs})...`); 112 - migrationLogs.push(`[${new Date().toISOString()}] Starting migration for blob ${cid} (${processedBlobs + 1} of ${totalBlobs})...`); 171 + console.log( 172 + `[${ 173 + new Date().toISOString() 174 + }] Starting migration for blob ${cid} (${ 175 + processedBlobs + 1 176 + } of ${totalBlobs})...`, 177 + ); 178 + migrationLogs.push( 179 + `[${ 180 + new Date().toISOString() 181 + }] Starting migration for blob ${cid} (${ 182 + processedBlobs + 1 183 + } of ${totalBlobs})...`, 184 + ); 113 185 114 186 const blobRes = await oldAgent.com.atproto.sync.getBlob({ 115 187 did: accountDid, ··· 123 195 124 196 const size = parseInt(contentLength, 10); 125 197 if (isNaN(size)) { 126 - throw new Error(`Blob ${cid} has invalid content length: ${contentLength}`); 198 + throw new Error( 199 + `Blob ${cid} has invalid content length: ${contentLength}`, 200 + ); 127 201 } 128 202 129 203 const MAX_SIZE = 200 * 1024 * 1024; // 200MB 130 204 if (size > MAX_SIZE) { 131 - throw new Error(`Blob ${cid} exceeds maximum size limit (${size} bytes)`); 205 + throw new Error( 206 + `Blob ${cid} exceeds maximum size limit (${size} bytes)`, 207 + ); 132 208 } 133 209 134 - console.log(`[${new Date().toISOString()}] Downloading blob ${cid} (${size} bytes)...`); 135 - migrationLogs.push(`[${new Date().toISOString()}] Downloading blob ${cid} (${size} bytes)...`); 210 + console.log( 211 + `[${ 212 + new Date().toISOString() 213 + }] Downloading blob ${cid} (${size} bytes)...`, 214 + ); 215 + migrationLogs.push( 216 + `[${ 217 + new Date().toISOString() 218 + }] Downloading blob ${cid} (${size} bytes)...`, 219 + ); 136 220 137 221 if (!blobRes.data) { 138 - throw new Error(`Failed to download blob ${cid}: No data received`); 222 + throw new Error( 223 + `Failed to download blob ${cid}: No data received`, 224 + ); 139 225 } 140 226 141 - console.log(`[${new Date().toISOString()}] Uploading blob ${cid} to new account...`); 142 - migrationLogs.push(`[${new Date().toISOString()}] Uploading blob ${cid} to new account...`); 227 + console.log( 228 + `[${ 229 + new Date().toISOString() 230 + }] Uploading blob ${cid} to new account...`, 231 + ); 232 + migrationLogs.push( 233 + `[${ 234 + new Date().toISOString() 235 + }] Uploading blob ${cid} to new account...`, 236 + ); 143 237 144 238 try { 145 239 await newAgent.com.atproto.repo.uploadBlob(blobRes.data); 146 240 const blobTime = Date.now() - blobStartTime; 147 - console.log(`[${new Date().toISOString()}] Successfully migrated blob ${cid} in ${blobTime/1000} seconds`); 148 - migrationLogs.push(`[${new Date().toISOString()}] Successfully migrated blob ${cid} in ${blobTime/1000} seconds`); 241 + console.log( 242 + `[${ 243 + new Date().toISOString() 244 + }] Successfully migrated blob ${cid} in ${ 245 + blobTime / 1000 246 + } seconds`, 247 + ); 248 + migrationLogs.push( 249 + `[${ 250 + new Date().toISOString() 251 + }] Successfully migrated blob ${cid} in ${ 252 + blobTime / 1000 253 + } seconds`, 254 + ); 149 255 migratedBlobs.push(cid); 150 256 } catch (uploadError) { 151 - console.error(`[${new Date().toISOString()}] Failed to upload blob ${cid}:`, uploadError); 152 - throw new Error(`Upload failed: ${uploadError instanceof Error ? uploadError.message : String(uploadError)}`); 257 + console.error( 258 + `[${new Date().toISOString()}] Failed to upload blob ${cid}:`, 259 + uploadError, 260 + ); 261 + throw new Error( 262 + `Upload failed: ${ 263 + uploadError instanceof Error 264 + ? uploadError.message 265 + : String(uploadError) 266 + }`, 267 + ); 153 268 } 154 269 } catch (error) { 155 - const errorMessage = error instanceof Error ? error.message : String(error); 156 - const detailedError = `[${new Date().toISOString()}] Failed to migrate blob ${cid}: ${errorMessage}`; 270 + const errorMessage = error instanceof Error 271 + ? error.message 272 + : String(error); 273 + const detailedError = `[${ 274 + new Date().toISOString() 275 + }] Failed to migrate blob ${cid}: ${errorMessage}`; 157 276 console.error(detailedError); 158 - console.error('Full error details:', error); 277 + console.error("Full error details:", error); 159 278 migrationLogs.push(detailedError); 160 279 failedBlobs.push(cid); 161 280 } 162 281 163 282 processedBlobs++; 164 - const progressLog = `[${new Date().toISOString()}] Progress: ${processedBlobs}/${totalBlobs} blobs processed (${Math.round((processedBlobs/totalBlobs)*100)}%)`; 283 + const progressLog = `[${ 284 + new Date().toISOString() 285 + }] Progress: ${processedBlobs}/${totalBlobs} blobs processed (${ 286 + Math.round((processedBlobs / totalBlobs) * 100) 287 + }%)`; 165 288 console.log(progressLog); 166 289 migrationLogs.push(progressLog); 167 290 } ··· 169 292 } while (blobCursor); 170 293 171 294 const totalTime = Date.now() - startTime; 172 - const completionMessage = `[${new Date().toISOString()}] Blob migration completed in ${totalTime/1000} seconds: ${migratedBlobs.length} blobs migrated${failedBlobs.length > 0 ? `, ${failedBlobs.length} failed` : ''} (${pageCount} pages processed)`; 295 + const completionMessage = `[${ 296 + new Date().toISOString() 297 + }] Blob migration completed in ${ 298 + totalTime / 1000 299 + } seconds: ${migratedBlobs.length} blobs migrated${ 300 + failedBlobs.length > 0 ? `, ${failedBlobs.length} failed` : "" 301 + } (${pageCount} pages processed)`; 173 302 console.log(completionMessage); 174 303 migrationLogs.push(completionMessage); 175 304 ··· 187 316 totalBlobs, 188 317 logs: migrationLogs, 189 318 timing: { 190 - totalTime: totalTime/1000 191 - } 319 + totalTime: totalTime / 1000, 320 + }, 192 321 }), 193 322 { 194 323 status: 200, 195 324 headers: { 196 325 "Content-Type": "application/json", 197 326 ...Object.fromEntries(res.headers), 198 - } 199 - } 327 + }, 328 + }, 200 329 ); 201 330 } catch (error) { 202 331 const message = error instanceof Error ? error.message : String(error); 203 - console.error(`[${new Date().toISOString()}] Blob migration error:`, message); 204 - console.error('Full error details:', error); 332 + console.error( 333 + `[${new Date().toISOString()}] Blob migration error:`, 334 + message, 335 + ); 336 + console.error("Full error details:", error); 205 337 return new Response( 206 338 JSON.stringify({ 207 339 success: false, 208 340 message: `Blob migration failed: ${message}`, 209 - error: error instanceof Error ? { 210 - name: error.name, 211 - message: error.message, 212 - stack: error.stack, 213 - } : String(error) 341 + error: error instanceof Error 342 + ? { 343 + name: error.name, 344 + message: error.message, 345 + stack: error.stack, 346 + } 347 + : String(error), 214 348 }), 215 349 { 216 350 status: 500, 217 351 headers: { 218 352 "Content-Type": "application/json", 219 353 ...Object.fromEntries(res.headers), 220 - } 221 - } 354 + }, 355 + }, 222 356 ); 223 357 } 224 - } 358 + }, 225 359 });
+92 -34
routes/api/migrate/data/prefs.ts
··· 1 1 import { getSessionAgent } from "../../../../lib/sessions.ts"; 2 + import { checkDidsMatch } from "../../../../lib/check-dids.ts"; 2 3 import { define } from "../../../../utils.ts"; 3 4 import { assertMigrationAllowed } from "../../../../lib/migration-state.ts"; 4 5 ··· 17 18 console.log("Preferences migration: Got new agent:", !!newAgent); 18 19 19 20 if (!oldAgent || !newAgent) { 20 - return new Response(JSON.stringify({ 21 - success: false, 22 - message: "Not authenticated" 23 - }), { 24 - status: 401, 25 - headers: { "Content-Type": "application/json" } 26 - }); 21 + return new Response( 22 + JSON.stringify({ 23 + success: false, 24 + message: "Not authenticated", 25 + }), 26 + { 27 + status: 401, 28 + headers: { "Content-Type": "application/json" }, 29 + }, 30 + ); 31 + } 32 + 33 + // Verify DIDs match between sessions 34 + const didsMatch = await checkDidsMatch(ctx.req); 35 + if (!didsMatch) { 36 + return new Response( 37 + JSON.stringify({ 38 + success: false, 39 + message: "Invalid state, original and target DIDs do not match", 40 + }), 41 + { 42 + status: 400, 43 + headers: { "Content-Type": "application/json" }, 44 + }, 45 + ); 27 46 } 28 47 29 48 // Migrate preferences 30 49 const migrationLogs: string[] = []; 31 50 const startTime = Date.now(); 32 - console.log(`[${new Date().toISOString()}] Starting preferences migration...`); 33 - migrationLogs.push(`[${new Date().toISOString()}] Starting preferences migration...`); 51 + console.log( 52 + `[${new Date().toISOString()}] Starting preferences migration...`, 53 + ); 54 + migrationLogs.push( 55 + `[${new Date().toISOString()}] Starting preferences migration...`, 56 + ); 34 57 35 58 // Fetch preferences 36 - console.log(`[${new Date().toISOString()}] Fetching preferences from old account...`); 37 - migrationLogs.push(`[${new Date().toISOString()}] Fetching preferences from old account...`); 59 + console.log( 60 + `[${ 61 + new Date().toISOString() 62 + }] Fetching preferences from old account...`, 63 + ); 64 + migrationLogs.push( 65 + `[${ 66 + new Date().toISOString() 67 + }] Fetching preferences from old account...`, 68 + ); 38 69 39 70 const fetchStartTime = Date.now(); 40 71 const prefs = await oldAgent.app.bsky.actor.getPreferences(); 41 72 const fetchTime = Date.now() - fetchStartTime; 42 73 43 - console.log(`[${new Date().toISOString()}] Preferences fetched in ${fetchTime/1000} seconds`); 44 - migrationLogs.push(`[${new Date().toISOString()}] Preferences fetched in ${fetchTime/1000} seconds`); 74 + console.log( 75 + `[${new Date().toISOString()}] Preferences fetched in ${ 76 + fetchTime / 1000 77 + } seconds`, 78 + ); 79 + migrationLogs.push( 80 + `[${new Date().toISOString()}] Preferences fetched in ${ 81 + fetchTime / 1000 82 + } seconds`, 83 + ); 45 84 46 85 // Update preferences 47 - console.log(`[${new Date().toISOString()}] Updating preferences on new account...`); 48 - migrationLogs.push(`[${new Date().toISOString()}] Updating preferences on new account...`); 86 + console.log( 87 + `[${new Date().toISOString()}] Updating preferences on new account...`, 88 + ); 89 + migrationLogs.push( 90 + `[${new Date().toISOString()}] Updating preferences on new account...`, 91 + ); 49 92 50 93 const updateStartTime = Date.now(); 51 94 await newAgent.app.bsky.actor.putPreferences(prefs.data); 52 95 const updateTime = Date.now() - updateStartTime; 53 96 54 - console.log(`[${new Date().toISOString()}] Preferences updated in ${updateTime/1000} seconds`); 55 - migrationLogs.push(`[${new Date().toISOString()}] Preferences updated in ${updateTime/1000} seconds`); 97 + console.log( 98 + `[${new Date().toISOString()}] Preferences updated in ${ 99 + updateTime / 1000 100 + } seconds`, 101 + ); 102 + migrationLogs.push( 103 + `[${new Date().toISOString()}] Preferences updated in ${ 104 + updateTime / 1000 105 + } seconds`, 106 + ); 56 107 57 108 const totalTime = Date.now() - startTime; 58 - const completionMessage = `[${new Date().toISOString()}] Preferences migration completed in ${totalTime/1000} seconds total`; 109 + const completionMessage = `[${ 110 + new Date().toISOString() 111 + }] Preferences migration completed in ${totalTime / 1000} seconds total`; 59 112 console.log(completionMessage); 60 113 migrationLogs.push(completionMessage); 61 114 ··· 65 118 message: "Preferences migration completed successfully", 66 119 logs: migrationLogs, 67 120 timing: { 68 - fetchTime: fetchTime/1000, 69 - updateTime: updateTime/1000, 70 - totalTime: totalTime/1000 71 - } 121 + fetchTime: fetchTime / 1000, 122 + updateTime: updateTime / 1000, 123 + totalTime: totalTime / 1000, 124 + }, 72 125 }), 73 126 { 74 127 status: 200, 75 128 headers: { 76 129 "Content-Type": "application/json", 77 130 ...Object.fromEntries(res.headers), 78 - } 79 - } 131 + }, 132 + }, 80 133 ); 81 134 } catch (error) { 82 135 const message = error instanceof Error ? error.message : String(error); 83 - console.error(`[${new Date().toISOString()}] Preferences migration error:`, message); 84 - console.error('Full error details:', error); 136 + console.error( 137 + `[${new Date().toISOString()}] Preferences migration error:`, 138 + message, 139 + ); 140 + console.error("Full error details:", error); 85 141 return new Response( 86 142 JSON.stringify({ 87 143 success: false, 88 144 message: `Preferences migration failed: ${message}`, 89 - error: error instanceof Error ? { 90 - name: error.name, 91 - message: error.message, 92 - stack: error.stack, 93 - } : String(error) 145 + error: error instanceof Error 146 + ? { 147 + name: error.name, 148 + message: error.message, 149 + stack: error.stack, 150 + } 151 + : String(error), 94 152 }), 95 153 { 96 154 status: 500, 97 155 headers: { 98 156 "Content-Type": "application/json", 99 157 ...Object.fromEntries(res.headers), 100 - } 101 - } 158 + }, 159 + }, 102 160 ); 103 161 } 104 - } 162 + }, 105 163 });
+86 -35
routes/api/migrate/data/repo.ts
··· 1 1 import { getSessionAgent } from "../../../../lib/sessions.ts"; 2 + import { checkDidsMatch } from "../../../../lib/check-dids.ts"; 2 3 import { define } from "../../../../utils.ts"; 3 4 import { assertMigrationAllowed } from "../../../../lib/migration-state.ts"; 4 5 ··· 13 14 const oldAgent = await getSessionAgent(ctx.req); 14 15 console.log("Repo migration: Got old agent:", !!oldAgent); 15 16 16 - 17 17 const newAgent = await getSessionAgent(ctx.req, res, true); 18 18 console.log("Repo migration: Got new agent:", !!newAgent); 19 19 20 20 if (!oldAgent || !newAgent) { 21 - return new Response(JSON.stringify({ 22 - success: false, 23 - message: "Not authenticated" 24 - }), { 25 - status: 401, 26 - headers: { "Content-Type": "application/json" } 27 - }); 21 + return new Response( 22 + JSON.stringify({ 23 + success: false, 24 + message: "Not authenticated", 25 + }), 26 + { 27 + status: 401, 28 + headers: { "Content-Type": "application/json" }, 29 + }, 30 + ); 31 + } 32 + 33 + // Verify DIDs match between sessions 34 + const didsMatch = await checkDidsMatch(ctx.req); 35 + if (!didsMatch) { 36 + return new Response( 37 + JSON.stringify({ 38 + success: false, 39 + message: "Invalid state, original and target DIDs do not match", 40 + }), 41 + { 42 + status: 400, 43 + headers: { "Content-Type": "application/json" }, 44 + }, 45 + ); 28 46 } 29 47 30 48 const session = await oldAgent.com.atproto.server.getSession(); ··· 33 51 const migrationLogs: string[] = []; 34 52 const startTime = Date.now(); 35 53 console.log(`[${new Date().toISOString()}] Starting repo migration...`); 36 - migrationLogs.push(`[${new Date().toISOString()}] Starting repo migration...`); 54 + migrationLogs.push( 55 + `[${new Date().toISOString()}] Starting repo migration...`, 56 + ); 37 57 38 58 // Get repo data from old account 39 - console.log(`[${new Date().toISOString()}] Fetching repo data from old account...`); 40 - migrationLogs.push(`[${new Date().toISOString()}] Fetching repo data from old account...`); 59 + console.log( 60 + `[${new Date().toISOString()}] Fetching repo data from old account...`, 61 + ); 62 + migrationLogs.push( 63 + `[${new Date().toISOString()}] Fetching repo data from old account...`, 64 + ); 41 65 42 66 const fetchStartTime = Date.now(); 43 67 const repoData = await oldAgent.com.atproto.sync.getRepo({ ··· 45 69 }); 46 70 const fetchTime = Date.now() - fetchStartTime; 47 71 48 - console.log(`[${new Date().toISOString()}] Repo data fetched in ${fetchTime/1000} seconds`); 49 - migrationLogs.push(`[${new Date().toISOString()}] Repo data fetched in ${fetchTime/1000} seconds`); 72 + console.log( 73 + `[${new Date().toISOString()}] Repo data fetched in ${ 74 + fetchTime / 1000 75 + } seconds`, 76 + ); 77 + migrationLogs.push( 78 + `[${new Date().toISOString()}] Repo data fetched in ${ 79 + fetchTime / 1000 80 + } seconds`, 81 + ); 50 82 51 - console.log(`[${new Date().toISOString()}] Importing repo data to new account...`); 52 - migrationLogs.push(`[${new Date().toISOString()}] Importing repo data to new account...`); 83 + console.log( 84 + `[${new Date().toISOString()}] Importing repo data to new account...`, 85 + ); 86 + migrationLogs.push( 87 + `[${new Date().toISOString()}] Importing repo data to new account...`, 88 + ); 53 89 54 90 // Import repo data to new account 55 91 const importStartTime = Date.now(); 56 92 await newAgent.com.atproto.repo.importRepo(repoData.data, { 57 - encoding: "application/vnd.ipld.car" 93 + encoding: "application/vnd.ipld.car", 58 94 }); 59 95 const importTime = Date.now() - importStartTime; 60 96 61 - console.log(`[${new Date().toISOString()}] Repo data imported in ${importTime/1000} seconds`); 62 - migrationLogs.push(`[${new Date().toISOString()}] Repo data imported in ${importTime/1000} seconds`); 97 + console.log( 98 + `[${new Date().toISOString()}] Repo data imported in ${ 99 + importTime / 1000 100 + } seconds`, 101 + ); 102 + migrationLogs.push( 103 + `[${new Date().toISOString()}] Repo data imported in ${ 104 + importTime / 1000 105 + } seconds`, 106 + ); 63 107 64 108 const totalTime = Date.now() - startTime; 65 - const completionMessage = `[${new Date().toISOString()}] Repo migration completed in ${totalTime/1000} seconds total`; 109 + const completionMessage = `[${ 110 + new Date().toISOString() 111 + }] Repo migration completed in ${totalTime / 1000} seconds total`; 66 112 console.log(completionMessage); 67 113 migrationLogs.push(completionMessage); 68 114 ··· 72 118 message: "Repo migration completed successfully", 73 119 logs: migrationLogs, 74 120 timing: { 75 - fetchTime: fetchTime/1000, 76 - importTime: importTime/1000, 77 - totalTime: totalTime/1000 78 - } 121 + fetchTime: fetchTime / 1000, 122 + importTime: importTime / 1000, 123 + totalTime: totalTime / 1000, 124 + }, 79 125 }), 80 126 { 81 127 status: 200, 82 128 headers: { 83 129 "Content-Type": "application/json", 84 130 ...Object.fromEntries(res.headers), 85 - } 86 - } 131 + }, 132 + }, 87 133 ); 88 134 } catch (error) { 89 135 const message = error instanceof Error ? error.message : String(error); 90 - console.error(`[${new Date().toISOString()}] Repo migration error:`, message); 91 - console.error('Full error details:', error); 136 + console.error( 137 + `[${new Date().toISOString()}] Repo migration error:`, 138 + message, 139 + ); 140 + console.error("Full error details:", error); 92 141 return new Response( 93 142 JSON.stringify({ 94 143 success: false, 95 144 message: `Repo migration failed: ${message}`, 96 - error: error instanceof Error ? { 97 - name: error.name, 98 - message: error.message, 99 - stack: error.stack, 100 - } : String(error) 145 + error: error instanceof Error 146 + ? { 147 + name: error.name, 148 + message: error.message, 149 + stack: error.stack, 150 + } 151 + : String(error), 101 152 }), 102 153 { 103 154 status: 500, 104 155 headers: { 105 156 "Content-Type": "application/json", 106 157 ...Object.fromEntries(res.headers), 107 - } 108 - } 158 + }, 159 + }, 109 160 ); 110 161 } 111 - } 162 + }, 112 163 });
+13
routes/api/migrate/finalize.ts
··· 1 1 import { getSessionAgent } from "../../../lib/sessions.ts"; 2 + import { checkDidsMatch } from "../../../lib/check-dids.ts"; 2 3 import { define } from "../../../utils.ts"; 3 4 import { assertMigrationAllowed } from "../../../lib/migration-state.ts"; 4 5 ··· 17 18 return new Response("Migration session not found or invalid", { 18 19 status: 400, 19 20 }); 21 + } 22 + 23 + // Verify DIDs match between sessions 24 + const didsMatch = await checkDidsMatch(ctx.req); 25 + if (!didsMatch) { 26 + return new Response( 27 + JSON.stringify({ 28 + success: false, 29 + message: "Invalid state, original and target DIDs do not match", 30 + }), 31 + { status: 400, headers: { "Content-Type": "application/json" } }, 32 + ); 20 33 } 21 34 22 35 // Activate new account and deactivate old account
+18 -4
routes/api/migrate/identity/request.ts
··· 1 - import { 2 - getSessionAgent, 3 - } from "../../../../lib/sessions.ts"; 1 + import { getSessionAgent } from "../../../../lib/sessions.ts"; 2 + import { checkDidsMatch } from "../../../../lib/check-dids.ts"; 4 3 import { define } from "../../../../utils.ts"; 5 4 import { assertMigrationAllowed } from "../../../../lib/migration-state.ts"; 6 5 ··· 56 55 ); 57 56 } 58 57 58 + // Verify DIDs match between sessions 59 + const didsMatch = await checkDidsMatch(ctx.req); 60 + if (!didsMatch) { 61 + return new Response( 62 + JSON.stringify({ 63 + success: false, 64 + message: "Invalid state, original and target DIDs do not match", 65 + }), 66 + { 67 + status: 400, 68 + headers: { "Content-Type": "application/json" }, 69 + }, 70 + ); 71 + } 72 + 59 73 // Request the signature 60 74 console.log("Requesting PLC operation signature..."); 61 75 try { ··· 65 79 console.error("Error requesting PLC operation signature:", { 66 80 name: error instanceof Error ? error.name : "Unknown", 67 81 message: error instanceof Error ? error.message : String(error), 68 - status: 400 82 + status: 400, 69 83 }); 70 84 throw error; 71 85 }
+17 -3
routes/api/migrate/identity/sign.ts
··· 1 - import { 2 - getSessionAgent, 3 - } from "../../../../lib/sessions.ts"; 1 + import { getSessionAgent } from "../../../../lib/sessions.ts"; 2 + import { checkDidsMatch } from "../../../../lib/check-dids.ts"; 4 3 import { Secp256k1Keypair } from "npm:@atproto/crypto"; 5 4 import * as ui8 from "npm:uint8arrays"; 6 5 import { define } from "../../../../utils.ts"; ··· 55 54 JSON.stringify({ 56 55 success: false, 57 56 message: "Migration session not found or invalid", 57 + }), 58 + { 59 + status: 400, 60 + headers: { "Content-Type": "application/json" }, 61 + }, 62 + ); 63 + } 64 + 65 + // Verify DIDs match between sessions 66 + const didsMatch = await checkDidsMatch(ctx.req); 67 + if (!didsMatch) { 68 + return new Response( 69 + JSON.stringify({ 70 + success: false, 71 + message: "Invalid state, original and target DIDs do not match", 58 72 }), 59 73 { 60 74 status: 400,
+2 -2
routes/api/migrate/next-step.ts
··· 17 17 // Check conditions in sequence to determine the next step 18 18 if (!newStatus.data) { 19 19 nextStep = 1; 20 - } else if (!(newStatus.data.repoCommit && 20 + } else if (!(newStatus.data.repoCommit && 21 21 newStatus.data.indexedRecords === oldStatus.data.indexedRecords && 22 22 newStatus.data.privateStateValues === oldStatus.data.privateStateValues && 23 23 newStatus.data.expectedBlobs === newStatus.data.importedBlobs && ··· 42 42 } 43 43 }); 44 44 } 45 - }) 45 + })
+130 -104
routes/api/migrate/status.ts
··· 1 + import { checkDidsMatch } from "../../../lib/check-dids.ts"; 1 2 import { getSessionAgent } from "../../../lib/sessions.ts"; 2 3 import { define } from "../../../utils.ts"; 3 4 4 5 export const handler = define.handlers({ 5 - async GET(ctx) { 6 - console.log("Status check: Starting"); 7 - const url = new URL(ctx.req.url); 8 - const params = new URLSearchParams(url.search); 9 - const step = params.get("step"); 10 - console.log("Status check: Step", step); 6 + async GET(ctx) { 7 + console.log("Status check: Starting"); 8 + const url = new URL(ctx.req.url); 9 + const params = new URLSearchParams(url.search); 10 + const step = params.get("step"); 11 + console.log("Status check: Step", step); 11 12 12 - console.log("Status check: Getting agents"); 13 - const oldAgent = await getSessionAgent(ctx.req); 14 - const newAgent = await getSessionAgent(ctx.req, new Response(), true); 15 - 16 - if (!oldAgent || !newAgent) { 17 - console.log("Status check: Unauthorized - missing agents", { 18 - hasOldAgent: !!oldAgent, 19 - hasNewAgent: !!newAgent 20 - }); 21 - return new Response("Unauthorized", { status: 401 }); 22 - } 13 + console.log("Status check: Getting agents"); 14 + const oldAgent = await getSessionAgent(ctx.req); 15 + const newAgent = await getSessionAgent(ctx.req, new Response(), true); 23 16 24 - console.log("Status check: Fetching account statuses"); 25 - const oldStatus = await oldAgent.com.atproto.server.checkAccountStatus(); 26 - const newStatus = await newAgent.com.atproto.server.checkAccountStatus(); 27 - 28 - if (!oldStatus.data || !newStatus.data) { 29 - console.error("Status check: Failed to verify status", { 30 - hasOldStatus: !!oldStatus.data, 31 - hasNewStatus: !!newStatus.data 32 - }); 33 - return new Response("Could not verify status", { status: 500 }); 34 - } 17 + if (!oldAgent || !newAgent) { 18 + console.log("Status check: Unauthorized - missing agents", { 19 + hasOldAgent: !!oldAgent, 20 + hasNewAgent: !!newAgent, 21 + }); 22 + return new Response("Unauthorized", { status: 401 }); 23 + } 35 24 36 - console.log("Status check: Account statuses", { 37 - old: oldStatus.data, 38 - new: newStatus.data 39 - }); 25 + const didsMatch = await checkDidsMatch(ctx.req); 40 26 41 - const readyToContinue = () => { 42 - if (step) { 43 - console.log("Status check: Evaluating step", step); 44 - switch (step) { 45 - case "1": { 46 - if (newStatus.data) { 47 - console.log("Status check: Step 1 ready"); 48 - return { ready: true }; 49 - } 50 - console.log("Status check: Step 1 not ready - new account status not available"); 51 - return { ready: false, reason: "New account status not available" }; 52 - } 53 - case "2": { 54 - const isReady = newStatus.data.repoCommit && 55 - newStatus.data.indexedRecords === oldStatus.data.indexedRecords && 56 - newStatus.data.privateStateValues === oldStatus.data.privateStateValues && 57 - newStatus.data.expectedBlobs === newStatus.data.importedBlobs && 58 - newStatus.data.importedBlobs === oldStatus.data.importedBlobs; 27 + console.log("Status check: Fetching account statuses"); 28 + const oldStatus = await oldAgent.com.atproto.server.checkAccountStatus(); 29 + const newStatus = await newAgent.com.atproto.server.checkAccountStatus(); 59 30 60 - if (isReady) { 61 - console.log("Status check: Step 2 ready"); 62 - return { ready: true }; 63 - } 31 + if (!oldStatus.data || !newStatus.data) { 32 + console.error("Status check: Failed to verify status", { 33 + hasOldStatus: !!oldStatus.data, 34 + hasNewStatus: !!newStatus.data, 35 + }); 36 + return new Response("Could not verify status", { status: 500 }); 37 + } 64 38 65 - const reasons = []; 66 - if (!newStatus.data.repoCommit) reasons.push("Repository not imported."); 67 - if (newStatus.data.indexedRecords < oldStatus.data.indexedRecords) 68 - reasons.push("Not all records imported."); 69 - if (newStatus.data.privateStateValues < oldStatus.data.privateStateValues) 70 - reasons.push("Not all private state values imported."); 71 - if (newStatus.data.expectedBlobs !== newStatus.data.importedBlobs) 72 - reasons.push("Expected blobs not fully imported."); 73 - if (newStatus.data.importedBlobs < oldStatus.data.importedBlobs) 74 - reasons.push("Not all blobs imported."); 39 + console.log("Status check: Account statuses", { 40 + old: oldStatus.data, 41 + new: newStatus.data, 42 + }); 75 43 76 - console.log("Status check: Step 2 not ready", { reasons }); 77 - return { ready: false, reason: reasons.join(", ") }; 78 - } 79 - case "3": { 80 - if (newStatus.data.validDid) { 81 - console.log("Status check: Step 3 ready"); 82 - return { ready: true }; 83 - } 84 - console.log("Status check: Step 3 not ready - DID not valid"); 85 - return { ready: false, reason: "DID not valid" }; 86 - } 87 - case "4": { 88 - if (newStatus.data.activated === true && oldStatus.data.activated === false) { 89 - console.log("Status check: Step 4 ready"); 90 - return { ready: true }; 91 - } 92 - console.log("Status check: Step 4 not ready - Account not activated"); 93 - return { ready: false, reason: "Account not activated" }; 94 - } 95 - } 96 - } else { 97 - console.log("Status check: No step specified, returning ready"); 98 - return { ready: true }; 44 + const readyToContinue = () => { 45 + if (!didsMatch) { 46 + return { 47 + ready: false, 48 + reason: "Invalid state, original and target DIDs do not match", 49 + }; 50 + } 51 + if (step) { 52 + console.log("Status check: Evaluating step", step); 53 + switch (step) { 54 + case "1": { 55 + if (newStatus.data) { 56 + console.log("Status check: Step 1 ready"); 57 + return { ready: true }; 58 + } 59 + console.log( 60 + "Status check: Step 1 not ready - new account status not available", 61 + ); 62 + return { ready: false, reason: "New account status not available" }; 63 + } 64 + case "2": { 65 + const isReady = newStatus.data.repoCommit && 66 + newStatus.data.indexedRecords === oldStatus.data.indexedRecords && 67 + newStatus.data.privateStateValues === 68 + oldStatus.data.privateStateValues && 69 + newStatus.data.expectedBlobs === newStatus.data.importedBlobs && 70 + newStatus.data.importedBlobs === oldStatus.data.importedBlobs; 71 + 72 + if (isReady) { 73 + console.log("Status check: Step 2 ready"); 74 + return { ready: true }; 75 + } 76 + 77 + const reasons = []; 78 + if (!newStatus.data.repoCommit) { 79 + reasons.push("Repository not imported."); 80 + } 81 + if (newStatus.data.indexedRecords < oldStatus.data.indexedRecords) { 82 + reasons.push("Not all records imported."); 83 + } 84 + if ( 85 + newStatus.data.privateStateValues < 86 + oldStatus.data.privateStateValues 87 + ) { 88 + reasons.push("Not all private state values imported."); 89 + } 90 + if (newStatus.data.expectedBlobs !== newStatus.data.importedBlobs) { 91 + reasons.push("Expected blobs not fully imported."); 92 + } 93 + if (newStatus.data.importedBlobs < oldStatus.data.importedBlobs) { 94 + reasons.push("Not all blobs imported."); 95 + } 96 + 97 + console.log("Status check: Step 2 not ready", { reasons }); 98 + return { ready: false, reason: reasons.join(", ") }; 99 + } 100 + case "3": { 101 + if (newStatus.data.validDid) { 102 + console.log("Status check: Step 3 ready"); 103 + return { ready: true }; 104 + } 105 + console.log("Status check: Step 3 not ready - DID not valid"); 106 + return { ready: false, reason: "DID not valid" }; 107 + } 108 + case "4": { 109 + if ( 110 + newStatus.data.activated === true && 111 + oldStatus.data.activated === false 112 + ) { 113 + console.log("Status check: Step 4 ready"); 114 + return { ready: true }; 99 115 } 116 + console.log( 117 + "Status check: Step 4 not ready - Account not activated", 118 + ); 119 + return { ready: false, reason: "Account not activated" }; 120 + } 100 121 } 122 + } else { 123 + console.log("Status check: No step specified, returning ready"); 124 + return { ready: true }; 125 + } 126 + }; 101 127 102 - const status = { 103 - activated: newStatus.data.activated, 104 - validDid: newStatus.data.validDid, 105 - repoCommit: newStatus.data.repoCommit, 106 - repoRev: newStatus.data.repoRev, 107 - repoBlocks: newStatus.data.repoBlocks, 108 - expectedRecords: oldStatus.data.indexedRecords, 109 - indexedRecords: newStatus.data.indexedRecords, 110 - privateStateValues: newStatus.data.privateStateValues, 111 - expectedBlobs: newStatus.data.expectedBlobs, 112 - importedBlobs: newStatus.data.importedBlobs, 113 - ...readyToContinue() 114 - } 128 + const status = { 129 + activated: newStatus.data.activated, 130 + validDid: newStatus.data.validDid, 131 + repoCommit: newStatus.data.repoCommit, 132 + repoRev: newStatus.data.repoRev, 133 + repoBlocks: newStatus.data.repoBlocks, 134 + expectedRecords: oldStatus.data.indexedRecords, 135 + indexedRecords: newStatus.data.indexedRecords, 136 + privateStateValues: newStatus.data.privateStateValues, 137 + expectedBlobs: newStatus.data.expectedBlobs, 138 + importedBlobs: newStatus.data.importedBlobs, 139 + ...readyToContinue(), 140 + }; 115 141 116 - console.log("Status check: Complete", status); 117 - return Response.json(status); 118 - } 119 - }) 142 + console.log("Status check: Complete", status); 143 + return Response.json(status); 144 + }, 145 + });
+2 -2
routes/migrate/progress.tsx
··· 10 10 11 11 if (!service || !handle || !email || !password) { 12 12 return ( 13 - <div class="min-h-screen bg-gray-50 dark:bg-gray-900 p-4"> 13 + <div class="bg-gray-50 dark:bg-gray-900 p-4"> 14 14 <div class="max-w-2xl mx-auto"> 15 15 <div class="bg-red-50 dark:bg-red-900 p-4 rounded-lg"> 16 16 <p class="text-red-800 dark:text-red-200"> ··· 24 24 } 25 25 26 26 return ( 27 - <div class="min-h-screen bg-gray-50 dark:bg-gray-900 p-4"> 27 + <div class="bg-gray-50 dark:bg-gray-900 p-4"> 28 28 <div class="max-w-2xl mx-auto"> 29 29 <h1 class="font-mono text-3xl font-bold text-gray-900 dark:text-white mb-8"> 30 30 Migration Progress