@rebuy/rebuy 3.0.0 → 3.1.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.
@@ -3676,12 +3676,20 @@ var reportUnreadableVerdict = (metadata, report) => {
3676
3676
  var dedupeById = (products) => (0, import_es_toolkit6.uniqBy)(products, (product) => product.id);
3677
3677
  var fetchDataSourceResults = async (input, ctx) => {
3678
3678
  const { shop } = await fetchUserConfig({ shop: input.shop }, ctx);
3679
+ const marketHeaders = {
3680
+ ...input.language && { "Accept-Language": input.language },
3681
+ ...input.country && { "X-Country-Code": input.country }
3682
+ };
3679
3683
  let upstream;
3680
3684
  try {
3681
3685
  upstream = await ctx.fetchUpstream(
3682
3686
  `/api/v1${input.dataSourcePath}`,
3683
3687
  {},
3684
- { host: ctx.host, search: serialize(buildEngineParams(input, shop.apiKey)) }
3688
+ {
3689
+ ...Object.keys(marketHeaders).length > 0 && { headers: marketHeaders },
3690
+ host: ctx.host,
3691
+ search: serialize(buildEngineParams(input, shop.apiKey))
3692
+ }
3685
3693
  );
3686
3694
  } catch (err) {
3687
3695
  if (err instanceof UpstreamError && (err.status === 404 || err.status === 410)) {
@@ -3851,10 +3859,13 @@ var validateGifts = async (input, ctx) => {
3851
3859
  const { metadata } = await fetchDataSourceResults(
3852
3860
  {
3853
3861
  cart: input.cart,
3862
+ /** Market context threads through so a market-gated gift rule answers for the buyer's market (REB-21102). */
3863
+ country: input.country,
3854
3864
  customerId: input.customerId,
3855
3865
  dataSourcePath,
3856
3866
  /** The gift being validated is in the cart — input filtering would erase it from the answer. */
3857
3867
  filterInputs: "no",
3868
+ language: input.language,
3858
3869
  /**
3859
3870
  * `limit: 0` fetches NO products — only the rule verdict is needed. Safe because the engine
3860
3871
  * derives `matchedRules` from rule-config evaluation, not from fetched results (traced through
@@ -4295,6 +4306,16 @@ var DataSourceInputSchema = import_zod28.z.object({
4295
4306
  * client's country-context market check is bypassed for B2B (it false-negatives B2B-catalog products).
4296
4307
  */
4297
4308
  companyLocationId: import_zod28.z.string().max(REQUEST_BOUNDS.STRING_ID_MAX).optional(),
4309
+ /**
4310
+ * Buyer's market country (ISO 3166-1 alpha-2) — forwarded to the engine as the `X-Country-Code`
4311
+ * HEADER (REB-21102), so it's regex-bound (not just length-bound like this schema's other passthrough
4312
+ * identifiers): an unfiltered value reaching a header sink can throw a raw `TypeError` (CRLF, or any
4313
+ * non-Latin1 codepoint) that isn't an `UpstreamError`, escaping `fetchDataSourceResults`'s error
4314
+ * handling as an unhandled 500 on this public, unauthenticated route instead of a clean 422 here.
4315
+ * Normalized to uppercase (the wire value is forwarded verbatim as the header) so a case-sensitive
4316
+ * engine lookup can't silently miss a lowercase code — `adsEngine.ts` uppercases for the same reason.
4317
+ */
4318
+ 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(),
4298
4319
  customerId: import_zod28.z.string().max(REQUEST_BOUNDS.STRING_ID_MAX).optional(),
4299
4320
  dataSourcePath: import_zod28.z.string().max(REQUEST_BOUNDS.STRING_KEY_MAX),
4300
4321
  /**
@@ -4306,6 +4327,14 @@ var DataSourceInputSchema = import_zod28.z.object({
4306
4327
  integrations: import_zod28.z.record(import_zod28.z.string().max(REQUEST_BOUNDS.STRING_KEY_MAX), import_zod28.z.boolean()).refine((record) => Object.keys(record).length <= REQUEST_BOUNDS.INTEGRATION_KEYS_MAX, {
4307
4328
  message: `Too many integration keys (max ${REQUEST_BOUNDS.INTEGRATION_KEYS_MAX})`
4308
4329
  }).optional(),
4330
+ /**
4331
+ * Buyer's checkout language (BCP 47) — forwarded to the engine as the `Accept-Language` HEADER
4332
+ * (REB-21102); regex-bound for the same header-sink reason as `country` above. MUST be a single tag
4333
+ * (e.g. `fr-CA`) — a q-value list like `fr-CA,fr;q=0.9` (the raw shape of a browser `Accept-Language`
4334
+ * request header) fails the regex. Callers should send the buyer's resolved locale (e.g. Shopify
4335
+ * checkout's `localization.language.isoCode`), not the request header verbatim.
4336
+ */
4337
+ 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(),
4309
4338
  limit: import_zod28.z.number().max(REQUEST_BOUNDS.LIMIT_MAX),
4310
4339
  productType: import_zod28.z.enum(["both", "one-time", "subscription"]).optional(),
4311
4340
  shop: import_zod28.z.string().max(REQUEST_BOUNDS.STRING_ID_MAX),
@@ -4320,8 +4349,19 @@ var GiftInputSchema = import_zod28.z.object({
4320
4349
  });
4321
4350
  var GiftValidationInputSchema = import_zod28.z.object({
4322
4351
  cart: CartInputSchema,
4352
+ /**
4353
+ * Buyer's market country — threaded into the per-widget data-source evaluations (REB-21102), which
4354
+ * forward it to a header sink; regex-bound and uppercase-normalized for the same reasons as
4355
+ * `DataSourceInputSchema`'s `country`.
4356
+ */
4357
+ 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(),
4323
4358
  customerId: import_zod28.z.string().max(REQUEST_BOUNDS.STRING_ID_MAX).optional(),
4324
4359
  gifts: import_zod28.z.array(GiftInputSchema).max(REQUEST_BOUNDS.GIFTS_MAX),
4360
+ /**
4361
+ * Buyer's checkout language — threaded into the per-widget data-source evaluations (REB-21102). MUST
4362
+ * be a single BCP-47 tag, for the same reason as `DataSourceInputSchema`'s `language` above.
4363
+ */
4364
+ 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(),
4325
4365
  shop: import_zod28.z.string().max(REQUEST_BOUNDS.STRING_ID_MAX),
4326
4366
  visitorId: import_zod28.z.string().max(REQUEST_BOUNDS.STRING_ID_MAX).optional()
4327
4367
  });
@@ -3628,12 +3628,20 @@ var reportUnreadableVerdict = (metadata, report) => {
3628
3628
  var dedupeById = (products) => uniqBy2(products, (product) => product.id);
3629
3629
  var fetchDataSourceResults = async (input, ctx) => {
3630
3630
  const { shop } = await fetchUserConfig({ shop: input.shop }, ctx);
3631
+ const marketHeaders = {
3632
+ ...input.language && { "Accept-Language": input.language },
3633
+ ...input.country && { "X-Country-Code": input.country }
3634
+ };
3631
3635
  let upstream;
3632
3636
  try {
3633
3637
  upstream = await ctx.fetchUpstream(
3634
3638
  `/api/v1${input.dataSourcePath}`,
3635
3639
  {},
3636
- { host: ctx.host, search: serialize(buildEngineParams(input, shop.apiKey)) }
3640
+ {
3641
+ ...Object.keys(marketHeaders).length > 0 && { headers: marketHeaders },
3642
+ host: ctx.host,
3643
+ search: serialize(buildEngineParams(input, shop.apiKey))
3644
+ }
3637
3645
  );
3638
3646
  } catch (err) {
3639
3647
  if (err instanceof UpstreamError && (err.status === 404 || err.status === 410)) {
@@ -3803,10 +3811,13 @@ var validateGifts = async (input, ctx) => {
3803
3811
  const { metadata } = await fetchDataSourceResults(
3804
3812
  {
3805
3813
  cart: input.cart,
3814
+ /** Market context threads through so a market-gated gift rule answers for the buyer's market (REB-21102). */
3815
+ country: input.country,
3806
3816
  customerId: input.customerId,
3807
3817
  dataSourcePath,
3808
3818
  /** The gift being validated is in the cart — input filtering would erase it from the answer. */
3809
3819
  filterInputs: "no",
3820
+ language: input.language,
3810
3821
  /**
3811
3822
  * `limit: 0` fetches NO products — only the rule verdict is needed. Safe because the engine
3812
3823
  * derives `matchedRules` from rule-config evaluation, not from fetched results (traced through
@@ -4247,6 +4258,16 @@ var DataSourceInputSchema = z28.object({
4247
4258
  * client's country-context market check is bypassed for B2B (it false-negatives B2B-catalog products).
4248
4259
  */
4249
4260
  companyLocationId: z28.string().max(REQUEST_BOUNDS.STRING_ID_MAX).optional(),
4261
+ /**
4262
+ * Buyer's market country (ISO 3166-1 alpha-2) — forwarded to the engine as the `X-Country-Code`
4263
+ * HEADER (REB-21102), so it's regex-bound (not just length-bound like this schema's other passthrough
4264
+ * identifiers): an unfiltered value reaching a header sink can throw a raw `TypeError` (CRLF, or any
4265
+ * non-Latin1 codepoint) that isn't an `UpstreamError`, escaping `fetchDataSourceResults`'s error
4266
+ * handling as an unhandled 500 on this public, unauthenticated route instead of a clean 422 here.
4267
+ * Normalized to uppercase (the wire value is forwarded verbatim as the header) so a case-sensitive
4268
+ * engine lookup can't silently miss a lowercase code — `adsEngine.ts` uppercases for the same reason.
4269
+ */
4270
+ 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(),
4250
4271
  customerId: z28.string().max(REQUEST_BOUNDS.STRING_ID_MAX).optional(),
4251
4272
  dataSourcePath: z28.string().max(REQUEST_BOUNDS.STRING_KEY_MAX),
4252
4273
  /**
@@ -4258,6 +4279,14 @@ var DataSourceInputSchema = z28.object({
4258
4279
  integrations: z28.record(z28.string().max(REQUEST_BOUNDS.STRING_KEY_MAX), z28.boolean()).refine((record) => Object.keys(record).length <= REQUEST_BOUNDS.INTEGRATION_KEYS_MAX, {
4259
4280
  message: `Too many integration keys (max ${REQUEST_BOUNDS.INTEGRATION_KEYS_MAX})`
4260
4281
  }).optional(),
4282
+ /**
4283
+ * Buyer's checkout language (BCP 47) — forwarded to the engine as the `Accept-Language` HEADER
4284
+ * (REB-21102); regex-bound for the same header-sink reason as `country` above. MUST be a single tag
4285
+ * (e.g. `fr-CA`) — a q-value list like `fr-CA,fr;q=0.9` (the raw shape of a browser `Accept-Language`
4286
+ * request header) fails the regex. Callers should send the buyer's resolved locale (e.g. Shopify
4287
+ * checkout's `localization.language.isoCode`), not the request header verbatim.
4288
+ */
4289
+ 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(),
4261
4290
  limit: z28.number().max(REQUEST_BOUNDS.LIMIT_MAX),
4262
4291
  productType: z28.enum(["both", "one-time", "subscription"]).optional(),
4263
4292
  shop: z28.string().max(REQUEST_BOUNDS.STRING_ID_MAX),
@@ -4272,8 +4301,19 @@ var GiftInputSchema = z28.object({
4272
4301
  });
4273
4302
  var GiftValidationInputSchema = z28.object({
4274
4303
  cart: CartInputSchema,
4304
+ /**
4305
+ * Buyer's market country — threaded into the per-widget data-source evaluations (REB-21102), which
4306
+ * forward it to a header sink; regex-bound and uppercase-normalized for the same reasons as
4307
+ * `DataSourceInputSchema`'s `country`.
4308
+ */
4309
+ 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(),
4275
4310
  customerId: z28.string().max(REQUEST_BOUNDS.STRING_ID_MAX).optional(),
4276
4311
  gifts: z28.array(GiftInputSchema).max(REQUEST_BOUNDS.GIFTS_MAX),
4312
+ /**
4313
+ * Buyer's checkout language — threaded into the per-widget data-source evaluations (REB-21102). MUST
4314
+ * be a single BCP-47 tag, for the same reason as `DataSourceInputSchema`'s `language` above.
4315
+ */
4316
+ 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(),
4277
4317
  shop: z28.string().max(REQUEST_BOUNDS.STRING_ID_MAX),
4278
4318
  visitorId: z28.string().max(REQUEST_BOUNDS.STRING_ID_MAX).optional()
4279
4319
  });
@@ -114,6 +114,7 @@ export declare const DataSourceInputSchema: z.ZodObject<{
114
114
  subtotal: z.ZodNumber;
115
115
  }, z.core.$strip>;
116
116
  companyLocationId: z.ZodOptional<z.ZodString>;
117
+ country: z.ZodOptional<z.ZodPipe<z.ZodString, z.ZodTransform<string, string>>>;
117
118
  customerId: z.ZodOptional<z.ZodString>;
118
119
  dataSourcePath: z.ZodString;
119
120
  filterInputs: z.ZodOptional<z.ZodEnum<{
@@ -121,6 +122,7 @@ export declare const DataSourceInputSchema: z.ZodObject<{
121
122
  yes: "yes";
122
123
  }>>;
123
124
  integrations: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodBoolean>>;
125
+ language: z.ZodOptional<z.ZodString>;
124
126
  limit: z.ZodNumber;
125
127
  productType: z.ZodOptional<z.ZodEnum<{
126
128
  "one-time": "one-time";
@@ -160,6 +162,7 @@ export declare const GiftValidationInputSchema: z.ZodObject<{
160
162
  }, z.core.$strip>>;
161
163
  subtotal: z.ZodNumber;
162
164
  }, z.core.$strip>;
165
+ country: z.ZodOptional<z.ZodPipe<z.ZodString, z.ZodTransform<string, string>>>;
163
166
  customerId: z.ZodOptional<z.ZodString>;
164
167
  gifts: z.ZodArray<z.ZodObject<{
165
168
  cost: z.ZodNumber;
@@ -168,6 +171,7 @@ export declare const GiftValidationInputSchema: z.ZodObject<{
168
171
  productId: z.ZodNumber;
169
172
  widgetId: z.ZodNumber;
170
173
  }, z.core.$strip>>;
174
+ language: z.ZodOptional<z.ZodString>;
171
175
  shop: z.ZodString;
172
176
  visitorId: z.ZodOptional<z.ZodString>;
173
177
  }, z.core.$strip>;
@@ -10,8 +10,10 @@ 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. Pass `method: 'POST'` with `body` (JSON
14
- * body) and optional `headers` (e.g. an auth token) for write-style calls like analytics ingest.
13
+ * Defaults to GET with `params`/`search` as the query string; `headers` (e.g. an auth token, or the
14
+ * market-context headers `fetchDataSourceResults` sends, REB-21102) apply on GET too implementations
15
+ * must not gate `headers` behind a `method === 'POST'` branch. Pass `method: 'POST'` with `body` (JSON
16
+ * body) for write-style calls like analytics ingest.
15
17
  */
16
18
  export type UpstreamFetch = (path: string, params: Record<string, string>, options?: {
17
19
  body?: unknown;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rebuy/rebuy",
3
- "version": "3.0.0",
3
+ "version": "3.1.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.",