Graphical PDS migrator for AT Protocol

huge auth refactor

+112
auth/creds/sessions.ts
···
+
import { Agent } from "npm:@atproto/api";
+
import { getIronSession, SessionOptions } from "npm:iron-session";
+
import { CredentialSession, createSessionOptions } from "../types.ts";
+
+
const migrationSessionOptions = createSessionOptions("migration_sid");
+
const credentialSessionOptions = createSessionOptions("cred_sid");
+
+
export function getCredentialSession(
+
req: Request,
+
res: Response = new Response(),
+
options: SessionOptions
+
) {
+
return getIronSession<CredentialSession>(
+
req,
+
res,
+
options,
+
);
+
}
+
+
export async function getCredentialAgent(
+
req: Request,
+
res: Response = new Response(),
+
isMigration: boolean = false,
+
) {
+
const session = await getCredentialSession(
+
req,
+
res,
+
isMigration ?
+
migrationSessionOptions : credentialSessionOptions
+
);
+
if (!session.did || !session.service) {
+
return null;
+
}
+
+
try {
+
return new Agent({
+
service: session.service,
+
});
+
} catch (err) {
+
console.warn("Failed to create migration agent:", err);
+
session.destroy();
+
return null;
+
}
+
}
+
+
export async function setCredentialSession(
+
req: Request,
+
res: Response,
+
data: CredentialSession,
+
isMigration: boolean = false,
+
) {
+
const session = await getCredentialSession(
+
req,
+
res,
+
isMigration ?
+
migrationSessionOptions : credentialSessionOptions
+
);
+
session.did = data.did;
+
session.handle = data.handle;
+
session.service = data.service;
+
session.password = data.password;
+
await session.save();
+
return session;
+
}
+
+
export async function getCredentialSessionAgent(
+
req: Request,
+
res: Response = new Response(),
+
isMigration: boolean = false,
+
) {
+
const session = await getCredentialSession(
+
req,
+
res,
+
isMigration ?
+
migrationSessionOptions : credentialSessionOptions
+
);
+
+
if (
+
!session.did || !session.service || !session.handle || !session.password
+
) {
+
return null;
+
}
+
+
try {
+
console.log("Creating agent with service:", session.service);
+
const agent = new Agent({ service: session.service });
+
+
// Attempt to restore session by creating a new one
+
try {
+
console.log("Attempting to create session for:", session.handle);
+
const sessionRes = await agent.com.atproto.server.createSession({
+
identifier: session.handle,
+
password: session.password,
+
});
+
console.log("Session created successfully:", !!sessionRes);
+
+
// Set the auth tokens in the agent
+
agent.setHeader("Authorization", `Bearer ${sessionRes.data.accessJwt}`);
+
+
return agent;
+
} catch (err) {
+
// Session creation failed, clear the session
+
console.error("Failed to create session:", err);
+
session.destroy();
+
return null;
+
}
+
} catch (err) {
+
console.warn("Failed to create migration agent:", err);
+
session.destroy();
+
return null;
+
}
+
}
+41
auth/oauth/sessions.ts
···
+
import { Agent } from "npm:@atproto/api";
+
import { getIronSession } from "npm:iron-session";
+
import { oauthClient } from "./client.ts";
+
import { OauthSession, createSessionOptions } from "../types.ts";
+
+
const oauthSessionOptions = createSessionOptions("oauth_sid");
+
+
export async function getOauthSessionAgent(
+
req: Request
+
) {
+
const res = new Response();
+
const session = await getIronSession<OauthSession>(
+
req,
+
res,
+
oauthSessionOptions,
+
);
+
+
if (!session.did) {
+
return null;
+
}
+
+
try {
+
const oauthSession = await oauthClient.restore(session.did);
+
return oauthSession ? new Agent(oauthSession) : null;
+
} catch (err) {
+
console.warn({ err }, "oauth restore failed");
+
session.destroy();
+
return null;
+
}
+
}
+
+
export function getOauthSession(
+
req: Request,
+
res: Response = new Response(),
+
) {
+
return getIronSession<OauthSession>(
+
req,
+
res,
+
oauthSessionOptions,
+
);
+
}
+51
auth/sessions.ts
···
+
import { Agent } from "npm:@atproto/api";
+
import { OauthSession, CredentialSession, createSessionOptions } from "./types.ts";
+
import { getCredentialSession, getCredentialSessionAgent } from "./creds/sessions.ts";
+
import { getOauthSession, getOauthSessionAgent } from "./oauth/sessions.ts";
+
import { IronSession } from "npm:iron-session";
+
+
const migrationSessionOptions = createSessionOptions("migration_sid");
+
const credentialSessionOptions = createSessionOptions("cred_sid");
+
+
export async function getSession(
+
req: Request,
+
res: Response = new Response(),
+
isMigration: boolean = false
+
): Promise<IronSession<OauthSession | CredentialSession>> {
+
if (isMigration) {
+
return await getCredentialSession(req, res, migrationSessionOptions);
+
}
+
const oauthSession = await getOauthSession(req);
+
const credentialSession = await getCredentialSession(req, res, credentialSessionOptions);
+
+
if (oauthSession) {
+
console.log("Oauth session found")
+
return oauthSession;
+
}
+
if (credentialSession) {
+
return credentialSession;
+
}
+
+
throw new Error("No session found");
+
}
+
+
export async function getSessionAgent(
+
req: Request,
+
res: Response = new Response(),
+
isMigration: boolean = false
+
): Promise<Agent | null> {
+
if (isMigration) {
+
return await getCredentialSessionAgent(req, res, isMigration);
+
}
+
+
const oauthAgent = await getOauthSessionAgent(req);
+
if (oauthAgent) {
+
return oauthAgent;
+
}
+
const credentialAgent = await getCredentialSessionAgent(req, res, isMigration);
+
if (credentialAgent) {
+
return credentialAgent;
+
}
+
+
return null;
+
}
+37
auth/types.ts
···
+
import { SessionOptions } from "npm:iron-session";
+
+
export interface OauthSession {
+
did: string
+
}
+
+
export interface CredentialSession {
+
did: string;
+
handle: string;
+
service: string;
+
password: string;
+
recoveryKey?: string;
+
recoveryKeyDid?: string;
+
credentials?: {
+
rotationKeys: string[];
+
[key: string]: unknown;
+
};
+
}
+
+
export const createSessionOptions = (cookieName: string): SessionOptions => {
+
const cookieSecret = Deno.env.get("COOKIE_SECRET");
+
if (!cookieSecret) {
+
throw new Error("COOKIE_SECRET is not set");
+
}
+
+
return {
+
cookieName: cookieName,
+
password: cookieSecret,
+
cookieOptions: {
+
secure: Deno.env.get("NODE_ENV") === "production" || Deno.env.get("NODE_ENV") === "staging",
+
httpOnly: true,
+
sameSite: "lax",
+
path: "/",
+
domain: undefined,
+
},
+
}
+
};
+7 -7
islands/MigrationProgress.tsx
···
console.log("Starting account creation...");
try {
-
const createRes = await fetch("/api/server/migrate/create", {
+
const createRes = await fetch("/api/migrate/create", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
···
console.log("Starting data migration...");
try {
-
const dataRes = await fetch("/api/server/migrate/data", {
+
const dataRes = await fetch("/api/migrate/data", {
method: "POST",
headers: { "Content-Type": "application/json" },
});
···
console.log("Requesting identity migration...");
try {
-
const requestRes = await fetch("/api/server/migrate/identity/request", {
+
const requestRes = await fetch("/api/migrate/identity/request", {
method: "POST",
headers: { "Content-Type": "application/json" },
});
···
try {
const identityRes = await fetch(
-
`/api/server/migrate/identity/sign?token=${encodeURIComponent(token)}`,
+
`/api/migrate/identity/sign?token=${encodeURIComponent(token)}`,
{
method: "POST",
headers: { "Content-Type": "application/json" },
···
if (!data.success) {
throw new Error(data.message || "Identity migration failed");
}
-
} catch (e) {
+
} catch {
throw new Error("Invalid response from server");
}
···
// Step 5: Finalize Migration
updateStepStatus(4, "in-progress");
try {
-
const finalizeRes = await fetch("/api/server/migrate/finalize", {
+
const finalizeRes = await fetch("/api/migrate/finalize", {
method: "POST",
headers: { "Content-Type": "application/json" },
});
···
if (!jsonData.success) {
throw new Error(jsonData.message || "Finalization failed");
}
-
} catch (e) {
+
} catch {
throw new Error("Invalid response from server during finalization");
}
+1 -1
oauth/client.ts auth/oauth/client.ts
···
import { AtprotoOAuthClient } from "jsr:@bigmoves/atproto-oauth-client";
-
import { SessionStore, StateStore } from "./storage.ts";
+
import { SessionStore, StateStore } from "../storage.ts";
export const createClient = (db: Deno.Kv) => {
if (Deno.env.get("NODE_ENV") == "production" && !Deno.env.get("PUBLIC_URL")) {
-174
oauth/session.ts
···
-
import { Agent } from "npm:@atproto/api";
-
import { getIronSession, SessionOptions } from "npm:iron-session";
-
import { oauthClient } from "./client.ts";
-
-
export interface Session {
-
did: string;
-
newAccountDid?: string;
-
}
-
-
export interface MigrationSession {
-
did: string;
-
handle: string;
-
service: string;
-
password: string;
-
recoveryKey?: string;
-
recoveryKeyDid?: string;
-
credentials?: {
-
rotationKeys: string[];
-
[key: string]: unknown;
-
};
-
}
-
-
export interface State {
-
session?: Session;
-
sessionUser?: Agent;
-
migrationSession?: MigrationSession;
-
migrationAgent?: Agent;
-
}
-
-
const cookieSecret = Deno.env.get("COOKIE_SECRET");
-
console.log("COOKIE_SECRET", cookieSecret);
-
-
const sessionOptions: SessionOptions = {
-
cookieName: "sid",
-
password: cookieSecret!,
-
cookieOptions: {
-
secure: Deno.env.get("NODE_ENV") === "production",
-
httpOnly: true,
-
sameSite: "lax",
-
path: "/",
-
domain: undefined,
-
},
-
};
-
-
const migrationSessionOptions: SessionOptions = {
-
cookieName: "migration_sid",
-
password: cookieSecret!,
-
cookieOptions: {
-
secure: Deno.env.get("NODE_ENV") === "production",
-
httpOnly: true,
-
sameSite: "lax",
-
path: "/",
-
domain: undefined,
-
},
-
};
-
-
export async function getSessionAgent(
-
req: Request
-
) {
-
const res = new Response();
-
const session = await getIronSession<Session>(
-
req,
-
res,
-
sessionOptions,
-
);
-
-
if (!session.did) {
-
return null;
-
}
-
-
try {
-
const oauthSession = await oauthClient.restore(session.did);
-
return oauthSession ? new Agent(oauthSession) : null;
-
} catch (err) {
-
console.warn({ err }, "oauth restore failed");
-
session.destroy();
-
return null;
-
}
-
}
-
-
export function getSession(req: Request, res: Response = new Response()) {
-
return getIronSession<Session>(req, res, sessionOptions);
-
}
-
-
export function getMigrationSession(
-
req: Request,
-
res: Response = new Response(),
-
) {
-
return getIronSession<MigrationSession>(req, res, migrationSessionOptions);
-
}
-
-
export async function getMigrationAgent(
-
req: Request,
-
res: Response = new Response(),
-
) {
-
const session = await getMigrationSession(req, res);
-
if (!session.did || !session.service) {
-
return null;
-
}
-
-
try {
-
return new Agent({
-
service: session.service,
-
});
-
} catch (err) {
-
console.warn("Failed to create migration agent:", err);
-
session.destroy();
-
return null;
-
}
-
}
-
-
export async function setMigrationSession(
-
req: Request,
-
res: Response,
-
data: MigrationSession,
-
) {
-
const session = await getMigrationSession(req, res);
-
session.did = data.did;
-
session.handle = data.handle;
-
session.service = data.service;
-
session.password = data.password;
-
await session.save();
-
return session;
-
}
-
-
export async function getMigrationSessionAgent(
-
req: Request,
-
res: Response = new Response(),
-
) {
-
const session = await getMigrationSession(req, res);
-
console.log("Migration session data:", {
-
hasDid: !!session.did,
-
hasService: !!session.service,
-
hasHandle: !!session.handle,
-
hasPassword: !!session.password,
-
sessionData: session,
-
});
-
-
if (
-
!session.did || !session.service || !session.handle || !session.password
-
) {
-
console.log("Missing required session data");
-
return null;
-
}
-
-
try {
-
console.log("Creating agent with service:", session.service);
-
const agent = new Agent({ service: session.service });
-
-
// Attempt to restore session by creating a new one
-
try {
-
console.log("Attempting to create session for:", session.handle);
-
const sessionRes = await agent.com.atproto.server.createSession({
-
identifier: session.handle,
-
password: session.password,
-
});
-
console.log("Session created successfully:", !!sessionRes);
-
-
// Set the auth tokens in the agent
-
agent.setHeader("Authorization", `Bearer ${sessionRes.data.accessJwt}`);
-
-
return agent;
-
} catch (err) {
-
// Session creation failed, clear the session
-
console.error("Failed to create session:", err);
-
await session.destroy();
-
return null;
-
}
-
} catch (err) {
-
console.warn("Failed to create migration agent:", err);
-
await session.destroy();
-
return null;
-
}
-
}
oauth/storage.ts auth/storage.ts
+2 -1
routes/api/me.ts
···
-
import { getSessionAgent } from "../../oauth/session.ts";
+
import { getSessionAgent } from "../../auth/sessions.ts";
import { define } from "../../utils.ts";
import { resolver } from "../../tools/id-resolver.ts";
···
const req = ctx.req;
const agent = await getSessionAgent(req);
if (!agent) {
+
console.log("No agent found")
return Response.json(null);
}
+96
routes/api/migrate/identity/request.ts
···
+
import {
+
getSessionAgent,
+
} from "../../../../auth/sessions.ts";
+
import { define } from "../../../../utils.ts";
+
+
export const handler = define.handlers({
+
async POST(ctx) {
+
const res = new Response();
+
try {
+
console.log("Starting identity migration request...");
+
const oldAgent = await getSessionAgent(ctx.req);
+
console.log("Got old agent:", {
+
hasDid: !!oldAgent?.did,
+
hasSession: !!oldAgent,
+
did: oldAgent?.did,
+
});
+
+
const newAgent = await getSessionAgent(ctx.req, res, true);
+
console.log("Got new agent:", {
+
hasAgent: !!newAgent,
+
});
+
+
if (!oldAgent) {
+
return new Response(
+
JSON.stringify({
+
success: false,
+
message: "Unauthorized",
+
}),
+
{
+
status: 401,
+
headers: { "Content-Type": "application/json" },
+
},
+
);
+
}
+
if (!newAgent) {
+
return new Response(
+
JSON.stringify({
+
success: false,
+
message: "Migration session not found or invalid",
+
}),
+
{
+
status: 400,
+
headers: { "Content-Type": "application/json" },
+
},
+
);
+
}
+
+
// Request the signature
+
console.log("Requesting PLC operation signature...");
+
try {
+
await oldAgent.com.atproto.identity.requestPlcOperationSignature();
+
console.log("Successfully requested PLC operation signature");
+
} catch (error) {
+
console.error("Error requesting PLC operation signature:", {
+
name: error instanceof Error ? error.name : "Unknown",
+
message: error instanceof Error ? error.message : String(error),
+
status: 400
+
});
+
throw error;
+
}
+
+
return new Response(
+
JSON.stringify({
+
success: true,
+
message:
+
"PLC operation signature requested successfully. Please check your email for the token.",
+
}),
+
{
+
status: 200,
+
headers: {
+
"Content-Type": "application/json",
+
...Object.fromEntries(res.headers), // Include session cookie headers
+
},
+
},
+
);
+
} catch (error) {
+
console.error("Identity migration request error:", {
+
name: error instanceof Error ? error.name : "Unknown",
+
message: error instanceof Error ? error.message : String(error),
+
stack: error instanceof Error ? error.stack : undefined,
+
});
+
return new Response(
+
JSON.stringify({
+
success: false,
+
message: error instanceof Error
+
? error.message
+
: "Failed to request identity migration",
+
}),
+
{
+
status: 400,
+
headers: { "Content-Type": "application/json" },
+
},
+
);
+
}
+
},
+
});
+137
routes/api/migrate/identity/sign.ts
···
+
import {
+
getSessionAgent,
+
} from "../../../../auth/sessions.ts";
+
import { Secp256k1Keypair } from "npm:@atproto/crypto";
+
import * as ui8 from "npm:uint8arrays";
+
import { define } from "../../../../utils.ts";
+
+
export const handler = define.handlers({
+
async POST(ctx) {
+
const res = new Response();
+
try {
+
const url = new URL(ctx.req.url);
+
const token = url.searchParams.get("token");
+
+
if (!token) {
+
return new Response(
+
JSON.stringify({
+
success: false,
+
message: "Missing param token",
+
}),
+
{
+
status: 400,
+
headers: { "Content-Type": "application/json" },
+
},
+
);
+
}
+
+
const oldAgent = await getSessionAgent(ctx.req);
+
const newAgent = await getSessionAgent(ctx.req, res, true);
+
+
if (!oldAgent) {
+
return new Response(
+
JSON.stringify({
+
success: false,
+
message: "Unauthorized",
+
}),
+
{
+
status: 401,
+
headers: { "Content-Type": "application/json" },
+
},
+
);
+
}
+
if (!newAgent) {
+
return new Response(
+
JSON.stringify({
+
success: false,
+
message: "Migration session not found or invalid",
+
}),
+
{
+
status: 400,
+
headers: { "Content-Type": "application/json" },
+
},
+
);
+
}
+
+
// Generate recovery key
+
console.log("Generating recovery key...");
+
const recoveryKey = await Secp256k1Keypair.create({ exportable: true });
+
const privateKeyBytes = await recoveryKey.export();
+
const privateKey = ui8.toString(privateKeyBytes, "hex");
+
const recoveryKeyDid = recoveryKey.did();
+
console.log("Generated recovery key and DID:", {
+
hasPrivateKey: !!privateKey,
+
recoveryDid: recoveryKeyDid,
+
});
+
+
// Get recommended credentials
+
console.log("Getting recommended credentials...");
+
let credentials;
+
try {
+
const getDidCredentials = await newAgent.com.atproto.identity
+
.getRecommendedDidCredentials();
+
console.log("Got recommended credentials:", {
+
hasRotationKeys: !!getDidCredentials.data.rotationKeys,
+
rotationKeysLength: getDidCredentials.data.rotationKeys?.length,
+
data: getDidCredentials.data,
+
});
+
+
const rotationKeys = getDidCredentials.data.rotationKeys ?? [];
+
if (!rotationKeys) {
+
throw new Error("No rotation key provided");
+
}
+
+
// Prepare credentials with recovery key
+
credentials = {
+
...getDidCredentials.data,
+
rotationKeys: [recoveryKeyDid, ...rotationKeys],
+
};
+
} catch (error) {
+
console.error("Error getting recommended credentials:", {
+
name: error instanceof Error ? error.name : "Unknown",
+
message: error instanceof Error ? error.message : String(error),
+
});
+
throw error;
+
}
+
+
// Sign and submit the operation
+
const plcOp = await oldAgent.com.atproto.identity.signPlcOperation({
+
token: token,
+
...credentials,
+
});
+
+
await newAgent.com.atproto.identity.submitPlcOperation({
+
operation: plcOp.data.operation,
+
});
+
+
return new Response(
+
JSON.stringify({
+
success: true,
+
message: "Identity migration completed successfully",
+
recoveryKey: privateKey,
+
}),
+
{
+
status: 200,
+
headers: {
+
"Content-Type": "application/json",
+
...Object.fromEntries(res.headers), // Include session cookie headers
+
},
+
},
+
);
+
} catch (error) {
+
console.error("Identity migration sign error:", error);
+
return new Response(
+
JSON.stringify({
+
success: false,
+
message: error instanceof Error
+
? error.message
+
: "Failed to complete identity migration",
+
}),
+
{
+
status: 400,
+
headers: { "Content-Type": "application/json" },
+
},
+
);
+
}
+
},
+
});
+3 -3
routes/api/oauth/callback.ts
···
-
import { oauthClient } from "../../../oauth/client.ts";
-
import { getSession } from "../../../oauth/session.ts";
+
import { oauthClient } from "../../../auth/oauth/client.ts";
+
import { getOauthSession } from "../../../auth/oauth/sessions.ts";
import { define } from "../../../utils.ts";
export const handler = define.handlers({
···
});
// Create and save our client session
-
const clientSession = await getSession(req, response);
+
const clientSession = await getOauthSession(req, response);
clientSession.did = session.did;
await clientSession.save();
+1 -1
routes/api/oauth/initiate.ts
···
import { isValidHandle } from 'npm:@atproto/syntax'
-
import { oauthClient } from "../../../oauth/client.ts";
+
import { oauthClient } from "../../../auth/oauth/client.ts";
import { define } from "../../../utils.ts";
function isValidUrl(url: string): boolean {
+3 -3
routes/api/oauth/logout.ts
···
-
import { getSession } from "../../../oauth/session.ts";
-
import { oauthClient } from "../../../oauth/client.ts";
+
import { getSession } from "../../../auth/sessions.ts";
+
import { oauthClient } from "../../../auth/oauth/client.ts";
import { define } from "../../../utils.ts";
export const handler = define.handlers({
···
// First destroy the oauth session
await oauthClient.revoke(session.did);
// Then destroy the iron session
-
await session.destroy();
+
session.destroy();
}
return response;
+1 -1
routes/api/server/describe.ts
···
import { Agent } from "@atproto/api";
-
import { getSessionAgent } from "../../../oauth/session.ts";
+
import { getSessionAgent } from "../../../auth/sessions.ts";
import { define } from "../../../utils.ts";
/**
* Describe the server configuration and capabilities
+5 -7
routes/api/server/migrate/create.ts routes/api/migrate/create.ts
···
-
import {
-
getSessionAgent,
-
setMigrationSession,
-
} from "../../../../oauth/session.ts";
+
import { getSessionAgent } from "../../../auth/sessions.ts";
+
import { setCredentialSession } from "../../../auth/creds/sessions.ts";
import { Agent } from "@atproto/api";
-
import { define } from "../../../../utils.ts";
+
import { define } from "../../../utils.ts";
export const handler = define.handlers({
async POST(ctx) {
···
});
// Store the migration session
-
await setMigrationSession(ctx.req, res, {
+
await setCredentialSession(ctx.req, res, {
did: sessionRes.data.did,
handle: newHandle,
service: serviceUrl,
password: newPassword,
-
});
+
}, true);
return new Response(
JSON.stringify({
+3 -4
routes/api/server/migrate/data.ts routes/api/migrate/data.ts
···
-
import { define } from "../../../../utils.ts";
+
import { define } from "../../../utils.ts";
import {
-
getMigrationSessionAgent,
getSessionAgent,
-
} from "../../../../oauth/session.ts";
+
} from "../../../auth/sessions.ts";
export const handler = define.handlers({
async POST(ctx) {
···
console.log("Data migration: Cookies present:", !!cookies);
console.log("Data migration: Cookie header:", cookies);
-
const newAgent = await getMigrationSessionAgent(ctx.req, res);
+
const newAgent = await getSessionAgent(ctx.req, res, true);
console.log("Data migration: Got new agent:", !!newAgent);
if (!oldAgent) {
+3 -6
routes/api/server/migrate/finalize.ts routes/api/migrate/finalize.ts
···
-
import {
-
getMigrationSessionAgent,
-
getSessionAgent,
-
} from "../../../../oauth/session.ts";
-
import { define } from "../../../../utils.ts";
+
import { getSessionAgent } from "../../../auth/sessions.ts";
+
import { define } from "../../../utils.ts";
export const handler = define.handlers({
async POST(ctx) {
const res = new Response();
try {
const oldAgent = await getSessionAgent(ctx.req);
-
const newAgent = await getMigrationSessionAgent(ctx.req, res);
+
const newAgent = await getSessionAgent(ctx.req, res, true);
if (!oldAgent) return new Response("Unauthorized", { status: 401 });
if (!newAgent) {
-148
routes/api/server/migrate/identity/request.ts
···
-
import {
-
getMigrationSession,
-
getMigrationSessionAgent,
-
getSessionAgent,
-
} from "../../../../../oauth/session.ts";
-
import { Secp256k1Keypair } from "npm:@atproto/crypto";
-
import * as ui8 from "npm:uint8arrays";
-
import { define } from "../../../../../utils.ts";
-
-
export const handler = define.handlers({
-
async POST(ctx) {
-
const res = new Response();
-
try {
-
console.log("Starting identity migration request...");
-
const oldAgent = await getSessionAgent(ctx.req);
-
console.log("Got old agent:", {
-
hasDid: !!oldAgent?.did,
-
hasSession: !!oldAgent,
-
did: oldAgent?.did,
-
});
-
-
const newAgent = await getMigrationSessionAgent(ctx.req, res);
-
console.log("Got new agent:", {
-
hasAgent: !!newAgent,
-
});
-
-
if (!oldAgent) {
-
return new Response(
-
JSON.stringify({
-
success: false,
-
message: "Unauthorized",
-
}),
-
{
-
status: 401,
-
headers: { "Content-Type": "application/json" },
-
},
-
);
-
}
-
if (!newAgent) {
-
return new Response(
-
JSON.stringify({
-
success: false,
-
message: "Migration session not found or invalid",
-
}),
-
{
-
status: 400,
-
headers: { "Content-Type": "application/json" },
-
},
-
);
-
}
-
-
// Generate recovery key
-
console.log("Generating recovery key...");
-
const recoveryKey = await Secp256k1Keypair.create({ exportable: true });
-
const privateKeyBytes = await recoveryKey.export();
-
const privateKey = ui8.toString(privateKeyBytes, "hex");
-
console.log("Generated recovery key and DID:", {
-
hasPrivateKey: !!privateKey,
-
recoveryDid: recoveryKey.did(),
-
});
-
-
// Store the recovery key and its DID in the session for the sign step
-
const session = await getMigrationSession(ctx.req, res);
-
session.recoveryKey = privateKey;
-
session.recoveryKeyDid = recoveryKey.did();
-
await session.save();
-
console.log("Stored recovery key in session");
-
-
// Get recommended credentials for later use
-
console.log("Getting recommended credentials...");
-
try {
-
const getDidCredentials = await newAgent.com.atproto.identity
-
.getRecommendedDidCredentials();
-
console.log("Got recommended credentials:", {
-
hasRotationKeys: !!getDidCredentials.data.rotationKeys,
-
rotationKeysLength: getDidCredentials.data.rotationKeys?.length,
-
data: getDidCredentials.data,
-
});
-
-
const rotationKeys = getDidCredentials.data.rotationKeys ?? [];
-
if (!rotationKeys) {
-
throw new Error("No rotation key provided");
-
}
-
-
// Store credentials in session for sign step
-
session.credentials = {
-
...getDidCredentials.data,
-
rotationKeys, // Ensure rotationKeys is always an array
-
};
-
await session.save();
-
console.log("Stored credentials in session");
-
} catch (error) {
-
console.error("Error getting recommended credentials:", {
-
name: error instanceof Error ? error.name : "Unknown",
-
message: error instanceof Error ? error.message : String(error),
-
});
-
throw error;
-
}
-
-
// Request the signature
-
console.log("Requesting PLC operation signature...");
-
try {
-
await oldAgent.com.atproto.identity.requestPlcOperationSignature();
-
console.log("Successfully requested PLC operation signature");
-
} catch (error) {
-
console.error("Error requesting PLC operation signature:", {
-
name: error instanceof Error ? error.name : "Unknown",
-
message: error instanceof Error ? error.message : String(error),
-
status: 400
-
});
-
throw error;
-
}
-
-
return new Response(
-
JSON.stringify({
-
success: true,
-
message:
-
"PLC operation signature requested successfully. Please check your email for the token.",
-
}),
-
{
-
status: 200,
-
headers: {
-
"Content-Type": "application/json",
-
...Object.fromEntries(res.headers), // Include session cookie headers
-
},
-
},
-
);
-
} catch (error) {
-
console.error("Identity migration request error:", {
-
name: error instanceof Error ? error.name : "Unknown",
-
message: error instanceof Error ? error.message : String(error),
-
stack: error instanceof Error ? error.stack : undefined,
-
});
-
return new Response(
-
JSON.stringify({
-
success: false,
-
message: error instanceof Error
-
? error.message
-
: "Failed to request identity migration",
-
}),
-
{
-
status: 400,
-
headers: { "Content-Type": "application/json" },
-
},
-
);
-
}
-
},
-
});
-110
routes/api/server/migrate/identity/sign.ts
···
-
import {
-
getMigrationSession,
-
getMigrationSessionAgent,
-
getSessionAgent,
-
} from "../../../../../oauth/session.ts";
-
import { define } from "../../../../../utils.ts";
-
-
export const handler = define.handlers({
-
async POST(ctx) {
-
const res = new Response();
-
try {
-
const url = new URL(ctx.req.url);
-
const token = url.searchParams.get("token");
-
-
if (!token) {
-
return new Response(
-
JSON.stringify({
-
success: false,
-
message: "Missing param token",
-
}),
-
{
-
status: 400,
-
headers: { "Content-Type": "application/json" },
-
},
-
);
-
}
-
-
const oldAgent = await getSessionAgent(ctx.req);
-
const newAgent = await getMigrationSessionAgent(ctx.req, res);
-
const session = await getMigrationSession(ctx.req, res);
-
-
if (!oldAgent) {
-
return new Response(
-
JSON.stringify({
-
success: false,
-
message: "Unauthorized",
-
}),
-
{
-
status: 401,
-
headers: { "Content-Type": "application/json" },
-
},
-
);
-
}
-
if (
-
!newAgent || !session.recoveryKey || !session.recoveryKeyDid ||
-
!session.credentials
-
) {
-
return new Response(
-
JSON.stringify({
-
success: false,
-
message:
-
"Migration session not found or invalid. Please restart the identity migration process.",
-
}),
-
{
-
status: 400,
-
headers: { "Content-Type": "application/json" },
-
},
-
);
-
}
-
-
// Prepare credentials with recovery key
-
const credentials = {
-
...session.credentials,
-
rotationKeys: [
-
session.recoveryKeyDid,
-
...session.credentials.rotationKeys,
-
],
-
};
-
-
// Sign and submit the operation
-
const plcOp = await oldAgent.com.atproto.identity.signPlcOperation({
-
token: token,
-
...credentials,
-
});
-
-
await newAgent.com.atproto.identity.submitPlcOperation({
-
operation: plcOp.data.operation,
-
});
-
-
return new Response(
-
JSON.stringify({
-
success: true,
-
message: "Identity migration completed successfully",
-
recoveryKey: session.recoveryKey,
-
}),
-
{
-
status: 200,
-
headers: {
-
"Content-Type": "application/json",
-
...Object.fromEntries(res.headers), // Include session cookie headers
-
},
-
},
-
);
-
} catch (error) {
-
console.error("Identity migration sign error:", error);
-
return new Response(
-
JSON.stringify({
-
success: false,
-
message: error instanceof Error
-
? error.message
-
: "Failed to complete identity migration",
-
}),
-
{
-
status: 400,
-
headers: { "Content-Type": "application/json" },
-
},
-
);
-
}
-
},
-
});