@dainprotocol/oauth2-token-manager 0.1.0

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 ADDED
@@ -0,0 +1,786 @@
1
+ # OAuth2 Token Manager
2
+
3
+ A powerful, storage-agnostic OAuth2 token management library built for scalable multi-system architectures. This library provides comprehensive token lifecycle management with pluggable storage adapters, built-in security features, and support for multiple OAuth2 providers.
4
+
5
+ ## ๐Ÿš€ Features
6
+
7
+ - **๐Ÿ”Œ Storage Agnostic**: Use any storage backend (In-Memory, PostgreSQL, or build your own adapter)
8
+ - **๐Ÿข Multi-System Support**: Manage tokens across multiple applications/systems
9
+ - **๐Ÿ” Advanced Security**: PKCE support, state validation, token encryption
10
+ - **โšก High Performance**: Efficient token validation, caching, and refresh strategies
11
+ - **๐Ÿ”„ Auto-Refresh**: Automatic token refresh with configurable buffers
12
+ - **๐Ÿ‘ค User Management**: Comprehensive user lifecycle with email/external ID support
13
+ - **๐Ÿ“ง Profile Integration**: Automatic profile fetching from OAuth providers
14
+ - **๐ŸŽฏ Flexible Scoping**: Fine-grained permission management
15
+ - **๐Ÿ’ก Developer Friendly**: Both context-managed and granular APIs
16
+ - **๐Ÿงช Fully Tested**: Comprehensive test coverage with Vitest
17
+
18
+ ## ๐Ÿ“ฆ Installation
19
+
20
+ ```bash
21
+ npm install @dainprotocol/oauth2-token-manager
22
+ ```
23
+
24
+ ### Storage Adapters
25
+
26
+ ```bash
27
+ # PostgreSQL adapter
28
+ npm install @dainprotocol/oauth2-storage-postgres
29
+ ```
30
+
31
+ ## ๐Ÿš€ Quick Start
32
+
33
+ ### Simple Setup
34
+
35
+ ```typescript
36
+ import { OAuth2Client } from '@dainprotocol/oauth2-token-manager';
37
+
38
+ // Quick setup for common use cases
39
+ const oauth = await OAuth2Client.quickSetup('MyApp', {
40
+ google: {
41
+ clientId: 'your-google-client-id',
42
+ clientSecret: 'your-google-client-secret',
43
+ authorizationUrl: 'https://accounts.google.com/o/oauth2/auth',
44
+ tokenUrl: 'https://oauth2.googleapis.com/token',
45
+ redirectUri: 'http://localhost:3000/auth/callback',
46
+ scopes: ['profile', 'email'],
47
+ },
48
+ github: {
49
+ clientId: 'your-github-client-id',
50
+ clientSecret: 'your-github-client-secret',
51
+ authorizationUrl: 'https://github.com/login/oauth/authorize',
52
+ tokenUrl: 'https://github.com/login/oauth/access_token',
53
+ redirectUri: 'http://localhost:3000/auth/callback',
54
+ scopes: ['user:email'],
55
+ },
56
+ });
57
+
58
+ // Create or get a user
59
+ const user = await oauth.getOrCreateUser({
60
+ email: 'user@example.com',
61
+ metadata: { role: 'user' },
62
+ });
63
+
64
+ // Start OAuth flow
65
+ const { url, state } = await oauth.authorize({
66
+ provider: 'google',
67
+ scopes: ['profile', 'email'],
68
+ });
69
+
70
+ // Handle callback
71
+ const result = await oauth.handleCallback(code, state);
72
+ console.log('User authenticated:', result.userId);
73
+ ```
74
+
75
+ ### Advanced Setup with Custom Storage
76
+
77
+ ```typescript
78
+ import { OAuth2Client } from '@dainprotocol/oauth2-token-manager';
79
+ import { PostgresStorageFactory } from '@dainprotocol/oauth2-storage-postgres';
80
+
81
+ // Custom storage adapter
82
+ const storage = await PostgresStorageFactory.create({
83
+ host: 'localhost',
84
+ port: 5432,
85
+ username: 'oauth2_user',
86
+ password: 'secure_password',
87
+ database: 'oauth2_db',
88
+ });
89
+
90
+ const oauth = new OAuth2Client({
91
+ storage,
92
+ providers: {
93
+ google: {
94
+ /* config */
95
+ },
96
+ github: {
97
+ /* config */
98
+ },
99
+ },
100
+ });
101
+
102
+ // Create system and scopes
103
+ const system = await oauth.createSystem('MyApp');
104
+ const scope = await oauth.createScope('api-access', {
105
+ type: 'access',
106
+ permissions: ['read:profile', 'write:data'],
107
+ isolated: true,
108
+ });
109
+ ```
110
+
111
+ ## ๐Ÿ—๏ธ Architecture
112
+
113
+ ### Core Components
114
+
115
+ ```
116
+ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
117
+ โ”‚ OAuth2Client โ”‚
118
+ โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚
119
+ โ”‚ โ”‚ Context API โ”‚ โ”‚ Granular API โ”‚ โ”‚
120
+ โ”‚ โ”‚ (Simplified) โ”‚ โ”‚ (Full Control) โ”‚ โ”‚
121
+ โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚
122
+ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
123
+ โ”‚
124
+ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
125
+ โ”‚ โ”‚ โ”‚
126
+ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ–ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ–ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ–ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
127
+ โ”‚ Providers โ”‚ โ”‚ Storage โ”‚ โ”‚ Profile โ”‚
128
+ โ”‚ (OAuth2) โ”‚ โ”‚ Adapter โ”‚ โ”‚ Fetchers โ”‚
129
+ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
130
+ ```
131
+
132
+ ### Data Model & Token Hierarchy
133
+
134
+ **Important**: Users can have multiple tokens for the same provider within the same scope. This allows for scenarios like different email accounts or token refresh cycles.
135
+
136
+ ```typescript
137
+ // Systems: Top-level applications/services
138
+ interface System {
139
+ id: string;
140
+ name: string;
141
+ description?: string;
142
+ scopes: Scope[];
143
+ metadata?: Record<string, any>;
144
+ }
145
+
146
+ // Scopes: Permission boundaries within systems
147
+ interface Scope {
148
+ id: string;
149
+ systemId: string;
150
+ name: string;
151
+ type: 'authentication' | 'access' | 'custom';
152
+ permissions: string[];
153
+ isolated: boolean; // Whether tokens are isolated to this scope
154
+ }
155
+
156
+ // Users: Identity within a system
157
+ interface User {
158
+ id: string;
159
+ systemId: string;
160
+ metadata?: Record<string, any>;
161
+ }
162
+
163
+ // User Tokens: OAuth2 tokens tied to user/system/scope/provider
164
+ // A user can have MULTIPLE tokens for the same provider/scope combination
165
+ interface UserToken {
166
+ id: string;
167
+ userId: string;
168
+ systemId: string;
169
+ scopeId: string;
170
+ provider: string;
171
+ token: OAuth2Token;
172
+ }
173
+ ```
174
+
175
+ ### Token Hierarchy Rules
176
+
177
+ 1. **One User** belongs to **One System**
178
+ 2. **One User** can have tokens in **Multiple Scopes** within their system
179
+ 3. **One User** in **One Scope** can have tokens from **Multiple Providers**
180
+ 4. **One User** in **One Scope** from **One Provider** can have **Multiple Tokens**
181
+ 5. **Email Uniqueness**: For the same provider, a user cannot have multiple tokens with the same email (validated via profile fetcher)
182
+ 6. **Cross-Provider Emails**: The same email can exist across different providers
183
+
184
+ ## ๐Ÿ“š API Reference
185
+
186
+ ### OAuth2Client
187
+
188
+ The main client class providing both context-managed and granular APIs.
189
+
190
+ #### Context-Managed API (Recommended)
191
+
192
+ ```typescript
193
+ // System management
194
+ await oauth.createSystem('MyApp');
195
+ await oauth.useSystem(systemId);
196
+
197
+ // User management
198
+ const user = await oauth.getOrCreateUser({ email: 'user@example.com' });
199
+ await oauth.useUser(userId);
200
+
201
+ // Authorization flow
202
+ const { url, state } = await oauth.authorize({ provider: 'google' });
203
+ const result = await oauth.handleCallback(code, state);
204
+
205
+ // Token operations (uses current context + default scope)
206
+ // Note: When multiple tokens exist, these methods use the first (most recent) token
207
+ const accessToken = await oauth.getAccessToken('google');
208
+ const validToken = await oauth.ensureValidToken('google');
209
+
210
+ // Get all user tokens with auto-refresh (for current user)
211
+ const userTokens = await oauth.getUserTokens();
212
+
213
+ // Get all valid tokens for a specific user
214
+ const allTokens = await oauth.getAllValidTokensForUser(userId);
215
+ // Returns: { provider: string; scopeId: string; token: OAuth2Token; userToken: UserToken }[]
216
+
217
+ // Revoke tokens (uses current context)
218
+ await oauth.revokeTokens('google'); // Revokes for current user/scope/provider
219
+ ```
220
+
221
+ #### Granular API (Advanced)
222
+
223
+ The granular API provides full control over the token hierarchy:
224
+
225
+ ```typescript
226
+ // === User-Centric Token Queries (Primary Key: User) ===
227
+
228
+ // Get ALL tokens for a user across all scopes/providers
229
+ const userTokens = await oauth.granular.getTokensByUser(userId);
230
+
231
+ // Get tokens for user in specific scope (across all providers)
232
+ const scopeTokens = await oauth.granular.getTokensByUserAndScope(userId, scopeId);
233
+
234
+ // Get tokens for user with specific provider (across all scopes)
235
+ const providerTokens = await oauth.granular.getTokensByUserAndProvider(userId, 'google');
236
+
237
+ // Get tokens for user/scope/provider combination (can be multiple!)
238
+ const specificTokens = await oauth.granular.getTokensByUserScopeProvider(userId, scopeId, 'google');
239
+
240
+ // === Cross-User Queries (System/Scope Level) ===
241
+
242
+ // Get all tokens in a scope across all users
243
+ const scopeAllTokens = await oauth.granular.getTokensByScope(systemId, scopeId);
244
+
245
+ // Get all tokens for a provider across all users in system
246
+ const providerAllTokens = await oauth.granular.getTokensByProvider(systemId, 'google');
247
+
248
+ // Get all tokens in a system
249
+ const systemTokens = await oauth.granular.getTokensBySystem(systemId);
250
+
251
+ // === Email-Based Queries ===
252
+
253
+ // Find tokens by email (cross-user, cross-provider)
254
+ const emailTokens = await oauth.granular.findTokensByEmail('user@example.com', systemId);
255
+
256
+ // Find tokens by email in specific scope
257
+ const emailScopeTokens = await oauth.granular.findTokensByEmailAndScope(
258
+ 'user@example.com',
259
+ systemId,
260
+ scopeId,
261
+ );
262
+
263
+ // Find tokens by email for specific provider
264
+ const emailProviderTokens = await oauth.granular.findTokensByEmailAndProvider(
265
+ 'user@example.com',
266
+ systemId,
267
+ 'google',
268
+ );
269
+
270
+ // Find specific token by email/scope/provider (returns single token or null)
271
+ const specificToken = await oauth.granular.findTokenByEmailScopeProvider(
272
+ 'user@example.com',
273
+ systemId,
274
+ scopeId,
275
+ 'google',
276
+ );
277
+
278
+ // === Token Operations ===
279
+
280
+ // Get valid token for user (auto-refresh, takes first if multiple exist)
281
+ const validToken = await oauth.granular.getValidTokenForUser(userId, scopeId, 'google');
282
+
283
+ // Get access token for user (convenience method)
284
+ const accessToken = await oauth.granular.getAccessTokenForUser(userId, scopeId, 'google');
285
+
286
+ // Save new token for user
287
+ const savedToken = await oauth.granular.saveTokenForUser(
288
+ userId,
289
+ systemId,
290
+ scopeId,
291
+ 'google',
292
+ 'user@example.com',
293
+ oauthToken,
294
+ );
295
+
296
+ // === Token Management ===
297
+
298
+ // Delete tokens by different criteria
299
+ await oauth.granular.deleteTokensByUser(userId); // All tokens for user
300
+ await oauth.granular.deleteTokensByUserAndScope(userId, scopeId); // User's tokens in scope
301
+ await oauth.granular.deleteTokensByUserAndProvider(userId, 'google'); // User's tokens for provider
302
+ ```
303
+
304
+ ### Storage Adapters
305
+
306
+ #### Built-in Memory Adapter
307
+
308
+ ```typescript
309
+ import { InMemoryStorageAdapter } from '@dainprotocol/oauth2-token-manager';
310
+
311
+ const storage = new InMemoryStorageAdapter();
312
+ const oauth = new OAuth2Client({ storage });
313
+ ```
314
+
315
+ #### PostgreSQL Adapter
316
+
317
+ ```typescript
318
+ import { PostgresStorageFactory } from '@dainprotocol/oauth2-storage-postgres';
319
+
320
+ const storage = await PostgresStorageFactory.create({
321
+ host: process.env.DB_HOST,
322
+ port: parseInt(process.env.DB_PORT || '5432'),
323
+ username: process.env.DB_USER,
324
+ password: process.env.DB_PASSWORD,
325
+ database: process.env.DB_NAME,
326
+ ssl: process.env.NODE_ENV === 'production',
327
+ });
328
+ ```
329
+
330
+ #### Custom Storage Adapter
331
+
332
+ ```typescript
333
+ import { StorageAdapter } from '@dainprotocol/oauth2-token-manager';
334
+
335
+ class MyCustomAdapter implements StorageAdapter {
336
+ async createSystem(system) {
337
+ /* implement */
338
+ }
339
+ async getSystem(id) {
340
+ /* implement */
341
+ }
342
+ // ... implement all required methods
343
+ }
344
+ ```
345
+
346
+ ### Provider Configuration
347
+
348
+ #### Google OAuth2
349
+
350
+ ```typescript
351
+ {
352
+ google: {
353
+ clientId: 'your-client-id',
354
+ clientSecret: 'your-client-secret',
355
+ authorizationUrl: 'https://accounts.google.com/o/oauth2/auth',
356
+ tokenUrl: 'https://oauth2.googleapis.com/token',
357
+ redirectUri: 'http://localhost:3000/auth/callback',
358
+ scopes: ['profile', 'email'],
359
+ profileUrl: 'https://www.googleapis.com/oauth2/v2/userinfo',
360
+ usePKCE: true, // Recommended for security
361
+ }
362
+ }
363
+ ```
364
+
365
+ #### GitHub OAuth2
366
+
367
+ ```typescript
368
+ {
369
+ github: {
370
+ clientId: 'your-client-id',
371
+ clientSecret: 'your-client-secret',
372
+ authorizationUrl: 'https://github.com/login/oauth/authorize',
373
+ tokenUrl: 'https://github.com/login/oauth/access_token',
374
+ redirectUri: 'http://localhost:3000/auth/callback',
375
+ scopes: ['user:email'],
376
+ profileUrl: 'https://api.github.com/user',
377
+ }
378
+ }
379
+ ```
380
+
381
+ #### Generic Provider
382
+
383
+ ```typescript
384
+ {
385
+ custom: {
386
+ clientId: 'your-client-id',
387
+ clientSecret: 'your-client-secret',
388
+ authorizationUrl: 'https://provider.com/oauth/authorize',
389
+ tokenUrl: 'https://provider.com/oauth/token',
390
+ redirectUri: 'http://localhost:3000/auth/callback',
391
+ scopes: ['read', 'write'],
392
+ profileUrl: 'https://provider.com/api/user',
393
+ additionalParams: {
394
+ audience: 'api.example.com'
395
+ },
396
+ responseRootKey: 'data' // For nested responses
397
+ }
398
+ }
399
+ ```
400
+
401
+ #### Google OAuth2 with Offline Access
402
+
403
+ ```typescript
404
+ const oauth = new OAuth2Client({
405
+ providers: {
406
+ google: {
407
+ clientId: 'your-client-id',
408
+ clientSecret: 'your-client-secret',
409
+ redirectUri: 'http://localhost:3000/auth/callback',
410
+ scopes: ['profile', 'email'],
411
+ // Override default offline access parameters
412
+ extraAuthParams: {
413
+ access_type: 'offline', // Request refresh token
414
+ prompt: 'consent', // Force consent screen
415
+ include_granted_scopes: 'true' // Include previously granted scopes
416
+ }
417
+ }
418
+ }
419
+ });
420
+
421
+ // The library automatically handles refresh tokens
422
+ const token = await oauth.getAccessToken('google', {
423
+ autoRefresh: true,
424
+ refreshBuffer: 5 // Refresh 5 minutes before expiry
425
+ });
426
+ ```
427
+
428
+ #### Customizing Authorization Parameters
429
+
430
+ Each provider supports customization through `extraAuthParams` and `additionalParams`:
431
+
432
+ ```typescript
433
+ {
434
+ google: {
435
+ // ... other config ...
436
+ extraAuthParams: {
437
+ access_type: 'offline', // For refresh tokens
438
+ prompt: 'select_account', // Force account selection
439
+ hd: 'yourdomain.com' // Limit to specific Google Workspace domain
440
+ }
441
+ },
442
+ microsoft: {
443
+ // ... other config ...
444
+ extraAuthParams: {
445
+ prompt: 'select_account',
446
+ domain_hint: 'yourdomain.com'
447
+ }
448
+ }
449
+ }
450
+ ```
451
+
452
+ Available parameters for Google OAuth2:
453
+ - `access_type`: 'online' (default) or 'offline' (for refresh tokens)
454
+ - `prompt`: 'none', 'consent', 'select_account'
455
+ - `include_granted_scopes`: 'true' or 'false'
456
+ - `login_hint`: User's email address
457
+ - `hd`: Google Workspace domain restriction
458
+
459
+ ## ๐Ÿ”ง Advanced Features
460
+
461
+ ### Token Auto-Refresh
462
+
463
+ ```typescript
464
+ const accessToken = await oauth.getAccessToken('google', {
465
+ autoRefresh: true,
466
+ refreshBuffer: 5, // Refresh 5 minutes before expiry
467
+ expirationBuffer: 30, // Consider expired 30 seconds early
468
+ });
469
+ ```
470
+
471
+ ### Profile-Based Token Management
472
+
473
+ ```typescript
474
+ const result = await oauth.handleCallback(code, state, {
475
+ profileOptions: {
476
+ checkProfileEmail: true, // Fetch and check email conflicts
477
+ replaceConflictingTokens: true, // Replace existing tokens with same email
478
+ mergeUserData: true, // Merge profile data into user metadata
479
+ },
480
+ });
481
+ ```
482
+
483
+ ### Email-Based Operations
484
+
485
+ ```typescript
486
+ // Get all valid tokens for an email across all providers in a system
487
+ // Note: This returns an array since one email can have tokens from multiple providers
488
+ const emailTokens = await oauth.getAllValidTokensForEmail('user@example.com', systemId);
489
+ // Returns: { provider: string; scopeId: string; token: OAuth2Token; userToken: UserToken }[]
490
+
491
+ // Get specific token by email (returns single token or null)
492
+ // This enforces the email uniqueness rule within provider/scope
493
+ const token = await oauth.getTokenForEmail('user@example.com', systemId, scopeId, 'google');
494
+
495
+ // Get valid token for email with auto-refresh
496
+ const validToken = await oauth.getValidTokenForEmail(
497
+ 'user@example.com',
498
+ systemId,
499
+ scopeId,
500
+ 'google',
501
+ { autoRefresh: true },
502
+ );
503
+
504
+ // Get access token for email
505
+ const accessToken = await oauth.getAccessTokenForEmail(
506
+ 'user@example.com',
507
+ systemId,
508
+ scopeId,
509
+ 'google',
510
+ );
511
+
512
+ // Execute with valid token for email
513
+ await oauth.withValidTokenForEmail(
514
+ 'user@example.com',
515
+ systemId,
516
+ scopeId,
517
+ 'google',
518
+ async (accessToken) => {
519
+ console.log('Using token for email:', accessToken);
520
+ },
521
+ );
522
+
523
+ // Check if email has token for specific provider/scope
524
+ const hasToken = await oauth.hasTokenForEmail('user@example.com', systemId, scopeId, 'google');
525
+
526
+ // Revoke tokens for email
527
+ await oauth.revokeTokensForEmail('user@example.com', systemId, scopeId, 'google');
528
+ ```
529
+
530
+ ### User-Centric Operations (Stateless)
531
+
532
+ For backend APIs where you have explicit user IDs:
533
+
534
+ ```typescript
535
+ // Get access token for specific user/scope/provider
536
+ // Note: Takes the first (most recent) token if multiple exist
537
+ const accessToken = await oauth.getAccessTokenForUser(userId, systemId, scopeId, 'google', {
538
+ autoRefresh: true,
539
+ });
540
+
541
+ // Execute with valid token for specific user
542
+ await oauth.withValidTokenForUser(userId, systemId, scopeId, 'google', async (accessToken) => {
543
+ // Make API calls with the token
544
+ return apiResponse;
545
+ });
546
+
547
+ // Get all valid tokens for a user with auto-refresh
548
+ const userTokens = await oauth.getAllValidTokensForUser(userId, {
549
+ autoRefresh: true,
550
+ refreshBuffer: 5, // Refresh 5 minutes before expiry
551
+ });
552
+
553
+ // Check if user has tokens for specific provider/scope
554
+ const hasToken = await oauth.hasTokenForUser(userId, systemId, scopeId, 'google');
555
+
556
+ // Get user token entity (includes metadata)
557
+ const userToken = await oauth.getUserTokenForUser(userId, systemId, scopeId, 'google');
558
+
559
+ // Revoke specific tokens
560
+ await oauth.revokeTokensForUser(userId, systemId, scopeId, 'google');
561
+ ```
562
+
563
+ ### PKCE Support
564
+
565
+ ```typescript
566
+ // Enable PKCE for enhanced security
567
+ const { url, state } = await oauth.authorize({
568
+ provider: 'google',
569
+ usePKCE: true, // Enables PKCE flow
570
+ });
571
+ ```
572
+
573
+ ### Token Validation
574
+
575
+ ```typescript
576
+ // Check if token is expired
577
+ const isExpired = oauth.isTokenExpired(token, {
578
+ expirationBuffer: 60, // Consider expired 60 seconds early
579
+ });
580
+
581
+ // Ensure valid token (auto-refresh if needed)
582
+ const validToken = await oauth.ensureValidToken('google');
583
+ ```
584
+
585
+ ## ๐Ÿ”’ Security Features
586
+
587
+ ### State Management
588
+
589
+ - Cryptographically secure state generation
590
+ - Automatic state validation and cleanup
591
+ - Configurable state expiration
592
+
593
+ ### PKCE (Proof Key for Code Exchange)
594
+
595
+ - Built-in PKCE support for public clients
596
+ - Automatic code verifier generation
597
+ - Enhanced security for mobile and SPA applications
598
+
599
+ ### Token Encryption
600
+
601
+ - Secure token storage with optional encryption
602
+ - Configurable seal keys for sensitive data
603
+ - Protection against token theft
604
+
605
+ ### Email Validation
606
+
607
+ - Automatic email conflict detection
608
+ - Profile-based user validation
609
+ - Cross-provider email consistency
610
+
611
+ ## ๐Ÿงช Testing
612
+
613
+ The library includes comprehensive tests using Vitest:
614
+
615
+ ```bash
616
+ # Run tests
617
+ npm test
618
+
619
+ # Run tests with UI
620
+ npm run test:ui
621
+
622
+ # Run tests with coverage
623
+ npm run test:coverage
624
+
625
+ # Watch mode
626
+ npm run test:watch
627
+ ```
628
+
629
+ ## ๐Ÿข Multi-System Examples
630
+
631
+ ### SaaS Platform with Multiple Apps
632
+
633
+ ```typescript
634
+ // Create systems for different applications
635
+ const crmSystem = await oauth.createSystem('CRM App');
636
+ const analyticsSystem = await oauth.createSystem('Analytics Dashboard');
637
+
638
+ // Create scopes for different access levels
639
+ await oauth.useSystem(crmSystem.id);
640
+ const readScope = await oauth.createScope('read-only', {
641
+ type: 'access',
642
+ permissions: ['read:contacts', 'read:deals'],
643
+ isolated: true,
644
+ });
645
+
646
+ const adminScope = await oauth.createScope('admin', {
647
+ type: 'access',
648
+ permissions: ['*'],
649
+ isolated: true,
650
+ });
651
+
652
+ // Users can have different permissions per system
653
+ const user = await oauth.getOrCreateUser({ email: 'user@company.com' });
654
+
655
+ // Authorize for specific system/scope
656
+ const { url } = await oauth.authorize({
657
+ provider: 'google',
658
+ scopes: ['profile', 'email'],
659
+ });
660
+ ```
661
+
662
+ ### Multi-Tenant Application
663
+
664
+ ```typescript
665
+ // Each tenant gets their own system
666
+ const tenantSystem = await oauth.createSystem(`Tenant-${tenantId}`);
667
+
668
+ // Tenant-specific user management
669
+ await oauth.useSystem(tenantSystem.id);
670
+ const tenantUser = await oauth.getOrCreateUser({
671
+ email: userEmail,
672
+ metadata: { tenantId, role: 'admin' },
673
+ });
674
+
675
+ // Tenant-isolated tokens
676
+ const tokens = await oauth.granular.getTokensBySystem(tenantSystem.id);
677
+ ```
678
+
679
+ ## ๐Ÿš€ Production Deployment
680
+
681
+ ### Environment Configuration
682
+
683
+ ```typescript
684
+ const oauth = new OAuth2Client({
685
+ storage: await PostgresStorageFactory.create({
686
+ host: process.env.DB_HOST,
687
+ port: parseInt(process.env.DB_PORT || '5432'),
688
+ username: process.env.DB_USER,
689
+ password: process.env.DB_PASSWORD,
690
+ database: process.env.DB_NAME,
691
+ ssl: {
692
+ rejectUnauthorized: process.env.NODE_ENV === 'production',
693
+ },
694
+ poolSize: 20,
695
+ logging: process.env.NODE_ENV === 'development',
696
+ }),
697
+ sealKey: process.env.OAUTH2_SEAL_KEY, // For token encryption
698
+ providers: {
699
+ google: {
700
+ clientId: process.env.GOOGLE_CLIENT_ID,
701
+ clientSecret: process.env.GOOGLE_CLIENT_SECRET,
702
+ redirectUri: process.env.GOOGLE_REDIRECT_URI,
703
+ // ... other config
704
+ },
705
+ },
706
+ });
707
+ ```
708
+
709
+ ### Performance Optimization
710
+
711
+ ```typescript
712
+ // Use token caching for high-traffic scenarios
713
+ const accessToken = await oauth.getAccessToken('google', {
714
+ autoRefresh: true,
715
+ refreshBuffer: 10, // Refresh early to avoid expiry
716
+ });
717
+
718
+ // Batch operations for efficiency
719
+ const allTokens = await oauth.getAllValidTokensForUser(userId);
720
+
721
+ // Clean up expired states regularly
722
+ setInterval(
723
+ async () => {
724
+ await oauth.cleanup(10 * 60 * 1000); // 10 minutes
725
+ },
726
+ 5 * 60 * 1000,
727
+ ); // Every 5 minutes
728
+ ```
729
+
730
+ ### Error Handling
731
+
732
+ ```typescript
733
+ try {
734
+ const token = await oauth.getAccessToken('google');
735
+ } catch (error) {
736
+ if (error.message.includes('Token expired')) {
737
+ // Handle token expiry
738
+ const { url } = await oauth.authorize({ provider: 'google' });
739
+ // Redirect to re-authorization
740
+ } else if (error.message.includes('Provider not found')) {
741
+ // Handle missing provider
742
+ }
743
+ }
744
+ ```
745
+
746
+ ## ๐Ÿค Contributing
747
+
748
+ 1. Fork the repository
749
+ 2. Create your feature branch (`git checkout -b feature/amazing-feature`)
750
+ 3. Commit your changes (`git commit -m 'Add some amazing feature'`)
751
+ 4. Push to the branch (`git push origin feature/amazing-feature`)
752
+ 5. Open a Pull Request
753
+
754
+ ### Development Setup
755
+
756
+ ```bash
757
+ # Install dependencies
758
+ npm install
759
+
760
+ # Run in development mode
761
+ npm run dev
762
+
763
+ # Run tests
764
+ npm test
765
+
766
+ # Build the project
767
+ npm run build
768
+
769
+ # Lint and format
770
+ npm run lint:fix
771
+ npm run format
772
+ ```
773
+
774
+ ## ๐Ÿ“„ License
775
+
776
+ This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
777
+
778
+ ## ๐Ÿ™‹โ€โ™‚๏ธ Support
779
+
780
+ - ๐Ÿ“š [Documentation](https://github.com/blureffect/oauth2-token-manager#readme)
781
+ - ๐Ÿ› [Issue Tracker](https://github.com/blureffect/oauth2-token-manager/issues)
782
+ - ๐Ÿ’ฌ [Discussions](https://github.com/blureffect/oauth2-token-manager/discussions)
783
+
784
+ ## ๐Ÿ† Credits
785
+
786
+ Created with โค๏ธ by [Blureffect](https://blureffect.co)