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/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Alex Mora
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { TokenBundle, FieldMapping } from '../types';
|
|
2
|
+
/**
|
|
3
|
+
* Auto-detect token fields from response body
|
|
4
|
+
*/
|
|
5
|
+
export declare function autoDetectFields(body: any, fieldMapping?: FieldMapping): TokenBundle;
|
|
6
|
+
/**
|
|
7
|
+
* Parse JWT payload without verification (for reading only)
|
|
8
|
+
*/
|
|
9
|
+
export declare function parseJWTPayload(token: string): Record<string, any> | null;
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
// packages/astro-tokenkit/src/auth/detector.ts
|
|
2
|
+
/**
|
|
3
|
+
* Common field names for access tokens
|
|
4
|
+
*/
|
|
5
|
+
const ACCESS_TOKEN_FIELDS = [
|
|
6
|
+
'access_token',
|
|
7
|
+
'accessToken',
|
|
8
|
+
'token',
|
|
9
|
+
'jwt',
|
|
10
|
+
'id_token',
|
|
11
|
+
'idToken',
|
|
12
|
+
];
|
|
13
|
+
/**
|
|
14
|
+
* Common field names for refresh tokens
|
|
15
|
+
*/
|
|
16
|
+
const REFRESH_TOKEN_FIELDS = [
|
|
17
|
+
'refresh_token',
|
|
18
|
+
'refreshToken',
|
|
19
|
+
'refresh',
|
|
20
|
+
];
|
|
21
|
+
/**
|
|
22
|
+
* Common field names for expiration timestamp
|
|
23
|
+
*/
|
|
24
|
+
const EXPIRES_AT_FIELDS = [
|
|
25
|
+
'expires_at',
|
|
26
|
+
'expiresAt',
|
|
27
|
+
'exp',
|
|
28
|
+
'expiry',
|
|
29
|
+
];
|
|
30
|
+
/**
|
|
31
|
+
* Common field names for expires_in (seconds)
|
|
32
|
+
*/
|
|
33
|
+
const EXPIRES_IN_FIELDS = [
|
|
34
|
+
'expires_in',
|
|
35
|
+
'expiresIn',
|
|
36
|
+
'ttl',
|
|
37
|
+
];
|
|
38
|
+
/**
|
|
39
|
+
* Common field names for session payload
|
|
40
|
+
*/
|
|
41
|
+
const SESSION_PAYLOAD_FIELDS = [
|
|
42
|
+
'user',
|
|
43
|
+
'profile',
|
|
44
|
+
'account',
|
|
45
|
+
'data',
|
|
46
|
+
];
|
|
47
|
+
/**
|
|
48
|
+
* Auto-detect token fields from response body
|
|
49
|
+
*/
|
|
50
|
+
export function autoDetectFields(body, fieldMapping) {
|
|
51
|
+
// Helper to find field
|
|
52
|
+
const findField = (candidates, mapping) => {
|
|
53
|
+
if (mapping && body[mapping] !== undefined) {
|
|
54
|
+
return body[mapping];
|
|
55
|
+
}
|
|
56
|
+
for (const candidate of candidates) {
|
|
57
|
+
if (body[candidate] !== undefined) {
|
|
58
|
+
return body[candidate];
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
return undefined;
|
|
62
|
+
};
|
|
63
|
+
// Detect access token
|
|
64
|
+
const accessToken = findField(ACCESS_TOKEN_FIELDS, fieldMapping === null || fieldMapping === void 0 ? void 0 : fieldMapping.accessToken);
|
|
65
|
+
if (!accessToken) {
|
|
66
|
+
throw new Error(`Could not detect access token field. Tried: ${ACCESS_TOKEN_FIELDS.join(', ')}. ` +
|
|
67
|
+
`Provide custom parseLogin/parseRefresh or field mapping.`);
|
|
68
|
+
}
|
|
69
|
+
// Detect refresh token
|
|
70
|
+
const refreshToken = findField(REFRESH_TOKEN_FIELDS, fieldMapping === null || fieldMapping === void 0 ? void 0 : fieldMapping.refreshToken);
|
|
71
|
+
if (!refreshToken) {
|
|
72
|
+
throw new Error(`Could not detect refresh token field. Tried: ${REFRESH_TOKEN_FIELDS.join(', ')}. ` +
|
|
73
|
+
`Provide custom parseLogin/parseRefresh or field mapping.`);
|
|
74
|
+
}
|
|
75
|
+
// Detect expiration
|
|
76
|
+
let accessExpiresAt;
|
|
77
|
+
// Try expires_at first (timestamp)
|
|
78
|
+
const expiresAtValue = findField(EXPIRES_AT_FIELDS, fieldMapping === null || fieldMapping === void 0 ? void 0 : fieldMapping.expiresAt);
|
|
79
|
+
if (expiresAtValue !== undefined) {
|
|
80
|
+
accessExpiresAt = typeof expiresAtValue === 'number'
|
|
81
|
+
? expiresAtValue
|
|
82
|
+
: parseInt(expiresAtValue, 10);
|
|
83
|
+
}
|
|
84
|
+
// Try expires_in (seconds from now)
|
|
85
|
+
if (accessExpiresAt === undefined) {
|
|
86
|
+
const expiresInValue = findField(EXPIRES_IN_FIELDS, fieldMapping === null || fieldMapping === void 0 ? void 0 : fieldMapping.expiresIn);
|
|
87
|
+
if (expiresInValue !== undefined) {
|
|
88
|
+
const expiresIn = typeof expiresInValue === 'number'
|
|
89
|
+
? expiresInValue
|
|
90
|
+
: parseInt(expiresInValue, 10);
|
|
91
|
+
accessExpiresAt = Math.floor(Date.now() / 1000) + expiresIn;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
if (accessExpiresAt === undefined) {
|
|
95
|
+
throw new Error(`Could not detect expiration field. Tried: ${[...EXPIRES_AT_FIELDS, ...EXPIRES_IN_FIELDS].join(', ')}. ` +
|
|
96
|
+
`Provide custom parseLogin/parseRefresh or field mapping.`);
|
|
97
|
+
}
|
|
98
|
+
// Detect session payload (optional)
|
|
99
|
+
const sessionPayload = findField(SESSION_PAYLOAD_FIELDS, fieldMapping === null || fieldMapping === void 0 ? void 0 : fieldMapping.sessionPayload);
|
|
100
|
+
return {
|
|
101
|
+
accessToken,
|
|
102
|
+
refreshToken,
|
|
103
|
+
accessExpiresAt,
|
|
104
|
+
sessionPayload: sessionPayload || undefined,
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* Parse JWT payload without verification (for reading only)
|
|
109
|
+
*/
|
|
110
|
+
export function parseJWTPayload(token) {
|
|
111
|
+
try {
|
|
112
|
+
const parts = token.split('.');
|
|
113
|
+
if (parts.length !== 3) {
|
|
114
|
+
return null;
|
|
115
|
+
}
|
|
116
|
+
const payload = parts[1];
|
|
117
|
+
const decoded = atob(payload.replace(/-/g, '+').replace(/_/g, '/'));
|
|
118
|
+
return JSON.parse(decoded);
|
|
119
|
+
}
|
|
120
|
+
catch (_a) {
|
|
121
|
+
return null;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import type { TokenBundle, Session, AuthConfig, TokenKitContext } from '../types';
|
|
2
|
+
/**
|
|
3
|
+
* Token Manager handles all token operations
|
|
4
|
+
*/
|
|
5
|
+
export declare class TokenManager {
|
|
6
|
+
private config;
|
|
7
|
+
private singleFlight;
|
|
8
|
+
private baseURL;
|
|
9
|
+
constructor(config: AuthConfig, baseURL: string);
|
|
10
|
+
/**
|
|
11
|
+
* Perform login
|
|
12
|
+
*/
|
|
13
|
+
login(ctx: TokenKitContext, credentials: any): Promise<TokenBundle>;
|
|
14
|
+
/**
|
|
15
|
+
* Perform token refresh
|
|
16
|
+
*/
|
|
17
|
+
refresh(ctx: TokenKitContext, refreshToken: string): Promise<TokenBundle | null>;
|
|
18
|
+
/**
|
|
19
|
+
* Ensure valid tokens (with automatic refresh)
|
|
20
|
+
*/
|
|
21
|
+
ensure(ctx: TokenKitContext): Promise<Session | null>;
|
|
22
|
+
/**
|
|
23
|
+
* Logout (clear tokens)
|
|
24
|
+
*/
|
|
25
|
+
logout(ctx: TokenKitContext): Promise<void>;
|
|
26
|
+
/**
|
|
27
|
+
* Get current session (no refresh)
|
|
28
|
+
*/
|
|
29
|
+
getSession(ctx: TokenKitContext): Session | null;
|
|
30
|
+
/**
|
|
31
|
+
* Check if authenticated
|
|
32
|
+
*/
|
|
33
|
+
isAuthenticated(ctx: TokenKitContext): boolean;
|
|
34
|
+
/**
|
|
35
|
+
* Create flight key for single-flight deduplication
|
|
36
|
+
*/
|
|
37
|
+
private createFlightKey;
|
|
38
|
+
}
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
// packages/astro-tokenkit/src/auth/manager.ts
|
|
2
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
3
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
4
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
5
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
6
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
7
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
8
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
9
|
+
});
|
|
10
|
+
};
|
|
11
|
+
import { autoDetectFields, parseJWTPayload } from './detector';
|
|
12
|
+
import { storeTokens, retrieveTokens, clearTokens } from './storage';
|
|
13
|
+
import { shouldRefresh, isExpired } from './policy';
|
|
14
|
+
/**
|
|
15
|
+
* Single-flight refresh manager
|
|
16
|
+
*/
|
|
17
|
+
class SingleFlight {
|
|
18
|
+
constructor() {
|
|
19
|
+
this.inFlight = new Map();
|
|
20
|
+
}
|
|
21
|
+
execute(key, fn) {
|
|
22
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
23
|
+
const existing = this.inFlight.get(key);
|
|
24
|
+
if (existing)
|
|
25
|
+
return existing;
|
|
26
|
+
const promise = this.doExecute(key, fn);
|
|
27
|
+
this.inFlight.set(key, promise);
|
|
28
|
+
return promise;
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
doExecute(key, fn) {
|
|
32
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
33
|
+
try {
|
|
34
|
+
return yield fn();
|
|
35
|
+
}
|
|
36
|
+
finally {
|
|
37
|
+
this.inFlight.delete(key);
|
|
38
|
+
}
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Token Manager handles all token operations
|
|
44
|
+
*/
|
|
45
|
+
export class TokenManager {
|
|
46
|
+
constructor(config, baseURL) {
|
|
47
|
+
this.config = config;
|
|
48
|
+
this.singleFlight = new SingleFlight();
|
|
49
|
+
this.baseURL = baseURL;
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Perform login
|
|
53
|
+
*/
|
|
54
|
+
login(ctx, credentials) {
|
|
55
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
56
|
+
const url = this.baseURL + this.config.login;
|
|
57
|
+
const response = yield fetch(url, {
|
|
58
|
+
method: 'POST',
|
|
59
|
+
headers: { 'Content-Type': 'application/json' },
|
|
60
|
+
body: JSON.stringify(credentials),
|
|
61
|
+
});
|
|
62
|
+
if (!response.ok) {
|
|
63
|
+
throw new Error(`Login failed: ${response.status} ${response.statusText}`);
|
|
64
|
+
}
|
|
65
|
+
const body = yield response.json();
|
|
66
|
+
// Parse response
|
|
67
|
+
const bundle = this.config.parseLogin
|
|
68
|
+
? this.config.parseLogin(body)
|
|
69
|
+
: autoDetectFields(body, this.config.fields);
|
|
70
|
+
// Store in cookies
|
|
71
|
+
storeTokens(ctx, bundle, this.config.cookies);
|
|
72
|
+
return bundle;
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Perform token refresh
|
|
77
|
+
*/
|
|
78
|
+
refresh(ctx, refreshToken) {
|
|
79
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
80
|
+
const url = this.baseURL + this.config.refresh;
|
|
81
|
+
try {
|
|
82
|
+
const response = yield fetch(url, {
|
|
83
|
+
method: 'POST',
|
|
84
|
+
headers: { 'Content-Type': 'application/json' },
|
|
85
|
+
body: JSON.stringify({ refreshToken }),
|
|
86
|
+
});
|
|
87
|
+
if (!response.ok) {
|
|
88
|
+
// 401/403 = invalid refresh token
|
|
89
|
+
if (response.status === 401 || response.status === 403) {
|
|
90
|
+
clearTokens(ctx, this.config.cookies);
|
|
91
|
+
return null;
|
|
92
|
+
}
|
|
93
|
+
throw new Error(`Refresh failed: ${response.status} ${response.statusText}`);
|
|
94
|
+
}
|
|
95
|
+
const body = yield response.json();
|
|
96
|
+
// Parse response
|
|
97
|
+
const bundle = this.config.parseRefresh
|
|
98
|
+
? this.config.parseRefresh(body)
|
|
99
|
+
: autoDetectFields(body, this.config.fields);
|
|
100
|
+
// Validate bundle
|
|
101
|
+
if (!bundle.accessToken || !bundle.refreshToken || !bundle.accessExpiresAt) {
|
|
102
|
+
throw new Error('Invalid token bundle returned from refresh endpoint');
|
|
103
|
+
}
|
|
104
|
+
// Store new tokens
|
|
105
|
+
storeTokens(ctx, bundle, this.config.cookies);
|
|
106
|
+
return bundle;
|
|
107
|
+
}
|
|
108
|
+
catch (error) {
|
|
109
|
+
clearTokens(ctx, this.config.cookies);
|
|
110
|
+
throw error;
|
|
111
|
+
}
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* Ensure valid tokens (with automatic refresh)
|
|
116
|
+
*/
|
|
117
|
+
ensure(ctx) {
|
|
118
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
119
|
+
var _a, _b, _c, _d, _e;
|
|
120
|
+
const now = Math.floor(Date.now() / 1000);
|
|
121
|
+
const tokens = retrieveTokens(ctx, this.config.cookies);
|
|
122
|
+
// No tokens
|
|
123
|
+
if (!tokens.accessToken || !tokens.refreshToken || !tokens.expiresAt) {
|
|
124
|
+
return null;
|
|
125
|
+
}
|
|
126
|
+
// Token expired
|
|
127
|
+
if (isExpired(tokens.expiresAt, now, this.config.policy)) {
|
|
128
|
+
const flightKey = this.createFlightKey(tokens.refreshToken);
|
|
129
|
+
const bundle = yield this.singleFlight.execute(flightKey, () => this.refresh(ctx, tokens.refreshToken));
|
|
130
|
+
if (!bundle)
|
|
131
|
+
return null;
|
|
132
|
+
return {
|
|
133
|
+
accessToken: bundle.accessToken,
|
|
134
|
+
expiresAt: bundle.accessExpiresAt,
|
|
135
|
+
payload: (_b = (_a = bundle.sessionPayload) !== null && _a !== void 0 ? _a : parseJWTPayload(bundle.accessToken)) !== null && _b !== void 0 ? _b : undefined,
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
// Proactive refresh
|
|
139
|
+
if (shouldRefresh(tokens.expiresAt, now, tokens.lastRefreshAt, this.config.policy)) {
|
|
140
|
+
const flightKey = this.createFlightKey(tokens.refreshToken);
|
|
141
|
+
const bundle = yield this.singleFlight.execute(flightKey, () => this.refresh(ctx, tokens.refreshToken));
|
|
142
|
+
if (bundle) {
|
|
143
|
+
return {
|
|
144
|
+
accessToken: bundle.accessToken,
|
|
145
|
+
expiresAt: bundle.accessExpiresAt,
|
|
146
|
+
payload: (_d = (_c = bundle.sessionPayload) !== null && _c !== void 0 ? _c : parseJWTPayload(bundle.accessToken)) !== null && _d !== void 0 ? _d : undefined,
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
// Refresh failed, check if tokens still exist
|
|
150
|
+
const currentTokens = retrieveTokens(ctx, this.config.cookies);
|
|
151
|
+
if (!currentTokens.accessToken) {
|
|
152
|
+
return null;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
// Return current session
|
|
156
|
+
return {
|
|
157
|
+
accessToken: tokens.accessToken,
|
|
158
|
+
expiresAt: tokens.expiresAt,
|
|
159
|
+
payload: (_e = parseJWTPayload(tokens.accessToken)) !== null && _e !== void 0 ? _e : undefined,
|
|
160
|
+
};
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
/**
|
|
164
|
+
* Logout (clear tokens)
|
|
165
|
+
*/
|
|
166
|
+
logout(ctx) {
|
|
167
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
168
|
+
// Optionally call logout endpoint
|
|
169
|
+
if (this.config.logout) {
|
|
170
|
+
try {
|
|
171
|
+
const url = this.baseURL + this.config.logout;
|
|
172
|
+
yield fetch(url, { method: 'POST' });
|
|
173
|
+
}
|
|
174
|
+
catch (error) {
|
|
175
|
+
// Ignore logout endpoint errors
|
|
176
|
+
console.warn('Logout endpoint failed:', error);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
clearTokens(ctx, this.config.cookies);
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
/**
|
|
183
|
+
* Get current session (no refresh)
|
|
184
|
+
*/
|
|
185
|
+
getSession(ctx) {
|
|
186
|
+
var _a;
|
|
187
|
+
const tokens = retrieveTokens(ctx, this.config.cookies);
|
|
188
|
+
if (!tokens.accessToken || !tokens.expiresAt) {
|
|
189
|
+
return null;
|
|
190
|
+
}
|
|
191
|
+
return {
|
|
192
|
+
accessToken: tokens.accessToken,
|
|
193
|
+
expiresAt: tokens.expiresAt,
|
|
194
|
+
payload: (_a = parseJWTPayload(tokens.accessToken)) !== null && _a !== void 0 ? _a : undefined,
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
/**
|
|
198
|
+
* Check if authenticated
|
|
199
|
+
*/
|
|
200
|
+
isAuthenticated(ctx) {
|
|
201
|
+
const tokens = retrieveTokens(ctx, this.config.cookies);
|
|
202
|
+
return !!(tokens.accessToken && tokens.refreshToken);
|
|
203
|
+
}
|
|
204
|
+
/**
|
|
205
|
+
* Create flight key for single-flight deduplication
|
|
206
|
+
*/
|
|
207
|
+
createFlightKey(token) {
|
|
208
|
+
let hash = 0;
|
|
209
|
+
for (let i = 0; i < token.length; i++) {
|
|
210
|
+
const char = token.charCodeAt(i);
|
|
211
|
+
hash = ((hash << 5) - hash) + char;
|
|
212
|
+
hash = hash & hash;
|
|
213
|
+
}
|
|
214
|
+
return `flight_${Math.abs(hash).toString(36)}`;
|
|
215
|
+
}
|
|
216
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { RefreshPolicy } from '../types';
|
|
2
|
+
/**
|
|
3
|
+
* Default refresh policy
|
|
4
|
+
*/
|
|
5
|
+
export declare const DEFAULT_POLICY: {
|
|
6
|
+
refreshBefore: number;
|
|
7
|
+
clockSkew: number;
|
|
8
|
+
minInterval: number;
|
|
9
|
+
};
|
|
10
|
+
/**
|
|
11
|
+
* Normalize refresh policy (convert time strings to seconds)
|
|
12
|
+
*/
|
|
13
|
+
export declare function normalizePolicy(policy?: RefreshPolicy): Required<RefreshPolicy>;
|
|
14
|
+
/**
|
|
15
|
+
* Check if token should be refreshed
|
|
16
|
+
*/
|
|
17
|
+
export declare function shouldRefresh(expiresAt: number, now: number, lastRefreshAt: number | null, policy?: RefreshPolicy): boolean;
|
|
18
|
+
/**
|
|
19
|
+
* Check if token is expired
|
|
20
|
+
*/
|
|
21
|
+
export declare function isExpired(expiresAt: number, now: number, policy?: RefreshPolicy): boolean;
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
// packages/astro-tokenkit/src/auth/policy.ts
|
|
2
|
+
import { parseTime } from '../utils/time';
|
|
3
|
+
/**
|
|
4
|
+
* Default refresh policy
|
|
5
|
+
*/
|
|
6
|
+
export const DEFAULT_POLICY = {
|
|
7
|
+
refreshBefore: 300, // 5 minutes
|
|
8
|
+
clockSkew: 60, // 1 minute
|
|
9
|
+
minInterval: 30, // 30 seconds
|
|
10
|
+
};
|
|
11
|
+
/**
|
|
12
|
+
* Normalize refresh policy (convert time strings to seconds)
|
|
13
|
+
*/
|
|
14
|
+
export function normalizePolicy(policy = {}) {
|
|
15
|
+
return {
|
|
16
|
+
refreshBefore: policy.refreshBefore
|
|
17
|
+
? parseTime(policy.refreshBefore)
|
|
18
|
+
: DEFAULT_POLICY.refreshBefore,
|
|
19
|
+
clockSkew: policy.clockSkew
|
|
20
|
+
? parseTime(policy.clockSkew)
|
|
21
|
+
: DEFAULT_POLICY.clockSkew,
|
|
22
|
+
minInterval: policy.minInterval
|
|
23
|
+
? parseTime(policy.minInterval)
|
|
24
|
+
: DEFAULT_POLICY.minInterval,
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Check if token should be refreshed
|
|
29
|
+
*/
|
|
30
|
+
export function shouldRefresh(expiresAt, now, lastRefreshAt, policy = {}) {
|
|
31
|
+
const normalized = normalizePolicy(policy);
|
|
32
|
+
const refreshBefore = typeof normalized.refreshBefore === 'number'
|
|
33
|
+
? normalized.refreshBefore
|
|
34
|
+
: parseTime(normalized.refreshBefore);
|
|
35
|
+
const clockSkew = typeof normalized.clockSkew === 'number'
|
|
36
|
+
? normalized.clockSkew
|
|
37
|
+
: parseTime(normalized.clockSkew);
|
|
38
|
+
const minInterval = typeof normalized.minInterval === 'number'
|
|
39
|
+
? normalized.minInterval
|
|
40
|
+
: parseTime(normalized.minInterval);
|
|
41
|
+
// Adjust for clock skew
|
|
42
|
+
const adjustedNow = now + clockSkew;
|
|
43
|
+
// Check if near expiration
|
|
44
|
+
const timeUntilExpiry = expiresAt - adjustedNow;
|
|
45
|
+
if (timeUntilExpiry > refreshBefore) {
|
|
46
|
+
return false;
|
|
47
|
+
}
|
|
48
|
+
// Check minimum interval
|
|
49
|
+
if (lastRefreshAt !== null) {
|
|
50
|
+
const timeSinceLastRefresh = now - lastRefreshAt;
|
|
51
|
+
if (timeSinceLastRefresh < minInterval) {
|
|
52
|
+
return false;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
return true;
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Check if token is expired
|
|
59
|
+
*/
|
|
60
|
+
export function isExpired(expiresAt, now, policy = {}) {
|
|
61
|
+
const normalized = normalizePolicy(policy);
|
|
62
|
+
const clockSkew = typeof normalized.clockSkew === 'number'
|
|
63
|
+
? normalized.clockSkew
|
|
64
|
+
: parseTime(normalized.clockSkew);
|
|
65
|
+
return now > expiresAt + clockSkew;
|
|
66
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import type { TokenBundle, CookieConfig, TokenKitContext } from '../types';
|
|
2
|
+
/**
|
|
3
|
+
* Cookie names
|
|
4
|
+
*/
|
|
5
|
+
export interface CookieNames {
|
|
6
|
+
accessToken: string;
|
|
7
|
+
refreshToken: string;
|
|
8
|
+
expiresAt: string;
|
|
9
|
+
lastRefreshAt: string;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Get cookie names with optional prefix
|
|
13
|
+
*/
|
|
14
|
+
export declare function getCookieNames(prefix?: string): CookieNames;
|
|
15
|
+
/**
|
|
16
|
+
* Get cookie options with smart defaults
|
|
17
|
+
*/
|
|
18
|
+
export declare function getCookieOptions(config?: CookieConfig): {
|
|
19
|
+
secure: boolean;
|
|
20
|
+
sameSite: "strict" | "lax" | "none";
|
|
21
|
+
httpOnly: boolean;
|
|
22
|
+
domain: string | undefined;
|
|
23
|
+
};
|
|
24
|
+
/**
|
|
25
|
+
* Store token bundle in cookies
|
|
26
|
+
*/
|
|
27
|
+
export declare function storeTokens(ctx: TokenKitContext, bundle: TokenBundle, cookieConfig?: CookieConfig): void;
|
|
28
|
+
/**
|
|
29
|
+
* Retrieve tokens from cookies
|
|
30
|
+
*/
|
|
31
|
+
export declare function retrieveTokens(ctx: TokenKitContext, cookieConfig?: CookieConfig): {
|
|
32
|
+
accessToken: string | null;
|
|
33
|
+
refreshToken: string | null;
|
|
34
|
+
expiresAt: number | null;
|
|
35
|
+
lastRefreshAt: number | null;
|
|
36
|
+
};
|
|
37
|
+
/**
|
|
38
|
+
* Clear all auth cookies
|
|
39
|
+
*/
|
|
40
|
+
export declare function clearTokens(ctx: TokenKitContext, cookieConfig?: CookieConfig): void;
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
// packages/astro-tokenkit/src/auth/storage.ts
|
|
2
|
+
/**
|
|
3
|
+
* Get cookie names with optional prefix
|
|
4
|
+
*/
|
|
5
|
+
export function getCookieNames(prefix) {
|
|
6
|
+
const p = prefix ? `${prefix}_` : '';
|
|
7
|
+
return {
|
|
8
|
+
accessToken: `${p}access_token`,
|
|
9
|
+
refreshToken: `${p}refresh_token`,
|
|
10
|
+
expiresAt: `${p}access_expires_at`,
|
|
11
|
+
lastRefreshAt: `${p}last_refresh_at`,
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Get cookie options with smart defaults
|
|
16
|
+
*/
|
|
17
|
+
export function getCookieOptions(config = {}) {
|
|
18
|
+
var _a, _b;
|
|
19
|
+
const isProduction = process.env.NODE_ENV === 'production';
|
|
20
|
+
return {
|
|
21
|
+
secure: (_a = config.secure) !== null && _a !== void 0 ? _a : isProduction,
|
|
22
|
+
sameSite: (_b = config.sameSite) !== null && _b !== void 0 ? _b : 'lax',
|
|
23
|
+
httpOnly: true, // Always HttpOnly for security
|
|
24
|
+
domain: config.domain,
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Store token bundle in cookies
|
|
29
|
+
*/
|
|
30
|
+
export function storeTokens(ctx, bundle, cookieConfig = {}) {
|
|
31
|
+
const names = getCookieNames(cookieConfig.prefix);
|
|
32
|
+
const options = getCookieOptions(cookieConfig);
|
|
33
|
+
const now = Math.floor(Date.now() / 1000);
|
|
34
|
+
// Calculate max age
|
|
35
|
+
const accessMaxAge = Math.max(0, bundle.accessExpiresAt - now);
|
|
36
|
+
const refreshMaxAge = bundle.refreshExpiresAt
|
|
37
|
+
? Math.max(0, bundle.refreshExpiresAt - now)
|
|
38
|
+
: 7 * 24 * 60 * 60; // Default 7 days
|
|
39
|
+
// Set access token
|
|
40
|
+
ctx.cookies.set(names.accessToken, bundle.accessToken, Object.assign(Object.assign({}, options), { maxAge: accessMaxAge, path: '/' }));
|
|
41
|
+
// Set refresh token (restricted path for security)
|
|
42
|
+
ctx.cookies.set(names.refreshToken, bundle.refreshToken, Object.assign(Object.assign({}, options), { maxAge: refreshMaxAge, path: '/' }));
|
|
43
|
+
// Set expiration timestamp
|
|
44
|
+
ctx.cookies.set(names.expiresAt, bundle.accessExpiresAt.toString(), Object.assign(Object.assign({}, options), { maxAge: accessMaxAge, path: '/' }));
|
|
45
|
+
// Set last refresh timestamp
|
|
46
|
+
ctx.cookies.set(names.lastRefreshAt, now.toString(), Object.assign(Object.assign({}, options), { maxAge: accessMaxAge, path: '/' }));
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Retrieve tokens from cookies
|
|
50
|
+
*/
|
|
51
|
+
export function retrieveTokens(ctx, cookieConfig = {}) {
|
|
52
|
+
var _a, _b, _c, _d;
|
|
53
|
+
const names = getCookieNames(cookieConfig.prefix);
|
|
54
|
+
const accessToken = ((_a = ctx.cookies.get(names.accessToken)) === null || _a === void 0 ? void 0 : _a.value) || null;
|
|
55
|
+
const refreshToken = ((_b = ctx.cookies.get(names.refreshToken)) === null || _b === void 0 ? void 0 : _b.value) || null;
|
|
56
|
+
const expiresAtStr = (_c = ctx.cookies.get(names.expiresAt)) === null || _c === void 0 ? void 0 : _c.value;
|
|
57
|
+
const expiresAt = expiresAtStr ? parseInt(expiresAtStr, 10) : null;
|
|
58
|
+
const lastRefreshAtStr = (_d = ctx.cookies.get(names.lastRefreshAt)) === null || _d === void 0 ? void 0 : _d.value;
|
|
59
|
+
const lastRefreshAt = lastRefreshAtStr ? parseInt(lastRefreshAtStr, 10) : null;
|
|
60
|
+
return { accessToken, refreshToken, expiresAt, lastRefreshAt };
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Clear all auth cookies
|
|
64
|
+
*/
|
|
65
|
+
export function clearTokens(ctx, cookieConfig = {}) {
|
|
66
|
+
const names = getCookieNames(cookieConfig.prefix);
|
|
67
|
+
const options = getCookieOptions(cookieConfig);
|
|
68
|
+
ctx.cookies.delete(names.accessToken, Object.assign(Object.assign({}, options), { path: '/' }));
|
|
69
|
+
ctx.cookies.delete(names.refreshToken, Object.assign(Object.assign({}, options), { path: '/' }));
|
|
70
|
+
ctx.cookies.delete(names.expiresAt, Object.assign(Object.assign({}, options), { path: '/' }));
|
|
71
|
+
ctx.cookies.delete(names.lastRefreshAt, Object.assign(Object.assign({}, options), { path: '/' }));
|
|
72
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import type { ClientConfig, RequestConfig, RequestOptions, Session, TokenKitContext } from '../types';
|
|
2
|
+
import { TokenManager } from '../auth/manager';
|
|
3
|
+
import { type ContextOptions } from './context';
|
|
4
|
+
/**
|
|
5
|
+
* API Client
|
|
6
|
+
*/
|
|
7
|
+
export declare class APIClient {
|
|
8
|
+
tokenManager?: TokenManager;
|
|
9
|
+
private config;
|
|
10
|
+
contextOptions: ContextOptions;
|
|
11
|
+
constructor(config: ClientConfig);
|
|
12
|
+
/**
|
|
13
|
+
* GET request
|
|
14
|
+
*/
|
|
15
|
+
get<T = any>(url: string, options?: RequestOptions): Promise<T>;
|
|
16
|
+
/**
|
|
17
|
+
* POST request
|
|
18
|
+
*/
|
|
19
|
+
post<T = any>(url: string, data?: any, options?: RequestOptions): Promise<T>;
|
|
20
|
+
/**
|
|
21
|
+
* PUT request
|
|
22
|
+
*/
|
|
23
|
+
put<T = any>(url: string, data?: any, options?: RequestOptions): Promise<T>;
|
|
24
|
+
/**
|
|
25
|
+
* PATCH request
|
|
26
|
+
*/
|
|
27
|
+
patch<T = any>(url: string, data?: any, options?: RequestOptions): Promise<T>;
|
|
28
|
+
/**
|
|
29
|
+
* DELETE request
|
|
30
|
+
*/
|
|
31
|
+
delete<T = any>(url: string, options?: RequestOptions): Promise<T>;
|
|
32
|
+
/**
|
|
33
|
+
* Generic request method
|
|
34
|
+
*/
|
|
35
|
+
request<T = any>(config: RequestConfig): Promise<T>;
|
|
36
|
+
/**
|
|
37
|
+
* Execute single request
|
|
38
|
+
*/
|
|
39
|
+
private executeRequest;
|
|
40
|
+
/**
|
|
41
|
+
* Parse response
|
|
42
|
+
*/
|
|
43
|
+
private parseResponse;
|
|
44
|
+
/**
|
|
45
|
+
* Build full URL with query params
|
|
46
|
+
*/
|
|
47
|
+
private buildURL;
|
|
48
|
+
/**
|
|
49
|
+
* Build request headers
|
|
50
|
+
*/
|
|
51
|
+
private buildHeaders;
|
|
52
|
+
/**
|
|
53
|
+
* Login
|
|
54
|
+
*/
|
|
55
|
+
login(credentials: any, ctx?: TokenKitContext): Promise<void>;
|
|
56
|
+
/**
|
|
57
|
+
* Logout
|
|
58
|
+
*/
|
|
59
|
+
logout(ctx?: TokenKitContext): Promise<void>;
|
|
60
|
+
/**
|
|
61
|
+
* Check if authenticated
|
|
62
|
+
*/
|
|
63
|
+
isAuthenticated(ctx?: TokenKitContext): boolean;
|
|
64
|
+
/**
|
|
65
|
+
* Get current session
|
|
66
|
+
*/
|
|
67
|
+
getSession(ctx?: TokenKitContext): Session | null;
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Create API client
|
|
71
|
+
*/
|
|
72
|
+
export declare function createClient(config: ClientConfig): APIClient;
|