@vunexa/lixa 0.1.3 → 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/README.template.md +271 -0
- package/dist/export-types/index.d.ts +2 -0
- package/dist/index.cjs +5 -3
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +2 -0
- package/dist/index.js +5 -3
- package/dist/index.js.map +1 -1
- package/dist/lixa.d.ts +2 -0
- package/dist/lixa.d.ts.map +1 -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/README.template.md
CHANGED
|
@@ -342,6 +342,277 @@ export interface Session<TRaw = OAuthTokenResponse> {
|
|
|
342
342
|
|
|
343
343
|
---
|
|
344
344
|
|
|
345
|
+
## Custom Session Storage Implementations
|
|
346
|
+
|
|
347
|
+
Lixa allows you to store sessions in any database or cache by implementing the `SessionStorage` interface.
|
|
348
|
+
|
|
349
|
+
### 1. SQLite Session Storage (`better-sqlite3`)
|
|
350
|
+
|
|
351
|
+
For relational persistence or single-node deployments using SQLite:
|
|
352
|
+
|
|
353
|
+
#### Table Schema (SQL DDL)
|
|
354
|
+
|
|
355
|
+
```sql
|
|
356
|
+
CREATE TABLE IF NOT EXISTS sessions (
|
|
357
|
+
id TEXT PRIMARY KEY,
|
|
358
|
+
user_email TEXT,
|
|
359
|
+
data TEXT NOT NULL,
|
|
360
|
+
expires_at INTEGER NOT NULL
|
|
361
|
+
);
|
|
362
|
+
|
|
363
|
+
CREATE INDEX IF NOT EXISTS idx_sessions_email ON sessions(user_email);
|
|
364
|
+
CREATE INDEX IF NOT EXISTS idx_sessions_expires ON sessions(expires_at);
|
|
365
|
+
```
|
|
366
|
+
|
|
367
|
+
#### TypeScript Implementation
|
|
368
|
+
|
|
369
|
+
```typescript
|
|
370
|
+
import Database from "better-sqlite3";
|
|
371
|
+
import { Lixa, type SessionStorage, type Session } from "@vunexa/lixa";
|
|
372
|
+
|
|
373
|
+
export class SqliteSessionStorage implements SessionStorage {
|
|
374
|
+
private db = new Database("lixa_sessions.db");
|
|
375
|
+
|
|
376
|
+
constructor() {
|
|
377
|
+
this.db.exec(`
|
|
378
|
+
CREATE TABLE IF NOT EXISTS sessions (
|
|
379
|
+
id TEXT PRIMARY KEY,
|
|
380
|
+
user_email TEXT,
|
|
381
|
+
data TEXT NOT NULL,
|
|
382
|
+
expires_at INTEGER NOT NULL
|
|
383
|
+
);
|
|
384
|
+
CREATE INDEX IF NOT EXISTS idx_sessions_email ON sessions(user_email);
|
|
385
|
+
`);
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
async saveSession<T extends Session>(sessionId: string, session: T, expiresInSeconds: number): Promise<void> {
|
|
389
|
+
const expiresAt = Math.floor(Date.now() / 1000) + expiresInSeconds;
|
|
390
|
+
const stmt = this.db.prepare(`
|
|
391
|
+
INSERT INTO sessions (id, user_email, data, expires_at)
|
|
392
|
+
VALUES (?, ?, ?, ?)
|
|
393
|
+
ON CONFLICT(id) DO UPDATE SET
|
|
394
|
+
user_email = excluded.user_email,
|
|
395
|
+
data = excluded.data,
|
|
396
|
+
expires_at = excluded.expires_at
|
|
397
|
+
`);
|
|
398
|
+
stmt.run(sessionId, session.email || null, JSON.stringify(session), expiresAt);
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
async getSession<T extends Session>(sessionId: string): Promise<T | null> {
|
|
402
|
+
const now = Math.floor(Date.now() / 1000);
|
|
403
|
+
const stmt = this.db.prepare(`SELECT data FROM sessions WHERE id = ? AND expires_at > ?`);
|
|
404
|
+
const row = stmt.get(sessionId, now) as { data: string } | undefined;
|
|
405
|
+
return row ? (JSON.parse(row.data) as T) : null;
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
async deleteSession(sessionId: string): Promise<void> {
|
|
409
|
+
const stmt = this.db.prepare(`DELETE FROM sessions WHERE id = ?`);
|
|
410
|
+
stmt.run(sessionId);
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
async getSessionByEmail<T extends Session>(email: string): Promise<{ sessionId: string; session: T } | null> {
|
|
414
|
+
const now = Math.floor(Date.now() / 1000);
|
|
415
|
+
const stmt = this.db.prepare(`SELECT id, data FROM sessions WHERE user_email = ? AND expires_at > ? LIMIT 1`);
|
|
416
|
+
const row = stmt.get(email, now) as { id: string; data: string } | undefined;
|
|
417
|
+
return row ? { sessionId: row.id, session: JSON.parse(row.data) as T } : null;
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
// Pass to Lixa instance
|
|
422
|
+
export const lixa = new Lixa({
|
|
423
|
+
sessionHandler: {
|
|
424
|
+
sessionStorage: new SqliteSessionStorage(),
|
|
425
|
+
},
|
|
426
|
+
providers: { /* ... */ },
|
|
427
|
+
});
|
|
428
|
+
```
|
|
429
|
+
|
|
430
|
+
#### Saved JSON Record Example in SQLite (`data` Column)
|
|
431
|
+
|
|
432
|
+
```json
|
|
433
|
+
{
|
|
434
|
+
"id": "e4a91f82c3b4a07f",
|
|
435
|
+
"token": "ya29.a0ARW5m7...",
|
|
436
|
+
"userId": "1049281048",
|
|
437
|
+
"email": "alex.developer@example.com",
|
|
438
|
+
"provider": "google",
|
|
439
|
+
"accounts": {
|
|
440
|
+
"google": {
|
|
441
|
+
"provider": "google",
|
|
442
|
+
"email": "alex.developer@example.com",
|
|
443
|
+
"providerUserId": "1049281048",
|
|
444
|
+
"accessToken": "ya29.a0ARW5m7...",
|
|
445
|
+
"linkedAt": 1771657200000
|
|
446
|
+
},
|
|
447
|
+
"github": {
|
|
448
|
+
"provider": "github",
|
|
449
|
+
"email": "alex.developer@example.com",
|
|
450
|
+
"providerUserId": "5829104",
|
|
451
|
+
"accessToken": "gho_8f7b2a9e1c3...",
|
|
452
|
+
"linkedAt": 1771657250000
|
|
453
|
+
}
|
|
454
|
+
},
|
|
455
|
+
"resources": {
|
|
456
|
+
"github": {
|
|
457
|
+
"provider": "github",
|
|
458
|
+
"accessToken": "gho_resource_repo_9a8b7c...",
|
|
459
|
+
"scopes": ["repo", "read:org"],
|
|
460
|
+
"connectedAt": 1771657300000
|
|
461
|
+
}
|
|
462
|
+
},
|
|
463
|
+
"raw": {
|
|
464
|
+
"access_token": "ya29.a0ARW5m7...",
|
|
465
|
+
"token_type": "Bearer",
|
|
466
|
+
"expires_in": 3599
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
```
|
|
470
|
+
|
|
471
|
+
---
|
|
472
|
+
|
|
473
|
+
### 2. AWS DynamoDB Session Storage (`@aws-sdk/lib-dynamodb`)
|
|
474
|
+
|
|
475
|
+
For serverless and distributed AWS deployments using DynamoDB:
|
|
476
|
+
|
|
477
|
+
#### Table Configuration
|
|
478
|
+
|
|
479
|
+
- **Table Name**: `LixaSessions`
|
|
480
|
+
- **Partition Key**: `sessionId` (String)
|
|
481
|
+
- **Global Secondary Index (GSI)**: `EmailIndex` (`email` as Partition Key)
|
|
482
|
+
- **TTL Attribute**: `ttl` (Unix timestamp in seconds for automatic AWS expiration)
|
|
483
|
+
|
|
484
|
+
#### TypeScript Implementation
|
|
485
|
+
|
|
486
|
+
```typescript
|
|
487
|
+
import { DynamoDBClient } from "@aws-sdk/client-dynamodb";
|
|
488
|
+
import { DynamoDBDocumentClient, PutCommand, GetCommand, DeleteCommand, QueryCommand } from "@aws-sdk/lib-dynamodb";
|
|
489
|
+
import { Lixa, type SessionStorage, type Session } from "@vunexa/lixa";
|
|
490
|
+
|
|
491
|
+
export class DynamoDbSessionStorage implements SessionStorage {
|
|
492
|
+
private docClient: DynamoDBDocumentClient;
|
|
493
|
+
private tableName = "LixaSessions";
|
|
494
|
+
|
|
495
|
+
constructor() {
|
|
496
|
+
const client = new DynamoDBClient({ region: process.env.AWS_REGION || "us-east-1" });
|
|
497
|
+
this.docClient = DynamoDBDocumentClient.from(client);
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
async saveSession<T extends Session>(sessionId: string, session: T, expiresInSeconds: number): Promise<void> {
|
|
501
|
+
const ttl = Math.floor(Date.now() / 1000) + expiresInSeconds;
|
|
502
|
+
await this.docClient.send(
|
|
503
|
+
new PutCommand({
|
|
504
|
+
TableName: this.tableName,
|
|
505
|
+
Item: {
|
|
506
|
+
sessionId,
|
|
507
|
+
email: session.email || "N/A",
|
|
508
|
+
sessionData: session,
|
|
509
|
+
ttl,
|
|
510
|
+
},
|
|
511
|
+
})
|
|
512
|
+
);
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
async getSession<T extends Session>(sessionId: string): Promise<T | null> {
|
|
516
|
+
const res = await this.docClient.send(
|
|
517
|
+
new GetCommand({
|
|
518
|
+
TableName: this.tableName,
|
|
519
|
+
Key: { sessionId },
|
|
520
|
+
})
|
|
521
|
+
);
|
|
522
|
+
|
|
523
|
+
if (!res.Item) return null;
|
|
524
|
+
const now = Math.floor(Date.now() / 1000);
|
|
525
|
+
if (res.Item.ttl && res.Item.ttl < now) return null;
|
|
526
|
+
|
|
527
|
+
return res.Item.sessionData as T;
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
async deleteSession(sessionId: string): Promise<void> {
|
|
531
|
+
await this.docClient.send(
|
|
532
|
+
new DeleteCommand({
|
|
533
|
+
TableName: this.tableName,
|
|
534
|
+
Key: { sessionId },
|
|
535
|
+
})
|
|
536
|
+
);
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
async getSessionByEmail<T extends Session>(email: string): Promise<{ sessionId: string; session: T } | null> {
|
|
540
|
+
const res = await this.docClient.send(
|
|
541
|
+
new QueryCommand({
|
|
542
|
+
TableName: this.tableName,
|
|
543
|
+
IndexName: "EmailIndex",
|
|
544
|
+
KeyConditionExpression: "email = :email",
|
|
545
|
+
ExpressionAttributeValues: { ":email": email },
|
|
546
|
+
Limit: 1,
|
|
547
|
+
})
|
|
548
|
+
);
|
|
549
|
+
|
|
550
|
+
if (!res.Items || res.Items.length === 0) return null;
|
|
551
|
+
const item = res.Items[0];
|
|
552
|
+
const now = Math.floor(Date.now() / 1000);
|
|
553
|
+
if (item.ttl && item.ttl < now) return null;
|
|
554
|
+
|
|
555
|
+
return { sessionId: item.sessionId, session: item.sessionData as T };
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
// Pass to Lixa instance
|
|
560
|
+
export const lixa = new Lixa({
|
|
561
|
+
sessionHandler: {
|
|
562
|
+
sessionStorage: new DynamoDbSessionStorage(),
|
|
563
|
+
},
|
|
564
|
+
providers: { /* ... */ },
|
|
565
|
+
});
|
|
566
|
+
```
|
|
567
|
+
|
|
568
|
+
#### Saved DynamoDB Item JSON Example
|
|
569
|
+
|
|
570
|
+
```json
|
|
571
|
+
{
|
|
572
|
+
"sessionId": "e4a91f82c3b4a07f",
|
|
573
|
+
"email": "alex.developer@example.com",
|
|
574
|
+
"ttl": 1771743600,
|
|
575
|
+
"sessionData": {
|
|
576
|
+
"id": "e4a91f82c3b4a07f",
|
|
577
|
+
"token": "ya29.a0ARW5m7...",
|
|
578
|
+
"userId": "1049281048",
|
|
579
|
+
"email": "alex.developer@example.com",
|
|
580
|
+
"provider": "google",
|
|
581
|
+
"accounts": {
|
|
582
|
+
"google": {
|
|
583
|
+
"provider": "google",
|
|
584
|
+
"email": "alex.developer@example.com",
|
|
585
|
+
"providerUserId": "1049281048",
|
|
586
|
+
"accessToken": "ya29.a0ARW5m7...",
|
|
587
|
+
"linkedAt": 1771657200000
|
|
588
|
+
},
|
|
589
|
+
"github": {
|
|
590
|
+
"provider": "github",
|
|
591
|
+
"email": "alex.developer@example.com",
|
|
592
|
+
"providerUserId": "5829104",
|
|
593
|
+
"accessToken": "gho_8f7b2a9e1c3...",
|
|
594
|
+
"linkedAt": 1771657250000
|
|
595
|
+
}
|
|
596
|
+
},
|
|
597
|
+
"resources": {
|
|
598
|
+
"github": {
|
|
599
|
+
"provider": "github",
|
|
600
|
+
"accessToken": "gho_resource_repo_9a8b7c...",
|
|
601
|
+
"scopes": ["repo", "read:org"],
|
|
602
|
+
"connectedAt": 1771657300000
|
|
603
|
+
}
|
|
604
|
+
},
|
|
605
|
+
"raw": {
|
|
606
|
+
"access_token": "ya29.a0ARW5m7...",
|
|
607
|
+
"token_type": "Bearer",
|
|
608
|
+
"expires_in": 3599
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
```
|
|
613
|
+
|
|
614
|
+
---
|
|
615
|
+
|
|
345
616
|
## User Info Utilities
|
|
346
617
|
|
|
347
618
|
Lixa provides utilities to extract user information from OAuth tokens:
|
|
@@ -740,6 +740,8 @@ export declare class Lixa<TConfig extends LixaConfig<Record<string, ProviderConf
|
|
|
740
740
|
provider: ConfiguredProviderKey<TConfig> | string;
|
|
741
741
|
scopes: string[];
|
|
742
742
|
state?: string;
|
|
743
|
+
prompt?: string;
|
|
744
|
+
extraConfig?: Record<string, string>;
|
|
743
745
|
}): Promise<string>;
|
|
744
746
|
/**
|
|
745
747
|
* Handles the OAuth callback for a connected resource provider and stores resource tokens on the session.
|
package/dist/index.cjs
CHANGED
|
@@ -869,7 +869,7 @@ var Lixa = class _Lixa {
|
|
|
869
869
|
* @returns The authorization URL for resource consent
|
|
870
870
|
*/
|
|
871
871
|
async getResourceAuthUrl(params) {
|
|
872
|
-
const { sessionId, provider, scopes, state } = params;
|
|
872
|
+
const { sessionId, provider, scopes, state, prompt, extraConfig } = params;
|
|
873
873
|
const sessionStorage = this.sessionHandler.sessionStorage || _Lixa.LOCAL_SESSION_HANDLER.sessionStorage;
|
|
874
874
|
const activeSession = await sessionStorage.getSession(sessionId);
|
|
875
875
|
if (!activeSession) {
|
|
@@ -902,7 +902,9 @@ var Lixa = class _Lixa {
|
|
|
902
902
|
response_type: "code",
|
|
903
903
|
code_challenge: codeChallenge,
|
|
904
904
|
code_challenge_method: "S256",
|
|
905
|
-
...
|
|
905
|
+
...prompt ? { prompt } : {},
|
|
906
|
+
...providerConfig.extraConfig,
|
|
907
|
+
...extraConfig
|
|
906
908
|
});
|
|
907
909
|
return `${providerImpl.authorizationEndpoint}?${searchParams.toString()}`;
|
|
908
910
|
}
|
|
@@ -934,7 +936,7 @@ var Lixa = class _Lixa {
|
|
|
934
936
|
await stateStorage.deleteState(state);
|
|
935
937
|
}
|
|
936
938
|
}
|
|
937
|
-
const tokens = await this.exchangeCodeForToken(
|
|
939
|
+
const tokens = await this.exchangeCodeForToken(code, providerConfig, providerImpl, codeVerifier);
|
|
938
940
|
activeSession.resources = activeSession.resources || {};
|
|
939
941
|
activeSession.resources[providerType] = {
|
|
940
942
|
provider: providerType,
|