@imtbl/sdk 0.2.0-alpha → 0.2.1-alpha

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/dist/passport.js CHANGED
@@ -1,5 +1,7 @@
1
1
  import { ImmutableX, Config, generateLegacyStarkPrivateKey, createStarkSigner } from '@imtbl/core-sdk';
2
2
  import { UserManager } from 'oidc-client-ts';
3
+ import axios$1 from 'axios';
4
+ import jwt_decode from 'jwt-decode';
3
5
  import { Magic } from 'magic-sdk';
4
6
  import { OpenIdExtension } from '@magic-ext/oidc';
5
7
  import BN from 'bn.js';
@@ -21,7 +23,7 @@ const createImmutableXConfiguration = ({ basePath, chainID, coreContractAddress,
21
23
  chainID,
22
24
  coreContractAddress,
23
25
  registrationContractAddress,
24
- sdkVersion: 'ts-immutable-sdk-0.2.0-alpha',
26
+ sdkVersion: 'ts-immutable-sdk-0.2.1-alpha',
25
27
  });
26
28
  /**
27
29
  * Sets `sdkVersion` at the time of build
@@ -4289,6 +4291,43 @@ const withPassportError = async (fn, customErrorType) => {
4289
4291
  }
4290
4292
  };
4291
4293
 
4294
+ const keyCrendentials = 'passport_credentials';
4295
+ const validCredentialsMinTtlSec = 3600; // 1 hour
4296
+ class DeviceCredentialsManager {
4297
+ saveCredentials(tokenResponse) {
4298
+ localStorage.setItem(keyCrendentials, JSON.stringify(tokenResponse));
4299
+ }
4300
+ getCredentials() {
4301
+ const credentialsJson = localStorage.getItem(keyCrendentials);
4302
+ if (credentialsJson) {
4303
+ return JSON.parse(credentialsJson);
4304
+ }
4305
+ return null;
4306
+ }
4307
+ areValid(tokenResponse) {
4308
+ if (tokenResponse) {
4309
+ const accessTokenValid = this.isTokenValid(tokenResponse.access_token);
4310
+ const idTokenValid = this.isTokenValid(tokenResponse.id_token);
4311
+ return accessTokenValid && idTokenValid;
4312
+ }
4313
+ return false;
4314
+ }
4315
+ isTokenValid(jwt) {
4316
+ const tokenPayload = jwt_decode(jwt);
4317
+ const expiresAt = tokenPayload.exp ?? 0;
4318
+ const now = (Date.now() / 1000) + validCredentialsMinTtlSec;
4319
+ return expiresAt > now;
4320
+ }
4321
+ clearCredentials() {
4322
+ localStorage.removeItem(keyCrendentials);
4323
+ }
4324
+ }
4325
+
4326
+ const formUrlEncodedHeader = {
4327
+ headers: {
4328
+ 'Content-Type': 'application/x-www-form-urlencoded',
4329
+ },
4330
+ };
4292
4331
  const getAuthConfiguration = ({ oidcConfiguration, authenticationDomain, }) => {
4293
4332
  const baseConfiguration = {
4294
4333
  authority: authenticationDomain,
@@ -4314,12 +4353,19 @@ const getAuthConfiguration = ({ oidcConfiguration, authenticationDomain, }) => {
4314
4353
  }
4315
4354
  return baseConfiguration;
4316
4355
  };
4356
+ function wait$1(ms) {
4357
+ return new Promise((resolve) => {
4358
+ setTimeout(resolve, ms);
4359
+ });
4360
+ }
4317
4361
  class AuthManager {
4318
4362
  userManager;
4319
4363
  config;
4364
+ deviceCredentialsManager;
4320
4365
  constructor(config) {
4321
4366
  this.config = config;
4322
4367
  this.userManager = new UserManager(getAuthConfiguration(config));
4368
+ this.deviceCredentialsManager = new DeviceCredentialsManager();
4323
4369
  }
4324
4370
  static mapOidcUserToDomainModel = (oidcUser) => {
4325
4371
  const passport = oidcUser.profile?.passport;
@@ -4349,6 +4395,27 @@ class AuthManager {
4349
4395
  }
4350
4396
  return user;
4351
4397
  };
4398
+ static mapDeviceTokenResponseToDomainUserModel = (tokenResponse) => {
4399
+ const idTokenPayload = jwt_decode(tokenResponse.id_token);
4400
+ const user = {
4401
+ idToken: tokenResponse.id_token,
4402
+ accessToken: tokenResponse.access_token,
4403
+ refreshToken: tokenResponse.refresh_token,
4404
+ profile: {
4405
+ sub: idTokenPayload.sub,
4406
+ email: idTokenPayload.email,
4407
+ nickname: idTokenPayload.nickname,
4408
+ },
4409
+ };
4410
+ if (idTokenPayload?.passport?.imx_eth_address) {
4411
+ user.imx = {
4412
+ ethAddress: idTokenPayload?.passport?.imx_eth_address,
4413
+ starkAddress: idTokenPayload?.passport?.imx_stark_address,
4414
+ userAdminAddress: idTokenPayload?.passport?.imx_user_admin_address,
4415
+ };
4416
+ }
4417
+ return user;
4418
+ };
4352
4419
  async login() {
4353
4420
  return withPassportError(async () => {
4354
4421
  const popupWindowFeatures = { width: 410, height: 450 };
@@ -4361,9 +4428,104 @@ class AuthManager {
4361
4428
  async loginCallback() {
4362
4429
  return withPassportError(async () => this.userManager.signinPopupCallback(), PassportErrorType.AUTHENTICATION_ERROR);
4363
4430
  }
4431
+ async loginWithDeviceFlow() {
4432
+ return withPassportError(async () => {
4433
+ const response = await axios$1.post(`${this.config.authenticationDomain}/oauth/device/code`, {
4434
+ client_id: this.config.oidcConfiguration.clientId,
4435
+ scope: this.config.oidcConfiguration.scope,
4436
+ audience: this.config.oidcConfiguration.audience,
4437
+ }, formUrlEncodedHeader);
4438
+ return {
4439
+ code: response.data.user_code,
4440
+ deviceCode: response.data.device_code,
4441
+ url: response.data.verification_uri_complete,
4442
+ interval: response.data.interval,
4443
+ };
4444
+ }, PassportErrorType.AUTHENTICATION_ERROR);
4445
+ }
4446
+ /* eslint-disable no-await-in-loop */
4447
+ async connectImxDeviceFlow(deviceCode, interval, timeoutMs) {
4448
+ return withPassportError(async () => {
4449
+ const startTime = Date.now();
4450
+ const loopCondition = true;
4451
+ while (loopCondition) {
4452
+ if (timeoutMs != null && Date.now() - startTime > timeoutMs) {
4453
+ throw new Error('Timed out');
4454
+ }
4455
+ await wait$1(interval * 1000);
4456
+ try {
4457
+ const tokenResponse = await this.getDeviceFlowToken(deviceCode);
4458
+ const user = AuthManager.mapDeviceTokenResponseToDomainUserModel(tokenResponse);
4459
+ // Only persist credentials that contain the necessary data
4460
+ if (user.imx?.ethAddress && user.imx?.starkAddress && user.imx?.userAdminAddress) {
4461
+ this.deviceCredentialsManager.saveCredentials(tokenResponse);
4462
+ }
4463
+ return user;
4464
+ }
4465
+ catch (error) {
4466
+ if (axios$1.isAxiosError(error)) {
4467
+ const responseError = error.response?.data;
4468
+ switch (responseError.error) {
4469
+ case 'authorization_pending':
4470
+ break;
4471
+ case 'slow_down':
4472
+ break;
4473
+ case 'expired_token':
4474
+ throw new Error('Token expired, please log in again');
4475
+ case 'access_denied':
4476
+ throw new Error('User denied access');
4477
+ default:
4478
+ throw new Error('Error getting token');
4479
+ }
4480
+ }
4481
+ else {
4482
+ throw error;
4483
+ }
4484
+ }
4485
+ }
4486
+ throw new Error('Failed to get credentials');
4487
+ }, PassportErrorType.AUTHENTICATION_ERROR);
4488
+ }
4489
+ /* eslint-enable no-await-in-loop */
4490
+ async getDeviceFlowToken(deviceCode) {
4491
+ const response = await axios$1.post(`${this.config.authenticationDomain}/oauth/token`, {
4492
+ client_id: this.config.oidcConfiguration.clientId,
4493
+ grant_type: 'urn:ietf:params:oauth:grant-type:device_code',
4494
+ device_code: deviceCode,
4495
+ }, formUrlEncodedHeader);
4496
+ return response.data;
4497
+ }
4498
+ async connectImxWithCredentials(tokenResponse) {
4499
+ return withPassportError(async () => {
4500
+ if (this.deviceCredentialsManager.areValid(tokenResponse)) {
4501
+ // Credentials exist and are still valid
4502
+ return AuthManager.mapDeviceTokenResponseToDomainUserModel(tokenResponse);
4503
+ }
4504
+ const refreshToken = tokenResponse?.refresh_token ?? null;
4505
+ if (refreshToken) {
4506
+ // Token is no longer valid, but refresh token can be used to a new one
4507
+ const newTokenResponse = await this.refreshToken(refreshToken);
4508
+ return AuthManager.mapDeviceTokenResponseToDomainUserModel(newTokenResponse);
4509
+ }
4510
+ return null;
4511
+ }, PassportErrorType.AUTHENTICATION_ERROR);
4512
+ }
4513
+ async refreshToken(refreshToken) {
4514
+ const response = await axios$1.post(`${this.config.authenticationDomain}/oauth/token`, {
4515
+ client_id: this.config.oidcConfiguration.clientId,
4516
+ grant_type: 'refresh_token',
4517
+ refresh_token: refreshToken,
4518
+ }, formUrlEncodedHeader);
4519
+ return response.data;
4520
+ }
4364
4521
  async logout() {
4365
4522
  return withPassportError(async () => this.userManager.signoutRedirect(), PassportErrorType.LOGOUT_ERROR);
4366
4523
  }
