Compare commits

...

3 Commits

Author SHA1 Message Date
neo773 4250890b75 fix(messaging): preserve same-domain imports for institutional accounts
Domain equality is unreliable at universities and other institutional
domains where unrelated members share an address space. Gate
filterOutInternals and the contact-self/workspace-member filter on a
Hipolabs-backed institutional domain list so legitimate collaborators
are not silently dropped during import.
2026-05-06 17:57:57 +05:30
neo773 5e6e5aaaf2 leh lint 2026-05-06 15:07:40 +05:30
neo773 0f6c997070 fix(sdk): pin chokidar v3 to restore fsevents on darwin
v4 dropped fsevents native binding, falling back to recursive fs.watch
which opens fd-per-dir on macOS and hits EMFILE on large monorepos.
Replace internal chokidar/handler.js EventName import with local type
since v3 has no such module.
2026-05-06 14:53:30 +05:30
12 changed files with 10761 additions and 60 deletions
+2 -1
View File
@@ -34,7 +34,8 @@
"@types/qs": "6.9.16",
"@wyw-in-js/transform@npm:0.6.0": "patch:@wyw-in-js/transform@npm%3A0.7.0#~/.yarn/patches/@wyw-in-js-transform-npm-0.7.0-ba641dc99f.patch",
"@wyw-in-js/transform@npm:0.7.0": "patch:@wyw-in-js/transform@npm%3A0.7.0#~/.yarn/patches/@wyw-in-js-transform-npm-0.7.0-ba641dc99f.patch",
"@opentelemetry/api": "1.9.1"
"@opentelemetry/api": "1.9.1",
"chokidar": "^3.6.0"
},
"version": "0.2.1",
"nx": {},
+11
View File
@@ -76,3 +76,14 @@ export type FunctionExecutionResult = {
stackTrace: string;
};
};
export type ChokidarFsEvent =
| 'add'
| 'addDir'
| 'change'
| 'unlink'
| 'unlinkDir'
| 'ready'
| 'raw'
| 'error'
| 'all';
@@ -1,7 +1,7 @@
import path, { relative } from 'path';
import chokidar, { type FSWatcher } from 'chokidar';
import { type EventName } from 'chokidar/handler.js';
import { ASSETS_DIR } from 'twenty-shared/application';
import { type ChokidarFsEvent } from '@/cli/types';
export type ManifestWatcherOptions = {
appPath: string;
@@ -13,7 +13,10 @@ const IGNORED_DIRECTORY_NAMES = new Set(['node_modules', 'dist', '.twenty']);
export class ManifestWatcher {
private appPath: string;
private handleChangeDetected: (filePath: string, event: EventName) => void;
private handleChangeDetected: (
filePath: string,
event: ChokidarFsEvent,
) => void;
private verbose: boolean;
private watcher: FSWatcher | null = null;
@@ -10,7 +10,7 @@ import { type ManifestBuildResult } from '@/cli/utilities/build/manifest/manifes
import { ManifestWatcher } from '@/cli/utilities/build/manifest/manifest-watcher';
import { type OrchestratorState } from '@/cli/utilities/dev/orchestrator/dev-mode-orchestrator-state';
import type { Location } from 'esbuild';
import { type EventName } from 'chokidar/handler.js';
import { type ChokidarFsEvent } from '@/cli/types';
import { ASSETS_DIR } from 'twenty-shared/application';
import { FileFolder } from 'twenty-shared/types';
@@ -102,7 +102,10 @@ export class StartWatchersOrchestratorStep {
]);
}
private handleChangeDetected(sourcePath: string, event: EventName): void {
private handleChangeDetected(
sourcePath: string,
event: ChokidarFsEvent,
): void {
this.state.addEvent({
message: `Change detected: ${sourcePath}`,
status: 'info',
@@ -0,0 +1,88 @@
import { type ConnectedAccountEntity } from 'src/engine/metadata-modules/connected-account/entities/connected-account.entity';
import { type Contact } from 'src/modules/contact-creation-manager/types/contact.type';
import { filterOutContactsThatBelongToSelfOrWorkspaceMembers } from 'src/modules/contact-creation-manager/utils/filter-out-contacts-that-belong-to-self-or-workspace-members.util';
import { type WorkspaceMemberWorkspaceEntity } from 'src/modules/workspace-member/standard-objects/workspace-member.workspace-entity';
const account = (
handle: string,
handleAliases: string[] = [],
): ConnectedAccountEntity =>
({ handle, handleAliases }) as ConnectedAccountEntity;
const member = (userEmail: string): WorkspaceMemberWorkspaceEntity =>
({ userEmail }) as unknown as WorkspaceMemberWorkspaceEntity;
const contact = (handle: string): Contact => ({ handle, displayName: handle });
describe('filterOutContactsThatBelongToSelfOrWorkspaceMembers', () => {
it('drops contacts sharing a company domain with the connected account', () => {
const result = filterOutContactsThatBelongToSelfOrWorkspaceMembers(
[contact('colleague@acme.com'), contact('lead@prospect.com')],
account('me@acme.com'),
[],
);
expect(result.map((c) => c.handle)).toEqual(['lead@prospect.com']);
});
it('keeps same-domain contacts when the connected account is on a consumer email provider', () => {
// gmail.com is shared by millions of unrelated people, so domain
// equality cannot mean "teammate".
const result = filterOutContactsThatBelongToSelfOrWorkspaceMembers(
[contact('friend@gmail.com')],
account('me@gmail.com'),
[],
);
expect(result).toHaveLength(1);
});
it('keeps same-domain contacts when the connected account is on a university domain', () => {
// Universities host thousands of unrelated members; treat them like
// consumer providers and let collaborators through.
const result = filterOutContactsThatBelongToSelfOrWorkspaceMembers(
[contact('professor@mit.edu'), contact('lab-member@mit.edu')],
account('researcher@mit.edu'),
[],
);
expect(result).toHaveLength(2);
});
it('drops workspace members regardless of domain', () => {
const result = filterOutContactsThatBelongToSelfOrWorkspaceMembers(
[
contact('teammate@partner.com'),
contact('outsider@partner.com'),
],
account('me@acme.com'),
[member('teammate@partner.com')],
);
expect(result.map((c) => c.handle)).toEqual(['outsider@partner.com']);
});
it('drops the connected account handle and any of its aliases', () => {
const result = filterOutContactsThatBelongToSelfOrWorkspaceMembers(
[
contact('me@acme.com'),
contact('me+work@acme.com'),
contact('real-contact@partner.com'),
],
account('me@acme.com', ['me+work@acme.com']),
[],
);
expect(result.map((c) => c.handle)).toEqual(['real-contact@partner.com']);
});
it('matches handles case-insensitively', () => {
const result = filterOutContactsThatBelongToSelfOrWorkspaceMembers(
[contact('Teammate@Partner.com')],
account('me@acme.com'),
[member('teammate@partner.com')],
);
expect(result).toEqual([]);
});
});
@@ -4,7 +4,7 @@ import { type ConnectedAccountEntity } from 'src/engine/metadata-modules/connect
import { type Contact } from 'src/modules/contact-creation-manager/types/contact.type';
import { getDomainNameFromHandle } from 'src/modules/contact-creation-manager/utils/get-domain-name-from-handle.util';
import { type WorkspaceMemberWorkspaceEntity } from 'src/modules/workspace-member/standard-objects/workspace-member.workspace-entity';
import { isWorkDomain } from 'src/utils/is-work-email';
import { isPersonalOrInstitutionalDomain } from 'src/utils/is-personal-or-institutional-email';
export function filterOutContactsThatBelongToSelfOrWorkspaceMembers(
contacts: Contact[],
@@ -41,7 +41,7 @@ export function filterOutContactsThatBelongToSelfOrWorkspaceMembers(
return contacts.filter(
(contact) =>
(isDifferentDomain(contact, selfDomainName) ||
!isWorkDomain(selfDomainName)) &&
isPersonalOrInstitutionalDomain(selfDomainName)) &&
// @ts-expect-error legacy noImplicitAny
!workspaceMembersMap[contact.handle.toLowerCase()] &&
!allHandles.includes(contact.handle.toLowerCase()),
@@ -37,6 +37,43 @@ describe('filterEmails', () => {
expect(filteredMessages).toEqual([]);
});
it('Should keep same-domain emails when primary handle is on a university domain', () => {
// Universities host thousands of unrelated members across departments
// and labs, so domain equality is not a reliable proxy for "internal
// teammate". Treat them like consumer providers and skip the same-domain
// filter — collaborators sharing the institution domain should be
// imported, not silently dropped.
const primaryHandle = 'researcher@mit.edu';
const messages: MessageWithParticipants[] = [
{
externalId: 'institution-internal',
subject: 'Research collaboration',
receivedAt: new Date('2025-01-09T09:54:37.000Z'),
text: 'Following up on the paper.',
headerMessageId: '<msg@mit.edu>',
messageThreadExternalId: 'thread-1',
direction: MessageDirection.OUTGOING,
participants: [
{
role: MessageParticipantRole.FROM,
handle: 'researcher@mit.edu',
displayName: 'Researcher',
},
{
role: MessageParticipantRole.TO,
handle: 'professor@mit.edu',
displayName: 'Professor',
},
],
attachments: [],
},
];
const result = filterEmails(primaryHandle, [], messages, []);
expect(result).toEqual(messages);
});
it('Should filter messages with participant from the blocklist', () => {
const primaryHandle = 'guillim@acme.com';
const messages = messagingGetMessagesServiceGetMessages.filter(
@@ -7,7 +7,7 @@ import { filterOutIcsAttachments } from 'src/modules/messaging/message-import-ma
import { filterOutInternals } from 'src/modules/messaging/message-import-manager/utils/filter-out-internals.util';
import { isGroupEmail } from 'src/modules/messaging/message-import-manager/utils/is-group-email';
import { isMessageSenderMatchingHandles } from 'src/modules/messaging/message-import-manager/utils/is-message-sender-matching-handles.util';
import { isWorkEmail } from 'src/utils/is-work-email';
import { isPersonalOrInstitutionalEmail } from 'src/utils/is-personal-or-institutional-email';
export const filterEmails = (
primaryHandle: string,
@@ -24,9 +24,9 @@ export const filterEmails = (
blocklist,
);
const messagesWithoutInternals = isWorkEmail(primaryHandle)
? filterOutInternals(primaryHandle, messagesWithoutBlocklisted)
: messagesWithoutBlocklisted;
const messagesWithoutInternals = isPersonalOrInstitutionalEmail(primaryHandle)
? messagesWithoutBlocklisted
: filterOutInternals(primaryHandle, messagesWithoutBlocklisted);
if (!excludeGroupEmails) {
return messagesWithoutInternals;
@@ -0,0 +1,39 @@
import {
isPersonalOrInstitutionalDomain,
isPersonalOrInstitutionalEmail,
} from 'src/utils/is-personal-or-institutional-email';
describe('isPersonalOrInstitutionalDomain', () => {
it('returns true for consumer email providers', () => {
expect(isPersonalOrInstitutionalDomain('gmail.com')).toBe(true);
expect(isPersonalOrInstitutionalDomain('outlook.com')).toBe(true);
});
it('returns true for universities present in the Hipolabs dataset', () => {
// Domains where many unrelated members share an address space.
expect(isPersonalOrInstitutionalDomain('mit.edu')).toBe(true);
expect(isPersonalOrInstitutionalDomain('cam.ac.uk')).toBe(true);
expect(isPersonalOrInstitutionalDomain('ethz.ch')).toBe(true);
});
it('returns false for company-owned domains', () => {
expect(isPersonalOrInstitutionalDomain('acme.com')).toBe(false);
expect(isPersonalOrInstitutionalDomain('example.io')).toBe(false);
});
});
describe('isPersonalOrInstitutionalEmail', () => {
it('extracts the domain and applies the same rules as the domain check', () => {
expect(isPersonalOrInstitutionalEmail('user@gmail.com')).toBe(true);
expect(isPersonalOrInstitutionalEmail('researcher@mit.edu')).toBe(true);
expect(isPersonalOrInstitutionalEmail('user@acme.com')).toBe(false);
});
it('returns false rather than throwing on malformed input', () => {
// Callers (filter-emails gate, contact filter) treat false as
// "domain looks org-defining" — failing closed is the safe default.
expect(isPersonalOrInstitutionalEmail('')).toBe(false);
expect(isPersonalOrInstitutionalEmail('not-an-email')).toBe(false);
expect(isPersonalOrInstitutionalEmail('missing-domain@')).toBe(false);
});
});
@@ -0,0 +1,14 @@
import { emailProvidersSet } from 'src/utils/email-providers';
import { getDomainNameByEmail } from 'src/utils/get-domain-name-by-email';
import { universityDomainsSet } from 'src/utils/university-domains';
export const isPersonalOrInstitutionalDomain = (domain: string) =>
emailProvidersSet.has(domain) || universityDomainsSet.has(domain);
export const isPersonalOrInstitutionalEmail = (email: string) => {
try {
return isPersonalOrInstitutionalDomain(getDomainNameByEmail(email));
} catch {
return false;
}
};
File diff suppressed because it is too large Load Diff
+5 -49
View File
@@ -25928,7 +25928,7 @@ __metadata:
languageName: node
linkType: hard
"anymatch@npm:^3.0.3, anymatch@npm:^3.1.3, anymatch@npm:~3.1.1, anymatch@npm:~3.1.2":
"anymatch@npm:^3.0.3, anymatch@npm:^3.1.3, anymatch@npm:~3.1.2":
version: 3.1.3
resolution: "anymatch@npm:3.1.3"
dependencies:
@@ -28507,26 +28507,7 @@ __metadata:
languageName: node
linkType: hard
"chokidar@npm:3.5.1":
version: 3.5.1
resolution: "chokidar@npm:3.5.1"
dependencies:
anymatch: "npm:~3.1.1"
braces: "npm:~3.0.2"
fsevents: "npm:~2.3.1"
glob-parent: "npm:~5.1.0"
is-binary-path: "npm:~2.1.0"
is-glob: "npm:~4.0.1"
normalize-path: "npm:~3.0.0"
readdirp: "npm:~3.5.0"
dependenciesMeta:
fsevents:
optional: true
checksum: 10c0/894d2fdeeef6a0bc61993a20b864e29e9296f2308628b8b2edf1bef2d59ab11f21938eebbbcbf581f15d16d3e030c08860d2fb035f7b9f3baebac57049a37959
languageName: node
linkType: hard
"chokidar@npm:3.6.0, chokidar@npm:^3.5.3, chokidar@npm:^3.6.0":
"chokidar@npm:^3.6.0":
version: 3.6.0
resolution: "chokidar@npm:3.6.0"
dependencies:
@@ -28545,15 +28526,6 @@ __metadata:
languageName: node
linkType: hard
"chokidar@npm:4.0.3, chokidar@npm:^4.0.0, chokidar@npm:^4.0.1, chokidar@npm:^4.0.3":
version: 4.0.3
resolution: "chokidar@npm:4.0.3"
dependencies:
readdirp: "npm:^4.0.1"
checksum: 10c0/a58b9df05bb452f7d105d9e7229ac82fa873741c0c40ddcc7bb82f8a909fbe3f7814c9ebe9bc9a2bef9b737c0ec6e2d699d179048ef06ad3ec46315df0ebe6ad
languageName: node
linkType: hard
"chownr@npm:^1.1.1":
version: 1.1.4
resolution: "chownr@npm:1.1.4"
@@ -34712,7 +34684,7 @@ __metadata:
languageName: node
linkType: hard
"fsevents@npm:^2.3.2, fsevents@npm:^2.3.3, fsevents@npm:~2.3.1, fsevents@npm:~2.3.2, fsevents@npm:~2.3.3":
"fsevents@npm:^2.3.2, fsevents@npm:^2.3.3, fsevents@npm:~2.3.2, fsevents@npm:~2.3.3":
version: 2.3.3
resolution: "fsevents@npm:2.3.3"
dependencies:
@@ -34731,7 +34703,7 @@ __metadata:
languageName: node
linkType: hard
"fsevents@patch:fsevents@npm%3A^2.3.2#optional!builtin<compat/fsevents>, fsevents@patch:fsevents@npm%3A^2.3.3#optional!builtin<compat/fsevents>, fsevents@patch:fsevents@npm%3A~2.3.1#optional!builtin<compat/fsevents>, fsevents@patch:fsevents@npm%3A~2.3.2#optional!builtin<compat/fsevents>, fsevents@patch:fsevents@npm%3A~2.3.3#optional!builtin<compat/fsevents>":
"fsevents@patch:fsevents@npm%3A^2.3.2#optional!builtin<compat/fsevents>, fsevents@patch:fsevents@npm%3A^2.3.3#optional!builtin<compat/fsevents>, fsevents@patch:fsevents@npm%3A~2.3.2#optional!builtin<compat/fsevents>, fsevents@patch:fsevents@npm%3A~2.3.3#optional!builtin<compat/fsevents>":
version: 2.3.3
resolution: "fsevents@patch:fsevents@npm%3A2.3.3#optional!builtin<compat/fsevents>::version=2.3.3&hash=df0bf1"
dependencies:
@@ -35141,7 +35113,7 @@ __metadata:
languageName: node
linkType: hard
"glob-parent@npm:^5.1.2, glob-parent@npm:~5.1.0, glob-parent@npm:~5.1.2":
"glob-parent@npm:^5.1.2, glob-parent@npm:~5.1.2":
version: 5.1.2
resolution: "glob-parent@npm:5.1.2"
dependencies:
@@ -48895,22 +48867,6 @@ __metadata:
languageName: node
linkType: hard
"readdirp@npm:^4.0.1":
version: 4.1.2
resolution: "readdirp@npm:4.1.2"
checksum: 10c0/60a14f7619dec48c9c850255cd523e2717001b0e179dc7037cfa0895da7b9e9ab07532d324bfb118d73a710887d1e35f79c495fa91582784493e085d18c72c62
languageName: node
linkType: hard
"readdirp@npm:~3.5.0":
version: 3.5.0
resolution: "readdirp@npm:3.5.0"
dependencies:
picomatch: "npm:^2.2.1"
checksum: 10c0/293de2ed981884a09e76fbf90bddc7e1a87667e57e0284ddc8c177e3151b0d179a9a56441d9f2f3654423924ec100af57ba9e507086527f98fd1d21bdd041c3e
languageName: node
linkType: hard
"readdirp@npm:~3.6.0":
version: 3.6.0
resolution: "readdirp@npm:3.6.0"