@rebuy/rebuy 3.6.0 → 3.8.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.
@@ -253,7 +253,29 @@ var Metadata = z2.object({
253
253
  unmatchedRules: z2.array(MatchedRule).optional(),
254
254
  widget: z2.object({
255
255
  discount: z2.object({ amount: z2.string(), type: z2.string() }).optional(),
256
- language: z2.record(z2.string(), z2.string()).optional()
256
+ /**
257
+ * Data-source-authored text content, dual-purpose in the CAB renderer (rebuy-shopify-extensions'
258
+ * `checkout-and-beyond`): every key becomes a `{{token}}` for the interpolation pass
259
+ * (`extractDataVariables`), e.g. `{ superTitle: 'Limited drop' }` resolves `{{superTitle}}`
260
+ * anywhere in the widget's copy, and the legacy add-to-order state keys (`extractLabelOverrides`)
261
+ * plus `variantOption`/`variantOptions` (`extractVariantLabels`) additionally keep their
262
+ * whole-label override behavior. The serve path passes metadata through verbatim
263
+ * (`fetchDataSourceResults` only camelCases keys, which lands snake_case names in the canonical
264
+ * token grammar); this schema is the contract of record. Parsing mirrors the renderer's
265
+ * fail-safe: an entry whose value is not a string or finite number — a data-source author's
266
+ * `null`, boolean, object, or a computed `NaN`/`Infinity` — is dropped at parse time rather
267
+ * than failing the whole `Metadata` block or rendering `[object Object]` (or a literal `NaN`)
268
+ * into buyer copy. Booleans stay excluded on purpose (unlike
269
+ * `Output.value` above, this is buyer-facing copy); an empty string is kept — it deliberately
270
+ * blanks a token.
271
+ */
272
+ language: z2.record(z2.string(), z2.unknown()).transform(
273
+ (raw) => Object.fromEntries(
274
+ Object.entries(raw).filter(
275
+ (entry) => typeof entry[1] === "string" || Number.isFinite(entry[1])
276
+ )
277
+ )
278
+ ).optional()
257
279
  }).optional()
258
280
  });
259
281
 
