A Deno-compatible AT Protocol OAuth client that serves as a drop-in replacement for @atproto/oauth-client-node
1/**
2 * @fileoverview Storage implementations for OAuth client session persistence
3 * @module
4 */
5
6import type { OAuthStorage } from "./types.ts";
7export type { OAuthStorage as Storage } from "./types.ts";
8
9/**
10 * Simple in-memory storage implementation for OAuth sessions.
11 *
12 * Stores data in memory with optional TTL support. Data is lost when the
13 * process restarts. Good for development, testing, and temporary sessions.
14 *
15 * @example
16 * ```ts
17 * const storage = new MemoryStorage();
18 *
19 * // Store with TTL
20 * await storage.set("session-123", sessionData, { ttl: 3600 }); // 1 hour
21 *
22 * // Retrieve
23 * const session = await storage.get("session-123");
24 *
25 * // Clean up
26 * await storage.delete("session-123");
27 * ```
28 */
29export class MemoryStorage implements OAuthStorage {
30 private data = new Map<string, { value: unknown; expiresAt?: number }>();
31
32 async get<T = unknown>(key: string): Promise<T | null> {
33 await Promise.resolve(); // Satisfy require-await linting rule
34 const item = this.data.get(key);
35 if (!item) return null;
36
37 if (item.expiresAt && Date.now() > item.expiresAt) {
38 this.data.delete(key);
39 return null;
40 }
41
42 return item.value as T;
43 }
44
45 async set<T = unknown>(key: string, value: T, options?: { ttl?: number }): Promise<void> {
46 await Promise.resolve(); // Satisfy require-await linting rule
47 const expiresAt = options?.ttl ? Date.now() + (options.ttl * 1000) : undefined;
48 this.data.set(key, { value, ...(expiresAt ? { expiresAt } : {}) });
49 }
50
51 async delete(key: string): Promise<void> {
52 await Promise.resolve(); // Satisfy require-await linting rule
53 this.data.delete(key);
54 }
55
56 // Utility method for cleanup in tests
57 clear(): void {
58 this.data.clear();
59 }
60}
61
62/**
63 * Example SQLite storage implementation (for reference)
64 * Users can implement similar patterns for their storage backend
65 */
66export class SQLiteStorage implements OAuthStorage {
67 constructor(
68 private sqlite: {
69 execute: (
70 query: { sql: string; args: unknown[] },
71 ) => Promise<{ columns: string[]; rows: unknown[][] }>;
72 },
73 ) {}
74
75 async get<T = unknown>(key: string): Promise<T | null> {
76 const result = await this.sqlite.execute({
77 sql: "SELECT value, expires_at FROM oauth_storage WHERE key = ?",
78 args: [key],
79 });
80
81 if (result.rows.length === 0) return null;
82
83 const row = result.rows[0];
84 if (!row || row.length < 2) return null;
85
86 const [value, expiresAt] = row;
87
88 // Validate types from database
89 if (typeof value !== "string") {
90 throw new Error("Invalid storage value: expected string");
91 }
92
93 if (expiresAt !== null && typeof expiresAt === "number" && Date.now() > expiresAt) {
94 await this.delete(key);
95 return null;
96 }
97
98 return JSON.parse(value) as T;
99 }
100
101 async set<T = unknown>(key: string, value: T, options?: { ttl?: number }): Promise<void> {
102 const expiresAt = options?.ttl ? Date.now() + (options.ttl * 1000) : null;
103
104 // Ensure table exists
105 await this.sqlite.execute({
106 sql: `CREATE TABLE IF NOT EXISTS oauth_storage (
107 key TEXT PRIMARY KEY,
108 value TEXT NOT NULL,
109 expires_at INTEGER,
110 created_at INTEGER DEFAULT (unixepoch() * 1000)
111 )`,
112 args: [],
113 });
114
115 await this.sqlite.execute({
116 sql: "INSERT OR REPLACE INTO oauth_storage (key, value, expires_at) VALUES (?, ?, ?)",
117 args: [key, JSON.stringify(value), expiresAt],
118 });
119 }
120
121 async delete(key: string): Promise<void> {
122 await this.sqlite.execute({
123 sql: "DELETE FROM oauth_storage WHERE key = ?",
124 args: [key],
125 });
126 }
127}
128
129/**
130 * Example localStorage-compatible storage (for browser/Deno environments with localStorage)
131 */
132export class LocalStorage implements OAuthStorage {
133 async get<T = unknown>(key: string): Promise<T | null> {
134 await Promise.resolve(); // Satisfy require-await linting rule
135 try {
136 const item = localStorage.getItem(key);
137 if (!item) return null;
138
139 const parsed = JSON.parse(item);
140 if (parsed.expiresAt && Date.now() > parsed.expiresAt) {
141 localStorage.removeItem(key);
142 return null;
143 }
144
145 return parsed.value as T;
146 } catch {
147 return null;
148 }
149 }
150
151 async set<T = unknown>(key: string, value: T, options?: { ttl?: number }): Promise<void> {
152 await Promise.resolve(); // Satisfy require-await linting rule
153 const expiresAt = options?.ttl ? Date.now() + (options.ttl * 1000) : undefined;
154 const item = { value, expiresAt };
155 localStorage.setItem(key, JSON.stringify(item));
156 }
157
158 async delete(key: string): Promise<void> {
159 await Promise.resolve(); // Satisfy require-await linting rule
160 localStorage.removeItem(key);
161 }
162}