@vunexa/lixa 0.1.6-alpha.13 → 0.1.6-alpha.15

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
@@ -1,785 +1,503 @@
1
- # @vunexa/lixa
1
+ <p align="center">
2
+ <img src="https://raw.githubusercontent.com/vunexa/lixa/master/docs/assets/lixa-banner.png" alt="Lixa Banner" width="600" onerror="this.style.display='none'"/>
3
+ </p>
2
4
 
3
- > Package is in active development.
5
+ # 🚀 Lixa (`@vunexa/lixa`)
4
6
 
5
- [![npm version](https://img.shields.io/npm/v/@vunexa/lixa.svg)](https://www.npmjs.com/package/@vunexa/lixa)
7
+ > **The modern, developer-first TypeScript authentication & authorization framework for Node.js.**
8
+ > Built for zero-friction developer experience, strict AuthN/AuthZ separation, multi-SSO account linking, normalized database persistence, and blazing-fast in-memory caching.
9
+
10
+ [![npm version](https://img.shields.io/npm/v/@vunexa/lixa.svg?color=blue)](https://www.npmjs.com/package/@vunexa/lixa)
6
11
  [![npm downloads](https://img.shields.io/npm/dm/@vunexa/lixa.svg)](https://www.npmjs.com/package/@vunexa/lixa)
7
12
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
8
-
9
- A flexible, provider-agnostic OAuth 2.0 and OpenID Connect (OIDC) client library for backend applications.
10
- `@vunexa/lixa` simplifies multi-provider authentication (e.g. Google, GitHub, Microsoft), enforces strict **AuthN/AuthZ separation**, enables **multi-SSO account linking**, and provides a dedicated post-login **Resource Connection API**.
13
+ [![TypeScript](https://img.shields.io/badge/TypeScript-5.0+-3178C6.svg?logo=typescript&logoColor=white)](https://www.typescriptlang.org/)
11
14
 
12
15
  ---
13
16
 
14
- ## Features
15
-
16
- - **Strict AuthN / AuthZ Separation**: Primary login is automatically restricted to minimal identity scopes (`openid`, `email`, `profile`). Resource scopes are isolated to post-login connection.
17
- - **Multi-SSO Account Linking**: Seamlessly merge accounts sharing the same verified email address under a single unified user session.
18
- - **Post-Login Resource Connection API**: Connect third-party API providers (GitHub Repositories, Google Drive, Slack) post-authentication and manage resource tokens on the user session.
19
- - **Multi-provider OAuth/OIDC support** with unified API.
20
- - **Official Extensions Package**: Built-in OAuth Providers (Google, GitHub), Database Storage (Prisma, Drizzle), and HTTP Frameworks (Express) via `@vunexa/lixa-extensions`.
21
- - **Unified session management** via `SessionHandler` (generation + storage).
22
- - **Unified state management** via `StateHandler` (PKCE + CSRF protection).
23
- - **Automatic PKCE** (Proof Key for Code Exchange) for all OAuth flows.
24
- - **TypeScript-first** with full type safety (zero `any` types).
17
+ ## 📑 Table of Contents
18
+
19
+ - [Why Lixa?](#-why-lixa)
20
+ - [Key Features](#-key-features)
21
+ - [Architecture & Sequence Diagrams](#-architecture--sequence-diagrams)
22
+ - [1. Authentication Flow (AuthN)](#1-authentication-flow-authn)
23
+ - [2. Post-Login Resource Connection (AuthZ)](#2-post-login-resource-connection-authz)
24
+ - [3. Multi-SSO Account Linking](#3-multi-sso-account-linking)
25
+ - [Installation](#-installation)
26
+ - [Quickstart: AWS Cognito + Express + Prisma](#-quickstart-aws-cognito--express--prisma)
27
+ - [Step 1: Database Schema (Prisma)](#step-1-database-schema-prisma)
28
+ - [Step 2: Initialize Lixa & Storage with 2-Minute Cache](#step-2-initialize-lixa--storage-with-2-minute-cache)
29
+ - [Step 3: Setup Express Server & Routes](#step-3-setup-express-server--routes)
30
+ - [Step 4: Protect Routes & Use Tokens](#step-4-protect-routes--use-tokens)
31
+ - [Connecting External Resources (AuthZ)](#-connecting-external-resources-authz)
32
+ - [In-Memory 2-Minute Caching (`withLocalCache`)](#-in-memory-2-minute-caching-withlocalcache)
33
+ - [Supported Identity Providers](#-supported-identity-providers)
34
+ - [TypeScript API Reference](#-typescript-api-reference)
35
+ - [Contributing & License](#-contributing--license)
25
36
 
26
37
  ---
27
38
 
28
- ## Architecture Overview
39
+ ## 💡 Why Lixa?
29
40
 
30
- ![Architecture Overview](https://cdn.jsdelivr.net/npm/@vunexa/lixa/docs/images/architecture.svg)
41
+ Most auth libraries blur the line between **authenticating a user** (AuthN) and **requesting access to third-party APIs** (AuthZ). They cram bloated, multi-token JSON blobs into session cookies or single database rows, making user sessions fragile, bloated, and vulnerable.
31
42
 
32
- ---
33
-
34
- ## Installation
35
-
36
- ```bash
37
- npm install @vunexa/lixa @vunexa/lixa-extensions
38
- # or
39
- yarn add @vunexa/lixa @vunexa/lixa-extensions
40
- ```
43
+ **Lixa solves this cleanly:**
44
+ 1. **Single-Purpose Sessions**: A user session contains *only* the authentication tokens (`accessToken`, `refreshToken`, `idToken`) needed for that specific session.
45
+ 2. **First-Class Normalized Database Columns**: No opaque JSON blobs. Every table (`sessions`, `user_accounts`, `user_resources`, `oauth_states`, `user_credentials`) uses primitive typed columns (`userId`, `provider`, `accessToken`, `expiresAt`, etc.) with built-in database indexing.
46
+ 3. **Built-in 2-Minute Local In-Memory Cache**: Eliminates redundant database reads on high-frequency API endpoints while guaranteeing safe invalidation on logout or token updates.
47
+ 4. **Universal Multi-SSO Account Linking**: Seamlessly links AWS Cognito, Google, GitHub, Okta, Auth0, or username/password credentials to a single unified user ID by verified email.
48
+ 5. **Zero-Boilerplate DevXP**: Type-safe out of the box, with full Express middleware, Prisma and Drizzle ORM adapters, and automatic PKCE security.
41
49
 
42
50
  ---
43
51
 
44
- ## Quick Start
52
+ ## ✨ Key Features
45
53
 
46
- ### 1. Configure Lixa with Account Linking & Providers
54
+ - 🛡️ **Strict AuthN / AuthZ Separation**: Identity tokens stay in `sessions`. External API tokens (e.g. GitHub repos, Google Drive) are isolated in `user_resources`.
55
+ - ⚡ **2-Minute In-Memory Cache (`withLocalCache`)**: Blazing-fast response times for session checks and user lookups with zero SQLite/Postgres I/O overhead.
56
+ - 🔗 **Multi-SSO Account Linking**: Automatic or interactive linking of multiple identity providers under one user account.
57
+ - 🗄️ **Clean Database Schema**: Fully normalized Prisma and Drizzle models with primitive column types and indexed foreign keys.
58
+ - 🔐 **PKCE & State Validation**: Automatic RFC 7636 Proof Key for Code Exchange and cryptographic CSRF state verification.
59
+ - ☁️ **Enterprise Identity Backends**: First-class support for **AWS Cognito**, **Auth0**, **Okta**, and **Self-Managed Database** credentials.
47
60
 
48
- ```typescript
49
- import { Lixa, AccountLinkingStrategy } from "@vunexa/lixa";
50
- import { GoogleProvider, GithubProvider } from "@vunexa/lixa-extensions/providers";
61
+ ---
51
62
 
52
- export const lixa = new Lixa({
53
- // Configure Multi-SSO Account Linking Strategy
54
- accountLinking: {
55
- mode: AccountLinkingStrategy.AUTO_LINK_BY_VERIFIED_EMAIL,
56
- requireVerifiedEmail: true,
57
- },
58
- providers: {
59
- google: {
60
- provider: new GoogleProvider(),
61
- clientId: process.env.GOOGLE_CLIENT_ID!,
62
- clientSecret: process.env.GOOGLE_CLIENT_SECRET!,
63
- redirectUri: "http://localhost:3000/auth/google/callback",
64
- scopes: ["openid", "email", "profile"],
65
- },
66
- github: {
67
- provider: new GithubProvider(),
68
- clientId: process.env.GITHUB_CLIENT_ID!,
69
- clientSecret: process.env.GITHUB_CLIENT_SECRET!,
70
- redirectUri: "http://localhost:3000/auth/github/callback",
71
- scopes: ["read:user", "user:email"],
72
- },
73
- },
74
- });
63
+ ## 📐 Architecture & Sequence Diagrams
64
+
65
+ ### 1. Authentication Flow (AuthN)
66
+
67
+ This sequence diagram illustrates how a user logs in via AWS Cognito (or Google/GitHub SSO), receives a secure session cookie, and benefits from Lixa's 2-minute memory cache:
68
+
69
+ ```mermaid
70
+ sequenceDiagram
71
+ autonumber
72
+ actor User as User Browser
73
+ participant Express as Express App (Lixa Middleware)
74
+ participant Lixa as Lixa Engine
75
+ participant Cache as In-Memory Cache (120s TTL)
76
+ participant DB as Prisma / Database
77
+ participant Cognito as AWS Cognito (OIDC / Hosted UI)
78
+
79
+ User->>Express: GET /api/v1/auth/login?provider=cognito
80
+ Express->>Lixa: getAuthUrl("cognito")
81
+ Lixa->>DB: Store state & PKCE codeVerifier in `oauth_states`
82
+ Lixa-->>Express: Return Authorization URL
83
+ Express-->>User: 302 Redirect to AWS Cognito
84
+
85
+ User->>Cognito: User signs in & consents
86
+ Cognito-->>User: 302 Redirect to /api/v1/auth/callback?code=...&state=...
87
+
88
+ User->>Express: GET /api/v1/auth/callback?code=...&state=...
89
+ Express->>Lixa: handleCallback("cognito", code, state)
90
+ Lixa->>DB: Validate & delete state from `oauth_states`
91
+ Lixa->>Cognito: Exchange code + PKCE verifier for tokens (AT, RT, idToken)
92
+ Cognito-->>Lixa: Return Cognito Tokens & User Claims
93
+ Lixa->>DB: Upsert normalized row in `sessions` (userId, AT, RT, idToken)
94
+ Lixa->>Cache: Cache session for 120 seconds
95
+ Lixa-->>Express: Issue Session ID (UUID)
96
+ Express-->>User: Set-Cookie: app_session=<sessionId>; HttpOnly; Secure
97
+
98
+ Note over User, Express: Subsequent Authenticated Requests
99
+ User->>Express: GET /api/v1/me (Cookie: app_session=...)
100
+ Express->>Cache: Check session in memory cache
101
+ alt Cache Hit (< 2 mins)
102
+ Cache-->>Express: Return cached session (0 DB queries!)
103
+ else Cache Miss / Expired
104
+ Express->>DB: Query `sessions` by id
105
+ DB-->>Express: Return session & refresh cache
106
+ end
107
+ Express-->>User: 200 OK (User Profile)
75
108
  ```
76
109
 
77
- ### 2. Primary Authentication & Callback
78
-
79
- ```typescript
80
- // 1. Redirect to provider authorization URL
81
- app.get("/auth/:provider", async (req, res) => {
82
- const authUrl = await lixa.getAuthUrl(req.params.provider);
83
- res.redirect(authUrl);
84
- });
85
-
86
- // 2. Handle provider callback
87
- app.get("/auth/:provider/callback", async (req, res) => {
88
- const { code, state } = req.query;
89
- const provider = req.params.provider;
110
+ ---
90
111
 
91
- try {
92
- const sessionId = await lixa.handleCallback({
93
- provider,
94
- code: code as string,
95
- state: state as string,
96
- });
97
-
98
- res.cookie("session_id", sessionId, { httpOnly: true, secure: true });
99
- res.redirect("/profile");
100
- } catch (error) {
101
- res.status(401).send("Authentication failed");
102
- }
103
- });
112
+ ### 2. Post-Login Resource Connection (AuthZ)
113
+
114
+ When an authenticated user grants your application access to external APIs (e.g. GitHub repos or Google Drive), tokens are stored in `user_resources` and bound to the user's account—completely separate from their login session:
115
+
116
+ ```mermaid
117
+ sequenceDiagram
118
+ autonumber
119
+ actor User as Authenticated User
120
+ participant Express as Express App
121
+ participant Lixa as Lixa Engine
122
+ participant DB as Database (`user_resources`)
123
+ participant GitHub as GitHub OAuth (Resource Provider)
124
+
125
+ User->>Express: GET /api/v1/resources/connect/github
126
+ Express->>Lixa: getResourceAuthUrl("github", { userId: req.session.userId })
127
+ Lixa-->>Express: Return OAuth URL with resource scopes (`repo`, `read:user`)
128
+ Express-->>User: 302 Redirect to GitHub OAuth
129
+
130
+ User->>GitHub: Authorize application for repository access
131
+ GitHub-->>User: 302 Redirect to /api/v1/resources/callback/github?code=...
132
+
133
+ User->>Express: GET /api/v1/resources/callback/github?code=...
134
+ Express->>Lixa: handleResourceCallback("github", code, userId)
135
+ Lixa->>GitHub: Exchange authorization code for Resource Access Token
136
+ GitHub-->>Lixa: Return Access Token & Scopes
137
+ Lixa->>DB: Save to `user_resources` (userId, provider="github", accessToken, scopes)
138
+ Lixa-->>Express: Resource Connected
139
+ Express-->>User: 200 OK: GitHub Connected Successfully!
140
+
141
+ Note over Express, GitHub: Background Tasks / API Requests
142
+ Express->>DB: Get GitHub Resource Token for userId
143
+ Express->>GitHub: Fetch user repositories with Resource Token
144
+ GitHub-->>Express: Repository List
104
145
  ```
105
146
 
106
147
  ---
107
148
 
108
- ## Multi-SSO Account Linking
109
-
110
- Lixa supports automatic identity merging for users signing in with different SSO providers (e.g. Google and GitHub) that share the same verified email address.
111
-
112
- ### Account Linking Sequence Diagram
113
-
114
- ![Account Linking Sequence](https://cdn.jsdelivr.net/npm/@vunexa/lixa/docs/images/account-linking.svg)
115
-
116
- ### Account Linking Configuration Modes
117
-
118
- ```typescript
119
- import { AccountLinkingStrategy } from "@vunexa/lixa";
120
-
121
- // Mode 1: Auto-link by verified email (Recommended)
122
- accountLinking: {
123
- mode: AccountLinkingStrategy.AUTO_LINK_BY_VERIFIED_EMAIL,
124
- requireVerifiedEmail: true,
125
- }
126
-
127
- // Mode 2: Treat provider logins as separate accounts
128
- accountLinking: {
129
- mode: AccountLinkingStrategy.ISOLATED,
130
- }
131
- ```
149
+ ### 3. Multi-SSO Account Linking
132
150
 
133
- ### Explicit Account Linking & Unlinking APIs
151
+ Lixa automatically links multiple identity providers (e.g. AWS Cognito + Google) sharing the same verified email address:
134
152
 
135
- ```typescript
136
- // Explicitly link a new provider while authenticated
137
- await lixa.linkAccount({
138
- sessionId: req.cookies.session_id,
139
- provider: "github",
140
- code: req.query.code as string,
141
- state: req.query.state as string,
142
- });
153
+ ```mermaid
154
+ sequenceDiagram
155
+ autonumber
156
+ actor User as User with alex@example.com
157
+ participant Lixa as Lixa Engine
158
+ participant DB as Database (`user_accounts`)
143
159
 
144
- // Unlink a provider account
145
- await lixa.unlinkAccount(req.cookies.session_id, "github");
160
+ Note over User, DB: User previously registered with AWS Cognito (userId="usr_123")
161
+ User->>Lixa: Signs in via Google SSO (email="alex@example.com", verified=true)
162
+ Lixa->>DB: Find existing account by verified email in `user_accounts`
163
+ DB-->>Lixa: Found existing account (userId="usr_123", provider="cognito")
164
+ Lixa->>DB: Link Google identity to same userId in `user_accounts` (userId="usr_123", provider="google")
165
+ Lixa->>DB: Create active session for userId="usr_123"
166
+ Lixa-->>User: Logged in! Unified profile and data preserved across both logins.
146
167
  ```
147
168
 
148
169
  ---
149
170
 
150
- ## Post-Login Resource Connection API (AuthZ)
151
-
152
- Primary authentication is strictly limited to identity scopes. To request third-party API permissions (such as GitHub Repositories or Google Drive), use Lixa's post-login **Resource Connection API**.
153
-
154
- ### Resource Connection Sequence Diagram
171
+ ## 📦 Installation
155
172
 
156
- ![Resource Connection Sequence](https://cdn.jsdelivr.net/npm/@vunexa/lixa/docs/images/resource-connection.svg)
157
-
158
- ### Resource Connection Usage Example
159
-
160
- ```typescript
161
- // 1. Generate Resource Authorization URL (Requires Active Session)
162
- app.get("/connect/github", async (req, res) => {
163
- const sessionId = req.cookies.session_id;
164
-
165
- const resourceAuthUrl = await lixa.getResourceAuthUrl({
166
- sessionId,
167
- provider: "github",
168
- scopes: ["repo", "read:org"], // Resource permissions requested post-login
169
- });
170
-
171
- res.redirect(resourceAuthUrl);
172
- });
173
-
174
- // 2. Handle Resource Callback
175
- app.get("/connect/github/callback", async (req, res) => {
176
- const sessionId = req.cookies.session_id;
177
-
178
- const updatedSession = await lixa.handleResourceCallback({
179
- sessionId,
180
- provider: "github",
181
- code: req.query.code as string,
182
- state: req.query.state as string,
183
- scopes: ["repo", "read:org"],
184
- });
185
-
186
- res.redirect("/dashboard");
187
- });
188
-
189
- // 3. Query Connected Resource Access Token (Auto-Refreshes Expired Tokens)
190
- app.get("/api/github/repos", async (req, res) => {
191
- // Queries by active session OR user ID, auto-refreshing expired access tokens
192
- const resource = await lixa.getConnectedResource(req.cookies.session_id, "github");
193
-
194
- if (!resource) {
195
- return res.status(403).json({ error: "GitHub resource not connected" });
196
- }
197
-
198
- // Call GitHub API with active resource access token
199
- const response = await fetch("https://api.github.com/user/repos", {
200
- headers: { Authorization: `Bearer ${resource.accessToken}` },
201
- });
202
-
203
- const repos = await response.json();
204
- res.json(repos);
205
- });
206
-
207
- // 4. Query Resource Directly by User ID (e.g. inside background cron jobs or webhooks)
208
- const githubResource = await lixa.getUserResource(userId, "github");
173
+ ```bash
174
+ # Core framework & official extensions
175
+ npm install @vunexa/lixa @vunexa/lixa-extensions
209
176
 
210
- // 5. Disconnect Resource Provider
211
- app.delete("/connect/github", async (req, res) => {
212
- await lixa.disconnectResource(req.cookies.session_id, "github");
213
- res.json({ success: true });
214
- });
177
+ # If using Prisma ORM (recommended)
178
+ npm install @prisma/client
179
+ npm install -D prisma
215
180
  ```
216
181
 
217
182
  ---
218
183
 
219
- ## Session Interface Structure
184
+ ## 🏁 Quickstart: AWS Cognito + Express + Prisma
220
185
 
221
- ```typescript
222
- export interface Session<TRaw = OAuthTokenResponse> {
223
- /** Unique session ID generated by Lixa */
224
- id?: string;
186
+ Here is a complete, production-ready example configuring **AWS Cognito SSO**, **Prisma storage with 2-minute caching**, and **Express**.
225
187
 
226
- /** Unified user ID across linked accounts */
227
- userId?: string;
188
+ ### Step 1: Database Schema (Prisma)
228
189
 
229
- /** Primary user email */
230
- email?: string;
190
+ Create or update your `prisma/schema.prisma` file with normalized primitive columns:
231
191
 
232
- /** Linked SSO provider accounts (AuthN) - Single Source of Truth */
233
- accounts?: Record<string, LinkedAccount>;
192
+ ```prisma
193
+ datasource db {
194
+ provider = "sqlite" // or "postgresql", "mysql"
195
+ url = env("DATABASE_URL")
196
+ }
234
197
 
235
- /** Connected third-party resource provider tokens (AuthZ) - Single Source of Truth */
236
- resources?: Record<string, ConnectedResource>;
198
+ generator client {
199
+ provider = "prisma-client-js"
200
+ }
237
201
 
238
- /** Optional primary access token or session token */
239
- token?: string;
202
+ // 1. Active User Sessions (AuthN)
203
+ model Session {
204
+ id String @id @map("id")
205
+ userId String? @map("user_id")
206
+ userEmail String? @map("user_email")
207
+ provider String? @map("provider")
208
+ accessToken String? @map("access_token")
209
+ refreshToken String? @map("refresh_token")
210
+ idToken String? @map("id_token")
211
+ expiresAt DateTime @map("expires_at")
212
+ createdAt DateTime @default(now()) @map("created_at")
213
+ updatedAt DateTime @default(now()) @updatedAt @map("updated_at")
214
+
215
+ @@index([userId])
216
+ @@index([userEmail])
217
+ @@index([expiresAt])
218
+ @@map("sessions")
219
+ }
240
220
 
241
- /** Optional current active auth provider */
242
- provider?: string;
221
+ // 2. Multi-SSO Linked Identity Accounts (AuthN)
222
+ model UserAccount {
223
+ userId String @map("user_id")
224
+ provider String @map("provider")
225
+ providerUserId String? @map("provider_user_id")
226
+ email String? @map("email")
227
+ linkedAt DateTime @default(now()) @map("linked_at")
228
+ updatedAt DateTime @default(now()) @updatedAt @map("updated_at")
229
+
230
+ @@id([userId, provider])
231
+ @@index([email])
232
+ @@map("user_accounts")
233
+ }
243
234
 
244
- /** Optional raw token response from provider */
245
- raw?: TRaw;
235
+ // 3. Connected Third-Party API Resources (AuthZ)
236
+ model ConnectedResource {
237
+ userId String @map("user_id")
238
+ provider String @map("provider")
239
+ accessToken String @map("access_token")
240
+ refreshToken String? @map("refresh_token")
241
+ idToken String? @map("id_token")
242
+ scopes String? @map("scopes")
243
+ expiresAt DateTime? @map("expires_at")
244
+ connectedAt DateTime @default(now()) @map("connected_at")
245
+ updatedAt DateTime @default(now()) @updatedAt @map("updated_at")
246
+
247
+ @@id([userId, provider])
248
+ @@map("user_resources")
246
249
  }
247
- ```
248
250
 
249
- ### `LinkedAccount` vs `ConnectedResource`
251
+ // 4. PKCE & OAuth Flow State
252
+ model OAuthState {
253
+ state String @id @map("state")
254
+ provider String? @map("provider")
255
+ redirectUri String? @map("redirect_uri")
256
+ codeVerifier String? @map("code_verifier")
257
+ codeChallenge String? @map("code_challenge")
258
+ expiresAt DateTime @map("expires_at")
259
+ createdAt DateTime @default(now()) @map("created_at")
260
+
261
+ @@index([expiresAt])
262
+ @@map("oauth_states")
263
+ }
264
+ ```
250
265
 
251
- | Concept | Purpose | Scopes Allowed | Stored Location |
252
- | :--- | :--- | :--- | :--- |
253
- | **`LinkedAccount`** | Identity verification & multi-SSO merging (**AuthN**) | Minimal identity scopes (`openid`, `email`, `read:user`) | `session.accounts[provider]` |
254
- | **`ConnectedResource`** | External API resource access (**AuthZ**) | Resource permissions (`repo`, `drive.readonly`) | `session.resources[provider]` |
266
+ Push the schema to your database:
267
+ ```bash
268
+ npx prisma db push
269
+ ```
255
270
 
256
271
  ---
257
272
 
258
- ## Custom Session Storage Implementations
259
-
260
- Lixa allows you to store sessions in any database or cache by implementing the `SessionStorage` interface.
261
-
262
- ### 1. SQLite Session Storage (`better-sqlite3`)
263
-
264
- For relational persistence or single-node deployments using SQLite:
273
+ ### Step 2: Initialize Lixa & Storage with 2-Minute Cache
265
274
 
266
- #### Table Schema (SQL DDL)
267
-
268
- ```sql
269
- CREATE TABLE IF NOT EXISTS sessions (
270
- id TEXT PRIMARY KEY,
271
- user_email TEXT,
272
- data TEXT NOT NULL,
273
- expires_at INTEGER NOT NULL
274
- );
275
-
276
- CREATE INDEX IF NOT EXISTS idx_sessions_email ON sessions(user_email);
277
- CREATE INDEX IF NOT EXISTS idx_sessions_expires ON sessions(expires_at);
278
- ```
279
-
280
- #### TypeScript Implementation
275
+ Create `src/config/lixa.ts`:
281
276
 
282
277
  ```typescript
283
- import Database from "better-sqlite3";
284
- import { Lixa, type SessionStorage, type Session } from "@vunexa/lixa";
285
-
286
- export class SqliteSessionStorage implements SessionStorage {
287
- private db = new Database("lixa_sessions.db");
288
-
289
- constructor() {
290
- this.db.exec(`
291
- CREATE TABLE IF NOT EXISTS sessions (
292
- id TEXT PRIMARY KEY,
293
- user_email TEXT,
294
- data TEXT NOT NULL,
295
- expires_at INTEGER NOT NULL
296
- );
297
- CREATE INDEX IF NOT EXISTS idx_sessions_email ON sessions(user_email);
298
- `);
299
- }
300
-
301
- async saveSession<T extends Session>(sessionId: string, session: T, expiresInSeconds: number): Promise<void> {
302
- const expiresAt = Math.floor(Date.now() / 1000) + expiresInSeconds;
303
- const stmt = this.db.prepare(`
304
- INSERT INTO sessions (id, user_email, data, expires_at)
305
- VALUES (?, ?, ?, ?)
306
- ON CONFLICT(id) DO UPDATE SET
307
- user_email = excluded.user_email,
308
- data = excluded.data,
309
- expires_at = excluded.expires_at
310
- `);
311
- stmt.run(sessionId, session.email || null, JSON.stringify(session), expiresAt);
312
- }
313
-
314
- async getSession<T extends Session>(sessionId: string): Promise<T | null> {
315
- const now = Math.floor(Date.now() / 1000);
316
- const stmt = this.db.prepare(`SELECT data FROM sessions WHERE id = ? AND expires_at > ?`);
317
- const row = stmt.get(sessionId, now) as { data: string } | undefined;
318
- return row ? (JSON.parse(row.data) as T) : null;
319
- }
278
+ import { Lixa, CompositeIdentityProvider, AccountLinkingStrategy } from "@vunexa/lixa";
279
+ import { CognitoIdentityProvider } from "@vunexa/lixa-extensions/identity";
280
+ import { GoogleProvider, GithubProvider } from "@vunexa/lixa-extensions/providers";
281
+ import { createPrismaAdapter } from "@vunexa/lixa-extensions/storage/prisma";
282
+ import { PrismaClient } from "@prisma/client";
320
283
 
321
- async deleteSession(sessionId: string): Promise<void> {
322
- const stmt = this.db.prepare(`DELETE FROM sessions WHERE id = ?`);
323
- stmt.run(sessionId);
324
- }
284
+ const prisma = new PrismaClient();
325
285
 
326
- async getSessionByEmail<T extends Session>(email: string): Promise<{ sessionId: string; session: T } | null> {
327
- const now = Math.floor(Date.now() / 1000);
328
- const stmt = this.db.prepare(`SELECT id, data FROM sessions WHERE user_email = ? AND expires_at > ? LIMIT 1`);
329
- const row = stmt.get(email, now) as { id: string; data: string } | undefined;
330
- return row ? { sessionId: row.id, session: JSON.parse(row.data) as T } : null;
331
- }
332
- }
333
-
334
- // Pass to Lixa instance
335
286
  export const lixa = new Lixa({
336
- sessionHandler: {
337
- sessionStorage: new SqliteSessionStorage(),
287
+ // 1. Unified storage with automatic 2-minute (120s) in-memory cache
288
+ storage: createPrismaAdapter(prisma, {
289
+ enableCache: true,
290
+ cacheTtlSeconds: 120, // Cache session validations locally
291
+ }),
292
+
293
+ // 2. Multi-SSO account linking strategy
294
+ accountLinking: {
295
+ mode: AccountLinkingStrategy.AUTO_LINK_BY_VERIFIED_EMAIL,
296
+ requireVerifiedEmail: true,
338
297
  },
339
- providers: { /* ... */ },
340
- });
341
- ```
342
298
 
343
- #### Saved JSON Record Example in SQLite (`data` Column)
344
-
345
- ```json
346
- {
347
- "id": "e4a91f82c3b4a07f",
348
- "userId": "1049281048",
349
- "email": "alex.developer@example.com",
350
- "accounts": {
351
- "google": {
352
- "provider": "google",
353
- "email": "alex.developer@example.com",
354
- "providerUserId": "1049281048",
355
- "accessToken": "ya29.a0ARW5m7...",
356
- "linkedAt": 1771657200000
299
+ // 3. Composite Identity Provider (AWS Cognito + Social SSO)
300
+ identityProvider: new CompositeIdentityProvider({
301
+ defaultProvider: "cognito",
302
+ providers: {
303
+ cognito: new CognitoIdentityProvider({
304
+ userPoolId: process.env.COGNITO_USER_POOL_ID!,
305
+ clientId: process.env.COGNITO_CLIENT_ID!,
306
+ clientSecret: process.env.COGNITO_CLIENT_SECRET, // optional
307
+ region: process.env.AWS_REGION || "us-east-1",
308
+ domain: process.env.COGNITO_DOMAIN, // e.g. "my-app.auth.us-east-1.amazoncognito.com"
309
+ }),
357
310
  },
358
- "github": {
359
- "provider": "github",
360
- "email": "alex.developer@example.com",
361
- "providerUserId": "5829104",
362
- "accessToken": "gho_8f7b2a9e1c3...",
363
- "linkedAt": 1771657250000
364
- }
365
- }
366
- }
311
+ oauthProviders: {
312
+ google: {
313
+ provider: new GoogleProvider(),
314
+ clientId: process.env.GOOGLE_CLIENT_ID!,
315
+ clientSecret: process.env.GOOGLE_CLIENT_SECRET!,
316
+ redirectUri: "http://localhost:3000/api/v1/auth/google/callback",
317
+ scopes: {
318
+ auth: ["openid", "email", "profile"],
319
+ resource: ["https://www.googleapis.com/auth/drive.readonly"],
320
+ },
321
+ },
322
+ github: {
323
+ provider: new GithubProvider(),
324
+ clientId: process.env.GITHUB_CLIENT_ID!,
325
+ clientSecret: process.env.GITHUB_CLIENT_SECRET!,
326
+ redirectUri: "http://localhost:3000/api/v1/auth/github/callback",
327
+ scopes: {
328
+ auth: ["read:user", "user:email"],
329
+ resource: ["repo", "read:org"],
330
+ },
331
+ },
332
+ },
333
+ }),
334
+ });
367
335
  ```
368
336
 
369
337
  ---
370
338
 
371
- ### 2. AWS DynamoDB Session Storage (`@aws-sdk/lib-dynamodb`)
372
-
373
- For serverless and distributed AWS deployments using DynamoDB:
339
+ ### Step 3: Setup Express Server & Routes
374
340
 
375
- #### Table Configuration
376
-
377
- - **Table Name**: `LixaSessions`
378
- - **Partition Key**: `sessionId` (String)
379
- - **Global Secondary Index (GSI)**: `EmailIndex` (`email` as Partition Key)
380
- - **TTL Attribute**: `ttl` (Unix timestamp in seconds for automatic AWS expiration)
381
-
382
- #### TypeScript Implementation
341
+ Create `src/server.ts`:
383
342
 
384
343
  ```typescript
385
- import { DynamoDBClient } from "@aws-sdk/client-dynamodb";
386
- import { DynamoDBDocumentClient, PutCommand, GetCommand, DeleteCommand, QueryCommand } from "@aws-sdk/lib-dynamodb";
387
- import { Lixa, type SessionStorage, type Session } from "@vunexa/lixa";
388
-
389
- export class DynamoDbSessionStorage implements SessionStorage {
390
- private docClient: DynamoDBDocumentClient;
391
- private tableName = "LixaSessions";
392
-
393
- constructor() {
394
- const client = new DynamoDBClient({ region: process.env.AWS_REGION || "us-east-1" });
395
- this.docClient = DynamoDBDocumentClient.from(client);
396
- }
397
-
398
- async saveSession<T extends Session>(sessionId: string, session: T, expiresInSeconds: number): Promise<void> {
399
- const ttl = Math.floor(Date.now() / 1000) + expiresInSeconds;
400
- await this.docClient.send(
401
- new PutCommand({
402
- TableName: this.tableName,
403
- Item: {
404
- sessionId,
405
- email: session.email || "N/A",
406
- sessionData: session,
407
- ttl,
408
- },
409
- })
410
- );
411
- }
412
-
413
- async getSession<T extends Session>(sessionId: string): Promise<T | null> {
414
- const res = await this.docClient.send(
415
- new GetCommand({
416
- TableName: this.tableName,
417
- Key: { sessionId },
418
- })
419
- );
420
-
421
- if (!res.Item) return null;
422
- const now = Math.floor(Date.now() / 1000);
423
- if (res.Item.ttl && res.Item.ttl < now) return null;
424
-
425
- return res.Item.sessionData as T;
426
- }
427
-
428
- async deleteSession(sessionId: string): Promise<void> {
429
- await this.docClient.send(
430
- new DeleteCommand({
431
- TableName: this.tableName,
432
- Key: { sessionId },
433
- })
434
- );
435
- }
436
-
437
- async getSessionByEmail<T extends Session>(email: string): Promise<{ sessionId: string; session: T } | null> {
438
- const res = await this.docClient.send(
439
- new QueryCommand({
440
- TableName: this.tableName,
441
- IndexName: "EmailIndex",
442
- KeyConditionExpression: "email = :email",
443
- ExpressionAttributeValues: { ":email": email },
444
- Limit: 1,
445
- })
446
- );
447
-
448
- if (!res.Items || res.Items.length === 0) return null;
449
- const item = res.Items[0];
450
- const now = Math.floor(Date.now() / 1000);
451
- if (item.ttl && item.ttl < now) return null;
452
-
453
- return { sessionId: item.sessionId, session: item.sessionData as T };
454
- }
455
- }
456
-
457
- // Pass to Lixa instance
458
- export const lixa = new Lixa({
459
- sessionHandler: {
460
- sessionStorage: new DynamoDbSessionStorage(),
461
- },
462
- providers: { /* ... */ },
344
+ import express from "express";
345
+ import cookieParser from "cookie-parser";
346
+ import cors from "cors";
347
+ import { createAuthRouter, createRequireAuthMiddleware } from "@vunexa/lixa-extensions/http/express";
348
+ import { lixa } from "./config/lixa";
349
+
350
+ const app = express();
351
+ app.use(express.json());
352
+ app.use(cookieParser(process.env.COOKIE_SECRET || "super-secret-key"));
353
+ app.use(cors({ origin: "http://localhost:5173", credentials: true }));
354
+
355
+ // Auth guard middleware
356
+ const requireAuth = createRequireAuthMiddleware(lixa);
357
+
358
+ // Mount Lixa authentication routes:
359
+ // - GET /api/v1/auth/login?provider=cognito
360
+ // - GET /api/v1/auth/callback
361
+ // - POST /api/v1/auth/logout
362
+ // - GET /api/v1/auth/me
363
+ const authRouter = express.Router();
364
+ createAuthRouter(lixa, {
365
+ router: authRouter,
366
+ requireAuth,
367
+ successRedirectUrl: "http://localhost:5173/dashboard",
368
+ errorRedirectUrl: "http://localhost:5173/login?error=auth_failed",
463
369
  });
464
- ```
465
-
466
- #### Saved DynamoDB Item JSON Example
467
-
468
- ```json
469
- {
470
- "sessionId": "e4a91f82c3b4a07f",
471
- "email": "alex.developer@example.com",
472
- "ttl": 1771743600,
473
- "sessionData": {
474
- "id": "e4a91f82c3b4a07f",
475
- "userId": "1049281048",
476
- "email": "alex.developer@example.com",
477
- "accounts": {
478
- "google": {
479
- "provider": "google",
480
- "email": "alex.developer@example.com",
481
- "providerUserId": "1049281048",
482
- "accessToken": "ya29.a0ARW5m7...",
483
- "linkedAt": 1771657200000
484
- },
485
- "github": {
486
- "provider": "github",
487
- "email": "alex.developer@example.com",
488
- "providerUserId": "5829104",
489
- "accessToken": "gho_8f7b2a9e1c3...",
490
- "linkedAt": 1771657250000
491
- }
492
- }
493
- }
494
- }
370
+ app.use("/api/v1/auth", authRouter);
495
371
  ```
496
372
 
497
373
  ---
498
374
 
499
- ## Custom Resource Storage Implementation (`ResourceStorage`)
500
-
501
- While `SessionStorage` manages short-lived user authentication sessions (indexed by `sessionId`), **`ResourceStorage`** manages long-lived third-party API tokens (**AuthZ**) bound directly to a **User ID** (or Email). This ensures that third-party credentials (such as GitHub Repositories or Google Drive) persist across session expirations and logouts.
502
-
503
- ### `ResourceStorage` Interface
375
+ ### Step 4: Protect Routes & Use Tokens
504
376
 
505
377
  ```typescript
506
- export interface ResourceStorage {
507
- saveResource(userId: string, provider: string, resource: ConnectedResource): Promise<void>;
508
- getResource(userId: string, provider: string): Promise<ConnectedResource | null>;
509
- getUserResources(userId: string): Promise<Record<string, ConnectedResource>>;
510
- deleteResource(userId: string, provider: string): Promise<void>;
511
- }
512
- ```
513
-
514
- ### 1. SQLite Resource Storage (`better-sqlite3`)
515
-
516
- #### Table Schema (SQL DDL)
378
+ // Protected route example
379
+ app.get("/api/v1/profile", requireAuth, async (req: any, res) => {
380
+ // req.session is populated automatically
381
+ const { userId, email, provider, accessToken, idToken } = req.session;
382
+
383
+ res.json({
384
+ message: "Authenticated successfully!",
385
+ user: {
386
+ userId,
387
+ email,
388
+ provider,
389
+ },
390
+ tokens: {
391
+ accessTokenPreview: accessToken ? `${accessToken.substring(0, 15)}...` : null,
392
+ idTokenPreview: idToken ? `${idToken.substring(0, 15)}...` : null,
393
+ },
394
+ });
395
+ });
517
396
 
518
- ```sql
519
- CREATE TABLE IF NOT EXISTS user_resources (
520
- user_id TEXT NOT NULL,
521
- provider TEXT NOT NULL,
522
- data TEXT NOT NULL,
523
- updated_at INTEGER NOT NULL,
524
- PRIMARY KEY (user_id, provider)
525
- );
397
+ app.listen(3000, () => {
398
+ console.log("Server running at http://localhost:3000");
399
+ });
526
400
  ```
527
401
 
528
- #### TypeScript Implementation
529
-
530
- ```typescript
531
- import Database from "better-sqlite3";
532
- import { Lixa, type ResourceStorage, type ConnectedResource } from "@vunexa/lixa";
533
-
534
- export class SqliteResourceStorage implements ResourceStorage {
535
- private db = new Database("lixa_resources.db");
536
-
537
- constructor() {
538
- this.db.exec(`
539
- CREATE TABLE IF NOT EXISTS user_resources (
540
- user_id TEXT NOT NULL,
541
- provider TEXT NOT NULL,
542
- data TEXT NOT NULL,
543
- updated_at INTEGER NOT NULL,
544
- PRIMARY KEY (user_id, provider)
545
- );
546
- `);
547
- }
548
-
549
- async saveResource(userId: string, provider: string, resource: ConnectedResource): Promise<void> {
550
- const stmt = this.db.prepare(`
551
- INSERT INTO user_resources (user_id, provider, data, updated_at)
552
- VALUES (?, ?, ?, ?)
553
- ON CONFLICT(user_id, provider) DO UPDATE SET
554
- data = excluded.data,
555
- updated_at = excluded.updated_at
556
- `);
557
- stmt.run(userId, provider.toLowerCase(), JSON.stringify(resource), Date.now());
558
- }
402
+ ---
559
403
 
560
- async getResource(userId: string, provider: string): Promise<ConnectedResource | null> {
561
- const stmt = this.db.prepare(`SELECT data FROM user_resources WHERE user_id = ? AND provider = ?`);
562
- const row = stmt.get(userId, provider.toLowerCase()) as { data: string } | undefined;
563
- return row ? (JSON.parse(row.data) as ConnectedResource) : null;
564
- }
404
+ ## 🔌 Connecting External Resources (AuthZ)
565
405
 
566
- async getUserResources(userId: string): Promise<Record<string, ConnectedResource>> {
567
- const stmt = this.db.prepare(`SELECT provider, data FROM user_resources WHERE user_id = ?`);
568
- const rows = stmt.all(userId) as Array<{ provider: string; data: string }>;
569
- const result: Record<string, ConnectedResource> = {};
570
- for (const r of rows) {
571
- result[r.provider] = JSON.parse(r.data);
572
- }
573
- return result;
574
- }
406
+ Unlike login authentication, **Resource Connections** allow your application to interact with external APIs (like GitHub repositories or Google Drive files) on behalf of the user.
575
407
 
576
- async deleteResource(userId: string, provider: string): Promise<void> {
577
- const stmt = this.db.prepare(`DELETE FROM user_resources WHERE user_id = ? AND provider = ?`);
578
- stmt.run(userId, provider.toLowerCase());
579
- }
580
- }
408
+ ```typescript
409
+ // 1. Generate connection redirect URL
410
+ app.get("/api/v1/resources/github/connect", requireAuth, async (req: any, res) => {
411
+ const url = await lixa.getResourceAuthUrl("github", {
412
+ userId: req.session.userId,
413
+ redirectUri: "http://localhost:3000/api/v1/resources/github/callback",
414
+ });
415
+ res.redirect(url);
416
+ });
581
417
 
582
- // Pass to Lixa instance via resourceHandler
583
- export const lixa = new Lixa({
584
- resourceHandler: {
585
- resourceStorage: new SqliteResourceStorage(),
586
- },
587
- providers: { /* ... */ },
418
+ // 2. Handle resource callback & store token
419
+ app.get("/api/v1/resources/github/callback", requireAuth, async (req: any, res) => {
420
+ const { code } = req.query;
421
+ await lixa.handleResourceCallback("github", code as string, req.session.userId);
422
+ res.redirect("http://localhost:5173/settings?connected=github");
588
423
  });
589
- ```
590
424
 
591
- #### Saved JSON Records Example in SQLite (`user_resources` Table)
592
-
593
- **Row 1 (GitHub Repositories)**:
594
- ```json
595
- {
596
- "user_id": "1049281048",
597
- "provider": "github",
598
- "data": {
599
- "accessToken": "gho_resource_repo_9a8b7c...",
600
- "refreshToken": "ghr_refresh_token_123...",
601
- "scopes": ["repo", "read:org"],
602
- "expiresAt": 1771743600000,
603
- "connectedAt": 1771657300000
425
+ // 3. Make third-party API calls using stored resource tokens
426
+ app.get("/api/v1/resources/github/repos", requireAuth, async (req: any, res) => {
427
+ const resource = await lixa.getResource(req.session.userId, "github");
428
+ if (!resource) {
429
+ return res.status(404).json({ error: "GitHub account not connected" });
604
430
  }
605
- }
606
- ```
607
431
 
608
- **Row 2 (Google Drive)**:
609
- ```json
610
- {
611
- "user_id": "1049281048",
612
- "provider": "google",
613
- "data": {
614
- "accessToken": "ya29.drive_resource_token_456...",
615
- "refreshToken": "1//09abc_google_refresh_token...",
616
- "scopes": ["https://www.googleapis.com/auth/drive.readonly"],
617
- "expiresAt": 1771660800000,
618
- "connectedAt": 1771657400000
619
- }
620
- }
432
+ // Call GitHub API with resource.accessToken
433
+ const response = await fetch("https://api.github.com/user/repos", {
434
+ headers: { Authorization: `Bearer ${resource.accessToken}` },
435
+ });
436
+ const repos = await response.json();
437
+ res.json({ repos });
438
+ });
621
439
  ```
622
440
 
623
441
  ---
624
442
 
625
- ### 2. AWS DynamoDB Resource Storage (`@aws-sdk/lib-dynamodb`)
626
-
627
- For serverless AWS deployments storing user API credentials in DynamoDB:
628
-
629
- #### Table Configuration
443
+ ## ⚡ In-Memory 2-Minute Caching (`withLocalCache`)
630
444
 
631
- - **Table Name**: `LixaUserResources`
632
- - **Partition Key (PK)**: `userId` (String)
633
- - **Sort Key (SK)**: `provider` (String)
445
+ Under high traffic, hitting your database on every single API request to validate a session creates heavy I/O bottlenecks.
634
446
 
635
- #### TypeScript Implementation
447
+ Lixa includes a high-performance **2-Minute In-Memory TTL Cache**:
636
448
 
637
449
  ```typescript
638
- import { DynamoDBClient } from "@aws-sdk/client-dynamodb";
639
- import { DynamoDBDocumentClient, PutCommand, GetCommand, DeleteCommand, QueryCommand } from "@aws-sdk/lib-dynamodb";
640
- import { Lixa, type ResourceStorage, type ConnectedResource } from "@vunexa/lixa";
641
-
642
- export class DynamoDbResourceStorage implements ResourceStorage {
643
- private docClient: DynamoDBDocumentClient;
644
- private tableName = "LixaUserResources";
645
-
646
- constructor() {
647
- const client = new DynamoDBClient({ region: process.env.AWS_REGION || "us-east-1" });
648
- this.docClient = DynamoDBDocumentClient.from(client);
649
- }
650
-
651
- async saveResource(userId: string, provider: string, resource: ConnectedResource): Promise<void> {
652
- await this.docClient.send(
653
- new PutCommand({
654
- TableName: this.tableName,
655
- Item: {
656
- userId,
657
- provider: provider.toLowerCase(),
658
- resourceData: resource,
659
- updatedAt: Date.now(),
660
- },
661
- })
662
- );
663
- }
450
+ import { withLocalCache } from "@vunexa/lixa-extensions/storage";
664
451
 
665
- async getResource(userId: string, provider: string): Promise<ConnectedResource | null> {
666
- const res = await this.docClient.send(
667
- new GetCommand({
668
- TableName: this.tableName,
669
- Key: {
670
- userId,
671
- provider: provider.toLowerCase(),
672
- },
673
- })
674
- );
675
-
676
- return res.Item ? (res.Item.resourceData as ConnectedResource) : null;
677
- }
678
-
679
- async getUserResources(userId: string): Promise<Record<string, ConnectedResource>> {
680
- const res = await this.docClient.send(
681
- new QueryCommand({
682
- TableName: this.tableName,
683
- KeyConditionExpression: "userId = :userId",
684
- ExpressionAttributeValues: { ":userId": userId },
685
- })
686
- );
687
-
688
- const result: Record<string, ConnectedResource> = {};
689
- if (res.Items) {
690
- for (const item of res.Items) {
691
- result[item.provider] = item.resourceData as ConnectedResource;
692
- }
693
- }
694
- return result;
695
- }
696
-
697
- async deleteResource(userId: string, provider: string): Promise<void> {
698
- await this.docClient.send(
699
- new DeleteCommand({
700
- TableName: this.tableName,
701
- Key: {
702
- userId,
703
- provider: provider.toLowerCase(),
704
- },
705
- })
706
- );
707
- }
708
- }
709
-
710
- // Pass to Lixa instance via resourceHandler
711
- export const lixa = new Lixa({
712
- resourceHandler: {
713
- resourceStorage: new DynamoDbResourceStorage(),
714
- },
715
- providers: { /* ... */ },
452
+ // Automatically applied when using createPrismaAdapter
453
+ const cachedStorage = withLocalCache(baseAdapter, {
454
+ ttlSeconds: 120, // 2 minutes (default)
716
455
  });
717
456
  ```
718
457
 
719
- #### Saved DynamoDB Resource Items Example (`LixaUserResources` Table)
720
-
721
- ```json
722
- [
723
- {
724
- "userId": "1049281048",
725
- "provider": "github",
726
- "updatedAt": 1771657300000,
727
- "resourceData": {
728
- "accessToken": "gho_resource_repo_9a8b7c...",
729
- "refreshToken": "ghr_refresh_token_123...",
730
- "scopes": ["repo", "read:org"],
731
- "expiresAt": 1771743600000,
732
- "connectedAt": 1771657300000
733
- }
734
- },
735
- {
736
- "userId": "1049281048",
737
- "provider": "google",
738
- "updatedAt": 1771657400000,
739
- "resourceData": {
740
- "accessToken": "ya29.drive_resource_token_456...",
741
- "refreshToken": "1//09abc_google_refresh_token...",
742
- "scopes": ["https://www.googleapis.com/auth/drive.readonly"],
743
- "expiresAt": 1771660800000,
744
- "connectedAt": 1771657400000
745
- }
746
- }
747
- ]
748
- ```
458
+ ### Cache Features & Invalidation Guarantees:
459
+ - **Zero Database I/O**: Session lookups within 120 seconds return in sub-millisecond memory time.
460
+ - **Auto-Invalidation on Logout**: Calling `deleteSession(sessionId)` immediately clears both the database record and memory cache.
461
+ - **Auto-Invalidation on Updates**: Saving or updating accounts/credentials purges stale cached keys automatically.
462
+ - **Safe Isolation**: OAuth states (single-use CSRF tokens) bypass the cache and validate directly against persistent storage.
749
463
 
750
464
  ---
751
465
 
752
- ### 3. Querying Connected Resources Directly by User ID
466
+ ## 🌐 Supported Identity Providers
753
467
 
754
- `ResourceStorage` enables background workers, cron jobs, and webhooks to access third-party API credentials by `userId` without an active HTTP session:
755
-
756
- ```typescript
757
- // Background worker querying user's GitHub Repos token
758
- const githubResource = await lixa.getUserResource(user.id, "github");
759
-
760
- if (githubResource) {
761
- // Lixa auto-refreshes expired access tokens transparently!
762
- const response = await fetch("https://api.github.com/user/repos", {
763
- headers: { Authorization: `Bearer ${githubResource.accessToken}` },
764
- });
765
- }
766
- ```
468
+ | Provider | Type | AuthN (SSO) | AuthZ (Resources) | Package |
469
+ | :--- | :--- | :---: | :---: | :--- |
470
+ | **AWS Cognito** | Enterprise IdP | ✅ | — | `@vunexa/lixa-extensions/identity` |
471
+ | **Google** | Social / OIDC | ✅ | ✅ (Drive, Gmail) | `@vunexa/lixa-extensions/providers` |
472
+ | **GitHub** | OAuth 2.0 | ✅ | ✅ (Repos, Orgs) | `@vunexa/lixa-extensions/providers` |
473
+ | **Microsoft** | Azure AD / OIDC | ✅ | ✅ (Graph API) | `@vunexa/lixa-extensions/providers` |
474
+ | **Auth0** | Enterprise IdP | ✅ | — | `@vunexa/lixa-extensions/identity` |
475
+ | **Okta** | Enterprise IdP | ✅ | — | `@vunexa/lixa-extensions/identity` |
476
+ | **Self-Managed** | SQLite / Postgres | ✅ | — | `@vunexa/lixa` (Built-in) |
767
477
 
768
478
  ---
769
479
 
770
- ## User Info Utilities
771
-
772
- Lixa provides utilities to extract user information from OAuth tokens:
480
+ ## 📚 TypeScript API Reference
773
481
 
774
- ```typescript
775
- import { extractUserInfo, type UserInfo } from "@vunexa/lixa";
482
+ ### `new Lixa(options)`
483
+ - `storage`: Storage adapter created via `createPrismaAdapter` or custom implementation.
484
+ - `identityProvider`: `CompositeIdentityProvider` or standalone IdP instance.
485
+ - `accountLinking`: Configuration for cross-provider email matching (`AccountLinkingStrategy.AUTO_LINK_BY_VERIFIED_EMAIL`).
486
+ - `sessionHandler`: Custom session serialization logic (optional).
776
487
 
777
- const { userInfo } = await extractUserInfo(tokenData, providerMetadata);
778
- console.log(userInfo.email, userInfo.name);
779
- ```
488
+ ### Methods
489
+ - `lixa.getAuthUrl(provider, options)`: Returns authorization URL with PKCE verifier stored in state.
490
+ - `lixa.handleCallback(provider, code, state)`: Validates state, exchanges code for tokens, creates session, and links account.
491
+ - `lixa.getSession(sessionId)`: Retrieves active session from memory cache or database.
492
+ - `lixa.deleteSession(sessionId)`: Deletes session and purges local cache.
493
+ - `lixa.getResourceAuthUrl(provider, options)`: Returns resource authorization URL.
494
+ - `lixa.handleResourceCallback(provider, code, userId)`: Stores connected resource tokens under `userId`.
495
+ - `lixa.getResource(userId, provider)`: Retrieves active resource token for third-party API requests.
780
496
 
781
497
  ---
782
498
 
783
- ## License
499
+ ## 🤝 Contributing & License
500
+
501
+ Contributions are welcome! Please open an issue or pull request on GitHub.
784
502
 
785
- [MIT](LICENSE)
503
+ Distributed under the **MIT License**. See `LICENSE` for details.