@vunexa/lixa 0.0.1-alpha.20 → 0.0.1-alpha.22

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 (40) hide show
  1. package/README.md +562 -79
  2. package/dist/dao/session-cache.d.ts +2 -2
  3. package/dist/dao/session-cache.d.ts.map +1 -1
  4. package/dist/dao/state-cache.d.ts +3 -3
  5. package/dist/dao/state-cache.d.ts.map +1 -1
  6. package/dist/dao/types.d.ts +246 -5
  7. package/dist/dao/types.d.ts.map +1 -1
  8. package/dist/export-types/index.d.ts +852 -49
  9. package/dist/index.cjs +282 -46
  10. package/dist/index.cjs.map +1 -1
  11. package/dist/index.d.cts +853 -46
  12. package/dist/index.d.ts +11 -3
  13. package/dist/index.d.ts.map +1 -1
  14. package/dist/index.js +282 -46
  15. package/dist/index.js.map +1 -1
  16. package/dist/lixa.d.ts +187 -18
  17. package/dist/lixa.d.ts.map +1 -1
  18. package/dist/models/session.d.ts +163 -7
  19. package/dist/models/session.d.ts.map +1 -1
  20. package/dist/providers/IProvider.d.ts +121 -4
  21. package/dist/providers/IProvider.d.ts.map +1 -1
  22. package/dist/providers/index.d.ts +0 -2
  23. package/dist/providers/index.d.ts.map +1 -1
  24. package/dist/types.d.ts +131 -15
  25. package/dist/types.d.ts.map +1 -1
  26. package/package.json +2 -16
  27. package/dist/IProvider-2tDwRAnH.d.cts +0 -18
  28. package/dist/IProvider-2tDwRAnH.d.ts +0 -18
  29. package/dist/export-types/providers.d.ts +0 -61
  30. package/dist/providers/github.d.ts +0 -13
  31. package/dist/providers/github.d.ts.map +0 -1
  32. package/dist/providers/google.d.ts +0 -13
  33. package/dist/providers/google.d.ts.map +0 -1
  34. package/dist/providers-entry.cjs +0 -48
  35. package/dist/providers-entry.cjs.map +0 -1
  36. package/dist/providers-entry.d.cts +0 -25
  37. package/dist/providers-entry.d.ts +0 -22
  38. package/dist/providers-entry.d.ts.map +0 -1
  39. package/dist/providers-entry.js +0 -20
  40. package/dist/providers-entry.js.map +0 -1
package/README.md CHANGED
@@ -18,7 +18,8 @@ A flexible, provider-agnostic OAuth 2.0 and OpenID Connect (OIDC) client library
18
18
  - Custom provider registration with extensible provider interface
19
19
  - Extensible session management via pluggable strategies
20
20
  - PKCE (Proof Key for Code Exchange) support
21
- - TypeScript-first with strong typing and async/await support
21
+ - TypeScript-first with full type safety (zero `any` types)
22
+ - OAuth 2.0 and OpenID Connect spec-compliant types
22
23
  - 100% test coverage with comprehensive error handling
23
24
 
24
25
  ---
@@ -33,14 +34,22 @@ yarn add @vunexa/lixa
33
34
 
34
35
  ## Quick Start
35
36
 
36
- ### 1. Configure lixa with multiple providers
37
+ ### 1. Install packages
38
+
39
+ ```bash
40
+ npm install @vunexa/lixa @vunexa/lixa-providers
41
+ ```
42
+
43
+ ### 2. Configure Lixa with providers
37
44
 
38
45
  ```typescript
39
46
  import { Lixa } from "@vunexa/lixa";
47
+ import { GoogleProvider, GithubProvider } from "@vunexa/lixa-providers";
40
48
 
41
49
  const lixa = new Lixa({
42
50
  providers: {
43
51
  google: {
52
+ provider: new GoogleProvider(),
44
53
  clientId: process.env.GOOGLE_CLIENT_ID!,
45
54
  clientSecret: process.env.GOOGLE_CLIENT_SECRET!,
46
55
  redirectUri: "https://yourapp.com/auth/google/callback",
@@ -51,20 +60,21 @@ const lixa = new Lixa({
51
60
  },
52
61
  },
53
62
  github: {
63
+ provider: new GithubProvider(),
54
64
  clientId: process.env.GITHUB_CLIENT_ID!,
55
65
  clientSecret: process.env.GITHUB_CLIENT_SECRET!,
56
66
  redirectUri: "https://yourapp.com/auth/github/callback",
57
67
  scopes: ["read:user", "user:email"],
58
- extraConfig: {},
59
68
  },
60
69
  },
61
70
 
62
71
  // Optional: custom session strategy
63
72
  sessionStrategy: {
64
73
  createSession: async (tokenData) => {
65
- // Custom session creation logic
74
+ // tokenData is typed as OAuthTokenResponse with proper OAuth 2.0 fields
75
+ // { access_token, token_type, expires_in?, refresh_token?, scope?, id_token? }
66
76
  return {
67
- token: "custom-session-token",
77
+ token: tokenData.access_token,
68
78
  raw: tokenData,
69
79
  };
70
80
  },
@@ -72,22 +82,22 @@ const lixa = new Lixa({
72
82
  });
73
83
  ```
