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