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 (
+
+
+ {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"