74
84
 
75
- ### 2. Redirect users to the provider's authorization URL
85
+ ### 3. Redirect users to the provider's authorization URL
76
86
 
77
87
  ```typescript
78
88
  app.get("/login", (req, res) => {
79
89
  const provider = req.query.provider as string; // 'google' or 'github'
80
- const state = lixa.generateRandomState();
90
+ const state = Lixa.generateRandomState();
81
91
 
82
92
  // Store state in session for validation
83
93
  req.session.oauthState = state;
84
94
 
85
- const authUrl = lixa.getAuthUrl(provider.toUpperCase(), state);
95
+ const authUrl = lixa.getAuthUrl(provider, state);
86
96
  res.redirect(authUrl);
87
97
  });
88
98
  ```
89
99
 
90
- ### 3. Handle the provider callback and establish a session
100
+ ### 4. Handle the provider callback and establish a session
91
101
 
92
102
  ```typescript
93
103
  app.get("/auth/:provider/callback", async (req, res) => {
@@ -100,14 +110,17 @@ app.get("/auth/:provider/callback", async (req, res) => {
100
110
  throw new Error("Invalid state parameter");
101
111
  }
102
112
 
103
- const session = await lixa.handleCallback({
113
+ const sessionId = await lixa.handleCallback({
104
114
  provider,
105
115
  code: code as string,
106
116
  state: state as string,
107
117
  });
108
118
 
109
- // Session established
110
- res.cookie("session_token", session.token, {
119
+ // Session established - retrieve session info if needed
120
+ const session = await lixa.fetchSessionInfo(sessionId);
121
+
122
+ // Store session ID in cookie
123
+ res.cookie("session_id", sessionId, {
111
124
  httpOnly: true,
112
125
  secure: true,
113
126
  sameSite: "strict",
@@ -121,6 +134,118 @@ app.get("/auth/:provider/callback", async (req, res) => {
121
134
  });
122
135
  ```
123
136
 
137
+ ## Provider Configuration
138
+
139
+ ### Using Built-in Providers
140
+
141
+ Built-in providers are available in the `@vunexa/lixa-providers` package:
142
+
143
+ ```typescript
144
+ import { Lixa } from "@vunexa/lixa";
145
+ import { GoogleProvider, GithubProvider } from "@vunexa/lixa-providers";
146
+
147
+ const lixa = new Lixa({
148
+ providers: {
149
+ google: {
150
+ provider: new GoogleProvider(),
151
+ clientId: process.env.GOOGLE_CLIENT_ID!,
152
+ clientSecret: process.env.GOOGLE_CLIENT_SECRET!,
153
+ redirectUri: "https://yourapp.com/auth/google/callback",
154
+ scopes: ["openid", "email", "profile"]
155
+ }
156
+ }
157
+ });
158
+ ```
159
+
160
+ ### Using Custom Inline Providers
161
+
162
+ You can create custom providers by implementing the `IProvider` interface:
163
+
164
+ ```typescript
165
+ import { Lixa, IProvider } from "@vunexa/lixa";
166
+
167
+ // Define your custom provider
168
+ const customProvider: IProvider = {
169
+ authorizationEndpoint: "https://provider.com/oauth/authorize",
170
+ tokenEndpoint: "https://provider.com/oauth/token",
171
+ userInfoEndpoint: "https://provider.com/api/user"
172
+ };
173
+
174
+ // Use it directly in configuration
175
+ const lixa = new Lixa({
176
+ providers: {
177
+ custom: {
178
+ provider: customProvider,
179
+ clientId: "your-client-id",
180
+ clientSecret: "your-client-secret",
181
+ redirectUri: "https://yourapp.com/auth/custom/callback",
182
+ scopes: ["read:user"]
183
+ }
184
+ }
185
+ });
186
+ ```
187
+
188
+ ### Mixing Built-in and Custom Providers
189
+
190
+ You can use both built-in and custom providers in the same configuration:
191
+
192
+ ```typescript
193
+ import { Lixa, IProvider } from "@vunexa/lixa";
194
+ import { GoogleProvider } from "@vunexa/lixa-providers";
195
+
196
+ const customProvider: IProvider = {
197
+ authorizationEndpoint: "https://custom.com/oauth/authorize",
198
+ tokenEndpoint: "https://custom.com/oauth/token",
199
+ userInfoEndpoint: "https://custom.com/api/user"
200
+ };
201
+
202
+ const lixa = new Lixa({
203
+ providers: {
204
+ google: {
205
+ provider: new GoogleProvider(),
206
+ clientId: process.env.GOOGLE_CLIENT_ID!,
207
+ clientSecret: process.env.GOOGLE_CLIENT_SECRET!,
208
+ redirectUri: "https://yourapp.com/auth/google/callback",
209
+ scopes: ["openid", "email", "profile"]
210
+ },
211
+ custom: {
212
+ provider: customProvider,
213
+ clientId: process.env.CUSTOM_CLIENT_ID!,
214
+ clientSecret: process.env.CUSTOM_CLIENT_SECRET!,
215
+ redirectUri: "https://yourapp.com/auth/custom/callback",
216
+ scopes: ["read:user"]
217
+ }
218
+ }
219
+ });
220
+ ```
221
+
222
+ ### Overriding Built-in Providers (Testing)
223
+
224
+ You can override built-in providers with custom implementations for testing:
225
+
226
+ ```typescript
227
+ import { Lixa, IProvider } from "@vunexa/lixa";
228
+
229
+ // Mock provider for testing
230
+ const mockGoogleProvider: IProvider = {
231
+ authorizationEndpoint: "http://localhost:3000/mock/authorize",
232
+ tokenEndpoint: "http://localhost:3000/mock/token",
233
+ userInfoEndpoint: "http://localhost:3000/mock/userinfo"
234
+ };
235
+
236
+ const lixa = new Lixa({
237
+ providers: {
238
+ google: {
239
+ provider: mockGoogleProvider, // Override with mock
240
+ clientId: "test-client-id",
241
+ clientSecret: "test-client-secret",
242
+ redirectUri: "http://localhost:3000/callback",
243
+ scopes: ["openid", "email"]
244
+ }
245
+ }
246
+ });
247
+ ```
248
+
124
249
  ## Advanced Usage
