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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (41) hide show
  1. package/README.md +103 -689
  2. package/dist/credentials/credentials-manager.d.ts +65 -0
  3. package/dist/credentials/credentials-manager.d.ts.map +1 -0
  4. package/dist/credentials/hasher.d.ts +88 -0
  5. package/dist/credentials/hasher.d.ts.map +1 -0
  6. package/dist/credentials/index.d.ts +6 -0
  7. package/dist/credentials/index.d.ts.map +1 -0
  8. package/dist/credentials/local-storage.d.ts +20 -0
  9. package/dist/credentials/local-storage.d.ts.map +1 -0
  10. package/dist/credentials/policy.d.ts +12 -0
  11. package/dist/credentials/policy.d.ts.map +1 -0
  12. package/dist/credentials/types.d.ts +314 -0
  13. package/dist/credentials/types.d.ts.map +1 -0
  14. package/dist/dao/session-cache.d.ts.map +1 -1
  15. package/dist/dao/types.d.ts +19 -0
  16. package/dist/dao/types.d.ts.map +1 -1
  17. package/dist/errors.d.ts +61 -0
  18. package/dist/errors.d.ts.map +1 -1
  19. package/dist/export-types/index.d.ts +1187 -34
  20. package/dist/identity/index.d.ts +3 -0
  21. package/dist/identity/index.d.ts.map +1 -0
  22. package/dist/identity/self-managed.d.ts +52 -0
  23. package/dist/identity/self-managed.d.ts.map +1 -0
  24. package/dist/identity/types.d.ts +224 -0
  25. package/dist/identity/types.d.ts.map +1 -0
  26. package/dist/index.cjs +1408 -249
  27. package/dist/index.cjs.map +1 -1
  28. package/dist/index.d.cts +1149 -29
  29. package/dist/index.d.ts +6 -3
  30. package/dist/index.d.ts.map +1 -1
  31. package/dist/index.js +1387 -237
  32. package/dist/index.js.map +1 -1
  33. package/dist/lixa.d.ts +165 -14
  34. package/dist/lixa.d.ts.map +1 -1
  35. package/dist/models/session.d.ts +9 -9
  36. package/dist/models/session.d.ts.map +1 -1
  37. package/dist/providers/IProvider.d.ts +4 -0
  38. package/dist/providers/IProvider.d.ts.map +1 -1
  39. package/dist/types.d.ts +97 -6
  40. package/dist/types.d.ts.map +1 -1
  41. package/package.json +3 -11
