@pixygon/auth 1.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.
package/dist/index.mjs ADDED
@@ -0,0 +1,28 @@
1
+ import {
2
+ AuthContext,
3
+ AuthProvider,
4
+ createAuthApiClient,
5
+ createTokenStorage,
6
+ useAuth,
7
+ useAuthContext,
8
+ useAuthError,
9
+ useAuthStatus,
10
+ useProfileSync,
11
+ useRequireAuth,
12
+ useToken,
13
+ useUser
14
+ } from "./chunk-E34M2RJD.mjs";
15
+ export {
16
+ AuthContext,
17
+ AuthProvider,
18
+ createAuthApiClient,
19
+ createTokenStorage,
20
+ useAuth,
21
+ useAuthContext,
22
+ useAuthError,
23
+ useAuthStatus,
24
+ useProfileSync,
25
+ useRequireAuth,
26
+ useToken,
27
+ useUser
28
+ };
package/package.json ADDED
@@ -0,0 +1,58 @@
1
+ {
2
+ "name": "@pixygon/auth",
3
+ "version": "1.0.0",
4
+ "description": "Shared authentication package for all Pixygon applications",
5
+ "main": "dist/index.js",
6
+ "module": "dist/index.mjs",
7
+ "types": "dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./dist/index.d.ts",
11
+ "import": "./dist/index.mjs",
12
+ "require": "./dist/index.js"
13
+ },
14
+ "./components": {
15
+ "types": "./dist/components/index.d.ts",
16
+ "import": "./dist/components/index.mjs",
17
+ "require": "./dist/components/index.js"
18
+ }
19
+ },
20
+ "scripts": {
21
+ "build": "tsup src/index.ts src/components/index.ts --format cjs,esm --dts --clean",
22
+ "dev": "tsup src/index.ts src/components/index.ts --format cjs,esm --dts --watch",
23
+ "lint": "eslint src/",
24
+ "typecheck": "tsc --noEmit"
25
+ },
26
+ "peerDependencies": {
27
+ "react": ">=17.0.0",
28
+ "react-dom": ">=17.0.0"
29
+ },
30
+ "devDependencies": {
31
+ "@types/react": "^18.2.0",
32
+ "@types/react-dom": "^18.2.0",
33
+ "react": "^18.2.0",
34
+ "react-dom": "^18.2.0",
35
+ "tsup": "^8.0.0",
36
+ "typescript": "^5.3.0"
37
+ },
38
+ "files": [
39
+ "dist",
40
+ "src"
41
+ ],
42
+ "keywords": [
43
+ "pixygon",
44
+ "auth",
45
+ "authentication",
46
+ "react",
47
+ "typescript"
48
+ ],
49
+ "author": "Pixygon",
50
+ "license": "MIT",
51
+ "publishConfig": {
52
+ "access": "public"
53
+ },
54
+ "repository": {
55
+ "type": "git",
56
+ "url": "https://github.com/pixygon/pixygon-packages"
57
+ }
58
+ }
@@ -0,0 +1,358 @@
1
+ /**
2
+ * @pixygon/auth - API Client
3
+ * Unified API client with automatic token injection and refresh
4
+ */
5
+
6
+ import type {
7
+ AuthConfig,
8
+ AuthError,
9
+ AuthErrorCode,
10
+ LoginRequest,
11
+ LoginResponse,
12
+ RegisterRequest,
13
+ RegisterResponse,
14
+ VerifyRequest,
15
+ VerifyResponse,
16
+ ForgotPasswordRequest,
17
+ ForgotPasswordResponse,
18
+ RecoverPasswordRequest,
19
+ RecoverPasswordResponse,
20
+ RefreshTokenRequest,
21
+ RefreshTokenResponse,
22
+ ResendVerificationRequest,
23
+ ResendVerificationResponse,
24
+ User,
25
+ } from '../types';
26
+ import type { TokenStorage } from '../utils/storage';
27
+
28
+ // ============================================================================
29
+ // Error Handling
30
+ // ============================================================================
31
+
32
+ function parseErrorCode(status: number, message?: string): AuthErrorCode {
33
+ if (message?.toLowerCase().includes('not found')) return 'USER_NOT_FOUND';
34
+ if (message?.toLowerCase().includes('exists')) return 'USER_EXISTS';
35
+ if (message?.toLowerCase().includes('verified')) return 'EMAIL_NOT_VERIFIED';
36
+ if (message?.toLowerCase().includes('invalid') && message?.toLowerCase().includes('code')) {
37
+ return 'INVALID_VERIFICATION_CODE';
38
+ }
39
+ if (message?.toLowerCase().includes('expired') && message?.toLowerCase().includes('code')) {
40
+ return 'EXPIRED_VERIFICATION_CODE';
41
+ }
42
+
43
+ switch (status) {
44
+ case 401:
45
+ return 'INVALID_CREDENTIALS';
46
+ case 403:
47
+ return 'TOKEN_INVALID';
48
+ case 404:
49
+ return 'USER_NOT_FOUND';
50
+ case 409:
51
+ return 'USER_EXISTS';
52
+ case 500:
53
+ return 'SERVER_ERROR';
54
+ default:
55
+ return 'UNKNOWN_ERROR';
56
+ }
57
+ }
58
+
59
+ function createAuthError(status: number, message?: string, details?: Record<string, unknown>): AuthError {
60
+ return {
61
+ code: parseErrorCode(status, message),
62
+ message: message || 'An unexpected error occurred',
63
+ details,
64
+ };
65
+ }
66
+
67
+ // ============================================================================
68
+ // API Client
69
+ // ============================================================================
70
+
71
+ export interface AuthApiClient {
72
+ // Auth endpoints
73
+ login: (data: LoginRequest) => Promise<LoginResponse>;
74
+ register: (data: RegisterRequest) => Promise<RegisterResponse>;
75
+ verify: (data: VerifyRequest) => Promise<VerifyResponse>;
76
+ resendVerification: (data: ResendVerificationRequest) => Promise<ResendVerificationResponse>;
77
+ forgotPassword: (data: ForgotPasswordRequest) => Promise<ForgotPasswordResponse>;
78
+ recoverPassword: (data: RecoverPasswordRequest) => Promise<RecoverPasswordResponse>;
79
+ refreshToken: (data: RefreshTokenRequest) => Promise<RefreshTokenResponse>;
80
+
81
+ // User endpoints
82
+ getMe: () => Promise<User>;
83
+ updateProfile: (data: Partial<User>) => Promise<User>;
84
+
85
+ // Utilities
86
+ setAccessToken: (token: string | null) => void;
87
+ getAccessToken: () => string | null;
88
+ request: <T>(endpoint: string, options?: RequestInit) => Promise<T>;
89
+ }
90
+
91
+ export function createAuthApiClient(config: AuthConfig, tokenStorage: TokenStorage): AuthApiClient {
92
+ let currentAccessToken: string | null = null;
93
+ let isRefreshing = false;
94
+ let refreshPromise: Promise<void> | null = null;
95
+
96
+ const log = (...args: unknown[]) => {
97
+ if (config.debug) {
98
+ console.log('[PixygonAuth]', ...args);
99
+ }
100
+ };
101
+
102
+ /**
103
+ * Make an authenticated request
104
+ */
105
+ async function request<T>(endpoint: string, options: RequestInit = {}): Promise<T> {
106
+ const url = `${config.baseUrl}${endpoint}`;
107
+ const headers: Record<string, string> = {
108
+ 'Content-Type': 'application/json',
109
+ ...(options.headers as Record<string, string>),
110
+ };
111
+
112
+ // Add auth header if we have a token
113
+ if (currentAccessToken) {
114
+ headers['Authorization'] = `Bearer ${currentAccessToken}`;
115
+ }
116
+
117
+ log(`Request: ${options.method || 'GET'} ${endpoint}`);
118
+
119
+ try {
120
+ const response = await fetch(url, {
121
+ ...options,
122
+ headers,
123
+ });
124
+
125
+ // Handle token expiry
126
+ if (response.status === 401 || response.status === 403) {
127
+ // Try to refresh token
128
+ if (currentAccessToken && !isRefreshing) {
129
+ log('Token expired, attempting refresh...');
130
+ try {
131
+ await refreshTokens();
132
+ // Retry the original request with new token
133
+ headers['Authorization'] = `Bearer ${currentAccessToken}`;
134
+ const retryResponse = await fetch(url, { ...options, headers });
135
+ if (!retryResponse.ok) {
136
+ const errorData = await retryResponse.json().catch(() => ({}));
137
+ throw createAuthError(retryResponse.status, errorData.message);
138
+ }
139
+ return retryResponse.json();
140
+ } catch (refreshError) {
141
+ log('Token refresh failed', refreshError);
142
+ throw createAuthError(401, 'Session expired. Please log in again.');
143
+ }
144
+ }
145
+ throw createAuthError(response.status, 'Authentication required');
146
+ }
147
+
148
+ if (!response.ok) {
149
+ const errorData = await response.json().catch(() => ({}));
150
+ throw createAuthError(response.status, errorData.message, errorData);
151
+ }
152
+
153
+ const data = await response.json();
154
+ return data;
155
+ } catch (error) {
156
+ if ((error as AuthError).code) {
157
+ throw error;
158
+ }
159
+ log('Network error:', error);
160
+ throw {
161
+ code: 'NETWORK_ERROR',
162
+ message: 'Unable to connect to the server',
163
+ details: { originalError: error },
164
+ } as AuthError;
165
+ }
166
+ }
167
+
168
+ /**
169
+ * Refresh the access token
170
+ */
171
+ async function refreshTokens(): Promise<void> {
172
+ if (isRefreshing && refreshPromise) {
173
+ return refreshPromise;
174
+ }
175
+
176
+ isRefreshing = true;
177
+ refreshPromise = (async () => {
178
+ try {
179
+ const refreshToken = await tokenStorage.getRefreshToken();
180
+ if (!refreshToken) {
181
+ throw new Error('No refresh token available');
182
+ }
183
+
184
+ const response = await fetch(`${config.baseUrl}/auth/refresh`, {
185
+ method: 'POST',
186
+ headers: { 'Content-Type': 'application/json' },
187
+ body: JSON.stringify({ refreshToken }),
188
+ });
189
+
190
+ if (!response.ok) {
191
+ throw new Error('Refresh failed');
192
+ }
193
+
194
+ const data: RefreshTokenResponse = await response.json();
195
+ currentAccessToken = data.token;
196
+
197
+ await tokenStorage.updateTokens({
198
+ accessToken: data.token,
199
+ refreshToken: data.refreshToken,
200
+ expiresIn: data.expiresIn,
201
+ refreshExpiresIn: data.expiresIn * 24, // Approximate
202
+ });
203
+
204
+ config.onTokenRefresh?.({
205
+ accessToken: data.token,
206
+ refreshToken: data.refreshToken,
207
+ expiresIn: data.expiresIn,
208
+ refreshExpiresIn: data.expiresIn * 24,
209
+ });
210
+
211
+ log('Token refreshed successfully');
212
+ } finally {
213
+ isRefreshing = false;
214
+ refreshPromise = null;
215
+ }
216
+ })();
217
+
218
+ return refreshPromise;
219
+ }
220
+
221
+ return {
222
+ // ========================================================================
223
+ // Auth Endpoints
224
+ // ========================================================================
225
+
226
+ async login(data: LoginRequest): Promise<LoginResponse> {
227
+ const response = await request<LoginResponse>('/auth/login', {
228
+ method: 'POST',
229
+ body: JSON.stringify(data),
230
+ });
231
+
232
+ currentAccessToken = response.token;
233
+ await tokenStorage.setTokens(
234
+ response.token,
235
+ response.refreshToken || null,
236
+ response.expiresIn || null,
237
+ response.user
238
+ );
239
+
240
+ config.onLogin?.(response.user);
241
+ log('Login successful:', response.user.userName);
242
+
243
+ return response;
244
+ },
245
+
246
+ async register(data: RegisterRequest): Promise<RegisterResponse> {
247
+ const response = await request<RegisterResponse>('/auth/register', {
248
+ method: 'POST',
249
+ body: JSON.stringify(data),
250
+ });
251
+
252
+ log('Registration successful:', response.user.userName);
253
+ return response;
254
+ },
255
+
256
+ async verify(data: VerifyRequest): Promise<VerifyResponse> {
257
+ const response = await request<VerifyResponse>('/auth/verify', {
258
+ method: 'POST',
259
+ body: JSON.stringify(data),
260
+ });
261
+
262
+ currentAccessToken = response.token;
263
+ await tokenStorage.setTokens(
264
+ response.token,
265
+ response.refreshToken || null,
266
+ null,
267
+ response.user
268
+ );
269
+
270
+ config.onLogin?.(response.user);
271
+ log('Verification successful:', response.user.userName);
272
+
273
+ return response;
274
+ },
275
+
276
+ async resendVerification(data: ResendVerificationRequest): Promise<ResendVerificationResponse> {
277
+ const response = await request<ResendVerificationResponse>('/auth/resendVerificationEmail', {
278
+ method: 'POST',
279
+ body: JSON.stringify(data),
280
+ });
281
+
282
+ log('Verification email resent');
283
+ return response;
284
+ },
285
+
286
+ async forgotPassword(data: ForgotPasswordRequest): Promise<ForgotPasswordResponse> {
287
+ const response = await request<ForgotPasswordResponse>('/auth/forgotPassword', {
288
+ method: 'POST',
289
+ body: JSON.stringify(data),
290
+ });
291
+
292
+ log('Password reset email sent');
293
+ return response;
294
+ },
295
+
296
+ async recoverPassword(data: RecoverPasswordRequest): Promise<RecoverPasswordResponse> {
297
+ const response = await request<RecoverPasswordResponse>('/auth/recoverPassword', {
298
+ method: 'POST',
299
+ body: JSON.stringify(data),
300
+ });
301
+
302
+ log('Password recovered successfully');
303
+ return response;
304
+ },
305
+
306
+ async refreshToken(data: RefreshTokenRequest): Promise<RefreshTokenResponse> {
307
+ const response = await request<RefreshTokenResponse>('/auth/refresh', {
308
+ method: 'POST',
309
+ body: JSON.stringify(data),
310
+ });
311
+
312
+ currentAccessToken = response.token;
313
+ await tokenStorage.updateTokens({
314
+ accessToken: response.token,
315
+ refreshToken: response.refreshToken,
316
+ expiresIn: response.expiresIn,
317
+ refreshExpiresIn: response.expiresIn * 24,
318
+ });
319
+
320
+ return response;
321
+ },
322
+
323
+ // ========================================================================
324
+ // User Endpoints
325
+ // ========================================================================
326
+
327
+ async getMe(): Promise<User> {
328
+ const response = await request<User>('/users/me');
329
+ await tokenStorage.updateUser(response);
330
+ return response;
331
+ },
332
+
333
+ async updateProfile(data: Partial<User>): Promise<User> {
334
+ const response = await request<User>('/users/me', {
335
+ method: 'PATCH',
336
+ body: JSON.stringify(data),
337
+ });
338
+ await tokenStorage.updateUser(response);
339
+ return response;
340
+ },
341
+
342
+ // ========================================================================
343
+ // Utilities
344
+ // ========================================================================
345
+
346
+ setAccessToken(token: string | null) {
347
+ currentAccessToken = token;
348
+ },
349
+
350
+ getAccessToken() {
351
+ return currentAccessToken;
352
+ },
353
+
354
+ request,
355
+ };
356
+ }
357
+
358
+ export type { TokenStorage };