astro-tokenkit 1.0.18 → 1.0.20

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
@@ -307,3 +307,17 @@ Run on a standard development machine using `npm run bench`:
307
307
  ## License
308
308
 
309
309
  MIT © [oamm](https://github.com/oamm)
310
+
311
+ ---
312
+
313
+ ## Playground
314
+
315
+ We've included a [playground](./playground) project to quickly test the integration.
316
+
317
+ To run the playground:
318
+
319
+ ```bash
320
+ npm run playground
321
+ ```
322
+
323
+ This will install the dependencies and start the Astro dev server for the playground.
@@ -23,7 +23,7 @@ export declare class TokenManager {
23
23
  /**
24
24
  * Ensure valid tokens (with automatic refresh)
25
25
  */
26
- ensure(ctx: TokenKitContext, options?: AuthOptions, headers?: Record<string, string>): Promise<Session | null>;
26
+ ensure(ctx: TokenKitContext, options?: AuthOptions, headers?: Record<string, string>, force?: boolean): Promise<Session | null>;
27
27
  /**
28
28
  * Logout (clear tokens)
29
29
  */
@@ -26,21 +26,18 @@ class SingleFlight {
26
26
  const existing = this.inFlight.get(key);
27
27
  if (existing)
28
28
  return existing;
29
- const promise = this.doExecute(key, fn);
29
+ const promise = (() => __awaiter(this, void 0, void 0, function* () {
30
+ try {
31
+ return yield fn();
32
+ }
33
+ finally {
34
+ this.inFlight.delete(key);
35
+ }
36
+ }))();
30
37
  this.inFlight.set(key, promise);
31
38
  return promise;
32
39
  });
33
40
  }
34
- doExecute(key, fn) {
35
- return __awaiter(this, void 0, void 0, function* () {
36
- try {
37
- return yield fn();
38
- }
39
- finally {
40
- this.inFlight.delete(key);
41
- }
42
- });
43
- }
44
41
  }
45
42
  /**
46
43
  * Token Manager handles all token operations
@@ -56,6 +53,7 @@ export class TokenManager {
56
53
  */
