@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 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
- - ✅ Authentication handling with token management
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', // 'google' | 'microsoft' and etc.
23
- tokenService: myTokenService,
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
- ## Automatic Token Refresh
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
- 1. Detects 401 errors from the API
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
- To use this feature:
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
- // Example token service with refresh capability
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
- const response = await fetch('https://api.example.com/oauth/token', {
86
- method: 'POST',
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
- tokenService,
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
- ## Token Service
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 TokenService {
149
- // Get a token for an account (accountId is optional, defaults to 'default')
150
- get: (accountId?: string) => Promise<Token>;
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
- ### Example Implementation
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
- const storedToken = localStorage.getItem(`token-${accountId}`);
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
- const existingToken = localStorage.getItem(`token-${accountId}`);
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
- // Real implementation would call your OAuth endpoint
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
- console.warn('Permission denied:', response);
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
- try {
313
- return await api.call({
314
- // No accountId needed - will use 'default' automatically
315
- method: 'GET',
316
- route: `/users/${userId}`,
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
- // Core settings
336
- shouldRetry: true, // Whether to retry the API call when this hook is triggered
337
- useRetryDelay: true, // Whether to apply delay between retries
338
- maxRetries: 3, // Maximum number of retry attempts for this status code (default: 4)
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
- // Main handler function called when this status code is encountered
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
- // Called when all retry attempts for this status code have failed
267
+ // ...
353
268
  },
354
-
355
269
  onHandlerError: async (accountId, error) => {
356
- // Called when the handler function throws an error
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 // Maximum delay in milliseconds between retries (default: 60000)
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
- ```javascript
386
- // Configure multiple API providers
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
- tokenService: primaryTokenService,
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
- tokenService: secondaryTokenService,
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;
@@ -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';
@@ -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 { TokenService, ApiCallParams, HookSettings, StatusCode } from './types';
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 tokenService;
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, tokenService, hooks, cacheTime, baseUrl, }: {
20
+ setup({ provider, authProvider, hooks, cacheTime, baseUrl, }: {
21
21
  provider: string;
22
- tokenService: TokenService;
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 token refresh behavior
29
+ * that implements standard credential refresh behavior
30
30
  */
31
- private createDefaultTokenRefreshHandler;
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.tokenService = {};
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, tokenService, hooks = {}, cacheTime, baseUrl = '', }) {
26
+ setup({ provider, authProvider, hooks = {}, cacheTime, baseUrl = '', }) {
26
27
  this.provider = provider;
27
- this.tokenService = tokenService;
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. TokenService has a refresh method
34
- if (hooks[401] === undefined && typeof this.tokenService.refresh === 'function') {
35
- finalHooks[401] = this.createDefaultTokenRefreshHandler();
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 token refresh behavior
54
+ * that implements standard credential refresh behavior
54
55
  */
55
- createDefaultTokenRefreshHandler() {
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 token refresh handler for ${accountId}`);
64
- // Get current token to extract refresh token
65
- const currentToken = await this.tokenService.get(accountId);
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
- // Refresh the token
70
- const newToken = await this.tokenService.refresh(currentToken.refresh_token, accountId);
71
- if (!newToken || !newToken.access_token) {
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(`Token refresh failed for ${accountId}:`, 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 token if needed
148
- const authToken = apiCallParams.useAuth !== false
149
- ? await this.tokenService.get(accountId)
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 && !apiCallParams.accessToken && !authToken.access_token) {
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, authToken);
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.1",
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
+ }