@startsimpli/auth 0.1.0 → 0.1.3

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.
Files changed (39) hide show
  1. package/dist/chunk-CDNZRZ7Q.mjs +767 -0
  2. package/dist/chunk-CDNZRZ7Q.mjs.map +1 -0
  3. package/dist/chunk-S6J5FYQY.mjs +134 -0
  4. package/dist/chunk-S6J5FYQY.mjs.map +1 -0
  5. package/dist/chunk-TA46ASDJ.mjs +37 -0
  6. package/dist/chunk-TA46ASDJ.mjs.map +1 -0
  7. package/dist/client/index.d.mts +175 -0
  8. package/dist/client/index.d.ts +175 -0
  9. package/dist/client/index.js +858 -0
  10. package/dist/client/index.js.map +1 -0
  11. package/dist/client/index.mjs +5 -0
  12. package/dist/client/index.mjs.map +1 -0
  13. package/dist/index.d.mts +68 -0
  14. package/dist/index.d.ts +68 -0
  15. package/dist/index.js +971 -0
  16. package/dist/index.js.map +1 -0
  17. package/dist/index.mjs +5 -0
  18. package/dist/index.mjs.map +1 -0
  19. package/dist/server/index.d.mts +83 -0
  20. package/dist/server/index.d.ts +83 -0
  21. package/dist/server/index.js +242 -0
  22. package/dist/server/index.js.map +1 -0
  23. package/dist/server/index.mjs +191 -0
  24. package/dist/server/index.mjs.map +1 -0
  25. package/dist/types/index.d.mts +209 -0
  26. package/dist/types/index.d.ts +209 -0
  27. package/dist/types/index.js +43 -0
  28. package/dist/types/index.js.map +1 -0
  29. package/dist/types/index.mjs +3 -0
  30. package/dist/types/index.mjs.map +1 -0
  31. package/package.json +50 -18
  32. package/src/__tests__/auth-client.test.ts +125 -0
  33. package/src/__tests__/auth-fetch.test.ts +128 -0
  34. package/src/__tests__/token-storage.test.ts +61 -0
  35. package/src/__tests__/validation.test.ts +60 -0
  36. package/src/client/auth-client.ts +11 -1
  37. package/src/client/functions.ts +83 -14
  38. package/src/types/index.ts +100 -0
  39. package/src/utils/validation.ts +190 -0
