@burdenoff/website-sdk 2026.1027.1 → 2026.1029.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.
Files changed (60) hide show
  1. package/README.md +186 -1
  2. package/dist/avatar-live/index.d.mts +11 -0
  3. package/dist/avatar-live/index.d.ts +11 -0
  4. package/dist/avatar-live/index.js +120 -0
  5. package/dist/avatar-live/index.js.map +1 -0
  6. package/dist/avatar-live/index.mjs +118 -0
  7. package/dist/avatar-live/index.mjs.map +1 -0
  8. package/dist/avatar-live-view/index.d.mts +42 -0
  9. package/dist/avatar-live-view/index.d.ts +42 -0
  10. package/dist/avatar-live-view/index.js +1422 -0
  11. package/dist/avatar-live-view/index.js.map +1 -0
  12. package/dist/avatar-live-view/index.mjs +1420 -0
  13. package/dist/avatar-live-view/index.mjs.map +1 -0
  14. package/dist/comparison/index.d.mts +83 -0
  15. package/dist/comparison/index.d.ts +83 -0
  16. package/dist/comparison/index.js +116 -0
  17. package/dist/comparison/index.js.map +1 -0
  18. package/dist/comparison/index.mjs +109 -0
  19. package/dist/comparison/index.mjs.map +1 -0
  20. package/dist/components/explore.d.mts +2 -2
  21. package/dist/components/explore.d.ts +2 -2
  22. package/dist/components/explore.js +11 -4
  23. package/dist/components/explore.js.map +1 -1
  24. package/dist/components/explore.mjs +11 -4
  25. package/dist/components/explore.mjs.map +1 -1
  26. package/dist/components/partners.js.map +1 -1
  27. package/dist/components/partners.mjs.map +1 -1
  28. package/dist/content/index.d.mts +1 -1
  29. package/dist/content/index.d.ts +1 -1
  30. package/dist/content/index.js +32 -4
  31. package/dist/content/index.js.map +1 -1
  32. package/dist/content/index.mjs +32 -5
  33. package/dist/content/index.mjs.map +1 -1
  34. package/dist/{explore-BEgq1_0O.d.mts → explore-DUTNYUOm.d.mts} +1 -1
  35. package/dist/{explore-CHiP9u-4.d.ts → explore-DjQE7Xia.d.ts} +1 -1
  36. package/dist/explore-avatar-live-config-B171o1Yo.d.ts +283 -0
  37. package/dist/explore-avatar-live-config-BGsGwbcq.d.mts +283 -0
  38. package/dist/{explore-types-_9UJu6EA.d.mts → explore-types-Bb2Ih3d8.d.mts} +1 -1
  39. package/dist/{explore-types-_9UJu6EA.d.ts → explore-types-Bb2Ih3d8.d.ts} +1 -1
  40. package/dist/hooks/index.d.mts +3 -2
  41. package/dist/hooks/index.d.ts +3 -2
  42. package/dist/hooks/index.js +6 -1
  43. package/dist/hooks/index.js.map +1 -1
  44. package/dist/hooks/index.mjs +6 -1
  45. package/dist/hooks/index.mjs.map +1 -1
  46. package/dist/{index-CB0Uw1xD.d.ts → index-87BLB-Hj.d.ts} +26 -1
  47. package/dist/{index-Ctws7kGs.d.mts → index-Chw0pDXS.d.mts} +26 -1
  48. package/dist/index.d.mts +19 -255
  49. package/dist/index.d.ts +19 -255
  50. package/dist/index.js +358 -40
  51. package/dist/index.js.map +1 -1
  52. package/dist/index.mjs +352 -41
  53. package/dist/index.mjs.map +1 -1
  54. package/dist/types-CndbmBK-.d.mts +68 -0
  55. package/dist/types-CndbmBK-.d.ts +68 -0
  56. package/dist/{use-progressive-reveal-C0hkdu_i.d.ts → use-explore-chat-DunjG-3p.d.ts} +10 -36
  57. package/dist/{use-progressive-reveal-BFPWbEXy.d.mts → use-explore-chat-zRuoAin6.d.mts} +10 -36
  58. package/dist/use-progressive-reveal-ppicEgzZ.d.mts +35 -0
  59. package/dist/use-progressive-reveal-ppicEgzZ.d.ts +35 -0
  60. package/package.json +20 -3
