Graphical PDS migrator for AT Protocol

jsdoc

+6
components/AirportSign.tsx
··· 1 + /** 2 + * The airport sign component, used on the landing page. 3 + * Looks like a physical airport sign with a screen. 4 + * @returns The airport sign component 5 + * @component 6 + */ 1 7 export default function AirportSign() { 2 8 return ( 3 9 <div class="relative inline-block mb-8 sm:mb-12">
+10
components/Button.tsx
··· 12 12 type ButtonProps = ButtonBaseProps & Omit<JSX.HTMLAttributes<HTMLButtonElement>, keyof ButtonBaseProps>; 13 13 type AnchorProps = ButtonBaseProps & Omit<JSX.HTMLAttributes<HTMLAnchorElement>, keyof ButtonBaseProps> & { href: string }; 14 14 15 + /** 16 + * The button props or anchor props for a button or link. 17 + * @type {Props} 18 + */ 15 19 type Props = ButtonProps | AnchorProps; 16 20 21 + /** 22 + * Styled button component. 23 + * @param props - The button props 24 + * @returns The button component 25 + * @component 26 + */ 17 27 export function Button(props: Props) { 18 28 const { color = "blue", icon, iconAlt, label, className = "", condensed = false, ...rest } = props; 19 29 const isAnchor = 'href' in props;
+5
islands/CredLogin.tsx
··· 1 1 import { useState } from "preact/hooks"; 2 2 import { JSX } from "preact"; 3 3 4 + /** 5 + * The credential login form. 6 + * @returns The credential login form 7 + * @component 8 + */ 4 9 export default function CredLogin() { 5 10 const [handle, setHandle] = useState(""); 6 11 const [password, setPassword] = useState("");
+5
islands/HandleInput.tsx
··· 1 1 import { useState } from "preact/hooks"; 2 2 import { JSX } from "preact"; 3 3 4 + /** 5 + * The OAuth handle input form. 6 + * @returns The handle input form 7 + * @component 8 + */ 4 9 export default function HandleInput() { 5 10 const [handle, setHandle] = useState(""); 6 11 const [error, setError] = useState<string | null>(null);
+15
islands/Header.tsx
··· 2 2 import { IS_BROWSER } from "fresh/runtime"; 3 3 import { Button } from "../components/Button.tsx"; 4 4 5 + /** 6 + * The user interface. 7 + * @type {User} 8 + */ 5 9 interface User { 6 10 did: string; 7 11 handle?: string; 8 12 } 9 13 14 + /** 15 + * Truncate text to a maximum length. 16 + * @param text - The text to truncate 17 + * @param maxLength - The maximum length 18 + * @returns The truncated text 19 + */ 10 20 function truncateText(text: string, maxLength: number) { 11 21 if (text.length <= maxLength) return text; 12 22 let truncated = text.slice(0, maxLength); ··· 17 27 return truncated + "..."; 18 28 } 19 29 30 + /** 31 + * The header component. 32 + * @returns The header component 33 + * @component 34 + */ 20 35 export default function Header() { 21 36 const [user, setUser] = useState<User | null>(null); 22 37 const [showDropdown, setShowDropdown] = useState(false);
+5
islands/LoginSelector.tsx
··· 2 2 import HandleInput from "./HandleInput.tsx" 3 3 import CredLogin from "./CredLogin.tsx" 4 4 5 + /** 6 + * The login method selector for OAuth or Credential. 7 + * @returns The login method selector 8 + * @component 9 + */ 5 10 export default function LoginMethodSelector() { 6 11 const [loginMethod, setLoginMethod] = useState<'oauth' | 'password'>('password') 7 12
+15
islands/MigrationProgress.tsx
··· 1 1 import { useEffect, useState } from "preact/hooks"; 2 2 3 + /** 4 + * The migration progress props. 5 + * @type {MigrationProgressProps} 6 + */ 3 7 interface MigrationProgressProps { 4 8 service: string; 5 9 handle: string; ··· 8 12 invite?: string; 9 13 } 10 14 15 + /** 16 + * The migration step. 17 + * @type {MigrationStep} 18 + */ 11 19 interface MigrationStep { 12 20 name: string; 13 21 status: "pending" | "in-progress" | "verifying" | "completed" | "error"; 14 22 error?: string; 15 23 } 16 24 25 + /** 26 + * The migration progress component. 27 + * @param props - The migration progress props 28 + * @returns The migration progress component 29 + * @component 30 + */ 17 31 export default function MigrationProgress(props: MigrationProgressProps) { 18 32 const [token, setToken] = useState(""); 19 33 ··· 538 552 Migration completed successfully! You can now close this page. 539 553 </p> 540 554 <button 555 + type="button" 541 556 onClick={async () => { 542 557 try { 543 558 const response = await fetch("/api/logout", {
+18
islands/MigrationSetup.tsx
··· 1 1 import { useState, useEffect } from "preact/hooks"; 2 2 import { IS_BROWSER } from "fresh/runtime"; 3 3 4 + /** 5 + * The migration setup props. 6 + * @type {MigrationSetupProps} 7 + */ 4 8 interface MigrationSetupProps { 5 9 service?: string | null; 6 10 handle?: string | null; ··· 8 12 invite?: string | null; 9 13 } 10 14 15 + /** 16 + * The server description. 17 + * @type {ServerDescription} 18 + */ 11 19 interface ServerDescription { 12 20 inviteCodeRequired: boolean; 13 21 availableUserDomains: string[]; 14 22 } 15 23 24 + /** 25 + * The user passport. 26 + * @type {UserPassport} 27 + */ 16 28 interface UserPassport { 17 29 did: string; 18 30 handle: string; ··· 20 32 createdAt?: string; 21 33 } 22 34 35 + /** 36 + * The migration setup component. 37 + * @param props - The migration setup props 38 + * @returns The migration setup component 39 + * @component 40 + */ 23 41 export default function MigrationSetup(props: MigrationSetupProps) { 24 42 const [service, setService] = useState(props.service || ""); 25 43 const [handlePrefix, setHandlePrefix] = useState(
+10
islands/OAuthCallback.tsx
··· 1 1 import { useEffect, useState } from "preact/hooks"; 2 2 import { IS_BROWSER } from "fresh/runtime"; 3 3 4 + /** 5 + * The OAuth callback props. 6 + * @type {OAuthCallbackProps} 7 + */ 4 8 interface OAuthCallbackProps { 5 9 error?: string; 6 10 } 7 11 12 + /** 13 + * The OAuth callback component. 14 + * @param props - The OAuth callback props 15 + * @returns The OAuth callback component 16 + * @component 17 + */ 8 18 export default function OAuthCallback( 9 19 { error: initialError }: OAuthCallbackProps, 10 20 ) {
+9
islands/SocialLinks.tsx
··· 1 1 import { useEffect, useState } from "preact/hooks"; 2 2 import * as Icon from 'npm:preact-feather'; 3 3 4 + /** 5 + * The GitHub repository. 6 + * @type {GitHubRepo} 7 + */ 4 8 interface GitHubRepo { 5 9 stargazers_count: number; 6 10 } 7 11 12 + /** 13 + * The social links component. 14 + * @returns The social links component 15 + * @component 16 + */ 8 17 export default function SocialLinks() { 9 18 const [starCount, setStarCount] = useState<number | null>(null); 10 19
+9
islands/Ticket.tsx
··· 1 1 import { useEffect, useState } from "preact/hooks"; 2 2 import { IS_BROWSER } from "fresh/runtime"; 3 3 4 + /** 5 + * The user interface for the ticket component. 6 + * @type {User} 7 + */ 4 8 interface User { 5 9 did: string; 6 10 handle?: string; 7 11 } 8 12 13 + /** 14 + * The ticket component for the landing page. 15 + * @returns The ticket component 16 + * @component 17 + */ 9 18 export default function Ticket() { 10 19 const [user, setUser] = useState<User | null>(null); 11 20
+34
lib/cred/sessions.ts
··· 5 5 let migrationSessionOptions: SessionOptions; 6 6 let credentialSessionOptions: SessionOptions; 7 7 8 + /** 9 + * Get the session options for the given request. 10 + * @param isMigration - Whether to get the migration session options 11 + * @returns The session options 12 + */ 8 13 async function getOptions(isMigration: boolean) { 9 14 if (isMigration) { 10 15 if (!migrationSessionOptions) { ··· 19 24 return credentialSessionOptions; 20 25 } 21 26 27 + /** 28 + * Get the credential session for the given request. 29 + * @param req - The request object 30 + * @param res - The response object 31 + * @param isMigration - Whether to get the migration session 32 + * @returns The credential session 33 + */ 22 34 export async function getCredentialSession( 23 35 req: Request, 24 36 res: Response = new Response(), ··· 32 44 ); 33 45 } 34 46 47 + /** 48 + * Get the credential agent for the given request. 49 + * @param req - The request object 50 + * @param res - The response object 51 + * @param isMigration - Whether to get the migration session 52 + * @returns The credential agent 53 + */ 35 54 export async function getCredentialAgent( 36 55 req: Request, 37 56 res: Response = new Response(), ··· 76 95 } 77 96 } 78 97 98 + /** 99 + * Set the credential session for the given request. 100 + * @param req - The request object 101 + * @param res - The response object 102 + * @param data - The credential session data 103 + * @param isMigration - Whether to set the migration session 104 + * @returns The credential session 105 + */ 79 106 export async function setCredentialSession( 80 107 req: Request, 81 108 res: Response, ··· 95 122 return session; 96 123 } 97 124 125 + /** 126 + * Get the credential session agent for the given request. 127 + * @param req - The request object 128 + * @param res - The response object 129 + * @param isMigration - Whether to get the migration session 130 + * @returns The credential session agent 131 + */ 98 132 export async function getCredentialSessionAgent( 99 133 req: Request, 100 134 res: Response = new Response(),
+16
lib/id-resolver.ts
··· 8 8 pds: string; 9 9 } 10 10 11 + /** 12 + * ID resolver instance. 13 + */ 11 14 const idResolver = createIdResolver(); 12 15 export const resolver = createBidirectionalResolver(idResolver); 13 16 17 + /** 18 + * Create the ID resolver. 19 + * @returns The ID resolver 20 + */ 14 21 export function createIdResolver() { 15 22 return new IdResolver(); 16 23 } 17 24 25 + /** 26 + * The bidirectional resolver. 27 + * @interface 28 + */ 18 29 export interface BidirectionalResolver { 19 30 resolveDidToHandle(did: string): Promise<string>; 20 31 resolveDidsToHandles(dids: string[]): Promise<Record<string, string>>; 21 32 resolveDidToPdsUrl(did: string): Promise<string | undefined>; 22 33 } 23 34 35 + /** 36 + * Create the bidirectional resolver. 37 + * @param resolver - The ID resolver 38 + * @returns The bidirectional resolver 39 + */ 24 40 export function createBidirectionalResolver(resolver: IdResolver) { 25 41 return { 26 42 async resolveDidToHandle(did: string): Promise<string> {
+5
lib/oauth/client.ts
··· 1 1 import { AtprotoOAuthClient } from "@bigmoves/atproto-oauth-client"; 2 2 import { SessionStore, StateStore } from "../storage.ts"; 3 3 4 + /** 5 + * Create the OAuth client. 6 + * @param db - The Deno KV instance for the database 7 + * @returns The OAuth client 8 + */ 4 9 export const createClient = (db: Deno.Kv) => { 5 10 if (Deno.env.get("NODE_ENV") == "production" && !Deno.env.get("PUBLIC_URL")) { 6 11 throw new Error("PUBLIC_URL is not set");
+15
lib/oauth/sessions.ts
··· 5 5 6 6 let oauthSessionOptions: SessionOptions; 7 7 8 + /** 9 + * Get the OAuth session options. 10 + * @returns The OAuth session options 11 + */ 8 12 async function getOptions() { 9 13 if (!oauthSessionOptions) { 10 14 oauthSessionOptions = await createSessionOptions("oauth_sid"); ··· 12 16 return oauthSessionOptions; 13 17 } 14 18 19 + /** 20 + * Get the OAuth session agent for the given request. 21 + * @param req - The request object 22 + * @returns The OAuth session agent 23 + */ 15 24 export async function getOauthSessionAgent( 16 25 req: Request 17 26 ) { ··· 47 56 } 48 57 } 49 58 59 + /** 60 + * Get the OAuth session for the given request. 61 + * @param req - The request object 62 + * @param res - The response object 63 + * @returns The OAuth session 64 + */ 50 65 export async function getOauthSession( 51 66 req: Request, 52 67 res: Response = new Response(),
+18
lib/sessions.ts
··· 4 4 import { getOauthSession, getOauthSessionAgent } from "./oauth/sessions.ts"; 5 5 import { IronSession } from "npm:iron-session"; 6 6 7 + /** 8 + * Get the session for the given request. 9 + * @param req - The request object 10 + * @param res - The response object 11 + * @param isMigration - Whether to get the migration session 12 + * @returns The session 13 + */ 7 14 export async function getSession( 8 15 req: Request, 9 16 res: Response = new Response(), ··· 26 33 throw new Error("No session found"); 27 34 } 28 35 36 + /** 37 + * Get the session agent for the given request. 38 + * @param req - The request object 39 + * @param res - The response object 40 + * @param isMigration - Whether to get the migration session 41 + * @returns The session agent 42 + */ 29 43 export async function getSessionAgent( 30 44 req: Request, 31 45 res: Response = new Response(), ··· 49 63 return null; 50 64 } 51 65 66 + /** 67 + * Destroy all sessions for the given request. 68 + * @param req - The request object 69 + */ 52 70 export async function destroyAllSessions(req: Request) { 53 71 const oauthSession = await getOauthSession(req); 54 72 const credentialSession = await getCredentialSession(req);
+8
lib/storage.ts
··· 5 5 NodeSavedStateStore, 6 6 } from "jsr:@bigmoves/atproto-oauth-client"; 7 7 8 + /** 9 + * The state store for sessions. 10 + * @implements {NodeSavedStateStore} 11 + */ 8 12 export class StateStore implements NodeSavedStateStore { 9 13 constructor(private db: Deno.Kv) {} 10 14 async get(key: string): Promise<NodeSavedState | undefined> { ··· 19 23 } 20 24 } 21 25 26 + /** 27 + * The session store for sessions. 28 + * @implements {NodeSavedSessionStore} 29 + */ 22 30 export class SessionStore implements NodeSavedSessionStore { 23 31 constructor(private db: Deno.Kv) {} 24 32 async get(key: string): Promise<NodeSavedSession | undefined> {
+24 -1
lib/types.ts
··· 1 1 import { SessionOptions as BaseSessionOptions } from "npm:iron-session"; 2 2 3 + /** 4 + * The session options. 5 + * @type {SessionOptions} 6 + * @implements {BaseSessionOptions} 7 + */ 3 8 interface SessionOptions extends BaseSessionOptions { 4 9 lockFn?: (key: string) => Promise<() => Promise<void>>; 5 10 } 6 11 7 - // Helper function to create a lock using Deno KV 12 + /** 13 + * Create a lock using Deno KV. 14 + * @param key - The key to lock 15 + * @param db - The Deno KV instance for the database 16 + * @returns The unlock function 17 + */ 8 18 async function createLock(key: string, db: Deno.Kv): Promise<() => Promise<void>> { 9 19 const lockKey = ["session_lock", key]; 10 20 const lockValue = Date.now(); ··· 25 35 }; 26 36 } 27 37 38 + /** 39 + * The OAuth session. 40 + * @type {OauthSession} 41 + */ 28 42 export interface OauthSession { 29 43 did: string 30 44 } 31 45 46 + /** 47 + * The credential session. 48 + * @type {CredentialSession} 49 + */ 32 50 export interface CredentialSession { 33 51 did: string; 34 52 handle: string; ··· 45 63 46 64 let db: Deno.Kv; 47 65 66 + /** 67 + * Create the session options. 68 + * @param cookieName - The name of the iron session cookie 69 + * @returns The session options for iron session 70 + */ 48 71 export const createSessionOptions = async (cookieName: string): Promise<SessionOptions> => { 49 72 const cookieSecret = Deno.env.get("COOKIE_SECRET"); 50 73 if (!cookieSecret) {
+7
routes/api/cred/login.ts
··· 3 3 import { define } from "../../../utils.ts"; 4 4 import { Agent } from "npm:@atproto/api"; 5 5 6 + /** 7 + * Handle credential login 8 + * Save iron session to cookies 9 + * Save credential session state to database 10 + * @param ctx - The context object containing the request and response 11 + * @returns A response object with the login result 12 + */ 6 13 export const handler = define.handlers({ 7 14 async POST(ctx) { 8 15 try {
+12
routes/api/migrate/create.ts
··· 3 3 import { Agent } from "@atproto/api"; 4 4 import { define } from "../../../utils.ts"; 5 5 6 + /** 7 + * Handle account creation 8 + * First step of the migration process 9 + * Body must contain: 10 + * - service: The service URL of the new account 11 + * - handle: The handle of the new account 12 + * - password: The password of the new account 13 + * - email: The email of the new account 14 + * - invite: The invite code of the new account (optional depending on the PDS) 15 + * @param ctx - The context object containing the request and response 16 + * @returns A response object with the creation result 17 + */ 6 18 export const handler = define.handlers({ 7 19 async POST(ctx) { 8 20 const res = new Response();
+24
routes/api/migrate/data.ts
··· 8 8 const MAX_RETRIES = 3; 9 9 const INITIAL_RETRY_DELAY = 1000; // 1 second 10 10 11 + /** 12 + * Retry options 13 + * @param maxRetries - The maximum number of retries 14 + * @param initialDelay - The initial delay between retries 15 + * @param onRetry - The function to call on retry 16 + */ 11 17 interface RetryOptions { 12 18 maxRetries?: number; 13 19 initialDelay?: number; 14 20 onRetry?: (attempt: number, error: Error) => void; 15 21 } 16 22 23 + /** 24 + * Retry function with exponential backoff 25 + * @param operation - The operation to retry 26 + * @param options - The retry options 27 + * @returns The result of the operation 28 + */ 17 29 async function withRetry<T>( 18 30 operation: () => Promise<T>, 19 31 options: RetryOptions = {}, ··· 49 61 throw lastError ?? new Error("Operation failed after retries"); 50 62 } 51 63 64 + /** 65 + * Handle blob upload to new PDS 66 + * Retries on errors 67 + * @param newAgent - The new agent 68 + * @param blobRes - The blob response 69 + * @param cid - The CID of the blob 70 + */ 52 71 async function handleBlobUpload( 53 72 newAgent: Agent, 54 73 blobRes: ComAtprotoSyncGetBlob.Response, ··· 81 100 } 82 101 } 83 102 103 + /** 104 + * Handle data migration 105 + * @param ctx - The context object containing the request and response 106 + * @returns A response object with the migration result 107 + */ 84 108 export const handler = define.handlers({ 85 109 async POST(ctx) { 86 110 const res = new Response();
+7
routes/api/migrate/identity/request.ts
··· 3 3 } from "../../../../lib/sessions.ts"; 4 4 import { define } from "../../../../utils.ts"; 5 5 6 + /** 7 + * Handle identity migration request 8 + * Sends a PLC operation signature request to the old account's email 9 + * Should be called after all data is migrated to the new account 10 + * @param ctx - The context object containing the request and response 11 + * @returns A response object with the migration result 12 + */ 6 13 export const handler = define.handlers({ 7 14 async POST(ctx) { 8 15 const res = new Response();
+7
routes/api/migrate/identity/sign.ts
··· 5 5 import * as ui8 from "npm:uint8arrays"; 6 6 import { define } from "../../../../utils.ts"; 7 7 8 + /** 9 + * Handle identity migration sign 10 + * Should be called after user receives the migration token via email 11 + * URL params must contain the token 12 + * @param ctx - The context object containing the request with the token in the URL params 13 + * @returns A response object with the migration result 14 + */ 8 15 export const handler = define.handlers({ 9 16 async POST(ctx) { 10 17 const res = new Response();