forked from
stevedylan.dev/sequoia
A CLI for publishing standard.site documents to ATProto
1import { JoseKey } from "@atproto/jwk-jose";
2import type {
3 Key,
4 InternalStateData,
5 SessionStore,
6 StateStore,
7} from "@atproto/oauth-client";
8import { RedisClient } from "bun";
9
10type SerializedStateData = Omit<InternalStateData, "dpopKey"> & {
11 dpopJwk: Record<string, unknown>;
12};
13
14type SerializedSession = Omit<Parameters<SessionStore["set"]>[1], "dpopKey"> & {
15 dpopJwk: Record<string, unknown>;
16};
17
18function serializeKey(key: Key): Record<string, unknown> {
19 const jwk = key.privateJwk;
20 if (!jwk) throw new Error("Private DPoP JWK is missing");
21 return jwk as Record<string, unknown>;
22}
23
24async function deserializeKey(jwk: Record<string, unknown>): Promise<Key> {
25 return JoseKey.fromJWK(jwk) as unknown as Key;
26}
27
28export function createStateStore(redis: RedisClient, ttl = 600): StateStore {
29 return {
30 async set(key, { dpopKey, ...rest }) {
31 const data: SerializedStateData = {
32 ...rest,
33 dpopJwk: serializeKey(dpopKey),
34 };
35 const redisKey = `oauth_state:${key}`;
36 await redis.set(redisKey, JSON.stringify(data));
37 await redis.expire(redisKey, ttl);
38 },
39 async get(key) {
40 const raw = await redis.get(`oauth_state:${key}`);
41 if (!raw) return undefined;
42 const { dpopJwk, ...rest }: SerializedStateData = JSON.parse(raw);
43 const dpopKey = await deserializeKey(dpopJwk);
44 return { ...rest, dpopKey };
45 },
46 async del(key) {
47 await redis.del(`oauth_state:${key}`);
48 },
49 };
50}
51
52export function createSessionStore(
53 redis: RedisClient,
54 ttl = 60 * 60 * 24 * 14,
55): SessionStore {
56 return {
57 async set(sub, { dpopKey, ...rest }) {
58 const data: SerializedSession = {
59 ...rest,
60 dpopJwk: serializeKey(dpopKey),
61 };
62 const redisKey = `oauth_session:${sub}`;
63 await redis.set(redisKey, JSON.stringify(data));
64 await redis.expire(redisKey, ttl);
65 },
66 async get(sub) {
67 const raw = await redis.get(`oauth_session:${sub}`);
68 if (!raw) return undefined;
69 const { dpopJwk, ...rest }: SerializedSession = JSON.parse(raw);
70 const dpopKey = await deserializeKey(dpopJwk);
71 return { ...rest, dpopKey };
72 },
73 async del(sub) {
74 await redis.del(`oauth_session:${sub}`);
75 },
76 };
77}