Barazo AppView backend
barazo.forum
1import type { NodeSavedSession, NodeSavedState } from '@atproto/oauth-client-node'
2import type { Cache } from '../cache/index.js'
3import type { Logger } from '../lib/logger.js'
4
5const STATE_KEY_PREFIX = 'barazo:oauth:state:'
6const SESSION_KEY_PREFIX = 'barazo:oauth:session:'
7
8/** Default state TTL: 5 minutes (OAuth state is short-lived) */
9const DEFAULT_STATE_TTL = 300
10
11/**
12 * Valkey-backed store for OAuth authorization state.
13 * State entries are short-lived (5 minutes) and used during the
14 * authorization code flow between redirect and callback.
15 */
16export class ValkeyStateStore {
17 private readonly cache: Cache
18 private readonly logger: Logger
19
20 constructor(cache: Cache, logger: Logger) {
21 this.cache = cache
22 this.logger = logger
23 }
24
25 async set(key: string, state: NodeSavedState): Promise<void> {
26 const cacheKey = `${STATE_KEY_PREFIX}${key}`
27 try {
28 await this.cache.set(cacheKey, JSON.stringify(state), 'EX', DEFAULT_STATE_TTL)
29 this.logger.debug({ key: cacheKey }, 'OAuth state stored')
30 } catch (err: unknown) {
31 this.logger.error({ err, key: cacheKey }, 'Failed to store OAuth state')
32 throw err
33 }
34 }
35
36 async get(key: string): Promise<NodeSavedState | undefined> {
37 const cacheKey = `${STATE_KEY_PREFIX}${key}`
38 try {
39 const data = await this.cache.get(cacheKey)
40 if (data === null) {
41 this.logger.debug({ key: cacheKey }, 'OAuth state not found')
42 return undefined
43 }
44 return JSON.parse(data) as NodeSavedState
45 } catch (err: unknown) {
46 this.logger.error({ err, key: cacheKey }, 'Failed to retrieve OAuth state')
47 throw err
48 }
49 }
50
51 async del(key: string): Promise<void> {
52 const cacheKey = `${STATE_KEY_PREFIX}${key}`
53 try {
54 await this.cache.del(cacheKey)
55 this.logger.debug({ key: cacheKey }, 'OAuth state deleted')
56 } catch (err: unknown) {
57 this.logger.error({ err, key: cacheKey }, 'Failed to delete OAuth state')
58 throw err
59 }
60 }
61}
62
63/**
64 * Valkey-backed store for OAuth sessions.
65 * Sessions persist across requests and are keyed by the user's DID (sub).
66 * Default TTL is 7 days (604800 seconds), configurable via OAUTH_SESSION_TTL.
67 */
68export class ValkeySessionStore {
69 private readonly cache: Cache
70 private readonly logger: Logger
71 private readonly ttl: number
72
73 constructor(cache: Cache, logger: Logger, ttl: number) {
74 this.cache = cache
75 this.logger = logger
76 this.ttl = ttl
77 }
78
79 async set(sub: string, session: NodeSavedSession): Promise<void> {
80 const cacheKey = `${SESSION_KEY_PREFIX}${sub}`
81 try {
82 await this.cache.set(cacheKey, JSON.stringify(session), 'EX', this.ttl)
83 this.logger.debug({ key: cacheKey }, 'OAuth session stored')
84 } catch (err: unknown) {
85 this.logger.error({ err, key: cacheKey }, 'Failed to store OAuth session')
86 throw err
87 }
88 }
89
90 async get(sub: string): Promise<NodeSavedSession | undefined> {
91 const cacheKey = `${SESSION_KEY_PREFIX}${sub}`
92 try {
93 const data = await this.cache.get(cacheKey)
94 if (data === null) {
95 this.logger.debug({ key: cacheKey }, 'OAuth session not found')
96 return undefined
97 }
98 return JSON.parse(data) as NodeSavedSession
99 } catch (err: unknown) {
100 this.logger.error({ err, key: cacheKey }, 'Failed to retrieve OAuth session')
101 throw err
102 }
103 }
104
105 async del(sub: string): Promise<void> {
106 const cacheKey = `${SESSION_KEY_PREFIX}${sub}`
107 try {
108 await this.cache.del(cacheKey)
109 this.logger.debug({ key: cacheKey }, 'OAuth session deleted')
110 } catch (err: unknown) {
111 this.logger.error({ err, key: cacheKey }, 'Failed to delete OAuth session')
112 throw err
113 }
114 }
115}