@vunexa/lixa 0.1.4 → 0.1.6-alpha.1
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 +301 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -28,7 +28,36 @@ A flexible, provider-agnostic OAuth 2.0 and OpenID Connect (OIDC) client library
|
|
|
28
28
|
|
|
29
29
|
## Architecture Overview
|
|
30
30
|
|
|
31
|
-
|
|
31
|
+
```mermaid
|
|
32
|
+
graph TD
|
|
33
|
+
User([User Client / Browser])
|
|
34
|
+
|
|
35
|
+
subgraph Lixa ["Lixa Core Engine"]
|
|
36
|
+
AuthNGuard["Primary AuthN Guard<br/>(Minimal Identity Scopes Only)"]
|
|
37
|
+
SessionMgr["Session Handler & Unified Session"]
|
|
38
|
+
AccountLinker["Account Linker<br/>(AUTO_LINK_BY_VERIFIED_EMAIL)"]
|
|
39
|
+
ResourceConn["Resource Connection API<br/>(Post-Login AuthZ)"]
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
subgraph IdentityProviders ["Identity Providers (AuthN)"]
|
|
43
|
+
GoogleAuth["Google / OIDC"]
|
|
44
|
+
GitHubAuth["GitHub OAuth"]
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
subgraph ResourceProviders ["Resource APIs (AuthZ)"]
|
|
48
|
+
GitHubAPI["GitHub API (repos, orgs)"]
|
|
49
|
+
GoogleDriveAPI["Google Drive API"]
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
User -->|"1. Primary Login / Link Account"| AuthNGuard
|
|
53
|
+
AuthNGuard -->|"Request Identity"| IdentityProviders
|
|
54
|
+
IdentityProviders -->|"Tokens + UserInfo"| AccountLinker
|
|
55
|
+
AccountLinker -->|"Unified Session (session.accounts)"| SessionMgr
|
|
56
|
+
|
|
57
|
+
User -->|"2. Connect Resource API (Post-Login)"| ResourceConn
|
|
58
|
+
ResourceConn -->|"Request Permissions (repo, drive)"| ResourceProviders
|
|
59
|
+
ResourceProviders -->|"Resource Access Tokens"| SessionMgr
|
|
60
|
+
```
|
|
32
61
|
|
|
33
62
|
---
|
|
34
63
|
|
|
@@ -252,6 +281,277 @@ export interface Session<TRaw = OAuthTokenResponse> {
|
|
|
252
281
|
|
|
253
282
|
---
|
|
254
283
|
|
|
284
|
+
## Custom Session Storage Implementations
|
|
285
|
+
|
|
286
|
+
Lixa allows you to store sessions in any database or cache by implementing the `SessionStorage` interface.
|
|
287
|
+
|
|
288
|
+
### 1. SQLite Session Storage (`better-sqlite3`)
|
|
289
|
+
|
|
290
|
+
For relational persistence or single-node deployments using SQLite:
|
|
291
|
+
|
|
292
|
+
#### Table Schema (SQL DDL)
|
|
293
|
+
|
|
294
|
+
```sql
|
|
295
|
+
CREATE TABLE IF NOT EXISTS sessions (
|
|
296
|
+
id TEXT PRIMARY KEY,
|
|
297
|
+
user_email TEXT,
|
|
298
|
+
data TEXT NOT NULL,
|
|
299
|
+
expires_at INTEGER NOT NULL
|
|
300
|
+
);
|
|
301
|
+
|
|
302
|
+
CREATE INDEX IF NOT EXISTS idx_sessions_email ON sessions(user_email);
|
|
303
|
+
CREATE INDEX IF NOT EXISTS idx_sessions_expires ON sessions(expires_at);
|
|
304
|
+
```
|
|
305
|
+
|
|
306
|
+
#### TypeScript Implementation
|
|
307
|
+
|
|
308
|
+
```typescript
|
|
309
|
+
import Database from "better-sqlite3";
|
|
310
|
+
import { Lixa, type SessionStorage, type Session } from "@vunexa/lixa";
|
|
311
|
+
|
|
312
|
+
export class SqliteSessionStorage implements SessionStorage {
|
|
313
|
+
private db = new Database("lixa_sessions.db");
|
|
314
|
+
|
|
315
|
+
constructor() {
|
|
316
|
+
this.db.exec(`
|
|
317
|
+
CREATE TABLE IF NOT EXISTS sessions (
|
|
318
|
+
id TEXT PRIMARY KEY,
|
|
319
|
+
user_email TEXT,
|
|
320
|
+
data TEXT NOT NULL,
|
|
321
|
+
expires_at INTEGER NOT NULL
|
|
322
|
+
);
|
|
323
|
+
CREATE INDEX IF NOT EXISTS idx_sessions_email ON sessions(user_email);
|
|
324
|
+
`);
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
async saveSession<T extends Session>(sessionId: string, session: T, expiresInSeconds: number): Promise<void> {
|
|
328
|
+
const expiresAt = Math.floor(Date.now() / 1000) + expiresInSeconds;
|
|
329
|
+
const stmt = this.db.prepare(`
|
|
330
|
+
INSERT INTO sessions (id, user_email, data, expires_at)
|
|
331
|
+
VALUES (?, ?, ?, ?)
|
|
332
|
+
ON CONFLICT(id) DO UPDATE SET
|
|
333
|
+
user_email = excluded.user_email,
|
|
334
|
+
data = excluded.data,
|
|
335
|
+
expires_at = excluded.expires_at
|
|
336
|
+
`);
|
|
337
|
+
stmt.run(sessionId, session.email || null, JSON.stringify(session), expiresAt);
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
async getSession<T extends Session>(sessionId: string): Promise<T | null> {
|
|
341
|
+
const now = Math.floor(Date.now() / 1000);
|
|
342
|
+
const stmt = this.db.prepare(`SELECT data FROM sessions WHERE id = ? AND expires_at > ?`);
|
|
343
|
+
const row = stmt.get(sessionId, now) as { data: string } | undefined;
|
|
344
|
+
return row ? (JSON.parse(row.data) as T) : null;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
async deleteSession(sessionId: string): Promise<void> {
|
|
348
|
+
const stmt = this.db.prepare(`DELETE FROM sessions WHERE id = ?`);
|
|
349
|
+
stmt.run(sessionId);
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
async getSessionByEmail<T extends Session>(email: string): Promise<{ sessionId: string; session: T } | null> {
|
|
353
|
+
const now = Math.floor(Date.now() / 1000);
|
|
354
|
+
const stmt = this.db.prepare(`SELECT id, data FROM sessions WHERE user_email = ? AND expires_at > ? LIMIT 1`);
|
|
355
|
+
const row = stmt.get(email, now) as { id: string; data: string } | undefined;
|
|
356
|
+
return row ? { sessionId: row.id, session: JSON.parse(row.data) as T } : null;
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
// Pass to Lixa instance
|
|
361
|
+
export const lixa = new Lixa({
|
|
362
|
+
sessionHandler: {
|
|
363
|
+
sessionStorage: new SqliteSessionStorage(),
|
|
364
|
+
},
|
|
365
|
+
providers: { /* ... */ },
|
|
366
|
+
});
|
|
367
|
+
```
|
|
368
|
+
|
|
369
|
+
#### Saved JSON Record Example in SQLite (`data` Column)
|
|
370
|
+
|
|
371
|
+
```json
|
|
372
|
+
{
|
|
373
|
+
"id": "e4a91f82c3b4a07f",
|
|
374
|
+
"token": "ya29.a0ARW5m7...",
|
|
375
|
+
"userId": "1049281048",
|
|
376
|
+
"email": "alex.developer@example.com",
|
|
377
|
+
"provider": "google",
|
|
378
|
+
"accounts": {
|
|
379
|
+
"google": {
|
|
380
|
+
"provider": "google",
|
|
381
|
+
"email": "alex.developer@example.com",
|
|
382
|
+
"providerUserId": "1049281048",
|
|
383
|
+
"accessToken": "ya29.a0ARW5m7...",
|
|
384
|
+
"linkedAt": 1771657200000
|
|
385
|
+
},
|
|
386
|
+
"github": {
|
|
387
|
+
"provider": "github",
|
|
388
|
+
"email": "alex.developer@example.com",
|
|
389
|
+
"providerUserId": "5829104",
|
|
390
|
+
"accessToken": "gho_8f7b2a9e1c3...",
|
|
391
|
+
"linkedAt": 1771657250000
|
|
392
|
+
}
|
|
393
|
+
},
|
|
394
|
+
"resources": {
|
|
395
|
+
"github": {
|
|
396
|
+
"provider": "github",
|
|
397
|
+
"accessToken": "gho_resource_repo_9a8b7c...",
|
|
398
|
+
"scopes": ["repo", "read:org"],
|
|
399
|
+
"connectedAt": 1771657300000
|
|
400
|
+
}
|
|
401
|
+
},
|
|
402
|
+
"raw": {
|
|
403
|
+
"access_token": "ya29.a0ARW5m7...",
|
|
404
|
+
"token_type": "Bearer",
|
|
405
|
+
"expires_in": 3599
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
```
|
|
409
|
+
|
|
410
|
+
---
|
|
411
|
+
|
|
412
|
+
### 2. AWS DynamoDB Session Storage (`@aws-sdk/lib-dynamodb`)
|
|
413
|
+
|
|
414
|
+
For serverless and distributed AWS deployments using DynamoDB:
|
|
415
|
+
|
|
416
|
+
#### Table Configuration
|
|
417
|
+
|
|
418
|
+
- **Table Name**: `LixaSessions`
|
|
419
|
+
- **Partition Key**: `sessionId` (String)
|
|
420
|
+
- **Global Secondary Index (GSI)**: `EmailIndex` (`email` as Partition Key)
|
|
421
|
+
- **TTL Attribute**: `ttl` (Unix timestamp in seconds for automatic AWS expiration)
|
|
422
|
+
|
|
423
|
+
#### TypeScript Implementation
|
|
424
|
+
|
|
425
|
+
```typescript
|
|
426
|
+
import { DynamoDBClient } from "@aws-sdk/client-dynamodb";
|
|
427
|
+
import { DynamoDBDocumentClient, PutCommand, GetCommand, DeleteCommand, QueryCommand } from "@aws-sdk/lib-dynamodb";
|
|
428
|
+
import { Lixa, type SessionStorage, type Session } from "@vunexa/lixa";
|
|
429
|
+
|
|
430
|
+
export class DynamoDbSessionStorage implements SessionStorage {
|
|
431
|
+
private docClient: DynamoDBDocumentClient;
|
|
432
|
+
private tableName = "LixaSessions";
|
|
433
|
+
|
|
434
|
+
constructor() {
|
|
435
|
+
const client = new DynamoDBClient({ region: process.env.AWS_REGION || "us-east-1" });
|
|
436
|
+
this.docClient = DynamoDBDocumentClient.from(client);
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
async saveSession<T extends Session>(sessionId: string, session: T, expiresInSeconds: number): Promise<void> {
|
|
440
|
+
const ttl = Math.floor(Date.now() / 1000) + expiresInSeconds;
|
|
441
|
+
await this.docClient.send(
|
|
442
|
+
new PutCommand({
|
|
443
|
+
TableName: this.tableName,
|
|
444
|
+
Item: {
|
|
445
|
+
sessionId,
|
|
446
|
+
email: session.email || "N/A",
|
|
447
|
+
sessionData: session,
|
|
448
|
+
ttl,
|
|
449
|
+
},
|
|
450
|
+
})
|
|
451
|
+
);
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
async getSession<T extends Session>(sessionId: string): Promise<T | null> {
|
|
455
|
+
const res = await this.docClient.send(
|
|
456
|
+
new GetCommand({
|
|
457
|
+
TableName: this.tableName,
|
|
458
|
+
Key: { sessionId },
|
|
459
|
+
})
|
|
460
|
+
);
|
|
461
|
+
|
|
462
|
+
if (!res.Item) return null;
|
|
463
|
+
const now = Math.floor(Date.now() / 1000);
|
|
464
|
+
if (res.Item.ttl && res.Item.ttl < now) return null;
|
|
465
|
+
|
|
466
|
+
return res.Item.sessionData as T;
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
async deleteSession(sessionId: string): Promise<void> {
|
|
470
|
+
await this.docClient.send(
|
|
471
|
+
new DeleteCommand({
|
|
472
|
+
TableName: this.tableName,
|
|
473
|
+
Key: { sessionId },
|
|
474
|
+
})
|
|
475
|
+
);
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
async getSessionByEmail<T extends Session>(email: string): Promise<{ sessionId: string; session: T } | null> {
|
|
479
|
+
const res = await this.docClient.send(
|
|
480
|
+
new QueryCommand({
|
|
481
|
+
TableName: this.tableName,
|
|
482
|
+
IndexName: "EmailIndex",
|
|
483
|
+
KeyConditionExpression: "email = :email",
|
|
484
|
+
ExpressionAttributeValues: { ":email": email },
|
|
485
|
+
Limit: 1,
|
|
486
|
+
})
|
|
487
|
+
);
|
|
488
|
+
|
|
489
|
+
if (!res.Items || res.Items.length === 0) return null;
|
|
490
|
+
const item = res.Items[0];
|
|
491
|
+
const now = Math.floor(Date.now() / 1000);
|
|
492
|
+
if (item.ttl && item.ttl < now) return null;
|
|
493
|
+
|
|
494
|
+
return { sessionId: item.sessionId, session: item.sessionData as T };
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
// Pass to Lixa instance
|
|
499
|
+
export const lixa = new Lixa({
|
|
500
|
+
sessionHandler: {
|
|
501
|
+
sessionStorage: new DynamoDbSessionStorage(),
|
|
502
|
+
},
|
|
503
|
+
providers: { /* ... */ },
|
|
504
|
+
});
|
|
505
|
+
```
|
|
506
|
+
|
|
507
|
+
#### Saved DynamoDB Item JSON Example
|
|
508
|
+
|
|
509
|
+
```json
|
|
510
|
+
{
|
|
511
|
+
"sessionId": "e4a91f82c3b4a07f",
|
|
512
|
+
"email": "alex.developer@example.com",
|
|
513
|
+
"ttl": 1771743600,
|
|
514
|
+
"sessionData": {
|
|
515
|
+
"id": "e4a91f82c3b4a07f",
|
|
516
|
+
"token": "ya29.a0ARW5m7...",
|
|
517
|
+
"userId": "1049281048",
|
|
518
|
+
"email": "alex.developer@example.com",
|
|
519
|
+
"provider": "google",
|
|
520
|
+
"accounts": {
|
|
521
|
+
"google": {
|
|
522
|
+
"provider": "google",
|
|
523
|
+
"email": "alex.developer@example.com",
|
|
524
|
+
"providerUserId": "1049281048",
|
|
525
|
+
"accessToken": "ya29.a0ARW5m7...",
|
|
526
|
+
"linkedAt": 1771657200000
|
|
527
|
+
},
|
|
528
|
+
"github": {
|
|
529
|
+
"provider": "github",
|
|
530
|
+
"email": "alex.developer@example.com",
|
|
531
|
+
"providerUserId": "5829104",
|
|
532
|
+
"accessToken": "gho_8f7b2a9e1c3...",
|
|
533
|
+
"linkedAt": 1771657250000
|
|
534
|
+
}
|
|
535
|
+
},
|
|
536
|
+
"resources": {
|
|
537
|
+
"github": {
|
|
538
|
+
"provider": "github",
|
|
539
|
+
"accessToken": "gho_resource_repo_9a8b7c...",
|
|
540
|
+
"scopes": ["repo", "read:org"],
|
|
541
|
+
"connectedAt": 1771657300000
|
|
542
|
+
}
|
|
543
|
+
},
|
|
544
|
+
"raw": {
|
|
545
|
+
"access_token": "ya29.a0ARW5m7...",
|
|
546
|
+
"token_type": "Bearer",
|
|
547
|
+
"expires_in": 3599
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
```
|
|
552
|
+
|
|
553
|
+
---
|
|
554
|
+
|
|
255
555
|
## User Info Utilities
|
|
256
556
|
|
|
257
557
|
Lixa provides utilities to extract user information from OAuth tokens:
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vunexa/lixa",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.6-alpha.1",
|
|
4
4
|
"description": "Lixa is a flexible, provider-agnostic OAuth and OpenID Connect (OIDC) client library that simplifies multi-provider authentication flows. It supports seamless integration with providers like Google and GitHub, offers extensible session management, and enables dynamic provider resolution based on callback URLs.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"oauth",
|