@vunexa/lixa 0.0.1-alpha.29 → 0.0.1-alpha.33

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/README.md CHANGED
@@ -13,14 +13,16 @@ A flexible, provider-agnostic OAuth 2.0 and OpenID Connect (OIDC) client library
13
13
 
14
14
  ## Features
15
15
 
16
- - Multi-provider OAuth/OIDC support with unified API
17
- - Built-in support for popular providers like Google and GitHub
18
- - Custom provider registration with extensible provider interface
19
- - Extensible session management via pluggable strategies
20
- - PKCE (Proof Key for Code Exchange) support
21
- - TypeScript-first with full type safety (zero `any` types)
22
- - OAuth 2.0 and OpenID Connect spec-compliant types
23
- - 100% test coverage with comprehensive error handling
16
+ - **Multi-provider OAuth/OIDC support** with unified API
17
+ - **Built-in providers** for Google, GitHub, and more via `@vunexa/lixa-providers`
18
+ - **Custom provider support** with extensible provider interface
19
+ - **Unified session management** via `SessionHandler` (generation + storage in one place)
20
+ - **Unified state management** via `StateHandler` (generation + storage in one place)
21
+ - **Automatic PKCE** (Proof Key for Code Exchange) for all OAuth flows
22
+ - **User info utilities** for extracting user data from OAuth tokens
23
+ - **TypeScript-first** with full type safety (zero `any` types)
24
+ - **OAuth 2.0 and OpenID Connect** spec-compliant types
25
+ - **100% test coverage** with comprehensive error handling
24
26
 
25
27
  ---
26
28
 
@@ -68,9 +70,10 @@ const lixa = new Lixa({
68
70
  },
69
71
  },
70
72
 
