@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/src/Auth.test.ts CHANGED
@@ -137,7 +137,7 @@ describe('Auth', () => {
137
137
  });
138
138
  });
139
139
 
140
- describe('username extraction', () => {
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 ONLY for invalid_grant error (refresh token invalid)', async () => {
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: 'refresh_token_invalid',
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 (permanent)', async () => {
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: 'refresh_token_invalid',
386
+ reason: 'refresh_failed',
306
387
  }),
307
388
  );
308
389
  expect(mockUserManager.removeUser).toHaveBeenCalled();
309
390
  });
310
391
 
311
- it('does not emit USER_REMOVED event for network errors (transient)', async () => {
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
- // Network errors are transient - should NOT remove user or emit USER_REMOVED
326
- expect(mockEventEmitter.emit).not.toHaveBeenCalledWith(
406
+ expect(mockEventEmitter.emit).toHaveBeenCalledWith(
327
407
  AuthEvents.USER_REMOVED,
328
- expect.anything(),
408
+ expect.objectContaining({
409
+ reason: 'refresh_failed',
410
+ }),
329
411
  );
330
- expect(mockUserManager.removeUser).not.toHaveBeenCalled();
412
+ expect(mockUserManager.removeUser).toHaveBeenCalled();
331
413
  });
332
414
 
333
- it('does not emit USER_REMOVED event for transient OAuth errors (server_error)', async () => {
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
- // server_error is a transient error - should NOT remove user
357
- expect(mockEventEmitter.emit).not.toHaveBeenCalledWith(
436
+ expect(mockEventEmitter.emit).toHaveBeenCalledWith(
358
437
  AuthEvents.USER_REMOVED,
359
- expect.anything(),
438
+ expect.objectContaining({
439
+ reason: 'refresh_failed',
440
+ }),
360
441
  );
361
- expect(mockUserManager.removeUser).not.toHaveBeenCalled();
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
- ethAddress: passport.zkevm_eth_address,
576
- userAdminAddress: passport.zkevm_user_admin_address,
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-nextjs) can sync
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-nextjs can clear the NextAuth session)
812
+ // (e.g., auth-next-client can clear the NextAuth session)
828
813
  this.eventEmitter.emit(AuthEvents.USER_REMOVED, {
829
- reason: removeReason,
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, RollupType, MarketingConsentStatus, AuthEvents,
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';