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