Graphical PDS migrator for AT Protocol

upgrade to fresh 2

+5 -5
auth/client.ts
···
-
import { AtprotoOAuthClient } from 'jsr:@bigmoves/atproto-oauth-client'
import { SessionStore, StateStore } from "./storage.ts";
export const createClient = (db: Deno.Kv) => {
···
}
const publicUrl = Deno.env.get("PUBLIC_URL");
-
const url = publicUrl || `http://127.0.0.1:${Deno.env.get("PORT")}`;
const enc = encodeURIComponent;
return new AtprotoOAuthClient({
···
dpop_bound_access_tokens: true,
},
stateStore: new StateStore(db),
-
sessionStore: new SessionStore(db)
});
};
-
const kv = await Deno.openKv()
-
export const oauthClient = await createClient(kv)
···
+
import { AtprotoOAuthClient } from "jsr:@bigmoves/atproto-oauth-client";
import { SessionStore, StateStore } from "./storage.ts";
export const createClient = (db: Deno.Kv) => {
···
}
const publicUrl = Deno.env.get("PUBLIC_URL");
+
const url = publicUrl || `http://127.0.0.1:8000`;
const enc = encodeURIComponent;
return new AtprotoOAuthClient({
···
dpop_bound_access_tokens: true,
},
stateStore: new StateStore(db),
+
sessionStore: new SessionStore(db),
});
};
+
const kv = await Deno.openKv();
+
export const oauthClient = createClient(kv);
+20 -23
auth/session.ts
···
import { Agent } from "npm:@atproto/api";
import { getIronSession, SessionOptions } from "npm:iron-session";
-
import { FreshContext } from "$fresh/server.ts";
import { oauthClient } from "./client.ts";
export interface Session {
···
migrationAgent?: Agent;
}
-
const cookieSecret = Deno.env.get("COOKIE_SECRET")
const sessionOptions: SessionOptions = {
cookieName: "sid",
···
};
export async function getSessionAgent(
-
req: Request,
-
ctx: FreshContext
) {
const res = new Response();
const session = await getIronSession<Session>(
req,
res,
-
sessionOptions
);
if (!session.did) {
···
const oauthSession = await oauthClient.restore(session.did);
return oauthSession ? new Agent(oauthSession) : null;
} catch (err) {
-
const logger = ctx.state.logger as {
-
warn: (obj: Record<string, unknown>, msg: string) => void;
-
};
-
logger.warn({ err }, "oauth restore failed");
session.destroy();
return null;
}
···
return getIronSession<Session>(req, res, sessionOptions);
}
-
export async 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) {
···
}
try {
-
return new Agent({
-
service: session.service
});
} catch (err) {
console.warn("Failed to create migration agent:", err);
···
export async function setMigrationSession(
req: Request,
res: Response,
-
data: MigrationSession
) {
const session = await getMigrationSession(req, res);
session.did = data.did;
···
export async function getMigrationSessionAgent(
req: Request,
-
res: Response = new Response()
) {
const session = await getMigrationSession(req, res);
console.log("Migration session data:", {
···
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
···
import { Agent } from "npm:@atproto/api";
import { getIronSession, SessionOptions } from "npm:iron-session";
import { oauthClient } from "./client.ts";
export interface Session {
···
migrationAgent?: Agent;
}
+
const cookieSecret = Deno.env.get("COOKIE_SECRET");
const sessionOptions: SessionOptions = {
cookieName: "sid",
···
};
export async function getSessionAgent(
+
req: Request
) {
const res = new Response();
const session = await getIronSession<Session>(
req,
res,
+
sessionOptions,
);
if (!session.did) {
···
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;
}
···
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) {
···
}
try {
+
return new Agent({
+
service: session.service,
});
} catch (err) {
console.warn("Failed to create migration agent:", err);
···
export async function setMigrationSession(
req: Request,
res: Response,
+
data: MigrationSession,
) {
const session = await getMigrationSession(req, res);
session.did = data.did;
···
export async function getMigrationSessionAgent(
req: Request,
+
res: Response = new Response(),
) {
const session = await getMigrationSession(req, res);
console.log("Migration session data:", {
···
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
+1 -1
components/Button.tsx
···
import { JSX } from "preact";
-
import { IS_BROWSER } from "$fresh/runtime.ts";
export function Button(props: JSX.HTMLAttributes<HTMLButtonElement>) {
return (
···
import { JSX } from "preact";
+
import { IS_BROWSER } from "fresh/runtime";
export function Button(props: JSX.HTMLAttributes<HTMLButtonElement>) {
return (
+14 -19
deno.json
···
{
-
"lock": false,
"tasks": {
-
"check": "deno fmt --check && deno lint && deno check **/*.ts && deno check **/*.tsx",
-
"cli": "echo \"import '\\$fresh/src/dev/cli.ts'\" | deno run --unstable -A -",
-
"manifest": "deno task cli manifest $(pwd)",
-
"start": "deno run -A --watch=static/,routes/ dev.ts",
"build": "deno run -A dev.ts build",
-
"preview": "deno run -A main.ts",
-
"update": "deno run -A -r https://fresh.deno.dev/update ."
},
"lint": {
"rules": {
···
},
"exclude": ["**/_fresh/*"],
"imports": {
-
"$fresh/": "https://deno.land/x/fresh@1.7.3/",
-
"preact": "https://esm.sh/preact@10.22.0",
-
"preact/": "https://esm.sh/preact@10.22.0/",
-
"@preact/signals": "https://esm.sh/*@preact/signals@1.2.2",
-
"@preact/signals-core": "https://esm.sh/*@preact/signals-core@1.5.1",
-
"tailwindcss": "npm:tailwindcss@3.4.1",
-
"tailwindcss/": "npm:/tailwindcss@3.4.1/",
-
"tailwindcss/plugin": "npm:/tailwindcss@3.4.1/plugin.js",
-
"$std/": "https://deno.land/std@0.216.0/"
},
"compilerOptions": {
-
"jsx": "react-jsx",
-
"jsxImportSource": "preact"
},
-
"nodeModulesDir": "auto",
"unstable": ["kv"]
}
···
{
"tasks": {
+
"check": "deno fmt --check . && deno lint . && deno check **/*.ts && deno check **/*.tsx",
+
"dev": "deno run -A --watch=static/,routes/ dev.ts",
"build": "deno run -A dev.ts build",
+
"start": "deno run -A main.ts",
+
"update": "deno run -A -r jsr:@fresh/update ."
},
"lint": {
"rules": {
···
},
"exclude": ["**/_fresh/*"],
"imports": {
+
"@atproto/api": "npm:@atproto/api@^0.15.6",
+
"fresh": "jsr:@fresh/core@^2.0.0-alpha.33",
+
"@fresh/plugin-tailwind": "jsr:@fresh/plugin-tailwind@^0.0.1-alpha.7",
+
"preact": "npm:preact@^10.26.6",
+
"@preact/signals": "npm:@preact/signals@^2.0.4",
+
"tailwindcss": "npm:tailwindcss@^3.4.3"
},
"compilerOptions": {
+
"lib": ["dom", "dom.asynciterable", "dom.iterable", "deno.ns"],
+
"jsx": "precompile",
+
"jsxImportSource": "preact",
+
"jsxPrecompileSkipElements": ["a", "img", "source", "body", "html", "head"]
},
"unstable": ["kv"]
}
+1606
deno.lock
···
···
+
{
+
"version": "5",
+
"specifiers": {
+
"jsr:@bigmoves/atproto-oauth-client@*": "0.1.0",
+
"jsr:@fresh/core@^2.0.0-alpha.1": "2.0.0-alpha.33",
+
"jsr:@fresh/core@^2.0.0-alpha.33": "2.0.0-alpha.33",
+
"jsr:@fresh/plugin-tailwind@^0.0.1-alpha.7": "0.0.1-alpha.7",
+
"jsr:@luca/esbuild-deno-loader@0.11": "0.11.1",
+
"jsr:@std/assert@0.221": "0.221.0",
+
"jsr:@std/bytes@^1.0.2": "1.0.5",
+
"jsr:@std/crypto@1": "1.0.4",
+
"jsr:@std/datetime@~0.225.2": "0.225.4",
+
"jsr:@std/encoding@1": "1.0.10",
+
"jsr:@std/encoding@^1.0.5": "1.0.10",
+
"jsr:@std/fmt@1": "1.0.8",
+
"jsr:@std/fs@1": "1.0.17",
+
"jsr:@std/html@1": "1.0.4",
+
"jsr:@std/http@^1.0.15": "1.0.15",
+
"jsr:@std/json@^1.0.2": "1.0.2",
+
"jsr:@std/jsonc@1": "1.0.2",
+
"jsr:@std/media-types@1": "1.1.0",
+
"jsr:@std/path@0.221": "0.221.0",
+
"jsr:@std/path@1": "1.0.9",
+
"jsr:@std/path@^1.0.6": "1.0.9",
+
"jsr:@std/path@^1.0.9": "1.0.9",
+
"jsr:@std/semver@1": "1.0.5",
+
"npm:@atproto-labs/handle-resolver-node@~0.1.14": "0.1.15",
+
"npm:@atproto/api@*": "0.15.6",
+
"npm:@atproto/api@~0.15.6": "0.15.6",
+
"npm:@atproto/crypto@*": "0.4.4",
+
"npm:@atproto/identity@*": "0.4.8",
+
"npm:@atproto/jwk@0.1.4": "0.1.4",
+
"npm:@atproto/oauth-client@~0.3.13": "0.3.16",
+
"npm:@atproto/syntax@*": "0.4.0",
+
"npm:@opentelemetry/api@^1.9.0": "1.9.0",
+
"npm:@preact/signals@^1.2.3": "1.3.2_preact@10.26.6",
+
"npm:@preact/signals@^2.0.4": "2.0.4_preact@10.26.6",
+
"npm:autoprefixer@10.4.17": "10.4.17_postcss@8.4.35",
+
"npm:cssnano@6.0.3": "6.0.3_postcss@8.4.35",
+
"npm:esbuild-wasm@0.23.1": "0.23.1",
+
"npm:esbuild@0.23.1": "0.23.1",
+
"npm:iron-session@*": "8.0.4",
+
"npm:jose@5.9.6": "5.9.6",
+
"npm:postcss@8.4.35": "8.4.35",
+
"npm:preact-render-to-string@^6.5.11": "6.5.13_preact@10.26.6",
+
"npm:preact@^10.25.1": "10.26.6",
+
"npm:preact@^10.26.6": "10.26.6",
+
"npm:tailwindcss@^3.4.1": "3.4.17_postcss@8.5.3",
+
"npm:tailwindcss@^3.4.3": "3.4.17_postcss@8.5.3",
+
"npm:uint8arrays@*": "5.1.0"
+
},
+
"jsr": {
+
"@bigmoves/atproto-oauth-client@0.1.0": {
+
"integrity": "d5858f534a800a46af28b1c03b447b179d15bbf164c24767601ae78513501711",
+
"dependencies": [
+
"npm:@atproto-labs/handle-resolver-node",
+
"npm:@atproto/jwk",
+
"npm:@atproto/oauth-client",
+
"npm:jose"
+
]
+
},
+
"@fresh/core@2.0.0-alpha.33": {
+
"integrity": "0263ad090120cca6f814bb5914383c74f67d494e552ed33cbf58d667f12d7e9f",
+
"dependencies": [
+
"jsr:@luca/esbuild-deno-loader",
+
"jsr:@std/crypto",
+
"jsr:@std/datetime",
+
"jsr:@std/encoding@1",
+
"jsr:@std/fmt",
+
"jsr:@std/fs",
+
"jsr:@std/html",
+
"jsr:@std/http",
+
"jsr:@std/jsonc",
+
"jsr:@std/media-types",
+
"jsr:@std/path@1",
+
"jsr:@std/semver",
+
"npm:@opentelemetry/api",
+
"npm:@preact/signals@^1.2.3",
+
"npm:esbuild",
+
"npm:esbuild-wasm",
+
"npm:preact-render-to-string",
+
"npm:preact@^10.25.1"
+
]
+
},
+
"@fresh/plugin-tailwind@0.0.1-alpha.7": {
+
"integrity": "b940991bdb76f0995dc58b25183f1001d72c4020e049d384ad3fb751556aa2a9",
+
"dependencies": [
+
"jsr:@fresh/core@^2.0.0-alpha.1",
+
"jsr:@std/path@0.221",
+
"npm:autoprefixer",
+
"npm:cssnano",
+
"npm:postcss",
+
"npm:tailwindcss@^3.4.1"
+
]
+
},
+
"@luca/esbuild-deno-loader@0.11.1": {
+
"integrity": "dc020d16d75b591f679f6b9288b10f38bdb4f24345edb2f5732affa1d9885267",
+
"dependencies": [
+
"jsr:@std/bytes",
+
"jsr:@std/encoding@^1.0.5",
+
"jsr:@std/path@^1.0.6"
+
]
+
},
+
"@std/assert@0.221.0": {
+
"integrity": "a5f1aa6e7909dbea271754fd4ab3f4e687aeff4873b4cef9a320af813adb489a"
+
},
+
"@std/bytes@1.0.5": {
+
"integrity": "4465dd739d7963d964c809202ebea6d5c6b8e3829ef25c6a224290fbb8a1021e"
+
},
+
"@std/crypto@1.0.4": {
+
"integrity": "cee245c453bd5366207f4d8aa25ea3e9c86cecad2be3fefcaa6cb17203d79340"
+
},
+
"@std/datetime@0.225.4": {
+
"integrity": "682bc21738b941a4ed1465be6da01704e8010a3a6d9b615de9458202b84e00ec"
+
},
+
"@std/encoding@1.0.10": {
+
"integrity": "8783c6384a2d13abd5e9e87a7ae0520a30e9f56aeeaa3bdf910a3eaaf5c811a1"
+
},
+
"@std/fmt@1.0.8": {
+
"integrity": "71e1fc498787e4434d213647a6e43e794af4fd393ef8f52062246e06f7e372b7"
+
},
+
"@std/fs@1.0.17": {
+
"integrity": "1c00c632677c1158988ef7a004cb16137f870aafdb8163b9dce86ec652f3952b",
+
"dependencies": [
+
"jsr:@std/path@^1.0.9"
+
]
+
},
+
"@std/html@1.0.4": {
+
"integrity": "eff3497c08164e6ada49b7f81a28b5108087033823153d065e3f89467dd3d50e"
+
},
+
"@std/http@1.0.15": {
+
"integrity": "435a4934b4e196e82a8233f724da525f7b7112f3566502f28815e94764c19159"
+
},
+
"@std/json@1.0.2": {
+
"integrity": "d9e5497801c15fb679f55a2c01c7794ad7a5dfda4dd1bebab5e409cb5e0d34d4"
+
},
+
"@std/jsonc@1.0.2": {
+
"integrity": "909605dae3af22bd75b1cbda8d64a32cf1fd2cf6efa3f9e224aba6d22c0f44c7",
+
"dependencies": [
+
"jsr:@std/json"
+
]
+
},
+
"@std/media-types@1.1.0": {
+
"integrity": "c9d093f0c05c3512932b330e3cc1fe1d627b301db33a4c2c2185c02471d6eaa4"
+
},
+
"@std/path@0.221.0": {
+
"integrity": "0a36f6b17314ef653a3a1649740cc8db51b25a133ecfe838f20b79a56ebe0095",
+
"dependencies": [
+
"jsr:@std/assert"
+
]
+
},
+
"@std/path@1.0.9": {
+
"integrity": "260a49f11edd3db93dd38350bf9cd1b4d1366afa98e81b86167b4e3dd750129e"
+
},
+
"@std/semver@1.0.5": {
+
"integrity": "529f79e83705714c105ad0ba55bec0f9da0f24d2f726b6cc1c15e505cc2c0624"
+
}
+
},
+
"npm": {
+
"@alloc/quick-lru@5.2.0": {
+
"integrity": "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw=="
+
},
+
"@atproto-labs/did-resolver@0.1.12": {
+
"integrity": "sha512-criWN7o21C5TFsauB+bGTlkqqerOU6gT2TbxdQVgZUWqNcfazUmUjT4gJAY02i+O4d3QmZa27fv9CcaRKWkSug==",
+
"dependencies": [
+
"@atproto-labs/fetch",
+
"@atproto-labs/pipe",
+
"@atproto-labs/simple-store",
+
"@atproto-labs/simple-store-memory",
+
"@atproto/did",
+
"zod"
+
]
+
},
+
"@atproto-labs/fetch-node@0.1.8": {
+
"integrity": "sha512-OOTIhZNPEDDm7kaYU8iYRgzM+D5n3mP2iiBSyKuLakKTaZBL5WwYlUsJVsqX26SnUXtGEroOJEVJ6f66OcG80w==",
+
"dependencies": [
+
"@atproto-labs/fetch",
+
"@atproto-labs/pipe",
+
"ipaddr.js",
+
"psl",
+
"undici"
+
]
+
},
+
"@atproto-labs/fetch@0.2.2": {
+
"integrity": "sha512-QyafkedbFeVaN20DYUpnY2hcArYxjdThPXbYMqOSoZhcvkrUqaw4xDND4wZB5TBD9cq2yqe9V6mcw9P4XQKQuQ==",
+
"dependencies": [
+
"@atproto-labs/pipe"
+
]
+
},
+
"@atproto-labs/handle-resolver-node@0.1.15": {
+
"integrity": "sha512-krl9KqfCCrGID35VAAHKBIiXOxe3gYxAtOJLYpZc5cOPFwnvPlAdhTYZLIc1dJRKDayi8gh6Q4XZRDv7i8dryg==",
+
"dependencies": [
+
"@atproto-labs/fetch-node",
+
"@atproto-labs/handle-resolver",
+
"@atproto/did"
+
]
+
},
+
"@atproto-labs/handle-resolver@0.1.8": {
+
"integrity": "sha512-Y0ckccoCGDo/3g4thPkgp9QcORmc+qqEaCBCYCZYtfLIQp4775u22wd+4fyEyJP4DqoReKacninkICgRGfs3dQ==",
+
"dependencies": [
+
"@atproto-labs/simple-store",
+
"@atproto-labs/simple-store-memory",
+
"@atproto/did",
+
"zod"
+
]
+
},
+
"@atproto-labs/identity-resolver@0.1.16": {
+
"integrity": "sha512-pFrtKT49cYBhCDd2U1t/CcUBiMmQzaNQxh8oSkDUlGs/K3P8rJFTAGAMm8UjokfGEKwF4hX9oo7O8Kn+GkyExw==",
+
"dependencies": [
+
"@atproto-labs/did-resolver",
+
"@atproto-labs/handle-resolver",
+
"@atproto/syntax"
+
]
+
},
+
"@atproto-labs/pipe@0.1.0": {
+
"integrity": "sha512-ghOqHFyJlQVFPESzlVHjKroP0tPzbmG5Jms0dNI9yLDEfL8xp4OFPWLX4f6T8mRq69wWs4nIDM3sSsFbFqLa1w=="
+
},
+
"@atproto-labs/simple-store-memory@0.1.3": {
+
"integrity": "sha512-jkitT9+AtU+0b28DoN92iURLaCt/q/q4yX8q6V+9LSwYlUTqKoj/5NFKvF7x6EBuG+gpUdlcycbH7e60gjOhRQ==",
+
"dependencies": [
+
"@atproto-labs/simple-store",
+
"lru-cache"
+
]
+
},
+
"@atproto-labs/simple-store@0.2.0": {
+
"integrity": "sha512-0bRbAlI8Ayh03wRwncAMEAyUKtZ+AuTS1jgPrfym1WVOAOiottI/ZmgccqLl6w5MbxVcClNQF7WYGKvGwGoIhA=="
+
},
+
"@atproto/api@0.15.6": {
+
"integrity": "sha512-hKwrBf60LcI4BqArWyrhWJWIpjwAWUJpW3PVvNzUB1q2W/ByC0JAuwq/F8tZpCEiiVBzHjHVRx4QNA2TA1cG3g==",
+
"dependencies": [
+
"@atproto/common-web",
+
"@atproto/lexicon",
+
"@atproto/syntax",
+
"@atproto/xrpc",
+
"await-lock",
+
"multiformats@9.9.0",
+
"tlds",
+
"zod"
+
]
+
},
+
"@atproto/common-web@0.4.2": {
+
"integrity": "sha512-vrXwGNoFGogodjQvJDxAeP3QbGtawgZute2ed1XdRO0wMixLk3qewtikZm06H259QDJVu6voKC5mubml+WgQUw==",
+
"dependencies": [
+
"graphemer",
+
"multiformats@9.9.0",
+
"uint8arrays@3.0.0",
+
"zod"
+
]
+
},
+
"@atproto/crypto@0.4.4": {
+
"integrity": "sha512-Yq9+crJ7WQl7sxStVpHgie5Z51R05etaK9DLWYG/7bR5T4bhdcIgF6IfklLShtZwLYdVVj+K15s0BqW9a8PSDA==",
+
"dependencies": [
+
"@noble/curves",
+
"@noble/hashes",
+
"uint8arrays@3.0.0"
+
]
+
},
+
"@atproto/did@0.1.5": {
+
"integrity": "sha512-8+1D08QdGE5TF0bB0vV8HLVrVZJeLNITpRTUVEoABNMRaUS7CoYSVb0+JNQDeJIVmqMjOL8dOjvCUDkp3gEaGQ==",
+
"dependencies": [
+
"zod"
+
]
+
},
+
"@atproto/identity@0.4.8": {
+
"integrity": "sha512-Z0sLnJ87SeNdAifT+rqpgE1Rc3layMMW25gfWNo4u40RGuRODbdfAZlTwBSU2r+Vk45hU+iE+xeQspfednCEnA==",
+
"dependencies": [
+
"@atproto/common-web",
+
"@atproto/crypto"
+
]
+
},
+
"@atproto/jwk@0.1.4": {
+
"integrity": "sha512-dSRuEi0FbxL5ln6hEFHp5ZW01xbQH9yJi5odZaEYpcA6beZHf/bawlU12CQy/CDsbC3FxSqrBw7Q2t7mvdSBqw==",
+
"dependencies": [
+
"multiformats@9.9.0",
+
"zod"
+
]
+
},
+
"@atproto/jwk@0.1.5": {
+
"integrity": "sha512-OzZFLhX41TOcMeanP3aZlL5bLeaUIZT15MI4aU5cwflNq/rwpGOpz3uwDjZc8ytgUjuTQ8LabSz5jMmwoTSWFg==",
+
"dependencies": [
+
"multiformats@9.9.0",
+
"zod"
+
]
+
},
+
"@atproto/lexicon@0.4.11": {
+
"integrity": "sha512-btefdnvNz2Ao2I+qbmj0F06HC8IlrM/IBz6qOBS50r0S6uDf5tOO+Mv2tSVdimFkdzyDdLtBI1sV36ONxz2cOw==",
+
"dependencies": [
+
"@atproto/common-web",
+
"@atproto/syntax",
+
"iso-datestring-validator",
+
"multiformats@9.9.0",
+
"zod"
+
]
+
},
+
"@atproto/oauth-client@0.3.16": {
+
"integrity": "sha512-AEtGLOXRJzBcBa8LyUXwFf/M7cZc+CcOBjLsiqmVQriSwccfyTkALgiyM0UcRHJqlwtLPuf9RYtgKPc8rW5F/w==",
+
"dependencies": [
+
"@atproto-labs/did-resolver",
+
"@atproto-labs/fetch",
+
"@atproto-labs/handle-resolver",
+
"@atproto-labs/identity-resolver",
+
"@atproto-labs/simple-store",
+
"@atproto-labs/simple-store-memory",
+
"@atproto/did",
+
"@atproto/jwk@0.1.5",
+
"@atproto/oauth-types",
+
"@atproto/xrpc",
+
"multiformats@9.9.0",
+
"zod"
+
]
+
},
+
"@atproto/oauth-types@0.2.7": {
+
"integrity": "sha512-2SlDveiSI0oowC+sfuNd/npV8jw/FhokSS26qyUyldTg1g9ZlhxXUfMP4IZOPeZcVn9EszzQRHs1H9ZJqVQIew==",
+
"dependencies": [
+
"@atproto/jwk@0.1.5",
+
"zod"
+
]
+
},
+
"@atproto/syntax@0.4.0": {
+
"integrity": "sha512-b9y5ceHS8YKOfP3mdKmwAx5yVj9294UN7FG2XzP6V5aKUdFazEYRnR9m5n5ZQFKa3GNvz7de9guZCJ/sUTcOAA=="
+
},
+
"@atproto/xrpc@0.7.0": {
+
"integrity": "sha512-SfhP9dGx2qclaScFDb58Jnrmim5nk4geZXCqg6sB0I/KZhZEkr9iIx1hLCp+sxkIfEsmEJjeWO4B0rjUIJW5cw==",
+
"dependencies": [
+
"@atproto/lexicon",
+
"zod"
+
]
+
},
+
"@esbuild/aix-ppc64@0.23.1": {
+
"integrity": "sha512-6VhYk1diRqrhBAqpJEdjASR/+WVRtfjpqKuNw11cLiaWpAT/Uu+nokB+UJnevzy/P9C/ty6AOe0dwueMrGh/iQ==",
+
"os": ["aix"],
+
"cpu": ["ppc64"]
+
},
+
"@esbuild/android-arm64@0.23.1": {
+
"integrity": "sha512-xw50ipykXcLstLeWH7WRdQuysJqejuAGPd30vd1i5zSyKK3WE+ijzHmLKxdiCMtH1pHz78rOg0BKSYOSB/2Khw==",
+
"os": ["android"],
+
"cpu": ["arm64"]
+
},
+
"@esbuild/android-arm@0.23.1": {
+
"integrity": "sha512-uz6/tEy2IFm9RYOyvKl88zdzZfwEfKZmnX9Cj1BHjeSGNuGLuMD1kR8y5bteYmwqKm1tj8m4cb/aKEorr6fHWQ==",
+
"os": ["android"],
+
"cpu": ["arm"]
+
},
+
"@esbuild/android-x64@0.23.1": {
+
"integrity": "sha512-nlN9B69St9BwUoB+jkyU090bru8L0NA3yFvAd7k8dNsVH8bi9a8cUAUSEcEEgTp2z3dbEDGJGfP6VUnkQnlReg==",
+
"os": ["android"],
+
"cpu": ["x64"]
+
},
+
"@esbuild/darwin-arm64@0.23.1": {
+
"integrity": "sha512-YsS2e3Wtgnw7Wq53XXBLcV6JhRsEq8hkfg91ESVadIrzr9wO6jJDMZnCQbHm1Guc5t/CdDiFSSfWP58FNuvT3Q==",
+
"os": ["darwin"],
+
"cpu": ["arm64"]
+
},
+
"@esbuild/darwin-x64@0.23.1": {
+
"integrity": "sha512-aClqdgTDVPSEGgoCS8QDG37Gu8yc9lTHNAQlsztQ6ENetKEO//b8y31MMu2ZaPbn4kVsIABzVLXYLhCGekGDqw==",
+
"os": ["darwin"],
+
"cpu": ["x64"]
+
},
+
"@esbuild/freebsd-arm64@0.23.1": {
+
"integrity": "sha512-h1k6yS8/pN/NHlMl5+v4XPfikhJulk4G+tKGFIOwURBSFzE8bixw1ebjluLOjfwtLqY0kewfjLSrO6tN2MgIhA==",
+
"os": ["freebsd"],
+
"cpu": ["arm64"]
+
},
+
"@esbuild/freebsd-x64@0.23.1": {
+
"integrity": "sha512-lK1eJeyk1ZX8UklqFd/3A60UuZ/6UVfGT2LuGo3Wp4/z7eRTRYY+0xOu2kpClP+vMTi9wKOfXi2vjUpO1Ro76g==",
+
"os": ["freebsd"],
+
"cpu": ["x64"]
+
},
+
"@esbuild/linux-arm64@0.23.1": {
+
"integrity": "sha512-/93bf2yxencYDnItMYV/v116zff6UyTjo4EtEQjUBeGiVpMmffDNUyD9UN2zV+V3LRV3/on4xdZ26NKzn6754g==",
+
"os": ["linux"],
+
"cpu": ["arm64"]
+
},
+
"@esbuild/linux-arm@0.23.1": {
+
"integrity": "sha512-CXXkzgn+dXAPs3WBwE+Kvnrf4WECwBdfjfeYHpMeVxWE0EceB6vhWGShs6wi0IYEqMSIzdOF1XjQ/Mkm5d7ZdQ==",
+
"os": ["linux"],
+
"cpu": ["arm"]
+
},
+
"@esbuild/linux-ia32@0.23.1": {
+
"integrity": "sha512-VTN4EuOHwXEkXzX5nTvVY4s7E/Krz7COC8xkftbbKRYAl96vPiUssGkeMELQMOnLOJ8k3BY1+ZY52tttZnHcXQ==",
+
"os": ["linux"],
+
"cpu": ["ia32"]
+
},
+
"@esbuild/linux-loong64@0.23.1": {
+
"integrity": "sha512-Vx09LzEoBa5zDnieH8LSMRToj7ir/Jeq0Gu6qJ/1GcBq9GkfoEAoXvLiW1U9J1qE/Y/Oyaq33w5p2ZWrNNHNEw==",
+
"os": ["linux"],
+
"cpu": ["loong64"]
+
},
+
"@esbuild/linux-mips64el@0.23.1": {
+
"integrity": "sha512-nrFzzMQ7W4WRLNUOU5dlWAqa6yVeI0P78WKGUo7lg2HShq/yx+UYkeNSE0SSfSure0SqgnsxPvmAUu/vu0E+3Q==",
+
"os": ["linux"],
+
"cpu": ["mips64el"]
+
},
+
"@esbuild/linux-ppc64@0.23.1": {
+
"integrity": "sha512-dKN8fgVqd0vUIjxuJI6P/9SSSe/mB9rvA98CSH2sJnlZ/OCZWO1DJvxj8jvKTfYUdGfcq2dDxoKaC6bHuTlgcw==",
+
"os": ["linux"],
+
"cpu": ["ppc64"]
+
},
+
"@esbuild/linux-riscv64@0.23.1": {
+
"integrity": "sha512-5AV4Pzp80fhHL83JM6LoA6pTQVWgB1HovMBsLQ9OZWLDqVY8MVobBXNSmAJi//Csh6tcY7e7Lny2Hg1tElMjIA==",
+
"os": ["linux"],
+
"cpu": ["riscv64"]
+
},
+
"@esbuild/linux-s390x@0.23.1": {
+
"integrity": "sha512-9ygs73tuFCe6f6m/Tb+9LtYxWR4c9yg7zjt2cYkjDbDpV/xVn+68cQxMXCjUpYwEkze2RcU/rMnfIXNRFmSoDw==",
+
"os": ["linux"],
+
"cpu": ["s390x"]
+
},
+
"@esbuild/linux-x64@0.23.1": {
+
"integrity": "sha512-EV6+ovTsEXCPAp58g2dD68LxoP/wK5pRvgy0J/HxPGB009omFPv3Yet0HiaqvrIrgPTBuC6wCH1LTOY91EO5hQ==",
+
"os": ["linux"],
+
"cpu": ["x64"]
+
},
+
"@esbuild/netbsd-x64@0.23.1": {
+
"integrity": "sha512-aevEkCNu7KlPRpYLjwmdcuNz6bDFiE7Z8XC4CPqExjTvrHugh28QzUXVOZtiYghciKUacNktqxdpymplil1beA==",
+
"os": ["netbsd"],
+
"cpu": ["x64"]
+
},
+
"@esbuild/openbsd-arm64@0.23.1": {
+
"integrity": "sha512-3x37szhLexNA4bXhLrCC/LImN/YtWis6WXr1VESlfVtVeoFJBRINPJ3f0a/6LV8zpikqoUg4hyXw0sFBt5Cr+Q==",
+
"os": ["openbsd"],
+
"cpu": ["arm64"]
+
},
+
"@esbuild/openbsd-x64@0.23.1": {
+
"integrity": "sha512-aY2gMmKmPhxfU+0EdnN+XNtGbjfQgwZj43k8G3fyrDM/UdZww6xrWxmDkuz2eCZchqVeABjV5BpildOrUbBTqA==",
+
"os": ["openbsd"],
+
"cpu": ["x64"]
+
},
+
"@esbuild/sunos-x64@0.23.1": {
+
"integrity": "sha512-RBRT2gqEl0IKQABT4XTj78tpk9v7ehp+mazn2HbUeZl1YMdaGAQqhapjGTCe7uw7y0frDi4gS0uHzhvpFuI1sA==",
+
"os": ["sunos"],
+
"cpu": ["x64"]
+
},
+
"@esbuild/win32-arm64@0.23.1": {
+
"integrity": "sha512-4O+gPR5rEBe2FpKOVyiJ7wNDPA8nGzDuJ6gN4okSA1gEOYZ67N8JPk58tkWtdtPeLz7lBnY6I5L3jdsr3S+A6A==",
+
"os": ["win32"],
+
"cpu": ["arm64"]
+
},
+
"@esbuild/win32-ia32@0.23.1": {
+
"integrity": "sha512-BcaL0Vn6QwCwre3Y717nVHZbAa4UBEigzFm6VdsVdT/MbZ38xoj1X9HPkZhbmaBGUD1W8vxAfffbDe8bA6AKnQ==",
+
"os": ["win32"],
+
"cpu": ["ia32"]
+
},
+
"@esbuild/win32-x64@0.23.1": {
+
"integrity": "sha512-BHpFFeslkWrXWyUPnbKm+xYYVYruCinGcftSBaa8zoF9hZO4BcSCFUvHVTtzpIY6YzUnYtuEhZ+C9iEXjxnasg==",
+
"os": ["win32"],
+
"cpu": ["x64"]
+
},
+
"@isaacs/cliui@8.0.2": {
+
"integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==",
+
"dependencies": [
+
"string-width@5.1.2",
+
"string-width-cjs@npm:string-width@4.2.3",
+
"strip-ansi@7.1.0",
+
"strip-ansi-cjs@npm:strip-ansi@6.0.1",
+
"wrap-ansi@8.1.0",
+
"wrap-ansi-cjs@npm:wrap-ansi@7.0.0"
+
]
+
},
+
"@jridgewell/gen-mapping@0.3.8": {
+
"integrity": "sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA==",
+
"dependencies": [
+
"@jridgewell/set-array",
+
"@jridgewell/sourcemap-codec",
+
"@jridgewell/trace-mapping"
+
]
+
},
+
"@jridgewell/resolve-uri@3.1.2": {
+
"integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw=="
+
},
+
"@jridgewell/set-array@1.2.1": {
+
"integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A=="
+
},
+
"@jridgewell/sourcemap-codec@1.5.0": {
+
"integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ=="
+
},
+
"@jridgewell/trace-mapping@0.3.25": {
+
"integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==",
+
"dependencies": [
+
"@jridgewell/resolve-uri",
+
"@jridgewell/sourcemap-codec"
+
]
+
},
+
"@noble/curves@1.9.1": {
+
"integrity": "sha512-k11yZxZg+t+gWvBbIswW0yoJlu8cHOC7dhunwOzoWH/mXGBiYyR4YY6hAEK/3EUs4UpB8la1RfdRpeGsFHkWsA==",
+
"dependencies": [
+
"@noble/hashes"
+
]
+
},
+
"@noble/hashes@1.8.0": {
+
"integrity": "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A=="
+
},
+
"@nodelib/fs.scandir@2.1.5": {
+
"integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==",
+
"dependencies": [
+
"@nodelib/fs.stat",
+
"run-parallel"
+
]
+
},
+
"@nodelib/fs.stat@2.0.5": {
+
"integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A=="
+
},
+
"@nodelib/fs.walk@1.2.8": {
+
"integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==",
+
"dependencies": [
+
"@nodelib/fs.scandir",
+
"fastq"
+
]
+
},
+
"@opentelemetry/api@1.9.0": {
+
"integrity": "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg=="
+
},
+
"@pkgjs/parseargs@0.11.0": {
+
"integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg=="
+
},
+
"@preact/signals-core@1.8.0": {
+
"integrity": "sha512-OBvUsRZqNmjzCZXWLxkZfhcgT+Fk8DDcT/8vD6a1xhDemodyy87UJRJfASMuSD8FaAIeGgGm85ydXhm7lr4fyA=="
+
},
+
"@preact/signals@1.3.2_preact@10.26.6": {
+
"integrity": "sha512-naxcJgUJ6BTOROJ7C3QML7KvwKwCXQJYTc5L/b0eEsdYgPB6SxwoQ1vDGcS0Q7GVjAenVq/tXrybVdFShHYZWg==",
+
"dependencies": [
+
"@preact/signals-core",
+
"preact"
+
]
+
},
+
"@preact/signals@2.0.4_preact@10.26.6": {
+
"integrity": "sha512-9241aGnIv7y0IGzaq2vkBMe8/0jGnnmEEUeFmAoTWsaj8q/BW2PVekL8nHVJcy69bBww6rwEy3A1tc6yPE0sJA==",
+
"dependencies": [
+
"@preact/signals-core",
+
"preact"
+
]
+
},
+
"@trysound/sax@0.2.0": {
+
"integrity": "sha512-L7z9BgrNEcYyUYtF+HaEfiS5ebkh9jXqbszz7pC0hRBPaatV0XjSD3+eHrpqFemQfgwiFF0QPIarnIihIDn7OA=="
+
},
+
"ansi-regex@5.0.1": {
+
"integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="
+
},
+
"ansi-regex@6.1.0": {
+
"integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA=="
+
},
+
"ansi-styles@4.3.0": {
+
"integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
+
"dependencies": [
+
"color-convert"
+
]
+
},
+
"ansi-styles@6.2.1": {
+
"integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug=="
+
},
+
"any-promise@1.3.0": {
+
"integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A=="
+
},
+
"anymatch@3.1.3": {
+
"integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==",
+
"dependencies": [
+
"normalize-path",
+
"picomatch"
+
]
+
},
+
"arg@5.0.2": {
+
"integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg=="
+
},
+
"autoprefixer@10.4.17_postcss@8.4.35": {
+
"integrity": "sha512-/cpVNRLSfhOtcGflT13P2794gVSgmPgTR+erw5ifnMLZb0UnSlkK4tquLmkd3BhA+nLo5tX8Cu0upUsGKvKbmg==",
+
"dependencies": [
+
"browserslist",
+
"caniuse-lite",
+
"fraction.js",
+
"normalize-range",
+
"picocolors",
+
"postcss@8.4.35",
+
"postcss-value-parser"
+
],
+
"bin": true
+
},
+
"await-lock@2.2.2": {
+
"integrity": "sha512-aDczADvlvTGajTDjcjpJMqRkOF6Qdz3YbPZm/PyW6tKPkx2hlYBzxMhEywM/tU72HrVZjgl5VCdRuMlA7pZ8Gw=="
+
},
+
"balanced-match@1.0.2": {
+
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="
+
},
+
"binary-extensions@2.3.0": {
+
"integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw=="
+
},
+
"boolbase@1.0.0": {
+
"integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww=="
+
},
+
"brace-expansion@2.0.1": {
+
"integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==",
+
"dependencies": [
+
"balanced-match"
+
]
+
},
+
"braces@3.0.3": {
+
"integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==",
+
"dependencies": [
+
"fill-range"
+
]
+
},
+
"browserslist@4.24.5": {
+
"integrity": "sha512-FDToo4Wo82hIdgc1CQ+NQD0hEhmpPjrZ3hiUgwgOG6IuTdlpr8jdjyG24P6cNP1yJpTLzS5OcGgSw0xmDU1/Tw==",
+
"dependencies": [
+
"caniuse-lite",
+
"electron-to-chromium",
+
"node-releases",
+
"update-browserslist-db"
+
],
+
"bin": true
+
},
+
"camelcase-css@2.0.1": {
+
"integrity": "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA=="
+
},
+
"caniuse-api@3.0.0": {
+
"integrity": "sha512-bsTwuIg/BZZK/vreVTYYbSWoe2F+71P7K5QGEX+pT250DZbfU1MQ5prOKpPR+LL6uWKK3KMwMCAS74QB3Um1uw==",
+
"dependencies": [
+
"browserslist",
+
"caniuse-lite",
+
"lodash.memoize",
+
"lodash.uniq"
+
]
+
},
+
"caniuse-lite@1.0.30001717": {
+
"integrity": "sha512-auPpttCq6BDEG8ZAuHJIplGw6GODhjw+/11e7IjpnYCxZcW/ONgPs0KVBJ0d1bY3e2+7PRe5RCLyP+PfwVgkYw=="
+
},
+
"chokidar@3.6.0": {
+
"integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==",
+
"dependencies": [
+
"anymatch",
+
"braces",
+
"glob-parent@5.1.2",
+
"is-binary-path",
+
"is-glob",
+
"normalize-path",
+
"readdirp"
+
],
+
"optionalDependencies": [
+
"fsevents"
+
]
+
},
+
"color-convert@2.0.1": {
+
"integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
+
"dependencies": [
+
"color-name"
+
]
+
},
+
"color-name@1.1.4": {
+
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="
+
},
+
"colord@2.9.3": {
+
"integrity": "sha512-jeC1axXpnb0/2nn/Y1LPuLdgXBLH7aDcHu4KEKfqw3CUhX7ZpfBSlPKyqXE6btIgEzfWtrX3/tyBCaCvXvMkOw=="
+
},
+
"commander@4.1.1": {
+
"integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA=="
+
},
+
"commander@7.2.0": {
+
"integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw=="
+
},
+
"cookie@0.7.2": {
+
"integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w=="
+
},
+
"cross-spawn@7.0.6": {
+
"integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==",
+
"dependencies": [
+
"path-key",
+
"shebang-command",
+
"which"
+
]
+
},
+
"css-declaration-sorter@7.2.0_postcss@8.4.35": {
+
"integrity": "sha512-h70rUM+3PNFuaBDTLe8wF/cdWu+dOZmb7pJt8Z2sedYbAcQVQV/tEchueg3GWxwqS0cxtbxmaHEdkNACqcvsow==",
+
"dependencies": [
+
"postcss@8.4.35"
+
]
+
},
+
"css-select@5.1.0": {
+
"integrity": "sha512-nwoRF1rvRRnnCqqY7updORDsuqKzqYJ28+oSMaJMMgOauh3fvwHqMS7EZpIPqK8GL+g9mKxF1vP/ZjSeNjEVHg==",
+
"dependencies": [
+
"boolbase",
+
"css-what",
+
"domhandler",
+
"domutils",
+
"nth-check"
+
]
+
},
+
"css-tree@2.2.1": {
+
"integrity": "sha512-OA0mILzGc1kCOCSJerOeqDxDQ4HOh+G8NbOJFOTgOCzpw7fCBubk0fEyxp8AgOL/jvLgYA/uV0cMbe43ElF1JA==",
+
"dependencies": [
+
"mdn-data@2.0.28",
+
"source-map-js"
+
]
+
},
+
"css-tree@2.3.1": {
+
"integrity": "sha512-6Fv1DV/TYw//QF5IzQdqsNDjx/wc8TrMBZsqjL9eW01tWb7R7k/mq+/VXfJCl7SoD5emsJop9cOByJZfs8hYIw==",
+
"dependencies": [
+
"mdn-data@2.0.30",
+
"source-map-js"
+
]
+
},
+
"css-what@6.1.0": {
+
"integrity": "sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw=="
+
},
+
"cssesc@3.0.0": {
+
"integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==",
+
"bin": true
+
},
+
"cssnano-preset-default@6.1.2_postcss@8.4.35": {
+
"integrity": "sha512-1C0C+eNaeN8OcHQa193aRgYexyJtU8XwbdieEjClw+J9d94E41LwT6ivKH0WT+fYwYWB0Zp3I3IZ7tI/BbUbrg==",
+
"dependencies": [
+
"browserslist",
+
"css-declaration-sorter",
+
"cssnano-utils",
+
"postcss@8.4.35",
+
"postcss-calc",
+
"postcss-colormin",
+
"postcss-convert-values",
+
"postcss-discard-comments",
+
"postcss-discard-duplicates",
+
"postcss-discard-empty",
+
"postcss-discard-overridden",
+
"postcss-merge-longhand",
+
"postcss-merge-rules",
+
"postcss-minify-font-values",
+
"postcss-minify-gradients",
+
"postcss-minify-params",
+
"postcss-minify-selectors",
+
"postcss-normalize-charset",
+
"postcss-normalize-display-values",
+
"postcss-normalize-positions",
+
"postcss-normalize-repeat-style",
+
"postcss-normalize-string",
+
"postcss-normalize-timing-functions",
+
"postcss-normalize-unicode",
+
"postcss-normalize-url",
+
"postcss-normalize-whitespace",
+
"postcss-ordered-values",
+
"postcss-reduce-initial",
+
"postcss-reduce-transforms",
+
"postcss-svgo",
+
"postcss-unique-selectors"
+
]
+
},
+
"cssnano-utils@4.0.2_postcss@8.4.35": {
+
"integrity": "sha512-ZR1jHg+wZ8o4c3zqf1SIUSTIvm/9mU343FMR6Obe/unskbvpGhZOo1J6d/r8D1pzkRQYuwbcH3hToOuoA2G7oQ==",
+
"dependencies": [
+
"postcss@8.4.35"
+
]
+
},
+
"cssnano@6.0.3_postcss@8.4.35": {
+
"integrity": "sha512-MRq4CIj8pnyZpcI2qs6wswoYoDD1t0aL28n+41c1Ukcpm56m1h6mCexIHBGjfZfnTqtGSSCP4/fB1ovxgjBOiw==",
+
"dependencies": [
+
"cssnano-preset-default",
+
"lilconfig",
+
"postcss@8.4.35"
+
]
+
},
+
"csso@5.0.5": {
+
"integrity": "sha512-0LrrStPOdJj+SPCCrGhzryycLjwcgUSHBtxNA8aIDxf0GLsRh1cKYhB00Gd1lDOS4yGH69+SNn13+TWbVHETFQ==",
+
"dependencies": [
+
"css-tree@2.2.1"
+
]
+
},
+
"didyoumean@1.2.2": {
+
"integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw=="
+
},
+
"dlv@1.1.3": {
+
"integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA=="
+
},
+
"dom-serializer@2.0.0": {
+
"integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==",
+
"dependencies": [
+
"domelementtype",
+
"domhandler",
+
"entities"
+
]
+
},
+
"domelementtype@2.3.0": {
+
"integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw=="
+
},
+
"domhandler@5.0.3": {
+
"integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==",
+
"dependencies": [
+
"domelementtype"
+
]
+
},
+
"domutils@3.2.2": {
+
"integrity": "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==",
+
"dependencies": [
+
"dom-serializer",
+
"domelementtype",
+
"domhandler"
+
]
+
},
+
"eastasianwidth@0.2.0": {
+
"integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA=="
+
},
+
"electron-to-chromium@1.5.151": {
+
"integrity": "sha512-Rl6uugut2l9sLojjS4H4SAr3A4IgACMLgpuEMPYCVcKydzfyPrn5absNRju38IhQOf/NwjJY8OGWjlteqYeBCA=="
+
},
+
"emoji-regex@8.0.0": {
+
"integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="
+
},
+
"emoji-regex@9.2.2": {
+
"integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg=="
+
},
+
"entities@4.5.0": {
+
"integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw=="
+
},
+
"esbuild-wasm@0.23.1": {
+
"integrity": "sha512-L3vn7ctvBrtScRfoB0zG1eOCiV4xYvpLYWfe6PDZuV+iDFDm4Mt3xeLIDllG8cDHQ8clUouK3XekulE+cxgkgw==",
+
"bin": true
+
},
+
"esbuild@0.23.1": {
+
"integrity": "sha512-VVNz/9Sa0bs5SELtn3f7qhJCDPCF5oMEl5cO9/SSinpE9hbPVvxbd572HH5AKiP7WD8INO53GgfDDhRjkylHEg==",
+
"optionalDependencies": [
+
"@esbuild/aix-ppc64",
+
"@esbuild/android-arm",
+
"@esbuild/android-arm64",
+
"@esbuild/android-x64",
+
"@esbuild/darwin-arm64",
+
"@esbuild/darwin-x64",
+
"@esbuild/freebsd-arm64",
+
"@esbuild/freebsd-x64",
+
"@esbuild/linux-arm",
+
"@esbuild/linux-arm64",
+
"@esbuild/linux-ia32",
+
"@esbuild/linux-loong64",
+
"@esbuild/linux-mips64el",
+
"@esbuild/linux-ppc64",
+
"@esbuild/linux-riscv64",
+
"@esbuild/linux-s390x",
+
"@esbuild/linux-x64",
+
"@esbuild/netbsd-x64",
+
"@esbuild/openbsd-arm64",
+
"@esbuild/openbsd-x64",
+
"@esbuild/sunos-x64",
+
"@esbuild/win32-arm64",
+
"@esbuild/win32-ia32",
+
"@esbuild/win32-x64"
+
],
+
"scripts": true,
+
"bin": true
+
},
+
"escalade@3.2.0": {
+
"integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA=="
+
},
+
"fast-glob@3.3.3": {
+
"integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==",
+
"dependencies": [
+
"@nodelib/fs.stat",
+
"@nodelib/fs.walk",
+
"glob-parent@5.1.2",
+
"merge2",
+
"micromatch"
+
]
+
},
+
"fastq@1.19.1": {
+
"integrity": "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==",
+
"dependencies": [
+
"reusify"
+
]
+
},
+
"fill-range@7.1.1": {
+
"integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==",
+
"dependencies": [
+
"to-regex-range"
+
]
+
},
+
"foreground-child@3.3.1": {
+
"integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==",
+
"dependencies": [
+
"cross-spawn",
+
"signal-exit"
+
]
+
},
+
"fraction.js@4.3.7": {
+
"integrity": "sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew=="
+
},
+
"fsevents@2.3.3": {
+
"integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
+
"os": ["darwin"],
+
"scripts": true
+
},
+
"function-bind@1.1.2": {
+
"integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="
+
},
+
"glob-parent@5.1.2": {
+
"integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
+
"dependencies": [
+
"is-glob"
+
]
+
},
+
"glob-parent@6.0.2": {
+
"integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==",
+
"dependencies": [
+
"is-glob"
+
]
+
},
+
"glob@10.4.5": {
+
"integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==",
+
"dependencies": [
+
"foreground-child",
+
"jackspeak",
+
"minimatch",
+
"minipass",
+
"package-json-from-dist",
+
"path-scurry"
+
],
+
"bin": true
+
},
+
"graphemer@1.4.0": {
+
"integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag=="
+
},
+
"hasown@2.0.2": {
+
"integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
+
"dependencies": [
+
"function-bind"
+
]
+
},
+
"ipaddr.js@2.2.0": {
+
"integrity": "sha512-Ag3wB2o37wslZS19hZqorUnrnzSkpOVy+IiiDEiTqNubEYpYuHWIf6K4psgN2ZWKExS4xhVCrRVfb/wfW8fWJA=="
+
},
+
"iron-session@8.0.4": {
+
"integrity": "sha512-9ivNnaKOd08osD0lJ3i6If23GFS2LsxyMU8Gf/uBUEgm8/8CC1hrrCHFDpMo3IFbpBgwoo/eairRsaD3c5itxA==",
+
"dependencies": [
+
"cookie",
+
"iron-webcrypto",
+
"uncrypto"
+
]
+
},
+
"iron-webcrypto@1.2.1": {
+
"integrity": "sha512-feOM6FaSr6rEABp/eDfVseKyTMDt+KGpeB35SkVn9Tyn0CqvVsY3EwI0v5i8nMHyJnzCIQf7nsy3p41TPkJZhg=="
+
},
+
"is-binary-path@2.1.0": {
+
"integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==",
+
"dependencies": [
+
"binary-extensions"
+
]
+
},
+
"is-core-module@2.16.1": {
+
"integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==",
+
"dependencies": [
+
"hasown"
+
]
+
},
+
"is-extglob@2.1.1": {
+
"integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ=="
+
},
+
"is-fullwidth-code-point@3.0.0": {
+
"integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg=="
+
},
+
"is-glob@4.0.3": {
+
"integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==",
+
"dependencies": [
+
"is-extglob"
+
]
+
},
+
"is-number@7.0.0": {
+
"integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng=="
+
},
+
"isexe@2.0.0": {
+
"integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="
+
},
+
"iso-datestring-validator@2.2.2": {
+
"integrity": "sha512-yLEMkBbLZTlVQqOnQ4FiMujR6T4DEcCb1xizmvXS+OxuhwcbtynoosRzdMA69zZCShCNAbi+gJ71FxZBBXx1SA=="
+
},
+
"jackspeak@3.4.3": {
+
"integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==",
+
"dependencies": [
+
"@isaacs/cliui"
+
],
+
"optionalDependencies": [
+
"@pkgjs/parseargs"
+
]
+
},
+
"jiti@1.21.7": {
+
"integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==",
+
"bin": true
+
},
+
"jose@5.9.6": {
+
"integrity": "sha512-AMlnetc9+CV9asI19zHmrgS/WYsWUwCn2R7RzlbJWD7F9eWYUTGyBmU9o6PxngtLGOiDGPRu+Uc4fhKzbpteZQ=="
+
},
+
"lilconfig@3.1.3": {
+
"integrity": "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw=="
+
},
+
"lines-and-columns@1.2.4": {
+
"integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg=="
+
},
+
"lodash.memoize@4.1.2": {
+
"integrity": "sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag=="
+
},
+
"lodash.uniq@4.5.0": {
+
"integrity": "sha512-xfBaXQd9ryd9dlSDvnvI0lvxfLJlYAZzXomUYzLKtUeOQvOP5piqAWuGtrhWeqaXK9hhoM/iyJc5AV+XfsX3HQ=="
+
},
+
"lru-cache@10.4.3": {
+
"integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ=="
+
},
+
"mdn-data@2.0.28": {
+
"integrity": "sha512-aylIc7Z9y4yzHYAJNuESG3hfhC+0Ibp/MAMiaOZgNv4pmEdFyfZhhhny4MNiAfWdBQ1RQ2mfDWmM1x8SvGyp8g=="
+
},
+
"mdn-data@2.0.30": {
+
"integrity": "sha512-GaqWWShW4kv/G9IEucWScBx9G1/vsFZZJUO+tD26M8J8z3Kw5RDQjaoZe03YAClgeS/SWPOcb4nkFBTEi5DUEA=="
+
},
+
"merge2@1.4.1": {
+
"integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg=="
+
},
+
"micromatch@4.0.8": {
+
"integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==",
+
"dependencies": [
+
"braces",
+
"picomatch"
+
]
+
},
+
"minimatch@9.0.5": {
+
"integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==",
+
"dependencies": [
+
"brace-expansion"
+
]
+
},
+
"minipass@7.1.2": {
+
"integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw=="
+
},
+
"multiformats@13.3.3": {
+
"integrity": "sha512-TlaFCzs3NHNzMpwiGwRYehnnhHlZcWfptygFekshlb9xCyO09GfN+9881+VBENCdRnKOeqmMxDCbupNecV8xRQ=="
+
},
+
"multiformats@9.9.0": {
+
"integrity": "sha512-HoMUjhH9T8DDBNT+6xzkrd9ga/XiBI4xLr58LJACwK6G3HTOPeMz4nB4KJs33L2BelrIJa7P0VuNaVF3hMYfjg=="
+
},
+
"mz@2.7.0": {
+
"integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==",
+
"dependencies": [
+
"any-promise",
+
"object-assign",
+
"thenify-all"
+
]
+
},
+
"nanoid@3.3.11": {
+
"integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==",
+
"bin": true
+
},
+
"node-releases@2.0.19": {
+
"integrity": "sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw=="
+
},
+
"normalize-path@3.0.0": {
+
"integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA=="
+
},
+
"normalize-range@0.1.2": {
+
"integrity": "sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA=="
+
},
+
"nth-check@2.1.1": {
+
"integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==",
+
"dependencies": [
+
"boolbase"
+
]
+
},
+
"object-assign@4.1.1": {
+
"integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg=="
+
},
+
"object-hash@3.0.0": {
+
"integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw=="
+
},
+
"package-json-from-dist@1.0.1": {
+
"integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw=="
+
},
+
"path-key@3.1.1": {
+
"integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="
+
},
+
"path-parse@1.0.7": {
+
"integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw=="
+
},
+
"path-scurry@1.11.1": {
+
"integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==",
+
"dependencies": [
+
"lru-cache",
+
"minipass"
+
]
+
},
+
"picocolors@1.1.1": {
+
"integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="
+
},
+
"picomatch@2.3.1": {
+
"integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="
+
},
+
"pify@2.3.0": {
+
"integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog=="
+
},
+
"pirates@4.0.7": {
+
"integrity": "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA=="
+
},
+
"postcss-calc@9.0.1_postcss@8.4.35": {
+
"integrity": "sha512-TipgjGyzP5QzEhsOZUaIkeO5mKeMFpebWzRogWG/ysonUlnHcq5aJe0jOjpfzUU8PeSaBQnrE8ehR0QA5vs8PQ==",
+
"dependencies": [
+
"postcss@8.4.35",
+
"postcss-selector-parser",
+
"postcss-value-parser"
+
]
+
},
+
"postcss-colormin@6.1.0_postcss@8.4.35": {
+
"integrity": "sha512-x9yX7DOxeMAR+BgGVnNSAxmAj98NX/YxEMNFP+SDCEeNLb2r3i6Hh1ksMsnW8Ub5SLCpbescQqn9YEbE9554Sw==",
+
"dependencies": [
+
"browserslist",
+
"caniuse-api",
+
"colord",
+
"postcss@8.4.35",
+
"postcss-value-parser"
+
]
+
},
+
"postcss-convert-values@6.1.0_postcss@8.4.35": {
+
"integrity": "sha512-zx8IwP/ts9WvUM6NkVSkiU902QZL1bwPhaVaLynPtCsOTqp+ZKbNi+s6XJg3rfqpKGA/oc7Oxk5t8pOQJcwl/w==",
+
"dependencies": [
+
"browserslist",
+
"postcss@8.4.35",
+
"postcss-value-parser"
+
]
+
},
+
"postcss-discard-comments@6.0.2_postcss@8.4.35": {
+
"integrity": "sha512-65w/uIqhSBBfQmYnG92FO1mWZjJ4GL5b8atm5Yw2UgrwD7HiNiSSNwJor1eCFGzUgYnN/iIknhNRVqjrrpuglw==",
+
"dependencies": [
+
"postcss@8.4.35"
+
]
+
},
+
"postcss-discard-duplicates@6.0.3_postcss@8.4.35": {
+
"integrity": "sha512-+JA0DCvc5XvFAxwx6f/e68gQu/7Z9ud584VLmcgto28eB8FqSFZwtrLwB5Kcp70eIoWP/HXqz4wpo8rD8gpsTw==",
+
"dependencies": [
+
"postcss@8.4.35"
+
]
+
},
+
"postcss-discard-empty@6.0.3_postcss@8.4.35": {
+
"integrity": "sha512-znyno9cHKQsK6PtxL5D19Fj9uwSzC2mB74cpT66fhgOadEUPyXFkbgwm5tvc3bt3NAy8ltE5MrghxovZRVnOjQ==",
+
"dependencies": [
+
"postcss@8.4.35"
+
]
+
},
+
"postcss-discard-overridden@6.0.2_postcss@8.4.35": {
+
"integrity": "sha512-j87xzI4LUggC5zND7KdjsI25APtyMuynXZSujByMaav2roV6OZX+8AaCUcZSWqckZpjAjRyFDdpqybgjFO0HJQ==",
+
"dependencies": [
+
"postcss@8.4.35"
+
]
+
},
+
"postcss-import@15.1.0_postcss@8.5.3": {
+
"integrity": "sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==",
+
"dependencies": [
+
"postcss@8.5.3",
+
"postcss-value-parser",
+
"read-cache",
+
"resolve"
+
]
+
},
+
"postcss-js@4.0.1_postcss@8.5.3": {
+
"integrity": "sha512-dDLF8pEO191hJMtlHFPRa8xsizHaM82MLfNkUHdUtVEV3tgTp5oj+8qbEqYM57SLfc74KSbw//4SeJma2LRVIw==",
+
"dependencies": [
+
"camelcase-css",
+
"postcss@8.5.3"
+
]
+
},
+
"postcss-load-config@4.0.2_postcss@8.5.3": {
+
"integrity": "sha512-bSVhyJGL00wMVoPUzAVAnbEoWyqRxkjv64tUl427SKnPrENtq6hJwUojroMz2VB+Q1edmi4IfrAPpami5VVgMQ==",
+
"dependencies": [
+
"lilconfig",
+
"postcss@8.5.3",
+
"yaml"
+
],
+
"optionalPeers": [
+
"postcss@8.5.3"
+
]
+
},
+
"postcss-merge-longhand@6.0.5_postcss@8.4.35": {
+
"integrity": "sha512-5LOiordeTfi64QhICp07nzzuTDjNSO8g5Ksdibt44d+uvIIAE1oZdRn8y/W5ZtYgRH/lnLDlvi9F8btZcVzu3w==",
+
"dependencies": [
+
"postcss@8.4.35",
+
"postcss-value-parser",
+
"stylehacks"
+
]
+
},
+
"postcss-merge-rules@6.1.1_postcss@8.4.35": {
+
"integrity": "sha512-KOdWF0gju31AQPZiD+2Ar9Qjowz1LTChSjFFbS+e2sFgc4uHOp3ZvVX4sNeTlk0w2O31ecFGgrFzhO0RSWbWwQ==",
+
"dependencies": [
+
"browserslist",
+
"caniuse-api",
+
"cssnano-utils",
+
"postcss@8.4.35",
+
"postcss-selector-parser"
+
]
+
},
+
"postcss-minify-font-values@6.1.0_postcss@8.4.35": {
+
"integrity": "sha512-gklfI/n+9rTh8nYaSJXlCo3nOKqMNkxuGpTn/Qm0gstL3ywTr9/WRKznE+oy6fvfolH6dF+QM4nCo8yPLdvGJg==",
+
"dependencies": [
+
"postcss@8.4.35",
+
"postcss-value-parser"
+
]
+
},
+
"postcss-minify-gradients@6.0.3_postcss@8.4.35": {
+
"integrity": "sha512-4KXAHrYlzF0Rr7uc4VrfwDJ2ajrtNEpNEuLxFgwkhFZ56/7gaE4Nr49nLsQDZyUe+ds+kEhf+YAUolJiYXF8+Q==",
+
"dependencies": [
+
"colord",
+
"cssnano-utils",
+
"postcss@8.4.35",
+
"postcss-value-parser"
+
]
+
},
+
"postcss-minify-params@6.1.0_postcss@8.4.35": {
+
"integrity": "sha512-bmSKnDtyyE8ujHQK0RQJDIKhQ20Jq1LYiez54WiaOoBtcSuflfK3Nm596LvbtlFcpipMjgClQGyGr7GAs+H1uA==",
+
"dependencies": [
+
"browserslist",
+
"cssnano-utils",
+
"postcss@8.4.35",
+
"postcss-value-parser"
+
]
+
},
+
"postcss-minify-selectors@6.0.4_postcss@8.4.35": {
+
"integrity": "sha512-L8dZSwNLgK7pjTto9PzWRoMbnLq5vsZSTu8+j1P/2GB8qdtGQfn+K1uSvFgYvgh83cbyxT5m43ZZhUMTJDSClQ==",
+
"dependencies": [
+
"postcss@8.4.35",
+
"postcss-selector-parser"
+
]
+
},
+
"postcss-nested@6.2.0_postcss@8.5.3": {
+
"integrity": "sha512-HQbt28KulC5AJzG+cZtj9kvKB93CFCdLvog1WFLf1D+xmMvPGlBstkpTEZfK5+AN9hfJocyBFCNiqyS48bpgzQ==",
+
"dependencies": [
+
"postcss@8.5.3",
+
"postcss-selector-parser"
+
]
+
},
+
"postcss-normalize-charset@6.0.2_postcss@8.4.35": {
+
"integrity": "sha512-a8N9czmdnrjPHa3DeFlwqst5eaL5W8jYu3EBbTTkI5FHkfMhFZh1EGbku6jhHhIzTA6tquI2P42NtZ59M/H/kQ==",
+
"dependencies": [
+
"postcss@8.4.35"
+
]
+
},
+
"postcss-normalize-display-values@6.0.2_postcss@8.4.35": {
+
"integrity": "sha512-8H04Mxsb82ON/aAkPeq8kcBbAtI5Q2a64X/mnRRfPXBq7XeogoQvReqxEfc0B4WPq1KimjezNC8flUtC3Qz6jg==",
+
"dependencies": [
+
"postcss@8.4.35",
+
"postcss-value-parser"
+
]
+
},
+
"postcss-normalize-positions@6.0.2_postcss@8.4.35": {
+
"integrity": "sha512-/JFzI441OAB9O7VnLA+RtSNZvQ0NCFZDOtp6QPFo1iIyawyXg0YI3CYM9HBy1WvwCRHnPep/BvI1+dGPKoXx/Q==",
+
"dependencies": [
+
"postcss@8.4.35",
+
"postcss-value-parser"
+
]
+
},
+
"postcss-normalize-repeat-style@6.0.2_postcss@8.4.35": {
+
"integrity": "sha512-YdCgsfHkJ2jEXwR4RR3Tm/iOxSfdRt7jplS6XRh9Js9PyCR/aka/FCb6TuHT2U8gQubbm/mPmF6L7FY9d79VwQ==",
+
"dependencies": [
+
"postcss@8.4.35",
+
"postcss-value-parser"
+
]
+
},
+
"postcss-normalize-string@6.0.2_postcss@8.4.35": {
+
"integrity": "sha512-vQZIivlxlfqqMp4L9PZsFE4YUkWniziKjQWUtsxUiVsSSPelQydwS8Wwcuw0+83ZjPWNTl02oxlIvXsmmG+CiQ==",
+
"dependencies": [
+
"postcss@8.4.35",
+
"postcss-value-parser"
+
]
+
},
+
"postcss-normalize-timing-functions@6.0.2_postcss@8.4.35": {
+
"integrity": "sha512-a+YrtMox4TBtId/AEwbA03VcJgtyW4dGBizPl7e88cTFULYsprgHWTbfyjSLyHeBcK/Q9JhXkt2ZXiwaVHoMzA==",
+
"dependencies": [
+
"postcss@8.4.35",
+
"postcss-value-parser"
+
]
+
},
+
"postcss-normalize-unicode@6.1.0_postcss@8.4.35": {
+
"integrity": "sha512-QVC5TQHsVj33otj8/JD869Ndr5Xcc/+fwRh4HAsFsAeygQQXm+0PySrKbr/8tkDKzW+EVT3QkqZMfFrGiossDg==",
+
"dependencies": [
+
"browserslist",
+
"postcss@8.4.35",
+
"postcss-value-parser"
+
]
+
},
+
"postcss-normalize-url@6.0.2_postcss@8.4.35": {
+
"integrity": "sha512-kVNcWhCeKAzZ8B4pv/DnrU1wNh458zBNp8dh4y5hhxih5RZQ12QWMuQrDgPRw3LRl8mN9vOVfHl7uhvHYMoXsQ==",
+
"dependencies": [
+
"postcss@8.4.35",
+
"postcss-value-parser"
+
]
+
},
+
"postcss-normalize-whitespace@6.0.2_postcss@8.4.35": {
+
"integrity": "sha512-sXZ2Nj1icbJOKmdjXVT9pnyHQKiSAyuNQHSgRCUgThn2388Y9cGVDR+E9J9iAYbSbLHI+UUwLVl1Wzco/zgv0Q==",
+
"dependencies": [
+
"postcss@8.4.35",
+
"postcss-value-parser"
+
]
+
},
+
"postcss-ordered-values@6.0.2_postcss@8.4.35": {
+
"integrity": "sha512-VRZSOB+JU32RsEAQrO94QPkClGPKJEL/Z9PCBImXMhIeK5KAYo6slP/hBYlLgrCjFxyqvn5VC81tycFEDBLG1Q==",
+
"dependencies": [
+
"cssnano-utils",
+
"postcss@8.4.35",
+
"postcss-value-parser"
+
]
+
},
+
"postcss-reduce-initial@6.1.0_postcss@8.4.35": {
+
"integrity": "sha512-RarLgBK/CrL1qZags04oKbVbrrVK2wcxhvta3GCxrZO4zveibqbRPmm2VI8sSgCXwoUHEliRSbOfpR0b/VIoiw==",
+
"dependencies": [
+
"browserslist",
+
"caniuse-api",
+
"postcss@8.4.35"
+
]
+
},
+
"postcss-reduce-transforms@6.0.2_postcss@8.4.35": {
+
"integrity": "sha512-sB+Ya++3Xj1WaT9+5LOOdirAxP7dJZms3GRcYheSPi1PiTMigsxHAdkrbItHxwYHr4kt1zL7mmcHstgMYT+aiA==",
+
"dependencies": [
+
"postcss@8.4.35",
+
"postcss-value-parser"
+
]
+
},
+
"postcss-selector-parser@6.1.2": {
+
"integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==",
+
"dependencies": [
+
"cssesc",
+
"util-deprecate"
+
]
+
},
+
"postcss-svgo@6.0.3_postcss@8.4.35": {
+
"integrity": "sha512-dlrahRmxP22bX6iKEjOM+c8/1p+81asjKT+V5lrgOH944ryx/OHpclnIbGsKVd3uWOXFLYJwCVf0eEkJGvO96g==",
+
"dependencies": [
+
"postcss@8.4.35",
+
"postcss-value-parser",
+
"svgo"
+
]
+
},
+
"postcss-unique-selectors@6.0.4_postcss@8.4.35": {
+
"integrity": "sha512-K38OCaIrO8+PzpArzkLKB42dSARtC2tmG6PvD4b1o1Q2E9Os8jzfWFfSy/rixsHwohtsDdFtAWGjFVFUdwYaMg==",
+
"dependencies": [
+
"postcss@8.4.35",
+
"postcss-selector-parser"
+
]
+
},
+
"postcss-value-parser@4.2.0": {
+
"integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ=="
+
},
+
"postcss@8.4.35": {
+
"integrity": "sha512-u5U8qYpBCpN13BsiEB0CbR1Hhh4Gc0zLFuedrHJKMctHCHAGrMdG0PRM/KErzAL3CU6/eckEtmHNB3x6e3c0vA==",
+
"dependencies": [
+
"nanoid",
+
"picocolors",
+
"source-map-js"
+
]
+
},
+
"postcss@8.5.3": {
+
"integrity": "sha512-dle9A3yYxlBSrt8Fu+IpjGT8SY8hN0mlaA6GY8t0P5PjIOZemULz/E2Bnm/2dcUOena75OTNkHI76uZBNUUq3A==",
+
"dependencies": [
+
"nanoid",
+
"picocolors",
+
"source-map-js"
+
]
+
},
+
"preact-render-to-string@6.5.13_preact@10.26.6": {
+
"integrity": "sha512-iGPd+hKPMFKsfpR2vL4kJ6ZPcFIoWZEcBf0Dpm3zOpdVvj77aY8RlLiQji5OMrngEyaxGogeakTb54uS2FvA6w==",
+
"dependencies": [
+
"preact"
+
]
+
},
+
"preact@10.26.6": {
+
"integrity": "sha512-5SRRBinwpwkaD+OqlBDeITlRgvd8I8QlxHJw9AxSdMNV6O+LodN9nUyYGpSF7sadHjs6RzeFShMexC6DbtWr9g=="
+
},
+
"psl@1.15.0": {
+
"integrity": "sha512-JZd3gMVBAVQkSs6HdNZo9Sdo0LNcQeMNP3CozBJb3JYC/QUYZTnKxP+f8oWRX4rHP5EurWxqAHTSwUCjlNKa1w==",
+
"dependencies": [
+
"punycode"
+
]
+
},
+
"punycode@2.3.1": {
+
"integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="
+
},
+
"queue-microtask@1.2.3": {
+
"integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A=="
+
},
+
"read-cache@1.0.0": {
+
"integrity": "sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==",
+
"dependencies": [
+
"pify"
+
]
+
},
+
"readdirp@3.6.0": {
+
"integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==",
+
"dependencies": [
+
"picomatch"
+
]
+
},
+
"resolve@1.22.10": {
+
"integrity": "sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==",
+
"dependencies": [
+
"is-core-module",
+
"path-parse",
+
"supports-preserve-symlinks-flag"
+
],
+
"bin": true
+
},
+
"reusify@1.1.0": {
+
"integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw=="
+
},
+
"run-parallel@1.2.0": {
+
"integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==",
+
"dependencies": [
+
"queue-microtask"
+
]
+
},
+
"shebang-command@2.0.0": {
+
"integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==",
+
"dependencies": [
+
"shebang-regex"
+
]
+
},
+
"shebang-regex@3.0.0": {
+
"integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A=="
+
},
+
"signal-exit@4.1.0": {
+
"integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw=="
+
},
+
"source-map-js@1.2.1": {
+
"integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="
+
},
+
"string-width@4.2.3": {
+
"integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
+
"dependencies": [
+
"emoji-regex@8.0.0",
+
"is-fullwidth-code-point",
+
"strip-ansi@6.0.1"
+
]
+
},
+
"string-width@5.1.2": {
+
"integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==",
+
"dependencies": [
+
"eastasianwidth",
+
"emoji-regex@9.2.2",
+
"strip-ansi@7.1.0"
+
]
+
},
+
"strip-ansi@6.0.1": {
+
"integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
+
"dependencies": [
+
"ansi-regex@5.0.1"
+
]
+
},
+
"strip-ansi@7.1.0": {
+
"integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==",
+
"dependencies": [
+
"ansi-regex@6.1.0"
+
]
+
},
+
"stylehacks@6.1.1_postcss@8.4.35": {
+
"integrity": "sha512-gSTTEQ670cJNoaeIp9KX6lZmm8LJ3jPB5yJmX8Zq/wQxOsAFXV3qjWzHas3YYk1qesuVIyYWWUpZ0vSE/dTSGg==",
+
"dependencies": [
+
"browserslist",
+
"postcss@8.4.35",
+
"postcss-selector-parser"
+
]
+
},
+
"sucrase@3.35.0": {
+
"integrity": "sha512-8EbVDiu9iN/nESwxeSxDKe0dunta1GOlHufmSSXxMD2z2/tMZpDMpvXQGsc+ajGo8y2uYUmixaSRUc/QPoQ0GA==",
+
"dependencies": [
+
"@jridgewell/gen-mapping",
+
"commander@4.1.1",
+
"glob",
+
"lines-and-columns",
+
"mz",
+
"pirates",
+
"ts-interface-checker"
+
],
+
"bin": true
+
},
+
"supports-preserve-symlinks-flag@1.0.0": {
+
"integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w=="
+
},
+
"svgo@3.3.2": {
+
"integrity": "sha512-OoohrmuUlBs8B8o6MB2Aevn+pRIH9zDALSR+6hhqVfa6fRwG/Qw9VUMSMW9VNg2CFc/MTIfabtdOVl9ODIJjpw==",
+
"dependencies": [
+
"@trysound/sax",
+
"commander@7.2.0",
+
"css-select",
+
"css-tree@2.3.1",
+
"css-what",
+
"csso",
+
"picocolors"
+
],
+
"bin": true
+
},
+
"tailwindcss@3.4.17_postcss@8.5.3": {
+
"integrity": "sha512-w33E2aCvSDP0tW9RZuNXadXlkHXqFzSkQew/aIa2i/Sj8fThxwovwlXHSPXTbAHwEIhBFXAedUhP2tueAKP8Og==",
+
"dependencies": [
+
"@alloc/quick-lru",
+
"arg",
+
"chokidar",
+
"didyoumean",
+
"dlv",
+
"fast-glob",
+
"glob-parent@6.0.2",
+
"is-glob",
+
"jiti",
+
"lilconfig",
+
"micromatch",
+
"normalize-path",
+
"object-hash",
+
"picocolors",
+
"postcss@8.5.3",
+
"postcss-import",
+
"postcss-js",
+
"postcss-load-config",
+
"postcss-nested",
+
"postcss-selector-parser",
+
"resolve",
+
"sucrase"
+
],
+
"bin": true
+
},
+
"thenify-all@1.6.0": {
+
"integrity": "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==",
+
"dependencies": [
+
"thenify"
+
]
+
},
+
"thenify@3.3.1": {
+
"integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==",
+
"dependencies": [
+
"any-promise"
+
]
+
},
+
"tlds@1.258.0": {
+
"integrity": "sha512-XGhStWuOlBA5D8QnyN2xtgB2cUOdJ3ztisne1DYVWMcVH29qh8eQIpRmP3HnuJLdgyzG0HpdGzRMu1lm/Oictw==",
+
"bin": true
+
},
+
"to-regex-range@5.0.1": {
+
"integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==",
+
"dependencies": [
+
"is-number"
+
]
+
},
+
"ts-interface-checker@0.1.13": {
+
"integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA=="
+
},
+
"uint8arrays@3.0.0": {
+
"integrity": "sha512-HRCx0q6O9Bfbp+HHSfQQKD7wU70+lydKVt4EghkdOvlK/NlrF90z+eXV34mUd48rNvVJXwkrMSPpCATkct8fJA==",
+
"dependencies": [
+
"multiformats@9.9.0"
+
]
+
},
+
"uint8arrays@5.1.0": {
+
"integrity": "sha512-vA6nFepEmlSKkMBnLBaUMVvAC4G3CTmO58C12y4sq6WPDOR7mOFYOi7GlrQ4djeSbP6JG9Pv9tJDM97PedRSww==",
+
"dependencies": [
+
"multiformats@13.3.3"
+
]
+
},
+
"uncrypto@0.1.3": {
+
"integrity": "sha512-Ql87qFHB3s/De2ClA9e0gsnS6zXG27SkTiSJwjCc9MebbfapQfuPzumMIUMi38ezPZVNFcHI9sUIepeQfw8J8Q=="
+
},
+
"undici@6.21.2": {
+
"integrity": "sha512-uROZWze0R0itiAKVPsYhFov9LxrPMHLMEQFszeI2gCN6bnIIZ8twzBCJcN2LJrBBLfrP0t1FW0g+JmKVl8Vk1g=="
+
},
+
"update-browserslist-db@1.1.3_browserslist@4.24.5": {
+
"integrity": "sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw==",
+
"dependencies": [
+
"browserslist",
+
"escalade",
+
"picocolors"
+
],
+
"bin": true
+
},
+
"util-deprecate@1.0.2": {
+
"integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="
+
},
+
"which@2.0.2": {
+
"integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==",
+
"dependencies": [
+
"isexe"
+
],
+
"bin": true
+
},
+
"wrap-ansi@7.0.0": {
+
"integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==",
+
"dependencies": [
+
"ansi-styles@4.3.0",
+
"string-width@4.2.3",
+
"strip-ansi@6.0.1"
+
]
+
},
+
"wrap-ansi@8.1.0": {
+
"integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==",
+
"dependencies": [
+
"ansi-styles@6.2.1",
+
"string-width@5.1.2",
+
"strip-ansi@7.1.0"
+
]
+
},
+
"yaml@2.8.0": {
+
"integrity": "sha512-4lLa/EcQCB0cJkyts+FpIRx5G/llPxfP6VQU5KByHEhLxY3IJCH0f0Hy1MHI8sClTvsIb8qwRJ6R/ZdlDJ/leQ==",
+
"bin": true
+
},
+
"zod@3.24.4": {
+
"integrity": "sha512-OdqJE9UDRPwWsrHjLN2F8bPxvwJBK22EHLWtanu0LSYr5YqzsaaW3RMgmjwr8Rypg5k+meEJdSPXJZXE/yqOMg=="
+
}
+
},
+
"workspace": {
+
"dependencies": [
+
"jsr:@fresh/core@^2.0.0-alpha.33",
+
"jsr:@fresh/plugin-tailwind@^0.0.1-alpha.7",
+
"npm:@atproto/api@~0.15.6",
+
"npm:@preact/signals@^2.0.4",
+
"npm:preact@^10.26.6",
+
"npm:tailwindcss@^3.4.3"
+
]
+
}
+
}
+15 -5
dev.ts
···
-
#!/usr/bin/env -S deno run -A --watch=static/,routes/
-
import dev from "$fresh/dev.ts";
-
import config from "./fresh.config.ts";
-
import "$std/dotenv/load.ts";
-
await dev(import.meta.url, "./main.ts", config);
···
+
import { Builder } from "fresh/dev";
+
import { tailwind } from "@fresh/plugin-tailwind";
+
import { app } from "./main.ts";
+
// Pass development only configuration here
+
const builder = new Builder({ target: "safari12" });
+
// Example: Enabling the tailwind plugin for Fresh
+
tailwind(builder, app, {});
+
// Create optimized assets for the browser when
+
// running `deno run -A dev.ts build`
+
if (Deno.args.includes("build")) {
+
await builder.build(app);
+
} else {
+
// ...otherwise start the development server
+
await builder.listen(app);
+
}
-7
fresh.config.ts
···
-
import { defineConfig } from "$fresh/server.ts";
-
import tailwind from "$fresh/plugins/tailwind.ts";
-
import didJson from "./plugins/did.ts";
-
-
export default defineConfig({
-
plugins: [tailwind(), didJson],
-
});
···
-71
fresh.gen.ts
···
-
// DO NOT EDIT. This file is generated by Fresh.
-
// This file SHOULD be checked into source version control.
-
// This file is automatically updated during development when running `dev.ts`.
-
-
import * as $_404 from "./routes/_404.tsx";
-
import * as $_app from "./routes/_app.tsx";
-
import * as $api_me from "./routes/api/me.ts";
-
import * as $api_oauth_callback from "./routes/api/oauth/callback.ts";
-
import * as $api_oauth_initiate from "./routes/api/oauth/initiate.ts";
-
import * as $api_oauth_logout from "./routes/api/oauth/logout.ts";
-
import * as $api_server_describe from "./routes/api/server/describe.ts";
-
import * as $api_server_migrate from "./routes/api/server/migrate.ts";
-
import * as $api_server_migrate_create from "./routes/api/server/migrate/create.ts";
-
import * as $api_server_migrate_data from "./routes/api/server/migrate/data.ts";
-
import * as $api_server_migrate_finalize from "./routes/api/server/migrate/finalize.ts";
-
import * as $api_server_migrate_identity_request from "./routes/api/server/migrate/identity/request.ts";
-
import * as $api_server_migrate_identity_sign from "./routes/api/server/migrate/identity/sign.ts";
-
import * as $index from "./routes/index.tsx";
-
import * as $login_callback from "./routes/login/callback.tsx";
-
import * as $login_index from "./routes/login/index.tsx";
-
import * as $migrate_index from "./routes/migrate/index.tsx";
-
import * as $migrate_progress from "./routes/migrate/progress.tsx";
-
import * as $AirportSign from "./islands/AirportSign.tsx";
-
import * as $Counter from "./islands/Counter.tsx";
-
import * as $HandleInput from "./islands/HandleInput.tsx";
-
import * as $Header from "./islands/Header.tsx";
-
import * as $MigrationFlow from "./islands/MigrationFlow.tsx";
-
import * as $MigrationProgress from "./islands/MigrationProgress.tsx";
-
import * as $MigrationSetup from "./islands/MigrationSetup.tsx";
-
import * as $OAuthCallback from "./islands/OAuthCallback.tsx";
-
import * as $Ticket from "./islands/Ticket.tsx";
-
import type { Manifest } from "$fresh/server.ts";
-
-
const manifest = {
-
routes: {
-
"./routes/_404.tsx": $_404,
-
"./routes/_app.tsx": $_app,
-
"./routes/api/me.ts": $api_me,
-
"./routes/api/oauth/callback.ts": $api_oauth_callback,
-
"./routes/api/oauth/initiate.ts": $api_oauth_initiate,
-
"./routes/api/oauth/logout.ts": $api_oauth_logout,
-
"./routes/api/server/describe.ts": $api_server_describe,
-
"./routes/api/server/migrate.ts": $api_server_migrate,
-
"./routes/api/server/migrate/create.ts": $api_server_migrate_create,
-
"./routes/api/server/migrate/data.ts": $api_server_migrate_data,
-
"./routes/api/server/migrate/finalize.ts": $api_server_migrate_finalize,
-
"./routes/api/server/migrate/identity/request.ts":
-
$api_server_migrate_identity_request,
-
"./routes/api/server/migrate/identity/sign.ts":
-
$api_server_migrate_identity_sign,
-
"./routes/index.tsx": $index,
-
"./routes/login/callback.tsx": $login_callback,
-
"./routes/login/index.tsx": $login_index,
-
"./routes/migrate/index.tsx": $migrate_index,
-
"./routes/migrate/progress.tsx": $migrate_progress,
-
},
-
islands: {
-
"./islands/AirportSign.tsx": $AirportSign,
-
"./islands/Counter.tsx": $Counter,
-
"./islands/HandleInput.tsx": $HandleInput,
-
"./islands/Header.tsx": $Header,
-
"./islands/MigrationFlow.tsx": $MigrationFlow,
-
"./islands/MigrationProgress.tsx": $MigrationProgress,
-
"./islands/MigrationSetup.tsx": $MigrationSetup,
-
"./islands/OAuthCallback.tsx": $OAuthCallback,
-
"./islands/Ticket.tsx": $Ticket,
-
},
-
baseUrl: import.meta.url,
-
} satisfies Manifest;
-
-
export default manifest;
···
+16 -14
islands/AirportSign.tsx
···
return (
<div class="relative inline-block mb-12">
{/* Left Pole */}
-
<div class="absolute left-8 -top-24 w-4 h-24 bg-gradient-to-r from-slate-800 via-slate-600 to-slate-800 rounded-t-lg"></div>
{/* Right Pole */}
-
<div class="absolute right-8 -top-24 w-4 h-24 bg-gradient-to-r from-slate-800 via-slate-600 to-slate-800 rounded-t-lg"></div>
{/* Display Board */}
<div class="relative bg-gradient-to-b from-slate-800 to-slate-900 p-1 rounded-lg shadow-[0_2px_10px_rgba(0,0,0,0.3)]">
{/* Metallic Frame */}
···
{/* Screen Background with Effects */}
<div class="absolute inset-0 bg-[#0a0a2f]">
{/* Scan lines */}
-
<div class="absolute inset-0 bg-[linear-gradient(transparent_0%,_rgba(255,255,255,0.02)_50%,_transparent_100%)] bg-[length:100%_4px]"></div>
{/* Screen noise */}
-
<div class="absolute inset-0 opacity-[0.03] [background-image:url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIzMDAiIGhlaWdodD0iMzAwIj48ZmlsdGVyIGlkPSJhIiB4PSIwIiB5PSIwIj48ZmVUdXJidWxlbmNlIHR5cGU9ImZyYWN0YWxOb2lzZSIgYmFzZUZyZXF1ZW5jeT0iLjc1IiBzdGl0Y2hUaWxlcz0ic3RpdGNoIi8+PC9maWx0ZXI+PHJlY3Qgd2lkdGg9IjMwMCIgaGVpZ2h0PSIzMDAiIGZpbHRlcj0idXJsKCNhKSIgb3BhY2l0eT0iMC4wNSIvPjwvc3ZnPg==')]"></div>
</div>
{/* Display Board Text */}
<div className="relative flex justify-center items-center py-2 pb-4 px-4">
<div className="relative text-center">
-
<span
-
className="font-mono text-[3em] font-semibold tracking-[0.12em] leading-[0.9] text-white
[text-shadow:0_0_20px_rgba(255,255,255,0.2),0_0_40px_rgba(255,255,255,0.1)]
-
relative z-10"
-
>
ATP INTERNECTIONAL AIRPORT
</span>
{/* Text glow effect */}
<div className="absolute inset-0 blur-[2px] opacity-50">
-
<span
-
className="font-mono text-[3em] font-semibold tracking-[0.12em] leading-[0.9] text-white"
-
>
ATP INTERNECTIONAL AIRPORT
</span>
</div>
···
</div>
{/* Screen reflection overlay */}
-
<div className="absolute inset-0 bg-gradient-to-b from-transparent via-transparent to-white/[0.03]"></div>
{/* Vignette effect */}
-
<div className="absolute inset-0 bg-[radial-gradient(circle_at_center,_transparent_0%,_rgba(0,0,0,0.2)_100%)]"></div>
</div>
</div>
</div>
</div>
);
-
}
···
return (
<div class="relative inline-block mb-12">
{/* Left Pole */}
+
<div class="absolute left-8 -top-24 w-4 h-24 bg-gradient-to-r from-slate-800 via-slate-600 to-slate-800 rounded-t-lg">
+
</div>
{/* Right Pole */}
+
<div class="absolute right-8 -top-24 w-4 h-24 bg-gradient-to-r from-slate-800 via-slate-600 to-slate-800 rounded-t-lg">
+
</div>
{/* Display Board */}
<div class="relative bg-gradient-to-b from-slate-800 to-slate-900 p-1 rounded-lg shadow-[0_2px_10px_rgba(0,0,0,0.3)]">
{/* Metallic Frame */}
···
{/* Screen Background with Effects */}
<div class="absolute inset-0 bg-[#0a0a2f]">
{/* Scan lines */}
+
<div class="absolute inset-0 bg-[linear-gradient(transparent_0%,_rgba(255,255,255,0.02)_50%,_transparent_100%)] bg-[length:100%_4px]">
+
</div>
{/* Screen noise */}
+
<div class="absolute inset-0 opacity-[0.03] [background-image:url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIzMDAiIGhlaWdodD0iMzAwIj48ZmlsdGVyIGlkPSJhIiB4PSIwIiB5PSIwIj48ZmVUdXJidWxlbmNlIHR5cGU9ImZyYWN0YWxOb2lzZSIgYmFzZUZyZXF1ZW5jeT0iLjc1IiBzdGl0Y2hUaWxlcz0ic3RpdGNoIi8+PC9maWx0ZXI+PHJlY3Qgd2lkdGg9IjMwMCIgaGVpZ2h0PSIzMDAiIGZpbHRlcj0idXJsKCNhKSIgb3BhY2l0eT0iMC4wNSIvPjwvc3ZnPg==')]">
+
</div>
</div>
{/* Display Board Text */}
<div className="relative flex justify-center items-center py-2 pb-4 px-4">
<div className="relative text-center">
+
<span className="font-mono text-[3em] font-semibold tracking-[0.12em] leading-[0.9] text-white
[text-shadow:0_0_20px_rgba(255,255,255,0.2),0_0_40px_rgba(255,255,255,0.1)]
+
relative z-10">
ATP INTERNECTIONAL AIRPORT
</span>
{/* Text glow effect */}
<div className="absolute inset-0 blur-[2px] opacity-50">
+
<span className="font-mono text-[3em] font-semibold tracking-[0.12em] leading-[0.9] text-white">
ATP INTERNECTIONAL AIRPORT
</span>
</div>
···
</div>
{/* Screen reflection overlay */}
+
<div className="absolute inset-0 bg-gradient-to-b from-transparent via-transparent to-white/[0.03]">
+
</div>
{/* Vignette effect */}
+
<div className="absolute inset-0 bg-[radial-gradient(circle_at_center,_transparent_0%,_rgba(0,0,0,0.2)_100%)]">
+
</div>
</div>
</div>
</div>
</div>
);
+
}
+31 -29
islands/HandleInput.tsx
···
-
import { useState } from 'preact/hooks'
-
import { JSX } from 'preact'
export default function HandleInput() {
-
const [handle, setHandle] = useState('')
-
const [error, setError] = useState<string | null>(null)
-
const [isPending, setIsPending] = useState(false)
const handleSubmit = async (e: JSX.TargetedEvent<HTMLFormElement>) => {
-
e.preventDefault()
-
if (!handle.trim()) return
-
setError(null)
-
setIsPending(true)
try {
-
const response = await fetch('/api/oauth/initiate', {
-
method: 'POST',
headers: {
-
'Content-Type': 'application/json',
},
body: JSON.stringify({ handle }),
-
})
if (!response.ok) {
-
const errorText = await response.text()
-
throw new Error(errorText || 'Login failed')
}
-
const data = await response.json()
// Add a small delay before redirecting for better UX
-
await new Promise((resolve) => setTimeout(resolve, 500))
// Redirect to ATProto OAuth flow
-
globalThis.location.href = data.redirectUrl
} catch (err) {
-
const message = err instanceof Error ? err.message : 'Login failed'
-
setError(message)
} finally {
-
setIsPending(false)
}
-
}
return (
<form onSubmit={handleSubmit}>
···
className="w-full p-3 border border-gray-300 dark:border-gray-700 bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-300 dark:focus:ring-blue-500 transition-colors"
/>
<p className="text-gray-400 dark:text-gray-500 text-sm mt-2">
-
You can also enter an AT Protocol PDS URL, i.e.{' '}
<span className="whitespace-nowrap">https://bsky.social</span>
</p>
</div>
···
type="submit"
disabled={isPending}
className={`w-full px-4 py-2 rounded-md bg-blue-500 dark:bg-blue-600 text-white font-medium hover:bg-blue-600 dark:hover:bg-blue-700 transition-colors focus:outline-none focus:ring-2 focus:ring-blue-300 dark:focus:ring-blue-500 relative ${
-
isPending ? 'opacity-90 cursor-not-allowed' : ''
}`}
>
-
<span className={isPending ? 'invisible' : ''}>Login</span>
{isPending && (
<span className="absolute inset-0 flex items-center justify-center">
<svg
···
r="10"
stroke="currentColor"
strokeWidth="4"
-
></circle>
<path
className="opacity-75"
fill="currentColor"
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
-
></path>
</svg>
<span>Connecting...</span>
</span>
)}
</button>
</form>
-
)
-
}
···
+
import { useState } from "preact/hooks";
+
import { JSX } from "preact";
export default function HandleInput() {
+
const [handle, setHandle] = useState("");
+
const [error, setError] = useState<string | null>(null);
+
const [isPending, setIsPending] = useState(false);
const handleSubmit = async (e: JSX.TargetedEvent<HTMLFormElement>) => {
+
e.preventDefault();
+
if (!handle.trim()) return;
+
setError(null);
+
setIsPending(true);
try {
+
const response = await fetch("/api/oauth/initiate", {
+
method: "POST",
headers: {
+
"Content-Type": "application/json",
},
body: JSON.stringify({ handle }),
+
});
if (!response.ok) {
+
const errorText = await response.text();
+
throw new Error(errorText || "Login failed");
}
+
const data = await response.json();
// Add a small delay before redirecting for better UX
+
await new Promise((resolve) => setTimeout(resolve, 500));
// Redirect to ATProto OAuth flow
+
globalThis.location.href = data.redirectUrl;
} catch (err) {
+
const message = err instanceof Error ? err.message : "Login failed";
+
setError(message);
} finally {
+
setIsPending(false);
}
+
};
return (
<form onSubmit={handleSubmit}>
···
className="w-full p-3 border border-gray-300 dark:border-gray-700 bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-300 dark:focus:ring-blue-500 transition-colors"
/>
<p className="text-gray-400 dark:text-gray-500 text-sm mt-2">
+
You can also enter an AT Protocol PDS URL, i.e.{" "}
<span className="whitespace-nowrap">https://bsky.social</span>
</p>
</div>
···
type="submit"
disabled={isPending}
className={`w-full px-4 py-2 rounded-md bg-blue-500 dark:bg-blue-600 text-white font-medium hover:bg-blue-600 dark:hover:bg-blue-700 transition-colors focus:outline-none focus:ring-2 focus:ring-blue-300 dark:focus:ring-blue-500 relative ${
+
isPending ? "opacity-90 cursor-not-allowed" : ""
}`}
>
+
<span className={isPending ? "invisible" : ""}>Login</span>
{isPending && (
<span className="absolute inset-0 flex items-center justify-center">
<svg
···
r="10"
stroke="currentColor"
strokeWidth="4"
+
>
+
</circle>
<path
className="opacity-75"
fill="currentColor"
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
+
>
+
</path>
</svg>
<span>Connecting...</span>
</span>
)}
</button>
</form>
+
);
+
}
+80 -39
islands/Header.tsx
···
import { useEffect, useState } from "preact/hooks";
-
import { IS_BROWSER } from "$fresh/runtime.ts";
interface User {
did: string;
···
if (text.length <= maxLength) return text;
let truncated = text.slice(0, maxLength);
// Remove trailing dots before adding ellipsis
-
while (truncated.endsWith('.')) {
truncated = truncated.slice(0, -1);
}
-
return truncated + '...';
}
export default function Header() {
···
throw new Error("Failed to fetch user profile");
}
const userData = await response.json();
-
setUser(userData ? {
-
did: userData.did,
-
handle: userData.handle
-
} : null);
} catch (error) {
console.error("Failed to fetch user:", error);
setUser(null);
···
<div className="max-w-7xl mx-auto px-4">
<div className="flex items-center justify-between py-4">
{/* Home Link */}
-
<a href="/" className="airport-sign bg-gradient-to-r from-blue-500 to-blue-600 text-white flex items-center px-6 py-3 hover:translate-y-1 transition-all duration-200 hover:from-blue-600 hover:to-blue-700">
-
<img src="/icons/plane_bold.svg" alt="Plane" className="w-6 h-6 mr-2" style={{ filter: 'brightness(0) invert(1)' }} />
<span className="font-mono font-bold tracking-wider">AIRPORT</span>
</a>
<div className="flex items-center gap-3">
{/* Departures (Migration) */}
<div className="relative group">
-
<a href="/migrate" className="airport-sign bg-gradient-to-r from-amber-400 to-amber-500 text-slate-900 flex items-center px-6 py-3 hover:translate-y-1 transition-all duration-200 hover:from-amber-500 hover:to-amber-600">
-
<img src="/icons/plane-departure_bold.svg" alt="Departures" className="w-6 h-6 mr-2" style={{ filter: 'brightness(0)' }} />
-
<span className="font-mono font-bold tracking-wider">DEPARTURES</span>
</a>
</div>
{/* Check-in (Login/Profile) */}
<div className="relative">
-
{user?.did ? (
-
<div className="relative group">
-
<div className="airport-sign bg-gradient-to-r from-amber-400 to-amber-500 text-slate-900 flex items-center px-6 py-3 hover:translate-y-1 transition-all duration-200 hover:from-amber-500 hover:to-amber-600 cursor-pointer">
-
<img src="/icons/ticket_bold.svg" alt="Check-in" className="w-6 h-6 mr-2" style={{ filter: 'brightness(0)' }} />
-
<span className="font-mono font-bold tracking-wider">CHECKED IN</span>
-
</div>
-
<div className="absolute opacity-0 translate-y-[-8px] pointer-events-none group-hover:opacity-100 group-hover:translate-y-0 group-hover:pointer-events-auto top-full right-0 w-56 bg-gradient-to-r from-amber-400 to-amber-500 text-slate-900 py-3 px-4 rounded-md transition-all duration-200">
-
<div className="text-sm font-mono mb-2 pb-2 border-b border-slate-900/10">
-
<div title={user.handle || 'Anonymous'}>
-
{truncateText(user.handle || 'Anonymous', 20)}
-
</div>
-
<div className="text-xs opacity-75" title={user.did}>
-
{truncateText(user.did, 25)}
</div>
</div>
-
<button
-
type="button"
-
onClick={handleLogout}
-
className="text-sm font-mono text-slate-900 hover:text-slate-700 w-full text-left transition-colors"
-
>
-
Sign Out
-
</button>
</div>
-
</div>
-
) : (
-
<a href="/login" className="airport-sign bg-gradient-to-r from-amber-400 to-amber-500 text-slate-900 flex items-center px-6 py-3 hover:translate-y-1 transition-all duration-200 hover:from-amber-500 hover:to-amber-600">
-
<img src="/icons/ticket_bold.svg" alt="Check-in" className="w-6 h-6 mr-2" style={{ filter: 'brightness(0)' }} />
-
<span className="font-mono font-bold tracking-wider">CHECK-IN</span>
-
</a>
-
)}
</div>
</div>
</div>
···
import { useEffect, useState } from "preact/hooks";
+
import { IS_BROWSER } from "fresh/runtime";
interface User {
did: string;
···
if (text.length <= maxLength) return text;
let truncated = text.slice(0, maxLength);
// Remove trailing dots before adding ellipsis
+
while (truncated.endsWith(".")) {
truncated = truncated.slice(0, -1);
}
+
return truncated + "...";
}
export default function Header() {
···
throw new Error("Failed to fetch user profile");
}
const userData = await response.json();
+
setUser(
+
userData
+
? {
+
did: userData.did,
+
handle: userData.handle,
+
}
+
: null,
+
);
} catch (error) {
console.error("Failed to fetch user:", error);
setUser(null);
···
<div className="max-w-7xl mx-auto px-4">
<div className="flex items-center justify-between py-4">
{/* Home Link */}
+
<a
+
href="/"
+
className="airport-sign bg-gradient-to-r from-blue-500 to-blue-600 text-white flex items-center px-6 py-3 hover:translate-y-1 transition-all duration-200 hover:from-blue-600 hover:to-blue-700"
+
>
+
<img
+
src="/icons/plane_bold.svg"
+
alt="Plane"
+
className="w-6 h-6 mr-2"
+
style={{ filter: "brightness(0) invert(1)" }}
+
/>
<span className="font-mono font-bold tracking-wider">AIRPORT</span>
</a>
<div className="flex items-center gap-3">
{/* Departures (Migration) */}
<div className="relative group">
+
<a
+
href="/migrate"
+
className="airport-sign bg-gradient-to-r from-amber-400 to-amber-500 text-slate-900 flex items-center px-6 py-3 hover:translate-y-1 transition-all duration-200 hover:from-amber-500 hover:to-amber-600"
+
>
+
<img
+
src="/icons/plane-departure_bold.svg"
+
alt="Departures"
+
className="w-6 h-6 mr-2"
+
style={{ filter: "brightness(0)" }}
+
/>
+
<span className="font-mono font-bold tracking-wider">
+
DEPARTURES
+
</span>
</a>
</div>
{/* Check-in (Login/Profile) */}
<div className="relative">
+
{user?.did
+
? (
+
<div className="relative group">
+
<div className="airport-sign bg-gradient-to-r from-amber-400 to-amber-500 text-slate-900 flex items-center px-6 py-3 hover:translate-y-1 transition-all duration-200 hover:from-amber-500 hover:to-amber-600 cursor-pointer">
+
<img
+
src="/icons/ticket_bold.svg"
+
alt="Check-in"
+
className="w-6 h-6 mr-2"
+
style={{ filter: "brightness(0)" }}
+
/>
+
<span className="font-mono font-bold tracking-wider">
+
CHECKED IN
+
</span>
+
</div>
+
<div className="absolute opacity-0 translate-y-[-8px] pointer-events-none group-hover:opacity-100 group-hover:translate-y-0 group-hover:pointer-events-auto top-full right-0 w-56 bg-gradient-to-r from-amber-400 to-amber-500 text-slate-900 py-3 px-4 rounded-md transition-all duration-200">
+
<div className="text-sm font-mono mb-2 pb-2 border-b border-slate-900/10">
+
<div title={user.handle || "Anonymous"}>
+
{truncateText(user.handle || "Anonymous", 20)}
+
</div>
+
<div className="text-xs opacity-75" title={user.did}>
+
{truncateText(user.did, 25)}
+
</div>
</div>
+
<button
+
type="button"
+
onClick={handleLogout}
+
className="text-sm font-mono text-slate-900 hover:text-slate-700 w-full text-left transition-colors"
+
>
+
Sign Out
+
</button>
</div>
</div>
+
)
+
: (
+
<a
+
href="/login"
+
className="airport-sign bg-gradient-to-r from-amber-400 to-amber-500 text-slate-900 flex items-center px-6 py-3 hover:translate-y-1 transition-all duration-200 hover:from-amber-500 hover:to-amber-600"
+
>
+
<img
+
src="/icons/ticket_bold.svg"
+
alt="Check-in"
+
className="w-6 h-6 mr-2"
+
style={{ filter: "brightness(0)" }}
+
/>
+
<span className="font-mono font-bold tracking-wider">
+
CHECK-IN
+
</span>
+
</a>
+
)}
</div>
</div>
</div>
-1
islands/MigrationFlow.tsx
···
-
···
+122 -47
islands/MigrationProgress.tsx
···
{ name: "Finalize Migration", status: "pending" },
]);
-
const updateStepStatus = (index: number, status: MigrationStep["status"], error?: string) => {
-
console.log(`Updating step ${index} to ${status}${error ? ` with error: ${error}` : ''}`);
-
setSteps(prevSteps => prevSteps.map((step, i) =>
-
i === index
-
? { ...step, status, error }
-
: i > index
? { ...step, status: "pending", error: undefined }
: step
-
));
};
const validateParams = () => {
···
handle: props.handle,
email: props.email,
hasPassword: !!props.password,
-
invite: props.invite
});
-
if (!validateParams()) {
console.log("Parameter validation failed");
return;
}
-
-
startMigration().catch(error => {
console.error("Unhandled migration error:", error);
-
updateStepStatus(0, "error", error instanceof Error ? error.message : String(error));
});
}, []);
···
// Step 1: Create Account
updateStepStatus(0, "in-progress");
console.log("Starting account creation...");
-
try {
const createRes = await fetch("/api/server/migrate/create", {
method: "POST",
headers: { "Content-Type": "application/json" },
-
body: JSON.stringify({
-
service: props.service,
-
handle: props.handle,
-
password: props.password,
-
email: props.email,
-
...(props.invite ? { invite: props.invite } : {})
}),
});
···
updateStepStatus(0, "completed");
} catch (error) {
-
updateStepStatus(0, "error", error instanceof Error ? error.message : String(error));
throw error;
}
// Step 2: Migrate Data
updateStepStatus(1, "in-progress");
console.log("Starting data migration...");
-
try {
const dataRes = await fetch("/api/server/migrate/data", {
method: "POST",
headers: { "Content-Type": "application/json" },
});
-
console.log("Data migration response status:", dataRes.status);
const dataText = await dataRes.text();
console.log("Data migration response:", dataText);
···
updateStepStatus(1, "completed");
} catch (error) {
-
updateStepStatus(1, "error", error instanceof Error ? error.message : String(error));
throw error;
}
// Step 3: Request Identity Migration
updateStepStatus(2, "in-progress");
console.log("Requesting identity migration...");
-
try {
const requestRes = await fetch("/api/server/migrate/identity/request", {
method: "POST",
···
console.log("Identity request response:", requestText);
if (!requestRes.ok) {
-
throw new Error(requestText || "Failed to request identity migration");
}
try {
const jsonData = JSON.parse(requestText);
if (!jsonData.success) {
-
throw new Error(jsonData.message || "Identity migration request failed");
}
console.log("Identity migration requested successfully");
} catch (e) {
console.error("Failed to parse identity request response:", e);
-
throw new Error("Invalid response from server during identity request");
}
updateStepStatus(2, "completed");
// Move to token input step
updateStepStatus(3, "in-progress");
} catch (error) {
-
updateStepStatus(2, "error", error instanceof Error ? error.message : String(error));
throw error;
}
} catch (error) {
···
if (!token) return;
try {
-
const identityRes = await fetch(`/api/server/migrate/identity/sign?token=${encodeURIComponent(token)}`, {
-
method: "POST",
-
headers: { "Content-Type": "application/json" },
-
});
const identityData = await identityRes.text();
if (!identityRes.ok) {
-
throw new Error(identityData || "Failed to complete identity migration");
}
let data;
···
updateStepStatus(4, "completed");
} catch (error) {
-
updateStepStatus(4, "error", error instanceof Error ? error.message : String(error));
throw error;
}
} catch (error) {
console.error("Identity migration error:", error);
-
updateStepStatus(3, "error", error instanceof Error ? error.message : String(error));
}
};
···
case "completed":
return (
<div class="w-8 h-8 rounded-full bg-green-500 flex items-center justify-center">
-
<svg class="w-5 h-5 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
-
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7" />
</svg>
</div>
);
case "error":
return (
<div class="w-8 h-8 rounded-full bg-red-500 flex items-center justify-center">
-
<svg class="w-5 h-5 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
-
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
</svg>
</div>
);
···
};
const getStepClasses = (status: MigrationStep["status"]) => {
-
const baseClasses = "flex items-center space-x-3 p-4 rounded-lg transition-colors duration-200";
switch (status) {
case "pending":
return `${baseClasses} bg-gray-50 dark:bg-gray-800`;
···
<div key={step.name} class={getStepClasses(step.status)}>
{getStepIcon(step.status)}
<div class="flex-1">
-
<p class={`font-medium ${
-
step.status === "error" ? "text-red-900 dark:text-red-200" :
-
step.status === "completed" ? "text-green-900 dark:text-green-200" :
-
step.status === "in-progress" ? "text-blue-900 dark:text-blue-200" :
-
"text-gray-900 dark:text-gray-200"
-
}`}>{step.name}</p>
{step.error && (
-
<p class="text-sm text-red-600 dark:text-red-400 mt-1">{step.error}</p>
)}
</div>
</div>
···
{ name: "Finalize Migration", status: "pending" },
]);
+
const updateStepStatus = (
+
index: number,
+
status: MigrationStep["status"],
+
error?: string,
+
) => {
+
console.log(
+
`Updating step ${index} to ${status}${
+
error ? ` with error: ${error}` : ""
+
}`,
+
);
+
setSteps((prevSteps) =>
+
prevSteps.map((step, i) =>
+
i === index
+
? { ...step, status, error }
+
: i > index
? { ...step, status: "pending", error: undefined }
: step
+
)
+
);
};
const validateParams = () => {
···
handle: props.handle,
email: props.email,
hasPassword: !!props.password,
+
invite: props.invite,
});
+
if (!validateParams()) {
console.log("Parameter validation failed");
return;
}
+
+
startMigration().catch((error) => {
console.error("Unhandled migration error:", error);
+
updateStepStatus(
+
0,
+
"error",
+
error instanceof Error ? error.message : String(error),
+
);
});
}, []);
···
// Step 1: Create Account
updateStepStatus(0, "in-progress");
console.log("Starting account creation...");
+
try {
const createRes = await fetch("/api/server/migrate/create", {
method: "POST",
headers: { "Content-Type": "application/json" },
+
body: JSON.stringify({
+
service: props.service,
+
handle: props.handle,
+
password: props.password,
+
email: props.email,
+
...(props.invite ? { invite: props.invite } : {}),
}),
});
···
updateStepStatus(0, "completed");
} catch (error) {
+
updateStepStatus(
+
0,
+
"error",
+
error instanceof Error ? error.message : String(error),
+
);
throw error;
}
// Step 2: Migrate Data
updateStepStatus(1, "in-progress");
console.log("Starting data migration...");
+
try {
const dataRes = await fetch("/api/server/migrate/data", {
method: "POST",
headers: { "Content-Type": "application/json" },
});
+
console.log("Data migration response status:", dataRes.status);
const dataText = await dataRes.text();
console.log("Data migration response:", dataText);
···
updateStepStatus(1, "completed");
} catch (error) {
+
updateStepStatus(
+
1,
+
"error",
+
error instanceof Error ? error.message : String(error),
+
);
throw error;
}
// Step 3: Request Identity Migration
updateStepStatus(2, "in-progress");
console.log("Requesting identity migration...");
+
try {
const requestRes = await fetch("/api/server/migrate/identity/request", {
method: "POST",
···
console.log("Identity request response:", requestText);
if (!requestRes.ok) {
+
throw new Error(
+
requestText || "Failed to request identity migration",
+
);
}
try {
const jsonData = JSON.parse(requestText);
if (!jsonData.success) {
+
throw new Error(
+
jsonData.message || "Identity migration request failed",
+
);
}
console.log("Identity migration requested successfully");
} catch (e) {
console.error("Failed to parse identity request response:", e);
+
throw new Error(
+
"Invalid response from server during identity request",
+
);
}
updateStepStatus(2, "completed");
// Move to token input step
updateStepStatus(3, "in-progress");
} catch (error) {
+
updateStepStatus(
+
2,
+
"error",
+
error instanceof Error ? error.message : String(error),
+
);
throw error;
}
} catch (error) {
···
if (!token) return;
try {
+
const identityRes = await fetch(
+
`/api/server/migrate/identity/sign?token=${encodeURIComponent(token)}`,
+
{
+
method: "POST",
+
headers: { "Content-Type": "application/json" },
+
},
+
);
const identityData = await identityRes.text();
if (!identityRes.ok) {
+
throw new Error(
+
identityData || "Failed to complete identity migration",
+
);
}
let data;
···
updateStepStatus(4, "completed");
} catch (error) {
+
updateStepStatus(
+
4,
+
"error",
+
error instanceof Error ? error.message : String(error),
+
);
throw error;
}
} catch (error) {
console.error("Identity migration error:", error);
+
updateStepStatus(
+
3,
+
"error",
+
error instanceof Error ? error.message : String(error),
+
);
}
};
···
case "completed":
return (
<div class="w-8 h-8 rounded-full bg-green-500 flex items-center justify-center">
+
<svg
+
class="w-5 h-5 text-white"
+
fill="none"
+
stroke="currentColor"
+
viewBox="0 0 24 24"
+
>
+
<path
+
stroke-linecap="round"
+
stroke-linejoin="round"
+
stroke-width="2"
+
d="M5 13l4 4L19 7"
+
/>
</svg>
</div>
);
case "error":
return (
<div class="w-8 h-8 rounded-full bg-red-500 flex items-center justify-center">
+
<svg
+
class="w-5 h-5 text-white"
+
fill="none"
+
stroke="currentColor"
+
viewBox="0 0 24 24"
+
>
+
<path
+
stroke-linecap="round"
+
stroke-linejoin="round"
+
stroke-width="2"
+
d="M6 18L18 6M6 6l12 12"
+
/>
</svg>
</div>
);
···
};
const getStepClasses = (status: MigrationStep["status"]) => {
+
const baseClasses =
+
"flex items-center space-x-3 p-4 rounded-lg transition-colors duration-200";
switch (status) {
case "pending":
return `${baseClasses} bg-gray-50 dark:bg-gray-800`;
···
<div key={step.name} class={getStepClasses(step.status)}>
{getStepIcon(step.status)}
<div class="flex-1">
+
<p
+
class={`font-medium ${
+
step.status === "error"
+
? "text-red-900 dark:text-red-200"
+
: step.status === "completed"
+
? "text-green-900 dark:text-green-200"
+
: step.status === "in-progress"
+
? "text-blue-900 dark:text-blue-200"
+
: "text-gray-900 dark:text-gray-200"
+
}`}
+
>
+
{step.name}
+
</p>
{step.error && (
+
<p class="text-sm text-red-600 dark:text-red-400 mt-1">
+
{step.error}
+
</p>
)}
</div>
</div>
+39 -29
islands/MigrationSetup.tsx
···
export default function MigrationSetup(props: MigrationSetupProps) {
const [service, setService] = useState(props.service || "");
-
const [handlePrefix, setHandlePrefix] = useState(props.handle?.split(".")[0] || "");
const [selectedDomain, setSelectedDomain] = useState("");
const [email, setEmail] = useState(props.email || "");
const [password, setPassword] = useState("");
···
const checkServerDescription = async (serviceUrl: string) => {
try {
setIsLoading(true);
-
const response = await fetch(`${serviceUrl}/xrpc/com.atproto.server.describeServer`);
if (!response.ok) {
throw new Error("Failed to fetch server description");
}
···
}
} catch (err) {
console.error("Failed to check server description:", err);
-
setError("Failed to connect to server. Please check the URL and try again.");
setInviteRequired(false);
setAvailableDomains([]);
} finally {
···
const handleSubmit = (e: Event) => {
e.preventDefault();
-
if (!service || !handlePrefix || !email || !password) {
setError("Please fill in all required fields");
return;
···
}
const fullHandle = `${handlePrefix}${
-
availableDomains.length === 1
? availableDomains[0]
-
: availableDomains.length > 1
-
? selectedDomain
-
: ".example.com"
}`;
// Redirect to progress page with parameters
···
handle: fullHandle,
email,
password,
-
...(invite ? { invite } : {})
});
globalThis.location.href = `/migrate/progress?${params.toString()}`;
};
···
required
class="flex-1 rounded-l-md border-r-0 border-gray-300 focus:border-blue-500 focus:ring-blue-500 dark:border-gray-600 dark:bg-gray-700 dark:text-white"
/>
-
{availableDomains.length > 0 ? (
-
availableDomains.length === 1 ? (
<span class="inline-flex items-center px-3 rounded-r-md bg-white text-gray-500 dark:bg-gray-700 dark:text-gray-400">
-
{availableDomains[0]}
</span>
-
) : (
-
<select
-
value={selectedDomain}
-
onChange={(e) => setSelectedDomain(e.currentTarget.value)}
-
class="rounded-r-md border-l-0 border-gray-300 bg-gray-50 focus:border-blue-500 focus:ring-blue-500 dark:border-gray-600 dark:bg-gray-700 dark:text-white"
-
>
-
{availableDomains.map(domain => (
-
<option key={domain} value={domain}>{domain}</option>
-
))}
-
</select>
-
)
-
) : (
-
<span class="inline-flex items-center px-3 rounded-r-md bg-white text-gray-500 dark:bg-gray-700 dark:text-gray-400">
-
.example.com
-
</span>
-
)}
</div>
</div>
···
</button>
</form>
);
-
}
···
export default function MigrationSetup(props: MigrationSetupProps) {
const [service, setService] = useState(props.service || "");
+
const [handlePrefix, setHandlePrefix] = useState(
+
props.handle?.split(".")[0] || "",
+
);
const [selectedDomain, setSelectedDomain] = useState("");
const [email, setEmail] = useState(props.email || "");
const [password, setPassword] = useState("");
···
const checkServerDescription = async (serviceUrl: string) => {
try {
setIsLoading(true);
+
const response = await fetch(
+
`${serviceUrl}/xrpc/com.atproto.server.describeServer`,
+
);
if (!response.ok) {
throw new Error("Failed to fetch server description");
}
···
}
} catch (err) {
console.error("Failed to check server description:", err);
+
setError(
+
"Failed to connect to server. Please check the URL and try again.",
+
);
setInviteRequired(false);
setAvailableDomains([]);
} finally {
···
const handleSubmit = (e: Event) => {
e.preventDefault();
+
if (!service || !handlePrefix || !email || !password) {
setError("Please fill in all required fields");
return;
···
}
const fullHandle = `${handlePrefix}${
+
availableDomains.length === 1
? availableDomains[0]
+
: availableDomains.length > 1
+
? selectedDomain
+
: ".example.com"
}`;
// Redirect to progress page with parameters
···
handle: fullHandle,
email,
password,
+
...(invite ? { invite } : {}),
});
globalThis.location.href = `/migrate/progress?${params.toString()}`;
};
···
required
class="flex-1 rounded-l-md border-r-0 border-gray-300 focus:border-blue-500 focus:ring-blue-500 dark:border-gray-600 dark:bg-gray-700 dark:text-white"
/>
+
{availableDomains.length > 0
+
? (
+
availableDomains.length === 1
+
? (
+
<span class="inline-flex items-center px-3 rounded-r-md bg-white text-gray-500 dark:bg-gray-700 dark:text-gray-400">
+
{availableDomains[0]}
+
</span>
+
)
+
: (
+
<select
+
value={selectedDomain}
+
onChange={(e) => setSelectedDomain(e.currentTarget.value)}
+
class="rounded-r-md border-l-0 border-gray-300 bg-gray-50 focus:border-blue-500 focus:ring-blue-500 dark:border-gray-600 dark:bg-gray-700 dark:text-white"
+
>
+
{availableDomains.map((domain) => (
+
<option key={domain} value={domain}>{domain}</option>
+
))}
+
</select>
+
)
+
)
+
: (
<span class="inline-flex items-center px-3 rounded-r-md bg-white text-gray-500 dark:bg-gray-700 dark:text-gray-400">
+
.example.com
</span>
+
)}
</div>
</div>
···
</button>
</form>
);
+
}
+52 -33
islands/OAuthCallback.tsx
···
import { useEffect, useState } from "preact/hooks";
-
import { IS_BROWSER } from "$fresh/runtime.ts";
interface OAuthCallbackProps {
error?: string;
}
-
export default function OAuthCallback({ error: initialError }: OAuthCallbackProps) {
const [error, setError] = useState<string | null>(initialError || null);
-
const [message, setMessage] = useState<string>("Completing authentication...");
useEffect(() => {
if (!IS_BROWSER) return;
···
const cookies = document.cookie
.split(";")
.map((c) => c.trim())
-
.filter(c => c.length > 0);
console.log("Current cookies:", cookies);
const response = await fetch("/api/me", {
-
credentials: 'include', // Explicitly include credentials
headers: {
-
'Accept': 'application/json'
-
}
});
if (!response.ok) {
-
console.error("Profile API error:", response.status, response.statusText);
const text = await response.text();
console.error("Response body:", text);
throw new Error(`API returned ${response.status}`);
···
}
} catch (apiErr: unknown) {
console.error("API error during auth check:", apiErr);
-
setError(`Failed to verify authentication: ${apiErr instanceof Error ? apiErr.message : 'Unknown error'}`);
}
} catch (err: unknown) {
console.error("General error in OAuth callback:", err);
-
setError(`Failed to complete authentication: ${err instanceof Error ? err.message : 'Unknown error'}`);
}
};
···
return (
<div class="flex items-center justify-center py-16">
<div class="bg-white dark:bg-gray-800 rounded-lg shadow-sm p-8 max-w-md w-full text-center">
-
{error ? (
-
<div>
-
<h2 class="text-2xl font-bold text-red-500 mb-4">
-
Authentication Failed
-
</h2>
-
<p class="text-red-500 mb-6">{error}</p>
-
<a
-
href="/login"
-
class="px-4 py-2 bg-blue-500 dark:bg-blue-600 text-white rounded-md hover:bg-blue-600 dark:hover:bg-blue-700 transition-colors"
-
>
-
Try Again
-
</a>
-
</div>
-
) : (
-
<div>
-
<h2 class="text-2xl font-bold text-gray-800 dark:text-gray-200 mb-4">
-
Authentication in Progress
-
</h2>
-
<div class="flex justify-center mb-4">
-
<div class="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-500 dark:border-blue-400"></div>
</div>
-
<p class="text-gray-600 dark:text-gray-400">{message}</p>
-
</div>
-
)}
</div>
</div>
);
···
import { useEffect, useState } from "preact/hooks";
+
import { IS_BROWSER } from "fresh/runtime";
interface OAuthCallbackProps {
error?: string;
}
+
export default function OAuthCallback(
+
{ error: initialError }: OAuthCallbackProps,
+
) {
const [error, setError] = useState<string | null>(initialError || null);
+
const [message, setMessage] = useState<string>(
+
"Completing authentication...",
+
);
useEffect(() => {
if (!IS_BROWSER) return;
···
const cookies = document.cookie
.split(";")
.map((c) => c.trim())
+
.filter((c) => c.length > 0);
console.log("Current cookies:", cookies);
const response = await fetch("/api/me", {
+
credentials: "include", // Explicitly include credentials
headers: {
+
"Accept": "application/json",
+
},
});
if (!response.ok) {
+
console.error(
+
"Profile API error:",
+
response.status,
+
response.statusText,
+
);
const text = await response.text();
console.error("Response body:", text);
throw new Error(`API returned ${response.status}`);
···
}
} catch (apiErr: unknown) {
console.error("API error during auth check:", apiErr);
+
setError(
+
`Failed to verify authentication: ${
+
apiErr instanceof Error ? apiErr.message : "Unknown error"
+
}`,
+
);
}
} catch (err: unknown) {
console.error("General error in OAuth callback:", err);
+
setError(
+
`Failed to complete authentication: ${
+
err instanceof Error ? err.message : "Unknown error"
+
}`,
+
);
}
};
···
return (
<div class="flex items-center justify-center py-16">
<div class="bg-white dark:bg-gray-800 rounded-lg shadow-sm p-8 max-w-md w-full text-center">
+
{error
+
? (
+
<div>
+
<h2 class="text-2xl font-bold text-red-500 mb-4">
+
Authentication Failed
+
</h2>
+
<p class="text-red-500 mb-6">{error}</p>
+
<a
+
href="/login"
+
class="px-4 py-2 bg-blue-500 dark:bg-blue-600 text-white rounded-md hover:bg-blue-600 dark:hover:bg-blue-700 transition-colors"
+
>
+
Try Again
+
</a>
</div>
+
)
+
: (
+
<div>
+
<h2 class="text-2xl font-bold text-gray-800 dark:text-gray-200 mb-4">
+
Authentication in Progress
+
</h2>
+
<div class="flex justify-center mb-4">
+
<div class="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-500 dark:border-blue-400">
+
</div>
+
</div>
+
<p class="text-gray-600 dark:text-gray-400">{message}</p>
+
</div>
+
)}
</div>
</div>
);
+34 -20
islands/Ticket.tsx
···
import { useEffect, useState } from "preact/hooks";
-
import { IS_BROWSER } from "$fresh/runtime.ts";
interface User {
did: string;
···
throw new Error("Failed to fetch user profile");
}
const userData = await response.json();
-
setUser(userData ? {
-
did: userData.did,
-
handle: userData.handle
-
} : null);
} catch (error) {
console.error("Failed to fetch user:", error);
setUser(null);
···
return (
<div class="max-w-4xl mx-auto">
<div class="ticket mb-8 bg-white dark:bg-slate-800 border border-slate-200 dark:border-slate-700 p-6 relative before:absolute before:inset-[1px] before:bg-white dark:before:bg-slate-800 before:-z-10 after:absolute after:inset-0 after:bg-slate-200 dark:after:bg-slate-700 after:-z-20 [clip-path:polygon(0_0,20px_0,100%_0,100%_calc(100%-20px),calc(100%-20px)_100%,0_100%,0_calc(100%-20px),20px_100%)] after:[clip-path:polygon(0_0,20px_0,100%_0,100%_calc(100%-20px),calc(100%-20px)_100%,0_100%,0_calc(100%-20px),20px_100%)]">
-
<div class="boarding-label text-amber-500 dark:text-amber-400 font-mono font-bold tracking-wider text-sm mb-4">BOARDING PASS</div>
<div class="flex justify-between items-start mb-4">
<h3 class="text-2xl font-mono">WHAT IS AIRPORT?</h3>
-
<div class="text-sm font-mono text-gray-500 dark:text-gray-400">GATE A1</div>
</div>
<div class="passenger-info text-slate-600 dark:text-slate-300 font-mono text-sm mb-4">
PASSENGER: {(user?.handle || "UNKNOWN").toUpperCase()}
···
DESTINATION: NEW PDS
</div>
<p class="mb-4">
-
ATP Airport is your digital terminal for AT Protocol account actions. We help you smoothly
-
transfer your PDS account between different providers – no lost luggage, just a first-class
-
experience for your data's journey to its new home.
</p>
<p>
-
Think you might need to migrate in the future but your PDS might be hostile or offline?
-
No worries! You can head to the ticket booth and get a PLC key free of charge and use it
-
for account recovery in the future. You can also go to baggage claim (take the air shuttle
-
to terminal four) and get a downloadable backup of all your current PDS data in case that
-
were to happen.
</p>
</div>
<div class="ticket bg-white dark:bg-slate-800 border border-slate-200 dark:border-slate-700 p-6 relative before:absolute before:inset-[1px] before:bg-white dark:before:bg-slate-800 before:-z-10 after:absolute after:inset-0 after:bg-slate-200 dark:after:bg-slate-700 after:-z-20 [clip-path:polygon(0_0,20px_0,100%_0,100%_calc(100%-20px),calc(100%-20px)_100%,0_100%,0_calc(100%-20px),20px_100%)] after:[clip-path:polygon(0_0,20px_0,100%_0,100%_calc(100%-20px),calc(100%-20px)_100%,0_100%,0_calc(100%-20px),20px_100%)]">
-
<div class="boarding-label text-amber-500 dark:text-amber-400 font-mono font-bold tracking-wider text-sm mb-4">FLIGHT DETAILS</div>
<div class="flex justify-between items-start mb-4">
<h3 class="text-2xl font-mono">GET READY TO FLY</h3>
-
<div class="text-sm font-mono text-gray-500 dark:text-gray-400">SEAT: 1A</div>
</div>
<div class="passenger-info mb-4 text-slate-600 dark:text-slate-300 font-mono text-sm">
CLASS: FIRST CLASS MIGRATION
···
<li>Sit back while we handle your data transfer</li>
</ol>
<div class="mt-6 text-sm text-gray-600 dark:text-gray-400 border-t border-dashed pt-4 border-slate-200 dark:border-slate-700">
-
Coming from a Bluesky PDS? This is currently a ONE WAY TICKET because Bluesky doesn't support
-
transfers back yet. Although they claim they will support it in the future, assume you won't be
-
able to.
</div>
<div class="flight-info mt-6 flex items-center justify-between text-slate-600 dark:text-slate-300 font-mono text-sm">
<div>
···
import { useEffect, useState } from "preact/hooks";
+
import { IS_BROWSER } from "fresh/runtime";
interface User {
did: string;
···
throw new Error("Failed to fetch user profile");
}
const userData = await response.json();
+
setUser(
+
userData
+
? {
+
did: userData.did,
+
handle: userData.handle,
+
}
+
: null,
+
);
} catch (error) {
console.error("Failed to fetch user:", error);
setUser(null);
···
return (
<div class="max-w-4xl mx-auto">
<div class="ticket mb-8 bg-white dark:bg-slate-800 border border-slate-200 dark:border-slate-700 p-6 relative before:absolute before:inset-[1px] before:bg-white dark:before:bg-slate-800 before:-z-10 after:absolute after:inset-0 after:bg-slate-200 dark:after:bg-slate-700 after:-z-20 [clip-path:polygon(0_0,20px_0,100%_0,100%_calc(100%-20px),calc(100%-20px)_100%,0_100%,0_calc(100%-20px),20px_100%)] after:[clip-path:polygon(0_0,20px_0,100%_0,100%_calc(100%-20px),calc(100%-20px)_100%,0_100%,0_calc(100%-20px),20px_100%)]">
+
<div class="boarding-label text-amber-500 dark:text-amber-400 font-mono font-bold tracking-wider text-sm mb-4">
+
BOARDING PASS
+
</div>
<div class="flex justify-between items-start mb-4">
<h3 class="text-2xl font-mono">WHAT IS AIRPORT?</h3>
+
<div class="text-sm font-mono text-gray-500 dark:text-gray-400">
+
GATE A1
+
</div>
</div>
<div class="passenger-info text-slate-600 dark:text-slate-300 font-mono text-sm mb-4">
PASSENGER: {(user?.handle || "UNKNOWN").toUpperCase()}
···
DESTINATION: NEW PDS
</div>
<p class="mb-4">
+
ATP Airport is your digital terminal for AT Protocol account actions.
+
We help you smoothly transfer your PDS account between different
+
providers – no lost luggage, just a first-class experience for your
+
data's journey to its new home.
</p>
<p>
+
Think you might need to migrate in the future but your PDS might be
+
hostile or offline? No worries! You can head to the ticket booth and
+
get a PLC key free of charge and use it for account recovery in the
+
future. You can also go to baggage claim (take the air shuttle to
+
terminal four) and get a downloadable backup of all your current PDS
+
data in case that were to happen.
</p>
</div>
<div class="ticket bg-white dark:bg-slate-800 border border-slate-200 dark:border-slate-700 p-6 relative before:absolute before:inset-[1px] before:bg-white dark:before:bg-slate-800 before:-z-10 after:absolute after:inset-0 after:bg-slate-200 dark:after:bg-slate-700 after:-z-20 [clip-path:polygon(0_0,20px_0,100%_0,100%_calc(100%-20px),calc(100%-20px)_100%,0_100%,0_calc(100%-20px),20px_100%)] after:[clip-path:polygon(0_0,20px_0,100%_0,100%_calc(100%-20px),calc(100%-20px)_100%,0_100%,0_calc(100%-20px),20px_100%)]">
+
<div class="boarding-label text-amber-500 dark:text-amber-400 font-mono font-bold tracking-wider text-sm mb-4">
+
FLIGHT DETAILS
+
</div>
<div class="flex justify-between items-start mb-4">
<h3 class="text-2xl font-mono">GET READY TO FLY</h3>
+
<div class="text-sm font-mono text-gray-500 dark:text-gray-400">
+
SEAT: 1A
+
</div>
</div>
<div class="passenger-info mb-4 text-slate-600 dark:text-slate-300 font-mono text-sm">
CLASS: FIRST CLASS MIGRATION
···
<li>Sit back while we handle your data transfer</li>
</ol>
<div class="mt-6 text-sm text-gray-600 dark:text-gray-400 border-t border-dashed pt-4 border-slate-200 dark:border-slate-700">
+
Coming from a Bluesky PDS? This is currently a ONE WAY TICKET because
+
Bluesky doesn't support transfers back yet. Although they claim they
+
will support it in the future, assume you won't be able to.
</div>
<div class="flight-info mt-6 flex items-center justify-between text-slate-600 dark:text-slate-300 font-mono text-sm">
<div>
+20 -10
main.ts
···
-
/// <reference no-default-lib="true" />
-
/// <reference lib="dom" />
-
/// <reference lib="dom.iterable" />
-
/// <reference lib="dom.asynciterable" />
-
/// <reference lib="deno.ns" />
/// <reference lib="deno.unstable" />
-
import "$std/dotenv/load.ts";
-
import { start } from "$fresh/server.ts";
-
import manifest from "./fresh.gen.ts";
-
import config from "./fresh.config.ts";
-
await start(manifest, config);
···
/// <reference lib="deno.unstable" />
+
import { App, fsRoutes, staticFiles } from "fresh";
+
import { define, type State } from "./utils.ts";
+
+
export const app = new App<State>();
+
app.use(staticFiles());
+
// this can also be defined via a file. feel free to delete this!
+
const exampleLoggerMiddleware = define.middleware((ctx) => {
+
console.log(`${ctx.req.method} ${ctx.req.url}`);
+
return ctx.next();
+
});
+
app.use(exampleLoggerMiddleware);
+
+
await fsRoutes(app, {
+
loadIsland: (path) => import(`./islands/${path}`),
+
loadRoute: (path) => import(`./routes/${path}`),
+
});
+
+
if (import.meta.main) {
+
await app.listen();
+
}
-45
plugins/did.ts
···
-
import { Plugin } from "$fresh/server.ts";
-
import { Secp256k1Keypair, formatMultikey } from 'npm:@atproto/crypto'
-
-
export default {
-
name: 'did-json',
-
routes: [
-
{
-
path: '/.well-known/did.json',
-
handler: async () => {
-
const domain = Deno.env.get("PUBLIC_URL")?.split('://')[1] || 'localhost'
-
const privateKey = Deno.env.get("APPVIEW_K256_PRIVATE_KEY_HEX")
-
if (!privateKey) {
-
throw new Error("APPVIEW_K256_PRIVATE_KEY_HEX environment variable is required")
-
}
-
const keypair = await Secp256k1Keypair.import(privateKey)
-
const multikey = formatMultikey(keypair.jwtAlg, keypair.publicKeyBytes())
-
-
return Response.json({
-
'@context': ['https://www.w3.org/ns/did/v1'],
-
id: `did:web:${domain}`,
-
verificationMethod: [
-
{
-
id: `did:web:${domain}#atproto`,
-
type: 'Multikey',
-
controller: `did:web:${domain}`,
-
publicKeyMultibase: multikey,
-
},
-
],
-
service: [
-
{
-
id: '#swsh_appview',
-
type: 'SwshAppView',
-
serviceEndpoint: `https://${domain}`,
-
},
-
{
-
id: '#atproto_pds',
-
type: 'AtprotoPersonalDataServer',
-
serviceEndpoint: `https://${domain}`,
-
},
-
],
-
})
-
}
-
}
-
]
-
} as Plugin;
···
+59
plugins/id-resolver.ts
···
···
+
import { IdResolver } from "npm:@atproto/identity";
+
+
interface AtprotoData {
+
did: string;
+
signingKey: string;
+
handle: string;
+
pds: string;
+
}
+
+
const idResolver = createIdResolver();
+
export const resolver = createBidirectionalResolver(idResolver);
+
+
export function createIdResolver() {
+
return new IdResolver();
+
}
+
+
export interface BidirectionalResolver {
+
resolveDidToHandle(did: string): Promise<string>;
+
resolveDidsToHandles(dids: string[]): Promise<Record<string, string>>;
+
resolveDidToPdsUrl(did: string): Promise<string | undefined>;
+
}
+
+
export function createBidirectionalResolver(resolver: IdResolver) {
+
return {
+
async resolveDidToHandle(did: string): Promise<string> {
+
const didDoc = await resolver.did.resolveAtprotoData(did) as AtprotoData;
+
const resolvedHandle = await resolver.handle.resolve(didDoc.handle);
+
if (resolvedHandle === did) {
+
return didDoc.handle;
+
}
+
return did;
+
},
+
+
async resolveDidToPdsUrl(did: string): Promise<string | undefined> {
+
try {
+
const didDoc = await resolver.did.resolveAtprotoData(
+
did,
+
) as AtprotoData;
+
return didDoc.pds;
+
} catch (err) {
+
console.error("Error resolving PDS URL:", err);
+
return undefined;
+
}
+
},
+
+
async resolveDidsToHandles(
+
dids: string[],
+
): Promise<Record<string, string>> {
+
const didHandleMap: Record<string, string> = {};
+
const resolves = await Promise.all(
+
dids.map((did) => this.resolveDidToHandle(did).catch((_) => did)),
+
);
+
for (let i = 0; i < dids.length; i++) {
+
didHandleMap[dids[i]] = resolves[i];
+
}
+
return didHandleMap;
+
},
+
};
+
}
-22
plugins/session.ts
···
-
import { FreshContext, Plugin } from "$fresh/server.ts";
-
import { oauthClient } from "../auth/client.ts";
-
-
const plugin: Plugin = {
-
name: "session",
-
routes: [],
-
middlewares: [{
-
path: "/",
-
middleware: {
-
handler: async (req: Request, ctx: FreshContext) => {
-
let res = await ctx.next();
-
if (!oauthClient) {
-
console.warn("Missing required oauthClient in state");
-
return res;
-
}
-
return res;
-
},
-
},
-
}],
-
};
-
-
export default plugin;
···
-50
routes/_404.tsx
···
-
import { Head } from "$fresh/runtime.ts";
-
-
export default function Error404() {
-
return (
-
<>
-
<Head>
-
<title>404 - Flight Not Found</title>
-
<style dangerouslySetInnerHTML={{ __html: `
-
:root { --is-dark: 0; }
-
@media (prefers-color-scheme: dark) {
-
:root { --is-dark: 1; }
-
}
-
`}} />
-
</Head>
-
<div class="px-4">
-
<div class="max-w-screen-xl mx-auto flex flex-col items-center text-center">
-
<div class="relative mb-4">
-
<img
-
src="/icons/plane_bold.svg"
-
class="w-32 h-32 sm:w-35 sm:h-35 brightness-[0.1] dark:invert dark:filter-none"
-
alt="Plane icon"
-
/>
-
</div>
-
-
<div class="bg-white dark:bg-slate-900 airport-board p-8 sm:p-12 rounded-lg border border-slate-200 dark:border-white/10">
-
<h1 class="text-6xl sm:text-8xl md:text-9xl font-mono tracking-wider text-amber-500 dark:text-amber-400 font-bold mb-6">
-
404
-
</h1>
-
<div class="space-y-4">
-
<p class="text-2xl sm:text-3xl md:text-4xl font-mono text-slate-900 dark:text-white/90">
-
FLIGHT NOT FOUND
-
</p>
-
<p class="text-lg sm:text-xl text-slate-600 dark:text-white/70 max-w-2xl">
-
We couldn't locate the destination you're looking for. Please check your flight number and try again.
-
</p>
-
<div class="mt-8">
-
<a
-
href="/"
-
class="inline-flex items-center px-8 py-4 bg-amber-500 dark:bg-amber-400 text-slate-900 rounded-md font-bold text-lg hover:bg-amber-600 dark:hover:bg-amber-500 transition-colors duration-200"
-
>
-
Return to Terminal
-
</a>
-
</div>
-
</div>
-
</div>
-
</div>
-
</div>
-
</>
-
);
-
}
···
+1 -1
routes/_app.tsx
···
-
import { type PageProps } from "$fresh/server.ts";
import Header from "../islands/Header.tsx";
export default function App({ Component }: PageProps) {
···
+
import { type PageProps } from "fresh";
import Header from "../islands/Header.tsx";
export default function App({ Component }: PageProps) {
+51
routes/_error.tsx
···
···
+
import { PageProps, HttpError } from "fresh";
+
+
export default function ErrorPage(props: PageProps) {
+
const error = props.error; // Contains the thrown Error or HTTPError
+
if (error instanceof HttpError) {
+
const status = error.status; // HTTP status code
+
// Render a 404 not found page
+
if (status === 404) {
+
return (
+
<>
+
<div class="px-4">
+
<div class="max-w-screen-xl mx-auto flex flex-col items-center text-center">
+
<div class="relative mb-4">
+
<img
+
src="/icons/plane_bold.svg"
+
class="w-32 h-32 sm:w-35 sm:h-35 brightness-[0.1] dark:invert dark:filter-none"
+
alt="Plane icon"
+
/>
+
</div>
+
+
<div class="bg-white dark:bg-slate-900 airport-board p-8 sm:p-12 rounded-lg border border-slate-200 dark:border-white/10">
+
<h1 class="text-6xl sm:text-8xl md:text-9xl font-mono tracking-wider text-amber-500 dark:text-amber-400 font-bold mb-6">
+
404
+
</h1>
+
<div class="space-y-4">
+
<p class="text-2xl sm:text-3xl md:text-4xl font-mono text-slate-900 dark:text-white/90">
+
FLIGHT NOT FOUND
+
</p>
+
<p class="text-lg sm:text-xl text-slate-600 dark:text-white/70 max-w-2xl">
+
We couldn't locate the destination you're looking for. Please
+
check your flight number and try again.
+
</p>
+
<div class="mt-8">
+
<a
+
href="/"
+
class="inline-flex items-center px-8 py-4 bg-amber-500 dark:bg-amber-400 text-slate-900 rounded-md font-bold text-lg hover:bg-amber-600 dark:hover:bg-amber-500 transition-colors duration-200"
+
>
+
Return to Terminal
+
</a>
+
</div>
+
</div>
+
</div>
+
</div>
+
</div>
+
</>
+
)
+
}
+
}
+
+
return <h1>Oh no...</h1>;
+
}
+9 -8
routes/api/me.ts
···
-
import { Handlers } from "$fresh/server.ts";
import { getSessionAgent } from "../../auth/session.ts";
-
import { resolver } from "../../utils/id-resolver.ts";
-
export const handler: Handlers = {
-
async GET(req, ctx) {
-
const agent = await getSessionAgent(req, ctx);
if (!agent) {
return Response.json(null);
}
try {
const did = agent.assertDid;
-
const handle = await resolver.resolveDidToHandle(did)
-
return Response.json({ did, handle })
} catch (err) {
console.error({ err }, "Failed to fetch profile");
return Response.json(null);
}
},
-
};
···
import { getSessionAgent } from "../../auth/session.ts";
+
import { define } from "../../utils.ts";
+
import { resolver } from "../../plugins/id-resolver.ts";
+
export const handler = define.handlers({
+
async GET(ctx) {
+
const req = ctx.req;
+
const agent = await getSessionAgent(req);
if (!agent) {
return Response.json(null);
}
try {
const did = agent.assertDid;
+
const handle = await resolver.resolveDidToHandle(did);
+
return Response.json({ did, handle });
} catch (err) {
console.error({ err }, "Failed to fetch profile");
return Response.json(null);
}
},
+
});
+10 -9
routes/api/oauth/callback.ts
···
-
import { Handlers } from "$fresh/server.ts"
import { oauthClient } from "../../../auth/client.ts";
-
import { getSession } from "../../../auth/session.ts"
export const handler: Handlers = {
-
async GET(req) {
const url = new URL(req.url);
const params = url.searchParams;
···
const response = new Response(null, {
status: 302,
headers: new Headers({
-
'Location': '/login/callback'
-
})
});
// Create and save our client session
···
console.info(
`OAuth callback successful for DID: ${session.did}, redirecting to /login/callback`,
{
-
cookies: response.headers.get('Set-Cookie'),
-
}
);
return response;
···
params: Object.fromEntries(params.entries()),
}, "OAuth callback failed");
-
return Response.redirect('/login/callback?error=auth');
}
-
}
};
···
import { oauthClient } from "../../../auth/client.ts";
+
import { getSession } from "../../../auth/session.ts";
+
import { Handlers } from "fresh/compat";
export const handler: Handlers = {
+
async GET(ctx) {
+
const req = ctx.req;
const url = new URL(req.url);
const params = url.searchParams;
···
const response = new Response(null, {
status: 302,
headers: new Headers({
+
"Location": "/login/callback",
+
}),
});
// Create and save our client session
···
console.info(
`OAuth callback successful for DID: ${session.did}, redirecting to /login/callback`,
{
+
cookies: response.headers.get("Set-Cookie"),
+
},
);
return response;
···
params: Object.fromEntries(params.entries()),
}, "OAuth callback failed");
+
return Response.redirect("/login/callback?error=auth");
}
+
},
};
+5 -5
routes/api/oauth/initiate.ts
···
-
import type { Handlers } from "$fresh/server.ts";
import { isValidHandle } from 'npm:@atproto/syntax'
import { oauthClient } from "../../../auth/client.ts";
function isValidUrl(url: string): boolean {
try {
···
}
}
-
export const handler: Handlers = {
-
async POST(_req) {
-
const data = await _req.json()
const handle = data.handle
if (
typeof handle !== 'string' ||
···
return new Response("Couldn't initiate login", {status: 500})
}
},
-
};
···
import { isValidHandle } from 'npm:@atproto/syntax'
import { oauthClient } from "../../../auth/client.ts";
+
import { define } from "../../../utils.ts";
function isValidUrl(url: string): boolean {
try {
···
}
}
+
export const handler = define.handlers({
+
async POST(ctx) {
+
const data = await ctx.req.json()
const handle = data.handle
if (
typeof handle !== 'string' ||
···
return new Response("Couldn't initiate login", {status: 500})
}
},
+
});
+6 -4
routes/api/oauth/logout.ts
···
-
import { Handlers } from "$fresh/server.ts";
import { getSession } from "../../../auth/session.ts";
import { oauthClient } from "../../../auth/client.ts";
-
export const handler: Handlers = {
-
async POST(req) {
try {
const response = new Response(null, { status: 200 });
const session = await getSession(req, response);
···
return new Response("Logout failed", { status: 500 });
}
},
-
};
···
import { getSession } from "../../../auth/session.ts";
import { oauthClient } from "../../../auth/client.ts";
+
import { define } from "../../../utils.ts";
+
export const handler = define.handlers({
+
async POST(ctx) {
+
const req = ctx.req;
+
try {
const response = new Response(null, { status: 200 });
const session = await getSession(req, response);
···
return new Response("Logout failed", { status: 500 });
}
},
+
});
+19 -14
routes/api/server/describe.ts
···
-
import { FreshContext } from "$fresh/server.ts";
-
import { Agent } from "npm:@atproto/api";
import { getSessionAgent } from "../../../auth/session.ts";
/**
* Describe the server configuration and capabilities
*/
-
export const handler = async (_req: Request, _ctx: FreshContext): Promise<Response> => {
-
const url = new URL(_req.url)
-
const serviceUrl = url.searchParams.get("service")
-
const agent = serviceUrl ? new Agent({ service: serviceUrl }) : await getSessionAgent(_req, _ctx)
-
if (!agent) {
-
return new Response(
-
serviceUrl ? "Could not create agent." : "Unauthorized",
-
{status: serviceUrl ? 400 : 401}
-
)
}
-
const result = await agent.com.atproto.server.describeServer();
-
return Response.json(result);
-
}
···
+
+
import { Agent } from "@atproto/api";
import { getSessionAgent } from "../../../auth/session.ts";
+
import { define } from "../../../utils.ts";
/**
* Describe the server configuration and capabilities
*/
+
export const handler = define.handlers({
+
async GET(ctx) {
+
const url = new URL(ctx.req.url);
+
const serviceUrl = url.searchParams.get("service");
+
const agent = serviceUrl
+
? new Agent({ service: serviceUrl })
+
: await getSessionAgent(ctx.req);
+
if (!agent) {
+
return new Response(
+
serviceUrl ? "Could not create agent." : "Unauthorized",
+
{ status: serviceUrl ? 400 : 401 },
+
);
+
}
+
const result = await agent.com.atproto.server.describeServer();
+
return Response.json(result);
}
+
});
-137
routes/api/server/migrate.ts
···
-
import { getSessionAgent } from "../../../auth/session.ts";
-
import { Agent } from "npm:@atproto/api"
-
import { Handlers } from "$fresh/server.ts"
-
import { Secp256k1Keypair } from "npm:@atproto/crypto";
-
import * as ui8 from 'npm:uint8arrays'
-
-
export const handler: Handlers = {
-
async POST(_req, _ctx) {
-
const url = new URL(_req.url)
-
const serviceUrl = url.searchParams.get("service")
-
const newHandle = url.searchParams.get("handle")
-
const newPassword = url.searchParams.get("password")
-
const email = url.searchParams.get("email")
-
const inviteCode = url.searchParams.get("invite")
-
-
-
if (!serviceUrl || !newHandle || !newPassword || !email) {
-
return new Response("Missing params service, handle, password, or email", { status: 400 })
-
}
-
-
const oldAgent = await getSessionAgent(_req, _ctx)
-
const newAgent = new Agent({ service: serviceUrl })
-
-
if (!oldAgent) { return new Response("Unauthorized", {status: 401}) }
-
if (!newAgent) { return new Response("Could not create new agent", {status: 400}) }
-
const accountDid = oldAgent.assertDid
-
-
// Create account
-
// ------------------
-
-
const describeRes = await newAgent.com.atproto.server.describeServer()
-
const newServerDid = describeRes.data.did
-
const inviteRequired = describeRes.data.inviteCodeRequired ?? false
-
-
if (inviteRequired && !inviteCode) {
-
return new Response("Missing param invite code", { status: 400 })
-
}
-
-
const serviceJwtRes = await oldAgent.com.atproto.server.getServiceAuth({
-
aud: newServerDid,
-
lxm: 'com.atproto.server.createAccount',
-
})
-
const serviceJwt = serviceJwtRes.data.token
-
-
-
await newAgent.com.atproto.server.createAccount(
-
{
-
handle: newHandle,
-
email: email,
-
password: newPassword,
-
did: accountDid,
-
inviteCode: inviteCode ?? undefined
-
},
-
{
-
headers: { authorization: `Bearer ${serviceJwt}` },
-
encoding: 'application/json',
-
},
-
)
-
await newAgent.com.atproto.server.createSession({
-
identifier: newHandle,
-
password: newPassword,
-
})
-
-
// Migrate Data
-
// ------------------
-
-
const repoRes = await oldAgent.com.atproto.sync.getRepo({ did: accountDid })
-
await newAgent.com.atproto.repo.importRepo(repoRes.data, {
-
encoding: 'application/vnd.ipld.car',
-
})
-
-
let blobCursor: string | undefined = undefined
-
do {
-
const listedBlobs = await oldAgent.com.atproto.sync.listBlobs({
-
did: accountDid,
-
cursor: blobCursor,
-
})
-
for (const cid of listedBlobs.data.cids) {
-
const blobRes = await oldAgent.com.atproto.sync.getBlob({
-
did: accountDid,
-
cid,
-
})
-
await newAgent.com.atproto.repo.uploadBlob(blobRes.data, {
-
encoding: blobRes.headers['content-type'],
-
})
-
}
-
blobCursor = listedBlobs.data.cursor
-
} while (blobCursor)
-
-
const prefs = await oldAgent.app.bsky.actor.getPreferences()
-
await newAgent.app.bsky.actor.putPreferences(prefs.data)
-
-
// Migrate Identity
-
// ------------------
-
-
const recoveryKey = await Secp256k1Keypair.create({ exportable: true })
-
const privateKeyBytes = await recoveryKey.export()
-
const privateKey = ui8.toString(privateKeyBytes, 'hex')
-
-
await oldAgent.com.atproto.identity.requestPlcOperationSignature()
-
-
const getDidCredentials =
-
await newAgent.com.atproto.identity.getRecommendedDidCredentials()
-
const rotationKeys = getDidCredentials.data.rotationKeys ?? []
-
if (!rotationKeys) {
-
throw new Error('No rotation key provided')
-
}
-
const credentials = {
-
...getDidCredentials.data,
-
rotationKeys: [recoveryKey.did(), ...rotationKeys],
-
}
-
-
// @NOTE, this token will need to come from the email from the previous step
-
const TOKEN = ''
-
-
const plcOp = await oldAgent.com.atproto.identity.signPlcOperation({
-
token: TOKEN,
-
...credentials,
-
})
-
-
console.log(
-
`❗ Your private recovery key is: ${privateKey}. Please store this in a secure location! ❗`,
-
)
-
-
await newAgent.com.atproto.identity.submitPlcOperation({
-
operation: plcOp.data.operation,
-
})
-
-
// Finalize Migration
-
// ------------------
-
-
await newAgent.com.atproto.server.activateAccount()
-
await oldAgent.com.atproto.server.deactivateAccount({})
-
-
return new Response("Migration successful!", { status: 200 })
-
}
-
}
···
+63 -47
routes/api/server/migrate/create.ts
···
-
import { getSessionAgent, setMigrationSession } from "../../../../auth/session.ts";
-
import { Agent } from "npm:@atproto/api"
-
import { Handlers } from "$fresh/server.ts"
-
export const handler: Handlers = {
-
async POST(_req, _ctx) {
const res = new Response();
try {
-
const body = await _req.json();
const serviceUrl = body.service;
const newHandle = body.handle;
const newPassword = body.password;
···
const inviteCode = body.invite;
if (!serviceUrl || !newHandle || !newPassword || !email) {
-
return new Response("Missing params service, handle, password, or email", { status: 400 })
}
-
const oldAgent = await getSessionAgent(_req, _ctx)
-
const newAgent = new Agent({ service: serviceUrl })
-
if (!oldAgent) { return new Response("Unauthorized", {status: 401}) }
-
if (!newAgent) { return new Response("Could not create new agent", {status: 400}) }
-
const accountDid = oldAgent.assertDid
// Create account
-
const describeRes = await newAgent.com.atproto.server.describeServer()
-
const newServerDid = describeRes.data.did
-
const inviteRequired = describeRes.data.inviteCodeRequired ?? false
if (inviteRequired && !inviteCode) {
-
return new Response("Missing param invite code", { status: 400 })
}
const serviceJwtRes = await oldAgent.com.atproto.server.getServiceAuth({
aud: newServerDid,
-
lxm: 'com.atproto.server.createAccount',
-
})
-
const serviceJwt = serviceJwtRes.data.token
await newAgent.com.atproto.server.createAccount(
{
···
email: email,
password: newPassword,
did: accountDid,
-
inviteCode: inviteCode ?? undefined
},
{
headers: { authorization: `Bearer ${serviceJwt}` },
-
encoding: 'application/json',
},
-
)
-
// Create session and store it
const sessionRes = await newAgent.com.atproto.server.createSession({
identifier: newHandle,
password: newPassword,
-
})
// Store the migration session
-
await setMigrationSession(_req, res, {
did: sessionRes.data.did,
handle: newHandle,
service: serviceUrl,
-
password: newPassword
});
-
return new Response(JSON.stringify({
-
success: true,
-
message: "Account created successfully",
-
did: accountDid,
-
handle: newHandle
-
}), {
-
status: 200,
-
headers: {
-
"Content-Type": "application/json",
-
...Object.fromEntries(res.headers) // Include session cookie headers
-
}
-
})
} catch (error) {
console.error("Create account error:", error);
-
return new Response(JSON.stringify({
-
success: false,
-
message: error instanceof Error ? error.message : "Failed to create account"
-
}), {
-
status: 400,
-
headers: { "Content-Type": "application/json" }
-
});
}
-
}
-
}
···
+
import {
+
getSessionAgent,
+
setMigrationSession,
+
} from "../../../../auth/session.ts";
+
import { Agent } from "@atproto/api";
+
import { define } from "../../../../utils.ts";
+
export const handler = define.handlers({
+
async POST(ctx) {
const res = new Response();
try {
+
const body = await ctx.req.json();
const serviceUrl = body.service;
const newHandle = body.handle;
const newPassword = body.password;
···
const inviteCode = body.invite;
if (!serviceUrl || !newHandle || !newPassword || !email) {
+
return new Response(
+
"Missing params service, handle, password, or email",
+
{ status: 400 },
+
);
}
+
const oldAgent = await getSessionAgent(ctx.req);
+
const newAgent = new Agent({ service: serviceUrl });
+
if (!oldAgent) return new Response("Unauthorized", { status: 401 });
+
if (!newAgent) {
+
return new Response("Could not create new agent", { status: 400 });
+
}
+
const accountDid = oldAgent.assertDid;
// Create account
+
const describeRes = await newAgent.com.atproto.server.describeServer();
+
const newServerDid = describeRes.data.did;
+
const inviteRequired = describeRes.data.inviteCodeRequired ?? false;
if (inviteRequired && !inviteCode) {
+
return new Response("Missing param invite code", { status: 400 });
}
const serviceJwtRes = await oldAgent.com.atproto.server.getServiceAuth({
aud: newServerDid,
+
lxm: "com.atproto.server.createAccount",
+
});
+
const serviceJwt = serviceJwtRes.data.token;
await newAgent.com.atproto.server.createAccount(
{
···
email: email,
password: newPassword,
did: accountDid,
+
inviteCode: inviteCode ?? undefined,
},
{
headers: { authorization: `Bearer ${serviceJwt}` },
+
encoding: "application/json",
},
+
);
+
// Create session and store it
const sessionRes = await newAgent.com.atproto.server.createSession({
identifier: newHandle,
password: newPassword,
+
});
// Store the migration session
+
await setMigrationSession(ctx.req, res, {
did: sessionRes.data.did,
handle: newHandle,
service: serviceUrl,
+
password: newPassword,
});
+
return new Response(
+
JSON.stringify({
+
success: true,
+
message: "Account created successfully",
+
did: accountDid,
+
handle: newHandle,
+
}),
+
{
+
status: 200,
+
headers: {
+
"Content-Type": "application/json",
+
...Object.fromEntries(res.headers), // Include session cookie headers
+
},
+
},
+
);
} catch (error) {
console.error("Create account error:", error);
+
return new Response(
+
JSON.stringify({
+
success: false,
+
message: error instanceof Error
+
? error.message
+
: "Failed to create account",
+
}),
+
{
+
status: 400,
+
headers: { "Content-Type": "application/json" },
+
},
+
);
}
+
},
+
});
+77 -58
routes/api/server/migrate/data.ts
···
-
import { getSessionAgent, getMigrationSessionAgent } from "../../../../auth/session.ts";
-
import { Handlers } from "$fresh/server.ts"
-
export const handler: Handlers = {
-
async POST(_req, _ctx) {
const res = new Response();
try {
console.log("Data migration: Starting session retrieval");
-
const oldAgent = await getSessionAgent(_req, _ctx)
console.log("Data migration: Got old agent:", !!oldAgent);
-
// Log cookie information
-
const cookies = _req.headers.get('cookie');
console.log("Data migration: Cookies present:", !!cookies);
console.log("Data migration: Cookie header:", cookies);
-
-
const newAgent = await getMigrationSessionAgent(_req, res)
console.log("Data migration: Got new agent:", !!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" }
-
});
}
-
const accountDid = oldAgent.assertDid
// Migrate repo data
-
const repoRes = await oldAgent.com.atproto.sync.getRepo({ did: accountDid })
await newAgent.com.atproto.repo.importRepo(repoRes.data, {
-
encoding: 'application/vnd.ipld.car',
-
})
// Migrate blobs
-
let blobCursor: string | undefined = undefined
-
const migratedBlobs: string[] = []
do {
const listedBlobs = await oldAgent.com.atproto.sync.listBlobs({
did: accountDid,
cursor: blobCursor,
-
})
for (const cid of listedBlobs.data.cids) {
const blobRes = await oldAgent.com.atproto.sync.getBlob({
did: accountDid,
cid,
-
})
await newAgent.com.atproto.repo.uploadBlob(blobRes.data, {
-
encoding: blobRes.headers['content-type'],
-
})
-
migratedBlobs.push(cid)
}
-
blobCursor = listedBlobs.data.cursor
-
} while (blobCursor)
// Migrate preferences
-
const prefs = await oldAgent.app.bsky.actor.getPreferences()
-
await newAgent.app.bsky.actor.putPreferences(prefs.data)
-
return new Response(JSON.stringify({
-
success: true,
-
message: "Data migration completed successfully",
-
migratedBlobs: migratedBlobs
-
}), {
-
status: 200,
-
headers: {
-
"Content-Type": "application/json",
-
...Object.fromEntries(res.headers) // Include session cookie headers
-
}
-
})
} catch (error) {
console.error("Data migration error:", error);
-
return new Response(JSON.stringify({
-
success: false,
-
message: error instanceof Error ? error.message : "Failed to migrate data"
-
}), {
-
status: 400,
-
headers: { "Content-Type": "application/json" }
-
});
}
-
}
-
}
···
+
import { define } from "../../../../utils.ts";
+
import {
+
getMigrationSessionAgent,
+
getSessionAgent,
+
} from "../../../../auth/session.ts";
+
export const handler = define.handlers({
+
async POST(ctx) {
const res = new Response();
try {
console.log("Data migration: Starting session retrieval");
+
const oldAgent = await getSessionAgent(ctx.req);
console.log("Data migration: Got old agent:", !!oldAgent);
+
// Log cookie information
+
const cookies = ctx.req.headers.get("cookie");
console.log("Data migration: Cookies present:", !!cookies);
console.log("Data migration: Cookie header:", cookies);
+
+
const newAgent = await getMigrationSessionAgent(ctx.req, res);
console.log("Data migration: Got new agent:", !!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" },
+
},
+
);
}
+
const accountDid = oldAgent.assertDid;
// Migrate repo data
+
const repoRes = await oldAgent.com.atproto.sync.getRepo({
+
did: accountDid,
+
});
await newAgent.com.atproto.repo.importRepo(repoRes.data, {
+
encoding: "application/vnd.ipld.car",
+
});
// Migrate blobs
+
let blobCursor: string | undefined = undefined;
+
const migratedBlobs: string[] = [];
do {
const listedBlobs = await oldAgent.com.atproto.sync.listBlobs({
did: accountDid,
cursor: blobCursor,
+
});
for (const cid of listedBlobs.data.cids) {
const blobRes = await oldAgent.com.atproto.sync.getBlob({
did: accountDid,
cid,
+
});
await newAgent.com.atproto.repo.uploadBlob(blobRes.data, {
+
encoding: blobRes.headers["content-type"],
+
});
+
migratedBlobs.push(cid);
}
+
blobCursor = listedBlobs.data.cursor;
+
} while (blobCursor);
// Migrate preferences
+
const prefs = await oldAgent.app.bsky.actor.getPreferences();
+
await newAgent.app.bsky.actor.putPreferences(prefs.data);
+
return new Response(
+
JSON.stringify({
+
success: true,
+
message: "Data migration completed successfully",
+
migratedBlobs: migratedBlobs,
+
}),
+
{
+
status: 200,
+
headers: {
+
"Content-Type": "application/json",
+
...Object.fromEntries(res.headers), // Include session cookie headers
+
},
+
},
+
);
} catch (error) {
console.error("Data migration error:", error);
+
return new Response(
+
JSON.stringify({
+
success: false,
+
message: error instanceof Error
+
? error.message
+
: "Failed to migrate data",
+
}),
+
{
+
status: 400,
+
headers: { "Content-Type": "application/json" },
+
},
+
);
}
+
},
+
});
+44 -29
routes/api/server/migrate/finalize.ts
···
-
import { getSessionAgent, getMigrationSessionAgent } from "../../../../auth/session.ts";
-
import { Handlers } from "$fresh/server.ts"
-
export const handler: Handlers = {
-
async POST(_req, _ctx) {
const res = new Response();
try {
-
const oldAgent = await getSessionAgent(_req, _ctx)
-
const newAgent = await getMigrationSessionAgent(_req, res)
-
if (!oldAgent) { return new Response("Unauthorized", {status: 401}) }
-
if (!newAgent) { return new Response("Migration session not found or invalid", {status: 400}) }
// Activate new account and deactivate old account
-
await newAgent.com.atproto.server.activateAccount()
-
await oldAgent.com.atproto.server.deactivateAccount({})
-
return new Response(JSON.stringify({
-
success: true,
-
message: "Migration finalized successfully"
-
}), {
-
status: 200,
-
headers: {
-
"Content-Type": "application/json",
-
...Object.fromEntries(res.headers) // Include session cookie headers
-
}
-
})
} catch (error) {
console.error("Finalize error:", error);
-
return new Response(JSON.stringify({
-
success: false,
-
message: error instanceof Error ? error.message : "Failed to finalize migration"
-
}), {
-
status: 400,
-
headers: { "Content-Type": "application/json" }
-
});
}
-
}
-
}
···
+
import {
+
getMigrationSessionAgent,
+
getSessionAgent,
+
} from "../../../../auth/session.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);
+
if (!oldAgent) return new Response("Unauthorized", { status: 401 });
+
if (!newAgent) {
+
return new Response("Migration session not found or invalid", {
+
status: 400,
+
});
+
}
// Activate new account and deactivate old account
+
await newAgent.com.atproto.server.activateAccount();
+
await oldAgent.com.atproto.server.deactivateAccount({});
+
return new Response(
+
JSON.stringify({
+
success: true,
+
message: "Migration finalized successfully",
+
}),
+
{
+
status: 200,
+
headers: {
+
"Content-Type": "application/json",
+
...Object.fromEntries(res.headers), // Include session cookie headers
+
},
+
},
+
);
} catch (error) {
console.error("Finalize error:", error);
+
return new Response(
+
JSON.stringify({
+
success: false,
+
message: error instanceof Error
+
? error.message
+
: "Failed to finalize migration",
+
}),
+
{
+
status: 400,
+
headers: { "Content-Type": "application/json" },
+
},
+
);
}
+
},
+
});
+82 -63
routes/api/server/migrate/identity/request.ts
···
-
import { getSessionAgent, getMigrationSessionAgent, getMigrationSession } from "../../../../../auth/session.ts";
-
import { Handlers } from "$fresh/server.ts"
import { Secp256k1Keypair } from "npm:@atproto/crypto";
-
import * as ui8 from 'npm:uint8arrays'
-
export const handler: Handlers = {
-
async POST(_req, _ctx) {
const res = new Response();
try {
console.log("Starting identity migration request...");
-
const oldAgent = await getSessionAgent(_req, _ctx)
-
console.log("Got old agent:", {
hasDid: !!oldAgent?.did,
hasSession: !!oldAgent,
-
did: oldAgent?.did
});
-
const newAgent = await getMigrationSessionAgent(_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(_req, res);
session.recoveryKey = privateKey;
session.recoveryKeyDid = recoveryKey.did();
await session.save();
···
// 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: error instanceof Error ? (error as any).status : undefined,
-
response: error instanceof Error ? (error as any).response : undefined
});
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" }
});
}
-
}
-
}
···
+
import {
+
getMigrationSession,
+
getMigrationSessionAgent,
+
getSessionAgent,
+
} from "../../../../../auth/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();
···
// 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" },
+
},
+
);
}
+
},
+
});
+83 -55
routes/api/server/migrate/identity/sign.ts
···
-
import { getSessionAgent, getMigrationSessionAgent, getMigrationSession } from "../../../../../auth/session.ts";
-
import { Handlers } from "$fresh/server.ts"
-
export const handler: Handlers = {
-
async POST(_req, _ctx) {
const res = new Response();
try {
-
const url = new URL(_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(_req, _ctx)
-
const newAgent = await getMigrationSessionAgent(_req, res)
-
const session = await getMigrationSession(_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" }
-
});
}
-
}
-
}
···
+
import {
+
getMigrationSession,
+
getMigrationSessionAgent,
+
getSessionAgent,
+
} from "../../../../../auth/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" },
+
},
+
);
}
+
},
+
});
+3 -3
routes/login/callback.tsx
···
-
import { PageProps } from "$fresh/server.ts";
import OAuthCallback from "../../islands/OAuthCallback.tsx";
export default function Callback(props: PageProps) {
const error = props.url.searchParams.get("error");
-
return (
<div class="min-h-screen bg-gray-50 dark:bg-gray-900">
<OAuthCallback error={error || undefined} />
</div>
);
-
}
···
+
import { PageProps } from "fresh";
import OAuthCallback from "../../islands/OAuthCallback.tsx";
export default function Callback(props: PageProps) {
const error = props.url.searchParams.get("error");
+
return (
<div class="min-h-screen bg-gray-50 dark:bg-gray-900">
<OAuthCallback error={error || undefined} />
</div>
);
+
}
+12 -16
routes/login/index.tsx
···
-
import { PageProps } from "$fresh/server.ts"
-
import { Head } from "$fresh/runtime.ts"
-
import HandleInput from "../../islands/HandleInput.tsx"
export async function submitHandle(handle: string) {
-
const response = await fetch('/api/oauth/initiate', {
-
method: 'POST',
headers: {
-
'Content-Type': 'application/json',
},
body: JSON.stringify({ handle }),
-
})
if (!response.ok) {
-
const errorText = await response.text()
-
throw new Error(errorText || 'Login failed')
}
-
const data = await response.json()
// Add a small delay before redirecting for better UX
-
await new Promise((resolve) => setTimeout(resolve, 500))
// Redirect to ATProto OAuth flow
-
globalThis.location.href = data.redirectUrl
}
export default function Login(_props: PageProps) {
return (
<>
-
<Head>
-
<title>Login - Airport</title>
-
</Head>
<div className="flex flex-col gap-8">
<div className="bg-white dark:bg-gray-800 rounded-lg p-6 shadow-sm max-w-md mx-auto w-full">
<h2 className="text-xl font-semibold mb-4">Login with ATProto</h2>
···
</div>
</div>
</>
-
)
}
···
+
import { PageProps } from "fresh";
+
import HandleInput from "../../islands/HandleInput.tsx";
export async function submitHandle(handle: string) {
+
const response = await fetch("/api/oauth/initiate", {
+
method: "POST",
headers: {
+
"Content-Type": "application/json",
},
body: JSON.stringify({ handle }),
+
});
if (!response.ok) {
+
const errorText = await response.text();
+
throw new Error(errorText || "Login failed");
}
+
const data = await response.json();
// Add a small delay before redirecting for better UX
+
await new Promise((resolve) => setTimeout(resolve, 500));
// Redirect to ATProto OAuth flow
+
globalThis.location.href = data.redirectUrl;
}
export default function Login(_props: PageProps) {
return (
<>
<div className="flex flex-col gap-8">
<div className="bg-white dark:bg-gray-800 rounded-lg p-6 shadow-sm max-w-md mx-auto w-full">
<h2 className="text-xl font-semibold mb-4">Login with ATProto</h2>
···
</div>
</div>
</>
+
);
}
+4 -20
routes/migrate/index.tsx
···
-
import { PageProps, Handlers } from "$fresh/server.ts";
import MigrationSetup from "../../islands/MigrationSetup.tsx";
-
import { getSession } from "../../auth/session.ts";
-
-
export const handler: Handlers = {
-
async GET(req, ctx) {
-
const session = await getSession(req);
-
-
// If no session, redirect to login
-
if (!session?.did) {
-
const url = new URL(req.url);
-
const redirectUrl = `/login?redirect=${encodeURIComponent(url.pathname + url.search)}`;
-
return new Response(null, {
-
status: 302,
-
headers: { Location: redirectUrl },
-
});
-
}
-
return ctx.render();
-
},
-
};
export default function Migrate(props: PageProps) {
const service = props.url.searchParams.get("service");
···
return (
<div class="min-h-screen 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">Account Migration</h1>
<MigrationSetup
service={service}
handle={handle}
···
+
import { PageProps } from "fresh";
import MigrationSetup from "../../islands/MigrationSetup.tsx";
export default function Migrate(props: PageProps) {
const service = props.url.searchParams.get("service");
···
return (
<div class="min-h-screen 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">
+
Account Migration
+
</h1>
<MigrationSetup
service={service}
handle={handle}
+7 -22
routes/migrate/progress.tsx
···
-
import { PageProps, Handlers } from "$fresh/server.ts";
import MigrationProgress from "../../islands/MigrationProgress.tsx";
-
import { getSession } from "../../auth/session.ts";
-
-
export const handler: Handlers = {
-
async GET(req, ctx) {
-
const session = await getSession(req);
-
-
// If no session, redirect to login
-
if (!session?.did) {
-
const url = new URL(req.url);
-
const redirectUrl = `/login?redirect=${encodeURIComponent(url.pathname + url.search)}`;
-
return new Response(null, {
-
status: 302,
-
headers: { Location: redirectUrl },
-
});
-
}
-
return ctx.render();
-
},
-
};
export default function MigrateProgress(props: PageProps) {
const service = props.url.searchParams.get("service") || "";
···
<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">
-
Missing required parameters. Please return to the migration setup page.
</p>
</div>
</div>
···
return (
<div class="min-h-screen 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</h1>
<MigrationProgress
service={service}
handle={handle}
···
</div>
</div>
);
-
}
···
+
import { PageProps } from "fresh";
import MigrationProgress from "../../islands/MigrationProgress.tsx";
export default function MigrateProgress(props: PageProps) {
const service = props.url.searchParams.get("service") || "";
···
<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">
+
Missing required parameters. Please return to the migration setup
+
page.
</p>
</div>
</div>
···
return (
<div class="min-h-screen 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
+
</h1>
<MigrationProgress
service={service}
handle={handle}
···
</div>
</div>
);
+
}
-29
types.ts
···
-
import { OAuthClient } from 'jsr:@bigmoves/atproto-oauth-client'
-
import { Agent } from 'npm:@atproto/api'
-
-
export interface AppContext {
-
oauthClient: OAuthClient
-
logger: {
-
warn: (obj: Record<string, unknown>, msg: string) => void
-
}
-
}
-
-
export interface Env {
-
COOKIE_SECRET: string
-
NODE_ENV: string
-
}
-
-
declare global {
-
const env: Env
-
}
-
-
// Extend Fresh's State interface
-
declare module "$fresh/server.ts" {
-
interface State {
-
oauthClient: OAuthClient
-
logger: {
-
warn: (obj: Record<string, unknown>, msg: string) => void
-
}
-
agent?: Agent | null
-
}
-
}
···
+6
utils.ts
···
···
+
import { createDefine } from "fresh";
+
+
// deno-lint-ignore no-empty-interface
+
export interface State {}
+
+
export const define = createDefine<State>();
-57
utils/id-resolver.ts
···
-
import { IdResolver } from 'npm:@atproto/identity'
-
-
interface AtprotoData {
-
did: string
-
signingKey: string
-
handle: string
-
pds: string
-
}
-
-
const idResolver = createIdResolver()
-
export const resolver = createBidirectionalResolver(idResolver)
-
-
export function createIdResolver() {
-
return new IdResolver()
-
}
-
-
export interface BidirectionalResolver {
-
resolveDidToHandle(did: string): Promise<string>
-
resolveDidsToHandles(dids: string[]): Promise<Record<string, string>>
-
resolveDidToPdsUrl(did: string): Promise<string | undefined>
-
}
-
-
export function createBidirectionalResolver(resolver: IdResolver) {
-
return {
-
async resolveDidToHandle(did: string): Promise<string> {
-
const didDoc = await resolver.did.resolveAtprotoData(did) as AtprotoData
-
const resolvedHandle = await resolver.handle.resolve(didDoc.handle)
-
if (resolvedHandle === did) {
-
return didDoc.handle
-
}
-
return did
-
},
-
-
async resolveDidToPdsUrl(did: string): Promise<string | undefined> {
-
try {
-
const didDoc = await resolver.did.resolveAtprotoData(did) as AtprotoData
-
return didDoc.pds
-
} catch (err) {
-
console.error('Error resolving PDS URL:', err)
-
return undefined
-
}
-
},
-
-
async resolveDidsToHandles(
-
dids: string[],
-
): Promise<Record<string, string>> {
-
const didHandleMap: Record<string, string> = {}
-
const resolves = await Promise.all(
-
dids.map((did) => this.resolveDidToHandle(did).catch((_) => did)),
-
)
-
for (let i = 0; i < dids.length; i++) {
-
didHandleMap[dids[i]] = resolves[i]
-
}
-
return didHandleMap
-
},
-
}
-
}
···