@capillarytech/creatives-library 8.0.353 → 8.0.355

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/index.html CHANGED
@@ -23,6 +23,7 @@
23
23
  <!-- End Google Tag Manager -->
24
24
  <script>try{Typekit.load({ async: true });}catch(e){console.log(e)}</script>
25
25
  <title>Capillary - Creatives</title>
26
+ <script>try{window.APP_ENV=__ENV_OBJECT__;}catch(e){window.APP_ENV={};}</script>
26
27
  </head>
27
28
  <body>
28
29
  <noscript>
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@capillarytech/creatives-library",
3
3
  "author": "meharaj",
4
- "version": "8.0.353",
4
+ "version": "8.0.355",
5
5
  "description": "Capillary creatives ui",
6
6
  "main": "./index.js",
7
7
  "module": "./index.es.js",
@@ -422,14 +422,74 @@ export function removeAllCdnLocalStorageItems() {
422
422
  }
423
423
 
424
424
  /**
425
- *
426
- * @param {*} configsResponse
425
+ * Safe JSON.parse — returns fallback if value is empty or malformed.
426
+ * Used to parse the JSON-string env vars (qualityCfg, mapping, s3CdnMap) injected via window.APP_ENV.
427
+ */
428
+ function safeJsonParse(value, fallback) {
429
+ if (!value) return fallback;
430
+ try {
431
+ return JSON.parse(value);
432
+ } catch (e) {
433
+ return fallback;
434
+ }
435
+ }
436
+
437
+ /**
438
+ * Reads CDN config from window.APP_ENV (injected at container startup by entrypoint.sh)
439
+ * and populates localStorage via saveCdnConfigs — same shape as the legacy API response.
440
+ *
441
+ * Returns true if env config was found and applied, false otherwise (saga falls back to API).
442
+ *
443
+ * In deployed containers, window.APP_ENV is populated → no API call. In local dev
444
+ * (`npm start`), window.APP_ENV is empty → returns false → saga falls back to the API
445
+ * (legacy path, retained for the rollout window).
446
+ *
447
+ * Closes the VAPT info-disclosure surface (CAP-183204) by eliminating the
448
+ * /getCdnTransformationConfig API response in production.
449
+ */
450
+ export function initCdnConfigFromEnv() {
451
+ try {
452
+ const env = (typeof window !== 'undefined' && window.APP_ENV) || {};
453
+
454
+ // All four fields are required to construct a CDN URL. If any is missing,
455
+ // partial config would silently make every getCdnUrl call fall through to the
456
+ // raw S3 URL. Return false so the saga falls back to the API (which may still
457
+ // have the values during the cluster-by-cluster rollout window).
458
+ if (!env.CDN_HOSTNAME
459
+ || !env.CDN_IMG_TRANSFORMATION_URL_SUFFIX
460
+ || !env.CREATIVE_ASSETS_BUCKET_PATH
461
+ || !env.S3_CDN_MAP) {
462
+ return false;
463
+ }
464
+
465
+ saveCdnConfigs({
466
+ hostname: env.CDN_HOSTNAME,
467
+ transformationUrlSuffix: env.CDN_IMG_TRANSFORMATION_URL_SUFFIX,
468
+ bucketPath: env.CREATIVE_ASSETS_BUCKET_PATH,
469
+ qualityCfg: safeJsonParse(env.CDN_IMG_QUALITY_CFG, {}),
470
+ overrideEmailQuality: env.OVERRIDE_IMAGE_QUALITY === 'true',
471
+ overrideEmailQualityMapping: safeJsonParse(env.IMAGE_QUALITY_MAPPING, []),
472
+ s3CdnMap: safeJsonParse(env.S3_CDN_MAP, {}),
473
+ });
474
+ return true;
475
+ } catch (e) {
476
+ Bugsnag.leaveBreadcrumb('initCdnConfigFromEnv failed to apply window.APP_ENV');
477
+ Bugsnag.notify(e, (event) => {
478
+ event.severity = 'error';
479
+ });
480
+ return false;
481
+ }
482
+ }
483
+
484
+ /**
485
+ *
486
+ * @param {*} configsResponse
427
487
  * This util function saves the getCdnConfigs response into the local storage.
428
488
  * The following response items are mapped into the following keys in local storage:
429
489
  * hostname -> CREATIVES_CDN_BASE_URL
430
490
  * qualityCfg ->CREATIVES_CDN_QUALITY_CONFIG
431
491
  * transformationUrlSuffix -> CREATIVES_CDN_TRANSFORMATION_URL_SUFFIX
432
- *
492
+ *
433
493
  * 1. If configsReponse is empty. All above mentioned keys are deleted from localstorage.
434
494
  * 2. If any one of the above keys is missing. The respective keys are deleted from localstorage.
435
495
  * 3. Else it is saved into the localstorage.
@@ -546,6 +546,117 @@ describe("cdnTransformationTests", () => {
546
546
  });
547
547
  });
548
548
 
549
+ describe("initCdnConfigFromEnv()", () => {
550
+ afterEach(() => {
551
+ delete window.APP_ENV;
552
+ });
553
+
554
+ it("returns false when window.APP_ENV is undefined", () => {
555
+ delete window.APP_ENV;
556
+ expect(cdnUtils.initCdnConfigFromEnv()).toBe(false);
557
+ });
558
+
559
+ const completeEnv = {
560
+ CDN_HOSTNAME: "https://storage.crm.n.content-cdn.io",
561
+ CDN_IMG_TRANSFORMATION_URL_SUFFIX: "cdn-cgi/image",
562
+ CREATIVE_ASSETS_BUCKET_PATH: "intouch_creative_assets",
563
+ OVERRIDE_IMAGE_QUALITY: "false",
564
+ CDN_IMG_QUALITY_CFG: "{}",
565
+ IMAGE_QUALITY_MAPPING: "[]",
566
+ S3_CDN_MAP: '{"host.s3.amazonaws.com":{"cdn_host":"cdn.example.com","bucket_path":"x"}}',
567
+ };
568
+
569
+ it.each([
570
+ ["CDN_HOSTNAME"],
571
+ ["CDN_IMG_TRANSFORMATION_URL_SUFFIX"],
572
+ ["CREATIVE_ASSETS_BUCKET_PATH"],
573
+ ["S3_CDN_MAP"],
574
+ ])("returns false when required field %s is missing", (missingKey) => {
575
+ window.APP_ENV = { ...completeEnv };
576
+ delete window.APP_ENV[missingKey];
577
+ expect(cdnUtils.initCdnConfigFromEnv()).toBe(false);
578
+ });
579
+
580
+ it.each([
581
+ ["CDN_HOSTNAME"],
582
+ ["CDN_IMG_TRANSFORMATION_URL_SUFFIX"],
583
+ ["CREATIVE_ASSETS_BUCKET_PATH"],
584
+ ["S3_CDN_MAP"],
585
+ ])("returns false when required field %s is empty string", (emptyKey) => {
586
+ window.APP_ENV = { ...completeEnv, [emptyKey]: "" };
587
+ expect(cdnUtils.initCdnConfigFromEnv()).toBe(false);
588
+ });
589
+
590
+ it("populates localStorage and returns true for a complete env", () => {
591
+ window.APP_ENV = {
592
+ CDN_HOSTNAME: "https://storage.crm.n.content-cdn.io",
593
+ CDN_IMG_TRANSFORMATION_URL_SUFFIX: "cdn-cgi/image",
594
+ CREATIVE_ASSETS_BUCKET_PATH: "intouch_creative_assets",
595
+ OVERRIDE_IMAGE_QUALITY: "true",
596
+ CDN_IMG_QUALITY_CFG: '{"EMAIL":65,"DEFAULT":70}',
597
+ IMAGE_QUALITY_MAPPING: '[[30,90],[80,85]]',
598
+ S3_CDN_MAP: '{"host.s3.amazonaws.com":{"cdn_host":"cdn.example.com","bucket_path":"x"}}',
599
+ };
600
+
601
+ expect(cdnUtils.initCdnConfigFromEnv()).toBe(true);
602
+ expect(localStorage.getItem("CREATIVES_CDN_BASE_URL")).toBe("https://storage.crm.n.content-cdn.io");
603
+ expect(localStorage.getItem("CREATIVES_CDN_TRANSFORMATION_URL_SUFFIX")).toBe("cdn-cgi/image");
604
+ expect(localStorage.getItem("CREATIVES_S3_BUCKET_PATH")).toBe("intouch_creative_assets");
605
+ expect(localStorage.getItem("CREATIVES_CDN_QUALITY_CONFIG")).toBe('{"EMAIL":65,"DEFAULT":70}');
606
+ expect(localStorage.getItem("CREATIVES_CDN_OVERRIDE_DEFAULT_EMAIL_QUALITY")).toBe("true");
607
+ expect(localStorage.getItem("CREATIVES_CDN_OVERRIDE_DEFAULT_EMAIL_QUALITY_MAPPING")).toBe("[[30,90],[80,85]]");
608
+ expect(JSON.parse(localStorage.getItem("S3_CDN_MAP"))).toEqual({
609
+ "host.s3.amazonaws.com": { cdn_host: "cdn.example.com", bucket_path: "x" },
610
+ });
611
+ });
612
+
613
+ it("uses fallback values for malformed JSON env vars instead of throwing", () => {
614
+ window.APP_ENV = {
615
+ CDN_HOSTNAME: "https://storage.crm.n.content-cdn.io",
616
+ CDN_IMG_TRANSFORMATION_URL_SUFFIX: "cdn-cgi/image",
617
+ CREATIVE_ASSETS_BUCKET_PATH: "intouch_creative_assets",
618
+ OVERRIDE_IMAGE_QUALITY: "false",
619
+ CDN_IMG_QUALITY_CFG: "not-valid-json{{",
620
+ IMAGE_QUALITY_MAPPING: "also-broken",
621
+ S3_CDN_MAP: "}{",
622
+ };
623
+
624
+ expect(cdnUtils.initCdnConfigFromEnv()).toBe(true);
625
+ expect(localStorage.getItem("CREATIVES_CDN_BASE_URL")).toBe("https://storage.crm.n.content-cdn.io");
626
+ });
627
+
628
+ it("treats OVERRIDE_IMAGE_QUALITY values other than the string \"true\" as false", () => {
629
+ window.APP_ENV = {
630
+ CDN_HOSTNAME: "https://storage.crm.n.content-cdn.io",
631
+ CDN_IMG_TRANSFORMATION_URL_SUFFIX: "cdn-cgi/image",
632
+ CREATIVE_ASSETS_BUCKET_PATH: "intouch_creative_assets",
633
+ OVERRIDE_IMAGE_QUALITY: "no",
634
+ CDN_IMG_QUALITY_CFG: "{}",
635
+ IMAGE_QUALITY_MAPPING: "[]",
636
+ S3_CDN_MAP: "{}",
637
+ };
638
+
639
+ cdnUtils.initCdnConfigFromEnv();
640
+ expect(localStorage.getItem("CREATIVES_CDN_OVERRIDE_DEFAULT_EMAIL_QUALITY")).toBe(undefined);
641
+ });
642
+
643
+ it("returns false and notifies Bugsnag when window.APP_ENV access throws", () => {
644
+ // Force the `window.APP_ENV` read inside initCdnConfigFromEnv to throw,
645
+ // which exercises the function's outer catch block.
646
+ Object.defineProperty(window, "APP_ENV", {
647
+ get() { throw new Error("APP_ENV access boom"); },
648
+ configurable: true,
649
+ });
650
+ const breadcrumbSpy = jest.spyOn(Bugsnag, "leaveBreadcrumb");
651
+
652
+ expect(cdnUtils.initCdnConfigFromEnv()).toBe(false);
653
+ expect(breadcrumbSpy).toHaveBeenCalledWith(
654
+ "initCdnConfigFromEnv failed to apply window.APP_ENV"
655
+ );
656
+ expect(bugsnagSpy).toHaveBeenCalled();
657
+ });
658
+ });
659
+
549
660
  describe("getEmailImageOverrideQuality()",()=>{
550
661
  it("Should return quality of last element in array if file size is greater than max provided in array",()=>{
551
662
  localStorage.setItem("CREATIVES_CDN_OVERRIDE_DEFAULT_EMAIL_QUALITY", "true");
@@ -6,7 +6,7 @@ import { CapNotification } from '@capillarytech/cap-ui-library';
6
6
  // import { schema, normalize } from 'normalizr';
7
7
  import * as Api from '../../services/api';
8
8
  import * as types from './constants';
9
- import { saveCdnConfigs, removeAllCdnLocalStorageItems } from '../../utils/cdnTransformation';
9
+ import { saveCdnConfigs, removeAllCdnLocalStorageItems, initCdnConfigFromEnv } from '../../utils/cdnTransformation';
10
10
  import { COPY_OF } from '../../constants/unified';
11
11
  import { ZALO_TEMPLATE_INFO_REQUEST } from '../Zalo/constants';
12
12
  import { getTemplateInfoById } from '../Zalo/saga';
@@ -107,6 +107,11 @@ export function* getOrgLevelCampaignSettings() {
107
107
 
108
108
  export function* getCdnTransformationConfig() {
109
109
  try {
110
+ // VAPT CAP-183204: prefer env vars injected via window.APP_ENV — avoids the
111
+ // API response that leaked CDN/S3 infrastructure details. Fallback to API
112
+ // keeps clusters that haven't received the env vars yet working during rollout.
113
+ if (initCdnConfigFromEnv()) return;
114
+
110
115
  const res = yield call(Api.getCdnTransformationConfig);
111
116
  if (res?.success && res?.status?.code === 200) {
112
117
  const cdnConfigs = res?.response;
@@ -54,10 +54,25 @@ jest.mock('@capillarytech/cap-ui-library', () => ({
54
54
  }));
55
55
 
56
56
  describe('getCdnTransformationConfig saga', () => {
57
- it("handle valid response from api", () => {
57
+ afterEach(() => {
58
+ delete window.APP_ENV;
59
+ });
60
+
61
+ it("short-circuits to env config and skips the API call when window.APP_ENV is set", async () => {
62
+ const initSpy = jest.spyOn(cdnUtils, "initCdnConfigFromEnv").mockReturnValue(true);
63
+ const apiSpy = jest.spyOn(api, "getCdnTransformationConfig");
64
+
65
+ await expectSaga(getCdnTransformationConfig).run();
66
+
67
+ expect(initSpy).toHaveBeenCalled();
68
+ expect(apiSpy).not.toHaveBeenCalled();
69
+ });
70
+
71
+ it("handle valid response from api", async () => {
72
+ jest.spyOn(cdnUtils, "initCdnConfigFromEnv").mockReturnValue(false);
58
73
  const saveCdnConfigsSpy = jest.spyOn(cdnUtils, "saveCdnConfigs");
59
74
 
60
- expectSaga(getCdnTransformationConfig)
75
+ await expectSaga(getCdnTransformationConfig)
61
76
  .provide([
62
77
  [
63
78
  call(api.getCdnTransformationConfig),
@@ -68,13 +83,14 @@ describe('getCdnTransformationConfig saga', () => {
68
83
  expect(saveCdnConfigsSpy).toHaveBeenCalled();
69
84
  });
70
85
 
71
- it("handle error response from api", () => {
86
+ it("handle error response from api", async () => {
87
+ jest.spyOn(cdnUtils, "initCdnConfigFromEnv").mockReturnValue(false);
72
88
  const removeAllCdnLocalStorageItemsSpy = jest.spyOn(
73
89
  cdnUtils,
74
90
  "removeAllCdnLocalStorageItems"
75
91
  );
76
92
 
77
- expectSaga(getCdnTransformationConfig)
93
+ await expectSaga(getCdnTransformationConfig)
78
94
  .provide([
79
95
  [
80
96
  call(api.getCdnTransformationConfig),
@@ -85,7 +101,8 @@ describe('getCdnTransformationConfig saga', () => {
85
101
  expect(removeAllCdnLocalStorageItemsSpy).toHaveBeenCalled();
86
102
  });
87
103
 
88
- it("remove localStorage items when an error is thrown", () => {
104
+ it("remove localStorage items when an error is thrown", async () => {
105
+ jest.spyOn(cdnUtils, "initCdnConfigFromEnv").mockReturnValue(false);
89
106
  const saveCdnConfigsSpy = jest.spyOn(cdnUtils, "saveCdnConfigs").mockImplementation(()=>{
90
107
  throw new Error()
91
108
  });
@@ -94,7 +111,7 @@ describe('getCdnTransformationConfig saga', () => {
94
111
  "removeAllCdnLocalStorageItems"
95
112
  );
96
113
 
97
- expectSaga(getCdnTransformationConfig)
114
+ await expectSaga(getCdnTransformationConfig)
98
115
  .provide([
99
116
  [
100
117
  call(api.getCdnTransformationConfig),