@mohasinac/appkit 4.32.2 → 4.32.4

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.
@@ -9,6 +9,7 @@ import { cache } from "react";
9
9
  import { categoriesRepository } from "../../../../repositories";
10
10
  import { productRepository } from "../../../../repositories";
11
11
  import { safeRead } from "../../../../errors/safe-read";
12
+ import { hidePublicTestData } from "../tester/visibility";
12
13
  /**
13
14
  * Fetch a single bundle by slug for the public detail page. Returns null
14
15
  * when the slug is empty or no matching `categoryType:"bundle"` row exists.
@@ -154,7 +155,7 @@ export const listFeaturedBundles = cache(async (limit = 8, _opts) => {
154
155
  key: "categories.listByType(bundle)",
155
156
  fallback: [],
156
157
  });
157
- return all;
158
+ return hidePublicTestData(all);
158
159
  });
159
160
  /**
160
161
  * "Related Bundles" for the bundle detail page — every other active bundle,
@@ -173,5 +174,7 @@ export const getRelatedBundles = cache(async (bundle, limit = 8) => {
173
174
  key: "categories.listByType(bundle)",
174
175
  fallback: [],
175
176
  });
176
- return all.filter((b) => b.id !== bundle.id).slice(0, limit);
177
+ return hidePublicTestData(all)
178
+ .filter((b) => b.id !== bundle.id)
179
+ .slice(0, limit);
177
180
  });
@@ -3,6 +3,7 @@ import { cache } from "react";
3
3
  import { categoriesRepository } from "../../../../repositories";
4
4
  import { safeRead } from "../../../../errors/safe-read";
5
5
  import { CATEGORIES_FEATURED_LIMIT, CATEGORIES_MENU_LIMIT, CATEGORIES_SITEMAP_LIMIT } from "../../../shared/features/categories/config";
6
+ import { hidePublicTestData } from "../tester/visibility";
6
7
  /** Full category document by slug — deduped per request via React.cache(). */
