astro-tokenkit 1.0.0

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.js ADDED
@@ -0,0 +1,983 @@
1
+ import { AsyncLocalStorage } from 'node:async_hooks';
2
+
3
+ /******************************************************************************
4
+ Copyright (c) Microsoft Corporation.
5
+
6
+ Permission to use, copy, modify, and/or distribute this software for any
7
+ purpose with or without fee is hereby granted.
8
+
9
+ THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
10
+ REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
11
+ AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
12
+ INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
13
+ LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
14
+ OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
15
+ PERFORMANCE OF THIS SOFTWARE.
16
+ ***************************************************************************** */
17
+ /* global Reflect, Promise, SuppressedError, Symbol, Iterator */
18
+
19
+
20
+ function __awaiter(thisArg, _arguments, P, generator) {
21
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
22
+ return new (P || (P = Promise))(function (resolve, reject) {
23
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
24
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
25
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
26
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
27
+ });
28
+ }
29
+
30
+ typeof SuppressedError === "function" ? SuppressedError : function (error, suppressed, message) {
31
+ var e = new Error(message);
32
+ return e.name = "SuppressedError", e.error = error, e.suppressed = suppressed, e;
33
+ };
34
+
35
+ // packages/astro-tokenkit/src/types.ts
36
+ /**
37
+ * API Error
38
+ */
39
+ class APIError extends Error {
40
+ constructor(message, status, response, request) {
41
+ super(message);
42
+ this.status = status;
43
+ this.response = response;
44
+ this.request = request;
45
+ this.name = 'APIError';
46
+ }
47
+ }
48
+ /**
49
+ * Authentication Error
50
+ */
51
+ class AuthError extends APIError {
52
+ constructor(message, status, response, request) {
53
+ super(message, status, response, request);
54
+ this.name = 'AuthError';
55
+ }
56
+ }
57
+ /**
58
+ * Network Error
59
+ */
60
+ class NetworkError extends APIError {
61
+ constructor(message, request) {
62
+ super(message, undefined, undefined, request);
63
+ this.name = 'NetworkError';
64
+ }
65
+ }
66
+ /**
67
+ * Timeout Error
68
+ */
69
+ class TimeoutError extends APIError {
70
+ constructor(message, request) {
71
+ super(message, undefined, undefined, request);
72
+ this.name = 'TimeoutError';
73
+ }
74
+ }
75
+
76
+ // packages/astro-tokenkit/src/auth/detector.ts
77
+ /**
78
+ * Common field names for access tokens
79
+ */
80
+ const ACCESS_TOKEN_FIELDS = [
81
+ 'access_token',
82
+ 'accessToken',
83
+ 'token',
84
+ 'jwt',
85
+ 'id_token',
86
+ 'idToken',
87
+ ];
88
+ /**
89
+ * Common field names for refresh tokens
90
+ */
91
+ const REFRESH_TOKEN_FIELDS = [
92
+ 'refresh_token',
93
+ 'refreshToken',
94
+ 'refresh',
95
+ ];
96
+ /**
97
+ * Common field names for expiration timestamp
98
+ */
99
+ const EXPIRES_AT_FIELDS = [
100
+ 'expires_at',
101
+ 'expiresAt',
102
+ 'exp',
103
+ 'expiry',
104
+ ];
105
+ /**
106
+ * Common field names for expires_in (seconds)
107
+ */
108
+ const EXPIRES_IN_FIELDS = [
109
+ 'expires_in',
110
+ 'expiresIn',
111
+ 'ttl',
112
+ ];
113
+ /**
114
+ * Common field names for session payload
115
+ */
116
+ const SESSION_PAYLOAD_FIELDS = [
117
+ 'user',
118
+ 'profile',
119
+ 'account',
120
+ 'data',
121
+ ];
122
+ /**
123
+ * Auto-detect token fields from response body
124
+ */
125
+ function autoDetectFields(body, fieldMapping) {
126
+ // Helper to find field
127
+ const findField = (candidates, mapping) => {
128
+ if (mapping && body[mapping] !== undefined) {
129
+ return body[mapping];
130
+ }
131
+ for (const candidate of candidates) {
132
+ if (body[candidate] !== undefined) {
133
+ return body[candidate];
134
+ }
135
+ }
136
+ return undefined;
137
+ };
138
+ // Detect access token
139
+ const accessToken = findField(ACCESS_TOKEN_FIELDS, fieldMapping === null || fieldMapping === void 0 ? void 0 : fieldMapping.accessToken);
140
+ if (!accessToken) {
141
+ throw new Error(`Could not detect access token field. Tried: ${ACCESS_TOKEN_FIELDS.join(', ')}. ` +
142
+ `Provide custom parseLogin/parseRefresh or field mapping.`);
143
+ }
144
+ // Detect refresh token
145
+ const refreshToken = findField(REFRESH_TOKEN_FIELDS, fieldMapping === null || fieldMapping === void 0 ? void 0 : fieldMapping.refreshToken);
146
+ if (!refreshToken) {
147
+ throw new Error(`Could not detect refresh token field. Tried: ${REFRESH_TOKEN_FIELDS.join(', ')}. ` +
148
+ `Provide custom parseLogin/parseRefresh or field mapping.`);
149
+ }
150
+ // Detect expiration
151
+ let accessExpiresAt;
152
+ // Try expires_at first (timestamp)
153
+ const expiresAtValue = findField(EXPIRES_AT_FIELDS, fieldMapping === null || fieldMapping === void 0 ? void 0 : fieldMapping.expiresAt);
154
+ if (expiresAtValue !== undefined) {
155
+ accessExpiresAt = typeof expiresAtValue === 'number'
156
+ ? expiresAtValue
157
+ : parseInt(expiresAtValue, 10);
158
+ }
159
+ // Try expires_in (seconds from now)
160
+ if (accessExpiresAt === undefined) {
161
+ const expiresInValue = findField(EXPIRES_IN_FIELDS, fieldMapping === null || fieldMapping === void 0 ? void 0 : fieldMapping.expiresIn);
162
+ if (expiresInValue !== undefined) {
163
+ const expiresIn = typeof expiresInValue === 'number'
164
+ ? expiresInValue
165
+ : parseInt(expiresInValue, 10);
166
+ accessExpiresAt = Math.floor(Date.now() / 1000) + expiresIn;
167
+ }
168
+ }
169
+ if (accessExpiresAt === undefined) {
170
+ throw new Error(`Could not detect expiration field. Tried: ${[...EXPIRES_AT_FIELDS, ...EXPIRES_IN_FIELDS].join(', ')}. ` +
171
+ `Provide custom parseLogin/parseRefresh or field mapping.`);
172
+ }
173
+ // Detect session payload (optional)
174
+ const sessionPayload = findField(SESSION_PAYLOAD_FIELDS, fieldMapping === null || fieldMapping === void 0 ? void 0 : fieldMapping.sessionPayload);
175
+ return {
176
+ accessToken,
177
+ refreshToken,
178
+ accessExpiresAt,
179
+ sessionPayload: sessionPayload || undefined,
180
+ };
181
+ }
182
+ /**
183
+ * Parse JWT payload without verification (for reading only)
184
+ */
185
+ function parseJWTPayload(token) {
186
+ try {
187
+ const parts = token.split('.');
188
+ if (parts.length !== 3) {
189
+ return null;
190
+ }
191
+ const payload = parts[1];
192
+ const decoded = atob(payload.replace(/-/g, '+').replace(/_/g, '/'));
193
+ return JSON.parse(decoded);
194
+ }
195
+ catch (_a) {
196
+ return null;
197
+ }
198
+ }
199
+
200
+ // packages/astro-tokenkit/src/auth/storage.ts
201
+ /**
202
+ * Get cookie names with optional prefix
203
+ */
204
+ function getCookieNames(prefix) {
205
+ const p = prefix ? `${prefix}_` : '';
206
+ return {
207
+ accessToken: `${p}access_token`,
208
+ refreshToken: `${p}refresh_token`,
209
+ expiresAt: `${p}access_expires_at`,
210
+ lastRefreshAt: `${p}last_refresh_at`,
211
+ };
212
+ }
213
+ /**
214
+ * Get cookie options with smart defaults
215
+ */
216
+ function getCookieOptions(config = {}) {
217
+ var _a, _b;
218
+ const isProduction = process.env.NODE_ENV === 'production';
219
+ return {
220
+ secure: (_a = config.secure) !== null && _a !== void 0 ? _a : isProduction,
221
+ sameSite: (_b = config.sameSite) !== null && _b !== void 0 ? _b : 'lax',
222
+ httpOnly: true, // Always HttpOnly for security
223
+ domain: config.domain,
224
+ };
225
+ }
226
+ /**
227
+ * Store token bundle in cookies
228
+ */
229
+ function storeTokens(ctx, bundle, cookieConfig = {}) {
230
+ const names = getCookieNames(cookieConfig.prefix);
231
+ const options = getCookieOptions(cookieConfig);
232
+ const now = Math.floor(Date.now() / 1000);
233
+ // Calculate max age
234
+ const accessMaxAge = Math.max(0, bundle.accessExpiresAt - now);
235
+ const refreshMaxAge = bundle.refreshExpiresAt
236
+ ? Math.max(0, bundle.refreshExpiresAt - now)
237
+ : 7 * 24 * 60 * 60; // Default 7 days
238
+ // Set access token
239
+ ctx.cookies.set(names.accessToken, bundle.accessToken, Object.assign(Object.assign({}, options), { maxAge: accessMaxAge, path: '/' }));
240
+ // Set refresh token (restricted path for security)
241
+ ctx.cookies.set(names.refreshToken, bundle.refreshToken, Object.assign(Object.assign({}, options), { maxAge: refreshMaxAge, path: '/' }));
242
+ // Set expiration timestamp
243
+ ctx.cookies.set(names.expiresAt, bundle.accessExpiresAt.toString(), Object.assign(Object.assign({}, options), { maxAge: accessMaxAge, path: '/' }));
244
+ // Set last refresh timestamp
245
+ ctx.cookies.set(names.lastRefreshAt, now.toString(), Object.assign(Object.assign({}, options), { maxAge: accessMaxAge, path: '/' }));
246
+ }
247
+ /**
248
+ * Retrieve tokens from cookies
249
+ */
250
+ function retrieveTokens(ctx, cookieConfig = {}) {
251
+ var _a, _b, _c, _d;
252
+ const names = getCookieNames(cookieConfig.prefix);
253
+ const accessToken = ((_a = ctx.cookies.get(names.accessToken)) === null || _a === void 0 ? void 0 : _a.value) || null;
254
+ const refreshToken = ((_b = ctx.cookies.get(names.refreshToken)) === null || _b === void 0 ? void 0 : _b.value) || null;
255
+ const expiresAtStr = (_c = ctx.cookies.get(names.expiresAt)) === null || _c === void 0 ? void 0 : _c.value;
256
+ const expiresAt = expiresAtStr ? parseInt(expiresAtStr, 10) : null;
257
+ const lastRefreshAtStr = (_d = ctx.cookies.get(names.lastRefreshAt)) === null || _d === void 0 ? void 0 : _d.value;
258
+ const lastRefreshAt = lastRefreshAtStr ? parseInt(lastRefreshAtStr, 10) : null;
259
+ return { accessToken, refreshToken, expiresAt, lastRefreshAt };
260
+ }
261
+ /**
262
+ * Clear all auth cookies
263
+ */
264
+ function clearTokens(ctx, cookieConfig = {}) {
265
+ const names = getCookieNames(cookieConfig.prefix);
266
+ const options = getCookieOptions(cookieConfig);
267
+ ctx.cookies.delete(names.accessToken, Object.assign(Object.assign({}, options), { path: '/' }));
268
+ ctx.cookies.delete(names.refreshToken, Object.assign(Object.assign({}, options), { path: '/' }));
269
+ ctx.cookies.delete(names.expiresAt, Object.assign(Object.assign({}, options), { path: '/' }));
270
+ ctx.cookies.delete(names.lastRefreshAt, Object.assign(Object.assign({}, options), { path: '/' }));
271
+ }
272
+
273
+ // packages/astro-tokenkit/src/utils/time.ts
274
+ /**
275
+ * Parse time string to seconds
276
+ * Supports: '5m', '30s', '1h', '2d'
277
+ */
278
+ function parseTime(input) {
279
+ if (typeof input === 'number') {
280
+ return input;
281
+ }
282
+ const match = input.match(/^(\d+)([smhd])$/);
283
+ if (!match) {
284
+ throw new Error(`Invalid time format: ${input}. Use format like '5m', '30s', '1h', '2d'`);
285
+ }
286
+ const value = parseInt(match[1], 10);
287
+ const unit = match[2];
288
+ const multipliers = {
289
+ s: 1,
290
+ m: 60,
291
+ h: 60 * 60,
292
+ d: 60 * 60 * 24,
293
+ };
294
+ return value * multipliers[unit];
295
+ }
296
+ /**
297
+ * Format seconds to human-readable string
298
+ */
299
+ function formatTime(seconds) {
300
+ if (seconds < 60)
301
+ return `${seconds}s`;
302
+ if (seconds < 3600)
303
+ return `${Math.floor(seconds / 60)}m`;
304
+ if (seconds < 86400)
305
+ return `${Math.floor(seconds / 3600)}h`;
306
+ return `${Math.floor(seconds / 86400)}d`;
307
+ }
308
+
309
+ // packages/astro-tokenkit/src/auth/policy.ts
310
+ /**
311
+ * Default refresh policy
312
+ */
313
+ const DEFAULT_POLICY = {
314
+ refreshBefore: 300, // 5 minutes
315
+ clockSkew: 60, // 1 minute
316
+ minInterval: 30, // 30 seconds
317
+ };
318
+ /**
319
+ * Normalize refresh policy (convert time strings to seconds)
320
+ */
321
+ function normalizePolicy(policy = {}) {
322
+ return {
323
+ refreshBefore: policy.refreshBefore
324
+ ? parseTime(policy.refreshBefore)
325
+ : DEFAULT_POLICY.refreshBefore,
326
+ clockSkew: policy.clockSkew
327
+ ? parseTime(policy.clockSkew)
328
+ : DEFAULT_POLICY.clockSkew,
329
+ minInterval: policy.minInterval
330
+ ? parseTime(policy.minInterval)
331
+ : DEFAULT_POLICY.minInterval,
332
+ };
333
+ }
334
+ /**
335
+ * Check if token should be refreshed
336
+ */
337
+ function shouldRefresh(expiresAt, now, lastRefreshAt, policy = {}) {
338
+ const normalized = normalizePolicy(policy);
339
+ const refreshBefore = typeof normalized.refreshBefore === 'number'
340
+ ? normalized.refreshBefore
341
+ : parseTime(normalized.refreshBefore);
342
+ const clockSkew = typeof normalized.clockSkew === 'number'
343
+ ? normalized.clockSkew
344
+ : parseTime(normalized.clockSkew);
345
+ const minInterval = typeof normalized.minInterval === 'number'
346
+ ? normalized.minInterval
347
+ : parseTime(normalized.minInterval);
348
+ // Adjust for clock skew
349
+ const adjustedNow = now + clockSkew;
350
+ // Check if near expiration
351
+ const timeUntilExpiry = expiresAt - adjustedNow;
352
+ if (timeUntilExpiry > refreshBefore) {
353
+ return false;
354
+ }
355
+ // Check minimum interval
356
+ if (lastRefreshAt !== null) {
357
+ const timeSinceLastRefresh = now - lastRefreshAt;
358
+ if (timeSinceLastRefresh < minInterval) {
359
+ return false;
360
+ }
361
+ }
362
+ return true;
363
+ }
364
+ /**
365
+ * Check if token is expired
366
+ */
367
+ function isExpired(expiresAt, now, policy = {}) {
368
+ const normalized = normalizePolicy(policy);
369
+ const clockSkew = typeof normalized.clockSkew === 'number'
370
+ ? normalized.clockSkew
371
+ : parseTime(normalized.clockSkew);
372
+ return now > expiresAt + clockSkew;
373
+ }
374
+
375
+ // packages/astro-tokenkit/src/auth/manager.ts
376
+ /**
377
+ * Single-flight refresh manager
378
+ */
379
+ class SingleFlight {
380
+ constructor() {
381
+ this.inFlight = new Map();
382
+ }
383
+ execute(key, fn) {
384
+ return __awaiter(this, void 0, void 0, function* () {
385
+ const existing = this.inFlight.get(key);
386
+ if (existing)
387
+ return existing;
388
+ const promise = this.doExecute(key, fn);
389
+ this.inFlight.set(key, promise);
390
+ return promise;
391
+ });
392
+ }
393
+ doExecute(key, fn) {
394
+ return __awaiter(this, void 0, void 0, function* () {
395
+ try {
396
+ return yield fn();
397
+ }
398
+ finally {
399
+ this.inFlight.delete(key);
400
+ }
401
+ });
402
+ }
403
+ }
404
+ /**
405
+ * Token Manager handles all token operations
406
+ */
407
+ class TokenManager {
408
+ constructor(config, baseURL) {
409
+ this.config = config;
410
+ this.singleFlight = new SingleFlight();
411
+ this.baseURL = baseURL;
412
+ }
413
+ /**
414
+ * Perform login
415
+ */
416
+ login(ctx, credentials) {
417
+ return __awaiter(this, void 0, void 0, function* () {
418
+ const url = this.baseURL + this.config.login;
419
+ const response = yield fetch(url, {
420
+ method: 'POST',
421
+ headers: { 'Content-Type': 'application/json' },
422
+ body: JSON.stringify(credentials),
423
+ });
424
+ if (!response.ok) {
425
+ throw new Error(`Login failed: ${response.status} ${response.statusText}`);
426
+ }
427
+ const body = yield response.json();
428
+ // Parse response
429
+ const bundle = this.config.parseLogin
430
+ ? this.config.parseLogin(body)
431
+ : autoDetectFields(body, this.config.fields);
432
+ // Store in cookies
433
+ storeTokens(ctx, bundle, this.config.cookies);
434
+ return bundle;
435
+ });
436
+ }
437
+ /**
438
+ * Perform token refresh
439
+ */
440
+ refresh(ctx, refreshToken) {
441
+ return __awaiter(this, void 0, void 0, function* () {
442
+ const url = this.baseURL + this.config.refresh;
443
+ 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}`);
456
+ }
457
+ const body = yield response.json();
458
+ // Parse response
459
+ const bundle = this.config.parseRefresh
460
+ ? this.config.parseRefresh(body)
461
+ : 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
+ }
470
+ catch (error) {
471
+ clearTokens(ctx, this.config.cookies);
472
+ throw error;
473
+ }
474
+ });
475
+ }
476
+ /**
477
+ * Ensure valid tokens (with automatic refresh)
478
+ */
479
+ ensure(ctx) {
480
+ return __awaiter(this, void 0, void 0, function* () {
481
+ var _a, _b, _c, _d, _e;
482
+ const now = Math.floor(Date.now() / 1000);
483
+ const tokens = retrieveTokens(ctx, this.config.cookies);
484
+ // No tokens
485
+ if (!tokens.accessToken || !tokens.refreshToken || !tokens.expiresAt) {
486
+ return null;
487
+ }
488
+ // Token expired
489
+ if (isExpired(tokens.expiresAt, now, this.config.policy)) {
490
+ const flightKey = this.createFlightKey(tokens.refreshToken);
491
+ const bundle = yield this.singleFlight.execute(flightKey, () => this.refresh(ctx, tokens.refreshToken));
492
+ if (!bundle)
493
+ return null;
494
+ return {
495
+ accessToken: bundle.accessToken,
496
+ expiresAt: bundle.accessExpiresAt,
497
+ payload: (_b = (_a = bundle.sessionPayload) !== null && _a !== void 0 ? _a : parseJWTPayload(bundle.accessToken)) !== null && _b !== void 0 ? _b : undefined,
498
+ };
499
+ }
500
+ // Proactive refresh
501
+ if (shouldRefresh(tokens.expiresAt, now, tokens.lastRefreshAt, this.config.policy)) {
502
+ const flightKey = this.createFlightKey(tokens.refreshToken);
503
+ const bundle = yield this.singleFlight.execute(flightKey, () => this.refresh(ctx, tokens.refreshToken));
504
+ if (bundle) {
505
+ return {
506
+ accessToken: bundle.accessToken,
507
+ expiresAt: bundle.accessExpiresAt,
508
+ payload: (_d = (_c = bundle.sessionPayload) !== null && _c !== void 0 ? _c : parseJWTPayload(bundle.accessToken)) !== null && _d !== void 0 ? _d : undefined,
509
+ };
510
+ }
511
+ // Refresh failed, check if tokens still exist
512
+ const currentTokens = retrieveTokens(ctx, this.config.cookies);
513
+ if (!currentTokens.accessToken) {
514
+ return null;
515
+ }
516
+ }
517
+ // Return current session
518
+ return {
519
+ accessToken: tokens.accessToken,
520
+ expiresAt: tokens.expiresAt,
521
+ payload: (_e = parseJWTPayload(tokens.accessToken)) !== null && _e !== void 0 ? _e : undefined,
522
+ };
523
+ });
524
+ }
525
+ /**
526
+ * Logout (clear tokens)
527
+ */
528
+ logout(ctx) {
529
+ return __awaiter(this, void 0, void 0, function* () {
530
+ // Optionally call logout endpoint
531
+ if (this.config.logout) {
532
+ try {
533
+ const url = this.baseURL + this.config.logout;
534
+ yield fetch(url, { method: 'POST' });
535
+ }
536
+ catch (error) {
537
+ // Ignore logout endpoint errors
538
+ console.warn('Logout endpoint failed:', error);
539
+ }
540
+ }
541
+ clearTokens(ctx, this.config.cookies);
542
+ });
543
+ }
544
+ /**
545
+ * Get current session (no refresh)
546
+ */
547
+ getSession(ctx) {
548
+ var _a;
549
+ const tokens = retrieveTokens(ctx, this.config.cookies);
550
+ if (!tokens.accessToken || !tokens.expiresAt) {
551
+ return null;
552
+ }
553
+ return {
554
+ accessToken: tokens.accessToken,
555
+ expiresAt: tokens.expiresAt,
556
+ payload: (_a = parseJWTPayload(tokens.accessToken)) !== null && _a !== void 0 ? _a : undefined,
557
+ };
558
+ }
559
+ /**
560
+ * Check if authenticated
561
+ */
562
+ isAuthenticated(ctx) {
563
+ const tokens = retrieveTokens(ctx, this.config.cookies);
564
+ return !!(tokens.accessToken && tokens.refreshToken);
565
+ }
566
+ /**
567
+ * Create flight key for single-flight deduplication
568
+ */
569
+ createFlightKey(token) {
570
+ let hash = 0;
571
+ for (let i = 0; i < token.length; i++) {
572
+ const char = token.charCodeAt(i);
573
+ hash = ((hash << 5) - hash) + char;
574
+ hash = hash & hash;
575
+ }
576
+ return `flight_${Math.abs(hash).toString(36)}`;
577
+ }
578
+ }
579
+
580
+ // packages/astro-tokenkit/src/client/context.ts
581
+ /**
582
+ * Async local storage for Astro context
583
+ */
584
+ const defaultContextStorage = new AsyncLocalStorage();
585
+ /**
586
+ * Bind Astro context for the current async scope
587
+ */
588
+ function bindContext(ctx, fn, options) {
589
+ const storage = (options === null || options === void 0 ? void 0 : options.context) || defaultContextStorage;
590
+ return storage.run(ctx, fn);
591
+ }
592
+ /**
593
+ * Get current Astro context (from middleware binding or explicit)
594
+ */
595
+ function getContext(explicitCtx, options) {
596
+ const store = (options === null || options === void 0 ? void 0 : options.getContextStore)
597
+ ? options.getContextStore()
598
+ : ((options === null || options === void 0 ? void 0 : options.context) || defaultContextStorage).getStore();
599
+ const ctx = explicitCtx || store;
600
+ if (!ctx) {
601
+ throw new Error('Astro context not found. Either:\n' +
602
+ '1. Use api.middleware() to bind context automatically, or\n' +
603
+ '2. Pass context explicitly: api.get("/path", { ctx: Astro })');
604
+ }
605
+ return ctx;
606
+ }
607
+
608
+ // packages/astro-tokenkit/src/utils/retry.ts
609
+ /**
610
+ * Default retry configuration
611
+ */
612
+ const DEFAULT_RETRY = {
613
+ attempts: 3,
614
+ statusCodes: [408, 429, 500, 502, 503, 504],
615
+ backoff: 'exponential',
616
+ delay: 1000,
617
+ };
618
+ /**
619
+ * Calculate retry delay
620
+ */
621
+ function calculateDelay(attempt, config = {}) {
622
+ const { backoff = DEFAULT_RETRY.backoff, delay = DEFAULT_RETRY.delay } = config;
623
+ if (backoff === 'linear') {
624
+ return delay * attempt;
625
+ }
626
+ // Exponential backoff: delay * 2^(attempt-1)
627
+ return delay * Math.pow(2, attempt - 1);
628
+ }
629
+ /**
630
+ * Check if error should be retried
631
+ */
632
+ function shouldRetry(status, attempt, config = {}) {
633
+ const { attempts = DEFAULT_RETRY.attempts, statusCodes = DEFAULT_RETRY.statusCodes, } = config;
634
+ if (attempt >= attempts) {
635
+ return false;
636
+ }
637
+ if (status === undefined) {
638
+ // Network errors are retryable
639
+ return true;
640
+ }
641
+ return statusCodes.includes(status);
642
+ }
643
+ /**
644
+ * Sleep for given milliseconds
645
+ */
646
+ function sleep(ms) {
647
+ return new Promise(resolve => setTimeout(resolve, ms));
648
+ }
649
+
650
+ // packages/astro-tokenkit/src/client/client.ts
651
+ /**
652
+ * API Client
653
+ */
654
+ class APIClient {
655
+ constructor(config) {
656
+ this.config = config;
657
+ this.contextOptions = {
658
+ context: config.context,
659
+ getContextStore: config.getContextStore,
660
+ };
661
+ // Initialize token manager if auth is configured
662
+ if (config.auth) {
663
+ this.tokenManager = new TokenManager(config.auth, config.baseURL);
664
+ }
665
+ }
666
+ /**
667
+ * GET request
668
+ */
669
+ get(url, options) {
670
+ return __awaiter(this, void 0, void 0, function* () {
671
+ return this.request(Object.assign({ method: 'GET', url }, options));
672
+ });
673
+ }
674
+ /**
675
+ * POST request
676
+ */
677
+ post(url, data, options) {
678
+ return __awaiter(this, void 0, void 0, function* () {
679
+ return this.request(Object.assign({ method: 'POST', url,
680
+ data }, options));
681
+ });
682
+ }
683
+ /**
684
+ * PUT request
685
+ */
686
+ put(url, data, options) {
687
+ return __awaiter(this, void 0, void 0, function* () {
688
+ return this.request(Object.assign({ method: 'PUT', url,
689
+ data }, options));
690
+ });
691
+ }
692
+ /**
693
+ * PATCH request
694
+ */
695
+ patch(url, data, options) {
696
+ return __awaiter(this, void 0, void 0, function* () {
697
+ return this.request(Object.assign({ method: 'PATCH', url,
698
+ data }, options));
699
+ });
700
+ }
701
+ /**
702
+ * DELETE request
703
+ */
704
+ delete(url, options) {
705
+ return __awaiter(this, void 0, void 0, function* () {
706
+ return this.request(Object.assign({ method: 'DELETE', url }, options));
707
+ });
708
+ }
709
+ /**
710
+ * Generic request method
711
+ */
712
+ request(config) {
713
+ return __awaiter(this, void 0, void 0, function* () {
714
+ const ctx = getContext(config.ctx, this.contextOptions);
715
+ let attempt = 0;
716
+ while (true) {
717
+ attempt++;
718
+ try {
719
+ const response = yield this.executeRequest(config, ctx, attempt);
720
+ return response.data;
721
+ }
722
+ catch (error) {
723
+ // Check if we should retry
724
+ if (shouldRetry(error.status, attempt, this.config.retry)) {
725
+ const delay = calculateDelay(attempt, this.config.retry);
726
+ yield sleep(delay);
727
+ continue;
728
+ }
729
+ // No more retries
730
+ throw error;
731
+ }
732
+ }
733
+ });
734
+ }
735
+ /**
736
+ * Execute single request
737
+ */
738
+ executeRequest(config, ctx, attempt) {
739
+ return __awaiter(this, void 0, void 0, function* () {
740
+ var _a, _b, _c, _d, _e;
741
+ // Ensure valid session (if auth is enabled)
742
+ if (this.tokenManager && !config.skipAuth) {
743
+ yield this.tokenManager.ensure(ctx);
744
+ }
745
+ // Build full URL
746
+ const fullURL = this.buildURL(config.url, config.params);
747
+ // Build headers
748
+ const headers = this.buildHeaders(config, ctx);
749
+ // Build request init
750
+ const init = {
751
+ method: config.method,
752
+ headers,
753
+ signal: config.signal,
754
+ };
755
+ // Add body for non-GET requests
756
+ if (config.data && config.method !== 'GET') {
757
+ init.body = JSON.stringify(config.data);
758
+ }
759
+ // Apply request interceptors
760
+ let requestConfig = Object.assign({}, config);
761
+ if ((_a = this.config.interceptors) === null || _a === void 0 ? void 0 : _a.request) {
762
+ for (const interceptor of this.config.interceptors.request) {
763
+ requestConfig = yield interceptor(requestConfig, ctx);
764
+ }
765
+ }
766
+ // Execute fetch with timeout
767
+ const timeout = (_c = (_b = config.timeout) !== null && _b !== void 0 ? _b : this.config.timeout) !== null && _c !== void 0 ? _c : 30000;
768
+ const controller = new AbortController();
769
+ const timeoutId = setTimeout(() => controller.abort(), timeout);
770
+ try {
771
+ const response = yield fetch(fullURL, Object.assign(Object.assign({}, init), { signal: controller.signal }));
772
+ clearTimeout(timeoutId);
773
+ // Handle 401 (try refresh and retry once)
774
+ if (response.status === 401 && this.tokenManager && !config.skipAuth && attempt === 1) {
775
+ // Clear and try fresh session
776
+ const session = yield this.tokenManager.ensure(ctx);
777
+ if (session) {
778
+ // Retry with new token
779
+ return this.executeRequest(config, ctx, attempt + 1);
780
+ }
781
+ }
782
+ // Parse response
783
+ const apiResponse = yield this.parseResponse(response, fullURL);
784
+ // Apply response interceptors
785
+ if ((_d = this.config.interceptors) === null || _d === void 0 ? void 0 : _d.response) {
786
+ let interceptedResponse = apiResponse;
787
+ for (const interceptor of this.config.interceptors.response) {
788
+ interceptedResponse = yield interceptor(interceptedResponse, ctx);
789
+ }
790
+ return interceptedResponse;
791
+ }
792
+ return apiResponse;
793
+ }
794
+ catch (error) {
795
+ clearTimeout(timeoutId);
796
+ // Apply error interceptors
797
+ if ((_e = this.config.interceptors) === null || _e === void 0 ? void 0 : _e.error) {
798
+ for (const interceptor of this.config.interceptors.error) {
799
+ yield interceptor(error, ctx);
800
+ }
801
+ }
802
+ // Transform errors
803
+ if (error instanceof Error && error.name === 'AbortError') {
804
+ throw new TimeoutError(`Request timeout after ${timeout}ms`, requestConfig);
805
+ }
806
+ if (error instanceof APIError) {
807
+ throw error;
808
+ }
809
+ throw new NetworkError(error.message, requestConfig);
810
+ }
811
+ });
812
+ }
813
+ /**
814
+ * Parse response
815
+ */
816
+ parseResponse(response, url) {
817
+ return __awaiter(this, void 0, void 0, function* () {
818
+ let data;
819
+ // Try to parse JSON
820
+ const contentType = response.headers.get('content-type');
821
+ if (contentType === null || contentType === void 0 ? void 0 : contentType.includes('application/json')) {
822
+ try {
823
+ data = yield response.json();
824
+ }
825
+ catch (_a) {
826
+ data = (yield response.text());
827
+ }
828
+ }
829
+ else {
830
+ data = (yield response.text());
831
+ }
832
+ // Check if response is ok
833
+ if (!response.ok) {
834
+ if (response.status === 401 || response.status === 403) {
835
+ throw new AuthError(`Authentication failed: ${response.status} ${response.statusText}`, response.status, data);
836
+ }
837
+ throw new APIError(`Request failed: ${response.status} ${response.statusText}`, response.status, data);
838
+ }
839
+ return {
840
+ data,
841
+ status: response.status,
842
+ statusText: response.statusText,
843
+ headers: response.headers,
844
+ url,
845
+ };
846
+ });
847
+ }
848
+ /**
849
+ * Build full URL with query params
850
+ */
851
+ buildURL(url, params) {
852
+ const fullURL = url.startsWith('http') ? url : this.config.baseURL + url;
853
+ if (!params)
854
+ return fullURL;
855
+ const urlObj = new URL(fullURL);
856
+ Object.entries(params).forEach(([key, value]) => {
857
+ if (value !== undefined && value !== null) {
858
+ urlObj.searchParams.append(key, String(value));
859
+ }
860
+ });
861
+ return urlObj.toString();
862
+ }
863
+ /**
864
+ * Build request headers
865
+ */
866
+ buildHeaders(config, ctx) {
867
+ var _a, _b;
868
+ const headers = Object.assign(Object.assign({ 'Content-Type': 'application/json' }, this.config.headers), config.headers);
869
+ // Add auth token if available
870
+ if (this.tokenManager && !config.skipAuth) {
871
+ const session = this.tokenManager.getSession(ctx);
872
+ if (session === null || session === void 0 ? void 0 : session.accessToken) {
873
+ const injectFn = (_b = (_a = this.config.auth) === null || _a === void 0 ? void 0 : _a.injectToken) !== null && _b !== void 0 ? _b : ((token) => `Bearer ${token}`);
874
+ headers['Authorization'] = injectFn(session.accessToken);
875
+ }
876
+ }
877
+ return headers;
878
+ }
879
+ /**
880
+ * Login
881
+ */
882
+ login(credentials, ctx) {
883
+ return __awaiter(this, void 0, void 0, function* () {
884
+ if (!this.tokenManager) {
885
+ throw new Error('Auth is not configured for this client');
886
+ }
887
+ const context = getContext(ctx, this.contextOptions);
888
+ yield this.tokenManager.login(context, credentials);
889
+ });
890
+ }
891
+ /**
892
+ * Logout
893
+ */
894
+ logout(ctx) {
895
+ return __awaiter(this, void 0, void 0, function* () {
896
+ if (!this.tokenManager) {
897
+ throw new Error('Auth is not configured for this client');
898
+ }
899
+ const context = getContext(ctx, this.contextOptions);
900
+ yield this.tokenManager.logout(context);
901
+ });
902
+ }
903
+ /**
904
+ * Check if authenticated
905
+ */
906
+ isAuthenticated(ctx) {
907
+ if (!this.tokenManager)
908
+ return false;
909
+ const context = getContext(ctx, this.contextOptions);
910
+ return this.tokenManager.isAuthenticated(context);
911
+ }
912
+ /**
913
+ * Get current session
914
+ */
915
+ getSession(ctx) {
916
+ if (!this.tokenManager)
917
+ return null;
918
+ const context = getContext(ctx, this.contextOptions);
919
+ return this.tokenManager.getSession(context);
920
+ }
921
+ }
922
+ /**
923
+ * Create API client
924
+ */
925
+ function createClient(config) {
926
+ return new APIClient(config);
927
+ }
928
+
929
+ // packages/astro-tokenkit/src/middleware.ts
930
+ /**
931
+ * Create middleware for context binding and automatic token rotation
932
+ */
933
+ function createMiddleware(client) {
934
+ return (ctx, next) => __awaiter(this, void 0, void 0, function* () {
935
+ const tokenManager = client.tokenManager;
936
+ const contextOptions = client.contextOptions;
937
+ const runLogic = () => __awaiter(this, void 0, void 0, function* () {
938
+ // Proactively ensure valid session if auth is configured
939
+ if (tokenManager) {
940
+ try {
941
+ // This handles token rotation (refresh) if needed
942
+ yield tokenManager.ensure(ctx);
943
+ }
944
+ catch (error) {
945
+ // Log but don't block request if rotation fails
946
+ console.error('[TokenKit] Automatic token rotation failed:', error);
947
+ }
948
+ }
949
+ return next();
950
+ });
951
+ // If getContextStore is defined, it means the context is managed externally (e.g., by a superior ALS)
952
+ // We skip bindContext to avoid nesting ALS.run() unnecessarily.
953
+ if (contextOptions === null || contextOptions === void 0 ? void 0 : contextOptions.getContextStore) {
954
+ return runLogic();
955
+ }
956
+ return bindContext(ctx, runLogic, contextOptions);
957
+ });
958
+ }
959
+
960
+ // packages/astro-tokenkit/src/integration.ts
961
+ /**
962
+ * Astro integration for TokenKit
963
+ *
964
+ * This integration facilitates the setup of TokenKit in an Astro project.
965
+ */
966
+ function tokenKit(client) {
967
+ return {
968
+ name: 'astro-tokenkit',
969
+ hooks: {
970
+ 'astro:config:setup': () => {
971
+ // Future-proofing: could add vite aliases or other setup here
972
+ console.log('[TokenKit] Integration initialized');
973
+ },
974
+ },
975
+ };
976
+ }
977
+ /**
978
+ * Helper to define middleware in a separate file if needed
979
+ */
980
+ const defineMiddleware = (client) => createMiddleware(client);
981
+
982
+ export { APIClient, APIError, AuthError, NetworkError, TimeoutError, createClient, createMiddleware, defineMiddleware, formatTime, parseTime, tokenKit };
983
+ //# sourceMappingURL=index.js.map