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