package/README.md CHANGED
@@ -6,780 +6,194 @@
6
6
  [![npm downloads](https://img.shields.io/npm/dm/@vunexa/lixa.svg)](https://www.npmjs.com/package/@vunexa/lixa)
7
7
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
8
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**.
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**.
11
11
 
12
12
  ---
13
13
 
14
14
  ## Features
15
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
- - **Pre-built Database Storage Adapters**: Official Prisma and Drizzle ORM adapters via `@vunexa/lixa-adapters` with support for PostgreSQL, MySQL, and SQLite.
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).
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.
25
23
 
26
24
  ---
27
25
 
28
26
  ## Architecture Overview
29
27
 
30
- ![Architecture Overview](https://cdn.jsdelivr.net/npm/@vunexa/lixa/docs/images/architecture.svg)
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
42
+
43
+ SessionStore -->|Session Cookie| Client
44
+ ResourceStore -->|Auto-Refreshed Tokens| Client
45
+ ```
31
46
 
32
47
  ---
33
48
 
34
49
  ## Installation
35
50
 
36
51
  ```bash
37
- npm install @vunexa/lixa @vunexa/lixa-providers
38
- # or
39
- yarn add @vunexa/lixa @vunexa/lixa-providers
52
+ npm install @vunexa/lixa @vunexa/lixa-extensions
40
53
  ```
41
54
 
42
55
  ---
43
56
 
44
57
  ## Quick Start
45
58
 
46
- ### 1. Configure Lixa with Account Linking & Providers
59
+ ### 1. Configure Lixa Instance
47
60
 
48
61
  ```typescript
49
62
  import { Lixa, AccountLinkingStrategy } from "@vunexa/lixa";
50
- import { GoogleProvider, GithubProvider } from "@vunexa/lixa-providers";
63
+ 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();
51
68
 
52
69
  export const lixa = new Lixa({
53
- // Configure Multi-SSO Account Linking Strategy
70
+ // Unified database storage for sessions, OAuth state, resources, and account linking
71
+ storage: createPrismaAdapter(prisma),
72
+
73
+ // Configure Multi-SSO Account Linking
54
74
  accountLinking: {
55
75
  mode: AccountLinkingStrategy.AUTO_LINK_BY_VERIFIED_EMAIL,
56
76
  requireVerifiedEmail: true,
57
77
  },
58
- providers: {
78
+
79
+ // Federated OAuth Providers
80
+ federatedOAuthProviders: {
59
81
  google: {
60
82
  provider: new GoogleProvider(),
61
83
  clientId: process.env.GOOGLE_CLIENT_ID!,
62
84
  clientSecret: process.env.GOOGLE_CLIENT_SECRET!,
63
- redirectUri: "http://localhost:3000/auth/google/callback",
85
+ redirectUri: "http://localhost:3000/api/v1/oidc/google/callback",
64
86
  scopes: ["openid", "email", "profile"],
65
87
  },
66
88
  github: {
67
89
  provider: new GithubProvider(),
68
90
  clientId: process.env.GITHUB_CLIENT_ID!,
69
91
  clientSecret: process.env.GITHUB_CLIENT_SECRET!,
70
- redirectUri: "http://localhost:3000/auth/github/callback",
92
+ redirectUri: "http://localhost:3000/api/v1/oidc/github/callback",
71
93
  scopes: ["read:user", "user:email"],
72
94
  },
73
95
  },
74
96
  });
75
97
  ```
76
98
 
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;
90
-
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
- });
104
- ```
105
-
106
99
  ---
107
100
 
108
101
  ## Multi-SSO Account Linking
109
102
 
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
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.
113
104
 
114
- ![Account Linking Sequence](https://cdn.jsdelivr.net/npm/@vunexa/lixa/docs/images/account-linking.svg)
105
+ ### Account Linking Sequence
115
106
 
116
- ### Account Linking Configuration Modes
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
117
116
 
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
- }
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
126
122
 
127
- // Mode 2: Treat provider logins as separate accounts
128
- accountLinking: {
129
- mode: AccountLinkingStrategy.ISOLATED,
130
- }
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
131
131
  ```
132
132
 
133
- ### Explicit Account Linking & Unlinking APIs
133
+ ### Account Linking Usage
134
134
 
135
135
  ```typescript
136
136
  // Explicitly link a new provider while authenticated
137
- await lixa.linkAccount({
138
- sessionId: req.cookies.session_id,
137
+ const sessionId = await lixa.handleCallback({
139
138
  provider: "github",
140
139
  code: req.query.code as string,
141
140
  state: req.query.state as string,
141
+ sessionId: activeSessionId, // Links GitHub directly to the active session
142
142
  });
143
143
 
144
144
  // Unlink a provider account
145
- await lixa.unlinkAccount(req.cookies.session_id, "github");
146
- ```
147
-
148
- ---
149
-
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
155
-
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");
209
-
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
- });
215
- ```
216
-
217
- ---
218
-
219
- ## Session Interface Structure
220
-
221
- ```typescript
222
- export interface Session<TRaw = OAuthTokenResponse> {
223
- /** Unique session ID generated by Lixa */
224
- id?: string;
225
-
226
- /** Unified user ID across linked accounts */
227
- userId?: string;
228
-
229
- /** Primary user email */
230
- email?: string;
231
-
232
- /** Linked SSO provider accounts (AuthN) - Single Source of Truth */
233
- accounts?: Record<string, LinkedAccount>;
234
-
235
- /** Connected third-party resource provider tokens (AuthZ) - Single Source of Truth */
236
- resources?: Record<string, ConnectedResource>;
237
-
238
- /** Optional primary access token or session token */
239
- token?: string;
240
-
241
- /** Optional current active auth provider */
242
- provider?: string;
243
-
244
- /** Optional raw token response from provider */
245
- raw?: TRaw;
246
- }
247
- ```
248
-
249
- ### `LinkedAccount` vs `ConnectedResource`
250
-
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]` |
255
-
256
- ---
257
-
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:
265
-
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
281
-
282
- ```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
- }
320
-
321
- async deleteSession(sessionId: string): Promise<void> {
322
- const stmt = this.db.prepare(`DELETE FROM sessions WHERE id = ?`);
323
- stmt.run(sessionId);
324
- }
325
-
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
- export const lixa = new Lixa({
336
- sessionHandler: {
337
- sessionStorage: new SqliteSessionStorage(),
338
- },
339
- providers: { /* ... */ },
340
- });
341
- ```
342
-
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
357
- },
358
- "github": {
359
- "provider": "github",
360
- "email": "alex.developer@example.com",
361
- "providerUserId": "5829104",
362
- "accessToken": "gho_8f7b2a9e1c3...",
363
- "linkedAt": 1771657250000
364
- }
365
- }
366
- }
367
- ```
368
-
369
- ---
370
-
371
- ### 2. AWS DynamoDB Session Storage (`@aws-sdk/lib-dynamodb`)
372
-
373
- For serverless and distributed AWS deployments using DynamoDB:
374
-
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
383
-
384
- ```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: { /* ... */ },
463
- });
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
- }
145
+ await lixa.unlinkAccount(activeSessionId, "github");
495
146
  ```
496
147
 
497
148
  ---
498
149
 
499
- ## Custom Resource Storage Implementation (`ResourceStorage`)
150
+ ## Post-Login Resource Connection (AuthZ API)
500
151
 
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.
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**.
502
153
 
503
- ### `ResourceStorage` Interface
154
+ ### Resource Connection Sequence
504
155
 
505
- ```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
- ```
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
513
164
 
514
- ### 1. SQLite Resource Storage (`better-sqlite3`)
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
515
170
 
516
- #### Table Schema (SQL DDL)
517
-
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
- );
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
526
178
  ```
527
179
 
528
- #### TypeScript Implementation
180
+ ### Querying Connected Resource Access Tokens
529
181
 
530
182
  ```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
- }
559
-
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
- }
565
-
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
- }
575
-
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
- }
183
+ // Query connected resource (automatically handles token refreshes)
184
+ const resource = await lixa.getConnectedResource(sessionId, "github");
581
185
 
