@imtbl/auth 2.12.5-alpha.9 → 2.12.6-alpha.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 +163 -0
- package/dist/browser/index.js +79 -27
- package/dist/node/index.cjs +97 -40
- package/dist/node/index.js +80 -28
- package/dist/types/index.d.ts +3 -2
- package/dist/types/login/standalone.d.ts +141 -0
- package/dist/types/types.d.ts +26 -5
- package/package.json +6 -6
- package/src/Auth.test.ts +99 -18
- package/src/Auth.ts +21 -36
- package/src/index.ts +22 -1
- package/src/login/standalone.ts +745 -0
- package/src/types.ts +30 -5
package/src/Auth.test.ts
CHANGED
|
@@ -137,7 +137,7 @@ describe('Auth', () => {
|
|
|
137
137
|
});
|
|
138
138
|
});
|
|
139
139
|
|
|
140
|
-
describe('
|
|
140
|
+
describe('mapOidcUserToDomainModel', () => {
|
|
141
141
|
it('extracts username from id token when present', () => {
|
|
142
142
|
const mockOidcUser = {
|
|
143
143
|
id_token: 'token',
|
|
@@ -158,6 +158,88 @@ describe('Auth', () => {
|
|
|
158
158
|
expect(result.profile.username).toEqual('username123');
|
|
159
159
|
});
|
|
160
160
|
|
|
161
|
+
it('extracts zkEvm chain data from passport metadata', () => {
|
|
162
|
+
const mockOidcUser = {
|
|
163
|
+
id_token: 'token',
|
|
164
|
+
access_token: 'access',
|
|
165
|
+
refresh_token: 'refresh',
|
|
166
|
+
expired: false,
|
|
167
|
+
profile: { sub: 'user-123', email: 'test@example.com', nickname: 'tester' },
|
|
168
|
+
};
|
|
169
|
+
|
|
170
|
+
(decodeJwtPayload as jest.Mock).mockReturnValue({
|
|
171
|
+
passport: {
|
|
172
|
+
zkevm_eth_address: '0xzkevmaddress',
|
|
173
|
+
zkevm_user_admin_address: '0xzkevmadmin',
|
|
174
|
+
},
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
const result = (Auth as any).mapOidcUserToDomainModel(mockOidcUser);
|
|
178
|
+
|
|
179
|
+
expect(result.zkEvm).toEqual({
|
|
180
|
+
ethAddress: '0xzkevmaddress',
|
|
181
|
+
userAdminAddress: '0xzkevmadmin',
|
|
182
|
+
});
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
it('extracts arbitrum_one chain data from nested passport metadata', () => {
|
|
186
|
+
const mockOidcUser = {
|
|
187
|
+
id_token: 'token',
|
|
188
|
+
access_token: 'access',
|
|
189
|
+
refresh_token: 'refresh',
|
|
190
|
+
expired: false,
|
|
191
|
+
profile: { sub: 'user-123', email: 'test@example.com', nickname: 'tester' },
|
|
192
|
+
};
|
|
193
|
+
|
|
194
|
+
(decodeJwtPayload as jest.Mock).mockReturnValue({
|
|
195
|
+
passport: {
|
|
196
|
+
arbitrum_one: {
|
|
197
|
+
eth_address: '0xarbaddress',
|
|
198
|
+
user_admin_address: '0xarbadmin',
|
|
199
|
+
},
|
|
200
|
+
},
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
const result = (Auth as any).mapOidcUserToDomainModel(mockOidcUser);
|
|
204
|
+
|
|
205
|
+
expect(result.arbitrum_one).toEqual({
|
|
206
|
+
ethAddress: '0xarbaddress',
|
|
207
|
+
userAdminAddress: '0xarbadmin',
|
|
208
|
+
});
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
it('extracts both zkEvm and arbitrum_one when present', () => {
|
|
212
|
+
const mockOidcUser = {
|
|
213
|
+
id_token: 'token',
|
|
214
|
+
access_token: 'access',
|
|
215
|
+
refresh_token: 'refresh',
|
|
216
|
+
expired: false,
|
|
217
|
+
profile: { sub: 'user-123', email: 'test@example.com', nickname: 'tester' },
|
|
218
|
+
};
|
|
219
|
+
|
|
220
|
+
(decodeJwtPayload as jest.Mock).mockReturnValue({
|
|
221
|
+
passport: {
|
|
222
|
+
zkevm_eth_address: '0xzkevmaddress',
|
|
223
|
+
zkevm_user_admin_address: '0xzkevmadmin',
|
|
224
|
+
arbitrum_one: {
|
|
225
|
+
eth_address: '0xarbaddress',
|
|
226
|
+
user_admin_address: '0xarbadmin',
|
|
227
|
+
},
|
|
228
|
+
},
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
const result = (Auth as any).mapOidcUserToDomainModel(mockOidcUser);
|
|
232
|
+
|
|
233
|
+
expect(result.zkEvm).toEqual({
|
|
234
|
+
ethAddress: '0xzkevmaddress',
|
|
235
|
+
userAdminAddress: '0xzkevmadmin',
|
|
236
|
+
});
|
|
237
|
+
expect(result.arbitrum_one).toEqual({
|
|
238
|
+
ethAddress: '0xarbaddress',
|
|
239
|
+
userAdminAddress: '0xarbadmin',
|
|
240
|
+
});
|
|
241
|
+
});
|
|
242
|
+
|
|
161
243
|
it('maps username when creating OIDC user from device tokens', () => {
|
|
162
244
|
const tokenResponse = {
|
|
163
245
|
id_token: 'token',
|
|
@@ -241,7 +323,7 @@ describe('Auth', () => {
|
|
|
241
323
|
expect(mockEventEmitter.emit).not.toHaveBeenCalled();
|
|
242
324
|
});
|
|
243
325
|
|
|
244
|
-
it('emits USER_REMOVED event
|
|
326
|
+
it('emits USER_REMOVED event for invalid_grant error', async () => {
|
|
245
327
|
const auth = Object.create(Auth.prototype) as Auth;
|
|
246
328
|
const mockEventEmitter = { emit: jest.fn() };
|
|
247
329
|
const mockUserManager = {
|
|
@@ -271,13 +353,13 @@ describe('Auth', () => {
|
|
|
271
353
|
expect(mockEventEmitter.emit).toHaveBeenCalledWith(
|
|
272
354
|
AuthEvents.USER_REMOVED,
|
|
273
355
|
expect.objectContaining({
|
|
274
|
-
reason: '
|
|
356
|
+
reason: 'refresh_failed',
|
|
275
357
|
}),
|
|
276
358
|
);
|
|
277
359
|
expect(mockUserManager.removeUser).toHaveBeenCalled();
|
|
278
360
|
});
|
|
279
361
|
|
|
280
|
-
it('emits USER_REMOVED event for login_required error
|
|
362
|
+
it('emits USER_REMOVED event for login_required error', async () => {
|
|
281
363
|
const auth = Object.create(Auth.prototype) as Auth;
|
|
282
364
|
const mockEventEmitter = { emit: jest.fn() };
|
|
283
365
|
const mockUserManager = {
|
|
@@ -298,17 +380,16 @@ describe('Auth', () => {
|
|
|
298
380
|
|
|
299
381
|
await expect((auth as any).refreshTokenAndUpdatePromise()).rejects.toThrow();
|
|
300
382
|
|
|
301
|
-
// login_required is a permanent error - should remove user
|
|
302
383
|
expect(mockEventEmitter.emit).toHaveBeenCalledWith(
|
|
303
384
|
AuthEvents.USER_REMOVED,
|
|
304
385
|
expect.objectContaining({
|
|
305
|
-
reason: '
|
|
386
|
+
reason: 'refresh_failed',
|
|
306
387
|
}),
|
|
307
388
|
);
|
|
308
389
|
expect(mockUserManager.removeUser).toHaveBeenCalled();
|
|
309
390
|
});
|
|
310
391
|
|
|
311
|
-
it('
|
|
392
|
+
it('emits USER_REMOVED event for network errors', async () => {
|
|
312
393
|
const auth = Object.create(Auth.prototype) as Auth;
|
|
313
394
|
const mockEventEmitter = { emit: jest.fn() };
|
|
314
395
|
const mockUserManager = {
|
|
@@ -322,15 +403,16 @@ describe('Auth', () => {
|
|
|
322
403
|
|
|
323
404
|
await expect((auth as any).refreshTokenAndUpdatePromise()).rejects.toThrow();
|
|
324
405
|
|
|
325
|
-
|
|
326
|
-
expect(mockEventEmitter.emit).not.toHaveBeenCalledWith(
|
|
406
|
+
expect(mockEventEmitter.emit).toHaveBeenCalledWith(
|
|
327
407
|
AuthEvents.USER_REMOVED,
|
|
328
|
-
expect.
|
|
408
|
+
expect.objectContaining({
|
|
409
|
+
reason: 'refresh_failed',
|
|
410
|
+
}),
|
|
329
411
|
);
|
|
330
|
-
expect(mockUserManager.removeUser).
|
|
412
|
+
expect(mockUserManager.removeUser).toHaveBeenCalled();
|
|
331
413
|
});
|
|
332
414
|
|
|
333
|
-
it('
|
|
415
|
+
it('emits USER_REMOVED event for server_error OAuth error', async () => {
|
|
334
416
|
const auth = Object.create(Auth.prototype) as Auth;
|
|
335
417
|
const mockEventEmitter = { emit: jest.fn() };
|
|
336
418
|
const mockUserManager = {
|
|
@@ -338,8 +420,6 @@ describe('Auth', () => {
|
|
|
338
420
|
removeUser: jest.fn().mockResolvedValue(undefined),
|
|
339
421
|
};
|
|
340
422
|
|
|
341
|
-
// Mock ErrorResponse with a transient error (server_error)
|
|
342
|
-
// These are temporary server issues - safe to keep user logged in
|
|
343
423
|
const { ErrorResponse } = jest.requireActual('oidc-client-ts');
|
|
344
424
|
const errorResponse = new ErrorResponse({
|
|
345
425
|
error: 'server_error',
|
|
@@ -353,12 +433,13 @@ describe('Auth', () => {
|
|
|
353
433
|
|
|
354
434
|
await expect((auth as any).refreshTokenAndUpdatePromise()).rejects.toThrow();
|
|
355
435
|
|
|
356
|
-
|
|
357
|
-
expect(mockEventEmitter.emit).not.toHaveBeenCalledWith(
|
|
436
|
+
expect(mockEventEmitter.emit).toHaveBeenCalledWith(
|
|
358
437
|
AuthEvents.USER_REMOVED,
|
|
359
|
-
expect.
|
|
438
|
+
expect.objectContaining({
|
|
439
|
+
reason: 'refresh_failed',
|
|
440
|
+
}),
|
|
360
441
|
);
|
|
361
|
-
expect(mockUserManager.removeUser).
|
|
442
|
+
expect(mockUserManager.removeUser).toHaveBeenCalled();
|
|
362
443
|
});
|
|
363
444
|
|
|
364
445
|
it('emits USER_REMOVED event for unknown errors (safer default)', async () => {
|
package/src/Auth.ts
CHANGED
|
@@ -29,6 +29,8 @@ import {
|
|
|
29
29
|
PassportMetadata,
|
|
30
30
|
IdTokenPayload,
|
|
31
31
|
isUserZkEvm,
|
|
32
|
+
EvmChain,
|
|
33
|
+
ChainAddress,
|
|
32
34
|
} from './types';
|
|
33
35
|
import EmbeddedLoginPrompt from './login/embeddedLoginPrompt';
|
|
34
36
|
import TypedEventEmitter from './utils/typedEventEmitter';
|
|
@@ -74,6 +76,11 @@ const extractTokenErrorMessage = (
|
|
|
74
76
|
return `Token request failed with status ${status}`;
|
|
75
77
|
};
|
|
76
78
|
|
|
79
|
+
const toChainAddress = (ethAddress: string, userAdminAddress: string): ChainAddress => ({
|
|
80
|
+
ethAddress: ethAddress as `0x${string}`,
|
|
81
|
+
userAdminAddress: userAdminAddress as `0x${string}`,
|
|
82
|
+
});
|
|
83
|
+
|
|
77
84
|
const logoutEndpoint = '/v2/logout';
|
|
78
85
|
const crossSdkBridgeLogoutEndpoint = '/im-logged-out';
|
|
79
86
|
const authorizeEndpoint = '/authorize';
|
|
@@ -570,12 +577,19 @@ export class Auth {
|
|
|
570
577
|
username,
|
|
571
578
|
},
|
|
572
579
|
};
|
|
580
|
+
|
|
573
581
|
if (passport?.zkevm_eth_address && passport?.zkevm_user_admin_address) {
|
|
574
|
-
user.zkEvm =
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
582
|
+
user.zkEvm = toChainAddress(passport.zkevm_eth_address, passport.zkevm_user_admin_address);
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
const chains = Object.values(EvmChain).filter((chain) => chain !== EvmChain.ZKEVM);
|
|
586
|
+
for (const chain of chains) {
|
|
587
|
+
const chainMetadata = passport?.[chain as Exclude<EvmChain, EvmChain.ZKEVM>];
|
|
588
|
+
if (chainMetadata?.eth_address && chainMetadata?.user_admin_address) {
|
|
589
|
+
user[chain] = toChainAddress(chainMetadata.eth_address, chainMetadata.user_admin_address);
|
|
590
|
+
}
|
|
578
591
|
}
|
|
592
|
+
|
|
579
593
|
return user;
|
|
580
594
|
};
|
|
581
595
|
|
|
@@ -766,7 +780,7 @@ export class Auth {
|
|
|
766
780
|
const newOidcUser = await this.userManager.signinSilent();
|
|
767
781
|
if (newOidcUser) {
|
|
768
782
|
const user = Auth.mapOidcUserToDomainModel(newOidcUser);
|
|
769
|
-
// Emit TOKEN_REFRESHED event so consumers (e.g., auth-
|
|
783
|
+
// Emit TOKEN_REFRESHED event so consumers (e.g., auth-next-client) can sync
|
|
770
784
|
// the new tokens to their session. This is critical for refresh token
|
|
771
785
|
// rotation - without this, the server-side session may have stale tokens.
|
|
772
786
|
this.eventEmitter.emit(AuthEvents.TOKEN_REFRESHED, user);
|
|
@@ -780,53 +794,24 @@ export class Auth {
|
|
|
780
794
|
// Default to REMOVING user - safer to log out on unknown errors
|
|
781
795
|
// Only keep user logged in for explicitly known transient errors
|
|
782
796
|
let removeUser = true;
|
|
783
|
-
let removeReason: 'refresh_token_invalid' | 'refresh_failed' | 'unknown' = 'unknown';
|
|
784
|
-
|
|
785
797
|
if (err instanceof ErrorTimeout) {
|
|
786
|
-
// Timeout is transient - safe to keep user logged in
|
|
787
|
-
// Note: removeReason is set but never used since removeUser=false
|
|
788
798
|
passportErrorType = PassportErrorType.SILENT_LOGIN_ERROR;
|
|
789
799
|
errorMessage = `${errorMessage}: ${err.message}`;
|
|
790
800
|
removeUser = false;
|
|
791
801
|
} else if (err instanceof ErrorResponse) {
|
|
792
802
|
passportErrorType = PassportErrorType.NOT_LOGGED_IN_ERROR;
|
|
793
803
|
errorMessage = `${errorMessage}: ${err.message || err.error_description}`;
|
|
794
|
-
// Check for known transient OAuth errors - safe to keep user logged in
|
|
795
|
-
// - server_error: auth server temporary issue
|
|
796
|
-
// - temporarily_unavailable: auth server overloaded
|
|
797
|
-
const transientErrors = ['server_error', 'temporarily_unavailable'];
|
|
798
|
-
if (err.error && transientErrors.includes(err.error)) {
|
|
799
|
-
removeUser = false;
|
|
800
|
-
removeReason = 'refresh_failed';
|
|
801
|
-
} else {
|
|
802
|
-
// All other OAuth errors (invalid_grant, login_required, etc.) are permanent
|
|
803
|
-
removeReason = 'refresh_token_invalid';
|
|
804
|
-
}
|
|
805
804
|
} else if (err instanceof Error) {
|
|
806
805
|
errorMessage = `${errorMessage}: ${err.message}`;
|
|
807
|
-
// Network/fetch errors are transient - safe to keep user logged in
|
|
808
|
-
const isNetworkError = err.message.toLowerCase().includes('network')
|
|
809
|
-
|| err.message.toLowerCase().includes('fetch')
|
|
810
|
-
|| err.message.toLowerCase().includes('failed to fetch')
|
|
811
|
-
|| err.message.toLowerCase().includes('networkerror');
|
|
812
|
-
if (isNetworkError) {
|
|
813
|
-
// Note: removeReason is not set since removeUser=false (event won't be emitted)
|
|
814
|
-
removeUser = false;
|
|
815
|
-
} else {
|
|
816
|
-
// Unknown errors - safer to remove user
|
|
817
|
-
removeReason = 'refresh_failed';
|
|
818
|
-
}
|
|
819
806
|
} else if (typeof err === 'string') {
|
|
820
807
|
errorMessage = `${errorMessage}: ${err}`;
|
|
821
|
-
// Unknown string error - safer to remove user
|
|
822
|
-
removeReason = 'refresh_failed';
|
|
823
808
|
}
|
|
824
809
|
|
|
825
810
|
if (removeUser) {
|
|
826
811
|
// Emit USER_REMOVED event BEFORE removing user so consumers can react
|
|
827
|
-
// (e.g., auth-
|
|
812
|
+
// (e.g., auth-next-client can clear the NextAuth session)
|
|
828
813
|
this.eventEmitter.emit(AuthEvents.USER_REMOVED, {
|
|
829
|
-
reason:
|
|
814
|
+
reason: 'refresh_failed',
|
|
830
815
|
error: errorMessage,
|
|
831
816
|
});
|
|
832
817
|
|
package/src/index.ts
CHANGED
|
@@ -17,13 +17,20 @@ export type {
|
|
|
17
17
|
AuthModuleConfiguration,
|
|
18
18
|
PopupOverlayOptions,
|
|
19
19
|
PassportMetadata,
|
|
20
|
+
PassportChainMetadata,
|
|
21
|
+
ChainAddress,
|
|
22
|
+
ZkEvmInfo,
|
|
20
23
|
IdTokenPayload,
|
|
21
24
|
PKCEData,
|
|
22
25
|
AuthEventMap,
|
|
23
26
|
UserRemovedReason,
|
|
24
27
|
} from './types';
|
|
25
28
|
export {
|
|
26
|
-
isUserZkEvm,
|
|
29
|
+
isUserZkEvm,
|
|
30
|
+
RollupType,
|
|
31
|
+
EvmChain,
|
|
32
|
+
MarketingConsentStatus,
|
|
33
|
+
AuthEvents,
|
|
27
34
|
} from './types';
|
|
28
35
|
|
|
29
36
|
// Export TypedEventEmitter
|
|
@@ -35,3 +42,17 @@ export {
|
|
|
35
42
|
} from './errors';
|
|
36
43
|
|
|
37
44
|
export { decodeJwtPayload } from './utils/jwt';
|
|
45
|
+
|
|
46
|
+
// ============================================================================
|
|
47
|
+
// Standalone Login Functions (stateless, for use with NextAuth or similar)
|
|
48
|
+
// ============================================================================
|
|
49
|
+
|
|
50
|
+
export {
|
|
51
|
+
loginWithPopup,
|
|
52
|
+
loginWithEmbedded,
|
|
53
|
+
loginWithRedirect,
|
|
54
|
+
handleLoginCallback,
|
|
55
|
+
type LoginConfig,
|
|
56
|
+
type TokenResponse,
|
|
57
|
+
type StandaloneLoginOptions,
|
|
58
|
+
} from './login/standalone';
|