57
54
  login(ctx, credentials, options) {
58
55
  return __awaiter(this, void 0, void 0, function* () {
56
+ var _a, _b;
59
57
  const url = this.joinURL(this.baseURL, this.config.login);
60
58
  const contentType = this.config.contentType || 'application/json';
61
59
  const headers = Object.assign(Object.assign({ 'Content-Type': contentType }, this.config.headers), options === null || options === void 0 ? void 0 : options.headers);
@@ -67,12 +65,16 @@ export class TokenManager {
67
65
  else {
68
66
  requestBody = JSON.stringify(data);
69
67
  }
68
+ const timeout = (_b = (_a = options === null || options === void 0 ? void 0 : options.timeout) !== null && _a !== void 0 ? _a : this.config.timeout) !== null && _b !== void 0 ? _b : 30000;
69
+ const controller = new AbortController();
70
+ const timeoutId = setTimeout(() => controller.abort(), timeout);
70
71
  let response;
71
72
  try {
72
73
  response = yield safeFetch(url, {
73
74
  method: 'POST',
74
75
  headers,
75
76
  body: requestBody,
77
+ signal: controller.signal,
76
78
  }, this.config);
77
79
  }
78
80
  catch (error) {
@@ -81,6 +83,9 @@ export class TokenManager {
81
83
  yield options.onError(authError, ctx);
82
84
  throw authError;
83
85
  }
86
+ finally {
87
+ clearTimeout(timeoutId);
88
+ }
84
89
  if (!response.ok) {
85
90
  const authError = new AuthError(`Login failed: ${response.status} ${response.statusText}`, response.status, response);
86
91
  if (options === null || options === void 0 ? void 0 : options.onError)
@@ -136,6 +141,7 @@ export class TokenManager {
136
141
  */
137
142
  performRefresh(ctx, refreshToken, options, extraHeaders) {
138
143
  return __awaiter(this, void 0, void 0, function* () {
144
+ var _a, _b;
139
145
  const url = this.joinURL(this.baseURL, this.config.refresh);
140
146
  const contentType = this.config.contentType || 'application/json';
141
147
  const headers = Object.assign(Object.assign({ 'Content-Type': contentType }, this.config.headers), extraHeaders);
@@ -148,17 +154,24 @@ export class TokenManager {
148
154
  else {
149
155
  requestBody = JSON.stringify(data);
150
156
  }
157
+ const timeout = (_b = (_a = options === null || options === void 0 ? void 0 : options.timeout) !== null && _a !== void 0 ? _a : this.config.timeout) !== null && _b !== void 0 ? _b : 30000;
158
+ const controller = new AbortController();
159
+ const timeoutId = setTimeout(() => controller.abort(), timeout);
151
160
  let response;
152
161
  try {
153
162
  response = yield safeFetch(url, {
154
163
  method: 'POST',
155
164
  headers,
156
165
  body: requestBody,
166
+ signal: controller.signal,
157
167
  }, this.config);
158
168
  }
159
169
  catch (error) {
160
170
  throw new AuthError(`Refresh request failed: ${error.message}`, undefined, undefined, undefined, error);
161
171
  }
172
+ finally {
173
+ clearTimeout(timeoutId);
174
+ }
162
175
  if (!response.ok) {
163
176
  // 401/403 = invalid refresh token
164
177
  if (response.status === 401 || response.status === 403) {
@@ -190,8 +203,8 @@ export class TokenManager {
190
203
  /**
191
204
  * Ensure valid tokens (with automatic refresh)
192
205
  */
193
- ensure(ctx, options, headers) {
194
- return __awaiter(this, void 0, void 0, function* () {
206
+ ensure(ctx_1, options_1, headers_1) {
207
+ return __awaiter(this, arguments, void 0, function* (ctx, options, headers, force = false) {
195
208
  var _a, _b, _c, _d, _e, _f;
196
209
  const now = Math.floor(Date.now() / 1000);
197
210
  const tokens = retrieveTokens(ctx, this.config.cookies);
@@ -199,12 +212,14 @@ export class TokenManager {
199
212
  if (!tokens.accessToken || !tokens.refreshToken || !tokens.expiresAt) {
200
213
  return null;
201
214
  }
202
- // Token expired
203
- if (isExpired(tokens.expiresAt, now, this.config.policy)) {
215
+ // Token expired or force refresh
216
+ if (force || isExpired(tokens.expiresAt, now, this.config.policy)) {
204
217
  const flightKey = this.createFlightKey(tokens.refreshToken);
205
218
  const bundle = yield this.singleFlight.execute(flightKey, () => this.refresh(ctx, tokens.refreshToken, options, headers));
206
219
  if (!bundle)
207
220
  return null;
221
+ // Ensure tokens are stored in the current context (in case of shared flight)
222
+ storeTokens(ctx, bundle, this.config.cookies);
208
223
  return {
209
224
  accessToken: bundle.accessToken,
210
225
  expiresAt: bundle.accessExpiresAt,
@@ -217,6 +232,8 @@ export class TokenManager {
217
232
  const flightKey = this.createFlightKey(tokens.refreshToken);
218
233
  const bundle = yield this.singleFlight.execute(flightKey, () => this.refresh(ctx, tokens.refreshToken, options, headers));
219
234
  if (bundle) {
235
+ // Ensure tokens are stored in the current context (in case of shared flight)
236
+ storeTokens(ctx, bundle, this.config.cookies);
220
237
  return {
221
238
  accessToken: bundle.accessToken,
222
239
  expiresAt: bundle.accessExpiresAt,
@@ -244,23 +261,33 @@ export class TokenManager {
244
261
  */
245
262
  logout(ctx) {
246
263
  return __awaiter(this, void 0, void 0, function* () {
247
- var _a;
264
+ var _a, _b;
248
265
  // Optionally call logout endpoint
249
266
  if (this.config.logout) {
267
+ const timeout = (_a = this.config.timeout) !== null && _a !== void 0 ? _a : 10000;
268
+ const controller = new AbortController();
269
+ const timeoutId = setTimeout(() => controller.abort(), timeout);
250
270
  try {
251
271
  const url = this.joinURL(this.baseURL, this.config.logout);
252
272
  const session = this.getSession(ctx);
253
273
  const headers = {};
254
274
  if (session === null || session === void 0 ? void 0 : session.accessToken) {
255
- const injectFn = (_a = this.config.injectToken) !== null && _a !== void 0 ? _a : ((token, type) => `${type !== null && type !== void 0 ? type : 'Bearer'} ${token}`);
275
+ const injectFn = (_b = this.config.injectToken) !== null && _b !== void 0 ? _b : ((token, type) => `${type !== null && type !== void 0 ? type : 'Bearer'} ${token}`);
256
276
  headers['Authorization'] = injectFn(session.accessToken, session.tokenType);
257
277
  }
258
- yield safeFetch(url, { method: 'POST', headers }, this.config);
278
+ yield safeFetch(url, {
279
+ method: 'POST',
280
+ headers,
281
+ signal: controller.signal,
282
+ }, this.config);
259
283
  }
260
284
  catch (error) {
261
285
  // Ignore logout endpoint errors
262
286
  logger.debug('[TokenKit] Logout endpoint failed:', error);
263
287
  }
288
+ finally {
289
+ clearTimeout(timeoutId);
290
+ }
264
291
  }
265
292
  clearTokens(ctx, this.config.cookies);
266
293
  });
@@ -143,6 +143,7 @@ export class APIClient {
143
143
  executeRequest(config, ctx, attempt) {
144
144
  return __awaiter(this, void 0, void 0, function* () {
145
145
  var _a, _b, _c, _d, _e;
146
+ const method = config.method.toUpperCase();
146
147
  // Ensure valid session (if auth is enabled)
147
148
  if (this.tokenManager && !config.skipAuth) {
148
149
  yield this.tokenManager.ensure(ctx, config.auth, config.headers);
@@ -153,13 +154,18 @@ export class APIClient {
153
154
  const headers = this.buildHeaders(config, ctx, fullURL);
154
155
  // Build request init
155
156
  const init = {
156
- method: config.method,
157
+ method,
157
158
  headers,
158
159
  signal: config.signal,
159
160
  };
160
- // Add body for non-GET requests
161
- if (config.data && config.method !== 'GET') {
161
+ // Add body for appropriate methods
162
+ const methodsWithNoBody = ['GET', 'HEAD', 'DELETE'];
163
+ if (config.data && !methodsWithNoBody.includes(method)) {
162
164
  init.body = JSON.stringify(config.data);
165
+ // Add Content-Type if not already present
166
+ if (!headers['Content-Type'] && !headers['content-type']) {
167
+ headers['Content-Type'] = 'application/json';
168
+ }
163
169
  }
164
170
  // Apply request interceptors
165
171
  let requestConfig = Object.assign({}, config);
@@ -177,8 +183,8 @@ export class APIClient {
177
183
  clearTimeout(timeoutId);
178
184
  // Handle 401 (try refresh and retry once)
179
185
  if (response.status === 401 && this.tokenManager && !config.skipAuth && attempt === 1) {
180
- // Clear and try fresh session
181
- const session = yield this.tokenManager.ensure(ctx, config.auth, config.headers);
186
+ // Clear and try fresh session (force refresh)
187
+ const session = yield this.tokenManager.ensure(ctx, config.auth, config.headers, true);
182
188
  if (session) {
183
189
  // Retry with new token
184
190
  return this.executeRequest(config, ctx, attempt + 1);
@@ -271,7 +277,7 @@ export class APIClient {
271
277
  */
272
278
  buildHeaders(config, ctx, targetURL) {
273
279
  var _a, _b;
274
- const headers = Object.assign(Object.assign({ 'Content-Type': 'application/json' }, this.config.headers), config.headers);
280
+ const headers = Object.assign(Object.assign({}, this.config.headers), config.headers);
275
281
  // Add auth token if available (only for safe URLs)
276
282
  if (this.tokenManager && !config.skipAuth && this.isSafeURL(targetURL)) {
277
283
  const session = this.tokenManager.getSession(ctx);
package/dist/index.cjs CHANGED
@@ -400,6 +400,7 @@ function isExpired(expiresAt, now, policy = {}) {
400
400
  }
401
401
 
402
402
  // packages/astro-tokenkit/src/utils/fetch.ts
403
+ let sharedInsecureAgent = null;
403
404
  /**
404
405
  * Perform a fetch request with optional certificate validation bypass
405
406
  */
@@ -408,16 +409,26 @@ function safeFetch(url, init, config) {
408
409
  const fetchFn = config.fetch || fetch;
409
410
  const fetchOptions = Object.assign({}, init);
410
411
  if (config.dangerouslyIgnoreCertificateErrors && typeof process !== 'undefined') {
411
- // In Node.js environment
412
412
  try {
413
- // Try to use undici Agent if available (it is built-in in Node 18+)
414
- // However, we might need to import it if we want to create an Agent.
415
- // Since we don't want to depend on undici in package.json, we use dynamic import.
416
- // But wait, undici's Agent is what we need.
417
- // As a fallback and most reliable way for self-signed certs in Node without extra deps:
418
- process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0';
419
- // NOTE: This affects the whole process. We should ideally only do this if it's not already 0.
420
- // But for a dev tool / specialized library, it's often what's needed.
413
+ // Try to use undici Agent if available to avoid global process.env changes
414
+ if (!sharedInsecureAgent) {
415
+ // @ts-ignore
416
+ const undici = yield import('undici').catch(() => null);
417
+ if (undici && undici.Agent) {
418
+ sharedInsecureAgent = new undici.Agent({
419
+ connect: { rejectUnauthorized: false }
420
+ });
421
+ }
422
+ }
423
+ if (sharedInsecureAgent) {
424
+ fetchOptions.dispatcher = sharedInsecureAgent;
425
+ }
426
+ else {
427
+ // Fallback to global setting (less secure, but only way without undici)
428
+ if (process.env.NODE_TLS_REJECT_UNAUTHORIZED !== '0') {
429
+ process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0';
430
+ }
431
+ }
421
432
  }
422
433
  catch (e) {
423
434
  // Ignore
@@ -528,21 +539,18 @@ class SingleFlight {
528
539
  const existing = this.inFlight.get(key);
529
540
  if (existing)
530
541
  return existing;
531
- const promise = this.doExecute(key, fn);
542
+ const promise = (() => __awaiter(this, void 0, void 0, function* () {
543
+ try {
544
+ return yield fn();
545
+ }
546
+ finally {
547
+ this.inFlight.delete(key);
548
+ }
549
+ }))();
532
550
  this.inFlight.set(key, promise);
533
551
  return promise;
534
552
  });
535
553
  }
536
- doExecute(key, fn) {
537
- return __awaiter(this, void 0, void 0, function* () {
538
- try {
539
- return yield fn();
540
- }
541
- finally {
542
- this.inFlight.delete(key);
543
- }
544
- });
545
- }
546
554
  }
547
555
  /**
548
556
  * Token Manager handles all token operations
@@ -558,6 +566,7 @@ class TokenManager {
558
566
  */
559
567
  login(ctx, credentials, options) {
560
568
  return __awaiter(this, void 0, void 0, function* () {
569
+ var _a, _b;
561
570
  const url = this.joinURL(this.baseURL, this.config.login);
562
571
  const contentType = this.config.contentType || 'application/json';
563
572
  const headers = Object.assign(Object.assign({ 'Content-Type': contentType }, this.config.headers), options === null || options === void 0 ? void 0 : options.headers);
@@ -569,12 +578,16 @@ class TokenManager {
569
578
  else {
570
579
  requestBody = JSON.stringify(data);
571
580
  }
581
+ const timeout = (_b = (_a = options === null || options === void 0 ? void 0 : options.timeout) !== null && _a !== void 0 ? _a : this.config.timeout) !== null && _b !== void 0 ? _b : 30000;
582
+ const controller = new AbortController();
583
+ const timeoutId = setTimeout(() => controller.abort(), timeout);
572
584
  let response;
573
585
  try {
574
586
  response = yield safeFetch(url, {
575
587
  method: 'POST',
576
588
  headers,
577
589
  body: requestBody,
590
+ signal: controller.signal,
578
591
  }, this.config);
579
592
  }
580
593
  catch (error) {
@@ -583,6 +596,9 @@ class TokenManager {
583
596
  yield options.onError(authError, ctx);
584
597
  throw authError;
585
598
  }
599
+ finally {
600
+ clearTimeout(timeoutId);
601
+ }
586
602
  if (!response.ok) {
587
603
  const authError = new AuthError(`Login failed: ${response.status} ${response.statusText}`, response.status, response);
588
604
  if (options === null || options === void 0 ? void 0 : options.onError)
@@ -638,6 +654,7 @@ class TokenManager {
638
654
  */
639
655
  performRefresh(ctx, refreshToken, options, extraHeaders) {
640
656
  return __awaiter(this, void 0, void 0, function* () {
657
+ var _a, _b;
641
658
  const url = this.joinURL(this.baseURL, this.config.refresh);
642
659
  const contentType = this.config.contentType || 'application/json';
643
660
  const headers = Object.assign(Object.assign({ 'Content-Type': contentType }, this.config.headers), extraHeaders);
@@ -650,17 +667,24 @@ class TokenManager {
650
667
  else {
651
668
  requestBody = JSON.stringify(data);
652
669
  }
670
+ const timeout = (_b = (_a = options === null || options === void 0 ? void 0 : options.timeout) !== null && _a !== void 0 ? _a : this.config.timeout) !== null && _b !== void 0 ? _b : 30000;
671
+ const controller = new AbortController();
672
+ const timeoutId = setTimeout(() => controller.abort(), timeout);
653
673
  let response;
654
674
  try {
655
675
  response = yield safeFetch(url, {
656
676
  method: 'POST',
657
677
  headers,
658
678
  body: requestBody,
679
+ signal: controller.signal,
659
680
  }, this.config);
660
681
  }
661
682
  catch (error) {
662
683
  throw new AuthError(`Refresh request failed: ${error.message}`, undefined, undefined, undefined, error);
663
684
  }
685
+ finally {
686
+ clearTimeout(timeoutId);
687
+ }
664
688
  if (!response.ok) {
665
689
  // 401/403 = invalid refresh token
666
690
  if (response.status === 401 || response.status === 403) {
@@ -692,8 +716,8 @@ class TokenManager {
692
716
  /**
693
717
  * Ensure valid tokens (with automatic refresh)
694
718
  */
695
- ensure(ctx, options, headers) {
696
- return __awaiter(this, void 0, void 0, function* () {
719
+ ensure(ctx_1, options_1, headers_1) {
720
+ return __awaiter(this, arguments, void 0, function* (ctx, options, headers, force = false) {
697
721
  var _a, _b, _c, _d, _e, _f;
698
722
  const now = Math.floor(Date.now() / 1000);
699
723
  const tokens = retrieveTokens(ctx, this.config.cookies);
@@ -701,12 +725,14 @@ class TokenManager {
701
725
  if (!tokens.accessToken || !tokens.refreshToken || !tokens.expiresAt) {
702
726
  return null;
703
727
  }
704
- // Token expired
705
- if (isExpired(tokens.expiresAt, now, this.config.policy)) {
728
+ // Token expired or force refresh
729
+ if (force || isExpired(tokens.expiresAt, now, this.config.policy)) {
706
730
  const flightKey = this.createFlightKey(tokens.refreshToken);
707
731
  const bundle = yield this.singleFlight.execute(flightKey, () => this.refresh(ctx, tokens.refreshToken, options, headers));
708
732
  if (!bundle)
709
733
  return null;
734
+ // Ensure tokens are stored in the current context (in case of shared flight)
735
+ storeTokens(ctx, bundle, this.config.cookies);
710
736
  return {
711
737
  accessToken: bundle.accessToken,
712
738
  expiresAt: bundle.accessExpiresAt,
@@ -719,6 +745,8 @@ class TokenManager {
719
745
  const flightKey = this.createFlightKey(tokens.refreshToken);
720
746
  const bundle = yield this.singleFlight.execute(flightKey, () => this.refresh(ctx, tokens.refreshToken, options, headers));
721
747
  if (bundle) {
748
+ // Ensure tokens are stored in the current context (in case of shared flight)
749
+ storeTokens(ctx, bundle, this.config.cookies);
722
750
  return {
723
751
  accessToken: bundle.accessToken,
724
752
  expiresAt: bundle.accessExpiresAt,
@@ -746,23 +774,33 @@ class TokenManager {
746
774
  */
747
775
  logout(ctx) {
748
776
  return __awaiter(this, void 0, void 0, function* () {
749
- var _a;
777
+ var _a, _b;
750
778
  // Optionally call logout endpoint
751
779
  if (this.config.logout) {
780
+ const timeout = (_a = this.config.timeout) !== null && _a !== void 0 ? _a : 10000;
781
+ const controller = new AbortController();
782
+ const timeoutId = setTimeout(() => controller.abort(), timeout);
752
783
  try {
753
784
  const url = this.joinURL(this.baseURL, this.config.logout);
754
785
  const session = this.getSession(ctx);
755
786
  const headers = {};
756
787
  if (session === null || session === void 0 ? void 0 : session.accessToken) {
757
- const injectFn = (_a = this.config.injectToken) !== null && _a !== void 0 ? _a : ((token, type) => `${type !== null && type !== void 0 ? type : 'Bearer'} ${token}`);
788
+ const injectFn = (_b = this.config.injectToken) !== null && _b !== void 0 ? _b : ((token, type) => `${type !== null && type !== void 0 ? type : 'Bearer'} ${token}`);
758
789
  headers['Authorization'] = injectFn(session.accessToken, session.tokenType);
759
790
  }
760
- yield safeFetch(url, { method: 'POST', headers }, this.config);
791
+ yield safeFetch(url, {
792
+ method: 'POST',
793
+ headers,
794
+ signal: controller.signal,
795
+ }, this.config);
761
796
  }
762
797
  catch (error) {
763
798
  // Ignore logout endpoint errors
764
799
  logger.debug('[TokenKit] Logout endpoint failed:', error);
765
800
  }
801
+ finally {
802
+ clearTimeout(timeoutId);
803
+ }
766
804
  }
767
805
  clearTokens(ctx, this.config.cookies);
768
806
  });
@@ -1065,6 +1103,7 @@ class APIClient {
1065
1103
  executeRequest(config, ctx, attempt) {
1066
1104
  return __awaiter(this, void 0, void 0, function* () {
1067
1105
  var _a, _b, _c, _d, _e;
1106
+ const method = config.method.toUpperCase();
1068
1107
  // Ensure valid session (if auth is enabled)
1069
1108
  if (this.tokenManager && !config.skipAuth) {
1070
1109
  yield this.tokenManager.ensure(ctx, config.auth, config.headers);
@@ -1075,13 +1114,18 @@ class APIClient {
1075
1114
  const headers = this.buildHeaders(config, ctx, fullURL);
1076
1115
  // Build request init
1077
1116
  const init = {
1078
- method: config.method,
1117
+ method,
1079
1118
  headers,
1080
1119
  signal: config.signal,
1081
1120
  };
1082
- // Add body for non-GET requests
1083
- if (config.data && config.method !== 'GET') {
1121
+ // Add body for appropriate methods
1122
+ const methodsWithNoBody = ['GET', 'HEAD', 'DELETE'];
1123
+ if (config.data && !methodsWithNoBody.includes(method)) {
1084
1124
  init.body = JSON.stringify(config.data);
1125
+ // Add Content-Type if not already present
1126
+ if (!headers['Content-Type'] && !headers['content-type']) {
1127
+ headers['Content-Type'] = 'application/json';
1128
+ }
1085
1129
  }
1086
1130
  // Apply request interceptors
1087
1131
  let requestConfig = Object.assign({}, config);
@@ -1099,8 +1143,8 @@ class APIClient {
1099
1143
  clearTimeout(timeoutId);
1100
1144
  // Handle 401 (try refresh and retry once)
1101
1145
  if (response.status === 401 && this.tokenManager && !config.skipAuth && attempt === 1) {
1102
- // Clear and try fresh session
1103
- const session = yield this.tokenManager.ensure(ctx, config.auth, config.headers);
1146
+ // Clear and try fresh session (force refresh)
1147
+ const session = yield this.tokenManager.ensure(ctx, config.auth, config.headers, true);
1104
1148
  if (session) {
1105
1149
  // Retry with new token
1106
1150
  return this.executeRequest(config, ctx, attempt + 1);
@@ -1193,7 +1237,7 @@ class APIClient {
1193
1237
  */
1194
1238
  buildHeaders(config, ctx, targetURL) {
1195
1239
  var _a, _b;
1196
- const headers = Object.assign(Object.assign({ 'Content-Type': 'application/json' }, this.config.headers), config.headers);
1240
+ const headers = Object.assign(Object.assign({}, this.config.headers), config.headers);
1197
1241
  // Add auth token if available (only for safe URLs)
1198
1242
  if (this.tokenManager && !config.skipAuth && this.isSafeURL(targetURL)) {
1199
1243
  const session = this.tokenManager.getSession(ctx);