astro-tokenkit 1.0.10 → 1.0.12
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/README.md +70 -8
- package/dist/auth/detector.js +14 -0
- package/dist/auth/manager.d.ts +9 -4
- package/dist/auth/manager.js +100 -41
- package/dist/auth/policy.js +2 -1
- package/dist/auth/storage.d.ts +2 -0
- package/dist/auth/storage.js +11 -4
- package/dist/client/client.d.ts +15 -11
- package/dist/client/client.js +33 -31
- package/dist/client/context-shared.d.ts +1 -1
- package/dist/client/context-shared.js +2 -8
- package/dist/client/context.d.ts +2 -2
- package/dist/client/context.js +6 -9
- package/dist/index.cjs +168 -84
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +65 -21
- package/dist/index.js +167 -85
- package/dist/index.js.map +1 -1
- package/dist/middleware.js +2 -2
- package/dist/types.d.ts +33 -6
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -61,8 +61,8 @@ Now you can use the `api` client anywhere in your Astro pages or components with
|
|
|
61
61
|
// src/pages/profile.astro
|
|
62
62
|
import { api } from 'astro-tokenkit';
|
|
63
63
|
|
|
64
|
-
//
|
|
65
|
-
const user = await api.get('/me');
|
|
64
|
+
// Request methods return an APIResponse object
|
|
65
|
+
const { data: user } = await api.get('/me');
|
|
66
66
|
---
|
|
67
67
|
|
|
68
68
|
<h1>Welcome, {user.name}</h1>
|
|
@@ -125,10 +125,15 @@ const specializedClient = createClient({
|
|
|
125
125
|
| `login` | `string` | Endpoint path for login (POST). |
|
|
126
126
|
| `refresh` | `string` | Endpoint path for token refresh (POST). |
|
|
127
127
|
| `logout` | `string` | Endpoint path for logout (POST). |
|
|
128
|
-
| `
|
|
128
|
+
| `contentType` | `'application/json' \| 'application/x-www-form-urlencoded'` | Content type for auth requests (default: `application/json`). |
|
|
129
|
+
| `headers` | `Record<string, string>` | Extra headers for login/refresh requests. |
|
|
130
|
+
| `loginData` | `Record<string, any>` | Extra data to be sent with login request. |
|
|
131
|
+
| `refreshData` | `Record<string, any>` | Extra data to be sent with refresh request. |
|
|
132
|
+
| `refreshRequestField` | `string` | Field name for the refresh token in the refresh request (default: `refreshToken`). |
|
|
133
|
+
| `fields` | `FieldMapping` | Custom mapping for token fields in API responses (`accessToken`, `refreshToken`, `expiresAt`, `expiresIn`, `tokenType`, `sessionPayload`). |
|
|
129
134
|
| `parseLogin` | `Function` | Custom parser for login response: `(body: any) => TokenBundle`. |
|
|
130
135
|
| `parseRefresh`| `Function` | Custom parser for refresh response: `(body: any) => TokenBundle`. |
|
|
131
|
-
| `injectToken` | `Function` | Custom token injection: `(token: string) => string` (default: Bearer). |
|
|
136
|
+
| `injectToken` | `Function` | Custom token injection: `(token: string, type?: string) => string` (default: Bearer). |
|
|
132
137
|
| `cookies` | `CookieConfig` | Configuration for auth cookies. |
|
|
133
138
|
| `policy` | `RefreshPolicy` | Strategy for when to trigger token refresh. |
|
|
134
139
|
|
|
@@ -136,17 +141,34 @@ const specializedClient = createClient({
|
|
|
136
141
|
|
|
137
142
|
| Property | Type | Description |
|
|
138
143
|
| :--- | :--- | :--- |
|
|
139
|
-
| `ctx` | `TokenKitContext` | Optional Astro context. |
|
|
140
144
|
| `onLogin` | `Function` | Callback after successful login: `(bundle, body, ctx) => void`. |
|
|
145
|
+
| `onError` | `Function` | Callback after failed login: `(error, ctx) => void`. |
|
|
146
|
+
| `headers` | `Record<string, string>` | Extra headers for this specific login request. |
|
|
147
|
+
| `data` | `Record<string, any>` | Extra data for this specific login request. |
|
|
148
|
+
|
|
149
|
+
### Request Auth Overrides
|
|
150
|
+
|
|
151
|
+
When calling `api.get()`, `api.post()`, etc., you can override auth configuration (e.g., for multi-tenancy). Headers provided in the request options are automatically propagated to any automatic token refresh operations:
|
|
152
|
+
|
|
153
|
+
```typescript
|
|
154
|
+
await api.get('/data', {
|
|
155
|
+
headers: { 'x-tenant-name': 'lynx' },
|
|
156
|
+
auth: {
|
|
157
|
+
data: { extra_refresh_param: 'value' }
|
|
158
|
+
}
|
|
159
|
+
});
|
|
160
|
+
```
|
|
141
161
|
|
|
142
162
|
## Advanced Usage
|
|
143
163
|
|
|
144
164
|
### Manual Context
|
|
145
165
|
|
|
146
|
-
If you prefer not to use middleware, you can
|
|
166
|
+
If you prefer not to use middleware, you can bind the Astro context manually for a specific scope:
|
|
147
167
|
|
|
148
168
|
```typescript
|
|
149
|
-
|
|
169
|
+
import { runWithContext } from 'astro-tokenkit';
|
|
170
|
+
|
|
171
|
+
const { data } = await runWithContext(Astro, () => api.get('/data'));
|
|
150
172
|
```
|
|
151
173
|
|
|
152
174
|
### Interceptors
|
|
@@ -169,16 +191,56 @@ const api = createClient({
|
|
|
169
191
|
|
|
170
192
|
```typescript
|
|
171
193
|
// In an API route or server-side component
|
|
172
|
-
await api.login({ username, password }, {
|
|
194
|
+
const { data: bundle } = await api.login({ username, password }, {
|
|
173
195
|
onLogin: (bundle, body, ctx) => {
|
|
174
196
|
// Post-login logic (e.g., sync session to another store)
|
|
175
197
|
console.log('User logged in!', bundle.sessionPayload);
|
|
198
|
+
},
|
|
199
|
+
onError: (error, ctx) => {
|
|
200
|
+
// Handle error (e.g., log it or perform cleanup)
|
|
201
|
+
console.error('Login failed:', error.message);
|
|
176
202
|
}
|
|
177
203
|
});
|
|
178
204
|
|
|
179
205
|
await api.logout();
|
|
180
206
|
```
|
|
181
207
|
|
|
208
|
+
### Using Promises (.then, .catch, .finally)
|
|
209
|
+
|
|
210
|
+
All API methods return a Promise that resolves to an `APIResponse` object. You can use traditional promise chaining:
|
|
211
|
+
|
|
212
|
+
```typescript
|
|
213
|
+
// Example with GET request
|
|
214
|
+
api.get('/me')
|
|
215
|
+
.then(({ data: user, status }) => {
|
|
216
|
+
console.log(`User ${user.name} fetched with status ${status}`);
|
|
217
|
+
})
|
|
218
|
+
.catch(err => {
|
|
219
|
+
console.error('Failed to fetch user:', err.message);
|
|
220
|
+
})
|
|
221
|
+
.finally(() => {
|
|
222
|
+
console.log('Request finished');
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
// Example with login
|
|
226
|
+
api.login(credentials)
|
|
227
|
+
.then(({ data: token }) => {
|
|
228
|
+
console.log('Successfully logged in!', token.accessToken);
|
|
229
|
+
})
|
|
230
|
+
.catch(err => {
|
|
231
|
+
if (err instanceof AuthError) {
|
|
232
|
+
console.error('Authentication failed:', err.message);
|
|
233
|
+
} else {
|
|
234
|
+
console.error('An unexpected error occurred:', err.message);
|
|
235
|
+
}
|
|
236
|
+
})
|
|
237
|
+
.finally(() => {
|
|
238
|
+
// E.g. stop loading state
|
|
239
|
+
});
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
> **Note:** Since all methods return an `APIResponse` object, you can use destructuring in `.then()` to access the data directly, which allows for clean syntax like `.then(({ data: token }) => ... )`.
|
|
243
|
+
|
|
182
244
|
## License
|
|
183
245
|
|
|
184
246
|
MIT © [oamm](https://github.com/oamm)
|
package/dist/auth/detector.js
CHANGED
|
@@ -35,6 +35,13 @@ const EXPIRES_IN_FIELDS = [
|
|
|
35
35
|
'expiresIn',
|
|
36
36
|
'ttl',
|
|
37
37
|
];
|
|
38
|
+
/**
|
|
39
|
+
* Common field names for token type
|
|
40
|
+
*/
|
|
41
|
+
const TOKEN_TYPE_FIELDS = [
|
|
42
|
+
'token_type',
|
|
43
|
+
'tokenType',
|
|
44
|
+
];
|
|
38
45
|
/**
|
|
39
46
|
* Common field names for session payload
|
|
40
47
|
*/
|
|
@@ -97,10 +104,13 @@ export function autoDetectFields(body, fieldMapping) {
|
|
|
97
104
|
}
|
|
98
105
|
// Detect session payload (optional)
|
|
99
106
|
const sessionPayload = findField(SESSION_PAYLOAD_FIELDS, fieldMapping === null || fieldMapping === void 0 ? void 0 : fieldMapping.sessionPayload);
|
|
107
|
+
// Detect token type (optional)
|
|
108
|
+
const tokenType = findField(TOKEN_TYPE_FIELDS, fieldMapping === null || fieldMapping === void 0 ? void 0 : fieldMapping.tokenType);
|
|
100
109
|
return {
|
|
101
110
|
accessToken,
|
|
102
111
|
refreshToken,
|
|
103
112
|
accessExpiresAt,
|
|
113
|
+
tokenType: tokenType || undefined,
|
|
104
114
|
sessionPayload: sessionPayload || undefined,
|
|
105
115
|
};
|
|
106
116
|
}
|
|
@@ -114,6 +124,10 @@ export function parseJWTPayload(token) {
|
|
|
114
124
|
return null;
|
|
115
125
|
}
|
|
116
126
|
const payload = parts[1];
|
|
127
|
+
// Better UTF-8 support for environments with Buffer (like Node.js/Astro)
|
|
128
|
+
if (typeof Buffer !== 'undefined') {
|
|
129
|
+
return JSON.parse(Buffer.from(payload, 'base64').toString('utf8'));
|
|
130
|
+
}
|
|
117
131
|
const decoded = atob(payload.replace(/-/g, '+').replace(/_/g, '/'));
|
|
118
132
|
return JSON.parse(decoded);
|
|
119
133
|
}
|
package/dist/auth/manager.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { APIResponse } from '../types';
|
|
2
|
+
import type { TokenBundle, Session, AuthConfig, TokenKitContext, AuthOptions, LoginOptions } from '../types';
|
|
2
3
|
/**
|
|
3
4
|
* Token Manager handles all token operations
|
|
4
5
|
*/
|
|
@@ -10,11 +11,11 @@ export declare class TokenManager {
|
|
|
10
11
|
/**
|
|
11
12
|
* Perform login
|
|
12
13
|
*/
|
|
13
|
-
login(ctx: TokenKitContext, credentials: any,
|
|
14
|
+
login(ctx: TokenKitContext, credentials: any, options?: LoginOptions): Promise<APIResponse<TokenBundle>>;
|
|
14
15
|
/**
|
|
15
16
|
* Perform token refresh
|
|
16
17
|
*/
|
|
17
|
-
refresh(ctx: TokenKitContext, refreshToken: string): Promise<TokenBundle | null>;
|
|
18
|
+
refresh(ctx: TokenKitContext, refreshToken: string, options?: AuthOptions, headers?: Record<string, string>): Promise<TokenBundle | null>;
|
|
18
19
|
/**
|
|
19
20
|
* Internal refresh implementation
|
|
20
21
|
*/
|
|
@@ -22,7 +23,7 @@ export declare class TokenManager {
|
|
|
22
23
|
/**
|
|
23
24
|
* Ensure valid tokens (with automatic refresh)
|
|
24
25
|
*/
|
|
25
|
-
ensure(ctx: TokenKitContext): Promise<Session | null>;
|
|
26
|
+
ensure(ctx: TokenKitContext, options?: AuthOptions, headers?: Record<string, string>): Promise<Session | null>;
|
|
26
27
|
/**
|
|
27
28
|
* Logout (clear tokens)
|
|
28
29
|
*/
|
|
@@ -39,4 +40,8 @@ export declare class TokenManager {
|
|
|
39
40
|
* Create flight key for single-flight deduplication
|
|
40
41
|
*/
|
|
41
42
|
private createFlightKey;
|
|
43
|
+
/**
|
|
44
|
+
* Join base URL and path safely
|
|
45
|
+
*/
|
|
46
|
+
private joinURL;
|
|
42
47
|
}
|
package/dist/auth/manager.js
CHANGED
|
@@ -52,18 +52,38 @@ export class TokenManager {
|
|
|
52
52
|
/**
|
|
53
53
|
* Perform login
|
|
54
54
|
*/
|
|
55
|
-
login(ctx, credentials,
|
|
55
|
+
login(ctx, credentials, options) {
|
|
56
56
|
return __awaiter(this, void 0, void 0, function* () {
|
|
57
|
-
const url = this.baseURL
|
|
58
|
-
const
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
}
|
|
57
|
+
const url = this.joinURL(this.baseURL, this.config.login);
|
|
58
|
+
const contentType = this.config.contentType || 'application/json';
|
|
59
|
+
const headers = Object.assign(Object.assign({ 'Content-Type': contentType }, this.config.headers), options === null || options === void 0 ? void 0 : options.headers);
|
|
60
|
+
const data = Object.assign(Object.assign(Object.assign({}, this.config.loginData), options === null || options === void 0 ? void 0 : options.data), credentials);
|
|
61
|
+
let requestBody;
|
|
62
|
+
if (contentType === 'application/x-www-form-urlencoded') {
|
|
63
|
+
requestBody = new URLSearchParams(data).toString();
|
|
64
|
+
}
|
|
65
|
+
else {
|
|
66
|
+
requestBody = JSON.stringify(data);
|
|
67
|
+
}
|
|
68
|
+
let response;
|
|
69
|
+
try {
|
|
70
|
+
response = yield fetch(url, {
|
|
71
|
+
method: 'POST',
|
|
72
|
+
headers,
|
|
73
|
+
body: requestBody,
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
catch (error) {
|
|
77
|
+
const authError = new AuthError(`Login request failed: ${error.message}`);
|
|
78
|
+
if (options === null || options === void 0 ? void 0 : options.onError)
|
|
79
|
+
yield options.onError(authError, ctx);
|
|
80
|
+
throw authError;
|
|
81
|
+
}
|
|
65
82
|
if (!response.ok) {
|
|
66
|
-
|
|
83
|
+
const authError = new AuthError(`Login failed: ${response.status} ${response.statusText}`, response.status, response);
|
|
84
|
+
if (options === null || options === void 0 ? void 0 : options.onError)
|
|
85
|
+
yield options.onError(authError, ctx);
|
|
86
|
+
throw authError;
|
|
67
87
|
}
|
|
68
88
|
const body = yield response.json().catch(() => ({}));
|
|
69
89
|
// Parse response
|
|
@@ -74,24 +94,34 @@ export class TokenManager {
|
|
|
74
94
|
: autoDetectFields(body, this.config.fields);
|
|
75
95
|
}
|
|
76
96
|
catch (error) {
|
|
77
|
-
|
|
97
|
+
const authError = new AuthError(`Invalid login response: ${error.message}`, response.status, response);
|
|
98
|
+
if (options === null || options === void 0 ? void 0 : options.onError)
|
|
99
|
+
yield options.onError(authError, ctx);
|
|
100
|
+
throw authError;
|
|
78
101
|
}
|
|
79
102
|
// Store in cookies
|
|
80
103
|
storeTokens(ctx, bundle, this.config.cookies);
|
|
81
104
|
// Call onLogin callback if provided
|
|
82
|
-
if (onLogin) {
|
|
83
|
-
yield onLogin(bundle, body, ctx);
|
|
105
|
+
if (options === null || options === void 0 ? void 0 : options.onLogin) {
|
|
106
|
+
yield options.onLogin(bundle, body, ctx);
|
|
84
107
|
}
|
|
85
|
-
return
|
|
108
|
+
return {
|
|
109
|
+
data: bundle,
|
|
110
|
+
status: response.status,
|
|
111
|
+
statusText: response.statusText,
|
|
112
|
+
headers: response.headers,
|
|
113
|
+
url: response.url,
|
|
114
|
+
ok: response.ok,
|
|
115
|
+
};
|
|
86
116
|
});
|
|
87
117
|
}
|
|
88
118
|
/**
|
|
89
119
|
* Perform token refresh
|
|
90
120
|
*/
|
|
91
|
-
refresh(ctx, refreshToken) {
|
|
121
|
+
refresh(ctx, refreshToken, options, headers) {
|
|
92
122
|
return __awaiter(this, void 0, void 0, function* () {
|
|
93
123
|
try {
|
|
94
|
-
return yield this.performRefresh(ctx, refreshToken);
|
|
124
|
+
return yield this.performRefresh(ctx, refreshToken, options, headers);
|
|
95
125
|
}
|
|
96
126
|
catch (error) {
|
|
97
127
|
clearTokens(ctx, this.config.cookies);
|
|
@@ -102,16 +132,31 @@ export class TokenManager {
|
|
|
102
132
|
/**
|
|
103
133
|
* Internal refresh implementation
|
|
104
134
|
*/
|
|
105
|
-
performRefresh(ctx, refreshToken) {
|
|
135
|
+
performRefresh(ctx, refreshToken, options, extraHeaders) {
|
|
106
136
|
return __awaiter(this, void 0, void 0, function* () {
|
|
107
|
-
const url = this.baseURL
|
|
108
|
-
const
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
137
|
+
const url = this.joinURL(this.baseURL, this.config.refresh);
|
|
138
|
+
const contentType = this.config.contentType || 'application/json';
|
|
139
|
+
const headers = Object.assign(Object.assign({ 'Content-Type': contentType }, this.config.headers), extraHeaders);
|
|
140
|
+
const refreshField = this.config.refreshRequestField || 'refreshToken';
|
|
141
|
+
const data = Object.assign(Object.assign(Object.assign({}, this.config.refreshData), options === null || options === void 0 ? void 0 : options.data), { [refreshField]: refreshToken });
|
|
142
|
+
let requestBody;
|
|
143
|
+
if (contentType === 'application/x-www-form-urlencoded') {
|
|
144
|
+
requestBody = new URLSearchParams(data).toString();
|
|
145
|
+
}
|
|
146
|
+
else {
|
|
147
|
+
requestBody = JSON.stringify(data);
|
|
148
|
+
}
|
|
149
|
+
let response;
|
|
150
|
+
try {
|
|
151
|
+
response = yield fetch(url, {
|
|
152
|
+
method: 'POST',
|
|
153
|
+
headers,
|
|
154
|
+
body: requestBody,
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
catch (error) {
|
|
113
158
|
throw new AuthError(`Refresh request failed: ${error.message}`);
|
|
114
|
-
}
|
|
159
|
+
}
|
|
115
160
|
if (!response.ok) {
|
|
116
161
|
// 401/403 = invalid refresh token
|
|
117
162
|
if (response.status === 401 || response.status === 403) {
|
|
@@ -143,9 +188,9 @@ export class TokenManager {
|
|
|
143
188
|
/**
|
|
144
189
|
* Ensure valid tokens (with automatic refresh)
|
|
145
190
|
*/
|
|
146
|
-
ensure(ctx) {
|
|
191
|
+
ensure(ctx, options, headers) {
|
|
147
192
|
return __awaiter(this, void 0, void 0, function* () {
|
|
148
|
-
var _a, _b, _c, _d, _e;
|
|
193
|
+
var _a, _b, _c, _d, _e, _f;
|
|
149
194
|
const now = Math.floor(Date.now() / 1000);
|
|
150
195
|
const tokens = retrieveTokens(ctx, this.config.cookies);
|
|
151
196
|
// No tokens
|
|
@@ -155,23 +200,25 @@ export class TokenManager {
|
|
|
155
200
|
// Token expired
|
|
156
201
|
if (isExpired(tokens.expiresAt, now, this.config.policy)) {
|
|
157
202
|
const flightKey = this.createFlightKey(tokens.refreshToken);
|
|
158
|
-
const bundle = yield this.singleFlight.execute(flightKey, () => this.refresh(ctx, tokens.refreshToken));
|
|
203
|
+
const bundle = yield this.singleFlight.execute(flightKey, () => this.refresh(ctx, tokens.refreshToken, options, headers));
|
|
159
204
|
if (!bundle)
|
|
160
205
|
return null;
|
|
161
206
|
return {
|
|
162
207
|
accessToken: bundle.accessToken,
|
|
163
208
|
expiresAt: bundle.accessExpiresAt,
|
|
209
|
+
tokenType: bundle.tokenType,
|
|
164
210
|
payload: (_b = (_a = bundle.sessionPayload) !== null && _a !== void 0 ? _a : parseJWTPayload(bundle.accessToken)) !== null && _b !== void 0 ? _b : undefined,
|
|
165
211
|
};
|
|
166
212
|
}
|
|
167
213
|
// Proactive refresh
|
|
168
214
|
if (shouldRefresh(tokens.expiresAt, now, tokens.lastRefreshAt, this.config.policy)) {
|
|
169
215
|
const flightKey = this.createFlightKey(tokens.refreshToken);
|
|
170
|
-
const bundle = yield this.singleFlight.execute(flightKey, () => this.refresh(ctx, tokens.refreshToken));
|
|
216
|
+
const bundle = yield this.singleFlight.execute(flightKey, () => this.refresh(ctx, tokens.refreshToken, options, headers));
|
|
171
217
|
if (bundle) {
|
|
172
218
|
return {
|
|
173
219
|
accessToken: bundle.accessToken,
|
|
174
220
|
expiresAt: bundle.accessExpiresAt,
|
|
221
|
+
tokenType: bundle.tokenType,
|
|
175
222
|
payload: (_d = (_c = bundle.sessionPayload) !== null && _c !== void 0 ? _c : parseJWTPayload(bundle.accessToken)) !== null && _d !== void 0 ? _d : undefined,
|
|
176
223
|
};
|
|
177
224
|
}
|
|
@@ -185,7 +232,8 @@ export class TokenManager {
|
|
|
185
232
|
return {
|
|
186
233
|
accessToken: tokens.accessToken,
|
|
187
234
|
expiresAt: tokens.expiresAt,
|
|
188
|
-
|
|
235
|
+
tokenType: (_e = tokens.tokenType) !== null && _e !== void 0 ? _e : undefined,
|
|
236
|
+
payload: (_f = parseJWTPayload(tokens.accessToken)) !== null && _f !== void 0 ? _f : undefined,
|
|
189
237
|
};
|
|
190
238
|
});
|
|
191
239
|
}
|
|
@@ -194,15 +242,22 @@ export class TokenManager {
|
|
|
194
242
|
*/
|
|
195
243
|
logout(ctx) {
|
|
196
244
|
return __awaiter(this, void 0, void 0, function* () {
|
|
245
|
+
var _a;
|
|
197
246
|
// Optionally call logout endpoint
|
|
198
247
|
if (this.config.logout) {
|
|
199
248
|
try {
|
|
200
|
-
const url = this.baseURL
|
|
201
|
-
|
|
249
|
+
const url = this.joinURL(this.baseURL, this.config.logout);
|
|
250
|
+
const session = this.getSession(ctx);
|
|
251
|
+
const headers = {};
|
|
252
|
+
if (session === null || session === void 0 ? void 0 : session.accessToken) {
|
|
253
|
+
const injectFn = (_a = this.config.injectToken) !== null && _a !== void 0 ? _a : ((token, type) => `${type !== null && type !== void 0 ? type : 'Bearer'} ${token}`);
|
|
254
|
+
headers['Authorization'] = injectFn(session.accessToken, session.tokenType);
|
|
255
|
+
}
|
|
256
|
+
yield fetch(url, { method: 'POST', headers });
|
|
202
257
|
}
|
|
203
258
|
catch (error) {
|
|
204
259
|
// Ignore logout endpoint errors
|
|
205
|
-
console.warn('Logout endpoint failed:', error);
|
|
260
|
+
console.warn('[TokenKit] Logout endpoint failed:', error);
|
|
206
261
|
}
|
|
207
262
|
}
|
|
208
263
|
clearTokens(ctx, this.config.cookies);
|
|
@@ -212,7 +267,7 @@ export class TokenManager {
|
|
|
212
267
|
* Get current session (no refresh)
|
|
213
268
|
*/
|
|
214
269
|
getSession(ctx) {
|
|
215
|
-
var _a;
|
|
270
|
+
var _a, _b;
|
|
216
271
|
const tokens = retrieveTokens(ctx, this.config.cookies);
|
|
217
272
|
if (!tokens.accessToken || !tokens.expiresAt) {
|
|
218
273
|
return null;
|
|
@@ -220,7 +275,8 @@ export class TokenManager {
|
|
|
220
275
|
return {
|
|
221
276
|
accessToken: tokens.accessToken,
|
|
222
277
|
expiresAt: tokens.expiresAt,
|
|
223
|
-
|
|
278
|
+
tokenType: (_a = tokens.tokenType) !== null && _a !== void 0 ? _a : undefined,
|
|
279
|
+
payload: (_b = parseJWTPayload(tokens.accessToken)) !== null && _b !== void 0 ? _b : undefined,
|
|
224
280
|
};
|
|
225
281
|
}
|
|
226
282
|
/**
|
|
@@ -234,12 +290,15 @@ export class TokenManager {
|
|
|
234
290
|
* Create flight key for single-flight deduplication
|
|
235
291
|
*/
|
|
236
292
|
createFlightKey(token) {
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
293
|
+
// Avoid weak hashing of sensitive tokens
|
|
294
|
+
return `refresh_${token}`;
|
|
295
|
+
}
|
|
296
|
+
/**
|
|
297
|
+
* Join base URL and path safely
|
|
298
|
+
*/
|
|
299
|
+
joinURL(base, path) {
|
|
300
|
+
const b = base.endsWith('/') ? base : base + '/';
|
|
301
|
+
const p = path.startsWith('/') ? path.slice(1) : path;
|
|
302
|
+
return b + p;
|
|
244
303
|
}
|
|
245
304
|
}
|
package/dist/auth/policy.js
CHANGED
|
@@ -62,5 +62,6 @@ export function isExpired(expiresAt, now, policy = {}) {
|
|
|
62
62
|
const clockSkew = typeof normalized.clockSkew === 'number'
|
|
63
63
|
? normalized.clockSkew
|
|
64
64
|
: parseTime(normalized.clockSkew);
|
|
65
|
-
|
|
65
|
+
// Pessimistic: consider it expired if current time + skew is past expiration
|
|
66
|
+
return now + clockSkew > expiresAt;
|
|
66
67
|
}
|
package/dist/auth/storage.d.ts
CHANGED
|
@@ -7,6 +7,7 @@ export interface CookieNames {
|
|
|
7
7
|
refreshToken: string;
|
|
8
8
|
expiresAt: string;
|
|
9
9
|
lastRefreshAt: string;
|
|
10
|
+
tokenType: string;
|
|
10
11
|
}
|
|
11
12
|
/**
|
|
12
13
|
* Get cookie names with optional prefix
|
|
@@ -33,6 +34,7 @@ export declare function retrieveTokens(ctx: TokenKitContext, cookieConfig?: Cook
|
|
|
33
34
|
refreshToken: string | null;
|
|
34
35
|
expiresAt: number | null;
|
|
35
36
|
lastRefreshAt: number | null;
|
|
37
|
+
tokenType: string | null;
|
|
36
38
|
};
|
|
37
39
|
/**
|
|
38
40
|
* Clear all auth cookies
|
package/dist/auth/storage.js
CHANGED
|
@@ -9,6 +9,7 @@ export function getCookieNames(prefix) {
|
|
|
9
9
|
refreshToken: `${p}refresh_token`,
|
|
10
10
|
expiresAt: `${p}access_expires_at`,
|
|
11
11
|
lastRefreshAt: `${p}last_refresh_at`,
|
|
12
|
+
tokenType: `${p}token_type`,
|
|
12
13
|
};
|
|
13
14
|
}
|
|
14
15
|
/**
|
|
@@ -44,20 +45,25 @@ export function storeTokens(ctx, bundle, cookieConfig = {}) {
|
|
|
44
45
|
ctx.cookies.set(names.expiresAt, bundle.accessExpiresAt.toString(), Object.assign(Object.assign({}, options), { maxAge: accessMaxAge, path: '/' }));
|
|
45
46
|
// Set last refresh timestamp
|
|
46
47
|
ctx.cookies.set(names.lastRefreshAt, now.toString(), Object.assign(Object.assign({}, options), { maxAge: accessMaxAge, path: '/' }));
|
|
48
|
+
// Set token type if available
|
|
49
|
+
if (bundle.tokenType) {
|
|
50
|
+
ctx.cookies.set(names.tokenType, bundle.tokenType, Object.assign(Object.assign({}, options), { maxAge: accessMaxAge, path: '/' }));
|
|
51
|
+
}
|
|
47
52
|
}
|
|
48
53
|
/**
|
|
49
54
|
* Retrieve tokens from cookies
|
|
50
55
|
*/
|
|
51
56
|
export function retrieveTokens(ctx, cookieConfig = {}) {
|
|
52
|
-
var _a, _b, _c, _d;
|
|
57
|
+
var _a, _b, _c, _d, _e;
|
|
53
58
|
const names = getCookieNames(cookieConfig.prefix);
|
|
54
59
|
const accessToken = ((_a = ctx.cookies.get(names.accessToken)) === null || _a === void 0 ? void 0 : _a.value) || null;
|
|
55
60
|
const refreshToken = ((_b = ctx.cookies.get(names.refreshToken)) === null || _b === void 0 ? void 0 : _b.value) || null;
|
|
56
|
-
const
|
|
61
|
+
const tokenType = ((_c = ctx.cookies.get(names.tokenType)) === null || _c === void 0 ? void 0 : _c.value) || null;
|
|
62
|
+
const expiresAtStr = (_d = ctx.cookies.get(names.expiresAt)) === null || _d === void 0 ? void 0 : _d.value;
|
|
57
63
|
const expiresAt = expiresAtStr ? parseInt(expiresAtStr, 10) : null;
|
|
58
|
-
const lastRefreshAtStr = (
|
|
64
|
+
const lastRefreshAtStr = (_e = ctx.cookies.get(names.lastRefreshAt)) === null || _e === void 0 ? void 0 : _e.value;
|
|
59
65
|
const lastRefreshAt = lastRefreshAtStr ? parseInt(lastRefreshAtStr, 10) : null;
|
|
60
|
-
return { accessToken, refreshToken, expiresAt, lastRefreshAt };
|
|
66
|
+
return { accessToken, refreshToken, expiresAt, lastRefreshAt, tokenType };
|
|
61
67
|
}
|
|
62
68
|
/**
|
|
63
69
|
* Clear all auth cookies
|
|
@@ -69,4 +75,5 @@ export function clearTokens(ctx, cookieConfig = {}) {
|
|
|
69
75
|
ctx.cookies.delete(names.refreshToken, Object.assign(Object.assign({}, options), { path: '/' }));
|
|
70
76
|
ctx.cookies.delete(names.expiresAt, Object.assign(Object.assign({}, options), { path: '/' }));
|
|
71
77
|
ctx.cookies.delete(names.lastRefreshAt, Object.assign(Object.assign({}, options), { path: '/' }));
|
|
78
|
+
ctx.cookies.delete(names.tokenType, Object.assign(Object.assign({}, options), { path: '/' }));
|
|
72
79
|
}
|
package/dist/client/client.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { ClientConfig, RequestConfig, RequestOptions, Session,
|
|
1
|
+
import type { APIResponse, ClientConfig, RequestConfig, RequestOptions, Session, TokenKitConfig, LoginOptions, TokenBundle } from '../types';
|
|
2
2
|
import { TokenManager } from '../auth/manager';
|
|
3
3
|
/**
|
|
4
4
|
* API Client
|
|
@@ -25,27 +25,27 @@ export declare class APIClient {
|
|
|
25
25
|
/**
|
|
26
26
|
* GET request
|
|
27
27
|
*/
|
|
28
|
-
get<T = any>(url: string, options?: RequestOptions): Promise<T
|
|
28
|
+
get<T = any>(url: string, options?: RequestOptions): Promise<APIResponse<T>>;
|
|
29
29
|
/**
|
|
30
30
|
* POST request
|
|
31
31
|
*/
|
|
32
|
-
post<T = any>(url: string, data?: any, options?: RequestOptions): Promise<T
|
|
32
|
+
post<T = any>(url: string, data?: any, options?: RequestOptions): Promise<APIResponse<T>>;
|
|
33
33
|
/**
|
|
34
34
|
* PUT request
|
|
35
35
|
*/
|
|
36
|
-
put<T = any>(url: string, data?: any, options?: RequestOptions): Promise<T
|
|
36
|
+
put<T = any>(url: string, data?: any, options?: RequestOptions): Promise<APIResponse<T>>;
|
|
37
37
|
/**
|
|
38
38
|
* PATCH request
|
|
39
39
|
*/
|
|
40
|
-
patch<T = any>(url: string, data?: any, options?: RequestOptions): Promise<T
|
|
40
|
+
patch<T = any>(url: string, data?: any, options?: RequestOptions): Promise<APIResponse<T>>;
|
|
41
41
|
/**
|
|
42
42
|
* DELETE request
|
|
43
43
|
*/
|
|
44
|
-
delete<T = any>(url: string, options?: RequestOptions): Promise<T
|
|
44
|
+
delete<T = any>(url: string, options?: RequestOptions): Promise<APIResponse<T>>;
|
|
45
45
|
/**
|
|
46
46
|
* Generic request method
|
|
47
47
|
*/
|
|
48
|
-
request<T = any>(config: RequestConfig): Promise<T
|
|
48
|
+
request<T = any>(config: RequestConfig): Promise<APIResponse<T>>;
|
|
49
49
|
/**
|
|
50
50
|
* Execute single request
|
|
51
51
|
*/
|
|
@@ -62,22 +62,26 @@ export declare class APIClient {
|
|
|
62
62
|
* Build request headers
|
|
63
63
|
*/
|
|
64
64
|
private buildHeaders;
|
|
65
|
+
/**
|
|
66
|
+
* Check if a URL is safe for token injection (same origin as baseURL)
|
|
67
|
+
*/
|
|
68
|
+
private isSafeURL;
|
|
65
69
|
/**
|
|
66
70
|
* Login
|
|
67
71
|
*/
|
|
68
|
-
login(credentials: any, options?: LoginOptions
|
|
72
|
+
login(credentials: any, options?: LoginOptions): Promise<APIResponse<TokenBundle>>;
|
|
69
73
|
/**
|
|
70
74
|
* Logout
|
|
71
75
|
*/
|
|
72
|
-
logout(
|
|
76
|
+
logout(): Promise<void>;
|
|
73
77
|
/**
|
|
74
78
|
* Check if authenticated
|
|
75
79
|
*/
|
|
76
|
-
isAuthenticated(
|
|
80
|
+
isAuthenticated(): boolean;
|
|
77
81
|
/**
|
|
78
82
|
* Get current session
|
|
79
83
|
*/
|
|
80
|
-
getSession(
|
|
84
|
+
getSession(): Session | null;
|
|
81
85
|
}
|
|
82
86
|
/**
|
|
83
87
|
* Global API client instance.
|