package/dist/index.js CHANGED
@@ -6468,6 +6468,11 @@ function ConceptDetail({
6468
6468
  );
6469
6469
  }
6470
6470
 
6471
+ // src/components/explore-fill.ts
6472
+ function fill(template, values) {
6473
+ return template.replace(/\{(\w+)\}/g, (match, key) => values[key] ?? match);
6474
+ }
6475
+
6471
6476
  // src/components/explore-i18n.ts
6472
6477
  var EXPLORE_LOCALES = ["en", "zh", "hi", "es", "ar", "ta"];
6473
6478
  var RTL_LOCALES = /* @__PURE__ */ new Set(["ar"]);
@@ -7255,9 +7260,6 @@ function resolveExploreLocale(explicit) {
7255
7260
  function exploreStrings(locale) {
7256
7261
  return STRINGS[locale] ?? EN;
7257
7262
  }
7258
- function fill(template, values) {
7259
- return template.replace(/\{(\w+)\}/g, (match, key) => values[key] ?? match);
7260
- }
7261
7263
 
7262
7264
  // src/components/answers-i18n.ts
7263
7265
  var EN2 = {
@@ -11031,6 +11033,10 @@ function useExploreChat(options = {}) {
11031
11033
  [client, productId]
11032
11034
  );
11033
11035
  const canEscalate = hasConversation && !isSharedView;
11036
+ const getConversationToken = React.useCallback(
11037
+ () => conversationTokenRef.current,
11038
+ []
11039
+ );
11034
11040
  return {
11035
11041
  phase,
11036
11042
  catalog,
@@ -11055,7 +11061,8 @@ function useExploreChat(options = {}) {
11055
11061
  escalate,
11056
11062
  canEscalate,
11057
11063
  createShareLink,
11058
- isSharedView
11064
+ isSharedView,
11065
+ getConversationToken
11059
11066
  };
11060
11067
  }
11061
11068
  var optimisticCounter = 0;
@@ -15162,6 +15169,164 @@ var ExploreCta = React.forwardRef(
15162
15169
  );
15163
15170
  }
15164
15171
  );
