Graphical PDS migrator for AT Protocol

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

+35 -9
.zed/settings.json
···
+
// Folder-specific settings
+
//
+
// For a full list of overridable settings, and general information on folder-specific settings,
+
// see the documentation: https://zed.dev/docs/configuring-zed#settings-files
{
+
"lsp": {
+
"deno": {
+
"settings": {
+
"deno": {
+
"enable": true,
+
"cacheOnSave": true,
+
"suggest": {
+
"imports": {
+
"autoDiscover": true
+
}
+
}
+
}
+
}
+
}
+
},
"languages": {
+
"JavaScript": {
+
"language_servers": [
+
"deno",
+
"!vtsls",
+
"!eslint",
+
"..."
+
]
+
},
"TypeScript": {
"language_servers": [
-
"wakatime",
"deno",
"!typescript-language-server",
"!vtsls",
-
"!eslint"
-
],
-
"formatter": "language_server"
+
"!eslint",
+
"..."
+
]
},
"TSX": {
"language_servers": [
-
"wakatime",
"deno",
"!typescript-language-server",
"!vtsls",
-
"!eslint"
-
],
-
"formatter": "language_server"
+
"!eslint",
+
"..."
+
]
}
-
}
+
},
+
"formatter": "language_server"
}
+155 -66
islands/MigrationProgress.tsx
···
*/
export default function MigrationProgress(props: MigrationProgressProps) {
const [token, setToken] = useState("");
-
const [migrationState, setMigrationState] = useState<MigrationStateInfo | null>(null);
+
const [migrationState, setMigrationState] = useState<
+
MigrationStateInfo | null
+
>(null);
const [steps, setSteps] = useState<MigrationStep[]>([
{ name: "Create Account", status: "pending" },
···
const getStepDisplayName = (step: MigrationStep, index: number) => {
if (step.status === "completed") {
switch (index) {
-
case 0: return "Account Created";
-
case 1: return "Data Migrated";
-
case 2: return "Identity Migrated";
-
case 3: return "Migration Finalized";
+
case 0:
+
return "Account Created";
+
case 1:
+
return "Data Migrated";
+
case 2:
+
return "Identity Migrated";
+
case 3:
+
return "Migration Finalized";
}
}
if (step.status === "in-progress") {
switch (index) {
-
case 0: return "Creating your new account...";
-
case 1: return "Migrating your data...";
-
case 2: return step.name === "Enter the token sent to your email to complete identity migration"
-
? step.name
-
: "Migrating your identity...";
-
case 3: return "Finalizing migration...";
+
case 0:
+
return "Creating your new account...";
+
case 1:
+
return "Migrating your data...";
+
case 2:
+
return step.name ===
+
"Enter the token sent to your email to complete identity migration"
+
? step.name
+
: "Migrating your identity...";
+
case 3:
+
return "Finalizing migration...";
}
}
if (step.status === "verifying") {
switch (index) {
-
case 0: return "Verifying account creation...";
-
case 1: return "Verifying data migration...";
-
case 2: return "Verifying identity migration...";
-
case 3: return "Verifying migration completion...";
+
case 0:
+
return "Verifying account creation...";
+
case 1:
+
return "Verifying data migration...";
+
case 2:
+
return "Verifying identity migration...";
+
case 3:
+
return "Verifying migration completion...";
}
}
···
console.error("Blob migration: Error response:", json);
throw new Error(json.message || "Failed to migrate blobs");
} catch {
-
console.error("Blob migration: Non-JSON error response:", blobsText);
+
console.error(
+
"Blob migration: Non-JSON error response:",
+
blobsText,
+
);
throw new Error(blobsText || "Failed to migrate blobs");
}
}
···
console.error("Preferences migration: Error response:", json);
throw new Error(json.message || "Failed to migrate preferences");
} catch {
-
console.error("Preferences migration: Non-JSON error response:", prefsText);
+
console.error(
+
"Preferences migration: Non-JSON error response:",
+
prefsText,
+
);
throw new Error(prefsText || "Failed to migrate preferences");
}
}
···
if (!requestRes.ok) {
try {
const json = JSON.parse(requestText);
-
throw new Error(json.message || "Failed to request identity migration");
+
throw new Error(
+
json.message || "Failed to request identity migration",
+
);
} catch {
-
throw new Error(requestText || "Failed to request identity migration");
+
throw new Error(
+
requestText || "Failed to request identity migration",
+
);
}
}
···
console.log("Identity migration requested successfully");
// Update step name to prompt for token
-
setSteps(prevSteps =>
+
setSteps((prevSteps) =>
prevSteps.map((step, i) =>
i === 2
-
? { ...step, name: "Enter the token sent to your email to complete identity migration" }
+
? {
+
...step,
+
name:
+
"Enter the token sent to your email to complete identity migration",
+
}
: step
)
);
···
if (!identityRes.ok) {
try {
const json = JSON.parse(identityData);
-
throw new Error(json.message || "Failed to complete identity migration");
+
throw new Error(
+
json.message || "Failed to complete identity migration",
+
);
} catch {
-
throw new Error(identityData || "Failed to complete identity migration");
+
throw new Error(
+
identityData || "Failed to complete identity migration",
+
);
}
}
···
} catch {
throw new Error("Invalid response from server");
}
-
updateStepStatus(2, "verifying");
const verified = await verifyStep(2);
···
updateStepStatus(stepNum, "completed");
return true;
} else {
-
console.log(`Verification: Step ${stepNum + 1} is not ready:`, data.reason);
+
console.log(
+
`Verification: Step ${stepNum + 1} is not ready:`,
+
data.reason,
+
);
const statusDetails = {
activated: data.activated,
validDid: data.validDid,
···
indexedRecords: data.indexedRecords,
privateStateValues: data.privateStateValues,
expectedBlobs: data.expectedBlobs,
-
importedBlobs: data.importedBlobs
+
importedBlobs: data.importedBlobs,
};
-
console.log(`Verification: Step ${stepNum + 1} status details:`, statusDetails);
-
const errorMessage = `${data.reason || "Verification failed"}\nStatus details: ${JSON.stringify(statusDetails, null, 2)}`;
+
console.log(
+
`Verification: Step ${stepNum + 1} status details:`,
+
statusDetails,
+
);
+
const errorMessage = `${
+
data.reason || "Verification failed"
+
}\nStatus details: ${JSON.stringify(statusDetails, null, 2)}`;
updateStepStatus(stepNum, "error", errorMessage);
return false;
}
} catch (e) {
console.error(`Verification: Error in step ${stepNum + 1}:`, e);
-
updateStepStatus(stepNum, "error", e instanceof Error ? e.message : String(e));
+
updateStepStatus(
+
stepNum,
+
"error",
+
e instanceof Error ? e.message : String(e),
+
);
return false;
}
};
···
<div class="space-y-8">
{/* Migration state alert */}
{migrationState && !migrationState.allowMigration && (
-
<div class={`p-4 rounded-lg border ${
-
migrationState.state === "maintenance"
-
? "bg-yellow-50 border-yellow-200 text-yellow-800 dark:bg-yellow-900/20 dark:border-yellow-800 dark:text-yellow-200"
-
: "bg-red-50 border-red-200 text-red-800 dark:bg-red-900/20 dark:border-red-800 dark:text-red-200"
-
}`}>
+
<div
+
class={`p-4 rounded-lg border ${
+
migrationState.state === "maintenance"
+
? "bg-yellow-50 border-yellow-200 text-yellow-800 dark:bg-yellow-900/20 dark:border-yellow-800 dark:text-yellow-200"
+
: "bg-red-50 border-red-200 text-red-800 dark:bg-red-900/20 dark:border-red-800 dark:text-red-200"
+
}`}
+
>
<div class="flex items-center">
-
<div class={`mr-3 ${
-
migrationState.state === "maintenance" ? "text-yellow-600 dark:text-yellow-400" : "text-red-600 dark:text-red-400"
-
}`}>
+
<div
+
class={`mr-3 ${
+
migrationState.state === "maintenance"
+
? "text-yellow-600 dark:text-yellow-400"
+
: "text-red-600 dark:text-red-400"
+
}`}
+
>
{migrationState.state === "maintenance" ? "⚠️" : "🚫"}
</div>
<div>
<h3 class="font-semibold mb-1">
-
{migrationState.state === "maintenance" ? "Maintenance Mode" : "Service Unavailable"}
+
{migrationState.state === "maintenance"
+
? "Maintenance Mode"
+
: "Service Unavailable"}
</h3>
<p class="text-sm">{migrationState.message}</p>
</div>
···
</p>
)}
{index === 2 && step.status === "in-progress" &&
-
step.name === "Enter the token sent to your email to complete identity migration" && (
+
step.name ===
+
"Enter the token sent to your email to complete identity migration" &&
+
(
<div class="mt-4 space-y-4">
<p class="text-sm text-blue-800 dark:text-blue-200">
-
Please check your email for the migration token and enter it below:
+
Please check your email for the migration token and enter
+
it below:
</p>
<div class="flex space-x-2">
<input
···
</button>
</div>
</div>
-
)
-
}
+
)}
</div>
</div>
))}
···
{steps[3].status === "completed" && (
<div class="p-4 bg-green-50 dark:bg-green-900 rounded-lg border-2 border-green-200 dark:border-green-800">
-
<p class="text-sm text-green-800 dark:text-green-200">
-
Migration completed successfully! You can now close this page.
+
<p class="text-sm text-green-800 dark:text-green-200 pb-2">
+
Migration completed successfully! Sign out to finish the process and
+
return home.<br />
+
Please consider donating to Airport to support server and
+
development costs.
</p>
-
<button
-
type="button"
-
onClick={async () => {
-
try {
-
const response = await fetch("/api/logout", {
-
method: "POST",
-
credentials: "include",
-
});
-
if (!response.ok) {
-
throw new Error("Logout failed");
+
<div class="flex space-x-4">
+
<button
+
type="button"
+
onClick={async () => {
+
try {
+
const response = await fetch("/api/logout", {
+
method: "POST",
+
credentials: "include",
+
});
+
if (!response.ok) {
+
throw new Error("Logout failed");
+
}
+
globalThis.location.href = "/";
+
} catch (error) {
+
console.error("Failed to logout:", error);
}
-
globalThis.location.href = "/";
-
} catch (error) {
-
console.error("Failed to logout:", error);
-
}
-
}}
-
class="mt-4 mr-4 px-4 py-2 bg-green-600 hover:bg-green-700 text-white rounded-md transition-colors duration-200"
-
>
-
Sign Out
-
</button>
-
<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">
-
Donate
-
</a>
+
}}
+
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"
+
>
+
<svg
+
class="w-5 h-5"
+
fill="none"
+
stroke="currentColor"
+
viewBox="0 0 24 24"
+
>
+
<path
+
stroke-linecap="round"
+
stroke-linejoin="round"
+
stroke-width="2"
+
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"
+
/>
+
</svg>
+
<span>Sign Out</span>
+
</button>
+
<a
+
href="https://ko-fi.com/knotbin"
+
target="_blank"
+
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"
+
>
+
<svg
+
class="w-5 h-5"
+
fill="none"
+
stroke="currentColor"
+
viewBox="0 0 24 24"
+
>
+
<path
+
stroke-linecap="round"
+
stroke-linejoin="round"
+
stroke-width="2"
+
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"
+
/>
+
</svg>
+
<span>Support Us</span>
+
</a>
+
</div>
</div>
)}
</div>
+7
lib/check-dids.ts
···
+
import { getSession } from "./sessions.ts";
+
+
export async function checkDidsMatch(req: Request): Promise<boolean> {
+
const oldSession = await getSession(req, undefined, false);
+
const newSession = await getSession(req, undefined, true);
+
return oldSession.did === newSession.did;
+
}
+1 -1
lib/migration-state.ts
···
case "maintenance":
return {
state: "maintenance",
-
message: "Migration services are temporarily unavailable for scheduled maintenance. Please try again later.",
+
message: "Migration services are temporarily unavailable for maintenance. Please try again later.",
allowMigration: false,
};
+31 -10
lib/sessions.ts
···
import { Agent } from "npm:@atproto/api";
-
import { OauthSession, CredentialSession } from "./types.ts";
-
import { getCredentialSession, getCredentialSessionAgent } from "./cred/sessions.ts";
+
import { CredentialSession, OauthSession } from "./types.ts";
+
import {
+
getCredentialSession,
+
getCredentialSessionAgent,
+
} from "./cred/sessions.ts";
import { getOauthSession, getOauthSessionAgent } from "./oauth/sessions.ts";
import { IronSession } from "npm:iron-session";
···
export async function getSession(
req: Request,
res: Response = new Response(),
-
isMigration: boolean = false
+
isMigration: boolean = false,
): Promise<IronSession<OauthSession | CredentialSession>> {
if (isMigration) {
return await getCredentialSession(req, res, true);
···
const credentialSession = await getCredentialSession(req, res);
if (oauthSession.did) {
-
console.log("Oauth session found")
+
console.log("Oauth session found");
return oauthSession;
}
if (credentialSession.did) {
···
export async function getSessionAgent(
req: Request,
res: Response = new Response(),
-
isMigration: boolean = false
+
isMigration: boolean = false,
): Promise<Agent | null> {
if (isMigration) {
return await getCredentialSessionAgent(req, res, isMigration);
}
const oauthAgent = await getOauthSessionAgent(req);
-
const credentialAgent = await getCredentialSessionAgent(req, res, isMigration);
+
const credentialAgent = await getCredentialSessionAgent(
+
req,
+
res,
+
isMigration,
+
);
if (oauthAgent) {
return oauthAgent;
···
/**
* Destroy all sessions for the given request.
* @param req - The request object
+
* @param res - The response object
*/
-
export async function destroyAllSessions(req: Request) {
-
const oauthSession = await getOauthSession(req);
-
const credentialSession = await getCredentialSession(req);
-
const migrationSession = await getCredentialSession(req, new Response(), true);
+
export async function destroyAllSessions(
+
req: Request,
+
res?: Response,
+
): Promise<Response> {
+
const response = res || new Response();
+
const oauthSession = await getOauthSession(req, response);
+
const credentialSession = await getCredentialSession(req, res);
+
const migrationSession = await getCredentialSession(
+
req,
+
res,
+
true,
+
);
if (oauthSession.did) {
oauthSession.destroy();
···
credentialSession.destroy();
}
if (migrationSession.did) {
+
console.log("DESTROYING MIGRATION SESSION", migrationSession);
migrationSession.destroy();
+
} else {
+
console.log("MIGRATION SESSION NOT FOUND", migrationSession);
}
+
+
return response;
}
+1 -1
lib/storage.ts
···
NodeSavedSessionStore,
NodeSavedState,
NodeSavedStateStore,
-
} from "jsr:@bigmoves/atproto-oauth-client";
+
} from "@bigmoves/atproto-oauth-client";
/**
* The state store for sessions.
+4 -4
routes/api/logout.ts
···
-
import { getSession, destroyAllSessions } from "../../lib/sessions.ts";
+
import { destroyAllSessions, getSession } from "../../lib/sessions.ts";
import { oauthClient } from "../../lib/oauth/client.ts";
import { define } from "../../utils.ts";
···
if (session.did) {
// Try to revoke both types of sessions - the one that doesn't exist will just no-op
await Promise.all([
-
oauthClient.revoke(session.did).catch(console.error)
+
oauthClient.revoke(session.did).catch(console.error),
]);
// Then destroy the iron session
session.destroy();
}
// Destroy all sessions including migration session
-
await destroyAllSessions(req);
+
const result = await destroyAllSessions(req, response);
-
return response;
+
return result;
} catch (error: unknown) {
const err = error instanceof Error ? error : new Error(String(error));
console.error("Logout failed:", err.message);
+2 -2
routes/api/migrate/create.ts
···
return new Response("Could not create new agent", { status: 400 });
}
-
console.log("getting did")
+
console.log("getting did");
const session = await oldAgent.com.atproto.server.getSession();
const accountDid = session.data.did;
-
console.log("got did")
+
console.log("got did");
const describeRes = await newAgent.com.atproto.server.describeServer();
const newServerDid = describeRes.data.did;
const inviteRequired = describeRes.data.inviteCodeRequired ?? false;
+178 -44
routes/api/migrate/data/blobs.ts
···
import { getSessionAgent } from "../../../../lib/sessions.ts";
+
import { checkDidsMatch } from "../../../../lib/check-dids.ts";
import { define } from "../../../../utils.ts";
import { assertMigrationAllowed } from "../../../../lib/migration-state.ts";
···
);
}
+
// Verify DIDs match between sessions
+
const didsMatch = await checkDidsMatch(ctx.req);
+
if (!didsMatch) {
+
return new Response(
+
JSON.stringify({
+
success: false,
+
message: "Invalid state, original and target DIDs do not match",
+
}),
+
{
+
status: 400,
+
headers: { "Content-Type": "application/json" },
+
},
+
);
+
}
+
// Migrate blobs
const migrationLogs: string[] = [];
const migratedBlobs: string[] = [];
···
const startTime = Date.now();
console.log(`[${new Date().toISOString()}] Starting blob migration...`);
-
migrationLogs.push(`[${new Date().toISOString()}] Starting blob migration...`);
+
migrationLogs.push(
+
`[${new Date().toISOString()}] Starting blob migration...`,
+
);
// First count total blobs
console.log(`[${new Date().toISOString()}] Starting blob count...`);
-
migrationLogs.push(`[${new Date().toISOString()}] Starting blob count...`);
+
migrationLogs.push(
+
`[${new Date().toISOString()}] Starting blob count...`,
+
);
const session = await oldAgent.com.atproto.server.getSession();
const accountDid = session.data.did;
do {
const pageStartTime = Date.now();
-
console.log(`[${new Date().toISOString()}] Counting blobs on page ${pageCount + 1}...`);
-
migrationLogs.push(`[${new Date().toISOString()}] Counting blobs on page ${pageCount + 1}...`);
+
console.log(
+
`[${new Date().toISOString()}] Counting blobs on page ${
+
pageCount + 1
+
}...`,
+
);
+
migrationLogs.push(
+
`[${new Date().toISOString()}] Counting blobs on page ${
+
pageCount + 1
+
}...`,
+
);
const listedBlobs = await oldAgent.com.atproto.sync.listBlobs({
did: accountDid,
cursor: blobCursor,
···
totalBlobs += newBlobs;
const pageTime = Date.now() - pageStartTime;
-
console.log(`[${new Date().toISOString()}] Found ${newBlobs} blobs on page ${pageCount + 1} in ${pageTime/1000} seconds, total so far: ${totalBlobs}`);
-
migrationLogs.push(`[${new Date().toISOString()}] Found ${newBlobs} blobs on page ${pageCount + 1} in ${pageTime/1000} seconds, total so far: ${totalBlobs}`);
+
console.log(
+
`[${new Date().toISOString()}] Found ${newBlobs} blobs on page ${
+
pageCount + 1
+
} in ${pageTime / 1000} seconds, total so far: ${totalBlobs}`,
+
);
+
migrationLogs.push(
+
`[${new Date().toISOString()}] Found ${newBlobs} blobs on page ${
+
pageCount + 1
+
} in ${pageTime / 1000} seconds, total so far: ${totalBlobs}`,
+
);
pageCount++;
blobCursor = listedBlobs.data.cursor;
} while (blobCursor);
-
console.log(`[${new Date().toISOString()}] Total blobs to migrate: ${totalBlobs}`);
-
migrationLogs.push(`[${new Date().toISOString()}] Total blobs to migrate: ${totalBlobs}`);
+
console.log(
+
`[${new Date().toISOString()}] Total blobs to migrate: ${totalBlobs}`,
+
);
+
migrationLogs.push(
+
`[${new Date().toISOString()}] Total blobs to migrate: ${totalBlobs}`,
+
);
// Reset cursor for actual migration
blobCursor = undefined;
···
do {
const pageStartTime = Date.now();
-
console.log(`[${new Date().toISOString()}] Fetching blob list page ${pageCount + 1}...`);
-
migrationLogs.push(`[${new Date().toISOString()}] Fetching blob list page ${pageCount + 1}...`);
+
console.log(
+
`[${new Date().toISOString()}] Fetching blob list page ${
+
pageCount + 1
+
}...`,
+
);
+
migrationLogs.push(
+
`[${new Date().toISOString()}] Fetching blob list page ${
+
pageCount + 1
+
}...`,
+
);
const listedBlobs = await oldAgent.com.atproto.sync.listBlobs({
did: accountDid,
···
});
const pageTime = Date.now() - pageStartTime;
-
console.log(`[${new Date().toISOString()}] Found ${listedBlobs.data.cids.length} blobs on page ${pageCount + 1} in ${pageTime/1000} seconds`);
-
migrationLogs.push(`[${new Date().toISOString()}] Found ${listedBlobs.data.cids.length} blobs on page ${pageCount + 1} in ${pageTime/1000} seconds`);
+
console.log(
+
`[${
+
new Date().toISOString()
+
}] Found ${listedBlobs.data.cids.length} blobs on page ${
+
pageCount + 1
+
} in ${pageTime / 1000} seconds`,
+
);
+
migrationLogs.push(
+
`[${
+
new Date().toISOString()
+
}] Found ${listedBlobs.data.cids.length} blobs on page ${
+
pageCount + 1
+
} in ${pageTime / 1000} seconds`,
+
);
blobCursor = listedBlobs.data.cursor;
for (const cid of listedBlobs.data.cids) {
try {
const blobStartTime = Date.now();
-
console.log(`[${new Date().toISOString()}] Starting migration for blob ${cid} (${processedBlobs + 1} of ${totalBlobs})...`);
-
migrationLogs.push(`[${new Date().toISOString()}] Starting migration for blob ${cid} (${processedBlobs + 1} of ${totalBlobs})...`);
+
console.log(
+
`[${
+
new Date().toISOString()
+
}] Starting migration for blob ${cid} (${
+
processedBlobs + 1
+
} of ${totalBlobs})...`,
+
);
+
migrationLogs.push(
+
`[${
+
new Date().toISOString()
+
}] Starting migration for blob ${cid} (${
+
processedBlobs + 1
+
} of ${totalBlobs})...`,
+
);
const blobRes = await oldAgent.com.atproto.sync.getBlob({
did: accountDid,
···
const size = parseInt(contentLength, 10);
if (isNaN(size)) {
-
throw new Error(`Blob ${cid} has invalid content length: ${contentLength}`);
+
throw new Error(
+
`Blob ${cid} has invalid content length: ${contentLength}`,
+
);
}
const MAX_SIZE = 200 * 1024 * 1024; // 200MB
if (size > MAX_SIZE) {
-
throw new Error(`Blob ${cid} exceeds maximum size limit (${size} bytes)`);
+
throw new Error(
+
`Blob ${cid} exceeds maximum size limit (${size} bytes)`,
+
);
}
-
console.log(`[${new Date().toISOString()}] Downloading blob ${cid} (${size} bytes)...`);
-
migrationLogs.push(`[${new Date().toISOString()}] Downloading blob ${cid} (${size} bytes)...`);
+
console.log(
+
`[${
+
new Date().toISOString()
+
}] Downloading blob ${cid} (${size} bytes)...`,
+
);
+
migrationLogs.push(
+
`[${
+
new Date().toISOString()
+
}] Downloading blob ${cid} (${size} bytes)...`,
+
);
if (!blobRes.data) {
-
throw new Error(`Failed to download blob ${cid}: No data received`);
+
throw new Error(
+
`Failed to download blob ${cid}: No data received`,
+
);
}
-
console.log(`[${new Date().toISOString()}] Uploading blob ${cid} to new account...`);
-
migrationLogs.push(`[${new Date().toISOString()}] Uploading blob ${cid} to new account...`);
+
console.log(
+
`[${
+
new Date().toISOString()
+
}] Uploading blob ${cid} to new account...`,
+
);
+
migrationLogs.push(
+
`[${
+
new Date().toISOString()
+
}] Uploading blob ${cid} to new account...`,
+
);
try {
await newAgent.com.atproto.repo.uploadBlob(blobRes.data);
const blobTime = Date.now() - blobStartTime;
-
console.log(`[${new Date().toISOString()}] Successfully migrated blob ${cid} in ${blobTime/1000} seconds`);
-
migrationLogs.push(`[${new Date().toISOString()}] Successfully migrated blob ${cid} in ${blobTime/1000} seconds`);
+
console.log(
+
`[${
+
new Date().toISOString()
+
}] Successfully migrated blob ${cid} in ${
+
blobTime / 1000
+
} seconds`,
+
);
+
migrationLogs.push(
+
`[${
+
new Date().toISOString()
+
}] Successfully migrated blob ${cid} in ${
+
blobTime / 1000
+
} seconds`,
+
);
migratedBlobs.push(cid);
} catch (uploadError) {
-
console.error(`[${new Date().toISOString()}] Failed to upload blob ${cid}:`, uploadError);
-
throw new Error(`Upload failed: ${uploadError instanceof Error ? uploadError.message : String(uploadError)}`);
+
console.error(
+
`[${new Date().toISOString()}] Failed to upload blob ${cid}:`,
+
uploadError,
+
);
+
throw new Error(
+
`Upload failed: ${
+
uploadError instanceof Error
+
? uploadError.message
+
: String(uploadError)
+
}`,
+
);
}
} catch (error) {
-
const errorMessage = error instanceof Error ? error.message : String(error);
-
const detailedError = `[${new Date().toISOString()}] Failed to migrate blob ${cid}: ${errorMessage}`;
+
const errorMessage = error instanceof Error
+
? error.message
+
: String(error);
+
const detailedError = `[${
+
new Date().toISOString()
+
}] Failed to migrate blob ${cid}: ${errorMessage}`;
console.error(detailedError);
-
console.error('Full error details:', error);
+
console.error("Full error details:", error);
migrationLogs.push(detailedError);
failedBlobs.push(cid);
}
processedBlobs++;
-
const progressLog = `[${new Date().toISOString()}] Progress: ${processedBlobs}/${totalBlobs} blobs processed (${Math.round((processedBlobs/totalBlobs)*100)}%)`;
+
const progressLog = `[${
+
new Date().toISOString()
+
}] Progress: ${processedBlobs}/${totalBlobs} blobs processed (${
+
Math.round((processedBlobs / totalBlobs) * 100)
+
}%)`;
console.log(progressLog);
migrationLogs.push(progressLog);
}
···
} while (blobCursor);
const totalTime = Date.now() - startTime;
-
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)`;
+
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)`;
console.log(completionMessage);
migrationLogs.push(completionMessage);
···
totalBlobs,
logs: migrationLogs,
timing: {
-
totalTime: totalTime/1000
-
}
+
totalTime: totalTime / 1000,
+
},
}),
{
status: 200,
headers: {
"Content-Type": "application/json",
...Object.fromEntries(res.headers),
-
}
-
}
+
},
+
},
);
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
-
console.error(`[${new Date().toISOString()}] Blob migration error:`, message);
-
console.error('Full error details:', error);
+
console.error(
+
`[${new Date().toISOString()}] Blob migration error:`,
+
message,
+
);
+
console.error("Full error details:", error);
return new Response(
JSON.stringify({
success: false,
message: `Blob migration failed: ${message}`,
-
error: error instanceof Error ? {
-
name: error.name,
-
message: error.message,
-
stack: error.stack,
-
} : String(error)
+
error: error instanceof Error
+
? {
+
name: error.name,
+
message: error.message,
+
stack: error.stack,
+
}
+
: String(error),
}),
{
status: 500,
headers: {
"Content-Type": "application/json",
...Object.fromEntries(res.headers),
-
}
-
}
+
},
+
},
);
}
-
}
+
},
});
+92 -34
routes/api/migrate/data/prefs.ts
···
import { getSessionAgent } from "../../../../lib/sessions.ts";
+
import { checkDidsMatch } from "../../../../lib/check-dids.ts";
import { define } from "../../../../utils.ts";
import { assertMigrationAllowed } from "../../../../lib/migration-state.ts";
···
console.log("Preferences migration: Got new agent:", !!newAgent);
if (!oldAgent || !newAgent) {
-
return new Response(JSON.stringify({
-
success: false,
-
message: "Not authenticated"
-
}), {
-
status: 401,
-
headers: { "Content-Type": "application/json" }
-
});
+
return new Response(
+
JSON.stringify({
+
success: false,
+
message: "Not authenticated",
+
}),
+
{
+
status: 401,
+
headers: { "Content-Type": "application/json" },
+
},
+
);
+
}
+
+
// Verify DIDs match between sessions
+
const didsMatch = await checkDidsMatch(ctx.req);
+
if (!didsMatch) {
+
return new Response(
+
JSON.stringify({
+
success: false,
+
message: "Invalid state, original and target DIDs do not match",
+
}),
+
{
+
status: 400,
+
headers: { "Content-Type": "application/json" },
+
},
+
);
}
// Migrate preferences
const migrationLogs: string[] = [];
const startTime = Date.now();
-
console.log(`[${new Date().toISOString()}] Starting preferences migration...`);
-
migrationLogs.push(`[${new Date().toISOString()}] Starting preferences migration...`);
+
console.log(
+
`[${new Date().toISOString()}] Starting preferences migration...`,
+
);
+
migrationLogs.push(
+
`[${new Date().toISOString()}] Starting preferences migration...`,
+
);
// Fetch preferences
-
console.log(`[${new Date().toISOString()}] Fetching preferences from old account...`);
-
migrationLogs.push(`[${new Date().toISOString()}] Fetching preferences from old account...`);
+
console.log(
+
`[${
+
new Date().toISOString()
+
}] Fetching preferences from old account...`,
+
);
+
migrationLogs.push(
+
`[${
+
new Date().toISOString()
+
}] Fetching preferences from old account...`,
+
);
const fetchStartTime = Date.now();
const prefs = await oldAgent.app.bsky.actor.getPreferences();
const fetchTime = Date.now() - fetchStartTime;
-
console.log(`[${new Date().toISOString()}] Preferences fetched in ${fetchTime/1000} seconds`);
-
migrationLogs.push(`[${new Date().toISOString()}] Preferences fetched in ${fetchTime/1000} seconds`);
+
console.log(
+
`[${new Date().toISOString()}] Preferences fetched in ${
+
fetchTime / 1000
+
} seconds`,
+
);
+
migrationLogs.push(
+
`[${new Date().toISOString()}] Preferences fetched in ${
+
fetchTime / 1000
+
} seconds`,
+
);
// Update preferences
-
console.log(`[${new Date().toISOString()}] Updating preferences on new account...`);
-
migrationLogs.push(`[${new Date().toISOString()}] Updating preferences on new account...`);
+
console.log(
+
`[${new Date().toISOString()}] Updating preferences on new account...`,
+
);
+
migrationLogs.push(
+
`[${new Date().toISOString()}] Updating preferences on new account...`,
+
);
const updateStartTime = Date.now();
await newAgent.app.bsky.actor.putPreferences(prefs.data);
const updateTime = Date.now() - updateStartTime;
-
console.log(`[${new Date().toISOString()}] Preferences updated in ${updateTime/1000} seconds`);
-
migrationLogs.push(`[${new Date().toISOString()}] Preferences updated in ${updateTime/1000} seconds`);
+
console.log(
+
`[${new Date().toISOString()}] Preferences updated in ${
+
updateTime / 1000
+
} seconds`,
+
);
+
migrationLogs.push(
+
`[${new Date().toISOString()}] Preferences updated in ${
+
updateTime / 1000
+
} seconds`,
+
);
const totalTime = Date.now() - startTime;
-
const completionMessage = `[${new Date().toISOString()}] Preferences migration completed in ${totalTime/1000} seconds total`;
+
const completionMessage = `[${
+
new Date().toISOString()
+
}] Preferences migration completed in ${totalTime / 1000} seconds total`;
console.log(completionMessage);
migrationLogs.push(completionMessage);
···
message: "Preferences migration completed successfully",
logs: migrationLogs,
timing: {
-
fetchTime: fetchTime/1000,
-
updateTime: updateTime/1000,
-
totalTime: totalTime/1000
-
}
+
fetchTime: fetchTime / 1000,
+
updateTime: updateTime / 1000,
+
totalTime: totalTime / 1000,
+
},
}),
{
status: 200,
headers: {
"Content-Type": "application/json",
...Object.fromEntries(res.headers),
-
}
-
}
+
},
+
},
);
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
-
console.error(`[${new Date().toISOString()}] Preferences migration error:`, message);
-
console.error('Full error details:', error);
+
console.error(
+
`[${new Date().toISOString()}] Preferences migration error:`,
+
message,
+
);
+
console.error("Full error details:", error);
return new Response(
JSON.stringify({
success: false,
message: `Preferences migration failed: ${message}`,
-
error: error instanceof Error ? {
-
name: error.name,
-
message: error.message,
-
stack: error.stack,
-
} : String(error)
+
error: error instanceof Error
+
? {
+
name: error.name,
+
message: error.message,
+
stack: error.stack,
+
}
+
: String(error),
}),
{
status: 500,
headers: {
"Content-Type": "application/json",
...Object.fromEntries(res.headers),
-
}
-
}
+
},
+
},
);
}
-
}
+
},
});
+86 -35
routes/api/migrate/data/repo.ts
···
import { getSessionAgent } from "../../../../lib/sessions.ts";
+
import { checkDidsMatch } from "../../../../lib/check-dids.ts";
import { define } from "../../../../utils.ts";
import { assertMigrationAllowed } from "../../../../lib/migration-state.ts";
···
const oldAgent = await getSessionAgent(ctx.req);
console.log("Repo migration: Got old agent:", !!oldAgent);
-
const newAgent = await getSessionAgent(ctx.req, res, true);
console.log("Repo migration: Got new agent:", !!newAgent);
if (!oldAgent || !newAgent) {
-
return new Response(JSON.stringify({
-
success: false,
-
message: "Not authenticated"
-
}), {
-
status: 401,
-
headers: { "Content-Type": "application/json" }
-
});
+
return new Response(
+
JSON.stringify({
+
success: false,
+
message: "Not authenticated",
+
}),
+
{
+
status: 401,
+
headers: { "Content-Type": "application/json" },
+
},
+
);
+
}
+
+
// Verify DIDs match between sessions
+
const didsMatch = await checkDidsMatch(ctx.req);
+
if (!didsMatch) {
+
return new Response(
+
JSON.stringify({
+
success: false,
+
message: "Invalid state, original and target DIDs do not match",
+
}),
+
{
+
status: 400,
+
headers: { "Content-Type": "application/json" },
+
},
+
);
}
const session = await oldAgent.com.atproto.server.getSession();
···
const migrationLogs: string[] = [];
const startTime = Date.now();
console.log(`[${new Date().toISOString()}] Starting repo migration...`);
-
migrationLogs.push(`[${new Date().toISOString()}] Starting repo migration...`);
+
migrationLogs.push(
+
`[${new Date().toISOString()}] Starting repo migration...`,
+
);
// Get repo data from old account
-
console.log(`[${new Date().toISOString()}] Fetching repo data from old account...`);
-
migrationLogs.push(`[${new Date().toISOString()}] Fetching repo data from old account...`);
+
console.log(
+
`[${new Date().toISOString()}] Fetching repo data from old account...`,
+
);
+
migrationLogs.push(
+
`[${new Date().toISOString()}] Fetching repo data from old account...`,
+
);
const fetchStartTime = Date.now();
const repoData = await oldAgent.com.atproto.sync.getRepo({
···
});
const fetchTime = Date.now() - fetchStartTime;
-
console.log(`[${new Date().toISOString()}] Repo data fetched in ${fetchTime/1000} seconds`);
-
migrationLogs.push(`[${new Date().toISOString()}] Repo data fetched in ${fetchTime/1000} seconds`);
+
console.log(
+
`[${new Date().toISOString()}] Repo data fetched in ${
+
fetchTime / 1000
+
} seconds`,
+
);
+
migrationLogs.push(
+
`[${new Date().toISOString()}] Repo data fetched in ${
+
fetchTime / 1000
+
} seconds`,
+
);
-
console.log(`[${new Date().toISOString()}] Importing repo data to new account...`);
-
migrationLogs.push(`[${new Date().toISOString()}] Importing repo data to new account...`);
+
console.log(
+
`[${new Date().toISOString()}] Importing repo data to new account...`,
+
);
+
migrationLogs.push(
+
`[${new Date().toISOString()}] Importing repo data to new account...`,
+
);
// Import repo data to new account
const importStartTime = Date.now();
await newAgent.com.atproto.repo.importRepo(repoData.data, {
-
encoding: "application/vnd.ipld.car"
+
encoding: "application/vnd.ipld.car",
});
const importTime = Date.now() - importStartTime;
-
console.log(`[${new Date().toISOString()}] Repo data imported in ${importTime/1000} seconds`);
-
migrationLogs.push(`[${new Date().toISOString()}] Repo data imported in ${importTime/1000} seconds`);
+
console.log(
+
`[${new Date().toISOString()}] Repo data imported in ${
+
importTime / 1000
+
} seconds`,
+
);
+
migrationLogs.push(
+
`[${new Date().toISOString()}] Repo data imported in ${
+
importTime / 1000
+
} seconds`,
+
);
const totalTime = Date.now() - startTime;
-
const completionMessage = `[${new Date().toISOString()}] Repo migration completed in ${totalTime/1000} seconds total`;
+
const completionMessage = `[${
+
new Date().toISOString()
+
}] Repo migration completed in ${totalTime / 1000} seconds total`;
console.log(completionMessage);
migrationLogs.push(completionMessage);
···
message: "Repo migration completed successfully",
logs: migrationLogs,
timing: {
-
fetchTime: fetchTime/1000,
-
importTime: importTime/1000,
-
totalTime: totalTime/1000
-
}
+
fetchTime: fetchTime / 1000,
+
importTime: importTime / 1000,
+
totalTime: totalTime / 1000,
+
},
}),
{
status: 200,
headers: {
"Content-Type": "application/json",
...Object.fromEntries(res.headers),
-
}
-
}
+
},
+
},
);
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
-
console.error(`[${new Date().toISOString()}] Repo migration error:`, message);
-
console.error('Full error details:', error);
+
console.error(
+
`[${new Date().toISOString()}] Repo migration error:`,
+
message,
+
);
+
console.error("Full error details:", error);
return new Response(
JSON.stringify({
success: false,
message: `Repo migration failed: ${message}`,
-
error: error instanceof Error ? {
-
name: error.name,
-
message: error.message,
-
stack: error.stack,
-
} : String(error)
+
error: error instanceof Error
+
? {
+
name: error.name,
+
message: error.message,
+
stack: error.stack,
+
}
+
: String(error),
}),
{
status: 500,
headers: {
"Content-Type": "application/json",
...Object.fromEntries(res.headers),
-
}
-
}
+
},
+
},
);
}
-
}
+
},
});
+13
routes/api/migrate/finalize.ts
···
import { getSessionAgent } from "../../../lib/sessions.ts";
+
import { checkDidsMatch } from "../../../lib/check-dids.ts";
import { define } from "../../../utils.ts";
import { assertMigrationAllowed } from "../../../lib/migration-state.ts";
···
return new Response("Migration session not found or invalid", {
status: 400,
});
+
}
+
+
// Verify DIDs match between sessions
+
const didsMatch = await checkDidsMatch(ctx.req);
+
if (!didsMatch) {
+
return new Response(
+
JSON.stringify({
+
success: false,
+
message: "Invalid state, original and target DIDs do not match",
+
}),
+
{ status: 400, headers: { "Content-Type": "application/json" } },
+
);
}
// Activate new account and deactivate old account
+18 -4
routes/api/migrate/identity/request.ts
···
-
import {
-
getSessionAgent,
-
} from "../../../../lib/sessions.ts";
+
import { getSessionAgent } from "../../../../lib/sessions.ts";
+
import { checkDidsMatch } from "../../../../lib/check-dids.ts";
import { define } from "../../../../utils.ts";
import { assertMigrationAllowed } from "../../../../lib/migration-state.ts";
···
);
}
+
// Verify DIDs match between sessions
+
const didsMatch = await checkDidsMatch(ctx.req);
+
if (!didsMatch) {
+
return new Response(
+
JSON.stringify({
+
success: false,
+
message: "Invalid state, original and target DIDs do not match",
+
}),
+
{
+
status: 400,
+
headers: { "Content-Type": "application/json" },
+
},
+
);
+
}
+
// Request the signature
console.log("Requesting PLC operation signature...");
try {
···
console.error("Error requesting PLC operation signature:", {
name: error instanceof Error ? error.name : "Unknown",
message: error instanceof Error ? error.message : String(error),
-
status: 400
+
status: 400,
});
throw error;
}
+17 -3
routes/api/migrate/identity/sign.ts
···
-
import {
-
getSessionAgent,
-
} from "../../../../lib/sessions.ts";
+
import { getSessionAgent } from "../../../../lib/sessions.ts";
+
import { checkDidsMatch } from "../../../../lib/check-dids.ts";
import { Secp256k1Keypair } from "npm:@atproto/crypto";
import * as ui8 from "npm:uint8arrays";
import { define } from "../../../../utils.ts";
···
JSON.stringify({
success: false,
message: "Migration session not found or invalid",
+
}),
+
{
+
status: 400,
+
headers: { "Content-Type": "application/json" },
+
},
+
);
+
}
+
+
// Verify DIDs match between sessions
+
const didsMatch = await checkDidsMatch(ctx.req);
+
if (!didsMatch) {
+
return new Response(
+
JSON.stringify({
+
success: false,
+
message: "Invalid state, original and target DIDs do not match",
}),
{
status: 400,
+2 -2
routes/api/migrate/next-step.ts
···
// Check conditions in sequence to determine the next step
if (!newStatus.data) {
nextStep = 1;
-
} else if (!(newStatus.data.repoCommit &&
+
} else if (!(newStatus.data.repoCommit &&
newStatus.data.indexedRecords === oldStatus.data.indexedRecords &&
newStatus.data.privateStateValues === oldStatus.data.privateStateValues &&
newStatus.data.expectedBlobs === newStatus.data.importedBlobs &&
···
}
});
}
-
})
+
})
+130 -104
routes/api/migrate/status.ts
···
+
import { checkDidsMatch } from "../../../lib/check-dids.ts";
import { getSessionAgent } from "../../../lib/sessions.ts";
import { define } from "../../../utils.ts";
export const handler = define.handlers({
-
async GET(ctx) {
-
console.log("Status check: Starting");
-
const url = new URL(ctx.req.url);
-
const params = new URLSearchParams(url.search);
-
const step = params.get("step");
-
console.log("Status check: Step", step);
+
async GET(ctx) {
+
console.log("Status check: Starting");
+
const url = new URL(ctx.req.url);
+
const params = new URLSearchParams(url.search);
+
const step = params.get("step");
+
console.log("Status check: Step", step);
-
console.log("Status check: Getting agents");
-
const oldAgent = await getSessionAgent(ctx.req);
-
const newAgent = await getSessionAgent(ctx.req, new Response(), true);
-
-
if (!oldAgent || !newAgent) {
-
console.log("Status check: Unauthorized - missing agents", {
-
hasOldAgent: !!oldAgent,
-
hasNewAgent: !!newAgent
-
});
-
return new Response("Unauthorized", { status: 401 });
-
}
+
console.log("Status check: Getting agents");
+
const oldAgent = await getSessionAgent(ctx.req);
+
const newAgent = await getSessionAgent(ctx.req, new Response(), true);
-
console.log("Status check: Fetching account statuses");
-
const oldStatus = await oldAgent.com.atproto.server.checkAccountStatus();
-
const newStatus = await newAgent.com.atproto.server.checkAccountStatus();
-
-
if (!oldStatus.data || !newStatus.data) {
-
console.error("Status check: Failed to verify status", {
-
hasOldStatus: !!oldStatus.data,
-
hasNewStatus: !!newStatus.data
-
});
-
return new Response("Could not verify status", { status: 500 });
-
}
+
if (!oldAgent || !newAgent) {
+
console.log("Status check: Unauthorized - missing agents", {
+
hasOldAgent: !!oldAgent,
+
hasNewAgent: !!newAgent,
+
});
+
return new Response("Unauthorized", { status: 401 });
+
}
-
console.log("Status check: Account statuses", {
-
old: oldStatus.data,
-
new: newStatus.data
-
});
+
const didsMatch = await checkDidsMatch(ctx.req);
-
const readyToContinue = () => {
-
if (step) {
-
console.log("Status check: Evaluating step", step);
-
switch (step) {
-
case "1": {
-
if (newStatus.data) {
-
console.log("Status check: Step 1 ready");
-
return { ready: true };
-
}
-
console.log("Status check: Step 1 not ready - new account status not available");
-
return { ready: false, reason: "New account status not available" };
-
}
-
case "2": {
-
const isReady = newStatus.data.repoCommit &&
-
newStatus.data.indexedRecords === oldStatus.data.indexedRecords &&
-
newStatus.data.privateStateValues === oldStatus.data.privateStateValues &&
-
newStatus.data.expectedBlobs === newStatus.data.importedBlobs &&
-
newStatus.data.importedBlobs === oldStatus.data.importedBlobs;
+
console.log("Status check: Fetching account statuses");
+
const oldStatus = await oldAgent.com.atproto.server.checkAccountStatus();
+
const newStatus = await newAgent.com.atproto.server.checkAccountStatus();
-
if (isReady) {
-
console.log("Status check: Step 2 ready");
-
return { ready: true };
-
}
+
if (!oldStatus.data || !newStatus.data) {
+
console.error("Status check: Failed to verify status", {
+
hasOldStatus: !!oldStatus.data,
+
hasNewStatus: !!newStatus.data,
+
});
+
return new Response("Could not verify status", { status: 500 });
+
}
-
const reasons = [];
-
if (!newStatus.data.repoCommit) reasons.push("Repository not imported.");
-
if (newStatus.data.indexedRecords < oldStatus.data.indexedRecords)
-
reasons.push("Not all records imported.");
-
if (newStatus.data.privateStateValues < oldStatus.data.privateStateValues)
-
reasons.push("Not all private state values imported.");
-
if (newStatus.data.expectedBlobs !== newStatus.data.importedBlobs)
-
reasons.push("Expected blobs not fully imported.");
-
if (newStatus.data.importedBlobs < oldStatus.data.importedBlobs)
-
reasons.push("Not all blobs imported.");
+
console.log("Status check: Account statuses", {
+
old: oldStatus.data,
+
new: newStatus.data,
+
});
-
console.log("Status check: Step 2 not ready", { reasons });
-
return { ready: false, reason: reasons.join(", ") };
-
}
-
case "3": {
-
if (newStatus.data.validDid) {
-
console.log("Status check: Step 3 ready");
-
return { ready: true };
-
}
-
console.log("Status check: Step 3 not ready - DID not valid");
-
return { ready: false, reason: "DID not valid" };
-
}
-
case "4": {
-
if (newStatus.data.activated === true && oldStatus.data.activated === false) {
-
console.log("Status check: Step 4 ready");
-
return { ready: true };
-
}
-
console.log("Status check: Step 4 not ready - Account not activated");
-
return { ready: false, reason: "Account not activated" };
-
}
-
}
-
} else {
-
console.log("Status check: No step specified, returning ready");
-
return { ready: true };
+
const readyToContinue = () => {
+
if (!didsMatch) {
+
return {
+
ready: false,
+
reason: "Invalid state, original and target DIDs do not match",
+
};
+
}
+
if (step) {
+
console.log("Status check: Evaluating step", step);
+
switch (step) {
+
case "1": {
+
if (newStatus.data) {
+
console.log("Status check: Step 1 ready");
+
return { ready: true };
+
}
+
console.log(
+
"Status check: Step 1 not ready - new account status not available",
+
);
+
return { ready: false, reason: "New account status not available" };
+
}
+
case "2": {
+
const isReady = newStatus.data.repoCommit &&
+
newStatus.data.indexedRecords === oldStatus.data.indexedRecords &&
+
newStatus.data.privateStateValues ===
+
oldStatus.data.privateStateValues &&
+
newStatus.data.expectedBlobs === newStatus.data.importedBlobs &&
+
newStatus.data.importedBlobs === oldStatus.data.importedBlobs;
+
+
if (isReady) {
+
console.log("Status check: Step 2 ready");
+
return { ready: true };
+
}
+
+
const reasons = [];
+
if (!newStatus.data.repoCommit) {
+
reasons.push("Repository not imported.");
+
}
+
if (newStatus.data.indexedRecords < oldStatus.data.indexedRecords) {
+
reasons.push("Not all records imported.");
+
}
+
if (
+
newStatus.data.privateStateValues <
+
oldStatus.data.privateStateValues
+
) {
+
reasons.push("Not all private state values imported.");
+
}
+
if (newStatus.data.expectedBlobs !== newStatus.data.importedBlobs) {
+
reasons.push("Expected blobs not fully imported.");
+
}
+
if (newStatus.data.importedBlobs < oldStatus.data.importedBlobs) {
+
reasons.push("Not all blobs imported.");
+
}
+
+
console.log("Status check: Step 2 not ready", { reasons });
+
return { ready: false, reason: reasons.join(", ") };
+
}
+
case "3": {
+
if (newStatus.data.validDid) {
+
console.log("Status check: Step 3 ready");
+
return { ready: true };
+
}
+
console.log("Status check: Step 3 not ready - DID not valid");
+
return { ready: false, reason: "DID not valid" };
+
}
+
case "4": {
+
if (
+
newStatus.data.activated === true &&
+
oldStatus.data.activated === false
+
) {
+
console.log("Status check: Step 4 ready");
+
return { ready: true };
}
+
console.log(
+
"Status check: Step 4 not ready - Account not activated",
+
);
+
return { ready: false, reason: "Account not activated" };
+
}
}
+
} else {
+
console.log("Status check: No step specified, returning ready");
+
return { ready: true };
+
}
+
};
-
const status = {
-
activated: newStatus.data.activated,
-
validDid: newStatus.data.validDid,
-
repoCommit: newStatus.data.repoCommit,
-
repoRev: newStatus.data.repoRev,
-
repoBlocks: newStatus.data.repoBlocks,
-
expectedRecords: oldStatus.data.indexedRecords,
-
indexedRecords: newStatus.data.indexedRecords,
-
privateStateValues: newStatus.data.privateStateValues,
-
expectedBlobs: newStatus.data.expectedBlobs,
-
importedBlobs: newStatus.data.importedBlobs,
-
...readyToContinue()
-
}
+
const status = {
+
activated: newStatus.data.activated,
+
validDid: newStatus.data.validDid,
+
repoCommit: newStatus.data.repoCommit,
+
repoRev: newStatus.data.repoRev,
+
repoBlocks: newStatus.data.repoBlocks,
+
expectedRecords: oldStatus.data.indexedRecords,
+
indexedRecords: newStatus.data.indexedRecords,
+
privateStateValues: newStatus.data.privateStateValues,
+
expectedBlobs: newStatus.data.expectedBlobs,
+
importedBlobs: newStatus.data.importedBlobs,
+
...readyToContinue(),
+
};
-
console.log("Status check: Complete", status);
-
return Response.json(status);
-
}
-
})
+
console.log("Status check: Complete", status);
+
return Response.json(status);
+
},
+
});
+2 -2
routes/migrate/progress.tsx
···
if (!service || !handle || !email || !password) {
return (
-
<div class="min-h-screen bg-gray-50 dark:bg-gray-900 p-4">
+
<div class="bg-gray-50 dark:bg-gray-900 p-4">
<div class="max-w-2xl mx-auto">
<div class="bg-red-50 dark:bg-red-900 p-4 rounded-lg">
<p class="text-red-800 dark:text-red-200">
···
}
return (
-
<div class="min-h-screen bg-gray-50 dark:bg-gray-900 p-4">
+
<div class="bg-gray-50 dark:bg-gray-900 p-4">
<div class="max-w-2xl mx-auto">
<h1 class="font-mono text-3xl font-bold text-gray-900 dark:text-white mb-8">
Migration Progress