@rendomnet/apiservice 1.3.2 → 1.3.3
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 +106 -217
- package/dist/ApiKeyAuthProvider.d.ts +8 -0
- package/dist/ApiKeyAuthProvider.js +18 -0
- package/dist/BasicAuthProvider.d.ts +7 -0
- package/dist/BasicAuthProvider.js +14 -0
- package/dist/TokenAuthProvider.d.ts +12 -0
- package/dist/TokenAuthProvider.js +25 -0
- package/dist/components.d.ts +3 -0
- package/dist/components.js +7 -1
- package/dist/index.d.ts +6 -6
- package/dist/index.js +47 -31
- package/dist/types.d.ts +20 -1
- package/package.json +10 -12
package/README.md
CHANGED
|
@@ -2,14 +2,14 @@
|
|
|
2
2
|
|
|
3
3
|
A robust TypeScript API service framework for making authenticated API calls with advanced features:
|
|
4
4
|
|
|
5
|
-
- ✅
|
|
5
|
+
- ✅ Multiple authentication strategies (token, API key, basic auth, custom)
|
|
6
6
|
- ✅ Request caching with configurable time periods
|
|
7
7
|
- ✅ Advanced retry mechanisms with exponential backoff
|
|
8
8
|
- ✅ Status code-specific hooks for handling errors
|
|
9
9
|
- ✅ Account state tracking
|
|
10
10
|
- ✅ File upload support
|
|
11
11
|
- ✅ Support for multiple accounts or a single default account
|
|
12
|
-
- ✅ Automatic token refresh for 401 errors
|
|
12
|
+
- ✅ Automatic token refresh for 401 errors (if supported by provider)
|
|
13
13
|
|
|
14
14
|
## Installation
|
|
15
15
|
|
|
@@ -36,15 +36,28 @@ npm run test:watch
|
|
|
36
36
|
|
|
37
37
|
```typescript
|
|
38
38
|
import ApiService from 'apiservice';
|
|
39
|
+
import { TokenAuthProvider, ApiKeyAuthProvider, BasicAuthProvider } from 'apiservice';
|
|
40
|
+
|
|
41
|
+
// Token-based (OAuth2, etc.)
|
|
42
|
+
const tokenProvider = new TokenAuthProvider(myTokenService);
|
|
43
|
+
|
|
44
|
+
// API key in header
|
|
45
|
+
const apiKeyHeaderProvider = new ApiKeyAuthProvider({ apiKey: 'my-key', headerName: 'x-api-key' });
|
|
46
|
+
|
|
47
|
+
// API key in query param
|
|
48
|
+
const apiKeyQueryProvider = new ApiKeyAuthProvider({ apiKey: 'my-key', queryParamName: 'api_key' });
|
|
49
|
+
|
|
50
|
+
// Basic Auth
|
|
51
|
+
const basicProvider = new BasicAuthProvider({ username: 'user', password: 'pass' });
|
|
39
52
|
|
|
40
53
|
// Create and setup the API service
|
|
41
54
|
const api = new ApiService();
|
|
42
55
|
api.setup({
|
|
43
|
-
provider: 'my-service',
|
|
44
|
-
|
|
56
|
+
provider: 'my-service',
|
|
57
|
+
authProvider: tokenProvider, // or apiKeyHeaderProvider, apiKeyQueryProvider, basicProvider
|
|
45
58
|
hooks: {
|
|
46
59
|
// You can define custom hooks here,
|
|
47
|
-
// or use the default token refresh handler for 401 errors
|
|
60
|
+
// or use the default token refresh handler for 401 errors (if supported)
|
|
48
61
|
},
|
|
49
62
|
cacheTime: 30000, // 30 seconds
|
|
50
63
|
baseUrl: 'https://api.example.com' // Set default base URL
|
|
@@ -74,64 +87,86 @@ const defaultResult = await api.call({
|
|
|
74
87
|
});
|
|
75
88
|
```
|
|
76
89
|
|
|
77
|
-
##
|
|
78
|
-
|
|
79
|
-
ApiService includes a built-in handler for 401 (Unauthorized) errors that automatically refreshes OAuth tokens. This feature:
|
|
80
|
-
|
|
81
|
-
1. Detects 401 errors from the API
|
|
82
|
-
2. Retrieves the current token for the account
|
|
83
|
-
3. Uses the `refresh` method from your tokenService to obtain a new token
|
|
84
|
-
4. Updates the stored token with the new one
|
|
85
|
-
5. Retries the original API request with the new token
|
|
90
|
+
## Authentication Providers
|
|
86
91
|
|
|
87
|
-
|
|
92
|
+
ApiService supports multiple authentication strategies via the `AuthProvider` interface. You can use built-in providers or implement your own.
|
|
88
93
|
|
|
89
|
-
|
|
90
|
-
2. Don't specify a custom 401 hook (the default will be used automatically)
|
|
94
|
+
### TokenAuthProvider (OAuth2, Bearer Token)
|
|
91
95
|
|
|
92
96
|
```typescript
|
|
93
|
-
|
|
97
|
+
import { TokenAuthProvider } from 'apiservice';
|
|
98
|
+
|
|
94
99
|
const tokenService = {
|
|
95
100
|
async get(accountId = 'default') {
|
|
96
101
|
// Get token from storage
|
|
97
102
|
return storedToken;
|
|
98
103
|
},
|
|
99
|
-
|
|
100
104
|
async set(token, accountId = 'default') {
|
|
101
105
|
// Save token to storage
|
|
102
106
|
},
|
|
103
|
-
|
|
104
107
|
async refresh(refreshToken, accountId = 'default') {
|
|
105
108
|
// Refresh the token with your OAuth provider
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
headers: { 'Content-Type': 'application/json' },
|
|
109
|
-
body: JSON.stringify({
|
|
110
|
-
grant_type: 'refresh_token',
|
|
111
|
-
refresh_token: refreshToken,
|
|
112
|
-
client_id: 'your-client-id'
|
|
113
|
-
})
|
|
114
|
-
});
|
|
115
|
-
|
|
116
|
-
if (!response.ok) {
|
|
117
|
-
throw new Error('Failed to refresh token');
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
return await response.json();
|
|
109
|
+
// ...
|
|
110
|
+
return newToken;
|
|
121
111
|
}
|
|
122
112
|
};
|
|
113
|
+
|
|
114
|
+
const tokenProvider = new TokenAuthProvider(tokenService);
|
|
123
115
|
```
|
|
124
116
|
|
|
117
|
+
### ApiKeyAuthProvider (Header or Query Param)
|
|
118
|
+
|
|
119
|
+
```typescript
|
|
120
|
+
import { ApiKeyAuthProvider } from 'apiservice';
|
|
121
|
+
|
|
122
|
+
// API key in header
|
|
123
|
+
const apiKeyHeaderProvider = new ApiKeyAuthProvider({ apiKey: 'my-key', headerName: 'x-api-key' });
|
|
124
|
+
|
|
125
|
+
// API key in query param
|
|
126
|
+
const apiKeyQueryProvider = new ApiKeyAuthProvider({ apiKey: 'my-key', queryParamName: 'api_key' });
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
### BasicAuthProvider
|
|
130
|
+
|
|
131
|
+
```typescript
|
|
132
|
+
import { BasicAuthProvider } from 'apiservice';
|
|
133
|
+
|
|
134
|
+
const basicProvider = new BasicAuthProvider({ username: 'user', password: 'pass' });
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
### Custom AuthProvider
|
|
138
|
+
|
|
139
|
+
You can implement your own provider by implementing the `AuthProvider` interface:
|
|
140
|
+
|
|
141
|
+
```typescript
|
|
142
|
+
interface AuthProvider {
|
|
143
|
+
getAuthHeaders(accountId?: string): Promise<Record<string, string>>;
|
|
144
|
+
refresh?(refreshToken: string, accountId?: string): Promise<any>;
|
|
145
|
+
}
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
## Automatic Token Refresh
|
|
149
|
+
|
|
150
|
+
If your provider supports token refresh (like `TokenAuthProvider`), ApiService includes a built-in handler for 401 (Unauthorized) errors that automatically refreshes tokens. This feature:
|
|
151
|
+
|
|
152
|
+
1. Detects 401 errors from the API
|
|
153
|
+
2. Calls the provider's `refresh` method
|
|
154
|
+
3. Retries the original API request with the new token
|
|
155
|
+
|
|
156
|
+
To use this feature:
|
|
157
|
+
|
|
158
|
+
- Use a provider that implements `refresh` (like `TokenAuthProvider`)
|
|
159
|
+
- Don't specify a custom 401 hook (the default will be used automatically)
|
|
160
|
+
|
|
125
161
|
If you prefer to handle token refresh yourself, you can either:
|
|
126
162
|
|
|
127
163
|
1. Provide your own handler for 401 errors which will override the default
|
|
128
164
|
2. Disable the default handler by setting `hooks: { 401: null }`
|
|
129
165
|
|
|
130
166
|
```typescript
|
|
131
|
-
// Disable default 401 handler without providing a custom one
|
|
132
167
|
api.setup({
|
|
133
168
|
provider: 'my-service',
|
|
134
|
-
|
|
169
|
+
authProvider: tokenProvider,
|
|
135
170
|
hooks: {
|
|
136
171
|
401: null // Explicitly disable the default handler
|
|
137
172
|
},
|
|
@@ -159,169 +194,44 @@ const result = await api.call({
|
|
|
159
194
|
|
|
160
195
|
If no accountId is provided, ApiService automatically uses 'default' as the account ID.
|
|
161
196
|
|
|
162
|
-
##
|
|
163
|
-
|
|
164
|
-
ApiService requires a `tokenService` for authentication. This service manages tokens for different accounts and handles token retrieval, storage, and refresh operations.
|
|
165
|
-
|
|
166
|
-
### TokenService Interface
|
|
197
|
+
## AuthProvider Interface
|
|
167
198
|
|
|
168
199
|
```typescript
|
|
169
|
-
interface
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
// Save a token for an account
|
|
174
|
-
set: (token: Partial<Token>, accountId?: string) => Promise<void>;
|
|
175
|
-
|
|
176
|
-
// Optional: Refresh an expired token
|
|
177
|
-
refresh?: (refreshToken: string, accountId?: string) => Promise<OAuthToken>;
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
// The Token interface
|
|
181
|
-
interface Token {
|
|
182
|
-
accountId: string;
|
|
183
|
-
access_token: string;
|
|
184
|
-
refresh_token: string;
|
|
185
|
-
provider: string;
|
|
186
|
-
enabled?: boolean;
|
|
187
|
-
updatedAt?: string;
|
|
188
|
-
primary?: boolean;
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
// OAuth token response
|
|
192
|
-
interface OAuthToken {
|
|
193
|
-
access_token: string;
|
|
194
|
-
expires_in: number;
|
|
195
|
-
id_token: string;
|
|
196
|
-
refresh_token: string;
|
|
197
|
-
scope: string;
|
|
198
|
-
token_type: string;
|
|
200
|
+
interface AuthProvider {
|
|
201
|
+
getAuthHeaders(accountId?: string): Promise<Record<string, string>>;
|
|
202
|
+
refresh?(refreshToken: string, accountId?: string): Promise<any>;
|
|
199
203
|
}
|
|
200
204
|
```
|
|
201
205
|
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
Here's a simple `tokenService` implementation using localStorage:
|
|
205
|
-
|
|
206
|
-
```typescript
|
|
207
|
-
// Simple token service implementation
|
|
208
|
-
const tokenService = {
|
|
209
|
-
// Get token for an account
|
|
210
|
-
async get(accountId = 'default'): Promise<Token> {
|
|
211
|
-
const storedToken = localStorage.getItem(`token-${accountId}`);
|
|
212
|
-
if (!storedToken) {
|
|
213
|
-
throw new Error(`No token found for account ${accountId}`);
|
|
214
|
-
}
|
|
215
|
-
return JSON.parse(storedToken);
|
|
216
|
-
},
|
|
217
|
-
|
|
218
|
-
// Save token for an account
|
|
219
|
-
async set(token: Partial<Token>, accountId = 'default'): Promise<void> {
|
|
220
|
-
const existingToken = localStorage.getItem(`token-${accountId}`);
|
|
221
|
-
const currentToken = existingToken ? JSON.parse(existingToken) : { accountId };
|
|
222
|
-
const updatedToken = { ...currentToken, ...token, updatedAt: new Date().toISOString() };
|
|
223
|
-
localStorage.setItem(`token-${accountId}`, JSON.stringify(updatedToken));
|
|
224
|
-
},
|
|
225
|
-
|
|
226
|
-
// Refresh token implementation
|
|
227
|
-
async refresh(refreshToken: string, accountId = 'default'): Promise<OAuthToken> {
|
|
228
|
-
// Make a request to your OAuth token endpoint
|
|
229
|
-
const response = await fetch('https://api.example.com/oauth/token', {
|
|
230
|
-
method: 'POST',
|
|
231
|
-
headers: { 'Content-Type': 'application/json' },
|
|
232
|
-
body: JSON.stringify({
|
|
233
|
-
grant_type: 'refresh_token',
|
|
234
|
-
refresh_token: refreshToken,
|
|
235
|
-
client_id: 'your-client-id'
|
|
236
|
-
})
|
|
237
|
-
});
|
|
238
|
-
|
|
239
|
-
if (!response.ok) {
|
|
240
|
-
throw new Error('Failed to refresh token');
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
const newToken = await response.json();
|
|
244
|
-
|
|
245
|
-
// Update the stored token
|
|
246
|
-
await this.set({
|
|
247
|
-
access_token: newToken.access_token,
|
|
248
|
-
refresh_token: newToken.refresh_token
|
|
249
|
-
}, accountId);
|
|
250
|
-
|
|
251
|
-
return newToken;
|
|
252
|
-
}
|
|
253
|
-
};
|
|
254
|
-
```
|
|
255
|
-
|
|
256
|
-
### Complete Authorization Flow Example
|
|
257
|
-
|
|
258
|
-
Here's a complete example showing how to use ApiService with automatic token refresh:
|
|
206
|
+
## Example: Complete Authorization Flow (TokenAuthProvider)
|
|
259
207
|
|
|
260
208
|
```typescript
|
|
261
209
|
import ApiService from 'apiservice';
|
|
210
|
+
import { TokenAuthProvider } from 'apiservice';
|
|
262
211
|
|
|
263
|
-
// Create token service with refresh capability
|
|
264
212
|
const tokenService = {
|
|
265
|
-
// Get token from storage
|
|
266
213
|
async get(accountId = 'default') {
|
|
267
|
-
|
|
268
|
-
if (!storedToken) {
|
|
269
|
-
throw new Error(`No token found for account ${accountId}`);
|
|
270
|
-
}
|
|
271
|
-
return JSON.parse(storedToken);
|
|
214
|
+
// ...
|
|
272
215
|
},
|
|
273
|
-
|
|
274
|
-
// Save token to storage
|
|
275
216
|
async set(token, accountId = 'default') {
|
|
276
|
-
|
|
277
|
-
const currentToken = existingToken ? JSON.parse(existingToken) : { accountId };
|
|
278
|
-
const updatedToken = { ...currentToken, ...token, updatedAt: new Date().toISOString() };
|
|
279
|
-
localStorage.setItem(`token-${accountId}`, JSON.stringify(updatedToken));
|
|
217
|
+
// ...
|
|
280
218
|
},
|
|
281
|
-
|
|
282
|
-
// Refresh token with OAuth provider
|
|
283
219
|
async refresh(refreshToken, accountId = 'default') {
|
|
284
|
-
//
|
|
285
|
-
const response = await fetch('https://api.example.com/oauth/token', {
|
|
286
|
-
method: 'POST',
|
|
287
|
-
headers: { 'Content-Type': 'application/json' },
|
|
288
|
-
body: JSON.stringify({
|
|
289
|
-
grant_type: 'refresh_token',
|
|
290
|
-
refresh_token: refreshToken,
|
|
291
|
-
client_id: 'your-client-id'
|
|
292
|
-
})
|
|
293
|
-
});
|
|
294
|
-
|
|
295
|
-
if (!response.ok) {
|
|
296
|
-
throw new Error('Failed to refresh token');
|
|
297
|
-
}
|
|
298
|
-
|
|
299
|
-
return await response.json();
|
|
220
|
+
// ...
|
|
300
221
|
}
|
|
301
222
|
};
|
|
302
223
|
|
|
303
|
-
// Create API service instance
|
|
304
224
|
const api = new ApiService();
|
|
305
|
-
|
|
306
|
-
// Configure API service with automatic token refresh
|
|
307
225
|
api.setup({
|
|
308
226
|
provider: 'example-api',
|
|
309
|
-
tokenService,
|
|
227
|
+
authProvider: new TokenAuthProvider(tokenService),
|
|
310
228
|
cacheTime: 30000,
|
|
311
229
|
baseUrl: 'https://api.example.com',
|
|
312
|
-
|
|
313
|
-
// You can still add custom hooks for other status codes
|
|
314
|
-
// The default 401 handler will be used automatically
|
|
315
230
|
hooks: {
|
|
316
|
-
// Handle 403 Forbidden errors - typically for insufficient permissions
|
|
317
231
|
403: {
|
|
318
232
|
shouldRetry: false,
|
|
319
233
|
handler: async (accountId, response) => {
|
|
320
|
-
|
|
321
|
-
// You could trigger a permissions UI here
|
|
322
|
-
window.dispatchEvent(new CustomEvent('permission:required', {
|
|
323
|
-
detail: { accountId, resource: response.resource }
|
|
324
|
-
}));
|
|
234
|
+
// ...
|
|
325
235
|
return null;
|
|
326
236
|
}
|
|
327
237
|
}
|
|
@@ -330,19 +240,11 @@ api.setup({
|
|
|
330
240
|
|
|
331
241
|
// Use the API service
|
|
332
242
|
async function fetchUserData(userId) {
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
useAuth: true
|
|
339
|
-
});
|
|
340
|
-
} catch (error) {
|
|
341
|
-
// 401 errors with valid refresh tokens will be automatically handled
|
|
342
|
-
// This catch will only trigger for other errors or if refresh fails
|
|
343
|
-
console.error('Failed to fetch user data:', error);
|
|
344
|
-
throw error;
|
|
345
|
-
}
|
|
243
|
+
return await api.call({
|
|
244
|
+
method: 'GET',
|
|
245
|
+
route: `/users/${userId}`,
|
|
246
|
+
useAuth: true
|
|
247
|
+
});
|
|
346
248
|
}
|
|
347
249
|
```
|
|
348
250
|
|
|
@@ -353,38 +255,24 @@ Hooks can be configured to handle specific HTTP status codes:
|
|
|
353
255
|
```typescript
|
|
354
256
|
const hooks = {
|
|
355
257
|
401: {
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
// Advanced options
|
|
362
|
-
preventConcurrentCalls: true, // Wait for an existing hook to complete before starting a new one
|
|
363
|
-
// Useful for avoiding duplicate refresh token calls
|
|
364
|
-
|
|
365
|
-
// Handler functions
|
|
258
|
+
shouldRetry: true,
|
|
259
|
+
useRetryDelay: true,
|
|
260
|
+
maxRetries: 3,
|
|
261
|
+
preventConcurrentCalls: true,
|
|
366
262
|
handler: async (accountId, response) => {
|
|
367
|
-
//
|
|
368
|
-
// Return an object to update the API call parameters for the retry
|
|
263
|
+
// ...
|
|
369
264
|
return { /* updated parameters */ };
|
|
370
265
|
},
|
|
371
|
-
|
|
372
266
|
onMaxRetriesExceeded: async (accountId, error) => {
|
|
373
|
-
//
|
|
267
|
+
// ...
|
|
374
268
|
},
|
|
375
|
-
|
|
376
269
|
onHandlerError: async (accountId, error) => {
|
|
377
|
-
//
|
|
270
|
+
// ...
|
|
378
271
|
},
|
|
379
|
-
|
|
380
|
-
// Delay strategy settings
|
|
381
272
|
delayStrategy: {
|
|
382
|
-
calculate: (attempt, response) =>
|
|
383
|
-
// Custom strategy for calculating delay between retries
|
|
384
|
-
return 1000 * Math.pow(2, attempt - 1); // Exponential backoff
|
|
385
|
-
}
|
|
273
|
+
calculate: (attempt, response) => 1000 * Math.pow(2, attempt - 1)
|
|
386
274
|
},
|
|
387
|
-
maxDelay: 30000
|
|
275
|
+
maxDelay: 30000
|
|
388
276
|
}
|
|
389
277
|
}
|
|
390
278
|
```
|
|
@@ -403,18 +291,23 @@ The codebase is built around a main `ApiService` class that coordinates several
|
|
|
403
291
|
|
|
404
292
|
### Multiple API Providers
|
|
405
293
|
|
|
406
|
-
```
|
|
407
|
-
|
|
294
|
+
```typescript
|
|
295
|
+
import { ApiService, TokenAuthProvider, ApiKeyAuthProvider } from 'apiservice';
|
|
296
|
+
|
|
297
|
+
const primaryProvider = new TokenAuthProvider(primaryTokenService);
|
|
298
|
+
const secondaryProvider = new ApiKeyAuthProvider({ apiKey: 'secondary-key', headerName: 'x-api-key' });
|
|
299
|
+
|
|
300
|
+
const api = new ApiService();
|
|
408
301
|
api.setup({
|
|
409
302
|
provider: 'primary-api',
|
|
410
|
-
|
|
303
|
+
authProvider: primaryProvider,
|
|
411
304
|
cacheTime: 30000,
|
|
412
305
|
baseUrl: 'https://api.primary.com'
|
|
413
306
|
});
|
|
414
307
|
|
|
415
308
|
api.setup({
|
|
416
309
|
provider: 'secondary-api',
|
|
417
|
-
|
|
310
|
+
authProvider: secondaryProvider,
|
|
418
311
|
cacheTime: 60000,
|
|
419
312
|
baseUrl: 'https://api.secondary.com'
|
|
420
313
|
});
|
|
@@ -428,15 +321,12 @@ async function fetchCombinedData() {
|
|
|
428
321
|
route: '/data',
|
|
429
322
|
useAuth: true
|
|
430
323
|
}),
|
|
431
|
-
|
|
432
324
|
api.call({
|
|
433
325
|
provider: 'secondary-api',
|
|
434
326
|
method: 'GET',
|
|
435
327
|
route: '/data',
|
|
436
328
|
useAuth: true
|
|
437
329
|
}),
|
|
438
|
-
|
|
439
|
-
// Override baseUrl for a specific API call
|
|
440
330
|
api.call({
|
|
441
331
|
provider: 'primary-api',
|
|
442
332
|
method: 'GET',
|
|
@@ -445,7 +335,6 @@ async function fetchCombinedData() {
|
|
|
445
335
|
base: 'https://special-api.primary.com'
|
|
446
336
|
})
|
|
447
337
|
]);
|
|
448
|
-
|
|
449
338
|
return { primaryData, secondaryData };
|
|
450
339
|
}
|
|
451
340
|
```
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { AuthProvider, ApiKeyAuthProviderOptions } from './types';
|
|
2
|
+
export declare class ApiKeyAuthProvider implements AuthProvider {
|
|
3
|
+
private apiKey;
|
|
4
|
+
private headerName?;
|
|
5
|
+
private queryParamName?;
|
|
6
|
+
constructor(options: ApiKeyAuthProviderOptions);
|
|
7
|
+
getAuthHeaders(): Promise<Record<string, string>>;
|
|
8
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.ApiKeyAuthProvider = void 0;
|
|
4
|
+
class ApiKeyAuthProvider {
|
|
5
|
+
constructor(options) {
|
|
6
|
+
this.apiKey = options.apiKey;
|
|
7
|
+
this.headerName = options.headerName;
|
|
8
|
+
this.queryParamName = options.queryParamName;
|
|
9
|
+
}
|
|
10
|
+
async getAuthHeaders() {
|
|
11
|
+
if (this.headerName) {
|
|
12
|
+
return { [this.headerName]: this.apiKey };
|
|
13
|
+
}
|
|
14
|
+
// If using query param, return empty headers (handled elsewhere)
|
|
15
|
+
return {};
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
exports.ApiKeyAuthProvider = ApiKeyAuthProvider;
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { AuthProvider, BasicAuthProviderOptions } from './types';
|
|
2
|
+
export declare class BasicAuthProvider implements AuthProvider {
|
|
3
|
+
private username;
|
|
4
|
+
private password;
|
|
5
|
+
constructor(options: BasicAuthProviderOptions);
|
|
6
|
+
getAuthHeaders(): Promise<Record<string, string>>;
|
|
7
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.BasicAuthProvider = void 0;
|
|
4
|
+
class BasicAuthProvider {
|
|
5
|
+
constructor(options) {
|
|
6
|
+
this.username = options.username;
|
|
7
|
+
this.password = options.password;
|
|
8
|
+
}
|
|
9
|
+
async getAuthHeaders() {
|
|
10
|
+
const encoded = Buffer.from(`${this.username}:${this.password}`).toString('base64');
|
|
11
|
+
return { Authorization: `Basic ${encoded}` };
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
exports.BasicAuthProvider = BasicAuthProvider;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { AuthProvider, Token, OAuthToken } from './types';
|
|
2
|
+
export type TokenService = {
|
|
3
|
+
get: (accountId?: string) => Promise<Token>;
|
|
4
|
+
set: (token: Partial<Token>, accountId?: string) => Promise<void>;
|
|
5
|
+
refresh?: (refreshToken: string, accountId?: string) => Promise<OAuthToken>;
|
|
6
|
+
};
|
|
7
|
+
export declare class TokenAuthProvider implements AuthProvider {
|
|
8
|
+
private tokenService;
|
|
9
|
+
constructor(tokenService: TokenService);
|
|
10
|
+
getAuthHeaders(accountId?: string): Promise<Record<string, string>>;
|
|
11
|
+
refresh(refreshToken: string, accountId?: string): Promise<any>;
|
|
12
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.TokenAuthProvider = void 0;
|
|
4
|
+
class TokenAuthProvider {
|
|
5
|
+
constructor(tokenService) {
|
|
6
|
+
this.tokenService = tokenService;
|
|
7
|
+
}
|
|
8
|
+
async getAuthHeaders(accountId) {
|
|
9
|
+
const token = await this.tokenService.get(accountId);
|
|
10
|
+
if (!(token === null || token === void 0 ? void 0 : token.access_token))
|
|
11
|
+
return {};
|
|
12
|
+
return { Authorization: `Bearer ${token.access_token}` };
|
|
13
|
+
}
|
|
14
|
+
async refresh(refreshToken, accountId) {
|
|
15
|
+
if (!this.tokenService.refresh)
|
|
16
|
+
throw new Error('Refresh not supported');
|
|
17
|
+
const newToken = await this.tokenService.refresh(refreshToken, accountId);
|
|
18
|
+
await this.tokenService.set({
|
|
19
|
+
access_token: newToken.access_token,
|
|
20
|
+
refresh_token: newToken.refresh_token || refreshToken,
|
|
21
|
+
}, accountId);
|
|
22
|
+
return newToken;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
exports.TokenAuthProvider = TokenAuthProvider;
|
package/dist/components.d.ts
CHANGED
|
@@ -6,3 +6,6 @@ export { RetryManager } from './RetryManager';
|
|
|
6
6
|
export { HookManager } from './HookManager';
|
|
7
7
|
export { HttpClient } from './HttpClient';
|
|
8
8
|
export { AccountManager } from './AccountManager';
|
|
9
|
+
export { TokenAuthProvider } from './TokenAuthProvider';
|
|
10
|
+
export { ApiKeyAuthProvider } from './ApiKeyAuthProvider';
|
|
11
|
+
export { BasicAuthProvider } from './BasicAuthProvider';
|
package/dist/components.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* Barrel file exporting all ApiService components
|
|
4
4
|
*/
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
-
exports.AccountManager = exports.HttpClient = exports.HookManager = exports.RetryManager = exports.CacheManager = void 0;
|
|
6
|
+
exports.BasicAuthProvider = exports.ApiKeyAuthProvider = exports.TokenAuthProvider = exports.AccountManager = exports.HttpClient = exports.HookManager = exports.RetryManager = exports.CacheManager = void 0;
|
|
7
7
|
var CacheManager_1 = require("./CacheManager");
|
|
8
8
|
Object.defineProperty(exports, "CacheManager", { enumerable: true, get: function () { return CacheManager_1.CacheManager; } });
|
|
9
9
|
var RetryManager_1 = require("./RetryManager");
|
|
@@ -14,3 +14,9 @@ var HttpClient_1 = require("./HttpClient");
|
|
|
14
14
|
Object.defineProperty(exports, "HttpClient", { enumerable: true, get: function () { return HttpClient_1.HttpClient; } });
|
|
15
15
|
var AccountManager_1 = require("./AccountManager");
|
|
16
16
|
Object.defineProperty(exports, "AccountManager", { enumerable: true, get: function () { return AccountManager_1.AccountManager; } });
|
|
17
|
+
var TokenAuthProvider_1 = require("./TokenAuthProvider");
|
|
18
|
+
Object.defineProperty(exports, "TokenAuthProvider", { enumerable: true, get: function () { return TokenAuthProvider_1.TokenAuthProvider; } });
|
|
19
|
+
var ApiKeyAuthProvider_1 = require("./ApiKeyAuthProvider");
|
|
20
|
+
Object.defineProperty(exports, "ApiKeyAuthProvider", { enumerable: true, get: function () { return ApiKeyAuthProvider_1.ApiKeyAuthProvider; } });
|
|
21
|
+
var BasicAuthProvider_1 = require("./BasicAuthProvider");
|
|
22
|
+
Object.defineProperty(exports, "BasicAuthProvider", { enumerable: true, get: function () { return BasicAuthProvider_1.BasicAuthProvider; } });
|
package/dist/index.d.ts
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { AuthProvider, ApiCallParams, HookSettings, StatusCode } from './types';
|
|
2
2
|
/**
|
|
3
3
|
* ApiService - Core API service for making authenticated API calls
|
|
4
4
|
* with caching, retry, and hook support.
|
|
5
5
|
*/
|
|
6
6
|
declare class ApiService {
|
|
7
7
|
provider: string;
|
|
8
|
-
private
|
|
8
|
+
private authProvider;
|
|
9
9
|
private baseUrl;
|
|
10
10
|
private cacheManager;
|
|
11
11
|
private retryManager;
|
|
@@ -17,18 +17,18 @@ declare class ApiService {
|
|
|
17
17
|
/**
|
|
18
18
|
* Setup the API service
|
|
19
19
|
*/
|
|
20
|
-
setup({ provider,
|
|
20
|
+
setup({ provider, authProvider, hooks, cacheTime, baseUrl, }: {
|
|
21
21
|
provider: string;
|
|
22
|
-
|
|
22
|
+
authProvider: AuthProvider;
|
|
23
23
|
hooks?: Record<StatusCode, HookSettings | null>;
|
|
24
24
|
cacheTime: number;
|
|
25
25
|
baseUrl?: string;
|
|
26
26
|
}): void;
|
|
27
27
|
/**
|
|
28
28
|
* Create a default handler for 401 (Unauthorized) errors
|
|
29
|
-
* that implements standard
|
|
29
|
+
* that implements standard credential refresh behavior
|
|
30
30
|
*/
|
|
31
|
-
private
|
|
31
|
+
private createDefaultAuthRefreshHandler;
|
|
32
32
|
/**
|
|
33
33
|
* Set the maximum number of retry attempts
|
|
34
34
|
*/
|
package/dist/index.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const ApiKeyAuthProvider_1 = require("./ApiKeyAuthProvider");
|
|
3
4
|
const components_1 = require("./components");
|
|
4
5
|
/**
|
|
5
6
|
* ApiService - Core API service for making authenticated API calls
|
|
@@ -11,7 +12,7 @@ class ApiService {
|
|
|
11
12
|
// Default max attempts for API calls
|
|
12
13
|
this.maxAttempts = 10;
|
|
13
14
|
this.provider = '';
|
|
14
|
-
this.
|
|
15
|
+
this.authProvider = {};
|
|
15
16
|
// Initialize component managers
|
|
16
17
|
this.cacheManager = new components_1.CacheManager();
|
|
17
18
|
this.retryManager = new components_1.RetryManager();
|
|
@@ -22,17 +23,17 @@ class ApiService {
|
|
|
22
23
|
/**
|
|
23
24
|
* Setup the API service
|
|
24
25
|
*/
|
|
25
|
-
setup({ provider,
|
|
26
|
+
setup({ provider, authProvider, hooks = {}, cacheTime, baseUrl = '', }) {
|
|
26
27
|
this.provider = provider;
|
|
27
|
-
this.
|
|
28
|
+
this.authProvider = authProvider;
|
|
28
29
|
this.baseUrl = baseUrl;
|
|
29
30
|
// Create a copy of hooks to avoid modifying the input
|
|
30
31
|
const finalHooks = {};
|
|
31
32
|
// Apply default 401 handler if:
|
|
32
33
|
// 1. No 401 hook is explicitly defined (or is explicitly null)
|
|
33
|
-
// 2.
|
|
34
|
-
if (hooks[401] === undefined && typeof this.
|
|
35
|
-
finalHooks[401] = this.
|
|
34
|
+
// 2. AuthProvider has a refresh method
|
|
35
|
+
if (hooks[401] === undefined && typeof this.authProvider.refresh === 'function') {
|
|
36
|
+
finalHooks[401] = this.createDefaultAuthRefreshHandler();
|
|
36
37
|
}
|
|
37
38
|
// Add user-defined hooks (skipping null/undefined values)
|
|
38
39
|
for (const [statusCode, hook] of Object.entries(hooks)) {
|
|
@@ -50,9 +51,9 @@ class ApiService {
|
|
|
50
51
|
}
|
|
51
52
|
/**
|
|
52
53
|
* Create a default handler for 401 (Unauthorized) errors
|
|
53
|
-
* that implements standard
|
|
54
|
+
* that implements standard credential refresh behavior
|
|
54
55
|
*/
|
|
55
|
-
|
|
56
|
+
createDefaultAuthRefreshHandler() {
|
|
56
57
|
return {
|
|
57
58
|
shouldRetry: true,
|
|
58
59
|
useRetryDelay: true,
|
|
@@ -60,27 +61,17 @@ class ApiService {
|
|
|
60
61
|
maxRetries: 1,
|
|
61
62
|
handler: async (accountId) => {
|
|
62
63
|
try {
|
|
63
|
-
console.log(`🔄 Using default
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
if (!currentToken.refresh_token) {
|
|
67
|
-
throw new Error(`No refresh token available for account ${accountId}`);
|
|
64
|
+
console.log(`🔄 Using default auth refresh handler for ${accountId}`);
|
|
65
|
+
if (!this.authProvider.refresh) {
|
|
66
|
+
throw new Error('No refresh method available on auth provider');
|
|
68
67
|
}
|
|
69
|
-
//
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
throw new Error('Token refresh returned invalid data');
|
|
73
|
-
}
|
|
74
|
-
// Update the token in storage
|
|
75
|
-
await this.tokenService.set({
|
|
76
|
-
access_token: newToken.access_token,
|
|
77
|
-
refresh_token: newToken.refresh_token || currentToken.refresh_token
|
|
78
|
-
}, accountId);
|
|
79
|
-
// Return empty object to retry with same parameters
|
|
68
|
+
// You may want to store refresh token in account data or pass it in another way
|
|
69
|
+
// For now, assume refresh token is managed internally by the provider
|
|
70
|
+
await this.authProvider.refresh('', accountId);
|
|
80
71
|
return {};
|
|
81
72
|
}
|
|
82
73
|
catch (error) {
|
|
83
|
-
console.error(`
|
|
74
|
+
console.error(`Auth refresh failed for ${accountId}:`, error);
|
|
84
75
|
throw error;
|
|
85
76
|
}
|
|
86
77
|
},
|
|
@@ -112,6 +103,15 @@ class ApiService {
|
|
|
112
103
|
accountId: apiCallParams.accountId || 'default',
|
|
113
104
|
base: apiCallParams.base || this.baseUrl,
|
|
114
105
|
};
|
|
106
|
+
// If using ApiKeyAuthProvider with queryParamName, add API key to queryParams
|
|
107
|
+
if (this.authProvider instanceof ApiKeyAuthProvider_1.ApiKeyAuthProvider &&
|
|
108
|
+
this.authProvider.queryParamName) {
|
|
109
|
+
const queryParamName = this.authProvider.queryParamName;
|
|
110
|
+
const apiKey = this.authProvider.apiKey;
|
|
111
|
+
const urlParams = params.queryParams ? new URLSearchParams(params.queryParams) : new URLSearchParams();
|
|
112
|
+
urlParams.set(queryParamName, apiKey);
|
|
113
|
+
params.queryParams = urlParams;
|
|
114
|
+
}
|
|
115
115
|
console.log('🔄 API call', this.provider, params.accountId, params.method, params.route);
|
|
116
116
|
// Check cache first
|
|
117
117
|
const cachedData = this.cacheManager.getFromCache(params);
|
|
@@ -138,22 +138,38 @@ class ApiService {
|
|
|
138
138
|
const { accountId } = apiCallParams;
|
|
139
139
|
let attempts = 0;
|
|
140
140
|
const statusRetries = {};
|
|
141
|
-
// Copy the params to avoid mutation issues
|
|
142
141
|
let currentParams = { ...apiCallParams };
|
|
142
|
+
// If using ApiKeyAuthProvider with queryParamName, add API key to queryParams
|
|
143
|
+
if (this.authProvider instanceof ApiKeyAuthProvider_1.ApiKeyAuthProvider &&
|
|
144
|
+
this.authProvider.queryParamName) {
|
|
145
|
+
const queryParamName = this.authProvider.queryParamName;
|
|
146
|
+
const apiKey = this.authProvider.apiKey;
|
|
147
|
+
const urlParams = currentParams.queryParams ? new URLSearchParams(currentParams.queryParams) : new URLSearchParams();
|
|
148
|
+
urlParams.set(queryParamName, apiKey);
|
|
149
|
+
currentParams.queryParams = urlParams;
|
|
150
|
+
}
|
|
143
151
|
// Main retry loop
|
|
144
152
|
while (attempts < this.maxAttempts) {
|
|
145
153
|
attempts++;
|
|
146
154
|
try {
|
|
147
|
-
// Get authentication
|
|
148
|
-
const
|
|
149
|
-
? await this.
|
|
155
|
+
// Get authentication headers if needed
|
|
156
|
+
const authHeaders = apiCallParams.useAuth !== false
|
|
157
|
+
? await this.authProvider.getAuthHeaders(accountId)
|
|
150
158
|
: {};
|
|
159
|
+
// Merge auth headers into params.headers
|
|
160
|
+
currentParams.headers = {
|
|
161
|
+
...(currentParams.headers || {}),
|
|
162
|
+
...authHeaders,
|
|
163
|
+
};
|
|
151
164
|
// Verify we have authentication if required
|
|
152
|
-
if (apiCallParams.useAuth !== false &&
|
|
165
|
+
if (apiCallParams.useAuth !== false &&
|
|
166
|
+
Object.keys(authHeaders).length === 0 &&
|
|
167
|
+
!(this.authProvider instanceof ApiKeyAuthProvider_1.ApiKeyAuthProvider &&
|
|
168
|
+
this.authProvider.queryParamName)) {
|
|
153
169
|
throw new Error(`${this.provider} credentials not found for account ID ${accountId}`);
|
|
154
170
|
}
|
|
155
171
|
// Make the actual API call
|
|
156
|
-
const response = await this.httpClient.makeRequest(currentParams,
|
|
172
|
+
const response = await this.httpClient.makeRequest(currentParams, {});
|
|
157
173
|
// Success - update account status and return result
|
|
158
174
|
this.accountManager.setLastRequestFailed(accountId, false);
|
|
159
175
|
return response;
|
package/dist/types.d.ts
CHANGED
|
@@ -80,9 +80,28 @@ interface HookSettings {
|
|
|
80
80
|
maxDelay?: number;
|
|
81
81
|
}
|
|
82
82
|
type StatusCode = string | number;
|
|
83
|
+
interface AuthProvider {
|
|
84
|
+
/**
|
|
85
|
+
* Returns headers or other auth data for a request
|
|
86
|
+
*/
|
|
87
|
+
getAuthHeaders(accountId?: string): Promise<Record<string, string>>;
|
|
88
|
+
/**
|
|
89
|
+
* Optional: refresh credentials if supported (for OAuth, etc.)
|
|
90
|
+
*/
|
|
91
|
+
refresh?(refreshToken: string, accountId?: string): Promise<any>;
|
|
92
|
+
}
|
|
93
|
+
interface ApiKeyAuthProviderOptions {
|
|
94
|
+
apiKey: string;
|
|
95
|
+
headerName?: string;
|
|
96
|
+
queryParamName?: string;
|
|
97
|
+
}
|
|
98
|
+
interface BasicAuthProviderOptions {
|
|
99
|
+
username: string;
|
|
100
|
+
password: string;
|
|
101
|
+
}
|
|
83
102
|
type TokenService = {
|
|
84
103
|
get: (accountId?: string) => Promise<Token>;
|
|
85
104
|
set: (token: Partial<Token>, accountId?: string) => Promise<void>;
|
|
86
105
|
refresh?: (refreshToken: string, accountId?: string) => Promise<OAuthToken>;
|
|
87
106
|
};
|
|
88
|
-
export { OAuthToken, DelayStrategy, Token, AccountData, ApiCallParams, HookSettings, StatusCode, TokenService, };
|
|
107
|
+
export { OAuthToken, DelayStrategy, Token, AccountData, ApiCallParams, HookSettings, StatusCode, AuthProvider, ApiKeyAuthProviderOptions, BasicAuthProviderOptions, TokenService, };
|
package/package.json
CHANGED
|
@@ -1,22 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rendomnet/apiservice",
|
|
3
|
-
"version": "1.3.
|
|
3
|
+
"version": "1.3.3",
|
|
4
4
|
"description": "A robust TypeScript API service framework for making authenticated API calls",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
7
7
|
"files": [
|
|
8
8
|
"dist"
|
|
9
9
|
],
|
|
10
|
-
"scripts": {
|
|
11
|
-
"build": "tsc",
|
|
12
|
-
"clean": "rimraf dist",
|
|
13
|
-
"prebuild": "npm run clean",
|
|
14
|
-
"prepublishOnly": "npm run build",
|
|
15
|
-
"test": "jest",
|
|
16
|
-
"test:watch": "jest --watch",
|
|
17
|
-
"test:coverage": "jest --coverage",
|
|
18
|
-
"prepare": "npm run build"
|
|
19
|
-
},
|
|
20
10
|
"keywords": [
|
|
21
11
|
"api",
|
|
22
12
|
"service",
|
|
@@ -50,5 +40,13 @@
|
|
|
50
40
|
},
|
|
51
41
|
"publishConfig": {
|
|
52
42
|
"access": "public"
|
|
43
|
+
},
|
|
44
|
+
"scripts": {
|
|
45
|
+
"build": "tsc",
|
|
46
|
+
"clean": "rimraf dist",
|
|
47
|
+
"prebuild": "npm run clean",
|
|
48
|
+
"test": "jest",
|
|
49
|
+
"test:watch": "jest --watch",
|
|
50
|
+
"test:coverage": "jest --coverage"
|
|
53
51
|
}
|
|
54
|
-
}
|
|
52
|
+
}
|