7
8
  export const getCategoryForDetail = cache(async (slug) => {
8
9
  /*
@@ -23,25 +24,34 @@ export const getCategoryForDetail = cache(async (slug) => {
23
24
  });
24
25
  /** Flat list of all categories at tier 1 (roots) — for nav/sitemap. */
25
26
  export const listRootCategories = cache(async () => {
26
- return safeRead(() => categoriesRepository.getCategoriesByTier(1), {
27
+ const rows = await safeRead(() => categoriesRepository.getCategoriesByTier(1), {
27
28
  route: "/categories",
28
29
  key: "categories.listRootCategories",
29
30
  fallback: [],
30
31
  });
32
+ return hidePublicTestData(rows);
31
33
  });
32
34
  /** Featured categories for homepage display (showOnHomepage: true). */
33
35
  export const listFeaturedCategories = cache(async () => {
34
- return categoriesRepository
36
+ const rows = await safeRead(() => categoriesRepository
35
37
  .list({ filters: "isActive==true,showOnHomepage==true", sorts: "order", page: 1, pageSize: CATEGORIES_FEATURED_LIMIT })
36
- .then((r) => r.data ?? [])
37
- .catch(() => []);
38
+ .then((r) => r.data ?? []), {
39
+ route: "/",
40
+ key: "categories.listFeaturedCategories",
41
+ fallback: [],
42
+ });
43
+ return hidePublicTestData(rows);
38
44
  });
39
45
  /** Nav-menu categories (showInMenu: true, tier 1 + 2). */
40
46
  export const listMenuCategories = cache(async () => {
41
- return categoriesRepository
47
+ const rows = await safeRead(() => categoriesRepository
42
48
  .list({ filters: "isActive==true,display.showInMenu==true", sorts: "order", page: 1, pageSize: CATEGORIES_MENU_LIMIT })
43
- .then((r) => r.data ?? [])
44
- .catch(() => []);
49
+ .then((r) => r.data ?? []), {
50
+ route: "nav/menu",
51
+ key: "categories.listMenuCategories",
52
+ fallback: [],
53
+ });
54
+ return hidePublicTestData(rows);
45
55
  });
46
56
  /** Full category tree rooted at a given rootId — for sidebar navigation. */
47
57
  export const getCategoryTree = cache(async (rootId) => {
@@ -442,6 +442,21 @@ export async function listPublicProducts(input, opts) {
442
442
  ? unavailableClausesFor(types).find((c) => c.op === "lt" && c.sortField === requestedSortField)
443
443
  : undefined;
444
444
  if (pushdown) {
445
+ /*
446
+ * 🛑 This branch keeps a POST-pagination sandbox filter, knowingly.
447
+ *
448
+ * It exists to give the ended-listing archive real, unbounded-depth
449
+ * Firestore pagination, which is incompatible with the over-fetch the
450
+ * window path uses. So an anonymous page here can come back a row or two
451
+ * SHORT when a sandbox fixture lands in it — bounded by the ~25 fixtures
452
+ * that exist, and nothing like the empty page the default view produced,
453
+ * because real ended listings dominate this scope.
454
+ *
455
+ * Falling through to Path C instead would fix the short page and cost every
456
+ * anonymous visitor the deep pagination this branch was written for. The
457
+ * short page is the better trade; it is recorded here so the next reader
458
+ * knows it was weighed rather than missed.
459
+ */
445
460
  const filters = sieveAnd(safeFilters, clauseToSieve(pushdown, now));
446
461
  const result = await runQuery(executor, {
447
462
  filters,
@@ -558,7 +573,28 @@ export async function listPublicProducts(input, opts) {
558
573
  const hasTypeFacet = Object.keys(input.typeFacets ?? {}).length > 0;
559
574
  /** Multi-value `tags`/`features` — no `array-contains-any`, so per-row. */
560
575
  const hasArrayMultiSelect = (input.tags?.length ?? 0) > 1 || (input.features?.length ?? 0) > 1;
561
- const inMemory = wantAvailable || hasTypeFacet || hasArrayMultiSelect || (hasDateRange && !canPushDate);
576
+ /*
577
+ * 🛑 The tester-sandbox filter must run BEFORE the page is sliced.
578
+ *
579
+ * It used to run only in `finish()`, which receives an already-paginated
580
+ * page — so a page whose rows were ALL sandbox fixtures came back empty with
581
+ * a non-zero `total`. That is not hypothetical: the fixtures carry
582
+ * `Date.now()`-relative dates by design (they have to re-arm on every
583
+ * reseed), so immediately after a seed load they are the newest rows in the
584
+ * collection and fill the whole first page of the default `-createdAt` sort.
585
+ * The public catalogue returned `items: []` alongside `total: 79`.
586
+ *
587
+ * So an anonymous viewer forces the bounded-window path, where the filter is
588
+ * one more per-row predicate applied before the slice. That is already the
589
+ * default for public browsing (`wantAvailable`), so this widens the window
590
+ * path only for the non-default scopes.
591
+ */
592
+ const hidesTestData = filterTestDataForViewer([{ isTestData: true }], opts?.viewer).length === 0;
593
+ const inMemory = wantAvailable ||
594
+ hasTypeFacet ||
595
+ hasArrayMultiSelect ||
596
+ hidesTestData ||
597
+ (hasDateRange && !canPushDate);
562
598
  // When a date range stays in memory, fetch in whichever direction front-loads
563
599
  // the rows that will PASS it: `dateFrom` (">=", still live) wants the most
564
600
  // future dates first; `dateTo` ("<=", ends by) wants the earliest first.
@@ -597,6 +633,10 @@ export async function listPublicProducts(input, opts) {
597
633
  return wanted.some((w) => raw.includes(w));
598
634
  };
599
635
  const filtered = items.filter((item) => {
636
+ // Before the slice — see the note on `hidesTestData` above.
637
+ if (hidesTestData && item.isTestData === true) {
638
+ return false;
639
+ }
600
640
  if (wantAvailable && !isListingRowAvailable(item, now))
601
641
  return false;
602
642
  if (hasArrayMultiSelect) {
@@ -4,6 +4,7 @@ import { storeRepository } from "../../../../repositories";
4
4
  import { STORES_PRODUCTS_PAGE_SIZE, STORES_SITEMAP_LIMIT, } from "../../../shared/features/stores/config";
5
5
  import { makeGetStoreListingsInitial } from "../shared/listing-data-factory";
6
6
  import { safeRead } from "../../../../errors/safe-read";
7
+ import { hidePublicTestData } from "../tester/visibility";
7
8
  /** Full store document by slug — deduped per request via React.cache(). */
8
9
  export const getStoreForDetail = cache(async (slug) => {
9
10
  return (await storeRepository.findBySlug(slug)) ?? null;
@@ -19,5 +20,10 @@ export const listSitemapStores = cache(async () => {
19
20
  * store — which is not an error to any crawler, just a smaller site.
20
21
  */
21
22
  const result = await safeRead(() => storeRepository.listStores({ filters: "status==active", sorts: "-updatedAt", page: 1, pageSize: STORES_SITEMAP_LIMIT }, true), { route: "sitemap/stores", key: "stores.listStores", fallback: null });
22
- return (result?.items ?? []).map(({ storeSlug, updatedAt }) => ({ storeSlug, updatedAt }));
23
+ // A sandbox store in the SITEMAP is the longest-lived form of this leak:
24
+ // a crawler keeps the URL long after the fixture is pruned.
25
+ return hidePublicTestData(result?.items ?? []).map(({ storeSlug, updatedAt }) => ({
26
+ storeSlug,
27
+ updatedAt,
28
+ }));
23
29
  });
@@ -12,3 +12,25 @@ export declare function canViewTestData(viewer: ViewerLike | null | undefined):
12
12
  export declare function filterTestDataForViewer<T extends TestDataFlagged>(items: T[], viewer: ViewerLike | null | undefined): T[];
13
13
  /** Returns null (as if not found) for an isTestData:true single doc when the viewer can't see it. */
14
14
  export declare function filterSingleTestData<T extends TestDataFlagged>(item: T | null, viewer: ViewerLike | null | undefined): T | null;
15
+ /**
16
+ * The PUBLIC projection of a list: sandbox rows removed, unconditionally.
17
+ *
18
+ * 🛑 Use this in any server read that feeds a public page. It is
19
+ * `filterTestDataForViewer(items, undefined)` under a name that says what it is
20
+ * for, because the two-argument form invites a call site to pass a viewer it
21
+ * does not actually have — and `undefined` there reads like an oversight rather
22
+ * than the fail-closed default it is.
23
+ *
24
+ * A signed-in tester still sees sandbox rows: the public pages hydrate from the
25
+ * APIs, which DO thread the real viewer. This covers the server-rendered first
26
+ * paint, which has no session in hand and must not publish the sandbox to
27
+ * everyone on the strength of that.
28
+ *
29
+ * Added 2026-09-01 after a reseed made the leak visible: the homepage carried
30
+ * 10 sandbox bundles, 8 events, 5 brands and 4 categories; /categories carried
31
+ * 9; /stores rendered a card for `store-tester-sandbox`. Only the products path
32
+ * had ever been filtered.
33
+ */
34
+ export declare function hidePublicTestData<T extends TestDataFlagged>(items: T[]): T[];
35
+ /** The single-document form of {@link hidePublicTestData}. */
36
+ export declare function hidePublicTestDoc<T extends TestDataFlagged>(item: T | null): T | null;
@@ -26,3 +26,29 @@ export function filterSingleTestData(item, viewer) {
26
26
  return null;
27
27
  return item;
28
28
  }
29
+ /**
30
+ * The PUBLIC projection of a list: sandbox rows removed, unconditionally.
31
+ *
32
+ * 🛑 Use this in any server read that feeds a public page. It is
33
+ * `filterTestDataForViewer(items, undefined)` under a name that says what it is
34
+ * for, because the two-argument form invites a call site to pass a viewer it
35
+ * does not actually have — and `undefined` there reads like an oversight rather
36
+ * than the fail-closed default it is.
37
+ *
38
+ * A signed-in tester still sees sandbox rows: the public pages hydrate from the
39
+ * APIs, which DO thread the real viewer. This covers the server-rendered first
40
+ * paint, which has no session in hand and must not publish the sandbox to
41
+ * everyone on the strength of that.
42
+ *
43
+ * Added 2026-09-01 after a reseed made the leak visible: the homepage carried
44
+ * 10 sandbox bundles, 8 events, 5 brands and 4 categories; /categories carried
45
+ * 9; /stores rendered a card for `store-tester-sandbox`. Only the products path
46
+ * had ever been filtered.
47
+ */
48
+ export function hidePublicTestData(items) {
49
+ return filterTestDataForViewer(items, undefined);
50
+ }
51
+ /** The single-document form of {@link hidePublicTestData}. */
52
+ export function hidePublicTestDoc(item) {
53
+ return filterSingleTestData(item, undefined);
54
+ }
@@ -4,6 +4,7 @@ import { Container, Heading, Main, Section } from "../../../ui";
4
4
  import { AdSlot } from "../../homepage/components/AdSlot";
5
5
  import { BlogIndexListing } from "./BlogIndexListing";
6
6
  import { safeRead } from "../../../errors/safe-read";
7
+ import { hidePublicTestData } from "../../../_internal/server/features/tester/visibility";
7
8
  function sp(params, key) {
8
9
  const v = params[key];
9
10
  return Array.isArray(v) ? v[0] ?? "" : v ?? "";
@@ -14,15 +15,19 @@ export async function BlogIndexPageView({ searchParams = {} }) {
14
15
  const pageSize = Number(sp(searchParams, "pageSize")) || 24;
15
16
  const category = sp(searchParams, "category");
16
17
  const sieveResult = await safeRead(() => blogRepository.listPublished({ ...(category ? { category } : {}) }, { sorts: sort, page, pageSize }), { route: "/blog", key: "blogPosts.listPublished", fallback: null });
17
- const initialData = sieveResult
18
+ // Sandbox posts are public rows by design — not on someone else's first paint.
19
+ const publicSieve = sieveResult
20
+ ? { ...sieveResult, items: hidePublicTestData(sieveResult.items) }
21
+ : sieveResult;
22
+ const initialData = publicSieve
18
23
  ? {
19
- posts: sieveResult.items,
24
+ posts: publicSieve.items,
20
25
  meta: {
21
- total: sieveResult.total,
22
- page: sieveResult.page,
23
- pageSize: sieveResult.pageSize,
24
- totalPages: sieveResult.totalPages,
25
- hasMore: sieveResult.hasMore,
26
+ total: publicSieve.total,
27
+ page: publicSieve.page,
28
+ pageSize: publicSieve.pageSize,
29
+ totalPages: publicSieve.totalPages,
30
+ hasMore: publicSieve.hasMore,
26
31
  },
27
32
  }
28
33
  : undefined;
@@ -17,6 +17,7 @@ import { CATEGORY_PAGE_TABS } from "../../products/constants/listing-tabs";
17
17
  import { enabledCategoryTypes, enabledListingTypes } from "../../../_internal/shared/listing-types/feature-flags";
18
18
  import { siteSettingsRepository } from "../../../repositories";
19
19
  import { safeRead } from "../../../errors/safe-read";
20
+ import { hidePublicTestData } from "../../../_internal/server/features/tester/visibility";
20
21
  const __O = {
21
22
  hidden: "overflow-hidden",
22
23
  };
@@ -48,13 +49,15 @@ export async function BrandDetailPageView({ slug, initialBrand }) {
48
49
  // We pull all bundle categories and filter client-side by brandSlug —
49
50
  // no dedicated repository query needed at this catalog's bundle volume.
50
51
  brand?.slug
51
- ? safeRead(() => categoriesRepository.listByType("bundle", { activeOnly: true, limit: 50 }), { route: "/brands/[slug]", key: "brand.bundles", fallback: [] })
52
+ ? safeRead(() => categoriesRepository
53
+ .listByType("bundle", { activeOnly: true, limit: 50 })
54
+ .then(hidePublicTestData), { route: "/brands/[slug]", key: "brand.bundles", fallback: [] })
52
55
  : Promise.resolve([]),
53
56
  // Related brands — every other active brand row, excluding this one.
54
57
  // `as unknown` is load-bearing only because CategoryDocument.createdAt is a
55
58
  // Date while CategoryItem.createdAt is a string — a pre-existing mismatch
56
59
  // the old `.catch(() => [])` union happened to paper over.
57
- safeRead(() => categoriesRepository.findActiveBrands(), {
60
+ safeRead(() => categoriesRepository.findActiveBrands().then(hidePublicTestData), {
58
61
  route: "/brands/[slug]",
59
62
  key: "brand.relatedBrands",
60
63
  fallback: [],
@@ -4,6 +4,7 @@ import { Container, Heading, Main, Section, Text } from "../../../ui";
4
4
  import { AdSlot } from "../../homepage/components/AdSlot";
5
5
  import { CategoryBundlesListing } from "./CategoryBundlesListing";
6
6
  import { safeRead } from "../../../errors/safe-read";
7
+ import { hidePublicTestData } from "../../../_internal/server/features/tester/visibility";
7
8
  const DEFAULT_LIMIT = 200;
8
9
  function sp(params, key) {
9
10
  const v = params[key];
@@ -22,8 +23,9 @@ export async function BundlesListView({ searchParams = {}, onBuyNow, }) {
22
23
  activeOnly: true,
23
24
  limit: DEFAULT_LIMIT,
24
25
  }), { route: "/bundles", key: "categories.listByType(bundle)", fallback: [] });
26
+ const visible = hidePublicTestData(all);
25
27
  const bundles = storeId
26
- ? all.filter((c) => c.createdByStoreId === storeId)
28
+ ? visible.filter((c) => c.createdByStoreId === storeId)
27
29
  : all;
28
30
  return (_jsx(Main, { children: _jsx(Section, { padding: "y-2xl", children: _jsxs(Container, { size: "xl", children: [_jsx(Heading, { level: 1, className: "mb-2", color: "primary", size: "3xl", weight: "semibold", children: "Collectible Bundles" }), _jsx(Text, { className: "mb-6 text-[var(--appkit-color-text-muted)]", size: "sm", children: "Curated multi-product bundles \u2014 Pok\u00E9mon starter sets, Beyblade tournament packs, Hot Wheels collector boxes and more. One price, one checkout, one shipment." }), _jsx(AdSlot, { id: "listing-sidebar-top", className: "mb-6" }), _jsx(CategoryBundlesListing, { initialBundles: bundles, onBuyNow: onBuyNow }), _jsx(AdSlot, { id: "listing-sidebar-bottom", className: "mt-8" })] }) }) }));
29
31
  }
@@ -4,6 +4,8 @@ import { categoriesRepository } from "../../../repositories";
4
4
  import { Container, Heading, Main, Section } from "../../../ui";
5
5
  import { AdSlot } from "../../homepage/components/AdSlot";
6
6
  import { CategoriesIndexListing } from "./CategoriesIndexListing";
7
+ import { safeRead } from "../../../errors/safe-read";
8
+ import { hidePublicTestData } from "../../../_internal/server/features/tester/visibility";
7
9
  function sp(params, key) {
8
10
  const v = params[key];
9
11
  return Array.isArray(v) ? v[0] ?? "" : v ?? "";
@@ -39,14 +41,17 @@ export async function CategoriesIndexPageView({ searchParams = {} }) {
39
41
  const sort = sp(searchParams, "sort") || "name";
40
42
  const page = Number(sp(searchParams, "page")) || 1;
41
43
  const filters = buildCategoryFilters(searchParams);
42
- const result = await categoriesRepository
43
- .list({
44
+ const result = await safeRead(() => categoriesRepository.list({
44
45
  page,
45
46
  pageSize: 200,
46
47
  sorts: sort,
47
48
  ...(filters ? { filters } : {}),
48
- })
49
- .catch(() => null);
50
- const initialData = (result?.items ?? []);
49
+ }), { route: "/categories", key: "categories.list", fallback: null });
50
+ /*
51
+ * This one page carries THREE entity kinds — categories, brands and bundles
52
+ * are all `CategoryDocument` rows discriminated by `categoryType` — so a
53
+ * single unfiltered read published sandbox fixtures of all three.
54
+ */
55
+ const initialData = hidePublicTestData((result?.items ?? []));
51
56
  return (_jsx(Main, { children: _jsx(Section, { padding: "y-2xl", children: _jsxs(Container, { size: "xl", children: [_jsx(Heading, { level: 1, className: "mb-8", color: "primary", size: "3xl", weight: "semibold", children: "Categories" }), _jsx(AdSlot, { id: "listing-sidebar-top", className: "mb-6" }), _jsx(CategoriesIndexListing, { initialData: initialData }), _jsx(AdSlot, { id: "listing-sidebar-bottom", className: "mt-8" })] }) }) }));
52
57
  }
@@ -16,6 +16,7 @@ import { CATEGORY_PAGE_TABS } from "../../products/constants/listing-tabs";
16
16
  import { enabledCategoryTypes, enabledListingTypes } from "../../../_internal/shared/listing-types/feature-flags";
17
17
  import { siteSettingsRepository } from "../../../repositories";
18
18
  import { safeRead } from "../../../errors/safe-read";
19
+ import { hidePublicTestData } from "../../../_internal/server/features/tester/visibility";
19
20
  const __O = {
20
21
  hidden: "overflow-hidden",
21
22
  xAuto: "overflow-x-auto",
@@ -52,7 +53,9 @@ export async function CategoryDetailPageView({ slug }) {
52
53
  // SB-UNI-D — bundles fetched from the categories collection. We pull
53
54
  // all active bundle rows; the carousel filters by category affinity.
54
55
  categoriesIn
55
- ? safeRead(() => categoriesRepository.listByType("bundle", { activeOnly: true, limit: 50 }), { route: "/categories/[slug]", key: "category.bundles", fallback: [] })
56
+ ? safeRead(() => categoriesRepository
57
+ .listByType("bundle", { activeOnly: true, limit: 50 })
58
+ .then(hidePublicTestData), { route: "/categories/[slug]", key: "category.bundles", fallback: [] })
56
59
  : Promise.resolve([]),
57
60
  category?.id
58
61
  ? safeRead(() => categoriesRepository.getChildren(category.id), {
@@ -6,6 +6,8 @@ import { EventsIndexListing } from "./EventsIndexListing";
6
6
  import { EVENT_FIELDS } from "../../../constants/field-names";
7
7
  import { sieveFilter, sieveMultiEq, sieveAnd, SIEVE_OP } from "../../../utils/sieve-builder";
8
8
  import { sortBy } from "../../../constants/sort";
9
+ import { safeRead } from "../../../errors/safe-read";
10
+ import { hidePublicTestData } from "../../../_internal/server/features/tester/visibility";
9
11
  function sp(params, key) {
10
12
  const v = params[key];
11
13
  return Array.isArray(v) ? v[0] ?? "" : v ?? "";
@@ -46,16 +48,28 @@ export async function EventsListPageView({ searchParams = {} }) {
46
48
  const pageSize = Number(sp(searchParams, "pageSize")) || 24;
47
49
  const filters = buildEventFilters(searchParams);
48
50
  const [result, facetCounts] = await Promise.all([
49
- eventRepository
50
- .list({ filters, sorts: sort, page, pageSize })
51
- .catch(() => null),
51
+ safeRead(() => eventRepository.list({ filters, sorts: sort, page, pageSize }), {
52
+ route: "/events",
53
+ key: "events.list",
54
+ fallback: null,
55
+ }),
52
56
  // Facet counts are deliberately NOT narrowed by the shopper's own type or
53
57
  // status selection: a count that shrank to zero as you ticked boxes would
54
58
  // hide the very facet you'd need to untick. They answer "does this kind of
55
59
  // event exist at all", which is the question that decides visibility.
56
- eventRepository
57
- .facetCountsForTypesAndStatuses()
58
- .catch(() => ({})),
60
+ safeRead(() => eventRepository.facetCountsForTypesAndStatuses(), {
61
+ route: "/events",
62
+ key: "events.facetCounts",
63
+ fallback: {},
64
+ }),
59
65
  ]);
60
- return (_jsx(Main, { children: _jsx(Section, { padding: "y-2xl", children: _jsxs(Container, { size: "xl", children: [_jsx(Heading, { level: 1, className: "mb-8", color: "primary", size: "3xl", weight: "semibold", children: "Events" }), _jsx(AdSlot, { id: "listing-sidebar-top", className: "mb-6" }), _jsx(EventsIndexListing, { initialData: result, facetCounts: facetCounts }), _jsx(AdSlot, { id: "listing-sidebar-bottom", className: "mt-8" })] }) }) }));
66
+ /*
67
+ * Sandbox events are seeded as ordinary public rows so testers exercise the
68
+ * real browse path; they must not reach anyone else's first paint. The client
69
+ * refetch threads the real viewer, so a signed-in tester still sees them.
70
+ */
71
+ const publicResult = result
72
+ ? { ...result, items: hidePublicTestData(result.items) }
73
+ : result;
74
+ return (_jsx(Main, { children: _jsx(Section, { padding: "y-2xl", children: _jsxs(Container, { size: "xl", children: [_jsx(Heading, { level: 1, className: "mb-8", color: "primary", size: "3xl", weight: "semibold", children: "Events" }), _jsx(AdSlot, { id: "listing-sidebar-top", className: "mb-6" }), _jsx(EventsIndexListing, { initialData: publicResult, facetCounts: facetCounts }), _jsx(AdSlot, { id: "listing-sidebar-bottom", className: "mt-8" })] }) }) }));
61
75
  }
@@ -40,6 +40,10 @@ export async function ReviewsIndexPageView({ searchParams = {} }) {
40
40
  const sort = sp(searchParams, "sort") || "-createdAt";
41
41
  const page = Number(sp(searchParams, "page")) || 1;
42
42
  const filters = buildReviewFilters(searchParams);
43
+ // audit-public-test-data-ok: `ReviewDocument` carries no `isTestData` field —
44
+ // tsc rejects the filter outright, which is the honest signal that reviews
45
+ // have no sandbox concept to strip. A review OF a sandbox product is not
46
+ // flagged today; that is a seed-schema gap, not something a cast should hide.
43
47
  const result = await safeRead(() => reviewRepository.listAll({ filters, sorts: sort, page, pageSize: 12 }), { route: "/reviews", key: "reviews.listAll", fallback: null });
44
48
  const initialData = result
45
49
  ? {
@@ -3,6 +3,7 @@ import { categoriesRepository } from "../../../repositories";
3
3
  import { getStoreBySlug } from "./StoreDetailLayoutView";
4
4
  import { CategoryBundlesListing } from "../../categories/components/CategoryBundlesListing";
5
5
  import { safeRead } from "../../../errors/safe-read";
6
+ import { hidePublicTestData } from "../../../_internal/server/features/tester/visibility";
6
7
  /**
7
8
  * Public store → Bundles tab.
8
9
  *
@@ -24,5 +25,5 @@ export async function StoreBundlesPageView({ storeSlug, onBuyNow, }) {
24
25
  key: "categories.listByType(bundle)",
25
26
  fallback: [],
26
27
  });
27
- return _jsx(CategoryBundlesListing, { initialBundles: bundles, onBuyNow: onBuyNow });
28
+ return (_jsx(CategoryBundlesListing, { initialBundles: hidePublicTestData(bundles), onBuyNow: onBuyNow }));
28
29
  }
@@ -61,6 +61,8 @@ export async function StoreDetailLayoutView({ storeSlug, activeTab, children, sc
61
61
  const [listingCounts, couponsCount, reviewsCount] = storeId
62
62
  ? await Promise.all([
63
63
  listingTabCounts(STORE_PAGE_TABS, { storeId }),
64
+ // audit-public-test-data-ok: a COUNT of the store's own coupons, and
65
+ // `CouponDocument` carries no isTestData field — there is nothing to strip.
64
66
  couponsRepository
65
67
  .list({ filters: sieveAnd(sieveFilter("storeId", SIEVE_OP.EQ, storeId), sieveFilter("validity.isActive", SIEVE_OP.EQ, "true")), page: 1, pageSize: 1 })
66
68
  .then((r) => r.total)
@@ -4,6 +4,7 @@ import { Container, Heading, Main, Section } from "../../../ui";
4
4
  import { AdSlot } from "../../homepage/components/AdSlot";
5
5
  import { StoresIndexListing } from "./StoresIndexListing";
6
6
  import { safeRead } from "../../../errors/safe-read";
7
+ import { hidePublicTestData } from "../../../_internal/server/features/tester/visibility";
7
8
  function sp(params, key) {
8
9
  const v = params[key];
9
10
  return Array.isArray(v) ? v[0] ?? "" : v ?? "";
@@ -13,5 +14,13 @@ export async function StoresIndexPageView({ searchParams = {} }) {
13
14
  const page = Number(sp(searchParams, "page")) || 1;
14
15
  const pageSize = Number(sp(searchParams, "pageSize")) || 24;
15
16
  const result = await safeRead(() => storeRepository.listStores({ page, pageSize, sorts: sort }, true), { route: "/stores", key: "stores.listStores", fallback: null });
16
- return (_jsx(Main, { children: _jsx(Section, { padding: "y-2xl", children: _jsxs(Container, { size: "xl", children: [_jsx(Heading, { level: 1, className: "mb-8", color: "primary", size: "3xl", weight: "semibold", children: "Stores" }), _jsx(AdSlot, { id: "listing-sidebar-top", className: "mb-6" }), _jsx(StoresIndexListing, { initialData: result ?? undefined }), _jsx(AdSlot, { id: "listing-sidebar-bottom", className: "mt-8" })] }) }) }));
17
+ /*
18
+ * The API already filters this; the SSR page called the repository directly
19
+ * and did not, so the first paint rendered a card for `store-tester-sandbox`
20
+ * while the client refetch dropped it.
21
+ */
22
+ const publicResult = result
23
+ ? { ...result, items: hidePublicTestData(result.items) }
24
+ : result;
25
+ return (_jsx(Main, { children: _jsx(Section, { padding: "y-2xl", children: _jsxs(Container, { size: "xl", children: [_jsx(Heading, { level: 1, className: "mb-8", color: "primary", size: "3xl", weight: "semibold", children: "Stores" }), _jsx(AdSlot, { id: "listing-sidebar-top", className: "mb-6" }), _jsx(StoresIndexListing, { initialData: publicResult ?? undefined }), _jsx(AdSlot, { id: "listing-sidebar-bottom", className: "mt-8" })] }) }) }));
17
26
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mohasinac/appkit",
3
- "version": "4.32.2",
3
+ "version": "4.32.4",
4
4
  "license": "MIT",
5
5
  "publishConfig": {
6
6
  "access": "public"