@rendomnet/apiservice 1.3.1 → 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 +127 -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 +13 -10
package/README.md
CHANGED
|
@@ -2,28 +2,62 @@
|
|
|
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
|
+
|
|
14
|
+
## Installation
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
npm install @rendomnet/apiservice
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
## Testing
|
|
21
|
+
|
|
22
|
+
ApiService includes a comprehensive test suite using Jest. To run the tests:
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
# Run tests
|
|
26
|
+
npm test
|
|
27
|
+
|
|
28
|
+
# Run tests with coverage report
|
|
29
|
+
npm run test:coverage
|
|
30
|
+
|
|
31
|
+
# Run tests in watch mode during development
|
|
32
|
+
npm run test:watch
|
|
33
|
+
```
|
|
13
34
|
|
|
14
35
|
## Usage
|
|
15
36
|
|
|
16
37
|
```typescript
|
|
17
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' });
|
|
18
52
|
|
|
19
53
|
// Create and setup the API service
|
|
20
54
|
const api = new ApiService();
|
|
21
55
|
api.setup({
|
|
22
|
-
provider: 'my-service',
|
|
23
|
-
|
|
56
|
+
provider: 'my-service',
|
|
57
|
+
authProvider: tokenProvider, // or apiKeyHeaderProvider, apiKeyQueryProvider, basicProvider
|
|
24
58
|
hooks: {
|
|
25
59
|
// You can define custom hooks here,
|
|
26
|
-
// or use the default token refresh handler for 401 errors
|
|
60
|
+
// or use the default token refresh handler for 401 errors (if supported)
|
|
27
61
|
},
|
|
28
62
|
cacheTime: 30000, // 30 seconds
|
|
29
63
|
baseUrl: 'https://api.example.com' // Set default base URL
|
|
@@ -53,64 +87,86 @@ const defaultResult = await api.call({
|
|
|
53
87
|
});
|
|
54
88
|
```
|
|
55
89
|
|
|
56
|
-
##
|
|
57
|
-
|
|
58
|
-
ApiService includes a built-in handler for 401 (Unauthorized) errors that automatically refreshes OAuth tokens. This feature:
|
|
90
|
+
## Authentication Providers
|
|
59
91
|
|
|
60
|
-
|
|
61
|
-
2. Retrieves the current token for the account
|
|
62
|
-
3. Uses the `refresh` method from your tokenService to obtain a new token
|
|
63
|
-
4. Updates the stored token with the new one
|
|
64
|
-
5. Retries the original API request with the new token
|
|
92
|
+
ApiService supports multiple authentication strategies via the `AuthProvider` interface. You can use built-in providers or implement your own.
|
|
65
93
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
1. Ensure your tokenService implements the `refresh` method
|
|
69
|
-
2. Don't specify a custom 401 hook (the default will be used automatically)
|
|
94
|
+
### TokenAuthProvider (OAuth2, Bearer Token)
|
|
70
95
|
|
|
71
96
|
```typescript
|
|
72
|
-
|
|
97
|
+
import { TokenAuthProvider } from 'apiservice';
|
|
98
|
+
|
|
73
99
|
const tokenService = {
|
|
74
100
|
async get(accountId = 'default') {
|
|
75
101
|
// Get token from storage
|
|
76
102
|
return storedToken;
|
|
77
103
|
},
|
|
78
|
-
|
|
79
104
|
async set(token, accountId = 'default') {
|
|
80
105
|
// Save token to storage
|
|
81
106
|
},
|
|
82
|
-
|
|
83
107
|
async refresh(refreshToken, accountId = 'default') {
|
|
84
108
|
// Refresh the token with your OAuth provider
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
headers: { 'Content-Type': 'application/json' },
|
|
88
|
-
body: JSON.stringify({
|
|
89
|
-
grant_type: 'refresh_token',
|
|
90
|
-
refresh_token: refreshToken,
|
|
91
|
-
client_id: 'your-client-id'
|
|
92
|
-
})
|
|
93
|
-
});
|
|
94
|
-
|
|
95
|
-
if (!response.ok) {
|
|
96
|
-
throw new Error('Failed to refresh token');
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
return await response.json();
|
|
109
|
+
// ...
|
|
110
|
+
return newToken;
|
|
100
111
|
}
|
|
101
112
|
};
|
|
113
|
+
|
|
114
|
+
const tokenProvider = new TokenAuthProvider(tokenService);
|
|
115
|
+
```
|
|
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
|
+
}
|
|
102
146
|
```
|
|
103
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
|
+
|
|
104
161
|
If you prefer to handle token refresh yourself, you can either:
|
|
105
162
|
|
|
106
163
|
1. Provide your own handler for 401 errors which will override the default
|
|
107
164
|
2. Disable the default handler by setting `hooks: { 401: null }`
|
|
108
165
|
|
|
109
166
|
```typescript
|
|
110
|
-
// Disable default 401 handler without providing a custom one
|
|
111
167
|
api.setup({
|
|
112
168
|
provider: 'my-service',
|
|
113
|
-
|
|
169
|
+
authProvider: tokenProvider,
|
|
114
170
|
hooks: {
|
|
115
171
|
401: null // Explicitly disable the default handler
|
|
116
172
|
},
|
|
@@ -138,169 +194,44 @@ const result = await api.call({
|
|
|
138
194
|
|
|
139
195
|
If no accountId is provided, ApiService automatically uses 'default' as the account ID.
|
|
140
196
|
|
|
141
|
-
##
|
|
142
|
-
|
|
143
|
-
ApiService requires a `tokenService` for authentication. This service manages tokens for different accounts and handles token retrieval, storage, and refresh operations.
|
|
144
|
-
|
|
145
|
-
### TokenService Interface
|
|
197
|
+
## AuthProvider Interface
|
|
146
198
|
|
|
147
199
|
```typescript
|
|
148
|
-
interface
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
// Save a token for an account
|
|
153
|
-
set: (token: Partial<Token>, accountId?: string) => Promise<void>;
|
|
154
|
-
|
|
155
|
-
// Optional: Refresh an expired token
|
|
156
|
-
refresh?: (refreshToken: string, accountId?: string) => Promise<OAuthToken>;
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
// The Token interface
|
|
160
|
-
interface Token {
|
|
161
|
-
accountId: string;
|
|
162
|
-
access_token: string;
|
|
163
|
-
refresh_token: string;
|
|
164
|
-
provider: string;
|
|
165
|
-
enabled?: boolean;
|
|
166
|
-
updatedAt?: string;
|
|
167
|
-
primary?: boolean;
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
// OAuth token response
|
|
171
|
-
interface OAuthToken {
|
|
172
|
-
access_token: string;
|
|
173
|
-
expires_in: number;
|
|
174
|
-
id_token: string;
|
|
175
|
-
refresh_token: string;
|
|
176
|
-
scope: string;
|
|
177
|
-
token_type: string;
|
|
200
|
+
interface AuthProvider {
|
|
201
|
+
getAuthHeaders(accountId?: string): Promise<Record<string, string>>;
|
|
202
|
+
refresh?(refreshToken: string, accountId?: string): Promise<any>;
|
|
178
203
|
}
|
|
179
204
|
```
|
|
180
205
|
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
Here's a simple `tokenService` implementation using localStorage:
|
|
184
|
-
|
|
185
|
-
```typescript
|
|
186
|
-
// Simple token service implementation
|
|
187
|
-
const tokenService = {
|
|
188
|
-
// Get token for an account
|
|
189
|
-
async get(accountId = 'default'): Promise<Token> {
|
|
190
|
-
const storedToken = localStorage.getItem(`token-${accountId}`);
|
|
191
|
-
if (!storedToken) {
|
|
192
|
-
throw new Error(`No token found for account ${accountId}`);
|
|
193
|
-
}
|
|
194
|
-
return JSON.parse(storedToken);
|
|
195
|
-
},
|
|
196
|
-
|
|
197
|
-
// Save token for an account
|
|
198
|
-
async set(token: Partial<Token>, accountId = 'default'): Promise<void> {
|
|
199
|
-
const existingToken = localStorage.getItem(`token-${accountId}`);
|
|
200
|
-
const currentToken = existingToken ? JSON.parse(existingToken) : { accountId };
|
|
201
|
-
const updatedToken = { ...currentToken, ...token, updatedAt: new Date().toISOString() };
|
|
202
|
-
localStorage.setItem(`token-${accountId}`, JSON.stringify(updatedToken));
|
|
203
|
-
},
|
|
204
|
-
|
|
205
|
-
// Refresh token implementation
|
|
206
|
-
async refresh(refreshToken: string, accountId = 'default'): Promise<OAuthToken> {
|
|
207
|
-
// Make a request to your OAuth token endpoint
|
|
208
|
-
const response = await fetch('https://api.example.com/oauth/token', {
|
|
209
|
-
method: 'POST',
|
|
210
|
-
headers: { 'Content-Type': 'application/json' },
|
|
211
|
-
body: JSON.stringify({
|
|
212
|
-
grant_type: 'refresh_token',
|
|
213
|
-
refresh_token: refreshToken,
|
|
214
|
-
client_id: 'your-client-id'
|
|
215
|
-
})
|
|
216
|
-
});
|
|
217
|
-
|
|
218
|
-
if (!response.ok) {
|
|
219
|
-
throw new Error('Failed to refresh token');
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
const newToken = await response.json();
|
|
223
|
-
|
|
224
|
-
// Update the stored token
|
|
225
|
-
await this.set({
|
|
226
|
-
access_token: newToken.access_token,
|
|
227
|
-
refresh_token: newToken.refresh_token
|
|
228
|
-
}, accountId);
|
|
229
|
-
|
|
230
|
-
return newToken;
|
|
231
|
-
}
|
|
232
|
-
};
|
|
233
|
-
```
|
|
234
|
-
|
|
235
|
-
### Complete Authorization Flow Example
|
|
236
|
-
|
|
237
|
-
Here's a complete example showing how to use ApiService with automatic token refresh:
|
|
206
|
+
## Example: Complete Authorization Flow (TokenAuthProvider)
|
|
238
207
|
|
|
239
208
|
```typescript
|
|
240
209
|
import ApiService from 'apiservice';
|
|
210
|
+
import { TokenAuthProvider } from 'apiservice';
|
|
241
211
|
|
|
242
|
-
// Create token service with refresh capability
|
|
243
212
|
const tokenService = {
|
|
244
|
-
// Get token from storage
|
|
245
213
|
async get(accountId = 'default') {
|
|
246
|
-
|
|
247
|
-
if (!storedToken) {
|
|
248
|
-
throw new Error(`No token found for account ${accountId}`);
|
|
249
|
-
}
|
|
250
|
-
return JSON.parse(storedToken);
|
|
214
|
+
// ...
|
|
251
215
|
},
|
|
252
|
-
|
|
253
|
-
// Save token to storage
|
|
254
216
|
async set(token, accountId = 'default') {
|
|
255
|
-
|
|
256
|
-
const currentToken = existingToken ? JSON.parse(existingToken) : { accountId };
|
|
257
|
-
const updatedToken = { ...currentToken, ...token, updatedAt: new Date().toISOString() };
|
|
258
|
-
localStorage.setItem(`token-${accountId}`, JSON.stringify(updatedToken));
|
|
217
|
+
// ...
|
|
259
218
|
},
|
|
260
|
-
|
|
261
|
-
// Refresh token with OAuth provider
|
|
262
219
|
async refresh(refreshToken, accountId = 'default') {
|
|
263
|
-
//
|
|
264
|
-
const response = await fetch('https://api.example.com/oauth/token', {
|
|
265
|
-
method: 'POST',
|
|
266
|
-
headers: { 'Content-Type': 'application/json' },
|
|
267
|
-
body: JSON.stringify({
|
|
268
|
-
grant_type: 'refresh_token',
|
|
269
|
-
refresh_token: refreshToken,
|
|
270
|
-
client_id: 'your-client-id'
|
|
271
|
-
})
|
|
272
|
-
});
|
|
273
|
-
|
|
274
|
-
if (!response.ok) {
|
|
275
|
-
throw new Error('Failed to refresh token');
|
|
276
|
-
}
|
|
277
|
-
|
|
278
|
-
return await response.json();
|
|
220
|
+
// ...
|
|
279
221
|
}
|
|
280
222
|
};
|
|
281
223
|
|
|
282
|
-
// Create API service instance
|
|
283
224
|
const api = new ApiService();
|
|
284
|
-
|
|
285
|
-
// Configure API service with automatic token refresh
|
|
286
225
|
api.setup({
|
|
287
226
|
provider: 'example-api',
|
|
288
|
-
tokenService,
|
|
227
|
+
authProvider: new TokenAuthProvider(tokenService),
|
|
289
228
|
cacheTime: 30000,
|
|
290
229
|
baseUrl: 'https://api.example.com',
|
|
291
|
-
|
|
292
|
-
// You can still add custom hooks for other status codes
|
|
293
|
-
// The default 401 handler will be used automatically
|
|
294
230
|
hooks: {
|
|
295
|
-
// Handle 403 Forbidden errors - typically for insufficient permissions
|
|
296
231
|
403: {
|
|
297
232
|
shouldRetry: false,
|
|
298
233
|
handler: async (accountId, response) => {
|
|
299
|
-
|
|
300
|
-
// You could trigger a permissions UI here
|
|
301
|
-
window.dispatchEvent(new CustomEvent('permission:required', {
|
|
302
|
-
detail: { accountId, resource: response.resource }
|
|
303
|
-
}));
|
|
234
|
+
// ...
|
|
304
235
|
return null;
|
|
305
236
|
}
|
|
306
237
|
}
|
|
@@ -309,19 +240,11 @@ api.setup({
|
|
|
309
240
|
|
|
310
241
|
// Use the API service
|
|
311
242
|
async function fetchUserData(userId) {
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
useAuth: true
|
|
318
|
-
});
|
|
319
|
-
} catch (error) {
|
|
320
|
-
// 401 errors with valid refresh tokens will be automatically handled
|
|
321
|
-
// This catch will only trigger for other errors or if refresh fails
|
|
322
|
-
console.error('Failed to fetch user data:', error);
|
|
323
|
-
throw error;
|
|
324
|
-
}
|
|
243
|
+
return await api.call({
|
|
244
|
+
method: 'GET',
|
|
245
|
+
route: `/users/${userId}`,
|
|
246
|
+
useAuth: true
|
|
247
|
+
});
|
|
325
248
|
}
|
|
326
249
|
```
|
|
327
250
|
|
|
@@ -332,38 +255,24 @@ Hooks can be configured to handle specific HTTP status codes:
|
|
|
332
255
|
```typescript
|
|
333
256
|
const hooks = {
|
|
334
257
|
401: {
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
// Advanced options
|
|
341
|
-
preventConcurrentCalls: true, // Wait for an existing hook to complete before starting a new one
|
|
342
|
-
// Useful for avoiding duplicate refresh token calls
|
|
343
|
-
|
|
344
|
-
// Handler functions
|
|
258
|
+
shouldRetry: true,
|
|
259
|
+
useRetryDelay: true,
|
|
260
|
+
maxRetries: 3,
|
|
261
|
+
preventConcurrentCalls: true,
|
|
345
262
|
handler: async (accountId, response) => {
|
|
346
|
-
//
|
|
347
|
-
// Return an object to update the API call parameters for the retry
|
|
263
|
+
// ...
|
|
348
264
|
return { /* updated parameters */ };
|
|
349
265
|
},
|
|
350
|
-
|
|
351
266
|
onMaxRetriesExceeded: async (accountId, error) => {
|
|
352
|
-
//
|
|
267
|
+
// ...
|
|
353
268
|
},
|
|
354
|
-
|
|
355
269
|
onHandlerError: async (accountId, error) => {
|
|
356
|
-
//
|
|
270
|
+
// ...
|
|
357
271
|
},
|
|
358
|
-
|
|
359
|
-
// Delay strategy settings
|
|
360
272
|
delayStrategy: {
|
|
361
|
-
calculate: (attempt, response) =>
|
|
362
|
-
// Custom strategy for calculating delay between retries
|
|
363
|
-
return 1000 * Math.pow(2, attempt - 1); // Exponential backoff
|
|
364
|
-
}
|
|
273
|
+
calculate: (attempt, response) => 1000 * Math.pow(2, attempt - 1)
|
|
365
274
|
},
|
|
366
|
-
maxDelay: 30000
|
|
275
|
+
maxDelay: 30000
|
|
367
276
|
}
|
|
368
277
|
}
|
|
369
278
|
```
|
|
@@ -382,18 +291,23 @@ The codebase is built around a main `ApiService` class that coordinates several
|
|
|
382
291
|
|
|
383
292
|
### Multiple API Providers
|
|
384
293
|
|
|
385
|
-
```
|
|
386
|
-
|
|
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();
|
|
387
301
|
api.setup({
|
|
388
302
|
provider: 'primary-api',
|
|
389
|
-
|
|
303
|
+
authProvider: primaryProvider,
|
|
390
304
|
cacheTime: 30000,
|
|
391
305
|
baseUrl: 'https://api.primary.com'
|
|
392
306
|
});
|
|
393
307
|
|
|
394
308
|
api.setup({
|
|
395
309
|
provider: 'secondary-api',
|
|
396
|
-
|
|
310
|
+
authProvider: secondaryProvider,
|
|
397
311
|
cacheTime: 60000,
|
|
398
312
|
baseUrl: 'https://api.secondary.com'
|
|
399
313
|
});
|
|
@@ -407,15 +321,12 @@ async function fetchCombinedData() {
|
|
|
407
321
|
route: '/data',
|
|
408
322
|
useAuth: true
|
|
409
323
|
}),
|
|
410
|
-
|
|
411
324
|
api.call({
|
|
412
325
|
provider: 'secondary-api',
|
|
413
326
|
method: 'GET',
|
|
414
327
|
route: '/data',
|
|
415
328
|
useAuth: true
|
|
416
329
|
}),
|
|
417
|
-
|
|
418
|
-
// Override baseUrl for a specific API call
|
|
419
330
|
api.call({
|
|
420
331
|
provider: 'primary-api',
|
|
421
332
|
method: 'GET',
|
|
@@ -424,7 +335,6 @@ async function fetchCombinedData() {
|
|
|
424
335
|
base: 'https://special-api.primary.com'
|
|
425
336
|
})
|
|
426
337
|
]);
|
|
427
|
-
|
|
428
338
|
return { primaryData, secondaryData };
|
|
429
339
|
}
|
|
430
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,20 +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": "echo \"Error: no test specified\" && exit 1",
|
|
16
|
-
"prepare": "npm run build"
|
|
17
|
-
},
|
|
18
10
|
"keywords": [
|
|
19
11
|
"api",
|
|
20
12
|
"service",
|
|
@@ -38,12 +30,23 @@
|
|
|
38
30
|
"qs": "^6.11.0"
|
|
39
31
|
},
|
|
40
32
|
"devDependencies": {
|
|
33
|
+
"@types/jest": "^29.5.5",
|
|
41
34
|
"@types/node": "^18.0.0",
|
|
42
35
|
"@types/qs": "^6.9.7",
|
|
36
|
+
"jest": "^29.7.0",
|
|
43
37
|
"rimraf": "^3.0.2",
|
|
38
|
+
"ts-jest": "^29.1.1",
|
|
44
39
|
"typescript": "^4.7.4"
|
|
45
40
|
},
|
|
46
41
|
"publishConfig": {
|
|
47
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"
|
|
48
51
|
}
|
|
49
|
-
}
|
|
52
|
+
}
|