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
··· 1 1 import { Builder } from "fresh/dev"; 2 2 import { tailwind } from "@fresh/plugin-tailwind"; 3 + import { State } from "./utils.ts"; 3 4 4 - const builder = new Builder({ target: "safari12" }); 5 + const builder = new Builder<State>({ target: "safari12" }); 5 6 tailwind(builder); 6 7 7 8 if (Deno.args.includes("build")) {
+21 -21
islands/MigrationProgress.tsx
··· 37 37 }`, 38 38 ); 39 39 setSteps((prevSteps) => 40 - prevSteps.map((step, i) => 41 - i === index 42 - ? { ...step, status, error, isVerificationError } 43 - : i > index 44 - ? { 45 - ...step, 46 - status: "pending", 47 - error: undefined, 48 - isVerificationError: undefined, 40 + prevSteps.map((step, i) => { 41 + if (i === index) { 42 + // Update the current step 43 + return { ...step, status, error, isVerificationError }; 44 + } else if (i > index) { 45 + // Reset future steps to pending only if current step is erroring 46 + if (status === "error") { 47 + return { 48 + ...step, 49 + status: "pending", 50 + error: undefined, 51 + isVerificationError: undefined, 52 + }; 49 53 } 50 - : step 51 - ) 54 + // Otherwise keep future steps as they are 55 + return step; 56 + } else { 57 + // Keep previous steps as they are (preserve completed status) 58 + return step; 59 + } 60 + }) 52 61 ); 53 62 }; 54 63 ··· 152 161 return; 153 162 } 154 163 155 - try { 156 - await client.startMigration(props); 157 - } catch (error) { 158 - console.error("Unhandled migration error:", error); 159 - updateStepStatus( 160 - 0, 161 - "error", 162 - error as string ?? "Unknown error occurred", 163 - ); 164 - } 164 + await client.startMigration(props); 165 165 })(); 166 166 }, []); 167 167
+2 -6
lib/client.ts
··· 205 205 await this.nextStepHook(2); 206 206 } 207 207 208 - // Step 4: Finalize Migration 209 - await this.finalizeMigration(); 210 - if (this.nextStepHook) { 211 - await this.nextStepHook(3); 212 - } 213 - 208 + // Stop here - finalization will be called from handleIdentityMigration 209 + // after user enters the token 214 210 return; 215 211 } catch (error) { 216 212 console.error("Migration error in try/catch:", error);
+19 -11
lib/oauth/client.ts
··· 1 1 import { AtprotoOAuthClient } from "@bigmoves/atproto-oauth-client"; 2 2 import { SessionStore, StateStore } from "../storage.ts"; 3 3 4 + export const scope = [ 5 + "atproto", 6 + "account:email", 7 + "account:status?action=manage", 8 + "identity:*", 9 + "rpc:*?aud=did:web:api.bsky.app#bsky_appview", 10 + "rpc:com.atproto.server.createAccount?aud=*", 11 + ].join(" "); 12 + const publicUrl = Deno.env.get("PUBLIC_URL"); 13 + const url = publicUrl || `http://127.0.0.1:8000`; 14 + export const clientId = publicUrl 15 + ? `${url}/oauth-client-metadata.json` 16 + : `http://localhost?redirect_uri=${ 17 + encodeURIComponent(`${url}/api/oauth/callback`) 18 + }&scope=${encodeURIComponent(scope)}`; 19 + console.log(`ClientId: ${clientId}`); 20 + 4 21 /** 5 22 * Create the OAuth client. 6 23 * @param db - The Deno KV instance for the database ··· 11 28 throw new Error("PUBLIC_URL is not set"); 12 29 } 13 30 14 - const publicUrl = Deno.env.get("PUBLIC_URL"); 15 - const url = publicUrl || `http://127.0.0.1:8000`; 16 - const enc = encodeURIComponent; 17 - const clientId = publicUrl 18 - ? `${url}/oauth-client-metadata.json` 19 - : `http://localhost?redirect_uri=${ 20 - enc(`${url}/api/oauth/callback`) 21 - }&scope=${enc("atproto transition:generic transition:chat.bsky")}`; 22 - console.log(`ClientId: ${clientId}`); 23 - 24 31 return new AtprotoOAuthClient({ 25 32 clientMetadata: { 26 33 client_name: "Statusphere React App", 27 34 client_id: clientId, 28 35 client_uri: url, 29 36 redirect_uris: [`${url}/api/oauth/callback`], 30 - scope: "atproto transition:generic transition:chat.bsky", 37 + scope: scope, 31 38 grant_types: ["authorization_code", "refresh_token"], 32 39 response_types: ["code"], 33 40 application_type: "web", ··· 36 43 }, 37 44 stateStore: new StateStore(db), 38 45 sessionStore: new SessionStore(db), 46 + didCache: undefined, 39 47 }); 40 48 }; 41 49
+2 -2
routes/api/oauth/initiate.ts
··· 1 1 import { isValidHandle } from "npm:@atproto/syntax"; 2 - import { oauthClient } from "../../../lib/oauth/client.ts"; 2 + import { oauthClient, scope } from "../../../lib/oauth/client.ts"; 3 3 import { define } from "../../../utils.ts"; 4 4 5 5 function isValidUrl(url: string): boolean { ··· 26 26 // Initiate the OAuth flow 27 27 try { 28 28 const url = await oauthClient.authorize(handle, { 29 - scope: "atproto transition:generic transition:chat.bsky", 29 + scope, 30 30 }); 31 31 return Response.json({ redirectUrl: url.toString() }); 32 32 } catch (err) {
+35
routes/oauth-client-metadata.json/index.ts
··· 1 + import { clientId, scope } from "../../lib/oauth/client.ts"; 2 + import { define } from "../../utils.ts"; 3 + 4 + /** 5 + * API endpoint to check the current migration state. 6 + * Returns the migration state information including whether migrations are allowed. 7 + */ 8 + export const handler = define.handlers({ 9 + GET(_ctx) { 10 + return Response.json({ 11 + client_name: "ATP Airport", 12 + client_id: clientId, 13 + client_uri: "https://atpairport.com", 14 + redirect_uris: [ 15 + "https://atpairport.com/api/oauth/callback", 16 + ], 17 + scope, 18 + grant_types: [ 19 + "authorization_code", 20 + "refresh_token", 21 + ], 22 + response_types: [ 23 + "code", 24 + ], 25 + application_type: "web", 26 + token_endpoint_auth_method: "none", 27 + dpop_bound_access_tokens: true, 28 + }, { 29 + status: 200, 30 + headers: { 31 + "Content-Type": "application/json", 32 + }, 33 + }); 34 + }, 35 + });
-19
static/oauth-client-metadata.json
··· 1 - { 2 - "client_name": "ATP Airport", 3 - "client_id": "https://atpairport.com/oauth-client-metadata.json", 4 - "client_uri": "https://atpairport.com", 5 - "redirect_uris": [ 6 - "https://atpairport.com/api/oauth/callback" 7 - ], 8 - "scope": "atproto transition:generic transition:chat.bsky", 9 - "grant_types": [ 10 - "authorization_code", 11 - "refresh_token" 12 - ], 13 - "response_types": [ 14 - "code" 15 - ], 16 - "application_type": "web", 17 - "token_endpoint_auth_method": "none", 18 - "dpop_bound_access_tokens": true 19 - }