Graphical PDS migrator for AT Protocol

ooth but not yet

Changed files
+81 -60
islands
lib
routes
api
oauth
oauth-client-metadata.json
static
+2 -1
dev.ts
···
import { Builder } from "fresh/dev";
import { tailwind } from "@fresh/plugin-tailwind";
+
import { State } from "./utils.ts";
-
const builder = new Builder({ target: "safari12" });
+
const builder = new Builder<State>({ target: "safari12" });
tailwind(builder);
if (Deno.args.includes("build")) {
+21 -21
islands/MigrationProgress.tsx
···
}`,
);
setSteps((prevSteps) =>
-
prevSteps.map((step, i) =>
-
i === index
-
? { ...step, status, error, isVerificationError }
-
: i > index
-
? {
-
...step,
-
status: "pending",
-
error: undefined,
-
isVerificationError: undefined,
+
prevSteps.map((step, i) => {
+
if (i === index) {
+
// Update the current step
+
return { ...step, status, error, isVerificationError };
+
} else if (i > index) {
+
// Reset future steps to pending only if current step is erroring
+
if (status === "error") {
+
return {
+
...step,
+
status: "pending",
+
error: undefined,
+
isVerificationError: undefined,
+
};
}
-
: step
-
)
+
// Otherwise keep future steps as they are
+
return step;
+
} else {
+
// Keep previous steps as they are (preserve completed status)
+
return step;
+
}
+
})
);
};
···
return;
}
-
try {
-
await client.startMigration(props);
-
} catch (error) {
-
console.error("Unhandled migration error:", error);
-
updateStepStatus(
-
0,
-
"error",
-
error as string ?? "Unknown error occurred",
-
);
-
}
+
await client.startMigration(props);
})();
}, []);
+2 -6
lib/client.ts
···
await this.nextStepHook(2);
}
-
// Step 4: Finalize Migration
-
await this.finalizeMigration();
-
if (this.nextStepHook) {
-
await this.nextStepHook(3);
-
}
-
+
// Stop here - finalization will be called from handleIdentityMigration
+
// after user enters the token
return;
} catch (error) {
console.error("Migration error in try/catch:", error);
+19 -11
lib/oauth/client.ts
···
import { AtprotoOAuthClient } from "@bigmoves/atproto-oauth-client";
import { SessionStore, StateStore } from "../storage.ts";
+
export const scope = [
+
"atproto",
+
"account:email",
+
"account:status?action=manage",
+
"identity:*",
+
"rpc:*?aud=did:web:api.bsky.app#bsky_appview",
+
"rpc:com.atproto.server.createAccount?aud=*",
+
].join(" ");
+
const publicUrl = Deno.env.get("PUBLIC_URL");
+
const url = publicUrl || `http://127.0.0.1:8000`;
+
export const clientId = publicUrl
+
? `${url}/oauth-client-metadata.json`
+
: `http://localhost?redirect_uri=${
+
encodeURIComponent(`${url}/api/oauth/callback`)
+
}&scope=${encodeURIComponent(scope)}`;
+
console.log(`ClientId: ${clientId}`);
+
/**
* Create the OAuth client.
* @param db - The Deno KV instance for the database
···
throw new Error("PUBLIC_URL is not set");
}
-
const publicUrl = Deno.env.get("PUBLIC_URL");
-
const url = publicUrl || `http://127.0.0.1:8000`;
-
const enc = encodeURIComponent;
-
const clientId = publicUrl
-
? `${url}/oauth-client-metadata.json`
-
: `http://localhost?redirect_uri=${
-
enc(`${url}/api/oauth/callback`)
-
}&scope=${enc("atproto transition:generic transition:chat.bsky")}`;
-
console.log(`ClientId: ${clientId}`);
-
return new AtprotoOAuthClient({
clientMetadata: {
client_name: "Statusphere React App",
client_id: clientId,
client_uri: url,
redirect_uris: [`${url}/api/oauth/callback`],
-
scope: "atproto transition:generic transition:chat.bsky",
+
scope: scope,
grant_types: ["authorization_code", "refresh_token"],
response_types: ["code"],
application_type: "web",
···
},
stateStore: new StateStore(db),
sessionStore: new SessionStore(db),
+
didCache: undefined,
});
};
+2 -2
routes/api/oauth/initiate.ts
···
import { isValidHandle } from "npm:@atproto/syntax";
-
import { oauthClient } from "../../../lib/oauth/client.ts";
+
import { oauthClient, scope } from "../../../lib/oauth/client.ts";
import { define } from "../../../utils.ts";
function isValidUrl(url: string): boolean {
···
// Initiate the OAuth flow
try {
const url = await oauthClient.authorize(handle, {
-
scope: "atproto transition:generic transition:chat.bsky",
+
scope,
});
return Response.json({ redirectUrl: url.toString() });
} catch (err) {
+35
routes/oauth-client-metadata.json/index.ts
···
+
import { clientId, scope } from "../../lib/oauth/client.ts";
+
import { define } from "../../utils.ts";
+
+
/**
+
* API endpoint to check the current migration state.
+
* Returns the migration state information including whether migrations are allowed.
+
*/
+
export const handler = define.handlers({
+
GET(_ctx) {
+
return Response.json({
+
client_name: "ATP Airport",
+
client_id: clientId,
+
client_uri: "https://atpairport.com",
+
redirect_uris: [
+
"https://atpairport.com/api/oauth/callback",
+
],
+
scope,
+
grant_types: [
+
"authorization_code",
+
"refresh_token",
+
],
+
response_types: [
+
"code",
+
],
+
application_type: "web",
+
token_endpoint_auth_method: "none",
+
dpop_bound_access_tokens: true,
+
}, {
+
status: 200,
+
headers: {
+
"Content-Type": "application/json",
+
},
+
});
+
},
+
});
-19
static/oauth-client-metadata.json
···
-
{
-
"client_name": "ATP Airport",
-
"client_id": "https://atpairport.com/oauth-client-metadata.json",
-
"client_uri": "https://atpairport.com",
-
"redirect_uris": [
-
"https://atpairport.com/api/oauth/callback"
-
],
-
"scope": "atproto transition:generic transition:chat.bsky",
-
"grant_types": [
-
"authorization_code",
-
"refresh_token"
-
],
-
"response_types": [
-
"code"
-
],
-
"application_type": "web",
-
"token_endpoint_auth_method": "none",
-
"dpop_bound_access_tokens": true
-
}