A minimal starter for ATProto logins in Astro
1import type { AstroCookies } from 'astro'
2import type {
3 NodeSavedSession,
4 NodeSavedSessionStore,
5 NodeSavedState,
6 NodeSavedStateStore,
7} from '@atproto/oauth-client-node'
8
9// Cookie-based storage for OAuth state and sessions
10// All data is serialized into cookies for stateless operation
11
12export class CookieStateStore implements NodeSavedStateStore {
13 constructor(private cookies: AstroCookies) {}
14
15 async get(key: string): Promise<NodeSavedState | undefined> {
16 const cookieName = `oauth_state_${key}`
17 const cookie = this.cookies.get(cookieName)
18 if (!cookie?.value) return undefined
19
20 try {
21 const decoded = atob(cookie.value)
22 return JSON.parse(decoded) as NodeSavedState
23 } catch (err) {
24 console.warn('Failed to decode OAuth state:', err)
25 return undefined
26 }
27 }
28
29 async set(key: string, val: NodeSavedState) {
30 const cookieName = `oauth_state_${key}`
31 const encoded = btoa(JSON.stringify(val))
32
33 this.cookies.set(cookieName, encoded, {
34 httpOnly: true,
35 secure: false,
36 sameSite: 'lax',
37 path: '/',
38 maxAge: 60 * 10, // 10 minutes (OAuth flow timeout)
39 })
40 }
41
42 async del(key: string) {
43 const cookieName = `oauth_state_${key}`
44 this.cookies.delete(cookieName, { path: '/' })
45 }
46}
47
48export class CookieSessionStore implements NodeSavedSessionStore {
49 constructor(private cookies: AstroCookies) {}
50
51 async get(key: string): Promise<NodeSavedSession | undefined> {
52 const cookieName = `oauth_session_${key}`
53 const cookie = this.cookies.get(cookieName)
54 if (!cookie?.value) return undefined
55
56 try {
57 const decoded = atob(cookie.value)
58 return JSON.parse(decoded) as NodeSavedSession
59 } catch (err) {
60 console.warn('Failed to decode OAuth session:', err)
61 return undefined
62 }
63 }
64
65 async set(key: string, val: NodeSavedSession) {
66 const cookieName = `oauth_session_${key}`
67 const encoded = btoa(JSON.stringify(val))
68
69 this.cookies.set(cookieName, encoded, {
70 httpOnly: true,
71 secure: false,
72 sameSite: 'lax',
73 path: '/',
74 maxAge: 60 * 60 * 24 * 30, // 30 days
75 })
76 }
77
78 async del(key: string) {
79 const cookieName = `oauth_session_${key}`
80 this.cookies.delete(cookieName, { path: '/' })
81 }
82}