diff --git a/app/_locales/en/messages.json b/app/_locales/en/messages.json index b9501b008..63ede4dfb 100644 --- a/app/_locales/en/messages.json +++ b/app/_locales/en/messages.json @@ -685,6 +685,9 @@ "etherscanView": { "message": "View account on Etherscan" }, + "example": { + "message": "Example" + }, "expandView": { "message": "Expand view" }, @@ -946,6 +949,15 @@ "ipfsGatewayDescription": { "message": "Enter the URL of the IPFS CID gateway to use for ENS content resolution." }, + "ipfsIpnsDescription": { + "message": "Now metamask will resolve # and # urls directly." + }, + "ipfsIpnsExample": { + "message": "If you type # it will be resolved as " + }, + "ipfsIpnsResolvingTitle": { + "message": "IPFS and IPNS URL resolving" + }, "jsonFile": { "message": "JSON File", "description": "format for importing an account" diff --git a/app/manifest/chrome.json b/app/manifest/chrome.json index 281c847a4..79496eb63 100644 --- a/app/manifest/chrome.json +++ b/app/manifest/chrome.json @@ -3,5 +3,8 @@ "matches": ["https://metamask.io/*"], "ids": ["*"] }, - "minimum_chrome_version": "63" + "minimum_chrome_version": "63", + "permissions": [ + "chrome-extension://*/*" + ] } diff --git a/app/manifest/firefox.json b/app/manifest/firefox.json index 930e1e758..e2b08ea4f 100644 --- a/app/manifest/firefox.json +++ b/app/manifest/firefox.json @@ -4,5 +4,22 @@ "id": "webextension@metamask.io", "strict_min_version": "68.0" } - } + }, + "protocol_handlers": [ + { + "protocol": "dweb", + "name": "IPFS Companion: DWEB Protocol Handler", + "uriTemplate": "https://dweb.link/ipfs/?uri=%s" + }, + { + "protocol": "ipfs", + "name": "IPFS Companion: IPFS Protocol Handler", + "uriTemplate": "https://dweb.link/ipfs/?uri=%s" + }, + { + "protocol": "ipns", + "name": "IPFS Companion: IPNS Protocol Handler", + "uriTemplate": "https://dweb.link/ipns/?uri=%s" + } + ] } diff --git a/app/scripts/controllers/ipfs.js b/app/scripts/controllers/ipfs.js new file mode 100644 index 000000000..9d667f946 --- /dev/null +++ b/app/scripts/controllers/ipfs.js @@ -0,0 +1,33 @@ +import { ObservableStore } from '@metamask/obs-store'; + +const DEFAULT_IPFS_IPNS_ENABLED = false; + +export default class IpfsIpnsController { + constructor() { + const initState = { + ipfsIpnsEnabled: DEFAULT_IPFS_IPNS_ENABLED, + ipfsIpnsHandlerShouldUpdate: false, + }; + this.store = new ObservableStore(initState); + } + + /** + * @param {boolean} status - indicates if ipfs ipns resolving is enabled + * @returns status of ipfs ipns url resolving + */ + setIpfsIpnsUrlResolving(status) { + this.store.updateState({ + ipfsIpnsEnabled: status, + }); + return Promise.resolve(status); + } + + /** + * @param {boolean} bool - indicates if protocol handler should be updated + * @returns bool + */ + setIpfsIpnsHandlerShouldUpdate(bool) { + this.store.updateState({ ipfsIpnsHandlerShouldUpdate: bool }); + return Promise.resolve(bool); + } +} diff --git a/app/scripts/lib/ens-ipfs/setup.js b/app/scripts/lib/ens-ipfs/setup.js index 8c30de975..5b5e72455 100644 --- a/app/scripts/lib/ens-ipfs/setup.js +++ b/app/scripts/lib/ens-ipfs/setup.js @@ -5,27 +5,61 @@ import resolveEnsToIpfsContentId from './resolver'; const fetchWithTimeout = getFetchWithTimeout(30000); const supportedTopLevelDomains = ['eth']; +const supportedProtocols = ['ipfs', 'ipns']; +const supportedBrowsers = ['chrome-extension'] export default function setupEnsIpfsResolver({ provider, getCurrentChainId, getIpfsGateway, -}) { // install listener +}) { const urlPatterns = supportedTopLevelDomains.map((tld) => `*://*.${tld}/*`); + + for (const browser of supportedBrowsers) { + // only setup ipfs ipns handler on supported browsers + if(window.location.href.startsWith(browser)) { + extension.webRequest.onBeforeRequest.addListener(ipfsIpnsUrlHandler, { + types: ['main_frame'], + urls: [`${browser}://*/*`], + }); + break; + } + } + extension.webRequest.onErrorOccurred.addListener(webRequestDidFail, { - urls: urlPatterns, types: ['main_frame'], + urls: urlPatterns, }); + // return api object return { // uninstall listener remove() { extension.webRequest.onErrorOccurred.removeListener(webRequestDidFail); + extension.webRequest.onBeforeRequest.removeListener(ipfsIpnsUrlHandler); }, }; + async function ipfsIpnsUrlHandler(details) { + const { tabId, url } = details; + // ignore requests that are not associated with tabs + if (tabId === -1) { + return; + } + + const unUrl = unescape(url); + supportedProtocols.forEach((protocol) => { + if (unUrl.includes(`${protocol}:`)) { + const identifier = unUrl.split(`${protocol}:`)[1]; + const ipfsGateway = getIpfsGateway(); + const newUrl = `https://${ipfsGateway}/${protocol}${identifier}`; + extension.tabs.update(tabId, { url: newUrl }); + } + }); + } + async function webRequestDidFail(details) { const { tabId, url } = details; // ignore requests that are not associated with tabs @@ -37,10 +71,12 @@ export default function setupEnsIpfsResolver({ const { hostname: name, pathname, search, hash: fragment } = new URL(url); const domainParts = name.split('.'); const topLevelDomain = domainParts[domainParts.length - 1]; + // if unsupported TLD, abort if (!supportedTopLevelDomains.includes(topLevelDomain)) { return; } + // otherwise attempt resolve attemptResolve({ tabId, name, pathname, search, fragment }); } diff --git a/app/scripts/metamask-controller.js b/app/scripts/metamask-controller.js index 361050d86..6d0c1430f 100644 --- a/app/scripts/metamask-controller.js +++ b/app/scripts/metamask-controller.js @@ -70,6 +70,7 @@ import { segment, segmentLegacy } from './lib/segment'; import createMetaRPCHandler from './lib/createMetaRPCHandler'; import { WORKER_BLOB_URL } from './lib/worker-blob'; import { FILSNAP_NAME, setupFilsnap } from './lib/filsnap'; +import IpfsIpnsController from './controllers/ipfs'; export const METAMASK_CONTROLLER_EVENTS = { // Fired after state changes that impact the extension badge (unapproved msg count) @@ -459,6 +460,8 @@ export default class MetamaskController extends EventEmitter { ), }); + this.ipfsIpnsController = new IpfsIpnsController(); + // ensure accountTracker updates balances after network change this.networkController.on(NETWORK_EVENTS.NETWORK_DID_CHANGE, () => { this.accountTracker._updateAccounts(); @@ -495,6 +498,7 @@ export default class MetamaskController extends EventEmitter { PluginController: this.pluginController.store, ThreeBoxController: this.threeBoxController.store, AssetsController: this.assetsController.store, + IpfsIpnsController: this.ipfsIpnsController.store, }); this.memStore = new ComposableObservableStore(null, { @@ -525,6 +529,7 @@ export default class MetamaskController extends EventEmitter { EnsController: this.ensController.store, ApprovalController: this.approvalController, AssetsController: this.assetsController.store, + IpfsIpnsController: this.ipfsIpnsController.store, }); this.memStore.subscribe(this.sendUpdate.bind(this)); @@ -727,6 +732,10 @@ export default class MetamaskController extends EventEmitter { setUseNonceField: this.setUseNonceField.bind(this), setUsePhishDetect: this.setUsePhishDetect.bind(this), setIpfsGateway: this.setIpfsGateway.bind(this), + setIpfsIpnsUrlResolving: this.setIpfsIpnsUrlResolving.bind(this), + setIpfsIpnsHandlerShouldUpdate: this.setIpfsIpnsHandlerShouldUpdate.bind( + this, + ), setParticipateInMetaMetrics: this.setParticipateInMetaMetrics.bind(this), setMetaMetricsSendCount: this.setMetaMetricsSendCount.bind(this), setFirstTimeFlowType: this.setFirstTimeFlowType.bind(this), @@ -2907,6 +2916,42 @@ export default class MetamaskController extends EventEmitter { } } + /** + * Sets IPFS and IPNS URL resolving + * @param {*} bool + * @param {*} cb + * @returns + */ + setIpfsIpnsUrlResolving(bool, cb) { + try { + this.ipfsIpnsController.setIpfsIpnsUrlResolving(bool); + cb(null); + return; + } catch (err) { + cb(err); + // eslint-disable-next-line no-useless-return + return; + } + } + + /** + * Sets if protocol handlers for ipfs and ipns should be updated + * @param {*} bool + * @param {*} cb + * @returns + */ + setIpfsIpnsHandlerShouldUpdate(bool, cb) { + try { + this.ipfsIpnsController.setIpfsIpnsHandlerShouldUpdate(bool); + cb(null); + return; + } catch (err) { + cb(err); + // eslint-disable-next-line no-useless-return + return; + } + } + /** * Sets whether or not the user will have usage data tracked with MetaMetrics * @param {boolean} bool - True for users that wish to opt-in, false for users that wish to remain out. diff --git a/ui/app/helpers/constants/routes.js b/ui/app/helpers/constants/routes.js index 61b21b50c..91af96057 100644 --- a/ui/app/helpers/constants/routes.js +++ b/ui/app/helpers/constants/routes.js @@ -66,6 +66,8 @@ const DECRYPT_MESSAGE_REQUEST_PATH = '/decrypt-message-request'; const ENCRYPTION_PUBLIC_KEY_REQUEST_PATH = '/encryption-public-key-request'; const CONFIRMATION_V_NEXT_ROUTE = '/confirmation'; +const IPFS_IPNS_URL_RESOLVING = '/ipfs-ipns-resolve'; + // Used to pull a convenient name for analytics tracking events. The key must // be react-router ready path, and can include params such as :id for popup windows const PATH_NAME_MAP = { @@ -135,6 +137,7 @@ const PATH_NAME_MAP = { [LOADING_QUOTES_ROUTE]: 'Swaps Loading Quotes Page', [AWAITING_SWAP_ROUTE]: 'Swaps Awaiting Swaps Page', [SWAPS_ERROR_ROUTE]: 'Swaps Error Page', + [IPFS_IPNS_URL_RESOLVING]: 'Ipfs and Ipns URL Resolving Info Page', }; export { @@ -201,4 +204,5 @@ export { AWAITING_SWAP_ROUTE, SWAPS_ERROR_ROUTE, SWAPS_MAINTENANCE_ROUTE, + IPFS_IPNS_URL_RESOLVING, }; diff --git a/ui/app/pages/ipfs-enable/index.js b/ui/app/pages/ipfs-enable/index.js new file mode 100644 index 000000000..ec256a9d9 --- /dev/null +++ b/ui/app/pages/ipfs-enable/index.js @@ -0,0 +1 @@ +export { default } from './ipfs-enable.container'; diff --git a/ui/app/pages/ipfs-enable/index.scss b/ui/app/pages/ipfs-enable/index.scss new file mode 100644 index 000000000..763a4e1bb --- /dev/null +++ b/ui/app/pages/ipfs-enable/index.scss @@ -0,0 +1,65 @@ +.ipfs-enable { + &__container { + display: flex; + min-height: 100%; + } + + &__main-view { + flex: 1 1 66.5%; + background: $white; + min-width: 0; + display: flex; + flex-direction: column; + } + + &__menu { + display: grid; + grid-template-columns: 30% minmax(30%, 1fr) 30%; + column-gap: 5px; + padding: 0 8px; + border-bottom: 1px solid $Grey-100; + height: 64px; + } + + &__title { + grid-column: 2/span 1; + place-self: center stretch; + display: flex; + flex-direction: center; + justify-content: center; + align-items: center; + flex: 1; + } + + &__content { + display: flex; + justify-content: space-between; + align-items: center; + // flex: 1; + min-height: 209px; + padding-top: 40px; + flex-direction: column; + width: 100%; + } + + &__content-item { + flex: 1; + min-width: 0; + display: flex; + flex-direction: column; + margin-bottom: 20px; + } + + &__description { + font-size: 0.875rem; + line-height: 140%; + font-style: normal; + font-weight: normal; + color: #9b9b9b; + padding-top: 5px; + } +} + +.highlight { + font-weight: bold; +} \ No newline at end of file diff --git a/ui/app/pages/ipfs-enable/ipfs-enable.component.js b/ui/app/pages/ipfs-enable/ipfs-enable.component.js new file mode 100644 index 000000000..00237a6d1 --- /dev/null +++ b/ui/app/pages/ipfs-enable/ipfs-enable.component.js @@ -0,0 +1,116 @@ +import React, { PureComponent } from 'react'; +import PropTypes from 'prop-types'; +import { IPFS_IPNS_URL_RESOLVING } from '../../helpers/constants/routes'; + +export default class IpfsEnable extends PureComponent { + static contextTypes = { + t: PropTypes.func, + }; + + static get propTypes() { + return { + ipfsGateway: PropTypes.string.isRequired, + ipfsIpnsEnabled: PropTypes.bool.isRequired, + ipfsIpnsHandlerShouldUpdate: PropTypes.bool.isRequired, + setIpfsIpnsHandlerShouldUpdate: PropTypes.func, + }; + } + + state = { + ipfsGateway: this.props.ipfsGateway, + ipfsIpnsIsEnabled: this.props.ipfsIpnsEnabled, + }; + + componentDidMount() { + const { ipfsIpnsEnabled, ipfsIpnsHandlerShouldUpdate } = this.props; + + if (ipfsIpnsHandlerShouldUpdate) { + const page = IPFS_IPNS_URL_RESOLVING.replace('/', ''); + if (ipfsIpnsEnabled) { + this.registerHandlers(page); + } else { + this.unregisterHandlers(page); + } + this.props.setIpfsIpnsHandlerShouldUpdate(false); + } + } + + registerHandlers(page) { + window.navigator.registerProtocolHandler( + 'ipfs', + window.location.href.replace(`/home.html#${page}`, '/%s'), + 'Ipfs handler', + ); + window.navigator.registerProtocolHandler( + 'ipns', + window.location.href.replace(`/home.html#${page}`, '/%s'), + 'Ipns handler', + ); + } + + unregisterHandlers(page) { + window.navigator.unregisterProtocolHandler( + 'ipfs', + window.location.href.replace(`/home.html#${page}`, '/%s'), + ); + window.navigator.unregisterProtocolHandler( + 'ipns', + window.location.href.replace(`/home.html#${page}`, '/%s'), + ); + } + + render() { + const { t } = this.context; + const ipfsIpnsDescriptionParts = t('ipfsIpnsDescription').split('#'); + const ipfsIpnsExampleParts = t('ipfsIpnsExample').split('#'); + return ( +
+
+
+
+
+ {t('ipfsIpnsResolvingTitle')} +
+
+
+ +
+
+ {t('ipfsIpnsResolvingTitle')} +
+ {this.state.ipfsIpnsIsEnabled ? 'Enabled' : 'Disabled'} +
+
+
+ {t('ipfsGateway')} +
+ {this.state.ipfsGateway} +
+
+
+ {t('example')} +
+ {ipfsIpnsDescriptionParts[0]}{' '} + ipfs://{' '} + {ipfsIpnsDescriptionParts[1]}{' '} + ipns:// + {ipfsIpnsDescriptionParts[2]} +
+
+ {ipfsIpnsExampleParts[0]}{' '} + ipfs://[CID]/{' '} + {ipfsIpnsExampleParts[1]}{' '} + + https://ipfs.io/ipfs/[CID]/ + {' '} + . +
+
+
+
+
+
+
+ ); + } +} diff --git a/ui/app/pages/ipfs-enable/ipfs-enable.container.js b/ui/app/pages/ipfs-enable/ipfs-enable.container.js new file mode 100644 index 000000000..fe0216783 --- /dev/null +++ b/ui/app/pages/ipfs-enable/ipfs-enable.container.js @@ -0,0 +1,28 @@ +import { connect } from 'react-redux'; +import { setIpfsIpnsHandlerShouldUpdate } from '../../store/actions'; +import IpfsEnable from './ipfs-enable.component'; + +const mapStateToProps = (state) => { + const { metamask } = state; + const { + ipfsGateway, + ipfsIpnsEnabled, + ipfsIpnsHandlerShouldUpdate, + } = metamask; + + return { + ipfsGateway, + ipfsIpnsEnabled, + ipfsIpnsHandlerShouldUpdate, + }; +}; + +const mapDispatchToProps = (dispatch) => { + return { + setIpfsIpnsHandlerShouldUpdate: (value) => { + dispatch(setIpfsIpnsHandlerShouldUpdate(value)); + }, + }; +}; + +export default connect(mapStateToProps, mapDispatchToProps)(IpfsEnable); diff --git a/ui/app/pages/pages.scss b/ui/app/pages/pages.scss index 09c1c58bc..cee238ecd 100644 --- a/ui/app/pages/pages.scss +++ b/ui/app/pages/pages.scss @@ -19,3 +19,4 @@ @import 'settings/index'; @import 'swaps/index'; @import 'unlock-page/index'; +@import 'ipfs-enable/index' diff --git a/ui/app/pages/routes/routes.component.js b/ui/app/pages/routes/routes.component.js index 09b5c2fca..26668dd31 100644 --- a/ui/app/pages/routes/routes.component.js +++ b/ui/app/pages/routes/routes.component.js @@ -32,6 +32,7 @@ import AppHeader from '../../components/app/app-header'; import UnlockPage from '../unlock-page'; import Alerts from '../../components/app/alerts'; import Asset from '../asset'; +import IpfsIpnsUrlHandling from '../ipfs-enable'; import { ADD_TOKEN_ROUTE, @@ -54,6 +55,7 @@ import { UNLOCK_ROUTE, BUILD_QUOTE_ROUTE, CONFIRMATION_V_NEXT_ROUTE, + IPFS_IPNS_URL_RESOLVING, } from '../../helpers/constants/routes'; import { @@ -169,6 +171,10 @@ export default class Routes extends Component { path={`${CONNECT_ROUTE}/:id`} component={PermissionsConnect} /> + diff --git a/ui/app/pages/settings/advanced-tab/advanced-tab.component.js b/ui/app/pages/settings/advanced-tab/advanced-tab.component.js index 742d1c233..be21dbec5 100644 --- a/ui/app/pages/settings/advanced-tab/advanced-tab.component.js +++ b/ui/app/pages/settings/advanced-tab/advanced-tab.component.js @@ -5,7 +5,12 @@ import { exportAsFile } from '../../../helpers/utils/util'; import ToggleButton from '../../../components/ui/toggle-button'; import TextField from '../../../components/ui/text-field'; import Button from '../../../components/ui/button'; -import { MOBILE_SYNC_ROUTE } from '../../../helpers/constants/routes'; +import { + MOBILE_SYNC_ROUTE, + IPFS_IPNS_URL_RESOLVING, +} from '../../../helpers/constants/routes'; +import { getPlatform } from '../../../../../app/scripts/lib/util'; +import { PLATFORM_CHROME } from '../../../../../shared/constants/app'; export default class AdvancedTab extends PureComponent { static contextTypes = { @@ -33,6 +38,9 @@ export default class AdvancedTab extends PureComponent { threeBoxDisabled: PropTypes.bool.isRequired, setIpfsGateway: PropTypes.func.isRequired, ipfsGateway: PropTypes.string.isRequired, + ipfsIpnsEnabled: PropTypes.bool, + setIpfsIpnsUrlResolving: PropTypes.func, + setIpfsIpnsHandlerShouldUpdate: PropTypes.func, }; state = { @@ -462,6 +470,55 @@ export default class AdvancedTab extends PureComponent { ); } + renderIpfsUrlResolveControl() { + const { t } = this.context; + const { + ipfsIpnsEnabled, + setIpfsIpnsUrlResolving, + setIpfsIpnsHandlerShouldUpdate, + } = this.props; + + const enabled = ipfsIpnsEnabled; + + if(getPlatform() == PLATFORM_CHROME) { + return ( +
+
+ Resolve IPFS urls (experimental) +
+ Turn on to have IPFS (ipfs://) and IPNS (ipns://) URLs being + resolved by Metamask. This feature is currently experimental; use at + your own risk. +
+
+
+
+ { + setIpfsIpnsUrlResolving(!enabled); + setIpfsIpnsHandlerShouldUpdate(true); + global.platform.openExtensionInBrowser(IPFS_IPNS_URL_RESOLVING); + }} + offLabel={t('off')} + onLabel={t('on')} + /> +
+
+
+ ); + } else { + return; + } + } + render() { const { warning } = this.props; @@ -478,6 +535,7 @@ export default class AdvancedTab extends PureComponent { {this.renderAutoLockTimeLimit()} {this.renderThreeBoxControl()} {this.renderIpfsGatewayControl()} + {this.renderIpfsUrlResolveControl()} ); } diff --git a/ui/app/pages/settings/advanced-tab/advanced-tab.container.js b/ui/app/pages/settings/advanced-tab/advanced-tab.container.js index 1bfa82f30..0641f4677 100644 --- a/ui/app/pages/settings/advanced-tab/advanced-tab.container.js +++ b/ui/app/pages/settings/advanced-tab/advanced-tab.container.js @@ -11,6 +11,8 @@ import { turnThreeBoxSyncingOnAndInitialize, setUseNonceField, setIpfsGateway, + setIpfsIpnsHandlerShouldUpdate, + setIpfsIpnsUrlResolving, } from '../../../store/actions'; import { getPreferences } from '../../../selectors'; import AdvancedTab from './advanced-tab.component'; @@ -26,6 +28,7 @@ export const mapStateToProps = (state) => { threeBoxDisabled, useNonceField, ipfsGateway, + ipfsIpnsEnabled, } = metamask; const { showFiatInTestnets, autoLockTimeLimit } = getPreferences(state); @@ -39,6 +42,7 @@ export const mapStateToProps = (state) => { threeBoxDisabled, useNonceField, ipfsGateway, + ipfsIpnsEnabled, }; }; @@ -68,6 +72,12 @@ export const mapDispatchToProps = (dispatch) => { setIpfsGateway: (value) => { return dispatch(setIpfsGateway(value)); }, + setIpfsIpnsUrlResolving: (value) => { + dispatch(setIpfsIpnsUrlResolving(value)); + }, + setIpfsIpnsHandlerShouldUpdate: (value) => { + dispatch(setIpfsIpnsHandlerShouldUpdate(value)); + }, }; }; diff --git a/ui/app/store/actions.js b/ui/app/store/actions.js index 81f4c5ae0..fadcfa2d5 100644 --- a/ui/app/store/actions.js +++ b/ui/app/store/actions.js @@ -2229,6 +2229,40 @@ export function setIpfsGateway(val) { }; } +export function setIpfsIpnsHandlerShouldUpdate(val) { + return (dispatch) => { + log.debug(`background.setIpfsIpnsHandlerShouldUpdate`); + return new Promise((resolve, reject) => { + background.setIpfsIpnsHandlerShouldUpdate(val, (err) => { + if (err) { + dispatch(displayWarning(err.message)); + reject(err); + return; + } + resolve(val); + }); + }); + }; +} + +export function setIpfsIpnsUrlResolving(val) { + return (dispatch) => { + dispatch(showLoadingIndication()); + log.debug(`background.setIpfsIpnsUrlResolving`); + return new Promise((resolve, reject) => { + background.setIpfsIpnsUrlResolving(val, (err) => { + dispatch(hideLoadingIndication()); + if (err) { + dispatch(displayWarning(err.message)); + reject(err); + return; + } + resolve(val); + }); + }); + }; +} + export function updateCurrentLocale(key) { return async (dispatch) => { dispatch(showLoadingIndication()); diff --git a/yarn.lock b/yarn.lock index eb076e4c0..dc3aa0c5f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6682,9 +6682,9 @@ camelcase@^6.0.0: integrity sha512-8KMDF1Vz2gzOq54ONPJS65IvTUaB1cHJ2DMM7MbPmLZljDH1qpzzLsWdiN9pHh6qvkRVDTi/07+eNGch/oLU4w== caniuse-lite@^1.0.30000810, caniuse-lite@^1.0.30000844, caniuse-lite@^1.0.30001035, caniuse-lite@^1.0.30001097, caniuse-lite@^1.0.30001157: - version "1.0.30001162" - resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001162.tgz#9f83aad1f42539ce9aab58bb177598f2f8e22ec6" - integrity sha512-E9FktFxaNnp4ky3ucIGzEXLM+Knzlpuq1oN1sFAU0KeayygabGTmOsndpo8QrL4D9pcThlf4D2pUKaDxPCUmVw== + version "1.0.30001257" + resolved "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001257.tgz" + integrity sha512-JN49KplOgHSXpIsVSF+LUyhD8PUp6xPpAXeRrrcBh4KBeP7W864jHn6RvzJgDlrReyeVjMFJL3PLpPvKIxlIHA== capture-stack-trace@^1.0.0: version "1.0.1"