@@ -0,0 +1,858 @@
1
+ 'use strict';
2
+
3
+ var react = require('react');
4
+ var jsxRuntime = require('react/jsx-runtime');
5
+
6
+ // src/utils/token.ts
7
+ function decodeToken(token) {
8
+ try {
9
+ const parts = token.split(".");
10
+ if (parts.length !== 3) {
11
+ return null;
12
+ }
13
+ const payload = parts[1];
14
+ const decoded = JSON.parse(atob(payload));
15
+ return decoded;
16
+ } catch (error) {
17
+ console.error("Failed to decode token:", error);
18
+ return null;
19
+ }
20
+ }
21
+ function isTokenExpired(token) {
22
+ const payload = decodeToken(token);
23
+ if (!payload) {
24
+ return true;
25
+ }
26
+ const now = Math.floor(Date.now() / 1e3);
27
+ return payload.exp <= now;
28
+ }
29
+ function getTokenExpiresAt(token) {
30
+ const payload = decodeToken(token);
31
+ if (!payload) {
32
+ return null;
33
+ }
34
+ return payload.exp * 1e3;
35
+ }
36
+ function shouldRefreshToken(token) {
37
+ const expiresAt = getTokenExpiresAt(token);
38
+ if (!expiresAt) {
39
+ return true;
40
+ }
41
+ const now = Date.now();
42
+ const timeUntilExpiry = expiresAt - now;
43
+ const FIVE_MINUTES = 5 * 60 * 1e3;
44
+ return timeUntilExpiry < FIVE_MINUTES;
45
+ }
46
+
47
+ // src/utils/cookies.ts
48
+ function getCookie(name) {
49
+ if (typeof document === "undefined") {
50
+ return null;
51
+ }
52
+ const value = `; ${document.cookie}`;
53
+ const parts = value.split(`; ${name}=`);
54
+ if (parts.length === 2) {
55
+ return parts.pop()?.split(";").shift() || null;
56
+ }
57
+ return null;
58
+ }
59
+ function getCsrfToken() {
60
+ return getCookie("csrftoken");
61
+ }
62
+
63
+ // src/client/auth-client.ts
64
+ var AuthClient = class {
65
+ constructor(config) {
66
+ this.session = null;
67
+ this.refreshTimer = null;
68
+ this.isRefreshing = false;
69
+ this.refreshPromise = null;
70
+ this.config = {
71
+ tokenRefreshInterval: 4 * 60 * 1e3,
72
+ // 4 minutes
73
+ onSessionExpired: () => {
74
+ },
75
+ onUnauthorized: () => {
76
+ },
77
+ ...config
78
+ };
79
+ }
80
+ /**
81
+ * Login with email and password
82
+ */
83
+ async login(email, password) {
84
+ const response = await fetch(`${this.config.apiBaseUrl}/api/v1/auth/token/`, {
85
+ method: "POST",
86
+ headers: { "Content-Type": "application/json" },
87
+ credentials: "include",
88
+ // Include cookies for refresh token
89
+ body: JSON.stringify({ email, password })
90
+ });
91
+ if (!response.ok) {
92
+ const error = await response.json().catch(() => ({ detail: "Login failed" }));
93
+ throw new Error(error.detail || "Login failed");
94
+ }
95
+ const data = await response.json();
96
+ const expiresAt = getTokenExpiresAt(data.access);
97
+ if (!expiresAt) {
98
+ throw new Error("Invalid token received");
99
+ }
100
+ const tempSession = {
101
+ user: data.user || { id: "", email: "", firstName: "", lastName: "", isEmailVerified: false, createdAt: "", updatedAt: "" },
102
+ accessToken: data.access,
103
+ expiresAt
104
+ };
105
+ this.session = tempSession;
106
+ if (!data.user) {
107
+ try {
108
+ const user = await this.getCurrentUser();
109
+ this.session.user = user;
110
+ } catch (error) {
111
+ console.error("Failed to fetch user data after login:", error);
112
+ }
113
+ }
114
+ this.startRefreshTimer();
115
+ return this.session;
116
+ }
117
+ /**
118
+ * Logout and clear session
119
+ */
120
+ async logout() {
121
+ try {
122
+ await fetch(`${this.config.apiBaseUrl}/api/v1/auth/logout/`, {
123
+ method: "POST",
124
+ headers: this.getAuthHeaders(),
125
+ credentials: "include"
126
+ });
127
+ } catch (error) {
128
+ console.error("Logout error:", error);
129
+ } finally {
130
+ this.clearSession();
131
+ }
132
+ }
133
+ /**
134
+ * Refresh access token using refresh token cookie
135
+ */
136
+ async refreshToken() {
137
+ if (this.isRefreshing && this.refreshPromise) {
138
+ return this.refreshPromise;
139
+ }
140
+ this.isRefreshing = true;
141
+ this.refreshPromise = this.performTokenRefresh();
142
+ try {
143
+ const newToken = await this.refreshPromise;
144
+ return newToken;
145
+ } finally {
146
+ this.isRefreshing = false;
147
+ this.refreshPromise = null;
148
+ }
149
+ }
150
+ async performTokenRefresh() {
151
+ const response = await fetch(`${this.config.apiBaseUrl}/api/v1/auth/token/refresh/`, {
152
+ method: "POST",
153
+ headers: { "Content-Type": "application/json" },
154
+ credentials: "include"
155
+ // Send refresh token cookie
156
+ });
157
+ if (!response.ok) {
158
+ this.clearSession();
159
+ this.config.onSessionExpired();
160
+ throw new Error("Token refresh failed");
161
+ }
162
+ const data = await response.json();
163
+ const expiresAt = getTokenExpiresAt(data.access);
164
+ if (!expiresAt) {
165
+ throw new Error("Invalid token received");
166
+ }
167
+ if (this.session) {
168
+ this.session.accessToken = data.access;
169
+ this.session.expiresAt = expiresAt;
170
+ }
171
+ this.startRefreshTimer();
172
+ return data.access;
173
+ }
174
+ /**
175
+ * Get current user data from backend
176
+ */
177
+ async getCurrentUser() {
178
+ const response = await fetch(`${this.config.apiBaseUrl}/api/v1/auth/me/`, {
179
+ headers: this.getAuthHeaders(),
180
+ credentials: "include"
181
+ });
182
+ if (!response.ok) {
183
+ if (response.status === 401) {
184
+ this.config.onUnauthorized();
185
+ }
186
+ throw new Error("Failed to fetch user data");
187
+ }
188
+ const data = await response.json();
189
+ const raw = data.user || data;
190
+ const user = {
191
+ id: raw.id,
192
+ email: raw.email,
193
+ firstName: raw.first_name || raw.firstName || "",
194
+ lastName: raw.last_name || raw.lastName || "",
195
+ isEmailVerified: raw.is_email_verified ?? raw.isEmailVerified ?? false,
196
+ createdAt: raw.created_at || raw.createdAt || "",
197
+ updatedAt: raw.updated_at || raw.updatedAt || ""
198
+ };
199
+ if (this.session) {
200
+ this.session.user = user;
201
+ }
202
+ return user;
203
+ }
204
+ /**
205
+ * Get current session
206
+ */
207
+ getSession() {
208
+ if (!this.session) {
209
+ return null;
210
+ }
211
+ if (isTokenExpired(this.session.accessToken)) {
212
+ this.clearSession();
213
+ this.config.onSessionExpired();
214
+ return null;
215
+ }
216
+ return this.session;
217
+ }
218
+ /**
219
+ * Set session (for SSR/hydration)
220
+ */
221
+ setSession(session) {
222
+ this.session = session;
223
+ this.startRefreshTimer();
224
+ }
225
+ /**
226
+ * Get auth headers for API requests
227
+ */
228
+ getAuthHeaders() {
229
+ const session = this.getSession();
230
+ if (!session) {
231
+ return {};
232
+ }
233
+ return {
234
+ Authorization: `Bearer ${session.accessToken}`
235
+ };
236
+ }
237
+ /**
238
+ * Get valid access token (refreshes if needed)
239
+ */
240
+ async getAccessToken() {
241
+ const session = this.getSession();
242
+ if (!session) {
243
+ return null;
244
+ }
245
+ if (shouldRefreshToken(session.accessToken)) {
246
+ try {
247
+ return await this.refreshToken();
248
+ } catch (error) {
249
+ console.error("Token refresh failed:", error);
250
+ return null;
251
+ }
252
+ }
253
+ return session.accessToken;
254
+ }
255
+ /**
256
+ * Start automatic token refresh timer
257
+ */
258
+ startRefreshTimer() {
259
+ if (this.refreshTimer) {
260
+ clearInterval(this.refreshTimer);
261
+ }
262
+ this.refreshTimer = setInterval(() => {
263
+ const session = this.getSession();
264
+ if (session && shouldRefreshToken(session.accessToken)) {
265
+ this.refreshToken().catch((error) => {
266
+ console.error("Auto-refresh failed:", error);
267
+ });
268
+ }
269
+ }, this.config.tokenRefreshInterval);
270
+ }
271
+ /**
272
+ * Clear session and stop refresh timer
273
+ */
274
+ clearSession() {
275
+ this.session = null;
276
+ if (this.refreshTimer) {
277
+ clearInterval(this.refreshTimer);
278
+ this.refreshTimer = null;
279
+ }
280
+ }
281
+ /**
282
+ * Cleanup resources
283
+ */
284
+ destroy() {
285
+ this.clearSession();
286
+ }
287
+ };
288
+ var AuthContext = react.createContext(void 0);
289
+ function AuthProvider({
290
+ children,
291
+ config,
292
+ initialSession
293
+ }) {
294
+ const [authClient] = react.useState(() => new AuthClient(config));
295
+ const [state, setState] = react.useState(() => ({
296
+ session: initialSession || null,
297
+ isLoading: !initialSession,
298
+ isAuthenticated: !!initialSession
299
+ }));
300
+ react.useEffect(() => {
301
+ if (initialSession) {
302
+ authClient.setSession(initialSession);
303
+ setState({
304
+ session: initialSession,
305
+ isLoading: false,
306
+ isAuthenticated: true
307
+ });
308
+ } else {
309
+ const session = authClient.getSession();
310
+ setState({
311
+ session,
312
+ isLoading: false,
313
+ isAuthenticated: !!session
314
+ });
315
+ }
316
+ }, [authClient, initialSession]);
317
+ react.useEffect(() => {
318
+ const originalOnExpired = config.onSessionExpired;
319
+ config.onSessionExpired = () => {
320
+ setState({
321
+ session: null,
322
+ isLoading: false,
323
+ isAuthenticated: false
324
+ });
325
+ originalOnExpired?.();
326
+ };
327
+ return () => {
328
+ authClient.destroy();
329
+ };
330
+ }, [authClient, config]);
331
+ const login = react.useCallback(
332
+ async (email, password) => {
333
+ setState((prev) => ({ ...prev, isLoading: true }));
334
+ try {
335
+ const session = await authClient.login(email, password);
336
+ setState({
337
+ session,
338
+ isLoading: false,
339
+ isAuthenticated: true
340
+ });
341
+ } catch (error) {
342
+ setState((prev) => ({ ...prev, isLoading: false }));
343
+ throw error;
344
+ }
345
+ },
346
+ [authClient]
347
+ );
348
+ const logout = react.useCallback(async () => {
349
+ setState((prev) => ({ ...prev, isLoading: true }));
350
+ try {
351
+ await authClient.logout();
352
+ } finally {
353
+ setState({
354
+ session: null,
355
+ isLoading: false,
356
+ isAuthenticated: false
357
+ });
358
+ }
359
+ }, [authClient]);
360
+ const refreshUser = react.useCallback(async () => {
361
+ try {
362
+ const user = await authClient.getCurrentUser();
363
+ setState((prev) => ({
364
+ ...prev,
365
+ session: prev.session ? { ...prev.session, user } : null
366
+ }));
367
+ } catch (error) {
368
+ console.error("Failed to refresh user:", error);
369
+ }
370
+ }, [authClient]);
371
+ const getAccessToken2 = react.useCallback(async () => {
372
+ return authClient.getAccessToken();
373
+ }, [authClient]);
374
+ const value = {
375
+ ...state,
376
+ login,
377
+ logout,
378
+ refreshUser,
379
+ getAccessToken: getAccessToken2
380
+ };
381
+ return /* @__PURE__ */ jsxRuntime.jsx(AuthContext.Provider, { value, children });
382
+ }
383
+ function useAuthContext() {
384
+ const context = react.useContext(AuthContext);
385
+ if (!context) {
386
+ throw new Error("useAuthContext must be used within AuthProvider");
387
+ }
388
+ return context;
389
+ }
390
+
391
+ // src/client/use-auth.ts
392
+ function useAuth() {
393
+ const {
394
+ session,
395
+ isLoading,
396
+ isAuthenticated,
397
+ login,
398
+ logout,
399
+ refreshUser,
400
+ getAccessToken: getAccessToken2
401
+ } = useAuthContext();
402
+ return {
403
+ user: session?.user || null,
404
+ session,
405
+ isLoading,
406
+ isAuthenticated,
407
+ login,
408
+ logout,
409
+ refreshUser,
410
+ getAccessToken: getAccessToken2
411
+ };
412
+ }
413
+
414
+ // src/types/index.ts
415
+ var ROLE_HIERARCHY = {
416
+ owner: 4,
417
+ admin: 3,
418
+ member: 2,
419
+ viewer: 1
420
+ };
421
+ function hasRolePermission(userRole, requiredRole) {
422
+ return ROLE_HIERARCHY[userRole] >= ROLE_HIERARCHY[requiredRole];
423
+ }
424
+
425
+ // src/client/use-permissions.ts
426
+ function usePermissions() {
427
+ const { user } = useAuth();
428
+ const currentCompanyId = user?.currentCompanyId || null;
429
+ const currentCompany = react.useMemo(() => {
430
+ if (!user?.companies || !currentCompanyId) {
431
+ return null;
432
+ }
433
+ return user.companies.find((c) => c.id === currentCompanyId);
434
+ }, [user, currentCompanyId]);
435
+ const currentRole = currentCompany?.role || null;
436
+ const hasRole = (requiredRole, companyId) => {
437
+ if (!user?.companies) {
438
+ return false;
439
+ }
440
+ const targetCompanyId = companyId || currentCompanyId;
441
+ if (!targetCompanyId) {
442
+ return false;
443
+ }
444
+ const company = user.companies.find((c) => c.id === targetCompanyId);
445
+ if (!company) {
446
+ return false;
447
+ }
448
+ return hasRolePermission(company.role, requiredRole);
449
+ };
450
+ const isOwner = (companyId) => {
451
+ return hasRole("owner", companyId);
452
+ };
453
+ const isAdmin = (companyId) => {
454
+ return hasRole("admin", companyId);
455
+ };
456
+ const canEdit = (companyId) => {
457
+ return hasRole("member", companyId);
458
+ };
459
+ const canView = (companyId) => {
460
+ return hasRole("viewer", companyId);
461
+ };
462
+ return {
463
+ hasRole,
464
+ isOwner,
465
+ isAdmin,
466
+ canEdit,
467
+ canView,
468
+ currentRole,
469
+ currentCompanyId
470
+ };
471
+ }
472
+
473
+ // src/client/functions.ts
474
+ var API_BASE = "/api/v1";
475
+ var AUTH_PATHS = {
476
+ TOKEN: `${API_BASE}/auth/token/`,
477
+ TOKEN_REFRESH: `${API_BASE}/auth/token/refresh/`,
478
+ REGISTER: `${API_BASE}/auth/register/`,
479
+ LOGOUT: `${API_BASE}/auth/logout/`,
480
+ FORGOT_PASSWORD: `${API_BASE}/auth/forgot-password/`,
481
+ RESET_PASSWORD: `${API_BASE}/auth/reset-password/`,
482
+ VERIFY_EMAIL: `${API_BASE}/auth/verify-email/`,
483
+ RESEND_VERIFICATION: `${API_BASE}/auth/resend-verification/`,
484
+ OAUTH_GOOGLE_INITIATE: `${API_BASE}/auth/oauth/google/initiate/`,
485
+ OAUTH_GOOGLE_CALLBACK: `${API_BASE}/auth/oauth/google/callback/`,
486
+ ME: `${API_BASE}/auth/me/`
487
+ };
488
+ var AUTH_BASE_URL = typeof process !== "undefined" && (process.env.NEXT_PUBLIC_API_URL || process.env.NEXT_PUBLIC_API_BASE_URL) || "";
489
+ var AUTH_BASE_IS_ABSOLUTE = /^https?:\/\//i.test(AUTH_BASE_URL);
490
+ function isAbsoluteUrl(url) {
491
+ return /^https?:\/\//i.test(url);
492
+ }
493
+ function resolveAuthUrl(path) {
494
+ if (isAbsoluteUrl(path)) return path;
495
+ const normalized = path.startsWith("/") ? path : `/${path}`;
496
+ if (AUTH_BASE_IS_ABSOLUTE) {
497
+ return new URL(normalized, AUTH_BASE_URL).toString();
498
+ }
499
+ if (normalized.startsWith("/api/")) return normalized;
500
+ if (!AUTH_BASE_URL) return normalized;
501
+ if (AUTH_BASE_URL.endsWith("/") && normalized.startsWith("/")) {
502
+ return `${AUTH_BASE_URL.slice(0, -1)}${normalized}`;
503
+ }
504
+ return `${AUTH_BASE_URL}${normalized}`;
505
+ }
506
+ var TOKEN_STORAGE_KEY = "auth_access_token";
507
+ var _memToken = null;
508
+ function _sessionStorageAvailable() {
509
+ try {
510
+ return typeof window !== "undefined" && !!window.sessionStorage;
511
+ } catch {
512
+ return false;
513
+ }
514
+ }
515
+ function getAccessToken() {
516
+ if (_sessionStorageAvailable()) {
517
+ return sessionStorage.getItem(TOKEN_STORAGE_KEY);
518
+ }
519
+ return _memToken;
520
+ }
521
+ function setAccessToken(token) {
522
+ if (_sessionStorageAvailable()) {
523
+ if (token === null) {
524
+ sessionStorage.removeItem(TOKEN_STORAGE_KEY);
525
+ } else {
526
+ sessionStorage.setItem(TOKEN_STORAGE_KEY, token);
527
+ }
528
+ return;
529
+ }
530
+ _memToken = token;
531
+ }
532
+ var AUTH_TIMEOUT_MS = 15e3;
533
+ function extractApiError(d, fallback) {
534
+ if (typeof d.detail === "string") return d.detail;
535
+ for (const val of Object.values(d)) {
536
+ if (typeof val === "string") return val;
537
+ if (Array.isArray(val) && val.length > 0 && typeof val[0] === "string") return val[0];
538
+ }
539
+ return fallback;
540
+ }
541
+ function fetchWithTimeout(url, options) {
542
+ const controller = new AbortController();
543
+ const timer = setTimeout(() => controller.abort(), AUTH_TIMEOUT_MS);
544
+ return fetch(url, { ...options, signal: controller.signal }).finally(
545
+ () => clearTimeout(timer)
546
+ );
547
+ }
548
+ function normalizeUser(raw) {
549
+ if (!raw || typeof raw !== "object") return null;
550
+ const obj = raw;
551
+ const payload = obj.user && typeof obj.user === "object" ? obj.user : obj;
552
+ if (!payload?.id || !payload?.email) return null;
553
+ const firstName = payload.first_name ?? payload.firstName ?? null;
554
+ const lastName = payload.last_name ?? payload.lastName ?? null;
555
+ const name = payload.name ?? ([firstName, lastName].filter(Boolean).join(" ") || null);
556
+ return {
557
+ id: payload.id,
558
+ email: payload.email,
559
+ name,
560
+ firstName,
561
+ lastName,
562
+ groups: Array.isArray(payload.groups) ? payload.groups : [],
563
+ permissions: Array.isArray(payload.permissions) ? payload.permissions : [],
564
+ isActive: payload.isActive ?? payload.is_active,
565
+ isEmailVerified: payload.isEmailVerified ?? payload.is_email_verified
566
+ };
567
+ }
568
+ function parseAuthResponse(data) {
569
+ if (!data || typeof data !== "object") return {};
570
+ const obj = data;
571
+ const payload = obj.data && typeof obj.data === "object" ? obj.data : obj;
572
+ const access = payload.access || payload.access_token || payload.token;
573
+ const userRaw = payload.user || payload.me || payload.profile || payload;
574
+ const user = normalizeUser(userRaw);
575
+ return { access, user: user ?? void 0 };
576
+ }
577
+ async function signInWithCredentials(email, password) {
578
+ const response = await fetchWithTimeout(resolveAuthUrl(AUTH_PATHS.TOKEN), {
579
+ method: "POST",
580
+ headers: { "Content-Type": "application/json" },
581
+ credentials: "include",
582
+ body: JSON.stringify({ email, password })
583
+ });
584
+ const data = await response.json().catch(() => ({}));
585
+ if (!response.ok) {
586
+ const d = data;
587
+ if (d?.error === "ACCOUNT_LOCKED" && d?.locked_until) {
588
+ throw new Error(`Account locked until ${new Date(d.locked_until).toLocaleTimeString()}`);
589
+ }
590
+ const message = d?.detail || d?.error || "Authentication failed";
591
+ throw new Error(message);
592
+ }
593
+ const parsed = parseAuthResponse(data);
594
+ if (parsed.access) {
595
+ setAccessToken(parsed.access);
596
+ }
597
+ return parsed;
598
+ }
599
+ async function registerAccount(payload) {
600
+ const rawName = payload.name?.trim() || "";
601
+ const [firstFromName, ...rest] = rawName ? rawName.split(/\s+/) : [];
602
+ const lastFromName = rest.length ? rest.join(" ") : void 0;
603
+ const response = await fetchWithTimeout(resolveAuthUrl(AUTH_PATHS.REGISTER), {
604
+ method: "POST",
605
+ headers: { "Content-Type": "application/json" },
606
+ credentials: "include",
607
+ body: JSON.stringify({
608
+ email: payload.email,
609
+ password: payload.password,
610
+ password_confirm: payload.passwordConfirm,
611
+ first_name: payload.firstName ?? firstFromName ?? void 0,
612
+ last_name: payload.lastName ?? lastFromName ?? void 0
613
+ })
614
+ });
615
+ const data = await response.json().catch(() => ({}));
616
+ if (!response.ok) {
617
+ throw new Error(extractApiError(data, "Registration failed"));
618
+ }
619
+ const parsed = parseAuthResponse(data);
620
+ if (parsed.access) {
621
+ setAccessToken(parsed.access);
622
+ }
623
+ return parsed;
624
+ }
625
+ async function requestPasswordReset(email) {
626
+ const response = await fetch(resolveAuthUrl(AUTH_PATHS.FORGOT_PASSWORD), {
627
+ method: "POST",
628
+ headers: { "Content-Type": "application/json" },
629
+ body: JSON.stringify({ email })
630
+ });
631
+ if (!response.ok) {
632
+ const data = await response.json().catch(() => ({}));
633
+ const d = data;
634
+ const message = d?.detail || d?.error || "Failed to send reset link";
635
+ throw new Error(message);
636
+ }
637
+ }
638
+ async function resetPassword(payload) {
639
+ const response = await fetchWithTimeout(resolveAuthUrl(AUTH_PATHS.RESET_PASSWORD), {
640
+ method: "POST",
641
+ headers: { "Content-Type": "application/json" },
642
+ body: JSON.stringify({
643
+ token: payload.token,
644
+ password: payload.password,
645
+ password_confirm: payload.passwordConfirm,
646
+ ...payload.email ? { email: payload.email } : {}
647
+ })
648
+ });
649
+ if (!response.ok) {
650
+ const data = await response.json().catch(() => ({}));
651
+ const d = data;
652
+ const message = d?.detail || d?.error || "Failed to reset password";
653
+ throw new Error(message);
654
+ }
655
+ }
656
+ async function verifyEmail(token) {
657
+ const response = await fetchWithTimeout(resolveAuthUrl(AUTH_PATHS.VERIFY_EMAIL), {
658
+ method: "POST",
659
+ headers: { "Content-Type": "application/json" },
660
+ body: JSON.stringify({ token })
661
+ });
662
+ if (!response.ok) {
663
+ const data = await response.json().catch(() => ({}));
664
+ const d = data;
665
+ const message = d?.detail || d?.error || "Failed to verify email";
666
+ throw new Error(message);
667
+ }
668
+ }
669
+ async function resendVerification(access) {
670
+ const response = await fetch(resolveAuthUrl(AUTH_PATHS.RESEND_VERIFICATION), {
671
+ method: "POST",
672
+ headers: {
673
+ "Content-Type": "application/json",
674
+ ...access ? { Authorization: `Bearer ${access}` } : {}
675
+ },
676
+ credentials: "include"
677
+ });
678
+ if (!response.ok) {
679
+ const data = await response.json().catch(() => ({}));
680
+ const d = data;
681
+ const message = d?.detail || d?.error || "Failed to resend verification email";
682
+ throw new Error(message);
683
+ }
684
+ }
685
+ async function initiateGoogleOAuth(redirectUri) {
686
+ const response = await fetch(resolveAuthUrl(AUTH_PATHS.OAUTH_GOOGLE_INITIATE), {
687
+ method: "POST",
688
+ headers: { "Content-Type": "application/json" },
689
+ credentials: "include",
690
+ body: JSON.stringify({ redirect_uri: redirectUri })
691
+ });
692
+ const data = await response.json().catch(() => ({}));
693
+ if (!response.ok) {
694
+ const d = data;
695
+ const message = d?.detail || d?.error || "Failed to initiate Google OAuth";
696
+ throw new Error(message);
697
+ }
698
+ return data;
699
+ }
700
+ async function completeGoogleOAuth(code, state) {
701
+ const response = await fetchWithTimeout(
702
+ resolveAuthUrl(
703
+ `${AUTH_PATHS.OAUTH_GOOGLE_CALLBACK}?code=${encodeURIComponent(code)}&state=${encodeURIComponent(state)}`
704
+ ),
705
+ { credentials: "include" }
706
+ );
707
+ const data = await response.json().catch(() => ({}));
708
+ if (!response.ok) {
709
+ const d = data;
710
+ const message = d?.detail || d?.error || "OAuth authentication failed";
711
+ throw new Error(message);
712
+ }
713
+ const parsed = parseAuthResponse(data);
714
+ if (parsed.access) {
715
+ setAccessToken(parsed.access);
716
+ }
717
+ return parsed;
718
+ }
719
+ async function fetchCsrfToken() {
720
+ if (getCsrfToken()) return;
721
+ try {
722
+ await fetch(resolveAuthUrl(`${API_BASE}/auth/csrf/`), {
723
+ credentials: "include",
724
+ cache: "no-store"
725
+ });
726
+ } catch (err) {
727
+ console.warn("[auth] CSRF token fetch failed \u2014 token refresh will fail:", err);
728
+ }
729
+ }
730
+ async function refreshAccessToken() {
731
+ await fetchCsrfToken();
732
+ const csrfToken = getCsrfToken();
733
+ if (!csrfToken) return null;
734
+ const response = await fetchWithTimeout(resolveAuthUrl(AUTH_PATHS.TOKEN_REFRESH), {
735
+ method: "POST",
736
+ headers: {
737
+ "Content-Type": "application/json",
738
+ "X-CSRFToken": csrfToken
739
+ },
740
+ credentials: "include"
741
+ });
742
+ const data = await response.json().catch(() => ({}));
743
+ if (!response.ok) {
744
+ return null;
745
+ }
746
+ const parsed = parseAuthResponse(data);
747
+ if (parsed.access) {
748
+ setAccessToken(parsed.access);
749
+ return parsed.access;
750
+ }
751
+ return null;
752
+ }
753
+ async function getMe() {
754
+ const token = getAccessToken();
755
+ if (!token) return null;
756
+ const response = await fetch(resolveAuthUrl(AUTH_PATHS.ME), {
757
+ headers: {
758
+ Authorization: `Bearer ${token}`
759
+ },
760
+ credentials: "include",
761
+ cache: "no-store"
762
+ });
763
+ if (!response.ok) {
764
+ return null;
765
+ }
766
+ const data = await response.json().catch(() => ({}));
767
+ const obj = data;
768
+ const payload = obj.data && typeof obj.data === "object" ? obj.data : obj;
769
+ const userRaw = payload.user || payload;
770
+ return normalizeUser(userRaw);
771
+ }
772
+ async function signOut() {
773
+ const csrfToken = getCsrfToken();
774
+ const token = getAccessToken();
775
+ try {
776
+ await fetch(resolveAuthUrl(AUTH_PATHS.LOGOUT), {
777
+ method: "POST",
778
+ headers: {
779
+ "Content-Type": "application/json",
780
+ ...csrfToken ? { "X-CSRFToken": csrfToken } : {},
781
+ ...token ? { Authorization: `Bearer ${token}` } : {}
782
+ },
783
+ credentials: "include"
784
+ });
785
+ } finally {
786
+ setAccessToken(null);
787
+ }
788
+ }
789
+ var _refreshPromise = null;
790
+ function _refreshOnce() {
791
+ if (!_refreshPromise) {
792
+ _refreshPromise = refreshAccessToken().finally(() => {
793
+ _refreshPromise = null;
794
+ });
795
+ }
796
+ return _refreshPromise;
797
+ }
798
+ async function authFetch(input, init = {}) {
799
+ const token = getAccessToken();
800
+ const headers = new Headers(init.headers || {});
801
+ if (token && !headers.has("Authorization")) {
802
+ headers.set("Authorization", `Bearer ${token}`);
803
+ }
804
+ const resolvedInput = typeof input === "string" ? resolveAuthUrl(input) : input;
805
+ const response = await fetch(resolvedInput, {
806
+ ...init,
807
+ headers,
808
+ credentials: init.credentials ?? "include"
809
+ });
810
+ if (response.status === 401) {
811
+ const refreshed = await _refreshOnce();
812
+ if (refreshed) {
813
+ const retryHeaders = new Headers(init.headers || {});
814
+ retryHeaders.set("Authorization", `Bearer ${refreshed}`);
815
+ return fetch(resolvedInput, {
816
+ ...init,
817
+ headers: retryHeaders,
818
+ credentials: init.credentials ?? "include"
819
+ });
820
+ }
821
+ }
822
+ return response;
823
+ }
824
+ function hasPermission(user, permission) {
825
+ if (!user) return false;
826
+ if (!user.permissions || user.permissions.length === 0) return false;
827
+ return user.permissions.includes(permission);
828
+ }
829
+ function hasGroup(user, group) {
830
+ if (!user) return false;
831
+ if (!user.groups || user.groups.length === 0) return false;
832
+ return user.groups.includes(group);
833
+ }
834
+
835
+ exports.AuthClient = AuthClient;
836
+ exports.AuthProvider = AuthProvider;
837
+ exports.authFetch = authFetch;
838
+ exports.completeGoogleOAuth = completeGoogleOAuth;
839
+ exports.getAccessToken = getAccessToken;
840
+ exports.getMe = getMe;
841
+ exports.hasGroup = hasGroup;
842
+ exports.hasPermission = hasPermission;
843
+ exports.initiateGoogleOAuth = initiateGoogleOAuth;
844
+ exports.refreshAccessToken = refreshAccessToken;
845
+ exports.registerAccount = registerAccount;
846
+ exports.requestPasswordReset = requestPasswordReset;
847
+ exports.resendVerification = resendVerification;
848
+ exports.resetPassword = resetPassword;
849
+ exports.resolveAuthUrl = resolveAuthUrl;
850
+ exports.setAccessToken = setAccessToken;
851
+ exports.signInWithCredentials = signInWithCredentials;
852
+ exports.signOut = signOut;
853
+ exports.useAuth = useAuth;
854
+ exports.useAuthContext = useAuthContext;
855
+ exports.usePermissions = usePermissions;
856
+ exports.verifyEmail = verifyEmail;
857
+ //# sourceMappingURL=index.js.map
858
+ //# sourceMappingURL=index.js.map