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.
- package/dist/index.cjs +25 -1
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +5 -0
- package/dist/index.js +25 -1
- package/dist/index.js.map +1 -1
- package/dist/integration.js +8 -1
- package/dist/middleware.cjs +796 -0
- package/dist/middleware.cjs.map +1 -0
- package/dist/middleware.d.ts +9 -2
- package/dist/middleware.js +758 -13
- package/dist/middleware.js.map +1 -0
- package/dist/types.d.ts +5 -0
- package/package.json +6 -1
package/dist/middleware.js
CHANGED
|
@@ -1,22 +1,760 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
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
|
-
|
|
12
|
-
|
|
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
|
-
|
|
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
|
|
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
|