nexu-app 2.0.0

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 (106) hide show
  1. package/README.md +149 -0
  2. package/dist/index.d.ts +2 -0
  3. package/dist/index.js +1192 -0
  4. package/package.json +43 -0
  5. package/templates/default/.changeset/config.json +11 -0
  6. package/templates/default/.eslintignore +16 -0
  7. package/templates/default/.eslintrc.js +67 -0
  8. package/templates/default/.github/actions/build/action.yml +35 -0
  9. package/templates/default/.github/actions/quality/action.yml +53 -0
  10. package/templates/default/.github/dependabot.yml +51 -0
  11. package/templates/default/.github/workflows/deploy-dev.yml +83 -0
  12. package/templates/default/.github/workflows/deploy-prod.yml +83 -0
  13. package/templates/default/.github/workflows/deploy-rec.yml +83 -0
  14. package/templates/default/.husky/commit-msg +1 -0
  15. package/templates/default/.husky/pre-commit +1 -0
  16. package/templates/default/.nexu-version +1 -0
  17. package/templates/default/.prettierignore +7 -0
  18. package/templates/default/.prettierrc +19 -0
  19. package/templates/default/.vscode/extensions.json +14 -0
  20. package/templates/default/.vscode/settings.json +36 -0
  21. package/templates/default/apps/gitkeep +0 -0
  22. package/templates/default/commitlint.config.js +26 -0
  23. package/templates/default/docker/docker-compose.dev.yml +49 -0
  24. package/templates/default/docker/docker-compose.prod.yml +64 -0
  25. package/templates/default/docker/docker-compose.yml +6 -0
  26. package/templates/default/docs/architecture.md +452 -0
  27. package/templates/default/docs/cli.md +330 -0
  28. package/templates/default/docs/contributing.md +462 -0
  29. package/templates/default/docs/scripts.md +460 -0
  30. package/templates/default/gitignore +44 -0
  31. package/templates/default/lintstagedrc.cjs +4 -0
  32. package/templates/default/package.json +51 -0
  33. package/templates/default/packages/auth/package.json +61 -0
  34. package/templates/default/packages/auth/src/components/ProtectedRoute.tsx +75 -0
  35. package/templates/default/packages/auth/src/components/SignInForm.tsx +153 -0
  36. package/templates/default/packages/auth/src/components/SignUpForm.tsx +179 -0
  37. package/templates/default/packages/auth/src/components/SocialButtons.tsx +147 -0
  38. package/templates/default/packages/auth/src/components/index.ts +4 -0
  39. package/templates/default/packages/auth/src/hooks/index.ts +4 -0
  40. package/templates/default/packages/auth/src/hooks/useAuth.ts +51 -0
  41. package/templates/default/packages/auth/src/hooks/useRequireAuth.ts +54 -0
  42. package/templates/default/packages/auth/src/hooks/useSession.ts +48 -0
  43. package/templates/default/packages/auth/src/hooks/useUser.ts +48 -0
  44. package/templates/default/packages/auth/src/index.ts +45 -0
  45. package/templates/default/packages/auth/src/next/index.ts +18 -0
  46. package/templates/default/packages/auth/src/next/middleware.ts +183 -0
  47. package/templates/default/packages/auth/src/next/server.ts +219 -0
  48. package/templates/default/packages/auth/src/providers/AuthContext.tsx +435 -0
  49. package/templates/default/packages/auth/src/providers/index.ts +1 -0
  50. package/templates/default/packages/auth/src/types/index.ts +284 -0
  51. package/templates/default/packages/auth/src/utils/api.ts +228 -0
  52. package/templates/default/packages/auth/src/utils/index.ts +3 -0
  53. package/templates/default/packages/auth/src/utils/oauth.ts +230 -0
  54. package/templates/default/packages/auth/src/utils/token.ts +204 -0
  55. package/templates/default/packages/auth/tsconfig.json +14 -0
  56. package/templates/default/packages/auth/tsup.config.ts +18 -0
  57. package/templates/default/packages/cache/package.json +26 -0
  58. package/templates/default/packages/cache/src/index.ts +137 -0
  59. package/templates/default/packages/cache/tsconfig.json +9 -0
  60. package/templates/default/packages/cache/tsup.config.ts +9 -0
  61. package/templates/default/packages/config/eslint/index.js +20 -0
  62. package/templates/default/packages/config/package.json +9 -0
  63. package/templates/default/packages/config/typescript/base.json +26 -0
  64. package/templates/default/packages/constants/package.json +26 -0
  65. package/templates/default/packages/constants/src/index.ts +121 -0
  66. package/templates/default/packages/constants/tsconfig.json +9 -0
  67. package/templates/default/packages/constants/tsup.config.ts +9 -0
  68. package/templates/default/packages/logger/package.json +27 -0
  69. package/templates/default/packages/logger/src/index.ts +197 -0
  70. package/templates/default/packages/logger/tsconfig.json +11 -0
  71. package/templates/default/packages/logger/tsup.config.ts +9 -0
  72. package/templates/default/packages/result/package.json +26 -0
  73. package/templates/default/packages/result/src/index.ts +142 -0
  74. package/templates/default/packages/result/tsconfig.json +9 -0
  75. package/templates/default/packages/result/tsup.config.ts +9 -0
  76. package/templates/default/packages/types/package.json +26 -0
  77. package/templates/default/packages/types/src/index.ts +78 -0
  78. package/templates/default/packages/types/tsconfig.json +9 -0
  79. package/templates/default/packages/types/tsup.config.ts +10 -0
  80. package/templates/default/packages/ui/package.json +38 -0
  81. package/templates/default/packages/ui/src/components/Button.tsx +58 -0
  82. package/templates/default/packages/ui/src/components/Card.tsx +85 -0
  83. package/templates/default/packages/ui/src/components/Input.tsx +45 -0
  84. package/templates/default/packages/ui/src/index.ts +15 -0
  85. package/templates/default/packages/ui/tsconfig.json +11 -0
  86. package/templates/default/packages/ui/tsup.config.ts +11 -0
  87. package/templates/default/packages/utils/package.json +30 -0
  88. package/templates/default/packages/utils/src/index.test.ts +130 -0
  89. package/templates/default/packages/utils/src/index.ts +154 -0
  90. package/templates/default/packages/utils/tsconfig.json +10 -0
  91. package/templates/default/packages/utils/tsup.config.ts +10 -0
  92. package/templates/default/pnpm-workspace.yaml +3 -0
  93. package/templates/default/scripts/audit.mjs +700 -0
  94. package/templates/default/scripts/deploy.mjs +40 -0
  95. package/templates/default/scripts/generate-app.mjs +808 -0
  96. package/templates/default/scripts/lib/package-manager.mjs +186 -0
  97. package/templates/default/scripts/setup.mjs +102 -0
  98. package/templates/default/services/.env.example +16 -0
  99. package/templates/default/services/docker-compose.yml +207 -0
  100. package/templates/default/services/grafana/provisioning/dashboards/dashboards.yml +11 -0
  101. package/templates/default/services/grafana/provisioning/datasources/datasources.yml +9 -0
  102. package/templates/default/services/postgres/init/gitkeep +2 -0
  103. package/templates/default/services/prometheus/prometheus.yml +13 -0
  104. package/templates/default/tsconfig.json +27 -0
  105. package/templates/default/turbo.json +40 -0
  106. package/templates/default/vitest.config.ts +15 -0
