@dev.smartpricing/message-composer-utils 3.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (119) hide show
  1. package/dist/types/api/aiIntegration.js +7 -0
  2. package/dist/types/api/aiIntegration.js.map +1 -0
  3. package/dist/types/api/apiMessages.js +2 -0
  4. package/dist/types/api/apiMessages.js.map +1 -0
  5. package/dist/types/api/index.js +3 -0
  6. package/dist/types/api/index.js.map +1 -0
  7. package/dist/types/app.js +23 -0
  8. package/dist/types/app.js.map +1 -0
  9. package/dist/types/brand.js +31 -0
  10. package/dist/types/brand.js.map +1 -0
  11. package/dist/types/dynamic-values.js +5 -0
  12. package/dist/types/dynamic-values.js.map +1 -0
  13. package/dist/types/emailblocks.v1.defaults.js +83 -0
  14. package/dist/types/emailblocks.v1.defaults.js.map +1 -0
  15. package/dist/types/emailblocks.v1.js +339 -0
  16. package/dist/types/emailblocks.v1.js.map +1 -0
  17. package/dist/types/index.js +16 -0
  18. package/dist/types/index.js.map +1 -0
  19. package/dist/types/kafka-events.js +2 -0
  20. package/dist/types/kafka-events.js.map +1 -0
  21. package/dist/types/language.js +2 -0
  22. package/dist/types/language.js.map +1 -0
  23. package/dist/types/media.js +6 -0
  24. package/dist/types/media.js.map +1 -0
  25. package/dist/types/message.js +41 -0
  26. package/dist/types/message.js.map +1 -0
  27. package/dist/types/meta-template.js +2 -0
  28. package/dist/types/meta-template.js.map +1 -0
  29. package/dist/types/meta-webhook.js +2 -0
  30. package/dist/types/meta-webhook.js.map +1 -0
  31. package/dist/types/submission-job.js +2 -0
  32. package/dist/types/submission-job.js.map +1 -0
  33. package/dist/types/tag.js +2 -0
  34. package/dist/types/tag.js.map +1 -0
  35. package/dist/types/utils.js +2 -0
  36. package/dist/types/utils.js.map +1 -0
  37. package/dist/types/whatsappblocks.v1.js +24 -0
  38. package/dist/types/whatsappblocks.v1.js.map +1 -0
  39. package/dist/utils/array.js +26 -0
  40. package/dist/utils/array.js.map +1 -0
  41. package/dist/utils/async.js +22 -0
  42. package/dist/utils/async.js.map +1 -0
  43. package/dist/utils/date.js +37 -0
  44. package/dist/utils/date.js.map +1 -0
  45. package/dist/utils/index.js +34 -0
  46. package/dist/utils/index.js.map +1 -0
  47. package/dist/utils/intl.js +37 -0
  48. package/dist/utils/intl.js.map +1 -0
  49. package/dist/utils/meta-status.js +21 -0
  50. package/dist/utils/meta-status.js.map +1 -0
  51. package/dist/utils/number.js +24 -0
  52. package/dist/utils/number.js.map +1 -0
  53. package/dist/utils/object.js +87 -0
  54. package/dist/utils/object.js.map +1 -0
  55. package/dist/utils/string.js +22 -0
  56. package/dist/utils/string.js.map +1 -0
  57. package/dist/utils/templates/brand-cascade.js +116 -0
  58. package/dist/utils/templates/brand-cascade.js.map +1 -0
  59. package/dist/utils/templates/email-compiler.js +126 -0
  60. package/dist/utils/templates/email-compiler.js.map +1 -0
  61. package/dist/utils/templates/email-mjml.js +444 -0
  62. package/dist/utils/templates/email-mjml.js.map +1 -0
  63. package/dist/utils/templates/email.js +580 -0
  64. package/dist/utils/templates/email.js.map +1 -0
  65. package/dist/utils/templates/html-sanitizer.js +95 -0
  66. package/dist/utils/templates/html-sanitizer.js.map +1 -0
  67. package/dist/utils/templates/messages.js +33 -0
  68. package/dist/utils/templates/messages.js.map +1 -0
  69. package/dist/utils/templates/meta.js +246 -0
  70. package/dist/utils/templates/meta.js.map +1 -0
  71. package/dist/utils/templates/tiptap.js +167 -0
  72. package/dist/utils/templates/tiptap.js.map +1 -0
  73. package/dist/utils/templates/whatsapp-compiler.js +168 -0
  74. package/dist/utils/templates/whatsapp-compiler.js.map +1 -0
  75. package/dist/utils/templates/whatsapp.js +329 -0
  76. package/dist/utils/templates/whatsapp.js.map +1 -0
  77. package/dist/utils/validations.js +39 -0
  78. package/dist/utils/validations.js.map +1 -0
  79. package/package.json +43 -0
  80. package/src/types/api/aiIntegration.ts +59 -0
  81. package/src/types/api/apiMessages.ts +22 -0
  82. package/src/types/api/index.ts +2 -0
  83. package/src/types/app.ts +76 -0
  84. package/src/types/brand.ts +80 -0
  85. package/src/types/dynamic-values.ts +47 -0
  86. package/src/types/emailblocks.v1.defaults.ts +123 -0
  87. package/src/types/emailblocks.v1.ts +655 -0
  88. package/src/types/index.ts +15 -0
  89. package/src/types/kafka-events.ts +46 -0
  90. package/src/types/language.ts +9 -0
  91. package/src/types/media.ts +26 -0
  92. package/src/types/message.ts +635 -0
  93. package/src/types/meta-template.ts +50 -0
  94. package/src/types/meta-webhook.ts +32 -0
  95. package/src/types/submission-job.ts +30 -0
  96. package/src/types/tag.ts +8 -0
  97. package/src/types/utils.ts +14 -0
  98. package/src/types/whatsappblocks.v1.ts +102 -0
  99. package/src/utils/array.ts +33 -0
  100. package/src/utils/async.ts +30 -0
  101. package/src/utils/date.ts +39 -0
  102. package/src/utils/index.ts +44 -0
  103. package/src/utils/intl.ts +51 -0
  104. package/src/utils/meta-status.ts +24 -0
  105. package/src/utils/number.ts +36 -0
  106. package/src/utils/object.ts +101 -0
  107. package/src/utils/string.ts +24 -0
  108. package/src/utils/templates/brand-cascade.ts +156 -0
  109. package/src/utils/templates/email-compiler.ts +155 -0
  110. package/src/utils/templates/email-mjml.ts +602 -0
  111. package/src/utils/templates/email.ts +713 -0
  112. package/src/utils/templates/html-sanitizer.ts +121 -0
  113. package/src/utils/templates/messages.ts +44 -0
  114. package/src/utils/templates/meta.ts +336 -0
  115. package/src/utils/templates/tiptap.ts +247 -0
  116. package/src/utils/templates/whatsapp-compiler.ts +212 -0
  117. package/src/utils/templates/whatsapp.ts +408 -0
  118. package/src/utils/validations.ts +49 -0
  119. package/tsconfig.json +10 -0