125
250
 
126
251
  ### Custom State Storage (StateDao)
@@ -128,11 +253,11 @@ app.get("/auth/:provider/callback", async (req, res) => {
128
253
  By default, Lixa uses an in-memory cache for state management. For production applications or serverless environments, you should implement a custom state storage:
129
254
 
130
255
  ```typescript
131
- import { Lixa } from "@vunexa/lixa";
256
+ import { Lixa, StateDao, StateData } from "@vunexa/lixa";
132
257
 
133
- // Example: DynamoDB state storage
134
- const dynamoDbStateDao = {
135
- saveState: async (state: string, data: any, expiresInSeconds: number) => {
258
+ // Example: DynamoDB state storage with proper typing
259
+ const dynamoDbStateDao: StateDao = {
260
+ saveState: async (state: string, data: StateData, expiresInSeconds: number) => {
136
261
  await dynamoDB.put({
137
262
  TableName: "oauth-states",
138
263
  Item: {
@@ -143,7 +268,7 @@ const dynamoDbStateDao = {
143
268
  });
144
269
  },
145
270
 
146
- getState: async (state: string) => {
271
+ getState: async (state: string): Promise<StateData | null> => {
147
272
  const result = await dynamoDB.get({
148
273
  TableName: "oauth-states",
149
274
  Key: { state },
@@ -155,7 +280,7 @@ const dynamoDbStateDao = {
155
280
  return null; // Expired
156
281
  }
157
282
 
158
- return JSON.parse(result.Item.data);
283
+ return JSON.parse(result.Item.data) as StateData;
159
284
  },
160
285
 
161
286
  deleteState: async (state: string) => {
@@ -172,15 +297,22 @@ const lixa = new Lixa({
172
297
  });
173
298
  ```
174
299
 
175
- **Important**: The state data includes a `codeVerifier` field required for PKCE. Make sure your storage preserves all fields in the data object.
300
+ **Important**: The `StateData` type includes required fields:
301
+ - `provider` (string): Provider name for callback routing
302
+ - `codeVerifier` (string): PKCE code verifier (64 hex characters)
303
+ - `createdAt` (number): Unix timestamp in milliseconds
304
+
305
+ Make sure your storage preserves all fields in the data object.
176
306
 
177
307
  ### Custom Session Storage (SessionDao)
178
308
 
179
- Similar to state storage, you can implement custom session storage:
309
+ Similar to state storage, you can implement custom session storage with proper typing:
180
310
 
181
311
  ```typescript
182
- const customSessionDao = {
183
- saveSession: async (sessionId: string, session: any, expiresInSeconds: number) => {
312
+ import { Lixa, SessionDao, Session } from "@vunexa/lixa";
313
+
314
+ const customSessionDao: SessionDao = {
315
+ saveSession: async <T = unknown>(sessionId: string, session: T, expiresInSeconds: number) => {
184
316
  // Store session in your database
185
317
  await db.sessions.create({
186
318
  id: sessionId,
@@ -189,12 +321,12 @@ const customSessionDao = {
189
321
  });
190
322
  },
191
323
 
192
- getSession: async (sessionId: string) => {
324
+ getSession: async <T = unknown>(sessionId: string): Promise<T | null> => {
193
325
  const session = await db.sessions.findById(sessionId);
194
326
  if (!session || session.expiresAt < Date.now()) {
195
327
  return null;
196
328
  }
197
- return session.data;
329
+ return session.data as T;
198
330
  },
199
331
 
200
332
  deleteSession: async (sessionId: string) => {
@@ -210,12 +342,29 @@ const lixa = new Lixa({
210
342
 
211
343
  ### Custom Session Strategy
212
344
 
213
- The session strategy controls how user sessions are created from OAuth tokens:
345
+ The session strategy controls how user sessions are created from OAuth tokens. The `tokenData` parameter is typed as `OAuthTokenResponse` with proper OAuth 2.0 fields:
214
346
 
215
347
  ```typescript
216
- const customSessionStrategy = {
217
- createSession: async (tokenData) => {
218
- // tokenData contains: access_token, refresh_token, id_token, etc.
348
+ import { Lixa, SessionStrategy, OAuthTokenResponse, Session } from "@vunexa/lixa";
349
+
350
+ // Define custom session data structure
351
+ interface CustomSessionData extends OAuthTokenResponse {
352
+ userId: string;
353
+ userInfo: {
354
+ email: string;
355
+ name: string;
356
+ };
357
+ }
358
+
359
+ const customSessionStrategy: SessionStrategy = {
360
+ createSession: async (tokenData: OAuthTokenResponse): Promise<Session<CustomSessionData>> => {
361
+ // tokenData is properly typed with OAuth 2.0 fields:
362
+ // - access_token: string (required)
363
+ // - token_type: string (required)
364
+ // - expires_in?: number
365
+ // - refresh_token?: string
366
+ // - scope?: string
367
+ // - id_token?: string (for OIDC providers)
219
368
 
220
369
  // Decode ID token to get user info (for OIDC providers like Google)
221
370
  let userInfo;
@@ -275,9 +424,11 @@ Debug mode logs:
275
424
  - Session creation
276
425
  - Error details from OAuth providers
277
426
 
278
- ### Custom Provider Registration
427
+ ### Legacy Provider Registration (Deprecated)
279
428
 
280
- You can register custom OAuth providers by implementing the `IProvider` interface:
429
+ > **Note**: This pattern is deprecated. Use inline providers instead (see examples above).
430
+
431
+ For backward compatibility, you can still register providers globally:
281
432
 
282
433
  ```typescript
283
434
  import { Lixa, IProvider } from "@vunexa/lixa";
@@ -293,7 +444,7 @@ Lixa.registerProvider({
293
444
  custom: new CustomProvider(),
294
445
  });
295
446
 
296
- // Use it in your configuration
447
+ // Use it in your configuration (without provider field)
297
448
  const lixa = new Lixa({
298
449
  providers: {
299
450
  custom: {
@@ -306,17 +457,17 @@ const lixa = new Lixa({
306
457
  });
307
458
  ```
308
459
 
309
- ### Check Provider Registration
460
+ ### Check Provider Configuration
310
461
 
311
462
  ```typescript
312
- // Check if a provider is registered
313
- if (Lixa.isProviderRegistered("google")) {
314
- console.log("Google provider is available");
463
+ // Check if a provider is configured for this instance
464
+ if (lixa.isProviderConfigured("google")) {
465
+ console.log("Google provider is configured");
315
466
  }
316
467
 
317
- // Get list of all registered providers
468
+ // Get list of all registered providers (legacy)
318
469
  const providers = Lixa.getRegisteredProviders();
319
- console.log("Available providers:", providers);
470
+ console.log("Registered providers:", providers);
320
471
  ```
321
472
 
322
473
  ## Common Pitfalls & Solutions
@@ -368,21 +519,30 @@ saveState: async (state: string, data: any, expiresInSeconds: number) => {
368
519
  **Solution**: Decode the `id_token` (for OIDC providers) or fetch user info from the provider's API:
369
520
 
370
521
  ```typescript
371
- createSession: async (tokenData) => {
372
- // For OIDC providers (Google, etc.)
373
- if (tokenData.id_token) {
374
- const base64Payload = tokenData.id_token.split('.')[1];
375
- const userInfo = JSON.parse(Buffer.from(base64Payload, 'base64').toString());
376
- }
377
-
378
- // For OAuth-only providers (GitHub, etc.)
379
- else if (tokenData.access_token) {
380
- const response = await fetch(providerUserInfoEndpoint, {
381
- headers: { Authorization: `Bearer ${tokenData.access_token}` }
382
- });
383
- const userInfo = await response.json();
522
+ import { SessionStrategy, OAuthTokenResponse } from "@vunexa/lixa";
523
+
524
+ const strategy: SessionStrategy = {
525
+ createSession: async (tokenData: OAuthTokenResponse) => {
526
+ // For OIDC providers (Google, etc.) - id_token is properly typed
527
+ if (tokenData.id_token) {
528
+ const base64Payload = tokenData.id_token.split('.')[1];
529
+ const userInfo = JSON.parse(Buffer.from(base64Payload, 'base64').toString());
530
+ }
531
+
532
+ // For OAuth-only providers (GitHub, etc.) - access_token is guaranteed to exist
533
+ else {
534
+ const response = await fetch(providerUserInfoEndpoint, {
535
+ headers: { Authorization: `Bearer ${tokenData.access_token}` }
536
+ });
537
+ const userInfo = await response.json();
538
+ }
539
+
540
+ return {
541
+ token: tokenData.access_token,
542
+ raw: tokenData
543
+ };
384
544
  }
385
- }
545
+ };
386
546
  ```
387
547
 
388
548
  ## API Reference
@@ -395,54 +555,77 @@ createSession: async (tokenData) => {
395
555
 
396
556
  #### Static Methods
397
557
 
398
- - `Lixa.registerProvider(providerMap: { [key: string]: IProvider })` - Register custom providers before creating instances
399
- - `Lixa.isProviderRegistered(provider: string): boolean` - Check if a provider is registered
400
- - `Lixa.getRegisteredProviders(): string[]` - Get list of all registered provider names
401
- - `Lixa.generateRandomState(): string` - Generate a cryptographically secure random state string
558
+ - `Lixa.generateRandomState(): string` - Generate a cryptographically secure random state string for CSRF protection
559
+ - `Lixa.registerProvider(providerMap: { [key: string]: IProvider })` - **[Deprecated]** Register custom providers globally. Use inline providers instead.
560
+ - `Lixa.getRegisteredProviders(): string[]` - **[Deprecated]** Get list of all registered provider names from legacy registry
402
561
 
403
562
  #### Instance Methods
404
563
 
405
- - `getAuthUrl(provider: string, state: string): string` - Generate authorization URL for a provider. Automatically stores state with PKCE code verifier.
406
- - `handleCallback({ provider, code, state }): Promise<string>` - Handle OAuth callback, validate state, exchange code for tokens, and create session. Returns session ID.
407
- - `fetchSessionInfo(sessionId: string): Promise<Session | null>` - Retrieve session information by session ID
564
+ - `getAuthUrl(provider: string, state: string): string` - Generate authorization URL for a provider. Automatically generates PKCE code verifier and challenge, stores state with code verifier for later validation.
565
+ - `handleCallback({ provider, code, state }): Promise<string>` - Handle OAuth callback, validate state parameter, retrieve PKCE code verifier, exchange authorization code for tokens, and create session using SessionStrategy. Returns session ID.
566
+ - `fetchSessionInfo(sessionId: string): Promise<Session | null>` - Retrieve session information by session ID from SessionDao.
567
+ - `isProviderConfigured(provider: string): boolean` - Check if a provider is configured for this Lixa instance. Useful for type guards and runtime validation.
408
568
 
409
569
  ### Types
410
570
 
571
+ All types are fully typed with zero `any` types, following OAuth 2.0 and OpenID Connect specifications:
572
+
411
573
  ```typescript
412
- interface ProviderConfig {
574
+ // OAuth 2.0 token response (RFC 6749 Section 5.1)
575
+ interface OAuthTokenResponse {
576
+ access_token: string; // Required: OAuth access token
577
+ token_type: string; // Required: Token type (usually "Bearer")
578
+ expires_in?: number; // Optional: Token expiration in seconds
579
+ refresh_token?: string; // Optional: Refresh token
580
+ scope?: string; // Optional: Granted scopes (space-separated)
581
+ id_token?: string; // Optional: OpenID Connect ID token (JWT)
582
+ [key: string]: unknown; // Additional provider-specific fields
583
+ }
584
+
585
+ // Provider configuration with inline provider support
586
+ type ProviderConfig = {
413
587
  clientId: string;
414
588
  clientSecret: string;
415
589
  redirectUri: string;
416
590
  scopes: string[];
417
- extraConfig?: Record<string, any>; // Provider-specific parameters
418
- }
419
-
420
- interface LixaConfig {
421
- providers: Record<string, ProviderConfig>;
591
+ extraConfig?: Record<string, string>;
592
+ } & (
593
+ | { provider?: never } // Built-in provider (no provider field)
594
+ | { provider: IProvider } // Custom provider (provider field required)
595
+ );
596
+
597
+ interface LixaConfig<TProviders = Record<string, ProviderConfig>> {
598
+ providers: TProviders;
422
599
  sessionStrategy?: SessionStrategy;
423
- stateDao?: StateDao; // Custom state storage
424
- sessionDao?: SessionDao; // Custom session storage
425
- debug?: boolean; // Enable debug logging
600
+ stateDao?: StateDao;
601
+ sessionDao?: SessionDao;
602
+ debug?: boolean;
426
603
  }
427
604
 
428
605
  interface SessionStrategy {
429
- createSession(tokenData: any): Promise<Session>;
606
+ createSession(tokenData: OAuthTokenResponse): Promise<Session>;
607
+ }
608
+
609
+ interface Session<TRaw = OAuthTokenResponse> {
610
+ token: string;
611
+ raw: TRaw; // Defaults to OAuthTokenResponse, can be extended
430
612
  }
431
613
 
432
- interface Session {
433
- token: string; // Your custom session identifier
434
- raw: any; // Raw data you want to store with the session
614
+ interface StateData {
615
+ provider: string; // Provider name for routing
616
+ codeVerifier: string; // PKCE code verifier (64 hex chars)
617
+ createdAt: number; // Unix timestamp in milliseconds
435
618
  }
436
619
 
437
620
  interface StateDao {
438
- saveState(state: string, data: any, expiresInSeconds: number): Promise<void>;
439
- getState(state: string): Promise<any | null>;
621
+ saveState(state: string, data: StateData, expiresInSeconds: number): Promise<void>;
622
+ getState(state: string): Promise<StateData | null>;
440
623
  deleteState(state: string): Promise<void>;
441
624
  }
442
625
 
443
626
  interface SessionDao {
444
- saveSession(sessionId: string, session: Session, expiresInSeconds: number): Promise<void>;
445
- getSession(sessionId: string): Promise<Session | null>;
627
+ saveSession<T = unknown>(sessionId: string, session: T, expiresInSeconds: number): Promise<void>;
628
+ getSession<T = unknown>(sessionId: string): Promise<T | null>;
446
629
  deleteSession(sessionId: string): Promise<void>;
447
630
  }
448
631
 
@@ -455,21 +638,321 @@ interface IProvider {
455
638
 
456
639
  ## Built-in Providers
457
640
 
458
- ### Google
641
+ Built-in providers are available in the `@vunexa/lixa-providers` package:
642
+
643
+ ```bash
644
+ npm install @vunexa/lixa-providers
645
+ ```
646
+
647
+ ### Google (`GoogleProvider`)
648
+
649
+ ```typescript
650
+ import { GoogleProvider } from "@vunexa/lixa-providers";
651
+ ```
652
+
459
653
  - **Type**: OAuth 2.0 + OpenID Connect (OIDC)
460
654
  - **Authorization Endpoint**: `https://accounts.google.com/o/oauth2/v2/auth`
461
655
  - **Token Endpoint**: `https://oauth2.googleapis.com/token`
462
656
  - **User Info Endpoint**: `https://www.googleapis.com/oauth2/v2/userinfo`
463
657
  - **Supports**: PKCE, ID tokens, refresh tokens
464
- - **Common Scopes**: `openid`, `email`, `profile`
658
+ - **Common Scopes**:
659
+ - `openid` - Required for OpenID Connect
660
+ - `email` - Access to user's email address
661
+ - `profile` - Access to user's basic profile information
662
+ - `https://www.googleapis.com/auth/drive.readonly` - Read-only access to Google Drive
663
+ - `https://www.googleapis.com/auth/calendar.readonly` - Read-only access to Google Calendar
664
+
665
+ [Full list of Google OAuth scopes](https://developers.google.com/identity/protocols/oauth2/scopes)
666
+
667
+ ### GitHub (`GithubProvider`)
668
+
669
+ ```typescript
670
+ import { GithubProvider } from "@vunexa/lixa-providers";
671
+ ```
465
672
 
466
- ### GitHub
467
673
  - **Type**: OAuth 2.0
468
674
  - **Authorization Endpoint**: `https://github.com/login/oauth/authorize`
469
675
  - **Token Endpoint**: `https://github.com/login/oauth/access_token`
470
676
  - **User Info Endpoint**: `https://api.github.com/user`
471
677
  - **Supports**: PKCE
472
- - **Common Scopes**: `read:user`, `user:email`
678
+ - **Common Scopes**:
679
+ - `user` - Read/write access to profile info
680
+ - `user:email` - Read access to user's email addresses
681
+ - `read:user` - Read-only access to profile info
682
+ - `repo` - Full control of private repositories
683
+ - `public_repo` - Access to public repositories
684
+ - `gist` - Create gists
685
+ - `read:org` - Read-only access to organization membership
686
+
687
+ [Full list of GitHub OAuth scopes](https://docs.github.com/en/apps/oauth-apps/building-oauth-apps/scopes-for-oauth-apps)
688
+
689
+ ## Extensibility Interfaces
690
+
691
+ ### SessionStrategy Interface
692
+
693
+ The `SessionStrategy` interface allows you to customize how OAuth tokens are converted into application sessions. The `tokenData` parameter is fully typed as `OAuthTokenResponse`:
694
+
695
+ ```typescript
696
+ interface SessionStrategy {
697
+ createSession(tokenData: OAuthTokenResponse): Promise<Session>;
698
+ }
699
+
700
+ interface Session<TRaw = OAuthTokenResponse> {
701
+ token: string; // Your session identifier
702
+ raw: TRaw; // Session data (defaults to OAuthTokenResponse)
703
+ }
704
+
705
+ interface OAuthTokenResponse {
706
+ access_token: string; // Required
707
+ token_type: string; // Required
708
+ expires_in?: number; // Optional
709
+ refresh_token?: string; // Optional
710
+ scope?: string; // Optional
711
+ id_token?: string; // Optional (OIDC)
712
+ [key: string]: unknown; // Provider-specific fields
713
+ }
714
+ ```
715
+
716
+ **Use cases:**
717
+ - Decode ID tokens for user information
718
+ - Create or update users in your database
719
+ - Generate custom session identifiers
720
+ - Store tokens securely
721
+ - Add custom claims or metadata
722
+
723
+ **Example with database integration and proper typing:**
724
+ ```typescript
725
+ import { SessionStrategy, OAuthTokenResponse, Session } from "@vunexa/lixa";
726
+
727
+ interface CustomSessionData extends OAuthTokenResponse {
728
+ userId: string;
729
+ }
730
+
731
+ const dbSessionStrategy: SessionStrategy = {
732
+ createSession: async (tokenData: OAuthTokenResponse): Promise<Session<CustomSessionData>> => {
733
+ // tokenData is properly typed with OAuth 2.0 fields
734
+ // TypeScript will catch errors like tokenData.id_tokn (typo)
735
+
736
+ // Decode ID token (for OIDC providers)
737
+ const payload = tokenData.id_token
738
+ ? decodeJwt(tokenData.id_token)
739
+ : null;
740
+
741
+ // Create/update user
742
+ const user = await db.users.upsert({
743
+ email: payload?.email,
744
+ name: payload?.name
745
+ });
746
+
747
+ // Generate session
748
+ const sessionId = generateId();
749
+ await db.sessions.create({
750
+ id: sessionId,
751
+ userId: user.id,
752
+ accessToken: tokenData.access_token,
753
+ refreshToken: tokenData.refresh_token,
754
+ expiresAt: tokenData.expires_in
755
+ ? new Date(Date.now() + tokenData.expires_in * 1000)
756
+ : null
757
+ });
758
+
759
+ return {
760
+ token: sessionId,
761
+ raw: {
762
+ ...tokenData,
763
+ userId: user.id
764
+ }
765
+ };
766
+ }
767
+ };
768
+ ```
769
+
770
+ ### StateDao Interface
771
+
772
+ The `StateDao` interface manages OAuth state storage for CSRF protection and PKCE:
773
+
774
+ ```typescript
775
+ interface StateDao {
776
+ saveState(state: string, data: StateData, expiresInSeconds: number): Promise<void>;
777
+ getState(state: string): Promise<StateData | null>;
778
+ deleteState(state: string): Promise<void>;
779
+ }
780
+
781
+ interface StateData {
782
+ provider: string; // Provider name for routing
783
+ codeVerifier: string; // PKCE code verifier (64 hex chars)
784
+ createdAt: number; // Unix timestamp in milliseconds
785
+ }
786
+ ```
787
+
788
+ **Required fields:**
789
+ - `provider`: Used to route callbacks to the correct provider configuration
790
+ - `codeVerifier`: Required for PKCE token exchange (RFC 7636)
791
+ - `createdAt`: Timestamp for debugging and validation
792
+
793
+ **Example with Redis:**
794
+ ```typescript
795
+ const redisStateDao: StateDao = {
796
+ saveState: async (state, data, expiresInSeconds) => {
797
+ await redis.setex(
798
+ `oauth:state:${state}`,
799
+ expiresInSeconds,
800
+ JSON.stringify(data)
801
+ );
802
+ },
803
+
804
+ getState: async (state) => {
805
+ const data = await redis.get(`oauth:state:${state}`);
806
+ return data ? JSON.parse(data) : null;
807
+ },
808
+
809
+ deleteState: async (state) => {
810
+ await redis.del(`oauth:state:${state}`);
811
+ }
812
+ };
813
+ ```
814
+
815
+ ### SessionDao Interface
816
+
817
+ The `SessionDao` interface manages persistent user session storage with generic typing for flexibility:
818
+
819
+ ```typescript
820
+ interface SessionDao {
821
+ saveSession<T = unknown>(sessionId: string, data: T, expiresInSeconds: number): Promise<void>;
822
+ getSession<T = unknown>(sessionId: string): Promise<T | null>;
823
+ deleteSession(sessionId: string): Promise<void>;
824
+ }
825
+ ```
826
+
827
+ **Example with database and proper typing:**
828
+ ```typescript
829
+ import { SessionDao, Session } from "@vunexa/lixa";
830
+
831
+ const dbSessionDao: SessionDao = {
832
+ saveSession: async <T = unknown>(sessionId: string, data: T, expiresInSeconds: number) => {
833
+ const expiresAt = new Date(Date.now() + expiresInSeconds * 1000);
834
+ await db.sessions.create({
835
+ id: sessionId,
836
+ data: JSON.stringify(data),
837
+ expiresAt
838
+ });
839
+ },
840
+
841
+ getSession: async <T = unknown>(sessionId: string): Promise<T | null> => {
842
+ const session = await db.sessions.findOne({
843
+ id: sessionId,
844
+ expiresAt: { $gt: new Date() }
845
+ });
846
+ return session ? JSON.parse(session.data) as T : null;
847
+ },
848
+
849
+ deleteSession: async (sessionId: string) => {
850
+ await db.sessions.delete({ id: sessionId });
851
+ }
852
+ };
853
+ ```
854
+
855
+ ## PKCE Implementation
856
+
857
+ Lixa automatically implements **PKCE (Proof Key for Code Exchange)** according to [RFC 7636](https://tools.ietf.org/html/rfc7636) for all OAuth flows.
858
+
859
+ ### How it Works
860
+
861
+ 1. **Code Verifier Generation**
862
+ - Generates 32 cryptographically random bytes
863
+ - Encodes as 64-character hexadecimal string
864
+ - Example: `a1b2c3d4e5f6...` (64 chars)
865
+
866
+ 2. **Code Challenge Generation**
867
+ - Creates SHA-256 hash of the code verifier
868
+ - Encodes as base64url (RFC 4648)
869
+ - Sends with authorization request
870
+
871
+ 3. **Token Exchange**
872
+ - Retrieves code verifier from state storage
873
+ - Sends original code verifier with token request
874
+ - Provider validates: `SHA256(code_verifier) === code_challenge`
875
+
876
+ ### Security Benefits
877
+
878
+ - **Prevents authorization code interception**: Even if an attacker intercepts the authorization code, they cannot exchange it for tokens without the code verifier
879
+ - **No client secret required**: PKCE works without client secrets, making it suitable for public clients
880
+ - **Standards compliant**: Follows RFC 7636 specification
881
+
882
+ ### Debug Logging
883
+
884
+ Enable debug mode to see PKCE flow details:
885
+
886
+ ```typescript
887
+ const lixa = new Lixa({
888
+ providers: { /* ... */ },
889
+ debug: true
890
+ });
891
+ ```
892
+
893
+ Debug output includes:
894
+ - `[Lixa] [timestamp] [INFO] [Auth]` - Code verifier and challenge generation
895
+ - `[Lixa] [timestamp] [INFO] [State]` - State storage with code verifier
896
+ - `[Lixa] [timestamp] [INFO] [Token]` - Token exchange with code verifier
897
+
898
+ ## Migration Guide
899
+
900
+ ### Migrating from Legacy Registration Pattern
901
+
902
+ **Old Pattern (Deprecated):**
903
+ ```typescript
904
+ import { Lixa, IProvider } from "@vunexa/lixa";
905
+ import { GoogleProvider } from "@vunexa/lixa";
906
+
907
+ // Step 1: Register providers globally
908
+ Lixa.registerProvider({
909
+ google: new GoogleProvider()
910
+ });
911
+
912
+ // Step 2: Configure with credentials only
913
+ const lixa = new Lixa({
914
+ providers: {
915
+ google: {
916
+ clientId: "...",
917
+ clientSecret: "...",
918
+ redirectUri: "...",
919
+ scopes: ["openid", "email"]
920
+ }
921
+ }
922
+ });
923
+ ```
924
+
925
+ **New Pattern (Recommended):**
926
+ ```typescript
927
+ import { Lixa } from "@vunexa/lixa";
928
+ import { GoogleProvider } from "@vunexa/lixa-providers";
929
+
930
+ // Single step: Pass provider inline
931
+ const lixa = new Lixa({
932
+ providers: {
933
+ google: {
934
+ provider: new GoogleProvider(), // ← Add this line
935
+ clientId: "...",
936
+ clientSecret: "...",
937
+ redirectUri: "...",
938
+ scopes: ["openid", "email"]
939
+ }
940
+ }
941
+ });
942
+ ```
943
+
944
+ **Migration Steps:**
945
+ 1. Install `@vunexa/lixa-providers`: `npm install @vunexa/lixa-providers`
946
+ 2. Update imports: `import { GoogleProvider } from "@vunexa/lixa-providers"`
947
+ 3. Remove `Lixa.registerProvider()` calls
948
+ 4. Add `provider` field to each provider configuration
949
+ 5. Test your OAuth flows
950
+
951
+ **Benefits:**
952
+ - ✅ No global state - each Lixa instance is independent
953
+ - ✅ Better testability - easy to mock providers per instance
954
+ - ✅ Clearer configuration - everything in one place
955
+ - ✅ Type safety - TypeScript knows which providers are configured
473
956
 
474
957
  ## Security Considerations
475
958