From 72797305ac9ce8639abb09334cf2471f0932ca88 Mon Sep 17 00:00:00 2001 From: Santiago Palladino Date: Tue, 29 Aug 2023 04:33:54 -0300 Subject: [PATCH] feat: Check sandbox version matches CLI's (#1849) Checks on every CLI command that the Aztec RPC server version matches the expected one. Checks exact matches for now, but can be extended to semver checks. If no match, emits a warn. ``` $ yarn aztec-cli get-accounts cli WARN Aztec Sandbox is running version 0.1.0 which is newer than the expected by this CLI (0.0.0). Consider upgrading your CLI to a newer version. Accounts found: Address: 0x2e13f0201905944184fc2c09d29fcf0cac07647be171656a275f63d99b819360, Public Key: 0x157922ba4defc5ccc36862afdf51dec2df3da3f683507ebffd59d472d6cbf0eb193a4e294e4fb994b22bd5dcb382b83b88734eef20e01a90f1498f9d60f0db8b, Partial Address: 0x044770258feb9223e966ae7bbae2b3a975d4a43e480688e7c51119ee4eb2e054 ``` To support this, the `NodeInfo` struct returned from the RPC server returns also the `client` identifier, which looks like `aztec-rpc@0.1.0-alpha47`. --- l1-contracts/package.json | 2 +- yarn-project/aztec-cli/package.json | 2 + yarn-project/aztec-cli/src/client.test.ts | 34 ++++++++++ yarn-project/aztec-cli/src/client.ts | 66 ++++++++++++++++++- yarn-project/aztec-cli/src/index.ts | 30 ++++----- .../src/aztec_rpc_server/aztec_rpc_server.ts | 7 +- yarn-project/aztec-rpc/src/config/index.ts | 13 ++++ yarn-project/aztec-sandbox/package.json | 2 +- yarn-project/aztec-sandbox/src/index.ts | 7 +- .../aztec.js/src/contract/contract.test.ts | 2 +- yarn-project/noir-compiler/package.json | 2 +- yarn-project/prover-client/package.json | 2 +- yarn-project/rollup-provider/package.json | 2 +- .../types/src/interfaces/aztec_rpc.ts | 4 ++ yarn-project/yarn.lock | 14 ++++ 15 files changed, 165 insertions(+), 24 deletions(-) create mode 100644 yarn-project/aztec-cli/src/client.test.ts diff --git a/l1-contracts/package.json b/l1-contracts/package.json index 2156f8abf3a..1969ca98520 100644 --- a/l1-contracts/package.json +++ b/l1-contracts/package.json @@ -1,6 +1,6 @@ { "name": "@aztec/l1-contracts", - "version": "0.0.1", + "version": "0.1.0", "license": "Apache-2.0", "description": "Aztec contracts for the Ethereum mainnet and testnets", "devDependencies": { diff --git a/yarn-project/aztec-cli/package.json b/yarn-project/aztec-cli/package.json index 6f575328879..8e82eac0e2a 100644 --- a/yarn-project/aztec-cli/package.json +++ b/yarn-project/aztec-cli/package.json @@ -43,6 +43,7 @@ "@aztec/noir-contracts": "workspace:^", "@aztec/types": "workspace:^", "commander": "^9.0.0", + "semver": "^7.5.4", "tslib": "^2.4.0", "viem": "^1.2.5" }, @@ -52,6 +53,7 @@ "@types/jest": "^29.5.0", "@types/node": "^18.7.23", "jest": "^29.5.0", + "jest-mock-extended": "^3.0.5", "ts-jest": "^29.1.0", "ts-node": "^10.9.1", "typescript": "^5.0.4" diff --git a/yarn-project/aztec-cli/src/client.test.ts b/yarn-project/aztec-cli/src/client.test.ts new file mode 100644 index 00000000000..d4e6de3fdb9 --- /dev/null +++ b/yarn-project/aztec-cli/src/client.test.ts @@ -0,0 +1,34 @@ +import { AztecRPC, NodeInfo } from '@aztec/types'; + +import { MockProxy, mock } from 'jest-mock-extended'; + +import { checkServerVersion } from './client.js'; + +describe('client', () => { + describe('checkServerVersion', () => { + let rpc: MockProxy; + + beforeEach(() => { + rpc = mock(); + }); + + it('checks versions match', async () => { + rpc.getNodeInfo.mockResolvedValue({ client: 'rpc@0.1.0-alpha47' } as NodeInfo); + await checkServerVersion(rpc, '0.1.0-alpha47'); + }); + + it('reports mismatch on older rpc version', async () => { + rpc.getNodeInfo.mockResolvedValue({ client: 'rpc@0.1.0-alpha47' } as NodeInfo); + await expect(checkServerVersion(rpc, '0.1.0-alpha48')).rejects.toThrowError( + /is older than the expected by this CLI/, + ); + }); + + it('reports mismatch on newer rpc version', async () => { + rpc.getNodeInfo.mockResolvedValue({ client: 'rpc@0.1.0-alpha48' } as NodeInfo); + await expect(checkServerVersion(rpc, '0.1.0-alpha47')).rejects.toThrowError( + /is newer than the expected by this CLI/, + ); + }); + }); +}); diff --git a/yarn-project/aztec-cli/src/client.ts b/yarn-project/aztec-cli/src/client.ts index 1eb6ac3ccb3..2878119a3d8 100644 --- a/yarn-project/aztec-cli/src/client.ts +++ b/yarn-project/aztec-cli/src/client.ts @@ -1,5 +1,11 @@ -import { createAztecRpcClient } from '@aztec/aztec.js'; +import { AztecRPC, createAztecRpcClient } from '@aztec/aztec.js'; import { makeFetch } from '@aztec/foundation/json-rpc/client'; +import { DebugLogger } from '@aztec/foundation/log'; + +import { readFileSync } from 'fs'; +import { dirname, resolve } from 'path'; +import { gtr, ltr, satisfies, valid } from 'semver'; +import { fileURLToPath } from 'url'; const retries = [1, 1, 2]; @@ -12,3 +18,61 @@ export function createClient(rpcUrl: string) { const fetch = makeFetch(retries, true); return createAztecRpcClient(rpcUrl, fetch); } + +/** + * Creates an Aztec RPC client with a given set of retries on non-server errors. + * Checks that the RPC server matches the expected version, and warns if not. + * @param rpcUrl - URL of the RPC server. + * @param logger - Debug logger to warn version incompatibilities. + * @returns An RPC client. + */ +export async function createCompatibleClient(rpcUrl: string, logger: DebugLogger) { + const client = createClient(rpcUrl); + const packageJsonPath = resolve(dirname(fileURLToPath(import.meta.url)), '../package.json'); + const packageJsonContents = JSON.parse(readFileSync(packageJsonPath).toString()); + const expectedVersionRange = packageJsonContents.version; // During sandbox, we'll expect exact matches + + try { + await checkServerVersion(client, expectedVersionRange, logger); + } catch (err) { + if (err instanceof VersionMismatchError) { + logger.warn(err.message); + } else { + throw err; + } + } + + return client; +} + +/** Mismatch between server and client versions. */ +class VersionMismatchError extends Error {} + +/** + * Checks that the RPC server version matches the expected one by this CLI. Throws if not. + * @param rpc - RPC server connection. + * @param expectedVersionRange - Expected version by CLI. + */ +export async function checkServerVersion(rpc: AztecRPC, expectedVersionRange: string, logger?: DebugLogger) { + const serverName = 'Aztec Sandbox'; + const { client } = await rpc.getNodeInfo(); + const version = client.split('@')[1]; + logger?.debug(`Comparing server version ${version} against CLI expected ${expectedVersionRange}`); + if (!version || !valid(version)) { + throw new VersionMismatchError(`Missing or invalid version identifier for ${serverName} (${version ?? 'empty'}).`); + } else if (!satisfies(version, expectedVersionRange)) { + if (gtr(version, expectedVersionRange)) { + throw new VersionMismatchError( + `${serverName} is running version ${version} which is newer than the expected by this CLI (${expectedVersionRange}). Consider upgrading your CLI to a newer version.`, + ); + } else if (ltr(version, expectedVersionRange)) { + throw new VersionMismatchError( + `${serverName} is running version ${version} which is older than the expected by this CLI (${expectedVersionRange}). Consider upgrading your ${serverName} to a newer version.`, + ); + } else { + throw new VersionMismatchError( + `${serverName} is running version ${version} which does not match the expected by this CLI (${expectedVersionRange}).`, + ); + } + } +} diff --git a/yarn-project/aztec-cli/src/index.ts b/yarn-project/aztec-cli/src/index.ts index 1399aba3f3c..ae3cdc24cc8 100644 --- a/yarn-project/aztec-cli/src/index.ts +++ b/yarn-project/aztec-cli/src/index.ts @@ -22,7 +22,7 @@ import { dirname, resolve } from 'path'; import { fileURLToPath } from 'url'; import { mnemonicToAccount } from 'viem/accounts'; -import { createClient } from './client.js'; +import { createCompatibleClient } from './client.js'; import { encodeArgs, parseStructString } from './encoding.js'; import { deployAztecContracts, @@ -127,7 +127,7 @@ export function getProgram(log: LogFn, debugLogger: DebugLogger): Command { ) .option('-u, --rpc-url ', 'URL of the Aztec RPC', AZTEC_RPC_HOST || 'http://localhost:8080') .action(async options => { - const client = createClient(options.rpcUrl); + const client = await createCompatibleClient(options.rpcUrl, debugLogger); const privateKey = options.privateKey ? new PrivateKey(Buffer.from(stripLeadingHex(options.privateKey), 'hex')) : PrivateKey.random(); @@ -161,7 +161,7 @@ export function getProgram(log: LogFn, debugLogger: DebugLogger): Command { const contractAbi = await getContractAbi(abiPath, log); const constructorAbi = contractAbi.functions.find(({ name }) => name === 'constructor'); - const client = createClient(options.rpcUrl); + const client = await createCompatibleClient(options.rpcUrl, debugLogger); const publicKey = options.publicKey ? Point.fromString(options.publicKey) : undefined; const salt = options.salt ? Fr.fromBuffer(Buffer.from(stripLeadingHex(options.salt), 'hex')) : undefined; const deployer = new ContractDeployer(contractAbi, client, publicKey); @@ -189,7 +189,7 @@ export function getProgram(log: LogFn, debugLogger: DebugLogger): Command { .requiredOption('-ca, --contract-address
', 'An Aztec address to check if contract has been deployed to.') .option('-u, --rpc-url ', 'URL of the Aztec RPC', AZTEC_RPC_HOST || 'http://localhost:8080') .action(async options => { - const client = createClient(options.rpcUrl); + const client = await createCompatibleClient(options.rpcUrl, debugLogger); const address = AztecAddress.fromString(options.contractAddress); const isDeployed = await isContractDeployed(client, address); if (isDeployed) log(`\nContract found at ${address.toString()}\n`); @@ -202,7 +202,7 @@ export function getProgram(log: LogFn, debugLogger: DebugLogger): Command { .description('Gets the receipt for the specified transaction hash.') .option('-u, --rpc-url ', 'URL of the Aztec RPC', AZTEC_RPC_HOST || 'http://localhost:8080') .action(async (_txHash, options) => { - const client = createClient(options.rpcUrl); + const client = await createCompatibleClient(options.rpcUrl, debugLogger); const txHash = TxHash.fromString(_txHash); const receipt = await client.getTxReceipt(txHash); if (!receipt) { @@ -219,7 +219,7 @@ export function getProgram(log: LogFn, debugLogger: DebugLogger): Command { .option('-u, --rpc-url ', 'URL of the Aztec RPC', AZTEC_RPC_HOST || 'http://localhost:8080') .option('-b, --include-bytecode ', "Include the contract's public function bytecode, if any.", false) .action(async (contractAddress, options) => { - const client = createClient(options.rpcUrl); + const client = await createCompatibleClient(options.rpcUrl, debugLogger); const address = AztecAddress.fromString(contractAddress); const contractDataWithOrWithoutBytecode = options.includeBytecode ? await client.getContractDataAndBytecode(address) @@ -255,7 +255,7 @@ export function getProgram(log: LogFn, debugLogger: DebugLogger): Command { const fromBlock = from ? parseInt(from) : 1; const limitCount = limit ? parseInt(limit) : 100; - const client = createClient(options.rpcUrl); + const client = await createCompatibleClient(options.rpcUrl, debugLogger); const logs = await client.getUnencryptedLogs(fromBlock, limitCount); if (!logs.length) { log(`No logs found in blocks ${fromBlock} to ${fromBlock + limitCount}`); @@ -273,7 +273,7 @@ export function getProgram(log: LogFn, debugLogger: DebugLogger): Command { .requiredOption('-pa, --partial-address ', 'URL of the Aztec RPC', AZTEC_RPC_HOST || 'http://localhost:8080') .action(async options => { - const client = createClient(options.rpcUrl); + const client = await createCompatibleClient(options.rpcUrl, debugLogger); const address = AztecAddress.fromString(options.address); const publicKey = Point.fromString(options.publicKey); const partialAddress = Fr.fromString(options.partialAddress); @@ -287,7 +287,7 @@ export function getProgram(log: LogFn, debugLogger: DebugLogger): Command { .description('Gets all the Aztec accounts stored in the Aztec RPC.') .option('-u, --rpc-url ', 'URL of the Aztec RPC', AZTEC_RPC_HOST || 'http://localhost:8080') .action(async (options: any) => { - const client = createClient(options.rpcUrl); + const client = await createCompatibleClient(options.rpcUrl, debugLogger); const accounts = await client.getAccounts(); if (!accounts.length) { log('No accounts found.'); @@ -305,7 +305,7 @@ export function getProgram(log: LogFn, debugLogger: DebugLogger): Command { .argument('
', 'The Aztec address to get account for') .option('-u, --rpc-url ', 'URL of the Aztec RPC', AZTEC_RPC_HOST || 'http://localhost:8080') .action(async (_address, options) => { - const client = createClient(options.rpcUrl); + const client = await createCompatibleClient(options.rpcUrl, debugLogger); const address = AztecAddress.fromString(_address); const account = await client.getAccount(address); @@ -321,7 +321,7 @@ export function getProgram(log: LogFn, debugLogger: DebugLogger): Command { .description('Gets all the recipients stored in the Aztec RPC.') .option('-u, --rpc-url ', 'URL of the Aztec RPC', AZTEC_RPC_HOST || 'http://localhost:8080') .action(async (options: any) => { - const client = createClient(options.rpcUrl); + const client = await createCompatibleClient(options.rpcUrl, debugLogger); const recipients = await client.getRecipients(); if (!recipients.length) { log('No recipients found.'); @@ -339,7 +339,7 @@ export function getProgram(log: LogFn, debugLogger: DebugLogger): Command { .argument('
', 'The Aztec address to get recipient for') .option('-u, --rpc-url ', 'URL of the Aztec RPC', AZTEC_RPC_HOST || 'http://localhost:8080') .action(async (_address, options) => { - const client = createClient(options.rpcUrl); + const client = await createCompatibleClient(options.rpcUrl, debugLogger); const address = AztecAddress.fromString(_address); const recipient = await client.getRecipient(address); @@ -381,7 +381,7 @@ export function getProgram(log: LogFn, debugLogger: DebugLogger): Command { const privateKey = new PrivateKey(Buffer.from(stripLeadingHex(options.privateKey), 'hex')); - const client = createClient(options.rpcUrl); + const client = await createCompatibleClient(options.rpcUrl, debugLogger); const wallet = await getAccountWallets( client, SchnorrAccountContractAbi, @@ -428,7 +428,7 @@ export function getProgram(log: LogFn, debugLogger: DebugLogger): Command { `Invalid number of args passed. Expected ${fnAbi.parameters.length}; Received: ${options.args.length}`, ); } - const client = createClient(options.rpcUrl); + const client = await createCompatibleClient(options.rpcUrl, debugLogger); const from = await getTxSender(client, options.from); const result = await client.viewTx(functionName, functionArgs, contractAddress, from); log('\nView result: ', JsonStringify(result, true), '\n'); @@ -463,7 +463,7 @@ export function getProgram(log: LogFn, debugLogger: DebugLogger): Command { .description('Gets the current Aztec L2 block number.') .option('-u, --rpcUrl ', 'URL of the Aztec RPC', AZTEC_RPC_HOST || 'http://localhost:8080') .action(async (options: any) => { - const client = createClient(options.rpcUrl); + const client = await createCompatibleClient(options.rpcUrl, debugLogger); const num = await client.getBlockNumber(); log(`${num}\n`); }); diff --git a/yarn-project/aztec-rpc/src/aztec_rpc_server/aztec_rpc_server.ts b/yarn-project/aztec-rpc/src/aztec_rpc_server/aztec_rpc_server.ts index acb4f54ad19..73800bc8f41 100644 --- a/yarn-project/aztec-rpc/src/aztec_rpc_server/aztec_rpc_server.ts +++ b/yarn-project/aztec-rpc/src/aztec_rpc_server/aztec_rpc_server.ts @@ -42,7 +42,7 @@ import { toContractDao, } from '@aztec/types'; -import { RpcServerConfig } from '../config/index.js'; +import { RpcServerConfig, getPackageInfo } from '../config/index.js'; import { ContractDataOracle } from '../contract_data_oracle/index.js'; import { Database } from '../database/index.js'; import { KernelOracle } from '../kernel_oracle/index.js'; @@ -56,6 +56,7 @@ import { Synchroniser } from '../synchroniser/index.js'; export class AztecRPCServer implements AztecRPC { private synchroniser: Synchroniser; private log: DebugLogger; + private clientInfo: string; constructor( private keyStore: KeyStore, @@ -66,6 +67,9 @@ export class AztecRPCServer implements AztecRPC { ) { this.log = createDebugLogger(logSuffix ? `aztec:rpc_server_${logSuffix}` : `aztec:rpc_server`); this.synchroniser = new Synchroniser(node, db, logSuffix); + + const { version, name } = getPackageInfo(); + this.clientInfo = `${name.split('/')[name.split('/').length - 1]}@${version}`; } /** @@ -276,6 +280,7 @@ export class AztecRPCServer implements AztecRPC { version, chainId, rollupAddress, + client: this.clientInfo, }; } diff --git a/yarn-project/aztec-rpc/src/config/index.ts b/yarn-project/aztec-rpc/src/config/index.ts index 5f19540ed4e..c44f505decd 100644 --- a/yarn-project/aztec-rpc/src/config/index.ts +++ b/yarn-project/aztec-rpc/src/config/index.ts @@ -1,3 +1,7 @@ +import { readFileSync } from 'fs'; +import { dirname, resolve } from 'path'; +import { fileURLToPath } from 'url'; + /** * Configuration settings for the RPC Server. */ @@ -18,3 +22,12 @@ export function getConfigEnvVars(): RpcServerConfig { l2BlockPollingIntervalMS: RPC_SERVER_BLOCK_POLLING_INTERVAL_MS ? +RPC_SERVER_BLOCK_POLLING_INTERVAL_MS : 1000, }; } + +/** + * Returns package name and version. + */ +export function getPackageInfo() { + const packageJsonPath = resolve(dirname(fileURLToPath(import.meta.url)), '../../package.json'); + const { version, name } = JSON.parse(readFileSync(packageJsonPath).toString()); + return { version, name }; +} diff --git a/yarn-project/aztec-sandbox/package.json b/yarn-project/aztec-sandbox/package.json index 059da6ad785..e1554d4cc79 100644 --- a/yarn-project/aztec-sandbox/package.json +++ b/yarn-project/aztec-sandbox/package.json @@ -1,6 +1,6 @@ { "name": "@aztec/aztec-sandbox", - "version": "0.0.0", + "version": "0.1.0", "type": "module", "exports": { ".": "./dest/index.js", diff --git a/yarn-project/aztec-sandbox/src/index.ts b/yarn-project/aztec-sandbox/src/index.ts index 6b90b3bcd63..fb79a0b14d9 100644 --- a/yarn-project/aztec-sandbox/src/index.ts +++ b/yarn-project/aztec-sandbox/src/index.ts @@ -7,6 +7,9 @@ import { deployL1Contracts } from '@aztec/ethereum'; import { createDebugLogger } from '@aztec/foundation/log'; import { retryUntil } from '@aztec/foundation/retry'; +import { readFileSync } from 'fs'; +import { dirname, resolve } from 'path'; +import { fileURLToPath } from 'url'; import { HDAccount, createPublicClient, http as httpViemTransport } from 'viem'; import { mnemonicToAccount } from 'viem/accounts'; import { foundry } from 'viem/chains'; @@ -61,8 +64,10 @@ async function main() { const rpcConfig = getRpcConfigEnvVars(); const hdAccount = mnemonicToAccount(MNEMONIC); const privKey = hdAccount.getHdKey().privateKey; + const packageJsonPath = resolve(dirname(fileURLToPath(import.meta.url)), '../package.json'); + const version: string = JSON.parse(readFileSync(packageJsonPath).toString()).version; - logger.info('Setting up Aztec Sandbox, please stand by...'); + logger.info(`Setting up Aztec Sandbox v${version}, please stand by...`); logger.info('Deploying rollup contracts to L1...'); const deployedL1Contracts = await waitThenDeploy(aztecNodeConfig.rpcUrl, hdAccount); aztecNodeConfig.publisherPrivateKey = new PrivateKey(Buffer.from(privKey!)); diff --git a/yarn-project/aztec.js/src/contract/contract.test.ts b/yarn-project/aztec.js/src/contract/contract.test.ts index c29107f6baa..898f60b0dfa 100644 --- a/yarn-project/aztec.js/src/contract/contract.test.ts +++ b/yarn-project/aztec.js/src/contract/contract.test.ts @@ -26,7 +26,7 @@ describe('Contract Class', () => { const mockTxHash = { type: 'TxHash' } as any as TxHash; const mockTxReceipt = { type: 'TxReceipt' } as any as TxReceipt; const mockViewResultValue = 1; - const mockNodeInfo: NodeInfo = { version: 1, chainId: 2, rollupAddress: EthAddress.random() }; + const mockNodeInfo: NodeInfo = { version: 1, chainId: 2, rollupAddress: EthAddress.random(), client: '' }; const defaultAbi: ContractAbi = { name: 'FooContract', diff --git a/yarn-project/noir-compiler/package.json b/yarn-project/noir-compiler/package.json index c24df172a5a..c5d9365430f 100644 --- a/yarn-project/noir-compiler/package.json +++ b/yarn-project/noir-compiler/package.json @@ -1,6 +1,6 @@ { "name": "@aztec/noir-compiler", - "version": "0.0.0", + "version": "0.1.0", "type": "module", "exports": { ".": "./dest/index.js", diff --git a/yarn-project/prover-client/package.json b/yarn-project/prover-client/package.json index 6772f6dfe5c..19f2aec28b8 100644 --- a/yarn-project/prover-client/package.json +++ b/yarn-project/prover-client/package.json @@ -1,6 +1,6 @@ { "name": "@aztec/prover-client", - "version": "0.0.0", + "version": "0.1.0", "type": "module", "exports": "./dest/index.js", "typedocOptions": { diff --git a/yarn-project/rollup-provider/package.json b/yarn-project/rollup-provider/package.json index 5c41382358c..604418c7338 100644 --- a/yarn-project/rollup-provider/package.json +++ b/yarn-project/rollup-provider/package.json @@ -1,6 +1,6 @@ { "name": "@aztec/rollup-provider", - "version": "0.0.0", + "version": "0.1.0", "main": "dest/index.js", "type": "module", "exports": "./dest/index.js", diff --git a/yarn-project/types/src/interfaces/aztec_rpc.ts b/yarn-project/types/src/interfaces/aztec_rpc.ts index 8878021954a..3c4ce90c0e8 100644 --- a/yarn-project/types/src/interfaces/aztec_rpc.ts +++ b/yarn-project/types/src/interfaces/aztec_rpc.ts @@ -46,6 +46,10 @@ export type NodeInfo = { * The rollup contract address */ rollupAddress: EthAddress; + /** + * Identifier of the client software. + */ + client: string; }; /** Provides up to which block has been synced by different components. */ diff --git a/yarn-project/yarn.lock b/yarn-project/yarn.lock index 01ffd54c2dd..50e77824359 100644 --- a/yarn-project/yarn.lock +++ b/yarn-project/yarn.lock @@ -322,6 +322,8 @@ __metadata: "@types/node": ^18.7.23 commander: ^9.0.0 jest: ^29.5.0 + jest-mock-extended: ^3.0.5 + semver: ^7.5.4 ts-jest: ^29.1.0 ts-node: ^10.9.1 tslib: ^2.4.0 @@ -8065,6 +8067,18 @@ __metadata: languageName: node linkType: hard +"jest-mock-extended@npm:^3.0.5": + version: 3.0.5 + resolution: "jest-mock-extended@npm:3.0.5" + dependencies: + ts-essentials: ^7.0.3 + peerDependencies: + jest: ^24.0.0 || ^25.0.0 || ^26.0.0 || ^27.0.0 || ^28.0.0 || ^29.0.0 + typescript: ^3.0.0 || ^4.0.0 || ^5.0.0 + checksum: 440c52f743af588493c2cd02fa7e4e42177748ac3f7ae720f414bd58a4a72fad4271878457bf8796b62abcf9cf32cde4dc5151caad0805037bd965cc9ef07ca8 + languageName: node + linkType: hard + "jest-mock@npm:^29.6.2": version: 29.6.2 resolution: "jest-mock@npm:29.6.2"