@imtbl/auth 2.12.5-alpha.16 → 2.12.5-alpha.18

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,256 +158,113 @@ describe('Auth', () => {
158
158
  expect(result.profile.username).toEqual('username123');
159
159
  });
160
160
 
161
- it('maps username when creating OIDC user from device tokens', () => {
162
- const tokenResponse = {
161
+ it('extracts zkEvm chain data from passport metadata', () => {
162
+ const mockOidcUser = {
163
163
  id_token: 'token',
164
164
  access_token: 'access',
165
165
  refresh_token: 'refresh',
166
- token_type: 'Bearer',
167
- expires_in: 3600,
166
+ expired: false,
167
+ profile: { sub: 'user-123', email: 'test@example.com', nickname: 'tester' },
168
168
  };
169
169
 
170
170
  (decodeJwtPayload as jest.Mock).mockReturnValue({
171
- sub: 'user-123',
172
- iss: 'issuer',
173
- aud: 'audience',
174
- exp: 1,
175
- iat: 0,
176
- email: 'test@example.com',
177
- nickname: 'tester',
178
- username: 'username123',
179
- passport: undefined,
171
+ passport: {
172
+ zkevm_eth_address: '0xzkevmaddress',
173
+ zkevm_user_admin_address: '0xzkevmadmin',
174
+ },
180
175
  });
181
176
 
182
- const oidcUser = (Auth as any).mapDeviceTokenResponseToOidcUser(tokenResponse);
177
+ const result = (Auth as any).mapOidcUserToDomainModel(mockOidcUser);
183
178
 
184
- expect(decodeJwtPayload).toHaveBeenCalledWith('token');
185
- expect(oidcUser.profile.username).toEqual('username123');
179
+ expect(result.zkEvm).toEqual({
180
+ ethAddress: '0xzkevmaddress',
181
+ userAdminAddress: '0xzkevmadmin',
182
+ });
186
183
  });
187
- });
188
184
 
189
- describe('refreshTokenAndUpdatePromise', () => {
190
- it('emits TOKEN_REFRESHED event when signinSilent succeeds', async () => {
185
+ it('extracts arbitrum_one chain data from nested passport metadata', () => {
191
186
  const mockOidcUser = {
192
- id_token: 'new-id',
193
- access_token: 'new-access',
194
- refresh_token: 'new-refresh',
187
+ id_token: 'token',
188
+ access_token: 'access',
189
+ refresh_token: 'refresh',
195
190
  expired: false,
196
191
  profile: { sub: 'user-123', email: 'test@example.com', nickname: 'tester' },
197
192
  };
198
193
 
199
194
  (decodeJwtPayload as jest.Mock).mockReturnValue({
200
- username: undefined,
201
- passport: undefined,
195
+ passport: {
196
+ arbitrum_one: {
197
+ eth_address: '0xarbaddress',
198
+ user_admin_address: '0xarbadmin',
199
+ },
200
+ },
202
201
  });
203
202
 
204
- const auth = Object.create(Auth.prototype) as Auth;
205
- const mockEventEmitter = { emit: jest.fn() };
206
- const mockUserManager = {
207
- signinSilent: jest.fn().mockResolvedValue(mockOidcUser),
208
- };
209
-
210
- (auth as any).eventEmitter = mockEventEmitter;
211
- (auth as any).userManager = mockUserManager;
212
- (auth as any).refreshingPromise = null;
213
-
214
- const user = await (auth as any).refreshTokenAndUpdatePromise();
215
-
216
- expect(user).toBeDefined();
217
- expect(user.accessToken).toBe('new-access');
218
- expect(mockEventEmitter.emit).toHaveBeenCalledWith(
219
- AuthEvents.TOKEN_REFRESHED,
220
- expect.objectContaining({
221
- accessToken: 'new-access',
222
- refreshToken: 'new-refresh',
223
- }),
224
- );
225
- });
226
-
227
- it('does not emit TOKEN_REFRESHED event when signinSilent returns null', async () => {
228
- const auth = Object.create(Auth.prototype) as Auth;
229
- const mockEventEmitter = { emit: jest.fn() };
230
- const mockUserManager = {
231
- signinSilent: jest.fn().mockResolvedValue(null),
232
- };
233
-
234
- (auth as any).eventEmitter = mockEventEmitter;
235
- (auth as any).userManager = mockUserManager;
236
- (auth as any).refreshingPromise = null;
237
-
238
- const user = await (auth as any).refreshTokenAndUpdatePromise();
239
-
240
- expect(user).toBeNull();
241
- expect(mockEventEmitter.emit).not.toHaveBeenCalled();
242
- });
243
-
244
- it('emits USER_REMOVED event for invalid_grant error', async () => {
245
- const auth = Object.create(Auth.prototype) as Auth;
246
- const mockEventEmitter = { emit: jest.fn() };
247
- const mockUserManager = {
248
- signinSilent: jest.fn().mockRejectedValue(
249
- Object.assign(new Error('invalid_grant'), {
250
- error: 'invalid_grant',
251
- error_description: 'Unknown or invalid refresh token',
252
- }),
253
- ),
254
- removeUser: jest.fn().mockResolvedValue(undefined),
255
- };
203
+ const result = (Auth as any).mapOidcUserToDomainModel(mockOidcUser);
256
204
 
257
- // Make the error an instance of ErrorResponse
258
- const { ErrorResponse } = jest.requireActual('oidc-client-ts');
259
- const errorResponse = new ErrorResponse({
260
- error: 'invalid_grant',
261
- error_description: 'Unknown or invalid refresh token',
205
+ expect(result.arbitrum_one).toEqual({
206
+ ethAddress: '0xarbaddress',
207
+ userAdminAddress: '0xarbadmin',
262
208
  });
263
- mockUserManager.signinSilent.mockRejectedValue(errorResponse);
264
-
265
- (auth as any).eventEmitter = mockEventEmitter;
266
- (auth as any).userManager = mockUserManager;
267
- (auth as any).refreshingPromise = null;
268
-
269
- await expect((auth as any).refreshTokenAndUpdatePromise()).rejects.toThrow();
270
-
271
- expect(mockEventEmitter.emit).toHaveBeenCalledWith(
272
- AuthEvents.USER_REMOVED,
273
- expect.objectContaining({
274
- reason: 'refresh_failed',
275
- }),
276
- );
277
- expect(mockUserManager.removeUser).toHaveBeenCalled();
278
209
  });
279
210
 
280
- it('emits USER_REMOVED event for login_required error', async () => {
281
- const auth = Object.create(Auth.prototype) as Auth;
282
- const mockEventEmitter = { emit: jest.fn() };
283
- const mockUserManager = {
284
- signinSilent: jest.fn(),
285
- removeUser: jest.fn().mockResolvedValue(undefined),
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' },
286
218
  };
287
219
 
288
- const { ErrorResponse } = jest.requireActual('oidc-client-ts');
289
- const errorResponse = new ErrorResponse({
290
- error: 'login_required',
291
- error_description: 'User must re-authenticate',
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
+ },
292
229
  });
293
- mockUserManager.signinSilent.mockRejectedValue(errorResponse);
294
230
 
295
- (auth as any).eventEmitter = mockEventEmitter;
296
- (auth as any).userManager = mockUserManager;
297
- (auth as any).refreshingPromise = null;
298
-
299
- await expect((auth as any).refreshTokenAndUpdatePromise()).rejects.toThrow();
300
-
301
- expect(mockEventEmitter.emit).toHaveBeenCalledWith(
302
- AuthEvents.USER_REMOVED,
303
- expect.objectContaining({
304
- reason: 'refresh_failed',
305
- }),
306
- );
307
- expect(mockUserManager.removeUser).toHaveBeenCalled();
308
- });
309
-
310
- it('emits USER_REMOVED event for network errors', async () => {
311
- const auth = Object.create(Auth.prototype) as Auth;
312
- const mockEventEmitter = { emit: jest.fn() };
313
- const mockUserManager = {
314
- signinSilent: jest.fn().mockRejectedValue(new Error('Network error: Failed to fetch')),
315
- removeUser: jest.fn().mockResolvedValue(undefined),
316
- };
317
-
318
- (auth as any).eventEmitter = mockEventEmitter;
319
- (auth as any).userManager = mockUserManager;
320
- (auth as any).refreshingPromise = null;
321
-
322
- await expect((auth as any).refreshTokenAndUpdatePromise()).rejects.toThrow();
323
-
324
- expect(mockEventEmitter.emit).toHaveBeenCalledWith(
325
- AuthEvents.USER_REMOVED,
326
- expect.objectContaining({
327
- reason: 'refresh_failed',
328
- }),
329
- );
330
- expect(mockUserManager.removeUser).toHaveBeenCalled();
331
- });
332
-
333
- it('emits USER_REMOVED event for server_error OAuth error', async () => {
334
- const auth = Object.create(Auth.prototype) as Auth;
335
- const mockEventEmitter = { emit: jest.fn() };
336
- const mockUserManager = {
337
- signinSilent: jest.fn(),
338
- removeUser: jest.fn().mockResolvedValue(undefined),
339
- };
231
+ const result = (Auth as any).mapOidcUserToDomainModel(mockOidcUser);
340
232
 
341
- const { ErrorResponse } = jest.requireActual('oidc-client-ts');
342
- const errorResponse = new ErrorResponse({
343
- error: 'server_error',
344
- error_description: 'Internal server error',
233
+ expect(result.zkEvm).toEqual({
234
+ ethAddress: '0xzkevmaddress',
235
+ userAdminAddress: '0xzkevmadmin',
236
+ });
237
+ expect(result.arbitrum_one).toEqual({
238
+ ethAddress: '0xarbaddress',
239
+ userAdminAddress: '0xarbadmin',
345
240
  });
346
- mockUserManager.signinSilent.mockRejectedValue(errorResponse);
347
-
348
- (auth as any).eventEmitter = mockEventEmitter;
349
- (auth as any).userManager = mockUserManager;
350
- (auth as any).refreshingPromise = null;
351
-
352
- await expect((auth as any).refreshTokenAndUpdatePromise()).rejects.toThrow();
353
-
354
- expect(mockEventEmitter.emit).toHaveBeenCalledWith(
355
- AuthEvents.USER_REMOVED,
356
- expect.objectContaining({
357
- reason: 'refresh_failed',
358
- }),
359
- );
360
- expect(mockUserManager.removeUser).toHaveBeenCalled();
361
- });
362
-
363
- it('emits USER_REMOVED event for unknown errors (safer default)', async () => {
364
- const auth = Object.create(Auth.prototype) as Auth;
365
- const mockEventEmitter = { emit: jest.fn() };
366
- const mockUserManager = {
367
- signinSilent: jest.fn().mockRejectedValue(new Error('Some unknown error')),
368
- removeUser: jest.fn().mockResolvedValue(undefined),
369
- };
370
-
371
- (auth as any).eventEmitter = mockEventEmitter;
372
- (auth as any).userManager = mockUserManager;
373
- (auth as any).refreshingPromise = null;
374
-
375
- await expect((auth as any).refreshTokenAndUpdatePromise()).rejects.toThrow();
376
-
377
- // Unknown errors should remove user (safer default)
378
- expect(mockEventEmitter.emit).toHaveBeenCalledWith(
379
- AuthEvents.USER_REMOVED,
380
- expect.objectContaining({
381
- reason: 'refresh_failed',
382
- }),
383
- );
384
- expect(mockUserManager.removeUser).toHaveBeenCalled();
385
241
  });
386
242
 
387
- it('does not emit USER_REMOVED event for ErrorTimeout', async () => {
388
- const auth = Object.create(Auth.prototype) as Auth;
389
- const mockEventEmitter = { emit: jest.fn() };
390
- const mockUserManager = {
391
- signinSilent: jest.fn(),
392
- removeUser: jest.fn().mockResolvedValue(undefined),
243
+ it('maps username when creating OIDC user from device tokens', () => {
244
+ const tokenResponse = {
245
+ id_token: 'token',
246
+ access_token: 'access',
247
+ refresh_token: 'refresh',
248
+ token_type: 'Bearer',
249
+ expires_in: 3600,
393
250
  };
394
251
 
395
- // Mock ErrorTimeout
396
- const { ErrorTimeout } = jest.requireActual('oidc-client-ts');
397
- const timeoutError = new ErrorTimeout('Silent sign-in timed out');
398
- mockUserManager.signinSilent.mockRejectedValue(timeoutError);
399
-
400
- (auth as any).eventEmitter = mockEventEmitter;
401
- (auth as any).userManager = mockUserManager;
402
- (auth as any).refreshingPromise = null;
252
+ (decodeJwtPayload as jest.Mock).mockReturnValue({
253
+ sub: 'user-123',
254
+ iss: 'issuer',
255
+ aud: 'audience',
256
+ exp: 1,
257
+ iat: 0,
258
+ email: 'test@example.com',
259
+ nickname: 'tester',
260
+ username: 'username123',
261
+ passport: undefined,
262
+ });
403
263
 
404
- await expect((auth as any).refreshTokenAndUpdatePromise()).rejects.toThrow();
264
+ const oidcUser = (Auth as any).mapDeviceTokenResponseToOidcUser(tokenResponse);
405
265
 
406
- expect(mockEventEmitter.emit).not.toHaveBeenCalledWith(
407
- AuthEvents.USER_REMOVED,
408
- expect.anything(),
409
- );
410
- expect(mockUserManager.removeUser).not.toHaveBeenCalled();
266
+ expect(decodeJwtPayload).toHaveBeenCalledWith('token');
267
+ expect(oidcUser.profile.username).toEqual('username123');
411
268
  });
412
269
  });
413
270
 
package/src/Auth.ts CHANGED
@@ -29,6 +29,7 @@ import {
29
29
  PassportMetadata,
30
30
  IdTokenPayload,
31
31
  isUserZkEvm,
32
+ EvmChain,
32
33
  } from './types';
33
34
  import EmbeddedLoginPrompt from './login/embeddedLoginPrompt';
34
35
  import TypedEventEmitter from './utils/typedEventEmitter';
@@ -570,12 +571,25 @@ export class Auth {
570
571
  username,
571
572
  },
572
573
  };
574
+
573
575
  if (passport?.zkevm_eth_address && passport?.zkevm_user_admin_address) {
574
576
  user.zkEvm = {
575
577
  ethAddress: passport.zkevm_eth_address,
576
578
  userAdminAddress: passport.zkevm_user_admin_address,
577
579
  };
578
580
  }
581
+
582
+ const chains = Object.values(EvmChain).filter((chain) => chain !== EvmChain.ZKEVM);
583
+ for (const chain of chains) {
584
+ const chainMetadata = passport?.[chain as Exclude<EvmChain, EvmChain.ZKEVM>];
585
+ if (chainMetadata?.eth_address && chainMetadata?.user_admin_address) {
586
+ user[chain] = {
587
+ ethAddress: chainMetadata.eth_address,
588
+ userAdminAddress: chainMetadata.user_admin_address,
589
+ };
590
+ }
591
+ }
592
+
579
593
  return user;
580
594
  };
581
595
 
@@ -765,21 +779,15 @@ export class Auth {
765
779
  try {
766
780
  const newOidcUser = await this.userManager.signinSilent();
767
781
  if (newOidcUser) {
768
- const user = Auth.mapOidcUserToDomainModel(newOidcUser);
769
- // Emit TOKEN_REFRESHED event so consumers (e.g., auth-next-client) can sync
770
- // the new tokens to their session. This is critical for refresh token
771
- // rotation - without this, the server-side session may have stale tokens.
772
- this.eventEmitter.emit(AuthEvents.TOKEN_REFRESHED, user);
773
- resolve(user);
782
+ resolve(Auth.mapOidcUserToDomainModel(newOidcUser));
774
783
  return;
775
784
  }
776
785
  resolve(null);
777
786
  } catch (err) {
778
787
  let passportErrorType = PassportErrorType.AUTHENTICATION_ERROR;
779
788
  let errorMessage = 'Failed to refresh token';
780
- // Default to REMOVING user - safer to log out on unknown errors
781
- // Only keep user logged in for explicitly known transient errors
782
789
  let removeUser = true;
790
+
783
791
  if (err instanceof ErrorTimeout) {
784
792
  passportErrorType = PassportErrorType.SILENT_LOGIN_ERROR;
785
793
  errorMessage = `${errorMessage}: ${err.message}`;
@@ -794,13 +802,6 @@ export class Auth {
794
802
  }
795
803
 
796
804
  if (removeUser) {
797
- // Emit USER_REMOVED event BEFORE removing user so consumers can react
798
- // (e.g., auth-next-client can clear the NextAuth session)
799
- this.eventEmitter.emit(AuthEvents.USER_REMOVED, {
800
- reason: 'refresh_failed',
801
- error: errorMessage,
802
- });
803
-
804
805
  try {
805
806
  await this.userManager.removeUser();
806
807
  } catch (removeUserError) {
package/src/index.ts CHANGED
@@ -17,13 +17,18 @@ export type {
17
17
  AuthModuleConfiguration,
18
18
  PopupOverlayOptions,
19
19
  PassportMetadata,
20
+ PassportChainMetadata,
21
+ ChainAddress,
20
22
  IdTokenPayload,
21
23
  PKCEData,
22
24
  AuthEventMap,
23
- UserRemovedReason,
24
25
  } from './types';
25
26
  export {
26
- isUserZkEvm, RollupType, MarketingConsentStatus, AuthEvents,
27
+ isUserZkEvm,
28
+ RollupType,
29
+ EvmChain,
30
+ MarketingConsentStatus,
31
+ AuthEvents,
27
32
  } from './types';
28
33
 
29
34
  // Export TypedEventEmitter
package/src/types.ts CHANGED
@@ -16,22 +16,45 @@ export enum RollupType {
16
16
  ZKEVM = 'zkEvm',
17
17
  }
18
18
 
19
+ /**
20
+ * Supported EVM chains for user registration
21
+ * Matches EvmChain from @imtbl/wallet but defined here to avoid circular dependency
22
+ */
23
+ export enum EvmChain {
24
+ ZKEVM = 'zkevm',
25
+ ARBITRUM_ONE = 'arbitrum_one',
26
+ }
27
+
28
+ export type ChainAddress = {
29
+ ethAddress: string;
30
+ userAdminAddress: string;
31
+ };
32
+
19
33
  export type User = {
20
34
  idToken?: string;
21
35
  accessToken: string;
22
36
  refreshToken?: string;
23
37
  profile: UserProfile;
24
38
  expired?: boolean;
25
- [RollupType.ZKEVM]?: {
26
- ethAddress: string;
27
- userAdminAddress: string;
28
- };
39
+ [RollupType.ZKEVM]?: ChainAddress;
40
+ } & {
41
+ [K in Exclude<EvmChain, EvmChain.ZKEVM>]?: ChainAddress;
42
+ };
43
+
44
+ export type PassportChainMetadata = {
45
+ eth_address: string;
46
+ user_admin_address: string;
29
47
  };
30
48
 
49
+ /**
50
+ * Passport metadata
51
+ * - zkEVM: flat fields (zkevm_eth_address, zkevm_user_admin_address)
52
+ * - Other chains: nested objects (arbitrum_one: { eth_address, user_admin_address })
53
+ */
31
54
  export type PassportMetadata = {
32
55
  zkevm_eth_address?: string;
33
56
  zkevm_user_admin_address?: string;
34
- };
57
+ } & Partial<Record<Exclude<EvmChain, EvmChain.ZKEVM>, PassportChainMetadata>>;
35
58
 
36
59
  export interface OidcConfiguration {
37
60
  clientId: string;
@@ -140,44 +163,12 @@ export type LoginOptions = {
140
163
  export enum AuthEvents {
141
164
  LOGGED_OUT = 'loggedOut',
142
165
  LOGGED_IN = 'loggedIn',
143
- /**
144
- * Emitted when tokens are refreshed via signinSilent().
145
- * This is critical for refresh token rotation - when client-side refresh happens,
146
- * the new tokens must be synced to server-side session to prevent race conditions.
147
- */
148
- TOKEN_REFRESHED = 'tokenRefreshed',
149
- /**
150
- * Emitted when the user is removed from local storage due to a permanent auth error.
151
- * Only emitted for errors where the refresh token is truly invalid:
152
- * - invalid_grant: refresh token expired, revoked, or already used
153
- * - login_required: user must re-authenticate
154
- * - consent_required / interaction_required: user must interact with auth server
155
- *
156
- * NOT emitted for transient errors (network, timeout, server errors) - user stays logged in.
157
- * Consumers should sync this state by clearing their session (e.g., NextAuth signOut).
158
- */
159
- USER_REMOVED = 'userRemoved',
160
166
  }
161
167
 
162
- /**
163
- * Error reason for USER_REMOVED event.
164
- * Note: Network/timeout errors do NOT emit USER_REMOVED (user stays logged in),
165
- * so 'network_error' is not a valid reason.
166
- */
167
- export type UserRemovedReason =
168
- // OAuth permanent errors (invalid_grant, login_required, etc.)
169
- | 'refresh_token_invalid'
170
- // Unknown non-OAuth errors
171
- | 'refresh_failed'
172
- // Fallback for truly unknown error types
173
- | 'unknown';
174
-
175
168
  /**
176
169
  * Event map for typed event emitter
177
170
  */
178
171
  export interface AuthEventMap extends Record<string, any> {
179
172
  [AuthEvents.LOGGED_OUT]: [];
180
173
  [AuthEvents.LOGGED_IN]: [User];
181
- [AuthEvents.TOKEN_REFRESHED]: [User];
182
- [AuthEvents.USER_REMOVED]: [{ reason: UserRemovedReason; error?: string }];
183
174
  }