diff --git a/package.json b/package.json index e562d108d45..95b7a8aac61 100644 --- a/package.json +++ b/package.json @@ -71,6 +71,7 @@ "@celo/utils": "^1.2.4", "@celo/wallet-rpc": "~1.2.0", "@crowdin/ota-client": "^0.4.0", + "@fiatconnect/fiatconnect-sdk": "0.3.1", "@fiatconnect/fiatconnect-types": "^3.2.0", "@google-cloud/storage": "^5.16.1", "@komenci/contracts": "1.1.0", diff --git a/src/fiatconnect/index.test.ts b/src/fiatconnect/index.test.ts index a8897437baf..f7766a00963 100644 --- a/src/fiatconnect/index.test.ts +++ b/src/fiatconnect/index.test.ts @@ -1,5 +1,8 @@ import { FiatAccountSchema, FiatAccountType } from '@fiatconnect/fiatconnect-types' import { FetchMock } from 'jest-fetch-mock' +import { Network } from '@fiatconnect/fiatconnect-types' +jest.mock('src/pincode/authentication') + import { CICOFlow } from 'src/fiatExchanges/utils' import { LocalCurrencyCode } from 'src/localCurrency/consts' import { CiCoCurrency } from 'src/utils/currencies' @@ -18,7 +21,12 @@ import { getFiatConnectProviders, getFiatConnectQuotes, QuotesInput, + loginWithFiatConnectProvider, + getSigningFunction, } from './index' +import { FiatConnectClient } from '@fiatconnect/fiatconnect-sdk' +import { KeychainWallet } from 'src/web3/KeychainWallet' +import { getPassword } from 'src/pincode/authentication' jest.mock('src/utils/Logger', () => ({ __esModule: true, @@ -29,9 +37,28 @@ jest.mock('src/utils/Logger', () => ({ }, })) +jest.mock('src/web3/KeychainWallet', () => { + return { + KeychainWallet: () => { + return jest.fn(() => { + return {} + }) + }, + } +}) + +jest.mock('@fiatconnect/fiatconnect-sdk', () => { + return { + FiatConnectClient: () => { + return jest.fn(() => { + return {} + }) + }, + } +}) + describe('FiatConnect helpers', () => { const mockFetch = fetch as FetchMock - beforeEach(() => { mockFetch.resetMocks() jest.clearAllMocks() @@ -134,4 +161,92 @@ describe('FiatConnect helpers', () => { ).rejects.toThrowError('Not implemented') }) }) + describe('getSigningFunction', () => { + const wallet = new KeychainWallet({ + address: 'some address', + createdAt: new Date(), + }) + beforeEach(() => { + wallet.getAccounts = jest.fn().mockReturnValue(['fakeAccount']) + wallet.isAccountUnlocked = jest.fn().mockReturnValue(true) + wallet.signPersonalMessage = jest.fn().mockResolvedValue('some signed message') + wallet.unlockAccount = jest.fn().mockResolvedValue(undefined) + }) + it('returns a signing function that signs a message', async () => { + const signingFunction = getSigningFunction(wallet) + const signedMessage = await signingFunction('test') + expect(wallet.signPersonalMessage).toHaveBeenCalled() + expect(wallet.unlockAccount).not.toHaveBeenCalled() + expect(signedMessage).toEqual('some signed message') + }) + it('returns a signing function that attempts to unlock accout if locked', async () => { + wallet.isAccountUnlocked = jest.fn().mockReturnValue(false) + const signingFunction = getSigningFunction(wallet) + const signedMessage = await signingFunction('test') + expect(wallet.signPersonalMessage).toHaveBeenCalled() + expect(wallet.unlockAccount).toHaveBeenCalled() + expect(getPassword).toHaveBeenCalled() + expect(signedMessage).toEqual('some signed message') + }) + }) + describe('loginWithFiatConnectProvider', () => { + const wallet = new KeychainWallet({ + address: 'some address', + createdAt: new Date(), + }) + const fiatConnectClient = new FiatConnectClient( + { + baseUrl: 'some url', + network: Network.Alfajores, + accountAddress: 'some address', + }, + (message: string): Promise => { + return Promise.resolve(message) + } + ) + + beforeEach(() => { + wallet.getAccounts = jest.fn().mockReturnValue(['fakeAccount']) + wallet.isAccountUnlocked = jest.fn().mockReturnValue(true) + wallet.unlockAccount = jest.fn().mockResolvedValue(undefined) + + fiatConnectClient.isLoggedIn = jest.fn().mockReturnValue(true) + fiatConnectClient.login = jest.fn().mockResolvedValue({ isOk: true, value: 'success' }) + }) + it('Does not attempt to login if already logged in', async () => { + await expect(loginWithFiatConnectProvider(wallet, fiatConnectClient)).resolves.toBeUndefined() + expect(fiatConnectClient.login).not.toHaveBeenCalled() + }) + it('Forces login attempt if already logged in', async () => { + await expect( + loginWithFiatConnectProvider(wallet, fiatConnectClient, true) + ).resolves.toBeUndefined() + expect(fiatConnectClient.login).toHaveBeenCalled() + }) + it('Attempts to login and prompts PIN when account is locked', async () => { + wallet.isAccountUnlocked = jest.fn().mockReturnValue(false) + fiatConnectClient.isLoggedIn = jest.fn().mockReturnValue(false) + await expect(loginWithFiatConnectProvider(wallet, fiatConnectClient)).resolves.toBeUndefined() + expect(wallet.unlockAccount).toHaveBeenCalled() + expect(getPassword).toHaveBeenCalled() + expect(fiatConnectClient.login).toHaveBeenCalled() + }) + it('Attempts to login without prompting for PIN when account is unlocked', async () => { + wallet.isAccountUnlocked = jest.fn().mockReturnValue(true) + fiatConnectClient.isLoggedIn = jest.fn().mockReturnValue(false) + await expect(loginWithFiatConnectProvider(wallet, fiatConnectClient)).resolves.toBeUndefined() + expect(wallet.unlockAccount).not.toHaveBeenCalled() + expect(getPassword).not.toHaveBeenCalled() + expect(fiatConnectClient.login).toHaveBeenCalled() + }) + it('Throws an error when login fails', async () => { + wallet.isAccountUnlocked = jest.fn().mockReturnValue(true) + fiatConnectClient.isLoggedIn = jest.fn().mockReturnValue(false) + fiatConnectClient.login = jest.fn().mockResolvedValue({ + isOk: false, + error: new Error('some error'), + }) + await expect(loginWithFiatConnectProvider(wallet, fiatConnectClient)).rejects.toThrow() + }) + }) }) diff --git a/src/fiatconnect/index.ts b/src/fiatconnect/index.ts index 0a832efcac6..86fc3605a8c 100644 --- a/src/fiatconnect/index.ts +++ b/src/fiatconnect/index.ts @@ -11,6 +11,11 @@ import networkConfig from 'src/web3/networkConfig' import { LocalCurrencyCode } from 'src/localCurrency/consts' import { CiCoCurrency } from 'src/utils/currencies' import Logger from 'src/utils/Logger' +import { FiatConnectApiClient } from '@fiatconnect/fiatconnect-sdk' +import { UnlockableWallet } from '@celo/wallet-base' +import { UNLOCK_DURATION } from 'src/web3/consts' +import { getPassword } from 'src/pincode/authentication' +import { ensureLeading0x } from '@celo/utils/lib/address' const TAG = 'FIATCONNECT' @@ -60,6 +65,43 @@ export async function getFiatConnectProviders( return providers } +/** + * Logs in with a FiatConnect provider. Will not attempt to log in if an + * unexpired session already exists, unless the `forceLogin` flag is set to `true`. + * If the user's wallet is currently locked, will prompt for PIN entry. + */ +export async function loginWithFiatConnectProvider( + wallet: UnlockableWallet, + fiatConnectClient: FiatConnectApiClient, + forceLogin: boolean = false +): Promise { + if (fiatConnectClient.isLoggedIn() && !forceLogin) { + return + } + + const [account] = wallet.getAccounts() + if (!wallet.isAccountUnlocked(account)) { + await wallet.unlockAccount(account, await getPassword(account), UNLOCK_DURATION) + } + + const response = await fiatConnectClient.login() + if (!response.isOk) { + Logger.error(TAG, `Failure logging in with FiatConnect provider: ${response.error}, throwing`) + throw response.error + } +} + +export function getSigningFunction(wallet: UnlockableWallet): (message: string) => Promise { + return async function (message: string): Promise { + const [account] = wallet.getAccounts() + if (!wallet.isAccountUnlocked(account)) { + await wallet.unlockAccount(account, await getPassword(account), UNLOCK_DURATION) + } + const encodedMessage = ensureLeading0x(Buffer.from(message, 'utf8').toString('hex')) + return await wallet.signPersonalMessage(account, encodedMessage) + } +} + export type QuotesInput = { fiatConnectProviders: FiatConnectProviderInfo[] flow: CICOFlow diff --git a/yarn.lock b/yarn.lock index 158fe48b966..599663683b4 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2294,6 +2294,11 @@ lodash "^4.17.19" to-fast-properties "^2.0.0" +"@badrap/result@^0.2.12": + version "0.2.12" + resolved "https://registry.yarnpkg.com/@badrap/result/-/result-0.2.12.tgz#ab5690ae04ee474ef6c912210b4126c56e7a0a9d" + integrity sha512-tSFeaiqM9p/2AtB0XNiTaZ0l+8icGxNTmF+v6sYBeV+JrFgoxhGn4BaM/SfP54Hc0eM3d8TwY0Q2k5AdSRaHIw== + "@bcoe/v8-coverage@^0.2.3": version "0.2.3" resolved "https://registry.yarnpkg.com/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz#75a2e8b51cb758a7553d6804a5932d7aace75c39" @@ -3236,11 +3241,29 @@ resolved "https://registry.yarnpkg.com/@faker-js/faker/-/faker-5.5.3.tgz#18e3af6b8eae7984072bbeb0c0858474d7c4cefe" integrity sha512-R11tGE6yIFwqpaIqcfkcg7AICXzFg14+5h5v0TfF/9+RMDL6jhzCy/pxHVOfbALGdtVYdt6JdR21tuxEgl34dw== +"@fiatconnect/fiatconnect-sdk@0.3.1": + version "0.3.1" + resolved "https://registry.yarnpkg.com/@fiatconnect/fiatconnect-sdk/-/fiatconnect-sdk-0.3.1.tgz#154a0442bcc3c561be8979b050e1d7a628c647e4" + integrity sha512-dijRaaRbjgj5mDo79x+Kk4BqTwRsfZrGnXOOY3DTU1a/q5aPtXRYO+6VBYqqcnuKHgKRQLUjXJHkElWeG0/Bjg== + dependencies: + "@badrap/result" "^0.2.12" + "@fiatconnect/fiatconnect-types" "^5.0.0" + ethers "^5.6.4" + fetch-cookie "^2.0.3" + node-fetch "^2.6.6" + siwe "^1.1.6" + tslib "^2.4.0" + "@fiatconnect/fiatconnect-types@^3.2.0": version "3.2.0" resolved "https://registry.yarnpkg.com/@fiatconnect/fiatconnect-types/-/fiatconnect-types-3.2.0.tgz#36eb018d22160ab114ee842dd843a659bbc53213" integrity sha512-OP4LJkEXPb+0WeUhO4iWf+X2EQuLWAGyWikRpyf74gLQZvPuV8hFN9TArCJYGqNuJHPvZ2VBOuzosTroSW5STg== +"@fiatconnect/fiatconnect-types@^5.0.0": + version "5.0.0" + resolved "https://registry.yarnpkg.com/@fiatconnect/fiatconnect-types/-/fiatconnect-types-5.0.0.tgz#29a1d8bf9cfd0f94bd34cad7581bf057a48289bd" + integrity sha512-fKNEJJQz1RuQqS/obFFFASy9pavuT6uQaVHIRxyIhM9DTwk8dsJvSzmwjhEhwLq6TTbriCEqmSkVG7ESWVDAVQ== + "@google-cloud/common@^3.8.1": version "3.8.1" resolved "https://registry.yarnpkg.com/@google-cloud/common/-/common-3.8.1.tgz#1313c55bb66df88f69bf7c828135fae25fbd2036" @@ -4621,6 +4644,13 @@ resolved "https://registry.yarnpkg.com/@sinonjs/text-encoding/-/text-encoding-0.7.1.tgz#8da5c6530915653f3a1f38fd5f101d8c3f8079c5" integrity sha512-+iTbntw2IZPb/anVDbypzfQa+ay64MW0Zo8aJ8gZPWMMK6/OubMVb6lUPMagqjOPnmtauXnFCACVl3O7ogjeqQ== +"@spruceid/siwe-parser@^1.1.3": + version "1.1.3" + resolved "https://registry.yarnpkg.com/@spruceid/siwe-parser/-/siwe-parser-1.1.3.tgz#0eebe8bbd63c6de89cb44c06b6329b00b305df65" + integrity sha512-oQ8PcwDqjGWJvLmvAF2yzd6iniiWxK0Qtz+Dw+gLD/W5zOQJiKIUXwslHOm8VB8OOOKW9vfR3dnPBhHaZDvRsw== + dependencies: + apg-js "^4.1.1" + "@stablelib/binary@^0.7.2": version "0.7.2" resolved "https://registry.yarnpkg.com/@stablelib/binary/-/binary-0.7.2.tgz#1b3392170c8a8741c8b8f843ea294de71aeb2cf7" @@ -4628,6 +4658,13 @@ dependencies: "@stablelib/int" "^0.5.0" +"@stablelib/binary@^1.0.1": + version "1.0.1" + resolved "https://registry.yarnpkg.com/@stablelib/binary/-/binary-1.0.1.tgz#c5900b94368baf00f811da5bdb1610963dfddf7f" + integrity sha512-ClJWvmL6UBM/wjkvv/7m5VP3GMr9t0osr4yVgLZsLCOz4hGN9gIAFEqnJ0TsSMAN+n840nf2cHZnA5/KFqHC7Q== + dependencies: + "@stablelib/int" "^1.0.1" + "@stablelib/blake2s@^0.10.4": version "0.10.4" resolved "https://registry.yarnpkg.com/@stablelib/blake2s/-/blake2s-0.10.4.tgz#8a708f28a9c78d4a1a9fbcc6ce8bacbda469f302" @@ -4656,11 +4693,29 @@ resolved "https://registry.yarnpkg.com/@stablelib/int/-/int-0.5.0.tgz#cca9225951d55d2de48656755784788633660c2b" integrity sha1-zKkiWVHVXS3khlZ1V4R4hjNmDCs= +"@stablelib/int@^1.0.1": + version "1.0.1" + resolved "https://registry.yarnpkg.com/@stablelib/int/-/int-1.0.1.tgz#75928cc25d59d73d75ae361f02128588c15fd008" + integrity sha512-byr69X/sDtDiIjIV6m4roLVWnNNlRGzsvxw+agj8CIEazqWGOQp2dTYgQhtyVXV9wpO6WyXRQUzLV/JRNumT2w== + +"@stablelib/random@^1.0.1": + version "1.0.1" + resolved "https://registry.yarnpkg.com/@stablelib/random/-/random-1.0.1.tgz#4357a00cb1249d484a9a71e6054bc7b8324a7009" + integrity sha512-zOh+JHX3XG9MSfIB0LZl/YwPP9w3o6WBiJkZvjPoKKu5LKFW4OLV71vMxWp9qG5T43NaWyn0QQTWgqCdO+yOBQ== + dependencies: + "@stablelib/binary" "^1.0.1" + "@stablelib/wipe" "^1.0.1" + "@stablelib/wipe@^0.5.0": version "0.5.0" resolved "https://registry.yarnpkg.com/@stablelib/wipe/-/wipe-0.5.0.tgz#a682d5f9448e950e099e537e6f72fc960275d151" integrity sha1-poLV+USOlQ4JnlN+b3L8lgJ10VE= +"@stablelib/wipe@^1.0.1": + version "1.0.1" + resolved "https://registry.yarnpkg.com/@stablelib/wipe/-/wipe-1.0.1.tgz#d21401f1d59ade56a62e139462a97f104ed19a36" + integrity sha512-WfqfX/eXGiAd3RJe4VU2snh/ZPwtSjLG4ynQ/vYzvghTh7dHFcI1wl+nrkWG6lGhukOxOsUHfv8dUXr58D0ayg== + "@szmarczak/http-timer@^1.1.2": version "1.1.2" resolved "https://registry.yarnpkg.com/@szmarczak/http-timer/-/http-timer-1.1.2.tgz#b1665e2c461a2cd92f4c1bbf50d5454de0d4b421" @@ -5942,6 +5997,11 @@ anymatch@~3.1.2: normalize-path "^3.0.0" picomatch "^2.0.4" +apg-js@^4.1.1: + version "4.1.1" + resolved "https://registry.yarnpkg.com/apg-js/-/apg-js-4.1.1.tgz#5ee8a74e26517073e1bda66705eca6fc22b305e2" + integrity sha512-DwTfzx1YuCrnEvywiU/AYKiX8Y6JzhY8PwaM9syh54zzBPaHzonN7c4YsAspC6YcdSu/jfBXBJ1S9hj1QsiePA== + apollo-boost@^0.4.9: version "0.4.9" resolved "https://registry.yarnpkg.com/apollo-boost/-/apollo-boost-0.4.9.tgz#ab3ba539c2ca944e6fd156583a1b1954b17a6791" @@ -9523,7 +9583,7 @@ ethereumjs-util@^7.0.8, ethereumjs-util@^7.1.0, ethereumjs-util@^7.1.3: ethereum-cryptography "^0.1.3" rlp "^2.2.4" -ethers@5.0.5, ethers@^5.0.13, ethers@^5.5.4: +ethers@5.0.5, ethers@^5.0.13, ethers@^5.5.4, ethers@^5.6.4: version "5.0.5" resolved "https://registry.yarnpkg.com/ethers/-/ethers-5.0.5.tgz#09b37c27d3b7d93c084af7ec4cad72ca8cfa5b7a" integrity sha512-hkVI7fi16ADWTctYMEfamU39hoR+JpkdEI7LH8PC7Bhl0Dp8y8dqFJ948pSEuuPJI5NR/L4+V6y2Alvyu+zROw== @@ -9956,6 +10016,14 @@ fd-slicer@~1.1.0: dependencies: pend "~1.2.0" +fetch-cookie@^2.0.3: + version "2.0.3" + resolved "https://registry.yarnpkg.com/fetch-cookie/-/fetch-cookie-2.0.3.tgz#e81a364d1fb1952f4ba3aa7881025d6c4a758b15" + integrity sha512-Awxvuqsf0Rc4tckszW1iJpBIRrKwEiYDL/XxQkRhpCTcuQdiCN1XP9MFOyzGaGbf0MU7gjBkcgwxT7IyjpCz3g== + dependencies: + set-cookie-parser "^2.4.8" + tough-cookie "^4.0.0" + figures@^1.7.0: version "1.7.0" resolved "https://registry.yarnpkg.com/figures/-/figures-1.7.0.tgz#cbe1e3affcf1cd44b80cadfed28dc793a9701d2e" @@ -14383,7 +14451,7 @@ node-environment-flags@1.0.6: object.getownpropertydescriptors "^2.0.3" semver "^5.7.0" -node-fetch@2.6.7, node-fetch@^1.0.1, node-fetch@^2.0.0-alpha.8, node-fetch@^2.1.2, node-fetch@^2.2.0, node-fetch@^2.6.0, node-fetch@^2.6.1, node-fetch@^2.6.7: +node-fetch@2.6.7, node-fetch@^1.0.1, node-fetch@^2.0.0-alpha.8, node-fetch@^2.1.2, node-fetch@^2.2.0, node-fetch@^2.6.0, node-fetch@^2.6.1, node-fetch@^2.6.6, node-fetch@^2.6.7: version "2.6.7" resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.7.tgz#24de9fba827e3b4ae44dc8b20256a379160052ad" integrity sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ== @@ -15658,7 +15726,7 @@ pseudomap@^1.0.2: resolved "https://registry.yarnpkg.com/pseudomap/-/pseudomap-1.0.2.tgz#f052a28da70e618917ef0a8ac34c1ae5a68286b3" integrity sha1-8FKijacOYYkX7wqKw0wa5aaChrM= -psl@^1.1.24, psl@^1.1.28: +psl@^1.1.24, psl@^1.1.28, psl@^1.1.33: version "1.8.0" resolved "https://registry.yarnpkg.com/psl/-/psl-1.8.0.tgz#9326f8bcfb013adcc005fdff056acce020e51c24" integrity sha512-RIdOzyoavK+hA18OGGWDqUTsCLhtA7IcZ/6NCs4fFJaHBDab+pDDmDIByWFRQJq2Cd7r1OoQxBGKOaztq+hjIQ== @@ -17464,6 +17532,11 @@ set-blocking@^2.0.0, set-blocking@~2.0.0: resolved "https://registry.yarnpkg.com/set-blocking/-/set-blocking-2.0.0.tgz#045f9782d011ae9a6803ddd382b24392b3d890f7" integrity sha1-BF+XgtARrppoA93TgrJDkrPYkPc= +set-cookie-parser@^2.4.8: + version "2.4.8" + resolved "https://registry.yarnpkg.com/set-cookie-parser/-/set-cookie-parser-2.4.8.tgz#d0da0ed388bc8f24e706a391f9c9e252a13c58b2" + integrity sha512-edRH8mBKEWNVIVMKejNnuJxleqYE/ZSdcT8/Nem9/mmosx12pctd80s2Oy00KNZzrogMZS5mauK2/ymL1bvlvg== + set-value@^0.4.3, set-value@^2.0.0, set-value@^4.0.1: version "4.1.0" resolved "https://registry.yarnpkg.com/set-value/-/set-value-4.1.0.tgz#aa433662d87081b75ad88a4743bd450f044e7d09" @@ -17640,6 +17713,15 @@ sisteransi@^1.0.5: resolved "https://registry.yarnpkg.com/sisteransi/-/sisteransi-1.0.5.tgz#134d681297756437cc05ca01370d3a7a571075ed" integrity sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg== +siwe@^1.1.6: + version "1.1.6" + resolved "https://registry.yarnpkg.com/siwe/-/siwe-1.1.6.tgz#b4b4fe7814654d5ea529171b2ede15f7ef7ef1ae" + integrity sha512-3WRdEil32Tc2vuNzqJ2/Z/MIvsvy0Nkzc2ov+QujmpHO7tM83dgcb47z0Pu236T4JQkOQCqQkq3AJ/rVIezniA== + dependencies: + "@spruceid/siwe-parser" "^1.1.3" + "@stablelib/random" "^1.0.1" + apg-js "^4.1.1" + sjcl@^1.0.3: version "1.0.7" resolved "https://registry.yarnpkg.com/sjcl/-/sjcl-1.0.7.tgz#32b365a50dc9bba26b88ba3c9df8ea34217d9f45" @@ -18565,6 +18647,15 @@ tough-cookie@^3.0.1: psl "^1.1.28" punycode "^2.1.1" +tough-cookie@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-4.0.0.tgz#d822234eeca882f991f0f908824ad2622ddbece4" + integrity sha512-tHdtEpQCMrc1YLrMaqXXcj6AxhYi/xgit6mZu1+EDWUn+qhUf8wMQoFIy9NXuq23zAwtcB0t/MjACGR18pcRbg== + dependencies: + psl "^1.1.33" + punycode "^2.1.1" + universalify "^0.1.2" + tough-cookie@~2.4.3: version "2.4.3" resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-2.4.3.tgz#53f36da3f47783b0925afa06ff9f3b165280f781" @@ -18686,6 +18777,11 @@ tslib@^2.0.1: resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.3.1.tgz#e8a335add5ceae51aa261d32a490158ef042ef01" integrity sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw== +tslib@^2.4.0: + version "2.4.0" + resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.4.0.tgz#7cecaa7f073ce680a05847aa77be941098f36dc3" + integrity sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ== + tsutils@^3.17.1: version "3.17.1" resolved "https://registry.yarnpkg.com/tsutils/-/tsutils-3.17.1.tgz#ed719917f11ca0dee586272b2ac49e015a2dd759" @@ -19002,7 +19098,7 @@ universal-user-agent@^6.0.0: resolved "https://registry.yarnpkg.com/universal-user-agent/-/universal-user-agent-6.0.0.tgz#3381f8503b251c0d9cd21bc1de939ec9df5480ee" integrity sha512-isyNax3wXoKaulPDZWHQqbmIx1k2tb9fb3GGDBRxCscfYV2Ch7WxPArBsFEG8s/safwXTT7H4QGhaIkTp9447w== -universalify@^0.1.0: +universalify@^0.1.0, universalify@^0.1.2: version "0.1.2" resolved "https://registry.yarnpkg.com/universalify/-/universalify-0.1.2.tgz#b646f69be3942dabcecc9d6639c80dc105efaa66" integrity sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==