astro-tokenkit 1.0.2 → 1.0.6

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.d.ts CHANGED
@@ -1,6 +1,353 @@
1
- export { createClient, APIClient } from './client/client';
2
- export { tokenKit, defineMiddleware } from './integration';
3
- export { createMiddleware } from './middleware';
4
- export type { ClientConfig, AuthConfig, RefreshPolicy, CookieConfig, RetryConfig, RequestOptions, RequestConfig, APIResponse, Session, TokenBundle, FieldMapping, RequestInterceptor, ResponseInterceptor, ErrorInterceptor, TokenKitContext } from './types';
5
- export { APIError, AuthError, NetworkError, TimeoutError, } from './types';
6
- export { parseTime, formatTime } from './utils/time';
1
+ import * as astro from 'astro';
2
+ import { AstroCookies, AstroIntegration, MiddlewareHandler } from 'astro';
3
+ import { AsyncLocalStorage } from 'node:async_hooks';
4
+
5
+ /**
6
+ * Token bundle returned from auth endpoints
7
+ */
8
+ interface TokenBundle {
9
+ accessToken: string;
10
+ refreshToken: string;
11
+ accessExpiresAt: number;
12
+ refreshExpiresAt?: number;
13
+ sessionPayload?: Record<string, any>;
14
+ }
15
+ /**
16
+ * Minimal context required by TokenKit
17
+ */
18
+ interface TokenKitContext {
19
+ cookies: AstroCookies;
20
+ [key: string]: any;
21
+ }
22
+ /**
23
+ * Session information
24
+ */
25
+ interface Session {
26
+ accessToken: string;
27
+ expiresAt: number;
28
+ payload?: Record<string, any>;
29
+ }
30
+ /**
31
+ * Request options
32
+ */
33
+ interface RequestOptions {
34
+ /** Astro context (optional if middleware binds it) */
35
+ ctx?: TokenKitContext;
36
+ /** Additional headers */
37
+ headers?: Record<string, string>;
38
+ /** Request timeout in ms */
39
+ timeout?: number;
40
+ /** Query parameters */
41
+ params?: Record<string, any>;
42
+ /** Skip authentication for this request */
43
+ skipAuth?: boolean;
44
+ /** Custom signal for cancellation */
45
+ signal?: AbortSignal;
46
+ }
47
+ /**
48
+ * Request configuration
49
+ */
50
+ interface RequestConfig extends RequestOptions {
51
+ method: 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE';
52
+ url: string;
53
+ data?: any;
54
+ }
55
+ /**
56
+ * HTTP response
57
+ */
58
+ interface APIResponse<T = any> {
59
+ data: T;
60
+ status: number;
61
+ statusText: string;
62
+ headers: Headers;
63
+ url: string;
64
+ }
65
+ /**
66
+ * Field mapping for auto-detection
67
+ */
68
+ interface FieldMapping {
69
+ accessToken?: string;
70
+ refreshToken?: string;
71
+ expiresAt?: string;
72
+ expiresIn?: string;
73
+ sessionPayload?: string;
74
+ }
75
+ /**
76
+ * Auth configuration
77
+ */
78
+ interface AuthConfig {
79
+ /** Login endpoint (relative to baseURL) */
80
+ login: string;
81
+ /** Refresh endpoint (relative to baseURL) */
82
+ refresh: string;
83
+ /** Logout endpoint (optional, relative to baseURL) */
84
+ logout?: string;
85
+ /** Field mapping (auto-detected if not provided) */
86
+ fields?: FieldMapping;
87
+ /** Custom login response parser */
88
+ parseLogin?: (body: any) => TokenBundle;
89
+ /** Custom refresh response parser */
90
+ parseRefresh?: (body: any) => TokenBundle;
91
+ /** Callback after successful login */
92
+ onLogin?: (bundle: TokenBundle, body: any, ctx: TokenKitContext) => void | Promise<void>;
93
+ /** Custom token injection function (default: Bearer) */
94
+ injectToken?: (token: string) => string;
95
+ /** Refresh policy */
96
+ policy?: RefreshPolicy;
97
+ /** Cookie configuration */
98
+ cookies?: CookieConfig;
99
+ }
100
+ /**
101
+ * Refresh policy
102
+ */
103
+ interface RefreshPolicy {
104
+ /** Refresh before expiry (e.g., '5m' or 300) */
105
+ refreshBefore?: string | number;
106
+ /** Clock skew tolerance (e.g., '1m' or 60) */
107
+ clockSkew?: string | number;
108
+ /** Minimum interval between refreshes (e.g., '30s' or 30) */
109
+ minInterval?: string | number;
110
+ }
111
+ /**
112
+ * Cookie configuration
113
+ */
114
+ interface CookieConfig {
115
+ /** Secure flag (auto-detected from NODE_ENV if not set) */
116
+ secure?: boolean;
117
+ /** SameSite policy */
118
+ sameSite?: 'strict' | 'lax' | 'none';
119
+ /** Cookie domain */
120
+ domain?: string;
121
+ /** Cookie names prefix */
122
+ prefix?: string;
123
+ }
124
+ /**
125
+ * Retry configuration
126
+ */
127
+ interface RetryConfig {
128
+ /** Number of retry attempts */
129
+ attempts?: number;
130
+ /** Status codes to retry */
131
+ statusCodes?: number[];
132
+ /** Backoff strategy */
133
+ backoff?: 'linear' | 'exponential';
134
+ /** Initial delay in ms */
135
+ delay?: number;
136
+ }
137
+ /**
138
+ * Request interceptor
139
+ */
140
+ type RequestInterceptor = (config: RequestConfig, ctx?: TokenKitContext) => RequestConfig | Promise<RequestConfig>;
141
+ /**
142
+ * Response interceptor
143
+ */
144
+ type ResponseInterceptor = <T = any>(response: APIResponse<T>, ctx?: TokenKitContext) => APIResponse<T> | Promise<APIResponse<T>>;
145
+ /**
146
+ * Error interceptor
147
+ */
148
+ type ErrorInterceptor = (error: APIError, ctx?: TokenKitContext) => never | Promise<never>;
149
+ /**
150
+ * Interceptors configuration
151
+ */
152
+ interface InterceptorsConfig {
153
+ request?: RequestInterceptor[];
154
+ response?: ResponseInterceptor[];
155
+ error?: ErrorInterceptor[];
156
+ }
157
+ /**
158
+ * Client configuration
159
+ */
160
+ interface ClientConfig {
161
+ /** Base URL for all requests */
162
+ baseURL: string;
163
+ /** Auth configuration (optional for non-auth clients) */
164
+ auth?: AuthConfig;
165
+ /** Default headers for all requests */
166
+ headers?: Record<string, string>;
167
+ /** Default timeout in ms */
168
+ timeout?: number;
169
+ /** Retry configuration */
170
+ retry?: RetryConfig;
171
+ /** Interceptors */
172
+ interceptors?: InterceptorsConfig;
173
+ /** External AsyncLocalStorage instance (optional) */
174
+ context?: AsyncLocalStorage<any>;
175
+ /** Method to get the context store (optional) */
176
+ getContextStore?: () => TokenKitContext | undefined | null;
177
+ }
178
+ /**
179
+ * API Error
180
+ */
181
+ declare class APIError extends Error {
182
+ status?: number | undefined;
183
+ response?: any | undefined;
184
+ request?: RequestConfig | undefined;
185
+ constructor(message: string, status?: number | undefined, response?: any | undefined, request?: RequestConfig | undefined);
186
+ }
187
+ /**
188
+ * Authentication Error
189
+ */
190
+ declare class AuthError extends APIError {
191
+ constructor(message: string, status?: number, response?: any, request?: RequestConfig);
192
+ }
193
+ /**
194
+ * Network Error
195
+ */
196
+ declare class NetworkError extends APIError {
197
+ constructor(message: string, request?: RequestConfig);
198
+ }
199
+ /**
200
+ * Timeout Error
201
+ */
202
+ declare class TimeoutError extends APIError {
203
+ constructor(message: string, request?: RequestConfig);
204
+ }
205
+
206
+ /**
207
+ * Token Manager handles all token operations
208
+ */
209
+ declare class TokenManager {
210
+ private config;
211
+ private singleFlight;
212
+ private baseURL;
213
+ constructor(config: AuthConfig, baseURL: string);
214
+ /**
215
+ * Perform login
216
+ */
217
+ login(ctx: TokenKitContext, credentials: any): Promise<TokenBundle>;
218
+ /**
219
+ * Perform token refresh
220
+ */
221
+ refresh(ctx: TokenKitContext, refreshToken: string): Promise<TokenBundle | null>;
222
+ /**
223
+ * Internal refresh implementation
224
+ */
225
+ private performRefresh;
226
+ /**
227
+ * Ensure valid tokens (with automatic refresh)
228
+ */
229
+ ensure(ctx: TokenKitContext): Promise<Session | null>;
230
+ /**
231
+ * Logout (clear tokens)
232
+ */
233
+ logout(ctx: TokenKitContext): Promise<void>;
234
+ /**
235
+ * Get current session (no refresh)
236
+ */
237
+ getSession(ctx: TokenKitContext): Session | null;
238
+ /**
239
+ * Check if authenticated
240
+ */
241
+ isAuthenticated(ctx: TokenKitContext): boolean;
242
+ /**
243
+ * Create flight key for single-flight deduplication
244
+ */
245
+ private createFlightKey;
246
+ }
247
+
248
+ /**
249
+ * Configuration for context handling
250
+ */
251
+ interface ContextOptions {
252
+ context?: AsyncLocalStorage<any>;
253
+ getContextStore?: () => TokenKitContext | undefined | null;
254
+ }
255
+
256
+ /**
257
+ * API Client
258
+ */
259
+ declare class APIClient {
260
+ tokenManager?: TokenManager;
261
+ private config;
262
+ contextOptions: ContextOptions;
263
+ constructor(config: ClientConfig);
264
+ /**
265
+ * GET request
266
+ */
267
+ get<T = any>(url: string, options?: RequestOptions): Promise<T>;
268
+ /**
269
+ * POST request
270
+ */
271
+ post<T = any>(url: string, data?: any, options?: RequestOptions): Promise<T>;
272
+ /**
273
+ * PUT request
274
+ */
275
+ put<T = any>(url: string, data?: any, options?: RequestOptions): Promise<T>;
276
+ /**
277
+ * PATCH request
278
+ */
279
+ patch<T = any>(url: string, data?: any, options?: RequestOptions): Promise<T>;
280
+ /**
281
+ * DELETE request
282
+ */
283
+ delete<T = any>(url: string, options?: RequestOptions): Promise<T>;
284
+ /**
285
+ * Generic request method
286
+ */
287
+ request<T = any>(config: RequestConfig): Promise<T>;
288
+ /**
289
+ * Execute single request
290
+ */
291
+ private executeRequest;
292
+ /**
293
+ * Parse response
294
+ */
295
+ private parseResponse;
296
+ /**
297
+ * Build full URL with query params
298
+ */
299
+ private buildURL;
300
+ /**
301
+ * Build request headers
302
+ */
303
+ private buildHeaders;
304
+ /**
305
+ * Login
306
+ */
307
+ login(credentials: any, ctx?: TokenKitContext): Promise<void>;
308
+ /**
309
+ * Logout
310
+ */
311
+ logout(ctx?: TokenKitContext): Promise<void>;
312
+ /**
313
+ * Check if authenticated
314
+ */
315
+ isAuthenticated(ctx?: TokenKitContext): boolean;
316
+ /**
317
+ * Get current session
318
+ */
319
+ getSession(ctx?: TokenKitContext): Session | null;
320
+ }
321
+ /**
322
+ * Create API client
323
+ */
324
+ declare function createClient(config: ClientConfig): APIClient;
325
+
326
+ /**
327
+ * Astro integration for TokenKit
328
+ *
329
+ * This integration facilitates the setup of TokenKit in an Astro project.
330
+ */
331
+ declare function tokenKit(client?: APIClient): AstroIntegration;
332
+ /**
333
+ * Helper to define middleware in a separate file if needed
334
+ */
335
+ declare const defineMiddleware: (client: APIClient) => astro.MiddlewareHandler;
336
+
337
+ /**
338
+ * Create middleware for context binding and automatic token rotation
339
+ */
340
+ declare function createMiddleware(client: APIClient): MiddlewareHandler;
341
+
342
+ /**
343
+ * Parse time string to seconds
344
+ * Supports: '5m', '30s', '1h', '2d'
345
+ */
346
+ declare function parseTime(input: string | number): number;
347
+ /**
348
+ * Format seconds to human-readable string
349
+ */
350
+ declare function formatTime(seconds: number): string;
351
+
352
+ export { APIClient, APIError, AuthError, NetworkError, TimeoutError, createClient, createMiddleware, defineMiddleware, formatTime, parseTime, tokenKit };
353
+ export type { APIResponse, AuthConfig, ClientConfig, CookieConfig, ErrorInterceptor, FieldMapping, RefreshPolicy, RequestConfig, RequestInterceptor, RequestOptions, ResponseInterceptor, RetryConfig, Session, TokenBundle, TokenKitContext };
package/dist/index.js CHANGED
@@ -420,17 +420,29 @@ class TokenManager {
420
420
  method: 'POST',
421
421
  headers: { 'Content-Type': 'application/json' },
422
422
  body: JSON.stringify(credentials),
423
+ }).catch(error => {
424
+ throw new AuthError(`Login request failed: ${error.message}`);
423
425
  });
