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