@tangle-network/agent-integrations 0.69.0 → 0.71.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/data/bundled-adapter-manifests.json +1 -1
- package/dist/{adapters-JMJ8fU1b.js → adapters-CURjrtYm.js} +1033 -19
- package/dist/{adapters-JMJ8fU1b.js.map → adapters-CURjrtYm.js.map} +1 -1
- package/dist/{bundled-manifest-data-Bv0ejHMQ.js → bundled-manifest-data-DUxxlN37.js} +447 -2
- package/dist/{bundled-manifest-data-Bv0ejHMQ.js.map → bundled-manifest-data-DUxxlN37.js.map} +1 -1
- package/dist/connectors/adapters/index.d.ts +2 -2
- package/dist/connectors/adapters/index.js +2 -2
- package/dist/connectors/index.d.ts +3 -3
- package/dist/connectors/index.js +3 -3
- package/dist/{consumer-BvaFhBN_.d.ts → consumer-DaKqPDG-.d.ts} +2 -2
- package/dist/{consumer-BvaFhBN_.d.ts.map → consumer-DaKqPDG-.d.ts.map} +1 -1
- package/dist/consumer.d.ts +1 -1
- package/dist/{index-D0uvn67q.d.ts → index-b05PzTtX.d.ts} +2 -2
- package/dist/{index-D0uvn67q.d.ts.map → index-b05PzTtX.d.ts.map} +1 -1
- package/dist/{index-DkNU5tcV.d.ts → index-udV3gGjx.d.ts} +14 -2
- package/dist/index-udV3gGjx.d.ts.map +1 -0
- package/dist/index.d.ts +4 -4
- package/dist/index.js +5 -5
- package/dist/internal/bundled-manifest-runtime.js +1 -1
- package/dist/mcp.d.ts +1 -1
- package/dist/mcp.js +1 -1
- package/dist/registry.js +1 -1
- package/dist/runtime.d.ts +1 -1
- package/dist/sandbox.d.ts +1 -1
- package/dist/{specs-BqZdi7Jk.js → specs-61KwR2bK.js} +2 -2
- package/dist/{specs-BqZdi7Jk.js.map → specs-61KwR2bK.js.map} +1 -1
- package/dist/specs.js +1 -1
- package/dist/{src-BFIGHp-s.js → src-iUK2d9AZ.js} +3 -3
- package/dist/{src-BFIGHp-s.js.map → src-iUK2d9AZ.js.map} +1 -1
- package/dist/triggers/index.d.ts +78 -0
- package/dist/triggers/index.d.ts.map +1 -0
- package/dist/triggers/index.js +730 -0
- package/dist/triggers/index.js.map +1 -0
- package/dist/webhooks/index.d.ts +1 -1
- package/dist/webhooks/index.d.ts.map +1 -1
- package/dist/webhooks/index.js +76 -1
- package/dist/webhooks/index.js.map +1 -1
- package/docs/integration-execution-audit.md +17 -8
- package/docs/integration-execution-matrix.json +537 -28
- package/package.json +6 -1
- package/dist/index-DkNU5tcV.d.ts.map +0 -1
|
@@ -0,0 +1,730 @@
|
|
|
1
|
+
//#region src/triggers/definitions.ts
|
|
2
|
+
/**
|
|
3
|
+
* The hosted triggers, one per (connector, event), each over that connector's
|
|
4
|
+
* existing READ capabilities. A definition lists candidate records created at
|
|
5
|
+
* or after `since`; `pollHostedTrigger` filters, orders and caps them.
|
|
6
|
+
*/
|
|
7
|
+
const EMAIL = /^[^\s@<>()[\]\\,;:"]{1,64}@[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?(?:\.[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?)+$/i;
|
|
8
|
+
const POLL_SECONDS = 120;
|
|
9
|
+
function object(value) {
|
|
10
|
+
return value && typeof value === "object" && !Array.isArray(value) ? value : {};
|
|
11
|
+
}
|
|
12
|
+
function list(value) {
|
|
13
|
+
return Array.isArray(value) ? value.map(object) : [];
|
|
14
|
+
}
|
|
15
|
+
function text(value, max = 500) {
|
|
16
|
+
return typeof value === "string" && value.trim() ? value.trim().slice(0, max) : null;
|
|
17
|
+
}
|
|
18
|
+
/** `Ada Lovelace <ada@example.com>` and bare addresses alike. */
|
|
19
|
+
function email(value) {
|
|
20
|
+
const raw = text(value, 400);
|
|
21
|
+
if (!raw) return null;
|
|
22
|
+
const candidate = (/<([^<>]+)>/.exec(raw)?.[1] ?? raw).trim().toLowerCase();
|
|
23
|
+
return EMAIL.test(candidate) ? candidate : null;
|
|
24
|
+
}
|
|
25
|
+
function displayName(value) {
|
|
26
|
+
const raw = text(value, 400);
|
|
27
|
+
return raw && raw.includes("<") ? text(raw.slice(0, raw.indexOf("<")).replace(/^"|"$/g, "")) : null;
|
|
28
|
+
}
|
|
29
|
+
/** An ISO time from epoch seconds or milliseconds, a numeric string, or a date string. */
|
|
30
|
+
function isoTime(value) {
|
|
31
|
+
if (typeof value === "number" && Number.isFinite(value)) return new Date(value > 0xe8d4a51000 ? value : value * 1e3).toISOString();
|
|
32
|
+
const raw = text(value, 100);
|
|
33
|
+
if (!raw) return null;
|
|
34
|
+
if (/^\d{10,13}$/.test(raw)) {
|
|
35
|
+
const n = Number(raw);
|
|
36
|
+
return new Date(n > 0xe8d4a51000 ? n : n * 1e3).toISOString();
|
|
37
|
+
}
|
|
38
|
+
const parsed = Date.parse(/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$/.test(raw) ? `${raw.replace(" ", "T")}Z` : raw);
|
|
39
|
+
return Number.isFinite(parsed) ? new Date(parsed).toISOString() : null;
|
|
40
|
+
}
|
|
41
|
+
function person(input) {
|
|
42
|
+
const address = email(input.email);
|
|
43
|
+
const name = text(input.name, 200);
|
|
44
|
+
const company = text(input.company, 200);
|
|
45
|
+
return address || name || company ? {
|
|
46
|
+
email: address,
|
|
47
|
+
name,
|
|
48
|
+
company
|
|
49
|
+
} : null;
|
|
50
|
+
}
|
|
51
|
+
function seconds(isoValue) {
|
|
52
|
+
return Math.floor(Date.parse(isoValue) / 1e3);
|
|
53
|
+
}
|
|
54
|
+
/** Senders that are never a person writing in. */
|
|
55
|
+
const AUTOMATED_SENDER = /^(no-?reply|do-?not-?reply|mailer-daemon|postmaster|notifications?|bounces?)([+.\-_][^@]*)?@/i;
|
|
56
|
+
function mailRecord(input) {
|
|
57
|
+
const occurredAt = isoTime(input.at);
|
|
58
|
+
const from = email(input.from);
|
|
59
|
+
if (!occurredAt || !from || AUTOMATED_SENDER.test(from)) return null;
|
|
60
|
+
const subject = text(input.subject, 300);
|
|
61
|
+
const to = (Array.isArray(input.to) ? input.to : typeof input.to === "string" ? input.to.split(",") : []).map(email).filter((value) => Boolean(value));
|
|
62
|
+
return {
|
|
63
|
+
key: input.key,
|
|
64
|
+
occurredAt,
|
|
65
|
+
person: {
|
|
66
|
+
email: from,
|
|
67
|
+
name: text(input.fromName, 200) ?? displayName(input.from),
|
|
68
|
+
company: null
|
|
69
|
+
},
|
|
70
|
+
summary: subject ? `Email: ${subject}` : "Email with no subject",
|
|
71
|
+
record: {
|
|
72
|
+
messageId: input.key,
|
|
73
|
+
threadId: text(input.threadId, 200),
|
|
74
|
+
from,
|
|
75
|
+
to,
|
|
76
|
+
subject,
|
|
77
|
+
preview: text(input.preview, 1e3)
|
|
78
|
+
}
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
const gmail = {
|
|
82
|
+
provider: "gmail",
|
|
83
|
+
event: "gmail.message.received",
|
|
84
|
+
description: "A new message arrived in the inbox from someone other than the account itself.",
|
|
85
|
+
intervalSeconds: POLL_SECONDS,
|
|
86
|
+
reads: ["list_messages"],
|
|
87
|
+
async list({ read, since }) {
|
|
88
|
+
return list(object(await read("list_messages", {
|
|
89
|
+
query: `in:inbox -from:me after:${seconds(since)}`,
|
|
90
|
+
maxResults: 50
|
|
91
|
+
})).messages).flatMap((message) => {
|
|
92
|
+
const id = text(message.id, 200);
|
|
93
|
+
const record = id ? mailRecord({
|
|
94
|
+
key: id,
|
|
95
|
+
at: message.internalDate,
|
|
96
|
+
from: message.from,
|
|
97
|
+
to: message.to,
|
|
98
|
+
subject: message.subject,
|
|
99
|
+
preview: message.snippet,
|
|
100
|
+
threadId: message.threadId
|
|
101
|
+
}) : null;
|
|
102
|
+
return record ? [record] : [];
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
};
|
|
106
|
+
const outlook = {
|
|
107
|
+
provider: "outlook-mail",
|
|
108
|
+
event: "outlook-mail.message.received",
|
|
109
|
+
description: "A new message arrived in the inbox.",
|
|
110
|
+
intervalSeconds: POLL_SECONDS,
|
|
111
|
+
reads: ["list_messages"],
|
|
112
|
+
async list({ read }) {
|
|
113
|
+
return list(object(await read("list_messages", {
|
|
114
|
+
folder: "inbox",
|
|
115
|
+
top: 50
|
|
116
|
+
})).messages).flatMap((message) => {
|
|
117
|
+
const id = text(message.id, 400);
|
|
118
|
+
const record = id ? mailRecord({
|
|
119
|
+
key: id,
|
|
120
|
+
at: message.receivedDateTime,
|
|
121
|
+
from: message.from,
|
|
122
|
+
fromName: message.fromName,
|
|
123
|
+
to: message.to,
|
|
124
|
+
subject: message.subject,
|
|
125
|
+
preview: message.bodyPreview,
|
|
126
|
+
threadId: message.conversationId
|
|
127
|
+
}) : null;
|
|
128
|
+
return record ? [record] : [];
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
};
|
|
132
|
+
async function hubspotPages(read, capability, key, since) {
|
|
133
|
+
const rows = [];
|
|
134
|
+
let after = null;
|
|
135
|
+
for (let page = 0; page < 3; page++) {
|
|
136
|
+
const data = object(await read(capability, {
|
|
137
|
+
createdAfter: since,
|
|
138
|
+
limit: 100,
|
|
139
|
+
...after ? { after } : {}
|
|
140
|
+
}));
|
|
141
|
+
rows.push(...list(data[key]));
|
|
142
|
+
after = text(data.after, 200);
|
|
143
|
+
if (!after) break;
|
|
144
|
+
}
|
|
145
|
+
return rows;
|
|
146
|
+
}
|
|
147
|
+
const hubspotContact = {
|
|
148
|
+
provider: "hubspot",
|
|
149
|
+
event: "hubspot.contact.created",
|
|
150
|
+
description: "A contact was created, from a form, an import or an integration.",
|
|
151
|
+
intervalSeconds: POLL_SECONDS,
|
|
152
|
+
reads: ["list_new_contacts"],
|
|
153
|
+
async list({ read, since }) {
|
|
154
|
+
return (await hubspotPages(read, "list_new_contacts", "contacts", since)).flatMap((contact) => {
|
|
155
|
+
const id = text(contact.id, 100);
|
|
156
|
+
const occurredAt = isoTime(contact.createdAt);
|
|
157
|
+
if (!id || !occurredAt) return [];
|
|
158
|
+
return [{
|
|
159
|
+
key: `contact:${id}`,
|
|
160
|
+
occurredAt,
|
|
161
|
+
person: person({
|
|
162
|
+
email: contact.email,
|
|
163
|
+
name: contact.name,
|
|
164
|
+
company: contact.company
|
|
165
|
+
}),
|
|
166
|
+
summary: `New HubSpot contact${contact.source ? ` from ${String(contact.source).toLowerCase().replaceAll("_", " ")}` : ""}`,
|
|
167
|
+
record: {
|
|
168
|
+
contactId: id,
|
|
169
|
+
jobTitle: text(contact.jobTitle, 200),
|
|
170
|
+
lifecycleStage: text(contact.lifecycleStage, 100),
|
|
171
|
+
source: text(contact.source, 100)
|
|
172
|
+
}
|
|
173
|
+
}];
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
};
|
|
177
|
+
const hubspotDeal = {
|
|
178
|
+
provider: "hubspot",
|
|
179
|
+
event: "hubspot.deal.created",
|
|
180
|
+
description: "A deal was created.",
|
|
181
|
+
intervalSeconds: POLL_SECONDS,
|
|
182
|
+
reads: ["list_new_deals"],
|
|
183
|
+
async list({ read, since }) {
|
|
184
|
+
return (await hubspotPages(read, "list_new_deals", "deals", since)).flatMap((deal) => {
|
|
185
|
+
const id = text(deal.id, 100);
|
|
186
|
+
const occurredAt = isoTime(deal.createdAt);
|
|
187
|
+
if (!id || !occurredAt) return [];
|
|
188
|
+
return [{
|
|
189
|
+
key: `deal:${id}`,
|
|
190
|
+
occurredAt,
|
|
191
|
+
person: null,
|
|
192
|
+
summary: `New HubSpot deal: ${text(deal.name, 200) ?? id}`,
|
|
193
|
+
record: {
|
|
194
|
+
dealId: id,
|
|
195
|
+
name: text(deal.name, 200),
|
|
196
|
+
stage: text(deal.stage, 100),
|
|
197
|
+
pipeline: text(deal.pipeline, 100),
|
|
198
|
+
amount: deal.amount ?? null,
|
|
199
|
+
ownerId: deal.ownerId ?? null
|
|
200
|
+
}
|
|
201
|
+
}];
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
};
|
|
205
|
+
function attioValue(values, slug, field) {
|
|
206
|
+
return object(list(values[slug])[0])[field];
|
|
207
|
+
}
|
|
208
|
+
const attioPerson = {
|
|
209
|
+
provider: "attio",
|
|
210
|
+
event: "attio.person.created",
|
|
211
|
+
description: "A person record was created.",
|
|
212
|
+
intervalSeconds: POLL_SECONDS,
|
|
213
|
+
reads: ["records.query"],
|
|
214
|
+
async list({ read, since }) {
|
|
215
|
+
return list(object(await read("records.query", {
|
|
216
|
+
object: "people",
|
|
217
|
+
filter: { created_at: { $gte: since } },
|
|
218
|
+
sorts: [{
|
|
219
|
+
attribute: "created_at",
|
|
220
|
+
direction: "asc"
|
|
221
|
+
}],
|
|
222
|
+
limit: 100
|
|
223
|
+
})).data).flatMap((row) => {
|
|
224
|
+
const id = text(object(row.id).record_id, 100);
|
|
225
|
+
const occurredAt = isoTime(row.created_at);
|
|
226
|
+
if (!id || !occurredAt) return [];
|
|
227
|
+
const values = object(row.values);
|
|
228
|
+
const name = attioValue(values, "name", "full_name");
|
|
229
|
+
return [{
|
|
230
|
+
key: `person:${id}`,
|
|
231
|
+
occurredAt,
|
|
232
|
+
person: person({
|
|
233
|
+
email: attioValue(values, "email_addresses", "email_address"),
|
|
234
|
+
name,
|
|
235
|
+
company: null
|
|
236
|
+
}),
|
|
237
|
+
summary: `New Attio person${typeof name === "string" ? `: ${name}` : ""}`,
|
|
238
|
+
record: {
|
|
239
|
+
recordId: id,
|
|
240
|
+
jobTitle: text(attioValue(values, "job_title", "value"), 200)
|
|
241
|
+
}
|
|
242
|
+
}];
|
|
243
|
+
});
|
|
244
|
+
}
|
|
245
|
+
};
|
|
246
|
+
async function pipedriveRecent(read, capability, since) {
|
|
247
|
+
const rows = [];
|
|
248
|
+
const sinceMs = Date.parse(since);
|
|
249
|
+
for (let start = 0; start < 300; start += 100) {
|
|
250
|
+
const page = list(object(await read(capability, {
|
|
251
|
+
sort: "add_time DESC",
|
|
252
|
+
start,
|
|
253
|
+
limit: 100
|
|
254
|
+
})).data);
|
|
255
|
+
rows.push(...page);
|
|
256
|
+
const oldest = page.at(-1);
|
|
257
|
+
if (page.length < 100 || !oldest || Date.parse(isoTime(oldest.add_time) ?? "") < sinceMs) break;
|
|
258
|
+
}
|
|
259
|
+
return rows;
|
|
260
|
+
}
|
|
261
|
+
function pipedriveEmail(value) {
|
|
262
|
+
const entries = list(value);
|
|
263
|
+
return (entries.find((entry) => entry.primary === true) ?? entries[0])?.value;
|
|
264
|
+
}
|
|
265
|
+
const pipedrivePerson = {
|
|
266
|
+
provider: "pipedrive",
|
|
267
|
+
event: "pipedrive.person.created",
|
|
268
|
+
description: "A person was added.",
|
|
269
|
+
intervalSeconds: POLL_SECONDS,
|
|
270
|
+
reads: ["persons.list"],
|
|
271
|
+
async list({ read, since }) {
|
|
272
|
+
return (await pipedriveRecent(read, "persons.list", since)).flatMap((row) => {
|
|
273
|
+
const id = row.id !== void 0 ? String(row.id) : null;
|
|
274
|
+
const occurredAt = isoTime(row.add_time);
|
|
275
|
+
if (!id || !occurredAt) return [];
|
|
276
|
+
const company = text(row.org_name, 200) ?? text(object(row.org_id).name, 200);
|
|
277
|
+
return [{
|
|
278
|
+
key: `person:${id}`,
|
|
279
|
+
occurredAt,
|
|
280
|
+
person: person({
|
|
281
|
+
email: pipedriveEmail(row.email),
|
|
282
|
+
name: row.name,
|
|
283
|
+
company
|
|
284
|
+
}),
|
|
285
|
+
summary: `New Pipedrive person${row.name ? `: ${String(row.name)}` : ""}`,
|
|
286
|
+
record: {
|
|
287
|
+
personId: id,
|
|
288
|
+
ownerId: object(row.owner_id).id ?? row.owner_id ?? null
|
|
289
|
+
}
|
|
290
|
+
}];
|
|
291
|
+
});
|
|
292
|
+
}
|
|
293
|
+
};
|
|
294
|
+
const pipedriveDeal = {
|
|
295
|
+
provider: "pipedrive",
|
|
296
|
+
event: "pipedrive.deal.created",
|
|
297
|
+
description: "A deal was added.",
|
|
298
|
+
intervalSeconds: POLL_SECONDS,
|
|
299
|
+
reads: ["deals.list"],
|
|
300
|
+
async list({ read, since }) {
|
|
301
|
+
return (await pipedriveRecent(read, "deals.list", since)).flatMap((row) => {
|
|
302
|
+
const id = row.id !== void 0 ? String(row.id) : null;
|
|
303
|
+
const occurredAt = isoTime(row.add_time);
|
|
304
|
+
if (!id || !occurredAt) return [];
|
|
305
|
+
const contact = object(row.person_id);
|
|
306
|
+
return [{
|
|
307
|
+
key: `deal:${id}`,
|
|
308
|
+
occurredAt,
|
|
309
|
+
person: person({
|
|
310
|
+
email: pipedriveEmail(contact.email),
|
|
311
|
+
name: contact.name ?? row.person_name,
|
|
312
|
+
company: text(object(row.org_id).name, 200) ?? row.org_name
|
|
313
|
+
}),
|
|
314
|
+
summary: `New Pipedrive deal: ${text(row.title, 200) ?? id}`,
|
|
315
|
+
record: {
|
|
316
|
+
dealId: id,
|
|
317
|
+
title: text(row.title, 200),
|
|
318
|
+
value: row.value ?? null,
|
|
319
|
+
currency: text(row.currency, 10),
|
|
320
|
+
stageId: row.stage_id ?? null
|
|
321
|
+
}
|
|
322
|
+
}];
|
|
323
|
+
});
|
|
324
|
+
}
|
|
325
|
+
};
|
|
326
|
+
/** A form answer's value as text, whatever its type. */
|
|
327
|
+
function answerText(answer) {
|
|
328
|
+
const choice = object(answer.choice);
|
|
329
|
+
const choices = object(answer.choices);
|
|
330
|
+
const value = answer.email ?? answer.text ?? answer.url ?? answer.phone_number ?? answer.number ?? answer.date ?? answer.boolean ?? choice.label ?? (Array.isArray(choices.labels) ? choices.labels.join(", ") : void 0);
|
|
331
|
+
return value === void 0 || value === null ? null : String(value).slice(0, 1e3);
|
|
332
|
+
}
|
|
333
|
+
/** Name, company and title from a form's answers, matched on the question titles. */
|
|
334
|
+
function formPerson(fields) {
|
|
335
|
+
const pick = (pattern) => fields.find((field) => pattern.test(field.title))?.value ?? null;
|
|
336
|
+
return person({
|
|
337
|
+
email: fields.find((field) => field.isEmail)?.value ?? fields.map((field) => email(field.value)).find(Boolean),
|
|
338
|
+
name: pick(/\bname\b/i),
|
|
339
|
+
company: pick(/company|organi[sz]ation|business|team/i)
|
|
340
|
+
});
|
|
341
|
+
}
|
|
342
|
+
const HOSTED_TRIGGER_DEFINITIONS = [
|
|
343
|
+
gmail,
|
|
344
|
+
outlook,
|
|
345
|
+
hubspotContact,
|
|
346
|
+
hubspotDeal,
|
|
347
|
+
attioPerson,
|
|
348
|
+
pipedrivePerson,
|
|
349
|
+
pipedriveDeal,
|
|
350
|
+
{
|
|
351
|
+
provider: "typeform",
|
|
352
|
+
event: "typeform.response.submitted",
|
|
353
|
+
description: "A completed response was submitted to any of the account's forms.",
|
|
354
|
+
intervalSeconds: POLL_SECONDS,
|
|
355
|
+
reads: [
|
|
356
|
+
"forms.list",
|
|
357
|
+
"forms.get",
|
|
358
|
+
"responses.list"
|
|
359
|
+
],
|
|
360
|
+
async list({ read, since }) {
|
|
361
|
+
const forms = list(object(await read("forms.list", { page_size: 20 })).items).slice(0, 10);
|
|
362
|
+
const records = [];
|
|
363
|
+
for (const form of forms) {
|
|
364
|
+
const formId = text(form.id, 100);
|
|
365
|
+
if (!formId) continue;
|
|
366
|
+
const responses = list(object(await read("responses.list", {
|
|
367
|
+
form_id: formId,
|
|
368
|
+
since,
|
|
369
|
+
completed: true,
|
|
370
|
+
sort: "submitted_at,asc",
|
|
371
|
+
page_size: 50
|
|
372
|
+
})).items);
|
|
373
|
+
if (responses.length === 0) continue;
|
|
374
|
+
const definition = object(await read("forms.get", { form_id: formId }));
|
|
375
|
+
const titles = new Map(list(definition.fields).map((field) => [String(field.id), text(field.title, 200) ?? String(field.ref ?? "")]));
|
|
376
|
+
for (const response of responses) {
|
|
377
|
+
const token = text(response.token, 200) ?? text(response.response_id, 200);
|
|
378
|
+
const occurredAt = isoTime(response.submitted_at);
|
|
379
|
+
if (!token || !occurredAt) continue;
|
|
380
|
+
const fields = list(response.answers).flatMap((answer) => {
|
|
381
|
+
const value = answerText(answer);
|
|
382
|
+
const field = object(answer.field);
|
|
383
|
+
return value ? [{
|
|
384
|
+
title: titles.get(String(field.id)) ?? String(field.ref ?? "answer"),
|
|
385
|
+
value,
|
|
386
|
+
isEmail: answer.type === "email"
|
|
387
|
+
}] : [];
|
|
388
|
+
});
|
|
389
|
+
records.push({
|
|
390
|
+
key: `response:${token}`,
|
|
391
|
+
occurredAt,
|
|
392
|
+
person: formPerson(fields),
|
|
393
|
+
summary: `Typeform response to "${text(form.title, 200) ?? formId}"`,
|
|
394
|
+
record: {
|
|
395
|
+
formId,
|
|
396
|
+
formTitle: text(form.title, 200),
|
|
397
|
+
responseId: token,
|
|
398
|
+
answers: fields.map(({ title, value }) => ({
|
|
399
|
+
question: title,
|
|
400
|
+
answer: value
|
|
401
|
+
}))
|
|
402
|
+
}
|
|
403
|
+
});
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
return records;
|
|
407
|
+
}
|
|
408
|
+
},
|
|
409
|
+
{
|
|
410
|
+
provider: "tally",
|
|
411
|
+
event: "tally.submission.created",
|
|
412
|
+
description: "A completed submission arrived on any of the account's forms.",
|
|
413
|
+
intervalSeconds: POLL_SECONDS,
|
|
414
|
+
reads: ["forms.list", "submissions.list"],
|
|
415
|
+
async list({ read, since }) {
|
|
416
|
+
const forms = list(object(await read("forms.list", { limit: 20 })).items).slice(0, 10);
|
|
417
|
+
const records = [];
|
|
418
|
+
for (const form of forms) {
|
|
419
|
+
const formId = text(form.id, 100);
|
|
420
|
+
if (!formId) continue;
|
|
421
|
+
const data = object(await read("submissions.list", {
|
|
422
|
+
formId,
|
|
423
|
+
filter: "completed",
|
|
424
|
+
startDate: since,
|
|
425
|
+
limit: 50
|
|
426
|
+
}));
|
|
427
|
+
const questions = new Map(list(data.questions).map((question) => [String(question.id), {
|
|
428
|
+
title: text(question.title, 200) ?? "answer",
|
|
429
|
+
type: String(question.type ?? "")
|
|
430
|
+
}]));
|
|
431
|
+
for (const submission of list(data.submissions)) {
|
|
432
|
+
const id = text(submission.id, 100);
|
|
433
|
+
const occurredAt = isoTime(submission.submittedAt ?? submission.createdAt);
|
|
434
|
+
if (!id || !occurredAt) continue;
|
|
435
|
+
const fields = list(submission.responses).flatMap((response) => {
|
|
436
|
+
const question = questions.get(String(response.questionId));
|
|
437
|
+
const raw = response.value ?? response.answer;
|
|
438
|
+
const value = raw === void 0 || raw === null ? null : (Array.isArray(raw) ? raw.join(", ") : typeof raw === "object" ? JSON.stringify(raw) : String(raw)).slice(0, 1e3);
|
|
439
|
+
return value ? [{
|
|
440
|
+
title: question?.title ?? "answer",
|
|
441
|
+
value,
|
|
442
|
+
isEmail: question?.type === "INPUT_EMAIL"
|
|
443
|
+
}] : [];
|
|
444
|
+
});
|
|
445
|
+
records.push({
|
|
446
|
+
key: `submission:${id}`,
|
|
447
|
+
occurredAt,
|
|
448
|
+
person: formPerson(fields),
|
|
449
|
+
summary: `Tally submission to "${text(form.name, 200) ?? formId}"`,
|
|
450
|
+
record: {
|
|
451
|
+
formId,
|
|
452
|
+
formName: text(form.name, 200),
|
|
453
|
+
submissionId: id,
|
|
454
|
+
answers: fields.map(({ title, value }) => ({
|
|
455
|
+
question: title,
|
|
456
|
+
answer: value
|
|
457
|
+
}))
|
|
458
|
+
}
|
|
459
|
+
});
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
return records;
|
|
463
|
+
}
|
|
464
|
+
},
|
|
465
|
+
{
|
|
466
|
+
provider: "webflow",
|
|
467
|
+
event: "webflow.form.submitted",
|
|
468
|
+
description: "A form on any of the account's sites was submitted.",
|
|
469
|
+
intervalSeconds: POLL_SECONDS,
|
|
470
|
+
reads: [
|
|
471
|
+
"sites.list",
|
|
472
|
+
"forms.list",
|
|
473
|
+
"forms.submissions"
|
|
474
|
+
],
|
|
475
|
+
async list({ read }) {
|
|
476
|
+
const sites = list(object(await read("sites.list", {})).sites).slice(0, 5);
|
|
477
|
+
const records = [];
|
|
478
|
+
for (const site of sites) {
|
|
479
|
+
const siteId = text(site.id, 100);
|
|
480
|
+
if (!siteId) continue;
|
|
481
|
+
const forms = list(object(await read("forms.list", {
|
|
482
|
+
siteId,
|
|
483
|
+
limit: 20
|
|
484
|
+
})).forms).slice(0, 10);
|
|
485
|
+
for (const form of forms) {
|
|
486
|
+
const formId = text(form.id, 100);
|
|
487
|
+
if (!formId) continue;
|
|
488
|
+
const submissions = [];
|
|
489
|
+
for (let offset = 0; offset < 1e3; offset += 100) {
|
|
490
|
+
const page = list(object(await read("forms.submissions", {
|
|
491
|
+
formId,
|
|
492
|
+
offset,
|
|
493
|
+
limit: 100
|
|
494
|
+
})).formSubmissions);
|
|
495
|
+
submissions.push(...page);
|
|
496
|
+
if (page.length < 100) break;
|
|
497
|
+
}
|
|
498
|
+
for (const submission of submissions) {
|
|
499
|
+
const id = text(submission.id, 100);
|
|
500
|
+
const occurredAt = isoTime(submission.dateSubmitted);
|
|
501
|
+
if (!id || !occurredAt) continue;
|
|
502
|
+
const fields = Object.entries(object(submission.formResponse)).flatMap(([title, value]) => value === null || value === void 0 || value === "" ? [] : [{
|
|
503
|
+
title,
|
|
504
|
+
value: String(value).slice(0, 1e3),
|
|
505
|
+
isEmail: /e-?mail/i.test(title)
|
|
506
|
+
}]);
|
|
507
|
+
records.push({
|
|
508
|
+
key: `submission:${id}`,
|
|
509
|
+
occurredAt,
|
|
510
|
+
person: formPerson(fields),
|
|
511
|
+
summary: `Webflow form "${text(form.displayName, 200) ?? formId}" submitted on ${text(site.displayName, 200) ?? siteId}`,
|
|
512
|
+
record: {
|
|
513
|
+
siteId,
|
|
514
|
+
formId,
|
|
515
|
+
submissionId: id,
|
|
516
|
+
answers: fields.map(({ title, value }) => ({
|
|
517
|
+
question: title,
|
|
518
|
+
answer: value
|
|
519
|
+
}))
|
|
520
|
+
}
|
|
521
|
+
});
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
return records;
|
|
526
|
+
}
|
|
527
|
+
},
|
|
528
|
+
{
|
|
529
|
+
provider: "calendly",
|
|
530
|
+
event: "calendly.invitee.created",
|
|
531
|
+
description: "Someone booked a meeting.",
|
|
532
|
+
intervalSeconds: POLL_SECONDS,
|
|
533
|
+
reads: [
|
|
534
|
+
"user.get-current",
|
|
535
|
+
"scheduled-events.list",
|
|
536
|
+
"scheduled-events.list-invitees"
|
|
537
|
+
],
|
|
538
|
+
async list({ read, since }) {
|
|
539
|
+
const me = object(await read("user.get-current", {}));
|
|
540
|
+
const user = text(object(me.resource).uri, 300) ?? text(me.uri, 300);
|
|
541
|
+
if (!user) throw new Error("calendly.invitee.created: Calendly returned no current user");
|
|
542
|
+
const events = list(object(await read("scheduled-events.list", {
|
|
543
|
+
user,
|
|
544
|
+
status: "active",
|
|
545
|
+
min_start_time: since,
|
|
546
|
+
count: 100
|
|
547
|
+
})).collection);
|
|
548
|
+
const records = [];
|
|
549
|
+
const sinceMs = Date.parse(since);
|
|
550
|
+
for (const event of events) {
|
|
551
|
+
const uri = text(event.uri, 300);
|
|
552
|
+
const created = isoTime(event.created_at);
|
|
553
|
+
if (!uri || !created || Date.parse(created) < sinceMs) continue;
|
|
554
|
+
const uuid = uri.split("/").pop();
|
|
555
|
+
for (const invitee of list(object(await read("scheduled-events.list-invitees", {
|
|
556
|
+
uuid,
|
|
557
|
+
count: 25
|
|
558
|
+
})).collection)) {
|
|
559
|
+
const occurredAt = isoTime(invitee.created_at) ?? created;
|
|
560
|
+
const answers = list(invitee.questions_and_answers).map((qa) => ({
|
|
561
|
+
question: text(qa.question, 200) ?? "",
|
|
562
|
+
answer: text(qa.answer, 1e3) ?? ""
|
|
563
|
+
}));
|
|
564
|
+
const inviteeKey = text(invitee.uri, 300) ?? `${uuid}:${email(invitee.email) ?? ""}`;
|
|
565
|
+
records.push({
|
|
566
|
+
key: `invitee:${inviteeKey}`,
|
|
567
|
+
occurredAt,
|
|
568
|
+
person: person({
|
|
569
|
+
email: invitee.email,
|
|
570
|
+
name: invitee.name,
|
|
571
|
+
company: answers.find((qa) => /company|organi[sz]ation/i.test(qa.question))?.answer
|
|
572
|
+
}),
|
|
573
|
+
summary: `Booked "${text(event.name, 200) ?? "a meeting"}" for ${text(event.start_time, 40) ?? "a future time"}`,
|
|
574
|
+
record: {
|
|
575
|
+
eventUri: uri,
|
|
576
|
+
start: text(event.start_time, 40),
|
|
577
|
+
end: text(event.end_time, 40),
|
|
578
|
+
answers
|
|
579
|
+
}
|
|
580
|
+
});
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
return records;
|
|
584
|
+
}
|
|
585
|
+
},
|
|
586
|
+
{
|
|
587
|
+
provider: "cal-com",
|
|
588
|
+
event: "cal-com.booking.created",
|
|
589
|
+
description: "Someone booked a meeting.",
|
|
590
|
+
intervalSeconds: POLL_SECONDS,
|
|
591
|
+
reads: ["bookings.list"],
|
|
592
|
+
async list({ read }) {
|
|
593
|
+
const bookings = [];
|
|
594
|
+
let cursor = null;
|
|
595
|
+
for (let page = 0; page < 3; page++) {
|
|
596
|
+
const data = object(await read("bookings.list", {
|
|
597
|
+
status: "upcoming",
|
|
598
|
+
limit: 100,
|
|
599
|
+
...cursor ? { cursor } : {}
|
|
600
|
+
}));
|
|
601
|
+
bookings.push(...list(Array.isArray(data.data) ? data.data : object(data.data).bookings ?? data.bookings));
|
|
602
|
+
cursor = text(object(data.pagination).nextCursor, 500);
|
|
603
|
+
if (!cursor) break;
|
|
604
|
+
}
|
|
605
|
+
return bookings.flatMap((booking) => {
|
|
606
|
+
const uid = text(booking.uid, 200) ?? (booking.id !== void 0 ? String(booking.id) : null);
|
|
607
|
+
const occurredAt = isoTime(booking.createdAt);
|
|
608
|
+
if (!uid || !occurredAt) return [];
|
|
609
|
+
const attendee = list(booking.attendees)[0] ?? {};
|
|
610
|
+
return [{
|
|
611
|
+
key: `booking:${uid}`,
|
|
612
|
+
occurredAt,
|
|
613
|
+
person: person({
|
|
614
|
+
email: attendee.email,
|
|
615
|
+
name: attendee.name,
|
|
616
|
+
company: null
|
|
617
|
+
}),
|
|
618
|
+
summary: `Booked "${text(booking.title, 200) ?? "a meeting"}" for ${text(booking.start ?? booking.startTime, 40) ?? "a future time"}`,
|
|
619
|
+
record: {
|
|
620
|
+
bookingUid: uid,
|
|
621
|
+
start: text(booking.start ?? booking.startTime, 40),
|
|
622
|
+
end: text(booking.end ?? booking.endTime, 40),
|
|
623
|
+
description: text(booking.description, 2e3),
|
|
624
|
+
eventTypeId: booking.eventTypeId ?? object(booking.eventType).id ?? null
|
|
625
|
+
}
|
|
626
|
+
}];
|
|
627
|
+
});
|
|
628
|
+
}
|
|
629
|
+
},
|
|
630
|
+
{
|
|
631
|
+
provider: "intercom",
|
|
632
|
+
event: "intercom.conversation.created",
|
|
633
|
+
description: "A customer or lead started a new conversation.",
|
|
634
|
+
intervalSeconds: POLL_SECONDS,
|
|
635
|
+
reads: ["tickets.search"],
|
|
636
|
+
async list({ read, since }) {
|
|
637
|
+
return list(object(await read("tickets.search", { body: {
|
|
638
|
+
query: {
|
|
639
|
+
field: "created_at",
|
|
640
|
+
operator: ">",
|
|
641
|
+
value: seconds(since) - 1
|
|
642
|
+
},
|
|
643
|
+
pagination: { per_page: 50 }
|
|
644
|
+
} })).conversations).flatMap((conversation) => {
|
|
645
|
+
const id = text(conversation.id, 100);
|
|
646
|
+
const occurredAt = isoTime(conversation.created_at);
|
|
647
|
+
if (!id || !occurredAt) return [];
|
|
648
|
+
const source = object(conversation.source);
|
|
649
|
+
const author = object(source.author);
|
|
650
|
+
if (author.type === "admin" || author.type === "bot") return [];
|
|
651
|
+
const body = text(String(source.body ?? "").replace(/<[^>]+>/g, " ").replace(/\s+/g, " "), 2e3);
|
|
652
|
+
return [{
|
|
653
|
+
key: `conversation:${id}`,
|
|
654
|
+
occurredAt,
|
|
655
|
+
person: person({
|
|
656
|
+
email: author.email,
|
|
657
|
+
name: author.name,
|
|
658
|
+
company: null
|
|
659
|
+
}),
|
|
660
|
+
summary: `New Intercom conversation${text(source.subject, 200) ? `: ${text(source.subject, 200)}` : ""}`,
|
|
661
|
+
record: {
|
|
662
|
+
conversationId: id,
|
|
663
|
+
subject: text(source.subject, 300),
|
|
664
|
+
body,
|
|
665
|
+
deliveredAs: text(source.delivered_as, 50)
|
|
666
|
+
}
|
|
667
|
+
}];
|
|
668
|
+
});
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
];
|
|
672
|
+
//#endregion
|
|
673
|
+
//#region src/triggers/index.ts
|
|
674
|
+
const MAX_EVENTS_PER_POLL = 50;
|
|
675
|
+
/** Poll one subscription once. `cursor` is null on the first poll after it is created. */
|
|
676
|
+
async function pollHostedTrigger(trigger, context) {
|
|
677
|
+
if (!context.cursor) return {
|
|
678
|
+
events: [],
|
|
679
|
+
cursor: {
|
|
680
|
+
since: context.now.toISOString(),
|
|
681
|
+
seen: []
|
|
682
|
+
},
|
|
683
|
+
more: false
|
|
684
|
+
};
|
|
685
|
+
const cursor = context.cursor;
|
|
686
|
+
const sinceMs = Date.parse(cursor.since);
|
|
687
|
+
const seen = new Set(cursor.seen);
|
|
688
|
+
const fresh = (await trigger.list({
|
|
689
|
+
read: context.read,
|
|
690
|
+
since: cursor.since,
|
|
691
|
+
now: context.now
|
|
692
|
+
})).filter((record) => !seen.has(record.key) && Number.isFinite(Date.parse(record.occurredAt)) && Date.parse(record.occurredAt) >= sinceMs).sort((a, b) => Date.parse(a.occurredAt) - Date.parse(b.occurredAt));
|
|
693
|
+
const emitted = fresh.slice(0, 50);
|
|
694
|
+
const since = emitted.reduce((latest, record) => Date.parse(record.occurredAt) > Date.parse(latest) ? new Date(Date.parse(record.occurredAt)).toISOString() : latest, cursor.since);
|
|
695
|
+
const keys = [...cursor.seen.filter((key) => !emitted.some((record) => record.key === key)), ...emitted.map((record) => record.key)];
|
|
696
|
+
return {
|
|
697
|
+
events: emitted.map((record) => ({
|
|
698
|
+
id: `${trigger.provider}:${record.key}`,
|
|
699
|
+
type: trigger.event,
|
|
700
|
+
occurredAt: new Date(Date.parse(record.occurredAt)).toISOString(),
|
|
701
|
+
person: record.person,
|
|
702
|
+
summary: record.summary,
|
|
703
|
+
record: record.record
|
|
704
|
+
})),
|
|
705
|
+
cursor: {
|
|
706
|
+
since,
|
|
707
|
+
seen: keys.slice(-200)
|
|
708
|
+
},
|
|
709
|
+
more: fresh.length > emitted.length
|
|
710
|
+
};
|
|
711
|
+
}
|
|
712
|
+
function hostedTrigger(provider, event) {
|
|
713
|
+
return HOSTED_TRIGGER_DEFINITIONS.find((trigger) => trigger.provider === provider && trigger.event === event);
|
|
714
|
+
}
|
|
715
|
+
/** Providers with at least one hosted trigger. */
|
|
716
|
+
const HOSTED_TRIGGER_PROVIDER_IDS = [...new Set(HOSTED_TRIGGER_DEFINITIONS.map((trigger) => trigger.provider))];
|
|
717
|
+
/** The closed catalog of a provider's hosted trigger events, for validating `on.provider_event.event`. */
|
|
718
|
+
function hostedTriggerEventCatalog(provider) {
|
|
719
|
+
const events = HOSTED_TRIGGER_DEFINITIONS.filter((trigger) => trigger.provider === provider);
|
|
720
|
+
if (events.length === 0) return void 0;
|
|
721
|
+
return {
|
|
722
|
+
namespace: `${provider}.`,
|
|
723
|
+
closed: true,
|
|
724
|
+
events: events.map((trigger) => ({ id: trigger.event }))
|
|
725
|
+
};
|
|
726
|
+
}
|
|
727
|
+
//#endregion
|
|
728
|
+
export { HOSTED_TRIGGER_DEFINITIONS, HOSTED_TRIGGER_PROVIDER_IDS, MAX_EVENTS_PER_POLL, hostedTrigger, hostedTriggerEventCatalog, pollHostedTrigger };
|
|
729
|
+
|
|
730
|
+
//# sourceMappingURL=index.js.map
|