Merge pull request #954 from omnivore-app/feat/extension-commands

Extension: support right click saveurl and shortcut key to save current tab
This commit is contained in:
Jackson Harper 2022-07-19 15:37:18 -07:00 committed by GitHub
commit e0a4478400
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
12 changed files with 248 additions and 207 deletions

View file

@ -2,7 +2,7 @@
"manifest_version": 2,
"name": "Omnivore",
"short_name": "Omnivore",
"version": "0.1.22",
"version": "0.1.24",
"description": "Save articles to your Omnivore library",
"author": "Omnivore Media, Inc",
"default_locale": "en",
@ -11,7 +11,7 @@
"url": "https://omnivore.app/"
},
"homepage_url": "https://omnivore.app/",
"content_security_policy": "default-src 'none'; child-src 'none'; manifest-src 'none'; media-src 'none'; object-src 'none'; prefetch-src 'none'; worker-src 'none'; connect-src https://storage.googleapis.com/ https://omnivore.app/api/ blob:; frame-src 'none'; font-src 'none'; img-src data:; script-src 'self'; script-src-elem 'self'; script-src-attr 'none'; style-src 'self'; style-src-elem 'self'; style-src-attr 'none'; base-uri 'none'; form-action 'none'; block-all-mixed-content; upgrade-insecure-requests; report-uri https://api.jeurissen.co/reports/csp/webext/omnivore/",
"content_security_policy": "default-src 'none'; child-src 'none'; manifest-src 'none'; media-src 'none'; object-src 'none'; prefetch-src 'none'; worker-src 'none'; connect-src https://storage.googleapis.com/ https://api-prod.omnivore.app/api/ blob:; frame-src 'none'; font-src 'none'; img-src data:; script-src 'self'; script-src-elem 'self'; script-src-attr 'none'; style-src 'self'; style-src-elem 'self'; style-src-attr 'none'; base-uri 'none'; form-action 'none'; block-all-mixed-content; upgrade-insecure-requests; report-uri https://api.jeurissen.co/reports/csp/webext/omnivore/",
"icons": {
"16": "/images/extension/icon-16.png",
"24": "/images/extension/icon-24.png",
@ -80,6 +80,14 @@
},
"default_title": "Omnivore Save Article"
},
"commands": {
"_execute_browser_action": {
"suggested_key": {
"default": "Alt + O"
},
"description": "Save the current tab to Omnivore"
}
},
"web_accessible_resources": [
"views/cta-popup.html"
]

File diff suppressed because one or more lines are too long

View file

