@vunexa/lixa 0.1.6-alpha.14 → 0.1.6-alpha.16

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,47 +1,88 @@
1
1
  # @vunexa/lixa
2
2
 
3
- > Package is in active development.
3
+ **A flexible, provider-agnostic OAuth 2.0 and OpenID Connect (OIDC) client library for Node.js.**
4
+
5
+ Lixa simplifies multi-provider authentication, identity management, and third-party resource authorization into a single, type-safe API. Use it with any OAuth provider — Google, GitHub, Microsoft, AWS Cognito, or your own custom provider — without being locked in to any vendor.
4
6
 
5
7
  [![npm version](https://img.shields.io/npm/v/@vunexa/lixa.svg)](https://www.npmjs.com/package/@vunexa/lixa)
6
- [![npm downloads](https://img.shields.io/npm/dm/@vunexa/lixa.svg)](https://www.npmjs.com/package/@vunexa/lixa)
7
8
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
8
9
 
9
- A flexible, provider-agnostic OAuth 2.0 and OpenID Connect (OIDC) client library for backend Node.js applications.
10
- `@vunexa/lixa` simplifies multi-provider authentication (e.g. Google, GitHub, Microsoft, AWS Cognito), enforces strict **AuthN/AuthZ separation**, enables **multi-SSO account linking**, and provides a dedicated post-login **Resource Connection API**.
10
+ 📖 **Full Documentation:** [lixa.vunexa.app](https://lixa.vunexa.app)
11
+
12
+ ---
13
+
14
+ ## Table of Contents
15
+
16
+ - [Why Lixa?](#why-lixa)
17
+ - [Architecture Overview](#architecture-overview)
18
+ - [Installation](#installation)
19
+ - [Quick Start](#quick-start)
20
+ - [Identity Providers](#identity-providers)
21
+ - [AWS Cognito](#aws-cognito)
22
+ - [Auth0](#auth0)
23
+ - [Okta](#okta)
24
+ - [Self-Managed (Database)](#self-managed-database)
25
+ - [Composite (Multiple Backends)](#composite-multiple-backends)
26
+ - [OAuth / SSO Providers](#oauth--sso-providers)
27
+ - [Built-in Providers](#built-in-providers)
28
+ - [Custom Providers](#custom-providers)
29
+ - [Credentials Authentication](#credentials-authentication)
30
+ - [Session Management](#session-management)
31
+ - [Account Linking](#account-linking)
32
+ - [Resource Connections (AuthZ)](#resource-connections-authz)
33
+ - [Database Storage](#database-storage)
34
+ - [Express Integration](#express-integration)
35
+ - [Error Handling](#error-handling)
36
+ - [API Reference](#api-reference)
37
+ - [License](#license)
11
38
 
12
39
  ---
13
40
 
14
- ## Features
41
+ ## Why Lixa?
15
42
 
16
- - **Strict AuthN / AuthZ Separation**: Primary login is automatically restricted to minimal identity scopes (`openid`, `email`, `profile`, `read:user`, `user:email`). Resource permissions are isolated to post-login resource connection.
17
- - **Multi-SSO Account Linking**: Seamlessly merge multiple authentication providers (e.g. AWS Cognito + Google + GitHub) under a single user profile based on verified email or active session context.
18
- - **Persistent Account Storage (`AccountStorage`)**: Linked identity accounts remain saved in persistent database storage across session logouts and log-ins.
19
- - **Post-Login Resource Connection API**: Connect third-party API providers (GitHub Repositories, Google Drive, Slack) post-authentication and manage resource access tokens bound to the user profile.
20
- - **Multi-Identity Provider Architecture**: Built-in support for Self-Managed Database credentials, AWS Cognito, Auth0, and Okta via `@vunexa/lixa-extensions`.
21
- - **Automatic PKCE** (Proof Key for Code Exchange) and state CSRF validation for all OAuth flows.
22
- - **TypeScript-first**: Comprehensive type safety across all core APIs and HTTP adapters.
43
+ | Feature | Lixa |
44
+ |---------|------|
45
+ | **Provider-agnostic** | Works with any OAuth 2.0 / OIDC provider |
46
+ | **Identity Management** | Built-in support for AWS Cognito, Auth0, Okta, or self-managed DB credentials |
47
+ | **PKCE by default** | Every OAuth flow uses RFC 7636 PKCE (S256) automatically |
48
+ | **AuthN / AuthZ separation** | Clean separation between login (identity) and post-login API access (resource connections) |
49
+ | **Account linking** | Auto-link multiple SSO providers (Google + GitHub + Cognito) under a single user |
50
+ | **Type-safe** | Full TypeScript with inferred provider keys and generic session types |
51
+ | **Zero vendor lock-in** | No AWS SDK, no Auth0 SDK — just `fetch()` calls to standard APIs |
52
+ | **Minimal dependencies** | Only `node-cache` as runtime dependency |
23
53
 
24
54
  ---
25
55
 
26
56
  ## Architecture Overview
27
57
 
28
- ```mermaid
29
- flowchart TD
30
- Client["Client App / Frontend"] -->|1. AuthN / Login Request| LixaEngine["Lixa Core Engine"]
31
-
32
- subgraph AuthN ["Primary Authentication & Account Linking (AuthN)"]
33
- LixaEngine -->|Minimal Scopes| IdP["Identity Provider / Social SSO (Cognito, Google, GitHub)"]
34
- IdP -->|Authorization Code| Callback["/callback Endpoint"]
35
- Callback -->|Verify Code + Link Accounts| SessionStore["Session & Account Storage (Prisma / Drizzle / DB)"]
36
- end
37
-
38
- subgraph AuthZ ["Post-Login Resource Connection (AuthZ)"]
39
- LixaEngine -->|Resource Scopes (e.g. repo, drive)| ResourceProvider["External Resource APIs (GitHub API, Google Drive)"]
40
- ResourceProvider -->|Resource Tokens| ResourceStore["Resource Storage (user_resources DB)"]
41
- end
58
+ Lixa is organized into a lightweight, modular **2-package architecture**:
42
59
 
43
- SessionStore -->|Session Cookie| Client
44
- ResourceStore -->|Auto-Refreshed Tokens| Client
60
+ ```
61
+ ┌──────────────────────────────────────────────────────────────┐
62
+ │ Your Application │
63
+ ├──────────────────────────────────────────────────────────────┤
64
+ │ │
65
+ │ ┌────────────────────────────────────────────────────────┐ │
66
+ │ │ @vunexa/lixa (Core Engine) │ │
67
+ │ │ │ │
68
+ │ │ • Lixa class • OAuth 2.0 + PKCE flows │ │
69
+ │ │ • IProvider • Session / State management │ │
70
+ │ │ • IIdentityProvider • Credentials (scrypt hashing) │ │
71
+ │ │ • Account linking • Resource connections (AuthZ) │ │
72
+ │ │ • Cookie utilities • Structured error hierarchy │ │
73
+ │ └────────────────────────────────────────────────────────┘ │
74
+ │ ▲ │
75
+ │ │ peer dependency │
76
+ │ ┌────────────────────────┴───────────────────────────────┐ │
77
+ │ │ @vunexa/lixa-extensions (Extensions) │ │
78
+ │ │ │ │
79
+ │ │ • OAuth Providers: Google, GitHub, Microsoft, Cognito │ │
80
+ │ │ • Identity: CognitoIdentityProvider, Auth0, Okta │ │
81
+ │ │ • Storage: Prisma adapter, Drizzle adapter, Cache │ │
82
+ │ │ • HTTP: Express route handlers & middleware │ │
83
+ │ └────────────────────────────────────────────────────────┘ │
84
+ │ │
85
+ └──────────────────────────────────────────────────────────────┘
45
86
  ```
46
87
 
47
88
  ---
@@ -52,148 +93,628 @@ flowchart TD
52
93
  npm install @vunexa/lixa @vunexa/lixa-extensions
53
94
  ```
54
95
 
96
+ ```bash
97
+ # yarn
98
+ yarn add @vunexa/lixa @vunexa/lixa-extensions
99
+
100
+ # pnpm
101
+ pnpm add @vunexa/lixa @vunexa/lixa-extensions
102
+ ```
103
+
55
104
  ---
56
105
 
57
106
  ## Quick Start
58
107
 
59
- ### 1. Configure Lixa Instance
108
+ Get up and running with Google OAuth in under 5 minutes:
60
109
 
61
110
  ```typescript
62
- import { Lixa, AccountLinkingStrategy } from "@vunexa/lixa";
111
+ import { Lixa } from "@vunexa/lixa";
63
112
  import { GoogleProvider, GithubProvider } from "@vunexa/lixa-extensions/providers";
64
- import { createPrismaAdapter } from "@vunexa/lixa-extensions/storage/prisma";
65
- import { PrismaClient } from "@prisma/client";
66
-
67
- const prisma = new PrismaClient();
68
113
 
69
- export const lixa = new Lixa({
70
- // Unified database storage for sessions, OAuth state, resources, and account linking
71
- storage: createPrismaAdapter(prisma),
72
-
73
- // Configure Multi-SSO Account Linking
74
- accountLinking: {
75
- mode: AccountLinkingStrategy.AUTO_LINK_BY_VERIFIED_EMAIL,
76
- requireVerifiedEmail: true,
77
- },
78
-
79
- // Federated OAuth Providers
80
- federatedOAuthProviders: {
114
+ // 1. Create a Lixa instance with your providers
115
+ const lixa = new Lixa({
116
+ providers: {
81
117
  google: {
82
118
  provider: new GoogleProvider(),
83
119
  clientId: process.env.GOOGLE_CLIENT_ID!,
84
120
  clientSecret: process.env.GOOGLE_CLIENT_SECRET!,
85
- redirectUri: "http://localhost:3000/api/v1/oidc/google/callback",
121
+ redirectUri: "http://localhost:3000/auth/google/callback",
86
122
  scopes: ["openid", "email", "profile"],
87
123
  },
88
124
  github: {
89
125
  provider: new GithubProvider(),
90
126
  clientId: process.env.GITHUB_CLIENT_ID!,
91
127
  clientSecret: process.env.GITHUB_CLIENT_SECRET!,
92
- redirectUri: "http://localhost:3000/api/v1/oidc/github/callback",
128
+ redirectUri: "http://localhost:3000/auth/github/callback",
93
129
  scopes: ["read:user", "user:email"],
94
130
  },
95
131
  },
96
132
  });
133
+
134
+ // 2. Generate an authorization URL and redirect the user
135
+ const authUrl = await lixa.getAuthUrl("google");
136
+ // → Redirect user to authUrl
137
+
138
+ // 3. Handle the callback after the user authorizes
139
+ const sessionId = await lixa.handleCallback({
140
+ provider: "google",
141
+ code: "authorization_code_from_query",
142
+ state: "state_from_query",
143
+ });
144
+
145
+ // 4. Fetch session info anywhere in your app
146
+ const session = await lixa.fetchSessionInfo(sessionId);
147
+ // → { sessionId, userId, email, provider: "google", token, raw }
97
148
  ```
98
149
 
99
150
  ---
100
151
 
101
- ## Multi-SSO Account Linking
152
+ ## Identity Providers
153
+
154
+ Lixa supports multiple identity backends for username/password authentication alongside OAuth SSO. Configure them via the `identityProvider` (or `identity`) field in `LixaConfig`.
155
+
156
+ ### AWS Cognito
157
+
158
+ The most mature integration. Communicates directly with AWS Cognito REST APIs — **zero AWS SDK dependency**.
159
+
160
+ ```typescript
161
+ import { Lixa, CompositeIdentityProvider } from "@vunexa/lixa";
162
+ import { CognitoIdentityProvider } from "@vunexa/lixa-extensions/identity/cognito";
163
+ import { GoogleProvider } from "@vunexa/lixa-extensions/providers";
164
+
165
+ // Option A: Cognito-only (username/password via User Pool)
166
+ const lixa = new Lixa({
167
+ identity: new CognitoIdentityProvider({
168
+ userPoolId: process.env.COGNITO_USER_POOL_ID!,
169
+ clientId: process.env.COGNITO_CLIENT_ID!,
170
+ region: "us-east-1",
171
+ clientSecret: process.env.COGNITO_CLIENT_SECRET, // optional
172
+ }),
173
+ });
174
+
175
+ // Option B: Cognito + Hosted UI federated SSO (Google, Apple, SAML, etc.)
176
+ const cognito = new CognitoIdentityProvider({
177
+ userPoolId: process.env.COGNITO_USER_POOL_ID!,
178
+ clientId: process.env.COGNITO_CLIENT_ID!,
179
+ region: "us-east-1",
180
+ clientSecret: process.env.COGNITO_CLIENT_SECRET,
181
+ hostedUi: {
182
+ domain: "https://my-pool.auth.us-east-1.amazoncognito.com",
183
+ redirectUri: "http://localhost:3000/auth/cognito/callback",
184
+ federated: {
185
+ google: { identityProvider: "Google" },
186
+ apple: { identityProvider: "SignInWithApple" },
187
+ },
188
+ },
189
+ });
190
+
191
+ const lixaWithSSO = new Lixa({
192
+ identity: cognito,
193
+ // cognito.getProviders() auto-registers 'cognito_sso', 'cognito_google', 'cognito_apple'
194
+ });
195
+ ```
102
196
 
103
- Lixa allows users to sign in with different identity providers (e.g. Username/Password, AWS Cognito, Google, GitHub) and links them to the same underlying user account.
197
+ **Cognito API coverage:**
104
198
 
105
- ### Account Linking Sequence
199
+ | Method | Cognito API Action |
200
+ |--------|--------------------|
201
+ | `lixa.signUp()` | `SignUp` |
202
+ | `lixa.signIn()` | `InitiateAuth` (USER_PASSWORD_AUTH) |
203
+ | `lixa.confirmSignUp()` | `ConfirmSignUp` |
204
+ | `lixa.resendConfirmationCode()` | `ResendConfirmationCode` |
205
+ | `lixa.forgotPassword()` | `ForgotPassword` |
206
+ | `lixa.confirmPasswordReset()` | `ConfirmForgotPassword` |
207
+ | `lixa.changePassword()` | `InitiateAuth` → `ChangePassword` |
106
208
 
107
- ```mermaid
108
- sequenceDiagram
109
- autonumber
110
- actor User
111
- participant Frontend
112
- participant Express as Backend Express App
113
- participant Lixa as Lixa Core Engine
114
- participant GitHub as GitHub OAuth
115
- participant DB as Prisma / Drizzle DB
209
+ **Features:**
210
+ - Automatic `SecretHash` HMAC-SHA256 computation when `clientSecret` is configured
211
+ - JWT ID token decoding for user profile extraction
212
+ - Cognito error mapping to Lixa error types (`UsernameExistsException` → `UserAlreadyExistsError`, etc.)
213
+ - Custom endpoint support for LocalStack, VPC endpoints, or mock servers
214
+ - Hosted UI federated SSO with automatic provider registration
116
215
 
117
- User->>Frontend: Click "Link GitHub"
118
- Frontend->>Express: GET /login?provider=github
119
- Express->>Lixa: getAuthUrl("github")
120
- Lixa-->>Express: Returns Auth URL + State
121
- Express-->>User: 302 Redirect to GitHub
216
+ ### Auth0
122
217
 
123
- User->>GitHub: Authenticate & Grant Identity Scope
124
- GitHub-->>Express: GET /callback?code=123&state=abc (Cookie: session_id)
125
- Express->>Lixa: handleCallback({ provider: "github", code, state, sessionId })
126
- Lixa->>GitHub: Exchange Code for Access Token
127
- GitHub-->>Lixa: Return Access Token + Profile (email)
128
- Lixa->>DB: Save Account Link (accounts.github) to user_resources DB
129
- Lixa-->>Express: Return Updated Session ID
130
- Express-->>Frontend: Redirect to /dashboard with updated session cookie
218
+ ```typescript
219
+ import { Auth0IdentityProvider } from "@vunexa/lixa-extensions/identity/auth0";
220
+
221
+ const lixa = new Lixa({
222
+ identity: new Auth0IdentityProvider({
223
+ domain: "your-tenant.auth0.com",
224
+ clientId: process.env.AUTH0_CLIENT_ID!,
225
+ clientSecret: process.env.AUTH0_CLIENT_SECRET,
226
+ connection: "Username-Password-Authentication", // optional
227
+ }),
228
+ });
131
229
  ```
132
230
 
133
- ### Account Linking Usage
231
+ ### Okta
134
232
 
135
233
  ```typescript
136
- // Explicitly link a new provider while authenticated
137
- const sessionId = await lixa.handleCallback({
138
- provider: "github",
139
- code: req.query.code as string,
140
- state: req.query.state as string,
141
- sessionId: activeSessionId, // Links GitHub directly to the active session
234
+ import { OktaIdentityProvider } from "@vunexa/lixa-extensions/identity/okta";
235
+
236
+ const lixa = new Lixa({
237
+ identity: new OktaIdentityProvider({
238
+ orgUrl: "https://your-org.okta.com",
239
+ clientId: process.env.OKTA_CLIENT_ID,
240
+ apiToken: process.env.OKTA_API_TOKEN, // for admin operations
241
+ }),
242
+ });
243
+ ```
244
+
245
+ ### Self-Managed (Database)
246
+
247
+ Use your own database for user credentials — no external identity service needed. Includes Node.js native `crypto.scrypt` hashing, configurable password policies, and timing-attack-safe verification.
248
+
249
+ ```typescript
250
+ import { Lixa } from "@vunexa/lixa";
251
+ import { createPrismaAdapter } from "@vunexa/lixa-extensions/storage/prisma";
252
+ import { PrismaClient } from "@prisma/client";
253
+
254
+ const prisma = new PrismaClient();
255
+ const storage = createPrismaAdapter(prisma);
256
+
257
+ const lixa = new Lixa({
258
+ storage,
259
+ credentials: {
260
+ policy: {
261
+ minLength: 10,
262
+ requireUppercase: true,
263
+ requireNumbers: true,
264
+ requireSpecialChars: true,
265
+ },
266
+ },
267
+ });
268
+ ```
269
+
270
+ ### Composite (Multiple Backends)
271
+
272
+ Combine multiple identity providers under a single Lixa instance:
273
+
274
+ ```typescript
275
+ import { Lixa, CompositeIdentityProvider, SelfManagedIdentityProvider } from "@vunexa/lixa";
276
+ import { CognitoIdentityProvider } from "@vunexa/lixa-extensions/identity/cognito";
277
+ import { GoogleProvider } from "@vunexa/lixa-extensions/providers";
278
+
279
+ const lixa = new Lixa({
280
+ identityProvider: new CompositeIdentityProvider({
281
+ defaultProvider: "cognito",
282
+ providers: {
283
+ cognito: new CognitoIdentityProvider({
284
+ userPoolId: "us-east-1_xxx",
285
+ clientId: "your-client-id",
286
+ region: "us-east-1",
287
+ }),
288
+ "self-managed": new SelfManagedIdentityProvider({
289
+ /* credentials config */
290
+ }),
291
+ },
292
+ oauthProviders: {
293
+ google: {
294
+ provider: new GoogleProvider(),
295
+ clientId: process.env.GOOGLE_CLIENT_ID!,
296
+ clientSecret: process.env.GOOGLE_CLIENT_SECRET!,
297
+ redirectUri: "http://localhost:3000/auth/google/callback",
298
+ scopes: ["openid", "email", "profile"],
299
+ },
300
+ },
301
+ }),
302
+ });
303
+
304
+ // Route to a specific backend:
305
+ await lixa.signIn({ identifier: "user@example.com", password: "pass", provider: "cognito" });
306
+ ```
307
+
308
+ ---
309
+
310
+ ## OAuth / SSO Providers
311
+
312
+ ### Built-in Providers
313
+
314
+ Available from `@vunexa/lixa-extensions/providers`:
315
+
316
+ | Provider | Class | Default Auth Scopes |
317
+ |----------|-------|-------------------|
318
+ | Google | `GoogleProvider` | `openid`, `profile`, `email` |
319
+ | GitHub | `GithubProvider` | `read:user`, `user:email` |
320
+ | Microsoft | `MicrosoftProvider` | `openid`, `profile`, `email`, `User.Read` |
321
+ | AWS Cognito SSO | `CognitoSSOProvider` | `openid`, `email`, `profile` |
322
+
323
+ ```typescript
324
+ import { GoogleProvider, GithubProvider, MicrosoftProvider, CognitoSSOProvider } from "@vunexa/lixa-extensions/providers";
325
+
326
+ const lixa = new Lixa({
327
+ providers: {
328
+ google: {
329
+ provider: new GoogleProvider(),
330
+ clientId: "...", clientSecret: "...", redirectUri: "...",
331
+ scopes: ["openid", "email", "profile"],
332
+ },
333
+ microsoft: {
334
+ provider: new MicrosoftProvider({ tenant: "common" }),
335
+ clientId: "...", clientSecret: "...", redirectUri: "...",
336
+ scopes: ["openid", "profile", "email", "User.Read"],
337
+ },
338
+ },
339
+ });
340
+ ```
341
+
342
+ ### Custom Providers
343
+
344
+ Integrate **any** OAuth 2.0 / OIDC provider by implementing the `IProvider` interface — just 3 URLs:
345
+
346
+ ```typescript
347
+ import { type IProvider, Lixa } from "@vunexa/lixa";
348
+
349
+ const discordProvider: IProvider = {
350
+ authorizationEndpoint: "https://discord.com/api/oauth2/authorize",
351
+ tokenEndpoint: "https://discord.com/api/oauth2/token",
352
+ userInfoEndpoint: "https://discord.com/api/users/@me",
353
+ };
354
+
355
+ const lixa = new Lixa({
356
+ providers: {
357
+ discord: {
358
+ provider: discordProvider,
359
+ clientId: "...", clientSecret: "...", redirectUri: "...",
360
+ scopes: ["identify", "email"],
361
+ },
362
+ },
363
+ });
364
+ ```
365
+
366
+ ---
367
+
368
+ ## Credentials Authentication
369
+
370
+ Sign up, sign in, change password, and forgot password — all through a unified API:
371
+
372
+ ```typescript
373
+ // Sign up a new user
374
+ const { user, sessionId } = await lixa.signUp({
375
+ identifier: "alice@example.com",
376
+ password: "SecurePassword123!",
377
+ email: "alice@example.com",
378
+ metadata: { name: "Alice Smith" },
379
+ });
380
+
381
+ // Sign in
382
+ const { user, sessionId, session } = await lixa.signIn({
383
+ identifier: "alice@example.com",
384
+ password: "SecurePassword123!",
385
+ });
386
+
387
+ // Change password
388
+ await lixa.changePassword({
389
+ identifier: "alice@example.com",
390
+ oldPassword: "SecurePassword123!",
391
+ newPassword: "NewSecurePassword456!",
142
392
  });
143
393
 
144
- // Unlink a provider account
145
- await lixa.unlinkAccount(activeSessionId, "github");
394
+ // Forgot password flow
395
+ const { deliveryMedium } = await lixa.forgotPassword({
396
+ identifier: "alice@example.com",
397
+ });
398
+
399
+ // Confirm password reset with verification code
400
+ await lixa.confirmPasswordReset({
401
+ identifier: "alice@example.com",
402
+ confirmationCode: "123456",
403
+ newPassword: "ResetPassword789!",
404
+ });
405
+
406
+ // Verify credentials without creating a session
407
+ const identity = await lixa.verifyCredentials({
408
+ identifier: "alice@example.com",
409
+ password: "ResetPassword789!",
410
+ });
411
+ ```
412
+
413
+ ---
414
+
415
+ ## Session Management
416
+
417
+ Lixa manages sessions with pluggable storage. By default, sessions are stored in-memory (suitable for development). For production, use a database adapter.
418
+
419
+ ```typescript
420
+ // Fetch session info
421
+ const session = await lixa.fetchSessionInfo(sessionId);
422
+ // → { sessionId, userId, email, provider, token, raw }
423
+
424
+ // Delete session (logout)
425
+ await lixa.deleteSession(sessionId);
426
+ ```
427
+
428
+ **Cookie utilities** are included for secure session cookie management:
429
+
430
+ ```typescript
431
+ import {
432
+ createSessionCookie,
433
+ clearSessionCookie,
434
+ DEFAULT_SESSION_COOKIE_NAME,
435
+ } from "@vunexa/lixa";
436
+
437
+ // Create a session cookie (HttpOnly, Secure, SameSite=Lax)
438
+ const cookie = createSessionCookie(sessionId);
439
+ // → { name: "lixa_session", value: sessionId, options: { httpOnly, secure, ... } }
146
440
  ```
147
441
 
148
442
  ---
149
443
 
150
- ## Post-Login Resource Connection (AuthZ API)
444
+ ## Account Linking
445
+
446
+ Automatically link multiple SSO accounts (Google + GitHub + Cognito) under a single user identity based on verified email:
447
+
448
+ ```typescript
449
+ import { Lixa, AccountLinkingStrategy } from "@vunexa/lixa";
450
+
451
+ const lixa = new Lixa({
452
+ accountLinking: {
453
+ mode: AccountLinkingStrategy.AUTO_LINK_BY_VERIFIED_EMAIL,
454
+ requireVerifiedEmail: true, // default
455
+ },
456
+ providers: { /* ... */ },
457
+ });
458
+ ```
151
459
 
152
- Primary authentication is strictly limited to identity scopes. To request third-party API permissions (such as GitHub Repositories or Google Drive access), use Lixa's post-login **Resource Connection API**.
460
+ **Account linking strategies:**
153
461
 
154
- ### Resource Connection Sequence
462
+ | Strategy | Behavior |
463
+ |----------|----------|
464
+ | `AUTO_LINK_BY_VERIFIED_EMAIL` | Auto-merge accounts with the same verified email |
465
+ | `ISOLATED` | Keep each provider login as a separate account (default) |
466
+ | `STEP_UP_VERIFICATION` | Require password verification before linking a new provider |
155
467
 
156
- ```mermaid
157
- sequenceDiagram
158
- autonumber
159
- actor User
160
- participant Frontend
161
- participant Express as Backend Express App
162
- participant Lixa as Lixa Core Engine
163
- participant ResourceAPI as GitHub / Google Drive API
468
+ **Manual account linking:**
164
469
 
165
- User->>Frontend: Click "Connect GitHub Repos"
166
- Frontend->>Express: GET /connect-resource/github
167
- Express->>Lixa: getResourceAuthUrl({ sessionId, provider: "github", scopes: ["repo"] })
168
- Lixa-->>Express: Returns Consent URL + Resource State
169
- Express-->>User: 302 Redirect to Provider Consent Page
470
+ ```typescript
471
+ // Explicitly link a provider to an active session
472
+ await lixa.linkAccount({ sessionId, provider: "github", code, state });
170
473
 
171
- User->>ResourceAPI: Grant Resource Permission ("repo")
172
- ResourceAPI-->>Express: GET /callback?code=xyz (Cookie: session_id, lixa_resource_flow=true)
173
- Express->>Lixa: handleResourceCallback({ sessionId, provider: "github", code })
174
- Lixa->>ResourceAPI: Exchange Code for Resource Access & Refresh Tokens
175
- Lixa->>DB: Save Resource Tokens to user_resources Table
176
- Lixa-->>Express: Return Updated Session
177
- Express-->>Frontend: 200 OK / Redirect to Dashboard
474
+ // Unlink a provider
475
+ await lixa.unlinkAccount(sessionId, "github");
476
+
477
+ // Get all linked accounts for a user
478
+ const accounts = await lixa.getUserAccounts(sessionId);
479
+ // → { google: { provider, email, linkedAt }, github: { ... } }
178
480
  ```
179
481
 
180
- ### Querying Connected Resource Access Tokens
482
+ ---
483
+
484
+ ## Resource Connections (AuthZ)
485
+
486
+ Connect third-party APIs (Google Drive, GitHub Repos) **post-login** with clean AuthN / AuthZ separation:
181
487
 
182
488
  ```typescript
183
- // Query connected resource (automatically handles token refreshes)
489
+ // Configure scopes for resource authorization
490
+ const lixa = new Lixa({
491
+ providers: {
492
+ github: {
493
+ provider: new GithubProvider(),
494
+ clientId: "...", clientSecret: "...", redirectUri: "...",
495
+ scopes: {
496
+ auth: ["read:user", "user:email"], // login scopes
497
+ resource: ["repo", "read:org"], // post-login API scopes
498
+ },
499
+ },
500
+ },
501
+ });
502
+
503
+ // 1. Generate resource auth URL (user must be logged in)
504
+ const resourceUrl = await lixa.getResourceAuthUrl({
505
+ sessionId,
506
+ provider: "github",
507
+ scopes: ["repo", "read:org"],
508
+ prompt: "consent",
509
+ });
510
+
511
+ // 2. Handle resource callback
512
+ const session = await lixa.handleResourceCallback({
513
+ sessionId,
514
+ provider: "github",
515
+ code: "auth_code",
516
+ state: "state_param",
517
+ });
518
+
519
+ // 3. Get connected resource token (auto-refreshes expired tokens)
184
520
  const resource = await lixa.getConnectedResource(sessionId, "github");
521
+ // → { provider, accessToken, refreshToken, expiresAt, scopes, raw }
522
+
523
+ // 4. Or fetch by user ID directly (session-independent)
524
+ const resource = await lixa.getUserResource("user@example.com", "github");
525
+
526
+ // 5. Disconnect a resource
527
+ await lixa.disconnectResource(sessionId, "github");
528
+ ```
529
+
530
+ ---
531
+
532
+ ## Database Storage
533
+
534
+ Lixa uses in-memory storage by default. For production, use the database adapters from `@vunexa/lixa-extensions`:
535
+
536
+ ```typescript
537
+ // Prisma
538
+ import { createPrismaAdapter } from "@vunexa/lixa-extensions/storage/prisma";
539
+ const storage = createPrismaAdapter(new PrismaClient());
540
+
541
+ // Drizzle
542
+ import { createDrizzleAdapter } from "@vunexa/lixa-extensions/storage/drizzle";
543
+ const storage = createDrizzleAdapter(db, {
544
+ sessions: schema.sessions,
545
+ userResources: schema.userResources,
546
+ oauthStates: schema.oauthStates,
547
+ userCredentials: schema.userCredentials,
548
+ });
185
549
 
186
- if (resource) {
187
- // Use access token to call external API
188
- const reposResponse = await fetch("https://api.github.com/user/repos", {
189
- headers: { Authorization: `Bearer ${resource.accessToken}` },
190
- });
191
- const repos = await reposResponse.json();
550
+ // Pass to Lixa
551
+ const lixa = new Lixa({
552
+ storage,
553
+ providers: { /* ... */ },
554
+ });
555
+ ```
556
+
557
+ The adapters include a built-in **2-minute in-memory TTL cache** to eliminate redundant database reads on rapid HTTP requests. Disable it with `{ enableCache: false }` (Prisma) or by not calling `withLocalCache`.
558
+
559
+ ---
560
+
561
+ ## Express Integration
562
+
563
+ Drop-in Express route handlers and middleware from `@vunexa/lixa-extensions/express`:
564
+
565
+ ```typescript
566
+ import {
567
+ handleLogin,
568
+ handleCallback,
569
+ handleLogout,
570
+ createRequireAuthMiddleware,
571
+ handleCredentialsSignUp,
572
+ handleCredentialsSignIn,
573
+ } from "@vunexa/lixa-extensions/express";
574
+
575
+ const requireAuth = createRequireAuthMiddleware(lixa);
576
+
577
+ // OAuth SSO
578
+ app.get("/login", (req, res) => handleLogin(req, res, lixa));
579
+ app.get("/auth/:provider/callback", (req, res) =>
580
+ handleCallback(req, res, lixa, {
581
+ successRedirectUrl: "/dashboard",
582
+ errorRedirectUrl: "/login?error=auth_failed",
583
+ })
584
+ );
585
+
586
+ // Credentials (username/password)
587
+ app.post("/auth/signup", (req, res) => handleCredentialsSignUp(req, res, lixa));
588
+ app.post("/auth/signin", (req, res) => handleCredentialsSignIn(req, res, lixa));
589
+
590
+ // Protected routes
591
+ app.get("/profile", requireAuth, (req, res) => {
592
+ res.json({ user: req.sessionInfo });
593
+ });
594
+
595
+ // Logout
596
+ app.post("/logout", (req, res) => handleLogout(req, res, lixa));
597
+ ```
598
+
599
+ ---
600
+
601
+ ## Error Handling
602
+
603
+ All Lixa errors extend `LixaError` with a `code` and optional `details`:
604
+
605
+ ```typescript
606
+ import {
607
+ LixaError,
608
+ InvalidCredentialsError,
609
+ UserAlreadyExistsError,
610
+ WeakPasswordError,
611
+ ProviderNotConfiguredError,
612
+ SessionNotFoundError,
613
+ AccountLinkingChallengeRequiredError,
614
+ } from "@vunexa/lixa";
615
+
616
+ try {
617
+ await lixa.signIn({ identifier: "user", password: "wrong" });
618
+ } catch (error) {
619
+ if (error instanceof InvalidCredentialsError) {
620
+ // error.code === "INVALID_CREDENTIALS"
621
+ }
622
+ if (error instanceof WeakPasswordError) {
623
+ // error.validationErrors === ["Must be at least 10 characters", ...]
624
+ }
625
+ if (error instanceof AccountLinkingChallengeRequiredError) {
626
+ // error.challengeToken, error.email, error.existingProviders
627
+ }
192
628
  }
193
629
  ```
194
630
 
631
+ **Error codes reference:**
632
+
633
+ | Error Class | Code | When |
634
+ |-------------|------|------|
635
+ | `InvalidStateError` | `INVALID_STATE` | OAuth state expired or tampered |
636
+ | `ProviderNotConfiguredError` | `PROVIDER_NOT_CONFIGURED` | Provider not in config |
637
+ | `InvalidProviderConfigError` | `INVALID_PROVIDER_CONFIG` | Missing clientId/secret/redirectUri |
638
+ | `TokenExchangeError` | `TOKEN_EXCHANGE_FAILED` | Code-to-token exchange failed |
639
+ | `SessionNotFoundError` | `SESSION_NOT_FOUND` | Session expired or invalid |
640
+ | `InvalidCredentialsError` | `INVALID_CREDENTIALS` | Wrong username/password |
641
+ | `UserAlreadyExistsError` | `USER_ALREADY_EXISTS` | Duplicate registration |
642
+ | `UserNotFoundError` | `USER_NOT_FOUND` | User not in database |
643
+ | `WeakPasswordError` | `WEAK_PASSWORD` | Password policy violation |
644
+ | `IdentityNotConfiguredError` | `CREDENTIALS_NOT_CONFIGURED` | No identity provider set |
645
+ | `AccountLinkingChallengeRequiredError` | `ACCOUNT_LINKING_CHALLENGE_REQUIRED` | Step-up verification needed |
646
+ | `SSOAccountAlreadyExistsError` | `SSO_ACCOUNT_EXISTS` | Email already has SSO account |
647
+ | `AccountNotVerifiedError` | `ACCOUNT_NOT_VERIFIED` | Email not confirmed yet |
648
+ | `RefreshTokenError` | `REFRESH_TOKEN_ERROR` | Token refresh failed |
649
+
650
+ ---
651
+
652
+ ## API Reference
653
+
654
+ ### `Lixa` Class
655
+
656
+ **Constructor:** `new Lixa(config: LixaConfig)`
657
+
658
+ | Config Property | Type | Description |
659
+ |----------------|------|-------------|
660
+ | `providers` | `Record<string, ProviderConfig>` | OAuth provider configurations |
661
+ | `identityProvider` / `identity` | `IIdentityProvider \| IdentityConfig` | Identity backend (Cognito, Auth0, Okta, Self-Managed) |
662
+ | `credentials` | `CredentialsConfig` | Self-managed credential settings (auto-creates SelfManagedIdentityProvider) |
663
+ | `storage` | `StorageAdapter` | Unified database adapter (Prisma / Drizzle) |
664
+ | `accountLinking` | `boolean \| AccountLinkingMode \| AccountLinkingConfig` | Multi-SSO account linking strategy |
665
+ | `sessionCookieName` | `string \| ((lixa, req?) => string)` | Custom session cookie name |
666
+ | `debug` | `boolean` | Enable structured debug logging |
667
+ | `logger` | `LixaLogger` | Custom logger implementation |
668
+
669
+ **OAuth Methods:**
670
+
671
+ | Method | Returns | Description |
672
+ |--------|---------|-------------|
673
+ | `getAuthUrl(provider, state?)` | `Promise<string>` | Generate authorization URL with PKCE |
674
+ | `handleCallback({ provider, code, state })` | `Promise<string>` | Exchange code for tokens, create session |
675
+ | `isProviderConfigured(provider)` | `boolean` | Check if a provider is configured |
676
+
677
+ **Identity Methods:**
678
+
679
+ | Method | Returns | Description |
680
+ |--------|---------|-------------|
681
+ | `signUp(params)` | `Promise<SignUpResult>` | Register a new user |
682
+ | `signIn(params)` | `Promise<SignInResult>` | Authenticate with credentials |
683
+ | `verifyCredentials(params)` | `Promise<UserIdentity \| null>` | Verify credentials without session |
684
+ | `changePassword(params)` | `Promise<boolean>` | Update user password |
685
+ | `forgotPassword(params)` | `Promise<ForgotPasswordResult>` | Initiate password reset |
686
+ | `confirmPasswordReset(params)` | `Promise<boolean>` | Complete password reset |
687
+ | `confirmSignUp(params)` | `Promise<boolean>` | Confirm registration with code |
688
+ | `resendConfirmationCode(params)` | `Promise<ResendConfirmationCodeResult>` | Resend verification code |
689
+
690
+ **Session & Account Methods:**
691
+
692
+ | Method | Returns | Description |
693
+ |--------|---------|-------------|
694
+ | `fetchSessionInfo(sessionId)` | `Promise<Session \| null>` | Get active session details |
695
+ | `deleteSession(sessionId)` | `Promise<void>` | Delete session (logout) |
696
+ | `linkAccount(params)` | `Promise<string>` | Link new OAuth provider to session |
697
+ | `unlinkAccount(sessionId, provider)` | `Promise<boolean>` | Unlink a provider account |
698
+ | `getUserAccounts(userIdOrSessionId)` | `Promise<Record<string, LinkedAccount>>` | Get all linked accounts |
699
+
700
+ **Resource Methods:**
701
+
702
+ | Method | Returns | Description |
703
+ |--------|---------|-------------|
704
+ | `getResourceAuthUrl(params)` | `Promise<string>` | Generate resource authorization URL |
705
+ | `handleResourceCallback(params)` | `Promise<Session>` | Handle resource OAuth callback |
706
+ | `getConnectedResource(sessionId, provider)` | `Promise<ConnectedResource \| null>` | Get resource token (auto-refreshes) |
707
+ | `getUserResource(userId, provider)` | `Promise<ConnectedResource \| null>` | Get resource by user ID |
708
+ | `disconnectResource(sessionId, provider)` | `Promise<boolean>` | Disconnect resource provider |
709
+
710
+ **Static Methods:**
711
+
712
+ | Method | Returns | Description |
713
+ |--------|---------|-------------|
714
+ | `Lixa.generateRandomState()` | `string` | Generate 32-char hex CSRF state |
715
+
195
716
  ---
196
717
 
197
718
  ## License
198
719
 
199
- MIT © [Vunexa](https://github.com/vunexa)
720
+ [MIT](https://opensource.org/licenses/MIT) © [Vunexa](https://vunexa.app)