astro-tokenkit 1.0.11 → 1.0.13

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/dist/index.cjs CHANGED
@@ -112,6 +112,13 @@ const EXPIRES_IN_FIELDS = [
112
112
  'expiresIn',
113
113
  'ttl',
114
114
  ];
115
+ /**
116
+ * Common field names for token type
117
+ */
118
+ const TOKEN_TYPE_FIELDS = [
119
+ 'token_type',
120
+ 'tokenType',
121
+ ];
115
122
  /**
116
123
  * Common field names for session payload
117
124
  */
@@ -174,10 +181,13 @@ function autoDetectFields(body, fieldMapping) {
174
181
  }
175
182
  // Detect session payload (optional)
176
183
  const sessionPayload = findField(SESSION_PAYLOAD_FIELDS, fieldMapping === null || fieldMapping === void 0 ? void 0 : fieldMapping.sessionPayload);
184
+ // Detect token type (optional)
185
+ const tokenType = findField(TOKEN_TYPE_FIELDS, fieldMapping === null || fieldMapping === void 0 ? void 0 : fieldMapping.tokenType);
177
186
  return {
178
187
  accessToken,
179
188
  refreshToken,
180
189
  accessExpiresAt,
190
+ tokenType: tokenType || undefined,
181
191
  sessionPayload: sessionPayload || undefined,
182
192
  };
183
193
  }
@@ -191,6 +201,10 @@ function parseJWTPayload(token) {
191
201
  return null;
192
202
  }
193
203
  const payload = parts[1];
204
+ // Better UTF-8 support for environments with Buffer (like Node.js/Astro)
205
+ if (typeof Buffer !== 'undefined') {
206
+ return JSON.parse(Buffer.from(payload, 'base64').toString('utf8'));
207
+ }
194
208
  const decoded = atob(payload.replace(/-/g, '+').replace(/_/g, '/'));
195
209
  return JSON.parse(decoded);
196
210
  }
@@ -210,6 +224,7 @@ function getCookieNames(prefix) {
210
224
  refreshToken: `${p}refresh_token`,
211
225
  expiresAt: `${p}access_expires_at`,
212
226
  lastRefreshAt: `${p}last_refresh_at`,
227
+ tokenType: `${p}token_type`,
213
228
  };
214
229
  }
215
230
  /**
@@ -245,20 +260,25 @@ function storeTokens(ctx, bundle, cookieConfig = {}) {
245
260
  ctx.cookies.set(names.expiresAt, bundle.accessExpiresAt.toString(), Object.assign(Object.assign({}, options), { maxAge: accessMaxAge, path: '/' }));
246
261
  // Set last refresh timestamp
247
262
  ctx.cookies.set(names.lastRefreshAt, now.toString(), Object.assign(Object.assign({}, options), { maxAge: accessMaxAge, path: '/' }));
263
+ // Set token type if available
264
+ if (bundle.tokenType) {
265
+ ctx.cookies.set(names.tokenType, bundle.tokenType, Object.assign(Object.assign({}, options), { maxAge: accessMaxAge, path: '/' }));
266
+ }
248
267
  }
249
268
  /**
250
269
  * Retrieve tokens from cookies
251
270
  */
252
271
  function retrieveTokens(ctx, cookieConfig = {}) {
253
- var _a, _b, _c, _d;
272
+ var _a, _b, _c, _d, _e;
254
273
  const names = getCookieNames(cookieConfig.prefix);
255
274
  const accessToken = ((_a = ctx.cookies.get(names.accessToken)) === null || _a === void 0 ? void 0 : _a.value) || null;
256
275
  const refreshToken = ((_b = ctx.cookies.get(names.refreshToken)) === null || _b === void 0 ? void 0 : _b.value) || null;
257
- const expiresAtStr = (_c = ctx.cookies.get(names.expiresAt)) === null || _c === void 0 ? void 0 : _c.value;
276
+ const tokenType = ((_c = ctx.cookies.get(names.tokenType)) === null || _c === void 0 ? void 0 : _c.value) || null;
277
+ const expiresAtStr = (_d = ctx.cookies.get(names.expiresAt)) === null || _d === void 0 ? void 0 : _d.value;
258
278
  const expiresAt = expiresAtStr ? parseInt(expiresAtStr, 10) : null;
259
- const lastRefreshAtStr = (_d = ctx.cookies.get(names.lastRefreshAt)) === null || _d === void 0 ? void 0 : _d.value;
279
+ const lastRefreshAtStr = (_e = ctx.cookies.get(names.lastRefreshAt)) === null || _e === void 0 ? void 0 : _e.value;
260
280
  const lastRefreshAt = lastRefreshAtStr ? parseInt(lastRefreshAtStr, 10) : null;
261
- return { accessToken, refreshToken, expiresAt, lastRefreshAt };
281
+ return { accessToken, refreshToken, expiresAt, lastRefreshAt, tokenType };
262
282
  }