15172
+
15173
+ // src/components/explore-avatar-live-config.ts
15174
+ var AVATAR_WEB_API_PATH = "/api/web-avatar/v1";
15175
+ var SITE = /^[a-z][a-z0-9-]{1,31}$/;
15176
+ var VERSION = /^[A-Za-z0-9._-]{1,32}$/;
15177
+ var SITE_KEY = /^[A-Za-z0-9_-]{1,128}$/;
15178
+ var LOOPBACK_HOSTS2 = /* @__PURE__ */ new Set(["localhost", "127.0.0.1", "[::1]"]);
15179
+ function isAvatarSite(value) {
15180
+ return SITE.test(value);
15181
+ }
15182
+ function isAvatarVersion(value) {
15183
+ return VERSION.test(value);
15184
+ }
15185
+ function avatarApiBase(gatewayUrl) {
15186
+ if (!gatewayUrl) return null;
15187
+ let url;
15188
+ try {
15189
+ url = new URL(gatewayUrl);
15190
+ } catch {
15191
+ return null;
15192
+ }
15193
+ const local = url.protocol === "http:" && LOOPBACK_HOSTS2.has(url.hostname.toLowerCase());
15194
+ if (url.protocol !== "https:" && !local) return null;
15195
+ return `${url.origin}${AVATAR_WEB_API_PATH}`;
15196
+ }
15197
+ function liveConfigComplete(live) {
15198
+ return typeof live.load === "function" && typeof live.site === "string" && isAvatarSite(live.site) && typeof live.manifestVersion === "string" && isAvatarVersion(live.manifestVersion) && typeof live.turnstileSiteKey === "string" && SITE_KEY.test(live.turnstileSiteKey);
15199
+ }
15200
+ var REVIEWED = /* @__PURE__ */ new Set(["en"]);
15201
+ function isAvatarLiveLocaleReviewed(locale) {
15202
+ return REVIEWED.has(locale);
15203
+ }
15204
+ var VIEW_LOAD_TIMEOUT_MS = 8e3;
15205
+ function loadAvatarLiveView() {
15206
+ return import('./avatar-live-view/index.js').then(
15207
+ (loaded) => loaded.AvatarLiveView
15208
+ );
15209
+ }
15210
+ function ExploreAvatarLive({
15211
+ live,
15212
+ active,
15213
+ locale,
15214
+ productName,
15215
+ still,
15216
+ fallback,
15217
+ onContinueInChat,
15218
+ loadView = loadAvatarLiveView
15219
+ }) {
15220
+ const T = exploreStrings(locale);
15221
+ const config = useWebSDKConfig();
15222
+ const conversation = React.useContext(ExploreConversationContext);
15223
+ const base = avatarApiBase(config.gatewayUrl);
15224
+ const headers = React.useMemo(
15225
+ () => cfAccessHeaders(config.gatewayUrl, config.cfAccess),
15226
+ [config.gatewayUrl, config.cfAccess]
15227
+ );
15228
+ const complete = liveConfigComplete(live);
15229
+ const off = !complete || base === null || conversation === null || conversation.isSharedView || !isAvatarLiveLocaleReviewed(locale);
15230
+ React.useEffect(() => {
15231
+ if (!complete) {
15232
+ console.warn(
15233
+ "[WebSDK] ExplorePanel: avatar.live needs site, load, turnstileSiteKey and manifestVersion; the live avatar stays off."
15234
+ );
15235
+ }
15236
+ }, [complete]);
15237
+ const [View, setView] = React.useState(null);
15238
+ const [failed, setFailed] = React.useState(false);
15239
+ const wanted = active && !off;
15240
+ React.useEffect(() => {
15241
+ if (!wanted || View !== null) return;
15242
+ let cancelled = false;
15243
+ let loading;
15244
+ try {
15245
+ loading = loadView();
15246
+ } catch (error) {
15247
+ loading = Promise.reject(error);
15248
+ }
15249
+ const patience = setTimeout(() => {
15250
+ if (!cancelled) setFailed(true);
15251
+ }, VIEW_LOAD_TIMEOUT_MS);
15252
+ loading.then(
15253
+ (component) => {
15254
+ if (!cancelled) setView(() => component);
15255
+ },
15256
+ () => {
15257
+ clearTimeout(patience);
15258
+ if (!cancelled) setFailed(true);
15259
+ }
15260
+ );
15261
+ return () => {
15262
+ cancelled = true;
15263
+ clearTimeout(patience);
15264
+ };
15265
+ }, [wanted, View, loadView]);
15266
+ if (View !== null) {
15267
+ return /* @__PURE__ */ jsxRuntime.jsx(
15268
+ View,
15269
+ {
15270
+ live,
15271
+ active,
15272
+ locale,
15273
+ productName,
15274
+ still,
15275
+ fallback,
15276
+ onContinueInChat,
15277
+ continueInChat: T.continueInChat,
15278
+ off,
15279
+ base,
15280
+ headers,
15281
+ conversation
15282
+ }
15283
+ );
15284
+ }
15285
+ if (off || failed) {
15286
+ return (
15287
+ // `display: contents`: the earlier Avatar view's picture, line and button lay out in the pane exactly as before.
15288
+ /* @__PURE__ */ jsxRuntime.jsx(
15289
+ "div",
15290
+ {
15291
+ style: { display: "contents" },
15292
+ "data-boff-explore": "avatar-live",
15293
+ "data-phase": off ? "off" : "inert",
15294
+ children: fallback
15295
+ }
15296
+ )
15297
+ );
15298
+ }
15299
+ return /* @__PURE__ */ jsxRuntime.jsxs(
15300
+ "div",
15301
+ {
15302
+ "data-boff-explore": "avatar-live",
15303
+ "data-phase": "idle",
15304
+ className: "flex max-h-full w-full flex-col items-center gap-4 overflow-y-auto",
15305
+ children: [
15306
+ /* @__PURE__ */ jsxRuntime.jsx(
15307
+ "img",
15308
+ {
15309
+ src: still.src,
15310
+ alt: still.alt,
15311
+ decoding: "async",
15312
+ width: 128,
15313
+ height: 128,
15314
+ className: "shrink-0 rounded-full border border-border object-cover shadow-sm h-32 w-32"
15315
+ }
15316
+ ),
15317
+ /* @__PURE__ */ jsxRuntime.jsx(
15318
+ "p",
15319
+ {
15320
+ role: "status",
15321
+ "data-boff-explore": "avatar-live-status",
15322
+ className: "text-sm text-muted-foreground"
15323
+ }
15324
+ ),
15325
+ /* @__PURE__ */ jsxRuntime.jsx("div", { "data-boff-explore": "avatar-live-bot-check" })
15326
+ ]
15327
+ }
15328
+ );
15329
+ }
15165
15330
  var MARK_KEY = "boffExplorePanel";
