ci: improve set up, upgrades

This commit is contained in:
kyranjamie
2023-05-16 17:06:07 +02:00
committed by kyranjamie
parent 3771c7075f
commit 8d45dc8551
39 changed files with 344 additions and 282 deletions

View File

@@ -47,8 +47,6 @@ jobs:
key: ${{ runner.os }}-modules-${{ hashFiles('**/yarn.lock') }}-${{ hashFiles('**/package.json') }}
- uses: actions/setup-node@v3
with:
node-version: 14.x
- name: Install packages
uses: ./.github/actions/provision
@@ -83,8 +81,6 @@ jobs:
- name: Set Node Version
uses: actions/setup-node@v3
with:
node-version: 14.x
- name: Restore cache
uses: actions/cache@v3

View File

@@ -33,23 +33,15 @@ jobs:
- name: File name checker
run: yarn lint:filename
audit:
lint-commit:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: ./.github/actions/provision
with:
fetch-depth: 0
- name: Audit
run: yarn audit-ci --high --skip-dev
typecheck:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: ./.github/actions/provision
- name: Typecheck
run: yarn typecheck
- name: Lint commit message
uses: wagoid/commitlint-github-action@v4
lint-deps:
runs-on: ubuntu-latest
@@ -60,27 +52,6 @@ jobs:
- name: Lint dependency rules
run: yarn lint:deps
locked-versions:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: ./.github/actions/provision
- name: Check exact versions
uses: ./.github/actions/check-version-lock
test-unit:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: ./.github/actions/provision
- name: Build
run: yarn build
- name: Test
run: yarn test:unit
lint-message-schema:
runs-on: ubuntu-latest
steps:
@@ -112,7 +83,34 @@ jobs:
- run: yarn web-ext lint
build:
locked-versions:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: ./.github/actions/provision
- name: Check exact versions
uses: ./.github/actions/check-version-lock
audit:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: ./.github/actions/provision
- name: Audit
run: yarn audit-ci --high --skip-dev
typecheck:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: ./.github/actions/provision
- name: Typecheck
run: yarn typecheck
test-unit:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
@@ -121,6 +119,18 @@ jobs:
- name: Build
run: yarn build
- name: Build extension
- name: Test
run: yarn test:unit
test-build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: ./.github/actions/provision
- name: Build
run: yarn build
- name: Package extension
run: sh build-ext.sh
shell: bash

View File

@@ -1,13 +0,0 @@
name: Commit lint
on: [pull_request]
jobs:
commitlint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
with:
fetch-depth: 0
- uses: wagoid/commitlint-github-action@v4

View File

@@ -10,12 +10,12 @@ jobs:
directories:
runs-on: ubuntu-latest
outputs:
dir: ${{ steps.set-dirs.outputs.dir }}
dir: ${{ steps.set-dirs.outputs.TEST_DIRECTORIES }}
steps:
- uses: actions/checkout@v3
- id: set-dirs
working-directory: ./tests-legacy/integration
run: echo "::set-output name=dir::$( ls -d */ | xargs -0 | sed 's/\///' | jq -R -s -c 'split("\n")[:-2]')"
run: echo "TEST_DIRECTORIES=$( ls -d */ | xargs -0 | sed 's/\///' | jq -R -s -c 'split("\n")[:-2]')" >> $GITHUB_OUTPUT
test-integration:
runs-on: ubuntu-latest
@@ -28,33 +28,37 @@ jobs:
- name: Check out Git repository
uses: actions/checkout@v3
- run: echo ${{ fromJson(needs.directories.outputs.dir) }}
- run: echo ${{ matrix.dir }}
- name: Get installed Playwright version
id: playwright-version
run: echo "PLAYWRIGHT_VERSION=$(node -e "console.log(require('./package.json').devDependencies['@playwright/test'])")" >> $GITHUB_ENV
- uses: actions/cache@v3
name: Cache node_modules
id: cache-node-modules
id: cache
with:
path: '**/node_modules'
key: ${{ runner.os }}-modules-${{ hashFiles('**/yarn.lock') }}-${{ hashFiles('**/package.json') }}
key: ${{ runner.os }}-${{ hashFiles('**/yarn.lock') }}-${{ hashFiles('**/package.json') }}-${{ env.PLAYWRIGHT_VERSION }}
- uses: actions/cache@v3
name: Cache playwright
id: cache-playwright-browsers
- name: Install dependencies
if: steps.cache.outputs.cache-hit != 'true'
env:
PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: 1
run: yarn --frozen-lockfile
shell: bash
- name: Cache playwright binaries
uses: actions/cache@v3
id: playwright-cache
with:
path: '~/.cache/ms-playwright'
key: ${{ runner.os }}-playwright-browser
path: ~/.cache/ms-playwright
key: ${{ runner.os }}-playwright-cache-${{ env.PLAYWRIGHT_VERSION }}
- uses: actions/setup-node@v3
with:
node-version: 18
- name: Install Playwright browsers
run: yarn playwright install chrome
if: steps.playwright-cache.outputs.cache-hit != 'true'
- name: Install packages
uses: ./.github/actions/provision
if: steps.cache-node-modules.outputs.cache-hit != 'true'
- name: Install Playwright dependencies
run: npx playwright install --with-deps
- name: Install Playwright deps
run: yarn playwright install-deps
if: steps.playwright-cache.outputs.cache-hit != 'true'
- name: Build assets
run: yarn build:test

View File

@@ -52,7 +52,7 @@ jobs:
key: ${{ runner.os }}-playwright-cache-${{ env.PLAYWRIGHT_VERSION }}
- name: Install Playwright browsers
run: yarn playwright install chrome
run: yarn playwright install chrome chromium
if: steps.playwright-cache.outputs.cache-hit != 'true'
- name: Install Playwright deps

View File

@@ -272,7 +272,7 @@
"@types/webpack": "5.28.1",
"@types/zxcvbn": "4.4.1",
"@typescript-eslint/parser": "5.59.6",
"@vitest/coverage-istanbul": "0.31.0",
"@vitest/coverage-istanbul": "0.31.1",
"audit-ci": "6.6.1",
"babel-loader": "9.1.2",
"base64-loader": "1.0.0",
@@ -317,7 +317,7 @@
"ts-unused-exports": "7.0.3",
"tsconfig-paths-webpack-plugin": "4.0.1",
"typescript": "5.0.4",
"vitest": "0.31.0",
"vitest": "0.31.1",
"vm-browserify": "1.1.2",
"web-ext": "7.4.0",
"web-ext-submit": "7.4.0",

