twenty-app-intake 0.3.0 → 0.4.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.
- package/README.md +292 -7
- package/manifest.json +389 -15
- package/package.json +1 -1
- package/src/front-components/settings-panel.mjs +7 -7
- package/src/front-components/settings-panel.mjs.map +2 -2
- package/src/front-components/settings-panel.tsx +49 -0
- package/src/logic-functions/check-silence.mjs +263 -0
- package/src/logic-functions/check-silence.mjs.map +7 -0
- package/src/logic-functions/check-silence.ts +52 -0
- package/src/logic-functions/contract.mjs +507 -0
- package/src/logic-functions/contract.mjs.map +7 -0
- package/src/logic-functions/contract.ts +172 -0
- package/src/logic-functions/health.mjs +125 -5
- package/src/logic-functions/health.mjs.map +3 -3
- package/src/logic-functions/health.ts +41 -5
- package/src/logic-functions/quarantine-discard.mjs +380 -0
- package/src/logic-functions/quarantine-discard.mjs.map +7 -0
- package/src/logic-functions/quarantine-discard.ts +65 -0
- package/src/logic-functions/quarantine-list.mjs +211 -0
- package/src/logic-functions/quarantine-list.mjs.map +7 -0
- package/src/logic-functions/quarantine-list.ts +93 -0
- package/src/logic-functions/quarantine-release.mjs +1974 -0
- package/src/logic-functions/quarantine-release.mjs.map +7 -0
- package/src/logic-functions/quarantine-release.ts +94 -0
- package/src/logic-functions/register.mjs +34 -1
- package/src/logic-functions/register.mjs.map +2 -2
- package/src/logic-functions/replay-bulk.mjs +2040 -0
- package/src/logic-functions/replay-bulk.mjs.map +7 -0
- package/src/logic-functions/replay-bulk.ts +83 -0
- package/src/logic-functions/replay-log.mjs +1987 -0
- package/src/logic-functions/replay-log.mjs.map +7 -0
- package/src/logic-functions/replay-log.ts +54 -0
- package/src/logic-functions/retry.mjs +1001 -248
- package/src/logic-functions/retry.mjs.map +4 -4
- package/src/logic-functions/retry.ts +45 -68
- package/src/logic-functions/test-ingest.mjs +616 -28
- package/src/logic-functions/test-ingest.mjs.map +4 -4
- package/src/logic-functions/test-ingest.ts +132 -28
- package/src/logic-functions/webhook.mjs +965 -209
- package/src/logic-functions/webhook.mjs.map +4 -4
- package/src/logic-functions/webhook.ts +23 -48
- package/src/post-install.mjs +34 -1
- package/src/post-install.mjs.map +2 -2
|
@@ -21,6 +21,9 @@ var __anyHandler = {
|
|
|
21
21
|
var __anyStub = new Proxy(() => void 0, __anyHandler);
|
|
22
22
|
var defineLogicFunction = __defineFactoryStub;
|
|
23
23
|
|
|
24
|
+
// src/logic-functions/test-ingest.ts
|
|
25
|
+
import { CoreApiClient } from "twenty-client-sdk/core";
|
|
26
|
+
|
|
24
27
|
// src/lib/flattener.ts
|
|
25
28
|
var MAX_DEPTH = 8;
|
|
26
29
|
function flatten(input, prefix = "", depth = 0, seen = /* @__PURE__ */ new WeakSet()) {
|
|
@@ -277,7 +280,113 @@ function classify(key, value) {
|
|
|
277
280
|
return "field";
|
|
278
281
|
}
|
|
279
282
|
|
|
283
|
+
// src/lib/merge.ts
|
|
284
|
+
function decideMerge(existing, incoming, policy, strategy = "INHERIT") {
|
|
285
|
+
if (isEmpty(incoming)) return { write: false, status: "preserved" };
|
|
286
|
+
if (isEmpty(existing)) return { write: true, status: "filled-empty" };
|
|
287
|
+
if (sameValue(existing, incoming)) return { write: false, status: "unchanged" };
|
|
288
|
+
const effective = strategy === "INHERIT" ? policy : strategy;
|
|
289
|
+
return effective === "NEWEST_WINS" ? { write: true, status: "set" } : { write: false, status: "preserved" };
|
|
290
|
+
}
|
|
291
|
+
function mergePatch(existing, incoming, policy, strategyFor = () => "INHERIT") {
|
|
292
|
+
const patch = {};
|
|
293
|
+
const decisions = {};
|
|
294
|
+
for (const [key, value] of Object.entries(incoming)) {
|
|
295
|
+
const decision = decideMerge(existing[key], value, policy, strategyFor(key));
|
|
296
|
+
decisions[key] = decision;
|
|
297
|
+
if (decision.write) patch[key] = value;
|
|
298
|
+
}
|
|
299
|
+
return { patch, decisions };
|
|
300
|
+
}
|
|
301
|
+
function parseFieldMergeStrategy(raw) {
|
|
302
|
+
switch ((raw ?? "").toUpperCase()) {
|
|
303
|
+
case "PRESERVE":
|
|
304
|
+
return "PRESERVE";
|
|
305
|
+
case "NEWEST_WINS":
|
|
306
|
+
return "NEWEST_WINS";
|
|
307
|
+
default:
|
|
308
|
+
return "INHERIT";
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
function isEmpty(value) {
|
|
312
|
+
if (value === null || value === void 0) return true;
|
|
313
|
+
if (typeof value === "string") return value.trim() === "";
|
|
314
|
+
if (Array.isArray(value)) return value.length === 0;
|
|
315
|
+
if (typeof value === "object") {
|
|
316
|
+
const entries = Object.entries(value);
|
|
317
|
+
if (entries.length === 0) return true;
|
|
318
|
+
const meaningful = entries.filter(([k]) => !IGNORED_COMPOSITE_KEYS.has(k));
|
|
319
|
+
if (meaningful.length === 0) return true;
|
|
320
|
+
return meaningful.every(([, v]) => isEmpty(v));
|
|
321
|
+
}
|
|
322
|
+
return false;
|
|
323
|
+
}
|
|
324
|
+
var IGNORED_COMPOSITE_KEYS = /* @__PURE__ */ new Set([
|
|
325
|
+
"currencyCode",
|
|
326
|
+
"primaryLinkLabel",
|
|
327
|
+
"primaryPhoneCountryCode",
|
|
328
|
+
"primaryPhoneCallingCode",
|
|
329
|
+
"additionalLinks",
|
|
330
|
+
"additionalEmails",
|
|
331
|
+
"additionalPhones"
|
|
332
|
+
]);
|
|
333
|
+
function sameValue(a, b) {
|
|
334
|
+
if (a === b) return true;
|
|
335
|
+
if (typeof a === "object" && typeof b === "object" && a !== null && b !== null) {
|
|
336
|
+
try {
|
|
337
|
+
return JSON.stringify(sortKeys(a)) === JSON.stringify(sortKeys(b));
|
|
338
|
+
} catch {
|
|
339
|
+
return false;
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
return String(a) === String(b);
|
|
343
|
+
}
|
|
344
|
+
function sortKeys(value) {
|
|
345
|
+
if (value === null || typeof value !== "object") return value;
|
|
346
|
+
if (Array.isArray(value)) return value.map(sortKeys);
|
|
347
|
+
const obj = value;
|
|
348
|
+
const out = {};
|
|
349
|
+
for (const key of Object.keys(obj).sort()) out[key] = sortKeys(obj[key]);
|
|
350
|
+
return out;
|
|
351
|
+
}
|
|
352
|
+
|
|
280
353
|
// src/lib/normalizer.ts
|
|
354
|
+
function actionForRule(rule) {
|
|
355
|
+
if (rule.action) return rule.action;
|
|
356
|
+
switch ((rule.fieldType ?? "").toUpperCase()) {
|
|
357
|
+
case "SKIP":
|
|
358
|
+
return "skip";
|
|
359
|
+
case "NOTE":
|
|
360
|
+
return "note";
|
|
361
|
+
default:
|
|
362
|
+
return "field";
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
function typeForRule(rule) {
|
|
366
|
+
switch ((rule.fieldType ?? "").toUpperCase()) {
|
|
367
|
+
case "NOTE":
|
|
368
|
+
return "RICH_TEXT";
|
|
369
|
+
case "SKIP":
|
|
370
|
+
return "TEXT";
|
|
371
|
+
case "":
|
|
372
|
+
return "TEXT";
|
|
373
|
+
default:
|
|
374
|
+
return rule.fieldType;
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
var OPPORTUNITY_KEY_PREFIXES = ["opportunity_", "opp_", "deal_"];
|
|
378
|
+
function parseTargetObject(raw) {
|
|
379
|
+
switch ((raw ?? "").toUpperCase()) {
|
|
380
|
+
case "PERSON":
|
|
381
|
+
return "person";
|
|
382
|
+
case "COMPANY":
|
|
383
|
+
return "company";
|
|
384
|
+
case "OPPORTUNITY":
|
|
385
|
+
return "opportunity";
|
|
386
|
+
default:
|
|
387
|
+
return "auto";
|
|
388
|
+
}
|
|
389
|
+
}
|
|
281
390
|
function normalizeFields(flat, rules = []) {
|
|
282
391
|
const sorted = [...rules].sort((a, b) => b.priority - a.priority);
|
|
283
392
|
return Object.entries(flat).map(
|
|
@@ -294,7 +403,8 @@ function normalizeField(key, value, rules) {
|
|
|
294
403
|
twentyType: builtIn.twentyType,
|
|
295
404
|
action: builtIn.action,
|
|
296
405
|
value,
|
|
297
|
-
source: "builtin"
|
|
406
|
+
source: "builtin",
|
|
407
|
+
targetObject: "auto"
|
|
298
408
|
};
|
|
299
409
|
}
|
|
300
410
|
for (const rule of rules) {
|
|
@@ -308,23 +418,29 @@ function normalizeField(key, value, rules) {
|
|
|
308
418
|
return {
|
|
309
419
|
originalKey: key,
|
|
310
420
|
canonicalName: rule.canonicalName,
|
|
311
|
-
twentyType: rule
|
|
312
|
-
action: rule
|
|
421
|
+
twentyType: typeForRule(rule),
|
|
422
|
+
action: actionForRule(rule),
|
|
313
423
|
value,
|
|
314
|
-
source: "rule"
|
|
424
|
+
source: "rule",
|
|
425
|
+
targetObject: parseTargetObject(rule.targetObject),
|
|
426
|
+
mergeStrategy: parseFieldMergeStrategy(rule.mergeStrategy)
|
|
315
427
|
};
|
|
316
428
|
}
|
|
317
429
|
}
|
|
318
430
|
const detectedType = detectType(value);
|
|
319
431
|
const classification = classify(key, value);
|
|
320
|
-
const
|
|
432
|
+
const prefix = OPPORTUNITY_KEY_PREFIXES.find((p) => lookupKey.startsWith(p));
|
|
433
|
+
const targetObject = prefix ? "opportunity" : "auto";
|
|
434
|
+
const bareKey = prefix ? lookupKey.slice(prefix.length) : key;
|
|
435
|
+
const canonicalName = classification === "field" ? toExtFieldName(bareKey) : lookupKey;
|
|
321
436
|
return {
|
|
322
437
|
originalKey: key,
|
|
323
438
|
canonicalName,
|
|
324
439
|
twentyType: detectedType,
|
|
325
440
|
action: classification === "field" ? "field" : classification,
|
|
326
441
|
value,
|
|
327
|
-
source: "passthrough"
|
|
442
|
+
source: "passthrough",
|
|
443
|
+
targetObject
|
|
328
444
|
};
|
|
329
445
|
}
|
|
330
446
|
function toExtFieldName(key) {
|
|
@@ -488,54 +604,526 @@ var coreApi = {
|
|
|
488
604
|
post: (path, body) => request(`${apiUrl()}/rest`, "POST", path, body),
|
|
489
605
|
patch: (path, body) => request(`${apiUrl()}/rest`, "PATCH", path, body)
|
|
490
606
|
};
|
|
607
|
+
var metaApi = {
|
|
608
|
+
get: (path) => request(`${apiUrl()}/rest/metadata`, "GET", path),
|
|
609
|
+
post: (path, body) => request(`${apiUrl()}/rest/metadata`, "POST", path, body)
|
|
610
|
+
};
|
|
611
|
+
|
|
612
|
+
// src/lib/lookup.ts
|
|
613
|
+
async function firstMatch(collection, query) {
|
|
614
|
+
try {
|
|
615
|
+
const res = await coreApi.get(`/${collection}?${query}&first=1`);
|
|
616
|
+
return res.data?.[collection]?.edges?.[0]?.node ?? null;
|
|
617
|
+
} catch {
|
|
618
|
+
return null;
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
async function findPersonByEmail(email) {
|
|
622
|
+
if (!email) return null;
|
|
623
|
+
return firstMatch("people", `filter=emails.primaryEmail[eq]:${encodeURIComponent(email)}`);
|
|
624
|
+
}
|
|
625
|
+
async function findCompanyByDomain(domain) {
|
|
626
|
+
if (!domain) return null;
|
|
627
|
+
const host = extractHost(domain);
|
|
628
|
+
if (!host) return null;
|
|
629
|
+
return firstMatch(
|
|
630
|
+
"companies",
|
|
631
|
+
`filter=domainName.primaryLinkUrl[like]:%25${encodeURIComponent(host)}%25`
|
|
632
|
+
);
|
|
633
|
+
}
|
|
634
|
+
function extractHost(url) {
|
|
635
|
+
try {
|
|
636
|
+
return new URL(url).hostname.replace(/^www\./, "");
|
|
637
|
+
} catch {
|
|
638
|
+
return url;
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
function primaryEmailOf(assembled) {
|
|
642
|
+
const emails = assembled["emails"];
|
|
643
|
+
const value = emails?.["primaryEmail"];
|
|
644
|
+
return typeof value === "string" ? value : "";
|
|
645
|
+
}
|
|
646
|
+
function primaryDomainOf(standard) {
|
|
647
|
+
const domainName = standard["domainName"];
|
|
648
|
+
const value = domainName?.["primaryLinkUrl"];
|
|
649
|
+
return typeof value === "string" ? value : "";
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
// src/lib/diff.ts
|
|
653
|
+
var STANDARD_BY_OBJECT = {
|
|
654
|
+
person: /* @__PURE__ */ new Set([
|
|
655
|
+
"name",
|
|
656
|
+
"emails",
|
|
657
|
+
"phones",
|
|
658
|
+
"firstName",
|
|
659
|
+
"lastName",
|
|
660
|
+
"fullName",
|
|
661
|
+
"email",
|
|
662
|
+
"phone",
|
|
663
|
+
"linkedInLink",
|
|
664
|
+
"xLink",
|
|
665
|
+
"jobTitle",
|
|
666
|
+
"city",
|
|
667
|
+
"companyName",
|
|
668
|
+
"domainName",
|
|
669
|
+
"address"
|
|
670
|
+
]),
|
|
671
|
+
company: /* @__PURE__ */ new Set([
|
|
672
|
+
"name",
|
|
673
|
+
"domainName",
|
|
674
|
+
"address",
|
|
675
|
+
"companyName",
|
|
676
|
+
"linkedInLink",
|
|
677
|
+
"xLink",
|
|
678
|
+
"employees",
|
|
679
|
+
"annualRecurringRevenue",
|
|
680
|
+
"idealCustomerProfile",
|
|
681
|
+
"city"
|
|
682
|
+
]),
|
|
683
|
+
opportunity: /* @__PURE__ */ new Set(["name", "stage", "amount", "closeDate"])
|
|
684
|
+
};
|
|
685
|
+
async function buildDiff(input) {
|
|
686
|
+
const clean = input.crmFields.filter((f) => !f.originalKey.startsWith("__"));
|
|
687
|
+
const { assembled, remainder } = assembleComposites(clean);
|
|
688
|
+
for (const f of remainder) assembled[f.canonicalName] = f.value;
|
|
689
|
+
const objects = [];
|
|
690
|
+
const existingFields = await objectFieldNames();
|
|
691
|
+
const strategyFor = (field) => clean.find((f) => f.canonicalName === field)?.mergeStrategy ?? "INHERIT";
|
|
692
|
+
const companyName = input.structuredCompany?.["name"] ?? assembled["companyName"];
|
|
693
|
+
if (companyName || input.targetObject === "COMPANY") {
|
|
694
|
+
const standard = { ...input.structuredCompany ?? {} };
|
|
695
|
+
if (companyName && !standard["name"]) standard["name"] = companyName;
|
|
696
|
+
if (!standard["domainName"] && assembled["domainName"]) standard["domainName"] = assembled["domainName"];
|
|
697
|
+
if (!standard["address"] && assembled["address"]) standard["address"] = assembled["address"];
|
|
698
|
+
const domain = primaryDomainOf(standard);
|
|
699
|
+
const existing = domain ? await findCompanyByDomain(domain) : null;
|
|
700
|
+
objects.push(describe(
|
|
701
|
+
"company",
|
|
702
|
+
existing,
|
|
703
|
+
domain ? { field: "domainName", value: domain } : null,
|
|
704
|
+
{ ...standard, ...customFor(clean, "company") },
|
|
705
|
+
clean,
|
|
706
|
+
existingFields["company"] ?? /* @__PURE__ */ new Set(),
|
|
707
|
+
input.policy,
|
|
708
|
+
strategyFor
|
|
709
|
+
));
|
|
710
|
+
}
|
|
711
|
+
const hasPersonData = assembled["emails"] || assembled["name"] || input.structuredPerson;
|
|
712
|
+
if (hasPersonData || input.targetObject === "PERSON") {
|
|
713
|
+
const standard = { ...input.structuredPerson ?? {} };
|
|
714
|
+
if (!standard["name"] && assembled["name"]) standard["name"] = assembled["name"];
|
|
715
|
+
if (!standard["emails"] && assembled["emails"]) standard["emails"] = assembled["emails"];
|
|
716
|
+
if (!standard["phones"] && assembled["phones"]) standard["phones"] = assembled["phones"];
|
|
717
|
+
const email = primaryEmailOf(standard) || primaryEmailOf(assembled);
|
|
718
|
+
const existing = email ? await findPersonByEmail(email) : null;
|
|
719
|
+
objects.push(describe(
|
|
720
|
+
"person",
|
|
721
|
+
existing,
|
|
722
|
+
email ? { field: "emails.primaryEmail", value: email } : null,
|
|
723
|
+
{ ...standard, ...customFor(clean, "person") },
|
|
724
|
+
clean,
|
|
725
|
+
existingFields["person"] ?? /* @__PURE__ */ new Set(),
|
|
726
|
+
input.policy,
|
|
727
|
+
strategyFor
|
|
728
|
+
));
|
|
729
|
+
}
|
|
730
|
+
if (input.createOpportunity && objects.some((o) => o.operation !== "skip")) {
|
|
731
|
+
const oppCustom = customFor(clean, "opportunity");
|
|
732
|
+
const oppStandard = {};
|
|
733
|
+
for (const f of clean) {
|
|
734
|
+
if (f.targetObject === "opportunity" && (f.canonicalName === "amount" || f.canonicalName === "closeDate")) {
|
|
735
|
+
oppStandard[f.canonicalName] = f.value;
|
|
736
|
+
}
|
|
737
|
+
}
|
|
738
|
+
objects.push(describe(
|
|
739
|
+
"opportunity",
|
|
740
|
+
null,
|
|
741
|
+
null,
|
|
742
|
+
{ ...oppStandard, ...oppCustom },
|
|
743
|
+
clean,
|
|
744
|
+
existingFields["opportunity"] ?? /* @__PURE__ */ new Set(),
|
|
745
|
+
input.policy,
|
|
746
|
+
strategyFor
|
|
747
|
+
));
|
|
748
|
+
}
|
|
749
|
+
return {
|
|
750
|
+
objects,
|
|
751
|
+
noteFields: input.noteFields.map((f) => f.canonicalName),
|
|
752
|
+
skipped: input.skipped.map((f) => f.originalKey),
|
|
753
|
+
warnings: input.warnings
|
|
754
|
+
};
|
|
755
|
+
}
|
|
756
|
+
function customFor(fields, object) {
|
|
757
|
+
const out = {};
|
|
758
|
+
for (const f of fields) {
|
|
759
|
+
const target = f.targetObject === "auto" ? impliedPrimary(fields) : f.targetObject;
|
|
760
|
+
if (target !== object) continue;
|
|
761
|
+
if (STANDARD_BY_OBJECT[object]?.has(f.canonicalName)) continue;
|
|
762
|
+
out[f.canonicalName] = f.value;
|
|
763
|
+
}
|
|
764
|
+
return out;
|
|
765
|
+
}
|
|
766
|
+
function impliedPrimary(fields) {
|
|
767
|
+
const hasPerson = fields.some((f) => ["firstName", "lastName", "email", "phone", "fullName", "emails", "phones", "name"].includes(f.canonicalName));
|
|
768
|
+
return hasPerson ? "person" : "company";
|
|
769
|
+
}
|
|
770
|
+
function describe(object, existing, matchedBy, incoming, normalized, objectFields, policy, strategyFor) {
|
|
771
|
+
const { decisions } = existing ? mergePatch(existing, incoming, policy, strategyFor) : { decisions: {} };
|
|
772
|
+
const fields = [];
|
|
773
|
+
const fieldsToCreate = [];
|
|
774
|
+
for (const [name, value] of Object.entries(incoming)) {
|
|
775
|
+
const source = normalized.find((f) => f.canonicalName === name);
|
|
776
|
+
const isStandard = STANDARD_BY_OBJECT[object]?.has(name) ?? false;
|
|
777
|
+
const wouldCreate = !isStandard && !objectFields.has(name);
|
|
778
|
+
if (wouldCreate) fieldsToCreate.push({ name, type: source?.twentyType ?? "TEXT" });
|
|
779
|
+
let status = "create";
|
|
780
|
+
if (existing) {
|
|
781
|
+
const decided = decisions[name]?.status;
|
|
782
|
+
status = decided === "filled-empty" ? "fill-empty" : decided === "unchanged" ? "unchanged" : decided === "set" ? "overwrite" : "preserved";
|
|
783
|
+
}
|
|
784
|
+
fields.push({
|
|
785
|
+
name,
|
|
786
|
+
from: source?.source ?? "derived",
|
|
787
|
+
incoming: value,
|
|
788
|
+
...existing ? { existing: existing[name] } : {},
|
|
789
|
+
status,
|
|
790
|
+
fieldWouldBeCreated: wouldCreate,
|
|
791
|
+
twentyType: source?.twentyType
|
|
792
|
+
});
|
|
793
|
+
}
|
|
794
|
+
return {
|
|
795
|
+
object,
|
|
796
|
+
operation: existing ? "update" : "create",
|
|
797
|
+
recordId: existing?.["id"] ?? null,
|
|
798
|
+
matchedBy,
|
|
799
|
+
fields,
|
|
800
|
+
fieldsToCreate
|
|
801
|
+
};
|
|
802
|
+
}
|
|
803
|
+
async function objectFieldNames() {
|
|
804
|
+
try {
|
|
805
|
+
const res = await metaApi.get("/objects");
|
|
806
|
+
const list = Array.isArray(res) ? res : res.data ?? [];
|
|
807
|
+
const out = {};
|
|
808
|
+
for (const node of list) {
|
|
809
|
+
out[node.nameSingular] = new Set((node.fields ?? []).map((f) => f.name));
|
|
810
|
+
}
|
|
811
|
+
return out;
|
|
812
|
+
} catch {
|
|
813
|
+
return {};
|
|
814
|
+
}
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
// src/lib/spam.ts
|
|
818
|
+
var HONEYPOT_SCORE = 100;
|
|
819
|
+
var DISPOSABLE_EMAIL_DOMAINS = /* @__PURE__ */ new Set([
|
|
820
|
+
"mailinator.com",
|
|
821
|
+
"guerrillamail.com",
|
|
822
|
+
"guerrillamail.net",
|
|
823
|
+
"sharklasers.com",
|
|
824
|
+
"10minutemail.com",
|
|
825
|
+
"10minutemail.net",
|
|
826
|
+
"tempmail.com",
|
|
827
|
+
"temp-mail.org",
|
|
828
|
+
"throwawaymail.com",
|
|
829
|
+
"yopmail.com",
|
|
830
|
+
"yopmail.net",
|
|
831
|
+
"trashmail.com",
|
|
832
|
+
"dispostable.com",
|
|
833
|
+
"getnada.com",
|
|
834
|
+
"nada.email",
|
|
835
|
+
"fakeinbox.com",
|
|
836
|
+
"maildrop.cc",
|
|
837
|
+
"mintemail.com",
|
|
838
|
+
"spamgourmet.com",
|
|
839
|
+
"mailnesia.com",
|
|
840
|
+
"tempinbox.com",
|
|
841
|
+
"emailondeck.com",
|
|
842
|
+
"moakt.com",
|
|
843
|
+
"mohmal.com",
|
|
844
|
+
"inboxkitten.com",
|
|
845
|
+
"mailcatch.com",
|
|
846
|
+
"mytemp.email",
|
|
847
|
+
"discard.email"
|
|
848
|
+
]);
|
|
849
|
+
var PLACEHOLDER_EMAIL_DOMAINS = /* @__PURE__ */ new Set([
|
|
850
|
+
"example.com",
|
|
851
|
+
"example.org",
|
|
852
|
+
"example.net",
|
|
853
|
+
"test.com",
|
|
854
|
+
"localhost",
|
|
855
|
+
"domain.com",
|
|
856
|
+
"email.com",
|
|
857
|
+
"yourdomain.com"
|
|
858
|
+
]);
|
|
859
|
+
var URL_RE2 = /https?:\/\/|www\./i;
|
|
860
|
+
var GLOBAL_URL_RE = /https?:\/\/[^\s<>"']+/gi;
|
|
861
|
+
var MARKUP_RE = /<\s*a\s|<\s*script|\[url[=\]]|\[link[=\]]|\]\(https?:/i;
|
|
862
|
+
var EMAIL_RE2 = /^[^\s@]+@[^\s@]+\.[^\s@]{2,}$/;
|
|
863
|
+
var NAME_KEYS = /* @__PURE__ */ new Set([
|
|
864
|
+
"name",
|
|
865
|
+
"fullName",
|
|
866
|
+
"firstName",
|
|
867
|
+
"lastName",
|
|
868
|
+
"contact_name",
|
|
869
|
+
"your_name"
|
|
870
|
+
]);
|
|
871
|
+
var MESSAGE_KEYS = /* @__PURE__ */ new Set([
|
|
872
|
+
"message",
|
|
873
|
+
"description",
|
|
874
|
+
"comments",
|
|
875
|
+
"notes",
|
|
876
|
+
"body",
|
|
877
|
+
"details",
|
|
878
|
+
"content",
|
|
879
|
+
"text",
|
|
880
|
+
"inquiry",
|
|
881
|
+
"summary",
|
|
882
|
+
"feedback"
|
|
883
|
+
]);
|
|
884
|
+
function scoreSpam(flat, honeypotKey) {
|
|
885
|
+
const signals = [];
|
|
886
|
+
if (honeypotKey) {
|
|
887
|
+
const hit = Object.entries(flat).find(
|
|
888
|
+
([k, v]) => k.toLowerCase() === honeypotKey.toLowerCase() && isFilled(v)
|
|
889
|
+
);
|
|
890
|
+
if (hit) {
|
|
891
|
+
return {
|
|
892
|
+
score: HONEYPOT_SCORE,
|
|
893
|
+
signals: [{ reason: `Honeypot field "${hit[0]}" was filled`, points: HONEYPOT_SCORE }],
|
|
894
|
+
honeypot: true
|
|
895
|
+
};
|
|
896
|
+
}
|
|
897
|
+
}
|
|
898
|
+
const add = (reason, points) => signals.push({ reason, points });
|
|
899
|
+
for (const [key, value] of Object.entries(flat)) {
|
|
900
|
+
if (typeof value !== "string" || value === "") continue;
|
|
901
|
+
const bare = key.replace(/^(opportunity_|opp_|deal_)/i, "");
|
|
902
|
+
if (NAME_KEYS.has(bare) || NAME_KEYS.has(key)) {
|
|
903
|
+
if (URL_RE2.test(value)) add(`Name field "${key}" contains a URL`, 4);
|
|
904
|
+
if (MARKUP_RE.test(value)) add(`Name field "${key}" contains markup or a link tag`, 4);
|
|
905
|
+
}
|
|
906
|
+
if (MESSAGE_KEYS.has(bare) || MESSAGE_KEYS.has(key)) {
|
|
907
|
+
const urls = value.match(GLOBAL_URL_RE) ?? [];
|
|
908
|
+
const distinct = new Set(urls.map((u) => u.toLowerCase()));
|
|
909
|
+
if (distinct.size >= 5) add(`Message field "${key}" contains ${distinct.size} links`, 3);
|
|
910
|
+
if (MARKUP_RE.test(value)) add(`Message field "${key}" contains anchor or BBCode markup`, 2);
|
|
911
|
+
}
|
|
912
|
+
}
|
|
913
|
+
const email = firstEmail(flat);
|
|
914
|
+
if (email !== null) {
|
|
915
|
+
if (!EMAIL_RE2.test(email)) {
|
|
916
|
+
add(`Email "${truncate(email)}" is not a valid address`, 3);
|
|
917
|
+
} else {
|
|
918
|
+
const domain = email.slice(email.lastIndexOf("@") + 1).toLowerCase();
|
|
919
|
+
if (DISPOSABLE_EMAIL_DOMAINS.has(domain)) {
|
|
920
|
+
add(`Email domain "${domain}" is a disposable inbox`, 4);
|
|
921
|
+
} else if (PLACEHOLDER_EMAIL_DOMAINS.has(domain)) {
|
|
922
|
+
add(`Email domain "${domain}" is a placeholder domain`, 3);
|
|
923
|
+
}
|
|
924
|
+
}
|
|
925
|
+
}
|
|
926
|
+
const repeated = mostRepeatedValue(flat);
|
|
927
|
+
if (repeated && repeated.count >= 4) {
|
|
928
|
+
add(`The same value appears in ${repeated.count} different fields`, 2);
|
|
929
|
+
}
|
|
930
|
+
const phone = firstPhone(flat);
|
|
931
|
+
if (phone && isImplausiblePhone(phone)) {
|
|
932
|
+
add(`Phone "${truncate(phone)}" is a placeholder number`, 2);
|
|
933
|
+
}
|
|
934
|
+
const score = signals.reduce((sum, s) => sum + s.points, 0);
|
|
935
|
+
return { score, signals, honeypot: false };
|
|
936
|
+
}
|
|
937
|
+
function describeVerdict(verdict) {
|
|
938
|
+
return verdict.signals.map((s) => s.reason).join("; ").slice(0, 500);
|
|
939
|
+
}
|
|
940
|
+
function isFilled(value) {
|
|
941
|
+
if (value === null || value === void 0) return false;
|
|
942
|
+
if (typeof value === "string") return value.trim() !== "";
|
|
943
|
+
if (typeof value === "boolean") return value;
|
|
944
|
+
return true;
|
|
945
|
+
}
|
|
946
|
+
function firstEmail(flat) {
|
|
947
|
+
for (const [key, value] of Object.entries(flat)) {
|
|
948
|
+
if (typeof value !== "string" || value === "") continue;
|
|
949
|
+
if (/mail/i.test(key)) return value.trim();
|
|
950
|
+
}
|
|
951
|
+
return null;
|
|
952
|
+
}
|
|
953
|
+
function firstPhone(flat) {
|
|
954
|
+
for (const [key, value] of Object.entries(flat)) {
|
|
955
|
+
if (typeof value !== "string" || value === "") continue;
|
|
956
|
+
if (/phone|mobile|tel|cell/i.test(key)) return value.trim();
|
|
957
|
+
}
|
|
958
|
+
return null;
|
|
959
|
+
}
|
|
960
|
+
function isImplausiblePhone(raw) {
|
|
961
|
+
const digits = raw.replace(/\D/g, "");
|
|
962
|
+
if (digits.length < 7) return false;
|
|
963
|
+
if (/^(\d)\1+$/.test(digits)) return true;
|
|
964
|
+
const ascending = digits.split("").every((d, i, arr) => i === 0 || Number(d) === (Number(arr[i - 1]) + 1) % 10);
|
|
965
|
+
return ascending;
|
|
966
|
+
}
|
|
967
|
+
function mostRepeatedValue(flat) {
|
|
968
|
+
const counts = /* @__PURE__ */ new Map();
|
|
969
|
+
for (const value of Object.values(flat)) {
|
|
970
|
+
if (typeof value !== "string") continue;
|
|
971
|
+
const v = value.trim();
|
|
972
|
+
if (v.length < 8) continue;
|
|
973
|
+
counts.set(v, (counts.get(v) ?? 0) + 1);
|
|
974
|
+
}
|
|
975
|
+
let best = null;
|
|
976
|
+
for (const [value, count] of counts) {
|
|
977
|
+
if (!best || count > best.count) best = { value, count };
|
|
978
|
+
}
|
|
979
|
+
return best;
|
|
980
|
+
}
|
|
981
|
+
function truncate(s) {
|
|
982
|
+
return s.length > 60 ? `${s.slice(0, 60)}\u2026` : s;
|
|
983
|
+
}
|
|
984
|
+
|
|
985
|
+
// src/lib/settings.ts
|
|
986
|
+
function str(key, fallback) {
|
|
987
|
+
const v = process.env[key];
|
|
988
|
+
return v === void 0 || v === "" ? fallback : v;
|
|
989
|
+
}
|
|
990
|
+
function int(key, fallback) {
|
|
991
|
+
const parsed = parseInt(process.env[key] ?? "", 10);
|
|
992
|
+
return Number.isFinite(parsed) ? parsed : fallback;
|
|
993
|
+
}
|
|
994
|
+
function bool(key, fallback) {
|
|
995
|
+
const v = process.env[key];
|
|
996
|
+
if (v === void 0 || v === "") return fallback;
|
|
997
|
+
return v.toLowerCase() === "true";
|
|
998
|
+
}
|
|
999
|
+
function mergePolicy() {
|
|
1000
|
+
return str("INTAKE_MERGE_POLICY", "PRESERVE").toUpperCase() === "NEWEST_WINS" ? "NEWEST_WINS" : "PRESERVE";
|
|
1001
|
+
}
|
|
1002
|
+
function spamFilterEnabled() {
|
|
1003
|
+
return bool("INTAKE_SPAM_FILTER_ENABLED", false);
|
|
1004
|
+
}
|
|
1005
|
+
function spamScoreThreshold() {
|
|
1006
|
+
return int("INTAKE_SPAM_SCORE_THRESHOLD", 5);
|
|
1007
|
+
}
|
|
1008
|
+
function honeypotField() {
|
|
1009
|
+
return str("INTAKE_HONEYPOT_FIELD", "");
|
|
1010
|
+
}
|
|
491
1011
|
|
|
492
1012
|
// src/logic-functions/test-ingest.ts
|
|
493
1013
|
var handler = async (params) => {
|
|
494
1014
|
const sourceSlug = params.pathParameters?.["slug"];
|
|
495
|
-
if (!sourceSlug) {
|
|
496
|
-
return { statusCode: 400, body: { error: "Missing source slug" } };
|
|
497
|
-
}
|
|
1015
|
+
if (!sourceSlug) return { statusCode: 400, body: { error: "Missing source slug" } };
|
|
498
1016
|
let rawPayload = {};
|
|
499
1017
|
if (params.body && typeof params.body === "object" && !Array.isArray(params.body)) {
|
|
500
1018
|
rawPayload = params.body;
|
|
501
1019
|
}
|
|
502
|
-
const
|
|
503
|
-
|
|
504
|
-
).catch(() => null);
|
|
505
|
-
const source = srcResult?.data?.intakeSources?.edges?.[0]?.node;
|
|
1020
|
+
const source = await loadSource(sourceSlug);
|
|
1021
|
+
const rules = await loadRules(source?.id);
|
|
506
1022
|
const structure = detectStructure(rawPayload);
|
|
507
|
-
const flat = structure.type === "flat" ? structure.data : flatten(structure.
|
|
508
|
-
const normalized = normalizeFields(flat);
|
|
1023
|
+
const flat = structure.type === "flat" ? structure.data : flatten(structure.extra ?? {});
|
|
1024
|
+
const normalized = normalizeFields(flat, rules);
|
|
509
1025
|
const { crmFields, noteFields, skipped } = partition(normalized);
|
|
510
|
-
const
|
|
511
|
-
|
|
512
|
-
const
|
|
513
|
-
const
|
|
1026
|
+
const honeypot = source?.honeypotField || honeypotField();
|
|
1027
|
+
const verdict = scoreSpam(flat, honeypot);
|
|
1028
|
+
const wouldQuarantine = verdict.honeypot || spamFilterEnabled() && verdict.score >= spamScoreThreshold();
|
|
1029
|
+
const warnings = [];
|
|
1030
|
+
if (!source) warnings.push(`No source registered with slug "${sourceSlug}" \u2014 previewing with default settings. A live request would return 404.`);
|
|
1031
|
+
if (source?.status === "PAUSED") warnings.push("This source is PAUSED \u2014 a live request would return 423.");
|
|
514
1032
|
const noteOverflow = {};
|
|
515
1033
|
for (const f of noteFields) noteOverflow[f.canonicalName] = f.value;
|
|
1034
|
+
const diff = wouldQuarantine ? null : await buildDiff({
|
|
1035
|
+
crmFields,
|
|
1036
|
+
noteFields,
|
|
1037
|
+
skipped,
|
|
1038
|
+
structuredCompany: structure.type === "structured" ? structure.company ?? void 0 : void 0,
|
|
1039
|
+
structuredPerson: structure.type === "structured" ? structure.person ?? void 0 : void 0,
|
|
1040
|
+
createOpportunity: source?.createOpportunity ?? true,
|
|
1041
|
+
targetObject: source?.targetObject ?? "AUTO",
|
|
1042
|
+
policy: parsePolicy(source?.mergePolicy) ?? mergePolicy(),
|
|
1043
|
+
warnings
|
|
1044
|
+
});
|
|
516
1045
|
return {
|
|
517
1046
|
statusCode: 200,
|
|
518
1047
|
body: {
|
|
519
1048
|
dryRun: true,
|
|
520
|
-
source: source ? { id: source.id, name: source.name } : null,
|
|
1049
|
+
source: source ? { id: source.id, name: source.name, slug: source.slug, status: source.status } : null,
|
|
521
1050
|
payloadHash: computePayloadHash(rawPayload),
|
|
522
1051
|
payloadStructure: structure.type,
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
1052
|
+
spam: {
|
|
1053
|
+
score: verdict.score,
|
|
1054
|
+
threshold: spamScoreThreshold(),
|
|
1055
|
+
filterEnabled: spamFilterEnabled(),
|
|
1056
|
+
honeypotField: honeypot || null,
|
|
1057
|
+
wouldQuarantine,
|
|
1058
|
+
signals: verdict.signals,
|
|
1059
|
+
reason: verdict.signals.length > 0 ? describeVerdict(verdict) : null
|
|
528
1060
|
},
|
|
529
|
-
|
|
1061
|
+
mergePolicy: parsePolicy(source?.mergePolicy) ?? mergePolicy(),
|
|
1062
|
+
/**
|
|
1063
|
+
* Per object: whether it would be created or updated, which record it
|
|
1064
|
+
* matched, what each field would do to the value already there, and which
|
|
1065
|
+
* fields do not exist yet and would be added to the schema.
|
|
1066
|
+
*/
|
|
1067
|
+
diff,
|
|
1068
|
+
note: noteFields.length > 0 ? { fields: noteFields.map((f) => f.canonicalName), preview: formatNote(source?.name ?? sourceSlug, noteOverflow) } : null,
|
|
1069
|
+
rulesApplied: rules.length,
|
|
1070
|
+
warnings,
|
|
530
1071
|
rawFlat: flat
|
|
531
1072
|
}
|
|
532
1073
|
};
|
|
533
1074
|
};
|
|
1075
|
+
async function loadSource(slug) {
|
|
1076
|
+
try {
|
|
1077
|
+
const res = await coreApi.get(
|
|
1078
|
+
`/intakeSources?filter=slug[eq]:${encodeURIComponent(slug)}&first=1`
|
|
1079
|
+
);
|
|
1080
|
+
return res.data?.intakeSources?.edges?.[0]?.node ?? null;
|
|
1081
|
+
} catch {
|
|
1082
|
+
return null;
|
|
1083
|
+
}
|
|
1084
|
+
}
|
|
1085
|
+
async function loadRules(sourceId) {
|
|
1086
|
+
try {
|
|
1087
|
+
const client = new CoreApiClient();
|
|
1088
|
+
const filter = sourceId ? { or: [{ intakeSource: { id: { eq: sourceId } } }, { intakeSource: { id: { is: "NULL" } } }] } : { intakeSource: { id: { is: "NULL" } } };
|
|
1089
|
+
const result = await client.query({
|
|
1090
|
+
intakeFieldRules: {
|
|
1091
|
+
__args: {
|
|
1092
|
+
filter: { and: [filter, { isActive: { eq: true } }] },
|
|
1093
|
+
orderBy: { priority: "DescNullsLast" }
|
|
1094
|
+
},
|
|
1095
|
+
edges: {
|
|
1096
|
+
node: {
|
|
1097
|
+
inputPattern: true,
|
|
1098
|
+
canonicalName: true,
|
|
1099
|
+
fieldType: true,
|
|
1100
|
+
priority: true,
|
|
1101
|
+
targetObject: true,
|
|
1102
|
+
mergeStrategy: true
|
|
1103
|
+
}
|
|
1104
|
+
}
|
|
1105
|
+
}
|
|
1106
|
+
});
|
|
1107
|
+
return (result.intakeFieldRules?.edges ?? []).map((e) => e.node);
|
|
1108
|
+
} catch {
|
|
1109
|
+
return [];
|
|
1110
|
+
}
|
|
1111
|
+
}
|
|
1112
|
+
function parsePolicy(raw) {
|
|
1113
|
+
switch ((raw ?? "").toUpperCase()) {
|
|
1114
|
+
case "PRESERVE":
|
|
1115
|
+
return "PRESERVE";
|
|
1116
|
+
case "NEWEST_WINS":
|
|
1117
|
+
return "NEWEST_WINS";
|
|
1118
|
+
default:
|
|
1119
|
+
return null;
|
|
1120
|
+
}
|
|
1121
|
+
}
|
|
534
1122
|
var test_ingest_default = defineLogicFunction({
|
|
535
1123
|
universalIdentifier: "3f7a9c21-8d4b-4e6f-a1c2-5b8d0f2e7a4c",
|
|
536
1124
|
name: "intake-test",
|
|
537
|
-
description: "Dry-run:
|
|
538
|
-
timeoutSeconds:
|
|
1125
|
+
description: "Dry-run: returns a structured diff of what would be created or updated, writing nothing.",
|
|
1126
|
+
timeoutSeconds: 20,
|
|
539
1127
|
handler,
|
|
540
1128
|
httpRouteTriggerSettings: {
|
|
541
1129
|
path: "/intake/:slug/test",
|