WIP! A BB-style forum, on the ATmosphere!
We're still working... we'll be back soon when we have something to show off!
node
typescript
hono
htmx
atproto
1/**
2 * Store adapters for @atproto/oauth-client-node
3 *
4 * The OAuth library expects stores that match the SimpleStore<K, V> interface.
5 * This file provides adapters that bridge our internal storage to the library's format.
6 * Each adapter wraps a generic TTLStore with the async interface expected by the library.
7 */
8
9import type { NodeSavedState, NodeSavedSession } from "@atproto/oauth-client-node";
10import { TTLStore } from "./ttl-store.js";
11
12/**
13 * Internal state wrapper with timestamp for expiration tracking.
14 */
15interface StateEntry {
16 state: NodeSavedState;
17 createdAt: number;
18}
19
20/** 10-minute TTL for OAuth authorization state. */
21const STATE_TTL_MS = 10 * 60 * 1000;
22
23/**
24 * State store adapter for OAuth authorization flow.
25 * Bridges our in-memory storage to the library's expected async SimpleStore interface.
26 */
27export class OAuthStateStore {
28 private store: TTLStore<StateEntry>;
29
30 constructor() {
31 this.store = new TTLStore<StateEntry>(
32 (entry) => Date.now() - entry.createdAt > STATE_TTL_MS,
33 "oauth_state_store"
34 );
35 }
36
37 async set(key: string, internalState: NodeSavedState): Promise<void> {
38 this.store.set(key, {
39 state: internalState,
40 createdAt: Date.now(),
41 });
42 }
43
44 async get(key: string): Promise<NodeSavedState | undefined> {
45 const entry = this.store.get(key);
46 return entry?.state;
47 }
48
49 async del(key: string): Promise<void> {
50 this.store.delete(key);
51 }
52
53 /**
54 * Stop cleanup timer (for graceful shutdown).
55 */
56 destroy(): void {
57 this.store.destroy();
58 }
59}
60
61/**
62 * Session store adapter for OAuth sessions.
63 * Bridges our in-memory storage to the library's expected async SimpleStore interface.
64 *
65 * The library stores sessions indexed by DID (sub), and handles token refresh internally.
66 */
67export class OAuthSessionStore {
68 private store: TTLStore<NodeSavedSession>;
69
70 constructor() {
71 this.store = new TTLStore<NodeSavedSession>(
72 (session) => {
73 // Only expire sessions where access token is expired and there's no refresh token.
74 // Keep sessions with refresh tokens — the library will handle refresh.
75 if (!session.tokenSet.refresh_token && session.tokenSet.expires_at) {
76 const expiresAt = new Date(session.tokenSet.expires_at).getTime();
77 return expiresAt < Date.now();
78 }
79 return false;
80 },
81 "oauth_session_store"
82 );
83 }
84
85 async set(sub: string, session: NodeSavedSession): Promise<void> {
86 this.store.set(sub, session);
87 }
88
89 async get(sub: string): Promise<NodeSavedSession | undefined> {
90 // Use getUnchecked so the library can manage token refresh internally.
91 // Background cleanup still evicts truly expired sessions without refresh tokens.
92 return this.store.getUnchecked(sub);
93 }
94
95 async del(sub: string): Promise<void> {
96 this.store.delete(sub);
97 }
98
99 /**
100 * Stop cleanup timer (for graceful shutdown).
101 */
102 destroy(): void {
103 this.store.destroy();
104 }
105}