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