15166
15331
  var POS_KEY = "boffExplorePanelPos";
15167
15332
  var DOC_KEY = "boffExplorePanelDoc";
@@ -15353,15 +15518,15 @@ function ExplorePanel({
15353
15518
  const rtl = isRtlLocale(resolvedLocale);
15354
15519
  const catalogProducts = React.useContext(ExploreCatalogProductsContext);
15355
15520
  const pinnedSlug = pinnedProductSlug(scope, productSlug);
15356
- const title = React.useMemo(() => {
15357
- if (pinnedSlug === null) return T.panelTitle;
15358
- const name = productNameFor(
15521
+ const pinnedName = React.useMemo(
15522
+ () => pinnedSlug === null ? null : productNameFor(
15359
15523
  catalogProducts ?? ECOSYSTEM_PRODUCTS,
15360
15524
  pinnedSlug,
15361
15525
  productName
15362
- );
15363
- return name === null ? T.panelTitle : fill(T.panelTitleProduct, { product: name });
15364
- }, [T, catalogProducts, pinnedSlug, productName]);
15526
+ ),
15527
+ [catalogProducts, pinnedSlug, productName]
15528
+ );
15529
+ const title = pinnedName === null ? T.panelTitle : fill(T.panelTitleProduct, { product: pinnedName });
15365
15530
  const uid = React.useId();
15366
15531
  const titleId = `${uid}-title`;
15367
15532
  const chatTabId = `${uid}-chat-tab`;
@@ -15682,6 +15847,32 @@ function ExplorePanel({
15682
15847
  const tabButton = "rounded-md px-3 py-1.5 text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring";
15683
15848
  const expanded = size === "expanded";
15684
15849
  const sizeLabel = expanded ? T.exitFullScreen : T.enterFullScreen;
15850
+ const avatarAlt = avatar?.name ? `${avatar.name}, ${T.liveAvatar}` : T.liveAvatar;
15851
+ const comingSoon = avatar ? /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
15852
+ /* @__PURE__ */ jsxRuntime.jsx(
15853
+ "img",
15854
+ {
15855
+ src: avatar.src,
15856
+ alt: avatarAlt,
15857
+ loading: "lazy",
15858
+ decoding: "async",
15859
+ width: 128,
15860
+ height: 128,
15861
+ className: "h-32 w-32 rounded-full border border-border object-cover shadow-sm"
15862
+ }
15863
+ ),
15864
+ /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-base font-semibold", children: T.avatarComingSoon }),
15865
+ /* @__PURE__ */ jsxRuntime.jsx(
15866
+ "button",
15867
+ {
15868
+ type: "button",
15869
+ "data-boff-explore": "panel-continue-chat",
15870
+ className: "inline-flex items-center justify-center rounded-md bg-primary px-4 py-2 text-sm font-medium text-primary-foreground transition-opacity hover:opacity-90 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background",
15871
+ onClick: continueInChat,
15872
+ children: T.continueInChat
15873
+ }
15874
+ )
15875
+ ] }) : null;
15685
15876
  const tabs = [
15686
15877
  { key: "chat", id: chatTabId, pane: chatPaneId, label: T.tabChat },
15687
15878
  { key: "avatar", id: avatarTabId, pane: avatarPaneId, label: T.tabAvatar }
@@ -15811,7 +16002,7 @@ function ExplorePanel({
15811
16002
  )
15812
16003
  }
15813
16004
  ),
15814
- avatar ? /* @__PURE__ */ jsxRuntime.jsxs(
16005
+ avatar ? /* @__PURE__ */ jsxRuntime.jsx(
15815
16006
  "div",
15816
16007
  {
15817
16008
  id: avatarPaneId,
@@ -15822,31 +16013,18 @@ function ExplorePanel({
15822
16013
  "absolute inset-0 flex-col items-center justify-center gap-4 overflow-y-auto bg-background p-6 text-center",
15823
16014
  view === "avatar" ? "flex" : "hidden"
15824
16015
  ),
15825
- children: [
15826
- /* @__PURE__ */ jsxRuntime.jsx(
15827
- "img",
15828
- {
15829
- src: avatar.src,
15830
- alt: avatar.name ? `${avatar.name}, ${T.liveAvatar}` : T.liveAvatar,
15831
- loading: "lazy",
15832
- decoding: "async",
15833
- width: 128,
15834
- height: 128,
15835
- className: "h-32 w-32 rounded-full border border-border object-cover shadow-sm"
15836
- }
15837
- ),
15838
- /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-base font-semibold", children: T.avatarComingSoon }),
15839
- /* @__PURE__ */ jsxRuntime.jsx(
15840
- "button",
15841
- {
15842
- type: "button",
15843
- "data-boff-explore": "panel-continue-chat",
15844
- className: "inline-flex items-center justify-center rounded-md bg-primary px-4 py-2 text-sm font-medium text-primary-foreground transition-opacity hover:opacity-90 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background",
15845
- onClick: continueInChat,
15846
- children: T.continueInChat
15847
- }
15848
- )
15849
- ]
16016
+ children: avatar.live ? /* @__PURE__ */ jsxRuntime.jsx(
16017
+ ExploreAvatarLive,
16018
+ {
16019
+ live: avatar.live,
16020
+ active: open && view === "avatar",
16021
+ locale: resolvedLocale,
16022
+ productName: pinnedName,
16023
+ still: { src: avatar.src, alt: avatarAlt },
16024
+ fallback: comingSoon,
16025
+ onContinueInChat: continueInChat
16026
+ }
16027
+ ) : comingSoon
15850
16028
  }
15851
16029
  ) : null
15852
16030
  ] })
