diff --git a/.github/workflows/build-lint-test.yml b/.github/workflows/build-lint-test.yml index 164d6c94..0b711268 100644 --- a/.github/workflows/build-lint-test.yml +++ b/.github/workflows/build-lint-test.yml @@ -24,7 +24,7 @@ jobs: - prepare strategy: matrix: - node-version: [14.x, 16.x, 18.x, 19.x] + node-version: [16.x, 18.x, 19.x] steps: - uses: actions/checkout@v3 - name: Use Node.js ${{ matrix.node-version }} @@ -49,7 +49,7 @@ jobs: - prepare strategy: matrix: - node-version: [14.x, 16.x, 18.x, 19.x] + node-version: [16.x, 18.x, 19.x] steps: - uses: actions/checkout@v3 - name: Use Node.js ${{ matrix.node-version }} @@ -80,7 +80,7 @@ jobs: - prepare strategy: matrix: - node-version: [14.x, 16.x, 18.x, 19.x] + node-version: [16.x, 18.x, 19.x] steps: - uses: actions/checkout@v3 - name: Use Node.js ${{ matrix.node-version }} diff --git a/jest.config.js b/jest.config.js index 408e18b9..1d1ca437 100644 --- a/jest.config.js +++ b/jest.config.js @@ -23,5 +23,5 @@ module.exports = { restoreMocks: true, testEnvironment: 'node', testRegex: ['\\.test\\.(ts|js)$'], - testTimeout: 2500, + testTimeout: 5000, }; diff --git a/package.json b/package.json index e4955626..21730aef 100644 --- a/package.json +++ b/package.json @@ -39,8 +39,13 @@ "@metamask/eth-hd-keyring": "^6.0.0", "@metamask/eth-sig-util": "5.0.2", "@metamask/eth-simple-keyring": "^5.0.0", - "@metamask/utils": "^5.0.0", - "obs-store": "^4.0.3" + "@metamask/utils": "^5.0.1", + "@types/node": "^18.16.1", + "@types/uuid": "^9.0.1", + "ajv": "^8.12.0", + "ajv-formats": "^2.1.1", + "obs-store": "^4.0.3", + "uuid": "^9.0.0" }, "devDependencies": { "@lavamoat/allow-scripts": "^2.1.0", @@ -62,7 +67,7 @@ "eslint-plugin-node": "^11.1.0", "eslint-plugin-prettier": "^4.2.1", "ethereumjs-wallet": "^1.0.1", - "jest": "^29.0.0", + "jest": "^29.5.0", "prettier": "^2.8.1", "prettier-plugin-packagejson": "^2.3.0", "sinon": "^15.0.1", diff --git a/src/KeyringController.ts b/src/KeyringController.ts index 298dd20d..3ca937d6 100644 --- a/src/KeyringController.ts +++ b/src/KeyringController.ts @@ -970,7 +970,7 @@ class KeyringController extends EventEmitter { keyring = await this.#newKeyring(type, data); } catch (error) { // Ignore error. - console.error(error); + console.log(`Unable to restore '${type}' keyring`); } if (!keyring) { diff --git a/src/Vault.test.ts b/src/Vault.test.ts new file mode 100644 index 00000000..f8c3a530 --- /dev/null +++ b/src/Vault.test.ts @@ -0,0 +1,605 @@ +/* eslint-disable no-restricted-globals */ +import { Json } from '@metamask/utils'; +import { webcrypto as crypto } from 'crypto'; + +import { Vault, VaultState, exportedForTesting } from './Vault'; + +const { + b64Encode, + b64Decode, + stringToBytes, + bytesToString, + jsonToBytes, + randomBytes, + ensureLength, + ensureNotNull, + ensureBytes, + deriveWrappingKey, + generateMasterKey, + unwrapMasterKey, + deriveEncryptionKey, + encryptData, + decryptData, + reEncryptData, +} = exportedForTesting; + +/** + * Generate a random encryption key. + * + * @returns A Promise to a key handler. + */ +async function generateKey(): Promise { + return await crypto.subtle.generateKey( + { name: 'AES-GCM', length: 256 }, + true, + ['encrypt', 'decrypt'], + ); +} + +describe('b64Encode', () => { + it('should encode an empty Uint8Array', () => { + const input = new Uint8Array([]); + const result = b64Encode(input); + expect(result).toBe(''); + }); + + it('should encode a simple input', () => { + const input = new Uint8Array([0x68, 0x65, 0x6c, 0x6c, 0x6f]); + const result = b64Encode(input); + expect(result).toBe('aGVsbG8='); + }); + + it('should encode an input with special characters', () => { + const input = new Uint8Array([0xe2, 0x9c, 0x93]); + const result = b64Encode(input); + expect(result).toBe('4pyT'); + }); +}); + +describe('b64Decode', () => { + it('should decode an empty string', () => { + const input = ''; + const result = b64Decode(input); + expect(result).toStrictEqual(new Uint8Array([])); + }); + + it('should decode a simple input', () => { + const input = 'aGVsbG8='; + const result = b64Decode(input); + expect(result).toStrictEqual( + new Uint8Array([0x68, 0x65, 0x6c, 0x6c, 0x6f]), + ); + }); + + it('should decode an input with special characters', () => { + const input = '4pyT'; + const result = b64Decode(input); + expect(result).toStrictEqual(new Uint8Array([0xe2, 0x9c, 0x93])); + }); +}); + +describe('stringToBytes', () => { + it('should return an empty Uint8Array for an empty string', () => { + const result = stringToBytes(''); + expect(result).toStrictEqual(new Uint8Array()); + }); + + it('should encode ASCII characters correctly', () => { + const result = stringToBytes('hello world'); + const expected = new Uint8Array([ + 104, 101, 108, 108, 111, 32, 119, 111, 114, 108, 100, + ]); + expect(result).toStrictEqual(expected); + }); + + it('should encode non-ASCII characters correctly', () => { + const result = stringToBytes('øçñ'); + const expected = new Uint8Array([195, 184, 195, 167, 195, 177]); + expect(result).toStrictEqual(expected); + }); + + it('should normalize text in NFC form', () => { + const result = stringToBytes('é'); + const expected = new Uint8Array([195, 169]); + expect(result).toStrictEqual(expected); + }); +}); + +describe('bytesToString', () => { + it('should return an empty string for an empty Uint8Array', () => { + const result = bytesToString(new Uint8Array()); + expect(result).toBe(''); + }); + + it('should decode ASCII characters correctly', () => { + const data = new Uint8Array([ + 104, 101, 108, 108, 111, 32, 119, 111, 114, 108, 100, + ]); + const expected = 'hello world'; + const result = bytesToString(data); + expect(result).toStrictEqual(expected); + }); + + it('should decode non-ASCII characters correctly', () => { + const data = new Uint8Array([195, 184, 195, 167, 195, 177]); + const expected = 'øçñ'; + const result = bytesToString(data); + expect(result).toStrictEqual(expected); + }); + + it('should handle invalid byte sequences by replacing them with the replacement character', () => { + const data = new Uint8Array([194, 194]); + const expected = '��'; + const result = bytesToString(data); + expect(result).toStrictEqual(expected); + }); +}); + +describe('jsonToBytes', () => { + it('should encode JSON data to UTF-8 bytes', () => { + const data = { name: 'John', age: 30 }; + const expected = new Uint8Array([ + 123, 34, 110, 97, 109, 101, 34, 58, 34, 74, 111, 104, 110, 34, 44, 34, 97, + 103, 101, 34, 58, 51, 48, 125, + ]); + const result = jsonToBytes(data); + expect(result).toStrictEqual(expected); + }); + + it('should handle empty JSON data', () => { + const data = {}; + const expected = new Uint8Array([123, 125]); + const result = jsonToBytes(data); + expect(result).toStrictEqual(expected); + }); +}); + +describe('randomBytes', () => { + it('should return a Uint8Array with the specified length', () => { + const length = 16; + const result = randomBytes(length); + expect(result).toHaveLength(length); + }); + + it('should return different values for each invocation', () => { + const length = 16; + const result1 = randomBytes(length); + const result2 = randomBytes(length); + expect(result1).not.toStrictEqual(result2); + }); + + it('should throw an error if length is negative', () => { + const length = -1; + expect(() => { + randomBytes(length); + }).toThrow('Invalid typed array length'); + }); +}); + +describe('ensureLength', () => { + it('returns input when length matches', () => { + const input = new Uint8Array([1, 2, 3]); + const result = ensureLength(input, 3); + expect(result).toStrictEqual(input); + }); + + it('throws an error when length does not match', () => { + const input = new Uint8Array([1, 2, 3]); + expect(() => { + ensureLength(input, 4); + }).toThrow('Invalid length: expected 4, got 3'); + }); +}); + +describe('ensureNotNull', () => { + it('should return the value if it is not null', () => { + const value = 'hello'; + const result = ensureNotNull(value, 'Error message'); + expect(result).toBe(value); + }); + + it('should throw an error with the specified message if the value is null', () => { + const value = null; + const errorMessage = 'Error message'; + expect(() => ensureNotNull(value, errorMessage)).toThrow(errorMessage); + }); +}); + +describe('ensureBytes', () => { + it('should return a Uint8Array for a string input', () => { + const result = ensureBytes('hello'); + expect(result).toBeInstanceOf(Uint8Array); + }); + + it('should return the input value for a Uint8Array input', () => { + const input = new Uint8Array([0x68, 0x65, 0x6c, 0x6c, 0x6f]); + const result = ensureBytes(input); + expect(result).toStrictEqual(input); + }); + + it('should convert a string input to the correct bytes', () => { + const input = '✓'; + const result = ensureBytes(input); + const expected = new Uint8Array([0xe2, 0x9c, 0x93]); + expect(result).toStrictEqual(expected); + }); +}); + +describe('deriveWrappingKey', () => { + it('should derive a wrapping key and use it to encrypt a known text', async () => { + const pt = new Uint8Array([ + 0xb3, 0x4e, 0x75, 0x12, 0x9f, 0x2b, 0x15, 0x35, 0xa2, 0x95, 0x8c, 0xf3, + 0x83, 0xe2, 0xe2, 0x08, + ]); + + const iv = new Uint8Array([ + 0xd1, 0x37, 0x31, 0x96, 0x41, 0x9b, 0x0d, 0x80, 0x98, 0x7d, 0x57, 0x25, + 0x52, 0x31, 0x0e, 0xd2, + ]); + + const additionalData = stringToBytes('additionalData'); + + // key: 669cfe52482116fda1aa2cbe409b2f56c8e4563752b7a28f6eaab614ee005178 + // source: https://gchq.github.io/CyberChef/#recipe=Derive_PBKDF2_key(%7B'option':'UTF8','string':'password'%7D,256,600000,'SHA256',%7B'option':'UTF8','string':'salt'%7D) + const wrappingKey = await deriveWrappingKey( + 'password', + stringToBytes('salt'), + ); + + // source: https://gchq.github.io/CyberChef/#recipe=AES_Encrypt(%7B'option':'Hex','string':'669cfe52482116fda1aa2cbe409b2f56c8e4563752b7a28f6eaab614ee005178'%7D,%7B'option':'Hex','string':'d1373196419b0d80987d572552310ed2'%7D,'GCM','Hex','Hex',%7B'option':'UTF8','string':'additionalData'%7D)&input=YjM0ZTc1MTI5ZjJiMTUzNWEyOTU4Y2YzODNlMmUyMDg + const expected = new Uint8Array([ + 0x5e, 0x82, 0x04, 0x4f, 0xb7, 0x3e, 0xd3, 0xc7, 0xcb, 0x2a, 0xa7, 0xef, + 0xc2, 0xe2, 0x34, 0xa4, 0x9b, 0x75, 0x0e, 0xf2, 0x59, 0x25, 0x80, 0x43, + 0x47, 0x7f, 0xe2, 0x3f, 0x03, 0x37, 0xfe, 0xfd, + ]); + + const ct = new Uint8Array( + await crypto.subtle.encrypt( + { + name: 'AES-GCM', + iv, + additionalData, + }, + wrappingKey, + pt, + ), + ); + + expect(ct).toStrictEqual(expected); + }); +}); + +describe('generateMasterKey', () => { + it('should generate a new master key and wrap it', async () => { + // key: 669cfe52482116fda1aa2cbe409b2f56c8e4563752b7a28f6eaab614ee005178 + // source: https://gchq.github.io/CyberChef/#recipe=Derive_PBKDF2_key(%7B'option':'UTF8','string':'password'%7D,256,600000,'SHA256',%7B'option':'UTF8','string':'salt'%7D) + const wrappingKey = await deriveWrappingKey( + 'password', + stringToBytes('salt'), + ); + + const additionalData = stringToBytes('vault'); + const { wrapped, handler } = await generateMasterKey( + wrappingKey, + additionalData, + ); + expect(handler).toBeDefined(); + expect(wrapped).toBeDefined(); + expect(wrapped.nonce).toHaveLength(12); + expect(wrapped.data).toHaveLength(32 + 16); // key + tag + }); +}); + +describe('unwrapMasterKey', () => { + it('should generate and unwrap the same key', async () => { + // key: 669cfe52482116fda1aa2cbe409b2f56c8e4563752b7a28f6eaab614ee005178 + // source: https://gchq.github.io/CyberChef/#recipe=Derive_PBKDF2_key(%7B'option':'UTF8','string':'password'%7D,256,600000,'SHA256',%7B'option':'UTF8','string':'salt'%7D) + const wrappingKey = await deriveWrappingKey( + 'password', + stringToBytes('salt'), + ); + + const additionalData = stringToBytes('vault'); + const { wrapped, handler } = await generateMasterKey( + wrappingKey, + additionalData, + ); + + const unwrappedKey = await unwrapMasterKey( + wrappingKey, + wrapped, + additionalData, + ); + expect(unwrappedKey).toBeDefined(); + + // Derive keys from the unwrapped and from the generated master key, both + // derived keys should have the same value. + const salt = stringToBytes('salt'); + const encKey1 = await deriveEncryptionKey(handler, 'info', salt); + const encKey2 = await deriveEncryptionKey(unwrappedKey, 'info', salt); + + // To check if encKey1 and encKey2 have the same value, we encrypt a text + // using encKey1 and try to decrypt it using encKey2. + const data = stringToBytes('hello world'); + const ct = await encryptData(encKey1, data, stringToBytes('additional')); + const pt = await decryptData(encKey2, ct, stringToBytes('additional')); + expect(ct.nonce).toHaveLength(12); + expect(pt).toStrictEqual(data); + }); + + it('should fail if we try to unwrap the master key with the wrong password', async () => { + const wrappingKey1 = await deriveWrappingKey('foo', stringToBytes('salt')); + const wrappingKey2 = await deriveWrappingKey('bar', stringToBytes('salt')); + const { wrapped } = await generateMasterKey(wrappingKey1); + + await expect(unwrapMasterKey(wrappingKey2, wrapped)).rejects.toThrow( + 'The operation failed for an operation-specific reason', + ); + }); + + it('should succeed to unwrap the master key with the correct password', async () => { + const wrappingKey = await deriveWrappingKey('foo', stringToBytes('salt')); + const { wrapped } = await generateMasterKey(wrappingKey); + + expect(async () => { + await unwrapMasterKey(wrappingKey, wrapped); + }).not.toThrow(); + }); +}); + +describe('reEncryptData', () => { + it('should re-encrypt data', async () => { + const decryptionKey = await generateKey(); + const encryptionKey = await generateKey(); + const data = stringToBytes('test'); + const encrypted = await encryptData(decryptionKey, data); + const reEncrypted = await reEncryptData( + decryptionKey, + encryptionKey, + encrypted, + ); + const decrypted = await decryptData(encryptionKey, reEncrypted); + expect(decrypted).toStrictEqual(data); + }); + + it('should re-encrypt data with additional data', async () => { + const decryptionKey = await generateKey(); + const encryptionKey = await generateKey(); + const data = stringToBytes('test'); + const additionalData = stringToBytes('metadata'); + const encrypted = await encryptData(decryptionKey, data, additionalData); + const reEncrypted = await reEncryptData( + decryptionKey, + encryptionKey, + encrypted, + additionalData, + ); + const decrypted = await decryptData( + encryptionKey, + reEncrypted, + additionalData, + ); + expect(decrypted).toStrictEqual(data); + }); +}); + +describe('Vault', () => { + let vault: Vault; + + beforeEach(() => { + vault = new Vault(); + }); + + it('should check if the vault was created uninitialized', () => { + expect(vault.isInitialized).toBe(false); + expect(vault.isUnlocked).toBe(false); + }); + + it('should initialize the vault', async () => { + await vault.unlock('password'); + expect(vault.isInitialized).toBe(true); + expect(vault.isUnlocked).toBe(true); + }); + + it('should lock the vault', async () => { + await vault.unlock('password'); + vault.lock(); + expect(vault.isInitialized).toBe(true); + expect(vault.isUnlocked).toBe(false); + }); + + it('should fail if we try to store a value in an uninitialized vault', async () => { + const value = { keyring: 'test' }; + await expect(vault.set('keyring', value)).rejects.toThrow( + 'Vault is not initialized', + ); + }); + + it('should fail if we try to store a value in a locked vault', async () => { + await vault.unlock('password'); + vault.lock(); + + const value = { keyring: 'test' }; + await expect(vault.set('keyring', value)).rejects.toThrow( + 'Vault is locked', + ); + }); + + it('should fail if we try to read a value from an uninitialized vault', async () => { + await expect(vault.get('keyring')).rejects.toThrow( + 'Vault is not initialized', + ); + }); + + it('should fail if we try to read a value from a locked vault', async () => { + await vault.unlock('password'); + vault.lock(); + await expect(vault.get('keyring')).rejects.toThrow('Vault is locked'); + }); + + it('should be possible to set and get a value', async () => { + await vault.unlock('password'); + const value = { keyring: 'test' }; + await vault.set('keyring', value); + expect(await vault.get('keyring')).toStrictEqual(value); + }); + + it('should be possible to set, lock, unlock, and get a value', async () => { + await vault.unlock('password'); + const value = { keyring: 'test' }; + await vault.set('keyring', value); + vault.lock(); + await vault.unlock('password'); + expect(await vault.get('keyring')).toStrictEqual(value); + }); + + it('should return undefined if we try to get a key that does not exist', async () => { + await vault.unlock('password'); + expect(await vault.get('foo')).toBeUndefined(); + }); + + it('should have a key that was previouslly inserted', async () => { + await vault.unlock('password'); + await vault.set('keyring', { keyring: 'test' }); + expect(vault.has('keyring')).toBe(true); + }); + + it('should not have a key that was not inserted', async () => { + await vault.unlock('password'); + expect(vault.has('keyring')).toBe(false); + }); + + it('should not have a key after it is deleted', async () => { + await vault.unlock('password'); + await vault.set('keyring', { keyring: 'test' }); + expect(vault.has('keyring')).toBe(true); + vault.delete('keyring'); + expect(vault.has('keyring')).toBe(false); + }); + + it('should be possible to update an existing entry', async () => { + await vault.unlock('password'); + await vault.set('keyring', { keyring: 'foo' }); + await vault.set('keyring', { keyring: 'bar' }); + expect(await vault.get('keyring')).toStrictEqual({ keyring: 'bar' }); + }); + + it('should fail to unlock a vault using a wrong password', async () => { + await vault.unlock('password'); + vault.lock(); + await expect(vault.unlock('foobar')).rejects.toThrow( + 'Invalid vault password', + ); + }); + + it('should fail to verify the password on an uninitialized vault', async () => { + expect(await vault.verifyPassword('foo')).toBe(false); + }); + + it('should successful verify the password if it is correct', async () => { + await vault.unlock('foo'); + expect(await vault.verifyPassword('foo')).toBe(true); + }); + + it('should fail to verify the password if it is incorrect', async () => { + await vault.unlock('foo'); + expect(await vault.verifyPassword('bar')).toBe(false); + }); + + it('should unlock the vault after the correct password is presented', async () => { + await vault.unlock('foo'); + vault.lock(); + expect(vault.isUnlocked).toBe(false); + await vault.verifyPassword('foo'); + expect(vault.isUnlocked).toBe(false); + }); + + it('should unlock the vault after a incorrect password is presented', async () => { + await vault.unlock('foo'); + vault.lock(); + expect(vault.isUnlocked).toBe(false); + await vault.verifyPassword('bar'); + expect(vault.isUnlocked).toBe(false); + }); + + it('should change the vault password', async () => { + await vault.unlock('foo'); + await vault.changePassword('foo', 'bar'); + expect(await vault.verifyPassword('foo')).toBe(false); + expect(await vault.verifyPassword('bar')).toBe(true); + }); + + it('should change the password of a locked vault', async () => { + await vault.unlock('foo'); + vault.lock(); + await vault.changePassword('foo', 'bar'); + expect(await vault.verifyPassword('foo')).toBe(false); + expect(await vault.verifyPassword('bar')).toBe(true); + }); + + it('should be possible to get a value after a password change', async () => { + await vault.unlock('foo'); + const value = { example: 123 }; + await vault.set('test', value); + await vault.changePassword('foo', 'bar'); + expect(await vault.get('test')).toStrictEqual(value); + }); + + it('should be possible to get a value after rekeying the vault', async () => { + await vault.unlock('foo'); + const value = { example: 123 }; + await vault.set('test', value); + await vault.rekey('foo'); + expect(await vault.get('test')).toStrictEqual(value); + }); + + it('should serialize and deserialize a vault', async () => { + await vault.unlock('foo'); + const value1 = { answer: 42 }; + const value2 = { answer: 42, verified: true }; + await vault.set('test-1', value1); + await vault.set('test-2', value2); + + const serialized = vault.getState(); + const newVault = new Vault(serialized); + await newVault.unlock('foo'); + expect(await newVault.get('test-1')).toStrictEqual(value1); + expect(await newVault.get('test-2')).toStrictEqual(value2); + expect(newVault.getState()).toStrictEqual(serialized); + }); + + it('should deserialize a vault from JSON', async () => { + await vault.unlock('foo'); + await vault.set('test', { answer: 42 }); + const state = vault.getState(); + const newVault = new Vault(JSON.parse(JSON.stringify(state))); + await newVault.unlock('foo'); + expect(await newVault.get('test')).toStrictEqual({ answer: 42 }); + }); + + it('should fail to import a state with an invalid version', async () => { + await vault.unlock('foo'); + await vault.set('test', { answer: 42 }); + const state = { ...vault.getState(), version: 1 } as unknown as VaultState; + expect(() => new Vault(state)).toThrow('Invalid vault state'); + }); + + it('should fail to import a state with an invalid base64 value', async () => { + await vault.unlock('foo'); + await vault.set('test', { answer: 42 }); + const state = vault.getState(); + state.key.data = 'invalid-base64-value'; + expect(() => new Vault(state)).toThrow('Invalid vault state'); + }); + + it('should fail to import a state with an invalid date', async () => { + await vault.unlock('foo'); + await vault.set('test', { answer: 42 }); + const state = vault.getState(); + expect(state.entries.test).toBeDefined(); + if (state.entries.test !== undefined) { + state.entries.test.createdAt = 'invalid-date-time-value'; + } + expect(() => new Vault(state)).toThrow('Invalid vault state'); + }); +}); diff --git a/src/Vault.ts b/src/Vault.ts new file mode 100644 index 00000000..8deab789 --- /dev/null +++ b/src/Vault.ts @@ -0,0 +1,868 @@ +/* eslint-disable no-restricted-globals */ +import { Json } from '@metamask/utils'; +import Ajv, { JSONSchemaType } from 'ajv'; +import addFormats from 'ajv-formats'; +// eslint-disable-next-line import/no-nodejs-modules +import { webcrypto as crypto } from 'crypto'; +import { v4 as uuid } from 'uuid'; + +const ajv = new Ajv(); +addFormats(ajv); + +// ---------------------------------------------------------------------------- +// Types + +type EncryptedData = { + nonce: Uint8Array; + data: Uint8Array; +}; + +type VaultEntry = { + id: string; + modifiedAt: Date; + createdAt: Date; + value: EncryptedData; +}; + +export type EncryptedDataState = { nonce: string; data: string }; + +export type VaultEntryState = { + id: string; + value: EncryptedDataState; + createdAt: string; + modifiedAt: string; +}; + +export type VaultState = { + version: '2'; + id: string; + salt: string; + key: EncryptedDataState; + entries: Record; +}; + +export class VaultError extends Error {} + +// ---------------------------------------------------------------------------- +// Schemas + +const vaultStateSchema: JSONSchemaType = { + type: 'object', + properties: { + version: { + type: 'string', + enum: ['2'], + }, + id: { + type: 'string', + format: 'uuid', + }, + salt: { + type: 'string', + format: 'byte', + }, + key: { + type: 'object', + properties: { + nonce: { + type: 'string', + format: 'byte', + }, + data: { + type: 'string', + format: 'byte', + }, + }, + required: ['nonce', 'data'], + }, + entries: { + type: 'object', + patternProperties: { + // eslint-disable-next-line @typescript-eslint/naming-convention + '^[a-zA-Z0-9-_]{1,64}$': { + type: 'object', + properties: { + id: { + type: 'string', + format: 'uuid', + }, + value: { + type: 'object', + properties: { + nonce: { + type: 'string', + format: 'byte', + }, + data: { + type: 'string', + format: 'byte', + }, + }, + required: ['nonce', 'data'], + }, + createdAt: { + type: 'string', + format: 'date-time', + }, + modifiedAt: { + type: 'string', + format: 'date-time', + }, + }, + required: ['id', 'value', 'createdAt', 'modifiedAt'], + }, + }, + required: [], + }, + }, + required: ['version', 'id', 'salt', 'key', 'entries'], +}; + +// ---------------------------------------------------------------------------- +// Util functions + +/** + * Encode binary data in base64. + * + * @param data - Data to encode. + * @returns The encoded data. + */ +function b64Encode(data: Uint8Array): string { + return Buffer.from(data).toString('base64'); +} + +/** + * Decode binary data from a base64 string. + * + * @param data - Encoded data. + * @returns The decoded data. + */ +function b64Decode(data: string): Uint8Array { + return Uint8Array.from(Buffer.from(data, 'base64')); +} + +/** + * Convert a string to bytes. + * + * @param text - Text to convert. + * @returns Bytes representing the text. + */ +function stringToBytes(text: string): Uint8Array { + const encoder = new TextEncoder(); + return encoder.encode(text.normalize('NFC')); +} + +/** + * Decodes a byte array into a string. + * + * @param data - Bytes to decode. + * @returns A string from the bytes. + */ +function bytesToString(data: Uint8Array): string { + const decoder = new TextDecoder(); + return decoder.decode(data); +} + +/** + * Convert a JSON object to bytes. + * + * @param data - Object to convert. + * @returns Bytes representing the JSON object. + */ +function jsonToBytes(data: Json): Uint8Array { + return stringToBytes(JSON.stringify(data)); +} + +/** + * Ensure that a value is not null. + * + * @param value - Value to check. + * @param cause - Error cause in case value is null. + * @returns The value if it is not null. + */ +function ensureNotNull(value: T | null, cause: string): T { + if (value === null) { + throw new VaultError(cause); + } + return value; +} + +/** + * Ensure that a Uint8Array has the expected length. + * + * @param data - Data array. + * @param length - Expected length. + * @returns The same data array. + */ +function ensureLength(data: Uint8Array, length: number): Uint8Array { + if (data.length !== length) { + throw new VaultError( + `Invalid length: expected ${length}, got ${data.length}`, + ); + } + return data; +} + +/** + * Ensure that a value is a Uint8Array. + * + * @param value - Value to check or convert. + * @returns A value whose type is Uint8Array. + */ +function ensureBytes(value: string | Uint8Array): Uint8Array { + if (typeof value === 'string') { + return stringToBytes(value); + } + return value; +} + +/** + * Encode an EncryptedData object. + * + * @param data - Encrypted data. + * @returns The encoded encrypted data. + */ +function encodeEncryptedData(data: EncryptedData): EncryptedDataState { + return { + nonce: b64Encode(data.nonce), + data: b64Encode(data.data), + }; +} + +/** + * Decode a serialized EncryptedData object. + * + * @param state - Encoded encrypted data. + * @returns A decoded EncryptedData object. + */ +function decodeEncryptedData(state: EncryptedDataState): EncryptedData { + return { + nonce: ensureLength(b64Decode(state.nonce), 12), + data: b64Decode(state.data), + }; +} + +// ---------------------------------------------------------------------------- +// Crypto functions + +/** + * Generate cryptographically secure random bytes. + * + * @param length - Number of bytes to generate. + * @returns Cryptographically secure random bytes. + */ +function randomBytes(length: number): Uint8Array { + return crypto.getRandomValues(new Uint8Array(length)); +} + +/** + * Import a password as a raw key. + * + * @param password - Password to import. + * @returns Password as a raw key. + */ +async function importPassword(password: string): Promise { + return crypto.subtle.importKey( + 'raw', + stringToBytes(password), + 'PBKDF2', + false, + ['deriveKey'], + ); +} + +/** + * Derive a wrapping key from a password and salt. + * + * The derived wrapping key actually have the encrypt and decrypt key usages + * due to a limitation in Web Crypto API: the API doesn't allow KDF keys to be + * generated, wrapped, nor unwrapped. So, as a workaround, we generate random + * bits and import them as the master key. + * + * @param password - Password to derive the wrapping key from. + * @param salt - Salt to be used in the key derivation. + * @returns A wrapping key derived from the password and salt. + */ +async function deriveWrappingKey( + password: string, + salt: Uint8Array, +): Promise { + return crypto.subtle.deriveKey( + { + name: 'PBKDF2', + hash: 'SHA-256', + salt, + iterations: 600_000, + }, + await importPassword(password), + { + name: 'AES-GCM', + length: 256, + }, + false, + ['encrypt', 'decrypt'], + ); +} + +/** + * Generate and wrap a random master key. + * + * @param wrappingKey - Wrapping key handler. + * @param additionalData - Additional data. + * @returns The wrapped master key and its handler. + */ +async function generateMasterKey( + wrappingKey: CryptoKey, + additionalData?: Uint8Array, +): Promise<{ + wrapped: EncryptedData; + handler: CryptoKey; +}> { + const rawKey = randomBytes(32); + const wrappedKey = encryptData(wrappingKey, rawKey, additionalData); + + return { + wrapped: await wrappedKey, + handler: await crypto.subtle.importKey('raw', rawKey, 'HKDF', false, [ + 'deriveKey', + ]), + }; +} + +/** + * Unwrap and import the master key. + * + * @param unwrappingKey - Unwrapping key handler. + * @param wrappedKey - Wrapped key data. + * @param additionalData - Additional data. + * @returns Handler to the master key. + */ +async function unwrapMasterKey( + unwrappingKey: CryptoKey, + wrappedKey: EncryptedData, + additionalData?: Uint8Array, +): Promise { + const rawKey = await decryptData(unwrappingKey, wrappedKey, additionalData); + return crypto.subtle.importKey('raw', rawKey, 'HKDF', false, ['deriveKey']); +} + +/** + * Unwrap and re-wrap a key. + * + * @param unwrappingKey - Unwrapping key. + * @param wrappingKey - Wrapping key. + * @param wrappedKey - Key to re-wrap. + * @param additionalData - Additional data. + * @returns The re-wrapped key. + */ +async function reWrapMasterKey( + unwrappingKey: CryptoKey, + wrappingKey: CryptoKey, + wrappedKey: EncryptedData, + additionalData?: Uint8Array, +): Promise { + const rawKey = await decryptData(unwrappingKey, wrappedKey, additionalData); + return encryptData(wrappingKey, rawKey, additionalData); +} + +/** + * Derive an encryption key from the master key. + * + * @param masterKey - Master key to derived from. + * @param info - Derivation information. + * @param salt - Optional salt to be used in the derivation. + * @returns The handler to the derived key. + */ +async function deriveEncryptionKey( + masterKey: CryptoKey, + info: string | Uint8Array, + salt?: Uint8Array, +): Promise { + return crypto.subtle.deriveKey( + { + name: 'HKDF', + hash: 'SHA-256', + info: ensureBytes(info), + salt: salt ?? new Uint8Array(), + }, + masterKey, + { + name: 'AES-GCM', + length: 256, + }, + false, + ['encrypt', 'decrypt'], + ); +} + +/** + * Encrypt data with additional data. + * + * @param key - Encryption key handler. + * @param data - Data to encrypt. + * @param additionalData - Associated data. + * @returns An object containing the nonce and the encrypted data. + */ +async function encryptData( + key: CryptoKey, + data: Uint8Array, + additionalData?: Uint8Array, +): Promise { + const iv = randomBytes(12); + const ct = crypto.subtle.encrypt( + { + name: 'AES-GCM', + iv, + additionalData, + tagLength: 128, + }, + key, + data, + ); + + return { nonce: iv, data: new Uint8Array(await ct) }; +} + +/** + * Decrypt data with additional data. + * + * @param key - Decryption key handler. + * @param ciphertext - Ciphertext object with nonce and data. + * @param additionalData - Additional data. + * @returns The decrypted data. + */ +async function decryptData( + key: CryptoKey, + ciphertext: EncryptedData, + additionalData?: Uint8Array, +): Promise { + return new Uint8Array( + await crypto.subtle.decrypt( + { + name: 'AES-GCM', + iv: ciphertext.nonce, + additionalData, + tagLength: 128, + }, + key, + ciphertext.data, + ), + ); +} + +/** + * Decrypt and re-encrypt data. + * + * @param decryptionKey - Decryption key. + * @param encryptionKey - Encryption key. + * @param ciphertext - Encrypted data. + * @param additionalData - Additional data. + * @returns The re-encrypted data. + */ +async function reEncryptData( + decryptionKey: CryptoKey, + encryptionKey: CryptoKey, + ciphertext: EncryptedData, + additionalData?: Uint8Array, +): Promise { + const data = await decryptData(decryptionKey, ciphertext, additionalData); + return encryptData(encryptionKey, data, additionalData); +} + +// ---------------------------------------------------------------------------- +// Main class + +export class Vault { + public readonly id: string; + + #entries: Map; + + #passwordSalt: Uint8Array; + + #wrappedMasterKey: EncryptedData | null; + + #masterKeyHandler: CryptoKey | null; + + static readonly validate = ajv.compile(vaultStateSchema); + + /** + * Create a new vault. + * + * If a state is given, the vault will be restored form it, otherwise, a new + * vault will be created. + * + * @param state - Existing serialized vault state. + */ + constructor(state?: VaultState) { + this.#entries = new Map(); + + if (state === undefined) { + this.id = uuid(); + this.#passwordSalt = randomBytes(32); + this.#wrappedMasterKey = null; + } else { + if (!Vault.validate(state)) { + throw new VaultError('Invalid vault state'); + } + + this.id = state.id; + this.#passwordSalt = ensureLength(b64Decode(state.salt), 32); + this.#wrappedMasterKey = decodeEncryptedData(state.key); + for (const [key, entry] of Object.entries(state.entries)) { + this.#entries.set(key, { + ...entry, + createdAt: new Date(entry.createdAt), + modifiedAt: new Date(entry.modifiedAt), + value: decodeEncryptedData(entry.value), + }); + } + } + + this.#masterKeyHandler = null; + } + + /** + * Initialize the vault after its creation. + * + * This method MUST to be called after the vault creation, otherwise the + * master key will not be generated. + * + * @param password - Vault password. + */ + async #init(password: string): Promise { + // istanbul ignore next: This check should always be false, but we leave it + // as a fail safe to prevent the master key from being overwritten. + if (this.isInitialized) { + throw new VaultError('Vault is already initialized'); + } + + const wrappingKey = await deriveWrappingKey(password, this.#passwordSalt); + ({ wrapped: this.#wrappedMasterKey, handler: this.#masterKeyHandler } = + await generateMasterKey(wrappingKey, this.#getAdditionalData())); + } + + /** + * Check if the vault is unlocked. + * + * @returns True if the vault is unlocked, false otherwise. + */ + get isUnlocked(): boolean { + return this.#masterKeyHandler !== null; + } + + /** + * Check if the vault was initialized. + * + * @returns True if the vault was initialized, false otherwise. + */ + get isInitialized(): boolean { + return this.#wrappedMasterKey !== null; + } + + /** + * Get the wrapped master key. + * + * This method will throw an error if the vault wasn't initialized. + * + * @returns The wrapped master key. + */ + #getWrappedMasterKey(): EncryptedData { + return ensureNotNull(this.#wrappedMasterKey, 'Vault is not initialized'); + } + + /** + * Get the master key handler. + * + * This method will throw an error if the vault is locked. + * + * @returns The master key handler. + */ + #getCachedMasterKey(): CryptoKey { + return ensureNotNull(this.#masterKeyHandler, 'Vault is locked'); + } + + /** + * Assert that the vault is initialized. + */ + #assertIsInitialized(): void { + this.#getWrappedMasterKey(); + } + + /** + * Assert that the vault is unlocked. + */ + #assertIsUnlocked(): void { + this.#getCachedMasterKey(); + } + + /** + * Assert that the vault is initialized and unlocked. + */ + #assertIsOperational(): void { + this.#assertIsInitialized(); + this.#assertIsUnlocked(); + } + + /** + * Add a new value to the vault. + * + * @param key - Key to store the value under. + * @param value - Value to be encrypted and added to the vault. + */ + async set(key: string, value: Value): Promise { + this.#assertIsOperational(); + + const now = new Date(); + const current = this.#entries.get(key); + const entryId = current?.id ?? uuid(); + const encryptionKey = await this.#deriveEncryptionKey(entryId, key); + + this.#entries.set(key, { + id: entryId, + value: await encryptData(encryptionKey, jsonToBytes(value)), + createdAt: current?.createdAt ?? now, + modifiedAt: now, + }); + } + + /** + * Get the value associated with a key. + * + * @param key - The key to get the value of. + * @returns The value associated with the key or undefined if the key does + * not exist. + */ + async get(key: string): Promise { + this.#assertIsOperational(); + + // Return undefined if the key does not exist. + const entry = this.#entries.get(key); + if (entry === undefined) { + return undefined; + } + + // Decrypt and parse the value back to an object. + const decryptionKey = await this.#deriveEncryptionKey(entry.id, key); + const data = await decryptData(decryptionKey, entry.value); + return JSON.parse(bytesToString(data)); + } + + /** + * Check if a key is present in the vault. + * + * @param key - Key to be checked. + * @returns True if the key is present, false otherwise. + */ + has(key: string): boolean { + this.#assertIsOperational(); + return this.#entries.has(key); + } + + /** + * Delete a vault entry. + * + * @param key - The key to delete. + * @returns True if the entry existed, false otherwise. + */ + delete(key: string): boolean { + this.#assertIsOperational(); + return this.#entries.delete(key); + } + + /** + * Change the vault master key. + * + * @param password - Vault password. + */ + async rekey(password: string): Promise { + const wrappingKey = await deriveWrappingKey(password, this.#passwordSalt); + const { wrapped: mkWrapped, handler: mkHandler } = await generateMasterKey( + wrappingKey, + this.#getAdditionalData(), + ); + + const newEntries = new Map(); + for (const [key, entry] of this.#entries.entries()) { + newEntries.set(key, { + ...entry, + value: await reEncryptData( + await this.#deriveEncryptionKey(entry.id, key), + await this.#deriveEncryptionKey(entry.id, key, mkHandler), + entry.value, + ), + }); + } + + // Update all fields "at once". + this.#masterKeyHandler = mkHandler; + this.#wrappedMasterKey = mkWrapped; + this.#entries = newEntries; + } + + /** + * Change the vault password and salt. + * + * @param oldPassword - Current password. + * @param newPassword - New password. + */ + async changePassword( + oldPassword: string, + newPassword: string, + ): Promise { + const oldWrappingKey = await deriveWrappingKey( + oldPassword, + this.#passwordSalt, + ); + + const newPasswordSalt = randomBytes(32); + const newWrappingKey = await deriveWrappingKey( + newPassword, + newPasswordSalt, + ); + + // Update the password salt _after_ setting the wrapped master key. + this.#wrappedMasterKey = await reWrapMasterKey( + oldWrappingKey, + newWrappingKey, + this.#getWrappedMasterKey(), + this.#getAdditionalData(), + ); + this.#passwordSalt = newPasswordSalt; + } + + /** + * Lock the vault. + * + * Note from the Web Crypto API specification: + * + * > This specification places no normative requirements on how + * > implementations handle key material once all references to it go away. + * > That is, conforming user agents are not required to zeroize key + * > material, and it may still be accessible on device storage or device + * > memory, even after all references to the CryptoKey have gone away. + */ + lock(): void { + this.#masterKeyHandler = null; + } + + /** + * Unlock the vault and cache the master key. + * + * Note that unlocking will also initialize an uninitialized vault. + * + * @param password - Password to unlock the vault. + * @param testOnly - Try to unlock the vault but don't cache the master key. + */ + async unlock(password: string, testOnly = false): Promise { + if (!this.isInitialized && !testOnly) { + await this.#init(password); + } + + // We must get the wrapped master key _outside_ the try-catch block below + // to distinguish an uninitialized vault from a wrong password. + const wrappedMasterKey = this.#getWrappedMasterKey(); + const wrappingKey = await deriveWrappingKey(password, this.#passwordSalt); + + try { + const masterKey = await unwrapMasterKey( + wrappingKey, + wrappedMasterKey, + this.#getAdditionalData(), + ); + + if (!testOnly) { + this.#masterKeyHandler = masterKey; + } + } catch (error) { + throw new VaultError('Invalid vault password'); + } + } + + /** + * Check if the provided password is correct. + * + * @param password - Password to verify. + * @returns True if the password is correct, false otherwise. + */ + async verifyPassword(password: string): Promise { + try { + await this.unlock(password, true); + return true; + } catch (error) { + return false; + } + } + + /** + * Derive the master key given a derivation information. + * + * If a master key is provided, it will be used instead of the cached master + * key. + * + * @param entryId - ID of the vault entry. + * @param key - Key of the vault entry. + * @param masterKey - Optional master key. + * @returns The handler to the derived key. + */ + async #deriveEncryptionKey( + entryId: string, + key: string, + masterKey?: CryptoKey, + ): Promise { + return deriveEncryptionKey( + masterKey ?? this.#getCachedMasterKey(), + `metamask:vault:${this.id}:entry:${entryId}:key:${key}`, + ); + } + + /** + * Get the vault's serialized state. + * + * @returns The vault's serialized state. + */ + getState(): VaultState { + // Create a map with the serialized vault entries. + const entries = new Map(); + for (const [key, entry] of this.#entries.entries()) { + entries.set(key, { + ...entry, + value: encodeEncryptedData(entry.value), + modifiedAt: entry.modifiedAt.toISOString(), + createdAt: entry.modifiedAt.toISOString(), + }); + } + + return { + version: '2', + id: this.id, + salt: b64Encode(this.#passwordSalt), + key: encodeEncryptedData(this.#getWrappedMasterKey()), + entries: Object.fromEntries(entries), + }; + } + + #getAdditionalData(): Uint8Array { + return jsonToBytes(['metamask', 'vault', 'version', 2, 'id', this.id]); + } +} + +export const exportedForTesting = { + b64Encode, + b64Decode, + stringToBytes, + bytesToString, + jsonToBytes, + randomBytes, + ensureLength, + ensureNotNull, + ensureBytes, + generateMasterKey, + importPassword, + deriveWrappingKey, + unwrapMasterKey, + encryptData, + decryptData, + deriveEncryptionKey, + reEncryptData, + reWrapMasterKey, +}; diff --git a/src/index.ts b/src/index.ts index 68943618..c4771b0a 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,3 +1,5 @@ export { KeyringController, keyringBuilderFactory } from './KeyringController'; export { KeyringType, KeyringControllerError } from './constants'; + +export { Vault } from './Vault'; diff --git a/yarn.lock b/yarn.lock index adfff89e..015c6a23 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1008,50 +1008,50 @@ __metadata: languageName: node linkType: hard -"@jest/console@npm:^29.4.3": - version: 29.4.3 - resolution: "@jest/console@npm:29.4.3" +"@jest/console@npm:^29.5.0": + version: 29.5.0 + resolution: "@jest/console@npm:29.5.0" dependencies: - "@jest/types": ^29.4.3 + "@jest/types": ^29.5.0 "@types/node": "*" chalk: ^4.0.0 - jest-message-util: ^29.4.3 - jest-util: ^29.4.3 + jest-message-util: ^29.5.0 + jest-util: ^29.5.0 slash: ^3.0.0 - checksum: 8d9b163febe735153b523db527742309f4d598eda22f17f04e030060329bd3da4de7420fc1f7812f7a16f08273654a7de094c4b4e8b81a99dbfc17cfb1629008 + checksum: 9f4f4b8fabd1221361b7f2e92d4a90f5f8c2e2b29077249996ab3c8b7f765175ffee795368f8d6b5b2bb3adb32dc09319f7270c7c787b0d259e624e00e0f64a5 languageName: node linkType: hard -"@jest/core@npm:^29.4.3": - version: 29.4.3 - resolution: "@jest/core@npm:29.4.3" +"@jest/core@npm:^29.5.0": + version: 29.5.0 + resolution: "@jest/core@npm:29.5.0" dependencies: - "@jest/console": ^29.4.3 - "@jest/reporters": ^29.4.3 - "@jest/test-result": ^29.4.3 - "@jest/transform": ^29.4.3 - "@jest/types": ^29.4.3 + "@jest/console": ^29.5.0 + "@jest/reporters": ^29.5.0 + "@jest/test-result": ^29.5.0 + "@jest/transform": ^29.5.0 + "@jest/types": ^29.5.0 "@types/node": "*" ansi-escapes: ^4.2.1 chalk: ^4.0.0 ci-info: ^3.2.0 exit: ^0.1.2 graceful-fs: ^4.2.9 - jest-changed-files: ^29.4.3 - jest-config: ^29.4.3 - jest-haste-map: ^29.4.3 - jest-message-util: ^29.4.3 + jest-changed-files: ^29.5.0 + jest-config: ^29.5.0 + jest-haste-map: ^29.5.0 + jest-message-util: ^29.5.0 jest-regex-util: ^29.4.3 - jest-resolve: ^29.4.3 - jest-resolve-dependencies: ^29.4.3 - jest-runner: ^29.4.3 - jest-runtime: ^29.4.3 - jest-snapshot: ^29.4.3 - jest-util: ^29.4.3 - jest-validate: ^29.4.3 - jest-watcher: ^29.4.3 + jest-resolve: ^29.5.0 + jest-resolve-dependencies: ^29.5.0 + jest-runner: ^29.5.0 + jest-runtime: ^29.5.0 + jest-snapshot: ^29.5.0 + jest-util: ^29.5.0 + jest-validate: ^29.5.0 + jest-watcher: ^29.5.0 micromatch: ^4.0.4 - pretty-format: ^29.4.3 + pretty-format: ^29.5.0 slash: ^3.0.0 strip-ansi: ^6.0.0 peerDependencies: @@ -1059,19 +1059,19 @@ __metadata: peerDependenciesMeta: node-notifier: optional: true - checksum: 4aa10644d66f44f051d5dd9cdcedce27acc71216dbcc5e7adebdea458e27aefe27c78f457d7efd49f58b968c35f42de5a521590876e2013593e675120b9e6ab1 + checksum: 9e8f5243fe82d5a57f3971e1b96f320058df7c315328a3a827263f3b17f64be10c80f4a9c1b1773628b64d2de6d607c70b5b2d5bf13e7f5ad04223e9ef6aac06 languageName: node linkType: hard -"@jest/environment@npm:^29.4.3": - version: 29.4.3 - resolution: "@jest/environment@npm:29.4.3" +"@jest/environment@npm:^29.5.0": + version: 29.5.0 + resolution: "@jest/environment@npm:29.5.0" dependencies: - "@jest/fake-timers": ^29.4.3 - "@jest/types": ^29.4.3 + "@jest/fake-timers": ^29.5.0 + "@jest/types": ^29.5.0 "@types/node": "*" - jest-mock: ^29.4.3 - checksum: 7c1b0cc4e84b90f8a3bbeca9bbf088882c88aee70a81b3b8e24265dcb1cbc302cd1eee3319089cf65bfd39adbaea344903c712afea106cb8da6c86088d99c5fb + jest-mock: ^29.5.0 + checksum: 921de6325cd4817dec6685e5ff299b499b6379f3f9cf489b4b13588ee1f3820a0c77b49e6a087996b6de8f629f6f5251e636cba08d1bdb97d8071cc7d033c88a languageName: node linkType: hard @@ -1084,51 +1084,60 @@ __metadata: languageName: node linkType: hard -"@jest/expect@npm:^29.4.3": - version: 29.4.3 - resolution: "@jest/expect@npm:29.4.3" +"@jest/expect-utils@npm:^29.5.0": + version: 29.5.0 + resolution: "@jest/expect-utils@npm:29.5.0" dependencies: - expect: ^29.4.3 - jest-snapshot: ^29.4.3 - checksum: 08d0d40077ec99a7491fe59d05821dbd31126cfba70875855d8a063698b7126b5f6c309c50811caacc6ae2f727c6e44f51bdcf1d6c1ea832b4f020045ef22d45 + jest-get-type: ^29.4.3 + checksum: c46fb677c88535cf83cf29f0a5b1f376c6a1109ddda266ad7da1a9cbc53cb441fa402dd61fc7b111ffc99603c11a9b3357ee41a1c0e035a58830bcb360871476 languageName: node linkType: hard -"@jest/fake-timers@npm:^29.4.3": - version: 29.4.3 - resolution: "@jest/fake-timers@npm:29.4.3" +"@jest/expect@npm:^29.5.0": + version: 29.5.0 + resolution: "@jest/expect@npm:29.5.0" dependencies: - "@jest/types": ^29.4.3 + expect: ^29.5.0 + jest-snapshot: ^29.5.0 + checksum: bd10e295111547e6339137107d83986ab48d46561525393834d7d2d8b2ae9d5626653f3f5e48e5c3fa742ac982e97bdf1f541b53b9e1d117a247b08e938527f6 + languageName: node + linkType: hard + +"@jest/fake-timers@npm:^29.5.0": + version: 29.5.0 + resolution: "@jest/fake-timers@npm:29.5.0" + dependencies: + "@jest/types": ^29.5.0 "@sinonjs/fake-timers": ^10.0.2 "@types/node": "*" - jest-message-util: ^29.4.3 - jest-mock: ^29.4.3 - jest-util: ^29.4.3 - checksum: adaceb9143c395cccf3d7baa0e49b7042c3092a554e8283146df19926247e34c21b5bde5688bb90e9e87b4a02e4587926c5d858ee0a38d397a63175d0a127874 + jest-message-util: ^29.5.0 + jest-mock: ^29.5.0 + jest-util: ^29.5.0 + checksum: 69930c6922341f244151ec0d27640852ec96237f730fc024da1f53143d31b43cde75d92f9d8e5937981cdce3b31416abc3a7090a0d22c2377512c4a6613244ee languageName: node linkType: hard -"@jest/globals@npm:^29.4.3": - version: 29.4.3 - resolution: "@jest/globals@npm:29.4.3" +"@jest/globals@npm:^29.5.0": + version: 29.5.0 + resolution: "@jest/globals@npm:29.5.0" dependencies: - "@jest/environment": ^29.4.3 - "@jest/expect": ^29.4.3 - "@jest/types": ^29.4.3 - jest-mock: ^29.4.3 - checksum: ea76b546ceb4aa5ce2bb3726df12f989b23150b51c9f7664790caa81b943012a657cf3a8525498af1c3518cdb387f54b816cfba1b0ddd22c7b20f03b1d7290b4 + "@jest/environment": ^29.5.0 + "@jest/expect": ^29.5.0 + "@jest/types": ^29.5.0 + jest-mock: ^29.5.0 + checksum: b309ab8f21b571a7c672608682e84bbdd3d2b554ddf81e4e32617fec0a69094a290ab42e3c8b2c66ba891882bfb1b8b2736720ea1285b3ad646d55c2abefedd9 languageName: node linkType: hard -"@jest/reporters@npm:^29.4.3": - version: 29.4.3 - resolution: "@jest/reporters@npm:29.4.3" +"@jest/reporters@npm:^29.5.0": + version: 29.5.0 + resolution: "@jest/reporters@npm:29.5.0" dependencies: "@bcoe/v8-coverage": ^0.2.3 - "@jest/console": ^29.4.3 - "@jest/test-result": ^29.4.3 - "@jest/transform": ^29.4.3 - "@jest/types": ^29.4.3 + "@jest/console": ^29.5.0 + "@jest/test-result": ^29.5.0 + "@jest/transform": ^29.5.0 + "@jest/types": ^29.5.0 "@jridgewell/trace-mapping": ^0.3.15 "@types/node": "*" chalk: ^4.0.0 @@ -1141,9 +1150,9 @@ __metadata: istanbul-lib-report: ^3.0.0 istanbul-lib-source-maps: ^4.0.0 istanbul-reports: ^3.1.3 - jest-message-util: ^29.4.3 - jest-util: ^29.4.3 - jest-worker: ^29.4.3 + jest-message-util: ^29.5.0 + jest-util: ^29.5.0 + jest-worker: ^29.5.0 slash: ^3.0.0 string-length: ^4.0.1 strip-ansi: ^6.0.0 @@ -1153,7 +1162,7 @@ __metadata: peerDependenciesMeta: node-notifier: optional: true - checksum: 7aa2e429c915bd96c3334962addd69d2bbf52065725757ddde26b293f8c4420a1e8c65363cc3e1e5ec89100a5273ccd3771bec58325a2cc0d97afdc81995073a + checksum: 481268aac9a4a75cc49c4df1273d6b111808dec815e9d009dad717c32383ebb0cebac76e820ad1ab44e207540e1c2fe1e640d44c4f262de92ab1933e057fdeeb languageName: node linkType: hard @@ -1177,50 +1186,50 @@ __metadata: languageName: node linkType: hard -"@jest/test-result@npm:^29.4.3": - version: 29.4.3 - resolution: "@jest/test-result@npm:29.4.3" +"@jest/test-result@npm:^29.5.0": + version: 29.5.0 + resolution: "@jest/test-result@npm:29.5.0" dependencies: - "@jest/console": ^29.4.3 - "@jest/types": ^29.4.3 + "@jest/console": ^29.5.0 + "@jest/types": ^29.5.0 "@types/istanbul-lib-coverage": ^2.0.0 collect-v8-coverage: ^1.0.0 - checksum: 164f102b96619ec283c2c39e208b8048e4674f75bf3c3a4f2e95048ae0f9226105add684b25f10d286d91c221625f877e2c1cfc3da46c42d7e1804da239318cb + checksum: 2e8ff5242227ab960c520c3ea0f6544c595cc1c42fa3873c158e9f4f685f4ec9670ec08a4af94ae3885c0005a43550a9595191ffbc27a0965df27d9d98bbf901 languageName: node linkType: hard -"@jest/test-sequencer@npm:^29.4.3": - version: 29.4.3 - resolution: "@jest/test-sequencer@npm:29.4.3" +"@jest/test-sequencer@npm:^29.5.0": + version: 29.5.0 + resolution: "@jest/test-sequencer@npm:29.5.0" dependencies: - "@jest/test-result": ^29.4.3 + "@jest/test-result": ^29.5.0 graceful-fs: ^4.2.9 - jest-haste-map: ^29.4.3 + jest-haste-map: ^29.5.0 slash: ^3.0.0 - checksum: 145e1fa9379e5be3587bde6d585b8aee5cf4442b06926928a87e9aec7de5be91b581711d627c6ca13144d244fe05e5d248c13b366b51bedc404f9dcfbfd79e9e + checksum: eca34b4aeb2fda6dfb7f9f4b064c858a7adf64ec5c6091b6f4ed9d3c19549177cbadcf1c615c4c182688fa1cf085c8c55c3ca6eea40719a34554b0bf071d842e languageName: node linkType: hard -"@jest/transform@npm:^29.4.3": - version: 29.4.3 - resolution: "@jest/transform@npm:29.4.3" +"@jest/transform@npm:^29.5.0": + version: 29.5.0 + resolution: "@jest/transform@npm:29.5.0" dependencies: "@babel/core": ^7.11.6 - "@jest/types": ^29.4.3 + "@jest/types": ^29.5.0 "@jridgewell/trace-mapping": ^0.3.15 babel-plugin-istanbul: ^6.1.1 chalk: ^4.0.0 convert-source-map: ^2.0.0 fast-json-stable-stringify: ^2.1.0 graceful-fs: ^4.2.9 - jest-haste-map: ^29.4.3 + jest-haste-map: ^29.5.0 jest-regex-util: ^29.4.3 - jest-util: ^29.4.3 + jest-util: ^29.5.0 micromatch: ^4.0.4 pirates: ^4.0.4 slash: ^3.0.0 write-file-atomic: ^4.0.2 - checksum: 082d74e04044213aa7baa8de29f8383e5010034f867969c8602a2447a4ef2f484cfaf2491eba3179ce42f369f7a0af419cbd087910f7e5caf7aa5d1fe03f2ff9 + checksum: d55d604085c157cf5112e165ff5ac1fa788873b3b31265fb4734ca59892ee24e44119964cc47eb6d178dd9512bbb6c576d1e20e51a201ff4e24d31e818a1c92d languageName: node linkType: hard @@ -1238,6 +1247,20 @@ __metadata: languageName: node linkType: hard +"@jest/types@npm:^29.5.0": + version: 29.5.0 + resolution: "@jest/types@npm:29.5.0" + dependencies: + "@jest/schemas": ^29.4.3 + "@types/istanbul-lib-coverage": ^2.0.0 + "@types/istanbul-reports": ^3.0.0 + "@types/node": "*" + "@types/yargs": ^17.0.8 + chalk: ^4.0.0 + checksum: 1811f94b19cf8a9460a289c4f056796cfc373480e0492692a6125a553cd1a63824bd846d7bb78820b7b6f758f6dd3c2d4558293bb676d541b2fa59c70fdf9d39 + languageName: node + linkType: hard + "@jridgewell/gen-mapping@npm:^0.1.0": version: 0.1.1 resolution: "@jridgewell/gen-mapping@npm:0.1.1" @@ -1421,11 +1444,15 @@ __metadata: "@metamask/eth-hd-keyring": ^6.0.0 "@metamask/eth-sig-util": 5.0.2 "@metamask/eth-simple-keyring": ^5.0.0 - "@metamask/utils": ^5.0.0 + "@metamask/utils": ^5.0.1 "@types/jest": ^29.4.0 + "@types/node": ^18.16.1 "@types/sinon": ^10.0.13 + "@types/uuid": ^9.0.1 "@typescript-eslint/eslint-plugin": ^5.55.0 "@typescript-eslint/parser": ^5.55.0 + ajv: ^8.12.0 + ajv-formats: ^2.1.1 depcheck: ^1.4.3 eslint: ^8.36.0 eslint-config-prettier: ^8.7.0 @@ -1435,7 +1462,7 @@ __metadata: eslint-plugin-node: ^11.1.0 eslint-plugin-prettier: ^4.2.1 ethereumjs-wallet: ^1.0.1 - jest: ^29.0.0 + jest: ^29.5.0 obs-store: ^4.0.3 prettier: ^2.8.1 prettier-plugin-packagejson: ^2.3.0 @@ -1444,6 +1471,7 @@ __metadata: ts-node: ^10.7.0 typedoc: ^0.23.15 typescript: ~5.0.2 + uuid: ^9.0.0 languageName: unknown linkType: soft @@ -1483,16 +1511,16 @@ __metadata: languageName: node linkType: hard -"@metamask/utils@npm:^5.0.0": - version: 5.0.0 - resolution: "@metamask/utils@npm:5.0.0" +"@metamask/utils@npm:^5.0.1": + version: 5.0.1 + resolution: "@metamask/utils@npm:5.0.1" dependencies: "@ethereumjs/tx": ^4.1.1 "@types/debug": ^4.1.7 debug: ^4.3.4 semver: ^7.3.8 superstruct: ^1.0.3 - checksum: 34e39fc0bf28db5fe92676753de3291b05a517f8c81dbe332a4b6002739a58450a89fb2bddd85922a4f420affb1674604e6ad4627cdf052459e5371361ef7dd2 + checksum: 29745bd3d2db06bf66263bdec04e93a8f417c46c69d8054149c0046ed54b5f13d26d94a998fff1a31b5a8e7a2200935bfc8392a5fa3c4261e3cecd3ccdd9ddc0 languageName: node linkType: hard @@ -1863,6 +1891,13 @@ __metadata: languageName: node linkType: hard +"@types/node@npm:^18.16.1": + version: 18.16.1 + resolution: "@types/node@npm:18.16.1" + checksum: 799026b949a48993cba7c9b81b2eabfdfb34c880744cb44c1c990fbedc9e315f3634d126eb2cf9a6e0795577c01016e2326d98565bef695ada9d363fadeb6946 + languageName: node + linkType: hard + "@types/parse-json@npm:^4.0.0": version: 4.0.0 resolution: "@types/parse-json@npm:4.0.0" @@ -1925,6 +1960,13 @@ __metadata: languageName: node linkType: hard +"@types/uuid@npm:^9.0.1": + version: 9.0.1 + resolution: "@types/uuid@npm:9.0.1" + checksum: c472b8a77cbeded4bc529220b8611afa39bd64677f507838f8083d8aac8033b1f88cb9ddaa2f8589e0dcd2317291d0f6e1379f82d5ceebd6f74f3b4825288e00 + languageName: node + linkType: hard + "@types/yargs-parser@npm:*": version: 20.2.0 resolution: "@types/yargs-parser@npm:20.2.0" @@ -2273,6 +2315,20 @@ __metadata: languageName: node linkType: hard +"ajv-formats@npm:^2.1.1": + version: 2.1.1 + resolution: "ajv-formats@npm:2.1.1" + dependencies: + ajv: ^8.0.0 + peerDependencies: + ajv: ^8.0.0 + peerDependenciesMeta: + ajv: + optional: true + checksum: 4a287d937f1ebaad4683249a4c40c0fa3beed30d9ddc0adba04859026a622da0d317851316ea64b3680dc60f5c3c708105ddd5d5db8fe595d9d0207fd19f90b7 + languageName: node + linkType: hard + "ajv@npm:^6.10.0, ajv@npm:^6.12.3, ajv@npm:^6.12.4": version: 6.12.6 resolution: "ajv@npm:6.12.6" @@ -2285,6 +2341,18 @@ __metadata: languageName: node linkType: hard +"ajv@npm:^8.0.0, ajv@npm:^8.12.0": + version: 8.12.0 + resolution: "ajv@npm:8.12.0" + dependencies: + fast-deep-equal: ^3.1.1 + json-schema-traverse: ^1.0.0 + require-from-string: ^2.0.2 + uri-js: ^4.2.2 + checksum: 4dc13714e316e67537c8b31bc063f99a1d9d9a497eb4bbd55191ac0dcd5e4985bbb71570352ad6f1e76684fb6d790928f96ba3b2d4fd6e10024be9612fe3f001 + languageName: node + linkType: hard + "ansi-escapes@npm:^4.2.1": version: 4.3.2 resolution: "ansi-escapes@npm:4.3.2" @@ -2519,20 +2587,20 @@ __metadata: languageName: node linkType: hard -"babel-jest@npm:^29.4.3": - version: 29.4.3 - resolution: "babel-jest@npm:29.4.3" +"babel-jest@npm:^29.5.0": + version: 29.5.0 + resolution: "babel-jest@npm:29.5.0" dependencies: - "@jest/transform": ^29.4.3 + "@jest/transform": ^29.5.0 "@types/babel__core": ^7.1.14 babel-plugin-istanbul: ^6.1.1 - babel-preset-jest: ^29.4.3 + babel-preset-jest: ^29.5.0 chalk: ^4.0.0 graceful-fs: ^4.2.9 slash: ^3.0.0 peerDependencies: "@babel/core": ^7.8.0 - checksum: a1a95937adb5e717dbffc2eb9e583fa6d26c7e5d5b07bb492a2d7f68631510a363e9ff097eafb642ad642dfac9dc2b13872b584f680e166a4f0922c98ea95853 + checksum: eafb6d37deb71f0c80bf3c80215aa46732153e5e8bcd73f6ff47d92e5c0c98c8f7f75995d0efec6289c371edad3693cd8fa2367b0661c4deb71a3a7117267ede languageName: node linkType: hard @@ -2549,15 +2617,15 @@ __metadata: languageName: node linkType: hard -"babel-plugin-jest-hoist@npm:^29.4.3": - version: 29.4.3 - resolution: "babel-plugin-jest-hoist@npm:29.4.3" +"babel-plugin-jest-hoist@npm:^29.5.0": + version: 29.5.0 + resolution: "babel-plugin-jest-hoist@npm:29.5.0" dependencies: "@babel/template": ^7.3.3 "@babel/types": ^7.3.3 "@types/babel__core": ^7.1.14 "@types/babel__traverse": ^7.0.6 - checksum: c8702a6db6b30ec39dfb9f8e72b501c13895231ed80b15ed2648448f9f0c7b7cc4b1529beac31802ae655f63479a05110ca612815aa25fb1b0e6c874e1589137 + checksum: 099b5254073b6bc985b6d2d045ad26fb8ed30ff8ae6404c4fe8ee7cd0e98a820f69e3dfb871c7c65aae0f4b65af77046244c07bb92d49ef9005c90eedf681539 languageName: node linkType: hard @@ -2583,15 +2651,15 @@ __metadata: languageName: node linkType: hard -"babel-preset-jest@npm:^29.4.3": - version: 29.4.3 - resolution: "babel-preset-jest@npm:29.4.3" +"babel-preset-jest@npm:^29.5.0": + version: 29.5.0 + resolution: "babel-preset-jest@npm:29.5.0" dependencies: - babel-plugin-jest-hoist: ^29.4.3 + babel-plugin-jest-hoist: ^29.5.0 babel-preset-current-node-syntax: ^1.0.0 peerDependencies: "@babel/core": ^7.0.0 - checksum: a091721861ea2f8d969ace8fe06570cff8f2e847dbc6e4800abacbe63f72131abde615ce0a3b6648472c97e55a5be7f8bf7ae381e2b194ad2fa1737096febcf5 + checksum: 5566ca2762766c9319b4973d018d2fa08c0fcf6415c72cc54f4c8e7199e851ea8f5e6c6730f03ed7ed44fc8beefa959dd15911f2647dee47c615ff4faeddb1ad languageName: node linkType: hard @@ -3934,7 +4002,7 @@ __metadata: languageName: node linkType: hard -"expect@npm:^29.0.0, expect@npm:^29.4.3": +"expect@npm:^29.0.0": version: 29.4.3 resolution: "expect@npm:29.4.3" dependencies: @@ -3947,6 +4015,19 @@ __metadata: languageName: node linkType: hard +"expect@npm:^29.5.0": + version: 29.5.0 + resolution: "expect@npm:29.5.0" + dependencies: + "@jest/expect-utils": ^29.5.0 + jest-get-type: ^29.4.3 + jest-matcher-utils: ^29.5.0 + jest-message-util: ^29.5.0 + jest-util: ^29.5.0 + checksum: 58f70b38693df6e5c6892db1bcd050f0e518d6f785175dc53917d4fa6a7359a048e5690e19ddcb96b65c4493881dd89a3dabdab1a84dfa55c10cdbdabf37b2d7 + languageName: node + linkType: hard + "extend@npm:~3.0.2": version: 3.0.2 resolution: "extend@npm:3.0.2" @@ -4951,57 +5032,58 @@ __metadata: languageName: node linkType: hard -"jest-changed-files@npm:^29.4.3": - version: 29.4.3 - resolution: "jest-changed-files@npm:29.4.3" +"jest-changed-files@npm:^29.5.0": + version: 29.5.0 + resolution: "jest-changed-files@npm:29.5.0" dependencies: execa: ^5.0.0 p-limit: ^3.1.0 - checksum: 9a70bd8e92b37e18ad26d8bea97c516f41119fb7046b4255a13c76d557b0e54fa0629726de5a093fadfd6a0a08ce45da65a57086664d505b8db4b3133133e141 + checksum: a67a7cb3c11f8f92bd1b7c79e84f724cbd11a9ad51f3cdadafe3ce7ee3c79ee50dbea128f920f5fddc807e9e4e83f5462143094391feedd959a77dd20ab96cf3 languageName: node linkType: hard -"jest-circus@npm:^29.4.3": - version: 29.4.3 - resolution: "jest-circus@npm:29.4.3" +"jest-circus@npm:^29.5.0": + version: 29.5.0 + resolution: "jest-circus@npm:29.5.0" dependencies: - "@jest/environment": ^29.4.3 - "@jest/expect": ^29.4.3 - "@jest/test-result": ^29.4.3 - "@jest/types": ^29.4.3 + "@jest/environment": ^29.5.0 + "@jest/expect": ^29.5.0 + "@jest/test-result": ^29.5.0 + "@jest/types": ^29.5.0 "@types/node": "*" chalk: ^4.0.0 co: ^4.6.0 dedent: ^0.7.0 is-generator-fn: ^2.0.0 - jest-each: ^29.4.3 - jest-matcher-utils: ^29.4.3 - jest-message-util: ^29.4.3 - jest-runtime: ^29.4.3 - jest-snapshot: ^29.4.3 - jest-util: ^29.4.3 + jest-each: ^29.5.0 + jest-matcher-utils: ^29.5.0 + jest-message-util: ^29.5.0 + jest-runtime: ^29.5.0 + jest-snapshot: ^29.5.0 + jest-util: ^29.5.0 p-limit: ^3.1.0 - pretty-format: ^29.4.3 + pretty-format: ^29.5.0 + pure-rand: ^6.0.0 slash: ^3.0.0 stack-utils: ^2.0.3 - checksum: 2739bef9c888743b49ff3fe303131381618e5d2f250f613a91240d9c86e19e6874fc904cbd8bcb02ec9ec59a84e5dae4ffec929f0c6171e87ddbc05508a137f4 + checksum: 44ff5d06acedae6de6c866e20e3b61f83e29ab94cf9f960826e7e667de49c12dd9ab9dffd7fa3b7d1f9688a8b5bfb1ebebadbea69d9ed0d3f66af4a0ff8c2b27 languageName: node linkType: hard -"jest-cli@npm:^29.4.3": - version: 29.4.3 - resolution: "jest-cli@npm:29.4.3" +"jest-cli@npm:^29.5.0": + version: 29.5.0 + resolution: "jest-cli@npm:29.5.0" dependencies: - "@jest/core": ^29.4.3 - "@jest/test-result": ^29.4.3 - "@jest/types": ^29.4.3 + "@jest/core": ^29.5.0 + "@jest/test-result": ^29.5.0 + "@jest/types": ^29.5.0 chalk: ^4.0.0 exit: ^0.1.2 graceful-fs: ^4.2.9 import-local: ^3.0.2 - jest-config: ^29.4.3 - jest-util: ^29.4.3 - jest-validate: ^29.4.3 + jest-config: ^29.5.0 + jest-util: ^29.5.0 + jest-validate: ^29.5.0 prompts: ^2.0.1 yargs: ^17.3.1 peerDependencies: @@ -5011,34 +5093,34 @@ __metadata: optional: true bin: jest: bin/jest.js - checksum: f4c9f6d76cde2c60a4169acbebb3f862728be03bcf3fe0077d2e55da7f9f3c3e9483cfa6e936832d35eabf96ee5ebf0300c4b0bd43cffff099801793466bfdd8 + checksum: 39897bbbc0f0d8a6b975ab12fd13887eaa28d92e3dee9e0173a5cb913ae8cc2ae46e090d38c6d723e84d9d6724429cd08685b4e505fa447d31ca615630c7dbba languageName: node linkType: hard -"jest-config@npm:^29.4.3": - version: 29.4.3 - resolution: "jest-config@npm:29.4.3" +"jest-config@npm:^29.5.0": + version: 29.5.0 + resolution: "jest-config@npm:29.5.0" dependencies: "@babel/core": ^7.11.6 - "@jest/test-sequencer": ^29.4.3 - "@jest/types": ^29.4.3 - babel-jest: ^29.4.3 + "@jest/test-sequencer": ^29.5.0 + "@jest/types": ^29.5.0 + babel-jest: ^29.5.0 chalk: ^4.0.0 ci-info: ^3.2.0 deepmerge: ^4.2.2 glob: ^7.1.3 graceful-fs: ^4.2.9 - jest-circus: ^29.4.3 - jest-environment-node: ^29.4.3 + jest-circus: ^29.5.0 + jest-environment-node: ^29.5.0 jest-get-type: ^29.4.3 jest-regex-util: ^29.4.3 - jest-resolve: ^29.4.3 - jest-runner: ^29.4.3 - jest-util: ^29.4.3 - jest-validate: ^29.4.3 + jest-resolve: ^29.5.0 + jest-runner: ^29.5.0 + jest-util: ^29.5.0 + jest-validate: ^29.5.0 micromatch: ^4.0.4 parse-json: ^5.2.0 - pretty-format: ^29.4.3 + pretty-format: ^29.5.0 slash: ^3.0.0 strip-json-comments: ^3.1.1 peerDependencies: @@ -5049,7 +5131,7 @@ __metadata: optional: true ts-node: optional: true - checksum: 92f9a9c6850b18682cb01892774a33967472af23a5844438d8c68077d5f2a29b15b665e4e4db7de3d74002a6dca158cd5b2cb9f5debfd2cce5e1aee6c74e3873 + checksum: c37c4dab964c54ab293d4e302d40b09687037ac9d00b88348ec42366970747feeaf265e12e3750cd3660b40c518d4031335eda11ac10b70b10e60797ebbd4b9c languageName: node linkType: hard @@ -5065,6 +5147,18 @@ __metadata: languageName: node linkType: hard +"jest-diff@npm:^29.5.0": + version: 29.5.0 + resolution: "jest-diff@npm:29.5.0" + dependencies: + chalk: ^4.0.0 + diff-sequences: ^29.4.3 + jest-get-type: ^29.4.3 + pretty-format: ^29.5.0 + checksum: dfd0f4a299b5d127779c76b40106c37854c89c3e0785098c717d52822d6620d227f6234c3a9291df204d619e799e3654159213bf93220f79c8e92a55475a3d39 + languageName: node + linkType: hard + "jest-docblock@npm:^29.4.3": version: 29.4.3 resolution: "jest-docblock@npm:29.4.3" @@ -5074,30 +5168,30 @@ __metadata: languageName: node linkType: hard -"jest-each@npm:^29.4.3": - version: 29.4.3 - resolution: "jest-each@npm:29.4.3" +"jest-each@npm:^29.5.0": + version: 29.5.0 + resolution: "jest-each@npm:29.5.0" dependencies: - "@jest/types": ^29.4.3 + "@jest/types": ^29.5.0 chalk: ^4.0.0 jest-get-type: ^29.4.3 - jest-util: ^29.4.3 - pretty-format: ^29.4.3 - checksum: 1f72738338399efab0139eaea18bc198be0c6ed889770c8cbfa70bf9c724e8171fe1d3a29a94f9f39b8493ee6b2529bb350fb7c7c75e0d7eddfd28c253c79f9d + jest-util: ^29.5.0 + pretty-format: ^29.5.0 + checksum: b8b297534d25834c5d4e31e4c687359787b1e402519e42664eb704cc3a12a7a91a017565a75acb02e8cf9afd3f4eef3350bd785276bec0900184641b765ff7a5 languageName: node linkType: hard -"jest-environment-node@npm:^29.4.3": - version: 29.4.3 - resolution: "jest-environment-node@npm:29.4.3" +"jest-environment-node@npm:^29.5.0": + version: 29.5.0 + resolution: "jest-environment-node@npm:29.5.0" dependencies: - "@jest/environment": ^29.4.3 - "@jest/fake-timers": ^29.4.3 - "@jest/types": ^29.4.3 + "@jest/environment": ^29.5.0 + "@jest/fake-timers": ^29.5.0 + "@jest/types": ^29.5.0 "@types/node": "*" - jest-mock: ^29.4.3 - jest-util: ^29.4.3 - checksum: 3c7362edfdbd516e83af7367c95dde35761a482b174de9735c07633405486ec73e19624e9bea4333fca33c24e8d65eaa1aa6594e0cb6bfeeeb564ccc431ee61d + jest-mock: ^29.5.0 + jest-util: ^29.5.0 + checksum: 57981911cc20a4219b0da9e22b2e3c9f31b505e43f78e61c899e3227ded455ce1a3a9483842c69cfa4532f02cfb536ae0995bf245f9211608edacfc1e478d411 languageName: node linkType: hard @@ -5108,11 +5202,11 @@ __metadata: languageName: node linkType: hard -"jest-haste-map@npm:^29.4.3": - version: 29.4.3 - resolution: "jest-haste-map@npm:29.4.3" +"jest-haste-map@npm:^29.5.0": + version: 29.5.0 + resolution: "jest-haste-map@npm:29.5.0" dependencies: - "@jest/types": ^29.4.3 + "@jest/types": ^29.5.0 "@types/graceful-fs": ^4.1.3 "@types/node": "*" anymatch: ^3.0.3 @@ -5120,24 +5214,24 @@ __metadata: fsevents: ^2.3.2 graceful-fs: ^4.2.9 jest-regex-util: ^29.4.3 - jest-util: ^29.4.3 - jest-worker: ^29.4.3 + jest-util: ^29.5.0 + jest-worker: ^29.5.0 micromatch: ^4.0.4 walker: ^1.0.8 dependenciesMeta: fsevents: optional: true - checksum: c7a83ebe6008b3fe96a96235e8153092e54b14df68e0f4205faedec57450df26b658578495a71c6d82494c01fbb44bca98c1506a6b2b9c920696dcc5d2e2bc59 + checksum: 3828ff7783f168e34be2c63887f82a01634261f605dcae062d83f979a61c37739e21b9607ecb962256aea3fbe5a530a1acee062d0026fcb47c607c12796cf3b7 languageName: node linkType: hard -"jest-leak-detector@npm:^29.4.3": - version: 29.4.3 - resolution: "jest-leak-detector@npm:29.4.3" +"jest-leak-detector@npm:^29.5.0": + version: 29.5.0 + resolution: "jest-leak-detector@npm:29.5.0" dependencies: jest-get-type: ^29.4.3 - pretty-format: ^29.4.3 - checksum: ec2b45e6f0abce81bd0dd0f6fd06b433c24d1ec865267af7640fae540ec868b93752598e407a9184d9c7419cbf32e8789007cc8c1be1a84f8f7321a0f8ad01f1 + pretty-format: ^29.5.0 + checksum: 0fb845da7ac9cdfc9b3b2e35f6f623a41c547d7dc0103ceb0349013459d00de5870b5689a625e7e37f9644934b40e8f1dcdd5422d14d57470600350364676313 languageName: node linkType: hard @@ -5153,6 +5247,18 @@ __metadata: languageName: node linkType: hard +"jest-matcher-utils@npm:^29.5.0": + version: 29.5.0 + resolution: "jest-matcher-utils@npm:29.5.0" + dependencies: + chalk: ^4.0.0 + jest-diff: ^29.5.0 + jest-get-type: ^29.4.3 + pretty-format: ^29.5.0 + checksum: 1d3e8c746e484a58ce194e3aad152eff21fd0896e8b8bf3d4ab1a4e2cbfed95fb143646f4ad9fdf6e42212b9e8fc033268b58e011b044a9929df45485deb5ac9 + languageName: node + linkType: hard + "jest-message-util@npm:^29.4.3": version: 29.4.3 resolution: "jest-message-util@npm:29.4.3" @@ -5170,14 +5276,31 @@ __metadata: languageName: node linkType: hard -"jest-mock@npm:^29.4.3": - version: 29.4.3 - resolution: "jest-mock@npm:29.4.3" +"jest-message-util@npm:^29.5.0": + version: 29.5.0 + resolution: "jest-message-util@npm:29.5.0" dependencies: - "@jest/types": ^29.4.3 + "@babel/code-frame": ^7.12.13 + "@jest/types": ^29.5.0 + "@types/stack-utils": ^2.0.0 + chalk: ^4.0.0 + graceful-fs: ^4.2.9 + micromatch: ^4.0.4 + pretty-format: ^29.5.0 + slash: ^3.0.0 + stack-utils: ^2.0.3 + checksum: daddece6bbf846eb6a2ab9be9f2446e54085bef4e5cecd13d2a538fa9c01cb89d38e564c6b74fd8e12d37ed9eface8a362240ae9f21d68b214590631e7a0d8bf + languageName: node + linkType: hard + +"jest-mock@npm:^29.5.0": + version: 29.5.0 + resolution: "jest-mock@npm:29.5.0" + dependencies: + "@jest/types": ^29.5.0 "@types/node": "*" - jest-util: ^29.4.3 - checksum: 8eb4a29b02d2cd03faac0290b6df6d23b4ffa43f72b21c7fff3c7dd04a2797355b1e85862b70b15341dd33ee3a693b17db5520a6f6e6b81ee75601987de6a1a2 + jest-util: ^29.5.0 + checksum: 2a9cf07509948fa8608898c445f04fe4dd6e2049ff431e5531eee028c808d3ba3c67f226ac87b0cf383feaa1055776900d197c895e89783016886ac17a4ff10c languageName: node linkType: hard @@ -5200,95 +5323,95 @@ __metadata: languageName: node linkType: hard -"jest-resolve-dependencies@npm:^29.4.3": - version: 29.4.3 - resolution: "jest-resolve-dependencies@npm:29.4.3" +"jest-resolve-dependencies@npm:^29.5.0": + version: 29.5.0 + resolution: "jest-resolve-dependencies@npm:29.5.0" dependencies: jest-regex-util: ^29.4.3 - jest-snapshot: ^29.4.3 - checksum: 3ad934cd2170c9658d8800f84a975dafc866ec85b7ce391c640c09c3744ced337787620d8667dc8d1fa5e0b1493f973caa1a1bb980e4e6a50b46a1720baf0bd1 + jest-snapshot: ^29.5.0 + checksum: 479d2e5365d58fe23f2b87001e2e0adcbffe0147700e85abdec8f14b9703b0a55758c1929a9989e3f5d5e954fb88870ea4bfa04783523b664562fcf5f10b0edf languageName: node linkType: hard -"jest-resolve@npm:^29.4.3": - version: 29.4.3 - resolution: "jest-resolve@npm:29.4.3" +"jest-resolve@npm:^29.5.0": + version: 29.5.0 + resolution: "jest-resolve@npm:29.5.0" dependencies: chalk: ^4.0.0 graceful-fs: ^4.2.9 - jest-haste-map: ^29.4.3 + jest-haste-map: ^29.5.0 jest-pnp-resolver: ^1.2.2 - jest-util: ^29.4.3 - jest-validate: ^29.4.3 + jest-util: ^29.5.0 + jest-validate: ^29.5.0 resolve: ^1.20.0 resolve.exports: ^2.0.0 slash: ^3.0.0 - checksum: 056a66beccf833f3c7e5a8fc9bfec218886e87b0b103decdbdf11893669539df489d1490cd6d5f0eea35731e8be0d2e955a6710498f970d2eae734da4df029dc + checksum: 9a125f3cf323ceef512089339d35f3ee37f79fe16a831fb6a26773ea6a229b9e490d108fec7af334142e91845b5996de8e7cdd85a4d8d617078737d804e29c8f languageName: node linkType: hard -"jest-runner@npm:^29.4.3": - version: 29.4.3 - resolution: "jest-runner@npm:29.4.3" +"jest-runner@npm:^29.5.0": + version: 29.5.0 + resolution: "jest-runner@npm:29.5.0" dependencies: - "@jest/console": ^29.4.3 - "@jest/environment": ^29.4.3 - "@jest/test-result": ^29.4.3 - "@jest/transform": ^29.4.3 - "@jest/types": ^29.4.3 + "@jest/console": ^29.5.0 + "@jest/environment": ^29.5.0 + "@jest/test-result": ^29.5.0 + "@jest/transform": ^29.5.0 + "@jest/types": ^29.5.0 "@types/node": "*" chalk: ^4.0.0 emittery: ^0.13.1 graceful-fs: ^4.2.9 jest-docblock: ^29.4.3 - jest-environment-node: ^29.4.3 - jest-haste-map: ^29.4.3 - jest-leak-detector: ^29.4.3 - jest-message-util: ^29.4.3 - jest-resolve: ^29.4.3 - jest-runtime: ^29.4.3 - jest-util: ^29.4.3 - jest-watcher: ^29.4.3 - jest-worker: ^29.4.3 + jest-environment-node: ^29.5.0 + jest-haste-map: ^29.5.0 + jest-leak-detector: ^29.5.0 + jest-message-util: ^29.5.0 + jest-resolve: ^29.5.0 + jest-runtime: ^29.5.0 + jest-util: ^29.5.0 + jest-watcher: ^29.5.0 + jest-worker: ^29.5.0 p-limit: ^3.1.0 source-map-support: 0.5.13 - checksum: c41108e5da01e0b8fdc2a06c5042eb49bb1d8db0e0d4651769fd1b9fe84ab45188617c11a3a8e1c83748b29bfe57dd77001ec57e86e3e3c30f3534e0314f8882 + checksum: 437dea69c5dddca22032259787bac74790d5a171c9d804711415f31e5d1abfb64fa52f54a9015bb17a12b858fd0cf3f75ef6f3c9e94255a8596e179f707229c4 languageName: node linkType: hard -"jest-runtime@npm:^29.4.3": - version: 29.4.3 - resolution: "jest-runtime@npm:29.4.3" +"jest-runtime@npm:^29.5.0": + version: 29.5.0 + resolution: "jest-runtime@npm:29.5.0" dependencies: - "@jest/environment": ^29.4.3 - "@jest/fake-timers": ^29.4.3 - "@jest/globals": ^29.4.3 + "@jest/environment": ^29.5.0 + "@jest/fake-timers": ^29.5.0 + "@jest/globals": ^29.5.0 "@jest/source-map": ^29.4.3 - "@jest/test-result": ^29.4.3 - "@jest/transform": ^29.4.3 - "@jest/types": ^29.4.3 + "@jest/test-result": ^29.5.0 + "@jest/transform": ^29.5.0 + "@jest/types": ^29.5.0 "@types/node": "*" chalk: ^4.0.0 cjs-module-lexer: ^1.0.0 collect-v8-coverage: ^1.0.0 glob: ^7.1.3 graceful-fs: ^4.2.9 - jest-haste-map: ^29.4.3 - jest-message-util: ^29.4.3 - jest-mock: ^29.4.3 + jest-haste-map: ^29.5.0 + jest-message-util: ^29.5.0 + jest-mock: ^29.5.0 jest-regex-util: ^29.4.3 - jest-resolve: ^29.4.3 - jest-snapshot: ^29.4.3 - jest-util: ^29.4.3 + jest-resolve: ^29.5.0 + jest-snapshot: ^29.5.0 + jest-util: ^29.5.0 slash: ^3.0.0 strip-bom: ^4.0.0 - checksum: b99f8a910d1a38e7476058ba04ad44dfd3d93e837bb7c301d691e646a1085412fde87f06fbe271c9145f0e72d89400bfa7f6994bc30d456c7742269f37d0f570 + checksum: 7af27bd9d54cf1c5735404cf8d76c6509d5610b1ec0106a21baa815c1aff15d774ce534ac2834bc440dccfe6348bae1885fd9a806f23a94ddafdc0f5bae4b09d languageName: node linkType: hard -"jest-snapshot@npm:^29.4.3": - version: 29.4.3 - resolution: "jest-snapshot@npm:29.4.3" +"jest-snapshot@npm:^29.5.0": + version: 29.5.0 + resolution: "jest-snapshot@npm:29.5.0" dependencies: "@babel/core": ^7.11.6 "@babel/generator": ^7.7.2 @@ -5296,25 +5419,24 @@ __metadata: "@babel/plugin-syntax-typescript": ^7.7.2 "@babel/traverse": ^7.7.2 "@babel/types": ^7.3.3 - "@jest/expect-utils": ^29.4.3 - "@jest/transform": ^29.4.3 - "@jest/types": ^29.4.3 + "@jest/expect-utils": ^29.5.0 + "@jest/transform": ^29.5.0 + "@jest/types": ^29.5.0 "@types/babel__traverse": ^7.0.6 "@types/prettier": ^2.1.5 babel-preset-current-node-syntax: ^1.0.0 chalk: ^4.0.0 - expect: ^29.4.3 + expect: ^29.5.0 graceful-fs: ^4.2.9 - jest-diff: ^29.4.3 + jest-diff: ^29.5.0 jest-get-type: ^29.4.3 - jest-haste-map: ^29.4.3 - jest-matcher-utils: ^29.4.3 - jest-message-util: ^29.4.3 - jest-util: ^29.4.3 + jest-matcher-utils: ^29.5.0 + jest-message-util: ^29.5.0 + jest-util: ^29.5.0 natural-compare: ^1.4.0 - pretty-format: ^29.4.3 + pretty-format: ^29.5.0 semver: ^7.3.5 - checksum: 79ba52f2435e23ce72b1309be4b17fdbcb299d1c2ce97ebb61df9a62711e9463035f63b4c849181b2fe5aa17b3e09d30ee4668cc25fb3c6f59511c010b4d9494 + checksum: fe5df54122ed10eed625de6416a45bc4958d5062b018f05b152bf9785ab7f355dcd55e40cf5da63895bf8278f8d7b2bb4059b2cfbfdee18f509d455d37d8aa2b languageName: node linkType: hard @@ -5332,56 +5454,70 @@ __metadata: languageName: node linkType: hard -"jest-validate@npm:^29.4.3": - version: 29.4.3 - resolution: "jest-validate@npm:29.4.3" +"jest-util@npm:^29.5.0": + version: 29.5.0 + resolution: "jest-util@npm:29.5.0" dependencies: - "@jest/types": ^29.4.3 + "@jest/types": ^29.5.0 + "@types/node": "*" + chalk: ^4.0.0 + ci-info: ^3.2.0 + graceful-fs: ^4.2.9 + picomatch: ^2.2.3 + checksum: fd9212950d34d2ecad8c990dda0d8ea59a8a554b0c188b53ea5d6c4a0829a64f2e1d49e6e85e812014933d17426d7136da4785f9cf76fff1799de51b88bc85d3 + languageName: node + linkType: hard + +"jest-validate@npm:^29.5.0": + version: 29.5.0 + resolution: "jest-validate@npm:29.5.0" + dependencies: + "@jest/types": ^29.5.0 camelcase: ^6.2.0 chalk: ^4.0.0 jest-get-type: ^29.4.3 leven: ^3.1.0 - pretty-format: ^29.4.3 - checksum: 983e56430d86bed238448cae031535c1d908f760aa312cd4a4ec0e92f3bc1b6675415ddf57cdeceedb8ad9c698e5bcd10f0a856dfc93a8923bdecc7733f4ba80 + pretty-format: ^29.5.0 + checksum: 43ca5df7cb75572a254ac3e92fbbe7be6b6a1be898cc1e887a45d55ea003f7a112717d814a674d37f9f18f52d8de40873c8f084f17664ae562736c78dd44c6a1 languageName: node linkType: hard -"jest-watcher@npm:^29.4.3": - version: 29.4.3 - resolution: "jest-watcher@npm:29.4.3" +"jest-watcher@npm:^29.5.0": + version: 29.5.0 + resolution: "jest-watcher@npm:29.5.0" dependencies: - "@jest/test-result": ^29.4.3 - "@jest/types": ^29.4.3 + "@jest/test-result": ^29.5.0 + "@jest/types": ^29.5.0 "@types/node": "*" ansi-escapes: ^4.2.1 chalk: ^4.0.0 emittery: ^0.13.1 - jest-util: ^29.4.3 + jest-util: ^29.5.0 string-length: ^4.0.1 - checksum: 44b64991b3414db853c3756f14690028f4edef7aebfb204a4291cc1901c2239fa27a8687c5c5abbecc74bf613e0bb9b1378bf766430c9febcc71e9c0cb5ad8fc + checksum: 62303ac7bdc7e61a8b4239a239d018f7527739da2b2be6a81a7be25b74ca769f1c43ee8558ce8e72bb857245c46d6e03af331227ffb00a57280abb2a928aa776 languageName: node linkType: hard -"jest-worker@npm:^29.4.3": - version: 29.4.3 - resolution: "jest-worker@npm:29.4.3" +"jest-worker@npm:^29.5.0": + version: 29.5.0 + resolution: "jest-worker@npm:29.5.0" dependencies: "@types/node": "*" - jest-util: ^29.4.3 + jest-util: ^29.5.0 merge-stream: ^2.0.0 supports-color: ^8.0.0 - checksum: c99ae66f257564613e72c5797c3a68f21a22e1c1fb5f30d14695ff5b508a0d2405f22748f13a3df8d1015b5e16abb130170f81f047ff68f58b6b1d2ff6ebc51b + checksum: 1151a1ae3602b1ea7c42a8f1efe2b5a7bf927039deaa0827bf978880169899b705744e288f80a63603fb3fc2985e0071234986af7dc2c21c7a64333d8777c7c9 languageName: node linkType: hard -"jest@npm:^29.0.0": - version: 29.4.3 - resolution: "jest@npm:29.4.3" +"jest@npm:^29.5.0": + version: 29.5.0 + resolution: "jest@npm:29.5.0" dependencies: - "@jest/core": ^29.4.3 - "@jest/types": ^29.4.3 + "@jest/core": ^29.5.0 + "@jest/types": ^29.5.0 import-local: ^3.0.2 - jest-cli: ^29.4.3 + jest-cli: ^29.5.0 peerDependencies: node-notifier: ^8.0.1 || ^9.0.0 || ^10.0.0 peerDependenciesMeta: @@ -5389,7 +5525,7 @@ __metadata: optional: true bin: jest: bin/jest.js - checksum: 084d10d1ceaade3c40e6d3bbd71b9b71b8919ba6fbd6f1f6699bdc259a6ba2f7350c7ccbfa10c11f7e3e01662853650a6244210179542fe4ba87e77dc3f3109f + checksum: a8ff2eb0f421623412236e23cbe67c638127fffde466cba9606bc0c0553b4c1e5cb116d7e0ef990b5d1712851652c8ee461373b578df50857fe635b94ff455d5 languageName: node linkType: hard @@ -5486,6 +5622,13 @@ __metadata: languageName: node linkType: hard +"json-schema-traverse@npm:^1.0.0": + version: 1.0.0 + resolution: "json-schema-traverse@npm:1.0.0" + checksum: 02f2f466cdb0362558b2f1fd5e15cce82ef55d60cd7f8fa828cf35ba74330f8d767fcae5c5c2adb7851fa811766c694b9405810879bc4e1ddd78a7c0e03658ad + languageName: node + linkType: hard + "json-schema@npm:0.2.3": version: 0.2.3 resolution: "json-schema@npm:0.2.3" @@ -6495,6 +6638,17 @@ __metadata: languageName: node linkType: hard +"pretty-format@npm:^29.5.0": + version: 29.5.0 + resolution: "pretty-format@npm:29.5.0" + dependencies: + "@jest/schemas": ^29.4.3 + ansi-styles: ^5.0.0 + react-is: ^18.0.0 + checksum: 4065356b558e6db25b4d41a01efb386935a6c06a0c9c104ef5ce59f2f476b8210edb8b3949b386e60ada0a6dc5ebcb2e6ccddc8c64dfd1a9943c3c3a9e7eaf89 + languageName: node + linkType: hard + "process-nextick-args@npm:~2.0.0": version: 2.0.1 resolution: "process-nextick-args@npm:2.0.1" @@ -6543,6 +6697,13 @@ __metadata: languageName: node linkType: hard +"pure-rand@npm:^6.0.0": + version: 6.0.1 + resolution: "pure-rand@npm:6.0.1" + checksum: 4bb565399993b815658a72e359f574ce4f04827a42a905105d61163ae86f456d91595a0e4241e7bce04328fae0638ae70ac0428d93ecb55971c465bd084f8648 + languageName: node + linkType: hard + "qs@npm:~6.5.2": version: 6.5.3 resolution: "qs@npm:6.5.3" @@ -6681,6 +6842,13 @@ __metadata: languageName: node linkType: hard +"require-from-string@npm:^2.0.2": + version: 2.0.2 + resolution: "require-from-string@npm:2.0.2" + checksum: a03ef6895445f33a4015300c426699bc66b2b044ba7b670aa238610381b56d3f07c686251740d575e22f4c87531ba662d06937508f0f3c0f1ddc04db3130560b + languageName: node + linkType: hard + "require-package-name@npm:^2.0.1": version: 2.0.1 resolution: "require-package-name@npm:2.0.1" @@ -7684,6 +7852,15 @@ __metadata: languageName: node linkType: hard +"uuid@npm:^9.0.0": + version: 9.0.0 + resolution: "uuid@npm:9.0.0" + bin: + uuid: dist/bin/uuid + checksum: 8dd2c83c43ddc7e1c71e36b60aea40030a6505139af6bee0f382ebcd1a56f6cd3028f7f06ffb07f8cf6ced320b76aea275284b224b002b289f89fe89c389b028 + languageName: node + linkType: hard + "v8-compile-cache-lib@npm:^3.0.1": version: 3.0.1 resolution: "v8-compile-cache-lib@npm:3.0.1"