twenty-app-intake 0.4.0 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -521,7 +521,7 @@ var FlattenStage = class {
521
521
 
522
522
  // src/constants/field-map.ts
523
523
  function normaliseKey(key) {
524
- return key.replace(/([A-Z])/g, "_$1").toLowerCase().replace(/[-\s.]/g, "_").replace(/_{2,}/g, "_");
524
+ return key.replace(/([a-z0-9])([A-Z])/g, "$1_$2").replace(/([A-Z]+)([A-Z][a-z])/g, "$1_$2").toLowerCase().replace(/[-\s.]/g, "_").replace(/_{2,}/g, "_");
525
525
  }
526
526
  var FIELD_MAP = {
527
527
  // ── Phone ─────────────────────────────────────────────────────────────────
@@ -828,15 +828,56 @@ function parseTargetObject(raw) {
828
828
  return "auto";
829
829
  }
830
830
  }
831
- function normalizeFields(flat, rules = []) {
832
- const sorted = [...rules].sort((a, b) => b.priority - a.priority);
831
+ var PROTECTED_CANONICAL_NAMES = /* @__PURE__ */ new Set([
832
+ "email",
833
+ "phone",
834
+ "firstName",
835
+ "lastName",
836
+ "fullName"
837
+ ]);
838
+ function compileRule(rule) {
839
+ try {
840
+ return { ...rule, matcher: new RegExp(rule.inputPattern, "i") };
841
+ } catch {
842
+ return { ...rule, matcher: null };
843
+ }
844
+ }
845
+ function ruleMatches(rule, key, lookupKey) {
846
+ if (rule.inputPattern === key || rule.inputPattern === lookupKey) return true;
847
+ if (!rule.matcher) return false;
848
+ return rule.matcher.test(key) || rule.matcher.test(lookupKey);
849
+ }
850
+ function normalizeFields(flat, rules = [], warnings = []) {
851
+ const compiled = [...rules].sort((a, b) => b.priority - a.priority).map(compileRule);
833
852
  return Object.entries(flat).map(
834
- ([key, value]) => normalizeField(key, value, sorted)
853
+ ([key, value]) => normalizeField(key, value, compiled, warnings)
835
854
  );
836
855
  }
837
- function normalizeField(key, value, rules) {
856
+ function normalizeField(key, value, rules, warnings) {
838
857
  const lookupKey = normaliseKey(key);
839
858
  const builtIn = FIELD_MAP[lookupKey];
859
+ for (const rule of rules) {
860
+ if (!ruleMatches(rule, key, lookupKey)) continue;
861
+ if (builtIn && PROTECTED_CANONICAL_NAMES.has(builtIn.canonicalName)) {
862
+ const displaces = rule.canonicalName !== builtIn.canonicalName || actionForRule(rule) !== "field";
863
+ if (displaces) {
864
+ warnings.push(
865
+ `Rule "${rule.inputPattern}" matched "${key}", which the built-in map holds as the contact's ${builtIn.canonicalName}. The built-in mapping was kept: a contact with no ${builtIn.canonicalName} cannot be found again, so every submission would create a duplicate. Point the rule at a key the built-in map does not already own.`
866
+ );
867
+ continue;
868
+ }
869
+ }
870
+ return {
871
+ originalKey: key,
872
+ canonicalName: rule.canonicalName,
873
+ twentyType: typeForRule(rule),
874
+ action: actionForRule(rule),
875
+ value,
876
+ source: "rule",
877
+ targetObject: parseTargetObject(rule.targetObject),
878
+ mergeStrategy: parseFieldMergeStrategy(rule.mergeStrategy)
879
+ };
880
+ }
840
881
  if (builtIn) {
841
882
  return {
842
883
  originalKey: key,
@@ -848,26 +889,6 @@ function normalizeField(key, value, rules) {
848
889
  targetObject: "auto"
849
890
  };
850
891
  }
851
- for (const rule of rules) {
852
- let matches = false;
853
- try {
854
- matches = rule.inputPattern === key || rule.inputPattern === lookupKey || new RegExp(rule.inputPattern, "i").test(key);
855
- } catch {
856
- matches = rule.inputPattern === key;
857
- }
858
- if (matches) {
859
- return {
860
- originalKey: key,
861
- canonicalName: rule.canonicalName,
862
- twentyType: typeForRule(rule),
863
- action: actionForRule(rule),
864
- value,
865
- source: "rule",
866
- targetObject: parseTargetObject(rule.targetObject),
867
- mergeStrategy: parseFieldMergeStrategy(rule.mergeStrategy)
868
- };
869
- }
870
- }
871
892
  const detectedType = detectType(value);
872
893
  const classification = classify(key, value);
873
894
  const prefix = OPPORTUNITY_KEY_PREFIXES.find((p) => lookupKey.startsWith(p));
@@ -982,7 +1003,7 @@ var NormalizeStage = class {
982
1003
  return err(ErrorCode.INTERNAL_ERROR, "FlattenStage must run before NormalizeStage");
983
1004
  }
984
1005
  const rules = await this.loadRules(ctx.source?.id);
985
- const normalized = normalizeFields(ctx.flat, rules);
1006
+ const normalized = normalizeFields(ctx.flat, rules, ctx.warnings);
986
1007
  const { crmFields, noteFields } = partition(normalized);
987
1008
  return ok({
988
1009
  ...ctx,
@@ -1446,6 +1467,183 @@ function formatValue(value) {
1446
1467
  return String(value);
1447
1468
  }
1448
1469
 
1470
+ // src/constants/free-email-domains.ts
1471
+ var FREE_EMAIL_DOMAINS = /* @__PURE__ */ new Set([
1472
+ // Google
1473
+ "gmail.com",
1474
+ "googlemail.com",
1475
+ // Microsoft
1476
+ "outlook.com",
1477
+ "outlook.co.uk",
1478
+ "outlook.fr",
1479
+ "outlook.de",
1480
+ "outlook.es",
1481
+ "hotmail.com",
1482
+ "hotmail.co.uk",
1483
+ "hotmail.fr",
1484
+ "hotmail.de",
1485
+ "hotmail.it",
1486
+ "hotmail.es",
1487
+ "live.com",
1488
+ "live.co.uk",
1489
+ "live.nl",
1490
+ "msn.com",
1491
+ "passport.com",
1492
+ // Yahoo
1493
+ "yahoo.com",
1494
+ "yahoo.co.uk",
1495
+ "yahoo.co.jp",
1496
+ "yahoo.ca",
1497
+ "yahoo.com.au",
1498
+ "yahoo.de",
1499
+ "yahoo.fr",
1500
+ "yahoo.es",
1501
+ "yahoo.it",
1502
+ "yahoo.in",
1503
+ "ymail.com",
1504
+ "rocketmail.com",
1505
+ // Apple
1506
+ "icloud.com",
1507
+ "me.com",
1508
+ "mac.com",
1509
+ // AOL / Verizon / legacy US ISPs — common on older consumer accounts
1510
+ "aol.com",
1511
+ "aim.com",
1512
+ "verizon.net",
1513
+ "att.net",
1514
+ "sbcglobal.net",
1515
+ "bellsouth.net",
1516
+ "comcast.net",
1517
+ "cox.net",
1518
+ "charter.net",
1519
+ "earthlink.net",
1520
+ "juno.com",
1521
+ "netzero.net",
1522
+ "roadrunner.com",
1523
+ "rr.com",
1524
+ "optonline.net",
1525
+ "frontier.com",
1526
+ "windstream.net",
1527
+ // Privacy-oriented providers
1528
+ "proton.me",
1529
+ "protonmail.com",
1530
+ "protonmail.ch",
1531
+ "pm.me",
1532
+ "tutanota.com",
1533
+ "tutanota.de",
1534
+ "tuta.io",
1535
+ "tuta.com",
1536
+ "hushmail.com",
1537
+ "fastmail.com",
1538
+ "fastmail.fm",
1539
+ "posteo.de",
1540
+ "mailbox.org",
1541
+ // Other large free providers
1542
+ "gmx.com",
1543
+ "gmx.net",
1544
+ "gmx.de",
1545
+ "gmx.at",
1546
+ "gmx.ch",
1547
+ "mail.com",
1548
+ "email.com",
1549
+ "usa.com",
1550
+ "inbox.com",
1551
+ "zoho.com",
1552
+ "zohomail.com",
1553
+ "yandex.com",
1554
+ "yandex.ru",
1555
+ "ya.ru",
1556
+ "mail.ru",
1557
+ "bk.ru",
1558
+ "list.ru",
1559
+ "inbox.ru",
1560
+ "rambler.ru",
1561
+ "qq.com",
1562
+ "foxmail.com",
1563
+ "163.com",
1564
+ "126.com",
1565
+ "sina.com",
1566
+ "sina.cn",
1567
+ "sohu.com",
1568
+ "naver.com",
1569
+ "hanmail.net",
1570
+ "daum.net",
1571
+ "nate.com",
1572
+ "seznam.cz",
1573
+ "wp.pl",
1574
+ "o2.pl",
1575
+ "onet.pl",
1576
+ "interia.pl",
1577
+ "libero.it",
1578
+ "virgilio.it",
1579
+ "alice.it",
1580
+ "tiscali.it",
1581
+ "orange.fr",
1582
+ "wanadoo.fr",
1583
+ "free.fr",
1584
+ "laposte.net",
1585
+ "sfr.fr",
1586
+ "bbox.fr",
1587
+ "neuf.fr",
1588
+ "web.de",
1589
+ "t-online.de",
1590
+ "freenet.de",
1591
+ "arcor.de",
1592
+ "btinternet.com",
1593
+ "sky.com",
1594
+ "talktalk.net",
1595
+ "virginmedia.com",
1596
+ "ntlworld.com",
1597
+ "bigpond.com",
1598
+ "bigpond.net.au",
1599
+ "optusnet.com.au",
1600
+ "iinet.net.au",
1601
+ "xtra.co.nz",
1602
+ "terra.com.br",
1603
+ "uol.com.br",
1604
+ "bol.com.br",
1605
+ "ig.com.br",
1606
+ "rediffmail.com",
1607
+ "sify.com",
1608
+ "indiatimes.com",
1609
+ // Throwaway / disposable services — never a real employer
1610
+ "mailinator.com",
1611
+ "guerrillamail.com",
1612
+ "guerrillamail.net",
1613
+ "sharklasers.com",
1614
+ "10minutemail.com",
1615
+ "temp-mail.org",
1616
+ "tempmail.com",
1617
+ "throwawaymail.com",
1618
+ "yopmail.com",
1619
+ "trashmail.com",
1620
+ "getnada.com",
1621
+ "dispostable.com",
1622
+ "maildrop.cc",
1623
+ "mintemail.com",
1624
+ "fakeinbox.com",
1625
+ "spamgourmet.com",
1626
+ "mailnesia.com",
1627
+ "moakt.com",
1628
+ "emailondeck.com",
1629
+ "burnermail.io",
1630
+ "anonaddy.com",
1631
+ "simplelogin.io",
1632
+ "duck.com",
1633
+ "relay.firefox.com",
1634
+ // Placeholder domains that show up in test and pasted payloads
1635
+ "example.com",
1636
+ "example.org",
1637
+ "example.net",
1638
+ "test.com",
1639
+ "localhost"
1640
+ ]);
1641
+ function isFreeEmailDomain(host) {
1642
+ const normalised = host.trim().toLowerCase().replace(/^www\./, "").replace(/\.$/, "");
1643
+ if (!normalised) return false;
1644
+ return FREE_EMAIL_DOMAINS.has(normalised);
1645
+ }
1646
+
1449
1647
  // src/lib/lookup.ts
1450
1648
  async function firstMatch(collection, query) {
1451
1649
  try {
@@ -1463,11 +1661,16 @@ async function findCompanyByDomain(domain) {
1463
1661
  if (!domain) return null;
1464
1662
  const host = extractHost(domain);
1465
1663
  if (!host) return null;
1664
+ if (isFreeEmailDomain(host)) return null;
1466
1665
  return firstMatch(
1467
1666
  "companies",
1468
1667
  `filter=domainName.primaryLinkUrl[like]:%25${encodeURIComponent(host)}%25`
1469
1668
  );
1470
1669
  }
1670
+ function isCompanyDomain(domain) {
1671
+ const host = extractHost(domain);
1672
+ return host.length > 0 && !isFreeEmailDomain(host);
1673
+ }
1471
1674
  function extractHost(url) {
1472
1675
  try {
1473
1676
  return new URL(url).hostname.replace(/^www\./, "");
@@ -1588,6 +1791,17 @@ var UpsertRecordStage = class {
1588
1791
  if (companyName && !standard["name"]) standard["name"] = companyName;
1589
1792
  if (!standard["domainName"] && assembled["domainName"]) standard["domainName"] = assembled["domainName"];
1590
1793
  if (!standard["address"] && assembled["address"]) standard["address"] = assembled["address"];
1794
+ const proposedDomain = typeof standard["domainName"] === "string" ? standard["domainName"] : standard["domainName"]?.["primaryLinkUrl"];
1795
+ if (typeof proposedDomain === "string" && proposedDomain && !isCompanyDomain(proposedDomain)) {
1796
+ delete standard["domainName"];
1797
+ ctx.warnings.push(
1798
+ `"${extractHost(proposedDomain)}" is an email provider, not a company domain \u2014 it was left off the company record.`
1799
+ );
1800
+ }
1801
+ if (!standard["name"] && !standard["domainName"]) {
1802
+ ctx.warnings.push("No company name or domain in this payload \u2014 no company was created.");
1803
+ return void 0;
1804
+ }
1591
1805
  const domain = standard["domainName"]?.["primaryLinkUrl"];
1592
1806
  let companyId;
1593
1807
  if (domain) {