@rebuy/rebuy 3.4.0 → 3.5.1
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/dist/index.cjs +25 -0
- package/dist/index.mjs +25 -0
- package/dist/schema/cabShopConfig.d.ts +15 -1
- package/dist/schema/driftedMarkets.d.ts +17 -0
- package/dist/server/index.cjs +55 -21
- package/dist/server/index.mjs +55 -21
- package/dist/server/shared.d.ts +7 -4
- package/dist/transforms/index.cjs +1 -0
- package/dist/transforms/index.mjs +1 -0
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -989,6 +989,9 @@ var RebuyClient = class {
|
|
|
989
989
|
// src/schema/cabShopConfig.ts
|
|
990
990
|
var import_zod2 = require("zod");
|
|
991
991
|
|
|
992
|
+
// src/schema/driftedMarkets.ts
|
|
993
|
+
var DRIFTED_MARKETS = Object.freeze({ enabled: false });
|
|
994
|
+
|
|
992
995
|
// src/schema/smartCart.ts
|
|
993
996
|
var import_zod = require("zod");
|
|
994
997
|
var SmartCartTierProduct = import_zod.z.looseObject({
|
|
@@ -1128,6 +1131,27 @@ var CabShopConfig = import_zod2.z.object({
|
|
|
1128
1131
|
* missing/renamed currency degrades to "unknown" (client falls back to buyer decimals), never a parse fail.
|
|
1129
1132
|
*/
|
|
1130
1133
|
currency: import_zod2.z.string().optional().catch(void 0),
|
|
1134
|
+
/**
|
|
1135
|
+
* The merchant's "Use Markets in Data Sources" toggle. The engine (CI2) never reads it on the serve
|
|
1136
|
+
* path, so the proxy honouring it before sending market context is what makes the setting mean
|
|
1137
|
+
* anything (REB-21688). Fail-CLOSED, unlike the fail-open siblings: a missing/null/drifted field reads
|
|
1138
|
+
* as markets OFF — market context silently dropped (engine serves shop-default market), never sent
|
|
1139
|
+
* against the merchant's setting. The OFF-reading salvages stay distinguishable: missing AND null are
|
|
1140
|
+
* the engine's normal absent vocabulary (`.nullish()`, like `monetize`/`billingVersion`) and read as
|
|
1141
|
+
* the plain default — not drift — while only a drifted SHAPE `.catch`es, to the {@link DRIFTED_MARKETS}
|
|
1142
|
+
* identity sentinel, so `fetchUserConfig` can emit a `shopConfig` drift for the one case that means an
|
|
1143
|
+
* engine rename/retype; otherwise a markets-enabled merchant degrading to shop-default pricing would
|
|
1144
|
+
* be indistinguishable from markets genuinely off. The missing-key case is EXPLICIT via `.default()`
|
|
1145
|
+
* (a factory, so parses never share a mutable object) rather than inferred from zod running an
|
|
1146
|
+
* optional key's transform on `undefined`; `null` flows past the default through `.nullish()` into the
|
|
1147
|
+
* transform. `Readonly` on the output type because the drifted salvage IS frozen — an assignment to
|
|
1148
|
+
* `markets.enabled` downstream would throw only for drifted shops in production, so surface direct
|
|
1149
|
+
* writes to tsc (a guard for the direct case only: TS doesn't check `readonly` in assignability, so
|
|
1150
|
+
* laundering through a mutable-shaped alias still compiles).
|
|
1151
|
+
* The inner `enabled` stays strict on purpose: `{ enabled: null }` is a genuine retype and drifting is
|
|
1152
|
+
* the correct answer (the editor-path `ShopConfig.markets` would hard-fail the same payload).
|
|
1153
|
+
*/
|
|
1154
|
+
markets: import_zod2.z.object({ enabled: import_zod2.z.boolean() }).nullish().catch(DRIFTED_MARKETS).default(() => ({ enabled: false })).transform((markets) => markets ?? { enabled: false }),
|
|
1131
1155
|
monetize: import_zod2.z.object({ publisherKey: import_zod2.z.string().nullable() }).nullish().catch(void 0),
|
|
1132
1156
|
shopId: import_zod2.z.number().optional().catch(void 0)
|
|
1133
1157
|
});
|
|
@@ -4662,6 +4686,7 @@ var convertOfferToV2 = ({ id, name, settings }) => {
|
|
|
4662
4686
|
sectionType: "carousel"
|
|
4663
4687
|
};
|
|
4664
4688
|
return CABRootSection.parse({
|
|
4689
|
+
alignment: { horizontal: "center", vertical: "top" },
|
|
4665
4690
|
direction: "rows",
|
|
4666
4691
|
editorMode: LOCATION_TO_EDITOR_MODE[settings.location ?? "checkout"],
|
|
4667
4692
|
// The widget name is required by the admin save schema; carry the legacy widget's name through.
|
package/dist/index.mjs
CHANGED
|
@@ -727,6 +727,9 @@ var RebuyClient = class {
|
|
|
727
727
|
// src/schema/cabShopConfig.ts
|
|
728
728
|
import { z as z2 } from "zod";
|
|
729
729
|
|
|
730
|
+
// src/schema/driftedMarkets.ts
|
|
731
|
+
var DRIFTED_MARKETS = Object.freeze({ enabled: false });
|
|
732
|
+
|
|
730
733
|
// src/schema/smartCart.ts
|
|
731
734
|
import { z } from "zod";
|
|
732
735
|
var SmartCartTierProduct = z.looseObject({
|
|
@@ -866,6 +869,27 @@ var CabShopConfig = z2.object({
|
|
|
866
869
|
* missing/renamed currency degrades to "unknown" (client falls back to buyer decimals), never a parse fail.
|
|
867
870
|
*/
|
|
868
871
|
currency: z2.string().optional().catch(void 0),
|
|
872
|
+
/**
|
|
873
|
+
* The merchant's "Use Markets in Data Sources" toggle. The engine (CI2) never reads it on the serve
|
|
874
|
+
* path, so the proxy honouring it before sending market context is what makes the setting mean
|
|
875
|
+
* anything (REB-21688). Fail-CLOSED, unlike the fail-open siblings: a missing/null/drifted field reads
|
|
876
|
+
* as markets OFF — market context silently dropped (engine serves shop-default market), never sent
|
|
877
|
+
* against the merchant's setting. The OFF-reading salvages stay distinguishable: missing AND null are
|
|
878
|
+
* the engine's normal absent vocabulary (`.nullish()`, like `monetize`/`billingVersion`) and read as
|
|
879
|
+
* the plain default — not drift — while only a drifted SHAPE `.catch`es, to the {@link DRIFTED_MARKETS}
|
|
880
|
+
* identity sentinel, so `fetchUserConfig` can emit a `shopConfig` drift for the one case that means an
|
|
881
|
+
* engine rename/retype; otherwise a markets-enabled merchant degrading to shop-default pricing would
|
|
882
|
+
* be indistinguishable from markets genuinely off. The missing-key case is EXPLICIT via `.default()`
|
|
883
|
+
* (a factory, so parses never share a mutable object) rather than inferred from zod running an
|
|
884
|
+
* optional key's transform on `undefined`; `null` flows past the default through `.nullish()` into the
|
|
885
|
+
* transform. `Readonly` on the output type because the drifted salvage IS frozen — an assignment to
|
|
886
|
+
* `markets.enabled` downstream would throw only for drifted shops in production, so surface direct
|
|
887
|
+
* writes to tsc (a guard for the direct case only: TS doesn't check `readonly` in assignability, so
|
|
888
|
+
* laundering through a mutable-shaped alias still compiles).
|
|
889
|
+
* The inner `enabled` stays strict on purpose: `{ enabled: null }` is a genuine retype and drifting is
|
|
890
|
+
* the correct answer (the editor-path `ShopConfig.markets` would hard-fail the same payload).
|
|
891
|
+
*/
|
|
892
|
+
markets: z2.object({ enabled: z2.boolean() }).nullish().catch(DRIFTED_MARKETS).default(() => ({ enabled: false })).transform((markets) => markets ?? { enabled: false }),
|
|
869
893
|
monetize: z2.object({ publisherKey: z2.string().nullable() }).nullish().catch(void 0),
|
|
870
894
|
shopId: z2.number().optional().catch(void 0)
|
|
871
895
|
});
|
|
@@ -4400,6 +4424,7 @@ var convertOfferToV2 = ({ id, name, settings }) => {
|
|
|
4400
4424
|
sectionType: "carousel"
|
|
4401
4425
|
};
|
|
4402
4426
|
return CABRootSection.parse({
|
|
4427
|
+
alignment: { horizontal: "center", vertical: "top" },
|
|
4403
4428
|
direction: "rows",
|
|
4404
4429
|
editorMode: LOCATION_TO_EDITOR_MODE[settings.location ?? "checkout"],
|
|
4405
4430
|
// The widget name is required by the admin save schema; carry the legacy widget's name through.
|
|
@@ -16,7 +16,7 @@ export declare const CabActiveExperiment: z.ZodObject<{
|
|
|
16
16
|
}, z.core.$strip>;
|
|
17
17
|
export type CabActiveExperiment = z.infer<typeof CabActiveExperiment>;
|
|
18
18
|
/**
|
|
19
|
-
* The lean shop config CAB actually consumes — the
|
|
19
|
+
* The lean shop config CAB actually consumes — the handful of load-bearing fields, not the ~30 the
|
|
20
20
|
* editor-facing `ShopConfig` carries. `apiKey` stays strict (every offers/monetize/gift/analytics
|
|
21
21
|
* request authenticates with it); the rest use `.catch()` so a peripheral engine change (a renamed
|
|
22
22
|
* field, a new `carousel` enum value like `'css'`, an experiment shape drift) degrades that one field
|
|
@@ -40,6 +40,13 @@ export declare const CabShopConfig: z.ZodObject<{
|
|
|
40
40
|
apiKey: z.ZodString;
|
|
41
41
|
billingVersion: z.ZodDefault<z.ZodCatch<z.ZodNullable<z.ZodString>>>;
|
|
42
42
|
currency: z.ZodCatch<z.ZodOptional<z.ZodString>>;
|
|
43
|
+
markets: z.ZodPipe<z.ZodDefault<z.ZodCatch<z.ZodOptional<z.ZodNullable<z.ZodObject<{
|
|
44
|
+
enabled: z.ZodBoolean;
|
|
45
|
+
}, z.core.$strip>>>>>, z.ZodTransform<Readonly<{
|
|
46
|
+
enabled: boolean;
|
|
47
|
+
}>, {
|
|
48
|
+
enabled: boolean;
|
|
49
|
+
} | null>>;
|
|
43
50
|
monetize: z.ZodCatch<z.ZodOptional<z.ZodNullable<z.ZodObject<{
|
|
44
51
|
publisherKey: z.ZodNullable<z.ZodString>;
|
|
45
52
|
}, z.core.$strip>>>>;
|
|
@@ -70,6 +77,13 @@ export declare const CabUserConfig: z.ZodObject<{
|
|
|
70
77
|
apiKey: z.ZodString;
|
|
71
78
|
billingVersion: z.ZodDefault<z.ZodCatch<z.ZodNullable<z.ZodString>>>;
|
|
72
79
|
currency: z.ZodCatch<z.ZodOptional<z.ZodString>>;
|
|
80
|
+
markets: z.ZodPipe<z.ZodDefault<z.ZodCatch<z.ZodOptional<z.ZodNullable<z.ZodObject<{
|
|
81
|
+
enabled: z.ZodBoolean;
|
|
82
|
+
}, z.core.$strip>>>>>, z.ZodTransform<Readonly<{
|
|
83
|
+
enabled: boolean;
|
|
84
|
+
}>, {
|
|
85
|
+
enabled: boolean;
|
|
86
|
+
} | null>>;
|
|
73
87
|
monetize: z.ZodCatch<z.ZodOptional<z.ZodNullable<z.ZodObject<{
|
|
74
88
|
publisherKey: z.ZodNullable<z.ZodString>;
|
|
75
89
|
}, z.core.$strip>>>>;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Identity sentinel for a drifted `CabShopConfig.markets` shape: the schema's `.catch` returns THIS
|
|
3
|
+
* frozen object, and `fetchUserConfig` detects drift by reference (`markets === DRIFTED_MARKETS`).
|
|
4
|
+
* Reference identity keeps the sentinel out of the wire vocabulary entirely — no key the engine could
|
|
5
|
+
* ever send (or forge) can mark or unmark drift, unlike a value marker in the object's own shape — and
|
|
6
|
+
* off the public type, which stays a plain readonly `{ enabled: boolean }`.
|
|
7
|
+
*
|
|
8
|
+
* Deliberately NOT re-exported from `src/schema/index.ts` (the barrel lists its files explicitly): each
|
|
9
|
+
* `exports` subpath bundles separately with no splitting, so identity only holds within one bundle, and
|
|
10
|
+
* a consumer-held copy from `@rebuy/rebuy` would silently never match a value from `@rebuy/rebuy/server`.
|
|
11
|
+
* Keeping the symbol off every public barrel makes that cross-bundle comparison unreachable instead of a
|
|
12
|
+
* documented caveat. Both importers (`cabShopConfig.ts`, `server/userConfig.ts`) land in the `./server`
|
|
13
|
+
* bundle, where the comparison is exact.
|
|
14
|
+
*/
|
|
15
|
+
export declare const DRIFTED_MARKETS: Readonly<{
|
|
16
|
+
readonly enabled: false;
|
|
17
|
+
}>;
|
package/dist/server/index.cjs
CHANGED
|
@@ -411,6 +411,9 @@ var NotFoundError = class extends Error {
|
|
|
411
411
|
// src/schema/cabShopConfig.ts
|
|
412
412
|
var import_zod6 = require("zod");
|
|
413
413
|
|
|
414
|
+
// src/schema/driftedMarkets.ts
|
|
415
|
+
var DRIFTED_MARKETS = Object.freeze({ enabled: false });
|
|
416
|
+
|
|
414
417
|
// src/schema/smartCart.ts
|
|
415
418
|
var import_zod5 = require("zod");
|
|
416
419
|
var SmartCartTierProduct = import_zod5.z.looseObject({
|
|
@@ -550,6 +553,27 @@ var CabShopConfig = import_zod6.z.object({
|
|
|
550
553
|
* missing/renamed currency degrades to "unknown" (client falls back to buyer decimals), never a parse fail.
|
|
551
554
|
*/
|
|
552
555
|
currency: import_zod6.z.string().optional().catch(void 0),
|
|
556
|
+
/**
|
|
557
|
+
* The merchant's "Use Markets in Data Sources" toggle. The engine (CI2) never reads it on the serve
|
|
558
|
+
* path, so the proxy honouring it before sending market context is what makes the setting mean
|
|
559
|
+
* anything (REB-21688). Fail-CLOSED, unlike the fail-open siblings: a missing/null/drifted field reads
|
|
560
|
+
* as markets OFF — market context silently dropped (engine serves shop-default market), never sent
|
|
561
|
+
* against the merchant's setting. The OFF-reading salvages stay distinguishable: missing AND null are
|
|
562
|
+
* the engine's normal absent vocabulary (`.nullish()`, like `monetize`/`billingVersion`) and read as
|
|
563
|
+
* the plain default — not drift — while only a drifted SHAPE `.catch`es, to the {@link DRIFTED_MARKETS}
|
|
564
|
+
* identity sentinel, so `fetchUserConfig` can emit a `shopConfig` drift for the one case that means an
|
|
565
|
+
* engine rename/retype; otherwise a markets-enabled merchant degrading to shop-default pricing would
|
|
566
|
+
* be indistinguishable from markets genuinely off. The missing-key case is EXPLICIT via `.default()`
|
|
567
|
+
* (a factory, so parses never share a mutable object) rather than inferred from zod running an
|
|
568
|
+
* optional key's transform on `undefined`; `null` flows past the default through `.nullish()` into the
|
|
569
|
+
* transform. `Readonly` on the output type because the drifted salvage IS frozen — an assignment to
|
|
570
|
+
* `markets.enabled` downstream would throw only for drifted shops in production, so surface direct
|
|
571
|
+
* writes to tsc (a guard for the direct case only: TS doesn't check `readonly` in assignability, so
|
|
572
|
+
* laundering through a mutable-shaped alias still compiles).
|
|
573
|
+
* The inner `enabled` stays strict on purpose: `{ enabled: null }` is a genuine retype and drifting is
|
|
574
|
+
* the correct answer (the editor-path `ShopConfig.markets` would hard-fail the same payload).
|
|
575
|
+
*/
|
|
576
|
+
markets: import_zod6.z.object({ enabled: import_zod6.z.boolean() }).nullish().catch(DRIFTED_MARKETS).default(() => ({ enabled: false })).transform((markets) => markets ?? { enabled: false }),
|
|
553
577
|
monetize: import_zod6.z.object({ publisherKey: import_zod6.z.string().nullable() }).nullish().catch(void 0),
|
|
554
578
|
shopId: import_zod6.z.number().optional().catch(void 0)
|
|
555
579
|
});
|
|
@@ -3212,6 +3236,7 @@ var convertOfferToV2 = ({ id, name, settings }) => {
|
|
|
3212
3236
|
sectionType: "carousel"
|
|
3213
3237
|
};
|
|
3214
3238
|
return CABRootSection.parse({
|
|
3239
|
+
alignment: { horizontal: "center", vertical: "top" },
|
|
3215
3240
|
direction: "rows",
|
|
3216
3241
|
editorMode: LOCATION_TO_EDITOR_MODE[settings.location ?? "checkout"],
|
|
3217
3242
|
// The widget name is required by the admin save schema; carry the legacy widget's name through.
|
|
@@ -3580,7 +3605,14 @@ var fetchUserConfig = async (input, ctx) => {
|
|
|
3580
3605
|
}
|
|
3581
3606
|
const data = unwrapData(upstream);
|
|
3582
3607
|
if (isMissingShopPayload(data)) throw new NotFoundError(`Shop config not found: ${input.shop}`);
|
|
3583
|
-
|
|
3608
|
+
const config = parseShielded(CabUserConfig, data);
|
|
3609
|
+
if (config.shop.markets === DRIFTED_MARKETS) {
|
|
3610
|
+
ctx.reportDrift?.({
|
|
3611
|
+
detail: "shop config markets field drifted to disabled (fail-closed salvage) \u2014 market context withheld",
|
|
3612
|
+
kind: "shopConfig"
|
|
3613
|
+
});
|
|
3614
|
+
}
|
|
3615
|
+
return config;
|
|
3584
3616
|
};
|
|
3585
3617
|
|
|
3586
3618
|
// src/utilities.ts
|
|
@@ -3681,19 +3713,16 @@ var reportUnreadableVerdict = (metadata, report) => {
|
|
|
3681
3713
|
var dedupeById = (products) => (0, import_es_toolkit6.uniqBy)(products, (product) => product.id);
|
|
3682
3714
|
var fetchDataSourceResults = async (input, ctx) => {
|
|
3683
3715
|
const { shop } = await fetchUserConfig({ shop: input.shop }, ctx);
|
|
3684
|
-
const
|
|
3685
|
-
|
|
3686
|
-
...input.country && { "X-Country-Code": input.country }
|
|
3687
|
-
};
|
|
3716
|
+
const locale = input.language?.split("-")[0]?.toLowerCase();
|
|
3717
|
+
const marketParams = shop.markets.enabled ? { ...input.country && { country_code: input.country }, ...locale && { locale } } : {};
|
|
3688
3718
|
let upstream;
|
|
3689
3719
|
try {
|
|
3690
3720
|
upstream = await ctx.fetchUpstream(
|
|
3691
3721
|
`/api/v1${input.dataSourcePath}`,
|
|
3692
3722
|
{},
|
|
3693
3723
|
{
|
|
3694
|
-
...Object.keys(marketHeaders).length > 0 && { headers: marketHeaders },
|
|
3695
3724
|
host: ctx.host,
|
|
3696
|
-
search: serialize(buildEngineParams(input, shop.apiKey))
|
|
3725
|
+
search: serialize({ ...buildEngineParams(input, shop.apiKey), ...marketParams })
|
|
3697
3726
|
}
|
|
3698
3727
|
);
|
|
3699
3728
|
} catch (err) {
|
|
@@ -3864,7 +3893,7 @@ var validateGifts = async (input, ctx) => {
|
|
|
3864
3893
|
const { metadata } = await fetchDataSourceResults(
|
|
3865
3894
|
{
|
|
3866
3895
|
cart: input.cart,
|
|
3867
|
-
/** Market context threads through so a market-gated gift rule answers for the buyer's market (REB-
|
|
3896
|
+
/** 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). */
|
|
3868
3897
|
country: input.country,
|
|
3869
3898
|
customerId: input.customerId,
|
|
3870
3899
|
dataSourcePath,
|
|
@@ -4313,13 +4342,12 @@ var DataSourceInputSchema = import_zod28.z.object({
|
|
|
4313
4342
|
*/
|
|
4314
4343
|
companyLocationId: import_zod28.z.string().max(REQUEST_BOUNDS.STRING_ID_MAX).optional(),
|
|
4315
4344
|
/**
|
|
4316
|
-
* Buyer's market country (ISO 3166-1 alpha-2) — forwarded to the engine as the `
|
|
4317
|
-
*
|
|
4318
|
-
*
|
|
4319
|
-
*
|
|
4320
|
-
*
|
|
4321
|
-
*
|
|
4322
|
-
* engine lookup can't silently miss a lowercase code — `adsEngine.ts` uppercases for the same reason.
|
|
4345
|
+
* Buyer's market country (ISO 3166-1 alpha-2) — forwarded to the engine as the `country_code` PARAM
|
|
4346
|
+
* when the shop's markets toggle is on (REB-21688; the sink was the `X-Country-Code` header under
|
|
4347
|
+
* REB-21102, which is why the regex exists — kept because a two-letter bound still turns garbage into
|
|
4348
|
+
* a clean 422 on this public, unauthenticated route instead of an engine round-trip). Normalized to
|
|
4349
|
+
* uppercase (the wire value is forwarded verbatim as the param) so a case-sensitive engine lookup
|
|
4350
|
+
* can't silently miss a lowercase code — `adsEngine.ts` uppercases for the same reason.
|
|
4323
4351
|
*/
|
|
4324
4352
|
country: import_zod28.z.string().max(REQUEST_BOUNDS.STRING_ID_MAX).regex(/^[A-Za-z]{2}$/, "must be an ISO 3166-1 alpha-2 country code (e.g. CA)").transform((value) => value.toUpperCase()).optional(),
|
|
4325
4353
|
customerId: import_zod28.z.string().max(REQUEST_BOUNDS.STRING_ID_MAX).optional(),
|
|
@@ -4334,8 +4362,11 @@ var DataSourceInputSchema = import_zod28.z.object({
|
|
|
4334
4362
|
message: `Too many integration keys (max ${REQUEST_BOUNDS.INTEGRATION_KEYS_MAX})`
|
|
4335
4363
|
}).optional(),
|
|
4336
4364
|
/**
|
|
4337
|
-
* Buyer's checkout language (BCP 47) —
|
|
4338
|
-
*
|
|
4365
|
+
* Buyer's checkout language (BCP 47) — normalized server-side (the whole subtag tail dropped, region
|
|
4366
|
+
* and script alike, then lowercased: `en-US` ⇒ `en`, `zh-Hant` ⇒ `zh` — PEV2 feeds key on the bare
|
|
4367
|
+
* 2-letter language, and the market half comes from `country`) and forwarded to the engine as the
|
|
4368
|
+
* `locale` PARAM when the shop's markets toggle is on (REB-21688); regex-bound for the same reason as
|
|
4369
|
+
* `country` above. MUST be a single tag
|
|
4339
4370
|
* (e.g. `fr-CA`) — a q-value list like `fr-CA,fr;q=0.9` (the raw shape of a browser `Accept-Language`
|
|
4340
4371
|
* request header) fails the regex. Callers should send the buyer's resolved locale (e.g. Shopify
|
|
4341
4372
|
* checkout's `localization.language.isoCode`), not the request header verbatim.
|
|
@@ -4356,16 +4387,19 @@ var GiftInputSchema = import_zod28.z.object({
|
|
|
4356
4387
|
var GiftValidationInputSchema = import_zod28.z.object({
|
|
4357
4388
|
cart: CartInputSchema,
|
|
4358
4389
|
/**
|
|
4359
|
-
* Buyer's market country — threaded into the per-widget data-source evaluations
|
|
4360
|
-
*
|
|
4390
|
+
* Buyer's market country — threaded into the per-widget data-source evaluations, which forward it to
|
|
4391
|
+
* the engine as the `country_code` param when the shop's markets toggle is on (REB-21688 — gift
|
|
4392
|
+
* validation inherits the gate, so a market-gated gift rule answers market-blind when the merchant
|
|
4393
|
+
* turns markets off); regex-bound and uppercase-normalized for the same reasons as
|
|
4361
4394
|
* `DataSourceInputSchema`'s `country`.
|
|
4362
4395
|
*/
|
|
4363
4396
|
country: import_zod28.z.string().max(REQUEST_BOUNDS.STRING_ID_MAX).regex(/^[A-Za-z]{2}$/, "must be an ISO 3166-1 alpha-2 country code (e.g. CA)").transform((value) => value.toUpperCase()).optional(),
|
|
4364
4397
|
customerId: import_zod28.z.string().max(REQUEST_BOUNDS.STRING_ID_MAX).optional(),
|
|
4365
4398
|
gifts: import_zod28.z.array(GiftInputSchema).max(REQUEST_BOUNDS.GIFTS_MAX),
|
|
4366
4399
|
/**
|
|
4367
|
-
* Buyer's checkout language — threaded into the per-widget data-source evaluations
|
|
4368
|
-
*
|
|
4400
|
+
* Buyer's checkout language — threaded into the per-widget data-source evaluations, which normalize
|
|
4401
|
+
* and forward it as the engine `locale` param under the same markets-toggle gate (REB-21688). MUST be
|
|
4402
|
+
* a single BCP-47 tag, for the same reason as `DataSourceInputSchema`'s `language` above.
|
|
4369
4403
|
*/
|
|
4370
4404
|
language: import_zod28.z.string().max(REQUEST_BOUNDS.STRING_ID_MAX).regex(/^[A-Za-z0-9-]+$/, "must be a single BCP-47 tag (e.g. fr-CA), not a q-value list").optional(),
|
|
4371
4405
|
shop: import_zod28.z.string().max(REQUEST_BOUNDS.STRING_ID_MAX),
|
package/dist/server/index.mjs
CHANGED
|
@@ -363,6 +363,9 @@ var NotFoundError = class extends Error {
|
|
|
363
363
|
// src/schema/cabShopConfig.ts
|
|
364
364
|
import { z as z6 } from "zod";
|
|
365
365
|
|
|
366
|
+
// src/schema/driftedMarkets.ts
|
|
367
|
+
var DRIFTED_MARKETS = Object.freeze({ enabled: false });
|
|
368
|
+
|
|
366
369
|
// src/schema/smartCart.ts
|
|
367
370
|
import { z as z5 } from "zod";
|
|
368
371
|
var SmartCartTierProduct = z5.looseObject({
|
|
@@ -502,6 +505,27 @@ var CabShopConfig = z6.object({
|
|
|
502
505
|
* missing/renamed currency degrades to "unknown" (client falls back to buyer decimals), never a parse fail.
|
|
503
506
|
*/
|
|
504
507
|
currency: z6.string().optional().catch(void 0),
|
|
508
|
+
/**
|
|
509
|
+
* The merchant's "Use Markets in Data Sources" toggle. The engine (CI2) never reads it on the serve
|
|
510
|
+
* path, so the proxy honouring it before sending market context is what makes the setting mean
|
|
511
|
+
* anything (REB-21688). Fail-CLOSED, unlike the fail-open siblings: a missing/null/drifted field reads
|
|
512
|
+
* as markets OFF — market context silently dropped (engine serves shop-default market), never sent
|
|
513
|
+
* against the merchant's setting. The OFF-reading salvages stay distinguishable: missing AND null are
|
|
514
|
+
* the engine's normal absent vocabulary (`.nullish()`, like `monetize`/`billingVersion`) and read as
|
|
515
|
+
* the plain default — not drift — while only a drifted SHAPE `.catch`es, to the {@link DRIFTED_MARKETS}
|
|
516
|
+
* identity sentinel, so `fetchUserConfig` can emit a `shopConfig` drift for the one case that means an
|
|
517
|
+
* engine rename/retype; otherwise a markets-enabled merchant degrading to shop-default pricing would
|
|
518
|
+
* be indistinguishable from markets genuinely off. The missing-key case is EXPLICIT via `.default()`
|
|
519
|
+
* (a factory, so parses never share a mutable object) rather than inferred from zod running an
|
|
520
|
+
* optional key's transform on `undefined`; `null` flows past the default through `.nullish()` into the
|
|
521
|
+
* transform. `Readonly` on the output type because the drifted salvage IS frozen — an assignment to
|
|
522
|
+
* `markets.enabled` downstream would throw only for drifted shops in production, so surface direct
|
|
523
|
+
* writes to tsc (a guard for the direct case only: TS doesn't check `readonly` in assignability, so
|
|
524
|
+
* laundering through a mutable-shaped alias still compiles).
|
|
525
|
+
* The inner `enabled` stays strict on purpose: `{ enabled: null }` is a genuine retype and drifting is
|
|
526
|
+
* the correct answer (the editor-path `ShopConfig.markets` would hard-fail the same payload).
|
|
527
|
+
*/
|
|
528
|
+
markets: z6.object({ enabled: z6.boolean() }).nullish().catch(DRIFTED_MARKETS).default(() => ({ enabled: false })).transform((markets) => markets ?? { enabled: false }),
|
|
505
529
|
monetize: z6.object({ publisherKey: z6.string().nullable() }).nullish().catch(void 0),
|
|
506
530
|
shopId: z6.number().optional().catch(void 0)
|
|
507
531
|
});
|
|
@@ -3164,6 +3188,7 @@ var convertOfferToV2 = ({ id, name, settings }) => {
|
|
|
3164
3188
|
sectionType: "carousel"
|
|
3165
3189
|
};
|
|
3166
3190
|
return CABRootSection.parse({
|
|
3191
|
+
alignment: { horizontal: "center", vertical: "top" },
|
|
3167
3192
|
direction: "rows",
|
|
3168
3193
|
editorMode: LOCATION_TO_EDITOR_MODE[settings.location ?? "checkout"],
|
|
3169
3194
|
// The widget name is required by the admin save schema; carry the legacy widget's name through.
|
|
@@ -3532,7 +3557,14 @@ var fetchUserConfig = async (input, ctx) => {
|
|
|
3532
3557
|
}
|
|
3533
3558
|
const data = unwrapData(upstream);
|
|
3534
3559
|
if (isMissingShopPayload(data)) throw new NotFoundError(`Shop config not found: ${input.shop}`);
|
|
3535
|
-
|
|
3560
|
+
const config = parseShielded(CabUserConfig, data);
|
|
3561
|
+
if (config.shop.markets === DRIFTED_MARKETS) {
|
|
3562
|
+
ctx.reportDrift?.({
|
|
3563
|
+
detail: "shop config markets field drifted to disabled (fail-closed salvage) \u2014 market context withheld",
|
|
3564
|
+
kind: "shopConfig"
|
|
3565
|
+
});
|
|
3566
|
+
}
|
|
3567
|
+
return config;
|
|
3536
3568
|
};
|
|
3537
3569
|
|
|
3538
3570
|
// src/utilities.ts
|
|
@@ -3633,19 +3665,16 @@ var reportUnreadableVerdict = (metadata, report) => {
|
|
|
3633
3665
|
var dedupeById = (products) => uniqBy2(products, (product) => product.id);
|
|
3634
3666
|
var fetchDataSourceResults = async (input, ctx) => {
|
|
3635
3667
|
const { shop } = await fetchUserConfig({ shop: input.shop }, ctx);
|
|
3636
|
-
const
|
|
3637
|
-
|
|
3638
|
-
...input.country && { "X-Country-Code": input.country }
|
|
3639
|
-
};
|
|
3668
|
+
const locale = input.language?.split("-")[0]?.toLowerCase();
|
|
3669
|
+
const marketParams = shop.markets.enabled ? { ...input.country && { country_code: input.country }, ...locale && { locale } } : {};
|
|
3640
3670
|
let upstream;
|
|
3641
3671
|
try {
|
|
3642
3672
|
upstream = await ctx.fetchUpstream(
|
|
3643
3673
|
`/api/v1${input.dataSourcePath}`,
|
|
3644
3674
|
{},
|
|
3645
3675
|
{
|
|
3646
|
-
...Object.keys(marketHeaders).length > 0 && { headers: marketHeaders },
|
|
3647
3676
|
host: ctx.host,
|
|
3648
|
-
search: serialize(buildEngineParams(input, shop.apiKey))
|
|
3677
|
+
search: serialize({ ...buildEngineParams(input, shop.apiKey), ...marketParams })
|
|
3649
3678
|
}
|
|
3650
3679
|
);
|
|
3651
3680
|
} catch (err) {
|
|
@@ -3816,7 +3845,7 @@ var validateGifts = async (input, ctx) => {
|
|
|
3816
3845
|
const { metadata } = await fetchDataSourceResults(
|
|
3817
3846
|
{
|
|
3818
3847
|
cart: input.cart,
|
|
3819
|
-
/** Market context threads through so a market-gated gift rule answers for the buyer's market (REB-
|
|
3848
|
+
/** 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). */
|
|
3820
3849
|
country: input.country,
|
|
3821
3850
|
customerId: input.customerId,
|
|
3822
3851
|
dataSourcePath,
|
|
@@ -4265,13 +4294,12 @@ var DataSourceInputSchema = z28.object({
|
|
|
4265
4294
|
*/
|
|
4266
4295
|
companyLocationId: z28.string().max(REQUEST_BOUNDS.STRING_ID_MAX).optional(),
|
|
4267
4296
|
/**
|
|
4268
|
-
* Buyer's market country (ISO 3166-1 alpha-2) — forwarded to the engine as the `
|
|
4269
|
-
*
|
|
4270
|
-
*
|
|
4271
|
-
*
|
|
4272
|
-
*
|
|
4273
|
-
*
|
|
4274
|
-
* engine lookup can't silently miss a lowercase code — `adsEngine.ts` uppercases for the same reason.
|
|
4297
|
+
* Buyer's market country (ISO 3166-1 alpha-2) — forwarded to the engine as the `country_code` PARAM
|
|
4298
|
+
* when the shop's markets toggle is on (REB-21688; the sink was the `X-Country-Code` header under
|
|
4299
|
+
* REB-21102, which is why the regex exists — kept because a two-letter bound still turns garbage into
|
|
4300
|
+
* a clean 422 on this public, unauthenticated route instead of an engine round-trip). Normalized to
|
|
4301
|
+
* uppercase (the wire value is forwarded verbatim as the param) so a case-sensitive engine lookup
|
|
4302
|
+
* can't silently miss a lowercase code — `adsEngine.ts` uppercases for the same reason.
|
|
4275
4303
|
*/
|
|
4276
4304
|
country: z28.string().max(REQUEST_BOUNDS.STRING_ID_MAX).regex(/^[A-Za-z]{2}$/, "must be an ISO 3166-1 alpha-2 country code (e.g. CA)").transform((value) => value.toUpperCase()).optional(),
|
|
4277
4305
|
customerId: z28.string().max(REQUEST_BOUNDS.STRING_ID_MAX).optional(),
|
|
@@ -4286,8 +4314,11 @@ var DataSourceInputSchema = z28.object({
|
|
|
4286
4314
|
message: `Too many integration keys (max ${REQUEST_BOUNDS.INTEGRATION_KEYS_MAX})`
|
|
4287
4315
|
}).optional(),
|
|
4288
4316
|
/**
|
|
4289
|
-
* Buyer's checkout language (BCP 47) —
|
|
4290
|
-
*
|
|
4317
|
+
* Buyer's checkout language (BCP 47) — normalized server-side (the whole subtag tail dropped, region
|
|
4318
|
+
* and script alike, then lowercased: `en-US` ⇒ `en`, `zh-Hant` ⇒ `zh` — PEV2 feeds key on the bare
|
|
4319
|
+
* 2-letter language, and the market half comes from `country`) and forwarded to the engine as the
|
|
4320
|
+
* `locale` PARAM when the shop's markets toggle is on (REB-21688); regex-bound for the same reason as
|
|
4321
|
+
* `country` above. MUST be a single tag
|
|
4291
4322
|
* (e.g. `fr-CA`) — a q-value list like `fr-CA,fr;q=0.9` (the raw shape of a browser `Accept-Language`
|
|
4292
4323
|
* request header) fails the regex. Callers should send the buyer's resolved locale (e.g. Shopify
|
|
4293
4324
|
* checkout's `localization.language.isoCode`), not the request header verbatim.
|
|
@@ -4308,16 +4339,19 @@ var GiftInputSchema = z28.object({
|
|
|
4308
4339
|
var GiftValidationInputSchema = z28.object({
|
|
4309
4340
|
cart: CartInputSchema,
|
|
4310
4341
|
/**
|
|
4311
|
-
* Buyer's market country — threaded into the per-widget data-source evaluations
|
|
4312
|
-
*
|
|
4342
|
+
* Buyer's market country — threaded into the per-widget data-source evaluations, which forward it to
|
|
4343
|
+
* the engine as the `country_code` param when the shop's markets toggle is on (REB-21688 — gift
|
|
4344
|
+
* validation inherits the gate, so a market-gated gift rule answers market-blind when the merchant
|
|
4345
|
+
* turns markets off); regex-bound and uppercase-normalized for the same reasons as
|
|
4313
4346
|
* `DataSourceInputSchema`'s `country`.
|
|
4314
4347
|
*/
|
|
4315
4348
|
country: z28.string().max(REQUEST_BOUNDS.STRING_ID_MAX).regex(/^[A-Za-z]{2}$/, "must be an ISO 3166-1 alpha-2 country code (e.g. CA)").transform((value) => value.toUpperCase()).optional(),
|
|
4316
4349
|
customerId: z28.string().max(REQUEST_BOUNDS.STRING_ID_MAX).optional(),
|
|
4317
4350
|
gifts: z28.array(GiftInputSchema).max(REQUEST_BOUNDS.GIFTS_MAX),
|
|
4318
4351
|
/**
|
|
4319
|
-
* Buyer's checkout language — threaded into the per-widget data-source evaluations
|
|
4320
|
-
*
|
|
4352
|
+
* Buyer's checkout language — threaded into the per-widget data-source evaluations, which normalize
|
|
4353
|
+
* and forward it as the engine `locale` param under the same markets-toggle gate (REB-21688). MUST be
|
|
4354
|
+
* a single BCP-47 tag, for the same reason as `DataSourceInputSchema`'s `language` above.
|
|
4321
4355
|
*/
|
|
4322
4356
|
language: z28.string().max(REQUEST_BOUNDS.STRING_ID_MAX).regex(/^[A-Za-z0-9-]+$/, "must be a single BCP-47 tag (e.g. fr-CA), not a q-value list").optional(),
|
|
4323
4357
|
shop: z28.string().max(REQUEST_BOUNDS.STRING_ID_MAX),
|
package/dist/server/shared.d.ts
CHANGED
|
@@ -10,10 +10,13 @@ export declare const REBUY_ENGINE_HOST = "rebuyengine.com";
|
|
|
10
10
|
* caller (rebuy-api owns the egress, timeouts, and the `X-Rebuy-Upstream` proxy header); MUST throw
|
|
11
11
|
* {@link UpstreamError} on a non-2xx response so orchestrators can interpret the status.
|
|
12
12
|
*
|
|
13
|
-
* Defaults to GET with `params`/`search` as the query string
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
13
|
+
* Defaults to GET with `params`/`search` as the query string. `headers` apply regardless of method —
|
|
14
|
+
* implementations must not gate them behind a `method === 'POST'` branch. Note the only current library
|
|
15
|
+
* caller sending `headers` is the analytics POST ingest (REB-21688 moved market context off the
|
|
16
|
+
* REB-21102 headers onto engine params, retiring the GET+headers case and the test that witnessed it),
|
|
17
|
+
* so a POST-gated implementation would pass every test in this repo today; the invariant stands so a
|
|
18
|
+
* future GET caller's headers can't silently drop. Pass `method: 'POST'` with `body` (JSON body) for
|
|
19
|
+
* write-style calls like analytics ingest.
|
|
17
20
|
*/
|
|
18
21
|
export type UpstreamFetch = (path: string, params: Record<string, string>, options?: {
|
|
19
22
|
body?: unknown;
|
|
@@ -3140,6 +3140,7 @@ var convertOfferToV2 = ({ id, name, settings }) => {
|
|
|
3140
3140
|
sectionType: "carousel"
|
|
3141
3141
|
};
|
|
3142
3142
|
return CABRootSection.parse({
|
|
3143
|
+
alignment: { horizontal: "center", vertical: "top" },
|
|
3143
3144
|
direction: "rows",
|
|
3144
3145
|
editorMode: LOCATION_TO_EDITOR_MODE[settings.location ?? "checkout"],
|
|
3145
3146
|
// The widget name is required by the admin save schema; carry the legacy widget's name through.
|
|
@@ -3083,6 +3083,7 @@ var convertOfferToV2 = ({ id, name, settings }) => {
|
|
|
3083
3083
|
sectionType: "carousel"
|
|
3084
3084
|
};
|
|
3085
3085
|
return CABRootSection.parse({
|
|
3086
|
+
alignment: { horizontal: "center", vertical: "top" },
|
|
3086
3087
|
direction: "rows",
|
|
3087
3088
|
editorMode: LOCATION_TO_EDITOR_MODE[settings.location ?? "checkout"],
|
|
3088
3089
|
// The widget name is required by the admin save schema; carry the legacy widget's name through.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rebuy/rebuy",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.5.1",
|
|
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.",
|