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/README.md CHANGED
@@ -61,8 +61,8 @@ Now you can use the `api` client anywhere in your Astro pages or components with
61
61
  // src/pages/profile.astro
62
62
  import { api } from 'astro-tokenkit';
63
63
 
64
- // No need to pass context, it's handled by middleware!
65
- const user = await api.get('/me');
64
+ // Request methods return an APIResponse object
65
+ const { data: user } = await api.get('/me');
66
66
  ---
67
67
 
68
68
  <h1>Welcome, {user.name}</h1>
@@ -130,10 +130,10 @@ const specializedClient = createClient({
130
130
  | `loginData` | `Record<string, any>` | Extra data to be sent with login request. |
131
131
  | `refreshData` | `Record<string, any>` | Extra data to be sent with refresh request. |
132
132
  | `refreshRequestField` | `string` | Field name for the refresh token in the refresh request (default: `refreshToken`). |
133
- | `fields` | `FieldMapping` | Custom mapping for token fields in API responses. |
133
+ | `fields` | `FieldMapping` | Custom mapping for token fields in API responses (`accessToken`, `refreshToken`, `expiresAt`, `expiresIn`, `tokenType`, `sessionPayload`). |
134
134
  | `parseLogin` | `Function` | Custom parser for login response: `(body: any) => TokenBundle`. |
135
135
  | `parseRefresh`| `Function` | Custom parser for refresh response: `(body: any) => TokenBundle`. |
136
- | `injectToken` | `Function` | Custom token injection: `(token: string) => string` (default: Bearer). |
136
+ | `injectToken` | `Function` | Custom token injection: `(token: string, type?: string) => string` (default: Bearer). |
137
137
  | `cookies` | `CookieConfig` | Configuration for auth cookies. |
138
138
  | `policy` | `RefreshPolicy` | Strategy for when to trigger token refresh. |
139
139
 
@@ -142,6 +142,7 @@ const specializedClient = createClient({
142
142
  | Property | Type | Description |
143
143
  | :--- | :--- | :--- |
144
144
  | `onLogin` | `Function` | Callback after successful login: `(bundle, body, ctx) => void`. |
145
+ | `onError` | `Function` | Callback after failed login: `(error, ctx) => void`. |
145
146
  | `headers` | `Record<string, string>` | Extra headers for this specific login request. |
146
147
  | `data` | `Record<string, any>` | Extra data for this specific login request. |
147
148
 
@@ -167,7 +168,7 @@ If you prefer not to use middleware, you can bind the Astro context manually for
167
168
  ```typescript
168
169
  import { runWithContext } from 'astro-tokenkit';
169
170
 
170
- const data = await runWithContext(Astro, () => api.get('/data'));
171
+ const { data } = await runWithContext(Astro, () => api.get('/data'));
171
172
  ```
172
173
 
173
174
  ### Interceptors
@@ -190,16 +191,56 @@ const api = createClient({
190
191
 
191
192
  ```typescript
192
193
  // In an API route or server-side component
193
- await api.login({ username, password }, {
194
+ const { data: bundle } = await api.login({ username, password }, {
194
195
  onLogin: (bundle, body, ctx) => {
195
196
  // Post-login logic (e.g., sync session to another store)
196
197
  console.log('User logged in!', bundle.sessionPayload);
198
+ },
199
+ onError: (error, ctx) => {
200
+ // Handle error (e.g., log it or perform cleanup)
201
+ console.error('Login failed:', error.message);
197
202
  }
198
203
  });
199
204
 
200
205
  await api.logout();
201
206
  ```
202
207
 
208
+ ### Using Promises (.then, .catch, .finally)
209
+
210
+ All API methods return a Promise that resolves to an `APIResponse` object. You can use traditional promise chaining:
211
+
212
+ ```typescript
213
+ // Example with GET request
214
+ api.get('/me')
215
+ .then(({ data: user, status }) => {
216
+ console.log(`User ${user.name} fetched with status ${status}`);
217
+ })
218
+ .catch(err => {
219
+ console.error('Failed to fetch user:', err.message);
220
+ })
221
+ .finally(() => {
222
+ console.log('Request finished');
223
+ });
224
+
225
+ // Example with login
226
+ api.login(credentials)
227
+ .then(({ data: token }) => {
228
+ console.log('Successfully logged in!', token.accessToken);
229
+ })
230
+ .catch(err => {
231
+ if (err instanceof AuthError) {
232
+ console.error('Authentication failed:', err.message);
233
+ } else {
234
+ console.error('An unexpected error occurred:', err.message);
235
+ }
236
+ })
237
+ .finally(() => {
238
+ // E.g. stop loading state
239
+ });
240
+ ```
241
+
242
+ > **Note:** Since all methods return an `APIResponse` object, you can use destructuring in `.then()` to access the data directly, which allows for clean syntax like `.then(({ data: token }) => ... )`.
243
+
203
244
  ## License
204
245
 
205
246
  MIT © [oamm](https://github.com/oamm)
@@ -35,6 +35,13 @@ const EXPIRES_IN_FIELDS = [
35
35
  'expiresIn',
36
36
  'ttl',
37
37
  ];
38
+ /**
39
+ * Common field names for token type
40
+ */
41
+ const TOKEN_TYPE_FIELDS = [
42
+ 'token_type',
43
+ 'tokenType',
44
+ ];
38
45
  /**
39
46
  * Common field names for session payload
40
47
  */
@@ -97,10 +104,13 @@ export function autoDetectFields(body, fieldMapping) {
97
104
  }
98
105
  // Detect session payload (optional)
99
106
  const sessionPayload = findField(SESSION_PAYLOAD_FIELDS, fieldMapping === null || fieldMapping === void 0 ? void 0 : fieldMapping.sessionPayload);
107
+ // Detect token type (optional)
108
+ const tokenType = findField(TOKEN_TYPE_FIELDS, fieldMapping === null || fieldMapping === void 0 ? void 0 : fieldMapping.tokenType);
100
109
  return {
101
110
  accessToken,
102
111
  refreshToken,
103
112
  accessExpiresAt,
113
+ tokenType: tokenType || undefined,
104
114
  sessionPayload: sessionPayload || undefined,
105
115
  };
106
116
  }
@@ -114,6 +124,10 @@ export function parseJWTPayload(token) {
114
124
  return null;
115
125
  }
116
126
  const payload = parts[1];
127
+ // Better UTF-8 support for environments with Buffer (like Node.js/Astro)
128
+ if (typeof Buffer !== 'undefined') {
129
+ return JSON.parse(Buffer.from(payload, 'base64').toString('utf8'));
130
+ }
117
131
  const decoded = atob(payload.replace(/-/g, '+').replace(/_/g, '/'));
118
132
  return JSON.parse(decoded);
119
133
  }
@@ -1,3 +1,4 @@
1
+ import { APIResponse } from '../types';
1
2
  import type { TokenBundle, Session, AuthConfig, TokenKitContext, AuthOptions, LoginOptions } from '../types';
2
3
  /**
3
4
  * Token Manager handles all token operations
@@ -10,7 +11,7 @@ export declare class TokenManager {
10
11
  /**
11
12
  * Perform login
12
13
  */
13
- login(ctx: TokenKitContext, credentials: any, options?: LoginOptions): Promise<TokenBundle>;
14
+ login(ctx: TokenKitContext, credentials: any, options?: LoginOptions): Promise<APIResponse<TokenBundle>>;
14
15
  /**
15
16
  * Perform token refresh
16
17
  */
@@ -39,4 +40,8 @@ export declare class TokenManager {
39
40
  * Create flight key for single-flight deduplication
40
41
  */
41
42
  private createFlightKey;
43
+ /**
44
+ * Join base URL and path safely
45
+ */
46
+ private joinURL;
42
47
  }
@@ -54,7 +54,7 @@ export class TokenManager {
54
54
  */
55
55
  login(ctx, credentials, options) {
56
56
  return __awaiter(this, void 0, void 0, function* () {
57
- const url = this.baseURL + this.config.login;
57
+ const url = this.joinURL(this.baseURL, this.config.login);
58
58
  const contentType = this.config.contentType || 'application/json';
59
59
  const headers = Object.assign(Object.assign({ 'Content-Type': contentType }, this.config.headers), options === null || options === void 0 ? void 0 : options.headers);
60
60
  const data = Object.assign(Object.assign(Object.assign({}, this.config.loginData), options === null || options === void 0 ? void 0 : options.data), credentials);
@@ -65,15 +65,25 @@ export class TokenManager {
65
65
  else {
66
66
  requestBody = JSON.stringify(data);
67
67
  }
68
- const response = yield fetch(url, {
69
- method: 'POST',
70
- headers,
71
- body: requestBody,
72
- }).catch(error => {
73
- throw new AuthError(`Login request failed: ${error.message}`);
74
- });
68
+ let response;
69
+ try {
70
+ response = yield fetch(url, {
71
+ method: 'POST',
72
+ headers,
73
+ body: requestBody,
74
+ });
75
+ }
76
+ catch (error) {
77
+ const authError = new AuthError(`Login request failed: ${error.message}`);
78
+ if (options === null || options === void 0 ? void 0 : options.onError)
79
+ yield options.onError(authError, ctx);
80
+ throw authError;
81
+ }
75
82
  if (!response.ok) {
76
- throw new AuthError(`Login failed: ${response.status} ${response.statusText}`, response.status, response);
83
+ const authError = new AuthError(`Login failed: ${response.status} ${response.statusText}`, response.status, response);
84
+ if (options === null || options === void 0 ? void 0 : options.onError)
85
+ yield options.onError(authError, ctx);
86
+ throw authError;
77
87
  }
78
88
  const body = yield response.json().catch(() => ({}));
79
89
  // Parse response
@@ -84,7 +94,10 @@ export class TokenManager {
84
94
  : autoDetectFields(body, this.config.fields);
85
95
  }
86
96
  catch (error) {
87
- throw new AuthError(`Invalid login response: ${error.message}`, response.status, response);
97
+ const authError = new AuthError(`Invalid login response: ${error.message}`, response.status, response);
98
+ if (options === null || options === void 0 ? void 0 : options.onError)
99
+ yield options.onError(authError, ctx);
100
+ throw authError;
88
101
  }
89
102
  // Store in cookies
90
103
  storeTokens(ctx, bundle, this.config.cookies);
@@ -92,7 +105,14 @@ export class TokenManager {
92
105
  if (options === null || options === void 0 ? void 0 : options.onLogin) {
93
106
  yield options.onLogin(bundle, body, ctx);
94
107
  }
95
- return bundle;
108
+ return {
109
+ data: bundle,
110
+ status: response.status,
111
+ statusText: response.statusText,
112
+ headers: response.headers,
113
+ url: response.url,
114
+ ok: response.ok,
115
+ };
96
116
  });
97
117
  }
98
118
  /**
@@ -114,7 +134,7 @@ export class TokenManager {
114
134
  */
115
135
  performRefresh(ctx, refreshToken, options, extraHeaders) {
116
136
  return __awaiter(this, void 0, void 0, function* () {
117
- const url = this.baseURL + this.config.refresh;
137
+ const url = this.joinURL(this.baseURL, this.config.refresh);
118
138
  const contentType = this.config.contentType || 'application/json';
119
139
  const headers = Object.assign(Object.assign({ 'Content-Type': contentType }, this.config.headers), extraHeaders);
120
140
  const refreshField = this.config.refreshRequestField || 'refreshToken';
@@ -126,13 +146,17 @@ export class TokenManager {
126
146
  else {
127
147
  requestBody = JSON.stringify(data);
128
148
  }
129
- const response = yield fetch(url, {
130
- method: 'POST',
131
- headers,
132
- body: requestBody,
133
- }).catch(error => {
149
+ let response;
150
+ try {
151
+ response = yield fetch(url, {
152
+ method: 'POST',
153
+ headers,
154
+ body: requestBody,
155
+ });
156
+ }
157
+ catch (error) {
134
158
  throw new AuthError(`Refresh request failed: ${error.message}`);
135
- });
159
+ }
136
160
  if (!response.ok) {
137
161
  // 401/403 = invalid refresh token
138
162
  if (response.status === 401 || response.status === 403) {
@@ -166,7 +190,7 @@ export class TokenManager {
166
190
  */
167
191
  ensure(ctx, options, headers) {
168
192
  return __awaiter(this, void 0, void 0, function* () {
169
- var _a, _b, _c, _d, _e;
193
+ var _a, _b, _c, _d, _e, _f;
170
194
  const now = Math.floor(Date.now() / 1000);
171
195
  const tokens = retrieveTokens(ctx, this.config.cookies);
172
196
  // No tokens
@@ -182,6 +206,7 @@ export class TokenManager {
182
206
  return {
183
207
  accessToken: bundle.accessToken,
184
208
  expiresAt: bundle.accessExpiresAt,
209
+ tokenType: bundle.tokenType,
185
210
  payload: (_b = (_a = bundle.sessionPayload) !== null && _a !== void 0 ? _a : parseJWTPayload(bundle.accessToken)) !== null && _b !== void 0 ? _b : undefined,
186
211
  };
187
212
  }
@@ -193,6 +218,7 @@ export class TokenManager {
193
218
  return {
194
219
  accessToken: bundle.accessToken,
195
220
  expiresAt: bundle.accessExpiresAt,
221
+ tokenType: bundle.tokenType,
196
222
  payload: (_d = (_c = bundle.sessionPayload) !== null && _c !== void 0 ? _c : parseJWTPayload(bundle.accessToken)) !== null && _d !== void 0 ? _d : undefined,
197
223
  };
198
224
  }
@@ -206,7 +232,8 @@ export class TokenManager {
206
232
  return {
207
233
  accessToken: tokens.accessToken,
208
234
  expiresAt: tokens.expiresAt,
209
- payload: (_e = parseJWTPayload(tokens.accessToken)) !== null && _e !== void 0 ? _e : undefined,
235
+ tokenType: (_e = tokens.tokenType) !== null && _e !== void 0 ? _e : undefined,
236
+ payload: (_f = parseJWTPayload(tokens.accessToken)) !== null && _f !== void 0 ? _f : undefined,
210
237
  };
211
238
  });
212
239
  }
@@ -215,15 +242,22 @@ export class TokenManager {
215
242
  */
216
243
  logout(ctx) {
217
244
  return __awaiter(this, void 0, void 0, function* () {
245
+ var _a;
218
246
  // Optionally call logout endpoint
219
247
  if (this.config.logout) {
220
248
  try {
221
- const url = this.baseURL + this.config.logout;
222
- yield fetch(url, { method: 'POST' });
249
+ const url = this.joinURL(this.baseURL, this.config.logout);
250
+ const session = this.getSession(ctx);
251
+ const headers = {};
252
+ if (session === null || session === void 0 ? void 0 : session.accessToken) {
253
+ const injectFn = (_a = this.config.injectToken) !== null && _a !== void 0 ? _a : ((token, type) => `${type !== null && type !== void 0 ? type : 'Bearer'} ${token}`);
254
+ headers['Authorization'] = injectFn(session.accessToken, session.tokenType);
255
+ }
256
+ yield fetch(url, { method: 'POST', headers });
223
257
  }
224
258
  catch (error) {
225
259
  // Ignore logout endpoint errors
226
- console.warn('Logout endpoint failed:', error);
260
+ console.warn('[TokenKit] Logout endpoint failed:', error);
227
261
  }
228
262
  }
229
263
  clearTokens(ctx, this.config.cookies);
@@ -233,7 +267,7 @@ export class TokenManager {
233
267
  * Get current session (no refresh)
234
268
  */
235
269
  getSession(ctx) {
236
- var _a;
270
+ var _a, _b;
237
271
  const tokens = retrieveTokens(ctx, this.config.cookies);
238
272
  if (!tokens.accessToken || !tokens.expiresAt) {
239
273
  return null;
@@ -241,7 +275,8 @@ export class TokenManager {
241
275
  return {
242
276
  accessToken: tokens.accessToken,
243
277
  expiresAt: tokens.expiresAt,
244
- payload: (_a = parseJWTPayload(tokens.accessToken)) !== null && _a !== void 0 ? _a : undefined,
278
+ tokenType: (_a = tokens.tokenType) !== null && _a !== void 0 ? _a : undefined,
279
+ payload: (_b = parseJWTPayload(tokens.accessToken)) !== null && _b !== void 0 ? _b : undefined,
245
280
  };
246
281
  }
247
282
  /**
@@ -255,12 +290,15 @@ export class TokenManager {
255
290
  * Create flight key for single-flight deduplication
256
291
  */
257
292
  createFlightKey(token) {
258
- let hash = 0;
259
- for (let i = 0; i < token.length; i++) {
260
- const char = token.charCodeAt(i);
261
- hash = ((hash << 5) - hash) + char;
262
- hash = hash & hash;
263
- }
264
- return `flight_${Math.abs(hash).toString(36)}`;
293
+ // Avoid weak hashing of sensitive tokens
294
+ return `refresh_${token}`;
295
+ }
296
+ /**
297
+ * Join base URL and path safely
298
+ */
299
+ joinURL(base, path) {
300
+ const b = base.endsWith('/') ? base : base + '/';
301
+ const p = path.startsWith('/') ? path.slice(1) : path;
302
+ return b + p;
265
303
  }
266
304
  }
@@ -62,5 +62,6 @@ export function isExpired(expiresAt, now, policy = {}) {
62
62
  const clockSkew = typeof normalized.clockSkew === 'number'
63
63
  ? normalized.clockSkew
64
64
  : parseTime(normalized.clockSkew);
65
- return now > expiresAt + clockSkew;
65
+ // Pessimistic: consider it expired if current time + skew is past expiration
66
+ return now + clockSkew > expiresAt;
66
67
  }
@@ -7,6 +7,7 @@ export interface CookieNames {
7
7
  refreshToken: string;
8
8
  expiresAt: string;
9
9
  lastRefreshAt: string;
10
+ tokenType: string;
10
11
  }
11
12
  /**
12
13
  * Get cookie names with optional prefix
@@ -33,6 +34,7 @@ export declare function retrieveTokens(ctx: TokenKitContext, cookieConfig?: Cook
33
34
  refreshToken: string | null;
34
35
  expiresAt: number | null;
35
36
  lastRefreshAt: number | null;
37
+ tokenType: string | null;
36
38
  };
37
39
  /**
38
40
  * Clear all auth cookies
@@ -9,6 +9,7 @@ export function getCookieNames(prefix) {
9
9
  refreshToken: `${p}refresh_token`,
10
10
  expiresAt: `${p}access_expires_at`,
11
11
  lastRefreshAt: `${p}last_refresh_at`,
12
+ tokenType: `${p}token_type`,
12
13
  };
13
14
  }
14
15
  /**
@@ -44,20 +45,25 @@ export function storeTokens(ctx, bundle, cookieConfig = {}) {
44
45
  ctx.cookies.set(names.expiresAt, bundle.accessExpiresAt.toString(), Object.assign(Object.assign({}, options), { maxAge: accessMaxAge, path: '/' }));
45
46
  // Set last refresh timestamp
46
47
  ctx.cookies.set(names.lastRefreshAt, now.toString(), Object.assign(Object.assign({}, options), { maxAge: accessMaxAge, path: '/' }));
48
+ // Set token type if available
49
+ if (bundle.tokenType) {
50
+ ctx.cookies.set(names.tokenType, bundle.tokenType, Object.assign(Object.assign({}, options), { maxAge: accessMaxAge, path: '/' }));
51
+ }
47
52
  }
48
53
  /**
49
54
  * Retrieve tokens from cookies
50
55
  */
51
56
  export function retrieveTokens(ctx, cookieConfig = {}) {
52
- var _a, _b, _c, _d;
57
+ var _a, _b, _c, _d, _e;
53
58
  const names = getCookieNames(cookieConfig.prefix);
54
59
  const accessToken = ((_a = ctx.cookies.get(names.accessToken)) === null || _a === void 0 ? void 0 : _a.value) || null;
55
60
  const refreshToken = ((_b = ctx.cookies.get(names.refreshToken)) === null || _b === void 0 ? void 0 : _b.value) || null;
56
- const expiresAtStr = (_c = ctx.cookies.get(names.expiresAt)) === null || _c === void 0 ? void 0 : _c.value;
61
+ const tokenType = ((_c = ctx.cookies.get(names.tokenType)) === null || _c === void 0 ? void 0 : _c.value) || null;
62
+ const expiresAtStr = (_d = ctx.cookies.get(names.expiresAt)) === null || _d === void 0 ? void 0 : _d.value;
57
63
  const expiresAt = expiresAtStr ? parseInt(expiresAtStr, 10) : null;
58
- const lastRefreshAtStr = (_d = ctx.cookies.get(names.lastRefreshAt)) === null || _d === void 0 ? void 0 : _d.value;
64
+ const lastRefreshAtStr = (_e = ctx.cookies.get(names.lastRefreshAt)) === null || _e === void 0 ? void 0 : _e.value;
59
65
  const lastRefreshAt = lastRefreshAtStr ? parseInt(lastRefreshAtStr, 10) : null;
60
- return { accessToken, refreshToken, expiresAt, lastRefreshAt };
66
+ return { accessToken, refreshToken, expiresAt, lastRefreshAt, tokenType };
61
67
  }
62
68
  /**
63
69
  * Clear all auth cookies
@@ -69,4 +75,5 @@ export function clearTokens(ctx, cookieConfig = {}) {
69
75
  ctx.cookies.delete(names.refreshToken, Object.assign(Object.assign({}, options), { path: '/' }));
70
76
  ctx.cookies.delete(names.expiresAt, Object.assign(Object.assign({}, options), { path: '/' }));
71
77
  ctx.cookies.delete(names.lastRefreshAt, Object.assign(Object.assign({}, options), { path: '/' }));
78
+ ctx.cookies.delete(names.tokenType, Object.assign(Object.assign({}, options), { path: '/' }));
72
79
  }
@@ -1,4 +1,4 @@
1
- import type { ClientConfig, RequestConfig, RequestOptions, Session, TokenKitConfig, LoginOptions } from '../types';
1
+ import type { APIResponse, ClientConfig, RequestConfig, RequestOptions, Session, TokenKitConfig, LoginOptions, TokenBundle } from '../types';
2
2
  import { TokenManager } from '../auth/manager';
3
3
  /**
4
4
  * API Client
@@ -25,27 +25,27 @@ export declare class APIClient {
25
25
  /**
26
26
  * GET request
27
27
  */
28
- get<T = any>(url: string, options?: RequestOptions): Promise<T>;
28
+ get<T = any>(url: string, options?: RequestOptions): Promise<APIResponse<T>>;
29
29
  /**
30
30
  * POST request
31
31
  */
32
- post<T = any>(url: string, data?: any, options?: RequestOptions): Promise<T>;
32
+ post<T = any>(url: string, data?: any, options?: RequestOptions): Promise<APIResponse<T>>;
33
33
  /**
34
34
  * PUT request
35
35
  */
36
- put<T = any>(url: string, data?: any, options?: RequestOptions): Promise<T>;
36
+ put<T = any>(url: string, data?: any, options?: RequestOptions): Promise<APIResponse<T>>;
37
37
  /**
38
38
  * PATCH request
39
39
  */
40
- patch<T = any>(url: string, data?: any, options?: RequestOptions): Promise<T>;
40
+ patch<T = any>(url: string, data?: any, options?: RequestOptions): Promise<APIResponse<T>>;
41
41
  /**
42
42
  * DELETE request
43
43
  */
44
- delete<T = any>(url: string, options?: RequestOptions): Promise<T>;
44
+ delete<T = any>(url: string, options?: RequestOptions): Promise<APIResponse<T>>;
45
45
  /**
46
46
  * Generic request method
47
47
  */
48
- request<T = any>(config: RequestConfig): Promise<T>;
48
+ request<T = any>(config: RequestConfig): Promise<APIResponse<T>>;
49
49
  /**
50
50
  * Execute single request
51
51
  */
@@ -62,10 +62,14 @@ export declare class APIClient {
62
62
  * Build request headers
63
63
  */
64
64
  private buildHeaders;
65
+ /**
66
+ * Check if a URL is safe for token injection (same origin as baseURL)
67
+ */
68
+ private isSafeURL;
65
69
  /**
66
70
  * Login
67
71
  */
68
- login(credentials: any, options?: LoginOptions): Promise<void>;
72
+ login(credentials: any, options?: LoginOptions): Promise<APIResponse<TokenBundle>>;
69
73
  /**
70
74
  * Logout
71
75
  */
@@ -115,15 +115,12 @@ export class APIClient {
115
115
  return __awaiter(this, void 0, void 0, function* () {
116
116
  const ctx = getContextStore();
117
117
  let attempt = 0;
118
- let lastError;
119
118
  while (true) {
120
119
  attempt++;
121
120
  try {
122
- const response = yield this.executeRequest(config, ctx, attempt);
123
- return response.data;
121
+ return yield this.executeRequest(config, ctx, attempt);
124
122
  }
125
123
  catch (error) {
126
- lastError = error;
127
124
  // Check if we should retry
128
125
  if (shouldRetry(error.status, attempt, this.config.retry)) {
129
126
  const delay = calculateDelay(attempt, this.config.retry);
@@ -149,7 +146,7 @@ export class APIClient {
149
146
  // Build full URL
150
147
  const fullURL = this.buildURL(config.url, config.params);
151
148
  // Build headers
152
- const headers = this.buildHeaders(config, ctx);
149
+ const headers = this.buildHeaders(config, ctx, fullURL);
153
150
  // Build request init
154
151
  const init = {
155
152
  method: config.method,
@@ -246,6 +243,7 @@ export class APIClient {
246
243
  statusText: response.statusText,
247
244
  headers: response.headers,
248
245
  url,
246
+ ok: response.ok,
249
247
  };
250
248
  });
251
249
  }
@@ -267,19 +265,33 @@ export class APIClient {
267
265
  /**
268
266
  * Build request headers
269
267
  */
270
- buildHeaders(config, ctx) {
268
+ buildHeaders(config, ctx, targetURL) {
271
269
  var _a, _b;
272
270
  const headers = Object.assign(Object.assign({ 'Content-Type': 'application/json' }, this.config.headers), config.headers);
273
- // Add auth token if available
274
- if (this.tokenManager && !config.skipAuth) {
271
+ // Add auth token if available (only for safe URLs)
272
+ if (this.tokenManager && !config.skipAuth && this.isSafeURL(targetURL)) {
275
273
  const session = this.tokenManager.getSession(ctx);
276
274
  if (session === null || session === void 0 ? void 0 : session.accessToken) {
277
- const injectFn = (_b = (_a = this.config.auth) === null || _a === void 0 ? void 0 : _a.injectToken) !== null && _b !== void 0 ? _b : ((token) => `Bearer ${token}`);
278
- headers['Authorization'] = injectFn(session.accessToken);
275
+ 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}`);
276
+ headers['Authorization'] = injectFn(session.accessToken, session.tokenType);
279
277
  }
280
278
  }
281
279
  return headers;
282
280
  }
281
+ /**
282
+ * Check if a URL is safe for token injection (same origin as baseURL)
283
+ */
284
+ isSafeURL(url) {
285
+ try {
286
+ const requestUrl = new URL(url, this.config.baseURL);
287
+ const baseUrl = new URL(this.config.baseURL || 'http://localhost');
288
+ return requestUrl.origin === baseUrl.origin;
289
+ }
290
+ catch (_a) {
291
+ // Only allow relative paths if baseURL is missing or invalid
292
+ return !url.startsWith('http') && !url.startsWith('//');
293
+ }
294
+ }
283
295
  /**
284
296
  * Login
285
297
  */
@@ -289,7 +301,7 @@ export class APIClient {
289
301
  throw new Error('Auth is not configured for this client');
290
302
  }
291
303
  const context = getContextStore();
292
- yield this.tokenManager.login(context, credentials, options);
304
+ return this.tokenManager.login(context, credentials, options);
293
305
  });
294
306
  }
295
307
  /**
package/dist/config.js CHANGED
@@ -1,43 +1,63 @@
1
1
  // packages/astro-tokenkit/src/config.ts
2
2
  import { TokenManager } from "./auth/manager";
3
- let config = {
4
- runWithContext: undefined,
5
- getContextStore: undefined,
6
- setContextStore: undefined,
7
- baseURL: "",
8
- };
9
- let tokenManager;
3
+ const CONFIG_KEY = Symbol.for('astro-tokenkit.config');
4
+ const MANAGER_KEY = Symbol.for('astro-tokenkit.manager');
5
+ const globalStorage = globalThis;
6
+ // Initialize global storage if not present
7
+ if (!globalStorage[CONFIG_KEY]) {
8
+ globalStorage[CONFIG_KEY] = {
9
+ runWithContext: undefined,
10
+ getContextStore: undefined,
11
+ setContextStore: undefined,
12
+ baseURL: "",
13
+ };
14
+ }
10
15
  /**
11
16
  * Set configuration
12
17
  */
13
18
  export function setConfig(userConfig) {
14
- const finalConfig = Object.assign(Object.assign({}, config), userConfig);
19
+ const currentConfig = globalStorage[CONFIG_KEY];
20
+ const finalConfig = Object.assign(Object.assign({}, currentConfig), userConfig);
15
21
  // Validate that getter and setter are defined together
16
22
  if ((finalConfig.getContextStore && !finalConfig.setContextStore) ||
17
23
  (!finalConfig.getContextStore && finalConfig.setContextStore)) {
18
24
  throw new Error("[TokenKit] getContextStore and setContextStore must be defined together.");
19
25
  }
20
- config = finalConfig;
26
+ globalStorage[CONFIG_KEY] = finalConfig;
21
27
  // Re-initialize global token manager if auth changed
22
- if (config.auth) {
23
- tokenManager = new TokenManager(config.auth, config.baseURL);
28
+ if (finalConfig.auth) {
29
+ globalStorage[MANAGER_KEY] = new TokenManager(finalConfig.auth, finalConfig.baseURL);
30
+ }
31
+ else {
32
+ globalStorage[MANAGER_KEY] = undefined;
24
33
  }
25
34
  }
26
35
  /**
27
36
  * Get current configuration
28
37
  */
29
38
  export function getConfig() {
30
- return config;
39
+ return globalStorage[CONFIG_KEY];
31
40
  }
32
41
  /**
33
42
  * Get global token manager
34
43
  */
35
44
  export function getTokenManager() {
36
- return tokenManager;
45
+ return globalStorage[MANAGER_KEY];
37
46
  }
38
47
  /**
39
48
  * Set global token manager (mainly for testing)
40
49
  */
41
50
  export function setTokenManager(manager) {
42
- tokenManager = manager;
51
+ globalStorage[MANAGER_KEY] = manager;
52
+ }
53
+ // Handle injected configuration from Astro integration
54
+ try {
55
+ // @ts-ignore
56
+ const injectedConfig = typeof __TOKENKIT_CONFIG__ !== 'undefined' ? __TOKENKIT_CONFIG__ : undefined;
57
+ if (injectedConfig) {
58
+ setConfig(injectedConfig);
59
+ }
60
+ }
61
+ catch (e) {
62
+ // Ignore errors in environments where __TOKENKIT_CONFIG__ might be restricted
43
63
  }