View File

@@ -32,7 +32,7 @@ const contentSecurityPolicyEnvironment = {
development:
"script-src 'self' 'wasm-unsafe-eval'; object-src 'self'; frame-src 'none'; frame-ancestors 'none';",
production:
"default-src 'none'; connect-src *; style-src 'unsafe-inline'; img-src 'self' https:; script-src 'self' 'wasm-unsafe-eval'; object-src 'none'; frame-src 'none'; frame-ancestors 'none';",
"default-src 'none'; connect-src *; style-src 'unsafe-inline'; img-src 'self' data: https:; script-src 'self' 'wasm-unsafe-eval'; object-src 'none'; frame-src 'none'; frame-ancestors 'none';",
};
const defaultIconEnvironment = {
@@ -42,18 +42,9 @@ const defaultIconEnvironment = {
const browserSpecificConfig = {
firefox: {
manifest_version: 2,
permissions: ['contextMenus', 'storage', '*://*/*'],
background: {
page: 'background.js',
scripts: ['background.js'],
},
web_accessible_resources: ['inpage.js'],
browser_action: {
default_title: 'Stacks',
default_popup: 'popup.html',
default_icon: defaultIconEnvironment[NODE_ENV],
},
content_security_policy: contentSecurityPolicyEnvironment[NODE_ENV],
browser_specific_settings: {
gecko: {
id: '{e22ae397-03d7-4622-bd8f-ecaca8c9b277}',
@@ -61,21 +52,9 @@ const browserSpecificConfig = {
},
},
chromium: {
manifest_version: 3,
host_permissions: ['*://*/*'],
permissions: ['contextMenus', 'storage'],
background: {
service_worker: 'background.js',
},
web_accessible_resources: [{ resources: ['inpage.js'], matches: ['*://*/*'] }],
action: {
default_title: 'Stacks',
default_popup: 'popup.html',
default_icon: defaultIconEnvironment[NODE_ENV],
},
content_security_policy: {
extension_pages: contentSecurityPolicyEnvironment[NODE_ENV],
},
},
};
@@ -83,9 +62,11 @@ const browserSpecificConfig = {
* @type {Manifest} manifest
*/
const manifest = {
manifest_version: 3,
author: 'Hiro PBC',
description:
'Hiro Wallet is a safe way to manage your STX, sign into apps, and protect your funds while interacting with Clarity smart contracts.',
permissions: ['contextMenus', 'storage'],
commands: {
_execute_browser_action: {
suggested_key: {
@@ -95,6 +76,16 @@ const manifest = {
description: 'Opens Stacks App',
},
},
host_permissions: ['*://*/*'],
content_security_policy: {
extension_pages: contentSecurityPolicyEnvironment[NODE_ENV],
},
web_accessible_resources: [{ resources: ['inpage.js'], matches: ['*://*/*'] }],
action: {
default_title: 'Stacks',
default_popup: 'popup.html',
default_icon: defaultIconEnvironment[NODE_ENV],
},
options_ui: {
page: 'index.html',
open_in_tab: true,
@@ -116,12 +107,8 @@ const name = PREVIEW_RELEASE ? 'Hiro Wallet Preview' : 'Hiro Wallet';
const prodManifest = {
name,
// CSP loosened to allow `wasm-eval` per
// https://bugs.chromium.org/p/chromium/issues/detail?id=1268576
content_security_policy:
"default-src 'none'; connect-src *; style-src 'unsafe-inline'; img-src 'self' data: https:; script-src 'self' 'wasm-eval'; object-src 'none'; frame-src 'none'; frame-ancestors 'none';",
icons: generateImageAssetUrlsWithSuffix(PREVIEW_RELEASE ? '-preview' : ''),
browser_action: {
action: {
default_icon: `assets/connect-logo/Stacks128w${PREVIEW_RELEASE ? '-preview' : ''}.png`,
},
};

View File

@@ -3,9 +3,7 @@ import { useMemo } from 'react';
import { generateSecretKey } from '@stacks/wallet-sdk';
import { logger } from '@shared/logger';
import { InternalMethods } from '@shared/message-types';
import { sendMessage } from '@shared/messages';
import { clearChromeStorage } from '@shared/storage';
import { clearChromeStorage } from '@shared/storage/redux-pesist';
import { queryClient } from '@app/common/persistence';
import { partiallyClearLocalStorage } from '@app/common/store-utils';
@@ -15,6 +13,7 @@ import { useBitcoinClient, useStacksClientAnchored } from '@app/store/common/api
import { inMemoryKeyActions } from '@app/store/in-memory-key/in-memory-key.actions';
import { keyActions } from '@app/store/keys/key.actions';
import { useCurrentKeyDetails } from '@app/store/keys/key.selectors';
import { clearWalletSession } from '@app/store/session-restore';
import { useAnalytics } from './analytics/use-analytics';
@@ -37,10 +36,6 @@ export function useKeyActions() {
return;
}
const secretKey = generateSecretKey(256);
sendMessage({
method: InternalMethods.ShareInMemoryKeyToBackground,
payload: { secretKey, keyId: 'default' },
});
return dispatch(inMemoryKeyActions.generateWalletKey(secretKey));
},
@@ -57,7 +52,7 @@ export function useKeyActions() {
},
async signOut() {
sendMessage({ method: InternalMethods.RemoveInMemoryKeys, payload: undefined });
await clearWalletSession();
dispatch(keyActions.signOut());
await clearChromeStorage();
partiallyClearLocalStorage();
@@ -65,8 +60,8 @@ export function useKeyActions() {
queryClient.clear();
},
lockWallet() {
sendMessage({ method: InternalMethods.RemoveInMemoryKeys, payload: undefined });
async lockWallet() {
await clearWalletSession();
return dispatch(inMemoryKeyActions.lockWallet());
},
}),

View File

@@ -3,7 +3,7 @@ import toast from 'react-hot-toast';
import * as reduxPersist from 'redux-persist';
import { getLogsFromBrowserStorage } from '@shared/logger-storage';
import { persistConfig } from '@shared/storage';
import { persistConfig } from '@shared/storage/redux-pesist';
import { store } from './store';
import { stxChainSlice } from './store/chains/stx-chain.slice';

View File

@@ -37,7 +37,6 @@ export function Container() {
<SwitchAccountDrawer />
<SettingsDropdown />
<Toaster position="bottom-center" toastOptions={{ style: { fontSize: '14px' } }} />
<ContainerLayout header={routeHeader}>
<Outlet />
</ContainerLayout>

View File

@@ -3,8 +3,6 @@ import toast from 'react-hot-toast';
import { useDispatch } from 'react-redux';
import { useNavigate } from 'react-router-dom';
import { InternalMethods } from '@shared/message-types';
import { sendMessage } from '@shared/messages';
import { RouteUrls } from '@shared/route-urls';
import { keySlice } from '@app/store/keys/key.slice';
@@ -29,10 +27,6 @@ export function useTriggerLedgerDeviceRequestStacksKeys() {
publicKeys,
})
);
// It's possible a user may have first generated a key, then decided
// they wanted to pair with Ledger. Here, we kill all in memory keys when
// a new Ledger wallet is created
sendMessage({ method: InternalMethods.RemoveInMemoryKeys, payload: undefined });
navigate(RouteUrls.Home);
},
}),

View File

@@ -1,15 +1,13 @@
import { createRoot } from 'react-dom/client';
import { InternalMethods } from '@shared/message-types';
import { initSentry } from '@shared/utils/analytics';
import { warnUsersAboutDevToolsDangers } from '@shared/utils/dev-tools-warning-log';
import { persistAndRenderApp } from '@app/common/persistence';
import { restoreWalletSession } from '@app/store/session-restore';
import { App } from './app';
import { setDebugOnGlobal } from './debug';
import { store } from './store';
import { inMemoryKeyActions } from './store/in-memory-key/in-memory-key.actions';
initSentry();
warnUsersAboutDevToolsDangers();
@@ -23,19 +21,8 @@ declare global {
window.__APP_VERSION__ = VERSION;
async function checkForInMemoryKeys() {
return new Promise(resolve =>
chrome.runtime.sendMessage({ method: InternalMethods.RequestInMemoryKeys }, resp => {
if (!resp) return resolve(true);
if (Object.keys(resp).length === 0) return resolve(true);
store.dispatch(inMemoryKeyActions.setKeysInMemory(resp));
resolve(true);
})
);
}
async function renderApp() {
await checkForInMemoryKeys();
await restoreWalletSession();
const container = document.getElementById('app');
return createRoot(container!).render(<App />);
}

View File

@@ -21,6 +21,9 @@ export function AccountGate({ children }: AccountGateProps) {
const currentKeyDetails = useCurrentKeyDetails();
const currentInMemorySecretKey = useDefaultWalletSecretKey();
// console.log('currentKeyDetails', currentKeyDetails);
// console.log('currentInMemorySecretKey', currentInMemorySecretKey);
if (currentKeyDetails?.type === 'ledger') return <>{children}</>;
if (shouldNavigateToOnboardingStartPage(currentKeyDetails))

View File

@@ -3,8 +3,9 @@ import { keyActions } from '@app/store/keys/key.actions';
export function useOnSignOut(handler: () => void) {
useOnMount(() => {
chrome.runtime.onMessage.addListener(message => {
chrome.runtime.onMessage.addListener((message, _sender, sendResponse) => {
if (message?.method === keyActions.signOut.type) handler();
sendResponse();
});
});
}

View File

@@ -11,12 +11,17 @@ export function useOnOriginTabClose(handler: () => void) {
const analytics = useAnalytics();
useEffect(() => {
const messageHandler = (message: BackgroundMessages) => {
const messageHandler = (
message: BackgroundMessages,
_sender: chrome.runtime.MessageSender,
sendResponse: () => void
) => {
if (message.method !== InternalMethods.OriginatingTabClosed) return;
if (message.payload.tabId === tabId) {
handler();
void analytics.track('requesting_origin_tab_closed_with_pending_action');
}
sendResponse();
};
chrome.runtime.onMessage.addListener(messageHandler);

View File

@@ -3,8 +3,9 @@ import { inMemoryKeyActions } from '@app/store/in-memory-key/in-memory-key.actio
export function useOnWalletLock(handler: () => void) {
useOnMount(() => {
chrome.runtime.onMessage.addListener(message => {
chrome.runtime.onMessage.addListener((message, _sender, sendResponse) => {
if (message?.method === inMemoryKeyActions.lockWallet.type) handler();
sendResponse();
});
});
}

View File

@@ -6,7 +6,7 @@ import { selectCurrentAccountIndex } from '../keys/key.selectors';
// This is only used when there is a pending transaction request and
// the user switches accounts during the signing process
export const hasSwitchedAccountsState = atom<boolean>(false);
export const hasSwitchedAccountsState = atom(false);
const hasCreatedAccountState = atom<boolean>(false);

View File

@@ -1,19 +1,30 @@
import { Wallet } from '@stacks/wallet-sdk';
import memoize from 'promise-memoize';
import { deriveStacksAccounts } from '@shared/crypto/stacks/derive-stacks-accounts';
import { InternalMethods } from '@shared/message-types';
import { RequestDerivedStxAccounts } from '@shared/messages';
import { delay } from '@app/common/utils';
import { RootState } from '@app/store';
export const selectStacksChain = (state: RootState) => state.chains.stx;
export const deriveWalletWithAccounts = memoize(
const requestDerivedStacksAccountFromBackground = memoize(async () => {
async (secretKey: string, highestAccountIndex: number): Promise<Wallet> => {
const message: RequestDerivedStxAccounts = {
method: InternalMethods.RequestDerivedStxAccounts,
payload: { secretKey, highestAccountIndex },
};
return new Promise(resolve => chrome.runtime.sendMessage(message, resp => resolve(resp)));
}
);
};
});
export async function deriveWalletWithAccounts(secretKey: string, highestAccountIndex: number) {
const cachedResult = await Promise.race([
requestDerivedStacksAccountFromBackground(),
delay(1000),
]);
if (cachedResult) return cachedResult as Wallet;
return deriveStacksAccounts(secretKey, highestAccountIndex);
}

View File

@@ -16,7 +16,7 @@ import {
import { PersistPartial } from 'redux-persist/es/persistReducer';
import { IS_DEV_ENV } from '@shared/environment';
import { persistConfig } from '@shared/storage';
import { persistConfig } from '@shared/storage/redux-pesist';
import { analyticsSlice } from './analytics/analytics.slice';
import { appPermissionsSlice } from './app-permissions/app-permissions.slice';

View File

@@ -2,8 +2,6 @@ import { AddressVersion } from '@stacks/transactions';
import { decryptMnemonic, encryptMnemonic } from '@shared/crypto/mnemonic-encryption';
import { logger } from '@shared/logger';
import { InternalMethods } from '@shared/message-types';
import { sendMessage } from '@shared/messages';
import { recurseAccountsForActivity } from '@app/common/account-restoration/account-restore';
import { checkForLegacyGaiaConfigWithKnownGeneratedAccountIndex } from '@app/common/account-restoration/legacy-gaia-config-lookup';
@@ -11,6 +9,7 @@ import { BitcoinClient } from '@app/query/bitcoin/bitcoin-client';
import { fetchNamesForAddress } from '@app/query/stacks/bns/bns.utils';
import { StacksClient } from '@app/query/stacks/stacks-client';
import { AppThunk } from '@app/store';
import { initalizeWalletSession } from '@app/store/session-restore';
import { getNativeSegwitMainnetAddressFromMnemonic } from '../accounts/blockchain/bitcoin/bitcoin-keychain';
import { getStacksAddressByIndex } from '../accounts/blockchain/stacks/stacks-keychain';
@@ -30,7 +29,13 @@ function setWalletEncryptionPassword(args: {
return async (dispatch, getState) => {
const secretKey = selectDefaultWalletKey(getState());
if (!secretKey) throw new Error('Cannot generate wallet without first having generated a key');
const { encryptedSecretKey, salt } = await encryptMnemonic({ secretKey, password });
const { encryptedSecretKey, salt, encryptionKey } = await encryptMnemonic({
secretKey,
password,
});
await initalizeWalletSession(encryptionKey, secretKey);
const legacyAccountActivityLookup =
await checkForLegacyGaiaConfigWithKnownGeneratedAccountIndex(secretKey);
@@ -76,12 +81,6 @@ function setWalletEncryptionPassword(args: {
dispatch(stxChainSlice.actions.restoreAccountIndex(recursiveActivityIndex));
});
sendMessage({
method: InternalMethods.ShareInMemoryKeyToBackground,
payload: { secretKey, keyId: defaultKeyId },
});
dispatch(inMemoryKeySlice.actions.setKeysInMemory({ default: secretKey }));
dispatch(
keySlice.actions.createStacksSoftwareWalletComplete({
type: 'software',
@@ -100,11 +99,9 @@ function unlockWalletAction(password: string): AppThunk {
const currentKey = selectCurrentKey(getState());
if (!currentKey) return;
if (currentKey.type !== 'software') return;
const { secretKey } = await decryptMnemonic({ password, ...currentKey });
sendMessage({
method: InternalMethods.ShareInMemoryKeyToBackground,
payload: { secretKey: secretKey, keyId: defaultKeyId },
});
const { secretKey, encryptionKey } = await decryptMnemonic({ password, ...currentKey });
await initalizeWalletSession(encryptionKey, secretKey);
dispatch(inMemoryKeySlice.actions.setKeysInMemory({ default: secretKey }));
};
}

View File

@@ -0,0 +1,72 @@
import { decrypt } from '@stacks/wallet-sdk';
import { InternalMethods } from '@shared/message-types';
import { sendMessage } from '@shared/messages';
import { whenBrowserRuntime } from '@shared/utils/get-browser-runtime';
import { store } from '@app/store';
import { inMemoryKeyActions } from '@app/store/in-memory-key/in-memory-key.actions';
import { selectCurrentKey } from '@app/store/keys/key.selectors';
import { defaultKeyId } from '@app/store/keys/key.slice';
export async function initalizeWalletSession(encryptionKey: string, secretKey: string) {
return await whenBrowserRuntime({
async chromium() {
return chrome.storage.session.set({ encryptionKey });
},
async firefox() {
return sendMessage({
method: InternalMethods.ShareInMemoryKeyToBackground,
payload: { secretKey, keyId: defaultKeyId },
});
},
})();
}
export async function clearWalletSession() {
return await whenBrowserRuntime({
async chromium() {
return chrome.storage.session.remove('encryptionKey');
},
async firefox() {
return chrome.runtime.sendMessage({ method: InternalMethods.RemoveInMemoryKeys });
},
})();
}
export async function restoreWalletSession() {
return whenBrowserRuntime({
async chromium() {
const key = await chrome.storage.session.get(['encryptionKey']);
if (!key.encryptionKey) return false;
try {
const currentKey = selectCurrentKey(store.getState());
if (currentKey?.type === 'software') {
const secretKey = await decrypt(currentKey.encryptedSecretKey, key.encryptionKey);
store.dispatch(inMemoryKeyActions.setKeysInMemory({ default: secretKey }));
return true;
}
} catch (e) {
return false;
}
return false;
},
async firefox() {
return checkForInMemoryKeys();
},
})();
}
async function checkForInMemoryKeys() {
return new Promise(resolve =>
chrome.runtime.sendMessage({ method: InternalMethods.RequestInMemoryKeys }, resp => {
if (!resp) resolve(false);
if (Object.keys(resp).length === 0) return resolve(false);
store.dispatch(inMemoryKeyActions.setKeysInMemory(resp));
resolve(true);
})
);
}

View File

@@ -21,10 +21,6 @@ initSentry();
initContextMenuActions();
warnUsersAboutDevToolsDangers();
const keepAlive = () => setInterval(async () => await chrome.runtime.getPlatformInfo(), 20_000);
chrome.runtime.onStartup.addListener(keepAlive);
keepAlive();
const IS_TEST_ENV = process.env.TEST_ENV === 'true';
chrome.runtime.onInstalled.addListener(async details => {
@@ -69,34 +65,34 @@ chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
return true;
});
const storageArea = chrome.storage.local as chrome.storage.LocalStorageArea;
// const storageArea = chrome.storage.local as chrome.storage.LocalStorageArea;
const testIntervalMs = 10000;
const storageWaitTimeMs = 100;
// const testIntervalMs = 10000;
// const storageWaitTimeMs = 100;
// @see https://bugs.chromium.org/p/chromium/issues/detail?id=1316588
async function hasChromiumIssue1316588() {
return new Promise(resolve => {
let dispatched = false;
const testEventDispatching = () => {
storageArea.onChanged.removeListener(testEventDispatching);
dispatched = true;
};
storageArea.onChanged.addListener(testEventDispatching);
void storageArea.set({ testEventDispatching: Math.random() });
setTimeout(() => resolve(!dispatched), storageWaitTimeMs);
});
}
// async function hasChromiumIssue1316588() {
// return new Promise(resolve => {
// let dispatched = false;
// const testEventDispatching = () => {
// storageArea.onChanged.removeListener(testEventDispatching);
// dispatched = true;
// };
// storageArea.onChanged.addListener(testEventDispatching);
// void storageArea.set({ testEventDispatching: Math.random() });
// setTimeout(() => resolve(!dispatched), storageWaitTimeMs);
// });
// }
function fixChromiumIssue1316588() {
void hasChromiumIssue1316588().then(hasIssue => {
if (hasIssue) {
chrome.runtime.reload();
return;
}
// function fixChromiumIssue1316588() {
// void hasChromiumIssue1316588().then(hasIssue => {
// if (hasIssue) {
// chrome.runtime.reload();
// return;
// }
setTimeout(fixChromiumIssue1316588, testIntervalMs);
});
}
// setTimeout(fixChromiumIssue1316588, testIntervalMs);
// });
// }
fixChromiumIssue1316588();
// fixChromiumIssue1316588();

View File

@@ -1,7 +1,4 @@
// import { StacksMainnet } from '@stacks/network';
import { generateNewAccount, generateWallet } from '@stacks/wallet-sdk';
import memoize from 'promise-memoize';
import { deriveStacksAccountsMemoized } from '@shared/crypto/stacks/derive-stacks-accounts';
import { logger } from '@shared/logger';
import { InternalMethods } from '@shared/message-types';
import { BackgroundMessages } from '@shared/messages';
@@ -11,18 +8,6 @@ function validateMessagesAreFromExtension(sender: chrome.runtime.MessageSender)
return sender.url?.startsWith(chrome.runtime.getURL(''));
}
const deriveWalletWithAccounts = memoize(async (secretKey: string, highestAccountIndex: number) => {
// Here we only want the resulting `Wallet` objects, but the API
// requires a password (so it can also return an encrypted key)
const wallet = await generateWallet({ secretKey, password: '' });
let walWithAccounts = wallet;
for (let i = 0; i < highestAccountIndex; i++) {
walWithAccounts = generateNewAccount(walWithAccounts);
}
return walWithAccounts;
});
// Persists keys in memory for the duration of the background scripts life
const inMemoryKeys = new Map();
const inMemoryFormState = new Map<number, object>();
@@ -35,17 +20,19 @@ export async function internalBackgroundMessageHandler(
sender: chrome.runtime.MessageSender,
sendResponse: (response?: any) => void
) {
// console.info('Internal msg', message, validateMessagesAreFromExtension(sender));
if (!validateMessagesAreFromExtension(sender)) {
logger.error('Error: Received background script msg from ' + sender.url);
sendResponse();
return;
}
// logger.debug('Internal message', message);
logger.debug('Internal message', message);
switch (message.method) {
case InternalMethods.RequestDerivedStxAccounts: {
const { secretKey, highestAccountIndex } = message.payload;
const walletsWithAccounts = await deriveWalletWithAccounts(secretKey, highestAccountIndex);
const walletsWithAccounts = await deriveStacksAccountsMemoized(
secretKey,
highestAccountIndex
);
sendResponse(walletsWithAccounts);
break;
}
@@ -59,7 +46,6 @@ export async function internalBackgroundMessageHandler(
case InternalMethods.RequestInMemoryKeys: {
sendResponse(Object.fromEntries(inMemoryKeys));
sendResponse();
break;
}
@@ -71,11 +57,13 @@ export async function internalBackgroundMessageHandler(
case InternalMethods.SetActiveFormState: {
const { tabId, ...state } = message.payload;
inMemoryFormState.set(tabId, state);
sendResponse();
break;
}
case InternalMethods.ClearActiveFormState: {
inMemoryFormState.delete(message.payload.tabId);
sendResponse();
break;
}
@@ -85,7 +73,4 @@ export async function internalBackgroundMessageHandler(
break;
}
}
// As browser is instructed that, if there is a response it will be
// asyncronous, we must always return a response, even if empty
sendResponse();
}

View File

@@ -1,11 +1,11 @@
import { WorkerScript, createWorker } from '../workers';
const worker = createWorker(WorkerScript.DecryptionWorker);
interface GenerateEncryptionKeyArgs {
interface DeriveEncryptionKeyArgs {
password: string;
salt: string;
}
export async function generateEncryptionKey(args: GenerateEncryptionKeyArgs): Promise<string> {
export async function deriveEncryptionKey(args: DeriveEncryptionKeyArgs): Promise<string> {
return new Promise(resolve => {
const handler = (e: MessageEvent<string>) => resolve(e.data);
worker.addEventListener('message', handler);

View File

@@ -1,7 +1,7 @@
import { bytesToHex } from '@stacks/common';
import { decrypt, encrypt } from '@stacks/wallet-sdk';
import { generateEncryptionKey } from './generate-encryption-key';
import { deriveEncryptionKey } from './generate-encryption-key';
import { generateRandomHexString } from './generate-random-hex';
interface EncryptMnemonicArgs {
@@ -10,11 +10,12 @@ interface EncryptMnemonicArgs {
}
export async function encryptMnemonic({ secretKey, password }: EncryptMnemonicArgs) {
const salt = generateRandomHexString();
const argonHash = await generateEncryptionKey({ password, salt });
const encryptedBuffer = await encrypt(secretKey, argonHash);
const encryptionKey = await deriveEncryptionKey({ password, salt });
const encryptedBuffer = await encrypt(secretKey, encryptionKey);
return {
salt,
encryptedSecretKey: bytesToHex(encryptedBuffer),
encryptionKey,
};
}
@@ -36,14 +37,16 @@ export async function decryptMnemonic({
encryptedSecretKey: string;
salt: string;
secretKey: string;
encryptionKey: string;
}> {
if (salt) {
const pw = await generateEncryptionKey({ password, salt });
const secretKey = await decrypt(Buffer.from(encryptedSecretKey, 'hex'), pw);
const encryptionKey = await deriveEncryptionKey({ password, salt });
const secretKey = await decrypt(Buffer.from(encryptedSecretKey, 'hex'), encryptionKey);
return {
secretKey,
encryptedSecretKey,
salt,
encryptionKey,
};
} else {
// if there is no salt, decrypt the secret key, then re-encrypt with an argon2 hash
@@ -53,6 +56,7 @@ export async function decryptMnemonic({
secretKey,
encryptedSecretKey: newEncryptedKey.encryptedSecretKey,
salt: newEncryptedKey.salt,
encryptionKey: newEncryptedKey.encryptedSecretKey,
};
}
}

View File

@@ -0,0 +1,15 @@
import { generateNewAccount, generateWallet } from '@stacks/wallet-sdk';
import memoize from 'promise-memoize';
export async function deriveStacksAccounts(secretKey: string, highestAccountIndex: number) {
// Here we only want the resulting `Wallet` objects, but the API
// requires a password (so it can also return an encrypted key)
const wallet = await generateWallet({ secretKey, password: '' });
let walWithAccounts = wallet;
for (let i = 0; i < highestAccountIndex; i++) {
walWithAccounts = generateNewAccount(walWithAccounts);
}
return walWithAccounts;
}
export const deriveStacksAccountsMemoized = memoize(deriveStacksAccounts);

View File

@@ -25,14 +25,14 @@ type ClearActiveFormState = BackgroundMessage<
{ tabId: number }
>;
type ShareInMemoryKeyToBackground = BackgroundMessage<
type FirefoxShareInMemoryKeyToBackground = BackgroundMessage<
InternalMethods.ShareInMemoryKeyToBackground,
{ secretKey: string; keyId: string }
>;
type RequestInMemoryKeys = BackgroundMessage<InternalMethods.RequestInMemoryKeys>;
type FirefoxRequestInMemoryKeys = BackgroundMessage<InternalMethods.RequestInMemoryKeys>;
type RemoveInMemoryKeys = BackgroundMessage<InternalMethods.RemoveInMemoryKeys>;
type FirefoxRemoveInMemoryKeys = BackgroundMessage<InternalMethods.RemoveInMemoryKeys>;
type OriginatingTabClosed = BackgroundMessage<
InternalMethods.OriginatingTabClosed,
@@ -44,9 +44,9 @@ export type BackgroundMessages =
| GetActiveFormState
| SetActiveFormState
| ClearActiveFormState
| ShareInMemoryKeyToBackground
| RequestInMemoryKeys
| RemoveInMemoryKeys
| FirefoxShareInMemoryKeyToBackground
| FirefoxRequestInMemoryKeys
| FirefoxRemoveInMemoryKeys
| OriginatingTabClosed;
export function sendMessage(message: BackgroundMessages) {

View File

@@ -2,7 +2,7 @@ import { PersistConfig, createMigrate, getStoredState } from 'redux-persist';
import type { RootState } from '@app/store';
import { analytics } from './utils/analytics';
import { analytics } from '../utils/analytics';
export async function clearChromeStorage(): Promise<void> {
return new Promise(resolve => chrome.storage.local.clear(resolve));

View File

@@ -8,7 +8,7 @@ import {
SENTRY_DSN,
WALLET_ENVIRONMENT,
} from '@shared/environment';
import { persistConfig } from '@shared/storage';
import { persistConfig } from '@shared/storage/redux-pesist';
import type { RootState } from '@app/store';

View File

@@ -0,0 +1,11 @@
type BrowserRuntime = 'chromium' | 'firefox';
function getBrowserRuntime(): BrowserRuntime {
return chrome.runtime.getURL('').startsWith('moz-extension://') ? 'firefox' : 'chromium';
}
type WhenBrowserRuntimeMap<T> = Record<BrowserRuntime, T>;
export function whenBrowserRuntime<T>(runtimeMap: WhenBrowserRuntimeMap<T>) {
return runtimeMap[getBrowserRuntime()];
}

View File

@@ -121,7 +121,11 @@ describe(`Onboarding integration tests`, () => {
await wallet.waitForHomePage();
await wallet.waitForsuggestedStepsList();
const stepsToStartBtns = await wallet.page.$$(wallet.$suggestedStepStartBtn);
await stepsToStartBtns[1].click();
const [newPage] = await Promise.all([
browser.context.waitForEvent('page'),
stepsToStartBtns[1].click(),
]);
await newPage.waitForLoadState();
const stepsDone = await wallet.page.$$(wallet.$suggestedStepDoneBadge);
expect(stepsDone.length).toEqual(2);
});

View File

@@ -1,5 +1,5 @@
import { BrowserContext, Page } from '@playwright/test';
import { OnboardingSelectors } from '@tests/selectors/onboarding.selectors';
import { BrowserContext, Page } from 'playwright';
import { createTestSelector } from '../integration/utils';

View File

@@ -1,5 +1,5 @@
import { Page } from '@playwright/test';
import { FundPageSelectors } from '@tests-legacy/page-objects/fund.selectors';
import { Page } from 'playwright';
import { createTestSelector } from '../integration/utils';

View File

@@ -1,5 +1,5 @@
import { Page } from '@playwright/test';
import { NetworkSelectors } from '@tests-legacy/integration/network.selectors';
import { Page } from 'playwright';
import { createTestSelector } from '../integration/utils';

View File

@@ -1,5 +1,5 @@
import { Page } from '@playwright/test';
import { ProfileUpdatingSelectors } from '@tests-legacy/integration/profile/profile-updating.selector';
import { Page } from 'playwright';
import { createTestSelector } from '../integration/utils';

View File

@@ -1,5 +1,5 @@
import { Page } from '@playwright/test';
import { SharedComponentsSelectors } from '@tests/selectors/shared-component.selectors';
import { Page } from 'playwright';
import { createTestSelector } from '../integration/utils';
import { TransactionSigningSelectors } from './transaction-signing.selectors';

View File

@@ -1,9 +1,9 @@
import { Page } from '@playwright/test';
import { SettingsSelectors } from '@tests-legacy/integration/settings.selectors';
import { HomePageSelectorsLegacy } from '@tests-legacy/page-objects/home.selectors';
import { HomePageSelectors } from '@tests/selectors/home.selectors';
import { OnboardingSelectors } from '@tests/selectors/onboarding.selectors';
import { SettingsMenuSelectors } from '@tests/selectors/settings.selectors';
import { Page } from 'playwright';
import { RouteUrls } from '@shared/route-urls';

View File

@@ -1,8 +1,6 @@
import { Page } from '@playwright/test';
import { setupMockApis } from '@tests/mocks/mock-apis';
// const delay = (ms: number) => new Promise(res => setTimeout(res, ms));
export class GlobalPage {
readonly page: Page;
@@ -17,6 +15,7 @@ export class GlobalPage {
async setupAndUseApiCalls(extensionId: string) {
await this.page.route(/.*/, route => route.continue());
await setupMockApis(this.page);
await this.page.waitForTimeout(600);
await this.gotoNakedRoot(extensionId);
// I've no idea why this delay is needed. Was required when switching to MV3. Without it,
// this error is thrown. Let's try removing with later versions of Playwright.
@@ -25,7 +24,6 @@ export class GlobalPage {
// =========================== logs ===========================
// navigating to "chrome-extension://bcokkkbghbnpbjpkoifjakaofdjfgeaj/index.html", waiting until "load"
// ============================================================
// await delay(600);
// await this.page.goto(`chrome-extension://${extensionId}/index.html`);
}

View File

@@ -5287,7 +5287,7 @@
dependencies:
"@types/chai" "*"
"@types/chai@*", "@types/chai@^4.3.4":
"@types/chai@*", "@types/chai@^4.3.5":
version "4.3.5"
resolved "https://registry.yarnpkg.com/@types/chai/-/chai-4.3.5.tgz#ae69bcbb1bebb68c4ac0b11e9d8ed04526b3562b"
integrity sha512-mEo1sAde+UCE6b2hxn332f1g1E8WfYRu6p5SvTKr2ZKC1f7gFJXk4h5PyGP9Dt6gCaG8y8XhwnXWC6Iy2cmBng==
@@ -6370,10 +6370,10 @@
"@typescript-eslint/types" "5.59.6"
eslint-visitor-keys "^3.3.0"
"@vitest/coverage-istanbul@0.31.0":
version "0.31.0"
resolved "https://registry.yarnpkg.com/@vitest/coverage-istanbul/-/coverage-istanbul-0.31.0.tgz#57d14c5d66685f9fe0d80e5884e940c0872a7fd6"
integrity sha512-SaTI1PSpCRtBhJ5ihBx7Z+jgrFAQlDjuI4MFmKQ/HjyYWzEoaU+I062SquRrOLjJtVOHnwJdjVJXKi0dgFiR9Q==
"@vitest/coverage-istanbul@0.31.1":
version "0.31.1"
resolved "https://registry.yarnpkg.com/@vitest/coverage-istanbul/-/coverage-istanbul-0.31.1.tgz#c9749bf404470c2b12dcb6ad851e89d27f88fc42"
integrity sha512-wLVROukTxWflIub4QUT5TenA2zx2ypUjRp636yiMTidN5hvRhnUNEEkRavJMEpYuWYYMN23E7EmAxo/MzRRmIA==
dependencies:
istanbul-lib-coverage "^3.2.0"
istanbul-lib-instrument "^5.2.1"
@@ -6382,45 +6382,45 @@
istanbul-reports "^3.1.5"
test-exclude "^6.0.0"
"@vitest/expect@0.31.0":
version "0.31.0"
resolved "https://registry.yarnpkg.com/@vitest/expect/-/expect-0.31.0.tgz#37ab35d4f75c12826c204f2a0290e0c2e5ef1192"
integrity sha512-Jlm8ZTyp6vMY9iz9Ny9a0BHnCG4fqBa8neCF6Pk/c/6vkUk49Ls6UBlgGAU82QnzzoaUs9E/mUhq/eq9uMOv/g==
"@vitest/expect@0.31.1":
version "0.31.1"
resolved "https://registry.yarnpkg.com/@vitest/expect/-/expect-0.31.1.tgz#db8cb5a14a91167b948f377b9d29442229c73747"
integrity sha512-BV1LyNvhnX+eNYzJxlHIGPWZpwJFZaCcOIzp2CNG0P+bbetenTupk6EO0LANm4QFt0TTit+yqx7Rxd1qxi/SQA==
dependencies:
"@vitest/spy" "0.31.0"
"@vitest/utils" "0.31.0"
"@vitest/spy" "0.31.1"
"@vitest/utils" "0.31.1"
chai "^4.3.7"
"@vitest/runner@0.31.0":
version "0.31.0"
resolved "https://registry.yarnpkg.com/@vitest/runner/-/runner-0.31.0.tgz#ca830405ae4c2744ae5fb7fbe85df81b56430ebc"
integrity sha512-H1OE+Ly7JFeBwnpHTrKyCNm/oZgr+16N4qIlzzqSG/YRQDATBYmJb/KUn3GrZaiQQyL7GwpNHVZxSQd6juLCgw==
"@vitest/runner@0.31.1":
version "0.31.1"
resolved "https://registry.yarnpkg.com/@vitest/runner/-/runner-0.31.1.tgz#fc06260d4824dde624abaeea1825d6a75bad4583"
integrity sha512-imWuc82ngOtxdCUpXwtEzZIuc1KMr+VlQ3Ondph45VhWoQWit5yvG/fFcldbnCi8DUuFi+NmNx5ehMUw/cGLUw==
dependencies:
"@vitest/utils" "0.31.0"
"@vitest/utils" "0.31.1"
concordance "^5.0.4"
p-limit "^4.0.0"
pathe "^1.1.0"
"@vitest/snapshot@0.31.0":
version "0.31.0"
resolved "https://registry.yarnpkg.com/@vitest/snapshot/-/snapshot-0.31.0.tgz#f59c4bcf0d03f1f494ee09286965e60a1e0cab64"
integrity sha512-5dTXhbHnyUMTMOujZPB0wjFjQ6q5x9c8TvAsSPUNKjp1tVU7i9pbqcKPqntyu2oXtmVxKbuHCqrOd+Ft60r4tg==
"@vitest/snapshot@0.31.1":
version "0.31.1"
resolved "https://registry.yarnpkg.com/@vitest/snapshot/-/snapshot-0.31.1.tgz#7fc3f1e48f0c4313e6cb795c17a2c1aa909a7d64"
integrity sha512-L3w5uU9bMe6asrNzJ8WZzN+jUTX4KSgCinEJPXyny0o90fG4FPQMV0OWsq7vrCWfQlAilMjDnOF9nP8lidsJ+g==
dependencies:
magic-string "^0.30.0"
pathe "^1.1.0"
pretty-format "^27.5.1"
"@vitest/spy@0.31.0":
version "0.31.0"
resolved "https://registry.yarnpkg.com/@vitest/spy/-/spy-0.31.0.tgz#98cb19046c0bd2673a73d6c90ee1533d1be82136"
integrity sha512-IzCEQ85RN26GqjQNkYahgVLLkULOxOm5H/t364LG0JYb3Apg0PsYCHLBYGA006+SVRMWhQvHlBBCyuByAMFmkg==
"@vitest/spy@0.31.1":
version "0.31.1"
resolved "https://registry.yarnpkg.com/@vitest/spy/-/spy-0.31.1.tgz#1c3b6a3eec4ce81b8889e19c7fac6a603b600b14"
integrity sha512-1cTpt2m9mdo3hRLDyCG2hDQvRrePTDgEJBFQQNz1ydHHZy03EiA6EpFxY+7ODaY7vMRCie+WlFZBZ0/dQWyssQ==
dependencies:
tinyspy "^2.1.0"
"@vitest/utils@0.31.0":
version "0.31.0"
resolved "https://registry.yarnpkg.com/@vitest/utils/-/utils-0.31.0.tgz#d0aae17150b95ebf7afdf4e5db8952ac21610ffa"
integrity sha512-kahaRyLX7GS1urekRXN2752X4gIgOGVX4Wo8eDUGUkTWlGpXzf5ZS6N9RUUS+Re3XEE8nVGqNyxkSxF5HXlGhQ==
"@vitest/utils@0.31.1":
version "0.31.1"
resolved "https://registry.yarnpkg.com/@vitest/utils/-/utils-0.31.1.tgz#b810a458b37ef16931ab0d384ce79a9500f34e07"
integrity sha512-yFyRD5ilwojsZfo3E0BnH72pSVSuLg2356cN1tCEe/0RtDzxTPYwOomIC+eQbot7m6DRy4tPZw+09mB7NkbMmA==
dependencies:
concordance "^5.0.4"
loupe "^2.3.6"
@@ -16868,7 +16868,7 @@ tiny-warning@^1.0.2:
resolved "https://registry.yarnpkg.com/tiny-warning/-/tiny-warning-1.0.3.tgz#94a30db453df4c643d0fd566060d60a875d84754"
integrity sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA==
tinybench@^2.4.0:
tinybench@^2.5.0:
version "2.5.0"
resolved "https://registry.yarnpkg.com/tinybench/-/tinybench-2.5.0.tgz#4711c99bbf6f3e986f67eb722fed9cddb3a68ba5"
integrity sha512-kRwSG8Zx4tjF9ZiyH4bhaebu+EDz1BOx9hOigYHlUW4xxI/wKIUQUqo018UlU4ar6ATPBsaMrdbKZ+tmPdohFA==
@@ -17488,10 +17488,10 @@ vinyl-buffer@^1.0.1:
bl "^1.2.1"
through2 "^2.0.3"
vite-node@0.31.0:
version "0.31.0"
resolved "https://registry.yarnpkg.com/vite-node/-/vite-node-0.31.0.tgz#8794a98f21b0cf2394bfd2aaa5fc85d2c42be084"
integrity sha512-8x1x1LNuPvE2vIvkSB7c1mApX5oqlgsxzHQesYF7l5n1gKrEmrClIiZuOFbFDQcjLsmcWSwwmrWrcGWm9Fxc/g==
vite-node@0.31.1:
version "0.31.1"
resolved "https://registry.yarnpkg.com/vite-node/-/vite-node-0.31.1.tgz#9fea18cbf9552ab262b969068249a8b8e7fb8b38"
integrity sha512-BajE/IsNQ6JyizPzu9zRgHrBwczkAs0erQf/JRpgTIESpKvNj9/Gd0vxX905klLkb0I0SJVCKbdrl5c6FnqYKA==
dependencies:
cac "^6.7.14"
debug "^4.3.4"
@@ -17511,19 +17511,19 @@ vite-node@0.31.0:
optionalDependencies:
fsevents "~2.3.2"
vitest@0.31.0:
version "0.31.0"
resolved "https://registry.yarnpkg.com/vitest/-/vitest-0.31.0.tgz#133e98f779aa81afbc7ee1fcb385a0c458b8c2c8"
integrity sha512-JwWJS9p3GU9GxkG7eBSmr4Q4x4bvVBSswaCFf1PBNHiPx00obfhHRJfgHcnI0ffn+NMlIh9QGvG75FlaIBdKGA==
vitest@0.31.1:
version "0.31.1"
resolved "https://registry.yarnpkg.com/vitest/-/vitest-0.31.1.tgz#e3d1b68a44e76e24f142c1156fe9772ef603e52c"
integrity sha512-/dOoOgzoFk/5pTvg1E65WVaobknWREN15+HF+0ucudo3dDG/vCZoXTQrjIfEaWvQXmqScwkRodrTbM/ScMpRcQ==
dependencies:
"@types/chai" "^4.3.4"
"@types/chai" "^4.3.5"
"@types/chai-subset" "^1.3.3"
"@types/node" "*"
"@vitest/expect" "0.31.0"
"@vitest/runner" "0.31.0"
"@vitest/snapshot" "0.31.0"
"@vitest/spy" "0.31.0"
"@vitest/utils" "0.31.0"
"@vitest/expect" "0.31.1"
"@vitest/runner" "0.31.1"
"@vitest/snapshot" "0.31.1"
"@vitest/spy" "0.31.1"
"@vitest/utils" "0.31.1"
acorn "^8.8.2"
acorn-walk "^8.2.0"
cac "^6.7.14"
@@ -17536,10 +17536,10 @@ vitest@0.31.0:
picocolors "^1.0.0"
std-env "^3.3.2"
strip-literal "^1.0.1"
tinybench "^2.4.0"
tinybench "^2.5.0"
tinypool "^0.5.0"
vite "^3.0.0 || ^4.0.0"
vite-node "0.31.0"
vite-node "0.31.1"
why-is-node-running "^2.2.2"
vm-browserify@1.1.2: