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.
Files changed (43) hide show
  1. package/README.md +292 -7
  2. package/manifest.json +389 -15
  3. package/package.json +1 -1
  4. package/src/front-components/settings-panel.mjs +7 -7
  5. package/src/front-components/settings-panel.mjs.map +2 -2
  6. package/src/front-components/settings-panel.tsx +49 -0
  7. package/src/logic-functions/check-silence.mjs +263 -0
  8. package/src/logic-functions/check-silence.mjs.map +7 -0
  9. package/src/logic-functions/check-silence.ts +52 -0
  10. package/src/logic-functions/contract.mjs +507 -0
  11. package/src/logic-functions/contract.mjs.map +7 -0
  12. package/src/logic-functions/contract.ts +172 -0
  13. package/src/logic-functions/health.mjs +125 -5
  14. package/src/logic-functions/health.mjs.map +3 -3
  15. package/src/logic-functions/health.ts +41 -5
  16. package/src/logic-functions/quarantine-discard.mjs +380 -0
  17. package/src/logic-functions/quarantine-discard.mjs.map +7 -0
  18. package/src/logic-functions/quarantine-discard.ts +65 -0
  19. package/src/logic-functions/quarantine-list.mjs +211 -0
  20. package/src/logic-functions/quarantine-list.mjs.map +7 -0
  21. package/src/logic-functions/quarantine-list.ts +93 -0
  22. package/src/logic-functions/quarantine-release.mjs +1974 -0
  23. package/src/logic-functions/quarantine-release.mjs.map +7 -0
  24. package/src/logic-functions/quarantine-release.ts +94 -0
  25. package/src/logic-functions/register.mjs +34 -1
  26. package/src/logic-functions/register.mjs.map +2 -2
  27. package/src/logic-functions/replay-bulk.mjs +2040 -0
  28. package/src/logic-functions/replay-bulk.mjs.map +7 -0
  29. package/src/logic-functions/replay-bulk.ts +83 -0
  30. package/src/logic-functions/replay-log.mjs +1987 -0
  31. package/src/logic-functions/replay-log.mjs.map +7 -0
  32. package/src/logic-functions/replay-log.ts +54 -0
  33. package/src/logic-functions/retry.mjs +1001 -248
  34. package/src/logic-functions/retry.mjs.map +4 -4
  35. package/src/logic-functions/retry.ts +45 -68
  36. package/src/logic-functions/test-ingest.mjs +616 -28
  37. package/src/logic-functions/test-ingest.mjs.map +4 -4
  38. package/src/logic-functions/test-ingest.ts +132 -28
  39. package/src/logic-functions/webhook.mjs +965 -209
  40. package/src/logic-functions/webhook.mjs.map +4 -4
  41. package/src/logic-functions/webhook.ts +23 -48
  42. package/src/post-install.mjs +34 -1
  43. package/src/post-install.mjs.map +2 -2
@@ -44,6 +44,55 @@ const SETTINGS = [
44
44
  description:
45
45
  'When true, every Intake Source must have a webhook secret and all requests must be signed with HMAC-SHA256. Unsigned or incorrectly signed requests are rejected with 401 regardless of source config.',
46
46
  },
47
+ {
48
+ key: 'INTAKE_MERGE_POLICY',
49
+ label: 'Merge Policy',
50
+ defaultValue: 'PRESERVE',
51
+ description:
52
+ 'What an update does to a field that already holds a value. PRESERVE fills blanks and leaves everything else alone, so a returning enquiry cannot overwrite a name someone corrected by hand. NEWEST_WINS lets the payload overwrite, which is how versions before 0.4.0 behaved. Under either setting, a blank value never overwrites.',
53
+ },
54
+ {
55
+ key: 'INTAKE_SPAM_FILTER_ENABLED',
56
+ label: 'Spam Filtering',
57
+ defaultValue: 'false',
58
+ description:
59
+ 'Score inbound payloads and hold anything at or above the threshold for review, before any record is created. Off by default. Watch the Spam Score on a few days of logs before switching it on. Honeypot fields work whether or not this is enabled.',
60
+ },
61
+ {
62
+ key: 'INTAKE_SPAM_SCORE_THRESHOLD',
63
+ label: 'Spam Score Threshold',
64
+ defaultValue: '5',
65
+ description:
66
+ 'The score at which a payload is quarantined. Individual signals are worth 2 to 4 points each, so the default requires two or more to agree. Lower is stricter.',
67
+ },
68
+ {
69
+ key: 'INTAKE_HONEYPOT_FIELD',
70
+ label: 'Honeypot Field',
71
+ defaultValue: '',
72
+ description:
73
+ 'Name of a form field hidden from people by CSS. Any payload arriving with it filled was filled by a script, and is quarantined immediately. Leave empty to disable. An individual source can override this.',
74
+ },
75
+ {
76
+ key: 'INTAKE_RAW_PAYLOAD_RETENTION',
77
+ label: 'Raw Payload Retention',
78
+ defaultValue: 'FULL',
79
+ description:
80
+ 'FULL keeps each payload on its log, which is what retry and replay run from. NONE keeps nothing and disables both. Passwords, tokens and card numbers are stripped before storage under either setting; everything else the sender submitted is kept as sent.',
81
+ },
82
+ {
83
+ key: 'INTAKE_RAW_PAYLOAD_MAX_BYTES',
84
+ label: 'Max Stored Payload (bytes)',
85
+ defaultValue: '65000',
86
+ description:
87
+ 'Largest payload kept for replay. Anything larger is still ingested normally, but not stored, and its log records that replay is unavailable.',
88
+ },
89
+ {
90
+ key: 'INTAKE_REPLAY_MAX_BATCH',
91
+ label: 'Max Replay Batch',
92
+ defaultValue: '50',
93
+ description:
94
+ 'How many logs one bulk replay may process. Every replayed log writes to the CRM, so this caps how much a single call can change. The hard ceiling is 500.',
95
+ },
47
96
  ] as const;
48
97
 
49
98
  type SettingKey = (typeof SETTINGS)[number]['key'];
@@ -0,0 +1,263 @@
1
+ import { createRequire as __createRequire } from 'module';
2
+ const require = __createRequire(import.meta.url);
3
+
4
+ // twenty-sdk-define-stub:__twenty-sdk-define-stub__
5
+ var __defineFactoryStub = (config) => ({
6
+ success: true,
7
+ config,
8
+ errors: []
9
+ });
10
+ var __anyHandler = {
11
+ get(_target, prop) {
12
+ if (prop === "__esModule") return true;
13
+ if (prop === Symbol.toPrimitive) return () => "";
14
+ if (typeof prop === "symbol") return void 0;
15
+ return new Proxy(() => void 0, __anyHandler);
16
+ },
17
+ apply() {
18
+ return new Proxy(() => void 0, __anyHandler);
19
+ }
20
+ };
21
+ var __anyStub = new Proxy(() => void 0, __anyHandler);
22
+ var defineLogicFunction = __defineFactoryStub;
23
+
24
+ // src/constants/universal-identifiers.ts
25
+ var IDS = {
26
+ APPLICATION: "96a389dd-384a-43a1-a376-9d3cf01b6ab7",
27
+ DEFAULT_ROLE: "3dd2cdb0-32c7-44b4-9f7b-0a9c667ec249",
28
+ // Objects
29
+ INTAKE_SOURCE_OBJECT: "8c1c227c-db7d-4834-b328-cacbe8af0bfc",
30
+ INTAKE_LOG_OBJECT: "b77a2439-1503-4ef6-ab8b-8f4293bc19da",
31
+ INTAKE_FIELD_RULE_OBJECT: "e74df817-ff25-4bfb-bdf0-651662ca327e",
32
+ // IntakeSource fields
33
+ INTAKE_SOURCE_NAME: "0deb7e4b-bb90-4c86-b903-5208b42041e2",
34
+ INTAKE_SOURCE_SLUG: "18f7645b-2dce-4145-b1b6-9cc3332c0578",
35
+ INTAKE_SOURCE_TARGET_OBJECT: "65f3d7f7-a601-4375-9c67-684d0044620b",
36
+ INTAKE_SOURCE_SECRET: "4c919402-07f5-4041-ade6-a42402f1881f",
37
+ INTAKE_SOURCE_STATUS: "6f1fa6fe-02b4-48ab-b1ee-97c600bbf483",
38
+ INTAKE_SOURCE_CREATE_OPP: "67ae59aa-2275-4ef2-b594-137f6bfafd99",
39
+ INTAKE_SOURCE_OPP_TEMPLATE: "3907b346-77a6-4c79-a4d7-6e1cc20bb82b",
40
+ INTAKE_SOURCE_TOTAL_INGESTED: "69655a93-bc70-48bd-874e-aeccc63ee1a8",
41
+ INTAKE_SOURCE_TOTAL_FAILED: "27b3a499-183e-4cc3-8e40-938f953a986d",
42
+ INTAKE_SOURCE_LAST_INGESTED: "e167f628-9865-468e-894a-b4b6150cec26",
43
+ // IntakeLog fields
44
+ INTAKE_LOG_SOURCE_RELATION: "b1c623ea-7a9a-47a5-bee4-35fdd668a637",
45
+ INTAKE_LOG_STATUS: "8b6d26d1-f89c-486c-a617-9f9f6a9ca36d",
46
+ INTAKE_LOG_PAYLOAD_HASH: "c43bb1ad-e78a-4c60-bd95-02475ede7f44",
47
+ INTAKE_LOG_RECORD_ID: "77a55116-a64b-4954-8369-341022dd5bdd",
48
+ INTAKE_LOG_RECORD_TYPE: "02154e96-5808-48ed-8838-51083f174550",
49
+ INTAKE_LOG_FIELDS_CREATED: "2295ca2b-b559-4b38-9771-12dd8d3d76d7",
50
+ INTAKE_LOG_FIELDS_MATCHED: "83d07a0f-5bcd-4fb1-9759-f9b04b9381d8",
51
+ INTAKE_LOG_OVERFLOW_COUNT: "4090acb1-f48a-4957-bb91-c642f8f434a7",
52
+ INTAKE_LOG_PROCESSING_MS: "a9191f27-4b40-42e9-8cce-20c75900e12c",
53
+ INTAKE_LOG_ERROR: "8d9979f1-9774-43a8-b958-d1b37bd4ecc7",
54
+ INTAKE_LOG_PROCESSED_AT: "a8d99fbd-722e-4987-8afe-b830ac026f0a",
55
+ // IntakeFieldRule fields
56
+ INTAKE_FIELD_RULE_SOURCE_RELATION: "396dd1f1-3873-4efa-a6bb-7b41eda14d83",
57
+ INTAKE_FIELD_RULE_INPUT_PATTERN: "757201a0-9f64-47b9-bf3a-a793074bf84e",
58
+ INTAKE_FIELD_RULE_CANONICAL_NAME: "fafebc55-a2cc-4c9f-975b-050c2676659d",
59
+ INTAKE_FIELD_RULE_FIELD_TYPE: "72d1f6d8-d63e-419e-a244-8127d7b4705f",
60
+ INTAKE_FIELD_RULE_PRIORITY: "911df299-940b-44f9-92f7-3fd01fde01a7",
61
+ INTAKE_FIELD_RULE_IS_ACTIVE: "6f9cd96d-aecd-437f-a7b5-d45548782771",
62
+ INTAKE_FIELD_RULE_TARGET_OBJECT: "7b8411a7-5e1d-40f2-a20c-683c07f67926",
63
+ // Logic Functions
64
+ WEBHOOK_LOGIC_FUNCTION: "8323c257-48fe-4bb6-b1dd-166844f575c0",
65
+ HEALTH_LOGIC_FUNCTION: "4ece4e39-286b-4820-81f4-b7beee71d092",
66
+ REGISTER_LOGIC_FUNCTION: "56d46e18-6cae-4d99-9893-b7d70740c9db",
67
+ // IntakeSource reverse relation fields
68
+ INTAKE_SOURCE_LOGS_REVERSE: "6ecfa369-22e0-4812-bb75-4338cea8be65",
69
+ INTAKE_SOURCE_RULES_REVERSE: "776fefaf-0eee-4ec0-93e0-d11bd80c1e20",
70
+ // v0.2.0 — rawPayload + retry + post-install + UI
71
+ INTAKE_LOG_RAW_PAYLOAD: "b65c9e1c-f2c1-4c52-a5b3-1e1ac3139692",
72
+ RETRY_LOGIC_FUNCTION: "90f20396-a69a-4ae4-af20-6be65c565bb5",
73
+ POST_INSTALL_LOGIC_FUNCTION: "e836fdd6-fd54-4aa4-ba6d-87d5a02148ef",
74
+ LOG_RETRY_FRONT_COMPONENT: "0cb97f6d-30e2-48c8-90e1-241b82870b4d",
75
+ FIELD_RULES_TABLE_FRONT_COMPONENT: "c35faad9-3468-4746-9c1c-b13d73e5e09b",
76
+ SOURCE_PAGE_LAYOUT: "8c894ba8-5c8c-45ed-b0af-1d22ede93eb9",
77
+ SOURCE_LAYOUT_TAB_RULES: "80f5c49a-083c-4062-8bd5-ae158abb6c33",
78
+ SOURCE_LAYOUT_TAB_RULES_WIDGET: "26a9ebb7-1d59-4b03-a018-56968f6df224",
79
+ LOG_PAGE_LAYOUT: "b0972802-ede5-463c-9f59-cb05f04957db",
80
+ LOG_LAYOUT_TAB_RETRY: "6b109695-ba3a-43de-a31a-23119ccf87ba",
81
+ LOG_LAYOUT_TAB_RETRY_WIDGET: "11727c1a-5145-46de-9742-88d07d64c72e",
82
+ SAMPLE_RULE_PHONE: "8440da8d-8ac4-4f4b-baa2-4d62936cde35",
83
+ SAMPLE_RULE_EMAIL: "d123cde4-05aa-44e1-86f6-454be1d7048b",
84
+ SAMPLE_RULE_FULLNAME: "00e49fe8-1b48-4f5b-aaf4-414d0923c712",
85
+ // Views & Navigation
86
+ SOURCES_VIEW: "4435cd43-5c13-472f-b4f9-b686c0f46285",
87
+ LOGS_VIEW: "a2cd0a93-9778-4ba5-99d9-b8b89c4fb39d",
88
+ INTAKE_NAV_ITEM: "e81bf288-5686-4713-86ae-e2947b124cab",
89
+ // ── v0.4.0 ────────────────────────────────────────────────────────────────
90
+ // IntakeLog fields — quarantine, idempotency, replay lineage
91
+ INTAKE_LOG_IDEMPOTENCY_KEY: "bf91e4c0-6477-41f2-a9bd-fb159d3091d4",
92
+ INTAKE_LOG_SPAM_SCORE: "4e250af2-925f-4724-85f6-f353a6bb4cfd",
93
+ INTAKE_LOG_QUARANTINE_REASON: "323b3c3e-a111-45d1-a211-cf70efa6cd9e",
94
+ INTAKE_LOG_REPLAY_OF: "b68876b1-0045-4eaa-a4e7-f0634e110ef1",
95
+ // IntakeSource fields — silence detection, spam, merge policy
96
+ INTAKE_SOURCE_EXPECTED_CADENCE: "a97c3906-5555-4e60-a6d6-84155a4e00a6",
97
+ INTAKE_SOURCE_HEALTH_STATUS: "2ad13b20-5bd7-450a-b52d-bcf208373cf5",
98
+ INTAKE_SOURCE_SILENT_SINCE: "d3395601-f29f-4e57-98eb-561ab011cc72",
99
+ INTAKE_SOURCE_ALERT_WEBHOOK: "5a066d62-0426-41c1-b34c-1a7e881ce854",
100
+ INTAKE_SOURCE_LAST_ALERT_AT: "fc64a598-ccbe-44e3-bd42-716e32d626a2",
101
+ INTAKE_SOURCE_HONEYPOT_FIELD: "5c145686-50de-496c-bb45-a30c8b39034c",
102
+ INTAKE_SOURCE_MERGE_POLICY: "5a946f85-b1c2-493e-92c6-31c50e637a7e",
103
+ // IntakeFieldRule fields
104
+ INTAKE_FIELD_RULE_MERGE_STRATEGY: "40c6fdcb-527a-4691-8518-c40a3d3e34c6",
105
+ // Logic Functions
106
+ CONTRACT_LOGIC_FUNCTION: "2eea4152-1b08-4b8c-903c-f4c7ee7275d0",
107
+ REPLAY_LOG_LOGIC_FUNCTION: "8ff98390-7358-4a91-84f3-435f758182a3",
108
+ REPLAY_BULK_LOGIC_FUNCTION: "b82ed4c4-07bc-4095-8ee5-611200626dc0",
109
+ QUARANTINE_LIST_LOGIC_FUNCTION: "0203643c-2a97-4e7c-b4a5-b76d721cf23a",
110
+ QUARANTINE_RELEASE_LOGIC_FUNCTION: "5a10d4fe-c807-45c6-9e61-c191d4e37b4e",
111
+ QUARANTINE_DISCARD_LOGIC_FUNCTION: "1454a56e-6197-4dfd-9b41-5cc3b1436af7",
112
+ CHECK_SILENCE_LOGIC_FUNCTION: "e88cd5f2-ab96-4f89-ac55-78db66b32d66",
113
+ // Application variables
114
+ VAR_MERGE_POLICY: "a84fe9fd-3d7c-41d9-b0e8-dfdc1a87f348",
115
+ VAR_SPAM_FILTER_ENABLED: "570d3ba8-1904-4bad-a5ce-27e208e6414a",
116
+ VAR_SPAM_SCORE_THRESHOLD: "4395574d-24b4-4e0e-afa3-1e90fcd4f470",
117
+ VAR_HONEYPOT_FIELD: "2cf56c56-f5cb-47c5-aa43-84f301060a95",
118
+ VAR_RAW_PAYLOAD_RETENTION: "f3314ad6-05b9-4127-bb88-cea5f2018dc9",
119
+ VAR_RAW_PAYLOAD_MAX_BYTES: "b002ca76-df3d-47b4-8ac5-d3f562239c48",
120
+ VAR_REPLAY_MAX_BATCH: "cf55956f-177e-4afd-8d9f-7f42c614209c"
121
+ };
122
+
123
+ // src/lib/rest-client.ts
124
+ var apiUrl = () => process.env["TWENTY_API_URL"] ?? "";
125
+ var token = () => process.env["TWENTY_APP_ACCESS_TOKEN"] ?? "";
126
+ async function request(base, method, path, body) {
127
+ const res = await fetch(`${base}${path}`, {
128
+ method,
129
+ headers: {
130
+ Authorization: `Bearer ${token()}`,
131
+ "Content-Type": "application/json"
132
+ },
133
+ body: body !== void 0 ? JSON.stringify(body) : void 0
134
+ });
135
+ const text = await res.text();
136
+ if (!res.ok) {
137
+ throw new Error(`${method} ${path} \u2192 ${res.status}: ${text}`);
138
+ }
139
+ return JSON.parse(text);
140
+ }
141
+ var coreApi = {
142
+ get: (path) => request(`${apiUrl()}/rest`, "GET", path),
143
+ post: (path, body) => request(`${apiUrl()}/rest`, "POST", path, body),
144
+ patch: (path, body) => request(`${apiUrl()}/rest`, "PATCH", path, body)
145
+ };
146
+
147
+ // src/lib/silence.ts
148
+ var HOUR_MS = 60 * 60 * 1e3;
149
+ function assess(source, now = Date.now()) {
150
+ const base = {
151
+ slug: source.slug,
152
+ name: source.name,
153
+ expectedCadenceHours: source.expectedCadenceHours
154
+ };
155
+ if (source.status === "PAUSED" || !source.expectedCadenceHours || source.expectedCadenceHours <= 0) {
156
+ return { ...base, health: "NOT_MONITORED", hoursSinceLastIngest: hoursSince(source.lastIngestedAt, now), newlySilent: false };
157
+ }
158
+ const wasSilent = source.healthStatus === "SILENT" || source.healthStatus === "NEVER_RECEIVED";
159
+ if (!source.lastIngestedAt) {
160
+ return { ...base, health: "NEVER_RECEIVED", hoursSinceLastIngest: null, newlySilent: !wasSilent };
161
+ }
162
+ const hours = hoursSince(source.lastIngestedAt, now);
163
+ if (hours === null || hours > source.expectedCadenceHours) {
164
+ return { ...base, health: "SILENT", hoursSinceLastIngest: hours, newlySilent: !wasSilent };
165
+ }
166
+ return { ...base, health: "HEALTHY", hoursSinceLastIngest: hours, newlySilent: false };
167
+ }
168
+ function hoursSince(iso, now) {
169
+ if (!iso) return null;
170
+ const then = new Date(iso).getTime();
171
+ if (!Number.isFinite(then)) return null;
172
+ return Math.round((now - then) / HOUR_MS * 10) / 10;
173
+ }
174
+ async function loadSources() {
175
+ try {
176
+ const res = await coreApi.get(
177
+ "/intakeSources?limit=200"
178
+ );
179
+ return (res.data?.intakeSources?.edges ?? []).map((e) => e.node);
180
+ } catch {
181
+ return [];
182
+ }
183
+ }
184
+ async function persist(source, verdict) {
185
+ if (verdict.health === "NOT_MONITORED") return;
186
+ if (source.healthStatus === verdict.health && !verdict.newlySilent) return;
187
+ const patch = { healthStatus: verdict.health };
188
+ if (verdict.health === "HEALTHY") {
189
+ patch["silentSince"] = null;
190
+ } else if (verdict.newlySilent) {
191
+ patch["silentSince"] = (/* @__PURE__ */ new Date()).toISOString();
192
+ }
193
+ await coreApi.patch(`/intakeSources/${source.id}`, patch).catch(() => null);
194
+ }
195
+ async function sendAlert(source, verdict) {
196
+ if (!source.alertWebhookUrl || !verdict.newlySilent) return false;
197
+ const text = verdict.health === "NEVER_RECEIVED" ? `Intake: source "${source.name}" (${source.slug}) has never received a payload, and a cadence of ${verdict.expectedCadenceHours}h is configured. The sending side is probably not wired up.` : `Intake: source "${source.name}" (${source.slug}) has received nothing for ${verdict.hoursSinceLastIngest}h, past its expected cadence of ${verdict.expectedCadenceHours}h.`;
198
+ try {
199
+ const res = await fetch(source.alertWebhookUrl, {
200
+ method: "POST",
201
+ headers: { "Content-Type": "application/json" },
202
+ // `text` is what Slack, Discord and Teams incoming webhooks all read, so
203
+ // the common case needs no transformation; the rest is there for anything
204
+ // consuming the payload properly.
205
+ body: JSON.stringify({
206
+ text,
207
+ source: { slug: source.slug, name: source.name },
208
+ health: verdict.health,
209
+ hoursSinceLastIngest: verdict.hoursSinceLastIngest,
210
+ expectedCadenceHours: verdict.expectedCadenceHours
211
+ })
212
+ });
213
+ if (!res.ok) return false;
214
+ } catch {
215
+ return false;
216
+ }
217
+ await coreApi.patch(`/intakeSources/${source.id}`, {
218
+ lastAlertAt: (/* @__PURE__ */ new Date()).toISOString()
219
+ }).catch(() => null);
220
+ return true;
221
+ }
222
+
223
+ // src/logic-functions/check-silence.ts
224
+ var handler = async () => {
225
+ const sources = await loadSources();
226
+ const verdicts = [];
227
+ let alertsSent = 0;
228
+ for (const source of sources) {
229
+ const verdict = assess(source);
230
+ await persist(source, verdict);
231
+ if (await sendAlert(source, verdict)) alertsSent++;
232
+ verdicts.push(verdict);
233
+ }
234
+ const silent = verdicts.filter((v) => v.health === "SILENT" || v.health === "NEVER_RECEIVED");
235
+ return {
236
+ statusCode: 200,
237
+ body: {
238
+ checkedAt: (/* @__PURE__ */ new Date()).toISOString(),
239
+ sourcesChecked: sources.length,
240
+ monitored: verdicts.filter((v) => v.health !== "NOT_MONITORED").length,
241
+ silentCount: silent.length,
242
+ alertsSent,
243
+ silent,
244
+ sources: verdicts
245
+ }
246
+ };
247
+ };
248
+ var check_silence_default = defineLogicFunction({
249
+ universalIdentifier: IDS.CHECK_SILENCE_LOGIC_FUNCTION,
250
+ name: "intake-check-silence",
251
+ description: "Check every source against its expected cadence and alert on sources that have fallen silent.",
252
+ timeoutSeconds: 60,
253
+ handler,
254
+ httpRouteTriggerSettings: {
255
+ path: "/intake/sources/check-silence",
256
+ httpMethod: "POST",
257
+ isAuthRequired: true
258
+ }
259
+ });
260
+ export {
261
+ check_silence_default as default
262
+ };
263
+ //# sourceMappingURL=check-silence.mjs.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["twenty-sdk-define-stub:__twenty-sdk-define-stub__", "../../../../src/constants/universal-identifiers.ts", "../../../../src/lib/rest-client.ts", "../../../../src/lib/silence.ts", "../../../../src/logic-functions/check-silence.ts"],
4
+ "sourcesContent": ["\n// Auto-generated stub for twenty-sdk/define injected by the SDK CLI build.\n// Real implementations would pull in zod, twenty-shared and ~1MB of code; at\n// runtime only `default.config.handler` is consumed, so tiny no-ops suffice.\nconst __defineFactoryStub = (config) => ({\n success: true,\n config,\n errors: [],\n});\n\nconst __anyHandler = {\n get(_target, prop) {\n if (prop === '__esModule') return true;\n if (prop === Symbol.toPrimitive) return () => '';\n if (typeof prop === 'symbol') return undefined;\n return new Proxy(() => undefined, __anyHandler);\n },\n apply() {\n return new Proxy(() => undefined, __anyHandler);\n },\n};\nconst __anyStub = new Proxy(() => undefined, __anyHandler);\n\nexport const createValidationResult = __defineFactoryStub;\nexport const defineAgent = __defineFactoryStub;\nexport const defineApplication = __defineFactoryStub;\nexport const defineApplicationRole = __defineFactoryStub;\nexport const defineCommandMenuItem = __defineFactoryStub;\nexport const defineConnectionProvider = __defineFactoryStub;\nexport const defineField = __defineFactoryStub;\nexport const defineFrontComponent = __defineFactoryStub;\nexport const defineIndex = __defineFactoryStub;\nexport const defineLogicFunction = __defineFactoryStub;\nexport const defineNavigationMenuItem = __defineFactoryStub;\nexport const defineObject = __defineFactoryStub;\nexport const definePageLayout = __defineFactoryStub;\nexport const definePageLayoutTab = __defineFactoryStub;\nexport const definePermissionFlag = __defineFactoryStub;\nexport const definePostInstallLogicFunction = __defineFactoryStub;\nexport const definePreInstallLogicFunction = __defineFactoryStub;\nexport const defineRole = __defineFactoryStub;\nexport const defineSettingsFrontComponent = __defineFactoryStub;\nexport const defineSkill = __defineFactoryStub;\nexport const defineUninstallLogicFunction = __defineFactoryStub;\nexport const defineView = __defineFactoryStub;\nexport const defineViewField = __defineFactoryStub;\nexport const AggregateOperations = __anyStub;\nexport const DateDisplayFormat = __anyStub;\nexport const FieldMetadataSettingsOnClickAction = __anyStub;\nexport const FieldType = __anyStub;\nexport const HTTPMethod = __anyStub;\nexport const NavigationMenuItemType = __anyStub;\nexport const NumberDataType = __anyStub;\nexport const ObjectOpenRecordIn = __anyStub;\nexport const ObjectRecordGroupByDateGranularity = __anyStub;\nexport const OnDeleteAction = __anyStub;\nexport const PageLayoutTabLayoutMode = __anyStub;\nexport const PageLayoutType = __anyStub;\nexport const RelationType = __anyStub;\nexport const RowLevelPermissionPredicateGroupLogicalOperator = __anyStub;\nexport const RowLevelPermissionPredicateOperand = __anyStub;\nexport const STANDARD_OBJECT = __anyStub;\nexport const STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS = __anyStub;\nexport const STANDARD_PAGE_LAYOUT = __anyStub;\nexport const STANDARD_PAGE_LAYOUT_UNIVERSAL_IDENTIFIERS = __anyStub;\nexport const SystemPermissionFlag = __anyStub;\nexport const ViewCalendarLayout = __anyStub;\nexport const ViewFilterGroupLogicalOperator = __anyStub;\nexport const ViewFilterOperand = __anyStub;\nexport const ViewKey = __anyStub;\nexport const ViewOpenRecordIn = __anyStub;\nexport const ViewSortDirection = __anyStub;\nexport const ViewType = __anyStub;\nexport const ViewVisibility = __anyStub;\nexport const canAccessFullAdminPanel = __anyStub;\nexport const canImpersonate = __anyStub;\nexport const every = __anyStub;\nexport const everyDefined = __anyStub;\nexport const everyEquals = __anyStub;\nexport const favoriteRecordIds = __anyStub;\nexport const featureFlags = __anyStub;\nexport const getFieldUniversalIdentifier = __anyStub;\nexport const getSystemRelationFieldUniversalIdentifier = __anyStub;\nexport const getSystemViewFieldUniversalIdentifier = __anyStub;\nexport const getSystemViewUniversalIdentifier = __anyStub;\nexport const hasAnySoftDeleteFilterOnView = __anyStub;\nexport const includes = __anyStub;\nexport const includesEvery = __anyStub;\nexport const isDashboardPageLayoutInEditMode = __anyStub;\nexport const isDefined = __anyStub;\nexport const isInSidePanel = __anyStub;\nexport const isLayoutCustomizationModeEnabled = __anyStub;\nexport const isNonEmptyString = __anyStub;\nexport const isSelectAll = __anyStub;\nexport const none = __anyStub;\nexport const noneDefined = __anyStub;\nexport const noneEquals = __anyStub;\nexport const numberOfSelectedRecords = __anyStub;\nexport const objectMetadataItem = __anyStub;\nexport const objectMetadataLabel = __anyStub;\nexport const objectPermissions = __anyStub;\nexport const pageType = __anyStub;\nexport const selectedRecords = __anyStub;\nexport const some = __anyStub;\nexport const someDefined = __anyStub;\nexport const someEquals = __anyStub;\nexport const someNonEmptyString = __anyStub;\nexport const targetObjectReadPermissions = __anyStub;\nexport const targetObjectWritePermissions = __anyStub;\nexport const validateFields = __anyStub;\n", "export const IDS = {\n APPLICATION: '96a389dd-384a-43a1-a376-9d3cf01b6ab7',\n DEFAULT_ROLE: '3dd2cdb0-32c7-44b4-9f7b-0a9c667ec249',\n\n // Objects\n INTAKE_SOURCE_OBJECT: '8c1c227c-db7d-4834-b328-cacbe8af0bfc',\n INTAKE_LOG_OBJECT: 'b77a2439-1503-4ef6-ab8b-8f4293bc19da',\n INTAKE_FIELD_RULE_OBJECT: 'e74df817-ff25-4bfb-bdf0-651662ca327e',\n\n // IntakeSource fields\n INTAKE_SOURCE_NAME: '0deb7e4b-bb90-4c86-b903-5208b42041e2',\n INTAKE_SOURCE_SLUG: '18f7645b-2dce-4145-b1b6-9cc3332c0578',\n INTAKE_SOURCE_TARGET_OBJECT: '65f3d7f7-a601-4375-9c67-684d0044620b',\n INTAKE_SOURCE_SECRET: '4c919402-07f5-4041-ade6-a42402f1881f',\n INTAKE_SOURCE_STATUS: '6f1fa6fe-02b4-48ab-b1ee-97c600bbf483',\n INTAKE_SOURCE_CREATE_OPP: '67ae59aa-2275-4ef2-b594-137f6bfafd99',\n INTAKE_SOURCE_OPP_TEMPLATE: '3907b346-77a6-4c79-a4d7-6e1cc20bb82b',\n INTAKE_SOURCE_TOTAL_INGESTED: '69655a93-bc70-48bd-874e-aeccc63ee1a8',\n INTAKE_SOURCE_TOTAL_FAILED: '27b3a499-183e-4cc3-8e40-938f953a986d',\n INTAKE_SOURCE_LAST_INGESTED: 'e167f628-9865-468e-894a-b4b6150cec26',\n\n // IntakeLog fields\n INTAKE_LOG_SOURCE_RELATION: 'b1c623ea-7a9a-47a5-bee4-35fdd668a637',\n INTAKE_LOG_STATUS: '8b6d26d1-f89c-486c-a617-9f9f6a9ca36d',\n INTAKE_LOG_PAYLOAD_HASH: 'c43bb1ad-e78a-4c60-bd95-02475ede7f44',\n INTAKE_LOG_RECORD_ID: '77a55116-a64b-4954-8369-341022dd5bdd',\n INTAKE_LOG_RECORD_TYPE: '02154e96-5808-48ed-8838-51083f174550',\n INTAKE_LOG_FIELDS_CREATED: '2295ca2b-b559-4b38-9771-12dd8d3d76d7',\n INTAKE_LOG_FIELDS_MATCHED: '83d07a0f-5bcd-4fb1-9759-f9b04b9381d8',\n INTAKE_LOG_OVERFLOW_COUNT: '4090acb1-f48a-4957-bb91-c642f8f434a7',\n INTAKE_LOG_PROCESSING_MS: 'a9191f27-4b40-42e9-8cce-20c75900e12c',\n INTAKE_LOG_ERROR: '8d9979f1-9774-43a8-b958-d1b37bd4ecc7',\n INTAKE_LOG_PROCESSED_AT: 'a8d99fbd-722e-4987-8afe-b830ac026f0a',\n\n // IntakeFieldRule fields\n INTAKE_FIELD_RULE_SOURCE_RELATION: '396dd1f1-3873-4efa-a6bb-7b41eda14d83',\n INTAKE_FIELD_RULE_INPUT_PATTERN: '757201a0-9f64-47b9-bf3a-a793074bf84e',\n INTAKE_FIELD_RULE_CANONICAL_NAME: 'fafebc55-a2cc-4c9f-975b-050c2676659d',\n INTAKE_FIELD_RULE_FIELD_TYPE: '72d1f6d8-d63e-419e-a244-8127d7b4705f',\n INTAKE_FIELD_RULE_PRIORITY: '911df299-940b-44f9-92f7-3fd01fde01a7',\n INTAKE_FIELD_RULE_IS_ACTIVE: '6f9cd96d-aecd-437f-a7b5-d45548782771',\n INTAKE_FIELD_RULE_TARGET_OBJECT: '7b8411a7-5e1d-40f2-a20c-683c07f67926',\n\n // Logic Functions\n WEBHOOK_LOGIC_FUNCTION: '8323c257-48fe-4bb6-b1dd-166844f575c0',\n HEALTH_LOGIC_FUNCTION: '4ece4e39-286b-4820-81f4-b7beee71d092',\n REGISTER_LOGIC_FUNCTION: '56d46e18-6cae-4d99-9893-b7d70740c9db',\n\n // IntakeSource reverse relation fields\n INTAKE_SOURCE_LOGS_REVERSE: '6ecfa369-22e0-4812-bb75-4338cea8be65',\n INTAKE_SOURCE_RULES_REVERSE: '776fefaf-0eee-4ec0-93e0-d11bd80c1e20',\n\n // v0.2.0 \u2014 rawPayload + retry + post-install + UI\n INTAKE_LOG_RAW_PAYLOAD: 'b65c9e1c-f2c1-4c52-a5b3-1e1ac3139692',\n RETRY_LOGIC_FUNCTION: '90f20396-a69a-4ae4-af20-6be65c565bb5',\n POST_INSTALL_LOGIC_FUNCTION: 'e836fdd6-fd54-4aa4-ba6d-87d5a02148ef',\n LOG_RETRY_FRONT_COMPONENT: '0cb97f6d-30e2-48c8-90e1-241b82870b4d',\n FIELD_RULES_TABLE_FRONT_COMPONENT: 'c35faad9-3468-4746-9c1c-b13d73e5e09b',\n SOURCE_PAGE_LAYOUT: '8c894ba8-5c8c-45ed-b0af-1d22ede93eb9',\n SOURCE_LAYOUT_TAB_RULES: '80f5c49a-083c-4062-8bd5-ae158abb6c33',\n SOURCE_LAYOUT_TAB_RULES_WIDGET: '26a9ebb7-1d59-4b03-a018-56968f6df224',\n LOG_PAGE_LAYOUT: 'b0972802-ede5-463c-9f59-cb05f04957db',\n LOG_LAYOUT_TAB_RETRY: '6b109695-ba3a-43de-a31a-23119ccf87ba',\n LOG_LAYOUT_TAB_RETRY_WIDGET: '11727c1a-5145-46de-9742-88d07d64c72e',\n SAMPLE_RULE_PHONE: '8440da8d-8ac4-4f4b-baa2-4d62936cde35',\n SAMPLE_RULE_EMAIL: 'd123cde4-05aa-44e1-86f6-454be1d7048b',\n SAMPLE_RULE_FULLNAME: '00e49fe8-1b48-4f5b-aaf4-414d0923c712',\n\n // Views & Navigation\n SOURCES_VIEW: '4435cd43-5c13-472f-b4f9-b686c0f46285',\n LOGS_VIEW: 'a2cd0a93-9778-4ba5-99d9-b8b89c4fb39d',\n INTAKE_NAV_ITEM: 'e81bf288-5686-4713-86ae-e2947b124cab',\n\n // \u2500\u2500 v0.4.0 \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n // IntakeLog fields \u2014 quarantine, idempotency, replay lineage\n INTAKE_LOG_IDEMPOTENCY_KEY: 'bf91e4c0-6477-41f2-a9bd-fb159d3091d4',\n INTAKE_LOG_SPAM_SCORE: '4e250af2-925f-4724-85f6-f353a6bb4cfd',\n INTAKE_LOG_QUARANTINE_REASON: '323b3c3e-a111-45d1-a211-cf70efa6cd9e',\n INTAKE_LOG_REPLAY_OF: 'b68876b1-0045-4eaa-a4e7-f0634e110ef1',\n\n // IntakeSource fields \u2014 silence detection, spam, merge policy\n INTAKE_SOURCE_EXPECTED_CADENCE: 'a97c3906-5555-4e60-a6d6-84155a4e00a6',\n INTAKE_SOURCE_HEALTH_STATUS: '2ad13b20-5bd7-450a-b52d-bcf208373cf5',\n INTAKE_SOURCE_SILENT_SINCE: 'd3395601-f29f-4e57-98eb-561ab011cc72',\n INTAKE_SOURCE_ALERT_WEBHOOK: '5a066d62-0426-41c1-b34c-1a7e881ce854',\n INTAKE_SOURCE_LAST_ALERT_AT: 'fc64a598-ccbe-44e3-bd42-716e32d626a2',\n INTAKE_SOURCE_HONEYPOT_FIELD: '5c145686-50de-496c-bb45-a30c8b39034c',\n INTAKE_SOURCE_MERGE_POLICY: '5a946f85-b1c2-493e-92c6-31c50e637a7e',\n\n // IntakeFieldRule fields\n INTAKE_FIELD_RULE_MERGE_STRATEGY: '40c6fdcb-527a-4691-8518-c40a3d3e34c6',\n\n // Logic Functions\n CONTRACT_LOGIC_FUNCTION: '2eea4152-1b08-4b8c-903c-f4c7ee7275d0',\n REPLAY_LOG_LOGIC_FUNCTION: '8ff98390-7358-4a91-84f3-435f758182a3',\n REPLAY_BULK_LOGIC_FUNCTION: 'b82ed4c4-07bc-4095-8ee5-611200626dc0',\n QUARANTINE_LIST_LOGIC_FUNCTION: '0203643c-2a97-4e7c-b4a5-b76d721cf23a',\n QUARANTINE_RELEASE_LOGIC_FUNCTION: '5a10d4fe-c807-45c6-9e61-c191d4e37b4e',\n QUARANTINE_DISCARD_LOGIC_FUNCTION: '1454a56e-6197-4dfd-9b41-5cc3b1436af7',\n CHECK_SILENCE_LOGIC_FUNCTION: 'e88cd5f2-ab96-4f89-ac55-78db66b32d66',\n\n // Application variables\n VAR_MERGE_POLICY: 'a84fe9fd-3d7c-41d9-b0e8-dfdc1a87f348',\n VAR_SPAM_FILTER_ENABLED: '570d3ba8-1904-4bad-a5ce-27e208e6414a',\n VAR_SPAM_SCORE_THRESHOLD: '4395574d-24b4-4e0e-afa3-1e90fcd4f470',\n VAR_HONEYPOT_FIELD: '2cf56c56-f5cb-47c5-aa43-84f301060a95',\n VAR_RAW_PAYLOAD_RETENTION: 'f3314ad6-05b9-4127-bb88-cea5f2018dc9',\n VAR_RAW_PAYLOAD_MAX_BYTES: 'b002ca76-df3d-47b4-8ac5-d3f562239c48',\n VAR_REPLAY_MAX_BATCH: 'cf55956f-177e-4afd-8d9f-7f42c614209c',\n} as const;\n", "/**\n * Minimal REST client for Twenty's Core and Metadata APIs.\n * Uses raw fetch so it works with dynamically created custom fields\n * that the typed SDK client doesn't know about at build time.\n */\n\nconst apiUrl = () => process.env['TWENTY_API_URL'] ?? '';\nconst token = () => process.env['TWENTY_APP_ACCESS_TOKEN'] ?? '';\n\nasync function request<T>(\n base: string,\n method: string,\n path: string,\n body?: unknown,\n): Promise<T> {\n const res = await fetch(`${base}${path}`, {\n method,\n headers: {\n Authorization: `Bearer ${token()}`,\n 'Content-Type': 'application/json',\n },\n body: body !== undefined ? JSON.stringify(body) : undefined,\n });\n\n const text = await res.text();\n if (!res.ok) {\n throw new Error(`${method} ${path} \u2192 ${res.status}: ${text}`);\n }\n return JSON.parse(text) as T;\n}\n\nexport const coreApi = {\n get: <T>(path: string) => request<T>(`${apiUrl()}/rest`, 'GET', path),\n post: <T>(path: string, body: unknown) => request<T>(`${apiUrl()}/rest`, 'POST', path, body),\n patch: <T>(path: string, body: unknown) => request<T>(`${apiUrl()}/rest`, 'PATCH', path, body),\n};\n\nexport const metaApi = {\n get: <T>(path: string) => request<T>(`${apiUrl()}/rest/metadata`, 'GET', path),\n post: <T>(path: string, body: unknown) => request<T>(`${apiUrl()}/rest/metadata`, 'POST', path, body),\n};\n\n/** Run a raw GraphQL query against the /graphql endpoint. */\nexport async function gql<T = unknown>(\n query: string,\n variables?: Record<string, unknown>,\n): Promise<T> {\n return request<T>(`${apiUrl()}`, 'POST', '/graphql', { query, variables });\n}\n", "/**\n * Noticing that nothing arrived.\n *\n * Every other failure in this app announces itself: a bad signature returns 401,\n * a rejected record writes a FAILED log. Silence returns nothing, logs nothing\n * and looks exactly like a quiet week. A form that breaks in February is found\n * in August, and the only evidence is leads that were never there.\n *\n * The check is deliberately small. A source states how long it may go quiet;\n * anything longer is a fault, recorded on the source and posted once to a\n * webhook if one is configured. There is no scheduler here \u2014 a Twenty App has\n * nowhere dependable to run one \u2014 so the endpoint is driven by whatever already\n * pings on a timer, which for most workspaces is an uptime monitor they have.\n */\n\nimport { coreApi } from './rest-client';\n\nexport type SourceHealth = {\n id: string;\n name: string;\n slug: string;\n status: string;\n expectedCadenceHours: number | null;\n lastIngestedAt: string | null;\n healthStatus: string | null;\n silentSince: string | null;\n lastAlertAt: string | null;\n alertWebhookUrl: string | null;\n};\n\nexport type SilenceVerdict = {\n slug: string;\n name: string;\n health: 'HEALTHY' | 'SILENT' | 'NEVER_RECEIVED' | 'NOT_MONITORED';\n hoursSinceLastIngest: number | null;\n expectedCadenceHours: number | null;\n /** True the first time a source is seen silent, which is when alerting fires. */\n newlySilent: boolean;\n};\n\nconst HOUR_MS = 60 * 60 * 1000;\n\nexport function assess(source: SourceHealth, now = Date.now()): SilenceVerdict {\n const base = {\n slug: source.slug,\n name: source.name,\n expectedCadenceHours: source.expectedCadenceHours,\n };\n\n // A paused source is quiet because someone made it quiet.\n if (source.status === 'PAUSED' || !source.expectedCadenceHours || source.expectedCadenceHours <= 0) {\n return { ...base, health: 'NOT_MONITORED', hoursSinceLastIngest: hoursSince(source.lastIngestedAt, now), newlySilent: false };\n }\n\n const wasSilent = source.healthStatus === 'SILENT' || source.healthStatus === 'NEVER_RECEIVED';\n\n if (!source.lastIngestedAt) {\n // A source that has never received anything is a different problem from one\n // that stopped: usually the webhook was never wired up on the sending side.\n return { ...base, health: 'NEVER_RECEIVED', hoursSinceLastIngest: null, newlySilent: !wasSilent };\n }\n\n const hours = hoursSince(source.lastIngestedAt, now);\n\n // A timestamp we cannot read must not be rounded down to \"arrived just now\" \u2014\n // that is a source reporting healthy forever, which is the exact failure this\n // check exists to catch. Unreadable counts as silent.\n if (hours === null || hours > source.expectedCadenceHours) {\n return { ...base, health: 'SILENT', hoursSinceLastIngest: hours, newlySilent: !wasSilent };\n }\n\n return { ...base, health: 'HEALTHY', hoursSinceLastIngest: hours, newlySilent: false };\n}\n\nfunction hoursSince(iso: string | null, now: number): number | null {\n if (!iso) return null;\n const then = new Date(iso).getTime();\n if (!Number.isFinite(then)) return null;\n return Math.round(((now - then) / HOUR_MS) * 10) / 10;\n}\n\nexport async function loadSources(): Promise<SourceHealth[]> {\n try {\n const res = await coreApi.get<{ data: { intakeSources: { edges: Array<{ node: SourceHealth }> } } }>(\n '/intakeSources?limit=200',\n );\n return (res.data?.intakeSources?.edges ?? []).map((e) => e.node);\n } catch {\n return [];\n }\n}\n\n/**\n * Write the verdict back to the source.\n *\n * `silentSince` is only stamped on the transition, so it keeps meaning \"when we\n * first noticed\" rather than sliding forward on every check.\n */\nexport async function persist(source: SourceHealth, verdict: SilenceVerdict): Promise<void> {\n if (verdict.health === 'NOT_MONITORED') return;\n if (source.healthStatus === verdict.health && !verdict.newlySilent) return;\n\n const patch: Record<string, unknown> = { healthStatus: verdict.health };\n if (verdict.health === 'HEALTHY') {\n patch['silentSince'] = null;\n } else if (verdict.newlySilent) {\n patch['silentSince'] = new Date().toISOString();\n }\n\n await coreApi.patch(`/intakeSources/${source.id}`, patch).catch(() => null);\n}\n\n/**\n * Post one alert.\n *\n * Fired only on the transition into silence, so a source that stays broken for a\n * month produces one message rather than one per check \u2014 an alert channel that\n * repeats itself is an alert channel people mute.\n */\nexport async function sendAlert(source: SourceHealth, verdict: SilenceVerdict): Promise<boolean> {\n if (!source.alertWebhookUrl || !verdict.newlySilent) return false;\n\n const text = verdict.health === 'NEVER_RECEIVED'\n ? `Intake: source \"${source.name}\" (${source.slug}) has never received a payload, and a cadence of ${verdict.expectedCadenceHours}h is configured. The sending side is probably not wired up.`\n : `Intake: source \"${source.name}\" (${source.slug}) has received nothing for ${verdict.hoursSinceLastIngest}h, past its expected cadence of ${verdict.expectedCadenceHours}h.`;\n\n try {\n const res = await fetch(source.alertWebhookUrl, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n // `text` is what Slack, Discord and Teams incoming webhooks all read, so\n // the common case needs no transformation; the rest is there for anything\n // consuming the payload properly.\n body: JSON.stringify({\n text,\n source: { slug: source.slug, name: source.name },\n health: verdict.health,\n hoursSinceLastIngest: verdict.hoursSinceLastIngest,\n expectedCadenceHours: verdict.expectedCadenceHours,\n }),\n });\n if (!res.ok) return false;\n } catch {\n return false;\n }\n\n await coreApi.patch(`/intakeSources/${source.id}`, {\n lastAlertAt: new Date().toISOString(),\n }).catch(() => null);\n\n return true;\n}\n", "import { defineLogicFunction } from 'twenty-sdk/define';\nimport { IDS } from '../constants/universal-identifiers';\nimport { loadSources, assess, persist, sendAlert } from '../lib/silence';\n\n/**\n * Evaluate every source's cadence, record the verdict, alert on new silences.\n *\n * Meant to be called on a timer by something that already exists \u2014 an uptime\n * monitor, a cron job, a Twenty workflow. Calling it more often than the\n * shortest configured cadence costs nothing: the transition test means repeat\n * calls are idempotent and only the first one alerts.\n */\nconst handler = async () => {\n const sources = await loadSources();\n const verdicts = [];\n let alertsSent = 0;\n\n for (const source of sources) {\n const verdict = assess(source);\n await persist(source, verdict);\n if (await sendAlert(source, verdict)) alertsSent++;\n verdicts.push(verdict);\n }\n\n const silent = verdicts.filter((v) => v.health === 'SILENT' || v.health === 'NEVER_RECEIVED');\n\n return {\n statusCode: 200,\n body: {\n checkedAt: new Date().toISOString(),\n sourcesChecked: sources.length,\n monitored: verdicts.filter((v) => v.health !== 'NOT_MONITORED').length,\n silentCount: silent.length,\n alertsSent,\n silent,\n sources: verdicts,\n },\n };\n};\n\nexport default defineLogicFunction({\n universalIdentifier: IDS.CHECK_SILENCE_LOGIC_FUNCTION,\n name: 'intake-check-silence',\n description: 'Check every source against its expected cadence and alert on sources that have fallen silent.',\n timeoutSeconds: 60,\n handler,\n httpRouteTriggerSettings: {\n path: '/intake/sources/check-silence',\n httpMethod: 'POST',\n isAuthRequired: true,\n },\n});\n"],
5
+ "mappings": "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",
6
+ "names": []
7
+ }
@@ -0,0 +1,52 @@
1
+ import { defineLogicFunction } from 'twenty-sdk/define';
2
+ import { IDS } from '../constants/universal-identifiers';
3
+ import { loadSources, assess, persist, sendAlert } from '../lib/silence';
4
+
5
+ /**
6
+ * Evaluate every source's cadence, record the verdict, alert on new silences.
7
+ *
8
+ * Meant to be called on a timer by something that already exists — an uptime
9
+ * monitor, a cron job, a Twenty workflow. Calling it more often than the
10
+ * shortest configured cadence costs nothing: the transition test means repeat
11
+ * calls are idempotent and only the first one alerts.
12
+ */
13
+ const handler = async () => {
14
+ const sources = await loadSources();
15
+ const verdicts = [];
16
+ let alertsSent = 0;
17
+
18
+ for (const source of sources) {
19
+ const verdict = assess(source);
20
+ await persist(source, verdict);
21
+ if (await sendAlert(source, verdict)) alertsSent++;
22
+ verdicts.push(verdict);
23
+ }
24
+
25
+ const silent = verdicts.filter((v) => v.health === 'SILENT' || v.health === 'NEVER_RECEIVED');
26
+
27
+ return {
28
+ statusCode: 200,
29
+ body: {
30
+ checkedAt: new Date().toISOString(),
31
+ sourcesChecked: sources.length,
32
+ monitored: verdicts.filter((v) => v.health !== 'NOT_MONITORED').length,
33
+ silentCount: silent.length,
34
+ alertsSent,
35
+ silent,
36
+ sources: verdicts,
37
+ },
38
+ };
39
+ };
40
+
41
+ export default defineLogicFunction({
42
+ universalIdentifier: IDS.CHECK_SILENCE_LOGIC_FUNCTION,
43
+ name: 'intake-check-silence',
44
+ description: 'Check every source against its expected cadence and alert on sources that have fallen silent.',
45
+ timeoutSeconds: 60,
46
+ handler,
47
+ httpRouteTriggerSettings: {
48
+ path: '/intake/sources/check-silence',
49
+ httpMethod: 'POST',
50
+ isAuthRequired: true,
51
+ },
52
+ });