@@ -1408,7 +1430,13 @@ import { z as z20 } from "zod";
1408
1430
  var CABMonetizeSection = z20.object({
1409
1431
  ...CABVariableFields,
1410
1432
  name: z20.string().optional(),
1411
- /** Privacy Policy footer href (the widget's `privacy_policy_url` setting); the client relays it to `/cab/monetize`. */
1433
+ /**
1434
+ * Server-hydrated offer cards (inline hydration): when `/cab/sections` was asked with a `monetize`
1435
+ * context block, the composed cards ride the marker itself — `[]` means "hydrated, no fill" (render
1436
+ * nothing, skip the fetch); ABSENT means "not hydrated" (the client fetches via `/cab/monetize`).
1437
+ */
1438
+ offers: z20.array(z20.lazy(() => CABSection)).optional(),
1439
+ /** Privacy Policy footer href (the widget's `privacy_policy_url` setting); read server-side for hydration, relayed by the client only on the legacy `/cab/monetize` fallback. */
1412
1440
  privacyPolicyUrl: z20.string().optional(),
1413
1441
  sectionId: z20.uuid().default(() => uuidv78()),
1414
1442
  sectionType: z20.literal(SectionType.monetize).default(SectionType.monetize),
@@ -3773,184 +3801,6 @@ var fetchDataSourceResults = async (input, ctx) => {
3773
3801
  };
3774
3802
  };
3775
3803
 
3776
- // src/server/salvageSections.ts
3777
- import { v7 as uuidv713 } from "uuid";
3778
- var STUB_SECTION_TYPE = "unknown";
3779
- var knownSectionTypes = new Set(sectionTypes);
3780
- var hasSectionsArray = (raw) => typeof raw === "object" && raw !== null && Array.isArray(raw.sections);
3781
- var stubSection = (raw, report) => {
3782
- const record = typeof raw === "object" && raw !== null ? raw : {};
3783
- const original = record.sectionType;
3784
- const rawId = record.sectionId;
3785
- report?.({
3786
- detail: `invalid section (${typeof original === "string" ? original : "no sectionType"})`,
3787
- kind: "section"
3788
- });
3789
- return {
3790
- sectionId: typeof rawId === "string" && rawId ? rawId : uuidv713(),
3791
- sectionType: typeof original === "string" && !knownSectionTypes.has(original) ? original : STUB_SECTION_TYPE
3792
- };
3793
- };
3794
- var salvageSection = (raw, report) => {
3795
- const parsed = CABSection.safeParse(raw);
3796
- if (parsed.success) return parsed.data;
3797
- if (hasSectionsArray(raw)) {
3798
- const shell = CABSection.safeParse({ ...raw, sections: [] });
3799
- if (shell.success)
3800
- return { ...shell.data, sections: raw.sections.map((child) => salvageSection(child, report)) };
3801
- }
3802
- return stubSection(raw, report);
3803
- };
3804
- var salvageRootSection = (raw, report) => {
3805
- const shell = CABRootSection.safeParse(hasSectionsArray(raw) ? { ...raw, sections: [] } : raw);
3806
- if (!shell.success) return CABRootSection.parse(raw);
3807
- const sections = hasSectionsArray(raw) ? raw.sections.map((child) => salvageSection(child, report)) : [];
3808
- return { ...shell.data, sections };
3809
- };
3810
-
3811
- // src/server/widgetSettings.ts
3812
- var isMissingUpstreamPayload = isEmptyObject;
3813
- var dispatchWidgetTransform = (id, normalized, report) => {
3814
- if (isCABRootSection(normalized)) return salvageRootSection(normalizeTextContent(normalized), report);
3815
- if (isContentBlockV1(normalized)) return convertContentBlockToV2({ id, settings: normalized });
3816
- if (isLineItemEditorV1(normalized)) return convertLineItemEditorToV2({ id, settings: normalized });
3817
- if (isMonetizeV1(normalized)) return convertMonetizeToV2({ id, settings: normalized });
3818
- if (isOfferV1(normalized)) return convertOfferToV2({ id, name: normalized.name, settings: normalized });
3819
- return normalized;
3820
- };
3821
- var fetchProgressBarContext = async (shop, ctx) => {
3822
- const config = await fetchUserConfig({ shop }, ctx).catch(() => void 0);
3823
- if (!config) return {};
3824
- if (config.shop.billingVersion === "v6" && config.shop.activePackages.some((pkg) => pkg.shortName === "")) {
3825
- ctx.reportDrift?.({
3826
- detail: "shop config active package entry inerted (drifted shape) \u2014 smart-cart entitlement may misgate",
3827
- kind: "shopConfig"
3828
- });
3829
- }
3830
- const entitled = config.shop.billingVersion !== "v6" || config.shop.activePackages.some((pkg) => pkg.shortName === "cart-merchandising" && pkg.isActive);
3831
- return {
3832
- bars: entitled ? extractSmartCartBars(config.smartCart) : [],
3833
- progressColor: await fetchThemeColor(config.shop.shopId, ctx)
3834
- };
3835
- };
3836
- var fetchThemeColor = async (shopId, ctx) => {
3837
- if (!shopId) return void 0;
3838
- try {
3839
- const theme = await ctx.fetchUpstream(`/admin/api/v1/shop/theme/${shopId}`, {}, { host: ctx.host });
3840
- const color = theme.theme?.settings?.buttonBackground;
3841
- return typeof color === "string" && color ? color : void 0;
3842
- } catch {
3843
- return void 0;
3844
- }
3845
- };
3846
- var colorlessProgressBars = (section) => {
3847
- const own = section.sectionType === SectionType.progressBar && section.progressColor === void 0;
3848
- const nested = (section.sections ?? []).flatMap((child) => colorlessProgressBars(child));
3849
- return own ? [section, ...nested] : nested;
3850
- };
3851
- var fetchAndConvert = async (id, input, ctx) => {
3852
- const params = { id: String(id), shop: input.shop };
3853
- if (input.cacheKey != null) params.cache_key = String(input.cacheKey);
3854
- if (input.key) params.key = input.key;
3855
- let upstream;
3856
- try {
3857
- upstream = await ctx.fetchUpstream("/api/v1/widgets/settings", params, { host: ctx.host });
3858
- } catch (err) {
3859
- if (err instanceof UpstreamError && [400, 404, 410].includes(err.status)) {
3860
- throw new NotFoundError(`Widget not found: ${id}`);
3861
- }
3862
- throw err;
3863
- }
3864
- const data = unwrapData(upstream);
3865
- if (isMissingUpstreamPayload(data)) throw new NotFoundError(`Widget not found: ${id}`);
3866
- const normalized = convertNumericObjects(deepCamelCase(data));
3867
- if (isProgressBarV1(normalized) && normalized.barSettings.barType === "smart-cart") {
3868
- const { bars, progressColor } = await fetchProgressBarContext(input.shop, ctx);
3869
- return convertSmartCartProgressBarToV2({ bars: bars ?? [], id, progressColor, settings: normalized });
3870
- }
3871
- if (isConvertibleProgressBarV1(normalized)) {
3872
- const { progressColor } = await fetchProgressBarContext(input.shop, ctx);
3873
- return convertProgressBarToV2({ id, progressColor, settings: normalized });
3874
- }
3875
- const converted = dispatchWidgetTransform(id, normalized, ctx.reportDrift);
3876
- const colorless = isCABRootSection(normalized) ? colorlessProgressBars(converted) : [];
3877
- if (colorless.length > 0) {
3878
- const { progressColor: color } = await fetchProgressBarContext(input.shop, ctx);
3879
- if (color) for (const bar of colorless) bar.progressColor = color;
3880
- }
3881
- return converted;
3882
- };
3883
- var resolveWidgetExperiment = async (input, ctx) => {
3884
- if (!input.visitorId) return void 0;
3885
- try {
3886
- const { shop } = await fetchUserConfig({ shop: input.shop }, ctx);
3887
- return resolveExperiment(shop.activeExperiments, input.id, input.visitorId);
3888
- } catch {
3889
- return void 0;
3890
- }
3891
- };
3892
- var isCABTree = (result) => result.version === 2;
3893
- var convertWidgetSettings = async (input, ctx) => {
3894
- const experiment = await resolveWidgetExperiment(input, ctx);
3895
- const result = await fetchAndConvert(experiment?.elementId ?? input.id, input, ctx);
3896
- return experiment && isCABTree(result) ? { ...result, experiment } : result;
3897
- };
3898
-
3899
- // src/server/giftValidation.ts
3900
- var resolveDataSourcePath = async (widgetId, shop, ctx) => {
3901
- const raw = await ctx.fetchUpstream("/api/v1/widgets/settings", { id: String(widgetId), shop }, { host: ctx.host });
3902
- const data = raw && typeof raw === "object" && "data" in raw ? raw.data : raw;
3903
- const endpoint = deepCamelCase(data)?.endpoint;
3904
- return typeof endpoint === "string" && endpoint ? endpoint : void 0;
3905
- };
3906
- var groupByWidget = (gifts) => {
3907
- const byWidget = /* @__PURE__ */ new Map();
3908
- for (const gift of gifts) {
3909
- byWidget.set(gift.widgetId, [...byWidget.get(gift.widgetId) ?? [], gift]);
3910
- }
3911
- return byWidget;
3912
- };
3913
- var validateGifts = async (input, ctx) => {
3914
- const perWidget = await Promise.all(
3915
- [...groupByWidget(input.gifts)].map(async ([widgetId, gifts]) => {
3916
- const discountLost = gifts.filter(hasLostDiscount).map((gift) => gift.lineId);
3917
- try {
3918
- const dataSourcePath = await resolveDataSourcePath(widgetId, input.shop, ctx);
3919
- if (!dataSourcePath) return discountLost;
3920
- const { metadata } = await fetchDataSourceResults(
3921
- {
3922
- cart: input.cart,
3923
- /** Market context threads through so a market-gated gift rule answers for the buyer's market — as engine params, inheriting the shop's markets-toggle gate (REB-21688). */
3924
- country: input.country,
3925
- customerId: input.customerId,
3926
- dataSourcePath,
3927
- /** The gift being validated is in the cart — input filtering would erase it from the answer. */
3928
- filterInputs: "no",
3929
- language: input.language,
3930
- /**
3931
- * `limit: 0` fetches NO products — only the rule verdict is needed. Safe because the engine
3932
- * derives `matchedRules` from rule-config evaluation, not from fetched results (traced through
3933
- * `api.php`), so the metadata is complete at limit 0. Don't raise it to "fix" a non-bug.
3934
- */
3935
- limit: 0,
3936
- shop: input.shop,
3937
- visitorId: input.visitorId
3938
- },
3939
- ctx
3940
- );
3941
- const meta = metadata ?? void 0;
3942
- if (!isDefinitiveRuleAnswer(meta) || hasUnsupportedRuleTypes(meta)) return discountLost;
3943
- const validIds = getValidGwpProductIds(meta.matchedRules);
3944
- if (validIds.size === 0 && !isConfirmedRuleMiss(meta)) return discountLost;
3945
- return gifts.filter((gift) => shouldRemoveGift(gift, validIds)).map((gift) => gift.lineId);
3946
- } catch {
3947
- return discountLost;
3948
- }
3949
- })
3950
- );
3951
- return { remove: perWidget.flat() };
3952
- };
3953
-
3954
3804
  // src/server/adsEngine.ts
3955
3805
  import { z as z28 } from "zod";
3956
3806
  var SUPPLY_KEY = { production: "AQF5RKLR", staging: "Z56LQ7PS" };
@@ -3997,6 +3847,7 @@ var buildAdsParams = (input, publisherKey, supplyKey, testing, userAgent, ipAddr
3997
3847
  testing: String(testing),
3998
3848
  transaction_total: String(input.transactionTotal ?? input.cart.subtotal),
3999
3849
  ...items.length > 0 && { transaction_items: JSON.stringify(items) },
3850
+ ...input.clientRequestId && { client_request_id: input.clientRequestId },
4000
3851
  ...input.country && { country_code: input.country },
4001
3852
  ...input.currency && { currency_code: input.currency },
4002
3853
  ...input.customerEmail && { customer_email: input.customerEmail },
@@ -4180,6 +4031,216 @@ var fetchMonetizeOffers = async (input, ctx) => {
4180
4031
  return { offers };
4181
4032
  };
4182
4033
 
4034
+ // src/server/salvageSections.ts
4035
+ import { v7 as uuidv713 } from "uuid";
4036
+ var STUB_SECTION_TYPE = "unknown";
4037
+ var knownSectionTypes = new Set(sectionTypes);
4038
+ var hasSectionsArray = (raw) => typeof raw === "object" && raw !== null && Array.isArray(raw.sections);
4039
+ var stubSection = (raw, report) => {
4040
+ const record = typeof raw === "object" && raw !== null ? raw : {};
4041
+ const original = record.sectionType;
4042
+ const rawId = record.sectionId;
4043
+ report?.({
4044
+ detail: `invalid section (${typeof original === "string" ? original : "no sectionType"})`,
4045
+ kind: "section"
4046
+ });
4047
+ return {
4048
+ sectionId: typeof rawId === "string" && rawId ? rawId : uuidv713(),
4049
+ sectionType: typeof original === "string" && !knownSectionTypes.has(original) ? original : STUB_SECTION_TYPE
4050
+ };
4051
+ };
4052
+ var salvageSection = (raw, report) => {
4053
+ const parsed = CABSection.safeParse(raw);
4054
+ if (parsed.success) return parsed.data;
4055
+ if (hasSectionsArray(raw)) {
4056
+ const shell = CABSection.safeParse({ ...raw, sections: [] });
4057
+ if (shell.success)
4058
+ return { ...shell.data, sections: raw.sections.map((child) => salvageSection(child, report)) };
4059
+ }
4060
+ return stubSection(raw, report);
4061
+ };
4062
+ var salvageRootSection = (raw, report) => {
4063
+ const shell = CABRootSection.safeParse(hasSectionsArray(raw) ? { ...raw, sections: [] } : raw);
4064
+ if (!shell.success) return CABRootSection.parse(raw);
4065
+ const sections = hasSectionsArray(raw) ? raw.sections.map((child) => salvageSection(child, report)) : [];
4066
+ return { ...shell.data, sections };
4067
+ };
4068
+
4069
+ // src/server/widgetSettings.ts
4070
+ var isMissingUpstreamPayload = isEmptyObject;
4071
+ var dispatchWidgetTransform = (id, normalized, report) => {
4072
+ if (isCABRootSection(normalized)) return salvageRootSection(normalizeTextContent(normalized), report);
4073
+ if (isContentBlockV1(normalized)) return convertContentBlockToV2({ id, settings: normalized });
4074
+ if (isLineItemEditorV1(normalized)) return convertLineItemEditorToV2({ id, settings: normalized });
4075
+ if (isMonetizeV1(normalized)) return convertMonetizeToV2({ id, settings: normalized });
4076
+ if (isOfferV1(normalized)) return convertOfferToV2({ id, name: normalized.name, settings: normalized });
4077
+ return normalized;
4078
+ };
4079
+ var fetchProgressBarContext = async (shop, ctx) => {
4080
+ const config = await fetchUserConfig({ shop }, ctx).catch(() => void 0);
4081
+ if (!config) return {};
4082
+ if (config.shop.billingVersion === "v6" && config.shop.activePackages.some((pkg) => pkg.shortName === "")) {
4083
+ ctx.reportDrift?.({
4084
+ detail: "shop config active package entry inerted (drifted shape) \u2014 smart-cart entitlement may misgate",
4085
+ kind: "shopConfig"
4086
+ });
4087
+ }
4088
+ const entitled = config.shop.billingVersion !== "v6" || config.shop.activePackages.some((pkg) => pkg.shortName === "cart-merchandising" && pkg.isActive);
4089
+ return {
4090
+ bars: entitled ? extractSmartCartBars(config.smartCart) : [],
4091
+ progressColor: await fetchThemeColor(config.shop.shopId, ctx)
4092
+ };
4093
+ };
4094
+ var fetchThemeColor = async (shopId, ctx) => {
4095
+ if (!shopId) return void 0;
4096
+ try {
4097
+ const theme = await ctx.fetchUpstream(`/admin/api/v1/shop/theme/${shopId}`, {}, { host: ctx.host });
4098
+ const color = theme.theme?.settings?.buttonBackground;
4099
+ return typeof color === "string" && color ? color : void 0;
4100
+ } catch {
4101
+ return void 0;
4102
+ }
4103
+ };
4104
+ var colorlessProgressBars = (section) => {
4105
+ const own = section.sectionType === SectionType.progressBar && section.progressColor === void 0;
4106
+ const nested = (section.sections ?? []).flatMap((child) => colorlessProgressBars(child));
4107
+ return own ? [section, ...nested] : nested;
4108
+ };
4109
+ var isCABTree = (result) => result.version === 2;
4110
+ var findMonetizeMarker = (section) => {
4111
+ if (section.sectionType === SectionType.monetize && section.offers === void 0) return section;
4112
+ if (!Array.isArray(section.sections)) return void 0;
4113
+ for (const child of section.sections) {
4114
+ const found = findMonetizeMarker(child);
4115
+ if (found) return found;
4116
+ }
4117
+ return void 0;
4118
+ };
4119
+ var hydrateMonetizeMarkers = async (converted, input, ctx) => {
4120
+ if (!input.monetize || !isCABTree(converted)) return;
4121
+ const first = findMonetizeMarker(converted);
4122
+ if (!first) return;
4123
+ try {
4124
+ const { offers } = await fetchMonetizeOffers(
4125
+ {
4126
+ ...input.monetize,
4127
+ privacyPolicyUrl: first.privacyPolicyUrl,
4128
+ shop: input.shop,
4129
+ visitorId: input.monetize.visitorId || input.visitorId
4130
+ },
4131
+ ctx
4132
+ );
4133
+ first.offers = offers;
4134
+ } catch (err) {
4135
+ ctx.reportDrift?.({
4136
+ detail: `inline hydration failed: ${err instanceof Error ? err.message : String(err)}`,
4137
+ kind: "monetizeOffer"
4138
+ });
4139
+ }
4140
+ };
4141
+ var fetchAndConvert = async (id, input, ctx) => {
4142
+ const params = { id: String(id), shop: input.shop };
4143
+ if (input.cacheKey != null) params.cache_key = String(input.cacheKey);
4144
+ if (input.key) params.key = input.key;
4145
+ let upstream;
4146
+ try {
4147
+ upstream = await ctx.fetchUpstream("/api/v1/widgets/settings", params, { host: ctx.host });
4148
+ } catch (err) {
4149
+ if (err instanceof UpstreamError && [400, 404, 410].includes(err.status)) {
4150
+ throw new NotFoundError(`Widget not found: ${id}`);
4151
+ }
4152
+ throw err;
4153
+ }
4154
+ const data = unwrapData(upstream);
4155
+ if (isMissingUpstreamPayload(data)) throw new NotFoundError(`Widget not found: ${id}`);
4156
+ const normalized = convertNumericObjects(deepCamelCase(data));
4157
+ if (isProgressBarV1(normalized) && normalized.barSettings.barType === "smart-cart") {
4158
+ const { bars, progressColor } = await fetchProgressBarContext(input.shop, ctx);
4159
+ return convertSmartCartProgressBarToV2({ bars: bars ?? [], id, progressColor, settings: normalized });
4160
+ }
4161
+ if (isConvertibleProgressBarV1(normalized)) {
4162
+ const { progressColor } = await fetchProgressBarContext(input.shop, ctx);
4163
+ return convertProgressBarToV2({ id, progressColor, settings: normalized });
4164
+ }
4165
+ const converted = dispatchWidgetTransform(id, normalized, ctx.reportDrift);
4166
+ const colorless = isCABRootSection(normalized) ? colorlessProgressBars(converted) : [];
4167
+ if (colorless.length > 0) {
4168
+ const { progressColor: color } = await fetchProgressBarContext(input.shop, ctx);
4169
+ if (color) for (const bar of colorless) bar.progressColor = color;
4170
+ }
4171
+ return converted;
4172
+ };
4173
+ var resolveWidgetExperiment = async (input, ctx) => {
4174
+ if (!input.visitorId) return void 0;
4175
+ try {
4176
+ const { shop } = await fetchUserConfig({ shop: input.shop }, ctx);
4177
+ return resolveExperiment(shop.activeExperiments, input.id, input.visitorId);
4178
+ } catch {
4179
+ return void 0;
4180
+ }
4181
+ };
4182
+ var convertWidgetSettings = async (input, ctx) => {
4183
+ const experiment = await resolveWidgetExperiment(input, ctx);
4184
+ const result = await fetchAndConvert(experiment?.elementId ?? input.id, input, ctx);
4185
+ await hydrateMonetizeMarkers(result, input, ctx);
4186
+ return experiment && isCABTree(result) ? { ...result, experiment } : result;
4187
+ };
4188
+
4189
+ // src/server/giftValidation.ts
4190
+ var resolveDataSourcePath = async (widgetId, shop, ctx) => {
4191
+ const raw = await ctx.fetchUpstream("/api/v1/widgets/settings", { id: String(widgetId), shop }, { host: ctx.host });
4192
+ const data = raw && typeof raw === "object" && "data" in raw ? raw.data : raw;
4193
+ const endpoint = deepCamelCase(data)?.endpoint;
4194
+ return typeof endpoint === "string" && endpoint ? endpoint : void 0;
4195
+ };
4196
+ var groupByWidget = (gifts) => {
4197
+ const byWidget = /* @__PURE__ */ new Map();
4198
+ for (const gift of gifts) {
4199
+ byWidget.set(gift.widgetId, [...byWidget.get(gift.widgetId) ?? [], gift]);
4200
+ }
4201
+ return byWidget;
4202
+ };
4203
+ var validateGifts = async (input, ctx) => {
4204
+ const perWidget = await Promise.all(
4205
+ [...groupByWidget(input.gifts)].map(async ([widgetId, gifts]) => {
4206
+ const discountLost = gifts.filter(hasLostDiscount).map((gift) => gift.lineId);
4207
+ try {
4208
+ const dataSourcePath = await resolveDataSourcePath(widgetId, input.shop, ctx);
4209
+ if (!dataSourcePath) return discountLost;
4210
+ const { metadata } = await fetchDataSourceResults(
4211
+ {
4212
+ cart: input.cart,
4213
+ /** Market context threads through so a market-gated gift rule answers for the buyer's market — as engine params, inheriting the shop's markets-toggle gate (REB-21688). */
4214
+ country: input.country,
4215
+ customerId: input.customerId,
4216
+ dataSourcePath,
4217
+ /** The gift being validated is in the cart — input filtering would erase it from the answer. */
4218
+ filterInputs: "no",
4219
+ language: input.language,
4220
+ /**
4221
+ * `limit: 0` fetches NO products — only the rule verdict is needed. Safe because the engine
4222
+ * derives `matchedRules` from rule-config evaluation, not from fetched results (traced through
4223
+ * `api.php`), so the metadata is complete at limit 0. Don't raise it to "fix" a non-bug.
4224
+ */
4225
+ limit: 0,
4226
+ shop: input.shop,
4227
+ visitorId: input.visitorId
4228
+ },
4229
+ ctx
4230
+ );
4231
+ const meta = metadata ?? void 0;
4232
+ if (!isDefinitiveRuleAnswer(meta) || hasUnsupportedRuleTypes(meta)) return discountLost;
4233
+ const validIds = getValidGwpProductIds(meta.matchedRules);
4234
+ if (validIds.size === 0 && !isConfirmedRuleMiss(meta)) return discountLost;
4235
+ return gifts.filter((gift) => shouldRemoveGift(gift, validIds)).map((gift) => gift.lineId);
4236
+ } catch {
4237
+ return discountLost;
4238
+ }
4239
+ })
4240
+ );
4241
+ return { remove: perWidget.flat() };
4242
+ };
4243
+
4183
4244
  // src/server/analytics.ts
4184
4245
  import { chunk } from "es-toolkit";
4185
4246
  var EXPERIMENT_TYPE_MAP = {
@@ -4351,14 +4412,6 @@ var CartInputSchema = z29.object({
4351
4412
  /** Presentment subtotal in cents. */
4352
4413
  subtotal: z29.number()
4353
4414
  });
4354
- var WidgetSettingsInputSchema = z29.object({
4355
- cacheKey: z29.number().optional(),
4356
- id: z29.number(),
4357
- key: z29.string().max(REQUEST_BOUNDS.STRING_ID_MAX).optional(),
4358
- shop: z29.string().max(REQUEST_BOUNDS.STRING_ID_MAX),
4359
- /** Stable per-buyer id; when present, enables sticky server-side A/B variant selection. */
4360
- visitorId: z29.string().max(REQUEST_BOUNDS.STRING_ID_MAX).optional()
4361
- });
4362
4415
  var UserConfigInputSchema = z29.object({ shop: z29.string().max(REQUEST_BOUNDS.STRING_ID_MAX) });
4363
4416
  var DataSourceInputSchema = z29.object({
4364
4417
  cart: CartInputSchema,
@@ -4441,6 +4494,8 @@ var MonetizeTransactionItemSchema = z29.object({
4441
4494
  });
4442
4495
  var MonetizeInputSchema = z29.object({
4443
4496
  cart: CartInputSchema,
4497
+ /** Client-minted render-beacon correlation id; forwarded to the ads engine as `client_request_id`. */
4498
+ clientRequestId: z29.string().max(REQUEST_BOUNDS.STRING_ID_MAX).optional(),
4444
4499
  country: z29.string().max(REQUEST_BOUNDS.STRING_ID_MAX).optional(),
4445
4500
  currency: z29.string().max(REQUEST_BOUNDS.STRING_ID_MAX).optional(),
4446
4501
  /** Ads-targeting identity/geo/locale fields (React parity); folded into the engine query verbatim. */
@@ -4470,6 +4525,22 @@ var MonetizeInputSchema = z29.object({
4470
4525
  visitorId: z29.string().max(REQUEST_BOUNDS.STRING_ID_MAX).optional(),
4471
4526
  zipCode: z29.string().max(REQUEST_BOUNDS.STRING_ID_MAX).optional()
4472
4527
  });
4528
+ var WidgetSettingsInputSchema = z29.object({
4529
+ cacheKey: z29.number().optional(),
4530
+ id: z29.number(),
4531
+ key: z29.string().max(REQUEST_BOUNDS.STRING_ID_MAX).optional(),
4532
+ /**
4533
+ * Monetize context for inline hydration: when present AND the widget converts to a Monetize marker,
4534
+ * the server fetches/composes the offer cards inside this same request (no second `/cab/monetize`
4535
+ * round trip). Presence doubles as the capability signal — an absent block keeps the legacy marker
4536
+ * flow, so old/new client×server pairs need no lockstep deploy. `shop` rides the outer input;
4537
+ * `privacyPolicyUrl` comes from the widget settings server-side (more authoritative than a relay).
4538
+ */
4539
+ monetize: MonetizeInputSchema.omit({ privacyPolicyUrl: true, shop: true }).optional(),
4540
+ shop: z29.string().max(REQUEST_BOUNDS.STRING_ID_MAX),
4541
+ /** Stable per-buyer id; when present, enables sticky server-side A/B variant selection. */
4542
+ visitorId: z29.string().max(REQUEST_BOUNDS.STRING_ID_MAX).optional()
4543
+ });
4473
4544
  var AnalyticsEventInputSchema = z29.object({
4474
4545
  name: z29.string().max(REQUEST_BOUNDS.STRING_KEY_MAX),
4475
4546
  properties: z29.record(z29.string().max(REQUEST_BOUNDS.STRING_KEY_MAX), z29.unknown()).optional(),
@@ -79,14 +79,6 @@ export declare const CartInputSchema: z.ZodObject<{
79
79
  }, z.core.$strip>>;
80
80
  subtotal: z.ZodNumber;
81
81
  }, z.core.$strip>;
82
- /** Input for `convertWidgetSettings`. */
83
- export declare const WidgetSettingsInputSchema: z.ZodObject<{
84
- cacheKey: z.ZodOptional<z.ZodNumber>;
85
- id: z.ZodNumber;
86
- key: z.ZodOptional<z.ZodString>;
87
- shop: z.ZodString;
88
- visitorId: z.ZodOptional<z.ZodString>;
89
- }, z.core.$strip>;
90
82
  /** Input for `fetchUserConfig`. */
91
83
  export declare const UserConfigInputSchema: z.ZodObject<{
92
84
  shop: z.ZodString;
@@ -208,6 +200,7 @@ export declare const MonetizeInputSchema: z.ZodObject<{
208
200
  }, z.core.$strip>>;
209
201
  subtotal: z.ZodNumber;
210
202
  }, z.core.$strip>;
203
+ clientRequestId: z.ZodOptional<z.ZodString>;
211
204
  country: z.ZodOptional<z.ZodString>;
212
205
  currency: z.ZodOptional<z.ZodString>;
213
206
  customerEmail: z.ZodOptional<z.ZodString>;
@@ -238,6 +231,64 @@ export declare const MonetizeInputSchema: z.ZodObject<{
238
231
  visitorId: z.ZodOptional<z.ZodString>;
239
232
  zipCode: z.ZodOptional<z.ZodString>;
240
233
  }, z.core.$strip>;
234
+ /** Input for `convertWidgetSettings`. */
235
+ export declare const WidgetSettingsInputSchema: z.ZodObject<{
236
+ cacheKey: z.ZodOptional<z.ZodNumber>;
237
+ id: z.ZodNumber;
238
+ key: z.ZodOptional<z.ZodString>;
239
+ monetize: z.ZodOptional<z.ZodObject<{
240
+ language: z.ZodOptional<z.ZodString>;
241
+ currency: z.ZodOptional<z.ZodString>;
242
+ cart: z.ZodObject<{
243
+ attributes: z.ZodOptional<z.ZodArray<z.ZodObject<{
244
+ key: z.ZodString;
245
+ value: z.ZodOptional<z.ZodString>;
246
+ }, z.core.$strip>>>;
247
+ currencyRate: z.ZodOptional<z.ZodNumber>;
248
+ discountCodes: z.ZodOptional<z.ZodArray<z.ZodString>>;
249
+ discountTotal: z.ZodOptional<z.ZodNumber>;
250
+ items: z.ZodArray<z.ZodObject<{
251
+ productId: z.ZodString;
252
+ properties: z.ZodOptional<z.ZodArray<z.ZodObject<{
253
+ key: z.ZodString;
254
+ value: z.ZodOptional<z.ZodString>;
255
+ }, z.core.$strip>>>;
256
+ quantity: z.ZodNumber;
257
+ subscriptionId: z.ZodOptional<z.ZodString>;
258
+ variantId: z.ZodString;
259
+ }, z.core.$strip>>;
260
+ subtotal: z.ZodNumber;
261
+ }, z.core.$strip>;
262
+ country: z.ZodOptional<z.ZodString>;
263
+ customerId: z.ZodOptional<z.ZodString>;
264
+ visitorId: z.ZodOptional<z.ZodString>;
265
+ clientRequestId: z.ZodOptional<z.ZodString>;
266
+ customerEmail: z.ZodOptional<z.ZodString>;
267
+ customerFirstName: z.ZodOptional<z.ZodString>;
268
+ customerLastName: z.ZodOptional<z.ZodString>;
269
+ orderName: z.ZodOptional<z.ZodString>;
270
+ placement: z.ZodEnum<{
271
+ customer_account: "customer_account";
272
+ order_status: "order_status";
273
+ thank_you_page: "thank_you_page";
274
+ }>;
275
+ sessionId: z.ZodOptional<z.ZodString>;
276
+ testing: z.ZodOptional<z.ZodBoolean>;
277
+ timezone: z.ZodOptional<z.ZodString>;
278
+ transactionId: z.ZodOptional<z.ZodString>;
279
+ transactionItems: z.ZodOptional<z.ZodArray<z.ZodObject<{
280
+ id: z.ZodString;
281
+ priceCents: z.ZodNumber;
282
+ quantity: z.ZodNumber;
283
+ title: z.ZodString;
284
+ type: z.ZodString;
285
+ }, z.core.$strip>>>;
286
+ transactionTotal: z.ZodOptional<z.ZodNumber>;
287
+ zipCode: z.ZodOptional<z.ZodString>;
288
+ }, z.core.$strip>>;
289
+ shop: z.ZodString;
290
+ visitorId: z.ZodOptional<z.ZodString>;
291
+ }, z.core.$strip>;
241
292
  /**
242
293
  * A generic analytics event from the extension; the server maps `name` → the engine's noun/verb/subject.
243
294
  * `properties` VALUES stay unbounded (`z.unknown`): the analytics rail is lenient-parsed (an over-limit
@@ -929,7 +929,13 @@ var import_zod14 = require("zod");
929
929
  var CABMonetizeSection = import_zod14.z.object({
930
930
  ...CABVariableFields,
931
931
  name: import_zod14.z.string().optional(),
932
- /** Privacy Policy footer href (the widget's `privacy_policy_url` setting); the client relays it to `/cab/monetize`. */
932
+ /**
933
+ * Server-hydrated offer cards (inline hydration): when `/cab/sections` was asked with a `monetize`
934
+ * context block, the composed cards ride the marker itself — `[]` means "hydrated, no fill" (render
935
+ * nothing, skip the fetch); ABSENT means "not hydrated" (the client fetches via `/cab/monetize`).
936
+ */
937
+ offers: import_zod14.z.array(import_zod14.z.lazy(() => CABSection)).optional(),
938
+ /** Privacy Policy footer href (the widget's `privacy_policy_url` setting); read server-side for hydration, relayed by the client only on the legacy `/cab/monetize` fallback. */
933
939
  privacyPolicyUrl: import_zod14.z.string().optional(),
934
940
  sectionId: import_zod14.z.uuid().default(() => (0, import_uuid8.v7)()),
935
941
  sectionType: import_zod14.z.literal(SectionType.monetize).default(SectionType.monetize),
@@ -872,7 +872,13 @@ import { z as z14 } from "zod";
872
872
  var CABMonetizeSection = z14.object({
873
873
  ...CABVariableFields,
874
874
  name: z14.string().optional(),
875
- /** Privacy Policy footer href (the widget's `privacy_policy_url` setting); the client relays it to `/cab/monetize`. */
875
+ /**
876
+ * Server-hydrated offer cards (inline hydration): when `/cab/sections` was asked with a `monetize`
877
+ * context block, the composed cards ride the marker itself — `[]` means "hydrated, no fill" (render
878
+ * nothing, skip the fetch); ABSENT means "not hydrated" (the client fetches via `/cab/monetize`).
879
+ */
880
+ offers: z14.array(z14.lazy(() => CABSection)).optional(),
881
+ /** Privacy Policy footer href (the widget's `privacy_policy_url` setting); read server-side for hydration, relayed by the client only on the legacy `/cab/monetize` fallback. */
876
882
  privacyPolicyUrl: z14.string().optional(),
877
883
  sectionId: z14.uuid().default(() => uuidv78()),
878
884
  sectionType: z14.literal(SectionType.monetize).default(SectionType.monetize),
@@ -3,8 +3,9 @@ import { type V1MonetizeConvertInput } from '../../transforms/monetizeV1/types';
3
3
  /**
4
4
  * Translate a v1 Monetize (standalone `ui_extension_ad`) widget into a `CABRootSection`: a stub root
5
5
  * holding one `monetize` marker section. The marker carries the widget's only real config — the Privacy
6
- * Policy href and the ads-taxonomy `vertical`/`subVertical` ids; the live offer cards are composed by
7
- * `POST /cab/monetize` when the client mounts the marker, so no ad content is converted here. The root
6
+ * Policy href and the ads-taxonomy `vertical`/`subVertical` ids; no ad content is converted here — the
7
+ * live offer cards are hydrated onto the marker by `convertWidgetSettings` when the request carries a
8
+ * `monetize` context block, else composed by `POST /cab/monetize` when the client mounts it. The root
8
9
  * keeps `type: ui_extension_ad` (the type is the widget's identity, not the converter's), and the
9
10
  * marker's `visibility` stays unset: React always mounts Monetize on its target surfaces, so the
10
11
  * converted tree must not invent a gate.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rebuy/rebuy",
3
- "version": "3.6.0",
3
+ "version": "3.8.0",
4
4
  "description": "Shared zod schemas, legacy-to-CAB widget transforms, and server orchestrators for Rebuy's Shopify consumers (rebuy-api, admin-nextjs, rebuy-shopify-extensions)",
5
5
  "license": "MIT",
6
6
  "author": "Rebuy, Inc.",