@dainprotocol/oauth2-token-manager 0.1.0 โ†’ 0.1.2

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 CHANGED
@@ -1,19 +1,25 @@
1
1
  # OAuth2 Token Manager
2
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.
3
+ A simple OAuth2 token management library for Node.js. Store and manage OAuth2 tokens with automatic refresh, multiple providers, and pluggable storage.
4
+
5
+ ## ๐Ÿ“‹ Table of Contents
6
+
7
+ - [Features](#-features)
8
+ - [Installation](#-installation)
9
+ - [Quick Start](#-quick-start)
10
+ - [Core Concepts](#-core-concepts)
11
+ - [API Reference](#-api-reference)
12
+ - [Storage Adapters](#-storage-adapters)
13
+ - [Examples](#-examples)
4
14
 
5
15
  ## ๐Ÿš€ Features
6
16
 
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
+ - **Simple Storage**: Tokens stored by provider + email (unique constraint)
18
+ - **Auto Refresh**: Tokens refresh automatically when expired
19
+ - **Multiple Providers**: Google, GitHub, Microsoft, Facebook, and custom providers
20
+ - **Profile Fetching**: Get user profiles during OAuth callback
21
+ - **Pluggable Storage**: In-memory, PostgreSQL, Drizzle, or custom adapters
22
+ - **Type Safe**: Full TypeScript support
17
23
 
18
24
  ## ๐Ÿ“ฆ Installation
19
25
 
@@ -24,763 +30,367 @@ npm install @dainprotocol/oauth2-token-manager
24
30
  ### Storage Adapters
25
31
 
26
32
  ```bash
27
- # PostgreSQL adapter
33
+ # PostgreSQL adapter (TypeORM based)
28
34
  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
35
 
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);
36
+ # Drizzle adapter (supports PostgreSQL, MySQL, SQLite)
37
+ npm install @dainprotocol/oauth2-storage-drizzle
73
38
  ```
74
39
 
75
- ### Advanced Setup with Custom Storage
40
+ ## ๐Ÿš€ Quick Start
76
41
 
77
42
  ```typescript
78
43
  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
44
 
45
+ // Initialize with provider configurations
90
46
  const oauth = new OAuth2Client({
91
- storage,
92
47
  providers: {
93
48
  google: {
94
- /* config */
49
+ clientId: process.env.GOOGLE_CLIENT_ID,
50
+ clientSecret: process.env.GOOGLE_CLIENT_SECRET,
51
+ authorizationUrl: 'https://accounts.google.com/o/oauth2/v2/auth',
52
+ tokenUrl: 'https://oauth2.googleapis.com/token',
53
+ redirectUri: 'http://localhost:3000/auth/google/callback',
54
+ scopes: ['profile', 'email'],
55
+ profileUrl: 'https://www.googleapis.com/oauth2/v2/userinfo',
95
56
  },
96
57
  github: {
97
- /* config */
58
+ clientId: process.env.GITHUB_CLIENT_ID,
59
+ clientSecret: process.env.GITHUB_CLIENT_SECRET,
60
+ authorizationUrl: 'https://github.com/login/oauth/authorize',
61
+ tokenUrl: 'https://github.com/login/oauth/access_token',
62
+ redirectUri: 'http://localhost:3000/auth/github/callback',
63
+ scopes: ['user:email'],
64
+ profileUrl: 'https://api.github.com/user',
98
65
  },
99
66
  },
100
67
  });
101
68
 
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,
69
+ // Start OAuth flow
70
+ const { url, state } = await oauth.authorize({
71
+ provider: 'google',
72
+ userId: 'user123',
73
+ email: 'user@example.com',
74
+ scopes: ['profile', 'email'],
108
75
  });
109
- ```
110
76
 
111
- ## ๐Ÿ—๏ธ Architecture
77
+ // Redirect user to authorization URL
78
+ res.redirect(url);
112
79
 
113
- ### Core Components
80
+ // Handle OAuth callback
81
+ const { token, profile } = await oauth.handleCallback(code, state);
114
82
 
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
- โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
83
+ // Use the token
84
+ const accessToken = await oauth.getAccessToken('google', 'user@example.com');
130
85
  ```
131
86
 
132
- ### Data Model & Token Hierarchy
87
+ ## ๐Ÿ”‘ Core Concepts
133
88
 
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.
89
+ ### How It Works
135
90
 
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
91
+ 1. **One token per provider/email**: Each email can have one token per OAuth provider
92
+ 2. **Automatic override**: Saving a token with same provider + email replaces the old one
93
+ 3. **Auto refresh**: Expired tokens refresh automatically when you request them
94
+ 4. **Simple storage**: Just provider (string), userId (string), and email (string)
183
95
 
184
96
  ## ๐Ÿ“š API Reference
185
97
 
186
98
  ### OAuth2Client
187
99
 
188
- The main client class providing both context-managed and granular APIs.
189
-
190
- #### Context-Managed API (Recommended)
100
+ The main class for managing OAuth2 tokens.
191
101
 
192
102
  ```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);
103
+ const oauth = new OAuth2Client({
104
+ storage?: StorageAdapter, // Optional, defaults to in-memory
105
+ providers?: { // OAuth provider configurations
106
+ [name: string]: OAuth2Config
107
+ }
108
+ });
109
+ ```
200
110
 
201
- // Authorization flow
202
- const { url, state } = await oauth.authorize({ provider: 'google' });
203
- const result = await oauth.handleCallback(code, state);
111
+ ### Methods
204
112
 
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');
113
+ #### ๐Ÿ” OAuth Flow
209
114
 
210
- // Get all user tokens with auto-refresh (for current user)
211
- const userTokens = await oauth.getUserTokens();
115
+ ##### `authorize(options)`
212
116
 
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 }[]
117
+ Start OAuth2 authorization flow.
216
118
 
217
- // Revoke tokens (uses current context)
218
- await oauth.revokeTokens('google'); // Revokes for current user/scope/provider
119
+ ```typescript
120
+ const { url, state } = await oauth.authorize({
121
+ provider: 'google',
122
+ userId: 'user123',
123
+ email: 'user@example.com',
124
+ scopes?: ['profile', 'email'], // Optional
125
+ usePKCE?: true, // Optional
126
+ metadata?: { source: 'signup' } // Optional
127
+ });
128
+ // Redirect user to `url`
219
129
  ```
220
130
 
221
- #### Granular API (Advanced)
131
+ ##### `handleCallback(code, state)`
222
132
 
223
- The granular API provides full control over the token hierarchy:
133
+ Handle OAuth2 callback and save tokens.
224
134
 
225
135
  ```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 ===
136
+ const { token, profile } = await oauth.handleCallback(code, state);
137
+ // token: { id, provider, userId, email, token, metadata, ... }
138
+ // profile: { id, email, name, picture, raw } or undefined
139
+ ```
279
140
 
280
- // Get valid token for user (auto-refresh, takes first if multiple exist)
281
- const validToken = await oauth.granular.getValidTokenForUser(userId, scopeId, 'google');
141
+ #### ๐Ÿ”‘ Token Management
282
142
 
283
- // Get access token for user (convenience method)
284
- const accessToken = await oauth.granular.getAccessTokenForUser(userId, scopeId, 'google');
143
+ ##### `getAccessToken(provider, email, options?)`
285
144
 
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
- );
145
+ Get access token string (auto-refreshes if expired).
295
146
 
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
147
+ ```typescript
148
+ const accessToken = await oauth.getAccessToken('google', 'user@example.com');
149
+ // Returns: "ya29.a0AfH6SMBx..."
302
150
  ```
303
151
 
304
- ### Storage Adapters
152
+ ##### `getValidToken(provider, email, options?)`
305
153
 
306
- #### Built-in Memory Adapter
154
+ Get full token object (auto-refreshes if expired).
307
155
 
308
156
  ```typescript
309
- import { InMemoryStorageAdapter } from '@dainprotocol/oauth2-token-manager';
310
-
311
- const storage = new InMemoryStorageAdapter();
312
- const oauth = new OAuth2Client({ storage });
157
+ const token = await oauth.getValidToken('google', 'user@example.com');
158
+ // Returns: { accessToken, refreshToken, expiresAt, tokenType, scope, ... }
313
159
  ```
314
160
 
315
- #### PostgreSQL Adapter
161
+ #### ๐Ÿ” Token Queries
316
162
 
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
- ```
163
+ ##### `getTokensByUserId(userId)`
329
164
 
330
- #### Custom Storage Adapter
165
+ Get all tokens for a user.
331
166
 
332
167
  ```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
- }
168
+ const tokens = await oauth.getTokensByUserId('user123');
169
+ // Returns: StoredToken[]
344
170
  ```
345
171
 
346
- ### Provider Configuration
172
+ ##### `getTokensByEmail(email)`
347
173
 
348
- #### Google OAuth2
174
+ Get all tokens for an email.
349
175
 
350
176
  ```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
- }
177
+ const tokens = await oauth.getTokensByEmail('user@example.com');
178
+ // Returns: StoredToken[]
379
179
  ```
380
180
 
381
- #### Generic Provider
181
+ #### ๐Ÿ—‘๏ธ Token Cleanup
382
182
 
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
- ```
183
+ ##### `deleteToken(provider, email)`
400
184
 
401
- #### Google OAuth2 with Offline Access
185
+ Delete a specific token.
402
186
 
403
187
  ```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
- });
188
+ const deleted = await oauth.deleteToken('google', 'user@example.com');
189
+ // Returns: boolean
426
190
  ```
427
191
 
428
- #### Customizing Authorization Parameters
192
+ ##### `cleanupExpiredTokens()`
429
193
 
430
- Each provider supports customization through `extraAuthParams` and `additionalParams`:
194
+ Delete all expired tokens.
431
195
 
432
196
  ```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
- }
197
+ const count = await oauth.cleanupExpiredTokens();
198
+ // Returns: number of deleted tokens
450
199
  ```
451
200
 
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
201
+ ##### `cleanupExpiredStates()`
460
202
 
461
- ### Token Auto-Refresh
203
+ Delete expired authorization states (older than 10 minutes).
462
204
 
463
205
  ```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
- });
206
+ const count = await oauth.cleanupExpiredStates();
207
+ // Returns: number of deleted states
469
208
  ```
470
209
 
471
- ### Profile-Based Token Management
210
+ #### โš™๏ธ Provider Management
472
211
 
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
- ```
212
+ ##### `registerProvider(name, config)`
482
213
 
483
- ### Email-Based Operations
214
+ Register a new OAuth provider.
484
215
 
485
216
  ```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');
217
+ oauth.registerProvider('custom', {
218
+ clientId: 'xxx',
219
+ clientSecret: 'xxx',
220
+ authorizationUrl: 'https://provider.com/oauth/authorize',
221
+ tokenUrl: 'https://provider.com/oauth/token',
222
+ redirectUri: 'http://localhost:3000/callback',
223
+ scopes: ['read'],
224
+ profileUrl?: 'https://provider.com/api/user', // Optional
225
+ usePKCE?: true, // Optional
226
+ });
528
227
  ```
529
228
 
530
- ### User-Centric Operations (Stateless)
229
+ ### Types
531
230
 
532
- For backend APIs where you have explicit user IDs:
231
+ #### OAuth2Config
533
232
 
534
233
  ```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');
234
+ interface OAuth2Config {
235
+ clientId: string;
236
+ clientSecret?: string;
237
+ authorizationUrl: string;
238
+ tokenUrl: string;
239
+ redirectUri: string;
240
+ scopes: string[];
241
+ profileUrl?: string;
242
+ usePKCE?: boolean;
243
+ extraAuthParams?: Record<string, string>;
244
+ }
561
245
  ```
562
246
 
563
- ### PKCE Support
247
+ #### StoredToken
564
248
 
565
249
  ```typescript
566
- // Enable PKCE for enhanced security
567
- const { url, state } = await oauth.authorize({
568
- provider: 'google',
569
- usePKCE: true, // Enables PKCE flow
570
- });
250
+ interface StoredToken {
251
+ id: string;
252
+ provider: string;
253
+ userId: string;
254
+ email: string;
255
+ token: OAuth2Token;
256
+ metadata?: Record<string, any>;
257
+ createdAt: Date;
258
+ updatedAt: Date;
259
+ }
571
260
  ```
572
261
 
573
- ### Token Validation
262
+ #### OAuth2Token
574
263
 
575
264
  ```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');
265
+ interface OAuth2Token {
266
+ accessToken: string;
267
+ refreshToken?: string;
268
+ expiresAt: Date;
269
+ tokenType: string;
270
+ scope?: string;
271
+ }
583
272
  ```
584
273
 
585
- ## ๐Ÿ”’ Security Features
586
-
587
- ### State Management
274
+ ## ๐Ÿ”Œ Storage Adapters
588
275
 
589
- - Cryptographically secure state generation
590
- - Automatic state validation and cleanup
591
- - Configurable state expiration
276
+ ### In-Memory (Default)
592
277
 
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
278
+ ```typescript
279
+ const oauth = new OAuth2Client(); // Uses in-memory storage
627
280
  ```
628
281
 
629
- ## ๐Ÿข Multi-System Examples
630
-
631
- ### SaaS Platform with Multiple Apps
282
+ ### PostgreSQL
632
283
 
633
284
  ```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
- });
285
+ import { PostgresStorageAdapter } from '@dainprotocol/oauth2-storage-postgres';
645
286
 
646
- const adminScope = await oauth.createScope('admin', {
647
- type: 'access',
648
- permissions: ['*'],
649
- isolated: true,
287
+ const storage = new PostgresStorageAdapter({
288
+ host: 'localhost',
289
+ port: 5432,
290
+ username: 'user',
291
+ password: 'password',
292
+ database: 'oauth_tokens',
650
293
  });
651
294
 
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
- });
295
+ const oauth = new OAuth2Client({ storage });
660
296
  ```
661
297
 
662
- ### Multi-Tenant Application
298
+ ### Drizzle ORM
663
299
 
664
300
  ```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
- });
301
+ import { DrizzleStorageAdapter } from '@dainprotocol/oauth2-storage-drizzle';
302
+ import { drizzle } from 'drizzle-orm/postgres-js';
674
303
 
675
- // Tenant-isolated tokens
676
- const tokens = await oauth.granular.getTokensBySystem(tenantSystem.id);
304
+ const db = drizzle(/* your db config */);
305
+ const storage = new DrizzleStorageAdapter(db, { dialect: 'postgres' });
306
+ const oauth = new OAuth2Client({ storage });
677
307
  ```
678
308
 
679
- ## ๐Ÿš€ Production Deployment
309
+ ## ๐Ÿ“ Examples
680
310
 
681
- ### Environment Configuration
311
+ ### Express.js Integration
682
312
 
683
313
  ```typescript
314
+ import express from 'express';
315
+ import { OAuth2Client } from '@dainprotocol/oauth2-token-manager';
316
+
317
+ const app = express();
684
318
  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
319
  providers: {
699
320
  google: {
700
321
  clientId: process.env.GOOGLE_CLIENT_ID,
701
322
  clientSecret: process.env.GOOGLE_CLIENT_SECRET,
702
- redirectUri: process.env.GOOGLE_REDIRECT_URI,
703
- // ... other config
323
+ authorizationUrl: 'https://accounts.google.com/o/oauth2/v2/auth',
324
+ tokenUrl: 'https://oauth2.googleapis.com/token',
325
+ redirectUri: 'http://localhost:3000/auth/google/callback',
326
+ scopes: ['profile', 'email'],
704
327
  },
705
328
  },
706
329
  });
707
- ```
708
330
 
709
- ### Performance Optimization
331
+ // Start OAuth flow
332
+ app.get('/auth/:provider', async (req, res) => {
333
+ const { url, state } = await oauth.authorize({
334
+ provider: req.params.provider,
335
+ userId: req.user.id,
336
+ email: req.user.email,
337
+ });
338
+
339
+ req.session.oauthState = state;
340
+ res.redirect(url);
341
+ });
710
342
 
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
343
+ // Handle callback
344
+ app.get('/auth/:provider/callback', async (req, res) => {
345
+ const { code } = req.query;
346
+ const { token, profile } = await oauth.handleCallback(code, req.session.oauthState);
347
+ res.json({ success: true, profile });
716
348
  });
717
349
 
718
- // Batch operations for efficiency
719
- const allTokens = await oauth.getAllValidTokensForUser(userId);
350
+ // Use tokens
351
+ app.get('/api/data', async (req, res) => {
352
+ const accessToken = await oauth.getAccessToken('google', req.user.email);
353
+ // Use accessToken for API calls...
354
+ });
355
+ ```
356
+
357
+ ### Scheduled Cleanup
720
358
 
721
- // Clean up expired states regularly
359
+ ```typescript
360
+ // Clean up expired tokens daily
722
361
  setInterval(
723
362
  async () => {
724
- await oauth.cleanup(10 * 60 * 1000); // 10 minutes
363
+ const tokens = await oauth.cleanupExpiredTokens();
364
+ const states = await oauth.cleanupExpiredStates();
365
+ console.log(`Cleaned up ${tokens} tokens and ${states} states`);
725
366
  },
726
- 5 * 60 * 1000,
727
- ); // Every 5 minutes
367
+ 24 * 60 * 60 * 1000,
368
+ );
728
369
  ```
729
370
 
730
- ### Error Handling
371
+ ### Multiple Providers
731
372
 
732
373
  ```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
- }
374
+ // User can connect multiple OAuth accounts
375
+ const providers = ['google', 'github', 'microsoft'];
376
+
377
+ for (const provider of providers) {
378
+ const { url, state } = await oauth.authorize({
379
+ provider,
380
+ userId: 'user123',
381
+ email: 'user@example.com',
382
+ });
383
+ // Handle each provider...
743
384
  }
744
- ```
745
385
 
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
386
+ // Get all connected accounts
387
+ const tokens = await oauth.getTokensByUserId('user123');
388
+ console.log(
389
+ 'Connected accounts:',
390
+ tokens.map((t) => t.provider),
391
+ );
772
392
  ```
773
393
 
774
394
  ## ๐Ÿ“„ License
775
395
 
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)
396
+ MIT ยฉ Dain Protocol