@@ -0,0 +1,228 @@
1
+ import type {
2
+ AuthConfig,
3
+ AuthError,
4
+ AuthErrorCode,
5
+ AuthResponse,
6
+ AuthUser,
7
+ SignInCredentials,
8
+ SignUpCredentials,
9
+ UpdatePasswordRequest,
10
+ } from '../types';
11
+
12
+ import { TokenManager } from './token';
13
+
14
+ /**
15
+ * Auth API client for making authenticated requests
16
+ */
17
+ export class AuthApiClient {
18
+ private baseUrl: string;
19
+ private headers: Record<string, string>;
20
+ private tokenManager: TokenManager;
21
+ private debug: boolean;
22
+
23
+ constructor(config: AuthConfig, tokenManager: TokenManager) {
24
+ this.baseUrl = config.apiBaseUrl.replace(/\/$/, '');
25
+ this.headers = config.headers || {};
26
+ this.tokenManager = tokenManager;
27
+ this.debug = config.debug || false;
28
+ }
29
+
30
+ private log(...args: unknown[]): void {
31
+ if (this.debug) {
32
+ // eslint-disable-next-line no-console
33
+ console.log('[Auth]', ...args);
34
+ }
35
+ }
36
+
37
+ private async request<T>(endpoint: string, options: RequestInit = {}): Promise<T> {
38
+ const url = `${this.baseUrl}${endpoint}`;
39
+ const token = this.tokenManager.getAccessToken();
40
+
41
+ const headers: Record<string, string> = {
42
+ 'Content-Type': 'application/json',
43
+ ...this.headers,
44
+ ...(options.headers as Record<string, string>),
45
+ };
46
+
47
+ if (token) {
48
+ headers['Authorization'] = `Bearer ${token}`;
49
+ }
50
+
51
+ this.log(`${options.method || 'GET'} ${url}`);
52
+
53
+ try {
54
+ const response = await fetch(url, {
55
+ ...options,
56
+ headers,
57
+ });
58
+
59
+ const data = (await response.json().catch(() => ({}))) as T & {
60
+ message?: string;
61
+ code?: string;
62
+ details?: unknown;
63
+ };
64
+
65
+ if (!response.ok) {
66
+ throw this.createError(response.status, data);
67
+ }
68
+
69
+ return data;
70
+ } catch (error) {
71
+ if ((error as AuthError).code) {
72
+ throw error;
73
+ }
74
+
75
+ throw this.createError(0, {
76
+ message: error instanceof Error ? error.message : 'Network error',
77
+ });
78
+ }
79
+ }
80
+
81
+ private createError(
82
+ status: number,
83
+ data: { message?: string; code?: string; details?: unknown }
84
+ ): AuthError {
85
+ let code: AuthErrorCode = 'UNKNOWN_ERROR';
86
+
87
+ if (data.code) {
88
+ code = data.code as AuthErrorCode;
89
+ } else if (status === 401) {
90
+ code = 'UNAUTHORIZED';
91
+ } else if (status === 403) {
92
+ code = 'FORBIDDEN';
93
+ } else if (status === 404) {
94
+ code = 'USER_NOT_FOUND';
95
+ } else if (status === 409) {
96
+ code = 'USER_ALREADY_EXISTS';
97
+ } else if (status === 0) {
98
+ code = 'NETWORK_ERROR';
99
+ }
100
+
101
+ return {
102
+ code,
103
+ message: data.message || 'An error occurred',
104
+ details: data.details,
105
+ };
106
+ }
107
+
108
+ /**
109
+ * Sign in with email and password
110
+ */
111
+ async signIn(credentials: SignInCredentials): Promise<AuthResponse> {
112
+ return this.request<AuthResponse>('/signin', {
113
+ method: 'POST',
114
+ body: JSON.stringify(credentials),
115
+ });
116
+ }
117
+
118
+ /**
119
+ * Sign up with email and password
120
+ */
121
+ async signUp(credentials: SignUpCredentials): Promise<AuthResponse> {
122
+ return this.request<AuthResponse>('/signup', {
123
+ method: 'POST',
124
+ body: JSON.stringify(credentials),
125
+ });
126
+ }
127
+
128
+ /**
129
+ * Sign out
130
+ */
131
+ async signOut(): Promise<void> {
132
+ const refreshToken = this.tokenManager.getRefreshToken();
133
+
134
+ await this.request('/signout', {
135
+ method: 'POST',
136
+ body: JSON.stringify({ refreshToken }),
137
+ });
138
+ }
139
+
140
+ /**
141
+ * Refresh the access token
142
+ */
143
+ async refreshToken(): Promise<AuthResponse> {
144
+ const refreshToken = this.tokenManager.getRefreshToken();
145
+
146
+ if (!refreshToken) {
147
+ throw this.createError(401, { message: 'No refresh token available' });
148
+ }
149
+
150
+ return this.request<AuthResponse>('/refresh', {
151
+ method: 'POST',
152
+ body: JSON.stringify({ refreshToken }),
153
+ });
154
+ }
155
+
156
+ /**
157
+ * Get the current user
158
+ */
159
+ async getUser(): Promise<AuthUser> {
160
+ return this.request<AuthUser>('/me');
161
+ }
162
+
163
+ /**
164
+ * Update the current user
165
+ */
166
+ async updateUser(data: Partial<AuthUser>): Promise<AuthUser> {
167
+ return this.request<AuthUser>('/me', {
168
+ method: 'PATCH',
169
+ body: JSON.stringify(data),
170
+ });
171
+ }
172
+
173
+ /**
174
+ * Request password reset
175
+ */
176
+ async resetPassword(email: string): Promise<void> {
177
+ await this.request('/reset-password', {
178
+ method: 'POST',
179
+ body: JSON.stringify({ email }),
180
+ });
181
+ }
182
+
183
+ /**
184
+ * Update password
185
+ */
186
+ async updatePassword(request: UpdatePasswordRequest): Promise<void> {
187
+ await this.request('/update-password', {
188
+ method: 'POST',
189
+ body: JSON.stringify(request),
190
+ });
191
+ }
192
+
193
+ /**
194
+ * Verify email
195
+ */
196
+ async verifyEmail(token: string): Promise<void> {
197
+ await this.request('/verify-email', {
198
+ method: 'POST',
199
+ body: JSON.stringify({ token }),
200
+ });
201
+ }
202
+
203
+ /**
204
+ * Resend verification email
205
+ */
206
+ async resendVerification(): Promise<void> {
207
+ await this.request('/resend-verification', {
208
+ method: 'POST',
209
+ });
210
+ }
211
+
212
+ /**
213
+ * Exchange OAuth code for tokens
214
+ */
215
+ async exchangeOAuthCode(provider: string, code: string, state?: string): Promise<AuthResponse> {
216
+ return this.request<AuthResponse>(`/oauth/${provider}/callback`, {
217
+ method: 'POST',
218
+ body: JSON.stringify({ code, state }),
219
+ });
220
+ }
221
+ }
222
+
223
+ /**
224
+ * Create an auth API client instance
225
+ */
226
+ export function createAuthApiClient(config: AuthConfig, tokenManager: TokenManager): AuthApiClient {
227
+ return new AuthApiClient(config, tokenManager);
228
+ }
@@ -0,0 +1,3 @@
1
+ export * from './token';
2
+ export * from './api';
3
+ export * from './oauth';
@@ -0,0 +1,230 @@
1
+ import type { AuthProvider, AuthProviderConfig, OAuthProviderConfig } from '../types';
2
+
3
+ /**
4
+ * OAuth provider URLs
5
+ */
6
+ const OAUTH_URLS: Record<string, { authUrl: string; defaultScope: string[] }> = {
7
+ google: {
8
+ authUrl: 'https://accounts.google.com/o/oauth2/v2/auth',
9
+ defaultScope: ['openid', 'email', 'profile'],
10
+ },
11
+ github: {
12
+ authUrl: 'https://github.com/login/oauth/authorize',
13
+ defaultScope: ['read:user', 'user:email'],
14
+ },
15
+ facebook: {
16
+ authUrl: 'https://www.facebook.com/v18.0/dialog/oauth',
17
+ defaultScope: ['email', 'public_profile'],
18
+ },
19
+ apple: {
20
+ authUrl: 'https://appleid.apple.com/auth/authorize',
21
+ defaultScope: ['name', 'email'],
22
+ },
23
+ twitter: {
24
+ authUrl: 'https://twitter.com/i/oauth2/authorize',
25
+ defaultScope: ['users.read', 'tweet.read'],
26
+ },
27
+ };
28
+
29
+ /**
30
+ * Generate a random state string for CSRF protection
31
+ */
32
+ export function generateState(): string {
33
+ const array = new Uint8Array(32);
34
+ crypto.getRandomValues(array);
35
+ return Array.from(array, byte => byte.toString(16).padStart(2, '0')).join('');
36
+ }
37
+
38
+ /**
39
+ * Generate a code verifier for PKCE
40
+ */
41
+ export function generateCodeVerifier(): string {
42
+ const array = new Uint8Array(32);
43
+ crypto.getRandomValues(array);
44
+ return base64UrlEncode(array);
45
+ }
46
+
47
+ /**
48
+ * Generate a code challenge from a code verifier
49
+ */
50
+ export async function generateCodeChallenge(verifier: string): Promise<string> {
51
+ const encoder = new TextEncoder();
52
+ const data = encoder.encode(verifier);
53
+ const hash = await crypto.subtle.digest('SHA-256', data);
54
+ return base64UrlEncode(new Uint8Array(hash));
55
+ }
56
+
57
+ /**
58
+ * Base64 URL encode
59
+ */
60
+ function base64UrlEncode(buffer: Uint8Array): string {
61
+ let binary = '';
62
+ for (let i = 0; i < buffer.length; i++) {
63
+ binary += String.fromCharCode(buffer[i]);
64
+ }
65
+ return btoa(binary).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
66
+ }
67
+
68
+ /**
69
+ * Store OAuth state in session storage
70
+ */
71
+ export function storeOAuthState(provider: string, state: string, codeVerifier?: string): void {
72
+ if (typeof window === 'undefined') return;
73
+
74
+ const data = { state, codeVerifier, timestamp: Date.now() };
75
+ sessionStorage.setItem(`oauth_state_${provider}`, JSON.stringify(data));
76
+ }
77
+
78
+ /**
79
+ * Get stored OAuth state
80
+ */
81
+ export function getStoredOAuthState(
82
+ provider: string
83
+ ): { state: string; codeVerifier?: string } | null {
84
+ if (typeof window === 'undefined') return null;
85
+
86
+ const stored = sessionStorage.getItem(`oauth_state_${provider}`);
87
+ if (!stored) return null;
88
+
89
+ try {
90
+ const data = JSON.parse(stored) as {
91
+ state: string;
92
+ codeVerifier?: string;
93
+ timestamp: number;
94
+ };
95
+
96
+ // Check if state is older than 10 minutes
97
+ if (Date.now() - data.timestamp > 10 * 60 * 1000) {
98
+ sessionStorage.removeItem(`oauth_state_${provider}`);
99
+ return null;
100
+ }
101
+
102
+ return { state: data.state, codeVerifier: data.codeVerifier };
103
+ } catch {
104
+ return null;
105
+ }
106
+ }
107
+
108
+ /**
109
+ * Clear stored OAuth state
110
+ */
111
+ export function clearOAuthState(provider: string): void {
112
+ if (typeof window === 'undefined') return;
113
+ sessionStorage.removeItem(`oauth_state_${provider}`);
114
+ }
115
+
116
+ /**
117
+ * Build OAuth authorization URL
118
+ */
119
+ export function buildOAuthUrl(
120
+ provider: AuthProvider,
121
+ config: OAuthProviderConfig,
122
+ options?: {
123
+ state?: string;
124
+ codeChallenge?: string;
125
+ codeChallengeMethod?: string;
126
+ }
127
+ ): string {
128
+ const providerConfig = OAUTH_URLS[provider];
129
+ if (!providerConfig) {
130
+ throw new Error(`Unknown OAuth provider: ${provider}`);
131
+ }
132
+
133
+ const params = new URLSearchParams({
134
+ client_id: config.clientId,
135
+ redirect_uri: config.redirectUri || `${window.location.origin}/auth/callback/${provider}`,
136
+ response_type: 'code',
137
+ scope: (config.scope || providerConfig.defaultScope).join(' '),
138
+ });
139
+
140
+ if (options?.state) {
141
+ params.set('state', options.state);
142
+ }
143
+
144
+ if (options?.codeChallenge) {
145
+ params.set('code_challenge', options.codeChallenge);
146
+ params.set('code_challenge_method', options.codeChallengeMethod || 'S256');
147
+ }
148
+
149
+ // Provider-specific parameters
150
+ if (provider === 'google') {
151
+ params.set('access_type', 'offline');
152
+ params.set('prompt', 'consent');
153
+ }
154
+
155
+ return `${providerConfig.authUrl}?${params.toString()}`;
156
+ }
157
+
158
+ /**
159
+ * Get the OAuth config for a provider
160
+ */
161
+ export function getProviderConfig(
162
+ provider: AuthProvider,
163
+ config?: AuthProviderConfig
164
+ ): OAuthProviderConfig | null {
165
+ if (!config) return null;
166
+
167
+ switch (provider) {
168
+ case 'google':
169
+ return config.google || null;
170
+ case 'github':
171
+ return config.github || null;
172
+ case 'facebook':
173
+ return config.facebook || null;
174
+ case 'apple':
175
+ return config.apple || null;
176
+ case 'twitter':
177
+ return config.twitter || null;
178
+ default:
179
+ return null;
180
+ }
181
+ }
182
+
183
+ /**
184
+ * Initiate OAuth flow
185
+ */
186
+ export async function initiateOAuthFlow(
187
+ provider: AuthProvider,
188
+ providerConfig: OAuthProviderConfig,
189
+ usePKCE: boolean = true
190
+ ): Promise<void> {
191
+ const state = generateState();
192
+ let codeVerifier: string | undefined;
193
+ let codeChallenge: string | undefined;
194
+
195
+ if (usePKCE) {
196
+ codeVerifier = generateCodeVerifier();
197
+ codeChallenge = await generateCodeChallenge(codeVerifier);
198
+ }
199
+
200
+ storeOAuthState(provider, state, codeVerifier);
201
+
202
+ const url = buildOAuthUrl(provider, providerConfig, {
203
+ state,
204
+ codeChallenge,
205
+ codeChallengeMethod: 'S256',
206
+ });
207
+
208
+ window.location.href = url;
209
+ }
210
+
211
+ /**
212
+ * Parse OAuth callback URL parameters
213
+ */
214
+ export function parseOAuthCallback(): {
215
+ code?: string;
216
+ state?: string;
217
+ error?: string;
218
+ errorDescription?: string;
219
+ } | null {
220
+ if (typeof window === 'undefined') return null;
221
+
222
+ const params = new URLSearchParams(window.location.search);
223
+
224
+ return {
225
+ code: params.get('code') || undefined,
226
+ state: params.get('state') || undefined,
227
+ error: params.get('error') || undefined,
228
+ errorDescription: params.get('error_description') || undefined,
229
+ };
230
+ }
@@ -0,0 +1,204 @@
1
+ import Cookies from 'js-cookie';
2
+ import { jwtDecode } from 'jwt-decode';
3
+
4
+ import type { TokenStorage, TokenPayload, AuthConfig } from '../types';
5
+
6
+ const ACCESS_TOKEN_KEY = 'auth_access_token';
7
+ const REFRESH_TOKEN_KEY = 'auth_refresh_token';
8
+
9
+ let memoryStorage: { accessToken?: string; refreshToken?: string } = {};
10
+
11
+ /**
12
+ * Get the token storage implementation based on config
13
+ */
14
+ function getStorage(storage: TokenStorage) {
15
+ switch (storage) {
16
+ case 'localStorage':
17
+ return {
18
+ get: (key: string) => {
19
+ if (typeof window === 'undefined') return null;
20
+ return localStorage.getItem(key);
21
+ },
22
+ set: (key: string, value: string) => {
23
+ if (typeof window === 'undefined') return;
24
+ localStorage.setItem(key, value);
25
+ },
26
+ remove: (key: string) => {
27
+ if (typeof window === 'undefined') return;
28
+ localStorage.removeItem(key);
29
+ },
30
+ };
31
+ case 'cookie':
32
+ return {
33
+ get: (key: string) => Cookies.get(key) || null,
34
+ set: (key: string, value: string, options?: Cookies.CookieAttributes) => {
35
+ Cookies.set(key, value, options);
36
+ },
37
+ remove: (key: string, options?: Cookies.CookieAttributes) => {
38
+ Cookies.remove(key, options);
39
+ },
40
+ };
41
+ case 'memory':
42
+ return {
43
+ get: (key: string) => {
44
+ if (key === ACCESS_TOKEN_KEY) return memoryStorage.accessToken || null;
45
+ if (key === REFRESH_TOKEN_KEY) return memoryStorage.refreshToken || null;
46
+ return null;
47
+ },
48
+ set: (key: string, value: string) => {
49
+ if (key === ACCESS_TOKEN_KEY) memoryStorage.accessToken = value;
50
+ if (key === REFRESH_TOKEN_KEY) memoryStorage.refreshToken = value;
51
+ },
52
+ remove: (key: string) => {
53
+ if (key === ACCESS_TOKEN_KEY) delete memoryStorage.accessToken;
54
+ if (key === REFRESH_TOKEN_KEY) delete memoryStorage.refreshToken;
55
+ },
56
+ };
57
+ default:
58
+ throw new Error(`Unknown storage type: ${storage as string}`);
59
+ }
60
+ }
61
+
62
+ /**
63
+ * Token manager class for handling token storage and validation
64
+ */
65
+ export class TokenManager {
66
+ private storage: TokenStorage;
67
+ private cookieOptions?: AuthConfig['cookieOptions'];
68
+
69
+ constructor(config: Pick<AuthConfig, 'tokenStorage' | 'cookieOptions'>) {
70
+ this.storage = config.tokenStorage || 'cookie';
71
+ this.cookieOptions = config.cookieOptions;
72
+ }
73
+
74
+ /**
75
+ * Get the access token
76
+ */
77
+ getAccessToken(): string | null {
78
+ return getStorage(this.storage).get(ACCESS_TOKEN_KEY);
79
+ }
80
+
81
+ /**
82
+ * Get the refresh token
83
+ */
84
+ getRefreshToken(): string | null {
85
+ return getStorage(this.storage).get(REFRESH_TOKEN_KEY);
86
+ }
87
+
88
+ /**
89
+ * Set the access token
90
+ */
91
+ setAccessToken(token: string, expiresIn?: number): void {
92
+ const storage = getStorage(this.storage);
93
+
94
+ if (this.storage === 'cookie') {
95
+ const expires = expiresIn ? new Date(Date.now() + expiresIn * 1000) : undefined;
96
+ storage.set(ACCESS_TOKEN_KEY, token, {
97
+ expires,
98
+ secure: this.cookieOptions?.secure ?? true,
99
+ sameSite: this.cookieOptions?.sameSite ?? 'lax',
100
+ domain: this.cookieOptions?.domain,
101
+ path: this.cookieOptions?.path ?? '/',
102
+ });
103
+ } else {
104
+ storage.set(ACCESS_TOKEN_KEY, token);
105
+ }
106
+ }
107
+
108
+ /**
109
+ * Set the refresh token
110
+ */
111
+ setRefreshToken(token: string): void {
112
+ const storage = getStorage(this.storage);
113
+
114
+ if (this.storage === 'cookie') {
115
+ storage.set(REFRESH_TOKEN_KEY, token, {
116
+ expires: 30, // 30 days
117
+ secure: this.cookieOptions?.secure ?? true,
118
+ sameSite: this.cookieOptions?.sameSite ?? 'lax',
119
+ domain: this.cookieOptions?.domain,
120
+ path: this.cookieOptions?.path ?? '/',
121
+ httpOnly: false, // Can't set httpOnly from client-side
122
+ });
123
+ } else {
124
+ storage.set(REFRESH_TOKEN_KEY, token);
125
+ }
126
+ }
127
+
128
+ /**
129
+ * Clear all tokens
130
+ */
131
+ clearTokens(): void {
132
+ const storage = getStorage(this.storage);
133
+
134
+ if (this.storage === 'cookie') {
135
+ storage.remove(ACCESS_TOKEN_KEY, {
136
+ domain: this.cookieOptions?.domain,
137
+ path: this.cookieOptions?.path ?? '/',
138
+ });
139
+ storage.remove(REFRESH_TOKEN_KEY, {
140
+ domain: this.cookieOptions?.domain,
141
+ path: this.cookieOptions?.path ?? '/',
142
+ });
143
+ } else {
144
+ storage.remove(ACCESS_TOKEN_KEY);
145
+ storage.remove(REFRESH_TOKEN_KEY);
146
+ }
147
+
148
+ // Also clear memory storage
149
+ memoryStorage = {};
150
+ }
151
+
152
+ /**
153
+ * Decode the access token
154
+ */
155
+ decodeToken(token?: string): TokenPayload | null {
156
+ const tokenToDecode = token || this.getAccessToken();
157
+ if (!tokenToDecode) return null;
158
+
159
+ try {
160
+ return jwtDecode<TokenPayload>(tokenToDecode);
161
+ } catch {
162
+ return null;
163
+ }
164
+ }
165
+
166
+ /**
167
+ * Check if the token is expired
168
+ */
169
+ isTokenExpired(token?: string, threshold: number = 0): boolean {
170
+ const payload = this.decodeToken(token);
171
+ if (!payload || !payload.exp) return true;
172
+
173
+ const expiresAt = payload.exp * 1000;
174
+ return Date.now() >= expiresAt - threshold * 1000;
175
+ }
176
+
177
+ /**
178
+ * Get token expiration time in milliseconds
179
+ */
180
+ getTokenExpiration(token?: string): number | null {
181
+ const payload = this.decodeToken(token);
182
+ if (!payload || !payload.exp) return null;
183
+ return payload.exp * 1000;
184
+ }
185
+
186
+ /**
187
+ * Check if we should refresh the token
188
+ */
189
+ shouldRefresh(threshold: number = 300): boolean {
190
+ const token = this.getAccessToken();
191
+ if (!token) return false;
192
+
193
+ return this.isTokenExpired(token, threshold);
194
+ }
195
+ }
196
+
197
+ /**
198
+ * Create a token manager instance
199
+ */
200
+ export function createTokenManager(
201
+ config: Pick<AuthConfig, 'tokenStorage' | 'cookieOptions'>
202
+ ): TokenManager {
203
+ return new TokenManager(config);
204
+ }
@@ -0,0 +1,14 @@
1
+ {
2
+ "extends": "@repo/config/typescript/base",
3
+ "compilerOptions": {
4
+ "outDir": "dist",
5
+ "rootDir": "src",
6
+ "jsx": "react-jsx",
7
+ "lib": ["ES2020", "DOM", "DOM.Iterable"],
8
+ "types": ["node"],
9
+ "declaration": true,
10
+ "declarationMap": true
11
+ },
12
+ "include": ["src/**/*"],
13
+ "exclude": ["node_modules", "dist"]
14
+ }
@@ -0,0 +1,18 @@
1
+ import { defineConfig } from 'tsup';
2
+
3
+ export default defineConfig({
4
+ entry: [
5
+ 'src/index.ts',
6
+ 'src/hooks/index.ts',
7
+ 'src/components/index.ts',
8
+ 'src/providers/index.ts',
9
+ 'src/next/index.ts',
10
+ ],
11
+ format: ['cjs', 'esm'],
12
+ dts: true,
13
+ splitting: false,
14
+ sourcemap: true,
15
+ clean: true,
16
+ external: ['react', 'react-dom', 'next', 'next/server', 'next/headers'],
17
+ treeshake: true,
18
+ });