@ -1 +1 @@
"use strict";window.browserApi="object"==typeof chrome&&chrome&&chrome.runtime&&chrome||"object"==typeof browser&&browser||{},window.browserActionApi=browserApi.action||browserApi.browserAction||browserApi.pageAction,window.browserScriptingApi=browserApi.scripting||browserApi.tabs,window.ENV_EXTENSION_ORIGIN=browserApi.runtime.getURL("PATH/").replace("/PATH/",""),window.ENV_IS_FIREFOX=ENV_EXTENSION_ORIGIN.startsWith("moz-extension://"),window.ENV_IS_EDGE=navigator.userAgent.toLowerCase().indexOf("edg")>-1,window.ENV_DOES_NOT_SUPPORT_BLOB_URL_ACCESS=/^((?!chrome|android).)*safari/i.test(navigator.userAgent),window.SELECTORS={CANONICAL_URL:["head > link[rel='canonical']"],TITLE:["head > meta[property='og:title']"]},window.ACTIONS={ShowMessage:"SHOW_MESSAGE",GetContent:"GET_CONTENT",GetPageInfo:"GET_PAGE_INFO",Ping:"PING",AddIframeContent:"ADD_IFRAME_CONTENT",RefreshDarkMode:"REFRESH_DARK_MODE",GetAuthToken:"GET_AUTH_TOKEN"},window.DONT_REMOVE_ELEMENTS=["meta","script","title"],window.CREATE_ARTICLE_QUERY="mutation CreateArticle ($input: CreateArticleInput!){\n createArticle(input:$input){\n ... on CreateArticleSuccess{\n createdArticle{\n id\n title\n slug\n hasContent\n }\n user {\n id\n profile {\n id\n username\n }\n }\n}\n ... on CreateArticleError{\n errorCodes\n }\n}\n}",window.CREATE_ARTICLE_SAVING_REQUEST_QUERY="mutation CreateArticleSavingRequest ($input: CreateArticleSavingRequestInput!){\n createArticleSavingRequest(input:$input){\n ... on CreateArticleSavingRequestSuccess{\n articleSavingRequest{\n id\n }\n }\n ... on CreateArticleSavingRequestError{\n errorCodes\n }\n }\n}";
"use strict";window.browserApi="object"==typeof chrome&&chrome&&chrome.runtime&&chrome||"object"==typeof browser&&browser||{},window.browserActionApi=browserApi.action||browserApi.browserAction||browserApi.pageAction,window.browserScriptingApi=browserApi.scripting||browserApi.tabs,window.ENV_EXTENSION_ORIGIN=browserApi.runtime.getURL("PATH/").replace("/PATH/",""),window.ENV_IS_FIREFOX=ENV_EXTENSION_ORIGIN.startsWith("moz-extension://"),window.ENV_IS_EDGE=navigator.userAgent.toLowerCase().indexOf("edg")>-1,window.ENV_DOES_NOT_SUPPORT_BLOB_URL_ACCESS=/^((?!chrome|android).)*safari/i.test(navigator.userAgent),window.SELECTORS={CANONICAL_URL:["head > link[rel='canonical']"],TITLE:["head > meta[property='og:title']"]},window.ACTIONS={ShowMessage:"SHOW_MESSAGE",GetContent:"GET_CONTENT",GetPageInfo:"GET_PAGE_INFO",Ping:"PING",AddIframeContent:"ADD_IFRAME_CONTENT",RefreshDarkMode:"REFRESH_DARK_MODE",GetAuthToken:"GET_AUTH_TOKEN"},window.DONT_REMOVE_ELEMENTS=["meta","script","title"],window.SAVE_URL_QUERY="mutation SaveUrl ($input: SaveUrlInput!) {\n saveUrl(input:$input){\n ... on SaveSuccess {\n url\n }\n ... on SaveError {\n errorCodes\n }\n }\n}",window.SAVE_FILE_QUERY="mutation SaveFile ($input: SaveFileInput!) {\n saveFile(input:$input){\n ... on SaveSuccess {\n url\n }\n ... on SaveError {\n errorCodes\n }\n }\n}",window.SAVE_PAGE_QUERY="mutation SavePage ($input: SavePageInput!) {\n savePage(input:$input){\n ... on SaveSuccess {\n url\n }\n ... on SaveError {\n errorCodes\n }\n }\n}",window.CREATE_ARTICLE_QUERY="mutation CreateArticle ($input: CreateArticleInput!){\n createArticle(input:$input){\n ... on CreateArticleSuccess{\n createdArticle{\n id\n title\n slug\n hasContent\n }\n user {\n id\n profile {\n id\n username\n }\n }\n}\n ... on CreateArticleError{\n errorCodes\n }\n}\n}",window.CREATE_ARTICLE_SAVING_REQUEST_QUERY="mutation CreateArticleSavingRequest ($input: CreateArticleSavingRequestInput!){\n createArticleSavingRequest(input:$input){\n ... on CreateArticleSavingRequestSuccess{\n articleSavingRequest{\n id\n }\n }\n ... on CreateArticleSavingRequestError{\n errorCodes\n }\n }\n}";

View file