4524
+ async logoutDeviceFlow() {
4525
+ return withPassportError(async () => {
4526
+ this.deviceCredentialsManager.clearCredentials();
4527
+ }, PassportErrorType.LOGOUT_ERROR);
4528
+ }
4367
4529
  async loginSilent() {
4368
4530
  return withPassportError(async () => {
4369
4531
  const existedUser = await this.getUser();
@@ -4380,12 +4542,28 @@ class AuthManager {
4380
4542
  async getUser() {
4381
4543
  return withPassportError(async () => {
4382
4544
  const oidcUser = await this.userManager.getUser();
4383
- if (!oidcUser) {
4384
- return null;
4545
+ if (oidcUser) {
4546
+ return AuthManager.mapOidcUserToDomainModel(oidcUser);
4385
4547
  }
4386
- return AuthManager.mapOidcUserToDomainModel(oidcUser);
4548
+ const deviceToken = this.deviceCredentialsManager.getCredentials();
4549
+ if (deviceToken) {
4550
+ return AuthManager.mapDeviceTokenResponseToDomainUserModel(deviceToken);
4551
+ }
4552
+ return null;
4387
4553
  }, PassportErrorType.NOT_LOGGED_IN_ERROR);
4388
4554
  }
4555
+ async getUserDeviceFlow() {
4556
+ return withPassportError(async () => {
4557
+ const deviceToken = this.deviceCredentialsManager.getCredentials();
4558
+ if (deviceToken) {
4559
+ return AuthManager.mapDeviceTokenResponseToDomainUserModel(deviceToken);
4560
+ }
4561
+ return null;
4562
+ }, PassportErrorType.NOT_LOGGED_IN_ERROR);
4563
+ }
4564
+ checkStoredDeviceFlowCredentials() {
4565
+ return this.deviceCredentialsManager.getCredentials();
4566
+ }
4389
4567
  }
4390
4568
 
4391
4569
  class MagicAdapter {
@@ -4913,6 +5091,17 @@ class PassportImxProviderFactory {
4913
5091
  }
4914
5092
  return this.createProviderInstance(user);
4915
5093
  }
5094
+ async getProviderWithDeviceFlow(deviceCode, interval, timeoutMs) {
5095
+ const user = await this.authManager.connectImxDeviceFlow(deviceCode, interval, timeoutMs);
5096
+ return this.createProviderInstance(user);
5097
+ }
5098
+ async getProviderWithCredentials(tokenResponse) {
5099
+ const user = await this.authManager.connectImxWithCredentials(tokenResponse);
5100
+ if (!user) {
5101
+ return null;
5102
+ }
5103
+ return this.createProviderInstance(user);
5104
+ }
4916
5105
  async createProviderInstance(user) {
4917
5106
  if (!user.idToken) {
4918
5107
  throw new PassportError('Failed to initialise', PassportErrorType.WALLET_CONNECTION_ERROR);
@@ -5220,16 +5409,38 @@ class Passport {
5220
5409
  async connectImx() {
5221
5410
  return this.passportImxProviderFactory.getProvider();
5222
5411
  }
5412
+ async loginWithDeviceFlow() {
5413
+ return this.authManager.loginWithDeviceFlow();
5414
+ }
5415
+ async connectImxDeviceFlow(deviceCode, interval, timeoutMs) {
5416
+ return this.passportImxProviderFactory.getProviderWithDeviceFlow(deviceCode, interval, timeoutMs);
5417
+ }
5418
+ /**
5419
+ * @returns {boolean} the stored device flow credentials if they exist
5420
+ */
5421
+ checkStoredDeviceFlowCredentials() {
5422
+ return this.authManager.checkStoredDeviceFlowCredentials();
5423
+ }
5424
+ async connectImxWithCredentials(tokenResponse) {
5425
+ return this.passportImxProviderFactory.getProviderWithCredentials(tokenResponse);
5426
+ }
5223
5427
  async loginCallback() {
5224
5428
  return this.authManager.loginCallback();
5225
5429
  }
5226
5430
  async logout() {
5227
5431
  return this.authManager.logout();
5228
5432
  }
5433
+ async logoutDeviceFlow() {
5434
+ return this.authManager.logoutDeviceFlow();
5435
+ }
5229
5436
  async getUserInfo() {
5230
5437
  const user = await this.authManager.getUser();
5231
5438
  return user?.profile;
5232
5439
  }
5440
+ async getUserInfoDeviceFlow() {
5441
+ const user = await this.authManager.getUserDeviceFlow();
5442
+ return user?.profile;
5443
+ }
5233
5444
  async getIdToken() {
5234
5445
  const user = await this.authManager.getUser();
5235
5446
  return user?.idToken;
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@imtbl/sdk",
3
3
  "description": "Immutable Unified SDK",
4
- "version": "0.2.0-alpha",
4
+ "version": "0.2.1-alpha",
5
5
  "author": "Immutable",
6
6
  "bugs": "https://github.com/immutable/ts-immutable-sdk/issues",
7
7
  "dependencies": {
@@ -110,6 +110,7 @@
110
110
  "main": "./dist/index.cjs",
111
111
  "module": "./dist/index.js",
112
112
  "peerDependencies": {
113
+ "ethers": "^5.7.2",
113
114
  "react": "^18.2.0",
114
115
  "react-dom": "^18.2.0"
115
116
  },