263
283
  /**
264
284
  * Clear all auth cookies
@@ -270,6 +290,7 @@ function clearTokens(ctx, cookieConfig = {}) {
270
290
  ctx.cookies.delete(names.refreshToken, Object.assign(Object.assign({}, options), { path: '/' }));
271
291
  ctx.cookies.delete(names.expiresAt, Object.assign(Object.assign({}, options), { path: '/' }));
272
292
  ctx.cookies.delete(names.lastRefreshAt, Object.assign(Object.assign({}, options), { path: '/' }));
293
+ ctx.cookies.delete(names.tokenType, Object.assign(Object.assign({}, options), { path: '/' }));
273
294
  }
274
295
 
275
296
  // packages/astro-tokenkit/src/utils/time.ts
@@ -371,7 +392,8 @@ function isExpired(expiresAt, now, policy = {}) {
371
392
  const clockSkew = typeof normalized.clockSkew === 'number'
372
393
  ? normalized.clockSkew
373
394
  : parseTime(normalized.clockSkew);
374
- return now > expiresAt + clockSkew;
395
+ // Pessimistic: consider it expired if current time + skew is past expiration
396
+ return now + clockSkew > expiresAt;
375
397
  }
376
398
 
377
399
  // packages/astro-tokenkit/src/auth/manager.ts
@@ -417,7 +439,7 @@ class TokenManager {
417
439
  */