424
426
  if (!response.ok) {
425
- throw new Error(`Login failed: ${response.status} ${response.statusText}`);
427
+ throw new AuthError(`Login failed: ${response.status} ${response.statusText}`, response.status, response);
426
428
  }
427
- const body = yield response.json();
429
+ const body = yield response.json().catch(() => ({}));
428
430
  // Parse response
429
- const bundle = this.config.parseLogin
430
- ? this.config.parseLogin(body)
431
- : autoDetectFields(body, this.config.fields);
431
+ let bundle;
432
+ try {
433
+ bundle = this.config.parseLogin
434
+ ? this.config.parseLogin(body)
435
+ : autoDetectFields(body, this.config.fields);
436
+ }
437
+ catch (error) {
438
+ throw new AuthError(`Invalid login response: ${error.message}`, response.status, response);
439
+ }
432
440
  // Store in cookies
433
441
  storeTokens(ctx, bundle, this.config.cookies);
442
+ // Call onLogin callback if provided
443
+ if (this.config.onLogin) {
444
+ yield this.config.onLogin(bundle, body, ctx);
445
+ }
434
446
  return bundle;
435
447
  });
436
448
  }
@@ -439,38 +451,54 @@ class TokenManager {
439
451
  */
440
452
  refresh(ctx, refreshToken) {
441
453
  return __awaiter(this, void 0, void 0, function* () {
442
- const url = this.baseURL + this.config.refresh;
443
454
  try {
444
- const response = yield fetch(url, {
445
- method: 'POST',
446
- headers: { 'Content-Type': 'application/json' },
447
- body: JSON.stringify({ refreshToken }),
448
- });
449
- if (!response.ok) {
450
- // 401/403 = invalid refresh token
451
- if (response.status === 401 || response.status === 403) {
452
- clearTokens(ctx, this.config.cookies);
453
- return null;
454
- }
455
- throw new Error(`Refresh failed: ${response.status} ${response.statusText}`);
455
+ return yield this.performRefresh(ctx, refreshToken);
456
+ }
457
+ catch (error) {
458
+ clearTokens(ctx, this.config.cookies);
459
+ throw error;
460
+ }
461
+ });
462
+ }
463
+ /**
464
+ * Internal refresh implementation
465
+ */
466
+ performRefresh(ctx, refreshToken) {
467
+ return __awaiter(this, void 0, void 0, function* () {
468
+ const url = this.baseURL + this.config.refresh;
469
+ const response = yield fetch(url, {
470
+ method: 'POST',
471
+ headers: { 'Content-Type': 'application/json' },
472
+ body: JSON.stringify({ refreshToken }),
473
+ }).catch(error => {
474
+ throw new AuthError(`Refresh request failed: ${error.message}`);
475
+ });
476
+ if (!response.ok) {
477
+ // 401/403 = invalid refresh token
478
+ if (response.status === 401 || response.status === 403) {
479
+ clearTokens(ctx, this.config.cookies);
480
+ return null;
456
481
  }
457
- const body = yield response.json();
458
- // Parse response
459
- const bundle = this.config.parseRefresh
482
+ throw new AuthError(`Refresh failed: ${response.status} ${response.statusText}`, response.status, response);
483
+ }
484
+ const body = yield response.json().catch(() => ({}));
485
+ // Parse response
486
+ let bundle;
487
+ try {
488
+ bundle = this.config.parseRefresh
460
489
  ? this.config.parseRefresh(body)
461
490
  : autoDetectFields(body, this.config.fields);
462
- // Validate bundle
463
- if (!bundle.accessToken || !bundle.refreshToken || !bundle.accessExpiresAt) {
464
- throw new Error('Invalid token bundle returned from refresh endpoint');
465
- }
466
- // Store new tokens
467
- storeTokens(ctx, bundle, this.config.cookies);
468
- return bundle;
469
491
  }
470
492
  catch (error) {
471
- clearTokens(ctx, this.config.cookies);
472
- throw error;
493
+ throw new AuthError(`Invalid refresh response: ${error.message}`, response.status, response);
494
+ }
495
+ // Validate bundle
496
+ if (!bundle.accessToken || !bundle.refreshToken || !bundle.accessExpiresAt) {
497
+ throw new AuthError('Invalid token bundle returned from refresh endpoint', response.status, response);
473
498
  }
499
+ // Store new tokens
500
+ storeTokens(ctx, bundle, this.config.cookies);
501
+ return bundle;
474
502
  });
475
503
  }
476
504
  /**