From 920d1592fd5720130caf372783ac5436a683347d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Yago=20P=C3=A9rez=20V=C3=A1zquez?= Date: Mon, 25 Nov 2024 11:55:19 +0100 Subject: [PATCH] feat(protocol-kit): Add react native compatibility (#1033) --- packages/protocol-kit/package.json | 5 +- packages/protocol-kit/src/Safe.ts | 15 +- packages/protocol-kit/src/index.ts | 2 - packages/protocol-kit/src/types/passkeys.ts | 5 +- .../src/utils/passkeys/PasskeyClient.ts | 44 ++++- .../src/utils/passkeys/extractPasskeyData.ts | 187 +++++++++++++++--- .../protocol-kit/tests/e2e/utils/passkeys.ts | 8 +- yarn.lock | 55 +++++- 8 files changed, 279 insertions(+), 42 deletions(-) diff --git a/packages/protocol-kit/package.json b/packages/protocol-kit/package.json index 7c26a1ea3..130618500 100644 --- a/packages/protocol-kit/package.json +++ b/packages/protocol-kit/package.json @@ -66,12 +66,15 @@ "web3": "^4.12.1" }, "dependencies": { - "@noble/hashes": "^1.3.3", "@safe-global/safe-deployments": "^1.37.14", "@safe-global/safe-modules-deployments": "^2.2.4", "@safe-global/types-kit": "^1.0.0", "abitype": "^1.0.2", "semver": "^7.6.3", "viem": "^2.21.8" + }, + "optionalDependencies": { + "@noble/curves": "^1.6.0", + "@peculiar/asn1-schema": "^2.3.13" } } diff --git a/packages/protocol-kit/src/Safe.ts b/packages/protocol-kit/src/Safe.ts index 3b9dd9859..6b39deeb8 100644 --- a/packages/protocol-kit/src/Safe.ts +++ b/packages/protocol-kit/src/Safe.ts @@ -42,7 +42,8 @@ import { SigningMethodType, SwapOwnerTxParams, SafeModulesPaginated, - RemovePasskeyOwnerTxParams + RemovePasskeyOwnerTxParams, + PasskeyArgType } from './types' import { EthSafeSignature, @@ -59,7 +60,8 @@ import { generateSignature, preimageSafeMessageHash, preimageSafeTransactionHash, - adjustVInSignature + adjustVInSignature, + extractPasskeyData } from './utils' import EthSafeTransaction from './utils/transactions/SafeTransaction' import { SafeTransactionOptionalProps } from './utils/transactions/types' @@ -1698,6 +1700,15 @@ class Safe { }): ContractInfo | undefined => { return getContractInfo(contractAddress) } + + /** + * This method creates a signer to be used with the init method + * @param {Credential} credential - The credential to be used to create the signer. Can be generated in the web with navigator.credentials.create + * @returns {PasskeyArgType} - The signer to be used with the init method + */ + static createPasskeySigner = async (credential: Credential): Promise => { + return extractPasskeyData(credential) + } } export default Safe diff --git a/packages/protocol-kit/src/index.ts b/packages/protocol-kit/src/index.ts index cd12b85cf..9c502e2e0 100644 --- a/packages/protocol-kit/src/index.ts +++ b/packages/protocol-kit/src/index.ts @@ -35,7 +35,6 @@ import { estimateTxGas, estimateSafeTxGas, estimateSafeDeploymentGas, - extractPasskeyCoordinates, extractPasskeyData, validateEthereumAddress, validateEip3770Address @@ -74,7 +73,6 @@ export { estimateSafeTxGas, estimateSafeDeploymentGas, extractPasskeyData, - extractPasskeyCoordinates, ContractManager, CreateCallBaseContract, createERC20TokenTransferTransaction, diff --git a/packages/protocol-kit/src/types/passkeys.ts b/packages/protocol-kit/src/types/passkeys.ts index 912c73e5c..e440692d4 100644 --- a/packages/protocol-kit/src/types/passkeys.ts +++ b/packages/protocol-kit/src/types/passkeys.ts @@ -3,8 +3,11 @@ export type PasskeyCoordinates = { y: string } +export type GetPasskeyCredentialFn = (options?: CredentialRequestOptions) => Promise + export type PasskeyArgType = { rawId: string // required to sign data coordinates: PasskeyCoordinates // required to sign data - customVerifierAddress?: string // optional + customVerifierAddress?: string + getFn?: GetPasskeyCredentialFn } diff --git a/packages/protocol-kit/src/utils/passkeys/PasskeyClient.ts b/packages/protocol-kit/src/utils/passkeys/PasskeyClient.ts index 3307cfd6a..40bec29dd 100644 --- a/packages/protocol-kit/src/utils/passkeys/PasskeyClient.ts +++ b/packages/protocol-kit/src/utils/passkeys/PasskeyClient.ts @@ -22,7 +22,8 @@ import { PasskeyArgType, PasskeyClient, SafeWebAuthnSignerFactoryContractImplementationType, - SafeWebAuthnSharedSignerContractImplementationType + SafeWebAuthnSharedSignerContractImplementationType, + GetPasskeyCredentialFn } from '@safe-global/protocol-kit/types' import { getDefaultFCLP256VerifierAddress } from './extractPasskeyData' import { asHex } from '../types' @@ -31,20 +32,29 @@ import isSharedSigner from './isSharedSigner' export const PASSKEY_CLIENT_KEY = 'passkeyWallet' export const PASSKEY_CLIENT_NAME = 'Passkey Wallet Client' -const sign = async (passkeyRawId: Uint8Array, data: Uint8Array): Promise => { - const assertion = (await navigator.credentials.get({ +const sign = async ( + passkeyRawId: Uint8Array, + data: Uint8Array, + getFn?: GetPasskeyCredentialFn +): Promise => { + // Avoid loosing the context for navigator.credentials.get function that leads to an error + const getCredentials = getFn || navigator.credentials.get.bind(navigator.credentials) + + const assertion = (await getCredentials({ publicKey: { challenge: data, allowCredentials: [{ type: 'public-key', id: passkeyRawId }], userVerification: 'required' } - })) as PublicKeyCredential & { response: AuthenticatorAssertionResponse } + })) as PublicKeyCredential + + const assertionResponse = assertion.response as AuthenticatorAssertionResponse - if (!assertion?.response?.authenticatorData) { + if (!assertionResponse?.authenticatorData) { throw new Error('Failed to sign data with passkey Signer') } - const { authenticatorData, signature, clientDataJSON } = assertion.response + const { authenticatorData, signature, clientDataJSON } = assertionResponse return encodeAbiParameters(parseAbiParameters('bytes, bytes, uint256[2]'), [ toHex(new Uint8Array(authenticatorData)), @@ -104,10 +114,14 @@ export const createPasskeyClient = async ( .extend(() => ({ signMessage({ message }: { message: SignableMessage }) { if (typeof message === 'string') { - return sign(passkeyRawId, toBytes(message)) + return sign(passkeyRawId, toBytes(message), passkey.getFn) } - return sign(passkeyRawId, isHex(message.raw) ? toBytes(message.raw) : message.raw) + return sign( + passkeyRawId, + isHex(message.raw) ? toBytes(message.raw) : message.raw, + passkey.getFn + ) }, signTransaction, signTypedData, @@ -145,6 +159,17 @@ export const createPasskeyClient = async ( })) as PasskeyClient } +function decodeClientDataJSON(clientDataJSON: ArrayBuffer): string { + const uint8Array = new Uint8Array(clientDataJSON) + + let result = '' + for (let i = 0; i < uint8Array.length; i++) { + result += String.fromCharCode(uint8Array[i]) + } + + return result +} + /** * Compute the additional client data JSON fields. This is the fields other than `type` and * `challenge` (including `origin` and any other additional client data fields that may be @@ -157,7 +182,8 @@ export const createPasskeyClient = async ( * @throws {Error} Throws an error if the client data JSON does not contain the expected 'challenge' field pattern. */ function extractClientDataFields(clientDataJSON: ArrayBuffer): Hex { - const decodedClientDataJSON = new TextDecoder('utf-8').decode(clientDataJSON) + const decodedClientDataJSON = decodeClientDataJSON(clientDataJSON) + const match = decodedClientDataJSON.match( /^\{"type":"webauthn.get","challenge":"[A-Za-z0-9\-_]{43}",(.*)\}$/ ) diff --git a/packages/protocol-kit/src/utils/passkeys/extractPasskeyData.ts b/packages/protocol-kit/src/utils/passkeys/extractPasskeyData.ts index 5760fd4b6..f11807270 100644 --- a/packages/protocol-kit/src/utils/passkeys/extractPasskeyData.ts +++ b/packages/protocol-kit/src/utils/passkeys/extractPasskeyData.ts @@ -1,43 +1,119 @@ -import { getFCLP256VerifierDeployment } from '@safe-global/safe-modules-deployments' import { Buffer } from 'buffer' -import { PasskeyCoordinates, PasskeyArgType } from '@safe-global/protocol-kit/types' +import { getFCLP256VerifierDeployment } from '@safe-global/safe-modules-deployments' +import { PasskeyArgType, PasskeyCoordinates } from '@safe-global/protocol-kit/types' /** - * Extracts and returns the passkey data (coordinates and rawId) from a given passkey Credential. + * Converts a Base64 URL-encoded string to a Uint8Array. * - * @param {Credential} passkeyCredential - The passkey credential generated via `navigator.credentials.create()` using correct parameters. - * @returns {Promise} A promise that resolves to an object containing the coordinates and the rawId derived from the passkey. - * @throws {Error} Throws an error if the coordinates could not be extracted + * This function handles Base64 URL variants by replacing URL-safe characters + * with standard Base64 characters, decodes the Base64 string into a binary string, + * and then converts it into a Uint8Array. + * + * @param {string} base64 - The Base64 URL-encoded string to convert. + * @returns {Uint8Array} The resulting Uint8Array from the decoded Base64 string. */ -export async function extractPasskeyData(passkeyCredential: Credential): Promise { - const passkey = passkeyCredential as PublicKeyCredential - const attestationResponse = passkey.response as AuthenticatorAttestationResponse +function base64ToUint8Array(base64: string): Uint8Array { + const base64Fixed = base64.replace(/-/g, '+').replace(/_/g, '/') + const binaryBuffer = Buffer.from(base64Fixed, 'base64') - const publicKey = attestationResponse.getPublicKey() + return new Uint8Array(binaryBuffer) +} - if (!publicKey) { - throw new Error('Failed to generate passkey Coordinates. getPublicKey() failed') +/** + * Dynamic import libraries required for decoding public keys. + */ +async function importLibs() { + const { p256 } = await import('@noble/curves/p256') + + const { AsnParser, AsnProp, AsnPropTypes, AsnType, AsnTypeTypes } = await import( + '@peculiar/asn1-schema' + ) + + @AsnType({ type: AsnTypeTypes.Sequence }) + class AlgorithmIdentifier { + @AsnProp({ type: AsnPropTypes.ObjectIdentifier }) + public id: string = '' + + @AsnProp({ type: AsnPropTypes.ObjectIdentifier, optional: true }) + public curve: string = '' } - const coordinates = await extractPasskeyCoordinates(publicKey) - const rawId = Buffer.from(passkey.rawId).toString('hex') + @AsnType({ type: AsnTypeTypes.Sequence }) + class ECPublicKey { + @AsnProp({ type: AlgorithmIdentifier }) + public algorithm = new AlgorithmIdentifier() + + @AsnProp({ type: AsnPropTypes.BitString }) + public publicKey: ArrayBuffer = new ArrayBuffer(0) + } return { - rawId, - coordinates + p256, + AsnParser, + ECPublicKey } } - /** - * Extracts and returns coordinates from a given passkey public key. + * Decodes a Base64-encoded ECDSA public key for React Native and extracts the x and y coordinates. * - * @param {ArrayBuffer} publicKey - The public key of the passkey from which coordinates will be extracted. - * @returns {Promise} A promise that resolves to an object containing the coordinates derived from the public key of the passkey. - * @throws {Error} Throws an error if the coordinates could not be extracted via `crypto.subtle.exportKey()` + * This function handles both ASN.1 DER-encoded keys and uncompressed keys. It decodes a Base64-encoded + * public key, checks its format, and extracts the x and y coordinates using the `@noble/curves` library. + * The coordinates are returned as hexadecimal strings prefixed with '0x'. + * + * @param {string} publicKey - The Base64-encoded public key to decode. + * @returns {PasskeyCoordinates} An object containing the x and y coordinates of the public key. + * @throws {Error} Throws an error if the key is empty or if the coordinates cannot be extracted. */ -export async function extractPasskeyCoordinates( - publicKey: ArrayBuffer +export async function decodePublicKeyForReactNative( + publicKey: string ): Promise { + const { p256, AsnParser, ECPublicKey } = await importLibs() + + let publicKeyBytes = base64ToUint8Array(publicKey) + + if (publicKeyBytes.length === 0) { + throw new Error('Decoded public key is empty.') + } + + const isAsn1Encoded = publicKeyBytes[0] === 0x30 + const isUncompressedKey = publicKeyBytes.length === 64 + + if (isAsn1Encoded) { + const asn1ParsedKey = AsnParser.parse(publicKeyBytes.buffer, ECPublicKey) + + publicKeyBytes = new Uint8Array(asn1ParsedKey.publicKey) + } else if (isUncompressedKey) { + const uncompressedKey = new Uint8Array(65) + uncompressedKey[0] = 0x04 + uncompressedKey.set(publicKeyBytes, 1) + + publicKeyBytes = uncompressedKey + } + + const point = p256.ProjectivePoint.fromHex(publicKeyBytes) + + const x = point.x.toString(16).padStart(64, '0') + const y = point.y.toString(16).padStart(64, '0') + + return { + x: '0x' + x, + y: '0x' + y + } +} + +/** + * Decodes an ECDSA public key for the web platform and extracts the x and y coordinates. + * + * This function uses the Web Crypto API to import a public key in SPKI format and then + * exports it to a JWK format to retrieve the x and y coordinates. The coordinates are + * returned as hexadecimal strings prefixed with '0x'. + * + * @param {ArrayBuffer} publicKey - The public key in SPKI format to decode. + * @returns {Promise} A promise that resolves to an object containing + * the x and y coordinates of the public key. + * @throws {Error} Throws an error if the key coordinates cannot be extracted. + */ +export async function decodePublicKeyForWeb(publicKey: ArrayBuffer): Promise { const algorithm = { name: 'ECDSA', namedCurve: 'P-256', @@ -60,6 +136,71 @@ export async function extractPasskeyCoordinates( } } +/** + * Decodes the x and y coordinates of the public key from a created public key credential response. + * + * @param {AuthenticatorResponse} response + * @returns {PasskeyCoordinates} Object containing the coordinates derived from the public key of the passkey. + * @throws {Error} Throws an error if the coordinates could not be extracted via `p256.ProjectivePoint.fromHex` + */ +export async function decodePublicKey( + response: AuthenticatorResponse +): Promise { + const publicKeyAuthenticatorResponse = response as AuthenticatorAttestationResponse + const publicKey = publicKeyAuthenticatorResponse.getPublicKey() + + if (!publicKey) { + throw new Error('Failed to generate passkey coordinates. getPublicKey() failed') + } + + if (typeof publicKey === 'string') { + // Public key is base64 encoded + // - React Native platform uses base64 encoded strings + return decodePublicKeyForReactNative(publicKey) + } + + if (publicKey instanceof ArrayBuffer) { + // Public key is an ArrayBuffer + // - Web platform uses ArrayBuffer + return await decodePublicKeyForWeb(publicKey) + } + + throw new Error('Unsupported public key format.') +} + +/** + * Extracts and returns the passkey data (coordinates and rawId) from a given passkey Credential. + * + * @param {Credential} passkeyCredential - The passkey credential generated via `navigator.credentials.create()` or other method in another platforms. + * @returns {Promise} A promise that resolves to an object containing the coordinates and the rawId derived from the passkey. + * This is the important information in the Safe account context and should be stored securely as it is used to verify the passkey and to instantiate the SDK + * as a signer (`Safe.init()) + * @throws {Error} Throws an error if the coordinates could not be extracted + */ +export async function extractPasskeyData(passkeyCredential: Credential): Promise { + const passkeyPublicKeyCredential = passkeyCredential as PublicKeyCredential + + const rawId = Buffer.from(passkeyPublicKeyCredential.rawId).toString('hex') + const coordinates = await decodePublicKey(passkeyPublicKeyCredential.response) + + return { + rawId, + coordinates + } +} + +/** + * Retrieves the default FCLP256 Verifier address for a given blockchain network. + * + * This function fetches the deployment information for the FCLP256 Verifier and + * returns the verifier address associated with the specified chain ID. It ensures + * that the correct version and release status are used. + * + * @param {string} chainId - The ID of the blockchain network to retrieve the verifier address for. + * @returns {string} The FCLP256 Verifier address for the specified chain ID. + * @throws {Error} Throws an error if the deployment information or address cannot be found. + */ + export function getDefaultFCLP256VerifierAddress(chainId: string): string { const FCLP256VerifierDeployment = getFCLP256VerifierDeployment({ version: '0.2.1', diff --git a/packages/protocol-kit/tests/e2e/utils/passkeys.ts b/packages/protocol-kit/tests/e2e/utils/passkeys.ts index 2053af959..c23e46489 100644 --- a/packages/protocol-kit/tests/e2e/utils/passkeys.ts +++ b/packages/protocol-kit/tests/e2e/utils/passkeys.ts @@ -1,7 +1,8 @@ -import { PasskeyArgType, PasskeyClient, extractPasskeyCoordinates } from '@safe-global/protocol-kit' +import { PasskeyArgType, PasskeyClient } from '@safe-global/protocol-kit' import { WebAuthnCredentials } from './webauthnShim' import { WalletClient, keccak256, toBytes, Transport, Chain, Account } from 'viem' import { asHex } from '@safe-global/protocol-kit/utils/types' +import { decodePublicKeyForWeb } from '@safe-global/protocol-kit/utils' let singleInstance: WebAuthnCredentials @@ -54,7 +55,7 @@ export async function createMockPasskey( webAuthnCredentials?: WebAuthnCredentials ): Promise { const credentialsInstance = webAuthnCredentials ?? getWebAuthnCredentials() - const passkeyCredential = await credentialsInstance.create({ + const passkeyCredential = credentialsInstance.create({ publicKey: { rp: { name: 'Safe', @@ -85,7 +86,8 @@ export async function createMockPasskey( const exportedPublicKey = await crypto.subtle.exportKey('spki', key) const rawId = Buffer.from(passkeyCredential.rawId).toString('hex') - const coordinates = await extractPasskeyCoordinates(exportedPublicKey) + + const coordinates = await decodePublicKeyForWeb(exportedPublicKey) const passkey: PasskeyArgType = { rawId, diff --git a/yarn.lock b/yarn.lock index 904bf6e2a..bc435613e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1136,6 +1136,13 @@ dependencies: "@noble/hashes" "1.4.0" +"@noble/curves@^1.6.0": + version "1.6.0" + resolved "https://registry.yarnpkg.com/@noble/curves/-/curves-1.6.0.tgz#be5296ebcd5a1730fccea4786d420f87abfeb40b" + integrity sha512-TlaHRXDehJuRNR9TfZDNQ45mMEd5dwUwmicsafcIX4SsNiqnCHKjE/1alYPd/lDRVhxdhUAlv8uEhMCI5zjIJQ== + dependencies: + "@noble/hashes" "1.5.0" + "@noble/curves@~1.4.0": version "1.4.2" resolved "https://registry.yarnpkg.com/@noble/curves/-/curves-1.4.2.tgz#40309198c76ed71bc6dbf7ba24e81ceb4d0d1fe9" @@ -1168,7 +1175,7 @@ resolved "https://registry.yarnpkg.com/@noble/hashes/-/hashes-1.4.0.tgz#45814aa329f30e4fe0ba49426f49dfccdd066426" integrity sha512-V1JJ1WTRUqHHrOSh597hURcMqVKVGL/ea3kv0gSnEdsEZ0/+VyPghM1lMNGc00z7CIQorSvbKpuJkxvuHbvdbg== -"@noble/hashes@^1.3.3", "@noble/hashes@^1.4.0", "@noble/hashes@~1.5.0": +"@noble/hashes@1.5.0", "@noble/hashes@^1.4.0", "@noble/hashes@~1.5.0": version "1.5.0" resolved "https://registry.yarnpkg.com/@noble/hashes/-/hashes-1.5.0.tgz#abadc5ca20332db2b1b2aa3e496e9af1213570b0" integrity sha512-1j6kQFb7QRru7eKN3ZDvRcP13rugwdxZqCjbiAVZfIJwgj2A65UmT4TgARXGlXgnRkORLTDTrO19ZErt7+QXgA== @@ -1715,6 +1722,15 @@ resolved "https://registry.npmjs.org/@openzeppelin/contracts/-/contracts-2.5.1.tgz" integrity sha512-qIy6tLx8rtybEsIOAlrM4J/85s2q2nPkDqj/Rx46VakBZ0LwtFhXIVub96LXHczQX0vaqmAueDqNPXtbSXSaYQ== +"@peculiar/asn1-schema@^2.3.13": + version "2.3.13" + resolved "https://registry.yarnpkg.com/@peculiar/asn1-schema/-/asn1-schema-2.3.13.tgz#ec8509cdcbc0da3abe73fd7e690556b57a61b8f4" + integrity sha512-3Xq3a01WkHRZL8X04Zsfg//mGaA21xlL4tlVn4v2xGT0JStiztATRkMwa5b+f/HXmY2smsiLXYK46Gwgzvfg3g== + dependencies: + asn1js "^3.0.5" + pvtsutils "^1.3.5" + tslib "^2.6.2" + "@pkgjs/parseargs@^0.11.0": version "0.11.0" resolved "https://registry.yarnpkg.com/@pkgjs/parseargs/-/parseargs-0.11.0.tgz#a77ea742fab25775145434eb1d2328cf5013ac33" @@ -1725,6 +1741,17 @@ resolved "https://registry.yarnpkg.com/@pkgr/core/-/core-0.1.1.tgz#1ec17e2edbec25c8306d424ecfbf13c7de1aaa31" integrity sha512-cq8o4cWH0ibXh9VGi5P20Tu9XF/0fFXl9EUinr9QfTM7a7p0oTA4iJRCQWppXR1Pg8dSM0UCItCkPwsk9qWWYA== +"@safe-global/protocol-kit@file:packages/sdk-starter-kit/.yalc/@safe-global/protocol-kit": + version "5.0.4" + dependencies: + "@noble/curves" "^1.6.0" + "@safe-global/safe-deployments" "^1.37.14" + "@safe-global/safe-modules-deployments" "^2.2.4" + "@safe-global/types-kit" "^1.0.0" + abitype "^1.0.2" + semver "^7.6.3" + viem "^2.21.8" + "@safe-global/safe-contracts-v1.4.1@npm:@safe-global/safe-contracts@1.4.1": version "1.4.1" resolved "https://registry.npmjs.org/@safe-global/safe-contracts/-/safe-contracts-1.4.1.tgz" @@ -2544,6 +2571,15 @@ arrify@^2.0.1: resolved "https://registry.npmjs.org/arrify/-/arrify-2.0.1.tgz" integrity sha512-3duEwti880xqi4eAMN8AyR4a0ByT90zoYdLlevfrvU43vb0YZwZVfxOgxWrLXXXpyugL0hNZc9G6BiB5B3nUug== +asn1js@^3.0.5: + version "3.0.5" + resolved "https://registry.yarnpkg.com/asn1js/-/asn1js-3.0.5.tgz#5ea36820443dbefb51cc7f88a2ebb5b462114f38" + integrity sha512-FVnvrKJwpt9LP2lAMl8qZswRNm3T4q9CON+bxldk2iwk3FFpuwhx2FfinyitizWHsVYyaY+y5JzDR0rCMV5yTQ== + dependencies: + pvtsutils "^1.3.2" + pvutils "^1.1.3" + tslib "^2.4.0" + assertion-error@^1.1.0: version "1.1.0" resolved "https://registry.npmjs.org/assertion-error/-/assertion-error-1.1.0.tgz" @@ -7263,6 +7299,18 @@ pure-rand@^6.0.0: resolved "https://registry.npmjs.org/pure-rand/-/pure-rand-6.0.1.tgz" integrity sha512-t+x1zEHDjBwkDGY5v5ApnZ/utcd4XYDiJsaQQoptTXgUXX95sDg1elCdJghzicm7n2mbCBJ3uYWr6M22SO19rg== +pvtsutils@^1.3.2, pvtsutils@^1.3.5: + version "1.3.5" + resolved "https://registry.yarnpkg.com/pvtsutils/-/pvtsutils-1.3.5.tgz#b8705b437b7b134cd7fd858f025a23456f1ce910" + integrity sha512-ARvb14YB9Nm2Xi6nBq1ZX6dAM0FsJnuk+31aUp4TrcZEdKUlSqOqsxJHUPJDNE3qiIp+iUPEIeR6Je/tgV7zsA== + dependencies: + tslib "^2.6.1" + +pvutils@^1.1.3: + version "1.1.3" + resolved "https://registry.yarnpkg.com/pvutils/-/pvutils-1.1.3.tgz#f35fc1d27e7cd3dfbd39c0826d173e806a03f5a3" + integrity sha512-pMpnA0qRdFp32b1sJl1wOJNxZLQ2cbQx+k6tjNtZ8CpvVhNqEPRgivZ2WOUev2YMajecdH7ctUPDvEe87nariQ== + qs@^6.9.4: version "6.11.1" resolved "https://registry.npmjs.org/qs/-/qs-6.11.1.tgz" @@ -8220,6 +8268,11 @@ tslib@^2.1.0, tslib@^2.3.0, tslib@^2.4.0, tslib@^2.6.2: resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.6.2.tgz#703ac29425e7b37cd6fd456e92404d46d1f3e4ae" integrity sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q== +tslib@^2.6.1: + version "2.8.1" + resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.8.1.tgz#612efe4ed235d567e8aba5f2a5fab70280ade83f" + integrity sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w== + tsort@0.0.1: version "0.0.1" resolved "https://registry.npmjs.org/tsort/-/tsort-0.0.1.tgz"