@@ -16628,6 +16806,10 @@ function BlogPostPage(props) {
16628
16806
  ] });
16629
16807
  }
16630
16808
  var CASE_STUDY_TAG = "case-study";
16809
+ function hasCustomerSignOff(page) {
16810
+ const signedAt = page.customerSignOff?.signedAt;
16811
+ return typeof signedAt === "string" && !Number.isNaN(new Date(signedAt).getTime());
16812
+ }
16631
16813
  var ANIMATION_STYLES = `
16632
16814
  @keyframes csFadeUp { from { opacity: 0; transform: translateY(16px); } to { opacity: 1; transform: translateY(0); } }
16633
16815
  @keyframes csFloat { 0%, 100% { transform: translateY(0); } 50% { transform: translateY(-14px); } }
@@ -16657,6 +16839,9 @@ var CASE_STUDIES_QUERY = `
16657
16839
  readingTime
16658
16840
  featured
16659
16841
  category
16842
+ customerSignOff {
16843
+ signedAt
16844
+ }
16660
16845
  }
16661
16846
  total
16662
16847
  hasMore
@@ -16683,6 +16868,9 @@ var CASE_STUDY_QUERY = `
16683
16868
  readingTime
16684
16869
  featured
16685
16870
  category
16871
+ customerSignOff {
16872
+ signedAt
16873
+ }
16686
16874
  }
16687
16875
  }
16688
16876
  `;
