- Migrated docs site to Hono + Cloudflare Worker that serves static assets
- Implemented basic OAuth with
@atproto/oauth-clientand stored sessions in Cloudflare KV
+383
-6
Diff
round #0
+17
-4
bun.lock
+17
-4
bun.lock
···
13
13
"name": "docs",
14
14
"version": "0.0.0",
15
15
"dependencies": {
16
+
"@atproto-labs/handle-resolver": "latest",
17
+
"@atproto/jwk-jose": "latest",
18
+
"@atproto/oauth-client": "latest",
19
+
"hono": "latest",
16
20
"react": "latest",
17
21
"react-dom": "latest",
18
22
"vocs": "latest",
19
23
},
20
24
"devDependencies": {
25
+
"@cloudflare/workers-types": "latest",
21
26
"@types/react": "latest",
22
27
"typescript": "latest",
23
28
},
···
90
95
91
96
"@atproto/lexicon": ["@atproto/lexicon@0.6.1", "", { "dependencies": { "@atproto/common-web": "^0.4.13", "@atproto/syntax": "^0.4.3", "iso-datestring-validator": "^2.2.2", "multiformats": "^9.9.0", "zod": "^3.23.8" } }, "sha512-/vI1kVlY50Si+5MXpvOucelnYwb0UJ6Qto5mCp+7Q5C+Jtp+SoSykAPVvjVtTnQUH2vrKOFOwpb3C375vSKzXw=="],
92
97
93
-
"@atproto/oauth-client": ["@atproto/oauth-client@0.5.14", "", { "dependencies": { "@atproto-labs/did-resolver": "0.2.6", "@atproto-labs/fetch": "0.2.3", "@atproto-labs/handle-resolver": "0.3.6", "@atproto-labs/identity-resolver": "0.3.6", "@atproto-labs/simple-store": "0.3.0", "@atproto-labs/simple-store-memory": "0.1.4", "@atproto/did": "0.3.0", "@atproto/jwk": "0.6.0", "@atproto/oauth-types": "0.6.2", "@atproto/xrpc": "0.7.7", "core-js": "^3", "multiformats": "^9.9.0", "zod": "^3.23.8" } }, "sha512-sPH+vcdq9maTEAhJI0HzmFcFAMrkCS19np+RUssNkX6kS8Xr3OYr57tvYRCbkcnIyYTfYcxKQgpwHKx3RVEaYw=="],
98
+
"@atproto/oauth-client": ["@atproto/oauth-client@0.6.0", "", { "dependencies": { "@atproto-labs/did-resolver": "^0.2.6", "@atproto-labs/fetch": "^0.2.3", "@atproto-labs/handle-resolver": "^0.3.6", "@atproto-labs/identity-resolver": "^0.3.6", "@atproto-labs/simple-store": "^0.3.0", "@atproto-labs/simple-store-memory": "^0.1.4", "@atproto/did": "^0.3.0", "@atproto/jwk": "^0.6.0", "@atproto/oauth-types": "^0.6.3", "@atproto/xrpc": "^0.7.7", "core-js": "^3", "multiformats": "^9.9.0", "zod": "^3.23.8" } }, "sha512-F7ZTKzFptXgyihMkd7QTdRSkrh4XqrS+qTw+V81k5Q6Bh3MB1L3ypvfSJ6v7SSUJa6XxoZYJTCahHC1e+ndE6Q=="],
94
99
95
100
"@atproto/oauth-client-node": ["@atproto/oauth-client-node@0.3.16", "", { "dependencies": { "@atproto-labs/did-resolver": "0.2.6", "@atproto-labs/handle-resolver-node": "0.1.25", "@atproto-labs/simple-store": "0.3.0", "@atproto/did": "0.3.0", "@atproto/jwk": "0.6.0", "@atproto/jwk-jose": "0.1.11", "@atproto/jwk-webcrypto": "0.2.0", "@atproto/oauth-client": "0.5.14", "@atproto/oauth-types": "0.6.2" } }, "sha512-2dooMzxAkiQ4MkOAZlEQ3iwbB9SEovrbIKMNuBbVCLQYORVNxe20tMdjs3lvhrzdpzvaHLlQnJJhw5dA9VELFw=="],
96
101
97
-
"@atproto/oauth-types": ["@atproto/oauth-types@0.6.2", "", { "dependencies": { "@atproto/did": "0.3.0", "@atproto/jwk": "0.6.0", "zod": "^3.23.8" } }, "sha512-2cuboM4RQBCYR8NQC5uGRkW6KgCgKyq/B5/+tnMmWZYtZGVUQvsUWQHK/ZiMCnVXbcDNtc/RIEJQJDZ8FXMoxg=="],
102
+
"@atproto/oauth-types": ["@atproto/oauth-types@0.6.3", "", { "dependencies": { "@atproto/did": "^0.3.0", "@atproto/jwk": "^0.6.0", "zod": "^3.23.8" } }, "sha512-jdKuoPknJuh/WjI+mYk7agSbx9mNVMbS6Dr3k1z2YMY2oRiCQjxYBuo4MLKATbxj05nMQaZRWlHRUazoAu5Cng=="],
98
103
99
104
"@atproto/syntax": ["@atproto/syntax@0.4.3", "", { "dependencies": { "tslib": "^2.8.1" } }, "sha512-YoZUz40YAJr5nPwvCDWgodEOlt5IftZqPJvA0JDWjuZKD8yXddTwSzXSaKQAzGOpuM+/A3uXRtPzJJqlScc+iA=="],
100
105
···
175
180
"@clack/core": ["@clack/core@1.0.0", "", { "dependencies": { "picocolors": "^1.0.0", "sisteransi": "^1.0.5" } }, "sha512-Orf9Ltr5NeiEuVJS8Rk2XTw3IxNC2Bic3ash7GgYeA8LJ/zmSNpSQ/m5UAhe03lA6KFgklzZ5KTHs4OAMA/SAQ=="],
176
181
177
182
"@clack/prompts": ["@clack/prompts@1.0.0", "", { "dependencies": { "@clack/core": "1.0.0", "picocolors": "^1.0.0", "sisteransi": "^1.0.5" } }, "sha512-rWPXg9UaCFqErJVQ+MecOaWsozjaxol4yjnmYcGNipAWzdaWa2x+VJmKfGq7L0APwBohQOYdHC+9RO4qRXej+A=="],
183
+
184
+
"@cloudflare/workers-types": ["@cloudflare/workers-types@4.20260228.0", "", {}, "sha512-9LfRg93ncQq6Oc4MFpqGSs+PmPhqWvg8TspXwbiYNR201IhXB4WqHR/aTSudPI0ujsf/NLc8E9fF3C+aA2g8KQ=="],
178
185
179
186
"@emotion/hash": ["@emotion/hash@0.9.2", "", {}, "sha512-MyqliTZGuOm3+5ZRSaaBGP3USLw6+EGykkwZns2EPC5g8jJ4z9OrdZY9apkl3+UP9+sdz76YYkwCKP5gh8iY3g=="],
180
187
···
596
603
597
604
"@types/node": ["@types/node@20.19.30", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-WJtwWJu7UdlvzEAUm484QNg5eAoq5QR08KDNx7g45Usrs2NtOPiX8ugDqmKdXkyL03rBqU5dYNYVQetEpBHq2g=="],
598
605
599
-
"@types/react": ["@types/react@19.2.10", "", { "dependencies": { "csstype": "^3.2.2" } }, "sha512-WPigyYuGhgZ/cTPRXB2EwUw+XvsRA3GqHlsP4qteqrnnjDrApbS7MxcGr/hke5iUoeB7E/gQtrs9I37zAJ0Vjw=="],
606
+
"@types/react": ["@types/react@19.2.14", "", { "dependencies": { "csstype": "^3.2.2" } }, "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w=="],
600
607
601
608
"@types/trusted-types": ["@types/trusted-types@2.0.7", "", {}, "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw=="],
602
609
···
954
961
955
962
"hastscript": ["hastscript@8.0.0", "", { "dependencies": { "@types/hast": "^3.0.0", "comma-separated-tokens": "^2.0.0", "hast-util-parse-selector": "^4.0.0", "property-information": "^6.0.0", "space-separated-tokens": "^2.0.0" } }, "sha512-dMOtzCEd3ABUeSIISmrETiKuyydk1w0pa+gE/uormcTpSYuaNJPbX1NU3JLyscSLjwAQM8bWMhhIlnCqnRvDTw=="],
956
963
957
-
"hono": ["hono@4.11.7", "", {}, "sha512-l7qMiNee7t82bH3SeyUCt9UF15EVmaBvsppY2zQtrbIhl/yzBTny+YUxsVjSjQ6gaqaeVtZmGocom8TzBlA4Yw=="],
964
+
"hono": ["hono@4.12.1", "", {}, "sha512-hi9afu8g0lfJVLolxElAZGANCTTl6bewIdsRNhaywfP9K8BPf++F2z6OLrYGIinUwpRKzbZHMhPwvc0ZEpAwGw=="],
958
965
959
966
"html-void-elements": ["html-void-elements@3.0.0", "", {}, "sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg=="],
960
967
···
1532
1539
1533
1540
"zwitch": ["zwitch@2.0.4", "", {}, "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A=="],
1534
1541
1542
+
"@atproto/oauth-client-node/@atproto/oauth-client": ["@atproto/oauth-client@0.5.14", "", { "dependencies": { "@atproto-labs/did-resolver": "0.2.6", "@atproto-labs/fetch": "0.2.3", "@atproto-labs/handle-resolver": "0.3.6", "@atproto-labs/identity-resolver": "0.3.6", "@atproto-labs/simple-store": "0.3.0", "@atproto-labs/simple-store-memory": "0.1.4", "@atproto/did": "0.3.0", "@atproto/jwk": "0.6.0", "@atproto/oauth-types": "0.6.2", "@atproto/xrpc": "0.7.7", "core-js": "^3", "multiformats": "^9.9.0", "zod": "^3.23.8" } }, "sha512-sPH+vcdq9maTEAhJI0HzmFcFAMrkCS19np+RUssNkX6kS8Xr3OYr57tvYRCbkcnIyYTfYcxKQgpwHKx3RVEaYw=="],
1543
+
1544
+
"@atproto/oauth-client-node/@atproto/oauth-types": ["@atproto/oauth-types@0.6.2", "", { "dependencies": { "@atproto/did": "0.3.0", "@atproto/jwk": "0.6.0", "zod": "^3.23.8" } }, "sha512-2cuboM4RQBCYR8NQC5uGRkW6KgCgKyq/B5/+tnMmWZYtZGVUQvsUWQHK/ZiMCnVXbcDNtc/RIEJQJDZ8FXMoxg=="],
1545
+
1535
1546
"@babel/helper-compilation-targets/lru-cache": ["lru-cache@5.1.1", "", { "dependencies": { "yallist": "^3.0.2" } }, "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w=="],
1536
1547
1537
1548
"@chevrotain/cst-dts-gen/lodash-es": ["lodash-es@4.17.21", "", {}, "sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw=="],
···
1605
1616
"send/debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="],
1606
1617
1607
1618
"vite/fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="],
1619
+
1620
+
"vocs/hono": ["hono@4.11.7", "", {}, "sha512-l7qMiNee7t82bH3SeyUCt9UF15EVmaBvsppY2zQtrbIhl/yzBTny+YUxsVjSjQ6gaqaeVtZmGocom8TzBlA4Yw=="],
1608
1621
1609
1622
"@radix-ui/react-label/@radix-ui/react-primitive/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.4", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-Jl+bCv8HxKnlTLVrcDE8zTMJ09R9/ukw4qBs/oZClOfoQk/cOTbDn+NceXfV7j09YPVQUryJPHurafcSg6EVKA=="],
1610
1623
+7
-1
docs/package.json
+7
-1
docs/package.json
···
5
5
"type": "module",
6
6
"scripts": {
7
7
"dev": "vocs dev",
8
+
"dev:api": "wrangler dev",
8
9
"build": "vocs build && bun inject-og-tags.ts",
9
-
"deploy": "bun run build && sequoia inject && bunx wrangler pages deploy docs/dist",
10
+
"deploy": "bun run build && sequoia inject && wrangler deploy",
10
11
"preview": "vocs preview"
11
12
},
12
13
"dependencies": {
14
+
"@atproto/oauth-client": "latest",
15
+
"@atproto/jwk-jose": "latest",
16
+
"@atproto-labs/handle-resolver": "latest",
17
+
"hono": "latest",
13
18
"react": "latest",
14
19
"react-dom": "latest",
15
20
"vocs": "latest"
16
21
},
17
22
"devDependencies": {
23
+
"@cloudflare/workers-types": "latest",
18
24
"@types/react": "latest",
19
25
"typescript": "latest"
20
26
}
+22
docs/src/index.ts
+22
docs/src/index.ts
···
1
+
import { Hono } from "hono";
2
+
import auth from "./routes/auth";
3
+
4
+
type Bindings = {
5
+
ASSETS: Fetcher;
6
+
SEQUOIA_SESSIONS: KVNamespace;
7
+
CLIENT_URL: string;
8
+
};
9
+
10
+
const app = new Hono<{ Bindings: Bindings }>();
11
+
12
+
app.route("/oauth", auth);
13
+
14
+
app.get("/api/health", (c) => {
15
+
return c.json({ status: "ok" });
16
+
});
17
+
18
+
app.all("*", (c) => {
19
+
return c.env.ASSETS.fetch(c.req.raw);
20
+
});
21
+
22
+
export default app;
+82
docs/src/lib/kv-stores.ts
+82
docs/src/lib/kv-stores.ts
···
1
+
import { JoseKey } from "@atproto/jwk-jose";
2
+
import type {
3
+
Key,
4
+
InternalStateData,
5
+
SessionStore,
6
+
StateStore,
7
+
} from "@atproto/oauth-client";
8
+
9
+
type SerializedStateData = Omit<InternalStateData, "dpopKey"> & {
10
+
dpopJwk: Record<string, unknown>;
11
+
};
12
+
13
+
type SerializedSession = Omit<
14
+
Parameters<SessionStore["set"]>[1],
15
+
"dpopKey"
16
+
> & {
17
+
dpopJwk: Record<string, unknown>;
18
+
};
19
+
20
+
function serializeKey(key: Key): Record<string, unknown> {
21
+
const jwk = key.privateJwk;
22
+
if (!jwk) throw new Error("Private DPoP JWK is missing");
23
+
return jwk as Record<string, unknown>;
24
+
}
25
+
26
+
async function deserializeKey(jwk: Record<string, unknown>): Promise<Key> {
27
+
return JoseKey.fromJWK(jwk);
28
+
}
29
+
30
+
export function createStateStore(
31
+
kv: KVNamespace,
32
+
ttl = 600,
33
+
): StateStore {
34
+
return {
35
+
async set(key, { dpopKey, ...rest }) {
36
+
const data: SerializedStateData = {
37
+
...rest,
38
+
dpopJwk: serializeKey(dpopKey),
39
+
};
40
+
await kv.put(`oauth_state:${key}`, JSON.stringify(data), {
41
+
expirationTtl: ttl,
42
+
});
43
+
},
44
+
async get(key) {
45
+
const raw = await kv.get(`oauth_state:${key}`);
46
+
if (!raw) return undefined;
47
+
const { dpopJwk, ...rest }: SerializedStateData = JSON.parse(raw);
48
+
const dpopKey = await deserializeKey(dpopJwk);
49
+
return { ...rest, dpopKey };
50
+
},
51
+
async del(key) {
52
+
await kv.delete(`oauth_state:${key}`);
53
+
},
54
+
};
55
+
}
56
+
57
+
export function createSessionStore(
58
+
kv: KVNamespace,
59
+
ttl = 60 * 60 * 24 * 14,
60
+
): SessionStore {
61
+
return {
62
+
async set(sub, { dpopKey, ...rest }) {
63
+
const data: SerializedSession = {
64
+
...rest,
65
+
dpopJwk: serializeKey(dpopKey),
66
+
};
67
+
await kv.put(`oauth_session:${sub}`, JSON.stringify(data), {
68
+
expirationTtl: ttl,
69
+
});
70
+
},
71
+
async get(sub) {
72
+
const raw = await kv.get(`oauth_session:${sub}`);
73
+
if (!raw) return undefined;
74
+
const { dpopJwk, ...rest }: SerializedSession = JSON.parse(raw);
75
+
const dpopKey = await deserializeKey(dpopJwk);
76
+
return { ...rest, dpopKey };
77
+
},
78
+
async del(sub) {
79
+
await kv.delete(`oauth_session:${sub}`);
80
+
},
81
+
};
82
+
}
+43
docs/src/lib/oauth-client.ts
+43
docs/src/lib/oauth-client.ts
···
1
+
import { JoseKey } from "@atproto/jwk-jose";
2
+
import { OAuthClient } from "@atproto/oauth-client";
3
+
import { AtprotoDohHandleResolver } from "@atproto-labs/handle-resolver";
4
+
import { createStateStore, createSessionStore } from "./kv-stores";
5
+
6
+
export function createOAuthClient(kv: KVNamespace, clientUrl: string) {
7
+
const clientId = `${clientUrl}/oauth/client-metadata.json`;
8
+
const redirectUri = `${clientUrl}/oauth/callback`;
9
+
10
+
return new OAuthClient({
11
+
responseMode: "query",
12
+
handleResolver: new AtprotoDohHandleResolver({
13
+
dohEndpoint: "https://cloudflare-dns.com/dns-query",
14
+
}),
15
+
clientMetadata: {
16
+
client_id: clientId,
17
+
client_name: "Sequoia",
18
+
client_uri: clientUrl,
19
+
redirect_uris: [redirectUri],
20
+
grant_types: ["authorization_code", "refresh_token"],
21
+
response_types: ["code"],
22
+
scope: "atproto transition:generic",
23
+
token_endpoint_auth_method: "none",
24
+
application_type: "web",
25
+
dpop_bound_access_tokens: true,
26
+
},
27
+
runtimeImplementation: {
28
+
createKey: (algs: string[]) => JoseKey.generate(algs),
29
+
getRandomValues: (length: number) =>
30
+
crypto.getRandomValues(new Uint8Array(length)),
31
+
digest: async (data: Uint8Array, { name }: { name: string }) => {
32
+
const buf = await crypto.subtle.digest(
33
+
name.replace("sha", "SHA-"),
34
+
new Uint8Array(data),
35
+
);
36
+
return new Uint8Array(buf);
37
+
},
38
+
requestLock: <T>(_name: string, fn: () => T | PromiseLike<T>) => fn(),
39
+
},
40
+
stateStore: createStateStore(kv),
41
+
sessionStore: createSessionStore(kv),
42
+
});
43
+
}
+47
docs/src/lib/session.ts
+47
docs/src/lib/session.ts
···
1
+
import type { Context } from "hono";
2
+
3
+
const SESSION_COOKIE_NAME = "session_id";
4
+
const SESSION_TTL = 60 * 60 * 24 * 14; // 14 days in seconds
5
+
6
+
/**
7
+
* Get DID from session cookie
8
+
*/
9
+
export function getSessionDid(c: Context): string | null {
10
+
const cookie = c.req.header("Cookie");
11
+
if (!cookie) return null;
12
+
13
+
const match = cookie.match(new RegExp(`${SESSION_COOKIE_NAME}=([^;]+)`));
14
+
return match ? decodeURIComponent(match[1]) : null;
15
+
}
16
+
17
+
/**
18
+
* Set session cookie with the user's DID
19
+
*/
20
+
export function setSessionCookie(
21
+
c: Context,
22
+
did: string,
23
+
clientUrl: string,
24
+
): void {
25
+
const isLocalhost = clientUrl.includes("localhost");
26
+
const domain = isLocalhost ? "" : "; Domain=.sequoia.pub";
27
+
const secure = isLocalhost ? "" : "; Secure";
28
+
29
+
c.header(
30
+
"Set-Cookie",
31
+
`${SESSION_COOKIE_NAME}=${encodeURIComponent(did)}; HttpOnly; SameSite=Lax; Path=/${domain}${secure}; Max-Age=${SESSION_TTL}`,
32
+
);
33
+
}
34
+
35
+
/**
36
+
* Clear session cookie
37
+
*/
38
+
export function clearSessionCookie(c: Context, clientUrl: string): void {
39
+
const isLocalhost = clientUrl.includes("localhost");
40
+
const domain = isLocalhost ? "" : "; Domain=.sequoia.pub";
41
+
const secure = isLocalhost ? "" : "; Secure";
42
+
43
+
c.header(
44
+
"Set-Cookie",
45
+
`${SESSION_COOKIE_NAME}=; HttpOnly; SameSite=Lax; Path=/${domain}${secure}; Max-Age=0`,
46
+
);
47
+
}
+144
docs/src/routes/auth.ts
+144
docs/src/routes/auth.ts
···
1
+
import { Hono } from "hono";
2
+
import { createOAuthClient } from "../lib/oauth-client";
3
+
import {
4
+
getSessionDid,
5
+
setSessionCookie,
6
+
clearSessionCookie,
7
+
} from "../lib/session";
8
+
9
+
interface Env {
10
+
SEQUOIA_SESSIONS: KVNamespace;
11
+
CLIENT_URL: string;
12
+
}
13
+
14
+
const auth = new Hono<{ Bindings: Env }>();
15
+
16
+
// OAuth client metadata endpoint
17
+
auth.get("/client-metadata.json", (c) => {
18
+
const clientId = `${c.env.CLIENT_URL}/oauth/client-metadata.json`;
19
+
const redirectUri = `${c.env.CLIENT_URL}/oauth/callback`;
20
+
21
+
return c.json({
22
+
client_id: clientId,
23
+
client_name: "Sequoia",
24
+
client_uri: c.env.CLIENT_URL,
25
+
redirect_uris: [redirectUri],
26
+
grant_types: ["authorization_code", "refresh_token"],
27
+
response_types: ["code"],
28
+
scope: "atproto transition:generic",
29
+
token_endpoint_auth_method: "none",
30
+
application_type: "web",
31
+
dpop_bound_access_tokens: true,
32
+
});
33
+
});
34
+
35
+
// Start OAuth login flow
36
+
auth.get("/login", async (c) => {
37
+
try {
38
+
const handle = c.req.query("handle");
39
+
if (!handle) {
40
+
return c.redirect(`${c.env.CLIENT_URL}/?error=missing_handle`);
41
+
}
42
+
43
+
const client = createOAuthClient(c.env.SEQUOIA_SESSIONS, c.env.CLIENT_URL);
44
+
const authUrl = await client.authorize(handle, {
45
+
scope: "atproto transition:generic",
46
+
});
47
+
48
+
return c.redirect(authUrl.toString());
49
+
} catch (error) {
50
+
console.error("Login error:", error);
51
+
return c.redirect(`${c.env.CLIENT_URL}/?error=login_failed`);
52
+
}
53
+
});
54
+
55
+
// OAuth callback handler
56
+
auth.get("/callback", async (c) => {
57
+
try {
58
+
const params = new URLSearchParams(c.req.url.split("?")[1] || "");
59
+
60
+
if (params.get("error")) {
61
+
const error = params.get("error");
62
+
console.error("OAuth error:", error, params.get("error_description"));
63
+
return c.redirect(
64
+
`${c.env.CLIENT_URL}/?error=${encodeURIComponent(error!)}`,
65
+
);
66
+
}
67
+
68
+
const client = createOAuthClient(c.env.SEQUOIA_SESSIONS, c.env.CLIENT_URL);
69
+
const { session } = await client.callback(params);
70
+
71
+
// Resolve handle from DID
72
+
let handle: string | undefined;
73
+
try {
74
+
const identity = await client.identityResolver.resolve(session.did);
75
+
handle = identity.handle;
76
+
} catch {
77
+
// Handle resolution is best-effort
78
+
}
79
+
80
+
// Store handle in KV alongside the session for quick lookup
81
+
if (handle) {
82
+
await c.env.SEQUOIA_SESSIONS.put(`oauth_handle:${session.did}`, handle, {
83
+
expirationTtl: 60 * 60 * 24 * 14,
84
+
});
85
+
}
86
+
87
+
setSessionCookie(c, session.did, c.env.CLIENT_URL);
88
+
return c.redirect(`${c.env.CLIENT_URL}/`);
89
+
} catch (error) {
90
+
console.error("Callback error:", error);
91
+
return c.redirect(`${c.env.CLIENT_URL}/?error=callback_failed`);
92
+
}
93
+
});
94
+
95
+
// Logout endpoint
96
+
auth.post("/logout", async (c) => {
97
+
const did = getSessionDid(c);
98
+
99
+
if (did) {
100
+
try {
101
+
const client = createOAuthClient(
102
+
c.env.SEQUOIA_SESSIONS,
103
+
c.env.CLIENT_URL,
104
+
);
105
+
await client.revoke(did);
106
+
} catch (error) {
107
+
console.error("Revoke error:", error);
108
+
}
109
+
await c.env.SEQUOIA_SESSIONS.delete(`oauth_handle:${did}`);
110
+
}
111
+
112
+
clearSessionCookie(c, c.env.CLIENT_URL);
113
+
return c.json({ success: true });
114
+
});
115
+
116
+
// Check auth status
117
+
auth.get("/status", async (c) => {
118
+
const did = getSessionDid(c);
119
+
120
+
if (!did) {
121
+
return c.json({ authenticated: false });
122
+
}
123
+
124
+
try {
125
+
const client = createOAuthClient(c.env.SEQUOIA_SESSIONS, c.env.CLIENT_URL);
126
+
const session = await client.restore(did);
127
+
128
+
const handle = await c.env.SEQUOIA_SESSIONS.get(
129
+
`oauth_handle:${session.did}`,
130
+
);
131
+
132
+
return c.json({
133
+
authenticated: true,
134
+
did: session.did,
135
+
handle: handle || undefined,
136
+
});
137
+
} catch (error) {
138
+
console.error("Session restore failed:", error);
139
+
clearSessionCookie(c, c.env.CLIENT_URL);
140
+
return c.json({ authenticated: false });
141
+
}
142
+
});
143
+
144
+
export default auth;
+2
-1
docs/tsconfig.json
+2
-1
docs/tsconfig.json
+18
docs/wrangler.toml
+18
docs/wrangler.toml
···
1
+
name = "sequoia-docs"
2
+
main = "src/index.ts"
3
+
compatibility_date = "2025-04-01"
4
+
compatibility_flags = ["nodejs_compat"]
5
+
6
+
[assets]
7
+
directory = "./docs/dist"
8
+
binding = "ASSETS"
9
+
not_found_handling = "single-page-application"
10
+
html_handling = "auto-trailing-slash"
11
+
run_worker_first = ["/api/*", "/oauth/*"]
12
+
13
+
[[kv_namespaces]]
14
+
binding = "SEQUOIA_SESSIONS"
15
+
id = "b9fedf2798a249669b3aeeaca70a0bf8"
16
+
17
+
[vars]
18
+
CLIENT_URL = "https://sequoia.pub"
History
1 round
0 comments
stevedylan.dev
submitted
#0
2 commits
expand
collapse
chore: refactored docs to worker + static assets
chore: refactored to use atproto oauth lib
1/1 success
expand
collapse
expand 0 comments
pull request successfully merged