@modainteract/moda-interact-shared 5.8.0 → 5.10.0

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.
@@ -33,6 +33,7 @@ import { z } from "zod";
33
33
  var hostId = z.string().trim().min(1).max(191);
34
34
  var positive = z.number().int().positive().safe();
35
35
  var https = z.string().max(2048).url().refine((v) => {
36
+ if (!URL.canParse(v)) return false;
36
37
  const u = new URL(v);
37
38
  return u.protocol === "https:" && !u.username && !u.password;
38
39
  });
@@ -88,7 +89,21 @@ var AssignedVariantSchema = z2.strictObject({ id: hostId, shopId: hostId, provid
88
89
  var ProjectionSchema = z2.strictObject({ variant: AssignedVariantSchema, canonicalLanguageTag: text, selectionSource: z2.enum(["exact", "base", "merchant-fallback", "platform-fallback"]) });
89
90
  var PreparedProjectionSchema = ProjectionSchema.extend({ textFallback: ProjectionSchema.nullable().optional() });
90
91
  var ReadyMediaSchema = z2.strictObject({ shopId: hostId, role: z2.enum(["LOGO", "SQUARE_LOGO", "COVER_IMAGE", "CHECKOUT_PRODUCT"]), status: z2.literal("READY"), kind: z2.literal("R2_DERIVATIVE"), httpsUrl: https });
91
- var InitialOutreachRenderInputSchema = z2.strictObject({ schemaVersion: z2.literal(1), evaluation: ReadyEvaluationSchema, preparedProjection: PreparedProjectionSchema.nullable(), variables: z2.strictObject({ shopName: z2.string().max(200).nullable(), customerFirstName: z2.string().max(200).nullable(), checkoutUrl: https, shopCategory: z2.string().max(200).nullable() }), headerMedia: ReadyMediaSchema.nullable() });
92
+ var PreparedOutreachBrandSchema = z2.strictObject({
93
+ shopId: hostId,
94
+ brandName: z2.string().max(200).nullable(),
95
+ logoAltText: z2.string().max(200).nullable(),
96
+ logoWidth: z2.number().int().min(1).max(1e4).nullable(),
97
+ logoHeight: z2.number().int().min(1).max(1e4).nullable(),
98
+ squareLogoAltText: z2.string().max(200).nullable()
99
+ });
100
+ var PreparedOutreachBrandMediaItemSchema = ReadyMediaSchema;
101
+ var PreparedOutreachBrandMediaSchema = z2.strictObject({
102
+ logo: PreparedOutreachBrandMediaItemSchema.nullable(),
103
+ squareLogo: PreparedOutreachBrandMediaItemSchema.nullable(),
104
+ coverImage: PreparedOutreachBrandMediaItemSchema.nullable()
105
+ });
106
+ var InitialOutreachRenderInputSchema = z2.strictObject({ schemaVersion: z2.literal(1), evaluation: ReadyEvaluationSchema, preparedProjection: PreparedProjectionSchema.nullable(), variables: z2.strictObject({ shopName: z2.string().max(200).nullable(), customerFirstName: z2.string().max(200).nullable(), checkoutUrl: https, shopCategory: z2.string().max(200).nullable() }), headerMedia: ReadyMediaSchema.nullable(), preparedBrand: PreparedOutreachBrandSchema.nullable().optional(), preparedBrandMedia: PreparedOutreachBrandMediaSchema.nullable().optional() });
92
107
  var RenderReasonSchema = z2.enum(["INVALID_REQUEST", "INVALID_LANGUAGE", "SHOP_UNAVAILABLE", "SENDER_UNAVAILABLE", "SENDER_MISMATCH", "NO_LANGUAGE", "NO_APPROVED_ASSIGNMENT", "LEGACY_ASSIGNMENT", "AMBIGUOUS_ASSIGNMENT", "REGISTRATION_UNAVAILABLE", "PROVIDER_UNAVAILABLE", "PROVIDER_DRIFT", "IDENTITY_MISMATCH", "DEFINITION_UNAVAILABLE", "LOCALE_UNAVAILABLE", "BINDINGS_INVALID", "BINDING_FINGERPRINT_MISMATCH", "BINDING_VALUE_UNAVAILABLE", "CHECKOUT_UNAVAILABLE", "CHECKOUT_UNTRUSTED", "MEDIA_NOT_READY", "PROMOTION_PRODUCT_URL_UNAVAILABLE", "MESSAGE_TOO_LARGE"]);
93
108
  var TextParameterSchema = z2.strictObject({ type: z2.literal("text"), text: z2.string().max(4096) });
94
109
  var PreparedComponentSchema = z2.union([
@@ -100,6 +115,12 @@ var InitialOutreachRenderResultSchema = z2.discriminatedUnion("kind", [
100
115
  z2.strictObject({ kind: z2.literal("ready"), purpose: z2.enum(["INITIAL_NO_OFFER", "INITIAL_CODE_OFFER", "INITIAL_AUTOMATIC_OFFER"]), templateProvenance: z2.strictObject({ variantId: hostId, registrationId: hostId, definitionId: hostId, providerTemplateId: hostId, providerTemplateName: text, providerLanguageCode: text, submittedContentHash: HashSchema, bindingFingerprint: HashSchema }), parameters: z2.array(PreparedComponentSchema).max(4), headerPreview: ReadyMediaSchema.nullable(), renderedText: z2.string().max(8192) }),
101
116
  z2.strictObject({ kind: z2.literal("refused"), reasonCode: RenderReasonSchema })
102
117
  ]);
118
+ var StudioOutreachTemplateSchema = z2.strictObject({ templateId: hostId, localeId: hostId, languageTag: z2.string().min(1).max(512), components: z2.array(ComponentSchema).min(1).max(4), bindings: FrozenBindingsSchema });
119
+ var StudioInitialOutreachRenderInputSchema = InitialOutreachRenderInputSchema.omit({ preparedProjection: true }).extend({ template: StudioOutreachTemplateSchema });
120
+ var StudioInitialOutreachRenderResultSchema = z2.discriminatedUnion("kind", [
121
+ z2.strictObject({ kind: z2.literal("ready"), renderedText: z2.string().max(8192), parameters: z2.array(PreparedComponentSchema).max(4), headerPreview: ReadyMediaSchema.nullable(), templateId: hostId, localeId: hostId, templateValidation: z2.literal("NOT_EVALUATED") }),
122
+ z2.strictObject({ kind: z2.literal("refused"), reasonCode: RenderReasonSchema })
123
+ ]);
103
124
 
104
125
  // src/commerce/outreach/evaluate.ts
105
126
  import { z as z3 } from "zod";
@@ -773,6 +794,188 @@ async function evaluateInitialOutreach(raw, ports) {
773
794
  }
774
795
  var evaluateInitialOutreachPreview = evaluateInitialOutreach;
775
796
 
797
+ // src/commerce/outreach/recovery-template-items-summary.ts
798
+ var MAX_LINE_ITEMS = 100;
799
+ var MAX_DISPLAYED_ITEMS = 3;
800
+ var MAX_TITLE_LENGTH = 160;
801
+ var MAX_SUMMARY_LENGTH = 400;
802
+ function safeText(value, limit) {
803
+ if (typeof value !== "string")
804
+ return null;
805
+ const text2 = value.normalize("NFC").trim().replace(/\s+/gu, " ");
806
+ if (!text2 || text2.length > limit || /[\p{Cc}\p{Cf}{}]/u.test(text2) || text2.includes("{{") || text2.includes("}}"))
807
+ return null;
808
+ try {
809
+ encodeURIComponent(text2);
810
+ } catch {
811
+ return null;
812
+ }
813
+ return text2;
814
+ }
815
+ function parseItem(value) {
816
+ if (!value || typeof value !== "object" || Array.isArray(value))
817
+ return null;
818
+ const item = value;
819
+ const title = safeText(item.title, MAX_TITLE_LENGTH);
820
+ const quantity = item.quantity;
821
+ if (!title || typeof quantity !== "number" || !Number.isSafeInteger(quantity) || quantity < 1 || quantity > 99999)
822
+ return null;
823
+ const variant = safeText(item.variantTitle, MAX_TITLE_LENGTH);
824
+ const displayTitle = variant && variant.toLowerCase() !== "default title" && variant !== title ? `${title} (${variant})` : title;
825
+ return { title: displayTitle, quantity };
826
+ }
827
+ function formatCheckoutItemsSummary(lineItems, languageTag) {
828
+ if (!Array.isArray(lineItems) || lineItems.length === 0 || lineItems.length > MAX_LINE_ITEMS)
829
+ return null;
830
+ let locale;
831
+ try {
832
+ locale = Intl.getCanonicalLocales(languageTag)[0] ?? "en";
833
+ } catch {
834
+ return null;
835
+ }
836
+ const items = lineItems.map(parseItem).filter((item) => item !== null);
837
+ if (!items.length)
838
+ return null;
839
+ const numbers = new Intl.NumberFormat(locale, { maximumFractionDigits: 0 });
840
+ const list = new Intl.ListFormat(locale, { style: "long", type: "conjunction" });
841
+ const visible = items.slice(0, MAX_DISPLAYED_ITEMS).map(({ title, quantity }) => `${numbers.format(quantity)} \xD7 ${title}`);
842
+ if (items.length > MAX_DISPLAYED_ITEMS) {
843
+ visible.push(`+${numbers.format(items.length - MAX_DISPLAYED_ITEMS)}`);
844
+ }
845
+ const summary = list.format(visible);
846
+ if (summary.length <= MAX_SUMMARY_LENGTH)
847
+ return summary;
848
+ return `${Array.from(summary).slice(0, MAX_SUMMARY_LENGTH - 1).join("").trimEnd()}\u2026`;
849
+ }
850
+
851
+ // src/commerce/outreach/variables.ts
852
+ function isSemanticKey(v) {
853
+ return SemanticKeySchema.safeParse(v).success;
854
+ }
855
+ var BindingUnavailable = class extends Error {
856
+ constructor(reason) {
857
+ super(reason);
858
+ this.reason = reason;
859
+ }
860
+ reason;
861
+ };
862
+ function boundedText(v, limit) {
863
+ if (typeof v !== "string" || !v.trim() || v.length > limit || /[\x00-\x1f\x7f\u2028\u2029]/u.test(v) || v.includes("{{") || v.includes("}}"))
864
+ throw new BindingUnavailable("BINDING_VALUE_UNAVAILABLE");
865
+ try {
866
+ encodeURIComponent(v);
867
+ } catch {
868
+ throw new BindingUnavailable("BINDING_VALUE_UNAVAILABLE");
869
+ }
870
+ return v.trim();
871
+ }
872
+ var PreparedVariableResolver = class {
873
+ constructor(input, languageTag, sendable) {
874
+ this.input = input;
875
+ this.languageTag = languageTag;
876
+ this.sendable = sendable;
877
+ }
878
+ input;
879
+ languageTag;
880
+ sendable;
881
+ encodeCheckoutButtonSuffix(_prefix) {
882
+ return encodeURIComponent(this.input.variables.checkoutUrl);
883
+ }
884
+ encodePromotionButtonSuffix(_prefix) {
885
+ throw new BindingUnavailable("PROMOTION_PRODUCT_URL_UNAVAILABLE");
886
+ }
887
+ checkStaticButtonUrl(value) {
888
+ const u = new URL(value), checkout = new URL(this.input.variables.checkoutUrl);
889
+ if (u.protocol !== "https:" || u.username || u.password || u.hash || u.hostname !== checkout.hostname)
890
+ throw new BindingUnavailable("CHECKOUT_UNTRUSTED");
891
+ }
892
+ imageHeader(role) {
893
+ const m = this.input.headerMedia;
894
+ if (!m || m.shopId !== this.input.evaluation.shopId || m.role !== role)
895
+ throw new BindingUnavailable("MEDIA_NOT_READY");
896
+ return m.httpsUrl;
897
+ }
898
+ brandMedia(role) {
899
+ const media = this.input.preparedBrandMedia?.[role];
900
+ if (!media) throw new BindingUnavailable("MEDIA_NOT_READY");
901
+ boundedText(media.httpsUrl, 2048);
902
+ return media.httpsUrl;
903
+ }
904
+ resolve(key) {
905
+ const i = this.input, d = this.sendable;
906
+ switch (key) {
907
+ case "customer.firstName":
908
+ return boundedText(i.variables.customerFirstName, 200);
909
+ case "shop.name":
910
+ return boundedText(i.variables.shopName, 200);
911
+ case "shop.categoryName":
912
+ return boundedText(i.variables.shopCategory, 200);
913
+ case "checkout.url":
914
+ return i.variables.checkoutUrl;
915
+ case "checkout.itemsSummary": {
916
+ const items = i.evaluation.lines.map((l) => ({ title: l.currentProduct.title, quantity: l.quantity, variantTitle: l.currentProduct.selectedOptions?.map((o) => o.value).join(" / ") || null }));
917
+ if (items.some((l) => l.quantity > 99999 || /[\p{Cc}\p{Cf}{}]/u.test(l.title) || l.title.length > 160 || l.variantTitle !== null && (/[\p{Cc}\p{Cf}{}]/u.test(l.variantTitle) || l.variantTitle.length > 160)))
918
+ throw new BindingUnavailable("BINDING_VALUE_UNAVAILABLE");
919
+ return boundedText(formatCheckoutItemsSummary(items, this.languageTag), 500);
920
+ }
921
+ case "offer.code":
922
+ if (d.presentation === "CODE_OFFER" && d.members.length === 1 && d.members[0]?.method === "CODE")
923
+ return boundedText(d.members[0].code, 100);
924
+ break;
925
+ case "offer.codes":
926
+ if (d.presentation === "CODE_OFFER" && d.members.every((m) => m.method === "CODE"))
927
+ return d.members.map((m) => boundedText(m.code, 100)).join(", ");
928
+ break;
929
+ case "offer.description":
930
+ if (d.presentation !== "NO_OFFER")
931
+ return boundedText(d.members.map((m) => m.description).join(", "), 200);
932
+ break;
933
+ case "shopBrand.brandName":
934
+ return boundedText(i.preparedBrand?.brandName, 200);
935
+ case "shopBrand.logoAltText":
936
+ return boundedText(i.preparedBrand?.logoAltText, 200);
937
+ case "shopBrand.squareLogoAltText":
938
+ return boundedText(i.preparedBrand?.squareLogoAltText, 200);
939
+ case "shopBrand.logoWidth":
940
+ case "shopBrand.logoHeight": {
941
+ const value = key === "shopBrand.logoWidth" ? i.preparedBrand?.logoWidth : i.preparedBrand?.logoHeight;
942
+ if (value === null || value === void 0) throw new BindingUnavailable("BINDING_VALUE_UNAVAILABLE");
943
+ return boundedText(String(value), 200);
944
+ }
945
+ case "shopBrand.logoUrl":
946
+ return this.brandMedia("logo");
947
+ case "shopBrand.squareLogoUrl":
948
+ return this.brandMedia("squareLogo");
949
+ case "shopBrand.coverImageUrl":
950
+ return this.brandMedia("coverImage");
951
+ }
952
+ throw new BindingUnavailable("BINDING_VALUE_UNAVAILABLE");
953
+ }
954
+ };
955
+
956
+ // src/commerce/outreach/component-render.ts
957
+ function renderStoredComponentText(components, parameters) {
958
+ function substitute(text3, type) {
959
+ const values = parameters.find((p) => p.type === type)?.parameters ?? [];
960
+ return text3.replace(/\{\{([1-9][0-9]*)\}\}/g, (_, position) => {
961
+ const value = values[Number(position) - 1];
962
+ if (!value || value.type !== "text") throw new BindingUnavailable("BINDINGS_INVALID");
963
+ return value.text;
964
+ });
965
+ }
966
+ const body = components.find((c) => c.type === "BODY");
967
+ if (!body || body.type !== "BODY") throw new BindingUnavailable("BINDINGS_INVALID");
968
+ if (substitute(body.text, "body").length > 4096) throw new BindingUnavailable("MESSAGE_TOO_LARGE");
969
+ const text2 = components.flatMap((c) => {
970
+ if (c.type === "BODY") return [substitute(c.text, "body")];
971
+ if (c.type === "FOOTER") return [c.text];
972
+ if (c.type === "HEADER" && c.format === "TEXT") return [substitute(c.text, "header")];
973
+ return [];
974
+ }).join("\n");
975
+ if (text2.length > 8192) throw new BindingUnavailable("MESSAGE_TOO_LARGE");
976
+ return text2;
977
+ }
978
+
776
979
  // src/commerce/outreach/initial-checkout-items-policy.ts
777
980
  function decideInitialCheckoutItemsBinding(input) {
778
981
  if (input.stage === "FOLLOW_UP")
@@ -940,152 +1143,6 @@ function selectRecoveryAssignment(input) {
940
1143
 
941
1144
  // src/commerce/outreach/bindings.ts
942
1145
  import { createHash as createHash2 } from "crypto";
943
-
944
- // src/commerce/outreach/recovery-template-items-summary.ts
945
- var MAX_LINE_ITEMS = 100;
946
- var MAX_DISPLAYED_ITEMS = 3;
947
- var MAX_TITLE_LENGTH = 160;
948
- var MAX_SUMMARY_LENGTH = 400;
949
- function safeText(value, limit) {
950
- if (typeof value !== "string")
951
- return null;
952
- const text2 = value.normalize("NFC").trim().replace(/\s+/gu, " ");
953
- if (!text2 || text2.length > limit || /[\p{Cc}\p{Cf}{}]/u.test(text2) || text2.includes("{{") || text2.includes("}}"))
954
- return null;
955
- try {
956
- encodeURIComponent(text2);
957
- } catch {
958
- return null;
959
- }
960
- return text2;
961
- }
962
- function parseItem(value) {
963
- if (!value || typeof value !== "object" || Array.isArray(value))
964
- return null;
965
- const item = value;
966
- const title = safeText(item.title, MAX_TITLE_LENGTH);
967
- const quantity = item.quantity;
968
- if (!title || typeof quantity !== "number" || !Number.isSafeInteger(quantity) || quantity < 1 || quantity > 99999)
969
- return null;
970
- const variant = safeText(item.variantTitle, MAX_TITLE_LENGTH);
971
- const displayTitle = variant && variant.toLowerCase() !== "default title" && variant !== title ? `${title} (${variant})` : title;
972
- return { title: displayTitle, quantity };
973
- }
974
- function formatCheckoutItemsSummary(lineItems, languageTag) {
975
- if (!Array.isArray(lineItems) || lineItems.length === 0 || lineItems.length > MAX_LINE_ITEMS)
976
- return null;
977
- let locale;
978
- try {
979
- locale = Intl.getCanonicalLocales(languageTag)[0] ?? "en";
980
- } catch {
981
- return null;
982
- }
983
- const items = lineItems.map(parseItem).filter((item) => item !== null);
984
- if (!items.length)
985
- return null;
986
- const numbers = new Intl.NumberFormat(locale, { maximumFractionDigits: 0 });
987
- const list = new Intl.ListFormat(locale, { style: "long", type: "conjunction" });
988
- const visible = items.slice(0, MAX_DISPLAYED_ITEMS).map(({ title, quantity }) => `${numbers.format(quantity)} \xD7 ${title}`);
989
- if (items.length > MAX_DISPLAYED_ITEMS) {
990
- visible.push(`+${numbers.format(items.length - MAX_DISPLAYED_ITEMS)}`);
991
- }
992
- const summary = list.format(visible);
993
- if (summary.length <= MAX_SUMMARY_LENGTH)
994
- return summary;
995
- return `${Array.from(summary).slice(0, MAX_SUMMARY_LENGTH - 1).join("").trimEnd()}\u2026`;
996
- }
997
-
998
- // src/commerce/outreach/variables.ts
999
- function isSemanticKey(v) {
1000
- return SemanticKeySchema.safeParse(v).success;
1001
- }
1002
- var BindingUnavailable = class extends Error {
1003
- constructor(reason) {
1004
- super(reason);
1005
- this.reason = reason;
1006
- }
1007
- reason;
1008
- };
1009
- function boundedText(v, limit) {
1010
- if (typeof v !== "string" || !v.trim() || v.length > limit || /[\x00-\x1f\x7f\u2028\u2029]/u.test(v) || v.includes("{{") || v.includes("}}"))
1011
- throw new BindingUnavailable("BINDING_VALUE_UNAVAILABLE");
1012
- try {
1013
- encodeURIComponent(v);
1014
- } catch {
1015
- throw new BindingUnavailable("BINDING_VALUE_UNAVAILABLE");
1016
- }
1017
- return v.trim();
1018
- }
1019
- var PreparedVariableResolver = class {
1020
- constructor(input, languageTag, sendable) {
1021
- this.input = input;
1022
- this.languageTag = languageTag;
1023
- this.sendable = sendable;
1024
- }
1025
- input;
1026
- languageTag;
1027
- sendable;
1028
- encodeCheckoutButtonSuffix(_prefix) {
1029
- return encodeURIComponent(this.input.variables.checkoutUrl);
1030
- }
1031
- encodePromotionButtonSuffix(_prefix) {
1032
- throw new BindingUnavailable("PROMOTION_PRODUCT_URL_UNAVAILABLE");
1033
- }
1034
- checkStaticButtonUrl(value) {
1035
- const u = new URL(value), checkout = new URL(this.input.variables.checkoutUrl);
1036
- if (u.protocol !== "https:" || u.username || u.password || u.hash || u.hostname !== checkout.hostname)
1037
- throw new BindingUnavailable("CHECKOUT_UNTRUSTED");
1038
- }
1039
- imageHeader(role) {
1040
- const m = this.input.headerMedia;
1041
- if (!m || m.shopId !== this.input.evaluation.shopId || m.role !== role)
1042
- throw new BindingUnavailable("MEDIA_NOT_READY");
1043
- return m.httpsUrl;
1044
- }
1045
- resolve(key) {
1046
- const i = this.input, d = this.sendable;
1047
- switch (key) {
1048
- case "customer.firstName":
1049
- return boundedText(i.variables.customerFirstName, 200);
1050
- case "shop.name":
1051
- return boundedText(i.variables.shopName, 200);
1052
- case "shop.categoryName":
1053
- return boundedText(i.variables.shopCategory, 200);
1054
- case "checkout.url":
1055
- return i.variables.checkoutUrl;
1056
- case "checkout.itemsSummary": {
1057
- const items = i.evaluation.lines.map((l) => ({ title: l.currentProduct.title, quantity: l.quantity, variantTitle: l.currentProduct.selectedOptions?.map((o) => o.value).join(" / ") || null }));
1058
- if (items.some((l) => l.quantity > 99999 || /[\p{Cc}\p{Cf}{}]/u.test(l.title) || l.title.length > 160 || l.variantTitle !== null && (/[\p{Cc}\p{Cf}{}]/u.test(l.variantTitle) || l.variantTitle.length > 160)))
1059
- throw new BindingUnavailable("BINDING_VALUE_UNAVAILABLE");
1060
- return boundedText(formatCheckoutItemsSummary(items, this.languageTag), 500);
1061
- }
1062
- case "offer.code":
1063
- if (d.presentation === "CODE_OFFER" && d.members.length === 1 && d.members[0]?.method === "CODE")
1064
- return boundedText(d.members[0].code, 100);
1065
- break;
1066
- case "offer.codes":
1067
- if (d.presentation === "CODE_OFFER" && d.members.every((m) => m.method === "CODE"))
1068
- return d.members.map((m) => boundedText(m.code, 100)).join(", ");
1069
- break;
1070
- case "offer.description":
1071
- if (d.presentation !== "NO_OFFER")
1072
- return boundedText(d.members.map((m) => m.description).join(", "), 200);
1073
- break;
1074
- // This input exposes only the host's prepared shop name/category and media, not arbitrary brand records.
1075
- case "shopBrand.brandName":
1076
- return boundedText(i.variables.shopName, 200);
1077
- case "shopBrand.logoUrl":
1078
- return this.imageHeader("LOGO");
1079
- case "shopBrand.squareLogoUrl":
1080
- return this.imageHeader("SQUARE_LOGO");
1081
- case "shopBrand.coverImageUrl":
1082
- return this.imageHeader("COVER_IMAGE");
1083
- }
1084
- throw new BindingUnavailable("BINDING_VALUE_UNAVAILABLE");
1085
- }
1086
- };
1087
-
1088
- // src/commerce/outreach/bindings.ts
1089
1146
  var MEDIA_ROLES = ["LOGO", "SQUARE_LOGO", "COVER_IMAGE", "CHECKOUT_PRODUCT"];
1090
1147
  var obj = (value) => value !== null && typeof value === "object" && !Array.isArray(value);
1091
1148
  function invalid() {
@@ -1196,6 +1253,12 @@ function prepareFrozenComponents(input) {
1196
1253
  }
1197
1254
  if (registration.category !== "MARKETING")
1198
1255
  return invalid();
1256
+ return { ...prepareStoredComponents({ components: approved, bindings: frozen, variables }), bindingFingerprint: fingerprint };
1257
+ }
1258
+ function prepareStoredComponents(input) {
1259
+ const approved = strictComponents(input.components);
1260
+ const frozen = parseFrozenBindings(input.bindings);
1261
+ const variables = input.variables;
1199
1262
  const result2 = [];
1200
1263
  let matchedHeader = false;
1201
1264
  let matchedBody = false;
@@ -1259,7 +1322,7 @@ function prepareFrozenComponents(input) {
1259
1322
  }
1260
1323
  if (!matchedBody || frozen.header.length > 0 && !matchedHeader || frozen.buttons.length > 0 && !matchedButtons)
1261
1324
  return invalid();
1262
- return { components: result2, bindingFingerprint: fingerprint };
1325
+ return { components: result2 };
1263
1326
  }
1264
1327
 
1265
1328
  // src/commerce/outreach/sendable-offer.ts
@@ -1283,10 +1346,17 @@ function selectSendableOutreachOffer(decision2) {
1283
1346
 
1284
1347
  // src/commerce/outreach/render.ts
1285
1348
  function renderInitialOutreachPreview(raw) {
1286
- const parsed = InitialOutreachRenderInputSchema.safeParse(raw);
1349
+ let parsed;
1350
+ try {
1351
+ parsed = InitialOutreachRenderInputSchema.safeParse(raw);
1352
+ } catch {
1353
+ return { kind: "refused", reasonCode: "INVALID_REQUEST" };
1354
+ }
1287
1355
  if (!parsed.success)
1288
1356
  return { kind: "refused", reasonCode: "INVALID_REQUEST" };
1289
1357
  const input = parsed.data;
1358
+ const assetError = validatePreparedAssets(input);
1359
+ if (assetError) return { kind: "refused", reasonCode: assetError };
1290
1360
  let projection = input.preparedProjection;
1291
1361
  if (!projection)
1292
1362
  return { kind: "refused", reasonCode: "NO_APPROVED_ASSIGNMENT" };
@@ -1308,48 +1378,65 @@ function renderInitialOutreachPreview(raw) {
1308
1378
  const registration = v.metaRegistration;
1309
1379
  try {
1310
1380
  const prepared = prepareFrozenComponents({ registration, variables: new PreparedVariableResolver(input, projection.canonicalLanguageTag, sendable) });
1311
- const body = registration.componentsSnapshot.find((c) => c.type === "BODY");
1312
- if (!body || !("text" in body))
1313
- throw new BindingUnavailable("BINDINGS_INVALID");
1314
- const params = prepared.components.find((c) => c.type === "body")?.parameters ?? [];
1315
- const renderedText = body.text.replace(/\{\{([1-9][0-9]*)\}\}/g, (_, n) => {
1316
- const value = params[Number(n) - 1];
1317
- if (!value || value.type !== "text")
1318
- throw new BindingUnavailable("BINDINGS_INVALID");
1319
- return value.text;
1320
- });
1321
- if (renderedText.length > 4096)
1322
- return { kind: "refused", reasonCode: "MESSAGE_TOO_LARGE" };
1323
- const fullText = registration.componentsSnapshot.flatMap((component) => {
1324
- if (component.type === "BODY")
1325
- return [renderedText];
1326
- if (component.type === "FOOTER")
1327
- return [component.text];
1328
- if (component.type === "HEADER" && component.format === "TEXT") {
1329
- const headerParams = prepared.components.find((c) => c.type === "header")?.parameters ?? [];
1330
- return [component.text.replace(/\{\{([1-9][0-9]*)\}\}/g, (_, n) => {
1331
- const p = headerParams[Number(n) - 1];
1332
- if (!p || p.type !== "text")
1333
- throw new BindingUnavailable("BINDINGS_INVALID");
1334
- return p.text;
1335
- })];
1336
- }
1337
- return [];
1338
- }).join("\n");
1381
+ const fullText = renderStoredComponentText(registration.componentsSnapshot, prepared.components);
1339
1382
  return InitialOutreachRenderResultSchema.parse({ kind: "ready", purpose, templateProvenance: { variantId: v.id, registrationId: registration.id, definitionId: registration.locale.definition.id, providerTemplateId: registration.providerTemplateId, providerTemplateName: registration.providerTemplateName, providerLanguageCode: registration.providerLanguageCode, submittedContentHash: registration.submittedContentHash, bindingFingerprint: prepared.bindingFingerprint }, parameters: prepared.components, headerPreview: image ? input.headerMedia : null, renderedText: fullText });
1340
1383
  } catch (e) {
1341
1384
  return { kind: "refused", reasonCode: e instanceof BindingUnavailable ? e.reason : "BINDINGS_INVALID" };
1342
1385
  }
1343
1386
  }
1387
+ function validatePreparedAssets(input) {
1388
+ if (input.preparedBrand && input.preparedBrand.shopId !== input.evaluation.shopId)
1389
+ return "IDENTITY_MISMATCH";
1390
+ for (const [key, role] of [["logo", "LOGO"], ["squareLogo", "SQUARE_LOGO"], ["coverImage", "COVER_IMAGE"]]) {
1391
+ const media = input.preparedBrandMedia?.[key];
1392
+ if (media) {
1393
+ const url = new URL(media.httpsUrl);
1394
+ if (media.shopId !== input.evaluation.shopId || media.role !== role || url.hash || media.httpsUrl.includes("#") || !url.hostname.includes("."))
1395
+ return "MEDIA_NOT_READY";
1396
+ }
1397
+ }
1398
+ return null;
1399
+ }
1400
+ function renderStudioInitialOutreachPreview(raw) {
1401
+ let parsed;
1402
+ try {
1403
+ parsed = StudioInitialOutreachRenderInputSchema.safeParse(raw);
1404
+ } catch {
1405
+ return { kind: "refused", reasonCode: "INVALID_REQUEST" };
1406
+ }
1407
+ if (!parsed.success) return { kind: "refused", reasonCode: "INVALID_REQUEST" };
1408
+ const input = parsed.data;
1409
+ const assetError = validatePreparedAssets(input);
1410
+ if (assetError) return { kind: "refused", reasonCode: assetError };
1411
+ try {
1412
+ const sendable = selectSendableOutreachOffer(input.evaluation.offerDecision);
1413
+ const prepared = prepareStoredComponents({ components: input.template.components, bindings: input.template.bindings, variables: new PreparedVariableResolver(input, input.template.languageTag, sendable) });
1414
+ const image = input.template.components.some((c) => c.type === "HEADER" && c.format === "IMAGE");
1415
+ return StudioInitialOutreachRenderResultSchema.parse({ kind: "ready", renderedText: renderStoredComponentText(input.template.components, prepared.components), parameters: prepared.components, headerPreview: image ? input.headerMedia : null, templateId: input.template.templateId, localeId: input.template.localeId, templateValidation: "NOT_EVALUATED" });
1416
+ } catch (error) {
1417
+ return { kind: "refused", reasonCode: error instanceof BindingUnavailable ? error.reason : "BINDINGS_INVALID" };
1418
+ }
1419
+ }
1344
1420
  export {
1421
+ ComponentSchema,
1422
+ FrozenBindingsSchema,
1345
1423
  InitialOutreachEvaluationInputSchema,
1346
1424
  InitialOutreachEvaluationResultSchema,
1347
1425
  InitialOutreachRenderInputSchema,
1348
1426
  InitialOutreachRenderResultSchema,
1427
+ PreparedOutreachBrandMediaItemSchema,
1428
+ PreparedOutreachBrandMediaSchema,
1429
+ PreparedOutreachBrandSchema,
1430
+ StudioInitialOutreachRenderInputSchema,
1431
+ StudioInitialOutreachRenderResultSchema,
1432
+ StudioOutreachTemplateSchema,
1349
1433
  evaluateInitialOutreach,
1350
1434
  evaluateInitialOutreachPreview,
1351
1435
  fingerprintInitialOutreachBasket,
1436
+ purposeFor,
1352
1437
  renderInitialOutreachPreview,
1353
- selectRecoveryAssignment
1438
+ renderStudioInitialOutreachPreview,
1439
+ selectRecoveryAssignment,
1440
+ selectSendableOutreachOffer
1354
1441
  };
1355
1442
  //# sourceMappingURL=index.js.map