@ -23,13 +23,11 @@ export const saveFile = async (
): Promise<SaveResult> => {
console.log('saving file with input', input)
// /* We do not trust the values from client, lookup upload file by querying
// * with filtering on user ID and URL to verify client's uploadFileId is valid.
// */
const uploadFile = await ctx.models.uploadFile.getWhere({
id: input.uploadFileId,
userId: saver.id,
})
if (!uploadFile) {
return {
errorCodes: [SaveErrorCode.Unauthorized],
@ -45,64 +43,6 @@ export const saveFile = async (
return ctx.models.uploadFile.setFileUploadComplete(input.uploadFileId, tx)
})
// if (!uploadFileData || !uploadFileData.id || !uploadFileData.fileName) {
// console.log('error completing upload file request', input)
// return {
// errorCodes: [SaveErrorCode.Unknown],
// }
// }
// // const uploadFileUrlOverride = await makeStorageFilePublic(
// // uploadFileData.id,
// // uploadFileData.fileName
// // )
// const matchedUserArticleRecord = await getPageByParam({
// userId: saver.id,
// url: uploadFileData.url,
// state: ArticleSavingRequestStatus.Succeeded,
// })
// if (matchedUserArticleRecord) {
// await updatePage(
// matchedUserArticleRecord.id,
// {
// savedAt: new Date(),
// archivedAt: null,
// },
// ctx
// )
// input.clientRequestId = matchedUserArticleRecord.id
// } else {
// const pageId = await createPage(
// {
// url: uploadFile.url,
// title: uploadFile.fileName,
// hash: uploadFileDetails.md5Hash,
// content: '',
// pageType: PageType.File,
// uploadFileId: input.uploadFileId,
// slug: generateSlug(uploadFile.fileName),
// userId: saver.id,
// id: input.clientRequestId,
// createdAt: new Date(),
// savedAt: new Date(),
// readingProgressPercent: 0,
// readingProgressAnchorIndex: 0,
// state: ArticleSavingRequestStatus.Succeeded,
// },
// ctx
// )
// if (!pageId) {
// console.log('error creating page in elastic', input)
// return {
// errorCodes: [SaveErrorCode.Unknown],
// }
// }
// input.clientRequestId = pageId
// }
return {
clientRequestId: input.clientRequestId,
url: `${homePageURL()}/${saver.profile.username}/links/${

View file

@ -1,3 +1,3 @@
OMNIVORE_URL="https://demo.omnivore.app"
OMNIVORE_GRAPHQL_URL="https://demo.omnivore.app/api/"
OMNIVORE_GRAPHQL_URL="https://api-demo.omnivore.app/api/"
EXTENSION_NAME="Omnivore *DEMO*"

View file

@ -1,3 +1,3 @@
OMNIVORE_URL="https://omnivore.app"
OMNIVORE_GRAPHQL_URL="https://omnivore.app/api/"
OMNIVORE_GRAPHQL_URL="https://api-prod.omnivore.app/api/"
EXTENSION_NAME="Omnivore"

View file

@ -22,5 +22,8 @@
"webpack": "^5.11.1",
"webpack-cli": "^4.3.1",
"webpack-merge": "^5.7.3"
},
"dependencies": {
"uuid": "^8.3.2"
}
}

View file

@ -22,7 +22,7 @@
"256": "/images/extension/icon-256.png"
},
"permissions": ["activeTab", "storage", "https://*/**", "http://*/**"],
"permissions": ["activeTab", "storage", "contextMenus", "https://*/**", "http://*/**"],
"background": {
"page": "/views/background.html",
@ -72,5 +72,14 @@
"default_title": "Omnivore Save Article"
},
"commands": {
"_execute_browser_action": {
"suggested_key": {
"default": "Alt + O"
},
"description": "Save the current tab to Omnivore"
}
},
"web_accessible_resources": ["views/cta-popup.html"]
}

View file

@ -13,6 +13,8 @@
'use strict';
import { v4 as uuidv4 } from 'uuid';
let authToken = undefined;
const omnivoreURL = process.env.OMNIVORE_URL;
const omnivoreGraphqlURL = process.env.OMNIVORE_GRAPHQL_URL;
@ -47,6 +49,17 @@ function removeStorage (itemsToRemove) {
});
}
function getCurrentTab () {
return new Promise((resolve) => {
browserApi.tabs.query({
active: true,
currentWindow: true
}, function (tabs) {
resolve(tabs[0] || null);
});
});
}
function setupConnection(xhr) {
xhr.setRequestHeader('Content-Type', 'application/json');
if (authToken) {
@ -65,7 +78,7 @@ function uploadFile ({ id, uploadSignedUrl }, contentType, contentObjUrl) {
xhr.setRequestHeader('Content-Type', contentType);
xhr.onerror = () => {
resolve(null);
resolve(undefined);
};
xhr.onload = () => {
// Uploaded.
@ -80,14 +93,13 @@ function uploadFile ({ id, uploadSignedUrl }, contentType, contentObjUrl) {
});
}
function savePdfFile (tab, url, contentType, contentObjUrl) {
function savePdfFile(tab, url, contentType, contentObjUrl) {
return new Promise(resolve => {
const xhr = new XMLHttpRequest();
xhr.onreadystatechange = async function () {
if (xhr.readyState === 4) {
if (xhr.status === 200) {
const { data } = JSON.parse(xhr.response);
if ('errorCodes' in data.uploadFileRequest) {
if (data.uploadFileRequest.errorCodes[0] === 'UNAUTHORIZED') {
clearClickCompleteState();
@ -103,7 +115,7 @@ function savePdfFile (tab, url, contentType, contentObjUrl) {
}
}
if (!data.uploadFileRequest || !data.uploadFileRequest.id || 'errorCodes' in data.uploadFileRequest) {
if (!data.uploadFileRequest || !data.uploadFileRequest.id || !data.uploadFileRequest.createdPageId || 'errorCodes' in data.uploadFileRequest) {
browserApi.tabs.sendMessage(tab.id, {
action: ACTIONS.ShowMessage,
payload: {
@ -112,9 +124,26 @@ function savePdfFile (tab, url, contentType, contentObjUrl) {
}
});
} else {
const uploadFileId = await uploadFile(data.uploadFileRequest, contentType, contentObjUrl);
const result = await uploadFile(data.uploadFileRequest, contentType, contentObjUrl);
URL.revokeObjectURL(contentObjUrl);
return resolve(uploadFileId);
if (!result) {
return undefined
}
const createdPageId = data.uploadFileRequest.createdPageId
const url = omnivoreURL + '/article/sr/' + createdPageId
browserApi.tabs.sendMessage(tab.id, {
action: ACTIONS.ShowMessage,
payload: {
text: 'Saved to Omnivore',
link: url,
linkText: 'Read Now',
type: 'success'
}
})
return resolve(data.uploadFileRequest);
}
} else if (xhr.status === 400) {
browserApi.tabs.sendMessage(tab.id, {
@ -137,6 +166,7 @@ function savePdfFile (tab, url, contentType, contentObjUrl) {
}
... on UploadFileRequestSuccess {
id
createdPageId
uploadSignedUrl
}
}
@ -144,7 +174,8 @@ function savePdfFile (tab, url, contentType, contentObjUrl) {
variables: {
input: {
url,
contentType
contentType,
createPageEntry: true,
}
}
});
@ -164,8 +195,71 @@ function clearClickCompleteState () {
});
}
function saveArticle (tab) {
browserApi.tabs.sendMessage(tab.id, {
function handleSaveResponse(tab, field, xhr) {
if (xhr.readyState === 4) {
if (xhr.status === 200) {
const { data } = JSON.parse(xhr.response);
const item = data[field]
if (!item) {
return undefined
}
if ('errorCodes' in item) {
const messagePayload = {
text: descriptions[data.createArticle.errorCodes[0]] || 'Unable to save page',
type: 'error'
}
if (item.errorCodes[0] === 'UNAUTHORIZED') {
messagePayload.errorCode = 401
messagePayload.url = omnivoreURL
clearClickCompleteState()
}
browserApi.tabs.sendMessage(tab.id, {
action: ACTIONS.ShowMessage,
payload: messagePayload
})
return undefined
}
const url = item['url']
browserApi.tabs.sendMessage(tab.id, {
action: ACTIONS.ShowMessage,
payload: {
text: 'Saved to Omnivore',
link: url ?? omnivoreURL + '/home',
linkText: 'Read Now',
type: 'success'
}
})
return item
} else if (xhr.status === 400) {
browserApi.tabs.sendMessage(tab.id, {
action: ACTIONS.ShowMessage,
payload: {
text: 'Unable to save page',
type: 'error'
}
})
return undefined
}
}
}
async function saveUrl(currentTab, url) {
const requestId = uuidv4()
await saveApiRequest(currentTab, SAVE_URL_QUERY, 'saveUrl', {
source: 'extension',
clientRequestId: requestId,
url: encodeURI(url),
})
}
async function saveApiRequest(currentTab, query, field, input) {
browserApi.tabs.sendMessage(currentTab.id, {
action: ACTIONS.ShowMessage,
payload: {
type: 'loading',
@ -173,85 +267,48 @@ function saveArticle (tab) {
}
});
const xhr = new XMLHttpRequest();
const descriptions = {
BAD_DATA: 'Unable to save page',
NOT_ALLOWED_TO_PARSE: 'Not allowed to parse this article',
UNAUTHORIZED: 'Please login to Omnivore to authorize this action',
UNABLE_TO_FETCH: 'Unable to fetch page',
PAYLOAD_TOO_LARGE: 'This article is too large'
};
xhr.onreadystatechange = function () {
if (xhr.readyState === 4) {
if (xhr.status === 200) {
const { data } = JSON.parse(xhr.response);
if ('createArticle' in data) {
if ('errorCodes' in data.createArticle) {
const messagePayload = {
text: descriptions[data.createArticle.errorCodes[0]] || 'Unable to save page',
type: 'error'
};
return new Promise((resolve, reject) => {
const xhr = new XMLHttpRequest()
xhr.open('POST', omnivoreGraphqlURL + 'graphql', true)
setupConnection(xhr)
if (data.createArticle.errorCodes[0] === 'UNAUTHORIZED') {
messagePayload.errorCode = 401;
messagePayload.url = omnivoreURL;
xhr.onerror = (err) => { reject(err) }
clearClickCompleteState();
}
browserApi.tabs.sendMessage(tab.id, {
action: ACTIONS.ShowMessage,
payload: messagePayload
});
} else {
const article = data.createArticle.createdArticle;
const user = data.createArticle.user;
const link = omnivoreURL + (article.hasContent ? (`/${user.profile.username}/` + article.slug) : '/home');
browserApi.tabs.sendMessage(tab.id, {
action: ACTIONS.ShowMessage,
payload: {
text: 'Saved to Omnivore',
link: link,
linkText: 'View',
type: 'success'
}
});
}
} else {
if ('errorCodes' in data.createArticleSavingRequest) {
browserApi.tabs.sendMessage(tab.id, {
action: ACTIONS.ShowMessage,
payload: {
text: descriptions[data.createArticleSavingRequest.errorCodes[0]] || 'Unable to save page',
type: 'error'
}
});
} else {
const articleSavingRequest = data.createArticleSavingRequest.articleSavingRequest;
const link = omnivoreURL + '/article/sr/' + articleSavingRequest.id;
browserApi.tabs.sendMessage(tab.id, {
action: ACTIONS.ShowMessage,
payload: {
text: 'Saved to Omnivore',
link: link,
linkText: 'View',
type: 'success'
}
});
}
xhr.onload = () => {
try {
const res = handleSaveResponse(currentTab, field, xhr)
if (!res) {
return reject()
}
} else if (xhr.status === 400) {
browserApi.tabs.sendMessage(tab.id, {
action: ACTIONS.ShowMessage,
payload: {
text: 'Unable to save page',
type: 'error'
}
});
resolve(res);
} catch (err) {
reject(err)
}
}
};
const data = JSON.stringify({
query,
variables: {
input
}
})
xhr.send(data);
})
.catch((err) => {
console.log('error saving page', err)
browserApi.tabs.sendMessage(currentTab.id, {
action: ACTIONS.ShowMessage,
payload: {
text: 'Unable to save page',
type: 'error',
}
});
return undefined
});
}
function saveArticle (tab) {
browserApi.tabs.sendMessage(tab.id, {
action: ACTIONS.GetContent
}, async (response) => {
@ -260,56 +317,42 @@ function saveArticle (tab) {
return;
}
const requestId = uuidv4()
const { type, pageInfo, doc, uploadContentObjUrl } = response;
let uploadResult = null;
switch(type) {
case 'pdf': {
// For PDFs, we first upload the PDF file before passing the upload file ID in createArticle
uploadResult = await savePdfFile(tab, encodeURI(tab.url), pageInfo.contentType, uploadContentObjUrl);
if (!uploadResult || !uploadResult.id) {
browserApi.tabs.sendMessage(tab.id, {
action: ACTIONS.ShowMessage,
payload: {
text: 'Unable to save page',
type: 'error'
}
});
return;
}
case 'html': {
await saveApiRequest(tab, SAVE_PAGE_QUERY, 'savePage', {
source: 'extension',
clientRequestId: requestId,
originalContent: doc,
title: pageInfo.title,
url: encodeURI(tab.url),
})
break
}
case 'url': {
// We don't have to special case URL, it will fall through
// and be handled when isContentAvailable returns false
await saveApiRequest(tab, SAVE_URL_QUERY, 'saveUrl', {
source: 'extension',
clientRequestId: requestId,
url: encodeURI(tab.url),
})
break
}
case 'pdf': {
const uploadResult = await savePdfFile(tab, encodeURI(tab.url), pageInfo.contentType, uploadContentObjUrl);
if (!uploadResult || !uploadResult.id) {
// If the upload failed for any reason, try to save the PDF URL instead
await saveApiRequest(tab, SAVE_URL_QUERY, 'saveUrl', {
source: 'extension',
clientRequestId: requestId,
url: encodeURI(tab.url),
})
return;
}
break
}
}
const isContentAvailable = (doc && doc.length) || uploadResult;
const query = isContentAvailable ? CREATE_ARTICLE_QUERY : CREATE_ARTICLE_SAVING_REQUEST_QUERY;
const input = {
url: encodeURI(tab.url)
};
if (isContentAvailable) {
input.preparedDocument = {
document: doc,
pageInfo
};
input.uploadFileId = (uploadResult && uploadResult.id) || null;
}
const data = JSON.stringify({
query,
variables: {
input
}
});
xhr.open('POST', omnivoreGraphqlURL + 'graphql', true);
setupConnection(xhr);
xhr.send(data);
});
}
@ -484,18 +527,13 @@ function checkAuthOnFirstClickPostInstall (tabId) {
});
}
function getCurrentTab () {
return new Promise((resolve) => {
browserApi.tabs.query({
active: true,
currentWindow: true
}, function (tabs) {
resolve(tabs[0] || null);
});
});
function handleActionClick () {
executeAction(function (currentTab) {
onExtensionClick(currentTab.id);
})
}
function handleActionClick () {
function executeAction(action) {
getCurrentTab().then((currentTab) => {
browserApi.tabs.sendMessage(currentTab.id, {
action: ACTIONS.Ping
@ -504,7 +542,7 @@ function handleActionClick () {
// Content script ready
const isSignedUp = await checkAuthOnFirstClickPostInstall(currentTab.id);
if (isSignedUp) {
onExtensionClick(currentTab.id);
action(currentTab);
}
} else {
const extensionManifest = browserApi.runtime.getManifest();
@ -518,7 +556,7 @@ function handleActionClick () {
executeScripts(currentTab.id, scriptFiles, async function () {
const isSignedUp = await checkAuthOnFirstClickPostInstall(currentTab.id);
if (isSignedUp) {
onExtensionClick(currentTab.id);
action(currentTab);
}
});
}
@ -644,6 +682,17 @@ function init () {
browserActionApi.setIcon({
path: getIconPath(true)
});
browserApi.contextMenus.create({
id: "save-selection",
title: "Save to Omnivore",
contexts: ["link"],
onclick: async function(obj) {
executeAction(async function (currentTab) {
await saveUrl(currentTab, obj.linkUrl)
})
},
});
}
init();

View file

@ -30,6 +30,39 @@ window.DONT_REMOVE_ELEMENTS = [
'title'
];
window.SAVE_URL_QUERY = `mutation SaveUrl ($input: SaveUrlInput!) {
saveUrl(input:$input){
... on SaveSuccess {
url
}
... on SaveError {
errorCodes
}
}
}`
window.SAVE_FILE_QUERY = `mutation SaveFile ($input: SaveFileInput!) {
saveFile(input:$input){
... on SaveSuccess {
url
}
... on SaveError {
errorCodes
}
}
}`
window.SAVE_PAGE_QUERY = `mutation SavePage ($input: SavePageInput!) {
savePage(input:$input){
... on SaveSuccess {
url
}
... on SaveError {
errorCodes
}
}
}`
window.CREATE_ARTICLE_QUERY = `mutation CreateArticle ($input: CreateArticleInput!){
createArticle(input:$input){
... on CreateArticleSuccess{

View file

@ -22,7 +22,6 @@
}
browserApi.runtime.onMessage.addListener(({ action, payload }, sender, sendResponse) => {
/* async actions */
if (action === ACTIONS.GetContent) {
prepareContent().then((pageContent) => {
sendResponse({

View file

@ -4655,7 +4655,7 @@ uuid@^3.3.2:
resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.4.0.tgz#b23e4358afa8a202fe7a100af1f5f883f02007ee"
integrity sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==
uuid@^8.3.0:
uuid@^8.3.0, uuid@^8.3.2:
version "8.3.2"
resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.3.2.tgz#80d5b5ced271bb9af6c445f21a1a04c606cefbe2"
integrity sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==