@jasperoosthoek/zustand-auth-registry 0.0.1 → 0.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/authStore.ts CHANGED
@@ -2,19 +2,17 @@ import { create, StoreApi, UseBoundStore } from 'zustand';
2
2
  import { ValidatedAuthConfig, TokenData } from './authConfig';
3
3
 
4
4
  export type AuthState<U> = {
5
- isAuthenticated: boolean;
5
+ isAuthenticated: boolean | null; // null = not checked yet (cookie mode)
6
6
  user: U | null;
7
- tokens: TokenData | null;
8
-
9
- // OAuth 2.0 methods
7
+ tokens: TokenData | null; // null in cookie mode or when logged out
8
+
9
+ // Methods
10
10
  setTokens: (tokens: TokenData) => void;
11
+ setBearerToken: (token: string) => void; // Convenience for simple Bearer token auth
12
+ setAuthenticated: (authenticated: boolean) => void; // For cookie mode
11
13
  setUser: (user: U) => void;
12
14
  unsetUser: () => void;
13
15
  isTokenExpired: () => boolean;
14
-
15
- // Backward compatibility
16
- token: string;
17
- setToken: (token: string) => void;
18
16
  };
19
17
 
20
18
  export type AuthStore<U> = UseBoundStore<StoreApi<AuthState<U>>> & {
@@ -22,23 +20,29 @@ export type AuthStore<U> = UseBoundStore<StoreApi<AuthState<U>>> & {
22
20
  };
23
21
 
24
22
  export const createAuthStore = <U>(config: ValidatedAuthConfig<U>): AuthStore<U> => {
25
- const { persistence } = config;
26
-
23
+ const { persistence, cookieAuth } = config;
24
+
27
25
  const getStoredTokens = (): TokenData | null => {
26
+ // Cookie mode: No client-side tokens
27
+ if (cookieAuth?.enabled) {
28
+ return null;
29
+ }
30
+
31
+ // Token mode: Read from storage
28
32
  if (!persistence.enabled) return null;
29
33
  try {
30
34
  const accessToken = persistence.storage.getItem(persistence.tokenKey);
31
35
  if (!accessToken) return null;
32
-
36
+
33
37
  const refreshToken = persistence.storage.getItem(persistence.refreshTokenKey);
34
38
  const expiryString = persistence.storage.getItem(persistence.expiryKey);
35
39
  const expiresAt = expiryString ? parseInt(expiryString, 10) : undefined;
36
-
40
+
37
41
  return {
38
42
  accessToken,
39
43
  refreshToken: refreshToken || undefined,
40
44
  expiresAt: expiresAt && !isNaN(expiresAt) ? expiresAt : undefined,
41
- tokenType: 'Bearer', // Default, will be updated on setTokens
45
+ tokenType: 'Bearer',
42
46
  };
43
47
  } catch {
44
48
  return null;
@@ -57,30 +61,37 @@ export const createAuthStore = <U>(config: ValidatedAuthConfig<U>): AuthStore<U>
57
61
 
58
62
  const initialTokens = getStoredTokens();
59
63
  const initialUser = getStoredUser();
60
- const initialIsAuthenticated = !!initialTokens?.accessToken;
64
+
65
+ // Cookie mode: null (unknown until checkAuth)
66
+ // Token mode: true/false based on token presence
67
+ const initialIsAuthenticated = cookieAuth?.enabled
68
+ ? null
69
+ : !!initialTokens?.accessToken;
61
70
 
62
71
  const store = create<AuthState<U>>((set, get) => ({
63
72
  tokens: initialTokens,
64
73
  user: initialUser,
65
74
  isAuthenticated: initialIsAuthenticated,
66
75
 
67
- // OAuth 2.0 methods
68
76
  setTokens: (tokens: TokenData) => {
69
- const user = get().user;
70
- const isAuthenticated = !!tokens.accessToken;
71
-
72
- set({ tokens, isAuthenticated, token: tokens.accessToken });
73
-
77
+ set({ tokens, isAuthenticated: true });
78
+
79
+ // Cookie mode: No localStorage persistence for tokens
80
+ if (cookieAuth?.enabled) {
81
+ return;
82
+ }
83
+
84
+ // Token mode: Persist to storage
74
85
  if (persistence.enabled) {
75
86
  try {
76
87
  persistence.storage.setItem(persistence.tokenKey, tokens.accessToken);
77
-
88
+
78
89
  if (tokens.refreshToken) {
79
90
  persistence.storage.setItem(persistence.refreshTokenKey, tokens.refreshToken);
80
91
  } else {
81
92
  persistence.storage.removeItem(persistence.refreshTokenKey);
82
93
  }
83
-
94
+
84
95
  if (tokens.expiresAt) {
85
96
  persistence.storage.setItem(persistence.expiryKey, tokens.expiresAt.toString());
86
97
  } else {
@@ -92,12 +103,17 @@ export const createAuthStore = <U>(config: ValidatedAuthConfig<U>): AuthStore<U>
92
103
  }
93
104
  },
94
105
 
106
+ setBearerToken: (token: string) => {
107
+ get().setTokens({ accessToken: token, tokenType: 'Bearer' });
108
+ },
109
+
110
+ setAuthenticated: (authenticated: boolean) => {
111
+ set({ isAuthenticated: authenticated });
112
+ },
113
+
95
114
  setUser: (user: U) => {
96
- const tokens = get().tokens;
97
- const isAuthenticated = !!tokens?.accessToken;
98
-
99
- set({ user, isAuthenticated });
100
-
115
+ set({ user });
116
+
101
117
  if (persistence.enabled) {
102
118
  try {
103
119
  persistence.storage.setItem(persistence.userKey, JSON.stringify(user));
@@ -108,8 +124,8 @@ export const createAuthStore = <U>(config: ValidatedAuthConfig<U>): AuthStore<U>
108
124
  },
109
125
 
110
126
  unsetUser: () => {
111
- set({ user: null, tokens: null, isAuthenticated: false, token: '' });
112
-
127
+ set({ user: null, tokens: null, isAuthenticated: false });
128
+
113
129
  if (persistence.enabled) {
114
130
  try {
115
131
  persistence.storage.removeItem(persistence.tokenKey);
@@ -124,36 +140,9 @@ export const createAuthStore = <U>(config: ValidatedAuthConfig<U>): AuthStore<U>
124
140
 
125
141
  isTokenExpired: () => {
126
142
  const tokens = get().tokens;
127
- if (!tokens?.expiresAt) return false; // No expiry info means no expiration
143
+ if (!tokens?.expiresAt) return false;
128
144
  return Date.now() >= tokens.expiresAt;
129
145
  },
130
-
131
- // Backward compatibility - computed property
132
- token: initialTokens?.accessToken || '',
133
-
134
- setToken: (token: string) => {
135
- const currentTokens = get().tokens;
136
- const user = get().user;
137
- const newTokens: TokenData = {
138
- accessToken: token,
139
- refreshToken: currentTokens?.refreshToken,
140
- expiresAt: currentTokens?.expiresAt,
141
- tokenType: currentTokens?.tokenType || 'Bearer', // Standard default
142
- scope: currentTokens?.scope,
143
- };
144
-
145
- const isAuthenticated = !!token;
146
- set({ tokens: newTokens, token, isAuthenticated });
147
-
148
- // Handle persistence
149
- if (persistence.enabled) {
150
- try {
151
- persistence.storage.setItem(persistence.tokenKey, token);
152
- } catch (error) {
153
- config.onError?.(error);
154
- }
155
- }
156
- },
157
146
  }));
158
147
 
159
148
  return Object.assign(store, { config });
@@ -19,4 +19,4 @@ export function createAuthRegistry<AuthModels extends Record<string, any>>() {
19
19
  }
20
20
 
21
21
  return getAuthStore;
22
- }
22
+ }
package/src/errors.ts ADDED
@@ -0,0 +1,104 @@
1
+ /**
2
+ * Authentication error codes
3
+ */
4
+ export enum AuthErrorCode {
5
+ INVALID_CREDENTIALS = 'INVALID_CREDENTIALS',
6
+ TOKEN_EXPIRED = 'TOKEN_EXPIRED',
7
+ TOKEN_INVALID = 'TOKEN_INVALID',
8
+ REFRESH_FAILED = 'REFRESH_FAILED',
9
+ NETWORK_ERROR = 'NETWORK_ERROR',
10
+ USER_NOT_FOUND = 'USER_NOT_FOUND',
11
+ UNAUTHORIZED = 'UNAUTHORIZED',
12
+ CSRF_TOKEN_MISSING = 'CSRF_TOKEN_MISSING',
13
+ FORBIDDEN = 'FORBIDDEN',
14
+ UNKNOWN = 'UNKNOWN',
15
+ }
16
+
17
+ /**
18
+ * Typed authentication error
19
+ */
20
+ export class AuthError extends Error {
21
+ constructor(
22
+ public code: AuthErrorCode,
23
+ public originalError?: any,
24
+ message?: string
25
+ ) {
26
+ super(message || code);
27
+ this.name = 'AuthError';
28
+
29
+ // Maintain proper stack trace for where our error was thrown (only available on V8)
30
+ if (Error.captureStackTrace) {
31
+ Error.captureStackTrace(this, AuthError);
32
+ }
33
+ }
34
+
35
+ /**
36
+ * Convert error to JSON (excludes originalError in production)
37
+ */
38
+ toJSON() {
39
+ return {
40
+ code: this.code,
41
+ message: this.message,
42
+ name: this.name,
43
+ };
44
+ }
45
+
46
+ /**
47
+ * Check if error is an AuthError
48
+ */
49
+ static isAuthError(error: any): error is AuthError {
50
+ return error instanceof AuthError;
51
+ }
52
+ }
53
+
54
+ /**
55
+ * Create typed AuthError from any error
56
+ */
57
+ export function createAuthError(error: any): AuthError {
58
+ if (AuthError.isAuthError(error)) {
59
+ return error;
60
+ }
61
+
62
+ // Axios error
63
+ if (error.response) {
64
+ const status = error.response.status;
65
+ const data = error.response.data;
66
+
67
+ switch (status) {
68
+ case 401:
69
+ // Check if it's an expired token
70
+ if (data?.detail?.toLowerCase().includes('expired') ||
71
+ data?.message?.toLowerCase().includes('expired')) {
72
+ return new AuthError(AuthErrorCode.TOKEN_EXPIRED, error, 'Token has expired');
73
+ }
74
+ if (data?.detail?.toLowerCase().includes('invalid') ||
75
+ data?.detail?.toLowerCase().includes('credentials')) {
76
+ return new AuthError(AuthErrorCode.INVALID_CREDENTIALS, error, 'Invalid credentials');
77
+ }
78
+ return new AuthError(AuthErrorCode.UNAUTHORIZED, error, 'Unauthorized');
79
+
80
+ case 403:
81
+ if (data?.detail?.toLowerCase().includes('csrf')) {
82
+ return new AuthError(AuthErrorCode.CSRF_TOKEN_MISSING, error, 'CSRF token missing or invalid');
83
+ }
84
+ return new AuthError(AuthErrorCode.FORBIDDEN, error, 'Access forbidden');
85
+
86
+ case 404:
87
+ if (data?.detail?.toLowerCase().includes('user')) {
88
+ return new AuthError(AuthErrorCode.USER_NOT_FOUND, error, 'User not found');
89
+ }
90
+ return new AuthError(AuthErrorCode.UNKNOWN, error, 'Resource not found');
91
+
92
+ default:
93
+ return new AuthError(AuthErrorCode.UNKNOWN, error, `HTTP ${status} error`);
94
+ }
95
+ }
96
+
97
+ // Network error (no response received)
98
+ if (error.request) {
99
+ return new AuthError(AuthErrorCode.NETWORK_ERROR, error, 'Network error - no response received');
100
+ }
101
+
102
+ // Other errors
103
+ return new AuthError(AuthErrorCode.UNKNOWN, error, error.message || 'Unknown error');
104
+ }
package/src/index.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  export * from './authConfig';
2
2
  export * from './authStore';
3
3
  export * from './createAuthRegistry';
4
- export * from './useAuth';
4
+ export * from './useAuth';
5
+ export * from './errors';
package/src/useAuth.ts CHANGED
@@ -1,151 +1,172 @@
1
1
  import { useEffect, useCallback } from 'react';
2
- import axios from 'axios';
3
2
  import { AuthStore } from './authStore';
4
- import { TokenData } from './authConfig';
3
+
4
+ // Promise deduplication: prevents multiple concurrent checkAuth calls per store
5
+ const pendingCheckAuth = new WeakMap<AuthStore<any>, Promise<boolean>>();
5
6
 
6
7
  export function useAuth<U>(store: AuthStore<U>) {
7
- const { setTokens, setUser, unsetUser, tokens, user, isTokenExpired } = store();
8
+ const { setTokens, setAuthenticated, setUser, unsetUser, tokens, user, isAuthenticated, isTokenExpired } = store();
8
9
  const config = store.config;
9
10
 
10
- // Backward compatibility
11
- const { setToken, token } = store();
11
+ // Set axios Authorization header (token mode only)
12
+ const setAxiosAuth = useCallback((token?: string, tokenType?: string) => {
13
+ if (config.cookieAuth?.enabled) {
14
+ // Cookie mode: CSRF header if enabled, no Authorization header
15
+ if (config.cookieAuth.csrf.enabled) {
16
+ const csrfToken = getCookie(config.cookieAuth.csrf.cookieName);
17
+ if (csrfToken) {
18
+ config.axios.defaults.headers.common[config.cookieAuth.csrf.headerName] = csrfToken;
19
+ }
20
+ }
21
+ return;
22
+ }
12
23
 
13
- // OAuth 2.0 refresh token functionality
14
- const refreshTokens = useCallback(async (): Promise<boolean> => {
15
- if (!tokens?.refreshToken) {
16
- return false;
24
+ // Token mode: Set Authorization header
25
+ if (token) {
26
+ config.axios.defaults.headers.common['Authorization'] = config.formatAuthHeader(token, tokenType);
27
+ } else {
28
+ delete config.axios.defaults.headers.common['Authorization'];
17
29
  }
30
+ }, [config]);
31
+
32
+ // Refresh tokens
33
+ const refresh = useCallback(async (): Promise<boolean> => {
34
+ if (!config.refreshUrl) return false;
18
35
 
19
36
  try {
20
- const response = await config.axios.post(config.tokenUrl, {
21
- grant_type: 'refresh_token',
37
+ // Cookie mode: Just call refresh endpoint, server handles cookie
38
+ if (config.cookieAuth?.enabled) {
39
+ const headers = getCsrfHeaders(config);
40
+ await config.axios.post(config.refreshUrl, {}, { headers });
41
+ return true;
42
+ }
43
+
44
+ // Token mode: Send refresh token, get new tokens
45
+ if (!tokens?.refreshToken) return false;
46
+
47
+ const response = await config.axios.post(config.refreshUrl, {
22
48
  refresh_token: tokens.refreshToken,
23
49
  });
24
50
 
25
51
  const newTokens = config.extractTokens(response.data);
26
52
  setTokens(newTokens);
27
53
  setAxiosAuth(newTokens.accessToken, newTokens.tokenType);
28
-
29
- config.onTokenRefresh?.(newTokens);
30
54
  return true;
31
55
  } catch (error) {
32
- // Refresh failed, clear tokens
33
56
  unsetUser();
34
57
  setAxiosAuth();
35
58
  config.onError?.(error);
36
59
  return false;
37
60
  }
38
- }, [tokens?.refreshToken, config, setTokens, unsetUser]);
61
+ }, [tokens, config, setTokens, unsetUser, setAxiosAuth]);
39
62
 
40
- // Auto-refresh and authentication setup
41
- useEffect(() => {
42
- // Handle current tokens
43
- if (tokens?.accessToken) {
44
- // Set axios headers immediately
45
- setAxiosAuth(tokens.accessToken, tokens.tokenType);
63
+ // Check authentication (cookie mode) - with promise deduplication
64
+ const checkAuth = useCallback(async (): Promise<boolean> => {
65
+ const authCheckUrl = config.authCheckUrl;
66
+ if (!config.cookieAuth?.enabled || !authCheckUrl) {
67
+ return false;
68
+ }
46
69
 
47
- // Check if token is expired or about to expire
48
- if (isTokenExpired()) {
49
- // Token is already expired, try to refresh
50
- if (tokens.refreshToken && config.autoRefresh) {
51
- refreshTokens();
52
- } else {
53
- unsetUser();
54
- }
55
- return;
56
- }
70
+ // Return existing promise if check is already in progress
71
+ const pending = pendingCheckAuth.get(store);
72
+ if (pending) {
73
+ return pending;
74
+ }
57
75
 
58
- // Set up auto-refresh timer if we have expiry info
59
- if (tokens.expiresAt && tokens.refreshToken && config.autoRefresh) {
60
- const timeUntilExpiry = tokens.expiresAt - Date.now();
61
- const refreshTime = Math.max(timeUntilExpiry - config.refreshThreshold, 0);
76
+ const doCheck = async (): Promise<boolean> => {
77
+ try {
78
+ const headers = getCsrfHeaders(config);
79
+ const response = await config.axios.get(authCheckUrl, { headers });
62
80
 
63
- const timer = setTimeout(() => {
64
- refreshTokens();
65
- }, refreshTime);
81
+ if (response.data.authenticated) {
82
+ setAuthenticated(true);
66
83
 
67
- return () => clearTimeout(timer);
68
- }
84
+ const extractedUser = config.extractUser?.(response.data);
85
+ if (extractedUser) {
86
+ setUser(extractedUser);
87
+ } else if (config.getUserUrl) {
88
+ await getCurrentUser();
89
+ }
69
90
 
70
- // Try to get user info if missing
71
- try {
72
- if (!user && (config.userInfoUrl || config.getUserUrl)) {
73
- getCurrentUser();
91
+ return true;
74
92
  }
93
+
94
+ setAuthenticated(false);
95
+ return false;
75
96
  } catch (error) {
76
- if (axios.isAxiosError(error) && error.response?.status === 403) {
77
- unsetUser();
78
- setAxiosAuth();
79
- }
97
+ setAuthenticated(false);
80
98
  config.onError?.(error);
99
+ return false;
100
+ } finally {
101
+ pendingCheckAuth.delete(store);
81
102
  }
82
- }
83
- }, [tokens, user, config.autoRefresh, config.refreshThreshold, config.userInfoUrl, config.getUserUrl, isTokenExpired, refreshTokens, unsetUser]);
103
+ };
84
104
 
85
- const setAxiosAuth = (token?: string, tokenType?: string) => {
86
- if (typeof token !== 'undefined' && token) {
87
- config.axios.defaults.headers.common['Authorization'] = config.formatAuthHeader(token, tokenType);
88
- } else {
89
- delete config.axios.defaults.headers.common['Authorization'];
90
- }
91
- };
105
+ const promise = doCheck();
106
+ pendingCheckAuth.set(store, promise);
107
+ return promise;
108
+ }, [store, config, setAuthenticated, setUser]);
109
+
110
+ // Get current user
111
+ const getCurrentUser = useCallback(async () => {
112
+ if (!config.getUserUrl) return;
92
113
 
93
- const login = async (
94
- credentials: Record<string, string>,
95
- callback?: () => void
96
- ) => {
97
114
  try {
98
- const res = await config.axios.post(config.tokenUrl, credentials);
99
- const tokens = config.extractTokens(res.data);
100
- setTokens(tokens);
101
- setAxiosAuth(tokens.accessToken, tokens.tokenType);
102
-
103
- if (config.userInfoUrl || config.getUserUrl) {
104
- await getCurrentUser();
105
- }
106
-
107
- if (user) {
108
- config.onLogin?.(user);
109
- }
110
- callback?.();
111
- } catch (err) {
115
+ const res = await config.axios.get<U>(config.getUserUrl);
116
+ setUser(res.data);
117
+ } catch (error) {
112
118
  unsetUser();
113
119
  setAxiosAuth();
114
- config.onError?.(err);
120
+ config.onError?.(error);
121
+ throw error;
115
122
  }
116
- };
123
+ }, [config, setUser, unsetUser, setAxiosAuth]);
117
124
 
118
- const getCurrentUser = async () => {
119
- const userUrl = config.userInfoUrl || config.getUserUrl;
120
- if (!userUrl) return;
125
+ // Login
126
+ const login = async (credentials: Record<string, string>, callback?: () => void) => {
121
127
  try {
122
- const res = await config.axios.get<U>(userUrl);
123
- setUser(res.data);
124
- } catch (err) {
128
+ const headers = config.cookieAuth?.enabled ? getCsrfHeaders(config) : {};
129
+ const res = await config.axios.post(config.loginUrl, credentials, { headers });
130
+
131
+ if (config.cookieAuth?.enabled) {
132
+ // Cookie mode: Server sets httpOnly cookie, just mark as authenticated
133
+ setAuthenticated(true);
134
+ } else {
135
+ // Token mode: Extract and store tokens
136
+ const newTokens = config.extractTokens(res.data);
137
+ setTokens(newTokens);
138
+ setAxiosAuth(newTokens.accessToken, newTokens.tokenType);
139
+ }
140
+
141
+ // Extract user from response
142
+ const extractedUser = config.extractUser?.(res.data);
143
+ if (extractedUser) {
144
+ setUser(extractedUser);
145
+ config.onLogin?.(extractedUser);
146
+ } else if (config.getUserUrl) {
147
+ await getCurrentUser();
148
+ const currentUser = store.getState().user;
149
+ if (currentUser) config.onLogin?.(currentUser);
150
+ }
151
+
152
+ callback?.();
153
+ } catch (error) {
125
154
  unsetUser();
126
155
  setAxiosAuth();
127
- config.onError?.(err);
156
+ config.onError?.(error);
157
+ throw error;
128
158
  }
129
159
  };
130
160
 
161
+ // Logout
131
162
  const logout = async () => {
132
163
  try {
133
- // If we have a revoke/logout URL, call it
134
- const logoutUrl = config.revokeUrl || config.logoutUrl;
135
- if (logoutUrl) {
136
- if (config.revokeUrl && tokens?.refreshToken) {
137
- // OAuth revoke
138
- await config.axios.post(logoutUrl, {
139
- token: tokens.refreshToken,
140
- token_type_hint: 'refresh_token'
141
- });
142
- } else {
143
- // Simple logout
144
- await config.axios.post(logoutUrl);
145
- }
164
+ if (config.logoutUrl) {
165
+ const headers = config.cookieAuth?.enabled ? getCsrfHeaders(config) : {};
166
+ await config.axios.post(config.logoutUrl, {}, { headers });
146
167
  }
147
- } catch (err) {
148
- config.onError?.(err);
168
+ } catch (error) {
169
+ config.onError?.(error);
149
170
  } finally {
150
171
  unsetUser();
151
172
  setAxiosAuth();
@@ -153,5 +174,64 @@ export function useAuth<U>(store: AuthStore<U>) {
153
174
  }
154
175
  };
155
176
 
156
- return { login, getCurrentUser, logout, refreshTokens };
157
- }
177
+ // Auto-setup on mount
178
+ useEffect(() => {
179
+ // Cookie mode: Check authentication if not yet determined
180
+ if (config.cookieAuth?.enabled) {
181
+ if (isAuthenticated === null) {
182
+ checkAuth();
183
+ }
184
+ return;
185
+ }
186
+
187
+ // Token mode: Setup headers and auto-refresh
188
+ if (tokens?.accessToken) {
189
+ setAxiosAuth(tokens.accessToken, tokens.tokenType);
190
+
191
+ // Check if expired
192
+ if (isTokenExpired()) {
193
+ if (tokens.refreshToken && config.autoRefresh) {
194
+ refresh();
195
+ } else {
196
+ unsetUser();
197
+ }
198
+ return;
199
+ }
200
+
201
+ // Setup auto-refresh timer
202
+ if (tokens.expiresAt && tokens.refreshToken && config.autoRefresh) {
203
+ const timeUntilExpiry = tokens.expiresAt - Date.now();
204
+ const refreshTime = Math.max(timeUntilExpiry - config.refreshThreshold, 0);
205
+
206
+ const timer = setTimeout(refresh, refreshTime);
207
+ return () => clearTimeout(timer);
208
+ }
209
+
210
+ // Fetch user if missing
211
+ if (!user && config.getUserUrl) {
212
+ getCurrentUser().catch(() => {});
213
+ }
214
+ }
215
+ }, [tokens, user, isAuthenticated, config, isTokenExpired, refresh, checkAuth, getCurrentUser, setAxiosAuth, unsetUser]);
216
+
217
+ return { login, logout, refresh, checkAuth, getCurrentUser };
218
+ }
219
+
220
+ // Helper: Get cookie value
221
+ function getCookie(name: string): string | null {
222
+ if (typeof document === 'undefined') return null;
223
+ const match = document.cookie.match(new RegExp(`(^| )${name}=([^;]+)`));
224
+ return match ? match[2] : null;
225
+ }
226
+
227
+ // Helper: Get CSRF headers if enabled
228
+ function getCsrfHeaders(config: any): Record<string, string> {
229
+ const headers: Record<string, string> = {};
230
+ if (config.cookieAuth?.csrf?.enabled) {
231
+ const csrfToken = getCookie(config.cookieAuth.csrf.cookieName);
232
+ if (csrfToken) {
233
+ headers[config.cookieAuth.csrf.headerName] = csrfToken;
234
+ }
235
+ }
236
+ return headers;
237
+ }
@@ -1 +0,0 @@
1
- import '@testing-library/react';