@deployfoundation/foundation-deploy 0.2.51 → 0.2.52
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 +4 -1
- package/dist/bin/app.js +6 -3
- package/dist/bin/foundation-deploy.js +139 -305
- package/dist/bin/release-account.js +1 -0
- package/dist/chunk-3614v9rr.js +907 -0
- package/dist/{chunk-efqdhd2e.js → chunk-59dpqyap.js} +254 -90
- package/dist/chunk-5wgwscd0.js +4 -0
- package/dist/{chunk-gnz747z2.js → chunk-96fjaysn.js} +204 -884
- package/dist/chunk-9zx95xs6.js +259 -0
- package/dist/chunk-jf5rw6h4.js +1117 -0
- package/dist/chunk-m76jt5rc.js +376 -0
- package/dist/chunk-mh1206ne.js +1764 -0
- package/dist/chunk-p7zkm4pp.js +151 -0
- package/dist/chunk-pxhftba1.js +336 -0
- package/dist/chunk-rxetj2p9.js +466 -0
- package/dist/chunk-sgdcmme0.js +301 -0
- package/dist/chunk-tey0gh86.js +38 -0
- package/dist/src/index.js +17 -13
- package/package.json +3 -1
- package/src/deploy/cli.ts +71 -0
- package/src/deploy/crm.ts +491 -0
- package/src/stacks/agent-stack.ts +34 -11
- package/src/stacks/api-stack.ts +340 -87
- package/src/stacks/data-stack.ts +27 -1
|
@@ -0,0 +1,907 @@
|
|
|
1
|
+
import {
|
|
2
|
+
wrapUntrusted
|
|
3
|
+
} from "./chunk-96fjaysn.js";
|
|
4
|
+
|
|
5
|
+
// ../connectors/src/crm.ts
|
|
6
|
+
var CRM_MAX_BATCH_SIZE = 100;
|
|
7
|
+
var CRM_MAX_REQUEST_BYTES = 512 * 1024;
|
|
8
|
+
var CRM_MAX_RESPONSE_BYTES = 512 * 1024;
|
|
9
|
+
var CRM_MAX_ID_CHARS = 256;
|
|
10
|
+
var CRM_MAX_IDEMPOTENCY_KEY_CHARS = 256;
|
|
11
|
+
var CRM_MAX_NAME_CHARS = 512;
|
|
12
|
+
var CRM_MAX_DOMAIN_CHARS = 253;
|
|
13
|
+
var CRM_MAX_WEBSITE_CHARS = 2048;
|
|
14
|
+
var CRM_MAX_SOURCE_CHARS = 128;
|
|
15
|
+
var CRM_MAX_EMAIL_CHARS = 320;
|
|
16
|
+
var CRM_MAX_PERSON_FIELD_CHARS = 256;
|
|
17
|
+
var CRM_MAX_STATUS_CHARS = 128;
|
|
18
|
+
var CRM_MAX_ACTIVITY_TYPE_CHARS = 128;
|
|
19
|
+
var CRM_MAX_SUMMARY_CHARS = 4000;
|
|
20
|
+
var CRM_MAX_CURSOR_CHARS = 8192;
|
|
21
|
+
var CRM_MAX_SEARCH_LIMIT = 100;
|
|
22
|
+
var CRM_MAX_ACTIVITY_PAGE_SIZE = 100;
|
|
23
|
+
var CRM_MAX_CONFIG_VALUES = 100;
|
|
24
|
+
var CRM_MAX_ATTRIBUTE_COUNT = 50;
|
|
25
|
+
var CRM_MAX_ATTRIBUTE_KEY_CHARS = 64;
|
|
26
|
+
var CRM_MAX_ATTRIBUTE_STRING_CHARS = 1024;
|
|
27
|
+
var CRM_MAX_TIMESTAMP_CHARS = 64;
|
|
28
|
+
var CRM_MAX_OUTREACH_QUEUE_ITEMS = 25;
|
|
29
|
+
var CRM_MAX_TIMEZONE_CHARS = 64;
|
|
30
|
+
var CRM_MAX_OUTREACH_CANDIDATES_PAGE = 200;
|
|
31
|
+
var CRM_MAX_VERSION = 1e9;
|
|
32
|
+
var CONTROL_CHARS = /[\u0000-\u001f\u007f]/;
|
|
33
|
+
var SIMPLE_EMAIL = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
34
|
+
var ISO_TIMESTAMP = /^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2})(?:\.\d{1,3})?(Z|[+-]\d{2}:\d{2})$/;
|
|
35
|
+
var CRM_STATUS_SETTERS = ["agent", "person", "system"];
|
|
36
|
+
var CRM_NOTE_ACTIVITY = "note";
|
|
37
|
+
var CRM_ACTIVITY_AUTHOR_ATTRIBUTE = "by";
|
|
38
|
+
var CRM_OUTREACH_DRAFTED_ACTIVITY = "email_drafted";
|
|
39
|
+
var CRM_OUTREACH_SENT_ACTIVITY = "email_sent";
|
|
40
|
+
var CRM_OUTREACH_REPLY_ACTIVITY = "reply_received";
|
|
41
|
+
var CRM_REFUSAL_REASONS = [
|
|
42
|
+
"weekly_limit_reached",
|
|
43
|
+
"follow_up_limit_reached",
|
|
44
|
+
"already_contacted",
|
|
45
|
+
"not_contacted",
|
|
46
|
+
"max_follow_ups_reached",
|
|
47
|
+
"suppressed",
|
|
48
|
+
"status_locked",
|
|
49
|
+
"transition_not_allowed"
|
|
50
|
+
];
|
|
51
|
+
var CRM_PROVIDER_OPERATIONS = [
|
|
52
|
+
"search",
|
|
53
|
+
"get_record",
|
|
54
|
+
"get_activity",
|
|
55
|
+
"upsert_records",
|
|
56
|
+
"update_status",
|
|
57
|
+
"log_activity",
|
|
58
|
+
"update_note",
|
|
59
|
+
"outreach_state",
|
|
60
|
+
"outreach_candidates",
|
|
61
|
+
"delete_record"
|
|
62
|
+
];
|
|
63
|
+
var CRM_REQUIRED_PROVIDER_OPERATIONS = [
|
|
64
|
+
"search",
|
|
65
|
+
"get_record",
|
|
66
|
+
"upsert_records",
|
|
67
|
+
"update_status",
|
|
68
|
+
"log_activity"
|
|
69
|
+
];
|
|
70
|
+
|
|
71
|
+
class CrmInputError extends Error {
|
|
72
|
+
constructor(message) {
|
|
73
|
+
super(message);
|
|
74
|
+
this.name = "CrmInputError";
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
class CrmResponseError extends Error {
|
|
79
|
+
constructor() {
|
|
80
|
+
super("CRM response did not match the bounded contract");
|
|
81
|
+
this.name = "CrmResponseError";
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
function crmResponseFitsBudget(value) {
|
|
85
|
+
try {
|
|
86
|
+
const serialized = JSON.stringify(value);
|
|
87
|
+
if (Buffer.byteLength(JSON.stringify({ ok: true, result: value }), "utf8") > CRM_MAX_RESPONSE_BYTES) {
|
|
88
|
+
return false;
|
|
89
|
+
}
|
|
90
|
+
return Buffer.byteLength(wrapUntrusted("CRM data", serialized), "utf8") <= CRM_MAX_RESPONSE_BYTES;
|
|
91
|
+
} catch {
|
|
92
|
+
return false;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
function parseCrmUpsertRecordsArgs(value, maxBatchSize = CRM_MAX_BATCH_SIZE) {
|
|
96
|
+
const input = inputObject(value, "upsert request");
|
|
97
|
+
assertAllowedFields(input, ["idempotencyKey", "companies", "contacts"], "upsert request");
|
|
98
|
+
const batchSize = validateBatchSize(maxBatchSize);
|
|
99
|
+
const idempotencyKey = parseIdempotencyKey(input.idempotencyKey);
|
|
100
|
+
const companies = input.companies === undefined ? [] : parseRecordArray(input.companies, "companies", parseCompany);
|
|
101
|
+
const contacts = input.contacts === undefined ? [] : parseRecordArray(input.contacts, "contacts", parseContact);
|
|
102
|
+
const count = companies.length + contacts.length;
|
|
103
|
+
if (count === 0)
|
|
104
|
+
throw new CrmInputError("upsert request must contain at least one record");
|
|
105
|
+
if (count > batchSize)
|
|
106
|
+
throw new CrmInputError(`upsert request may contain at most ${batchSize} records`);
|
|
107
|
+
assertDistinctExternalIds(companies, "companies");
|
|
108
|
+
assertDistinctExternalIds(contacts, "contacts");
|
|
109
|
+
const result = {
|
|
110
|
+
idempotencyKey,
|
|
111
|
+
...companies.length === 0 ? {} : { companies },
|
|
112
|
+
...contacts.length === 0 ? {} : { contacts }
|
|
113
|
+
};
|
|
114
|
+
assertRequestSize(result);
|
|
115
|
+
return result;
|
|
116
|
+
}
|
|
117
|
+
function parseCrmSearchResponse(value) {
|
|
118
|
+
try {
|
|
119
|
+
const input = inputObject(value, "search response");
|
|
120
|
+
assertAllowedFields(input, ["records", "cursor"], "search response");
|
|
121
|
+
if (!Array.isArray(input.records) || input.records.length > CRM_MAX_SEARCH_LIMIT) {
|
|
122
|
+
throw new CrmInputError(`search response records must be an array of at most ${CRM_MAX_SEARCH_LIMIT} records`);
|
|
123
|
+
}
|
|
124
|
+
const records = input.records.map(parseSearchRecord);
|
|
125
|
+
const cursor = optionalText(input.cursor, "search response cursor", CRM_MAX_CURSOR_CHARS);
|
|
126
|
+
const response = {
|
|
127
|
+
records,
|
|
128
|
+
...cursor === undefined ? {} : { cursor }
|
|
129
|
+
};
|
|
130
|
+
if (!crmResponseFitsBudget(response)) {
|
|
131
|
+
throw new CrmInputError(`CRM response may not exceed ${CRM_MAX_RESPONSE_BYTES} bytes`);
|
|
132
|
+
}
|
|
133
|
+
return response;
|
|
134
|
+
} catch {
|
|
135
|
+
throw new CrmResponseError;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
function parseCrmGetRecordResponse(value) {
|
|
139
|
+
try {
|
|
140
|
+
const input = inputObject(value, "get record response");
|
|
141
|
+
assertAllowedFields(input, ["profile", "activities", "cursor"], "get record response");
|
|
142
|
+
if (!Array.isArray(input.activities) || input.activities.length > CRM_MAX_ACTIVITY_PAGE_SIZE) {
|
|
143
|
+
throw new CrmInputError(`get record response activities must be an array of at most ${CRM_MAX_ACTIVITY_PAGE_SIZE} activities`);
|
|
144
|
+
}
|
|
145
|
+
const profile = parseRecordProfile(input.profile);
|
|
146
|
+
const activities = input.activities.map(parseActivity);
|
|
147
|
+
const cursor = optionalText(input.cursor, "get record response cursor", CRM_MAX_CURSOR_CHARS);
|
|
148
|
+
const response = {
|
|
149
|
+
profile,
|
|
150
|
+
activities,
|
|
151
|
+
...cursor === undefined ? {} : { cursor }
|
|
152
|
+
};
|
|
153
|
+
if (!crmResponseFitsBudget(response)) {
|
|
154
|
+
throw new CrmInputError(`CRM response may not exceed ${CRM_MAX_RESPONSE_BYTES} bytes`);
|
|
155
|
+
}
|
|
156
|
+
return response;
|
|
157
|
+
} catch {
|
|
158
|
+
throw new CrmResponseError;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
function parseCrmOutreachQueueResponse(value) {
|
|
162
|
+
try {
|
|
163
|
+
const input = inputObject(value, "outreach queue response");
|
|
164
|
+
assertAllowedFields(input, [
|
|
165
|
+
"weekStart",
|
|
166
|
+
"timezone",
|
|
167
|
+
"weeklyLimit",
|
|
168
|
+
"newDraftedThisWeek",
|
|
169
|
+
"remaining",
|
|
170
|
+
"followUpLimit",
|
|
171
|
+
"followUpsDraftedThisWeek",
|
|
172
|
+
"followUpsRemaining",
|
|
173
|
+
"followUps",
|
|
174
|
+
"newLeads",
|
|
175
|
+
"truncated"
|
|
176
|
+
], "outreach queue response");
|
|
177
|
+
const weekStart = requiredText(input.weekStart, "weekStart", 10);
|
|
178
|
+
if (!/^\d{4}-\d{2}-\d{2}$/.test(weekStart))
|
|
179
|
+
throw new CrmInputError("weekStart is invalid");
|
|
180
|
+
if (!Array.isArray(input.followUps) || input.followUps.length > CRM_MAX_OUTREACH_QUEUE_ITEMS || !Array.isArray(input.newLeads) || input.newLeads.length > CRM_MAX_OUTREACH_QUEUE_ITEMS) {
|
|
181
|
+
throw new CrmInputError("outreach queue lists are bounded");
|
|
182
|
+
}
|
|
183
|
+
if (input.truncated !== undefined && input.truncated !== true) {
|
|
184
|
+
throw new CrmInputError("truncated must be true when present");
|
|
185
|
+
}
|
|
186
|
+
const response = {
|
|
187
|
+
weekStart,
|
|
188
|
+
timezone: requiredText(input.timezone, "timezone", CRM_MAX_TIMEZONE_CHARS),
|
|
189
|
+
weeklyLimit: count(input.weeklyLimit, "weeklyLimit"),
|
|
190
|
+
newDraftedThisWeek: count(input.newDraftedThisWeek, "newDraftedThisWeek"),
|
|
191
|
+
remaining: count(input.remaining, "remaining"),
|
|
192
|
+
followUpLimit: count(input.followUpLimit, "followUpLimit"),
|
|
193
|
+
followUpsDraftedThisWeek: count(input.followUpsDraftedThisWeek, "followUpsDraftedThisWeek"),
|
|
194
|
+
followUpsRemaining: count(input.followUpsRemaining, "followUpsRemaining"),
|
|
195
|
+
followUps: input.followUps.map(parseOutreachFollowUp),
|
|
196
|
+
newLeads: input.newLeads.map(parseOutreachNewLead),
|
|
197
|
+
...input.truncated === true ? { truncated: true } : {}
|
|
198
|
+
};
|
|
199
|
+
if (!crmResponseFitsBudget(response)) {
|
|
200
|
+
throw new CrmInputError(`CRM response may not exceed ${CRM_MAX_RESPONSE_BYTES} bytes`);
|
|
201
|
+
}
|
|
202
|
+
return response;
|
|
203
|
+
} catch {
|
|
204
|
+
throw new CrmResponseError;
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
var OUTREACH_CONTACT_FIELDS = [
|
|
208
|
+
"contactId",
|
|
209
|
+
"email",
|
|
210
|
+
"status",
|
|
211
|
+
"firstName",
|
|
212
|
+
"lastName",
|
|
213
|
+
"title",
|
|
214
|
+
"companyExternalId",
|
|
215
|
+
"companyId",
|
|
216
|
+
"company"
|
|
217
|
+
];
|
|
218
|
+
function parseOutreachContact(input) {
|
|
219
|
+
const contact = {
|
|
220
|
+
contactId: requiredText(input.contactId, "contactId", CRM_MAX_ID_CHARS),
|
|
221
|
+
email: email(input.email),
|
|
222
|
+
status: requiredText(input.status, "status", CRM_MAX_STATUS_CHARS)
|
|
223
|
+
};
|
|
224
|
+
for (const [field, maximum] of [
|
|
225
|
+
["firstName", CRM_MAX_PERSON_FIELD_CHARS],
|
|
226
|
+
["lastName", CRM_MAX_PERSON_FIELD_CHARS],
|
|
227
|
+
["title", CRM_MAX_PERSON_FIELD_CHARS],
|
|
228
|
+
["companyExternalId", CRM_MAX_ID_CHARS],
|
|
229
|
+
["companyId", CRM_MAX_ID_CHARS],
|
|
230
|
+
["company", CRM_MAX_NAME_CHARS]
|
|
231
|
+
]) {
|
|
232
|
+
const text = optionalText(input[field], field, maximum);
|
|
233
|
+
if (text !== undefined)
|
|
234
|
+
contact[field] = text;
|
|
235
|
+
}
|
|
236
|
+
return contact;
|
|
237
|
+
}
|
|
238
|
+
function parseOutreachFollowUp(value) {
|
|
239
|
+
const input = inputObject(value, "outreach follow-up");
|
|
240
|
+
assertAllowedFields(input, [...OUTREACH_CONTACT_FIELDS, "lastTouchAt", "followUpNumber", "threadId"], "outreach follow-up");
|
|
241
|
+
const threadId = optionalText(input.threadId, "threadId", CRM_MAX_ATTRIBUTE_STRING_CHARS);
|
|
242
|
+
return {
|
|
243
|
+
...parseOutreachContact(input),
|
|
244
|
+
lastTouchAt: parseCrmTimestamp(input.lastTouchAt, "lastTouchAt"),
|
|
245
|
+
followUpNumber: positiveInteger(input.followUpNumber, "followUpNumber", 10),
|
|
246
|
+
...threadId === undefined ? {} : { threadId }
|
|
247
|
+
};
|
|
248
|
+
}
|
|
249
|
+
function parseOutreachNewLead(value) {
|
|
250
|
+
const input = inputObject(value, "outreach new lead");
|
|
251
|
+
assertAllowedFields(input, [...OUTREACH_CONTACT_FIELDS, "rank"], "outreach new lead");
|
|
252
|
+
const rank = input.rank;
|
|
253
|
+
if (rank !== undefined && (typeof rank !== "number" || !Number.isFinite(rank))) {
|
|
254
|
+
throw new CrmInputError("rank must be a finite number");
|
|
255
|
+
}
|
|
256
|
+
return {
|
|
257
|
+
...parseOutreachContact(input),
|
|
258
|
+
...rank === undefined ? {} : { rank }
|
|
259
|
+
};
|
|
260
|
+
}
|
|
261
|
+
function count(value, field) {
|
|
262
|
+
if (typeof value !== "number" || !Number.isInteger(value) || value < 0 || value > 1e6) {
|
|
263
|
+
throw new CrmInputError(`${field} must be a non-negative integer`);
|
|
264
|
+
}
|
|
265
|
+
return value;
|
|
266
|
+
}
|
|
267
|
+
function parseCrmOperationAck(value) {
|
|
268
|
+
try {
|
|
269
|
+
const input = inputObject(value, "operation acknowledgement");
|
|
270
|
+
if (input.accepted !== true)
|
|
271
|
+
throw new CrmInputError("operation acknowledgement must be accepted");
|
|
272
|
+
switch (operation(input.operation)) {
|
|
273
|
+
case "upsert_records": {
|
|
274
|
+
assertAllowedFields(input, ["accepted", "operation", "records"], "operation acknowledgement");
|
|
275
|
+
return {
|
|
276
|
+
accepted: true,
|
|
277
|
+
operation: "upsert_records",
|
|
278
|
+
records: positiveInteger(input.records, "records", CRM_MAX_BATCH_SIZE)
|
|
279
|
+
};
|
|
280
|
+
}
|
|
281
|
+
case "update_status": {
|
|
282
|
+
assertAllowedFields(input, ["accepted", "operation", "recordType", "recordId", "status"], "operation acknowledgement");
|
|
283
|
+
return {
|
|
284
|
+
accepted: true,
|
|
285
|
+
operation: "update_status",
|
|
286
|
+
recordType: recordType(input.recordType),
|
|
287
|
+
recordId: requiredText(input.recordId, "recordId", CRM_MAX_ID_CHARS),
|
|
288
|
+
status: requiredText(input.status, "status", CRM_MAX_STATUS_CHARS)
|
|
289
|
+
};
|
|
290
|
+
}
|
|
291
|
+
case "log_activity": {
|
|
292
|
+
assertAllowedFields(input, ["accepted", "operation", "recordType", "recordId", "activityType"], "operation acknowledgement");
|
|
293
|
+
return {
|
|
294
|
+
accepted: true,
|
|
295
|
+
operation: "log_activity",
|
|
296
|
+
recordType: recordType(input.recordType),
|
|
297
|
+
recordId: requiredText(input.recordId, "recordId", CRM_MAX_ID_CHARS),
|
|
298
|
+
activityType: requiredText(input.activityType, "activityType", CRM_MAX_ACTIVITY_TYPE_CHARS)
|
|
299
|
+
};
|
|
300
|
+
}
|
|
301
|
+
case "update_note": {
|
|
302
|
+
assertAllowedFields(input, ["accepted", "operation", "recordType", "recordId", "activityId", "version"], "operation acknowledgement");
|
|
303
|
+
return {
|
|
304
|
+
accepted: true,
|
|
305
|
+
operation: "update_note",
|
|
306
|
+
recordType: recordType(input.recordType),
|
|
307
|
+
recordId: requiredText(input.recordId, "recordId", CRM_MAX_ID_CHARS),
|
|
308
|
+
activityId: requiredText(input.activityId, "activityId", CRM_MAX_ID_CHARS),
|
|
309
|
+
version: positiveInteger(input.version, "version", CRM_MAX_VERSION)
|
|
310
|
+
};
|
|
311
|
+
}
|
|
312
|
+
case "delete_record": {
|
|
313
|
+
assertAllowedFields(input, ["accepted", "operation", "recordType", "recordId"], "operation acknowledgement");
|
|
314
|
+
return {
|
|
315
|
+
accepted: true,
|
|
316
|
+
operation: "delete_record",
|
|
317
|
+
recordType: recordType(input.recordType),
|
|
318
|
+
recordId: requiredText(input.recordId, "recordId", CRM_MAX_ID_CHARS)
|
|
319
|
+
};
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
} catch {
|
|
323
|
+
throw new CrmResponseError;
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
function parseCrmUpdateStatusArgs(value) {
|
|
327
|
+
const input = inputObject(value, "status update request");
|
|
328
|
+
assertAllowedFields(input, ["recordType", "recordId", "status", "idempotencyKey"], "status update request");
|
|
329
|
+
return {
|
|
330
|
+
recordType: recordType(input.recordType),
|
|
331
|
+
recordId: requiredText(input.recordId, "recordId", CRM_MAX_ID_CHARS),
|
|
332
|
+
status: requiredText(input.status, "status", CRM_MAX_STATUS_CHARS),
|
|
333
|
+
idempotencyKey: parseIdempotencyKey(input.idempotencyKey)
|
|
334
|
+
};
|
|
335
|
+
}
|
|
336
|
+
function parseCrmLogActivityArgs(value) {
|
|
337
|
+
const input = inputObject(value, "activity request");
|
|
338
|
+
assertAllowedFields(input, [
|
|
339
|
+
"recordType",
|
|
340
|
+
"recordId",
|
|
341
|
+
"activityType",
|
|
342
|
+
"summary",
|
|
343
|
+
"occurredAt",
|
|
344
|
+
"attributes",
|
|
345
|
+
"idempotencyKey"
|
|
346
|
+
], "activity request");
|
|
347
|
+
const result = {
|
|
348
|
+
recordType: recordType(input.recordType),
|
|
349
|
+
recordId: requiredText(input.recordId, "recordId", CRM_MAX_ID_CHARS),
|
|
350
|
+
activityType: requiredText(input.activityType, "activityType", CRM_MAX_ACTIVITY_TYPE_CHARS),
|
|
351
|
+
summary: requiredText(input.summary, "summary", CRM_MAX_SUMMARY_CHARS),
|
|
352
|
+
idempotencyKey: parseIdempotencyKey(input.idempotencyKey)
|
|
353
|
+
};
|
|
354
|
+
if (input.occurredAt !== undefined)
|
|
355
|
+
result.occurredAt = parseCrmTimestamp(input.occurredAt, "occurredAt");
|
|
356
|
+
const attributes = parseAttributes(input.attributes);
|
|
357
|
+
if (attributes !== undefined && Object.hasOwn(attributes, CRM_ACTIVITY_AUTHOR_ATTRIBUTE)) {
|
|
358
|
+
throw new CrmInputError("attributes.by is set by the CRM, not the caller");
|
|
359
|
+
}
|
|
360
|
+
if (attributes !== undefined)
|
|
361
|
+
result.attributes = attributes;
|
|
362
|
+
return result;
|
|
363
|
+
}
|
|
364
|
+
function parseCrmGetActivityResponse(value) {
|
|
365
|
+
try {
|
|
366
|
+
const input = inputObject(value, "get activity response");
|
|
367
|
+
assertAllowedFields(input, ["activity"], "get activity response");
|
|
368
|
+
return parseActivity(input.activity);
|
|
369
|
+
} catch {
|
|
370
|
+
throw new CrmResponseError;
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
function parseCrmOutreachState(value) {
|
|
374
|
+
try {
|
|
375
|
+
const input = inputObject(value, "outreach state");
|
|
376
|
+
assertAllowedFields(input, [
|
|
377
|
+
"contactId",
|
|
378
|
+
"status",
|
|
379
|
+
"statusSetBy",
|
|
380
|
+
"doNotContact",
|
|
381
|
+
"companyId",
|
|
382
|
+
"companyStatus",
|
|
383
|
+
"companyLastTouchAt",
|
|
384
|
+
"touch"
|
|
385
|
+
], "outreach state");
|
|
386
|
+
if (typeof input.doNotContact !== "boolean") {
|
|
387
|
+
throw new CrmInputError("doNotContact must be a boolean");
|
|
388
|
+
}
|
|
389
|
+
const state = {
|
|
390
|
+
contactId: requiredText(input.contactId, "contactId", CRM_MAX_ID_CHARS),
|
|
391
|
+
status: requiredText(input.status, "status", CRM_MAX_STATUS_CHARS),
|
|
392
|
+
doNotContact: input.doNotContact,
|
|
393
|
+
touch: parseTouchSummary(input.touch)
|
|
394
|
+
};
|
|
395
|
+
if (input.statusSetBy !== undefined)
|
|
396
|
+
state.statusSetBy = statusSetter(input.statusSetBy);
|
|
397
|
+
const companyId = optionalText(input.companyId, "companyId", CRM_MAX_ID_CHARS);
|
|
398
|
+
if (companyId !== undefined)
|
|
399
|
+
state.companyId = companyId;
|
|
400
|
+
const companyStatus = optionalText(input.companyStatus, "companyStatus", CRM_MAX_STATUS_CHARS);
|
|
401
|
+
if (companyStatus !== undefined)
|
|
402
|
+
state.companyStatus = companyStatus;
|
|
403
|
+
if (input.companyLastTouchAt !== undefined) {
|
|
404
|
+
state.companyLastTouchAt = parseCrmTimestamp(input.companyLastTouchAt, "companyLastTouchAt");
|
|
405
|
+
}
|
|
406
|
+
return state;
|
|
407
|
+
} catch {
|
|
408
|
+
throw new CrmResponseError;
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
function parseCrmOutreachCandidatesResponse(value) {
|
|
412
|
+
try {
|
|
413
|
+
const input = inputObject(value, "outreach candidates response");
|
|
414
|
+
assertAllowedFields(input, ["candidates", "companies", "cursor"], "outreach candidates response");
|
|
415
|
+
if (!Array.isArray(input.candidates) || input.candidates.length > CRM_MAX_OUTREACH_CANDIDATES_PAGE || !Array.isArray(input.companies) || input.companies.length > CRM_MAX_OUTREACH_CANDIDATES_PAGE) {
|
|
416
|
+
throw new CrmInputError("outreach candidate lists are bounded");
|
|
417
|
+
}
|
|
418
|
+
const cursor = optionalText(input.cursor, "cursor", CRM_MAX_CURSOR_CHARS);
|
|
419
|
+
return {
|
|
420
|
+
candidates: input.candidates.map(parseOutreachCandidate),
|
|
421
|
+
companies: input.companies.map((entry) => {
|
|
422
|
+
const company = inputObject(entry, "candidate company");
|
|
423
|
+
assertAllowedFields(company, ["companyId", "lastTouchAt"], "candidate company");
|
|
424
|
+
return {
|
|
425
|
+
companyId: requiredText(company.companyId, "companyId", CRM_MAX_ID_CHARS),
|
|
426
|
+
...company.lastTouchAt === undefined ? {} : { lastTouchAt: parseCrmTimestamp(company.lastTouchAt, "lastTouchAt") }
|
|
427
|
+
};
|
|
428
|
+
}),
|
|
429
|
+
...cursor === undefined ? {} : { cursor }
|
|
430
|
+
};
|
|
431
|
+
} catch {
|
|
432
|
+
throw new CrmResponseError;
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
function parseCrmProviderMeta(value) {
|
|
436
|
+
try {
|
|
437
|
+
const input = inputObject(value, "provider meta");
|
|
438
|
+
assertAllowedFields(input, [
|
|
439
|
+
"contract",
|
|
440
|
+
"tenant",
|
|
441
|
+
"principal",
|
|
442
|
+
"statuses",
|
|
443
|
+
"activityTypes",
|
|
444
|
+
"operations",
|
|
445
|
+
"maxBatchSize",
|
|
446
|
+
"policy"
|
|
447
|
+
], "provider meta");
|
|
448
|
+
const contract = requiredText(input.contract, "contract", 32);
|
|
449
|
+
if (!/^crm\/\d{1,4}$/.test(contract))
|
|
450
|
+
throw new CrmInputError("contract is invalid");
|
|
451
|
+
const tenantInput = inputObject(input.tenant, "tenant");
|
|
452
|
+
assertAllowedFields(tenantInput, ["id", "name"], "tenant");
|
|
453
|
+
const tenantName = optionalText(tenantInput.name, "tenant name", CRM_MAX_NAME_CHARS);
|
|
454
|
+
const statusesInput = inputObject(input.statuses, "statuses");
|
|
455
|
+
assertAllowedFields(statusesInput, ["company", "contact"], "statuses");
|
|
456
|
+
const operations = parseStringArray(input.operations, "operations", CRM_PROVIDER_OPERATIONS.length, 32);
|
|
457
|
+
for (const name of operations) {
|
|
458
|
+
if (!CRM_PROVIDER_OPERATIONS.includes(name)) {
|
|
459
|
+
throw new CrmInputError("operations names an unknown operation");
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
const principal = optionalText(input.principal, "principal", CRM_MAX_NAME_CHARS);
|
|
463
|
+
const meta = {
|
|
464
|
+
contract,
|
|
465
|
+
tenant: {
|
|
466
|
+
id: requiredText(tenantInput.id, "tenant id", CRM_MAX_ID_CHARS),
|
|
467
|
+
...tenantName === undefined ? {} : { name: tenantName }
|
|
468
|
+
},
|
|
469
|
+
...principal === undefined ? {} : { principal },
|
|
470
|
+
statuses: {
|
|
471
|
+
company: parseStringArray(statusesInput.company, "company statuses", CRM_MAX_CONFIG_VALUES, CRM_MAX_STATUS_CHARS),
|
|
472
|
+
contact: parseStringArray(statusesInput.contact, "contact statuses", CRM_MAX_CONFIG_VALUES, CRM_MAX_STATUS_CHARS)
|
|
473
|
+
},
|
|
474
|
+
activityTypes: parseStringArray(input.activityTypes, "activityTypes", CRM_MAX_CONFIG_VALUES, CRM_MAX_ACTIVITY_TYPE_CHARS),
|
|
475
|
+
operations,
|
|
476
|
+
maxBatchSize: positiveInteger(input.maxBatchSize, "maxBatchSize", 1e4)
|
|
477
|
+
};
|
|
478
|
+
if (input.policy !== undefined)
|
|
479
|
+
meta.policy = parseProviderPolicy(input.policy);
|
|
480
|
+
return meta;
|
|
481
|
+
} catch {
|
|
482
|
+
throw new CrmResponseError;
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
function parseProviderPolicy(value) {
|
|
486
|
+
const input = inputObject(value, "provider policy");
|
|
487
|
+
assertAllowedFields(input, ["firstTouchWeeklyLimit", "followUpWeeklyLimit", "maxFollowUps", "timezone", "personSet"], "provider policy");
|
|
488
|
+
const policy = {};
|
|
489
|
+
for (const field of ["firstTouchWeeklyLimit", "followUpWeeklyLimit", "maxFollowUps"]) {
|
|
490
|
+
if (input[field] !== undefined)
|
|
491
|
+
policy[field] = count(input[field], field);
|
|
492
|
+
}
|
|
493
|
+
const timezone = optionalText(input.timezone, "timezone", CRM_MAX_TIMEZONE_CHARS);
|
|
494
|
+
if (timezone !== undefined)
|
|
495
|
+
policy.timezone = timezone;
|
|
496
|
+
if (input.personSet !== undefined) {
|
|
497
|
+
if (input.personSet !== "lock" && input.personSet !== "advance") {
|
|
498
|
+
throw new CrmInputError("personSet must be lock or advance");
|
|
499
|
+
}
|
|
500
|
+
policy.personSet = input.personSet;
|
|
501
|
+
}
|
|
502
|
+
return policy;
|
|
503
|
+
}
|
|
504
|
+
function parseTouchSummary(value) {
|
|
505
|
+
const input = inputObject(value, "touch");
|
|
506
|
+
assertAllowedFields(input, ["firstTouchAt", "lastTouchAt", "lastReplyAt", "followUps", "lastThreadId", "lastDraftId"], "touch");
|
|
507
|
+
const touch = { followUps: count(input.followUps, "followUps") };
|
|
508
|
+
for (const field of ["firstTouchAt", "lastTouchAt", "lastReplyAt"]) {
|
|
509
|
+
if (input[field] !== undefined)
|
|
510
|
+
touch[field] = parseCrmTimestamp(input[field], field);
|
|
511
|
+
}
|
|
512
|
+
for (const field of ["lastThreadId", "lastDraftId"]) {
|
|
513
|
+
const text = optionalText(input[field], field, CRM_MAX_ATTRIBUTE_STRING_CHARS);
|
|
514
|
+
if (text !== undefined)
|
|
515
|
+
touch[field] = text;
|
|
516
|
+
}
|
|
517
|
+
return touch;
|
|
518
|
+
}
|
|
519
|
+
function parseOutreachCandidate(value) {
|
|
520
|
+
const input = inputObject(value, "outreach candidate");
|
|
521
|
+
assertAllowedFields(input, [
|
|
522
|
+
"contactId",
|
|
523
|
+
"email",
|
|
524
|
+
"status",
|
|
525
|
+
"doNotContact",
|
|
526
|
+
"firstName",
|
|
527
|
+
"lastName",
|
|
528
|
+
"title",
|
|
529
|
+
"companyId",
|
|
530
|
+
"companyExternalId",
|
|
531
|
+
"company",
|
|
532
|
+
"companyStatus",
|
|
533
|
+
"rank",
|
|
534
|
+
"createdAt",
|
|
535
|
+
"touch"
|
|
536
|
+
], "outreach candidate");
|
|
537
|
+
if (typeof input.doNotContact !== "boolean") {
|
|
538
|
+
throw new CrmInputError("doNotContact must be a boolean");
|
|
539
|
+
}
|
|
540
|
+
const candidate = {
|
|
541
|
+
contactId: requiredText(input.contactId, "contactId", CRM_MAX_ID_CHARS),
|
|
542
|
+
email: email(input.email),
|
|
543
|
+
status: requiredText(input.status, "status", CRM_MAX_STATUS_CHARS),
|
|
544
|
+
doNotContact: input.doNotContact,
|
|
545
|
+
touch: parseTouchSummary(input.touch)
|
|
546
|
+
};
|
|
547
|
+
for (const [field, maximum] of [
|
|
548
|
+
["firstName", CRM_MAX_PERSON_FIELD_CHARS],
|
|
549
|
+
["lastName", CRM_MAX_PERSON_FIELD_CHARS],
|
|
550
|
+
["title", CRM_MAX_PERSON_FIELD_CHARS],
|
|
551
|
+
["companyId", CRM_MAX_ID_CHARS],
|
|
552
|
+
["companyExternalId", CRM_MAX_ID_CHARS],
|
|
553
|
+
["company", CRM_MAX_NAME_CHARS],
|
|
554
|
+
["companyStatus", CRM_MAX_STATUS_CHARS]
|
|
555
|
+
]) {
|
|
556
|
+
const text = optionalText(input[field], field, maximum);
|
|
557
|
+
if (text !== undefined)
|
|
558
|
+
candidate[field] = text;
|
|
559
|
+
}
|
|
560
|
+
if (input.rank !== undefined) {
|
|
561
|
+
if (typeof input.rank !== "number" || !Number.isFinite(input.rank)) {
|
|
562
|
+
throw new CrmInputError("rank must be a finite number");
|
|
563
|
+
}
|
|
564
|
+
candidate.rank = input.rank;
|
|
565
|
+
}
|
|
566
|
+
if (input.createdAt !== undefined) {
|
|
567
|
+
candidate.createdAt = parseCrmTimestamp(input.createdAt, "createdAt");
|
|
568
|
+
}
|
|
569
|
+
return candidate;
|
|
570
|
+
}
|
|
571
|
+
function statusSetter(value) {
|
|
572
|
+
if (!CRM_STATUS_SETTERS.includes(value)) {
|
|
573
|
+
throw new CrmInputError("statusSetBy must be agent, person or system");
|
|
574
|
+
}
|
|
575
|
+
return value;
|
|
576
|
+
}
|
|
577
|
+
function isCrmIdempotencyKey(value) {
|
|
578
|
+
return typeof value === "string" && value.length <= CRM_MAX_IDEMPOTENCY_KEY_CHARS && /^[\x21-\x7e](?:[\x20-\x7e]*[\x21-\x7e])?$/.test(value);
|
|
579
|
+
}
|
|
580
|
+
function parseIdempotencyKey(value) {
|
|
581
|
+
if (!isCrmIdempotencyKey(value)) {
|
|
582
|
+
throw new CrmInputError(`idempotencyKey must be printable ASCII (letters, digits, punctuation; no accents), at most ${CRM_MAX_IDEMPOTENCY_KEY_CHARS} characters`);
|
|
583
|
+
}
|
|
584
|
+
return value;
|
|
585
|
+
}
|
|
586
|
+
function inputObject(value, label) {
|
|
587
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
588
|
+
throw new CrmInputError(`${label} must be an object`);
|
|
589
|
+
}
|
|
590
|
+
return Object.fromEntries(Object.entries(value));
|
|
591
|
+
}
|
|
592
|
+
function assertAllowedFields(input, fields, label) {
|
|
593
|
+
for (const key of Object.keys(input)) {
|
|
594
|
+
if (!fields.includes(key))
|
|
595
|
+
throw new CrmInputError(`${label} contains an unsupported field`);
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
function requiredText(value, field, max) {
|
|
599
|
+
if (typeof value !== "string" || value.length === 0 || value.length > max || value.trim() === "" || CONTROL_CHARS.test(value)) {
|
|
600
|
+
throw new CrmInputError(`${field} must be a non-empty string of at most ${max} characters`);
|
|
601
|
+
}
|
|
602
|
+
return value;
|
|
603
|
+
}
|
|
604
|
+
function optionalText(value, field, max) {
|
|
605
|
+
if (value === undefined)
|
|
606
|
+
return;
|
|
607
|
+
return requiredText(value, field, max);
|
|
608
|
+
}
|
|
609
|
+
function positiveInteger(value, field, max) {
|
|
610
|
+
if (typeof value !== "number" || !Number.isInteger(value) || value < 1 || value > max) {
|
|
611
|
+
throw new CrmInputError(`${field} must be an integer between 1 and ${max}`);
|
|
612
|
+
}
|
|
613
|
+
return value;
|
|
614
|
+
}
|
|
615
|
+
function parseRecordArray(value, field, parse) {
|
|
616
|
+
if (!Array.isArray(value))
|
|
617
|
+
throw new CrmInputError(`${field} must be an array`);
|
|
618
|
+
if (value.length > CRM_MAX_BATCH_SIZE)
|
|
619
|
+
throw new CrmInputError(`${field} may contain at most ${CRM_MAX_BATCH_SIZE} records`);
|
|
620
|
+
return value.map(parse);
|
|
621
|
+
}
|
|
622
|
+
function parseCompany(value) {
|
|
623
|
+
const input = inputObject(value, "company");
|
|
624
|
+
assertAllowedFields(input, ["externalId", "name", "domain", "website", "source", "attributes"], "company");
|
|
625
|
+
const company = {
|
|
626
|
+
externalId: requiredText(input.externalId, "company externalId", CRM_MAX_ID_CHARS),
|
|
627
|
+
name: requiredText(input.name, "company name", CRM_MAX_NAME_CHARS)
|
|
628
|
+
};
|
|
629
|
+
const domain = optionalText(input.domain, "company domain", CRM_MAX_DOMAIN_CHARS);
|
|
630
|
+
if (domain !== undefined)
|
|
631
|
+
company.domain = domain;
|
|
632
|
+
const website = optionalText(input.website, "company website", CRM_MAX_WEBSITE_CHARS);
|
|
633
|
+
if (website !== undefined)
|
|
634
|
+
company.website = website;
|
|
635
|
+
const source = optionalText(input.source, "company source", CRM_MAX_SOURCE_CHARS);
|
|
636
|
+
if (source !== undefined)
|
|
637
|
+
company.source = source;
|
|
638
|
+
const attributes = parseAttributes(input.attributes);
|
|
639
|
+
if (attributes !== undefined)
|
|
640
|
+
company.attributes = attributes;
|
|
641
|
+
return company;
|
|
642
|
+
}
|
|
643
|
+
function parseContact(value) {
|
|
644
|
+
const input = inputObject(value, "contact");
|
|
645
|
+
assertAllowedFields(input, [
|
|
646
|
+
"externalId",
|
|
647
|
+
"email",
|
|
648
|
+
"firstName",
|
|
649
|
+
"lastName",
|
|
650
|
+
"title",
|
|
651
|
+
"companyExternalId",
|
|
652
|
+
"source",
|
|
653
|
+
"attributes"
|
|
654
|
+
], "contact");
|
|
655
|
+
const contact = {
|
|
656
|
+
externalId: requiredText(input.externalId, "contact externalId", CRM_MAX_ID_CHARS)
|
|
657
|
+
};
|
|
658
|
+
if (input.email !== undefined)
|
|
659
|
+
contact.email = email(input.email);
|
|
660
|
+
const firstName = optionalText(input.firstName, "contact firstName", CRM_MAX_PERSON_FIELD_CHARS);
|
|
661
|
+
if (firstName !== undefined)
|
|
662
|
+
contact.firstName = firstName;
|
|
663
|
+
const lastName = optionalText(input.lastName, "contact lastName", CRM_MAX_PERSON_FIELD_CHARS);
|
|
664
|
+
if (lastName !== undefined)
|
|
665
|
+
contact.lastName = lastName;
|
|
666
|
+
const title = optionalText(input.title, "contact title", CRM_MAX_PERSON_FIELD_CHARS);
|
|
667
|
+
if (title !== undefined)
|
|
668
|
+
contact.title = title;
|
|
669
|
+
const companyExternalId = optionalText(input.companyExternalId, "contact companyExternalId", CRM_MAX_ID_CHARS);
|
|
670
|
+
if (companyExternalId !== undefined)
|
|
671
|
+
contact.companyExternalId = companyExternalId;
|
|
672
|
+
const source = optionalText(input.source, "contact source", CRM_MAX_SOURCE_CHARS);
|
|
673
|
+
if (source !== undefined)
|
|
674
|
+
contact.source = source;
|
|
675
|
+
const attributes = parseAttributes(input.attributes);
|
|
676
|
+
if (attributes !== undefined)
|
|
677
|
+
contact.attributes = attributes;
|
|
678
|
+
return contact;
|
|
679
|
+
}
|
|
680
|
+
function parseSearchRecord(value) {
|
|
681
|
+
const input = inputObject(value, "search record");
|
|
682
|
+
assertAllowedFields(input, [
|
|
683
|
+
"recordType",
|
|
684
|
+
"id",
|
|
685
|
+
"externalId",
|
|
686
|
+
"status",
|
|
687
|
+
"name",
|
|
688
|
+
"domain",
|
|
689
|
+
"website",
|
|
690
|
+
"source",
|
|
691
|
+
"email",
|
|
692
|
+
"firstName",
|
|
693
|
+
"lastName",
|
|
694
|
+
"title",
|
|
695
|
+
"companyExternalId"
|
|
696
|
+
], "search record");
|
|
697
|
+
return parseRecordSummary(input, "search record");
|
|
698
|
+
}
|
|
699
|
+
function parseRecordProfile(value) {
|
|
700
|
+
const input = inputObject(value, "record profile");
|
|
701
|
+
assertAllowedFields(input, [
|
|
702
|
+
"recordType",
|
|
703
|
+
"id",
|
|
704
|
+
"externalId",
|
|
705
|
+
"status",
|
|
706
|
+
"name",
|
|
707
|
+
"domain",
|
|
708
|
+
"website",
|
|
709
|
+
"source",
|
|
710
|
+
"email",
|
|
711
|
+
"firstName",
|
|
712
|
+
"lastName",
|
|
713
|
+
"title",
|
|
714
|
+
"companyExternalId",
|
|
715
|
+
"attributes",
|
|
716
|
+
"createdAt",
|
|
717
|
+
"updatedAt",
|
|
718
|
+
"statusSetBy",
|
|
719
|
+
"statusSetAt",
|
|
720
|
+
"statusVersion",
|
|
721
|
+
"doNotContact"
|
|
722
|
+
], "record profile");
|
|
723
|
+
const attributes = parseAttributes(input.attributes);
|
|
724
|
+
const profile = {
|
|
725
|
+
...parseRecordSummary(input, "record profile"),
|
|
726
|
+
externalId: requiredText(input.externalId, "record profile externalId", CRM_MAX_ID_CHARS),
|
|
727
|
+
status: requiredText(input.status, "record profile status", CRM_MAX_STATUS_CHARS),
|
|
728
|
+
...attributes === undefined ? {} : { attributes },
|
|
729
|
+
createdAt: parseCrmTimestamp(input.createdAt, "record profile createdAt"),
|
|
730
|
+
updatedAt: parseCrmTimestamp(input.updatedAt, "record profile updatedAt")
|
|
731
|
+
};
|
|
732
|
+
if (input.statusSetBy !== undefined)
|
|
733
|
+
profile.statusSetBy = statusSetter(input.statusSetBy);
|
|
734
|
+
if (input.statusSetAt !== undefined) {
|
|
735
|
+
profile.statusSetAt = parseCrmTimestamp(input.statusSetAt, "record profile statusSetAt");
|
|
736
|
+
}
|
|
737
|
+
if (input.statusVersion !== undefined) {
|
|
738
|
+
profile.statusVersion = count(input.statusVersion, "statusVersion");
|
|
739
|
+
}
|
|
740
|
+
if (input.doNotContact !== undefined) {
|
|
741
|
+
if (typeof input.doNotContact !== "boolean") {
|
|
742
|
+
throw new CrmInputError("doNotContact must be a boolean");
|
|
743
|
+
}
|
|
744
|
+
profile.doNotContact = input.doNotContact;
|
|
745
|
+
}
|
|
746
|
+
return profile;
|
|
747
|
+
}
|
|
748
|
+
function parseRecordSummary(input, label) {
|
|
749
|
+
const record = {
|
|
750
|
+
recordType: recordType(input.recordType),
|
|
751
|
+
id: requiredText(input.id, `${label} id`, CRM_MAX_ID_CHARS)
|
|
752
|
+
};
|
|
753
|
+
for (const [field, maximum] of [
|
|
754
|
+
["externalId", CRM_MAX_ID_CHARS],
|
|
755
|
+
["status", CRM_MAX_STATUS_CHARS],
|
|
756
|
+
["name", CRM_MAX_NAME_CHARS],
|
|
757
|
+
["domain", CRM_MAX_DOMAIN_CHARS],
|
|
758
|
+
["website", CRM_MAX_WEBSITE_CHARS],
|
|
759
|
+
["source", CRM_MAX_SOURCE_CHARS],
|
|
760
|
+
["firstName", CRM_MAX_PERSON_FIELD_CHARS],
|
|
761
|
+
["lastName", CRM_MAX_PERSON_FIELD_CHARS],
|
|
762
|
+
["title", CRM_MAX_PERSON_FIELD_CHARS],
|
|
763
|
+
["companyExternalId", CRM_MAX_ID_CHARS]
|
|
764
|
+
]) {
|
|
765
|
+
const text = optionalText(input[field], `${label} ${field}`, maximum);
|
|
766
|
+
if (text !== undefined)
|
|
767
|
+
record[field] = text;
|
|
768
|
+
}
|
|
769
|
+
if (input.email !== undefined)
|
|
770
|
+
record.email = email(input.email);
|
|
771
|
+
return record;
|
|
772
|
+
}
|
|
773
|
+
function parseActivity(value) {
|
|
774
|
+
const input = inputObject(value, "activity");
|
|
775
|
+
assertAllowedFields(input, ["id", "activityType", "summary", "occurredAt", "createdAt", "attributes", "version"], "activity");
|
|
776
|
+
const attributes = parseAttributes(input.attributes);
|
|
777
|
+
return {
|
|
778
|
+
id: requiredText(input.id, "activity id", CRM_MAX_ID_CHARS),
|
|
779
|
+
activityType: requiredText(input.activityType, "activityType", CRM_MAX_ACTIVITY_TYPE_CHARS),
|
|
780
|
+
summary: requiredText(input.summary, "activity summary", CRM_MAX_SUMMARY_CHARS),
|
|
781
|
+
occurredAt: parseCrmTimestamp(input.occurredAt, "activity occurredAt"),
|
|
782
|
+
createdAt: parseCrmTimestamp(input.createdAt, "activity createdAt"),
|
|
783
|
+
...attributes === undefined ? {} : { attributes },
|
|
784
|
+
...input.version === undefined ? {} : { version: positiveInteger(input.version, "activity version", CRM_MAX_VERSION) }
|
|
785
|
+
};
|
|
786
|
+
}
|
|
787
|
+
function parseAttributes(value) {
|
|
788
|
+
if (value === undefined)
|
|
789
|
+
return;
|
|
790
|
+
const input = inputObject(value, "attributes");
|
|
791
|
+
const keys = Object.keys(input);
|
|
792
|
+
if (keys.length > CRM_MAX_ATTRIBUTE_COUNT) {
|
|
793
|
+
throw new CrmInputError(`attributes may contain at most ${CRM_MAX_ATTRIBUTE_COUNT} keys`);
|
|
794
|
+
}
|
|
795
|
+
const attributes = {};
|
|
796
|
+
for (const key of keys) {
|
|
797
|
+
if (key.length === 0 || key.length > CRM_MAX_ATTRIBUTE_KEY_CHARS || key.trim() === "" || CONTROL_CHARS.test(key) || key === "__proto__" || key === "constructor" || key === "prototype") {
|
|
798
|
+
throw new CrmInputError(`attribute keys must be non-empty strings of at most ${CRM_MAX_ATTRIBUTE_KEY_CHARS} characters`);
|
|
799
|
+
}
|
|
800
|
+
const scalar = attributeValue(input[key]);
|
|
801
|
+
Object.defineProperty(attributes, key, {
|
|
802
|
+
value: scalar,
|
|
803
|
+
enumerable: true,
|
|
804
|
+
configurable: false,
|
|
805
|
+
writable: false
|
|
806
|
+
});
|
|
807
|
+
}
|
|
808
|
+
return attributes;
|
|
809
|
+
}
|
|
810
|
+
function attributeValue(value) {
|
|
811
|
+
if (value === null || typeof value === "boolean")
|
|
812
|
+
return value;
|
|
813
|
+
if (typeof value === "number") {
|
|
814
|
+
if (!Number.isFinite(value))
|
|
815
|
+
throw new CrmInputError("attribute values must be JSON scalars");
|
|
816
|
+
return value;
|
|
817
|
+
}
|
|
818
|
+
if (typeof value === "string") {
|
|
819
|
+
if (value.length > CRM_MAX_ATTRIBUTE_STRING_CHARS || CONTROL_CHARS.test(value)) {
|
|
820
|
+
throw new CrmInputError(`attribute strings may contain at most ${CRM_MAX_ATTRIBUTE_STRING_CHARS} characters`);
|
|
821
|
+
}
|
|
822
|
+
return value;
|
|
823
|
+
}
|
|
824
|
+
throw new CrmInputError("attribute values must be JSON scalars");
|
|
825
|
+
}
|
|
826
|
+
function assertDistinctExternalIds(records, field) {
|
|
827
|
+
const ids = new Set;
|
|
828
|
+
for (const record of records) {
|
|
829
|
+
if (ids.has(record.externalId))
|
|
830
|
+
throw new CrmInputError(`${field} may not repeat externalId`);
|
|
831
|
+
ids.add(record.externalId);
|
|
832
|
+
}
|
|
833
|
+
}
|
|
834
|
+
function assertRequestSize(value) {
|
|
835
|
+
if (Buffer.byteLength(JSON.stringify(value), "utf8") > CRM_MAX_REQUEST_BYTES) {
|
|
836
|
+
throw new CrmInputError(`CRM request may not exceed ${CRM_MAX_REQUEST_BYTES} bytes`);
|
|
837
|
+
}
|
|
838
|
+
}
|
|
839
|
+
function parseStringArray(value, field, maxItems, maxItemChars) {
|
|
840
|
+
if (!Array.isArray(value))
|
|
841
|
+
throw new CrmInputError(`${field} must be an array`);
|
|
842
|
+
if (value.length > maxItems)
|
|
843
|
+
throw new CrmInputError(`${field} may contain at most ${maxItems} values`);
|
|
844
|
+
const seen = new Set;
|
|
845
|
+
const output = [];
|
|
846
|
+
for (const item of value) {
|
|
847
|
+
const parsed = requiredText(item, field, maxItemChars);
|
|
848
|
+
if (seen.has(parsed))
|
|
849
|
+
throw new CrmInputError(`${field} may not contain duplicates`);
|
|
850
|
+
seen.add(parsed);
|
|
851
|
+
output.push(parsed);
|
|
852
|
+
}
|
|
853
|
+
return output;
|
|
854
|
+
}
|
|
855
|
+
function email(value) {
|
|
856
|
+
const parsed = requiredText(value, "contact email", CRM_MAX_EMAIL_CHARS);
|
|
857
|
+
if (!SIMPLE_EMAIL.test(parsed))
|
|
858
|
+
throw new CrmInputError("contact email must be a valid address");
|
|
859
|
+
return parsed;
|
|
860
|
+
}
|
|
861
|
+
function recordType(value) {
|
|
862
|
+
if (value !== "company" && value !== "contact")
|
|
863
|
+
throw new CrmInputError("recordType must be company or contact");
|
|
864
|
+
return value;
|
|
865
|
+
}
|
|
866
|
+
function operation(value) {
|
|
867
|
+
if (value !== "upsert_records" && value !== "update_status" && value !== "log_activity" && value !== "update_note" && value !== "delete_record") {
|
|
868
|
+
throw new CrmInputError("operation is unsupported");
|
|
869
|
+
}
|
|
870
|
+
return value;
|
|
871
|
+
}
|
|
872
|
+
function parseCrmTimestamp(value, field) {
|
|
873
|
+
const parsed = requiredText(value, field, CRM_MAX_TIMESTAMP_CHARS);
|
|
874
|
+
const match = ISO_TIMESTAMP.exec(parsed);
|
|
875
|
+
if (match === null)
|
|
876
|
+
throw new CrmInputError(`${field} must be an ISO-8601 timestamp`);
|
|
877
|
+
const year = Number(match[1]);
|
|
878
|
+
const month = Number(match[2]);
|
|
879
|
+
const day = Number(match[3]);
|
|
880
|
+
const hour = Number(match[4]);
|
|
881
|
+
const minute = Number(match[5]);
|
|
882
|
+
const second = Number(match[6]);
|
|
883
|
+
const zone = match[7] ?? "";
|
|
884
|
+
const maxDay = daysInMonth(year, month);
|
|
885
|
+
const validZone = zone === "Z" || /^[+-]\d{2}:\d{2}$/.test(zone) && Number(zone.slice(1, 3)) <= 23 && Number(zone.slice(4, 6)) <= 59;
|
|
886
|
+
if (month < 1 || month > 12 || day < 1 || day > maxDay || hour > 23 || minute > 59 || second > 59 || !validZone) {
|
|
887
|
+
throw new CrmInputError(`${field} must be an ISO-8601 timestamp`);
|
|
888
|
+
}
|
|
889
|
+
const normalized = new Date(parsed);
|
|
890
|
+
if (!Number.isFinite(normalized.getTime()) || !/^\d{4}-\d{2}-\d{2}T/.test(normalized.toISOString())) {
|
|
891
|
+
throw new CrmInputError(`${field} must normalize to a four-digit UTC year`);
|
|
892
|
+
}
|
|
893
|
+
return parsed;
|
|
894
|
+
}
|
|
895
|
+
function daysInMonth(year, month) {
|
|
896
|
+
if (month === 2)
|
|
897
|
+
return year % 4 === 0 && (year % 100 !== 0 || year % 400 === 0) ? 29 : 28;
|
|
898
|
+
return month === 4 || month === 6 || month === 9 || month === 11 ? 30 : 31;
|
|
899
|
+
}
|
|
900
|
+
function validateBatchSize(value) {
|
|
901
|
+
if (!Number.isInteger(value) || value < 1 || value > CRM_MAX_BATCH_SIZE) {
|
|
902
|
+
throw new CrmInputError(`maxBatchSize must be an integer between 1 and ${CRM_MAX_BATCH_SIZE}`);
|
|
903
|
+
}
|
|
904
|
+
return value;
|
|
905
|
+
}
|
|
906
|
+
|
|
907
|
+
export { CRM_MAX_REQUEST_BYTES, CRM_MAX_RESPONSE_BYTES, CRM_MAX_ID_CHARS, CRM_MAX_CURSOR_CHARS, CRM_MAX_SEARCH_LIMIT, CRM_MAX_ACTIVITY_PAGE_SIZE, CRM_MAX_ATTRIBUTE_STRING_CHARS, CRM_MAX_OUTREACH_QUEUE_ITEMS, CRM_MAX_OUTREACH_CANDIDATES_PAGE, CRM_NOTE_ACTIVITY, CRM_OUTREACH_DRAFTED_ACTIVITY, CRM_OUTREACH_SENT_ACTIVITY, CRM_OUTREACH_REPLY_ACTIVITY, CRM_REFUSAL_REASONS, CRM_REQUIRED_PROVIDER_OPERATIONS, CrmInputError, crmResponseFitsBudget, parseCrmUpsertRecordsArgs, parseCrmSearchResponse, parseCrmGetRecordResponse, parseCrmOutreachQueueResponse, parseCrmOperationAck, parseCrmUpdateStatusArgs, parseCrmLogActivityArgs, parseCrmGetActivityResponse, parseCrmOutreachState, parseCrmOutreachCandidatesResponse, parseCrmProviderMeta, isCrmIdempotencyKey, parseCrmTimestamp };
|