@rebuy/rebuy 3.23.0 → 3.25.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/README.md +1 -0
- package/dist/cab/asLayout.d.ts +12 -0
- package/dist/cab/contentText.d.ts +8 -0
- package/dist/cab/decoratedOrderId.d.ts +2 -0
- package/dist/cab/gid.d.ts +2 -0
- package/dist/cab/index.cjs +177 -0
- package/dist/cab/index.d.ts +9 -0
- package/dist/cab/index.mjs +154 -0
- package/dist/cab/interpolateAndSanitize.d.ts +8 -0
- package/dist/cab/listOf.d.ts +9 -0
- package/dist/cab/selectLanguage.d.ts +8 -0
- package/dist/cab/tiptapPlain.d.ts +10 -0
- package/dist/cab/tiptapRuns.d.ts +40 -0
- package/dist/cab/toCents.d.ts +2 -0
- package/dist/index.cjs +9 -0
- package/dist/index.mjs +9 -0
- package/dist/schema/checkout-and-beyond/index.cjs +9 -0
- package/dist/schema/checkout-and-beyond/index.mjs +9 -0
- package/dist/schema/widget-data.cjs +6 -0
- package/dist/schema/widget-data.mjs +6 -0
- package/dist/schema/widgets/checkout-and-beyond/monetize.d.ts +15 -0
- package/dist/schema/widgets/checkout-and-beyond/regex.d.ts +23 -1
- package/dist/server/adsEngine.d.ts +10 -1
- package/dist/server/index.cjs +51 -22
- package/dist/server/index.mjs +51 -22
- package/dist/server/monetizeOffers.d.ts +3 -1
- package/dist/server/requestSchemas.d.ts +1 -1
- package/dist/transforms/htmlToTiptap/linkSafety.d.ts +19 -0
- package/dist/transforms/index.cjs +27 -9
- package/dist/transforms/index.mjs +27 -9
- package/package.json +9 -1
|
@@ -1,7 +1,21 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
2
|
import { CABSection, type SectionDetails } from './shared';
|
|
3
3
|
import { CABVariableFields } from './variables';
|
|
4
|
+
/**
|
|
5
|
+
* The ads engine's own verdict on the hydrated request, echoed for the render beacon (REB-24362):
|
|
6
|
+
* `testing` is server-final (the engine can force it, e.g. an armed offers-testing fixture) and
|
|
7
|
+
* `requestId` is its `ad_requests` row. Present only alongside hydrated `offers`.
|
|
8
|
+
*/
|
|
9
|
+
export declare const CABMonetizeAds: z.ZodObject<{
|
|
10
|
+
requestId: z.ZodOptional<z.ZodString>;
|
|
11
|
+
testing: z.ZodDefault<z.ZodBoolean>;
|
|
12
|
+
}, z.core.$strip>;
|
|
13
|
+
export type CABMonetizeAds = z.infer<typeof CABMonetizeAds>;
|
|
4
14
|
export declare const CABMonetizeSection: z.ZodObject<{
|
|
15
|
+
ads: z.ZodOptional<z.ZodObject<{
|
|
16
|
+
requestId: z.ZodOptional<z.ZodString>;
|
|
17
|
+
testing: z.ZodDefault<z.ZodBoolean>;
|
|
18
|
+
}, z.core.$strip>>;
|
|
5
19
|
name: z.ZodOptional<z.ZodString>;
|
|
6
20
|
offers: z.ZodOptional<z.ZodArray<z.ZodLazy<z.ZodType<CABSection, unknown, z.core.$ZodTypeInternals<CABSection, unknown>>>>>;
|
|
7
21
|
privacyPolicyUrl: z.ZodOptional<z.ZodString>;
|
|
@@ -197,6 +211,7 @@ export declare const CABMonetizeSection: z.ZodObject<{
|
|
|
197
211
|
}, z.core.$strip>;
|
|
198
212
|
/** Twin of the schema above — `offers` makes this a recursive node (see recursiveTwins.test.ts). */
|
|
199
213
|
export type CABMonetizeSection = CABVariableFields & {
|
|
214
|
+
ads?: CABMonetizeAds;
|
|
200
215
|
name?: string;
|
|
201
216
|
offers?: CABSection[];
|
|
202
217
|
privacyPolicyUrl?: string;
|
|
@@ -1,7 +1,29 @@
|
|
|
1
1
|
/** Matches a valid array index string (e.g., "0", "1", "42"). */
|
|
2
2
|
export declare const ARRAY_INDEX_STRING: RegExp;
|
|
3
|
-
/**
|
|
3
|
+
/**
|
|
4
|
+
* VALIDATION grammar: whether a whole value IS a dynamic token. Gates what may be STORED (a link href in
|
|
5
|
+
* `text.ts`, an image source in `image.ts`), so widening it is a schema shape change that ripples to every
|
|
6
|
+
* consumer. Deliberately narrow — letters only.
|
|
7
|
+
*/
|
|
4
8
|
export declare const DYNAMIC_TOKEN_REGEX: RegExp;
|
|
9
|
+
/**
|
|
10
|
+
* DETECTION grammar: whether a value still CONTAINS a token after interpolation, i.e. one that failed to
|
|
11
|
+
* bind. Deliberately NOT derived from the validation grammar above — the two pull in opposite directions.
|
|
12
|
+
* Validation must stay narrow to keep stored values predictable; detection must be as wide as what
|
|
13
|
+
* `interpolateAndSanitize` binds in the CANONICAL double-brace spelling, because anything it misses there
|
|
14
|
+
* is a dead link handed to a renderer. Hence `[A-Za-z0-9_]+` on the key: `resolveKey` camelizes legacy
|
|
15
|
+
* snake_case (`{{utm_source}}` binds to `utmSource`), so those spellings have to be detectable too.
|
|
16
|
+
*
|
|
17
|
+
* Still double-brace only, and still no `.`: that is what keeps third-party click macros — Google Ads'
|
|
18
|
+
* `{placement}`, Meta's `{{ad.id}}` — out of it. Nothing binds those names today, and dropping a working
|
|
19
|
+
* ad link is worse than leaving a dead one.
|
|
20
|
+
*
|
|
21
|
+
* BOTH exclusions are deliberate carve-outs rather than faithful widenings. `TOKEN_REGEX` quantifies open
|
|
22
|
+
* and close braces independently, so `{slug}` and `{{slug}` bind yet go undetected; its key class is
|
|
23
|
+
* `[^{}\s]+`, so a dotted name would bind too if anything were ever registered under one. Each keeps a
|
|
24
|
+
* probably-dead link, known and tolerated as the cheaper error.
|
|
25
|
+
*/
|
|
26
|
+
export declare const DYNAMIC_TOKEN_ANYWHERE_REGEX: RegExp;
|
|
5
27
|
/** Matches a valid hex color code. */
|
|
6
28
|
export declare const HEX_COLOR_REGEX: RegExp;
|
|
7
29
|
/**
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
2
|
import { type CabUserConfig } from '../schema/cabShopConfig';
|
|
3
|
+
import { type CABMonetizeAds } from '../schema/widgets/checkout-and-beyond/monetize';
|
|
3
4
|
import { type MonetizeInput, type ServerContext } from '../server/shared';
|
|
4
5
|
/**
|
|
5
6
|
* A single ads-engine offer. The `*_url` fields are tracking endpoints the engine returns fully
|
|
@@ -35,8 +36,16 @@ export declare const isTestingRequest: (input: MonetizeInput, isProduction: bool
|
|
|
35
36
|
* (total preferred over subtotal). Empty values are omitted.
|
|
36
37
|
*/
|
|
37
38
|
export declare const buildAdsParams: (input: MonetizeInput, publisherKey: string, supplyKey: string, testing: boolean, userAgent?: string, ipAddress?: string) => Record<string, string>;
|
|
38
|
-
/**
|
|
39
|
+
/**
|
|
40
|
+
* The engine offers plus the shop's display name (for the header copy), resolved on the same config
|
|
41
|
+
* read, plus `ads` — what the engine decided about the request, for the render beacon to echo verbatim
|
|
42
|
+
* (REB-24362): `testing` is the engine's response flag OR-ed with the flag we sent (never only our own
|
|
43
|
+
* rule, because the engine can force it server-side) and `requestId` is its `ad_requests` row id. Present
|
|
44
|
+
* only once the engine was actually called (absent on every early return, where there is no request to
|
|
45
|
+
* describe).
|
|
46
|
+
*/
|
|
39
47
|
export type AdsEngineResult = {
|
|
48
|
+
ads?: CABMonetizeAds;
|
|
40
49
|
offers: AdsEngineOffer[];
|
|
41
50
|
storeName?: string;
|
|
42
51
|
};
|
package/dist/server/index.cjs
CHANGED
|
@@ -1579,8 +1579,14 @@ var CABDiscountSection = import_zod19.z.object({
|
|
|
1579
1579
|
// src/schema/widgets/checkout-and-beyond/monetize.ts
|
|
1580
1580
|
var import_uuid8 = require("uuid");
|
|
1581
1581
|
var import_zod20 = require("zod");
|
|
1582
|
+
var CABMonetizeAds = import_zod20.z.object({
|
|
1583
|
+
requestId: import_zod20.z.string().optional(),
|
|
1584
|
+
/** Defaulted here, never in the beacon: absence of information is `ads` itself being absent. */
|
|
1585
|
+
testing: import_zod20.z.boolean().default(false)
|
|
1586
|
+
});
|
|
1582
1587
|
var CABMonetizeSection = import_zod20.z.object({
|
|
1583
1588
|
...CABVariableFields,
|
|
1589
|
+
ads: CABMonetizeAds.optional(),
|
|
1584
1590
|
name: import_zod20.z.string().optional(),
|
|
1585
1591
|
/**
|
|
1586
1592
|
* Server-hydrated offer cards (inline hydration): when `/cab/sections` was asked with a `monetize`
|
|
@@ -2453,13 +2459,19 @@ var isSkipping = (stack = []) => stack.some(({ skip }) => skip);
|
|
|
2453
2459
|
var makeDoc = (content, blockSpacing) => TiptapDocument.parse({ attrs: { blockSpacing }, content });
|
|
2454
2460
|
|
|
2455
2461
|
// src/transforms/htmlToTiptap/isValidHref.ts
|
|
2456
|
-
var
|
|
2462
|
+
var RELATIVE_BASE = "https://x.invalid";
|
|
2457
2463
|
var ALLOWED_PROTOCOLS = /* @__PURE__ */ new Set(["http:", "https:", "mailto:", "tel:"]);
|
|
2458
2464
|
var isValidHref = (href) => {
|
|
2459
2465
|
if (!href) return false;
|
|
2460
2466
|
if (href === "#") return true;
|
|
2461
|
-
if (
|
|
2462
|
-
if (href.startsWith("/")
|
|
2467
|
+
if (DYNAMIC_TOKEN_REGEX.test(href)) return true;
|
|
2468
|
+
if (href.startsWith("/")) {
|
|
2469
|
+
try {
|
|
2470
|
+
return new URL(href, RELATIVE_BASE).origin === RELATIVE_BASE;
|
|
2471
|
+
} catch {
|
|
2472
|
+
return false;
|
|
2473
|
+
}
|
|
2474
|
+
}
|
|
2463
2475
|
try {
|
|
2464
2476
|
const url = new URL(href);
|
|
2465
2477
|
return ALLOWED_PROTOCOLS.has(url.protocol);
|
|
@@ -2468,26 +2480,32 @@ var isValidHref = (href) => {
|
|
|
2468
2480
|
}
|
|
2469
2481
|
};
|
|
2470
2482
|
|
|
2471
|
-
// src/transforms/htmlToTiptap/
|
|
2472
|
-
var
|
|
2473
|
-
var
|
|
2483
|
+
// src/transforms/htmlToTiptap/linkSafety.ts
|
|
2484
|
+
var DEFAULT_LINK_TARGET = "_blank";
|
|
2485
|
+
var DEFAULT_LINK_REL = "noopener noreferrer nofollow";
|
|
2474
2486
|
var ensureSafeRel = (rel, target) => {
|
|
2475
|
-
if (target !==
|
|
2487
|
+
if (target !== DEFAULT_LINK_TARGET) return rel;
|
|
2476
2488
|
const sourceTokens = (rel ?? "").split(/\s+/).filter(Boolean);
|
|
2477
|
-
if (sourceTokens.length === 0) return
|
|
2489
|
+
if (sourceTokens.length === 0) return DEFAULT_LINK_REL;
|
|
2478
2490
|
const tokens = new Set(sourceTokens.filter((token) => token.toLowerCase() !== "opener"));
|
|
2479
2491
|
tokens.add("noopener");
|
|
2480
2492
|
tokens.add("noreferrer");
|
|
2481
2493
|
return Array.from(tokens).join(" ");
|
|
2482
2494
|
};
|
|
2495
|
+
|
|
2496
|
+
// src/transforms/htmlToTiptap/buildLinkMark.ts
|
|
2483
2497
|
var buildLinkMark = (attrs = {}) => {
|
|
2484
2498
|
const { href } = attrs;
|
|
2485
2499
|
if (!href || !isValidHref(href)) return null;
|
|
2486
|
-
const target = attrs.target ??
|
|
2500
|
+
const target = attrs.target ?? DEFAULT_LINK_TARGET;
|
|
2487
2501
|
return {
|
|
2488
2502
|
attrs: {
|
|
2489
2503
|
class: null,
|
|
2490
2504
|
href,
|
|
2505
|
+
// `Mark` is the POST-parse shape, where `rel` is always a string. This value is PRE-parse: a
|
|
2506
|
+
// non-`_blank` link whose source omits `rel` deliberately stays undefined so the schema's
|
|
2507
|
+
// `.default()` supplies it. Hence the cast — the old implicit `rel: string` parameter was
|
|
2508
|
+
// making the same claim, just without saying so.
|
|
2491
2509
|
rel: ensureSafeRel(attrs.rel, target),
|
|
2492
2510
|
target
|
|
2493
2511
|
},
|
|
@@ -4278,7 +4296,15 @@ var AdsEngineResponse = import_zod28.z.object({
|
|
|
4278
4296
|
const result = AdsEngineOffer.safeParse(offer);
|
|
4279
4297
|
return result.success ? [result.data] : [];
|
|
4280
4298
|
})
|
|
4281
|
-
)
|
|
4299
|
+
),
|
|
4300
|
+
/**
|
|
4301
|
+
* Server-decided facts about THIS ads request, echoed so the render beacon can copy them (REB-24362).
|
|
4302
|
+
* `testing` is the engine's final verdict — it can be true when the caller sent false (an armed
|
|
4303
|
+
* offers-testing fixture on a test store forces it, REB-24467). Both optional: an older engine or a
|
|
4304
|
+
* non-JSON body must not fail the parse and drop the offers.
|
|
4305
|
+
*/
|
|
4306
|
+
request_id: import_zod28.z.string().optional().catch(void 0),
|
|
4307
|
+
testing: import_zod28.z.boolean().optional().catch(void 0)
|
|
4282
4308
|
});
|
|
4283
4309
|
var adsHost = (host) => `offers.${host}`;
|
|
4284
4310
|
var isTestingRequest = (input, isProduction) => !isProduction || !!input.testing || input.customerEmail?.toLowerCase() === TEST_CUSTOMER_EMAIL;
|
|
@@ -4326,19 +4352,21 @@ var fetchAdsEngineOffers = async (input, ctx, prefetched) => {
|
|
|
4326
4352
|
if (!publisherKey) return { offers: [], storeName };
|
|
4327
4353
|
const isProduction = ctx.host === REBUY_ENGINE_HOST;
|
|
4328
4354
|
const supplyKey = isProduction ? SUPPLY_KEY.production : SUPPLY_KEY.staging;
|
|
4355
|
+
const testing = isTestingRequest(input, isProduction);
|
|
4329
4356
|
const upstream = await ctx.fetchUpstream(
|
|
4330
4357
|
"/api/v1/offers",
|
|
4331
|
-
buildAdsParams(
|
|
4332
|
-
input,
|
|
4333
|
-
publisherKey,
|
|
4334
|
-
supplyKey,
|
|
4335
|
-
isTestingRequest(input, isProduction),
|
|
4336
|
-
ctx.userAgent,
|
|
4337
|
-
ctx.ipAddress
|
|
4338
|
-
),
|
|
4358
|
+
buildAdsParams(input, publisherKey, supplyKey, testing, ctx.userAgent, ctx.ipAddress),
|
|
4339
4359
|
{ host: adsHost(ctx.host) }
|
|
4340
4360
|
);
|
|
4341
|
-
|
|
4361
|
+
const response = AdsEngineResponse.parse(upstream);
|
|
4362
|
+
return {
|
|
4363
|
+
ads: {
|
|
4364
|
+
...response.request_id && { requestId: response.request_id },
|
|
4365
|
+
testing: testing || response.testing === true
|
|
4366
|
+
},
|
|
4367
|
+
offers: response.offers,
|
|
4368
|
+
storeName
|
|
4369
|
+
};
|
|
4342
4370
|
};
|
|
4343
4371
|
|
|
4344
4372
|
// src/transforms/htmlToPlainText.ts
|
|
@@ -4537,7 +4565,7 @@ var composeOfferCard = (offer, input, storeName) => {
|
|
|
4537
4565
|
|
|
4538
4566
|
// src/server/monetizeOffers.ts
|
|
4539
4567
|
var fetchMonetizeOffers = async (input, ctx, prefetched) => {
|
|
4540
|
-
const { offers: engineOffers, storeName } = await fetchAdsEngineOffers(input, ctx, prefetched);
|
|
4568
|
+
const { ads, offers: engineOffers, storeName } = await fetchAdsEngineOffers(input, ctx, prefetched);
|
|
4541
4569
|
const composed = engineOffers.map((offer) => composeOfferCard(offer, input, storeName));
|
|
4542
4570
|
const offers = composed.filter((card) => card !== void 0);
|
|
4543
4571
|
if (offers.length < composed.length)
|
|
@@ -4545,7 +4573,7 @@ var fetchMonetizeOffers = async (input, ctx, prefetched) => {
|
|
|
4545
4573
|
detail: `${composed.length - offers.length} monetize offer(s) failed to compose`,
|
|
4546
4574
|
kind: "monetizeOffer"
|
|
4547
4575
|
});
|
|
4548
|
-
return { offers };
|
|
4576
|
+
return { ...ads && { ads }, offers };
|
|
4549
4577
|
};
|
|
4550
4578
|
|
|
4551
4579
|
// src/server/salvageSections.ts
|
|
@@ -4639,7 +4667,7 @@ var hydrateMonetizeMarkers = async (converted, input, ctx, config) => {
|
|
|
4639
4667
|
const first = findMonetizeMarker(converted);
|
|
4640
4668
|
if (!first) return;
|
|
4641
4669
|
try {
|
|
4642
|
-
const { offers } = await fetchMonetizeOffers(
|
|
4670
|
+
const { ads, offers } = await fetchMonetizeOffers(
|
|
4643
4671
|
{
|
|
4644
4672
|
...input.monetize,
|
|
4645
4673
|
privacyPolicyUrl: first.privacyPolicyUrl,
|
|
@@ -4650,6 +4678,7 @@ var hydrateMonetizeMarkers = async (converted, input, ctx, config) => {
|
|
|
4650
4678
|
config ?? void 0
|
|
4651
4679
|
);
|
|
4652
4680
|
first.offers = offers;
|
|
4681
|
+
if (ads) first.ads = ads;
|
|
4653
4682
|
} catch (err) {
|
|
4654
4683
|
ctx.reportDrift?.({
|
|
4655
4684
|
detail: `inline hydration failed: ${err instanceof Error ? err.message : String(err)}`,
|
package/dist/server/index.mjs
CHANGED
|
@@ -1528,8 +1528,14 @@ var CABDiscountSection = z19.object({
|
|
|
1528
1528
|
// src/schema/widgets/checkout-and-beyond/monetize.ts
|
|
1529
1529
|
import { v7 as uuidv78 } from "uuid";
|
|
1530
1530
|
import { z as z20 } from "zod";
|
|
1531
|
+
var CABMonetizeAds = z20.object({
|
|
1532
|
+
requestId: z20.string().optional(),
|
|
1533
|
+
/** Defaulted here, never in the beacon: absence of information is `ads` itself being absent. */
|
|
1534
|
+
testing: z20.boolean().default(false)
|
|
1535
|
+
});
|
|
1531
1536
|
var CABMonetizeSection = z20.object({
|
|
1532
1537
|
...CABVariableFields,
|
|
1538
|
+
ads: CABMonetizeAds.optional(),
|
|
1533
1539
|
name: z20.string().optional(),
|
|
1534
1540
|
/**
|
|
1535
1541
|
* Server-hydrated offer cards (inline hydration): when `/cab/sections` was asked with a `monetize`
|
|
@@ -2402,13 +2408,19 @@ var isSkipping = (stack = []) => stack.some(({ skip }) => skip);
|
|
|
2402
2408
|
var makeDoc = (content, blockSpacing) => TiptapDocument.parse({ attrs: { blockSpacing }, content });
|
|
2403
2409
|
|
|
2404
2410
|
// src/transforms/htmlToTiptap/isValidHref.ts
|
|
2405
|
-
var
|
|
2411
|
+
var RELATIVE_BASE = "https://x.invalid";
|
|
2406
2412
|
var ALLOWED_PROTOCOLS = /* @__PURE__ */ new Set(["http:", "https:", "mailto:", "tel:"]);
|
|
2407
2413
|
var isValidHref = (href) => {
|
|
2408
2414
|
if (!href) return false;
|
|
2409
2415
|
if (href === "#") return true;
|
|
2410
|
-
if (
|
|
2411
|
-
if (href.startsWith("/")
|
|
2416
|
+
if (DYNAMIC_TOKEN_REGEX.test(href)) return true;
|
|
2417
|
+
if (href.startsWith("/")) {
|
|
2418
|
+
try {
|
|
2419
|
+
return new URL(href, RELATIVE_BASE).origin === RELATIVE_BASE;
|
|
2420
|
+
} catch {
|
|
2421
|
+
return false;
|
|
2422
|
+
}
|
|
2423
|
+
}
|
|
2412
2424
|
try {
|
|
2413
2425
|
const url = new URL(href);
|
|
2414
2426
|
return ALLOWED_PROTOCOLS.has(url.protocol);
|
|
@@ -2417,26 +2429,32 @@ var isValidHref = (href) => {
|
|
|
2417
2429
|
}
|
|
2418
2430
|
};
|
|
2419
2431
|
|
|
2420
|
-
// src/transforms/htmlToTiptap/
|
|
2421
|
-
var
|
|
2422
|
-
var
|
|
2432
|
+
// src/transforms/htmlToTiptap/linkSafety.ts
|
|
2433
|
+
var DEFAULT_LINK_TARGET = "_blank";
|
|
2434
|
+
var DEFAULT_LINK_REL = "noopener noreferrer nofollow";
|
|
2423
2435
|
var ensureSafeRel = (rel, target) => {
|
|
2424
|
-
if (target !==
|
|
2436
|
+
if (target !== DEFAULT_LINK_TARGET) return rel;
|
|
2425
2437
|
const sourceTokens = (rel ?? "").split(/\s+/).filter(Boolean);
|
|
2426
|
-
if (sourceTokens.length === 0) return
|
|
2438
|
+
if (sourceTokens.length === 0) return DEFAULT_LINK_REL;
|
|
2427
2439
|
const tokens = new Set(sourceTokens.filter((token) => token.toLowerCase() !== "opener"));
|
|
2428
2440
|
tokens.add("noopener");
|
|
2429
2441
|
tokens.add("noreferrer");
|
|
2430
2442
|
return Array.from(tokens).join(" ");
|
|
2431
2443
|
};
|
|
2444
|
+
|
|
2445
|
+
// src/transforms/htmlToTiptap/buildLinkMark.ts
|
|
2432
2446
|
var buildLinkMark = (attrs = {}) => {
|
|
2433
2447
|
const { href } = attrs;
|
|
2434
2448
|
if (!href || !isValidHref(href)) return null;
|
|
2435
|
-
const target = attrs.target ??
|
|
2449
|
+
const target = attrs.target ?? DEFAULT_LINK_TARGET;
|
|
2436
2450
|
return {
|
|
2437
2451
|
attrs: {
|
|
2438
2452
|
class: null,
|
|
2439
2453
|
href,
|
|
2454
|
+
// `Mark` is the POST-parse shape, where `rel` is always a string. This value is PRE-parse: a
|
|
2455
|
+
// non-`_blank` link whose source omits `rel` deliberately stays undefined so the schema's
|
|
2456
|
+
// `.default()` supplies it. Hence the cast — the old implicit `rel: string` parameter was
|
|
2457
|
+
// making the same claim, just without saying so.
|
|
2440
2458
|
rel: ensureSafeRel(attrs.rel, target),
|
|
2441
2459
|
target
|
|
2442
2460
|
},
|
|
@@ -4227,7 +4245,15 @@ var AdsEngineResponse = z28.object({
|
|
|
4227
4245
|
const result = AdsEngineOffer.safeParse(offer);
|
|
4228
4246
|
return result.success ? [result.data] : [];
|
|
4229
4247
|
})
|
|
4230
|
-
)
|
|
4248
|
+
),
|
|
4249
|
+
/**
|
|
4250
|
+
* Server-decided facts about THIS ads request, echoed so the render beacon can copy them (REB-24362).
|
|
4251
|
+
* `testing` is the engine's final verdict — it can be true when the caller sent false (an armed
|
|
4252
|
+
* offers-testing fixture on a test store forces it, REB-24467). Both optional: an older engine or a
|
|
4253
|
+
* non-JSON body must not fail the parse and drop the offers.
|
|
4254
|
+
*/
|
|
4255
|
+
request_id: z28.string().optional().catch(void 0),
|
|
4256
|
+
testing: z28.boolean().optional().catch(void 0)
|
|
4231
4257
|
});
|
|
4232
4258
|
var adsHost = (host) => `offers.${host}`;
|
|
4233
4259
|
var isTestingRequest = (input, isProduction) => !isProduction || !!input.testing || input.customerEmail?.toLowerCase() === TEST_CUSTOMER_EMAIL;
|
|
@@ -4275,19 +4301,21 @@ var fetchAdsEngineOffers = async (input, ctx, prefetched) => {
|
|
|
4275
4301
|
if (!publisherKey) return { offers: [], storeName };
|
|
4276
4302
|
const isProduction = ctx.host === REBUY_ENGINE_HOST;
|
|
4277
4303
|
const supplyKey = isProduction ? SUPPLY_KEY.production : SUPPLY_KEY.staging;
|
|
4304
|
+
const testing = isTestingRequest(input, isProduction);
|
|
4278
4305
|
const upstream = await ctx.fetchUpstream(
|
|
4279
4306
|
"/api/v1/offers",
|
|
4280
|
-
buildAdsParams(
|
|
4281
|
-
input,
|
|
4282
|
-
publisherKey,
|
|
4283
|
-
supplyKey,
|
|
4284
|
-
isTestingRequest(input, isProduction),
|
|
4285
|
-
ctx.userAgent,
|
|
4286
|
-
ctx.ipAddress
|
|
4287
|
-
),
|
|
4307
|
+
buildAdsParams(input, publisherKey, supplyKey, testing, ctx.userAgent, ctx.ipAddress),
|
|
4288
4308
|
{ host: adsHost(ctx.host) }
|
|
4289
4309
|
);
|
|
4290
|
-
|
|
4310
|
+
const response = AdsEngineResponse.parse(upstream);
|
|
4311
|
+
return {
|
|
4312
|
+
ads: {
|
|
4313
|
+
...response.request_id && { requestId: response.request_id },
|
|
4314
|
+
testing: testing || response.testing === true
|
|
4315
|
+
},
|
|
4316
|
+
offers: response.offers,
|
|
4317
|
+
storeName
|
|
4318
|
+
};
|
|
4291
4319
|
};
|
|
4292
4320
|
|
|
4293
4321
|
// src/transforms/htmlToPlainText.ts
|
|
@@ -4486,7 +4514,7 @@ var composeOfferCard = (offer, input, storeName) => {
|
|
|
4486
4514
|
|
|
4487
4515
|
// src/server/monetizeOffers.ts
|
|
4488
4516
|
var fetchMonetizeOffers = async (input, ctx, prefetched) => {
|
|
4489
|
-
const { offers: engineOffers, storeName } = await fetchAdsEngineOffers(input, ctx, prefetched);
|
|
4517
|
+
const { ads, offers: engineOffers, storeName } = await fetchAdsEngineOffers(input, ctx, prefetched);
|
|
4490
4518
|
const composed = engineOffers.map((offer) => composeOfferCard(offer, input, storeName));
|
|
4491
4519
|
const offers = composed.filter((card) => card !== void 0);
|
|
4492
4520
|
if (offers.length < composed.length)
|
|
@@ -4494,7 +4522,7 @@ var fetchMonetizeOffers = async (input, ctx, prefetched) => {
|
|
|
4494
4522
|
detail: `${composed.length - offers.length} monetize offer(s) failed to compose`,
|
|
4495
4523
|
kind: "monetizeOffer"
|
|
4496
4524
|
});
|
|
4497
|
-
return { offers };
|
|
4525
|
+
return { ...ads && { ads }, offers };
|
|
4498
4526
|
};
|
|
4499
4527
|
|
|
4500
4528
|
// src/server/salvageSections.ts
|
|
@@ -4588,7 +4616,7 @@ var hydrateMonetizeMarkers = async (converted, input, ctx, config) => {
|
|
|
4588
4616
|
const first = findMonetizeMarker(converted);
|
|
4589
4617
|
if (!first) return;
|
|
4590
4618
|
try {
|
|
4591
|
-
const { offers } = await fetchMonetizeOffers(
|
|
4619
|
+
const { ads, offers } = await fetchMonetizeOffers(
|
|
4592
4620
|
{
|
|
4593
4621
|
...input.monetize,
|
|
4594
4622
|
privacyPolicyUrl: first.privacyPolicyUrl,
|
|
@@ -4599,6 +4627,7 @@ var hydrateMonetizeMarkers = async (converted, input, ctx, config) => {
|
|
|
4599
4627
|
config ?? void 0
|
|
4600
4628
|
);
|
|
4601
4629
|
first.offers = offers;
|
|
4630
|
+
if (ads) first.ads = ads;
|
|
4602
4631
|
} catch (err) {
|
|
4603
4632
|
ctx.reportDrift?.({
|
|
4604
4633
|
detail: `inline hydration failed: ${err instanceof Error ? err.message : String(err)}`,
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import { type CabUserConfig } from '../schema/cabShopConfig';
|
|
2
|
-
import { type CABSection } from '../schema/widgets/checkout-and-beyond';
|
|
2
|
+
import { type CABMonetizeAds, type CABSection } from '../schema/widgets/checkout-and-beyond';
|
|
3
3
|
import { type MonetizeInput, type ServerContext } from '../server/shared';
|
|
4
|
+
/** Cards plus the engine's own verdict on the request (`ads`), absent when the engine was never called. */
|
|
4
5
|
export type MonetizeOffers = {
|
|
6
|
+
ads?: CABMonetizeAds;
|
|
5
7
|
offers: CABSection[];
|
|
6
8
|
};
|
|
7
9
|
/**
|
|
@@ -290,6 +290,7 @@ export declare const WidgetSettingsInputSchema: z.ZodObject<{
|
|
|
290
290
|
}, z.core.$strip>>;
|
|
291
291
|
subtotal: z.ZodNumber;
|
|
292
292
|
}, z.core.$strip>;
|
|
293
|
+
testing: z.ZodOptional<z.ZodBoolean>;
|
|
293
294
|
country: z.ZodOptional<z.ZodString>;
|
|
294
295
|
customerId: z.ZodOptional<z.ZodString>;
|
|
295
296
|
visitorId: z.ZodOptional<z.ZodString>;
|
|
@@ -304,7 +305,6 @@ export declare const WidgetSettingsInputSchema: z.ZodObject<{
|
|
|
304
305
|
thank_you_page: "thank_you_page";
|
|
305
306
|
}>;
|
|
306
307
|
sessionId: z.ZodOptional<z.ZodString>;
|
|
307
|
-
testing: z.ZodOptional<z.ZodBoolean>;
|
|
308
308
|
timezone: z.ZodOptional<z.ZodString>;
|
|
309
309
|
transactionId: z.ZodOptional<z.ZodString>;
|
|
310
310
|
transactionItems: z.ZodOptional<z.ZodArray<z.ZodObject<{
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The schema defaults for an `<a>` mark when a payload omits them. Match `TiptapText` in
|
|
3
|
+
* `~/schema/widgets/checkout-and-beyond/text` so anything resolving a link BEFORE (or without) a parse
|
|
4
|
+
* lands on the same effective values the downstream parse would have used.
|
|
5
|
+
*/
|
|
6
|
+
export declare const DEFAULT_LINK_TARGET = "_blank";
|
|
7
|
+
export declare const DEFAULT_LINK_REL = "noopener noreferrer nofollow";
|
|
8
|
+
/**
|
|
9
|
+
* Forces `noopener noreferrer` on every `_blank` link to defend against reverse-tabnabbing
|
|
10
|
+
* (window.opener attacks). When the source omits `rel` entirely, fall back to the schema default
|
|
11
|
+
* (`noopener noreferrer nofollow`). When source `rel` is non-empty, preserve the existing tokens, drop
|
|
12
|
+
* `opener`, and ensure the safe pair is present.
|
|
13
|
+
*
|
|
14
|
+
* Non-`_blank` targets pass through UNCHANGED — they can't be tabnabbed. That includes passing `undefined`
|
|
15
|
+
* straight back: `TiptapText`'s `rel: z.string().default('noopener noreferrer nofollow')` fires on
|
|
16
|
+
* `undefined` only, so substituting `''` here would silently strip `nofollow` off every `_self` link at
|
|
17
|
+
* parse time. The absent value is the signal that the schema default should apply.
|
|
18
|
+
*/
|
|
19
|
+
export declare const ensureSafeRel: (rel: string | undefined, target: string) => string | undefined;
|
|
@@ -963,8 +963,14 @@ var CABDiscountSection = import_zod13.z.object({
|
|
|
963
963
|
// src/schema/widgets/checkout-and-beyond/monetize.ts
|
|
964
964
|
var import_uuid8 = require("uuid");
|
|
965
965
|
var import_zod14 = require("zod");
|
|
966
|
+
var CABMonetizeAds = import_zod14.z.object({
|
|
967
|
+
requestId: import_zod14.z.string().optional(),
|
|
968
|
+
/** Defaulted here, never in the beacon: absence of information is `ads` itself being absent. */
|
|
969
|
+
testing: import_zod14.z.boolean().default(false)
|
|
970
|
+
});
|
|
966
971
|
var CABMonetizeSection = import_zod14.z.object({
|
|
967
972
|
...CABVariableFields,
|
|
973
|
+
ads: CABMonetizeAds.optional(),
|
|
968
974
|
name: import_zod14.z.string().optional(),
|
|
969
975
|
/**
|
|
970
976
|
* Server-hydrated offer cards (inline hydration): when `/cab/sections` was asked with a `monetize`
|
|
@@ -1837,13 +1843,19 @@ var isSkipping = (stack = []) => stack.some(({ skip }) => skip);
|
|
|
1837
1843
|
var makeDoc = (content, blockSpacing) => TiptapDocument.parse({ attrs: { blockSpacing }, content });
|
|
1838
1844
|
|
|
1839
1845
|
// src/transforms/htmlToTiptap/isValidHref.ts
|
|
1840
|
-
var
|
|
1846
|
+
var RELATIVE_BASE = "https://x.invalid";
|
|
1841
1847
|
var ALLOWED_PROTOCOLS = /* @__PURE__ */ new Set(["http:", "https:", "mailto:", "tel:"]);
|
|
1842
1848
|
var isValidHref = (href) => {
|
|
1843
1849
|
if (!href) return false;
|
|
1844
1850
|
if (href === "#") return true;
|
|
1845
|
-
if (
|
|
1846
|
-
if (href.startsWith("/")
|
|
1851
|
+
if (DYNAMIC_TOKEN_REGEX.test(href)) return true;
|
|
1852
|
+
if (href.startsWith("/")) {
|
|
1853
|
+
try {
|
|
1854
|
+
return new URL(href, RELATIVE_BASE).origin === RELATIVE_BASE;
|
|
1855
|
+
} catch {
|
|
1856
|
+
return false;
|
|
1857
|
+
}
|
|
1858
|
+
}
|
|
1847
1859
|
try {
|
|
1848
1860
|
const url = new URL(href);
|
|
1849
1861
|
return ALLOWED_PROTOCOLS.has(url.protocol);
|
|
@@ -1852,26 +1864,32 @@ var isValidHref = (href) => {
|
|
|
1852
1864
|
}
|
|
1853
1865
|
};
|
|
1854
1866
|
|
|
1855
|
-
// src/transforms/htmlToTiptap/
|
|
1856
|
-
var
|
|
1857
|
-
var
|
|
1867
|
+
// src/transforms/htmlToTiptap/linkSafety.ts
|
|
1868
|
+
var DEFAULT_LINK_TARGET = "_blank";
|
|
1869
|
+
var DEFAULT_LINK_REL = "noopener noreferrer nofollow";
|
|
1858
1870
|
var ensureSafeRel = (rel, target) => {
|
|
1859
|
-
if (target !==
|
|
1871
|
+
if (target !== DEFAULT_LINK_TARGET) return rel;
|
|
1860
1872
|
const sourceTokens = (rel ?? "").split(/\s+/).filter(Boolean);
|
|
1861
|
-
if (sourceTokens.length === 0) return
|
|
1873
|
+
if (sourceTokens.length === 0) return DEFAULT_LINK_REL;
|
|
1862
1874
|
const tokens = new Set(sourceTokens.filter((token) => token.toLowerCase() !== "opener"));
|
|
1863
1875
|
tokens.add("noopener");
|
|
1864
1876
|
tokens.add("noreferrer");
|
|
1865
1877
|
return Array.from(tokens).join(" ");
|
|
1866
1878
|
};
|
|
1879
|
+
|
|
1880
|
+
// src/transforms/htmlToTiptap/buildLinkMark.ts
|
|
1867
1881
|
var buildLinkMark = (attrs = {}) => {
|
|
1868
1882
|
const { href } = attrs;
|
|
1869
1883
|
if (!href || !isValidHref(href)) return null;
|
|
1870
|
-
const target = attrs.target ??
|
|
1884
|
+
const target = attrs.target ?? DEFAULT_LINK_TARGET;
|
|
1871
1885
|
return {
|
|
1872
1886
|
attrs: {
|
|
1873
1887
|
class: null,
|
|
1874
1888
|
href,
|
|
1889
|
+
// `Mark` is the POST-parse shape, where `rel` is always a string. This value is PRE-parse: a
|
|
1890
|
+
// non-`_blank` link whose source omits `rel` deliberately stays undefined so the schema's
|
|
1891
|
+
// `.default()` supplies it. Hence the cast — the old implicit `rel: string` parameter was
|
|
1892
|
+
// making the same claim, just without saying so.
|
|
1875
1893
|
rel: ensureSafeRel(attrs.rel, target),
|
|
1876
1894
|
target
|
|
1877
1895
|
},
|
|
@@ -904,8 +904,14 @@ var CABDiscountSection = z13.object({
|
|
|
904
904
|
// src/schema/widgets/checkout-and-beyond/monetize.ts
|
|
905
905
|
import { v7 as uuidv78 } from "uuid";
|
|
906
906
|
import { z as z14 } from "zod";
|
|
907
|
+
var CABMonetizeAds = z14.object({
|
|
908
|
+
requestId: z14.string().optional(),
|
|
909
|
+
/** Defaulted here, never in the beacon: absence of information is `ads` itself being absent. */
|
|
910
|
+
testing: z14.boolean().default(false)
|
|
911
|
+
});
|
|
907
912
|
var CABMonetizeSection = z14.object({
|
|
908
913
|
...CABVariableFields,
|
|
914
|
+
ads: CABMonetizeAds.optional(),
|
|
909
915
|
name: z14.string().optional(),
|
|
910
916
|
/**
|
|
911
917
|
* Server-hydrated offer cards (inline hydration): when `/cab/sections` was asked with a `monetize`
|
|
@@ -1778,13 +1784,19 @@ var isSkipping = (stack = []) => stack.some(({ skip }) => skip);
|
|
|
1778
1784
|
var makeDoc = (content, blockSpacing) => TiptapDocument.parse({ attrs: { blockSpacing }, content });
|
|
1779
1785
|
|
|
1780
1786
|
// src/transforms/htmlToTiptap/isValidHref.ts
|
|
1781
|
-
var
|
|
1787
|
+
var RELATIVE_BASE = "https://x.invalid";
|
|
1782
1788
|
var ALLOWED_PROTOCOLS = /* @__PURE__ */ new Set(["http:", "https:", "mailto:", "tel:"]);
|
|
1783
1789
|
var isValidHref = (href) => {
|
|
1784
1790
|
if (!href) return false;
|
|
1785
1791
|
if (href === "#") return true;
|
|
1786
|
-
if (
|
|
1787
|
-
if (href.startsWith("/")
|
|
1792
|
+
if (DYNAMIC_TOKEN_REGEX.test(href)) return true;
|
|
1793
|
+
if (href.startsWith("/")) {
|
|
1794
|
+
try {
|
|
1795
|
+
return new URL(href, RELATIVE_BASE).origin === RELATIVE_BASE;
|
|
1796
|
+
} catch {
|
|
1797
|
+
return false;
|
|
1798
|
+
}
|
|
1799
|
+
}
|
|
1788
1800
|
try {
|
|
1789
1801
|
const url = new URL(href);
|
|
1790
1802
|
return ALLOWED_PROTOCOLS.has(url.protocol);
|
|
@@ -1793,26 +1805,32 @@ var isValidHref = (href) => {
|
|
|
1793
1805
|
}
|
|
1794
1806
|
};
|
|
1795
1807
|
|
|
1796
|
-
// src/transforms/htmlToTiptap/
|
|
1797
|
-
var
|
|
1798
|
-
var
|
|
1808
|
+
// src/transforms/htmlToTiptap/linkSafety.ts
|
|
1809
|
+
var DEFAULT_LINK_TARGET = "_blank";
|
|
1810
|
+
var DEFAULT_LINK_REL = "noopener noreferrer nofollow";
|
|
1799
1811
|
var ensureSafeRel = (rel, target) => {
|
|
1800
|
-
if (target !==
|
|
1812
|
+
if (target !== DEFAULT_LINK_TARGET) return rel;
|
|
1801
1813
|
const sourceTokens = (rel ?? "").split(/\s+/).filter(Boolean);
|
|
1802
|
-
if (sourceTokens.length === 0) return
|
|
1814
|
+
if (sourceTokens.length === 0) return DEFAULT_LINK_REL;
|
|
1803
1815
|
const tokens = new Set(sourceTokens.filter((token) => token.toLowerCase() !== "opener"));
|
|
1804
1816
|
tokens.add("noopener");
|
|
1805
1817
|
tokens.add("noreferrer");
|
|
1806
1818
|
return Array.from(tokens).join(" ");
|
|
1807
1819
|
};
|
|
1820
|
+
|
|
1821
|
+
// src/transforms/htmlToTiptap/buildLinkMark.ts
|
|
1808
1822
|
var buildLinkMark = (attrs = {}) => {
|
|
1809
1823
|
const { href } = attrs;
|
|
1810
1824
|
if (!href || !isValidHref(href)) return null;
|
|
1811
|
-
const target = attrs.target ??
|
|
1825
|
+
const target = attrs.target ?? DEFAULT_LINK_TARGET;
|
|
1812
1826
|
return {
|
|
1813
1827
|
attrs: {
|
|
1814
1828
|
class: null,
|
|
1815
1829
|
href,
|
|
1830
|
+
// `Mark` is the POST-parse shape, where `rel` is always a string. This value is PRE-parse: a
|
|
1831
|
+
// non-`_blank` link whose source omits `rel` deliberately stays undefined so the schema's
|
|
1832
|
+
// `.default()` supplies it. Hence the cast — the old implicit `rel: string` parameter was
|
|
1833
|
+
// making the same claim, just without saying so.
|
|
1816
1834
|
rel: ensureSafeRel(attrs.rel, target),
|
|
1817
1835
|
target
|
|
1818
1836
|
},
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rebuy/rebuy",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.25.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.",
|
|
@@ -12,6 +12,11 @@
|
|
|
12
12
|
"import": "./dist/index.mjs",
|
|
13
13
|
"require": "./dist/index.cjs"
|
|
14
14
|
},
|
|
15
|
+
"./cab": {
|
|
16
|
+
"types": "./dist/cab/index.d.ts",
|
|
17
|
+
"import": "./dist/cab/index.mjs",
|
|
18
|
+
"require": "./dist/cab/index.cjs"
|
|
19
|
+
},
|
|
15
20
|
"./client": {
|
|
16
21
|
"types": "./dist/client.d.ts",
|
|
17
22
|
"import": "./dist/client.mjs",
|
|
@@ -79,6 +84,9 @@
|
|
|
79
84
|
"types": "./dist/index.d.ts",
|
|
80
85
|
"typesVersions": {
|
|
81
86
|
"*": {
|
|
87
|
+
"cab": [
|
|
88
|
+
"./dist/cab/index.d.ts"
|
|
89
|
+
],
|
|
82
90
|
"client": [
|
|
83
91
|
"./dist/client.d.ts"
|
|
84
92
|
],
|