Graphical PDS migrator for AT Protocol
15
fork

Configure Feed

Select the types of activity you want to include in your feed.

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

+774 -321
+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