From a12c1aad5e5b0b5e5915fe38330f76d7c08faafc Mon Sep 17 00:00:00 2001 From: Aditya Pimpalkar Date: Thu, 30 May 2024 11:58:45 +0100 Subject: [PATCH] fix: user has to login every time chrome sidepanel is opened (#5544) We can pass the auth tokens to our front app via post message, which will also allow us to pass route names to navigate on it --- package.json | 3 - packages/twenty-chrome-extension/project.json | 7 +- .../src/background/index.ts | 21 ++-- .../src/contentScript/createButton.ts | 1 + .../contentScript/extractCompanyProfile.ts | 24 ++-- .../src/contentScript/extractPersonProfile.ts | 21 ++-- .../src/contentScript/index.ts | 27 +++-- .../src/contentScript/insertSettingsButton.ts | 59 ++++++++++ .../contentScript/utils/changeSidepanelUrl.ts | 15 +-- .../twenty-chrome-extension/src/manifest.ts | 5 +- .../src/options/App.tsx | 42 +++++++ .../src/options/Settings.tsx | 50 ++++++-- .../src/options/Sidepanel.tsx | 109 ++++++++++++++---- .../src/options/index.tsx | 4 +- .../src/utils/apolloClient.ts | 107 ++++++----------- packages/twenty-front/src/App.tsx | 67 ++++++----- .../twenty-front/src/generated/graphql.tsx | 4 +- .../ChromeExtensionSidecarEffect.tsx | 58 ++++++++++ .../ChromeExtensionSidecarProvider.tsx | 56 +++++++++ .../isLoadingTokensFromExtensionState.ts | 6 + .../components/ClientConfigProviderEffect.tsx | 6 + .../graphql/queries/getClientConfig.ts | 1 + .../states/chromeExtensionIdState.ts | 6 + packages/twenty-front/src/utils/isInIframe.ts | 7 ++ packages/twenty-server/.env.example | 2 +- .../auth/services/auth.service.ts | 4 +- .../client-config/client-config.entity.ts | 3 + .../client-config/client-config.resolver.ts | 1 + .../environment/environment-variables.ts | 2 +- yarn.lock | 24 ---- 30 files changed, 511 insertions(+), 231 deletions(-) create mode 100644 packages/twenty-chrome-extension/src/contentScript/insertSettingsButton.ts create mode 100644 packages/twenty-chrome-extension/src/options/App.tsx create mode 100644 packages/twenty-front/src/modules/chrome-extension-sidecar/components/ChromeExtensionSidecarEffect.tsx create mode 100644 packages/twenty-front/src/modules/chrome-extension-sidecar/components/ChromeExtensionSidecarProvider.tsx create mode 100644 packages/twenty-front/src/modules/chrome-extension-sidecar/states/isLoadingTokensFromExtensionState.ts create mode 100644 packages/twenty-front/src/modules/client-config/states/chromeExtensionIdState.ts create mode 100644 packages/twenty-front/src/utils/isInIframe.ts diff --git a/package.json b/package.json index ce4ad42a7..6b789da97 100644 --- a/package.json +++ b/package.json @@ -75,7 +75,6 @@ "class-transformer": "^0.5.1", "clsx": "^2.1.1", "cross-env": "^7.0.3", - "crypto-js": "^4.2.0", "danger-plugin-todos": "^1.3.1", "dataloader": "^2.2.2", "date-fns": "^2.30.0", @@ -241,8 +240,6 @@ "@types/better-sqlite3": "^7.6.8", "@types/bytes": "^3.1.1", "@types/chrome": "^0.0.267", - "@types/crypto-js": "^4.2.2", - "@types/dagre": "^0.7.52", "@types/deep-equal": "^1.0.1", "@types/express": "^4.17.13", "@types/graphql-fields": "^1.3.6", diff --git a/packages/twenty-chrome-extension/project.json b/packages/twenty-chrome-extension/project.json index 48cdd4560..5f0e8815e 100644 --- a/packages/twenty-chrome-extension/project.json +++ b/packages/twenty-chrome-extension/project.json @@ -11,10 +11,11 @@ } }, "start": { - "executor": "@nx/vite:dev-server", + "executor": "nx:run-commands", + "dependsOn": ["build"], "options": { - "buildTarget": "twenty-chrome-extension:build", - "hmr": true + "cwd": "packages/twenty-chrome-extension", + "command": "VITE_MODE=development vite" } }, "preview": { diff --git a/packages/twenty-chrome-extension/src/background/index.ts b/packages/twenty-chrome-extension/src/background/index.ts index a5d30b2a2..b892790d4 100644 --- a/packages/twenty-chrome-extension/src/background/index.ts +++ b/packages/twenty-chrome-extension/src/background/index.ts @@ -30,17 +30,6 @@ chrome.runtime.onMessage.addListener((message, _, sendResponse) => { }); break; } - case 'changeSidepanelUrl': { - chrome.tabs.query({ active: true, currentWindow: true }, ([tab]) => { - if (isDefined(tab) && isDefined(tab.id)) { - chrome.tabs.sendMessage(tab.id, { - action: 'changeSidepanelUrl', - message, - }); - } - }); - break; - } default: break; } @@ -82,7 +71,15 @@ const setTokenStateFromCookie = (cookie: string) => { chrome.cookies.onChanged.addListener(async ({ cookie }) => { if (cookie.name === 'tokenPair') { - setTokenStateFromCookie(cookie.value); + const store = await chrome.storage.local.get(['clientUrl']); + const clientUrl = isDefined(store.clientUrl) + ? store.clientUrl + : import.meta.env.VITE_FRONT_BASE_URL; + chrome.cookies.get({ name: 'tokenPair', url: `${clientUrl}` }, (cookie) => { + if (isDefined(cookie)) { + setTokenStateFromCookie(cookie.value); + } + }); } }); diff --git a/packages/twenty-chrome-extension/src/contentScript/createButton.ts b/packages/twenty-chrome-extension/src/contentScript/createButton.ts index d86418f0d..b68adca35 100644 --- a/packages/twenty-chrome-extension/src/contentScript/createButton.ts +++ b/packages/twenty-chrome-extension/src/contentScript/createButton.ts @@ -36,6 +36,7 @@ export const createDefaultButton = ( padding: '0 1rem', cursor: 'pointer', height: '32px', + width: 'max-content', }; Object.assign(div.style, divStyles); diff --git a/packages/twenty-chrome-extension/src/contentScript/extractCompanyProfile.ts b/packages/twenty-chrome-extension/src/contentScript/extractCompanyProfile.ts index 12d6cc7b5..14d65addb 100644 --- a/packages/twenty-chrome-extension/src/contentScript/extractCompanyProfile.ts +++ b/packages/twenty-chrome-extension/src/contentScript/extractCompanyProfile.ts @@ -75,9 +75,7 @@ export const addCompany = async () => { const companyId = await createCompany(companyInputData); if (isDefined(companyId)) { - await changeSidePanelUrl( - `${import.meta.env.VITE_FRONT_BASE_URL}/object/company/${companyId}`, - ); + await changeSidePanelUrl(`/object/company/${companyId}`); } return companyId; @@ -86,16 +84,15 @@ export const addCompany = async () => { export const insertButtonForCompany = async () => { const companyButtonDiv = createDefaultButton('twenty-company-btn'); - const parentDiv: HTMLDivElement | null = document.querySelector( - '.org-top-card-primary-actions__inner', + const companyDiv: HTMLDivElement | null = document.querySelector( + '.org-top-card__primary-content', ); - if (isDefined(parentDiv)) { + if (isDefined(companyDiv)) { Object.assign(companyButtonDiv.style, { - marginLeft: '.8rem', - marginTop: '.4rem', + marginTop: '.8rem', }); - parentDiv.prepend(companyButtonDiv); + companyDiv.parentElement?.append(companyButtonDiv); } const companyButtonSpan = companyButtonDiv.getElementsByTagName('span')[0]; @@ -104,19 +101,16 @@ export const insertButtonForCompany = async () => { const openCompanyOnSidePanel = (companyId: string) => { companyButtonSpan.textContent = 'View in Twenty'; companyButtonDiv.onClickHandler(async () => { - await changeSidePanelUrl( - `${import.meta.env.VITE_FRONT_BASE_URL}/object/company/${companyId}`, - ); + await changeSidePanelUrl(`/object/company/${companyId}`); chrome.runtime.sendMessage({ action: 'openSidepanel' }); }); }; if (isDefined(company)) { - await changeSidePanelUrl( - `${import.meta.env.VITE_FRONT_BASE_URL}/object/company/${company.id}`, - ); + await changeSidePanelUrl(`/object/company/${company.id}`); if (isDefined(company.id)) openCompanyOnSidePanel(company.id); } else { + await changeSidePanelUrl(`/objects/companies`); companyButtonSpan.textContent = 'Add to Twenty'; companyButtonDiv.onClickHandler(async () => { diff --git a/packages/twenty-chrome-extension/src/contentScript/extractPersonProfile.ts b/packages/twenty-chrome-extension/src/contentScript/extractPersonProfile.ts index 044745a07..eefcfefac 100644 --- a/packages/twenty-chrome-extension/src/contentScript/extractPersonProfile.ts +++ b/packages/twenty-chrome-extension/src/contentScript/extractPersonProfile.ts @@ -86,9 +86,7 @@ export const addPerson = async () => { const personId = await createPerson(personData); if (isDefined(personId)) { - await changeSidePanelUrl( - `${import.meta.env.VITE_FRONT_BASE_URL}/object/person/${personId}`, - ); + await changeSidePanelUrl(`/object/person/${personId}`); } return personId; @@ -98,15 +96,13 @@ export const insertButtonForPerson = async () => { const personButtonDiv = createDefaultButton('twenty-person-btn'); if (isDefined(personButtonDiv)) { - const addedProfileDiv: HTMLDivElement | null = document.querySelector( - '.pv-top-card-v2-ctas__custom', - ); + const addedProfileDiv = document.querySelector('.artdeco-card > .ph5'); if (isDefined(addedProfileDiv)) { Object.assign(personButtonDiv.style, { - marginRight: '.8rem', + marginTop: '.8rem', }); - addedProfileDiv.prepend(personButtonDiv); + addedProfileDiv.append(personButtonDiv); } const personButtonSpan = personButtonDiv.getElementsByTagName('span')[0]; @@ -115,19 +111,16 @@ export const insertButtonForPerson = async () => { const openPersonOnSidePanel = (personId: string) => { personButtonSpan.textContent = 'View in Twenty'; personButtonDiv.onClickHandler(async () => { - await changeSidePanelUrl( - `${import.meta.env.VITE_FRONT_BASE_URL}/object/person/${personId}`, - ); + await changeSidePanelUrl(`/object/person/${personId}`); chrome.runtime.sendMessage({ action: 'openSidepanel' }); }); }; if (isDefined(person)) { - await changeSidePanelUrl( - `${import.meta.env.VITE_FRONT_BASE_URL}/object/person/${person.id}`, - ); + await changeSidePanelUrl(`/object/person/${person.id}`); if (isDefined(person.id)) openPersonOnSidePanel(person.id); } else { + await changeSidePanelUrl(`/objects/people`); personButtonSpan.textContent = 'Add to Twenty'; personButtonDiv.onClickHandler(async () => { personButtonSpan.textContent = 'Saving...'; diff --git a/packages/twenty-chrome-extension/src/contentScript/index.ts b/packages/twenty-chrome-extension/src/contentScript/index.ts index 7bf17bb09..e5b0216c2 100644 --- a/packages/twenty-chrome-extension/src/contentScript/index.ts +++ b/packages/twenty-chrome-extension/src/contentScript/index.ts @@ -5,10 +5,23 @@ import { isDefined } from '~/utils/isDefined'; // Inject buttons into the DOM when SPA is reloaded on the resource url. // e.g. reload the page when on https://www.linkedin.com/in/mabdullahabaid/ // await insertButtonForCompany(); -(async () => { - await insertButtonForCompany(); - await insertButtonForPerson(); -})(); + +const companyRoute = /^https?:\/\/(?:www\.)?linkedin\.com\/company(?:\/\S+)?/; +const personRoute = /^https?:\/\/(?:www\.)?linkedin\.com\/in(?:\/\S+)?/; + +const executeScript = async () => { + const loc = window.location.href; + switch (true) { + case companyRoute.test(loc): + await insertButtonForCompany(); + break; + case personRoute.test(loc): + await insertButtonForPerson(); + break; + default: + break; + } +}; // The content script gets executed upon load, so the the content script is executed when a user visits https://www.linkedin.com/feed/. // However, there would never be another reload in a single page application unless triggered manually. @@ -16,8 +29,7 @@ import { isDefined } from '~/utils/isDefined'; // e.g. create "Add to Twenty" button when a user navigates to https://www.linkedin.com/in/mabdullahabaid/ from https://www.linkedin.com/feed/ chrome.runtime.onMessage.addListener(async (message, _, sendResponse) => { if (message.action === 'executeContentScript') { - await insertButtonForCompany(); - await insertButtonForPerson(); + await executeScript(); } sendResponse('Executing!'); @@ -26,8 +38,7 @@ chrome.runtime.onMessage.addListener(async (message, _, sendResponse) => { chrome.storage.local.onChanged.addListener(async (store) => { if (isDefined(store.accessToken)) { if (isDefined(store.accessToken.newValue)) { - await insertButtonForCompany(); - await insertButtonForPerson(); + await executeScript(); } } }); diff --git a/packages/twenty-chrome-extension/src/contentScript/insertSettingsButton.ts b/packages/twenty-chrome-extension/src/contentScript/insertSettingsButton.ts new file mode 100644 index 000000000..e5722355f --- /dev/null +++ b/packages/twenty-chrome-extension/src/contentScript/insertSettingsButton.ts @@ -0,0 +1,59 @@ +import { isDefined } from '~/utils/isDefined'; + +const btn = document.getElementById('twenty-settings-btn'); +if (!isDefined(btn)) { + const div = document.createElement('div'); + const img = document.createElement('img'); + img.src = + 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAACXBIWXMAAA7EAAAOxAGVKw4bAAACb0lEQVR4nO2VO4taQRTHr3AblbjxEVlwCwVhg7BoqqCIjy/gAyyFWNlYBOxsfH0KuxgQGwXRUkGuL2S7i1barGAgiwbdW93SnGOc4BonPiKahf3DwXFmuP/fPM4ZlvmlTxAhCBdzHnEQWYiv7Mr4C3NeuVYhQYDPzOUUQgDLBQGcLHNhvQK8DACPx8PTxiqVyvISG43GbyaT6Qfpn06n0m63e/tPAPF4vJ1MJu8kEsnWTCkWi1yr1RKGw+GDRqPBOTfr44vFQvD7/Q/lcpmaaVQAr9fLp1IpO22c47hGOBz+MB6PH+Vy+VYDAL8qlUoGtVotzOfzq4MAgsHgE/6KojiQyWR/bKVSqbSszHFM8Pl8z1YK48JsNltCOBwOnrYLO+8AAIjb+nHbycoTiUQfDJ7tFq4YAHiVSmXBxcD41u8flQU8z7fhzO0r83atVns3Go3u9Xr9x0O/RQXo9/tsIBBg6vX606a52Wz+bZ7P5/WwG29gxSJzhKgA6XTaDoFNF+krFAocmC//4yWEcSf2wTm7mCO19xFgSsKOLI16vV7b7XY7mRNoLwA0JymJ5uQIzgIAuX5PzDElT2m+E8BqtQ4ymcx7Yq7T6a6ZE4sKgOadTucaCwkxp1UzlEKh0GDxIXOwDWHAdi6Xe3swQDQa/Q7mywoolUpvsaptymazDWKxmBHTlWXZm405BFZoNpuGgwEmk4mE2SGtVivii4f1AO7J3ZopkQCQj7Ar1FeRChCJRJzVapX6DKNIfSc1Ax+wtQWQ55h6bH8FWDfYV4fO3wlwDr0C/BcADYiTPCxHqIEA2QsCZAkAKnRGkMbKN/sTX5YHPQ1e7SkAAAAASUVORK5CYII='; + img.height = 20; + img.width = 20; + img.alt = 'Twenty logo'; + + // Write universal styles for the button + const divStyles = { + border: '1px solid black', + borderRadius: '50%', + backgroundColor: 'black', + color: 'white', + fontWeight: '600', + fontSize: '1.5rem', + display: 'flex', + alignItems: 'center', + gap: '5px', + justifyContent: 'center', + padding: '0 1rem', + cursor: 'pointer', + height: '50px', + width: '50px', + position: 'fixed', + bottom: '80px', + right: '20px', + zIndex: '9999999999999999999999999', + }; + + div.addEventListener('mouseenter', () => { + const hoverStyles = { + //eslint-disable-next-line @nx/workspace-no-hardcoded-colors + backgroundColor: '#5e5e5e', + //eslint-disable-next-line @nx/workspace-no-hardcoded-colors + borderColor: '#5e5e5e', + }; + Object.assign(div.style, hoverStyles); + }); + + div.addEventListener('mouseleave', () => { + Object.assign(div.style, divStyles); + }); + + div.onclick = async () => { + chrome.runtime.sendMessage({ action: 'openSidepanel' }); + chrome.storage.local.set({ navigateSidepanel: 'settings' }); + }; + + div.appendChild(img); + + Object.assign(div.style, divStyles); + + document.body.appendChild(div); +} diff --git a/packages/twenty-chrome-extension/src/contentScript/utils/changeSidepanelUrl.ts b/packages/twenty-chrome-extension/src/contentScript/utils/changeSidepanelUrl.ts index 9a21fc620..087346e57 100644 --- a/packages/twenty-chrome-extension/src/contentScript/utils/changeSidepanelUrl.ts +++ b/packages/twenty-chrome-extension/src/contentScript/utils/changeSidepanelUrl.ts @@ -1,15 +1,12 @@ import { isDefined } from '~/utils/isDefined'; const changeSidePanelUrl = async (url: string) => { - const { tab: activeTab } = await chrome.runtime.sendMessage({ - action: 'getActiveTab', - }); - if (isDefined(activeTab) && isDefined(url)) { - chrome.storage.local.set({ [`sidepanelUrl_${activeTab.id}`]: url }); - chrome.runtime.sendMessage({ - action: 'changeSidepanelUrl', - message: { url }, - }); + if (isDefined(url)) { + chrome.storage.local.set({ navigateSidepanel: 'sidepanel' }); + // we first clear the sidepanelUrl to trigger the onchange listener on sidepanel + // which will pass the post meessage to handle internal navigation of iframe + chrome.storage.local.set({ sidepanelUrl: '' }); + chrome.storage.local.set({ sidepanelUrl: url }); } }; diff --git a/packages/twenty-chrome-extension/src/manifest.ts b/packages/twenty-chrome-extension/src/manifest.ts index b48422207..797d705ae 100644 --- a/packages/twenty-chrome-extension/src/manifest.ts +++ b/packages/twenty-chrome-extension/src/manifest.ts @@ -32,7 +32,10 @@ export default defineManifest({ content_scripts: [ { matches: ['https://www.linkedin.com/*'], - js: ['src/contentScript/index.ts'], + js: [ + 'src/contentScript/index.ts', + 'src/contentScript/insertSettingsButton.ts', + ], run_at: 'document_end', }, ], diff --git a/packages/twenty-chrome-extension/src/options/App.tsx b/packages/twenty-chrome-extension/src/options/App.tsx new file mode 100644 index 000000000..ba8e79110 --- /dev/null +++ b/packages/twenty-chrome-extension/src/options/App.tsx @@ -0,0 +1,42 @@ +import { useEffect, useState } from 'react'; + +import Settings from '~/options/Settings'; +import Sidepanel from '~/options/Sidepanel'; +import { isDefined } from '~/utils/isDefined'; + +const App = () => { + const [currentScreen, setCurrentScreen] = useState(''); + + useEffect(() => { + const setCurrentScreenState = async () => { + const store = await chrome.storage.local.get(['navigateSidepanel']); + if (isDefined(store.navigateSidepanel)) { + setCurrentScreen(store.navigateSidepanel); + } + }; + + setCurrentScreenState(); + }, []); + + useEffect(() => { + chrome.storage.local.onChanged.addListener((updatedStore) => { + if ( + isDefined(updatedStore.navigateSidepanel) && + isDefined(updatedStore.navigateSidepanel.newValue) + ) { + setCurrentScreen(updatedStore.navigateSidepanel.newValue); + } + }); + }, [setCurrentScreen]); + + switch (currentScreen) { + case 'sidepanel': + return ; + case 'settings': + return ; + default: + return ; + } +}; + +export default App; diff --git a/packages/twenty-chrome-extension/src/options/Settings.tsx b/packages/twenty-chrome-extension/src/options/Settings.tsx index 1df9ae9e6..e4c959ba0 100644 --- a/packages/twenty-chrome-extension/src/options/Settings.tsx +++ b/packages/twenty-chrome-extension/src/options/Settings.tsx @@ -1,7 +1,9 @@ import { useEffect, useState } from 'react'; import styled from '@emotion/styled'; +import { MainButton } from '@/ui/input/button/MainButton'; import { TextInput } from '@/ui/input/components/TextInput'; +import { clearStore } from '~/utils/apolloClient'; import { isDefined } from '~/utils/isDefined'; const StyledWrapper = styled.div` @@ -34,33 +36,47 @@ const StyledActionContainer = styled.div` const Settings = () => { const [serverBaseUrl, setServerBaseUrl] = useState(''); const [clientUrl, setClientUrl] = useState(''); + const [currentClientUrl, setCurrentClientUrl] = useState(''); + const [currentServerUrl, setCurrentServerUrl] = useState(''); useEffect(() => { const getState = async () => { - const store = await chrome.storage.local.get(); + const store = await chrome.storage.local.get([ + 'serverBaseUrl', + 'clientUrl', + ]); if (isDefined(store.serverBaseUrl)) { setServerBaseUrl(store.serverBaseUrl); + setCurrentServerUrl(store.serverBaseUrl); } else { setServerBaseUrl(import.meta.env.VITE_SERVER_BASE_URL); + setCurrentServerUrl(import.meta.env.VITE_SERVER_BASE_URL); } if (isDefined(store.clientUrl)) { setClientUrl(store.clientUrl); + setCurrentClientUrl(store.clientUrl); } else { setClientUrl(import.meta.env.VITE_FRONT_BASE_URL); + setCurrentClientUrl(import.meta.env.VITE_FRONT_BASE_URL); } }; void getState(); }, []); - const handleBaseUrlChange = (value: string) => { - setServerBaseUrl(value); - chrome.storage.local.set({ serverBaseUrl: value }); + const handleSettingsChange = () => { + chrome.storage.local.set({ + serverBaseUrl, + clientUrl, + navigateSidepanel: 'sidepanel', + }); + clearStore(); }; - const handleClientUrlChange = (value: string) => { - setClientUrl(value); - chrome.storage.local.set({ clientUrl: value }); + const handleCloseSettings = () => { + chrome.storage.local.set({ + navigateSidepanel: 'sidepanel', + }); }; return ( @@ -71,17 +87,33 @@ const Settings = () => { + + diff --git a/packages/twenty-chrome-extension/src/options/Sidepanel.tsx b/packages/twenty-chrome-extension/src/options/Sidepanel.tsx index 7632bda49..3bea68701 100644 --- a/packages/twenty-chrome-extension/src/options/Sidepanel.tsx +++ b/packages/twenty-chrome-extension/src/options/Sidepanel.tsx @@ -46,44 +46,103 @@ const Sidepanel = () => { const iframeRef = useRef(null); const setIframeState = useCallback(async () => { - const store = await chrome.storage.local.get(); - if (isDefined(store.isAuthenticated)) setIsAuthenticated(true); - const { tab: activeTab } = await chrome.runtime.sendMessage({ - action: 'getActiveTab', - }); + const store = await chrome.storage.local.get([ + 'isAuthenticated', + 'sidepanelUrl', + 'clientUrl', + 'accessToken', + 'refreshToken', + ]); if ( - isDefined(activeTab) && - isDefined(store[`sidepanelUrl_${activeTab.id}`]) + store.isAuthenticated === true && + isDefined(store.accessToken) && + isDefined(store.refreshToken) && + new Date(store.accessToken.expiresAt).getTime() >= Date.now() ) { - const url = store[`sidepanelUrl_${activeTab.id}`]; - setClientUrl(url); - } else if (isDefined(store.clientUrl)) { - setClientUrl(store.clientUrl); + setIsAuthenticated(true); + if (isDefined(store.sidepanelUrl)) { + if (isDefined(store.clientUrl)) { + setClientUrl(`${store.clientUrl}${store.sidepanelUrl}`); + } else { + setClientUrl( + `${import.meta.env.VITE_FRONT_BASE_URL}${store.sidepanelUrl}`, + ); + } + } + } else { + chrome.storage.local.set({ isAuthenticated: false }); + if (isDefined(store.clientUrl)) { + setClientUrl(store.clientUrl); + } } }, [setClientUrl]); useEffect(() => { - const initState = async () => { - const store = await chrome.storage.local.get(); - if (isDefined(store.isAuthenticated)) setIsAuthenticated(true); - void setIframeState(); - }; - void initState(); - // eslint-disable-next-line react-hooks/exhaustive-deps + void setIframeState(); + }, [setIframeState]); + + useEffect(() => { + window.addEventListener('message', async (event) => { + const store = await chrome.storage.local.get([ + 'clientUrl', + 'accessToken', + 'refreshToken', + ]); + const clientUrl = isDefined(store.clientUrl) + ? store.clientUrl + : import.meta.env.VITE_FRONT_BASE_URL; + + if ( + isDefined(store.accessToken) && + isDefined(store.refreshToken) && + event.origin === clientUrl && + event.data === 'loaded' + ) { + event.source?.postMessage( + { + type: 'tokens', + value: { + accessToken: { + token: store.accessToken.token, + expiresAt: store.accessToken.expiresAt, + }, + refreshToken: { + token: store.refreshToken.token, + expiresAt: store.refreshToken.expiresAt, + }, + }, + }, + clientUrl, + ); + } + }); }, []); useEffect(() => { - void setIframeState(); - }, [setIframeState, clientUrl]); - - useEffect(() => { - chrome.storage.local.onChanged.addListener((store) => { - if (isDefined(store.isAuthenticated)) { - if (store.isAuthenticated.newValue === true) { + chrome.storage.local.onChanged.addListener(async (updatedStore) => { + if (isDefined(updatedStore.isAuthenticated)) { + if (updatedStore.isAuthenticated.newValue === true) { setIframeState(); } } + + if (isDefined(updatedStore.sidepanelUrl)) { + if (isDefined(updatedStore.sidepanelUrl.newValue)) { + const store = await chrome.storage.local.get(['clientUrl']); + const clientUrl = isDefined(store.clientUrl) + ? store.clientUrl + : import.meta.env.VITE_FRONT_BASE_URL; + + iframeRef.current?.contentWindow?.postMessage( + { + type: 'navigate', + value: updatedStore.sidepanelUrl.newValue, + }, + clientUrl, + ); + } + } }); }, [setIframeState]); diff --git a/packages/twenty-chrome-extension/src/options/index.tsx b/packages/twenty-chrome-extension/src/options/index.tsx index 1aef04520..01656e4af 100644 --- a/packages/twenty-chrome-extension/src/options/index.tsx +++ b/packages/twenty-chrome-extension/src/options/index.tsx @@ -3,14 +3,14 @@ import ReactDOM from 'react-dom/client'; import { AppThemeProvider } from '@/ui/theme/components/AppThemeProvider'; import { ThemeType } from '@/ui/theme/constants/ThemeLight'; -import Sidepanel from '~/options/Sidepanel'; +import App from '~/options/App'; import '~/index.css'; ReactDOM.createRoot(document.getElementById('app') as HTMLElement).render( - + , ); diff --git a/packages/twenty-chrome-extension/src/utils/apolloClient.ts b/packages/twenty-chrome-extension/src/utils/apolloClient.ts index 23f54fc83..058a2a16b 100644 --- a/packages/twenty-chrome-extension/src/utils/apolloClient.ts +++ b/packages/twenty-chrome-extension/src/utils/apolloClient.ts @@ -1,34 +1,19 @@ -import { - ApolloClient, - from, - fromPromise, - HttpLink, - InMemoryCache, -} from '@apollo/client'; +import { ApolloClient, from, HttpLink, InMemoryCache } from '@apollo/client'; import { setContext } from '@apollo/client/link/context'; import { onError } from '@apollo/client/link/error'; -import { renewToken } from '~/db/token.db'; -import { Tokens } from '~/db/types/auth.types'; import { isDefined } from '~/utils/isDefined'; -const clearStore = () => { - chrome.storage.local.remove(['loginToken', 'accessToken', 'refreshToken']); +export const clearStore = () => { + chrome.storage.local.remove([ + 'loginToken', + 'accessToken', + 'refreshToken', + 'sidepanelUrl', + ]); chrome.storage.local.set({ isAuthenticated: false }); }; -const setStore = (tokens: Tokens) => { - if (isDefined(tokens.loginToken)) { - chrome.storage.local.set({ - loginToken: tokens.loginToken, - }); - } - chrome.storage.local.set({ - accessToken: tokens.accessToken, - refreshToken: tokens.refreshToken, - }); -}; - export const getServerUrl = async () => { const store = await chrome.storage.local.get(); const serverUrl = `${ @@ -46,8 +31,6 @@ const getAuthToken = async () => { }; const getApolloClient = async () => { - const store = await chrome.storage.local.get(); - const authLink = setContext(async (_, { headers }) => { const token = await getAuthToken(); return { @@ -57,57 +40,37 @@ const getApolloClient = async () => { }, }; }); - const errorLink = onError( - ({ graphQLErrors, networkError, forward, operation }) => { - if (isDefined(graphQLErrors)) { - for (const graphQLError of graphQLErrors) { - if (graphQLError.message === 'Unauthorized') { - return fromPromise( - renewToken(store.refreshToken.token) - .then((response) => { - if (isDefined(response)) { - setStore(response.renewToken.tokens); - } - }) - .catch(() => { - clearStore(); - }), - ).flatMap(() => forward(operation)); - } - switch (graphQLError?.extensions?.code) { - case 'UNAUTHENTICATED': { - return fromPromise( - renewToken(store.refreshToken.token) - .then((response) => { - if (isDefined(response)) { - setStore(response.renewToken.tokens); - } - }) - .catch(() => { - clearStore(); - }), - ).flatMap(() => forward(operation)); - } - default: - // eslint-disable-next-line no-console - console.error( - `[GraphQL error]: Message: ${graphQLError.message}, Location: ${ - graphQLError.locations - ? JSON.stringify(graphQLError.locations) - : graphQLError.locations - }, Path: ${graphQLError.path}`, - ); - break; + const errorLink = onError(({ graphQLErrors, networkError }) => { + if (isDefined(graphQLErrors)) { + for (const graphQLError of graphQLErrors) { + if (graphQLError.message === 'Unauthorized') { + clearStore(); + return; + } + switch (graphQLError?.extensions?.code) { + case 'UNAUTHENTICATED': { + clearStore(); + break; } + default: + // eslint-disable-next-line no-console + console.error( + `[GraphQL error]: Message: ${graphQLError.message}, Location: ${ + graphQLError.locations + ? JSON.stringify(graphQLError.locations) + : graphQLError.locations + }, Path: ${graphQLError.path}`, + ); + break; } } + } - if (isDefined(networkError)) { - // eslint-disable-next-line no-console - console.error(`[Network error]: ${networkError}`); - } - }, - ); + if (isDefined(networkError)) { + // eslint-disable-next-line no-console + console.error(`[Network error]: ${networkError}`); + } + }); const httpLink = new HttpLink({ uri: await getServerUrl(), diff --git a/packages/twenty-front/src/App.tsx b/packages/twenty-front/src/App.tsx index 53c879b5f..93b36eb89 100644 --- a/packages/twenty-front/src/App.tsx +++ b/packages/twenty-front/src/App.tsx @@ -13,6 +13,8 @@ import { useRecoilValue } from 'recoil'; import { ApolloProvider } from '@/apollo/components/ApolloProvider'; import { VerifyEffect } from '@/auth/components/VerifyEffect'; +import { ChromeExtensionSidecarEffect } from '@/chrome-extension-sidecar/components/ChromeExtensionSidecarEffect'; +import { ChromeExtensionSidecarProvider } from '@/chrome-extension-sidecar/components/ChromeExtensionSidecarProvider'; import { ClientConfigProvider } from '@/client-config/components/ClientConfigProvider'; import { ClientConfigProviderEffect } from '@/client-config/components/ClientConfigProviderEffect'; import { billingState } from '@/client-config/states/billingState'; @@ -85,36 +87,41 @@ const ProvidersThatNeedRouterContext = () => { const pageTitle = getPageTitleFromPath(pathname); return ( - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + <> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ); }; diff --git a/packages/twenty-front/src/generated/graphql.tsx b/packages/twenty-front/src/generated/graphql.tsx index 829b47714..dbe37ed4d 100644 --- a/packages/twenty-front/src/generated/graphql.tsx +++ b/packages/twenty-front/src/generated/graphql.tsx @@ -133,6 +133,7 @@ export type ClientConfig = { authProviders: AuthProviders; billing: Billing; captcha: Captcha; + chromeExtensionId?: Maybe; debugMode: Scalars['Boolean']; sentry: Sentry; signInPrefilled: Scalars['Boolean']; @@ -1186,7 +1187,7 @@ export type UpdateBillingSubscriptionMutation = { __typename?: 'Mutation', updat export type GetClientConfigQueryVariables = Exact<{ [key: string]: never; }>; -export type GetClientConfigQuery = { __typename?: 'Query', clientConfig: { __typename?: 'ClientConfig', signInPrefilled: boolean, signUpDisabled: boolean, debugMode: boolean, authProviders: { __typename?: 'AuthProviders', google: boolean, password: boolean, microsoft: boolean }, billing: { __typename?: 'Billing', isBillingEnabled: boolean, billingUrl?: string | null, billingFreeTrialDurationInDays?: number | null }, telemetry: { __typename?: 'Telemetry', enabled: boolean, anonymizationEnabled: boolean }, support: { __typename?: 'Support', supportDriver: string, supportFrontChatId?: string | null }, sentry: { __typename?: 'Sentry', dsn?: string | null, environment?: string | null, release?: string | null }, captcha: { __typename?: 'Captcha', provider?: CaptchaDriverType | null, siteKey?: string | null } } }; +export type GetClientConfigQuery = { __typename?: 'Query', clientConfig: { __typename?: 'ClientConfig', signInPrefilled: boolean, signUpDisabled: boolean, debugMode: boolean, chromeExtensionId?: string | null, authProviders: { __typename?: 'AuthProviders', google: boolean, password: boolean, microsoft: boolean }, billing: { __typename?: 'Billing', isBillingEnabled: boolean, billingUrl?: string | null, billingFreeTrialDurationInDays?: number | null }, telemetry: { __typename?: 'Telemetry', enabled: boolean, anonymizationEnabled: boolean }, support: { __typename?: 'Support', supportDriver: string, supportFrontChatId?: string | null }, sentry: { __typename?: 'Sentry', dsn?: string | null, environment?: string | null, release?: string | null }, captcha: { __typename?: 'Captcha', provider?: CaptchaDriverType | null, siteKey?: string | null } } }; export type UserQueryFragmentFragment = { __typename?: 'User', id: any, firstName: string, lastName: string, email: string, canImpersonate: boolean, supportUserHash?: string | null, workspaceMember?: { __typename?: 'WorkspaceMember', id: any, colorScheme: string, avatarUrl?: string | null, locale: string, name: { __typename?: 'FullName', firstName: string, lastName: string } } | null, defaultWorkspace: { __typename?: 'Workspace', id: any, displayName?: string | null, logo?: string | null, domainName?: string | null, inviteHash?: string | null, allowImpersonation: boolean, subscriptionStatus: string, activationStatus: string, currentCacheVersion?: string | null, featureFlags?: Array<{ __typename?: 'FeatureFlag', id: any, key: string, value: boolean, workspaceId: string }> | null, currentBillingSubscription?: { __typename?: 'BillingSubscription', id: any, status: string, interval?: string | null } | null }, workspaces: Array<{ __typename?: 'UserWorkspace', workspace?: { __typename?: 'Workspace', id: any, logo?: string | null, displayName?: string | null, domainName?: string | null } | null }> }; @@ -2307,6 +2308,7 @@ export const GetClientConfigDocument = gql` provider siteKey } + chromeExtensionId } } `; diff --git a/packages/twenty-front/src/modules/chrome-extension-sidecar/components/ChromeExtensionSidecarEffect.tsx b/packages/twenty-front/src/modules/chrome-extension-sidecar/components/ChromeExtensionSidecarEffect.tsx new file mode 100644 index 000000000..f7f98e702 --- /dev/null +++ b/packages/twenty-front/src/modules/chrome-extension-sidecar/components/ChromeExtensionSidecarEffect.tsx @@ -0,0 +1,58 @@ +import { useEffect } from 'react'; +import { useNavigate } from 'react-router-dom'; +import { useRecoilValue, useSetRecoilState } from 'recoil'; + +import { tokenPairState } from '@/auth/states/tokenPairState'; +import { isLoadingTokensFromExtensionState } from '@/chrome-extension-sidecar/states/isLoadingTokensFromExtensionState'; +import { chromeExtensionIdState } from '@/client-config/states/chromeExtensionIdState'; +import { isDefined } from '~/utils/isDefined'; +import { isInFrame } from '~/utils/isInIframe'; + +export const ChromeExtensionSidecarEffect = () => { + const navigate = useNavigate(); + const setTokenPair = useSetRecoilState(tokenPairState); + const chromeExtensionId = useRecoilValue(chromeExtensionIdState); + const setIsLoadingTokensFromExtension = useSetRecoilState( + isLoadingTokensFromExtensionState, + ); + + useEffect(() => { + if (isInFrame() && isDefined(chromeExtensionId)) { + window.parent.postMessage( + 'loaded', + `chrome-extension://${chromeExtensionId}`, + ); + + const handleWindowEvents = (event: MessageEvent) => { + if (event.origin === `chrome-extension://${chromeExtensionId}`) { + switch (event.data.type) { + case 'tokens': { + setTokenPair(event.data.value); + setIsLoadingTokensFromExtension(true); + break; + } + case 'navigate': + navigate(event.data.value); + break; + default: + break; + } + } else { + setIsLoadingTokensFromExtension(false); + return; + } + }; + window.addEventListener('message', handleWindowEvents); + return () => { + window.removeEventListener('message', handleWindowEvents); + }; + } + }, [ + chromeExtensionId, + setIsLoadingTokensFromExtension, + setTokenPair, + navigate, + ]); + + return <>; +}; diff --git a/packages/twenty-front/src/modules/chrome-extension-sidecar/components/ChromeExtensionSidecarProvider.tsx b/packages/twenty-front/src/modules/chrome-extension-sidecar/components/ChromeExtensionSidecarProvider.tsx new file mode 100644 index 000000000..3c4d8556a --- /dev/null +++ b/packages/twenty-front/src/modules/chrome-extension-sidecar/components/ChromeExtensionSidecarProvider.tsx @@ -0,0 +1,56 @@ +import styled from '@emotion/styled'; +import { useRecoilValue } from 'recoil'; + +import { isLoadingTokensFromExtensionState } from '@/chrome-extension-sidecar/states/isLoadingTokensFromExtensionState'; +import { chromeExtensionIdState } from '@/client-config/states/chromeExtensionIdState'; +import { isDefined } from '~/utils/isDefined'; +import { isInFrame } from '~/utils/isInIframe'; + +const StyledContainer = styled.div` + align-items: center; + display: flex; + flex-direction: column; + height: 100vh; + justify-content: center; +`; + +const AppInaccessible = ({ message }: { message: string }) => { + return ( + + twenty-icon +

{message}

+
+ ); +}; + +export const ChromeExtensionSidecarProvider: React.FC< + React.PropsWithChildren +> = ({ children }) => { + const isLoadingTokensFromExtension = useRecoilValue( + isLoadingTokensFromExtensionState, + ); + const chromeExtensionId = useRecoilValue(chromeExtensionIdState); + + if (!isInFrame()) return <>{children}; + + if (!isDefined(chromeExtensionId)) + return ( + + ); + + if (isDefined(isLoadingTokensFromExtension) && !isLoadingTokensFromExtension) + return ( + + ); + + return isLoadingTokensFromExtension && <>{children}; +}; diff --git a/packages/twenty-front/src/modules/chrome-extension-sidecar/states/isLoadingTokensFromExtensionState.ts b/packages/twenty-front/src/modules/chrome-extension-sidecar/states/isLoadingTokensFromExtensionState.ts new file mode 100644 index 000000000..e04798f19 --- /dev/null +++ b/packages/twenty-front/src/modules/chrome-extension-sidecar/states/isLoadingTokensFromExtensionState.ts @@ -0,0 +1,6 @@ +import { createState } from 'twenty-ui'; + +export const isLoadingTokensFromExtensionState = createState({ + key: 'isLoadingTokensFromExtensionState', + defaultValue: null, +}); diff --git a/packages/twenty-front/src/modules/client-config/components/ClientConfigProviderEffect.tsx b/packages/twenty-front/src/modules/client-config/components/ClientConfigProviderEffect.tsx index 66ccb5f38..af45dc0fd 100644 --- a/packages/twenty-front/src/modules/client-config/components/ClientConfigProviderEffect.tsx +++ b/packages/twenty-front/src/modules/client-config/components/ClientConfigProviderEffect.tsx @@ -4,6 +4,7 @@ import { useRecoilState, useSetRecoilState } from 'recoil'; import { authProvidersState } from '@/client-config/states/authProvidersState'; import { billingState } from '@/client-config/states/billingState'; import { captchaProviderState } from '@/client-config/states/captchaProviderState'; +import { chromeExtensionIdState } from '@/client-config/states/chromeExtensionIdState'; import { isClientConfigLoadedState } from '@/client-config/states/isClientConfigLoadedState'; import { isDebugModeState } from '@/client-config/states/isDebugModeState'; import { isSignInPrefilledState } from '@/client-config/states/isSignInPrefilledState'; @@ -32,6 +33,8 @@ export const ClientConfigProviderEffect = () => { const setCaptchaProvider = useSetRecoilState(captchaProviderState); + const setChromeExtensionId = useSetRecoilState(chromeExtensionIdState); + const { data, loading } = useGetClientConfigQuery({ skip: isClientConfigLoaded, }); @@ -63,6 +66,8 @@ export const ClientConfigProviderEffect = () => { provider: data?.clientConfig?.captcha?.provider, siteKey: data?.clientConfig?.captcha?.siteKey, }); + + setChromeExtensionId(data?.clientConfig?.chromeExtensionId); } }, [ data, @@ -77,6 +82,7 @@ export const ClientConfigProviderEffect = () => { loading, setIsClientConfigLoaded, setCaptchaProvider, + setChromeExtensionId, ]); return <>; diff --git a/packages/twenty-front/src/modules/client-config/graphql/queries/getClientConfig.ts b/packages/twenty-front/src/modules/client-config/graphql/queries/getClientConfig.ts index 563543bba..528e68f38 100644 --- a/packages/twenty-front/src/modules/client-config/graphql/queries/getClientConfig.ts +++ b/packages/twenty-front/src/modules/client-config/graphql/queries/getClientConfig.ts @@ -33,6 +33,7 @@ export const GET_CLIENT_CONFIG = gql` provider siteKey } + chromeExtensionId } } `; diff --git a/packages/twenty-front/src/modules/client-config/states/chromeExtensionIdState.ts b/packages/twenty-front/src/modules/client-config/states/chromeExtensionIdState.ts new file mode 100644 index 000000000..bec5ae986 --- /dev/null +++ b/packages/twenty-front/src/modules/client-config/states/chromeExtensionIdState.ts @@ -0,0 +1,6 @@ +import { createState } from 'twenty-ui'; + +export const chromeExtensionIdState = createState({ + key: 'chromeExtensionIdState', + defaultValue: null, +}); diff --git a/packages/twenty-front/src/utils/isInIframe.ts b/packages/twenty-front/src/utils/isInIframe.ts new file mode 100644 index 000000000..7d160ccae --- /dev/null +++ b/packages/twenty-front/src/utils/isInIframe.ts @@ -0,0 +1,7 @@ +export const isInFrame = () => { + try { + return window.self !== window.top; + } catch (e) { + return true; + } +}; diff --git a/packages/twenty-server/.env.example b/packages/twenty-server/.env.example index 058fd4300..f7ecf57c3 100644 --- a/packages/twenty-server/.env.example +++ b/packages/twenty-server/.env.example @@ -71,5 +71,5 @@ SIGN_IN_PREFILLED=true # API_RATE_LIMITING_TTL= # API_RATE_LIMITING_LIMIT= # MUTATION_MAXIMUM_RECORD_AFFECTED=100 -# CHROME_EXTENSION_REDIRECT_URL=https://bggmipldbceihilonnbpgoeclgbkblkp.chromiumapp.org +# CHROME_EXTENSION_ID=bggmipldbceihilonnbpgoeclgbkblkp # PG_SSL_ALLOW_SELF_SIGNED=true diff --git a/packages/twenty-server/src/engine/core-modules/auth/services/auth.service.ts b/packages/twenty-server/src/engine/core-modules/auth/services/auth.service.ts index e82ffe6f2..391e1c7da 100644 --- a/packages/twenty-server/src/engine/core-modules/auth/services/auth.service.ts +++ b/packages/twenty-server/src/engine/core-modules/auth/services/auth.service.ts @@ -203,7 +203,9 @@ export class AuthService { this.environmentService.get('NODE_ENV') === NodeEnvironment.development ? authorizeAppInput.redirectUrl - : `${this.environmentService.get('CHROME_EXTENSION_REDIRECT_URL')}`, + : `https://${this.environmentService.get( + 'CHROME_EXTENSION_ID', + )}.chromiumapp.org/`, }, ]; diff --git a/packages/twenty-server/src/engine/core-modules/client-config/client-config.entity.ts b/packages/twenty-server/src/engine/core-modules/client-config/client-config.entity.ts index 103fbc9ea..b895f4277 100644 --- a/packages/twenty-server/src/engine/core-modules/client-config/client-config.entity.ts +++ b/packages/twenty-server/src/engine/core-modules/client-config/client-config.entity.ts @@ -96,4 +96,7 @@ export class ClientConfig { @Field(() => Captcha) captcha: Captcha; + + @Field(() => String, { nullable: true }) + chromeExtensionId: string | undefined; } diff --git a/packages/twenty-server/src/engine/core-modules/client-config/client-config.resolver.ts b/packages/twenty-server/src/engine/core-modules/client-config/client-config.resolver.ts index ad609ca70..5d55f4e17 100644 --- a/packages/twenty-server/src/engine/core-modules/client-config/client-config.resolver.ts +++ b/packages/twenty-server/src/engine/core-modules/client-config/client-config.resolver.ts @@ -48,6 +48,7 @@ export class ClientConfigResolver { provider: this.environmentService.get('CAPTCHA_DRIVER'), siteKey: this.environmentService.get('CAPTCHA_SITE_KEY'), }, + chromeExtensionId: this.environmentService.get('CHROME_EXTENSION_ID'), }; return Promise.resolve(clientConfig); diff --git a/packages/twenty-server/src/engine/integrations/environment/environment-variables.ts b/packages/twenty-server/src/engine/integrations/environment/environment-variables.ts index aab54ab1a..670440d3c 100644 --- a/packages/twenty-server/src/engine/integrations/environment/environment-variables.ts +++ b/packages/twenty-server/src/engine/integrations/environment/environment-variables.ts @@ -380,7 +380,7 @@ export class EnvironmentVariables { AUTH_GOOGLE_APIS_CALLBACK_URL: string; - CHROME_EXTENSION_REDIRECT_URL: string; + CHROME_EXTENSION_ID: string; } export const validate = ( diff --git a/yarn.lock b/yarn.lock index 039279ebe..305906fc5 100644 --- a/yarn.lock +++ b/yarn.lock @@ -16113,13 +16113,6 @@ __metadata: languageName: node linkType: hard -"@types/crypto-js@npm:^4.2.2": - version: 4.2.2 - resolution: "@types/crypto-js@npm:4.2.2" - checksum: 760a2078f36f2a3a1089ef367b0d13229876adcf4bcd6e8824d00d9e9bfad8118dc7e6a3cc66322b083535e12be3a29044ccdc9603bfb12519ff61551a3322c6 - languageName: node - linkType: hard - "@types/d3-array@npm:*": version: 3.2.1 resolution: "@types/d3-array@npm:3.2.1" @@ -16452,13 +16445,6 @@ __metadata: languageName: node linkType: hard -"@types/dagre@npm:^0.7.52": - version: 0.7.52 - resolution: "@types/dagre@npm:0.7.52" - checksum: 0e196a8c17a92765d6e28b10d78d5c1cb1ee540598428cbb61ce3b90e0fedaac2b11f6dbeebf0d2f69d5332d492b12091be5f1e575f538194e20d8887979d006 - languageName: node - linkType: hard - "@types/debug@npm:^4.0.0": version: 4.1.12 resolution: "@types/debug@npm:4.1.12" @@ -23960,13 +23946,6 @@ __metadata: languageName: node linkType: hard -"crypto-js@npm:^4.2.0": - version: 4.2.0 - resolution: "crypto-js@npm:4.2.0" - checksum: 8fbdf9d56f47aea0794ab87b0eb9833baf80b01a7c5c1b0edc7faf25f662fb69ab18dc2199e2afcac54670ff0cd9607a9045a3f7a80336cccd18d77a55b9fdf0 - languageName: node - linkType: hard - "crypto-random-string@npm:^2.0.0": version: 2.0.0 resolution: "crypto-random-string@npm:2.0.0" @@ -46918,8 +46897,6 @@ __metadata: "@types/better-sqlite3": "npm:^7.6.8" "@types/bytes": "npm:^3.1.1" "@types/chrome": "npm:^0.0.267" - "@types/crypto-js": "npm:^4.2.2" - "@types/dagre": "npm:^0.7.52" "@types/deep-equal": "npm:^1.0.1" "@types/dompurify": "npm:^3.0.5" "@types/express": "npm:^4.17.13" @@ -46980,7 +46957,6 @@ __metadata: concurrently: "npm:^8.2.2" cross-env: "npm:^7.0.3" cross-var: "npm:^1.1.0" - crypto-js: "npm:^4.2.0" danger: "npm:^11.3.0" danger-plugin-todos: "npm:^1.3.1" dataloader: "npm:^2.2.2"