@codematic/opencdp 5.0.13

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.
@@ -0,0 +1,883 @@
1
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
2
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
3
+ return new (P || (P = Promise))(function (resolve, reject) {
4
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
5
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
6
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
7
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
8
+ });
9
+ };
10
+ import axios from "axios";
11
+ import { RegionEU, RegionUS, TrackClient } from "customerio-node";
12
+ import pLimit from "p-limit";
13
+ /**
14
+ * Validates that the identifier is not empty
15
+ */
16
+ function validateIdentifier(identifier) {
17
+ if (identifier === null ||
18
+ identifier === undefined ||
19
+ identifier === "" ||
20
+ (typeof identifier === "string" && identifier.trim() === "")) {
21
+ throw new Error("Identifier cannot be empty");
22
+ }
23
+ }
24
+ /**
25
+ * Validates that the event name is not empty
26
+ */
27
+ function validateEventName(eventName) {
28
+ if (!eventName || eventName.trim() === "") {
29
+ throw new Error("Event name cannot be empty");
30
+ }
31
+ }
32
+ /**
33
+ * Validates that properties is a valid object
34
+ */
35
+ function validateProperties(properties) {
36
+ if (properties === null || properties === undefined) {
37
+ return {};
38
+ }
39
+ // if (typeof properties !== 'object' || Array.isArray(properties)) {
40
+ // throw new Error('Properties must be a valid object');
41
+ // }
42
+ return properties;
43
+ }
44
+ /**
45
+ * Validates that the email address is not empty
46
+ */
47
+ function validateEmail(email) {
48
+ if (!email || email.trim() === "") {
49
+ throw new Error("Email address cannot be empty");
50
+ }
51
+ // Basic email validation
52
+ const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
53
+ if (!emailRegex.test(email)) {
54
+ throw new Error("Invalid email address format");
55
+ }
56
+ }
57
+ /**
58
+ * Validates that the send email request has required fields
59
+ */
60
+ function validateSendEmailRequest(request) {
61
+ const message = request.message;
62
+ // Validate required fields
63
+ if (!message.to) {
64
+ throw new Error("to is required");
65
+ }
66
+ validateEmail(message.to);
67
+ // Validate identifiers - must contain exactly one of: id or email
68
+ if (!message.identifiers) {
69
+ throw new Error("identifiers is required");
70
+ }
71
+ const hasId = "id" in message.identifiers &&
72
+ message.identifiers.id !== undefined &&
73
+ message.identifiers.id !== null &&
74
+ message.identifiers.id !== "";
75
+ const hasEmail = "email" in message.identifiers &&
76
+ message.identifiers.email !== undefined &&
77
+ message.identifiers.email !== null &&
78
+ message.identifiers.email !== "";
79
+ if (!hasId && !hasEmail) {
80
+ throw new Error("identifiers must contain exactly one of: id, email, or cdp_id");
81
+ }
82
+ if (hasId && hasEmail) {
83
+ throw new Error("identifiers must contain exactly one of: id, email, or cdp_id");
84
+ }
85
+ // Validate email fields
86
+ if ("from" in message && message.from) {
87
+ validateEmail(message.from);
88
+ }
89
+ if (message.bcc && message.bcc.length > 0) {
90
+ if (Array.isArray(message.bcc)) {
91
+ message.bcc.forEach(email => validateEmail(email));
92
+ }
93
+ else {
94
+ validateEmail(message.bcc);
95
+ }
96
+ }
97
+ if (message.cc && message.cc.length > 0) {
98
+ if (Array.isArray(message.cc)) {
99
+ message.cc.forEach(email => validateEmail(email));
100
+ }
101
+ else {
102
+ validateEmail(message.cc);
103
+ }
104
+ }
105
+ if (message.reply_to) {
106
+ validateEmail(message.reply_to);
107
+ }
108
+ // Validate send_at if provided
109
+ if (message.send_at !== undefined) {
110
+ if (!Number.isInteger(message.send_at) || message.send_at < 0) {
111
+ throw new Error("send_at must be a positive integer");
112
+ }
113
+ }
114
+ // Validate body fields
115
+ if ("body" in message &&
116
+ message.body !== undefined &&
117
+ message.body !== null &&
118
+ message.body.trim() === "") {
119
+ throw new Error("body cannot be empty if provided");
120
+ }
121
+ if (message.amp_body !== undefined &&
122
+ message.amp_body !== null &&
123
+ message.amp_body.trim() === "") {
124
+ throw new Error("amp_body cannot be empty if provided");
125
+ }
126
+ if (message.plaintext_body !== undefined &&
127
+ message.plaintext_body !== null &&
128
+ message.plaintext_body.trim() === "") {
129
+ throw new Error("plaintext_body cannot be empty if provided");
130
+ }
131
+ // Validate headers if provided
132
+ if (message.headers !== undefined) {
133
+ if (typeof message.headers !== "object" || Array.isArray(message.headers)) {
134
+ throw new Error("headers must be an object");
135
+ }
136
+ }
137
+ // Type guard to check if it's a template-based request
138
+ const isTemplateRequest = message.transactional_message_id !== undefined;
139
+ if (!isTemplateRequest) {
140
+ // Raw email - body, subject, and from are required
141
+ const errors = [];
142
+ if (!("body" in message) || !message.body) {
143
+ errors.push("body is required when not using a template");
144
+ }
145
+ if (!("subject" in message) || !message.subject) {
146
+ errors.push("subject is required when not using a template");
147
+ }
148
+ if (!("from" in message) || !message.from) {
149
+ errors.push("from is required when not using a template");
150
+ }
151
+ if (errors.length > 0) {
152
+ throw new Error(`When not using a template: ${errors.join(", ")}`);
153
+ }
154
+ }
155
+ }
156
+ /**
157
+ * Validates that the send push request has required fields
158
+ */
159
+ function validateSendPushRequest(request) {
160
+ // Validate required fields
161
+ if (!request.identifiers) {
162
+ throw new Error("identifiers is required");
163
+ }
164
+ const hasId = "id" in request.identifiers &&
165
+ request.identifiers.id !== undefined &&
166
+ request.identifiers.id !== null &&
167
+ request.identifiers.id !== "";
168
+ const hasEmail = "email" in request.identifiers &&
169
+ request.identifiers.email !== undefined &&
170
+ request.identifiers.email !== null &&
171
+ request.identifiers.email !== "";
172
+ const hasCdpId = "cdp_id" in request.identifiers &&
173
+ request.identifiers.cdp_id !== undefined &&
174
+ request.identifiers.cdp_id !== null &&
175
+ request.identifiers.cdp_id !== "";
176
+ if (!hasId && !hasEmail && !hasCdpId) {
177
+ throw new Error("identifiers must contain exactly one of: id, email, or cdp_id");
178
+ }
179
+ if ((hasId ? 1 : 0) + (hasEmail ? 1 : 0) + (hasCdpId ? 1 : 0) > 1) {
180
+ throw new Error("identifiers must contain exactly one of: id, email, or cdp_id");
181
+ }
182
+ if (!request.transactional_message_id) {
183
+ throw new Error("transactional_message_id is required");
184
+ }
185
+ // Validate body field
186
+ if (request.body !== undefined &&
187
+ request.body !== null &&
188
+ request.body.trim() === "") {
189
+ throw new Error("body cannot be empty if provided");
190
+ }
191
+ }
192
+ /**
193
+ * Validates phone number format (E.164 format)
194
+ */
195
+ function validatePhoneNumber(phone) {
196
+ if (!phone || phone.trim() === "") {
197
+ throw new Error("Phone number cannot be empty");
198
+ }
199
+ // E.164 format: ^\+?[1-9]\d{1,14}$
200
+ const phoneRegex = /^\+?[1-9]\d{1,14}$/;
201
+ if (!phoneRegex.test(phone)) {
202
+ throw new Error("Phone number must be in international format (e.g., +1234567890)");
203
+ }
204
+ }
205
+ /**
206
+ * Validates that the send SMS request has required fields
207
+ */
208
+ function validateSendSmsRequest(request) {
209
+ // Validate required fields
210
+ if (!request.identifiers) {
211
+ throw new Error("identifiers is required");
212
+ }
213
+ const hasId = "id" in request.identifiers &&
214
+ request.identifiers.id !== undefined &&
215
+ request.identifiers.id !== null &&
216
+ request.identifiers.id !== "";
217
+ const hasEmail = "email" in request.identifiers &&
218
+ request.identifiers.email !== undefined &&
219
+ request.identifiers.email !== null &&
220
+ request.identifiers.email !== "";
221
+ const hasCdpId = "cdp_id" in request.identifiers &&
222
+ request.identifiers.cdp_id !== undefined &&
223
+ request.identifiers.cdp_id !== null &&
224
+ request.identifiers.cdp_id !== "";
225
+ if (!hasId && !hasEmail && !hasCdpId) {
226
+ throw new Error("identifiers must contain exactly one of: id, email, or cdp_id");
227
+ }
228
+ if ((hasId ? 1 : 0) + (hasEmail ? 1 : 0) + (hasCdpId ? 1 : 0) > 1) {
229
+ throw new Error("identifiers must contain exactly one of: id, email, or cdp_id");
230
+ }
231
+ // Validate conditional requirement: body is required if no transactional_message_id
232
+ const hasTemplateId = request.transactional_message_id !== undefined &&
233
+ request.transactional_message_id !== null &&
234
+ request.transactional_message_id !== "";
235
+ if (!hasTemplateId && !request.body) {
236
+ throw new Error("body is required when not using a template");
237
+ }
238
+ // Validate phone number format if to is provided
239
+ if (request.to) {
240
+ validatePhoneNumber(request.to);
241
+ }
242
+ // Validate from phone number format if provided
243
+ if (request.from) {
244
+ validatePhoneNumber(request.from);
245
+ }
246
+ // Validate body field
247
+ if (request.body !== undefined &&
248
+ request.body !== null &&
249
+ request.body.trim() === "") {
250
+ throw new Error("body cannot be empty if provided");
251
+ }
252
+ // Validate message_data is an object if provided
253
+ if (request.message_data !== undefined) {
254
+ if (request.message_data === null ||
255
+ typeof request.message_data !== "object" ||
256
+ Array.isArray(request.message_data)) {
257
+ throw new Error("message_data must be an object");
258
+ }
259
+ }
260
+ }
261
+ const DEFAULT_CONCURRENCY = 10;
262
+ const MAX_SAFE_CONCURRENCY = 30;
263
+ export class CDPClient {
264
+ constructor(config) {
265
+ this.config = config;
266
+ this.customerIoClient = null;
267
+ this.apiRoot =
268
+ config.cdpEndpoint || "https://api.opencdp.io/gateway/data-gateway";
269
+ this.sendToCustomerIo = Boolean(config.sendToCustomerIo && config.customerIo);
270
+ this.timeout = config.timeout || 10000;
271
+ // Create axios instance with connection pooling and reuse
272
+ this.axiosInstance = axios.create({
273
+ baseURL: this.apiRoot,
274
+ timeout: this.timeout,
275
+ headers: {
276
+ Authorization: this.config.cdpApiKey,
277
+ "Content-Type": "application/json",
278
+ },
279
+ // Enable connection pooling and reuse
280
+ httpAgent: new (require("http").Agent)({
281
+ keepAlive: true,
282
+ keepAliveMsecs: 1000,
283
+ maxSockets: 50,
284
+ maxFreeSockets: 10,
285
+ timeout: 60000,
286
+ freeSocketTimeout: 30000,
287
+ }),
288
+ httpsAgent: new (require("https").Agent)({
289
+ keepAlive: true,
290
+ keepAliveMsecs: 1000,
291
+ maxSockets: 50,
292
+ maxFreeSockets: 10,
293
+ timeout: 60000,
294
+ freeSocketTimeout: 30000,
295
+ }),
296
+ });
297
+ let requestedConcurrency = config.maxConcurrentRequests || DEFAULT_CONCURRENCY;
298
+ if (requestedConcurrency < 1) {
299
+ requestedConcurrency = DEFAULT_CONCURRENCY;
300
+ }
301
+ const concurrencyLimit = Math.min(requestedConcurrency, MAX_SAFE_CONCURRENCY);
302
+ if (config.cdpLogger) {
303
+ this.logger = config.cdpLogger;
304
+ }
305
+ else {
306
+ this.logger = {
307
+ debug: console.debug.bind(console),
308
+ error: console.error.bind(console),
309
+ warn: console.warn.bind(console),
310
+ };
311
+ }
312
+ if (requestedConcurrency > MAX_SAFE_CONCURRENCY && this.config.debug) {
313
+ this.logger.debug(`[CDP] maxConcurrentRequests (${requestedConcurrency}) exceeds limit. Using capped value: ${concurrencyLimit}`);
314
+ }
315
+ // Initialize the concurrency limiter
316
+ this.limit = pLimit(concurrencyLimit);
317
+ if (this.sendToCustomerIo && config.customerIo) {
318
+ const region = config.customerIo.region === "eu" ? RegionEU : RegionUS;
319
+ try {
320
+ this.customerIoClient = new TrackClient(config.customerIo.siteId, config.customerIo.apiKey, { region });
321
+ }
322
+ catch (error) {
323
+ if (this.config.debug) {
324
+ this.logger.error("[Customer.io] Initialize error", { error });
325
+ }
326
+ }
327
+ }
328
+ }
329
+ /**
330
+ * Tests the connection to the OpenCDP API server.
331
+ * Sends a ping request to verify that the configured endpoint is reachable and valid.
332
+ *
333
+ * This method ensures that credentials, and network access are configured correctly.
334
+ * It does NOT establish a persistent connection.
335
+ *
336
+ * Do not ping before sending each request
337
+ * @throws Error only when config.failOnException === true and the connection fails due to invalid credentials, network issues, or timeouts.
338
+ */
339
+ ping() {
340
+ return __awaiter(this, void 0, void 0, function* () {
341
+ yield this.validateConnection();
342
+ });
343
+ }
344
+ validateConnection() {
345
+ return __awaiter(this, void 0, void 0, function* () {
346
+ var _a, _b, _c;
347
+ try {
348
+ const response = yield this.axiosInstance.get("/v1/health/ping");
349
+ if (this.config.debug) {
350
+ this.logger.debug(`[CDP] Connection Established! Status: ${response.status}`);
351
+ }
352
+ }
353
+ catch (error) {
354
+ // Extract details for better debugging
355
+ const statusCode = (_a = error.response) === null || _a === void 0 ? void 0 : _a.status;
356
+ const statusText = (_b = error.response) === null || _b === void 0 ? void 0 : _b.statusText;
357
+ const responseData = (_c = error.response) === null || _c === void 0 ? void 0 : _c.data;
358
+ const dnsError = error.code === "ENOTFOUND";
359
+ const timeoutError = error.code === "ECONNABORTED";
360
+ // Error summary
361
+ const errorSummary = Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign({ message: error.message }, (statusCode && { statusCode })), (statusText && { statusText })), (dnsError && { dnsError: true })), (timeoutError && { timeout: true })), (responseData && { responseData })), { stack: this.config.debug ? error.stack : undefined });
362
+ if (this.config.debug) {
363
+ this.logger.error("[CDP] Failed to connect to CDP Server", errorSummary);
364
+ }
365
+ if (this.config.failOnException) {
366
+ throw error;
367
+ }
368
+ else {
369
+ return;
370
+ }
371
+ }
372
+ });
373
+ }
374
+ limited(fn) {
375
+ return __awaiter(this, void 0, void 0, function* () {
376
+ return this.limit(fn);
377
+ });
378
+ }
379
+ /**
380
+ * Identify a person in the CDP
381
+ * This method is concurrency-limited using p-limit to avoid overwhelming traffic external traffic.
382
+ * @param identifier The person identifier
383
+ * @param properties Additional properties for the person
384
+ * @throws Error only when config.failOnException === true (e.g., when the identifier is empty or the request fails)
385
+ */
386
+ identify(identifier, properties) {
387
+ return __awaiter(this, void 0, void 0, function* () {
388
+ return this.limited(() => __awaiter(this, void 0, void 0, function* () {
389
+ var _a, _b, _c;
390
+ try {
391
+ validateIdentifier(identifier);
392
+ }
393
+ catch (error) {
394
+ if (this.config.debug) {
395
+ this.logger.error("[CDP] Identify validation error", { error });
396
+ }
397
+ if (this.config.failOnException) {
398
+ throw error;
399
+ }
400
+ return;
401
+ }
402
+ const normalizedProps = validateProperties(properties);
403
+ if (this.sendToCustomerIo && this.customerIoClient) {
404
+ try {
405
+ yield this.customerIoClient.identify(identifier, normalizedProps);
406
+ if (this.config.debug) {
407
+ this.logger.debug(`[Customer.io] Identified ${identifier}`);
408
+ }
409
+ }
410
+ catch (error) {
411
+ if (this.config.debug) {
412
+ this.logger.error("[Customer.io] Identify error", { error });
413
+ }
414
+ if (this.config.failOnException) {
415
+ throw error;
416
+ }
417
+ }
418
+ }
419
+ try {
420
+ yield this.axiosInstance.post("/v1/persons/identify", {
421
+ identifier,
422
+ properties: normalizedProps,
423
+ });
424
+ if (this.config.debug) {
425
+ this.logger.debug(`[CDP] Identified ${identifier}`);
426
+ }
427
+ }
428
+ catch (error) {
429
+ // NB: Avoid logging large error objects directly to reduce memory footprint on high traffic apps
430
+ if (this.config.debug) {
431
+ const errorSummary = {
432
+ message: error === null || error === void 0 ? void 0 : error.message,
433
+ status: (_a = error === null || error === void 0 ? void 0 : error.response) === null || _a === void 0 ? void 0 : _a.status,
434
+ data: ((_c = (_b = error === null || error === void 0 ? void 0 : error.response) === null || _b === void 0 ? void 0 : _b.data) === null || _c === void 0 ? void 0 : _c.message) || "[truncated]",
435
+ };
436
+ this.logger.error("[CDP] Identify error", { errorSummary });
437
+ }
438
+ // Re-throw the error so users can handle failures
439
+ if (this.config.failOnException) {
440
+ throw error;
441
+ }
442
+ return;
443
+ }
444
+ }));
445
+ });
446
+ }
447
+ /**
448
+ * Track an event for a person.
449
+ * @param identifier The person identifier
450
+ * @param eventName The event name
451
+ * @param properties Additional properties for the event
452
+ * @throws Error only when config.failOnException === true (e.g., when validation or request fails)
453
+ */
454
+ track(identifier, eventName, properties) {
455
+ return __awaiter(this, void 0, void 0, function* () {
456
+ return this.limited(() => __awaiter(this, void 0, void 0, function* () {
457
+ var _a, _b, _c;
458
+ try {
459
+ validateIdentifier(identifier);
460
+ validateEventName(eventName);
461
+ }
462
+ catch (error) {
463
+ if (this.config.debug) {
464
+ this.logger.error("[CDP] Track validation error", { error });
465
+ }
466
+ if (this.config.failOnException) {
467
+ throw error;
468
+ }
469
+ return;
470
+ }
471
+ try {
472
+ const normalizedProps = validateProperties(properties);
473
+ if (this.sendToCustomerIo && this.customerIoClient) {
474
+ try {
475
+ yield this.customerIoClient.track(identifier, {
476
+ name: eventName,
477
+ data: normalizedProps,
478
+ });
479
+ if (this.config.debug) {
480
+ this.logger.debug(`[Customer.io] Tracked event ${eventName} for ${identifier}`);
481
+ }
482
+ }
483
+ catch (error) {
484
+ if (this.config.debug) {
485
+ this.logger.error("[Customer.io] Track error", { error });
486
+ }
487
+ if (this.config.failOnException) {
488
+ throw error;
489
+ }
490
+ }
491
+ }
492
+ yield this.axiosInstance.post("/v1/persons/track", {
493
+ identifier,
494
+ eventName: eventName,
495
+ properties: normalizedProps,
496
+ });
497
+ if (this.config.debug) {
498
+ this.logger.debug(`[CDP] Tracked event ${eventName} for ${identifier}`);
499
+ }
500
+ }
501
+ catch (error) {
502
+ if (this.config.debug) {
503
+ const errorSummary = {
504
+ message: error === null || error === void 0 ? void 0 : error.message,
505
+ status: (_a = error === null || error === void 0 ? void 0 : error.response) === null || _a === void 0 ? void 0 : _a.status,
506
+ data: ((_c = (_b = error === null || error === void 0 ? void 0 : error.response) === null || _b === void 0 ? void 0 : _b.data) === null || _c === void 0 ? void 0 : _c.message) || "[truncated]",
507
+ };
508
+ this.logger.error("[CDP] Track error:", { errorSummary });
509
+ }
510
+ if (this.config.failOnException) {
511
+ throw error;
512
+ }
513
+ return;
514
+ }
515
+ }));
516
+ });
517
+ }
518
+ /**
519
+ * Register a device for a person. A device must be registered to send push notifications
520
+ * @param identifier
521
+ * @param deviceRegistrationParameters
522
+ * @throws Error only when config.failOnException === true (e.g., when validation or request fails)
523
+ */
524
+ registerDevice(identifier, deviceRegistrationParameters) {
525
+ return __awaiter(this, void 0, void 0, function* () {
526
+ return this.limited(() => __awaiter(this, void 0, void 0, function* () {
527
+ var _a, _b, _c;
528
+ try {
529
+ validateIdentifier(identifier);
530
+ }
531
+ catch (error) {
532
+ if (this.config.debug) {
533
+ this.logger.error("[CDP] Register device validation error", {
534
+ error,
535
+ });
536
+ }
537
+ if (this.config.failOnException) {
538
+ throw error;
539
+ }
540
+ return;
541
+ }
542
+ if (this.sendToCustomerIo && this.customerIoClient) {
543
+ try {
544
+ yield this.customerIoClient.addDevice(identifier, deviceRegistrationParameters.deviceId, deviceRegistrationParameters.platform, deviceRegistrationParameters);
545
+ }
546
+ catch (error) {
547
+ if (this.config.debug) {
548
+ this.logger.error("[Customer.io] Register device error", { error });
549
+ }
550
+ if (this.config.failOnException) {
551
+ throw error;
552
+ }
553
+ }
554
+ }
555
+ try {
556
+ yield this.axiosInstance.post("/v1/persons/registerDevice", Object.assign({ identifier }, deviceRegistrationParameters));
557
+ }
558
+ catch (error) {
559
+ if (this.config.debug) {
560
+ // NB: Avoid logging large error objects directly to reduce memory footprint on high traffic apps
561
+ const errorSummary = {
562
+ message: error === null || error === void 0 ? void 0 : error.message,
563
+ status: (_a = error === null || error === void 0 ? void 0 : error.response) === null || _a === void 0 ? void 0 : _a.status,
564
+ data: ((_c = (_b = error === null || error === void 0 ? void 0 : error.response) === null || _b === void 0 ? void 0 : _b.data) === null || _c === void 0 ? void 0 : _c.message) || "[truncated]",
565
+ };
566
+ this.logger.error("[CDP] Register device error:", { errorSummary });
567
+ }
568
+ if (this.config.failOnException) {
569
+ throw error;
570
+ }
571
+ }
572
+ }));
573
+ });
574
+ }
575
+ /**
576
+ * Send an email using the CDP transactional email service
577
+ * @param request The send email request parameters
578
+ * @returns Promise that resolves when the email is sent
579
+ * @throws Error only when config.failOnException === true and validation or the request fails
580
+ */
581
+ sendEmail(request) {
582
+ return __awaiter(this, void 0, void 0, function* () {
583
+ return this.limited(() => __awaiter(this, void 0, void 0, function* () {
584
+ var _a, _b, _c, _d;
585
+ try {
586
+ validateSendEmailRequest(request);
587
+ }
588
+ catch (error) {
589
+ if (this.config.debug) {
590
+ this.logger.error("[CDP] Send email validation error", { error });
591
+ }
592
+ if (this.config.failOnException) {
593
+ throw error;
594
+ }
595
+ return;
596
+ }
597
+ // Check for unsupported fields and log warnings
598
+ this.warnUnsupportedFields(request);
599
+ // Build the request payload - pass through all fields as they match the schema
600
+ const message = request.message;
601
+ const emailPayload = {
602
+ to: message.to,
603
+ identifiers: message.identifiers,
604
+ message_data: message.message_data,
605
+ send_at: message.send_at,
606
+ disable_message_retention: message.disable_message_retention,
607
+ send_to_unsubscribed: message.send_to_unsubscribed,
608
+ queue_draft: message.queue_draft,
609
+ bcc: message.bcc,
610
+ cc: message.cc,
611
+ fake_bcc: message.fake_bcc,
612
+ reply_to: message.reply_to,
613
+ preheader: message.preheader,
614
+ headers: message.headers,
615
+ disable_css_preprocessing: message.disable_css_preprocessing,
616
+ tracked: message.tracked,
617
+ transactional_message_id: "transactional_message_id" in message
618
+ ? message.transactional_message_id
619
+ : undefined,
620
+ body: "body" in message ? message.body : undefined,
621
+ body_amp: message.amp_body,
622
+ body_plain: message.plaintext_body,
623
+ subject: "subject" in message ? message.subject : undefined,
624
+ from: "from" in message ? message.from : undefined,
625
+ language: message.language,
626
+ };
627
+ // Remove undefined values to keep the payload clean
628
+ const cleanPayload = Object.fromEntries(Object.entries(emailPayload).filter(([_, value]) => value !== undefined));
629
+ if (this.sendToCustomerIo && this.customerIoClient && this.config.debug) {
630
+ // Warning that to avoid sending twice it will not be sent to CIO. to turn this off set sendToCustomerIo to false.
631
+ this.logger.warn("[CDP] Warning: Transactional messaging email will NOT be sent to Customer.io to avoid sending twice. To turn this warning off set `sendToCustomerIo` to false.");
632
+ }
633
+ try {
634
+ const response = yield this.axiosInstance.post("/v1/send/email", cleanPayload);
635
+ if (this.config.debug) {
636
+ this.logger.debug(`[CDP] Email sent successfully to ${message.to}`);
637
+ }
638
+ return response.data;
639
+ }
640
+ catch (error) {
641
+ const errorSummary = {
642
+ message: error === null || error === void 0 ? void 0 : error.message,
643
+ status: (_a = error === null || error === void 0 ? void 0 : error.response) === null || _a === void 0 ? void 0 : _a.status,
644
+ data: ((_c = (_b = error === null || error === void 0 ? void 0 : error.response) === null || _b === void 0 ? void 0 : _b.data) === null || _c === void 0 ? void 0 : _c.message) || "[truncated]",
645
+ };
646
+ if (this.config.debug) {
647
+ this.logger.error("[CDP] Send email error:", { errorSummary });
648
+ }
649
+ // Clean up the error to remove technical details while preserving helpful information
650
+ const cleanError = error;
651
+ // Set consistent error properties
652
+ if (errorSummary.data) {
653
+ cleanError.message = errorSummary.data;
654
+ }
655
+ else if (errorSummary.message) {
656
+ cleanError.message = errorSummary.message;
657
+ }
658
+ cleanError.name = "CDPEmailError";
659
+ cleanError.code = "EMAIL_SEND_FAILED";
660
+ cleanError.summary = errorSummary;
661
+ cleanError.status = ((_d = error === null || error === void 0 ? void 0 : error.response) === null || _d === void 0 ? void 0 : _d.status) || 400;
662
+ // Remove technical details that clutter the error
663
+ delete cleanError.config;
664
+ delete cleanError.request;
665
+ // delete cleanError.response;
666
+ delete cleanError.stack;
667
+ if (this.config.failOnException) {
668
+ throw cleanError;
669
+ }
670
+ return { ok: false, error: cleanError };
671
+ }
672
+ }));
673
+ });
674
+ }
675
+ /**
676
+ * Warns about unsupported fields that are accepted but not processed by the backend
677
+ * @param request The send email request parameters
678
+ */
679
+ warnUnsupportedFields(request) {
680
+ const message = request.message;
681
+ const unsupportedFields = [];
682
+ if (message.send_at !== undefined) {
683
+ unsupportedFields.push("send_at");
684
+ }
685
+ if (message.disable_message_retention !== undefined) {
686
+ unsupportedFields.push("disable_message_retention");
687
+ }
688
+ if (message.send_to_unsubscribed !== undefined) {
689
+ unsupportedFields.push("send_to_unsubscribed");
690
+ }
691
+ if (message.queue_draft !== undefined) {
692
+ unsupportedFields.push("queue_draft");
693
+ }
694
+ if (message.headers !== undefined) {
695
+ unsupportedFields.push("headers");
696
+ }
697
+ if (message.disable_css_preprocessing !== undefined) {
698
+ unsupportedFields.push("disable_css_preprocessing");
699
+ }
700
+ if (message.tracked !== undefined) {
701
+ unsupportedFields.push("tracked");
702
+ }
703
+ if (message.fake_bcc !== undefined) {
704
+ unsupportedFields.push("fake_bcc");
705
+ }
706
+ if (message.reply_to !== undefined) {
707
+ unsupportedFields.push("reply_to");
708
+ }
709
+ if (message.preheader !== undefined) {
710
+ unsupportedFields.push("preheader");
711
+ }
712
+ if (message.attachments !== undefined) {
713
+ unsupportedFields.push("attachments");
714
+ }
715
+ if (unsupportedFields.length > 0) {
716
+ this.logger.warn(`[CDP] Warning: The following fields are not yet supported by the backend and will be ignored: ${unsupportedFields.join(", ")}. ` +
717
+ "These fields are included for future compatibility but have no effect on email delivery.");
718
+ }
719
+ }
720
+ /**
721
+ * Send a push notification using the OpenCDP transactional push service
722
+ * @param request The send push request parameters
723
+ * @returns Promise that resolves when the push notification is sent
724
+ * @throws Error only when config.failOnException === true and validation or the request fails
725
+ */
726
+ sendPush(request) {
727
+ return __awaiter(this, void 0, void 0, function* () {
728
+ return this.limited(() => __awaiter(this, void 0, void 0, function* () {
729
+ var _a, _b, _c, _d;
730
+ try {
731
+ validateSendPushRequest(request);
732
+ }
733
+ catch (error) {
734
+ if (this.config.debug) {
735
+ this.logger.error("[CDP] Send push validation error", { error });
736
+ }
737
+ if (this.config.failOnException) {
738
+ throw error;
739
+ }
740
+ return;
741
+ }
742
+ // Build the request payload - pass through all fields as they match the schema
743
+ const pushPayload = {
744
+ identifiers: request.identifiers,
745
+ transactional_message_id: request.transactional_message_id,
746
+ title: request.title,
747
+ body: request.body,
748
+ message_data: request.message_data,
749
+ };
750
+ // Remove undefined values to keep the payload clean
751
+ const cleanPayload = Object.fromEntries(Object.entries(pushPayload).filter(([_, value]) => value !== undefined));
752
+ if (this.sendToCustomerIo && this.customerIoClient && this.config.debug) {
753
+ // Warning that to avoid sending twice it will not be sent to CIO. to turn this off set sendToCustomerIo to false.
754
+ this.logger.warn("[CDP] Warning: Transactional messaging push will NOT be sent to Customer.io to avoid sending twice. To turn this warning off set `sendToCustomerIo` to false.");
755
+ }
756
+ try {
757
+ const response = yield this.axiosInstance.post("/v1/send/push", cleanPayload);
758
+ if (this.config.debug) {
759
+ this.logger.debug(`[CDP] Push notification sent successfully`);
760
+ }
761
+ return response.data;
762
+ }
763
+ catch (error) {
764
+ const errorSummary = {
765
+ message: error === null || error === void 0 ? void 0 : error.message,
766
+ status: (_a = error === null || error === void 0 ? void 0 : error.response) === null || _a === void 0 ? void 0 : _a.status,
767
+ data: ((_c = (_b = error === null || error === void 0 ? void 0 : error.response) === null || _b === void 0 ? void 0 : _b.data) === null || _c === void 0 ? void 0 : _c.message) || "[truncated]",
768
+ };
769
+ if (this.config.debug) {
770
+ this.logger.error("[CDP] Send push error:", { errorSummary });
771
+ }
772
+ // Clean up the error to remove technical details while preserving helpful information
773
+ const cleanError = error;
774
+ if (errorSummary.data) {
775
+ cleanError.message = errorSummary.data;
776
+ }
777
+ else if (errorSummary.message) {
778
+ cleanError.message = errorSummary.message;
779
+ }
780
+ // Set consistent error properties
781
+ cleanError.name = "CDPPushError";
782
+ cleanError.code = "PUSH_SEND_FAILED";
783
+ cleanError.summary = errorSummary;
784
+ cleanError.status = ((_d = error === null || error === void 0 ? void 0 : error.response) === null || _d === void 0 ? void 0 : _d.status) || 400;
785
+ // Remove technical details that clutter the error
786
+ delete cleanError.config;
787
+ delete cleanError.request;
788
+ delete cleanError.response;
789
+ delete cleanError.stack;
790
+ if (this.config.failOnException) {
791
+ throw cleanError;
792
+ }
793
+ return;
794
+ }
795
+ }));
796
+ });
797
+ }
798
+ /**
799
+ * Send an SMS using the OpenCDP transactional SMS service
800
+ * @param request The send SMS request parameters
801
+ * @returns Promise that resolves when the SMS is sent
802
+ * @throws Error only when config.failOnException === true and validation or the request fails
803
+ */
804
+ sendSms(request) {
805
+ return __awaiter(this, void 0, void 0, function* () {
806
+ return this.limited(() => __awaiter(this, void 0, void 0, function* () {
807
+ var _a, _b, _c, _d;
808
+ try {
809
+ validateSendSmsRequest(request);
810
+ }
811
+ catch (error) {
812
+ if (this.config.debug) {
813
+ this.logger.error("[CDP] Send SMS validation error", { error });
814
+ }
815
+ if (this.config.failOnException) {
816
+ throw error;
817
+ }
818
+ return;
819
+ }
820
+ // Build the request payload - pass through all fields as they match the schema
821
+ // Convert transactional_message_id to string if it's a number (backend expects string)
822
+ const transactionalMessageId = request.transactional_message_id !== undefined &&
823
+ request.transactional_message_id !== null &&
824
+ request.transactional_message_id !== ""
825
+ ? String(request.transactional_message_id)
826
+ : undefined;
827
+ const smsPayload = {
828
+ identifiers: request.identifiers,
829
+ transactional_message_id: transactionalMessageId,
830
+ to: request.to,
831
+ from: request.from,
832
+ body: request.body,
833
+ message_data: request.message_data,
834
+ };
835
+ // Remove undefined values to keep the payload clean
836
+ const cleanPayload = Object.fromEntries(Object.entries(smsPayload).filter(([_, value]) => value !== undefined));
837
+ if (this.sendToCustomerIo && this.customerIoClient && this.config.debug) {
838
+ // Warning that to avoid sending twice it will not be sent to CIO. to turn this off set sendToCustomerIo to false.
839
+ this.logger.warn("[CDP] Warning: Transactional messaging SMS will NOT be sent to Customer.io to avoid sending twice. To turn this warning off set `sendToCustomerIo` to false.");
840
+ }
841
+ try {
842
+ const response = yield this.axiosInstance.post("/v1/send/sms", cleanPayload);
843
+ if (this.config.debug) {
844
+ this.logger.debug(`[CDP] SMS sent successfully`);
845
+ }
846
+ return response.data;
847
+ }
848
+ catch (error) {
849
+ const errorSummary = {
850
+ message: error === null || error === void 0 ? void 0 : error.message,
851
+ status: (_a = error === null || error === void 0 ? void 0 : error.response) === null || _a === void 0 ? void 0 : _a.status,
852
+ data: ((_c = (_b = error === null || error === void 0 ? void 0 : error.response) === null || _b === void 0 ? void 0 : _b.data) === null || _c === void 0 ? void 0 : _c.message) || "[truncated]",
853
+ };
854
+ if (this.config.debug) {
855
+ this.logger.error("[CDP] Send SMS error:", { errorSummary });
856
+ }
857
+ // Clean up the error to remove technical details while preserving helpful information
858
+ const cleanError = error;
859
+ if (errorSummary.data) {
860
+ cleanError.message = errorSummary.data;
861
+ }
862
+ else if (errorSummary.message) {
863
+ cleanError.message = errorSummary.message;
864
+ }
865
+ // Set consistent error properties
866
+ cleanError.name = "CDPSmsError";
867
+ cleanError.code = "SMS_SEND_FAILED";
868
+ cleanError.summary = errorSummary;
869
+ cleanError.status = ((_d = error === null || error === void 0 ? void 0 : error.response) === null || _d === void 0 ? void 0 : _d.status) || 400;
870
+ // Remove technical details that clutter the error
871
+ delete cleanError.config;
872
+ delete cleanError.request;
873
+ delete cleanError.response;
874
+ delete cleanError.stack;
875
+ if (this.config.failOnException) {
876
+ throw cleanError;
877
+ }
878
+ return;
879
+ }
880
+ }));
881
+ });
882
+ }
883
+ }