forked from
smokesignal.events/smokesignal
The smokesignal.events web application
1/**
2 * Authentication utilities for session management
3 *
4 * Provides session refresh functionality that should be called before
5 * making authenticated requests to AT Protocol services.
6 */
7
8/**
9 * Session refresh error types
10 */
11export class AuthRequiredError extends Error {
12 constructor(message = 'Authentication required') {
13 super(message)
14 this.name = 'AuthRequiredError'
15 }
16}
17
18export class SessionExpiredError extends Error {
19 constructor(message = 'Your session has expired. Please log in again.') {
20 super(message)
21 this.name = 'SessionExpiredError'
22 }
23}
24
25/**
26 * Refreshes the OAuth session token.
27 *
28 * This should be called before any request that uses AT Protocol OAuth tokens.
29 * The server will use the refresh token to obtain new access tokens if needed.
30 *
31 * @throws {AuthRequiredError} If no session exists
32 * @throws {SessionExpiredError} If the session cannot be refreshed
33 */
34export async function refreshSession(): Promise<void> {
35 try {
36 const response = await fetch('/oauth/refresh', {
37 method: 'POST',
38 credentials: 'same-origin',
39 })
40
41 if (!response.ok) {
42 if (response.status === 401) {
43 throw new AuthRequiredError()
44 }
45 // For other errors, we'll let them pass through silently
46 // The actual request will handle auth failures
47 console.warn('Session refresh returned non-OK status:', response.status)
48 }
49 } catch (e) {
50 if (e instanceof AuthRequiredError) throw e
51 // Network errors during refresh shouldn't block the request
52 // The actual request will handle auth failures
53 console.warn('Session refresh failed:', e)
54 }
55}
56
57/**
58 * Refreshes authentication before an authenticated request.
59 * Provides user-friendly error messages for session expiration.
60 *
61 * @throws {SessionExpiredError} If session has expired and cannot be refreshed
62 */
63export async function refreshAuth(): Promise<void> {
64 try {
65 await refreshSession()
66 } catch (e) {
67 if (e instanceof AuthRequiredError) {
68 throw new SessionExpiredError()
69 }
70 // For other errors, let them pass - the actual request will handle it
71 console.warn('Auth refresh warning:', e)
72 }
73}
74
75/**
76 * Options for authenticated fetch requests
77 */
78export interface AuthFetchOptions extends RequestInit {
79 /**
80 * Whether to skip session refresh before the request.
81 * Default: false (always refresh)
82 */
83 skipRefresh?: boolean
84}
85
86/**
87 * Fetch wrapper that refreshes the session before making authenticated requests.
88 *
89 * Use this for any requests that require AT Protocol OAuth authentication.
90 *
91 * @param url - The URL to fetch
92 * @param options - Fetch options with optional skipRefresh flag
93 * @returns The fetch response
94 * @throws {SessionExpiredError} If authentication fails
95 */
96export async function authFetch(url: string, options: AuthFetchOptions = {}): Promise<Response> {
97 const { skipRefresh = false, ...fetchOptions } = options
98
99 if (!skipRefresh) {
100 await refreshAuth()
101 }
102
103 const response = await fetch(url, {
104 ...fetchOptions,
105 credentials: 'same-origin',
106 })
107
108 // Handle authentication failures
109 if (response.status === 401) {
110 throw new SessionExpiredError()
111 }
112
113 return response
114}
115
116/**
117 * JSON fetch helper that refreshes session and handles JSON parsing.
118 *
119 * @param url - The URL to fetch
120 * @param options - Fetch options
121 * @returns Parsed JSON response
122 * @throws {SessionExpiredError} If authentication fails
123 */
124export async function authFetchJson<T>(url: string, options: AuthFetchOptions = {}): Promise<T> {
125 const response = await authFetch(url, options)
126 return response.json() as Promise<T>
127}
128
129/**
130 * POST JSON helper that refreshes session before posting.
131 *
132 * @param url - The URL to post to
133 * @param data - Data to send as JSON body
134 * @param options - Additional fetch options
135 * @returns The fetch response
136 * @throws {SessionExpiredError} If authentication fails
137 */
138export async function authPostJson(
139 url: string,
140 data: unknown,
141 options: AuthFetchOptions = {}
142): Promise<Response> {
143 return authFetch(url, {
144 method: 'POST',
145 headers: {
146 'Content-Type': 'application/json',
147 ...((options.headers as Record<string, string>) || {}),
148 },
149 body: JSON.stringify(data),
150 ...options,
151 })
152}