@@ -17053,11 +17241,15 @@ function CaseStudiesListPage(props) {
17053
17241
  setPosts([]);
17054
17242
  return;
17055
17243
  }
17056
- const caseStudies = (result.data?.publicContentPages?.items || []).filter(
17057
- (p) => p.tags?.includes(CASE_STUDY_TAG)
17244
+ const items = result.data?.publicContentPages?.items || [];
17245
+ const caseStudies = items.filter(
17246
+ (p) => p.tags?.includes(CASE_STUDY_TAG) && hasCustomerSignOff(p)
17058
17247
  );
17059
17248
  setPosts(caseStudies);
17060
- setTotal(result.data?.publicContentPages?.total || 0);
17249
+ const dropped = items.length - caseStudies.length;
17250
+ setTotal(
17251
+ Math.max(0, (result.data?.publicContentPages?.total || 0) - dropped)
17252
+ );
17061
17253
  setError(null);
17062
17254
  } catch {
17063
17255
  setError("load-failed");
@@ -17239,7 +17431,8 @@ function CaseStudyPage(props) {
17239
17431
  setError(result.errors[0]?.message ?? "Failed to load case study");
17240
17432
  return;
17241
17433
  }
17242
- setPost(result.data?.publicContentPage ?? null);
17434
+ const page = result.data?.publicContentPage ?? null;
17435
+ setPost(page && hasCustomerSignOff(page) ? page : null);
17243
17436
  setError(null);
17244
17437
  } catch {
17245
17438
  setError("Failed to load case study");
@@ -17308,6 +17501,7 @@ function CaseStudyPage(props) {
17308
17501
  ] }) });
17309
17502
  }
17310
17503
  const formattedDate = formatDate(post.publishedAt || post.lastUpdated);
17504
+ const approvedDate = formatDate(post.customerSignOff?.signedAt);
17311
17505
  return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: `w-full ${className}`, children: [
17312
17506
  /* @__PURE__ */ jsxRuntime.jsx(ProseStyles, {}),
17313
17507
  /* @__PURE__ */ jsxRuntime.jsx(
@@ -17345,6 +17539,17 @@ function CaseStudyPage(props) {
17345
17539
  ] })
17346
17540
  ] }) : null
17347
17541
  ] }),
