@rebuy/rebuy 3.4.0 → 3.5.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.
- package/dist/index.cjs +24 -0
- package/dist/index.mjs +24 -0
- package/dist/schema/cabShopConfig.d.ts +15 -1
- package/dist/schema/driftedMarkets.d.ts +17 -0
- package/dist/server/index.cjs +54 -21
- package/dist/server/index.mjs +54 -21
- package/dist/server/shared.d.ts +7 -4
- 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
|
});
|
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
|
});
|
|
@@ -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
|
});
|
|
@@ -3580,7 +3604,14 @@ var fetchUserConfig = async (input, ctx) => {
|
|
|
3580
3604
|
}
|
|
3581
3605
|
const data = unwrapData(upstream);
|
|
3582
3606
|
if (isMissingShopPayload(data)) throw new NotFoundError(`Shop config not found: ${input.shop}`);
|
|
3583
|
-
|
|
3607
|
+
const config = parseShielded(CabUserConfig, data);
|
|
3608
|
+
if (config.shop.markets === DRIFTED_MARKETS) {
|
|
3609
|
+
ctx.reportDrift?.({
|
|
3610
|
+
detail: "shop config markets field drifted to disabled (fail-closed salvage) \u2014 market context withheld",
|
|
3611
|
+
kind: "shopConfig"
|
|
3612
|
+
});
|
|
3613
|
+
}
|
|
3614
|
+
return config;
|
|
3584
3615
|
};
|
|
3585
3616
|
|
|
3586
3617
|
// src/utilities.ts
|
|
@@ -3681,19 +3712,16 @@ var reportUnreadableVerdict = (metadata, report) => {
|
|
|
3681
3712
|
var dedupeById = (products) => (0, import_es_toolkit6.uniqBy)(products, (product) => product.id);
|
|
3682
3713
|
var fetchDataSourceResults = async (input, ctx) => {
|
|
3683
3714
|
const { shop } = await fetchUserConfig({ shop: input.shop }, ctx);
|
|
3684
|
-
const
|
|
3685
|
-
|
|
3686
|
-
...input.country && { "X-Country-Code": input.country }
|
|
3687
|
-
};
|
|
3715
|
+
const locale = input.language?.split("-")[0]?.toLowerCase();
|
|
3716
|
+
const marketParams = shop.markets.enabled ? { ...input.country && { country_code: input.country }, ...locale && { locale } } : {};
|
|
3688
3717
|
let upstream;
|
|
3689
3718
|
try {
|
|
3690
3719
|
upstream = await ctx.fetchUpstream(
|
|
3691
3720
|
`/api/v1${input.dataSourcePath}`,
|
|
3692
3721
|
{},
|
|
3693
3722
|
{
|
|
3694
|
-
...Object.keys(marketHeaders).length > 0 && { headers: marketHeaders },
|
|
3695
3723
|
host: ctx.host,
|
|
3696
|
-
search: serialize(buildEngineParams(input, shop.apiKey))
|
|
3724
|
+
search: serialize({ ...buildEngineParams(input, shop.apiKey), ...marketParams })
|
|
3697
3725
|
}
|
|
3698
3726
|
);
|
|
3699
3727
|
} catch (err) {
|
|
@@ -3864,7 +3892,7 @@ var validateGifts = async (input, ctx) => {
|
|
|
3864
3892
|
const { metadata } = await fetchDataSourceResults(
|
|
3865
3893
|
{
|
|
3866
3894
|
cart: input.cart,
|
|
3867
|
-
/** Market context threads through so a market-gated gift rule answers for the buyer's market (REB-
|
|
3895
|
+
/** 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
3896
|
country: input.country,
|
|
3869
3897
|
customerId: input.customerId,
|
|
3870
3898
|
dataSourcePath,
|
|
@@ -4313,13 +4341,12 @@ var DataSourceInputSchema = import_zod28.z.object({
|
|
|
4313
4341
|
*/
|
|
4314
4342
|
companyLocationId: import_zod28.z.string().max(REQUEST_BOUNDS.STRING_ID_MAX).optional(),
|
|
4315
4343
|
/**
|
|
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.
|
|
4344
|
+
* Buyer's market country (ISO 3166-1 alpha-2) — forwarded to the engine as the `country_code` PARAM
|
|
4345
|
+
* when the shop's markets toggle is on (REB-21688; the sink was the `X-Country-Code` header under
|
|
4346
|
+
* REB-21102, which is why the regex exists — kept because a two-letter bound still turns garbage into
|
|
4347
|
+
* a clean 422 on this public, unauthenticated route instead of an engine round-trip). Normalized to
|
|
4348
|
+
* uppercase (the wire value is forwarded verbatim as the param) so a case-sensitive engine lookup
|
|
4349
|
+
* can't silently miss a lowercase code — `adsEngine.ts` uppercases for the same reason.
|
|
4323
4350
|
*/
|
|
4324
4351
|
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
4352
|
customerId: import_zod28.z.string().max(REQUEST_BOUNDS.STRING_ID_MAX).optional(),
|
|
@@ -4334,8 +4361,11 @@ var DataSourceInputSchema = import_zod28.z.object({
|
|
|
4334
4361
|
message: `Too many integration keys (max ${REQUEST_BOUNDS.INTEGRATION_KEYS_MAX})`
|
|
4335
4362
|
}).optional(),
|
|
4336
4363
|
/**
|
|
4337
|
-
* Buyer's checkout language (BCP 47) —
|
|
4338
|
-
*
|
|
4364
|
+
* Buyer's checkout language (BCP 47) — normalized server-side (the whole subtag tail dropped, region
|
|
4365
|
+
* and script alike, then lowercased: `en-US` ⇒ `en`, `zh-Hant` ⇒ `zh` — PEV2 feeds key on the bare
|
|
4366
|
+
* 2-letter language, and the market half comes from `country`) and forwarded to the engine as the
|
|
4367
|
+
* `locale` PARAM when the shop's markets toggle is on (REB-21688); regex-bound for the same reason as
|
|
4368
|
+
* `country` above. MUST be a single tag
|
|
4339
4369
|
* (e.g. `fr-CA`) — a q-value list like `fr-CA,fr;q=0.9` (the raw shape of a browser `Accept-Language`
|
|
4340
4370
|
* request header) fails the regex. Callers should send the buyer's resolved locale (e.g. Shopify
|
|
4341
4371
|
* checkout's `localization.language.isoCode`), not the request header verbatim.
|
|
@@ -4356,16 +4386,19 @@ var GiftInputSchema = import_zod28.z.object({
|
|
|
4356
4386
|
var GiftValidationInputSchema = import_zod28.z.object({
|
|
4357
4387
|
cart: CartInputSchema,
|
|
4358
4388
|
/**
|
|
4359
|
-
* Buyer's market country — threaded into the per-widget data-source evaluations
|
|
4360
|
-
*
|
|
4389
|
+
* Buyer's market country — threaded into the per-widget data-source evaluations, which forward it to
|
|
4390
|
+
* the engine as the `country_code` param when the shop's markets toggle is on (REB-21688 — gift
|
|
4391
|
+
* validation inherits the gate, so a market-gated gift rule answers market-blind when the merchant
|
|
4392
|
+
* turns markets off); regex-bound and uppercase-normalized for the same reasons as
|
|
4361
4393
|
* `DataSourceInputSchema`'s `country`.
|
|
4362
4394
|
*/
|
|
4363
4395
|
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
4396
|
customerId: import_zod28.z.string().max(REQUEST_BOUNDS.STRING_ID_MAX).optional(),
|
|
4365
4397
|
gifts: import_zod28.z.array(GiftInputSchema).max(REQUEST_BOUNDS.GIFTS_MAX),
|
|
4366
4398
|
/**
|
|
4367
|
-
* Buyer's checkout language — threaded into the per-widget data-source evaluations
|
|
4368
|
-
*
|
|
4399
|
+
* Buyer's checkout language — threaded into the per-widget data-source evaluations, which normalize
|
|
4400
|
+
* and forward it as the engine `locale` param under the same markets-toggle gate (REB-21688). MUST be
|
|
4401
|
+
* a single BCP-47 tag, for the same reason as `DataSourceInputSchema`'s `language` above.
|
|
4369
4402
|
*/
|
|
4370
4403
|
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
4404
|
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
|
});
|
|
@@ -3532,7 +3556,14 @@ var fetchUserConfig = async (input, ctx) => {
|
|
|
3532
3556
|
}
|
|
3533
3557
|
const data = unwrapData(upstream);
|
|
3534
3558
|
if (isMissingShopPayload(data)) throw new NotFoundError(`Shop config not found: ${input.shop}`);
|
|
3535
|
-
|
|
3559
|
+
const config = parseShielded(CabUserConfig, data);
|
|
3560
|
+
if (config.shop.markets === DRIFTED_MARKETS) {
|
|
3561
|
+
ctx.reportDrift?.({
|
|
3562
|
+
detail: "shop config markets field drifted to disabled (fail-closed salvage) \u2014 market context withheld",
|
|
3563
|
+
kind: "shopConfig"
|
|
3564
|
+
});
|
|
3565
|
+
}
|
|
3566
|
+
return config;
|
|
3536
3567
|
};
|
|
3537
3568
|
|
|
3538
3569
|
// src/utilities.ts
|
|
@@ -3633,19 +3664,16 @@ var reportUnreadableVerdict = (metadata, report) => {
|
|
|
3633
3664
|
var dedupeById = (products) => uniqBy2(products, (product) => product.id);
|
|
3634
3665
|
var fetchDataSourceResults = async (input, ctx) => {
|
|
3635
3666
|
const { shop } = await fetchUserConfig({ shop: input.shop }, ctx);
|
|
3636
|
-
const
|
|
3637
|
-
|
|
3638
|
-
...input.country && { "X-Country-Code": input.country }
|
|
3639
|
-
};
|
|
3667
|
+
const locale = input.language?.split("-")[0]?.toLowerCase();
|
|
3668
|
+
const marketParams = shop.markets.enabled ? { ...input.country && { country_code: input.country }, ...locale && { locale } } : {};
|
|
3640
3669
|
let upstream;
|
|
3641
3670
|
try {
|
|
3642
3671
|
upstream = await ctx.fetchUpstream(
|
|
3643
3672
|
`/api/v1${input.dataSourcePath}`,
|
|
3644
3673
|
{},
|
|
3645
3674
|
{
|
|
3646
|
-
...Object.keys(marketHeaders).length > 0 && { headers: marketHeaders },
|
|
3647
3675
|
host: ctx.host,
|
|
3648
|
-
search: serialize(buildEngineParams(input, shop.apiKey))
|
|
3676
|
+
search: serialize({ ...buildEngineParams(input, shop.apiKey), ...marketParams })
|
|
3649
3677
|
}
|
|
3650
3678
|
);
|
|
3651
3679
|
} catch (err) {
|
|
@@ -3816,7 +3844,7 @@ var validateGifts = async (input, ctx) => {
|
|
|
3816
3844
|
const { metadata } = await fetchDataSourceResults(
|
|
3817
3845
|
{
|
|
3818
3846
|
cart: input.cart,
|
|
3819
|
-
/** Market context threads through so a market-gated gift rule answers for the buyer's market (REB-
|
|
3847
|
+
/** 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
3848
|
country: input.country,
|
|
3821
3849
|
customerId: input.customerId,
|
|
3822
3850
|
dataSourcePath,
|
|
@@ -4265,13 +4293,12 @@ var DataSourceInputSchema = z28.object({
|
|
|
4265
4293
|
*/
|
|
4266
4294
|
companyLocationId: z28.string().max(REQUEST_BOUNDS.STRING_ID_MAX).optional(),
|
|
4267
4295
|
/**
|
|
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.
|
|
4296
|
+
* Buyer's market country (ISO 3166-1 alpha-2) — forwarded to the engine as the `country_code` PARAM
|
|
4297
|
+
* when the shop's markets toggle is on (REB-21688; the sink was the `X-Country-Code` header under
|
|
4298
|
+
* REB-21102, which is why the regex exists — kept because a two-letter bound still turns garbage into
|
|
4299
|
+
* a clean 422 on this public, unauthenticated route instead of an engine round-trip). Normalized to
|
|
4300
|
+
* uppercase (the wire value is forwarded verbatim as the param) so a case-sensitive engine lookup
|
|
4301
|
+
* can't silently miss a lowercase code — `adsEngine.ts` uppercases for the same reason.
|
|
4275
4302
|
*/
|
|
4276
4303
|
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
4304
|
customerId: z28.string().max(REQUEST_BOUNDS.STRING_ID_MAX).optional(),
|
|
@@ -4286,8 +4313,11 @@ var DataSourceInputSchema = z28.object({
|
|
|
4286
4313
|
message: `Too many integration keys (max ${REQUEST_BOUNDS.INTEGRATION_KEYS_MAX})`
|
|
4287
4314
|
}).optional(),
|
|
4288
4315
|
/**
|
|
4289
|
-
* Buyer's checkout language (BCP 47) —
|
|
4290
|
-
*
|
|
4316
|
+
* Buyer's checkout language (BCP 47) — normalized server-side (the whole subtag tail dropped, region
|
|
4317
|
+
* and script alike, then lowercased: `en-US` ⇒ `en`, `zh-Hant` ⇒ `zh` — PEV2 feeds key on the bare
|
|
4318
|
+
* 2-letter language, and the market half comes from `country`) and forwarded to the engine as the
|
|
4319
|
+
* `locale` PARAM when the shop's markets toggle is on (REB-21688); regex-bound for the same reason as
|
|
4320
|
+
* `country` above. MUST be a single tag
|
|
4291
4321
|
* (e.g. `fr-CA`) — a q-value list like `fr-CA,fr;q=0.9` (the raw shape of a browser `Accept-Language`
|
|
4292
4322
|
* request header) fails the regex. Callers should send the buyer's resolved locale (e.g. Shopify
|
|
4293
4323
|
* checkout's `localization.language.isoCode`), not the request header verbatim.
|
|
@@ -4308,16 +4338,19 @@ var GiftInputSchema = z28.object({
|
|
|
4308
4338
|
var GiftValidationInputSchema = z28.object({
|
|
4309
4339
|
cart: CartInputSchema,
|
|
4310
4340
|
/**
|
|
4311
|
-
* Buyer's market country — threaded into the per-widget data-source evaluations
|
|
4312
|
-
*
|
|
4341
|
+
* Buyer's market country — threaded into the per-widget data-source evaluations, which forward it to
|
|
4342
|
+
* the engine as the `country_code` param when the shop's markets toggle is on (REB-21688 — gift
|
|
4343
|
+
* validation inherits the gate, so a market-gated gift rule answers market-blind when the merchant
|
|
4344
|
+
* turns markets off); regex-bound and uppercase-normalized for the same reasons as
|
|
4313
4345
|
* `DataSourceInputSchema`'s `country`.
|
|
4314
4346
|
*/
|
|
4315
4347
|
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
4348
|
customerId: z28.string().max(REQUEST_BOUNDS.STRING_ID_MAX).optional(),
|
|
4317
4349
|
gifts: z28.array(GiftInputSchema).max(REQUEST_BOUNDS.GIFTS_MAX),
|
|
4318
4350
|
/**
|
|
4319
|
-
* Buyer's checkout language — threaded into the per-widget data-source evaluations
|
|
4320
|
-
*
|
|
4351
|
+
* Buyer's checkout language — threaded into the per-widget data-source evaluations, which normalize
|
|
4352
|
+
* and forward it as the engine `locale` param under the same markets-toggle gate (REB-21688). MUST be
|
|
4353
|
+
* a single BCP-47 tag, for the same reason as `DataSourceInputSchema`'s `language` above.
|
|
4321
4354
|
*/
|
|
4322
4355
|
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
4356
|
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;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rebuy/rebuy",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.5.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.",
|