@imtbl/auth 2.10.7-alpha.6 → 2.11.1-alpha.1
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/.eslintrc.cjs +1 -1
- package/dist/browser/index.js +29 -31
- package/dist/node/index.cjs +26 -29
- package/dist/node/index.js +19 -22
- package/dist/types/Auth.d.ts +1 -1
- package/dist/types/errors.d.ts +2 -1
- package/dist/types/index.d.ts +2 -1
- package/dist/types/types.d.ts +2 -0
- package/dist/types/utils/jwt.d.ts +1 -0
- package/dist/types/utils/typedEventEmitter.d.ts +7 -4
- package/jest.config.ts +27 -0
- package/jest.setup.js +4 -0
- package/package.json +12 -9
- package/src/Auth.test.ts +188 -0
- package/src/Auth.ts +83 -67
- package/src/errors.test.ts +21 -0
- package/src/errors.ts +35 -4
- package/src/index.ts +5 -1
- package/src/storage/device_credentials_manager.ts +2 -2
- package/src/types.ts +2 -0
- package/src/utils/jwt.ts +78 -0
- package/src/utils/token.ts +2 -2
- package/src/utils/typedEventEmitter.ts +36 -11
- package/tsconfig.eslint.json +6 -0
- package/tsconfig.json +4 -0
package/src/Auth.ts
CHANGED
|
@@ -7,8 +7,6 @@ import {
|
|
|
7
7
|
UserManagerSettings,
|
|
8
8
|
WebStorageStateStore,
|
|
9
9
|
} from 'oidc-client-ts';
|
|
10
|
-
import axios from 'axios';
|
|
11
|
-
import jwt_decode from 'jwt-decode';
|
|
12
10
|
import localForage from 'localforage';
|
|
13
11
|
import {
|
|
14
12
|
Detail,
|
|
@@ -35,6 +33,7 @@ import {
|
|
|
35
33
|
import EmbeddedLoginPrompt from './login/embeddedLoginPrompt';
|
|
36
34
|
import TypedEventEmitter from './utils/typedEventEmitter';
|
|
37
35
|
import { withMetricsAsync } from './utils/metrics';
|
|
36
|
+
import { decodeJwtPayload } from './utils/jwt';
|
|
38
37
|
import DeviceCredentialsManager from './storage/device_credentials_manager';
|
|
39
38
|
import { PassportError, PassportErrorType, withPassportError } from './errors';
|
|
40
39
|
import logger from './utils/logger';
|
|
@@ -42,12 +41,37 @@ import { isAccessTokenExpiredOrExpiring } from './utils/token';
|
|
|
42
41
|
import LoginPopupOverlay from './overlay/loginPopupOverlay';
|
|
43
42
|
import { LocalForageAsyncStorage } from './storage/LocalForageAsyncStorage';
|
|
44
43
|
|
|
45
|
-
const
|
|
44
|
+
const formUrlEncodedHeaders = {
|
|
45
|
+
'Content-Type': 'application/x-www-form-urlencoded',
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
const parseJsonSafely = (text: string): unknown => {
|
|
49
|
+
if (!text) {
|
|
50
|
+
return undefined;
|
|
51
|
+
}
|
|
52
|
+
try {
|
|
53
|
+
return JSON.parse(text);
|
|
54
|
+
} catch {
|
|
55
|
+
return undefined;
|
|
56
|
+
}
|
|
57
|
+
};
|
|
46
58
|
|
|
47
|
-
const
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
59
|
+
const extractTokenErrorMessage = (
|
|
60
|
+
payload: unknown,
|
|
61
|
+
fallbackText: string,
|
|
62
|
+
status: number,
|
|
63
|
+
): string => {
|
|
64
|
+
if (payload && typeof payload === 'object') {
|
|
65
|
+
const data = payload as Record<string, unknown>;
|
|
66
|
+
const description = data.error_description ?? data.message ?? data.error;
|
|
67
|
+
if (typeof description === 'string' && description.trim().length > 0) {
|
|
68
|
+
return description;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
if (fallbackText.trim().length > 0) {
|
|
72
|
+
return fallbackText;
|
|
73
|
+
}
|
|
74
|
+
return `Token request failed with status ${status}`;
|
|
51
75
|
};
|
|
52
76
|
|
|
53
77
|
const logoutEndpoint = '/v2/logout';
|
|
@@ -206,8 +230,7 @@ export class Auth {
|
|
|
206
230
|
|
|
207
231
|
// Emit LOGGED_IN event and identify user if logged in
|
|
208
232
|
if (user) {
|
|
209
|
-
this.
|
|
210
|
-
identify({ passportId: user.profile.sub });
|
|
233
|
+
this.handleSuccessfulLogin(user);
|
|
211
234
|
}
|
|
212
235
|
|
|
213
236
|
return user;
|
|
@@ -233,8 +256,7 @@ export class Auth {
|
|
|
233
256
|
return withMetricsAsync(async () => {
|
|
234
257
|
const user = await this.loginCallbackInternal();
|
|
235
258
|
if (user) {
|
|
236
|
-
this.
|
|
237
|
-
identify({ passportId: user.profile.sub });
|
|
259
|
+
this.handleSuccessfulLogin(user);
|
|
238
260
|
}
|
|
239
261
|
return user;
|
|
240
262
|
}, 'loginCallback');
|
|
@@ -256,7 +278,7 @@ export class Auth {
|
|
|
256
278
|
* @returns Promise that resolves with the user or null if not authenticated
|
|
257
279
|
*/
|
|
258
280
|
async getUser(): Promise<User | null> {
|
|
259
|
-
return
|
|
281
|
+
return this.getUserInternal();
|
|
260
282
|
}
|
|
261
283
|
|
|
262
284
|
/**
|
|
@@ -275,7 +297,9 @@ export class Auth {
|
|
|
275
297
|
return user;
|
|
276
298
|
}
|
|
277
299
|
|
|
278
|
-
|
|
300
|
+
const loggedInUser = await this.loginWithPopup();
|
|
301
|
+
this.handleSuccessfulLogin(loggedInUser);
|
|
302
|
+
return loggedInUser;
|
|
279
303
|
}
|
|
280
304
|
|
|
281
305
|
/**
|
|
@@ -354,8 +378,7 @@ export class Auth {
|
|
|
354
378
|
async loginWithPKCEFlowCallback(authorizationCode: string, state: string): Promise<User> {
|
|
355
379
|
return withMetricsAsync(async () => {
|
|
356
380
|
const user = await this.loginWithPKCEFlowCallbackInternal(authorizationCode, state);
|
|
357
|
-
this.
|
|
358
|
-
identify({ passportId: user.profile.sub });
|
|
381
|
+
this.handleSuccessfulLogin(user);
|
|
359
382
|
return user;
|
|
360
383
|
}, 'loginWithPKCEFlowCallback');
|
|
361
384
|
}
|
|
@@ -368,8 +391,7 @@ export class Auth {
|
|
|
368
391
|
async storeTokens(tokenResponse: DeviceTokenResponse): Promise<User> {
|
|
369
392
|
return withMetricsAsync(async () => {
|
|
370
393
|
const user = await this.storeTokensInternal(tokenResponse);
|
|
371
|
-
this.
|
|
372
|
-
identify({ passportId: user.profile.sub });
|
|
394
|
+
this.handleSuccessfulLogin(user);
|
|
373
395
|
return user;
|
|
374
396
|
}, 'storeTokens');
|
|
375
397
|
}
|
|
@@ -413,6 +435,11 @@ export class Auth {
|
|
|
413
435
|
return this.config.oidcConfiguration.clientId;
|
|
414
436
|
}
|
|
415
437
|
|
|
438
|
+
private handleSuccessfulLogin(user: User): void {
|
|
439
|
+
this.eventEmitter.emit(AuthEvents.LOGGED_IN, user);
|
|
440
|
+
identify({ passportId: user.profile.sub });
|
|
441
|
+
}
|
|
442
|
+
|
|
416
443
|
private buildExtraQueryParams(
|
|
417
444
|
directLoginOptions?: DirectLoginOptions,
|
|
418
445
|
imPassportTraceId?: string,
|
|
@@ -420,7 +447,6 @@ export class Auth {
|
|
|
420
447
|
const params: Record<string, string> = {
|
|
421
448
|
...(this.userManager.settings?.extraQueryParams ?? {}),
|
|
422
449
|
rid: getDetail(Detail.RUNTIME_ID) || '',
|
|
423
|
-
third_party_a_id: '',
|
|
424
450
|
};
|
|
425
451
|
|
|
426
452
|
if (directLoginOptions) {
|
|
@@ -472,7 +498,7 @@ export class Auth {
|
|
|
472
498
|
const signinPopup = async () => {
|
|
473
499
|
const extraQueryParams = this.buildExtraQueryParams(directLoginOptionsToUse, imPassportTraceId);
|
|
474
500
|
|
|
475
|
-
|
|
501
|
+
return this.userManager.signinPopup({
|
|
476
502
|
extraQueryParams,
|
|
477
503
|
popupWindowFeatures: {
|
|
478
504
|
width: 410,
|
|
@@ -480,27 +506,6 @@ export class Auth {
|
|
|
480
506
|
},
|
|
481
507
|
popupWindowTarget,
|
|
482
508
|
});
|
|
483
|
-
|
|
484
|
-
const popupRef = window.open('', popupWindowTarget);
|
|
485
|
-
if (popupRef) {
|
|
486
|
-
const popupClosedPromise = new Promise<never>((_, reject) => {
|
|
487
|
-
const timer = setInterval(() => {
|
|
488
|
-
if (popupRef.closed) {
|
|
489
|
-
clearInterval(timer);
|
|
490
|
-
reject(new Error('Popup closed by user'));
|
|
491
|
-
}
|
|
492
|
-
}, LOGIN_POPUP_CLOSED_POLLING_DURATION);
|
|
493
|
-
|
|
494
|
-
userPromise.finally(() => {
|
|
495
|
-
clearInterval(timer);
|
|
496
|
-
popupRef.close();
|
|
497
|
-
});
|
|
498
|
-
});
|
|
499
|
-
|
|
500
|
-
return Promise.race([userPromise, popupClosedPromise]);
|
|
501
|
-
}
|
|
502
|
-
|
|
503
|
-
return userPromise;
|
|
504
509
|
};
|
|
505
510
|
|
|
506
511
|
return new Promise((resolve, reject) => {
|
|
@@ -542,8 +547,13 @@ export class Auth {
|
|
|
542
547
|
|
|
543
548
|
private static mapOidcUserToDomainModel = (oidcUser: OidcUser): User => {
|
|
544
549
|
let passport: PassportMetadata | undefined;
|
|
550
|
+
let username: string | undefined;
|
|
545
551
|
if (oidcUser.id_token) {
|
|
546
|
-
|
|
552
|
+
const idTokenPayload = decodeJwtPayload<IdTokenPayload>(oidcUser.id_token);
|
|
553
|
+
passport = idTokenPayload?.passport;
|
|
554
|
+
if (idTokenPayload?.username) {
|
|
555
|
+
username = idTokenPayload?.username;
|
|
556
|
+
}
|
|
547
557
|
}
|
|
548
558
|
|
|
549
559
|
const user: User = {
|
|
@@ -555,6 +565,7 @@ export class Auth {
|
|
|
555
565
|
sub: oidcUser.profile.sub,
|
|
556
566
|
email: oidcUser.profile.email,
|
|
557
567
|
nickname: oidcUser.profile.nickname,
|
|
568
|
+
username,
|
|
558
569
|
},
|
|
559
570
|
};
|
|
560
571
|
if (passport?.zkevm_eth_address && passport?.zkevm_user_admin_address) {
|
|
@@ -567,7 +578,7 @@ export class Auth {
|
|
|
567
578
|
};
|
|
568
579
|
|
|
569
580
|
private static mapDeviceTokenResponseToOidcUser = (tokenResponse: DeviceTokenResponse): OidcUser => {
|
|
570
|
-
const idTokenPayload: IdTokenPayload =
|
|
581
|
+
const idTokenPayload: IdTokenPayload = decodeJwtPayload(tokenResponse.id_token);
|
|
571
582
|
return new OidcUser({
|
|
572
583
|
id_token: tokenResponse.id_token,
|
|
573
584
|
access_token: tokenResponse.access_token,
|
|
@@ -582,29 +593,13 @@ export class Auth {
|
|
|
582
593
|
email: idTokenPayload.email,
|
|
583
594
|
nickname: idTokenPayload.nickname,
|
|
584
595
|
passport: idTokenPayload.passport,
|
|
596
|
+
...(idTokenPayload.username ? { username: idTokenPayload.username } : {}),
|
|
585
597
|
},
|
|
586
598
|
});
|
|
587
599
|
};
|
|
588
600
|
|
|
589
|
-
private static shouldUseSigninPopupCallback(): boolean {
|
|
590
|
-
try {
|
|
591
|
-
const urlParams = new URLSearchParams(window.location.search);
|
|
592
|
-
const stateParam = urlParams.get('state');
|
|
593
|
-
const localStorageKey = `oidc.${stateParam}`;
|
|
594
|
-
const localStorageValue = localStorage.getItem(localStorageKey);
|
|
595
|
-
const loginState = JSON.parse(localStorageValue || '{}');
|
|
596
|
-
return loginState?.request_type === 'si:p';
|
|
597
|
-
} catch (err) {
|
|
598
|
-
return false;
|
|
599
|
-
}
|
|
600
|
-
}
|
|
601
|
-
|
|
602
601
|
private async loginCallbackInternal(): Promise<User | undefined> {
|
|
603
602
|
return withPassportError(async () => {
|
|
604
|
-
if (Auth.shouldUseSigninPopupCallback()) {
|
|
605
|
-
await this.userManager.signinPopupCallback(undefined, true);
|
|
606
|
-
return undefined;
|
|
607
|
-
}
|
|
608
603
|
const oidcUser = await this.userManager.signinCallback();
|
|
609
604
|
if (!oidcUser) {
|
|
610
605
|
return undefined;
|
|
@@ -681,18 +676,39 @@ export class Auth {
|
|
|
681
676
|
}
|
|
682
677
|
|
|
683
678
|
private async getPKCEToken(authorizationCode: string, codeVerifier: string): Promise<DeviceTokenResponse> {
|
|
684
|
-
const response = await
|
|
679
|
+
const response = await fetch(
|
|
685
680
|
`${this.config.authenticationDomain}/oauth/token`,
|
|
686
681
|
{
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
682
|
+
method: 'POST',
|
|
683
|
+
headers: formUrlEncodedHeaders,
|
|
684
|
+
body: new URLSearchParams({
|
|
685
|
+
client_id: this.config.oidcConfiguration.clientId,
|
|
686
|
+
grant_type: 'authorization_code',
|
|
687
|
+
code_verifier: codeVerifier,
|
|
688
|
+
code: authorizationCode,
|
|
689
|
+
redirect_uri: this.config.oidcConfiguration.redirectUri,
|
|
690
|
+
}),
|
|
692
691
|
},
|
|
693
|
-
formUrlEncodedHeader,
|
|
694
692
|
);
|
|
695
|
-
|
|
693
|
+
|
|
694
|
+
const responseText = await response.text();
|
|
695
|
+
const parsedBody = parseJsonSafely(responseText);
|
|
696
|
+
|
|
697
|
+
if (!response.ok) {
|
|
698
|
+
throw new Error(
|
|
699
|
+
extractTokenErrorMessage(
|
|
700
|
+
parsedBody,
|
|
701
|
+
responseText,
|
|
702
|
+
response.status,
|
|
703
|
+
),
|
|
704
|
+
);
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
if (!parsedBody || typeof parsedBody !== 'object') {
|
|
708
|
+
throw new Error('Token endpoint returned an invalid response');
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
return parsedBody as DeviceTokenResponse;
|
|
696
712
|
}
|
|
697
713
|
|
|
698
714
|
private async storeTokensInternal(tokenResponse: DeviceTokenResponse): Promise<User> {
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { isAPIError } from './errors';
|
|
2
|
+
|
|
3
|
+
describe('isAPIError', () => {
|
|
4
|
+
it('returns true when code and message fields exist', () => {
|
|
5
|
+
expect(isAPIError({ code: 'BAD_REQUEST', message: 'Invalid' })).toBe(true);
|
|
6
|
+
});
|
|
7
|
+
|
|
8
|
+
it.each([
|
|
9
|
+
'Not found',
|
|
10
|
+
404,
|
|
11
|
+
null,
|
|
12
|
+
undefined,
|
|
13
|
+
])('returns false for non-object value: %p', (value) => {
|
|
14
|
+
expect(isAPIError(value)).toBe(false);
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
it('returns false when required fields are missing', () => {
|
|
18
|
+
expect(isAPIError({ code: 'BAD_REQUEST' })).toBe(false);
|
|
19
|
+
expect(isAPIError({ message: 'Invalid' })).toBe(false);
|
|
20
|
+
});
|
|
21
|
+
});
|
package/src/errors.ts
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { isAxiosError } from 'axios';
|
|
2
1
|
import { imx } from '@imtbl/generated-clients';
|
|
3
2
|
|
|
4
3
|
export enum PassportErrorType {
|
|
@@ -23,12 +22,43 @@ export enum PassportErrorType {
|
|
|
23
22
|
LINK_WALLET_DUPLICATE_NONCE_ERROR = 'LINK_WALLET_DUPLICATE_NONCE_ERROR',
|
|
24
23
|
LINK_WALLET_GENERIC_ERROR = 'LINK_WALLET_GENERIC_ERROR',
|
|
25
24
|
SERVICE_UNAVAILABLE_ERROR = 'SERVICE_UNAVAILABLE_ERROR',
|
|
25
|
+
TRANSACTION_REJECTED = 'TRANSACTION_REJECTED',
|
|
26
26
|
}
|
|
27
27
|
|
|
28
28
|
export function isAPIError(error: any): error is imx.APIError {
|
|
29
|
-
return
|
|
29
|
+
return (
|
|
30
|
+
typeof error === 'object'
|
|
31
|
+
&& error !== null
|
|
32
|
+
&& 'code' in error
|
|
33
|
+
&& 'message' in error
|
|
34
|
+
);
|
|
30
35
|
}
|
|
31
36
|
|
|
37
|
+
type AxiosLikeError = {
|
|
38
|
+
response?: {
|
|
39
|
+
data?: unknown;
|
|
40
|
+
};
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
const extractApiError = (error: unknown): imx.APIError | undefined => {
|
|
44
|
+
if (isAPIError(error)) {
|
|
45
|
+
return error;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
if (
|
|
49
|
+
typeof error === 'object'
|
|
50
|
+
&& error !== null
|
|
51
|
+
&& 'response' in error
|
|
52
|
+
) {
|
|
53
|
+
const { response } = error as AxiosLikeError;
|
|
54
|
+
if (response?.data && isAPIError(response.data)) {
|
|
55
|
+
return response.data;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return undefined;
|
|
60
|
+
};
|
|
61
|
+
|
|
32
62
|
export class PassportError extends Error {
|
|
33
63
|
public type: PassportErrorType;
|
|
34
64
|
|
|
@@ -51,8 +81,9 @@ export const withPassportError = async <T>(
|
|
|
51
81
|
throw new PassportError(error.message, error.type);
|
|
52
82
|
}
|
|
53
83
|
|
|
54
|
-
|
|
55
|
-
|
|
84
|
+
const apiError = extractApiError(error);
|
|
85
|
+
if (apiError) {
|
|
86
|
+
errorMessage = apiError.message;
|
|
56
87
|
} else {
|
|
57
88
|
errorMessage = (error as Error).message;
|
|
58
89
|
}
|
package/src/index.ts
CHANGED
|
@@ -29,4 +29,8 @@ export {
|
|
|
29
29
|
export { default as TypedEventEmitter } from './utils/typedEventEmitter';
|
|
30
30
|
|
|
31
31
|
// Export errors
|
|
32
|
-
export {
|
|
32
|
+
export {
|
|
33
|
+
PassportError, PassportErrorType, withPassportError, isAPIError,
|
|
34
|
+
} from './errors';
|
|
35
|
+
|
|
36
|
+
export { decodeJwtPayload } from './utils/jwt';
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/* eslint-disable class-methods-use-this */
|
|
2
|
-
import jwt_decode from 'jwt-decode';
|
|
3
2
|
import { TokenPayload, PKCEData } from '../types';
|
|
3
|
+
import { decodeJwtPayload } from '../utils/jwt';
|
|
4
4
|
|
|
5
5
|
const KEY_PKCE_STATE = 'pkce_state';
|
|
6
6
|
const KEY_PKCE_VERIFIER = 'pkce_verifier';
|
|
@@ -9,7 +9,7 @@ const validCredentialsMinTtlSec = 3600; // 1 hour
|
|
|
9
9
|
export default class DeviceCredentialsManager {
|
|
10
10
|
private isTokenValid(jwt: string): boolean {
|
|
11
11
|
try {
|
|
12
|
-
const tokenPayload: TokenPayload =
|
|
12
|
+
const tokenPayload: TokenPayload = decodeJwtPayload(jwt);
|
|
13
13
|
const expiresAt = tokenPayload.exp ?? 0;
|
|
14
14
|
const now = (Date.now() / 1000) + validCredentialsMinTtlSec;
|
|
15
15
|
return expiresAt > now;
|
package/src/types.ts
CHANGED
|
@@ -9,6 +9,7 @@ export type UserProfile = {
|
|
|
9
9
|
email?: string;
|
|
10
10
|
nickname?: string;
|
|
11
11
|
sub: string;
|
|
12
|
+
username?: string;
|
|
12
13
|
};
|
|
13
14
|
|
|
14
15
|
export enum RollupType {
|
|
@@ -91,6 +92,7 @@ export type TokenPayload = {
|
|
|
91
92
|
|
|
92
93
|
export type IdTokenPayload = {
|
|
93
94
|
passport?: PassportMetadata;
|
|
95
|
+
username?: string;
|
|
94
96
|
email: string;
|
|
95
97
|
nickname: string;
|
|
96
98
|
aud: string;
|
package/src/utils/jwt.ts
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/* eslint-disable no-restricted-globals */
|
|
2
|
+
const getGlobal = (): typeof globalThis => {
|
|
3
|
+
if (typeof globalThis !== 'undefined') {
|
|
4
|
+
return globalThis;
|
|
5
|
+
}
|
|
6
|
+
if (typeof self !== 'undefined') {
|
|
7
|
+
return self;
|
|
8
|
+
}
|
|
9
|
+
if (typeof window !== 'undefined') {
|
|
10
|
+
return window;
|
|
11
|
+
}
|
|
12
|
+
if (typeof global !== 'undefined') {
|
|
13
|
+
return global;
|
|
14
|
+
}
|
|
15
|
+
return {} as typeof globalThis;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
const base64UrlToBase64 = (input: string): string => {
|
|
19
|
+
const normalized = input.replace(/-/g, '+').replace(/_/g, '/');
|
|
20
|
+
const padding = normalized.length % 4 === 0 ? '' : '='.repeat(4 - (normalized.length % 4));
|
|
21
|
+
return normalized + padding;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
const decodeWithAtob = (value: string): string | null => {
|
|
25
|
+
const globalRef = getGlobal();
|
|
26
|
+
if (typeof globalRef.atob !== 'function') {
|
|
27
|
+
return null;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const binary = globalRef.atob(value);
|
|
31
|
+
const bytes = new Uint8Array(binary.length);
|
|
32
|
+
for (let i = 0; i < binary.length; i += 1) {
|
|
33
|
+
bytes[i] = binary.charCodeAt(i);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
if (typeof globalRef.TextDecoder === 'function') {
|
|
37
|
+
return new globalRef.TextDecoder('utf-8').decode(bytes);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
let result = '';
|
|
41
|
+
for (let i = 0; i < bytes.length; i += 1) {
|
|
42
|
+
result += String.fromCharCode(bytes[i]);
|
|
43
|
+
}
|
|
44
|
+
return result;
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
const base64Decode = (value: string): string => {
|
|
48
|
+
if (typeof Buffer !== 'undefined') {
|
|
49
|
+
return Buffer.from(value, 'base64').toString('utf-8');
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const decoded = decodeWithAtob(value);
|
|
53
|
+
if (decoded === null) {
|
|
54
|
+
throw new Error('Base64 decoding is not supported in this environment');
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return decoded;
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
export const decodeJwtPayload = <T>(token: string): T => {
|
|
61
|
+
if (typeof token !== 'string') {
|
|
62
|
+
throw new Error('JWT must be a string');
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const segments = token.split('.');
|
|
66
|
+
if (segments.length < 2) {
|
|
67
|
+
throw new Error('Invalid JWT: payload segment is missing');
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const payloadSegment = segments[1];
|
|
71
|
+
const json = base64Decode(base64UrlToBase64(payloadSegment));
|
|
72
|
+
|
|
73
|
+
try {
|
|
74
|
+
return JSON.parse(json) as T;
|
|
75
|
+
} catch {
|
|
76
|
+
throw new Error('Invalid JWT payload: unable to parse JSON');
|
|
77
|
+
}
|
|
78
|
+
};
|
package/src/utils/token.ts
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
|
-
import jwt_decode from 'jwt-decode';
|
|
2
1
|
import {
|
|
3
2
|
User as OidcUser,
|
|
4
3
|
} from 'oidc-client-ts';
|
|
5
4
|
import { IdTokenPayload, TokenPayload } from '../types';
|
|
5
|
+
import { decodeJwtPayload } from './jwt';
|
|
6
6
|
|
|
7
7
|
function isTokenExpiredOrExpiring(token: string): boolean {
|
|
8
8
|
try {
|
|
9
9
|
// try to decode the token as access token payload or id token payload
|
|
10
|
-
const decodedToken =
|
|
10
|
+
const decodedToken = decodeJwtPayload<TokenPayload | IdTokenPayload>(token);
|
|
11
11
|
const now = Math.floor(Date.now() / 1000);
|
|
12
12
|
|
|
13
13
|
// Tokens without expiration claims are invalid (security vulnerability)
|
|
@@ -1,26 +1,51 @@
|
|
|
1
|
-
|
|
1
|
+
type StringEventKey<T> = Extract<keyof T, string>;
|
|
2
|
+
|
|
3
|
+
type AnyListener = (...args: any[]) => void;
|
|
4
|
+
type EventArgs<TEvents, TEventName extends keyof TEvents> =
|
|
5
|
+
TEvents[TEventName] extends readonly [...infer A]
|
|
6
|
+
? [...A]
|
|
7
|
+
: TEvents[TEventName] extends readonly any[]
|
|
8
|
+
? [...TEvents[TEventName]]
|
|
9
|
+
: [TEvents[TEventName]];
|
|
2
10
|
|
|
3
11
|
export default class TypedEventEmitter<TEvents extends Record<string, any>> {
|
|
4
|
-
private
|
|
12
|
+
private listeners = new Map<StringEventKey<TEvents>, Set<AnyListener>>();
|
|
5
13
|
|
|
6
|
-
emit<TEventName extends
|
|
14
|
+
emit<TEventName extends StringEventKey<TEvents>>(
|
|
7
15
|
eventName: TEventName,
|
|
8
|
-
...eventArg: TEvents
|
|
16
|
+
...eventArg: EventArgs<TEvents, TEventName>
|
|
9
17
|
) {
|
|
10
|
-
this.
|
|
18
|
+
const handlers = this.listeners.get(eventName);
|
|
19
|
+
if (!handlers || handlers.size === 0) {
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// Copy handlers to avoid issues if listeners mutate during emit
|
|
24
|
+
[...handlers].forEach((handler) => {
|
|
25
|
+
handler(...eventArg);
|
|
26
|
+
});
|
|
11
27
|
}
|
|
12
28
|
|
|
13
|
-
on<TEventName extends
|
|
29
|
+
on<TEventName extends StringEventKey<TEvents>>(
|
|
14
30
|
eventName: TEventName,
|
|
15
|
-
handler: (...eventArg: TEvents
|
|
31
|
+
handler: (...eventArg: EventArgs<TEvents, TEventName>) => void,
|
|
16
32
|
) {
|
|
17
|
-
this.
|
|
33
|
+
const handlers = this.listeners.get(eventName) ?? new Set<AnyListener>();
|
|
34
|
+
handlers.add(handler as AnyListener);
|
|
35
|
+
this.listeners.set(eventName, handlers);
|
|
18
36
|
}
|
|
19
37
|
|
|
20
|
-
removeListener<TEventName extends
|
|
38
|
+
removeListener<TEventName extends StringEventKey<TEvents>>(
|
|
21
39
|
eventName: TEventName,
|
|
22
|
-
handler: (...eventArg: TEvents
|
|
40
|
+
handler: (...eventArg: EventArgs<TEvents, TEventName>) => void,
|
|
23
41
|
) {
|
|
24
|
-
this.
|
|
42
|
+
const handlers = this.listeners.get(eventName);
|
|
43
|
+
if (!handlers) {
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
handlers.delete(handler as AnyListener);
|
|
47
|
+
if (handlers.size === 0) {
|
|
48
|
+
this.listeners.delete(eventName);
|
|
49
|
+
}
|
|
25
50
|
}
|
|
26
51
|
}
|