Highly ambitious ATProtocol AppView service and sdks
1import type { SessionStore } from "./store.ts";
2import type { SessionData } from "./types.ts";
3import {
4 OAuthClient,
5 type OAuthTokens,
6 type OAuthConfig,
7 type OAuthStorage,
8} from "@slices/oauth";
9
10export interface OAuthSessionOptions {
11 sessionStore: SessionStore;
12 oauthConfig: OAuthConfig;
13 oauthStorage: OAuthStorage;
14 autoRefresh?: boolean;
15 onTokenRefresh?: (sessionId: string, tokens: OAuthTokens) => Promise<void>;
16 onLogout?: (sessionId: string) => Promise<void>;
17}
18
19export class OAuthSessionManager {
20 private options: OAuthSessionOptions;
21
22 constructor(options: OAuthSessionOptions) {
23 this.options = options;
24 }
25
26 async createOAuthSession(tokens: OAuthTokens): Promise<string | null> {
27 try {
28 // Create temporary OAuth client to fetch user info
29 const tempClient = new OAuthClient(
30 this.options.oauthConfig,
31 this.options.oauthStorage,
32 "temp_" + Date.now()
33 );
34
35 // Temporarily store tokens to fetch user info
36 await this.options.oauthStorage.setTokens(
37 tokens,
38 tempClient.getSessionId()
39 );
40
41 // Get user info from OAuth
42 const userInfo = await tempClient.getUserInfo();
43 if (!userInfo) {
44 await this.options.oauthStorage.clearTokens(tempClient.getSessionId());
45 return null;
46 }
47
48 // Clean up temp tokens
49 await this.options.oauthStorage.clearTokens(tempClient.getSessionId());
50
51 // Create session FIRST to get sessionId
52 const sessionId = await this.options.sessionStore.createSession(
53 userInfo.sub,
54 userInfo.name,
55 {
56 userInfo: {
57 ...userInfo,
58 handle: userInfo.name,
59 },
60 }
61 );
62
63 // NOW store tokens by sessionId
64 await this.options.oauthStorage.setTokens(tokens, sessionId);
65
66 return sessionId;
67 } catch (error) {
68 console.error("Failed to create OAuth session:", error);
69 return null;
70 }
71 }
72
73 async getOAuthSession(sessionId: string): Promise<SessionData | null> {
74 const session = await this.options.sessionStore.getSession(sessionId);
75 if (!session) {
76 return null;
77 }
78
79 // Auto-refresh is handled by OAuth client, not session storage
80 if (this.options.autoRefresh) {
81 try {
82 // Create session-scoped OAuth client
83 const sessionClient = new OAuthClient(
84 this.options.oauthConfig,
85 this.options.oauthStorage,
86 sessionId
87 );
88
89 // This ensures tokens are fresh in OAuth storage
90 await sessionClient.ensureValidToken();
91
92 // Call refresh callback if provided
93 if (this.options.onTokenRefresh) {
94 const tokens = await sessionClient.getTokens();
95 if (tokens) {
96 await this.options.onTokenRefresh(sessionId, tokens);
97 }
98 }
99 } catch (error) {
100 console.error("Failed to refresh OAuth tokens:", error);
101 // Session is still valid even if token refresh fails
102 }
103 }
104
105 return session;
106 }
107
108 async logout(sessionId: string): Promise<void> {
109 try {
110 // Create session-scoped OAuth client and logout
111 const sessionClient = new OAuthClient(
112 this.options.oauthConfig,
113 this.options.oauthStorage,
114 sessionId
115 );
116 await sessionClient.logout();
117
118 // Delete session
119 await this.options.sessionStore.deleteSession(sessionId);
120
121 // Call logout callback if provided
122 if (this.options.onLogout) {
123 await this.options.onLogout(sessionId);
124 }
125 } catch (error) {
126 console.error("Failed to logout OAuth session:", error);
127 // Still delete the session even if OAuth logout fails
128 await this.options.sessionStore.deleteSession(sessionId);
129 }
130 }
131
132 async hasValidOAuthTokens(sessionId: string): Promise<boolean> {
133 const session = await this.getOAuthSession(sessionId);
134 if (!session) return false;
135
136 try {
137 // Create session-scoped OAuth client
138 const sessionClient = new OAuthClient(
139 this.options.oauthConfig,
140 this.options.oauthStorage,
141 sessionId
142 );
143 // Let OAuth client determine token validity
144 const tokens = await sessionClient.ensureValidToken();
145 return !!tokens.accessToken;
146 } catch (_error) {
147 return false;
148 }
149 }
150
151 async getAccessToken(sessionId: string): Promise<string | null> {
152 const session = await this.getOAuthSession(sessionId);
153 if (!session) return null;
154
155 try {
156 // Create session-scoped OAuth client
157 const sessionClient = new OAuthClient(
158 this.options.oauthConfig,
159 this.options.oauthStorage,
160 sessionId
161 );
162 // Get fresh tokens from OAuth client
163 const tokens = await sessionClient.ensureValidToken();
164 return tokens.accessToken || null;
165 } catch (_error) {
166 return null;
167 }
168 }
169}
170
171// Convenience function to create an OAuth-enabled session store
172export function withOAuthSession(
173 sessionStore: SessionStore,
174 oauthConfig: OAuthConfig,
175 oauthStorage: OAuthStorage,
176 options: Partial<
177 Omit<OAuthSessionOptions, "sessionStore" | "oauthConfig" | "oauthStorage">
178 > = {}
179): OAuthSessionManager {
180 return new OAuthSessionManager({
181 sessionStore,
182 oauthConfig,
183 oauthStorage,
184 autoRefresh: true,
185 ...options,
186 });
187}
188
189// Helper middleware for frameworks
190export interface OAuthSessionMiddlewareOptions extends OAuthSessionOptions {
191 cookieName?: string;
192 loginRedirect?: string;
193 logoutRedirect?: string;
194}
195
196export function createOAuthSessionMiddleware(
197 options: OAuthSessionMiddlewareOptions
198) {
199 const manager = new OAuthSessionManager(options);
200 const cookieName = options.cookieName || "slice-session";
201
202 return {
203 // Get current user from session
204 async getCurrentUser(request: Request) {
205 const sessionId = getSessionIdFromRequest(request, cookieName);
206 if (!sessionId) {
207 return { isAuthenticated: false };
208 }
209
210 const session = await manager.getOAuthSession(sessionId);
211 if (!session) {
212 return { isAuthenticated: false };
213 }
214
215 return {
216 isAuthenticated: true,
217 sub: session.userId,
218 handle: session.handle,
219 // User info from session data (no OAuth tokens)
220 ...(session.data?.userInfo || {}),
221 };
222 },
223
224 // Login handler - expects tokens from OAuth callback
225 async login(tokens: OAuthTokens): Promise<Response> {
226 const sessionId = await manager.createOAuthSession(tokens);
227 if (!sessionId) {
228 return new Response("Authentication failed", { status: 401 });
229 }
230
231 const cookie = options.sessionStore.createSessionCookie(sessionId);
232 const redirectUrl = options.loginRedirect || "/";
233
234 return new Response("", {
235 status: 302,
236 headers: {
237 Location: redirectUrl,
238 "Set-Cookie": cookie,
239 },
240 });
241 },
242
243 // Logout handler
244 async logout(request: Request): Promise<Response> {
245 const sessionId = getSessionIdFromRequest(request, cookieName);
246 if (sessionId) {
247 await manager.logout(sessionId);
248 }
249
250 const cookie = options.sessionStore.createLogoutCookie();
251 const redirectUrl = options.logoutRedirect || "/";
252
253 return new Response("", {
254 status: 302,
255 headers: {
256 Location: redirectUrl,
257 "Set-Cookie": cookie,
258 },
259 });
260 },
261
262 manager,
263 };
264}
265
266// Helper to extract session ID from request
267function getSessionIdFromRequest(
268 request: Request,
269 cookieName: string
270): string | null {
271 const cookieHeader = request.headers.get("cookie");
272 if (!cookieHeader) return null;
273
274 const cookies = parseCookies(cookieHeader);
275 return cookies[cookieName] || null;
276}
277
278// Simple cookie parser
279function parseCookies(cookieString: string): Record<string, string> {
280 const cookies: Record<string, string> = {};
281
282 cookieString.split(";").forEach((cookie) => {
283 const [key, value] = cookie.split("=").map((s) => s.trim());
284 if (key && value) {
285 cookies[decodeURIComponent(key)] = decodeURIComponent(value);
286 }
287 });
288
289 return cookies;
290}