@dainprotocol/oauth2-token-manager 0.1.1 โ†’ 0.1.4

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,39 +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
4
 
5
5
  ## ๐Ÿ“‹ Table of Contents
6
6
 
7
7
  - [Features](#-features)
8
8
  - [Installation](#-installation)
9
9
  - [Quick Start](#-quick-start)
10
- - [Granular API (Advanced)](#-granular-api-advanced)
11
- - [Context-Aware Resolution](#context-aware-resolution)
12
- - [User Management](#user-management)
13
- - [Token Operations](#token-operations)
14
- - [Bulk Operations](#bulk-operations)
15
- - [System & Scope Management](#system--scope-management)
16
- - [Architecture](#-architecture)
10
+ - [Core Concepts](#-core-concepts)
17
11
  - [API Reference](#-api-reference)
18
- - [Storage Adapters](#storage-adapters)
19
- - [Provider Configuration](#provider-configuration)
20
- - [Advanced Features](#-advanced-features)
12
+ - [Storage Adapters](#-storage-adapters)
21
13
  - [Examples](#-examples)
22
- - [Production Deployment](#-production-deployment)
23
14
 
24
15
  ## ๐Ÿš€ Features
25
16
 
26
- - **๐Ÿ”Œ Storage Agnostic**: Use any storage backend (In-Memory, PostgreSQL, or build your own adapter)
27
- - **๐Ÿข Multi-System Support**: Manage tokens across multiple applications/systems
28
- - **๐Ÿ” Advanced Security**: PKCE support, state validation, token encryption
29
- - **โšก High Performance**: Efficient token validation, caching, and refresh strategies
30
- - **๐Ÿ”„ Auto-Refresh**: Automatic token refresh with configurable buffers
31
- - **๐Ÿ‘ค User Management**: Comprehensive user lifecycle with email/external ID support
32
- - **๐Ÿ“ง Profile Integration**: Automatic profile fetching from OAuth providers
33
- - **๐ŸŽฏ Flexible Scoping**: Fine-grained permission management
34
- - **๐ŸŽจ Default Configuration**: Automatic default system and scope creation
35
- - **๐Ÿ’ก Developer Friendly**: Both context-managed and granular APIs
36
- - **๐Ÿงช 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
37
23
 
38
24
  ## ๐Ÿ“ฆ Installation
39
25
 
@@ -44,7 +30,7 @@ npm install @dainprotocol/oauth2-token-manager
44
30
  ### Storage Adapters
45
31
 
46
32
  ```bash
47
- # PostgreSQL adapter
33
+ # PostgreSQL adapter (TypeORM based)
48
34
  npm install @dainprotocol/oauth2-storage-postgres
49
35
 
50
36
  # Drizzle adapter (supports PostgreSQL, MySQL, SQLite)
@@ -53,814 +39,358 @@ npm install @dainprotocol/oauth2-storage-drizzle
53
39
 
54
40
  ## ๐Ÿš€ Quick Start
55
41
 
56
- ### Simple Setup
57
-
58
42
  ```typescript
59
43
  import { OAuth2Client } from '@dainprotocol/oauth2-token-manager';
60
44
 
61
- // Quick setup for common use cases
62
- const oauth = await OAuth2Client.quickSetup('MyApp', {
63
- google: {
64
- clientId: 'your-google-client-id',
65
- clientSecret: 'your-google-client-secret',
66
- authorizationUrl: 'https://accounts.google.com/o/oauth2/auth',
67
- tokenUrl: 'https://oauth2.googleapis.com/token',
68
- redirectUri: 'http://localhost:3000/auth/callback',
69
- scopes: ['profile', 'email'],
70
- },
71
- github: {
72
- clientId: 'your-github-client-id',
73
- clientSecret: 'your-github-client-secret',
74
- authorizationUrl: 'https://github.com/login/oauth/authorize',
75
- tokenUrl: 'https://github.com/login/oauth/access_token',
76
- redirectUri: 'http://localhost:3000/auth/callback',
77
- scopes: ['user:email'],
45
+ // Initialize with provider configurations
46
+ const oauth = new OAuth2Client({
47
+ providers: {
48
+ google: {
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',
56
+ },
57
+ github: {
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',
65
+ },
78
66
  },
79
67
  });
80
68
 
81
- // Create or get a user
82
- const user = await oauth.getOrCreateUser({
83
- email: 'user@example.com',
84
- metadata: { role: 'user' },
85
- });
86
-
87
69
  // Start OAuth flow
88
70
  const { url, state } = await oauth.authorize({
89
71
  provider: 'google',
90
- scopes: ['profile', 'email'],
91
- });
92
-
93
- // Handle callback
94
- const result = await oauth.handleCallback(code, state);
95
- console.log('User authenticated:', result.userId);
96
-
97
- // === NEW: Simplified Granular API (V2) ===
98
- // Access tokens without specifying system/scope IDs
99
- const accessToken = await oauth.granularV2.getAccessToken({
72
+ userId: 'user123',
100
73
  email: 'user@example.com',
101
- provider: 'google',
74
+ scopes: ['profile', 'email'],
102
75
  });
103
- ```
104
-
105
- ## ๐ŸŽฏ Granular API (Advanced)
106
-
107
- The granular API (`oauth.granularV2`) provides full control over the token hierarchy with intelligent system and scope resolution. It's the recommended way to interact with tokens when you need fine-grained control.
108
-
109
- ### Context-Aware Resolution
110
76
 
111
- The API follows this priority order for resolving system and scope:
77
+ // Redirect user to authorization URL
78
+ res.redirect(url);
112
79
 
113
- 1. **Explicit parameters** - If you provide systemId/scopeId in the method call, those are used
114
- 2. **Context from OAuth2Client** - If not provided, it checks the current context (useSystem/setDefaultScope)
115
- 3. **Default system/scope** - If no context is set, it falls back to the default system and scope
80
+ // Handle OAuth callback
81
+ const { token, profile } = await oauth.handleCallback(code, state);
116
82
 
117
- ```typescript
118
- // === Context-Aware Usage ===
119
-
120
- // Set context in main client
121
- await oauth.useSystem('production-system');
122
- oauth.setDefaultScope('api-access');
123
-
124
- // Granular V2 will now use this context automatically
125
- const user = await oauth.granularV2.getOrCreateUser({
126
- email: 'user@example.com',
127
- options: { metadata: { role: 'user' } },
128
- // No need to specify systemId - uses 'production-system' from context
129
- });
130
-
131
- // Token operations use the context scope
132
- const token = await oauth.granularV2.getAccessToken({
133
- email: 'user@example.com',
134
- provider: 'google',
135
- // No need to specify scopeId - uses 'api-access' from context
136
- });
137
-
138
- // Override with explicit parameters when needed
139
- const differentToken = await oauth.granularV2.getAccessToken({
140
- email: 'user@example.com',
141
- provider: 'google',
142
- systemId: 'another-system', // This takes precedence
143
- scopeId: 'different-scope', // This takes precedence
144
- });
83
+ // Use the token
84
+ const accessToken = await oauth.getAccessToken('google', 'user@example.com');
145
85
  ```
146
86
 
147
- ### User Management
87
+ ## ๐Ÿ”‘ Core Concepts
148
88
 
149
- ```typescript
150
- // Create or get users with optional system specification
151
- const user = await oauth.granularV2.getOrCreateUser({
152
- email: 'user@example.com',
153
- options: { metadata: { role: 'admin' } },
154
- // systemId is optional - uses context or defaults
155
- });
89
+ ### How It Works
156
90
 
157
- // Find users by various criteria
158
- const userByEmail = await oauth.granularV2.findUserByEmail({
159
- email: 'user@example.com',
160
- });
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)
161
95
 
162
- const userByExternalId = await oauth.granularV2.findUserByExternalId({
163
- externalId: 'external-123',
164
- });
96
+ ## ๐Ÿ“š API Reference
165
97
 
166
- // Get all users in a system
167
- const systemUsers = await oauth.granularV2.getUsersBySystem();
168
- // Or specify a different system
169
- const otherSystemUsers = await oauth.granularV2.getUsersBySystem('other-system-id');
170
- ```
98
+ ### OAuth2Client
171
99
 
172
- ### Token Operations
100
+ The main class for managing OAuth2 tokens.
173
101
 
174
102
  ```typescript
175
- // Save new tokens
176
- const savedToken = await oauth.granularV2.saveToken({
177
- userId: user.id,
178
- provider: 'google',
179
- email: 'user@example.com',
180
- token: oauthToken,
181
- // systemId and scopeId are optional
182
- });
183
-
184
- // Query tokens with flexible parameters
185
- const userTokens = await oauth.granularV2.getTokens({
186
- userId: user.id,
187
- });
188
-
189
- const providerTokens = await oauth.granularV2.getTokens({
190
- userId: user.id,
191
- provider: 'google',
192
- });
193
-
194
- const emailTokens = await oauth.granularV2.getTokens({
195
- email: 'user@example.com',
196
- // systemId resolved from context or defaults
197
- });
198
-
199
- // Get valid tokens with auto-refresh
200
- const validToken = await oauth.granularV2.getValidToken({
201
- email: 'user@example.com',
202
- provider: 'google',
203
- options: {
204
- autoRefresh: true,
205
- refreshBuffer: 5, // Refresh 5 minutes before expiry
206
- },
207
- });
208
-
209
- // Get access token directly
210
- const accessToken = await oauth.granularV2.getAccessToken({
211
- userId: user.id,
212
- provider: 'google',
103
+ const oauth = new OAuth2Client({
104
+ storage?: StorageAdapter, // Optional, defaults to in-memory
105
+ providers?: { // OAuth provider configurations
106
+ [name: string]: OAuth2Config
107
+ }
213
108
  });
214
-
215
- // Execute with valid token
216
- await oauth.granularV2.withValidToken(
217
- {
218
- email: 'user@example.com',
219
- provider: 'google',
220
- },
221
- async (accessToken) => {
222
- // Make API calls with the token
223
- const response = await fetch('https://api.example.com/data', {
224
- headers: { Authorization: `Bearer ${accessToken}` },
225
- });
226
- return response.json();
227
- },
228
- );
229
109
  ```
230
110
 
231
- ### Bulk Operations
111
+ ### Methods
232
112
 
233
- ```typescript
234
- // Get all valid tokens for a user
235
- const allUserTokens = await oauth.granularV2.getAllValidTokens({
236
- userId: user.id,
237
- options: { autoRefresh: true },
238
- });
113
+ #### ๐Ÿ” OAuth Flow
239
114
 
240
- // Get all valid tokens for an email
241
- const allEmailTokens = await oauth.granularV2.getAllValidTokens({
242
- email: 'user@example.com',
243
- });
115
+ ##### `authorize(options)`
244
116
 
245
- // Filter by provider
246
- const googleTokens = await oauth.granularV2.getAllValidTokens({
247
- email: 'user@example.com',
248
- provider: 'google',
249
- });
117
+ Start OAuth2 authorization flow.
250
118
 
251
- // Check token existence
252
- const hasToken = await oauth.granularV2.hasToken({
253
- email: 'user@example.com',
119
+ ```typescript
120
+ const { url, state } = await oauth.authorize({
254
121
  provider: 'google',
255
- });
256
-
257
- // Delete tokens
258
- await oauth.granularV2.deleteTokens({
122
+ userId: 'user123',
259
123
  email: 'user@example.com',
260
- provider: 'google',
261
- });
262
-
263
- // Delete all tokens for a user
264
- await oauth.granularV2.deleteTokens({
265
- userId: user.id,
124
+ scopes?: ['profile', 'email'], // Optional
125
+ usePKCE?: true, // Optional
126
+ metadata?: { source: 'signup' } // Optional
266
127
  });
128
+ // Redirect user to `url`
267
129
  ```
268
130
 
269
- ### System & Scope Management
131
+ ##### `handleCallback(code, state)`
270
132
 
271
- ```typescript
272
- // Get current defaults
273
- const defaultSystem = await oauth.granularV2.getSystem();
274
- const defaultScope = await oauth.granularV2.getScope();
275
-
276
- // Create new scope in current/default system
277
- const newScope = await oauth.granularV2.createScope({
278
- name: 'api-access',
279
- type: 'access',
280
- permissions: ['read', 'write'],
281
- isolated: true,
282
- });
283
-
284
- // Create scope in specific system
285
- const anotherScope = await oauth.granularV2.createScope({
286
- systemId: 'specific-system-id',
287
- name: 'admin-access',
288
- type: 'access',
289
- permissions: ['*'],
290
- });
133
+ Handle OAuth2 callback and save tokens.
291
134
 
292
- // Get all scopes in current system
293
- const scopes = await oauth.granularV2.getScopesBySystem();
294
-
295
- // Initialize defaults explicitly if needed
296
- const { system, scope } = await oauth.granularV2.ensureDefaults();
135
+ ```typescript
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
297
139
  ```
298
140
 
299
- ### Why Use Granular API?
300
-
301
- The granular API is ideal for:
302
-
303
- - **Single-system applications** - Simplified usage with automatic defaults
304
- - **Multi-tenant applications** - Easy context switching between systems
305
- - **Backend services** - Stateless operations with explicit parameters
306
- - **Complex token management** - Full control over token lifecycle
307
- - **Gradual migration** - Start simple, add complexity as needed
141
+ #### ๐Ÿ”‘ Token Management
308
142
 
309
- ## ๐Ÿ—๏ธ Architecture
143
+ ##### `getAccessToken(provider, email, options?)`
310
144
 
311
- ### Core Components
145
+ Get access token string (auto-refreshes if expired).
312
146
 
313
- ```
314
- โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
315
- โ”‚ OAuth2Client โ”‚
316
- โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚
317
- โ”‚ โ”‚ Context API โ”‚ โ”‚ Granular API V2 โ”‚ โ”‚
318
- โ”‚ โ”‚ (Simplified) โ”‚ โ”‚ (Full Control + Smart) โ”‚ โ”‚
319
- โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚
320
- โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
321
- โ”‚
322
- โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
323
- โ”‚ โ”‚ โ”‚
324
- โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ–ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ–ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ–ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
325
- โ”‚ Providers โ”‚ โ”‚ Storage โ”‚ โ”‚ Profile โ”‚
326
- โ”‚ (OAuth2) โ”‚ โ”‚ Adapter โ”‚ โ”‚ Fetchers โ”‚
327
- โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
147
+ ```typescript
148
+ const accessToken = await oauth.getAccessToken('google', 'user@example.com');
149
+ // Returns: "ya29.a0AfH6SMBx..."
328
150
  ```
329
151
 
330
- ### Data Model & Token Hierarchy
152
+ ##### `getValidToken(provider, email, options?)`
331
153
 
332
- **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.
154
+ Get full token object (auto-refreshes if expired).
333
155
 
334
156
  ```typescript
335
- // Systems: Top-level applications/services
336
- interface System {
337
- id: string;
338
- name: string;
339
- description?: string;
340
- scopes: Scope[];
341
- metadata?: Record<string, any>;
342
- }
343
-
344
- // Scopes: Permission boundaries within systems
345
- interface Scope {
346
- id: string;
347
- systemId: string;
348
- name: string;
349
- type: 'authentication' | 'access' | 'custom';
350
- permissions: string[];
351
- isolated: boolean; // Whether tokens are isolated to this scope
352
- }
353
-
354
- // Users: Identity within a system
355
- interface User {
356
- id: string;
357
- systemId: string;
358
- metadata?: Record<string, any>;
359
- }
360
-
361
- // User Tokens: OAuth2 tokens tied to user/system/scope/provider
362
- // A user can have MULTIPLE tokens for the same provider/scope combination
363
- interface UserToken {
364
- id: string;
365
- userId: string;
366
- systemId: string;
367
- scopeId: string;
368
- provider: string;
369
- token: OAuth2Token;
370
- }
157
+ const token = await oauth.getValidToken('google', 'user@example.com');
158
+ // Returns: { accessToken, refreshToken, expiresAt, tokenType, scope, ... }
371
159
  ```
372
160
 
373
- ### Token Hierarchy Rules
161
+ #### ๐Ÿ” Token Queries
374
162
 
375
- 1. **One User** belongs to **One System**
376
- 2. **One User** can have tokens in **Multiple Scopes** within their system
377
- 3. **One User** in **One Scope** can have tokens from **Multiple Providers**
378
- 4. **One User** in **One Scope** from **One Provider** can have **Multiple Tokens**
379
- 5. **Email Uniqueness**: For the same provider, a user cannot have multiple tokens with the same email (validated via profile fetcher)
380
- 6. **Cross-Provider Emails**: The same email can exist across different providers
163
+ ##### `getTokensByUserId(userId)`
381
164
 
382
- ### Default System and Scope
383
-
384
- The library provides automatic initialization of a default system and scope for simplified usage:
165
+ Get all tokens for a user.
385
166
 
386
167
  ```typescript
387
- import {
388
- DEFAULT_SYSTEM_NAME,
389
- DEFAULT_SCOPE_NAME,
390
- initializeDefaults,
391
- } from '@dainprotocol/oauth2-token-manager';
392
-
393
- // Automatic initialization
394
- const oauth = new OAuth2Client({
395
- providers: {
396
- /* ... */
397
- },
398
- });
399
-
400
- // If no system/scope is set, they will be automatically initialized on first use
401
- const { url, state } = await oauth.authorize({ provider: 'google' });
402
-
403
- // Or manually initialize defaults
404
- await oauth.initializeDefaults();
405
-
406
- // Check if defaults exist
407
- const defaultSystem = await oauth.getDefaultSystem();
408
- const defaultScope = await oauth.getDefaultScope();
409
-
410
- // Default constants
411
- console.log(DEFAULT_SYSTEM_NAME); // 'oauth2-token-manager-default-system'
412
- console.log(DEFAULT_SCOPE_NAME); // 'oauth2-token-manager-default-scope'
168
+ const tokens = await oauth.getTokensByUserId('user123');
169
+ // Returns: StoredToken[]
413
170
  ```
414
171
 
415
- The default system and scope are created with:
172
+ ##### `getTokensByEmail(email)`
416
173
 
417
- - **System**: Name-based identification, includes metadata `{ isDefault: true }`
418
- - **Scope**: Type 'access', permissions ['read', 'write'], not isolated
174
+ Get all tokens for an email.
419
175
 
420
- ## ๐Ÿ“š API Reference
176
+ ```typescript
177
+ const tokens = await oauth.getTokensByEmail('user@example.com');
178
+ // Returns: StoredToken[]
179
+ ```
421
180
 
422
- ### OAuth2Client
181
+ #### ๐Ÿ—‘๏ธ Token Cleanup
423
182
 
424
- The main client class providing both context-managed and granular APIs.
183
+ ##### `deleteToken(provider, email)`
425
184
 
426
- #### Context-Managed API (Recommended for Simple Use Cases)
185
+ Delete a specific token.
427
186
 
428
187
  ```typescript
429
- // System management
430
- await oauth.createSystem('MyApp');
431
- await oauth.getOrCreateSystem('MyApp'); // Idempotent - returns existing if found
432
- await oauth.useSystem(systemId);
433
-
434
- // Scope management
435
- await oauth.createScope('api-access');
436
- await oauth.getOrCreateScope('api-access'); // Idempotent - returns existing if found
437
-
438
- // Default system/scope (automatic initialization)
439
- await oauth.initializeDefaults(); // Creates default system and scope if they don't exist
440
- const defaultSystem = await oauth.getDefaultSystem();
441
- const defaultScope = await oauth.getDefaultScope();
442
-
443
- // User management
444
- const user = await oauth.getOrCreateUser({ email: 'user@example.com' });
445
- await oauth.useUser(userId);
446
-
447
- // Authorization flow
448
- const { url, state } = await oauth.authorize({ provider: 'google' });
449
- const result = await oauth.handleCallback(code, state);
450
-
451
- // Token operations (uses current context + default scope)
452
- // Note: When multiple tokens exist, these methods use the first (most recent) token
453
- const accessToken = await oauth.getAccessToken('google');
454
- const validToken = await oauth.ensureValidToken('google');
455
-
456
- // Get all user tokens with auto-refresh (for current user)
457
- const userTokens = await oauth.getUserTokens();
458
-
459
- // Get all valid tokens for a specific user
460
- const allTokens = await oauth.getAllValidTokensForUser(userId);
461
- // Returns: { provider: string; scopeId: string; token: OAuth2Token; userToken: UserToken }[]
462
-
463
- // Revoke tokens (uses current context)
464
- await oauth.revokeTokens('google'); // Revokes for current user/scope/provider
188
+ const deleted = await oauth.deleteToken('google', 'user@example.com');
189
+ // Returns: boolean
465
190
  ```
466
191
 
467
- ### Storage Adapters
192
+ ##### `cleanupExpiredTokens()`
468
193
 
469
- #### Built-in Memory Adapter
194
+ Delete all expired tokens.
470
195
 
471
196
  ```typescript
472
- import { InMemoryStorageAdapter } from '@dainprotocol/oauth2-token-manager';
473
-
474
- const storage = new InMemoryStorageAdapter();
475
- const oauth = new OAuth2Client({ storage });
197
+ const count = await oauth.cleanupExpiredTokens();
198
+ // Returns: number of deleted tokens
476
199
  ```
477
200
 
478
- #### PostgreSQL Adapter
201
+ ##### `cleanupExpiredStates()`
479
202
 
480
- ```typescript
481
- import { PostgresStorageFactory } from '@dainprotocol/oauth2-storage-postgres';
482
-
483
- const storage = await PostgresStorageFactory.create({
484
- host: process.env.DB_HOST,
485
- port: parseInt(process.env.DB_PORT || '5432'),
486
- username: process.env.DB_USER,
487
- password: process.env.DB_PASSWORD,
488
- database: process.env.DB_NAME,
489
- ssl: process.env.NODE_ENV === 'production',
490
- });
491
- ```
492
-
493
- #### Drizzle Adapter (Multi-Database)
203
+ Delete expired authorization states (older than 10 minutes).
494
204
 
495
205
  ```typescript
496
- import { DrizzleStorageFactory } from '@dainprotocol/oauth2-storage-drizzle';
497
-
498
- // PostgreSQL
499
- const pgStorage = await DrizzleStorageFactory.create({
500
- type: 'postgresql',
501
- config: {
502
- host: 'localhost',
503
- port: 5432,
504
- user: 'oauth2_user',
505
- password: 'password',
506
- database: 'oauth2_db',
507
- },
508
- });
509
-
510
- // MySQL
511
- const mysqlStorage = await DrizzleStorageFactory.create({
512
- type: 'mysql',
513
- config: {
514
- host: 'localhost',
515
- port: 3306,
516
- user: 'oauth2_user',
517
- password: 'password',
518
- database: 'oauth2_db',
519
- },
520
- });
521
-
522
- // SQLite
523
- const sqliteStorage = await DrizzleStorageFactory.create({
524
- type: 'sqlite',
525
- config: {
526
- filename: './oauth2.db',
527
- },
528
- });
206
+ const count = await oauth.cleanupExpiredStates();
207
+ // Returns: number of deleted states
529
208
  ```
530
209
 
531
- #### Custom Storage Adapter
210
+ #### โš™๏ธ Provider Management
532
211
 
533
- ```typescript
534
- import { StorageAdapter } from '@dainprotocol/oauth2-token-manager';
212
+ ##### `registerProvider(name, config)`
535
213
 
536
- class MyCustomAdapter implements StorageAdapter {
537
- // System operations
538
- async createSystem(system) {
539
- /* implement */
540
- }
541
- async getOrCreateSystem(system) {
542
- // Find by name, create if not exists
543
- /* implement */
544
- }
545
- async getSystem(id) {
546
- /* implement */
547
- }
214
+ Register a new OAuth provider.
548
215
 
549
- // Scope operations
550
- async createScope(scope) {
551
- /* implement */
552
- }
553
- async getOrCreateScope(scope) {
554
- // Find by systemId + name, create if not exists
555
- /* implement */
556
- }
557
-
558
- // User operations
559
- async getOrCreateUser(input) {
560
- // Find by email or externalId, create if not exists
561
- /* implement */
562
- }
563
-
564
- // ... implement all required methods
565
- }
216
+ ```typescript
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
+ });
566
227
  ```
567
228
 
568
- ### Provider Configuration
229
+ ### Types
569
230
 
570
- #### Google OAuth2
231
+ #### OAuth2Config
571
232
 
572
233
  ```typescript
573
- {
574
- google: {
575
- clientId: 'your-client-id',
576
- clientSecret: 'your-client-secret',
577
- authorizationUrl: 'https://accounts.google.com/o/oauth2/auth',
578
- tokenUrl: 'https://oauth2.googleapis.com/token',
579
- redirectUri: 'http://localhost:3000/auth/callback',
580
- scopes: ['profile', 'email'],
581
- profileUrl: 'https://www.googleapis.com/oauth2/v2/userinfo',
582
- usePKCE: true, // Recommended for security
583
- }
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>;
584
244
  }
585
245
  ```
586
246
 
587
- #### GitHub OAuth2
247
+ #### StoredToken
588
248
 
589
249
  ```typescript
590
- {
591
- github: {
592
- clientId: 'your-client-id',
593
- clientSecret: 'your-client-secret',
594
- authorizationUrl: 'https://github.com/login/oauth/authorize',
595
- tokenUrl: 'https://github.com/login/oauth/access_token',
596
- redirectUri: 'http://localhost:3000/auth/callback',
597
- scopes: ['user:email'],
598
- profileUrl: 'https://api.github.com/user',
599
- }
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;
600
259
  }
601
260
  ```
602
261
 
603
- #### Generic Provider
262
+ #### OAuth2Token
604
263
 
605
264
  ```typescript
606
- {
607
- custom: {
608
- clientId: 'your-client-id',
609
- clientSecret: 'your-client-secret',
610
- authorizationUrl: 'https://provider.com/oauth/authorize',
611
- tokenUrl: 'https://provider.com/oauth/token',
612
- redirectUri: 'http://localhost:3000/auth/callback',
613
- scopes: ['read', 'write'],
614
- profileUrl: 'https://provider.com/api/user',
615
- additionalParams: {
616
- audience: 'api.example.com'
617
- },
618
- responseRootKey: 'data' // For nested responses
619
- }
265
+ interface OAuth2Token {
266
+ accessToken: string;
267
+ refreshToken?: string;
268
+ expiresAt: Date;
269
+ tokenType: string;
270
+ scope?: string;
620
271
  }
621
272
  ```
622
273
 
623
- ## ๐Ÿ”ง Advanced Features
274
+ ## ๐Ÿ”Œ Storage Adapters
624
275
 
625
- ### Token Auto-Refresh
276
+ ### In-Memory (Default)
626
277
 
627
278
  ```typescript
628
- const accessToken = await oauth.getAccessToken('google', {
629
- autoRefresh: true,
630
- refreshBuffer: 10, // Refresh early to avoid expiry
631
- expirationBuffer: 30, // Consider expired 30 seconds early
632
- });
279
+ const oauth = new OAuth2Client(); // Uses in-memory storage
633
280
  ```
634
281
 
635
- ### Profile-Based Token Management
282
+ ### PostgreSQL
636
283
 
637
284
  ```typescript
638
- const result = await oauth.handleCallback(code, state, {
639
- profileOptions: {
640
- checkProfileEmail: true, // Fetch and check email conflicts
641
- replaceConflictingTokens: true, // Replace existing tokens with same email
642
- mergeUserData: true, // Merge profile data into user metadata
643
- },
644
- });
645
- ```
646
-
647
- ### PKCE Support
648
-
649
- ```typescript
650
- // Enable PKCE for enhanced security
651
- const { url, state } = await oauth.authorize({
652
- provider: 'google',
653
- usePKCE: true, // Enables PKCE flow
654
- });
655
- ```
656
-
657
- ### Token Validation
658
-
659
- ```typescript
660
- // Check if token is expired
661
- const isExpired = oauth.isTokenExpired(token, {
662
- expirationBuffer: 60, // Consider expired 60 seconds early
663
- });
664
-
665
- // Ensure valid token (auto-refresh if needed)
666
- const validToken = await oauth.ensureValidToken('google');
667
- ```
668
-
669
- ## ๐Ÿข Examples
285
+ import { PostgresStorageAdapter } from '@dainprotocol/oauth2-storage-postgres';
670
286
 
671
- ### Multi-Tenant Application
672
-
673
- ```typescript
674
- // Each tenant gets their own system
675
- const tenantSystem = await oauth.createSystem(`Tenant-${tenantId}`);
676
-
677
- // Tenant-specific user management
678
- await oauth.useSystem(tenantSystem.id);
679
- const tenantUser = await oauth.getOrCreateUser({
680
- email: userEmail,
681
- metadata: { tenantId, role: 'admin' },
287
+ const storage = new PostgresStorageAdapter({
288
+ host: 'localhost',
289
+ port: 5432,
290
+ username: 'user',
291
+ password: 'password',
292
+ database: 'oauth_tokens',
682
293
  });
683
294
 
684
- // Tenant-isolated tokens
685
- const tokens = await oauth.granularV2.getTokens({ systemId: tenantSystem.id });
295
+ const oauth = new OAuth2Client({ storage });
686
296
  ```
687
297
 
688
- ### SaaS Platform with Multiple Apps
298
+ ### Drizzle ORM
689
299
 
690
300
  ```typescript
691
- // Create systems for different applications
692
- const crmSystem = await oauth.createSystem('CRM App');
693
- const analyticsSystem = await oauth.createSystem('Analytics Dashboard');
694
-
695
- // Create scopes for different access levels
696
- await oauth.useSystem(crmSystem.id);
697
- const readScope = await oauth.createScope('read-only', {
698
- type: 'access',
699
- permissions: ['read:contacts', 'read:deals'],
700
- isolated: true,
701
- });
702
-
703
- const adminScope = await oauth.createScope('admin', {
704
- type: 'access',
705
- permissions: ['*'],
706
- isolated: true,
707
- });
301
+ import { DrizzleStorageAdapter } from '@dainprotocol/oauth2-storage-drizzle';
302
+ import { drizzle } from 'drizzle-orm/postgres-js';
708
303
 
709
- // Users can have different permissions per system
710
- const user = await oauth.getOrCreateUser({ email: 'user@company.com' });
711
-
712
- // Authorize for specific system/scope
713
- const { url } = await oauth.authorize({
714
- provider: 'google',
715
- scopes: ['profile', 'email'],
716
- });
304
+ const db = drizzle(/* your db config */);
305
+ const storage = new DrizzleStorageAdapter(db, { dialect: 'postgres' });
306
+ const oauth = new OAuth2Client({ storage });
717
307
  ```
718
308
 
719
- ## ๐Ÿš€ Production Deployment
309
+ ## ๐Ÿ“ Examples
720
310
 
721
- ### Environment Configuration
311
+ ### Express.js Integration
722
312
 
723
313
  ```typescript
314
+ import express from 'express';
315
+ import { OAuth2Client } from '@dainprotocol/oauth2-token-manager';
316
+
317
+ const app = express();
724
318
  const oauth = new OAuth2Client({
725
- storage: await PostgresStorageFactory.create({
726
- host: process.env.DB_HOST,
727
- port: parseInt(process.env.DB_PORT || '5432'),
728
- username: process.env.DB_USER,
729
- password: process.env.DB_PASSWORD,
730
- database: process.env.DB_NAME,
731
- ssl: {
732
- rejectUnauthorized: process.env.NODE_ENV === 'production',
733
- },
734
- poolSize: 20,
735
- logging: process.env.NODE_ENV === 'development',
736
- }),
737
- sealKey: process.env.OAUTH2_SEAL_KEY, // For token encryption
738
319
  providers: {
739
320
  google: {
740
321
  clientId: process.env.GOOGLE_CLIENT_ID,
741
322
  clientSecret: process.env.GOOGLE_CLIENT_SECRET,
742
- redirectUri: process.env.GOOGLE_REDIRECT_URI,
743
- // ... 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'],
744
327
  },
745
328
  },
746
329
  });
747
- ```
748
330
 
749
- ### 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
+ });
750
338
 
751
- ```typescript
752
- // Use token caching for high-traffic scenarios
753
- const accessToken = await oauth.getAccessToken('google', {
754
- autoRefresh: true,
755
- refreshBuffer: 10, // Refresh early to avoid expiry
339
+ req.session.oauthState = state;
340
+ res.redirect(url);
756
341
  });
757
342
 
758
- // Batch operations for efficiency
759
- const allTokens = await oauth.getAllValidTokensForUser(userId);
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 });
348
+ });
760
349
 
761
- // Clean up expired states regularly
762
- setInterval(
763
- async () => {
764
- await oauth.cleanup(10 * 60 * 1000); // 10 minutes
765
- },
766
- 5 * 60 * 1000,
767
- ); // Every 5 minutes
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
+ });
768
355
  ```
769
356
 
770
- ### Error Handling
357
+ ### Scheduled Cleanup
771
358
 
772
359
  ```typescript
773
- try {
774
- const token = await oauth.getAccessToken('google');
775
- } catch (error) {
776
- if (error.message.includes('Token expired')) {
777
- // Handle token expiry
778
- const { url } = await oauth.authorize({ provider: 'google' });
779
- // Redirect to re-authorization
780
- } else if (error.message.includes('Provider not found')) {
781
- // Handle missing provider
782
- }
783
- }
784
- ```
785
-
786
- ## ๐Ÿ”’ Security Features
787
-
788
- ### State Management
789
-
790
- - Cryptographically secure state generation
791
- - Automatic state validation and cleanup
792
- - Configurable state expiration
793
-
794
- ### PKCE (Proof Key for Code Exchange)
795
-
796
- - Built-in PKCE support for public clients
797
- - Automatic code verifier generation
798
- - Enhanced security for mobile and SPA applications
799
-
800
- ### Token Encryption
801
-
802
- - Secure token storage with optional encryption
803
- - Configurable seal keys for sensitive data
804
- - Protection against token theft
805
-
806
- ### Email Validation
807
-
808
- - Automatic email conflict detection
809
- - Profile-based user validation
810
- - Cross-provider email consistency
811
-
812
- ## ๐Ÿงช Testing
813
-
814
- The library includes comprehensive tests using Vitest:
815
-
816
- ```bash
817
- # Run tests
818
- npm test
819
-
820
- # Run tests with UI
821
- npm run test:ui
822
-
823
- # Run tests with coverage
824
- npm run test:coverage
825
-
826
- # Watch mode
827
- npm run test:watch
360
+ // Clean up expired tokens daily
361
+ setInterval(
362
+ async () => {
363
+ const tokens = await oauth.cleanupExpiredTokens();
364
+ const states = await oauth.cleanupExpiredStates();
365
+ console.log(`Cleaned up ${tokens} tokens and ${states} states`);
366
+ },
367
+ 24 * 60 * 60 * 1000,
368
+ );
828
369
  ```
829
370
 
830
- ## ๐Ÿค Contributing
831
-
832
- 1. Fork the repository
833
- 2. Create your feature branch (`git checkout -b feature/amazing-feature`)
834
- 3. Commit your changes (`git commit -m 'Add some amazing feature'`)
835
- 4. Push to the branch (`git push origin feature/amazing-feature`)
836
- 5. Open a Pull Request
837
-
838
- ### Development Setup
839
-
840
- ```bash
841
- # Install dependencies
842
- npm install
843
-
844
- # Run in development mode
845
- npm run dev
371
+ ### Multiple Providers
846
372
 
847
- # Run tests
848
- npm test
373
+ ```typescript
374
+ // User can connect multiple OAuth accounts
375
+ const providers = ['google', 'github', 'microsoft'];
849
376
 
850
- # Build the project
851
- npm run build
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...
384
+ }
852
385
 
853
- # Lint and format
854
- npm run lint:fix
855
- 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
+ );
856
392
  ```
857
393
 
858
394
  ## ๐Ÿ“„ License
859
395
 
860
- This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
861
-
862
- ## ๐Ÿ™‹โ€โ™‚๏ธ Support
863
-
864
- - ๐Ÿ“š [Documentation](https://github.com/dainprotocol/oauth2-token-manager#readme)
865
- - ๐Ÿ› [Issue Tracker](https://github.com/dainprotocol/oauth2-token-manager/issues)
866
- - ๐Ÿ’ฌ [Discussions](https://github.com/dainprotocol/oauth2-token-manager/discussions)
396
+ MIT ยฉ Dain Protocol