582
- // Pass to Lixa instance via resourceHandler
583
- export const lixa = new Lixa({
584
- resourceHandler: {
585
- resourceStorage: new SqliteResourceStorage(),
586
- },
587
- providers: { /* ... */ },
588
- });
589
- ```
590
-
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
604
- }
605
- }
606
- ```
607
-
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
- }
621
- ```
622
-
623
- ---
624
-
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
630
-
631
- - **Table Name**: `LixaUserResources`
632
- - **Partition Key (PK)**: `userId` (String)
633
- - **Sort Key (SK)**: `provider` (String)
634
-
635
- #### TypeScript Implementation
636
-
637
- ```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
- }
664
-
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: { /* ... */ },
716
- });
717
- ```
718
-
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
- ```
749
-
750
- ---
751
-
752
- ### 3. Querying Connected Resources Directly by User ID
753
-
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}` },
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}` },
764
190
  });
191
+ const repos = await reposResponse.json();
765
192
  }
766
193
  ```
767
194
 
768
195
  ---
769
196
 
770
- ## User Info Utilities
771
-
772
- Lixa provides utilities to extract user information from OAuth tokens:
773
-
774
- ```typescript
775
- import { extractUserInfo, type UserInfo } from "@vunexa/lixa";
776
-
777
- const { userInfo } = await extractUserInfo(tokenData, providerMetadata);
778
- console.log(userInfo.email, userInfo.name);
779
- ```
780
-
781
- ---
782
-
783
197
  ## License
784
198
 
785
- [MIT](LICENSE)
199
+ MIT © [Vunexa](https://github.com/vunexa)