@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 +635 -114
- package/dist/credentials/credentials-manager.d.ts +14 -0
- package/dist/credentials/credentials-manager.d.ts.map +1 -1
- package/dist/credentials/types.d.ts +23 -0
- package/dist/credentials/types.d.ts.map +1 -1
- package/dist/dao/types.d.ts +5 -0
- package/dist/dao/types.d.ts.map +1 -1
- package/dist/errors.d.ts +43 -0
- package/dist/errors.d.ts.map +1 -1
- package/dist/export-types/index.d.ts +392 -95
- package/dist/identity/composite.d.ts +65 -0
- package/dist/identity/composite.d.ts.map +1 -0
- package/dist/identity/index.d.ts +1 -0
- package/dist/identity/index.d.ts.map +1 -1
- package/dist/identity/self-managed.d.ts +16 -2
- package/dist/identity/self-managed.d.ts.map +1 -1
- package/dist/identity/types.d.ts +8 -0
- package/dist/identity/types.d.ts.map +1 -1
- package/dist/index.cjs +903 -143
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +354 -63
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +883 -127
- package/dist/index.js.map +1 -1
- package/dist/lixa.d.ts +90 -29
- package/dist/lixa.d.ts.map +1 -1
- package/dist/models/session.d.ts +10 -4
- package/dist/models/session.d.ts.map +1 -1
- package/dist/types.d.ts +83 -30
- package/dist/types.d.ts.map +1 -1
- package/dist/utils/user-info.d.ts.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,47 +1,88 @@
|
|
|
1
1
|
# @vunexa/lixa
|
|
2
2
|
|
|
3
|
-
|
|
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
|
[](https://www.npmjs.com/package/@vunexa/lixa)
|
|
6
|
-
[](https://www.npmjs.com/package/@vunexa/lixa)
|
|
7
8
|
[](https://opensource.org/licenses/MIT)
|
|
8
9
|
|
|
9
|
-
|
|
10
|
-
|
|
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
|
-
##
|
|
41
|
+
## Why Lixa?
|
|
15
42
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
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
|
-
|
|
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
|
-
|
|
44
|
-
|
|
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
|
-
|
|
108
|
+
Get up and running with Google OAuth in under 5 minutes:
|
|
60
109
|
|
|
61
110
|
```typescript
|
|
62
|
-
import { 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
|
-
|
|
70
|
-
|
|
71
|
-
|
|
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/
|
|
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/
|
|
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
|
-
##
|
|
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
|
-
|
|
197
|
+
**Cognito API coverage:**
|
|
104
198
|
|
|
105
|
-
|
|
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
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
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
|
-
|
|
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
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
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
|
-
###
|
|
231
|
+
### Okta
|
|
134
232
|
|
|
135
233
|
```typescript
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
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
|
-
//
|
|
145
|
-
await lixa.
|
|
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
|
-
##
|
|
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
|
-
|
|
460
|
+
**Account linking strategies:**
|
|
153
461
|
|
|
154
|
-
|
|
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
|
-
|
|
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
|
-
|
|
166
|
-
|
|
167
|
-
|
|
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
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
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
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
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://
|
|
720
|
+
[MIT](https://opensource.org/licenses/MIT) © [Vunexa](https://vunexa.app)
|