@@ -0,0 +1,121 @@
1
+ /**
2
+ * HTML Sanitization for Email Blocks
3
+ *
4
+ * Sanitizes HTML content to remove potentially dangerous elements
5
+ * and ensure email client compatibility.
6
+ */
7
+
8
+ /**
9
+ * List of HTML event handler attributes to remove
10
+ */
11
+ const EVENT_HANDLERS = [
12
+ "onclick",
13
+ "ondblclick",
14
+ "onmousedown",
15
+ "onmouseup",
16
+ "onmouseover",
17
+ "onmousemove",
18
+ "onmouseout",
19
+ "onmouseenter",
20
+ "onmouseleave",
21
+ "onload",
22
+ "onunload",
23
+ "onchange",
24
+ "onsubmit",
25
+ "onreset",
26
+ "onselect",
27
+ "onblur",
28
+ "onfocus",
29
+ "onkeydown",
30
+ "onkeypress",
31
+ "onkeyup",
32
+ "onerror",
33
+ "onabort",
34
+ "onresize",
35
+ "onscroll",
36
+ ];
37
+
38
+ /**
39
+ * Sanitize HTML content for email use
40
+ *
41
+ * Removes:
42
+ * - <script> tags and their content
43
+ * - <style> tags (inline styles are allowed)
44
+ * - Event handlers (onclick, onerror, etc.)
45
+ * - javascript: URLs
46
+ * - Non-HTTPS external resources
47
+ *
48
+ * @param html - Raw HTML content
49
+ * @returns Sanitized HTML content
50
+ */
51
+ export function sanitizeEmailHtml(html: string): string {
52
+ if (!html || html.trim() === "") {
53
+ return "";
54
+ }
55
+
56
+ let sanitized = html;
57
+
58
+ // Remove <script> tags and their content
59
+ sanitized = sanitized.replace(
60
+ /<script\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/script>/gi,
61
+ "",
62
+ );
63
+
64
+ // Remove <style> tags and their content
65
+ sanitized = sanitized.replace(
66
+ /<style\b[^<]*(?:(?!<\/style>)<[^<]*)*<\/style>/gi,
67
+ "",
68
+ );
69
+
70
+ // Remove javascript: URLs in href and src attributes
71
+ sanitized = sanitized.replace(/href\s*=\s*["']?\s*javascript:/gi, 'href="#"');
72
+ sanitized = sanitized.replace(/src\s*=\s*["']?\s*javascript:/gi, 'src=""');
73
+
74
+ // Remove event handler attributes
75
+ for (const handler of EVENT_HANDLERS) {
76
+ const regex = new RegExp(`\\s${handler}\\s*=\\s*["'][^"']*["']`, "gi");
77
+ sanitized = sanitized.replace(regex, "");
78
+ }
79
+
80
+ // Replace HTTP URLs with HTTPS in src and href attributes
81
+ // This is a best-effort approach - broken HTTPS links are better than HTTP for email security
82
+ sanitized = sanitized.replace(
83
+ /(src|href)\s*=\s*["']http:\/\//gi,
84
+ '$1="https://',
85
+ );
86
+
87
+ return sanitized;
88
+ }
89
+
90
+ /**
91
+ * Validate HTML content length
92
+ *
93
+ * @param html - HTML content
94
+ * @param maxLength - Maximum allowed length
95
+ * @returns True if valid, false otherwise
96
+ */
97
+ export function validateHtmlLength(html: string, maxLength: number): boolean {
98
+ return html.length <= maxLength;
99
+ }
100
+
101
+ /**
102
+ * Get validation errors for HTML content
103
+ *
104
+ * @param html - HTML content
105
+ * @param maxLength - Maximum allowed length
106
+ * @returns Array of validation error messages
107
+ */
108
+ export function getHtmlValidationErrors(
109
+ html: string,
110
+ maxLength: number,
111
+ ): string[] {
112
+ const errors: string[] = [];
113
+
114
+ if (html.length > maxLength) {
115
+ errors.push(
116
+ `HTML content exceeds maximum length of ${maxLength} characters`,
117
+ );
118
+ }
119
+
120
+ return errors;
121
+ }
@@ -0,0 +1,44 @@
1
+ import type { MessageFromDb, RenderType } from "../../types/index.js";
2
+ import {
3
+ generateWhatsappTextPreview,
4
+ generateWhatsappText,
5
+ generateWhatsappHtml,
6
+ } from "./whatsapp.js";
7
+ import { JSONContent } from "@tiptap/core";
8
+ import {
9
+ generateEmailHtml,
10
+ generateEmailText,
11
+ generateEmailTextPreview,
12
+ } from "./email.js";
13
+
14
+ export function generateTextPreview(message: MessageFromDb) {
15
+ switch (message.render_type) {
16
+ case "whatsapp_v1":
17
+ return generateWhatsappTextPreview(message);
18
+ case "email_v1":
19
+ return generateEmailTextPreview(message);
20
+ default:
21
+ return "";
22
+ }
23
+ }
24
+
25
+ export function generateText(content: JSONContent, render_type: RenderType) {
26
+ switch (render_type) {
27
+ case "whatsapp_v1":
28
+ return generateWhatsappText(content);
29
+ case "email_v1":
30
+ return generateEmailText(content);
31
+ default:
32
+ return "";
33
+ }
34
+ }
35
+ export function generateHtml(content: JSONContent, render_type: RenderType) {
36
+ switch (render_type) {
37
+ case "whatsapp_v1":
38
+ return generateWhatsappHtml(content);
39
+ case "email_v1":
40
+ return generateEmailHtml(content);
41
+ default:
42
+ return "";
43
+ }
44
+ }
@@ -0,0 +1,336 @@
1
+ import {
2
+ type WhatsappMessage,
3
+ type WithDbTimestamps,
4
+ type HeaderBlockV1,
5
+ type BodyBlockV1,
6
+ type FooterBlockV1,
7
+ type ButtonBlockV1,
8
+ type WaButtonV1,
9
+ type NamedParam,
10
+ type MetaTemplate,
11
+ type MetaHeaderComponent,
12
+ type MetaBodyComponent,
13
+ type MetaFooterComponent,
14
+ type MetaButtonComponent,
15
+ WA_BODY_MAX_LENGTH,
16
+ WA_HEADER_TEXT_MAX_LENGTH,
17
+ WA_FOOTER_MAX_LENGTH,
18
+ } from "../../types/index.js";
19
+ import { convertToWhatsappMarkdown } from "./whatsapp.js";
20
+
21
+ /**
22
+ * Generates a unique template name for Meta
23
+ */
24
+ function generateMetaTemplateName(
25
+ groupName: string,
26
+ languageId: string,
27
+ prefix: string,
28
+ ): string {
29
+ const sanitized = groupName
30
+ .toLowerCase()
31
+ .replace(/[^a-z0-9]/g, "_")
32
+ .replace(/_+/g, "_")
33
+ .replace(/^_|_$/g, "");
34
+
35
+ const randomSuffix = Math.random().toString(36).substring(2, 8);
36
+ const name = `${prefix}_${sanitized}_${languageId}_${randomSuffix}`;
37
+
38
+ // Ensure max 512 characters
39
+ return name.substring(0, 512);
40
+ }
41
+
42
+ function generateExampleContent(
43
+ text: string,
44
+ componentType: "body" | "header",
45
+ ): { [key: string]: NamedParam[] } | undefined {
46
+ const paramRegex = /\{\{([^}]+)\}\}/g;
47
+ const matches = [...text.matchAll(paramRegex)];
48
+
49
+ if (matches.length === 0) {
50
+ return undefined;
51
+ }
52
+
53
+ /**
54
+ * Generates an example value based on parameter name patterns
55
+ */
56
+ function getExampleValue(paramName: string): string {
57
+ const lowerParam = paramName.toLowerCase();
58
+
59
+ // Check patterns in order of specificity
60
+ if (
61
+ lowerParam.includes("link") ||
62
+ lowerParam.includes("url") ||
63
+ lowerParam.includes("website")
64
+ ) {
65
+ return "https://checkin.smartness.com?checkin=123";
66
+ }
67
+ if (lowerParam.includes("phone")) {
68
+ return "+39 123 456 7890";
69
+ }
70
+ if (
71
+ lowerParam.includes("number") ||
72
+ lowerParam.includes("code") ||
73
+ lowerParam.includes("pin") ||
74
+ lowerParam.includes("id")
75
+ ) {
76
+ return "111";
77
+ }
78
+ if (lowerParam.includes("mail")) {
79
+ return "example@example.com";
80
+ }
81
+ if (
82
+ lowerParam.includes("accommodation") ||
83
+ lowerParam.includes("property")
84
+ ) {
85
+ return "AB Hotel";
86
+ }
87
+ if (lowerParam.includes("surname")) {
88
+ return "Rossi";
89
+ }
90
+ if (lowerParam.includes("name")) {
91
+ return "Mario";
92
+ }
93
+ if (lowerParam.includes("date")) {
94
+ return "2024-06-15";
95
+ }
96
+
97
+ if (
98
+ lowerParam.includes("amount") ||
99
+ lowerParam.includes("price") ||
100
+ lowerParam.includes("tax")
101
+ ) {
102
+ return "100";
103
+ }
104
+
105
+ // Default fallback
106
+ return `Example ${paramName}`;
107
+ }
108
+
109
+ const namedParams = matches.map((match) => {
110
+ const paramName = match[1].trim();
111
+ const example = getExampleValue(paramName);
112
+
113
+ return {
114
+ param_name: paramName,
115
+ example: example,
116
+ };
117
+ });
118
+
119
+ const propertyName = `${componentType}_text_named_params`;
120
+
121
+ return {
122
+ [propertyName]: namedParams,
123
+ };
124
+ }
125
+
126
+ /**
127
+ * Translates WhatsApp header to Meta header component
128
+ */
129
+ function translateHeaderToMeta(
130
+ header: HeaderBlockV1,
131
+ ): MetaHeaderComponent | null {
132
+ switch (header.type) {
133
+ case "none": {
134
+ return null;
135
+ }
136
+ case "text": {
137
+ if (!header.content) return null;
138
+ const text = convertToWhatsappMarkdown(header.content);
139
+ const exampleContent = generateExampleContent(text, "header");
140
+
141
+ const result: MetaHeaderComponent = {
142
+ type: "HEADER",
143
+ format: "TEXT",
144
+ text: text.substring(0, WA_HEADER_TEXT_MAX_LENGTH), // Meta limit
145
+ };
146
+
147
+ if (exampleContent && "header_text_named_params" in exampleContent) {
148
+ result.example = exampleContent as {
149
+ header_text_named_params: NamedParam[];
150
+ };
151
+ }
152
+
153
+ return result;
154
+ }
155
+ case "image":
156
+ case "video":
157
+ case "document": {
158
+ const result: MetaHeaderComponent = {
159
+ type: "HEADER",
160
+ format: header.type.toUpperCase() as "IMAGE" | "VIDEO" | "DOCUMENT",
161
+ };
162
+
163
+ // Include media handle if available
164
+ if (header.metaMediaId) {
165
+ result.example = { header_handle: [header.metaMediaId] };
166
+ }
167
+
168
+ return result;
169
+ }
170
+ default: {
171
+ return null;
172
+ }
173
+ }
174
+ }
175
+
176
+ /**
177
+ * Translates WhatsApp body to Meta body component
178
+ */
179
+ function translateBodyToMeta(body: BodyBlockV1): MetaBodyComponent {
180
+ const text = convertToWhatsappMarkdown(body.content);
181
+ const exampleContent = generateExampleContent(text, "body");
182
+
183
+ const result: MetaBodyComponent = {
184
+ type: "BODY",
185
+ text: text.substring(0, WA_BODY_MAX_LENGTH), // Meta limit
186
+ };
187
+
188
+ if (exampleContent && "body_text_named_params" in exampleContent) {
189
+ result.example = exampleContent as { body_text_named_params: NamedParam[] };
190
+ }
191
+
192
+ return result;
193
+ }
194
+
195
+ /**
196
+ * Translates WhatsApp footer to Meta footer component
197
+ */
198
+ function translateFooterToMeta(
199
+ footer: FooterBlockV1,
200
+ ): MetaFooterComponent | null {
201
+ if (!footer.enabled || !footer.content.trim()) {
202
+ return null;
203
+ }
204
+
205
+ return {
206
+ type: "FOOTER",
207
+ text: footer.content.substring(0, WA_FOOTER_MAX_LENGTH), // Meta limit
208
+ };
209
+ }
210
+
211
+ /**
212
+ * Generates example values for button URL parameters
213
+ */
214
+ function generateButtonExamples(url: string): string[] | undefined {
215
+ const paramRegex = /\{\{([^}]+)\}\}/g;
216
+ const matches = [...url.matchAll(paramRegex)];
217
+
218
+ if (matches.length === 0) {
219
+ return undefined;
220
+ }
221
+
222
+ return Array.from({ length: matches.length }).map((_, index) => {
223
+ return `https://connect.smartness.com/123/book/${index + 1}`;
224
+ });
225
+ }
226
+
227
+ /**
228
+ * Translates WhatsApp buttons to Meta button component
229
+ */
230
+ function translateButtonsToMeta(
231
+ buttons: ButtonBlockV1,
232
+ ): MetaButtonComponent | null {
233
+ if (!buttons.enabled || buttons.buttons.length === 0) {
234
+ return null;
235
+ }
236
+
237
+ const metaButtons = buttons.buttons
238
+ .map((button: WaButtonV1, index: number) => {
239
+ switch (button.type) {
240
+ case "quick_reply":
241
+ case "unsubscribe":
242
+ return {
243
+ type: "QUICK_REPLY" as const,
244
+ text: button.text.substring(0, 20),
245
+ };
246
+ case "phone":
247
+ return {
248
+ type: "PHONE_NUMBER" as const,
249
+ text: button.text.substring(0, 20),
250
+ phone_number: button.phoneNumber,
251
+ };
252
+ case "link": {
253
+ const examples = generateButtonExamples(button.url);
254
+
255
+ // Extract {{<variable_name>}} (single param case) and replace with {{1}} for url as required by Meta
256
+ let metaUrl = button.url;
257
+ const singleParamMatch = metaUrl.match(/\{\{([^}]+)\}\}/);
258
+ if (singleParamMatch) {
259
+ metaUrl = metaUrl.replace(singleParamMatch[0], `{{${index + 1}}}`);
260
+ }
261
+
262
+ const result: {
263
+ type: "URL";
264
+ text: string;
265
+ url: string;
266
+ example?: string[];
267
+ } = {
268
+ type: "URL" as const,
269
+ text: button.text.substring(0, 20),
270
+ url: metaUrl,
271
+ };
272
+
273
+ if (examples) {
274
+ result.example = examples;
275
+ }
276
+
277
+ return result;
278
+ }
279
+ default:
280
+ return null;
281
+ }
282
+ })
283
+ .filter((btn): btn is NonNullable<typeof btn> => btn !== null);
284
+
285
+ if (metaButtons.length === 0) return null;
286
+
287
+ return {
288
+ type: "BUTTONS",
289
+ buttons: metaButtons.slice(0, 10), // Meta limit
290
+ };
291
+ }
292
+
293
+ /**
294
+ * Translates a WhatsApp message to Meta template format
295
+ */
296
+ export function translateWhatsappToMeta(
297
+ message: WithDbTimestamps<WhatsappMessage>,
298
+ groupName: string,
299
+ groupCategory: "marketing" | "utility",
300
+ prefix: string,
301
+ ): MetaTemplate {
302
+ const components: MetaTemplate["components"] = [];
303
+
304
+ // Header
305
+ const headerComponent = translateHeaderToMeta(message.body.header);
306
+ if (headerComponent) {
307
+ components.push(headerComponent);
308
+ }
309
+
310
+ // Body (mandatory)
311
+ const bodyComponent = translateBodyToMeta(message.body.body);
312
+ components.push(bodyComponent);
313
+
314
+ // Footer
315
+ const footerComponent = translateFooterToMeta(message.body.footer);
316
+ if (footerComponent) {
317
+ components.push(footerComponent);
318
+ }
319
+
320
+ // Buttons
321
+ const buttonComponent = translateButtonsToMeta(message.body.buttons);
322
+ if (buttonComponent) {
323
+ components.push(buttonComponent);
324
+ }
325
+
326
+ return {
327
+ name:
328
+ message.metadata?.meta?.template_name ||
329
+ generateMetaTemplateName(groupName, message.language_id, prefix),
330
+ language: message.language_id,
331
+ category: groupCategory.toUpperCase() as "MARKETING" | "UTILITY",
332
+ parameter_format: "NAMED",
333
+ allow_category_change: true,
334
+ components,
335
+ };
336
+ }