astro-tokenkit 1.0.14 → 1.0.16

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.
@@ -0,0 +1,796 @@
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
+ // packages/astro-tokenkit/src/auth/detector.ts
61
+ /**
62
+ * Common field names for access tokens
63
+ */
64
+ const ACCESS_TOKEN_FIELDS = [
65
+ 'access_token',
66
+ 'accessToken',
67
+ 'token',
68
+ 'jwt',
69
+ 'id_token',
70
+ 'idToken',
71
+ ];
72
+ /**
73
+ * Common field names for refresh tokens
74
+ */
75
+ const REFRESH_TOKEN_FIELDS = [
76
+ 'refresh_token',
77
+ 'refreshToken',
78
+ 'refresh',
79
+ ];
80
+ /**
81
+ * Common field names for expiration timestamp
82
+ */
83
+ const EXPIRES_AT_FIELDS = [
84
+ 'expires_at',
85
+ 'expiresAt',
86
+ 'exp',
87
+ 'expiry',
88
+ ];
89
+ /**
90
+ * Common field names for expires_in (seconds)
91
+ */
92
+ const EXPIRES_IN_FIELDS = [
93
+ 'expires_in',
94
+ 'expiresIn',
95
+ 'ttl',
96
+ ];
97
+ /**
98
+ * Common field names for token type
99
+ */
100
+ const TOKEN_TYPE_FIELDS = [
101
+ 'token_type',
102
+ 'tokenType',
103
+ ];
104
+ /**
105
+ * Common field names for session payload
106
+ */
107
+ const SESSION_PAYLOAD_FIELDS = [
108
+ 'user',
109
+ 'profile',
110
+ 'account',
111
+ 'data',
112
+ ];
113
+ /**
114
+ * Auto-detect token fields from response body
115
+ */
116
+ function autoDetectFields(body, fieldMapping) {
117
+ // Helper to find field
118
+ const findField = (candidates, mapping) => {
119
+ if (mapping && body[mapping] !== undefined) {
120
+ return body[mapping];
121
+ }
122
+ for (const candidate of candidates) {
123
+ if (body[candidate] !== undefined) {
124
+ return body[candidate];
125
+ }
126
+ }
127
+ return undefined;
128
+ };
129
+ // Detect access token
130
+ const accessToken = findField(ACCESS_TOKEN_FIELDS, fieldMapping === null || fieldMapping === void 0 ? void 0 : fieldMapping.accessToken);
131
+ if (!accessToken) {
132
+ throw new Error(`Could not detect access token field. Tried: ${ACCESS_TOKEN_FIELDS.join(', ')}. ` +
133
+ `Provide custom parseLogin/parseRefresh or field mapping.`);
134
+ }
135
+ // Detect refresh token
136
+ const refreshToken = findField(REFRESH_TOKEN_FIELDS, fieldMapping === null || fieldMapping === void 0 ? void 0 : fieldMapping.refreshToken);
137
+ if (!refreshToken) {
138
+ throw new Error(`Could not detect refresh token field. Tried: ${REFRESH_TOKEN_FIELDS.join(', ')}. ` +
139
+ `Provide custom parseLogin/parseRefresh or field mapping.`);
140
+ }
141
+ // Detect expiration
142
+ let accessExpiresAt;
143
+ // Try expires_at first (timestamp)
144
+ const expiresAtValue = findField(EXPIRES_AT_FIELDS, fieldMapping === null || fieldMapping === void 0 ? void 0 : fieldMapping.expiresAt);
145
+ if (expiresAtValue !== undefined) {
146
+ accessExpiresAt = typeof expiresAtValue === 'number'
147
+ ? expiresAtValue
148
+ : parseInt(expiresAtValue, 10);
149
+ }
150
+ // Try expires_in (seconds from now)
151
+ if (accessExpiresAt === undefined) {
152
+ const expiresInValue = findField(EXPIRES_IN_FIELDS, fieldMapping === null || fieldMapping === void 0 ? void 0 : fieldMapping.expiresIn);
153
+ if (expiresInValue !== undefined) {
154
+ const expiresIn = typeof expiresInValue === 'number'
155
+ ? expiresInValue
156
+ : parseInt(expiresInValue, 10);
157
+ accessExpiresAt = Math.floor(Date.now() / 1000) + expiresIn;
158
+ }
159
+ }
160
+ if (accessExpiresAt === undefined) {
161
+ throw new Error(`Could not detect expiration field. Tried: ${[...EXPIRES_AT_FIELDS, ...EXPIRES_IN_FIELDS].join(', ')}. ` +
162
+ `Provide custom parseLogin/parseRefresh or field mapping.`);
163
+ }
164
+ // Detect session payload (optional)
165
+ const sessionPayload = findField(SESSION_PAYLOAD_FIELDS, fieldMapping === null || fieldMapping === void 0 ? void 0 : fieldMapping.sessionPayload);
166
+ // Detect token type (optional)
167
+ const tokenType = findField(TOKEN_TYPE_FIELDS, fieldMapping === null || fieldMapping === void 0 ? void 0 : fieldMapping.tokenType);
168
+ return {
169
+ accessToken,
170
+ refreshToken,
171
+ accessExpiresAt,
172
+ tokenType: tokenType || undefined,
173
+ sessionPayload: sessionPayload || undefined,
174
+ };
175
+ }
176
+ /**
177
+ * Parse JWT payload without verification (for reading only)
178
+ */
179
+ function parseJWTPayload(token) {
180
+ try {
181
+ const parts = token.split('.');
182
+ if (parts.length !== 3) {
183
+ return null;
184
+ }
185
+ const payload = parts[1];
186
+ // Better UTF-8 support for environments with Buffer (like Node.js/Astro)
187
+ if (typeof Buffer !== 'undefined') {
188
+ return JSON.parse(Buffer.from(payload, 'base64').toString('utf8'));
189
+ }
190
+ const decoded = atob(payload.replace(/-/g, '+').replace(/_/g, '/'));
191
+ return JSON.parse(decoded);
192
+ }
193
+ catch (_a) {
194
+ return null;
195
+ }
196
+ }
197
+
198
+ // packages/astro-tokenkit/src/auth/storage.ts
199
+ /**
200
+ * Get cookie names with optional prefix
201
+ */
202
+ function getCookieNames(prefix) {
203
+ const p = prefix ? `${prefix}_` : '';
204
+ return {
205
+ accessToken: `${p}access_token`,
206
+ refreshToken: `${p}refresh_token`,
207
+ expiresAt: `${p}access_expires_at`,
208
+ lastRefreshAt: `${p}last_refresh_at`,
209
+ tokenType: `${p}token_type`,
210
+ };
211
+ }
212
+ /**
213
+ * Get cookie options with smart defaults
214
+ */
215
+ function getCookieOptions(config = {}) {
216
+ var _a, _b;
217
+ const isProduction = process.env.NODE_ENV === 'production';
218
+ return {
219
+ secure: (_a = config.secure) !== null && _a !== void 0 ? _a : isProduction,
220
+ sameSite: (_b = config.sameSite) !== null && _b !== void 0 ? _b : 'lax',
221
+ httpOnly: true, // Always HttpOnly for security
222
+ domain: config.domain,
223
+ };
224
+ }
225
+ /**
226
+ * Store token bundle in cookies
227
+ */
228
+ function storeTokens(ctx, bundle, cookieConfig = {}) {
229
+ const names = getCookieNames(cookieConfig.prefix);
230
+ const options = getCookieOptions(cookieConfig);
231
+ const now = Math.floor(Date.now() / 1000);
232
+ // Calculate max age
233
+ const accessMaxAge = Math.max(0, bundle.accessExpiresAt - now);
234
+ const refreshMaxAge = bundle.refreshExpiresAt
235
+ ? Math.max(0, bundle.refreshExpiresAt - now)
236
+ : 7 * 24 * 60 * 60; // Default 7 days
237
+ // Set access token
238
+ ctx.cookies.set(names.accessToken, bundle.accessToken, Object.assign(Object.assign({}, options), { maxAge: accessMaxAge, path: '/' }));
239
+ // Set refresh token (restricted path for security)
240
+ ctx.cookies.set(names.refreshToken, bundle.refreshToken, Object.assign(Object.assign({}, options), { maxAge: refreshMaxAge, path: '/' }));
241
+ // Set expiration timestamp
242
+ ctx.cookies.set(names.expiresAt, bundle.accessExpiresAt.toString(), Object.assign(Object.assign({}, options), { maxAge: accessMaxAge, path: '/' }));
243
+ // Set last refresh timestamp
244
+ ctx.cookies.set(names.lastRefreshAt, now.toString(), Object.assign(Object.assign({}, options), { maxAge: accessMaxAge, path: '/' }));
245
+ // Set token type if available
246
+ if (bundle.tokenType) {
247
+ ctx.cookies.set(names.tokenType, bundle.tokenType, Object.assign(Object.assign({}, options), { maxAge: accessMaxAge, path: '/' }));
248
+ }
249
+ }
250
+ /**
251
+ * Retrieve tokens from cookies
252
+ */
253
+ function retrieveTokens(ctx, cookieConfig = {}) {
254
+ var _a, _b, _c, _d, _e;
255
+ const names = getCookieNames(cookieConfig.prefix);
256
+ const accessToken = ((_a = ctx.cookies.get(names.accessToken)) === null || _a === void 0 ? void 0 : _a.value) || null;
257
+ const refreshToken = ((_b = ctx.cookies.get(names.refreshToken)) === null || _b === void 0 ? void 0 : _b.value) || null;
258
+ const tokenType = ((_c = ctx.cookies.get(names.tokenType)) === null || _c === void 0 ? void 0 : _c.value) || null;
259
+ const expiresAtStr = (_d = ctx.cookies.get(names.expiresAt)) === null || _d === void 0 ? void 0 : _d.value;
260
+ const expiresAt = expiresAtStr ? parseInt(expiresAtStr, 10) : null;
261
+ const lastRefreshAtStr = (_e = ctx.cookies.get(names.lastRefreshAt)) === null || _e === void 0 ? void 0 : _e.value;
262
+ const lastRefreshAt = lastRefreshAtStr ? parseInt(lastRefreshAtStr, 10) : null;
263
+ return { accessToken, refreshToken, expiresAt, lastRefreshAt, tokenType };
264
+ }
265
+ /**
266
+ * Clear all auth cookies
267
+ */
268
+ function clearTokens(ctx, cookieConfig = {}) {
269
+ const names = getCookieNames(cookieConfig.prefix);
270
+ const options = getCookieOptions(cookieConfig);
271
+ ctx.cookies.delete(names.accessToken, Object.assign(Object.assign({}, options), { path: '/' }));
272
+ ctx.cookies.delete(names.refreshToken, Object.assign(Object.assign({}, options), { path: '/' }));
273
+ ctx.cookies.delete(names.expiresAt, Object.assign(Object.assign({}, options), { path: '/' }));
274
+ ctx.cookies.delete(names.lastRefreshAt, Object.assign(Object.assign({}, options), { path: '/' }));
275
+ ctx.cookies.delete(names.tokenType, Object.assign(Object.assign({}, options), { path: '/' }));
276
+ }
277
+
278
+ // packages/astro-tokenkit/src/utils/time.ts
279
+ /**
280
+ * Parse time string to seconds
281
+ * Supports: '5m', '30s', '1h', '2d'
282
+ */
283
+ function parseTime(input) {
284
+ if (typeof input === 'number') {
285
+ return input;
286
+ }
287
+ const match = input.match(/^(\d+)([smhd])$/);
288
+ if (!match) {
289
+ throw new Error(`Invalid time format: ${input}. Use format like '5m', '30s', '1h', '2d'`);
290
+ }
291
+ const value = parseInt(match[1], 10);
292
+ const unit = match[2];
293
+ const multipliers = {
294
+ s: 1,
295
+ m: 60,
296
+ h: 60 * 60,
297
+ d: 60 * 60 * 24,
298
+ };
299
+ return value * multipliers[unit];
300
+ }
301
+
302
+ // packages/astro-tokenkit/src/auth/policy.ts
303
+ /**
304
+ * Default refresh policy
305
+ */
306
+ const DEFAULT_POLICY = {
307
+ refreshBefore: 300, // 5 minutes
308
+ clockSkew: 60, // 1 minute
309
+ minInterval: 30, // 30 seconds
310
+ };
311
+ /**
312
+ * Normalize refresh policy (convert time strings to seconds)
313
+ */
314
+ function normalizePolicy(policy = {}) {
315
+ return {
316
+ refreshBefore: policy.refreshBefore
317
+ ? parseTime(policy.refreshBefore)
318
+ : DEFAULT_POLICY.refreshBefore,
319
+ clockSkew: policy.clockSkew
320
+ ? parseTime(policy.clockSkew)
321
+ : DEFAULT_POLICY.clockSkew,
322
+ minInterval: policy.minInterval
323
+ ? parseTime(policy.minInterval)
324
+ : DEFAULT_POLICY.minInterval,
325
+ };
326
+ }
327
+ /**
328
+ * Check if token should be refreshed
329
+ */
330
+ function shouldRefresh(expiresAt, now, lastRefreshAt, policy = {}) {
331
+ const normalized = normalizePolicy(policy);
332
+ const refreshBefore = typeof normalized.refreshBefore === 'number'
333
+ ? normalized.refreshBefore
334
+ : parseTime(normalized.refreshBefore);
335
+ const clockSkew = typeof normalized.clockSkew === 'number'
336
+ ? normalized.clockSkew
337
+ : parseTime(normalized.clockSkew);
338
+ const minInterval = typeof normalized.minInterval === 'number'
339
+ ? normalized.minInterval
340
+ : parseTime(normalized.minInterval);
341
+ // Adjust for clock skew
342
+ const adjustedNow = now + clockSkew;
343
+ // Check if near expiration
344
+ const timeUntilExpiry = expiresAt - adjustedNow;
345
+ if (timeUntilExpiry > refreshBefore) {
346
+ return false;
347
+ }
348
+ // Check minimum interval
349
+ if (lastRefreshAt !== null) {
350
+ const timeSinceLastRefresh = now - lastRefreshAt;
351
+ if (timeSinceLastRefresh < minInterval) {
352
+ return false;
353
+ }
354
+ }
355
+ return true;
356
+ }
357
+ /**
358
+ * Check if token is expired
359
+ */
360
+ function isExpired(expiresAt, now, policy = {}) {
361
+ const normalized = normalizePolicy(policy);
362
+ const clockSkew = typeof normalized.clockSkew === 'number'
363
+ ? normalized.clockSkew
364
+ : parseTime(normalized.clockSkew);
365
+ // Pessimistic: consider it expired if current time + skew is past expiration
366
+ return now + clockSkew > expiresAt;
367
+ }
368
+
369
+ // packages/astro-tokenkit/src/auth/manager.ts
370
+ /**
371
+ * Single-flight refresh manager
372
+ */
373
+ class SingleFlight {
374
+ constructor() {
375
+ this.inFlight = new Map();
376
+ }
377
+ execute(key, fn) {
378
+ return __awaiter(this, void 0, void 0, function* () {
379
+ const existing = this.inFlight.get(key);
380
+ if (existing)
381
+ return existing;
382
+ const promise = this.doExecute(key, fn);
383
+ this.inFlight.set(key, promise);
384
+ return promise;
385
+ });
386
+ }
387
+ doExecute(key, fn) {
388
+ return __awaiter(this, void 0, void 0, function* () {
389
+ try {
390
+ return yield fn();
391
+ }
392
+ finally {
393
+ this.inFlight.delete(key);
394
+ }
395
+ });
396
+ }
397
+ }
398
+ /**
399
+ * Token Manager handles all token operations
400
+ */
401
+ class TokenManager {
402
+ constructor(config, baseURL) {
403
+ this.config = config;
404
+ this.singleFlight = new SingleFlight();
405
+ this.baseURL = baseURL;
406
+ }
407
+ /**
408
+ * Perform login
409
+ */
410
+ login(ctx, credentials, options) {
411
+ return __awaiter(this, void 0, void 0, function* () {
412
+ const url = this.joinURL(this.baseURL, this.config.login);
413
+ const contentType = this.config.contentType || 'application/json';
414
+ const headers = Object.assign(Object.assign({ 'Content-Type': contentType }, this.config.headers), options === null || options === void 0 ? void 0 : options.headers);
415
+ const data = Object.assign(Object.assign(Object.assign({}, this.config.loginData), options === null || options === void 0 ? void 0 : options.data), credentials);
416
+ let requestBody;
417
+ if (contentType === 'application/x-www-form-urlencoded') {
418
+ requestBody = new URLSearchParams(data).toString();
419
+ }
420
+ else {
421
+ requestBody = JSON.stringify(data);
422
+ }
423
+ let response;
424
+ try {
425
+ response = yield fetch(url, {
426
+ method: 'POST',
427
+ headers,
428
+ body: requestBody,
429
+ });
430
+ }
431
+ catch (error) {
432
+ const authError = new AuthError(`Login request failed: ${error.message}`);
433
+ if (options === null || options === void 0 ? void 0 : options.onError)
434
+ yield options.onError(authError, ctx);
435
+ throw authError;
436
+ }
437
+ if (!response.ok) {
438
+ const authError = new AuthError(`Login failed: ${response.status} ${response.statusText}`, response.status, response);
439
+ if (options === null || options === void 0 ? void 0 : options.onError)
440
+ yield options.onError(authError, ctx);
441
+ throw authError;
442
+ }
443
+ const body = yield response.json().catch(() => ({}));
444
+ // Parse response
445
+ let bundle;
446
+ try {
447
+ bundle = this.config.parseLogin
448
+ ? this.config.parseLogin(body)
449
+ : autoDetectFields(body, this.config.fields);
450
+ }
451
+ catch (error) {
452
+ const authError = new AuthError(`Invalid login response: ${error.message}`, response.status, response);
453
+ if (options === null || options === void 0 ? void 0 : options.onError)
454
+ yield options.onError(authError, ctx);
455
+ throw authError;
456
+ }
457
+ // Store in cookies
458
+ storeTokens(ctx, bundle, this.config.cookies);
459
+ // Call onLogin callback if provided
460
+ if (options === null || options === void 0 ? void 0 : options.onLogin) {
461
+ yield options.onLogin(bundle, body, ctx);
462
+ }
463
+ return {
464
+ data: bundle,
465
+ status: response.status,
466
+ statusText: response.statusText,
467
+ headers: response.headers,
468
+ url: response.url,
469
+ ok: response.ok,
470
+ };
471
+ });
472
+ }
473
+ /**
474
+ * Perform token refresh
475
+ */
476
+ refresh(ctx, refreshToken, options, headers) {
477
+ return __awaiter(this, void 0, void 0, function* () {
478
+ try {
479
+ return yield this.performRefresh(ctx, refreshToken, options, headers);
480
+ }
481
+ catch (error) {
482
+ clearTokens(ctx, this.config.cookies);
483
+ throw error;
484
+ }
485
+ });
486
+ }
487
+ /**
488
+ * Internal refresh implementation
489
+ */
490
+ performRefresh(ctx, refreshToken, options, extraHeaders) {
491
+ return __awaiter(this, void 0, void 0, function* () {
492
+ const url = this.joinURL(this.baseURL, this.config.refresh);
493
+ const contentType = this.config.contentType || 'application/json';
494
+ const headers = Object.assign(Object.assign({ 'Content-Type': contentType }, this.config.headers), extraHeaders);
495
+ const refreshField = this.config.refreshRequestField || 'refreshToken';
496
+ const data = Object.assign(Object.assign(Object.assign({}, this.config.refreshData), options === null || options === void 0 ? void 0 : options.data), { [refreshField]: refreshToken });
497
+ let requestBody;
498
+ if (contentType === 'application/x-www-form-urlencoded') {
499
+ requestBody = new URLSearchParams(data).toString();
500
+ }
501
+ else {
502
+ requestBody = JSON.stringify(data);
503
+ }
504
+ let response;
505
+ try {
506
+ response = yield fetch(url, {
507
+ method: 'POST',
508
+ headers,
509
+ body: requestBody,
510
+ });
511
+ }
512
+ catch (error) {
513
+ throw new AuthError(`Refresh request failed: ${error.message}`);
514
+ }
515
+ if (!response.ok) {
516
+ // 401/403 = invalid refresh token
517
+ if (response.status === 401 || response.status === 403) {
518
+ clearTokens(ctx, this.config.cookies);
519
+ return null;
520
+ }
521
+ throw new AuthError(`Refresh failed: ${response.status} ${response.statusText}`, response.status, response);
522
+ }
523
+ const body = yield response.json().catch(() => ({}));
524
+ // Parse response
525
+ let bundle;
526
+ try {
527
+ bundle = this.config.parseRefresh
528
+ ? this.config.parseRefresh(body)
529
+ : autoDetectFields(body, this.config.fields);
530
+ }
531
+ catch (error) {
532
+ throw new AuthError(`Invalid refresh response: ${error.message}`, response.status, response);
533
+ }
534
+ // Validate bundle
535
+ if (!bundle.accessToken || !bundle.refreshToken || !bundle.accessExpiresAt) {
536
+ throw new AuthError('Invalid token bundle returned from refresh endpoint', response.status, response);
537
+ }
538
+ // Store new tokens
539
+ storeTokens(ctx, bundle, this.config.cookies);
540
+ return bundle;
541
+ });
542
+ }
543
+ /**
544
+ * Ensure valid tokens (with automatic refresh)
545
+ */
546
+ ensure(ctx, options, headers) {
547
+ return __awaiter(this, void 0, void 0, function* () {
548
+ var _a, _b, _c, _d, _e, _f;
549
+ const now = Math.floor(Date.now() / 1000);
550
+ const tokens = retrieveTokens(ctx, this.config.cookies);
551
+ // No tokens
552
+ if (!tokens.accessToken || !tokens.refreshToken || !tokens.expiresAt) {
553
+ return null;
554
+ }
555
+ // Token expired
556
+ if (isExpired(tokens.expiresAt, now, this.config.policy)) {
557
+ const flightKey = this.createFlightKey(tokens.refreshToken);
558
+ const bundle = yield this.singleFlight.execute(flightKey, () => this.refresh(ctx, tokens.refreshToken, options, headers));
559
+ if (!bundle)
560
+ return null;
561
+ return {
562
+ accessToken: bundle.accessToken,
563
+ expiresAt: bundle.accessExpiresAt,
564
+ tokenType: bundle.tokenType,
565
+ payload: (_b = (_a = bundle.sessionPayload) !== null && _a !== void 0 ? _a : parseJWTPayload(bundle.accessToken)) !== null && _b !== void 0 ? _b : undefined,
566
+ };
567
+ }
568
+ // Proactive refresh
569
+ if (shouldRefresh(tokens.expiresAt, now, tokens.lastRefreshAt, this.config.policy)) {
570
+ const flightKey = this.createFlightKey(tokens.refreshToken);
571
+ const bundle = yield this.singleFlight.execute(flightKey, () => this.refresh(ctx, tokens.refreshToken, options, headers));
572
+ if (bundle) {
573
+ return {
574
+ accessToken: bundle.accessToken,
575
+ expiresAt: bundle.accessExpiresAt,
576
+ tokenType: bundle.tokenType,
577
+ payload: (_d = (_c = bundle.sessionPayload) !== null && _c !== void 0 ? _c : parseJWTPayload(bundle.accessToken)) !== null && _d !== void 0 ? _d : undefined,
578
+ };
579
+ }
580
+ // Refresh failed, check if tokens still exist
581
+ const currentTokens = retrieveTokens(ctx, this.config.cookies);
582
+ if (!currentTokens.accessToken) {
583
+ return null;
584
+ }
585
+ }
586
+ // Return current session
587
+ return {
588
+ accessToken: tokens.accessToken,
589
+ expiresAt: tokens.expiresAt,
590
+ tokenType: (_e = tokens.tokenType) !== null && _e !== void 0 ? _e : undefined,
591
+ payload: (_f = parseJWTPayload(tokens.accessToken)) !== null && _f !== void 0 ? _f : undefined,
592
+ };
593
+ });
594
+ }
595
+ /**
596
+ * Logout (clear tokens)
597
+ */
598
+ logout(ctx) {
599
+ return __awaiter(this, void 0, void 0, function* () {
600
+ var _a;
601
+ // Optionally call logout endpoint
602
+ if (this.config.logout) {
603
+ try {
604
+ const url = this.joinURL(this.baseURL, this.config.logout);
605
+ const session = this.getSession(ctx);
606
+ const headers = {};
607
+ if (session === null || session === void 0 ? void 0 : session.accessToken) {
608
+ const injectFn = (_a = this.config.injectToken) !== null && _a !== void 0 ? _a : ((token, type) => `${type !== null && type !== void 0 ? type : 'Bearer'} ${token}`);
609
+ headers['Authorization'] = injectFn(session.accessToken, session.tokenType);
610
+ }
611
+ yield fetch(url, { method: 'POST', headers });
612
+ }
613
+ catch (error) {
614
+ // Ignore logout endpoint errors
615
+ console.warn('[TokenKit] Logout endpoint failed:', error);
616
+ }
617
+ }
618
+ clearTokens(ctx, this.config.cookies);
619
+ });
620
+ }
621
+ /**
622
+ * Get current session (no refresh)
623
+ */
624
+ getSession(ctx) {
625
+ var _a, _b;
626
+ const tokens = retrieveTokens(ctx, this.config.cookies);
627
+ if (!tokens.accessToken || !tokens.expiresAt) {
628
+ return null;
629
+ }
630
+ return {
631
+ accessToken: tokens.accessToken,
632
+ expiresAt: tokens.expiresAt,
633
+ tokenType: (_a = tokens.tokenType) !== null && _a !== void 0 ? _a : undefined,
634
+ payload: (_b = parseJWTPayload(tokens.accessToken)) !== null && _b !== void 0 ? _b : undefined,
635
+ };
636
+ }
637
+ /**
638
+ * Check if authenticated
639
+ */
640
+ isAuthenticated(ctx) {
641
+ const tokens = retrieveTokens(ctx, this.config.cookies);
642
+ return !!(tokens.accessToken && tokens.refreshToken);
643
+ }
644
+ /**
645
+ * Create flight key for single-flight deduplication
646
+ */
647
+ createFlightKey(token) {
648
+ // Avoid weak hashing of sensitive tokens
649
+ return `refresh_${token}`;
650
+ }
651
+ /**
652
+ * Join base URL and path safely
653
+ */
654
+ joinURL(base, path) {
655
+ const b = base.endsWith('/') ? base : base + '/';
656
+ const p = path.startsWith('/') ? path.slice(1) : path;
657
+ return b + p;
658
+ }
659
+ }
660
+
661
+ // packages/astro-tokenkit/src/config.ts
662
+ const CONFIG_KEY = Symbol.for('astro-tokenkit.config');
663
+ const MANAGER_KEY = Symbol.for('astro-tokenkit.manager');
664
+ const globalStorage = globalThis;
665
+ // Initialize global storage if not present
666
+ if (!globalStorage[CONFIG_KEY]) {
667
+ globalStorage[CONFIG_KEY] = {
668
+ runWithContext: undefined,
669
+ getContextStore: undefined,
670
+ setContextStore: undefined,
671
+ baseURL: "",
672
+ };
673
+ }
674
+ /**
675
+ * Set configuration
676
+ */
677
+ function setConfig(userConfig) {
678
+ const currentConfig = globalStorage[CONFIG_KEY];
679
+ const finalConfig = Object.assign(Object.assign({}, currentConfig), userConfig);
680
+ // Validate that getter and setter are defined together
681
+ if ((finalConfig.getContextStore && !finalConfig.setContextStore) ||
682
+ (!finalConfig.getContextStore && finalConfig.setContextStore)) {
683
+ throw new Error("[TokenKit] getContextStore and setContextStore must be defined together.");
684
+ }
685
+ globalStorage[CONFIG_KEY] = finalConfig;
686
+ // Re-initialize global token manager if auth changed
687
+ if (finalConfig.auth) {
688
+ globalStorage[MANAGER_KEY] = new TokenManager(finalConfig.auth, finalConfig.baseURL);
689
+ }
690
+ else {
691
+ globalStorage[MANAGER_KEY] = undefined;
692
+ }
693
+ }
694
+ /**
695
+ * Get current configuration
696
+ */
697
+ function getConfig() {
698
+ return globalStorage[CONFIG_KEY];
699
+ }
700
+ /**
701
+ * Get global token manager
702
+ */
703
+ function getTokenManager() {
704
+ return globalStorage[MANAGER_KEY];
705
+ }
706
+ // Handle injected configuration from Astro integration
707
+ try {
708
+ // @ts-ignore
709
+ const injectedConfig = typeof __TOKENKIT_CONFIG__ !== 'undefined' ? __TOKENKIT_CONFIG__ : undefined;
710
+ if (injectedConfig) {
711
+ setConfig(injectedConfig);
712
+ }
713
+ }
714
+ catch (e) {
715
+ // Ignore errors in environments where __TOKENKIT_CONFIG__ might be restricted
716
+ }
717
+
718
+ // packages/astro-tokenkit/src/client/context.ts
719
+ /**
720
+ * Async local storage for Astro context
721
+ */
722
+ const als = new node_async_hooks.AsyncLocalStorage();
723
+ /**
724
+ * Bind Astro context for the current async scope
725
+ */
726
+ function runWithContext(ctx, fn) {
727
+ const config = getConfig();
728
+ const runner = config.runWithContext;
729
+ if (runner) {
730
+ return runner(ctx, fn);
731
+ }
732
+ return als.run(ctx, fn);
733
+ }
734
+
735
+ // packages/astro-tokenkit/src/middleware.ts
736
+ const LOGGED_KEY = Symbol.for('astro-tokenkit.middleware.logged');
737
+ /**
738
+ * Create middleware for context binding and automatic token rotation
739
+ */
740
+ function createMiddleware() {
741
+ return (ctx, next) => __awaiter(this, void 0, void 0, function* () {
742
+ const tokenManager = getTokenManager();
743
+ const config = getConfig();
744
+ const globalStorage = globalThis;
745
+ if (!globalStorage[LOGGED_KEY]) {
746
+ const authStatus = tokenManager ? 'enabled' : 'disabled';
747
+ let contextStrategy = 'default';
748
+ if (config.runWithContext) {
749
+ contextStrategy = 'custom (runWithContext)';
750
+ }
751
+ else if (config.setContextStore) {
752
+ contextStrategy = 'custom (getter/setter)';
753
+ }
754
+ else if (config.context) {
755
+ contextStrategy = 'custom (external AsyncLocalStorage)';
756
+ }
757
+ console.log(`[TokenKit] Middleware initialized (auth: ${authStatus}, context: ${contextStrategy})`);
758
+ globalStorage[LOGGED_KEY] = true;
759
+ }
760
+ const runLogic = () => __awaiter(this, void 0, void 0, function* () {
761
+ // Proactively ensure a valid session if auth is configured
762
+ if (tokenManager) {
763
+ try {
764
+ // This handles token rotation (refresh) if needed
765
+ yield tokenManager.ensure(ctx);
766
+ }
767
+ catch (error) {
768
+ // Log only the message to avoid leaking sensitive data in the error object
769
+ console.error('[TokenKit] Automatic token rotation failed:', error.message || error);
770
+ }
771
+ }
772
+ return next();
773
+ });
774
+ const setupAndRun = () => __awaiter(this, void 0, void 0, function* () {
775
+ if (config.setContextStore) {
776
+ config.setContextStore(ctx);
777
+ }
778
+ return runLogic();
779
+ });
780
+ if (config.runWithContext) {
781
+ return config.runWithContext(ctx, setupAndRun);
782
+ }
783
+ if (config.setContextStore) {
784
+ return setupAndRun();
785
+ }
786
+ return runWithContext(ctx, runLogic);
787
+ });
788
+ }
789
+ /**
790
+ * Standard Astro middleware export for autoinjection
791
+ */
792
+ const onRequest = createMiddleware();
793
+
794
+ exports.createMiddleware = createMiddleware;
795
+ exports.onRequest = onRequest;
796
+ //# sourceMappingURL=middleware.cjs.map