418
440
  login(ctx, credentials, options) {
419
441
  return __awaiter(this, void 0, void 0, function* () {
420
- const url = this.baseURL + this.config.login;
442
+ const url = this.joinURL(this.baseURL, this.config.login);
421
443
  const contentType = this.config.contentType || 'application/json';
422
444
  const headers = Object.assign(Object.assign({ 'Content-Type': contentType }, this.config.headers), options === null || options === void 0 ? void 0 : options.headers);
423
445
  const data = Object.assign(Object.assign(Object.assign({}, this.config.loginData), options === null || options === void 0 ? void 0 : options.data), credentials);
@@ -428,15 +450,25 @@ class TokenManager {
428
450
  else {
429
451
  requestBody = JSON.stringify(data);
430
452
  }
431
- const response = yield fetch(url, {
432
- method: 'POST',
433
- headers,
434
- body: requestBody,
435
- }).catch(error => {
436
- throw new AuthError(`Login request failed: ${error.message}`);
437
- });
453
+ let response;
454
+ try {
455
+ response = yield fetch(url, {
456
+ method: 'POST',
457
+ headers,
458
+ body: requestBody,
459
+ });
460
+ }
461
+ catch (error) {
462
+ const authError = new AuthError(`Login request failed: ${error.message}`);
463
+ if (options === null || options === void 0 ? void 0 : options.onError)
464
+ yield options.onError(authError, ctx);
465
+ throw authError;
466
+ }
438
467
  if (!response.ok) {
439
- throw new AuthError(`Login failed: ${response.status} ${response.statusText}`, response.status, response);
468
+ const authError = new AuthError(`Login failed: ${response.status} ${response.statusText}`, response.status, response);
469
+ if (options === null || options === void 0 ? void 0 : options.onError)
470
+ yield options.onError(authError, ctx);
471
+ throw authError;
440
472
  }
441
473
  const body = yield response.json().catch(() => ({}));
442
474
  // Parse response
@@ -447,7 +479,10 @@ class TokenManager {
447
479
  : autoDetectFields(body, this.config.fields);
448
480
  }
449
481
  catch (error) {
450
- throw new AuthError(`Invalid login response: ${error.message}`, response.status, response);
482
+ const authError = new AuthError(`Invalid login response: ${error.message}`, response.status, response);
483
+ if (options === null || options === void 0 ? void 0 : options.onError)
484
+ yield options.onError(authError, ctx);
485
+ throw authError;
451
486
  }
452
487
  // Store in cookies
453
488
  storeTokens(ctx, bundle, this.config.cookies);
@@ -455,7 +490,14 @@ class TokenManager {
455
490
  if (options === null || options === void 0 ? void 0 : options.onLogin) {
456
491
  yield options.onLogin(bundle, body, ctx);
457
492
  }
458
- return bundle;
493
+ return {
494
+ data: bundle,
495
+ status: response.status,
496
+ statusText: response.statusText,
497
+ headers: response.headers,
498
+ url: response.url,
499
+ ok: response.ok,
500
+ };
459
501
  });
460
502
  }
461
503
  /**
@@ -477,7 +519,7 @@ class TokenManager {
477
519
  */
478
520
  performRefresh(ctx, refreshToken, options, extraHeaders) {
479
521
  return __awaiter(this, void 0, void 0, function* () {
480
- const url = this.baseURL + this.config.refresh;
522
+ const url = this.joinURL(this.baseURL, this.config.refresh);
481
523
  const contentType = this.config.contentType || 'application/json';
482
524
  const headers = Object.assign(Object.assign({ 'Content-Type': contentType }, this.config.headers), extraHeaders);
483
525
  const refreshField = this.config.refreshRequestField || 'refreshToken';
@@ -489,13 +531,17 @@ class TokenManager {
489
531
  else {
490
532
  requestBody = JSON.stringify(data);
491
533
  }
492
- const response = yield fetch(url, {
493
- method: 'POST',
494
- headers,
495
- body: requestBody,
496
- }).catch(error => {
534
+ let response;
535
+ try {
536
+ response = yield fetch(url, {
537
+ method: 'POST',
538
+ headers,
539
+ body: requestBody,
540
+ });
541
+ }
542
+ catch (error) {
497
543
  throw new AuthError(`Refresh request failed: ${error.message}`);
498
- });
544
+ }
499
545
  if (!response.ok) {
500
546
  // 401/403 = invalid refresh token
501
547
  if (response.status === 401 || response.status === 403) {
@@ -529,7 +575,7 @@ class TokenManager {
529
575
  */
530
576
  ensure(ctx, options, headers) {
531
577
  return __awaiter(this, void 0, void 0, function* () {
532
- var _a, _b, _c, _d, _e;
578
+ var _a, _b, _c, _d, _e, _f;
533
579
  const now = Math.floor(Date.now() / 1000);
534
580
  const tokens = retrieveTokens(ctx, this.config.cookies);
535
581
  // No tokens
@@ -545,6 +591,7 @@ class TokenManager {
545
591
  return {
546
592
  accessToken: bundle.accessToken,
547
593
  expiresAt: bundle.accessExpiresAt,
594
+ tokenType: bundle.tokenType,
548
595
  payload: (_b = (_a = bundle.sessionPayload) !== null && _a !== void 0 ? _a : parseJWTPayload(bundle.accessToken)) !== null && _b !== void 0 ? _b : undefined,
549
596
  };
550
597
  }
@@ -556,6 +603,7 @@ class TokenManager {
556
603
  return {
557
604
  accessToken: bundle.accessToken,
558
605
  expiresAt: bundle.accessExpiresAt,
606
+ tokenType: bundle.tokenType,
559
607
  payload: (_d = (_c = bundle.sessionPayload) !== null && _c !== void 0 ? _c : parseJWTPayload(bundle.accessToken)) !== null && _d !== void 0 ? _d : undefined,
560
608
  };
561
609
  }
@@ -569,7 +617,8 @@ class TokenManager {
569
617
  return {
570
618
  accessToken: tokens.accessToken,
571
619
  expiresAt: tokens.expiresAt,
572
- payload: (_e = parseJWTPayload(tokens.accessToken)) !== null && _e !== void 0 ? _e : undefined,
620
+ tokenType: (_e = tokens.tokenType) !== null && _e !== void 0 ? _e : undefined,
621
+ payload: (_f = parseJWTPayload(tokens.accessToken)) !== null && _f !== void 0 ? _f : undefined,
573
622
  };
574
623
  });
575
624
  }
@@ -578,15 +627,22 @@ class TokenManager {
578
627
  */
579
628
  logout(ctx) {
580
629
  return __awaiter(this, void 0, void 0, function* () {
630
+ var _a;
581
631
  // Optionally call logout endpoint
582
632
  if (this.config.logout) {
583
633
  try {
584
- const url = this.baseURL + this.config.logout;
585
- yield fetch(url, { method: 'POST' });
634
+ const url = this.joinURL(this.baseURL, this.config.logout);
635
+ const session = this.getSession(ctx);
636
+ const headers = {};
637
+ if (session === null || session === void 0 ? void 0 : session.accessToken) {
638
+ const injectFn = (_a = this.config.injectToken) !== null && _a !== void 0 ? _a : ((token, type) => `${type !== null && type !== void 0 ? type : 'Bearer'} ${token}`);
639
+ headers['Authorization'] = injectFn(session.accessToken, session.tokenType);
640
+ }
641
+ yield fetch(url, { method: 'POST', headers });
586
642
  }
587
643
  catch (error) {
588
644
  // Ignore logout endpoint errors
589
- console.warn('Logout endpoint failed:', error);
645
+ console.warn('[TokenKit] Logout endpoint failed:', error);
590
646
  }
591
647
  }
592
648
  clearTokens(ctx, this.config.cookies);
@@ -596,7 +652,7 @@ class TokenManager {
596
652
  * Get current session (no refresh)
597
653
  */
598
654
  getSession(ctx) {
599
- var _a;
655
+ var _a, _b;
600
656
  const tokens = retrieveTokens(ctx, this.config.cookies);
601
657
  if (!tokens.accessToken || !tokens.expiresAt) {
602
658
  return null;
@@ -604,7 +660,8 @@ class TokenManager {
604
660
  return {
605
661
  accessToken: tokens.accessToken,
606
662
  expiresAt: tokens.expiresAt,
607
- payload: (_a = parseJWTPayload(tokens.accessToken)) !== null && _a !== void 0 ? _a : undefined,
663
+ tokenType: (_a = tokens.tokenType) !== null && _a !== void 0 ? _a : undefined,
664
+ payload: (_b = parseJWTPayload(tokens.accessToken)) !== null && _b !== void 0 ? _b : undefined,
608
665
  };
609
666
  }
610
667
  /**
@@ -618,57 +675,80 @@ class TokenManager {
618
675
  * Create flight key for single-flight deduplication
619
676
  */
620
677
  createFlightKey(token) {
621
- let hash = 0;
622
- for (let i = 0; i < token.length; i++) {
623
- const char = token.charCodeAt(i);
624
- hash = ((hash << 5) - hash) + char;
625
- hash = hash & hash;
626
- }
627
- return `flight_${Math.abs(hash).toString(36)}`;
678
+ // Avoid weak hashing of sensitive tokens
679
+ return `refresh_${token}`;
680
+ }
681
+ /**
682
+ * Join base URL and path safely
683
+ */
684
+ joinURL(base, path) {
685
+ const b = base.endsWith('/') ? base : base + '/';
686
+ const p = path.startsWith('/') ? path.slice(1) : path;
687
+ return b + p;
628
688
  }
629
689
  }
630
690
 
631
691
  // packages/astro-tokenkit/src/config.ts
632
- let config = {
633
- runWithContext: undefined,
634
- getContextStore: undefined,
635
- setContextStore: undefined,
636
- baseURL: "",
637
- };
638
- let tokenManager;
692
+ const CONFIG_KEY = Symbol.for('astro-tokenkit.config');
693
+ const MANAGER_KEY = Symbol.for('astro-tokenkit.manager');
694
+ const globalStorage = globalThis;
695
+ // Initialize global storage if not present
696
+ if (!globalStorage[CONFIG_KEY]) {
697
+ globalStorage[CONFIG_KEY] = {
698
+ runWithContext: undefined,
699
+ getContextStore: undefined,
700
+ setContextStore: undefined,
701
+ baseURL: "",
702
+ };
703
+ }
639
704
  /**
640
705
  * Set configuration
641
706
  */
642
707
  function setConfig(userConfig) {
643
- const finalConfig = Object.assign(Object.assign({}, config), userConfig);
708
+ const currentConfig = globalStorage[CONFIG_KEY];
709
+ const finalConfig = Object.assign(Object.assign({}, currentConfig), userConfig);
644
710
  // Validate that getter and setter are defined together
645
711
  if ((finalConfig.getContextStore && !finalConfig.setContextStore) ||
646
712
  (!finalConfig.getContextStore && finalConfig.setContextStore)) {
647
713
  throw new Error("[TokenKit] getContextStore and setContextStore must be defined together.");
648
714
  }
649
- config = finalConfig;
715
+ globalStorage[CONFIG_KEY] = finalConfig;
650
716
  // Re-initialize global token manager if auth changed
651
- if (config.auth) {
652
- tokenManager = new TokenManager(config.auth, config.baseURL);
717
+ if (finalConfig.auth) {
718
+ globalStorage[MANAGER_KEY] = new TokenManager(finalConfig.auth, finalConfig.baseURL);
719
+ }
720
+ else {
721
+ globalStorage[MANAGER_KEY] = undefined;
653
722
  }
654
723
  }
655
724
  /**
656
725
  * Get current configuration
657
726
  */
658
727
  function getConfig() {
659
- return config;
728
+ return globalStorage[CONFIG_KEY];
660
729
  }
661
730
  /**
662
731
  * Get global token manager
663
732
  */
664
733
  function getTokenManager() {
665
- return tokenManager;
734
+ return globalStorage[MANAGER_KEY];
666
735
  }
667
736
  /**
668
737
  * Set global token manager (mainly for testing)
669
738
  */
670
739
  function setTokenManager(manager) {
671
- tokenManager = manager;
740
+ globalStorage[MANAGER_KEY] = manager;
741
+ }
742
+ // Handle injected configuration from Astro integration
743
+ try {
744
+ // @ts-ignore
745
+ const injectedConfig = typeof __TOKENKIT_CONFIG__ !== 'undefined' ? __TOKENKIT_CONFIG__ : undefined;
746
+ if (injectedConfig) {
747
+ setConfig(injectedConfig);
748
+ }
749
+ }
750
+ catch (e) {
751
+ // Ignore errors in environments where __TOKENKIT_CONFIG__ might be restricted
672
752
  }
673
753
 
674
754
  // packages/astro-tokenkit/src/client/context.ts
@@ -762,8 +842,8 @@ function createMiddleware() {
762
842
  yield tokenManager.ensure(ctx);
763
843
  }
764
844
  catch (error) {
765
- // Log but don't block a request if rotation fails
766
- console.error('[TokenKit] Automatic token rotation failed:', error);
845
+ // Log only the message to avoid leaking sensitive data in the error object
846
+ console.error('[TokenKit] Automatic token rotation failed:', error.message || error);
767
847
  }
768
848
  }
769
849
  return next();
@@ -892,8 +972,7 @@ class APIClient {
892
972
  while (true) {
893
973
  attempt++;
894
974
  try {
895
- const response = yield this.executeRequest(config, ctx, attempt);
896
- return response.data;
975
+ return yield this.executeRequest(config, ctx, attempt);
897
976
  }
898
977
  catch (error) {
899
978
  // Check if we should retry
@@ -921,7 +1000,7 @@ class APIClient {
921
1000
  // Build full URL
922
1001
  const fullURL = this.buildURL(config.url, config.params);
923
1002
  // Build headers
924
- const headers = this.buildHeaders(config, ctx);
1003
+ const headers = this.buildHeaders(config, ctx, fullURL);
925
1004
  // Build request init
926
1005
  const init = {
927
1006
  method: config.method,
@@ -1018,6 +1097,7 @@ class APIClient {
1018
1097
  statusText: response.statusText,
1019
1098
  headers: response.headers,
1020
1099
  url,
1100
+ ok: response.ok,
1021
1101
  };
1022
1102
  });
1023
1103
  }
@@ -1039,19 +1119,33 @@ class APIClient {
1039
1119
  /**
1040
1120
  * Build request headers
1041
1121
  */
1042
- buildHeaders(config, ctx) {
1122
+ buildHeaders(config, ctx, targetURL) {
1043
1123
  var _a, _b;
1044
1124
  const headers = Object.assign(Object.assign({ 'Content-Type': 'application/json' }, this.config.headers), config.headers);
1045
- // Add auth token if available
1046
- if (this.tokenManager && !config.skipAuth) {
1125
+ // Add auth token if available (only for safe URLs)
1126
+ if (this.tokenManager && !config.skipAuth && this.isSafeURL(targetURL)) {
1047
1127
  const session = this.tokenManager.getSession(ctx);
1048
1128
  if (session === null || session === void 0 ? void 0 : session.accessToken) {
1049
- const injectFn = (_b = (_a = this.config.auth) === null || _a === void 0 ? void 0 : _a.injectToken) !== null && _b !== void 0 ? _b : ((token) => `Bearer ${token}`);
1050
- headers['Authorization'] = injectFn(session.accessToken);
1129
+ const injectFn = (_b = (_a = this.config.auth) === null || _a === void 0 ? void 0 : _a.injectToken) !== null && _b !== void 0 ? _b : ((token, type) => `${type !== null && type !== void 0 ? type : 'Bearer'} ${token}`);
1130
+ headers['Authorization'] = injectFn(session.accessToken, session.tokenType);
1051
1131
  }
1052
1132
  }
1053
1133
  return headers;
1054
1134
  }
1135
+ /**
1136
+ * Check if a URL is safe for token injection (same origin as baseURL)
1137
+ */
1138
+ isSafeURL(url) {
1139
+ try {
1140
+ const requestUrl = new URL(url, this.config.baseURL);
1141
+ const baseUrl = new URL(this.config.baseURL || 'http://localhost');
1142
+ return requestUrl.origin === baseUrl.origin;
1143
+ }
1144
+ catch (_a) {
1145
+ // Only allow relative paths if baseURL is missing or invalid
1146
+ return !url.startsWith('http') && !url.startsWith('//');
1147
+ }
1148
+ }
1055
1149
  /**
1056
1150
  * Login
1057
1151
  */
@@ -1061,7 +1155,7 @@ class APIClient {
1061
1155
  throw new Error('Auth is not configured for this client');
1062
1156
  }
1063
1157
  const context = getContextStore();
1064
- yield this.tokenManager.login(context, credentials, options);
1158
+ return this.tokenManager.login(context, credentials, options);
1065
1159
  });
1066
1160
  }
1067
1161
  /**
@@ -1139,11 +1233,23 @@ function createClient(config) {
1139
1233
  */
1140
1234
  function tokenKit(config) {
1141
1235
  setConfig(config);
1236
+ // Create a serializable version of the config for the runtime
1237
+ const serializableConfig = JSON.parse(JSON.stringify(config, (key, value) => {
1238
+ if (typeof value === 'function')
1239
+ return undefined;
1240
+ return value;
1241
+ }));
1142
1242
  return {
1143
1243
  name: 'astro-tokenkit',
1144
1244
  hooks: {
1145
- 'astro:config:setup': () => {
1146
- // Future-proofing: could add vite aliases or other setup here
1245
+ 'astro:config:setup': ({ updateConfig }) => {
1246
+ updateConfig({
1247
+ vite: {
1248
+ define: {
1249
+ '__TOKENKIT_CONFIG__': JSON.stringify(serializableConfig)
1250
+ }
1251
+ }
1252
+ });
1147
1253
  console.log('[TokenKit] Integration initialized');
1148
1254
  },
1149
1255
  },