@crimson-education/sdk 0.2.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/README.md +377 -0
- package/dist/core/account.d.ts +14 -0
- package/dist/core/account.js +30 -0
- package/dist/core/auth/index.d.ts +11 -0
- package/dist/core/auth/index.js +25 -0
- package/dist/core/auth/oauth-adapter.d.ts +78 -0
- package/dist/core/auth/oauth-adapter.js +341 -0
- package/dist/core/auth/pkce.d.ts +20 -0
- package/dist/core/auth/pkce.js +112 -0
- package/dist/core/auth/token-manager.d.ts +68 -0
- package/dist/core/auth/token-manager.js +294 -0
- package/dist/core/auth/token-storage.d.ts +46 -0
- package/dist/core/auth/token-storage.js +155 -0
- package/dist/core/auth/types.d.ts +148 -0
- package/dist/core/auth/types.js +15 -0
- package/dist/core/client.d.ts +84 -0
- package/dist/core/client.js +229 -0
- package/dist/core/index.d.ts +11 -0
- package/dist/core/index.js +47 -0
- package/dist/core/missionLibrary.d.ts +68 -0
- package/dist/core/missionLibrary.js +143 -0
- package/dist/core/missions.d.ts +45 -0
- package/dist/core/missions.js +140 -0
- package/dist/core/roadmap.d.ts +8 -0
- package/dist/core/roadmap.js +18 -0
- package/dist/core/studentProfile.d.ts +21 -0
- package/dist/core/studentProfile.js +41 -0
- package/dist/core/tasks.d.ts +117 -0
- package/dist/core/tasks.js +288 -0
- package/dist/core/types.d.ts +402 -0
- package/dist/core/types.js +2 -0
- package/dist/core/users.d.ts +21 -0
- package/dist/core/users.js +46 -0
- package/dist/iframe/auth-state.d.ts +7 -0
- package/dist/iframe/auth-state.js +125 -0
- package/dist/iframe/constants.d.ts +8 -0
- package/dist/iframe/constants.js +29 -0
- package/dist/iframe/index.d.ts +5 -0
- package/dist/iframe/index.js +17 -0
- package/dist/iframe/listener.d.ts +2 -0
- package/dist/iframe/listener.js +57 -0
- package/dist/iframe/types.d.ts +18 -0
- package/dist/iframe/types.js +2 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +22 -0
- package/dist/react/hooks/index.d.ts +10 -0
- package/dist/react/hooks/index.js +48 -0
- package/dist/react/hooks/useAccount.d.ts +13 -0
- package/dist/react/hooks/useAccount.js +39 -0
- package/dist/react/hooks/useAuthState.d.ts +2 -0
- package/dist/react/hooks/useAuthState.js +18 -0
- package/dist/react/hooks/useMissionLibrary.d.ts +31 -0
- package/dist/react/hooks/useMissionLibrary.js +183 -0
- package/dist/react/hooks/useMissions.d.ts +24 -0
- package/dist/react/hooks/useMissions.js +104 -0
- package/dist/react/hooks/useOAuth.d.ts +94 -0
- package/dist/react/hooks/useOAuth.js +211 -0
- package/dist/react/hooks/useRoadmapContext.d.ts +2 -0
- package/dist/react/hooks/useRoadmapContext.js +29 -0
- package/dist/react/hooks/useStudentProfile.d.ts +24 -0
- package/dist/react/hooks/useStudentProfile.js +65 -0
- package/dist/react/hooks/useTasks.d.ts +26 -0
- package/dist/react/hooks/useTasks.js +137 -0
- package/dist/react/hooks/useUsers.d.ts +9 -0
- package/dist/react/hooks/useUsers.js +50 -0
- package/dist/react/index.d.ts +3 -0
- package/dist/react/index.js +21 -0
- package/dist/react/provider.d.ts +16 -0
- package/dist/react/provider.js +41 -0
- package/package.json +61 -0
|
@@ -0,0 +1,341 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* OAuth Adapter
|
|
4
|
+
* Main entry point for OAuth 2.0 authentication with PKCE
|
|
5
|
+
*/
|
|
6
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
7
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
8
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
9
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
10
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
11
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
12
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
13
|
+
});
|
|
14
|
+
};
|
|
15
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
16
|
+
exports.OAuthAdapter = void 0;
|
|
17
|
+
exports.createCrimsonOAuthAdapter = createCrimsonOAuthAdapter;
|
|
18
|
+
const pkce_1 = require("./pkce");
|
|
19
|
+
const token_manager_1 = require("./token-manager");
|
|
20
|
+
const token_storage_1 = require("./token-storage");
|
|
21
|
+
const PKCE_STATE_KEY = "crimson_oauth_pkce_state";
|
|
22
|
+
class OAuthAdapter {
|
|
23
|
+
constructor(config) {
|
|
24
|
+
var _a, _b;
|
|
25
|
+
this.initialized = false;
|
|
26
|
+
this.config = config;
|
|
27
|
+
this.storage = (_a = config.storage) !== null && _a !== void 0 ? _a : (0, token_storage_1.createDefaultTokenStorage)();
|
|
28
|
+
this.tokenManager = new token_manager_1.TokenManager({
|
|
29
|
+
storage: this.storage,
|
|
30
|
+
onRefreshNeeded: this.refreshTokenWithApi.bind(this),
|
|
31
|
+
autoRefresh: (_b = config.autoRefresh) !== null && _b !== void 0 ? _b : true,
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Initialize the OAuth adapter
|
|
36
|
+
* Loads existing tokens and sets up auto-refresh
|
|
37
|
+
*/
|
|
38
|
+
initialize() {
|
|
39
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
40
|
+
if (this.initialized) {
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
yield this.tokenManager.initialize();
|
|
44
|
+
this.initialized = true;
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Get the authorization URL for OAuth flow
|
|
49
|
+
*/
|
|
50
|
+
getAuthorizeUrl(options) {
|
|
51
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
52
|
+
var _a, _b, _c;
|
|
53
|
+
const codeVerifier = (0, pkce_1.generateCodeVerifier)();
|
|
54
|
+
const codeChallenge = yield (0, pkce_1.generateCodeChallenge)(codeVerifier);
|
|
55
|
+
const state = (0, pkce_1.generateState)();
|
|
56
|
+
// Store PKCE state for callback verification
|
|
57
|
+
const pkceState = {
|
|
58
|
+
codeVerifier,
|
|
59
|
+
state,
|
|
60
|
+
createdAt: Date.now(),
|
|
61
|
+
};
|
|
62
|
+
this.storePkceState(pkceState);
|
|
63
|
+
// Build authorization URL
|
|
64
|
+
const params = new URLSearchParams({
|
|
65
|
+
response_type: "code",
|
|
66
|
+
client_id: this.config.clientId,
|
|
67
|
+
redirect_uri: (_a = options === null || options === void 0 ? void 0 : options.redirectUri) !== null && _a !== void 0 ? _a : this.config.redirectUri,
|
|
68
|
+
scope: ((_c = (_b = options === null || options === void 0 ? void 0 : options.scope) !== null && _b !== void 0 ? _b : this.config.scope) !== null && _c !== void 0 ? _c : ["profile"]).join(" "),
|
|
69
|
+
state,
|
|
70
|
+
code_challenge: codeChallenge,
|
|
71
|
+
code_challenge_method: "S256",
|
|
72
|
+
});
|
|
73
|
+
return `${this.config.authorizationEndpoint}?${params.toString()}`;
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Start the OAuth authorization flow
|
|
78
|
+
* Redirects the user to the authorization page
|
|
79
|
+
*/
|
|
80
|
+
authorize(options) {
|
|
81
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
82
|
+
const url = yield this.getAuthorizeUrl(options);
|
|
83
|
+
if (typeof window !== "undefined") {
|
|
84
|
+
window.location.href = url;
|
|
85
|
+
}
|
|
86
|
+
else {
|
|
87
|
+
throw new Error("authorize() can only be called in a browser environment");
|
|
88
|
+
}
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Handle the OAuth callback
|
|
93
|
+
* Exchanges the authorization code for tokens
|
|
94
|
+
*/
|
|
95
|
+
handleCallback(params) {
|
|
96
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
97
|
+
// Check for error response
|
|
98
|
+
if (params.error) {
|
|
99
|
+
const error = {
|
|
100
|
+
error: params.error,
|
|
101
|
+
errorDescription: params.errorDescription,
|
|
102
|
+
};
|
|
103
|
+
throw error;
|
|
104
|
+
}
|
|
105
|
+
// Validate required parameters
|
|
106
|
+
if (!params.code) {
|
|
107
|
+
throw {
|
|
108
|
+
error: "invalid_request",
|
|
109
|
+
errorDescription: "Missing authorization code",
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
if (!params.state) {
|
|
113
|
+
throw {
|
|
114
|
+
error: "invalid_request",
|
|
115
|
+
errorDescription: "Missing state parameter",
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
// Retrieve and validate PKCE state
|
|
119
|
+
const pkceState = this.getPkceState();
|
|
120
|
+
if (!pkceState) {
|
|
121
|
+
throw {
|
|
122
|
+
error: "invalid_request",
|
|
123
|
+
errorDescription: "No PKCE state found",
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
// Validate state matches
|
|
127
|
+
if (pkceState.state !== params.state) {
|
|
128
|
+
throw {
|
|
129
|
+
error: "invalid_request",
|
|
130
|
+
errorDescription: "State mismatch - possible CSRF attack",
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
// Check if PKCE state is expired (10 minutes)
|
|
134
|
+
if (Date.now() - pkceState.createdAt > 10 * 60 * 1000) {
|
|
135
|
+
this.clearPkceState();
|
|
136
|
+
throw {
|
|
137
|
+
error: "invalid_request",
|
|
138
|
+
errorDescription: "Authorization request expired",
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
// Exchange code for tokens
|
|
142
|
+
try {
|
|
143
|
+
const tokens = yield this.exchangeCode(params.code, pkceState.codeVerifier, params.redirectUri);
|
|
144
|
+
// Clear PKCE state after successful exchange
|
|
145
|
+
this.clearPkceState();
|
|
146
|
+
// Store tokens
|
|
147
|
+
yield this.tokenManager.setTokens(tokens);
|
|
148
|
+
return tokens;
|
|
149
|
+
}
|
|
150
|
+
catch (error) {
|
|
151
|
+
this.clearPkceState();
|
|
152
|
+
throw error;
|
|
153
|
+
}
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
/**
|
|
157
|
+
* Manually refresh the access token
|
|
158
|
+
*/
|
|
159
|
+
refreshToken() {
|
|
160
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
161
|
+
return this.tokenManager.refreshToken();
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
/**
|
|
165
|
+
* Logout - revoke tokens and clear storage
|
|
166
|
+
*/
|
|
167
|
+
logout() {
|
|
168
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
169
|
+
const state = this.tokenManager.getState();
|
|
170
|
+
const tokens = state.tokens;
|
|
171
|
+
// Try to revoke tokens on the server
|
|
172
|
+
if (tokens === null || tokens === void 0 ? void 0 : tokens.accessToken) {
|
|
173
|
+
try {
|
|
174
|
+
yield this.revokeToken(tokens.accessToken, "access_token");
|
|
175
|
+
}
|
|
176
|
+
catch (_a) {
|
|
177
|
+
// Ignore revocation errors - we're logging out anyway
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
// Clear local tokens
|
|
181
|
+
yield this.tokenManager.clearTokens();
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
/**
|
|
185
|
+
* Get current access token if valid
|
|
186
|
+
*/
|
|
187
|
+
getAccessToken() {
|
|
188
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
189
|
+
return this.tokenManager.getAccessToken();
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
/**
|
|
193
|
+
* Get current authentication state
|
|
194
|
+
*/
|
|
195
|
+
getAuthState() {
|
|
196
|
+
return this.tokenManager.getState();
|
|
197
|
+
}
|
|
198
|
+
/**
|
|
199
|
+
* Subscribe to authentication state changes
|
|
200
|
+
*/
|
|
201
|
+
subscribe(listener) {
|
|
202
|
+
return this.tokenManager.subscribe(listener);
|
|
203
|
+
}
|
|
204
|
+
/**
|
|
205
|
+
* Check if user is authenticated
|
|
206
|
+
*/
|
|
207
|
+
isAuthenticated() {
|
|
208
|
+
return this.tokenManager.getState().isAuthenticated;
|
|
209
|
+
}
|
|
210
|
+
/**
|
|
211
|
+
* Clean up resources
|
|
212
|
+
*/
|
|
213
|
+
destroy() {
|
|
214
|
+
this.tokenManager.destroy();
|
|
215
|
+
}
|
|
216
|
+
// Private methods
|
|
217
|
+
exchangeCode(code, codeVerifier, redirectUri) {
|
|
218
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
219
|
+
var _a, _b;
|
|
220
|
+
const body = new URLSearchParams({
|
|
221
|
+
grant_type: "authorization_code",
|
|
222
|
+
code,
|
|
223
|
+
redirect_uri: redirectUri !== null && redirectUri !== void 0 ? redirectUri : this.config.redirectUri,
|
|
224
|
+
client_id: this.config.clientId,
|
|
225
|
+
code_verifier: codeVerifier,
|
|
226
|
+
});
|
|
227
|
+
const response = yield fetch(this.config.tokenEndpoint, {
|
|
228
|
+
method: "POST",
|
|
229
|
+
headers: {
|
|
230
|
+
"Content-Type": "application/x-www-form-urlencoded",
|
|
231
|
+
},
|
|
232
|
+
body: body.toString(),
|
|
233
|
+
});
|
|
234
|
+
if (!response.ok) {
|
|
235
|
+
const error = yield response.json().catch(() => ({}));
|
|
236
|
+
throw {
|
|
237
|
+
error: (_a = error.error) !== null && _a !== void 0 ? _a : "token_error",
|
|
238
|
+
errorDescription: (_b = error.error_description) !== null && _b !== void 0 ? _b : "Failed to exchange authorization code",
|
|
239
|
+
};
|
|
240
|
+
}
|
|
241
|
+
const data = yield response.json();
|
|
242
|
+
return this.parseTokenResponse(data);
|
|
243
|
+
});
|
|
244
|
+
}
|
|
245
|
+
refreshTokenWithApi(refreshToken) {
|
|
246
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
247
|
+
var _a;
|
|
248
|
+
const body = new URLSearchParams({
|
|
249
|
+
grant_type: "refresh_token",
|
|
250
|
+
refresh_token: refreshToken,
|
|
251
|
+
client_id: this.config.clientId,
|
|
252
|
+
});
|
|
253
|
+
const response = yield fetch(this.config.tokenEndpoint, {
|
|
254
|
+
method: "POST",
|
|
255
|
+
headers: {
|
|
256
|
+
"Content-Type": "application/x-www-form-urlencoded",
|
|
257
|
+
},
|
|
258
|
+
body: body.toString(),
|
|
259
|
+
});
|
|
260
|
+
if (!response.ok) {
|
|
261
|
+
const error = yield response.json().catch(() => ({}));
|
|
262
|
+
throw new Error((_a = error.error) !== null && _a !== void 0 ? _a : "refresh_failed");
|
|
263
|
+
}
|
|
264
|
+
const data = yield response.json();
|
|
265
|
+
return this.parseTokenResponse(data);
|
|
266
|
+
});
|
|
267
|
+
}
|
|
268
|
+
revokeToken(token, tokenTypeHint) {
|
|
269
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
270
|
+
if (!this.config.revocationEndpoint) {
|
|
271
|
+
return;
|
|
272
|
+
}
|
|
273
|
+
const body = new URLSearchParams({
|
|
274
|
+
token,
|
|
275
|
+
client_id: this.config.clientId,
|
|
276
|
+
});
|
|
277
|
+
if (tokenTypeHint) {
|
|
278
|
+
body.set("token_type_hint", tokenTypeHint);
|
|
279
|
+
}
|
|
280
|
+
yield fetch(this.config.revocationEndpoint, {
|
|
281
|
+
method: "POST",
|
|
282
|
+
headers: {
|
|
283
|
+
"Content-Type": "application/x-www-form-urlencoded",
|
|
284
|
+
},
|
|
285
|
+
body: body.toString(),
|
|
286
|
+
});
|
|
287
|
+
// RFC 7009: revocation endpoint returns 200 even on error
|
|
288
|
+
// We don't need to check the response
|
|
289
|
+
});
|
|
290
|
+
}
|
|
291
|
+
parseTokenResponse(data) {
|
|
292
|
+
var _a, _b;
|
|
293
|
+
const accessToken = data.access_token;
|
|
294
|
+
const expiresIn = (_a = data.expires_in) !== null && _a !== void 0 ? _a : 3600;
|
|
295
|
+
const refreshToken = data.refresh_token;
|
|
296
|
+
const scope = data.scope;
|
|
297
|
+
const tokenType = (_b = data.token_type) !== null && _b !== void 0 ? _b : "Bearer";
|
|
298
|
+
if (!accessToken) {
|
|
299
|
+
throw {
|
|
300
|
+
error: "invalid_response",
|
|
301
|
+
errorDescription: "Missing access_token in response",
|
|
302
|
+
};
|
|
303
|
+
}
|
|
304
|
+
return {
|
|
305
|
+
accessToken,
|
|
306
|
+
refreshToken,
|
|
307
|
+
expiresAt: Date.now() + expiresIn * 1000,
|
|
308
|
+
scope: scope === null || scope === void 0 ? void 0 : scope.split(" "),
|
|
309
|
+
tokenType,
|
|
310
|
+
};
|
|
311
|
+
}
|
|
312
|
+
storePkceState(state) {
|
|
313
|
+
if (typeof window !== "undefined" && window.sessionStorage) {
|
|
314
|
+
sessionStorage.setItem(PKCE_STATE_KEY, JSON.stringify(state));
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
getPkceState() {
|
|
318
|
+
if (typeof window === "undefined" || !window.sessionStorage) {
|
|
319
|
+
return null;
|
|
320
|
+
}
|
|
321
|
+
try {
|
|
322
|
+
const stored = sessionStorage.getItem(PKCE_STATE_KEY);
|
|
323
|
+
return stored ? JSON.parse(stored) : null;
|
|
324
|
+
}
|
|
325
|
+
catch (_a) {
|
|
326
|
+
return null;
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
clearPkceState() {
|
|
330
|
+
if (typeof window !== "undefined" && window.sessionStorage) {
|
|
331
|
+
sessionStorage.removeItem(PKCE_STATE_KEY);
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
exports.OAuthAdapter = OAuthAdapter;
|
|
336
|
+
/**
|
|
337
|
+
* Create an OAuth adapter with default configuration for Crimson API
|
|
338
|
+
*/
|
|
339
|
+
function createCrimsonOAuthAdapter(config) {
|
|
340
|
+
return new OAuthAdapter(Object.assign(Object.assign({}, config), { authorizationEndpoint: `${config.apiUrl}/oauth/authorize`, tokenEndpoint: `${config.apiUrl}/oauth/token`, revocationEndpoint: `${config.apiUrl}/oauth/revoke` }));
|
|
341
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PKCE (Proof Key for Code Exchange) Utilities
|
|
3
|
+
* Client-side implementation for OAuth 2.0 with PKCE
|
|
4
|
+
*/
|
|
5
|
+
/**
|
|
6
|
+
* Generate a code verifier for PKCE
|
|
7
|
+
* @returns A 64-character random string
|
|
8
|
+
*/
|
|
9
|
+
export declare function generateCodeVerifier(): string;
|
|
10
|
+
/**
|
|
11
|
+
* Generate a state parameter for CSRF protection
|
|
12
|
+
* @returns A 32-character random string
|
|
13
|
+
*/
|
|
14
|
+
export declare function generateState(): string;
|
|
15
|
+
/**
|
|
16
|
+
* Generate a code challenge from a code verifier using SHA256 (S256 method)
|
|
17
|
+
* @param codeVerifier The code verifier
|
|
18
|
+
* @returns Base64URL-encoded SHA256 hash
|
|
19
|
+
*/
|
|
20
|
+
export declare function generateCodeChallenge(codeVerifier: string): Promise<string>;
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* PKCE (Proof Key for Code Exchange) Utilities
|
|
4
|
+
* Client-side implementation for OAuth 2.0 with PKCE
|
|
5
|
+
*/
|
|
6
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
7
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
8
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
9
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
10
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
11
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
12
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
13
|
+
});
|
|
14
|
+
};
|
|
15
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
16
|
+
exports.generateCodeVerifier = generateCodeVerifier;
|
|
17
|
+
exports.generateState = generateState;
|
|
18
|
+
exports.generateCodeChallenge = generateCodeChallenge;
|
|
19
|
+
/**
|
|
20
|
+
* Generate a cryptographically secure random string
|
|
21
|
+
*/
|
|
22
|
+
function generateRandomString(length) {
|
|
23
|
+
const charset = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-._~";
|
|
24
|
+
const randomValues = new Uint8Array(length);
|
|
25
|
+
if (typeof window !== "undefined" && window.crypto) {
|
|
26
|
+
window.crypto.getRandomValues(randomValues);
|
|
27
|
+
}
|
|
28
|
+
else if (typeof globalThis !== "undefined" && globalThis.crypto) {
|
|
29
|
+
globalThis.crypto.getRandomValues(randomValues);
|
|
30
|
+
}
|
|
31
|
+
else {
|
|
32
|
+
// Fallback for Node.js environments
|
|
33
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
34
|
+
const nodeCrypto = require("crypto");
|
|
35
|
+
const buffer = nodeCrypto.randomBytes(length);
|
|
36
|
+
for (let i = 0; i < length; i++) {
|
|
37
|
+
randomValues[i] = buffer[i];
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
let result = "";
|
|
41
|
+
for (let i = 0; i < length; i++) {
|
|
42
|
+
result += charset[randomValues[i] % charset.length];
|
|
43
|
+
}
|
|
44
|
+
return result;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Generate a code verifier for PKCE
|
|
48
|
+
* @returns A 64-character random string
|
|
49
|
+
*/
|
|
50
|
+
function generateCodeVerifier() {
|
|
51
|
+
return generateRandomString(64);
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Generate a state parameter for CSRF protection
|
|
55
|
+
* @returns A 32-character random string
|
|
56
|
+
*/
|
|
57
|
+
function generateState() {
|
|
58
|
+
return generateRandomString(32);
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Generate a code challenge from a code verifier using SHA256 (S256 method)
|
|
62
|
+
* @param codeVerifier The code verifier
|
|
63
|
+
* @returns Base64URL-encoded SHA256 hash
|
|
64
|
+
*/
|
|
65
|
+
function generateCodeChallenge(codeVerifier) {
|
|
66
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
67
|
+
var _a, _b;
|
|
68
|
+
// Convert string to Uint8Array
|
|
69
|
+
const encoder = new TextEncoder();
|
|
70
|
+
const data = encoder.encode(codeVerifier);
|
|
71
|
+
// Hash with SHA-256
|
|
72
|
+
let hashBuffer;
|
|
73
|
+
if (typeof window !== "undefined" && ((_a = window.crypto) === null || _a === void 0 ? void 0 : _a.subtle)) {
|
|
74
|
+
hashBuffer = yield window.crypto.subtle.digest("SHA-256", data);
|
|
75
|
+
}
|
|
76
|
+
else if (typeof globalThis !== "undefined" && ((_b = globalThis.crypto) === null || _b === void 0 ? void 0 : _b.subtle)) {
|
|
77
|
+
hashBuffer = yield globalThis.crypto.subtle.digest("SHA-256", data);
|
|
78
|
+
}
|
|
79
|
+
else {
|
|
80
|
+
// Fallback for Node.js environments
|
|
81
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
82
|
+
const nodeCrypto = require("crypto");
|
|
83
|
+
const hash = nodeCrypto.createHash("sha256").update(codeVerifier).digest();
|
|
84
|
+
hashBuffer = hash.buffer.slice(hash.byteOffset, hash.byteOffset + hash.byteLength);
|
|
85
|
+
}
|
|
86
|
+
// Convert to base64url encoding
|
|
87
|
+
return base64UrlEncode(new Uint8Array(hashBuffer));
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Base64URL encode a buffer
|
|
92
|
+
* @param buffer The buffer to encode
|
|
93
|
+
* @returns Base64URL-encoded string (no padding)
|
|
94
|
+
*/
|
|
95
|
+
function base64UrlEncode(buffer) {
|
|
96
|
+
// Convert to base64
|
|
97
|
+
let base64 = "";
|
|
98
|
+
const bytes = new Uint8Array(buffer);
|
|
99
|
+
for (let i = 0; i < bytes.byteLength; i++) {
|
|
100
|
+
base64 += String.fromCharCode(bytes[i]);
|
|
101
|
+
}
|
|
102
|
+
// Use btoa if available (browser), otherwise Buffer (Node.js)
|
|
103
|
+
let encoded;
|
|
104
|
+
if (typeof btoa !== "undefined") {
|
|
105
|
+
encoded = btoa(base64);
|
|
106
|
+
}
|
|
107
|
+
else {
|
|
108
|
+
encoded = Buffer.from(base64, "binary").toString("base64");
|
|
109
|
+
}
|
|
110
|
+
// Convert to base64url (no padding)
|
|
111
|
+
return encoded.replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
112
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Token Manager
|
|
3
|
+
* Handles token lifecycle, automatic refresh, and state management
|
|
4
|
+
*/
|
|
5
|
+
import { OAuthTokens, TokenStorage, OAuthAuthState, OAuthAuthStateListener, TokenRefreshFunction } from "./types";
|
|
6
|
+
export interface TokenManagerConfig {
|
|
7
|
+
/** Token storage implementation */
|
|
8
|
+
storage: TokenStorage;
|
|
9
|
+
/** Function to call when tokens need refreshing */
|
|
10
|
+
onRefreshNeeded: TokenRefreshFunction;
|
|
11
|
+
/** Buffer time before expiration to trigger refresh (default: 5 minutes) */
|
|
12
|
+
refreshBufferMs?: number;
|
|
13
|
+
/** Enable automatic background refresh (default: true) */
|
|
14
|
+
autoRefresh?: boolean;
|
|
15
|
+
}
|
|
16
|
+
export declare class TokenManager {
|
|
17
|
+
private storage;
|
|
18
|
+
private onRefreshNeeded;
|
|
19
|
+
private refreshBufferMs;
|
|
20
|
+
private autoRefresh;
|
|
21
|
+
private listeners;
|
|
22
|
+
private refreshPromise;
|
|
23
|
+
private refreshTimer;
|
|
24
|
+
private lastRefreshAttempt;
|
|
25
|
+
private currentState;
|
|
26
|
+
constructor(config: TokenManagerConfig);
|
|
27
|
+
/**
|
|
28
|
+
* Initialize the token manager
|
|
29
|
+
* Loads tokens from storage and sets up auto-refresh if enabled
|
|
30
|
+
*/
|
|
31
|
+
initialize(): Promise<void>;
|
|
32
|
+
/**
|
|
33
|
+
* Get current authentication state
|
|
34
|
+
*/
|
|
35
|
+
getState(): OAuthAuthState;
|
|
36
|
+
/**
|
|
37
|
+
* Get current access token if valid
|
|
38
|
+
* Automatically refreshes if needed and possible
|
|
39
|
+
*/
|
|
40
|
+
getAccessToken(): Promise<string | null>;
|
|
41
|
+
/**
|
|
42
|
+
* Set new tokens (after successful authorization)
|
|
43
|
+
*/
|
|
44
|
+
setTokens(tokens: OAuthTokens): Promise<void>;
|
|
45
|
+
/**
|
|
46
|
+
* Refresh the access token
|
|
47
|
+
* Uses a lock to prevent concurrent refresh attempts
|
|
48
|
+
*/
|
|
49
|
+
refreshToken(): Promise<OAuthTokens | null>;
|
|
50
|
+
private doRefresh;
|
|
51
|
+
/**
|
|
52
|
+
* Clear tokens (logout)
|
|
53
|
+
*/
|
|
54
|
+
clearTokens(): Promise<void>;
|
|
55
|
+
/**
|
|
56
|
+
* Subscribe to auth state changes
|
|
57
|
+
*/
|
|
58
|
+
subscribe(listener: OAuthAuthStateListener): () => void;
|
|
59
|
+
/**
|
|
60
|
+
* Clean up resources
|
|
61
|
+
*/
|
|
62
|
+
destroy(): void;
|
|
63
|
+
private updateState;
|
|
64
|
+
private isTokenExpired;
|
|
65
|
+
private shouldRefresh;
|
|
66
|
+
private scheduleRefresh;
|
|
67
|
+
private cancelScheduledRefresh;
|
|
68
|
+
}
|