71
- // Optional: custom session strategy
72
- sessionStrategy: {
73
- createSession: async (tokenData) => {
73
+ // Optional: custom session handler
74
+ sessionHandler: {
75
+ // Optional: customize session generation
76
+ generateSession: async (tokenData, providerMetadata) => {
74
77
  // tokenData is typed as OAuthTokenResponse with proper OAuth 2.0 fields
75
78
  // { access_token, token_type, expires_in?, refresh_token?, scope?, id_token? }
76
79
  return {
@@ -78,6 +81,19 @@ const lixa = new Lixa({
78
81
  raw: tokenData,
79
82
  };
80
83
  },
84
+ // Optional: custom session storage
85
+ sessionStorage: {
86
+ saveSession: async (sessionId, session, ttl) => {
87
+ await db.sessions.create({ id: sessionId, data: session, expiresAt: Date.now() + ttl * 1000 });
88
+ },
89
+ getSession: async (sessionId) => {
90
+ const session = await db.sessions.findOne({ id: sessionId });
91
+ return session?.data || null;
92
+ },
93
+ deleteSession: async (sessionId) => {
94
+ await db.sessions.delete({ id: sessionId });
95
+ },
96
+ },
81
97
  },
82
98
  });
83
99
  ```
@@ -85,14 +101,11 @@ const lixa = new Lixa({
85
101
  ### 3. Redirect users to the provider's authorization URL
86
102
 
87
103
  ```typescript
88
- app.get("/login", (req, res) => {
104
+ app.get("/login", async (req, res) => {
89
105
  const provider = req.query.provider as string; // 'google' or 'github'
90
- const state = Lixa.generateRandomState();
91
-
92
- // Store state in session for validation
93
- req.session.oauthState = state;
94
106
 
95
- const authUrl = lixa.getAuthUrl(provider, state);
107
+ // getAuthUrl automatically generates state if not provided
108
+ const authUrl = await lixa.getAuthUrl(provider);
96
109
  res.redirect(authUrl);
97
110
  });
98
111
  ```
@@ -105,11 +118,7 @@ app.get("/auth/:provider/callback", async (req, res) => {
105
118
  const provider = req.params.provider;
106
119
 
107
120
  try {
108
- // Validate state parameter
109
- if (state !== req.session.oauthState) {
110
- throw new Error("Invalid state parameter");
111
- }
112
-
121
+ // State validation is handled automatically by lixa
113
122
  const sessionId = await lixa.handleCallback({
114
123
  provider,
115
124
  code: code as string,
@@ -248,15 +257,15 @@ const lixa = new Lixa({
248
257
 
249
258
  ## Advanced Usage
250
259
 
251
- ### Custom State Storage (StateDao)
260
+ ### Custom State Handler
252
261
 
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:
262
+ By default, Lixa uses an in-memory cache for state management. For production applications or serverless environments, you should implement a custom state handler:
254
263
 
255
264
  ```typescript
256
- import { Lixa, StateDao, StateData } from "@vunexa/lixa";
265
+ import { Lixa, StateHandler, StateStorage, StateData } from "@vunexa/lixa";
257
266
 
258
- // Example: DynamoDB state storage with proper typing
259
- const dynamoDbStateDao: StateDao = {
267
+ // Example: DynamoDB state storage
268
+ const dynamoDbStateStorage: StateStorage = {
260
269
  saveState: async (state: string, data: StateData, expiresInSeconds: number) => {
261
270
  await dynamoDB.put({
262
271
  TableName: "oauth-states",
@@ -293,27 +302,32 @@ const dynamoDbStateDao: StateDao = {
293
302
 
294
303
  const lixa = new Lixa({
295
304
  providers: { /* ... */ },
296
- stateDao: dynamoDbStateDao,
305
+ stateHandler: {
306
+ stateStorage: dynamoDbStateStorage,
307
+ // Optional: customize state generation
308
+ // generateState: async (provider) => { ... }
309
+ },
297
310
  });
298
311
  ```
299
312
 
300
- **Important**: The `StateData` type includes required fields:
313
+ **StateHandler Properties:**
314
+ - `stateStorage` (optional): Custom storage for OAuth state (save/get/delete operations)
315
+ - `generateState` (optional): Custom state and PKCE verifier generation
316
+
317
+ **StateData Fields:**
301
318
  - `provider` (string): Provider name for callback routing
302
319
  - `codeVerifier` (string): PKCE code verifier (64 hex characters)
303
320
  - `createdAt` (number): Unix timestamp in milliseconds
304
321
 
305
- Make sure your storage preserves all fields in the data object.
306
-
307
- ### Custom Session Storage (SessionDao)
322
+ ### Custom Session Handler
308
323
 
309
- Similar to state storage, you can implement custom session storage with proper typing:
324
+ The session handler manages both session generation and storage. You can customize either or both:
310
325
 
311
326
  ```typescript
312
- import { Lixa, SessionDao, Session } from "@vunexa/lixa";
327
+ import { Lixa, SessionHandler, SessionStorage, extractUserInfo } from "@vunexa/lixa";
313
328
 
314
- const customSessionDao: SessionDao = {
329
+ const customSessionStorage: SessionStorage = {
315
330
  saveSession: async <T = unknown>(sessionId: string, session: T, expiresInSeconds: number) => {
316
- // Store session in your database
317
331
  await db.sessions.create({
318
332
  id: sessionId,
319
333
  data: session,
@@ -336,16 +350,39 @@ const customSessionDao: SessionDao = {
336
350
 
337
351
  const lixa = new Lixa({
338
352
  providers: { /* ... */ },
339
- sessionDao: customSessionDao,
353
+ sessionHandler: {
354
+ // Optional: customize session generation
355
+ generateSession: async (tokenData, providerMetadata) => {
356
+ // Extract user info from OAuth tokens
357
+ const { userInfo } = await extractUserInfo(tokenData, providerMetadata);
358
+
359
+ // Create or update user in database
360
+ const user = await db.users.upsert({
361
+ email: userInfo.email,
362
+ name: userInfo.name,
363
+ });
364
+
365
+ return {
366
+ token: tokenData.access_token,
367
+ raw: { ...tokenData, userId: user.id },
368
+ };
369
+ },
370
+ // Optional: custom session storage
371
+ sessionStorage: customSessionStorage,
372
+ },
340
373
  });
341
374
  ```
342
375
 
343
- ### Custom Session Strategy
376
+ **SessionHandler Properties:**
377
+ - `generateSession` (optional): Customize how OAuth tokens are converted to session data
378
+ - `sessionStorage` (optional): Custom storage for sessions (save/get/delete operations)
379
+
380
+ ### Custom Session Generation
344
381
 
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:
382
+ The `generateSession` method controls how user sessions are created from OAuth tokens:
346
383
 
347
384
  ```typescript
348
- import { Lixa, SessionStrategy, OAuthTokenResponse, Session } from "@vunexa/lixa";
385
+ import { Lixa, SessionHandler, OAuthTokenResponse, Session, extractUserInfo } from "@vunexa/lixa";
349
386
 
350
387
  // Define custom session data structure
351
388
  interface CustomSessionData extends OAuthTokenResponse {
@@ -356,8 +393,8 @@ interface CustomSessionData extends OAuthTokenResponse {
356
393
  };
357
394
  }
358
395
 
359
- const customSessionStrategy: SessionStrategy = {
360
- createSession: async (tokenData: OAuthTokenResponse): Promise<Session<CustomSessionData>> => {
396
+ const customSessionHandler: SessionHandler = {
397
+ generateSession: async (tokenData: OAuthTokenResponse, providerMetadata): Promise<Session<CustomSessionData>> => {
361
398
  // tokenData is properly typed with OAuth 2.0 fields:
362
399
  // - access_token: string (required)
363
400
  // - token_type: string (required)
@@ -366,13 +403,8 @@ const customSessionStrategy: SessionStrategy = {
366
403
  // - scope?: string
367
404
  // - id_token?: string (for OIDC providers)
368
405
 
369
- // Decode ID token to get user info (for OIDC providers like Google)
370
- let userInfo;
371
- if (tokenData.id_token) {
372
- const base64Payload = tokenData.id_token.split('.')[1];
373
- const payload = Buffer.from(base64Payload, 'base64').toString();
374
- userInfo = JSON.parse(payload);
375
- }
406
+ // Extract user info using lixa's utility
407
+ const { userInfo } = await extractUserInfo(tokenData, providerMetadata);
376
408
 
377
409
  // Create or update user in your database
378
410
  const user = await db.users.upsert({
@@ -381,17 +413,8 @@ const customSessionStrategy: SessionStrategy = {
381
413
  // ... other fields
382
414
  });
383
415
 
384
- // Create session in your database
385
- const sessionId = generateUniqueId();
386
- await db.sessions.create({
387
- id: sessionId,
388
- userId: user.id,
389
- accessToken: tokenData.access_token,
390
- refreshToken: tokenData.refresh_token,
391
- });
392
-
393
416
  return {
394
- token: sessionId,
417
+ token: tokenData.access_token,
395
418
  raw: {
396
419
  ...tokenData,
397
420
  userId: user.id,
@@ -399,11 +422,25 @@ const customSessionStrategy: SessionStrategy = {
399
422
  },
400
423
  };
401
424
  },
425
+
426
+ // Optional: custom storage
427
+ sessionStorage: {
428
+ saveSession: async (sessionId, session, ttl) => {
429
+ await db.sessions.create({ id: sessionId, data: session, expiresAt: Date.now() + ttl * 1000 });
430
+ },
431
+ getSession: async (sessionId) => {
432
+ const session = await db.sessions.findOne({ id: sessionId });
433
+ return session?.data || null;
434
+ },
435
+ deleteSession: async (sessionId) => {
436
+ await db.sessions.delete({ id: sessionId });
437
+ },
438
+ },
402
439
  };
403
440
 
404
441
  const lixa = new Lixa({
405
442
  providers: { /* ... */ },
406
- sessionStrategy: customSessionStrategy,
443
+ sessionHandler: customSessionHandler,
407
444
  });
408
445
  ```
409
446
 
@@ -476,21 +513,36 @@ console.log("Registered providers:", providers);
476
513
 
477
514
  **Cause**: The state parameter is not being stored or retrieved correctly.
478
515
 
479
- **Solution**: Implement a custom `stateDao` that persists state across requests. The default in-memory cache doesn't work in serverless or multi-instance environments.
516
+ **Solution**: Implement a custom `stateHandler` with persistent storage. The default in-memory cache doesn't work in serverless or multi-instance environments.
517
+
518
+ ```typescript
519
+ const lixa = new Lixa({
520
+ providers: { /* ... */ },
521
+ stateHandler: {
522
+ stateStorage: {
523
+ saveState: async (state, data, ttl) => await redis.setex(state, ttl, JSON.stringify(data)),
524
+ getState: async (state) => JSON.parse(await redis.get(state) || 'null'),
525
+ deleteState: async (state) => await redis.del(state),
526
+ },
527
+ },
528
+ });
529
+ ```
480
530
 
481
531
  ### 2. "Missing code verifier" Error
482
532
 
483
533
  **Cause**: The `codeVerifier` field is not being preserved in your state storage.
484
534
 
485
- **Solution**: Ensure your `stateDao.saveState()` stores ALL fields from the data object, including `codeVerifier`:
535
+ **Solution**: Ensure your `stateStorage.saveState()` stores ALL fields from the data object, including `codeVerifier`:
486
536
 
487
537
  ```typescript
488
- saveState: async (state: string, data: any, expiresInSeconds: number) => {
489
- // Correct: Store the entire data object
490
- await storage.save({ state, data, expiresAt: ... });
491
-
492
- // ❌ Wrong: Only storing some fields
493
- await storage.save({ state, provider: data.provider, expiresAt: ... });
538
+ stateStorage: {
539
+ saveState: async (state: string, data: StateData, expiresInSeconds: number) => {
540
+ // Correct: Store the entire data object
541
+ await storage.save({ state, data, expiresAt: ... });
542
+
543
+ // Wrong: Only storing some fields
544
+ await storage.save({ state, provider: data.provider, expiresAt: ... });
545
+ }
494
546
  }
495
547
  ```
496
548
 
@@ -510,32 +562,37 @@ saveState: async (state: string, data: any, expiresInSeconds: number) => {
510
562
 
511
563
  **Cause**: Using default in-memory session storage in serverless/distributed environments.
512
564
 
513
- **Solution**: Implement a custom `sessionDao` that uses persistent storage (database, Redis, etc.).
565
+ **Solution**: Implement a custom `sessionHandler` with persistent storage (database, Redis, etc.):
566
+
567
+ ```typescript
568
+ const lixa = new Lixa({
569
+ providers: { /* ... */ },
570
+ sessionHandler: {
571
+ sessionStorage: {
572
+ saveSession: async (id, session, ttl) => await db.sessions.create({ id, session, ttl }),
573
+ getSession: async (id) => await db.sessions.findOne({ id }),
574
+ deleteSession: async (id) => await db.sessions.delete({ id }),
575
+ },
576
+ },
577
+ });
578
+ ```
514
579
 
515
580
  ### 5. User Info is Undefined
516
581
 
517
- **Cause**: The `sessionStrategy.createSession()` receives raw token data, not user info.
582
+ **Cause**: The `generateSession()` receives raw token data, not user info.
518
583
 
519
- **Solution**: Decode the `id_token` (for OIDC providers) or fetch user info from the provider's API:
584
+ **Solution**: Use lixa's `extractUserInfo()` utility to automatically extract user info:
520
585
 
521
586
  ```typescript
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
- }
587
+ import { SessionHandler, extractUserInfo } from "@vunexa/lixa";
588
+
589
+ const sessionHandler: SessionHandler = {
590
+ generateSession: async (tokenData, providerMetadata) => {
591
+ // Automatically extracts user info from ID token or fetches from API
592
+ const { userInfo } = await extractUserInfo(tokenData, providerMetadata);
531
593
 
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
- }
594
+ // Now you have user info
595
+ console.log(userInfo.email, userInfo.name);
539
596
 
540
597
  return {
541
598
  token: tokenData.access_token,
@@ -556,9 +613,9 @@ High-level function that automatically extracts user information from OAuth toke
556
613
  ```typescript
557
614
  import { extractUserInfo, type UserInfo } from "@vunexa/lixa";
558
615
 
559
- // In your session strategy
560
- const sessionStrategy = {
561
- createSession: async (tokenData, providerMetadata) => {
616
+ // In your session handler
617
+ const sessionHandler = {
618
+ generateSession: async (tokenData, providerMetadata) => {
562
619
  // Automatically extracts user info from ID token or fetches from API
563
620
  const { userInfo } = await extractUserInfo(tokenData, providerMetadata);
564
621
 
@@ -684,8 +741,8 @@ const lixa = new Lixa({
684
741
  }
685
742
  },
686
743
 
687
- sessionStrategy: {
688
- createSession: async (tokenData, providerMetadata) => {
744
+ sessionHandler: {
745
+ generateSession: async (tokenData, providerMetadata) => {
689
746
  // Extract user info - works for both Google (ID token) and GitHub (API fetch)
690
747
  const { userInfo } = await extractUserInfo(tokenData, providerMetadata);
691
748
 
@@ -701,26 +758,32 @@ const lixa = new Lixa({
701
758
  emailVerified: userInfo.email_verified,
702
759
  });
703
760
 
704
- // Create session
705
- const sessionId = generateUniqueId();
706
- await db.sessions.create({
707
- id: sessionId,
708
- userId: user.id,
709
- accessToken: tokenData.access_token,
710
- refreshToken: tokenData.refresh_token,
711
- expiresAt: tokenData.expires_in
712
- ? new Date(Date.now() + tokenData.expires_in * 1000)
713
- : null,
714
- });
715
-
716
761
  return {
717
- token: sessionId,
762
+ token: tokenData.access_token,
718
763
  raw: {
719
764
  ...tokenData,
720
765
  userId: user.id,
721
766
  }
722
767
  };
723
- }
768
+ },
769
+ sessionStorage: {
770
+ saveSession: async (sessionId, session, ttl) => {
771
+ await db.sessions.create({
772
+ id: sessionId,
773
+ userId: session.raw.userId,
774
+ accessToken: session.raw.access_token,
775
+ refreshToken: session.raw.refresh_token,
776
+ expiresAt: new Date(Date.now() + ttl * 1000),
777
+ });
778
+ },
779
+ getSession: async (sessionId) => {
780
+ const session = await db.sessions.findOne({ id: sessionId });
781
+ return session ? { token: session.accessToken, raw: session } : null;
782
+ },
783
+ deleteSession: async (sessionId) => {
784
+ await db.sessions.delete({ id: sessionId });
785
+ },
786
+ },
724
787
  }
725
788
  });
726
789
  ```
@@ -744,15 +807,14 @@ const lixa = new Lixa({
744
807
 
745
808
  #### Static Methods
746
809
 
747
- - `Lixa.generateRandomState(): string` - Generate a cryptographically secure random state string for CSRF protection
748
810
  - `Lixa.registerProvider(providerMap: { [key: string]: IProvider })` - **[Deprecated]** Register custom providers globally. Use inline providers instead.
749
811
  - `Lixa.getRegisteredProviders(): string[]` - **[Deprecated]** Get list of all registered provider names from legacy registry
750
812
 
751
813
  #### Instance Methods
752
814
 
753
- - `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.
754
- - `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.
755
- - `fetchSessionInfo(sessionId: string): Promise<Session | null>` - Retrieve session information by session ID from SessionDao.
815
+ - `getAuthUrl(provider: string, state?: string): Promise<string>` - Generate authorization URL for a provider. Automatically generates state if not provided, creates PKCE code verifier and challenge, stores state with code verifier for later validation.
816
+ - `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 SessionHandler. Returns session ID.
817
+ - `fetchSessionInfo(sessionId: string): Promise<Session | null>` - Retrieve session information by session ID from SessionHandler storage.
756
818
  - `isProviderConfigured(provider: string): boolean` - Check if a provider is configured for this Lixa instance. Useful for type guards and runtime validation.
757
819
 
758
820
  ### Types
@@ -785,14 +847,34 @@ type ProviderConfig = {
785
847
 
786
848
  interface LixaConfig<TProviders = Record<string, ProviderConfig>> {
787
849
  providers: TProviders;
788
- sessionStrategy?: SessionStrategy;
789
- stateDao?: StateDao;
790
- sessionDao?: SessionDao;
850
+ sessionHandler?: SessionHandler;
851
+ stateHandler?: StateHandler;
791
852
  debug?: boolean;
792
853
  }
793
854
 
794
- interface SessionStrategy {
795
- createSession(tokenData: OAuthTokenResponse): Promise<Session>;
855
+ interface SessionHandler {
856
+ generateSession?<T extends Session>(
857
+ tokenData: OAuthTokenResponse,
858
+ providerMetadata: ProviderMetadata
859
+ ): Promise<T>;
860
+ sessionStorage?: SessionStorage;
861
+ }
862
+
863
+ interface SessionStorage {
864
+ saveSession<T extends Session>(sessionId: string, session: T, expiresInSeconds: number): Promise<void>;
865
+ getSession<T extends Session>(sessionId: string): Promise<T | null>;
866
+ deleteSession(sessionId: string): Promise<void>;
867
+ }
868
+
869
+ interface StateHandler {
870
+ generateState?(provider: string): Promise<{ state: string; data: StateData }>;
871
+ stateStorage?: StateStorage;
872
+ }
873
+
874
+ interface StateStorage {
875
+ saveState(state: string, data: StateData, expiresInSeconds: number): Promise<void>;
876
+ getState(state: string): Promise<StateData | null>;
877
+ deleteState(state: string): Promise<void>;
796
878
  }
797
879
 
798
880
  interface Session<TRaw = OAuthTokenResponse> {
@@ -806,16 +888,13 @@ interface StateData {
806
888
  createdAt: number; // Unix timestamp in milliseconds
807
889
  }
808
890
 
809
- interface StateDao {
810
- saveState(state: string, data: StateData, expiresInSeconds: number): Promise<void>;
811
- getState(state: string): Promise<StateData | null>;
812
- deleteState(state: string): Promise<void>;
813
- }
814
-
815
- interface SessionDao {
816
- saveSession<T = unknown>(sessionId: string, session: T, expiresInSeconds: number): Promise<void>;
817
- getSession<T = unknown>(sessionId: string): Promise<T | null>;
818
- deleteSession(sessionId: string): Promise<void>;
891
+ interface ProviderMetadata {
892
+ name: string;
893
+ endpoints: {
894
+ authorization: string;
895
+ token: string;
896
+ userInfo: string;
897
+ };
819
898
  }
820
899
 
821
900
  interface IProvider {
@@ -877,13 +956,23 @@ import { GithubProvider } from "@vunexa/lixa-providers";
877
956
 
878
957
  ## Extensibility Interfaces
879
958
 
880
- ### SessionStrategy Interface
959
+ ### SessionHandler Interface
881
960
 
882
- The `SessionStrategy` interface allows you to customize how OAuth tokens are converted into application sessions. The `tokenData` parameter is fully typed as `OAuthTokenResponse`:
961
+ The `SessionHandler` interface allows you to customize session generation and storage:
883
962
 
884
963
  ```typescript
885
- interface SessionStrategy {
886
- createSession(tokenData: OAuthTokenResponse): Promise<Session>;
964
+ interface SessionHandler {
965
+ generateSession?<T extends Session>(
966
+ tokenData: OAuthTokenResponse,
967
+ providerMetadata: ProviderMetadata
968
+ ): Promise<T>;
969
+ sessionStorage?: SessionStorage;
970
+ }
971
+
972
+ interface SessionStorage {
973
+ saveSession<T extends Session>(sessionId: string, session: T, expiresInSeconds: number): Promise<void>;
974
+ getSession<T extends Session>(sessionId: string): Promise<T | null>;
975
+ deleteSession(sessionId: string): Promise<void>;
887
976
  }
888
977
 
889
978
  interface Session<TRaw = OAuthTokenResponse> {
@@ -900,68 +989,84 @@ interface OAuthTokenResponse {
900
989
  id_token?: string; // Optional (OIDC)
901
990
  [key: string]: unknown; // Provider-specific fields
902
991
  }
992
+
993
+ interface ProviderMetadata {
994
+ name: string;
995
+ endpoints: {
996
+ authorization: string;
997
+ token: string;
998
+ userInfo: string;
999
+ };
1000
+ }
903
1001
  ```
904
1002
 
905
1003
  **Use cases:**
906
- - Decode ID tokens for user information
1004
+ - Extract user info from OAuth tokens
907
1005
  - Create or update users in your database
908
- - Generate custom session identifiers
909
- - Store tokens securely
1006
+ - Store tokens securely in your database
910
1007
  - Add custom claims or metadata
1008
+ - Implement custom session storage (Redis, database, etc.)
911
1009
 
912
- **Example with database integration and proper typing:**
1010
+ **Example with database integration:**
913
1011
  ```typescript
914
- import { SessionStrategy, OAuthTokenResponse, Session } from "@vunexa/lixa";
1012
+ import { SessionHandler, OAuthTokenResponse, Session, extractUserInfo } from "@vunexa/lixa";
915
1013
 
916
1014
  interface CustomSessionData extends OAuthTokenResponse {
917
1015
  userId: string;
918
1016
  }
919
1017
 
920
- const dbSessionStrategy: SessionStrategy = {
921
- createSession: async (tokenData: OAuthTokenResponse): Promise<Session<CustomSessionData>> => {
922
- // tokenData is properly typed with OAuth 2.0 fields
923
- // TypeScript will catch errors like tokenData.id_tokn (typo)
924
-
925
- // Decode ID token (for OIDC providers)
926
- const payload = tokenData.id_token
927
- ? decodeJwt(tokenData.id_token)
928
- : null;
1018
+ const dbSessionHandler: SessionHandler = {
1019
+ generateSession: async (tokenData: OAuthTokenResponse, providerMetadata): Promise<Session<CustomSessionData>> => {
1020
+ // Extract user info using lixa's utility
1021
+ const { userInfo } = await extractUserInfo(tokenData, providerMetadata);
929
1022
 
930
1023
  // Create/update user
931
1024
  const user = await db.users.upsert({
932
- email: payload?.email,
933
- name: payload?.name
934
- });
935
-
936
- // Generate session
937
- const sessionId = generateId();
938
- await db.sessions.create({
939
- id: sessionId,
940
- userId: user.id,
941
- accessToken: tokenData.access_token,
942
- refreshToken: tokenData.refresh_token,
943
- expiresAt: tokenData.expires_in
944
- ? new Date(Date.now() + tokenData.expires_in * 1000)
945
- : null
1025
+ email: userInfo.email,
1026
+ name: userInfo.name
946
1027
  });
947
1028
 
948
1029
  return {
949
- token: sessionId,
1030
+ token: tokenData.access_token,
950
1031
  raw: {
951
1032
  ...tokenData,
952
1033
  userId: user.id
953
1034
  }
954
1035
  };
955
- }
1036
+ },
1037
+
1038
+ sessionStorage: {
1039
+ saveSession: async (sessionId, session, ttl) => {
1040
+ await db.sessions.create({
1041
+ id: sessionId,
1042
+ userId: session.raw.userId,
1043
+ accessToken: session.raw.access_token,
1044
+ refreshToken: session.raw.refresh_token,
1045
+ expiresAt: new Date(Date.now() + ttl * 1000),
1046
+ });
1047
+ },
1048
+ getSession: async (sessionId) => {
1049
+ const session = await db.sessions.findOne({ id: sessionId });
1050
+ return session ? { token: session.accessToken, raw: session } : null;
1051
+ },
1052
+ deleteSession: async (sessionId) => {
1053
+ await db.sessions.delete({ id: sessionId });
1054
+ },
1055
+ },
956
1056
  };
957
1057
  ```
958
1058
 
959
- ### StateDao Interface
1059
+ ### StateHandler Interface
960
1060
 
961
- The `StateDao` interface manages OAuth state storage for CSRF protection and PKCE:
1061
+ The `StateHandler` interface manages OAuth state generation and storage for CSRF protection and PKCE:
962
1062
 
963
1063
  ```typescript
964
- interface StateDao {
1064
+ interface StateHandler {
1065
+ generateState?(provider: string): Promise<{ state: string; data: StateData }>;
1066
+ stateStorage?: StateStorage;
1067
+ }
1068
+
1069
+ interface StateStorage {
965
1070
  saveState(state: string, data: StateData, expiresInSeconds: number): Promise<void>;
966
1071
  getState(state: string): Promise<StateData | null>;
967
1072
  deleteState(state: string): Promise<void>;
@@ -974,60 +1079,62 @@ interface StateData {
974
1079
  }
975
1080
  ```
976
1081
 
977
- **Required fields:**
1082
+ **StateData fields:**
978
1083
  - `provider`: Used to route callbacks to the correct provider configuration
979
1084
  - `codeVerifier`: Required for PKCE token exchange (RFC 7636)
980
1085
  - `createdAt`: Timestamp for debugging and validation
981
1086
 
982
1087
  **Example with Redis:**
983
1088
  ```typescript
984
- const redisStateDao: StateDao = {
985
- saveState: async (state, data, expiresInSeconds) => {
986
- await redis.setex(
987
- `oauth:state:${state}`,
988
- expiresInSeconds,
989
- JSON.stringify(data)
990
- );
991
- },
992
-
993
- getState: async (state) => {
994
- const data = await redis.get(`oauth:state:${state}`);
995
- return data ? JSON.parse(data) : null;
996
- },
997
-
998
- deleteState: async (state) => {
999
- await redis.del(`oauth:state:${state}`);
1089
+ const redisStateHandler: StateHandler = {
1090
+ stateStorage: {
1091
+ saveState: async (state, data, expiresInSeconds) => {
1092
+ await redis.setex(
1093
+ `oauth:state:${state}`,
1094
+ expiresInSeconds,
1095
+ JSON.stringify(data)
1096
+ );
1097
+ },
1098
+
1099
+ getState: async (state) => {
1100
+ const data = await redis.get(`oauth:state:${state}`);
1101
+ return data ? JSON.parse(data) : null;
1102
+ },
1103
+
1104
+ deleteState: async (state) => {
1105
+ await redis.del(`oauth:state:${state}`);
1106
+ }
1000
1107
  }
1001
1108
  };
1002
1109
  ```
1003
1110
 
1004
- ### SessionDao Interface
1111
+ ### SessionStorage Interface
1005
1112
 
1006
- The `SessionDao` interface manages persistent user session storage with generic typing for flexibility:
1113
+ The `SessionStorage` interface manages persistent user session storage:
1007
1114
 
1008
1115
  ```typescript
1009
- interface SessionDao {
1010
- saveSession<T = unknown>(sessionId: string, data: T, expiresInSeconds: number): Promise<void>;
1011
- getSession<T = unknown>(sessionId: string): Promise<T | null>;
1116
+ interface SessionStorage {
1117
+ saveSession<T extends Session>(sessionId: string, session: T, expiresInSeconds: number): Promise<void>;
1118
+ getSession<T extends Session>(sessionId: string): Promise<T | null>;
1012
1119
  deleteSession(sessionId: string): Promise<void>;
1013
1120
  }
1014
1121
  ```
1015
1122
 
1016
- **Example with database and proper typing:**
1123
+ **Example with database:**
1017
1124
  ```typescript
1018
- import { SessionDao, Session } from "@vunexa/lixa";
1125
+ import { SessionStorage, Session } from "@vunexa/lixa";
1019
1126
 
1020
- const dbSessionDao: SessionDao = {
1021
- saveSession: async <T = unknown>(sessionId: string, data: T, expiresInSeconds: number) => {
1127
+ const dbSessionStorage: SessionStorage = {
1128
+ saveSession: async <T extends Session>(sessionId: string, session: T, expiresInSeconds: number) => {
1022
1129
  const expiresAt = new Date(Date.now() + expiresInSeconds * 1000);
1023
1130
  await db.sessions.create({
1024
1131
  id: sessionId,
1025
- data: JSON.stringify(data),
1132
+ data: JSON.stringify(session),
1026
1133
  expiresAt
1027
1134
  });
1028
1135
  },
1029
1136
 
1030
- getSession: async <T = unknown>(sessionId: string): Promise<T | null> => {
1137
+ getSession: async <T extends Session>(sessionId: string): Promise<T | null> => {
1031
1138
  const session = await db.sessions.findOne({
1032
1139
  id: sessionId,
1033
1140
  expiresAt: { $gt: new Date() }
@@ -1084,65 +1191,6 @@ Debug output includes:
1084
1191
  - `[Lixa] [timestamp] [INFO] [State]` - State storage with code verifier
1085
1192
  - `[Lixa] [timestamp] [INFO] [Token]` - Token exchange with code verifier
1086
1193
 
1087
- ## Migration Guide
1088
-
1089
- ### Migrating from Legacy Registration Pattern
1090
-
1091
- **Old Pattern (Deprecated):**
1092
- ```typescript
1093
- import { Lixa, IProvider } from "@vunexa/lixa";
1094
- import { GoogleProvider } from "@vunexa/lixa";
1095
-
1096
- // Step 1: Register providers globally
1097
- Lixa.registerProvider({
1098
- google: new GoogleProvider()
1099
- });
1100
-
1101
- // Step 2: Configure with credentials only
1102
- const lixa = new Lixa({
1103
- providers: {
1104
- google: {
1105
- clientId: "...",
1106
- clientSecret: "...",
1107
- redirectUri: "...",
1108
- scopes: ["openid", "email"]
1109
- }
1110
- }
1111
- });
1112
- ```
1113
-
1114
- **New Pattern (Recommended):**
1115
- ```typescript
1116
- import { Lixa } from "@vunexa/lixa";
1117
- import { GoogleProvider } from "@vunexa/lixa-providers";
1118
-
1119
- // Single step: Pass provider inline
1120
- const lixa = new Lixa({
1121
- providers: {
1122
- google: {
1123
- provider: new GoogleProvider(), // ← Add this line
1124
- clientId: "...",
1125
- clientSecret: "...",
1126
- redirectUri: "...",
1127
- scopes: ["openid", "email"]
1128
- }
1129
- }
1130
- });
1131
- ```
1132
-
1133
- **Migration Steps:**
1134
- 1. Install `@vunexa/lixa-providers`: `npm install @vunexa/lixa-providers`
1135
- 2. Update imports: `import { GoogleProvider } from "@vunexa/lixa-providers"`
1136
- 3. Remove `Lixa.registerProvider()` calls
1137
- 4. Add `provider` field to each provider configuration
1138
- 5. Test your OAuth flows
1139
-
1140
- **Benefits:**
1141
- - ✅ No global state - each Lixa instance is independent
1142
- - ✅ Better testability - easy to mock providers per instance
1143
- - ✅ Clearer configuration - everything in one place
1144
- - ✅ Type safety - TypeScript knows which providers are configured
1145
-
1146
1194
  ## Security Considerations
1147
1195
 
1148
1196
  ### PKCE (Proof Key for Code Exchange)