17542
+ approvedDate && /* @__PURE__ */ jsxRuntime.jsxs(
17543
+ "p",
17544
+ {
17545
+ className: "-mt-4 mb-8 text-xs text-muted-foreground",
17546
+ "data-cs": "customer-signoff",
17547
+ children: [
17548
+ "Approved for publication by the customer on ",
17549
+ approvedDate
17550
+ ]
17551
+ }
17552
+ ),
17348
17553
  post.thumbnail && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "mb-10 aspect-[16/9] overflow-hidden rounded-xl", children: /* @__PURE__ */ jsxRuntime.jsx(
17349
17554
  "img",
17350
17555
  {
@@ -20637,6 +20842,112 @@ function shouldRenderSiteFooter(pathname) {
20637
20842
  return !isFooterlessPath(pathname);
20638
20843
  }
20639
20844
 
20845
+ // src/comparison/index.ts
20846
+ var DEFAULT_COMPARISON_MAX_AGE_DAYS = 180;
20847
+ var DAY_MS = 24 * 60 * 60 * 1e3;
20848
+ var ISO_DATE = /^\d{4}-\d{2}-\d{2}$/;
20849
+ function parseVerifiedAt(value) {
20850
+ if (typeof value !== "string" || !ISO_DATE.test(value)) return null;
20851
+ const date = /* @__PURE__ */ new Date(`${value}T00:00:00.000Z`);
20852
+ if (Number.isNaN(date.getTime())) return null;
20853
+ return date.toISOString().slice(0, 10) === value ? date : null;
20854
+ }
20855
+ function isHttpUrl(value) {
20856
+ try {
20857
+ const url = new URL(value);
20858
+ return url.protocol === "https:" || url.protocol === "http:";
20859
+ } catch {
20860
+ return false;
20861
+ }
20862
+ }
20863
+ function checkComparisonFreshness(claims, options = {}) {
20864
+ const maxAgeDays = options.maxAgeDays ?? DEFAULT_COMPARISON_MAX_AGE_DAYS;
20865
+ const now2 = options.now ?? /* @__PURE__ */ new Date();
20866
+ const issues = [];
20867
+ for (const claim of claims) {
20868
+ const sources = claim.sources ?? [];
20869
+ if (sources.length === 0) {
20870
+ issues.push({
20871
+ path: claim.path,
20872
+ problem: "missing-source",
20873
+ detail: "no source: add { url, verifiedAt } for the vendor page this claim was checked against"
20874
+ });
20875
+ continue;
20876
+ }
20877
+ let newest = null;
20878
+ for (const source of sources) {
20879
+ if (!isHttpUrl(source.url)) {
20880
+ issues.push({
20881
+ path: claim.path,
20882
+ problem: "invalid-url",
20883
+ detail: `source URL is not an http(s) URL: ${JSON.stringify(source.url)}`
20884
+ });
20885
+ }
20886
+ const verified = parseVerifiedAt(source.verifiedAt);
20887
+ if (!verified) {
20888
+ issues.push({
20889
+ path: claim.path,
20890
+ problem: "invalid-date",
20891
+ detail: `verifiedAt must be YYYY-MM-DD, got ${JSON.stringify(source.verifiedAt)}`
20892
+ });
20893
+ continue;
20894
+ }
20895
+ if (verified.getTime() > now2.getTime() + DAY_MS) {
20896
+ issues.push({
20897
+ path: claim.path,
20898
+ problem: "future-date",
20899
+ detail: `verifiedAt ${source.verifiedAt} is in the future`
20900
+ });
20901
+ continue;
20902
+ }
20903
+ if (!newest || verified.getTime() > newest.getTime()) newest = verified;
20904
+ }
20905
+ if (newest) {
20906
+ const ageDays = Math.floor((now2.getTime() - newest.getTime()) / DAY_MS);
20907
+ if (ageDays > maxAgeDays) {
20908
+ issues.push({
20909
+ path: claim.path,
20910
+ problem: "stale",
20911
+ detail: `last verified ${newest.toISOString().slice(0, 10)} (${ageDays} days ago, limit ${maxAgeDays}): re-read the vendor page and update verifiedAt or the claim`
20912
+ });
20913
+ }
20914
+ }
20915
+ }
20916
+ return issues;
20917
+ }
20918
+ function latestVerification(claims) {
20919
+ let latest = null;
20920
+ for (const claim of claims) {
20921
+ for (const source of claim.sources ?? []) {
20922
+ if (parseVerifiedAt(source.verifiedAt) && (!latest || source.verifiedAt > latest)) {
20923
+ latest = source.verifiedAt;
20924
+ }
20925
+ }
20926
+ }
20927
+ return latest;
20928
+ }
20929
+ function oldestVerification(claims) {
20930
+ let oldest = null;
20931
+ for (const claim of claims) {
20932
+ for (const source of claim.sources ?? []) {
20933
+ if (parseVerifiedAt(source.verifiedAt) && (!oldest || source.verifiedAt < oldest)) {
20934
+ oldest = source.verifiedAt;
20935
+ }
20936
+ }
20937
+ }
20938
+ return oldest;
20939
+ }
20940
+ function formatFreshnessReport(issues) {
20941
+ if (issues.length === 0)
20942
+ return "Comparison sources: all claims sourced and fresh.";
20943
+ const lines = issues.map((i) => ` - [${i.problem}] ${i.path}: ${i.detail}`);
20944
+ return [
20945
+ `Comparison sources: ${issues.length} problem(s) found.`,
20946
+ ...lines,
20947
+ "Re-check each claim against the vendor's current page, then update the claim and its verifiedAt."
20948
+ ].join("\n");
20949
+ }
20950
+
20640
20951
  exports.ANSWERS_PATH = ANSWERS_PATH;
20641
20952
  exports.ANSWER_HEADING_SHIFT = ANSWER_HEADING_SHIFT;
20642
20953
  exports.ANSWER_PATH_PREFIX = ANSWER_PATH_PREFIX;
@@ -20659,6 +20970,7 @@ exports.ConceptsPage = ConceptsPage;
20659
20970
  exports.ContactPage = ContactPage;
20660
20971
  exports.ContentRenderer = ContentRenderer;
20661
20972
  exports.ContractsPage = ContractsPage;
20973
+ exports.DEFAULT_COMPARISON_MAX_AGE_DAYS = DEFAULT_COMPARISON_MAX_AGE_DAYS;
20662
20974
  exports.DEFAULT_CONTACT_CATEGORIES = DEFAULT_CONTACT_CATEGORIES;
20663
20975
  exports.DEFAULT_EXPLORE_EXAMPLE_PROMPTS = DEFAULT_EXPLORE_EXAMPLE_PROMPTS;
20664
20976
  exports.DOMAINS_URL = DOMAINS_URL;
@@ -20726,6 +21038,7 @@ exports.answerSpeechText = answerSpeechText;
20726
21038
  exports.answerStrings = answerStrings;
20727
21039
  exports.availablePlanCurrencies = availablePlanCurrencies;
20728
21040
  exports.buttonVariants = buttonVariants;
21041
+ exports.checkComparisonFreshness = checkComparisonFreshness;
20729
21042
  exports.classifyExploreError = classifyExploreError;
20730
21043
  exports.cleanPlanDisplayName = cleanPlanDisplayName;
20731
21044
  exports.cn = cn;
@@ -20742,11 +21055,13 @@ exports.fetchPublicProduct = fetchPublicProduct;
20742
21055
  exports.findLegalDocument = findLegalDocument;
20743
21056
  exports.formatAnswerDate = formatAnswerDate;
20744
21057
  exports.formatDownloads = formatDownloads;
21058
+ exports.formatFreshnessReport = formatFreshnessReport;
20745
21059
  exports.formatLegalDate = formatLegalDate;
20746
21060
  exports.formatPrice = formatPrice2;
20747
21061
  exports.getAnswerBySlug = getAnswerBySlug;
20748
21062
  exports.getConceptBySlug = getConceptBySlug;
20749
21063
  exports.getRecaptchaV3Token = getRecaptchaV3Token;
21064
+ exports.hasCustomerSignOff = hasCustomerSignOff;
20750
21065
  exports.inlineImageOrigins = inlineImageOrigins;
20751
21066
  exports.isFooterlessPath = isFooterlessPath;
20752
21067
  exports.isInlineImageAllowed = isInlineImageAllowed;
@@ -20755,11 +21070,14 @@ exports.isPublishedSourceUrl = isPublishedSourceUrl;
20755
21070
  exports.isSameProductSlug = isSameProductSlug;
20756
21071
  exports.isUuidLike = isUuidLike;
20757
21072
  exports.isValidUrl = isValidUrl;
21073
+ exports.latestVerification = latestVerification;
20758
21074
  exports.legacyProductSlug = legacyProductSlug;
20759
21075
  exports.loadRecaptchaV3 = loadRecaptchaV3;
20760
21076
  exports.looksLikeDeliverableEmail = looksLikeDeliverableEmail;
20761
21077
  exports.normalizeProductSlug = normalizeProductSlug;
20762
21078
  exports.normalizeRoutePath = normalizeRoutePath;
21079
+ exports.oldestVerification = oldestVerification;
21080
+ exports.parseVerifiedAt = parseVerifiedAt;
20763
21081
  exports.planPriceIn = planPriceIn;
20764
21082
  exports.productSlugVariants = productSlugVariants;
20765
21083
  exports.productSlugsByOrigin = productSlugsByOrigin;