@mohasinac/appkit 4.64.0 → 4.65.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.
Files changed (163) hide show
  1. package/dist/.build-stamp.json +3 -3
  2. package/dist/_internal/server/features/checkout/actions.js +18 -1
  3. package/dist/_internal/server/features/search/index-build/sources.js +27 -12
  4. package/dist/_internal/server/features/seo/sitemap.js +78 -12
  5. package/dist/_internal/server/features/site-settings/adapters.d.ts +1 -1
  6. package/dist/_internal/server/features/site-settings/adapters.js +16 -1
  7. package/dist/_internal/server/features/stores/adapters.d.ts +1 -1
  8. package/dist/_internal/server/features/stores/adapters.js +14 -0
  9. package/dist/_internal/server/features/stores/tabs.d.ts +19 -15
  10. package/dist/_internal/server/features/stores/tabs.js +20 -21
  11. package/dist/_internal/server/jobs/core/analyticsAlertEvaluate.js +9 -4
  12. package/dist/_internal/server/jobs/core/categoryTaxonomyBackfill.js +22 -0
  13. package/dist/_internal/server/jobs/core/countersReconcile.js +0 -1
  14. package/dist/_internal/server/jobs/handlers/onCatalogueSubmittedForApproval.js +1 -1
  15. package/dist/_internal/shared/features/site-settings/types.d.ts +10 -2
  16. package/dist/_internal/shared/features/sticker-trace/trace/segment.js +13 -2
  17. package/dist/_internal/shared/listing-types/feature-flags.d.ts +54 -0
  18. package/dist/_internal/shared/listing-types/feature-flags.js +72 -0
  19. package/dist/client.d.ts +5 -2
  20. package/dist/client.js +8 -1
  21. package/dist/constants/field-names.d.ts +1 -0
  22. package/dist/constants/field-names.js +14 -0
  23. package/dist/features/account/api/[userId]/addresses/route.js +1 -5
  24. package/dist/features/addresses/repository/addresses.repository.js +1 -1
  25. package/dist/features/admin/api/bids/route.js +1 -5
  26. package/dist/features/admin/api/coupons/route.js +1 -5
  27. package/dist/features/admin/api/products/route.js +1 -5
  28. package/dist/features/admin/api/reviews/route.js +1 -5
  29. package/dist/features/admin/components/AdminCarouselEditorView.js +0 -1
  30. package/dist/features/admin/components/AdminFeatureEditorView.js +1 -2
  31. package/dist/features/admin/components/AdminGuideHubView.js +1 -1
  32. package/dist/features/admin/components/AdminProductEditorView.js +4 -1
  33. package/dist/features/admin/components/AdminSiteSettingsView.js +19 -3
  34. package/dist/features/admin/components/OwnerQuickActions.d.ts +7 -1
  35. package/dist/features/admin/components/OwnerQuickActions.js +1 -1
  36. package/dist/features/admin/schemas/admin-editor-forms.js +0 -1
  37. package/dist/features/admin/schemas/firestore.d.ts +52 -0
  38. package/dist/features/admin/schemas/firestore.js +20 -0
  39. package/dist/features/auctions/api/route.js +1 -8
  40. package/dist/features/auctions/components/AuctionDetailPageView.js +1 -7
  41. package/dist/features/auth/hooks/useAuth.js +1 -1
  42. package/dist/features/before-after/api/route.js +1 -5
  43. package/dist/features/blog/api/route.js +1 -8
  44. package/dist/features/blog/components/BlogIndexPageView.d.ts +2 -3
  45. package/dist/features/blog/components/BlogIndexPageView.js +6 -9
  46. package/dist/features/cart/actions/cart-actions.js +1 -1
  47. package/dist/features/cart/api/route.js +1 -5
  48. package/dist/features/cart/utils/guest-cart.js +1 -5
  49. package/dist/features/catalogue/repository/catalogue.repository.js +1 -1
  50. package/dist/features/categories/api/route.js +5 -23
  51. package/dist/features/categories/components/BundlesListView.d.ts +2 -3
  52. package/dist/features/categories/components/BundlesListView.js +2 -5
  53. package/dist/features/categories/components/CategoriesIndexListing.js +47 -5
  54. package/dist/features/categories/components/CategoriesIndexPageView.d.ts +2 -3
  55. package/dist/features/categories/components/CategoriesIndexPageView.js +0 -32
  56. package/dist/features/categories/constants/category-tabs.d.ts +1 -1
  57. package/dist/features/categories/constants/category-tabs.js +7 -0
  58. package/dist/features/categories/hooks/useCategories.d.ts +6 -0
  59. package/dist/features/categories/hooks/useCategories.js +73 -2
  60. package/dist/features/categories/types/index.d.ts +1 -1
  61. package/dist/features/classified/components/ClassifiedDetailPageView.js +2 -8
  62. package/dist/features/collections/api/route.js +1 -5
  63. package/dist/features/consultation/api/route.js +1 -5
  64. package/dist/features/contact/email.js +2 -5
  65. package/dist/features/corporate/api/route.js +1 -5
  66. package/dist/features/digital-codes/components/DigitalCodeDetailPageView.js +1 -7
  67. package/dist/features/events/components/AdminEventEditorView.js +1 -1
  68. package/dist/features/events/components/EventsListPageView.d.ts +2 -3
  69. package/dist/features/events/components/EventsListPageView.js +8 -11
  70. package/dist/features/faq/constants/help-guides.d.ts +8 -5
  71. package/dist/features/faq/constants/help-guides.js +9 -5
  72. package/dist/features/filters/AsyncFacetSection.js +2 -2
  73. package/dist/features/filters/FilterFacetSection.js +5 -5
  74. package/dist/features/filters/RangeFilter.js +2 -2
  75. package/dist/features/filters/SwitchFilter.js +2 -2
  76. package/dist/features/filters/filterUtils.d.ts +0 -2
  77. package/dist/features/filters/filterUtils.js +0 -4
  78. package/dist/features/filters/index.d.ts +1 -1
  79. package/dist/features/filters/index.js +1 -1
  80. package/dist/features/forms/Checkbox.js +4 -3
  81. package/dist/features/forms/Input.js +5 -4
  82. package/dist/features/forms/Radio.js +6 -5
  83. package/dist/features/forms/Select.js +3 -2
  84. package/dist/features/forms/Slider.js +3 -2
  85. package/dist/features/forms/Textarea.js +3 -2
  86. package/dist/features/forms/Toggle.js +3 -3
  87. package/dist/features/forms/index.d.ts +1 -1
  88. package/dist/features/forms/index.js +1 -1
  89. package/dist/features/forms/utils.d.ts +0 -2
  90. package/dist/features/forms/utils.js +0 -4
  91. package/dist/features/grouped/schemas/grouped-listing-form.d.ts +6 -6
  92. package/dist/features/grouped/schemas/index.d.ts +2 -2
  93. package/dist/features/history/utils/guest-history.js +1 -5
  94. package/dist/features/homepage/components/CustomCardsSection.js +8 -2
  95. package/dist/features/homepage/components/FeaturedBundlesSection.d.ts +6 -1
  96. package/dist/features/homepage/components/FeaturedBundlesSection.js +1 -1
  97. package/dist/features/layout/AppLayoutShell.d.ts +23 -1
  98. package/dist/features/layout/AppLayoutShell.js +13 -12
  99. package/dist/features/layout/BottomNavItem.d.ts +9 -1
  100. package/dist/features/layout/BottomNavItem.js +18 -4
  101. package/dist/features/layout/index.d.ts +1 -1
  102. package/dist/features/live/components/LiveItemDetailPageView.js +1 -7
  103. package/dist/features/media/MediaLightbox.js +3 -5
  104. package/dist/features/orders/components/OrderCompletionPanel.d.ts +1 -1
  105. package/dist/features/orders/components/OrderCompletionPanel.js +1 -1
  106. package/dist/features/orders/components/RaiseDisputeForm.js +1 -1
  107. package/dist/features/orders/components/RefundRequestView.js +1 -1
  108. package/dist/features/parts/schemas/firestore.d.ts +274 -0
  109. package/dist/features/parts/schemas/firestore.js +19 -0
  110. package/dist/features/payments/repository/saved-payment-methods.repository.js +1 -1
  111. package/dist/features/pre-orders/api/admin/route.js +1 -5
  112. package/dist/features/pre-orders/components/PreOrderDetailPageView.js +1 -7
  113. package/dist/features/products/api/route.js +1 -3
  114. package/dist/features/products/components/CustomSectionTabContent.js +0 -5
  115. package/dist/features/products/components/GroupMemberPicker.js +2 -2
  116. package/dist/features/products/components/GroupSettingsPanel.js +1 -1
  117. package/dist/features/products/components/PrizeDrawDetailPageView.js +1 -7
  118. package/dist/features/products/components/PrizeDrawEntryActions.d.ts +1 -1
  119. package/dist/features/products/components/PrizeDrawEntryActions.js +1 -1
  120. package/dist/features/products/components/ProductDetailPageView.js +1 -7
  121. package/dist/features/products/repository/products.repository.js +0 -1
  122. package/dist/features/promotions/api/route.js +1 -8
  123. package/dist/features/reviews/components/ReviewsIndexPageView.d.ts +2 -3
  124. package/dist/features/reviews/components/ReviewsIndexPageView.js +9 -12
  125. package/dist/features/seller/api/coupons/route.js +1 -5
  126. package/dist/features/seller/api/products/route.js +1 -5
  127. package/dist/features/seller/components/SellerProductShell.js +1 -2
  128. package/dist/features/seller/hooks/useSellerListingData.js +0 -30
  129. package/dist/features/stores/api/[storeSlug]/auctions/route.js +1 -10
  130. package/dist/features/stores/api/[storeSlug]/products/route.js +1 -10
  131. package/dist/features/stores/components/InteractiveStoreCard.js +9 -1
  132. package/dist/features/stores/components/StoreGuideHubView.js +1 -1
  133. package/dist/features/stores/components/StoresIndexPageView.d.ts +2 -3
  134. package/dist/features/stores/components/StoresIndexPageView.js +6 -9
  135. package/dist/features/stores/schemas/firestore.d.ts +27 -0
  136. package/dist/features/stores/schemas/firestore.js +2 -0
  137. package/dist/features/wishlist/utils/guest-wishlist.js +1 -5
  138. package/dist/index.d.ts +4 -1
  139. package/dist/index.js +28 -1
  140. package/dist/providers/db-firebase/admin-app-lite.js +14 -2
  141. package/dist/providers/db-firebase/admin.js +8 -2
  142. package/dist/react/hooks/useBrowsePageFlags.d.ts +26 -0
  143. package/dist/react/hooks/useBrowsePageFlags.js +18 -0
  144. package/dist/schemas/registry.d.ts +4 -4
  145. package/dist/security/csp.d.ts +32 -2
  146. package/dist/security/csp.js +32 -2
  147. package/dist/styles.css +1 -1
  148. package/dist/tailwind-utilities.css +1 -1
  149. package/dist/ui/components/Button.js +1 -1
  150. package/dist/ui/components/Chart.js +0 -1
  151. package/dist/ui/rich-text/RichText.js +1 -3
  152. package/dist/ui/rich-text/RichTextRenderer.js +1 -3
  153. package/dist/utils/browser-storage.d.ts +14 -0
  154. package/dist/utils/browser-storage.js +18 -0
  155. package/dist/utils/classnames.d.ts +19 -0
  156. package/dist/utils/classnames.js +21 -0
  157. package/dist/utils/search-params.d.ts +44 -0
  158. package/dist/utils/search-params.js +46 -0
  159. package/dist/utils/sieve-builder.d.ts +13 -0
  160. package/dist/utils/sieve-builder.js +22 -0
  161. package/dist/utils/string.formatter.d.ts +12 -0
  162. package/dist/utils/string.formatter.js +17 -0
  163. package/package.json +2 -2
@@ -143,7 +143,7 @@ const ACTION_ICON_SIZE = {
143
143
  md: "md",
144
144
  lg: "lg",
145
145
  };
146
- function ActionIcon({ action, confirmationBody, size, }) {
146
+ function ActionIcon({ action, size, }) {
147
147
  const Glyph = resolveIcon(action?.iconKey);
148
148
  if (!Glyph)
149
149
  return null;
@@ -91,7 +91,6 @@ function useChartPalette(ref) {
91
91
  const series = Array.from({ length: CHART_MAX_SERIES }, (_, i) => v(`--chart-${i + 1}`));
92
92
  if (!series[0] && !warnedMissingStylesheet) {
93
93
  warnedMissingStylesheet = true;
94
- // eslint-disable-next-line no-console
95
94
  console.warn("[appkit] <Chart> found no --chart-* custom properties. " +
96
95
  'Import the stylesheet once at your app root: import "@mohasinac/appkit/styles". ' +
97
96
  "Rendering with recharts' default colours until then.");
@@ -215,7 +215,5 @@ export function RichText({ html, className = "", proseClass = "prose max-w-none
215
215
  if (segments.length === 1 && segments[0].type === "html") {
216
216
  return (_jsx(Div, { ref: ref, className: `appkit-rich-text ${proseClass} ${className}`, dangerouslySetInnerHTML: { __html: segments[0].html } }));
217
217
  }
218
- return (_jsx(Div, { ref: ref, className: `appkit-rich-text ${proseClass} ${className}`, children: segments.map((seg, i) => seg.type === "html" ? (_jsx("div", { className: "appkit-rich-text__seg", dangerouslySetInnerHTML: { __html: seg.html } }, `h${i}`)) : (
219
- // eslint-disable-next-line react/no-array-index-key
220
- _jsx(ChartFromJson, { json: seg.json }, `c${i}`))) }));
218
+ return (_jsx(Div, { ref: ref, className: `appkit-rich-text ${proseClass} ${className}`, children: segments.map((seg, i) => seg.type === "html" ? (_jsx("div", { className: "appkit-rich-text__seg", dangerouslySetInnerHTML: { __html: seg.html } }, `h${i}`)) : (_jsx(ChartFromJson, { json: seg.json }, `c${i}`))) }));
221
219
  }
@@ -41,7 +41,5 @@ export function RichTextRenderer({ html, className = "", proseClass = "prose max
41
41
  if (segments.length === 1 && segments[0].type === "html") {
42
42
  return (_jsx("div", { className: wrapperClass, dangerouslySetInnerHTML: { __html: segments[0].html } }));
43
43
  }
44
- return (_jsx("div", { className: wrapperClass, children: segments.map((seg, i) => seg.type === "html" ? (_jsx("div", { className: "appkit-rich-text__seg", dangerouslySetInnerHTML: { __html: seg.html } }, `h${i}`)) : (
45
- // eslint-disable-next-line react/no-array-index-key
46
- _jsx(ChartFromJson, { json: seg.json }, `c${i}`))) }));
44
+ return (_jsx("div", { className: wrapperClass, children: segments.map((seg, i) => seg.type === "html" ? (_jsx("div", { className: "appkit-rich-text__seg", dangerouslySetInnerHTML: { __html: seg.html } }, `h${i}`)) : (_jsx(ChartFromJson, { json: seg.json }, `c${i}`))) }));
47
45
  }
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Guest-state storage access.
3
+ *
4
+ * The guest cart, guest wishlist and guest history each carried a byte-identical private
5
+ * copy of this before 2026-10-24. All three are `"use client"` modules that are nonetheless
6
+ * imported into module graphs the server renders, which is why the `typeof window` guard is
7
+ * the entire point: reading `window.localStorage` during SSR throws, and the three features
8
+ * that need it are the three a signed-out visitor touches on first paint.
9
+ *
10
+ * Returns `null` rather than throwing or returning a stub — a caller must decide what "no
11
+ * storage" means for it, and a silent no-op stub would make a failed write indistinguishable
12
+ * from a successful one (Root Cause #59).
13
+ */
14
+ export declare function getDefaultStorage(): Storage | null;
@@ -0,0 +1,18 @@
1
+ /**
2
+ * Guest-state storage access.
3
+ *
4
+ * The guest cart, guest wishlist and guest history each carried a byte-identical private
5
+ * copy of this before 2026-10-24. All three are `"use client"` modules that are nonetheless
6
+ * imported into module graphs the server renders, which is why the `typeof window` guard is
7
+ * the entire point: reading `window.localStorage` during SSR throws, and the three features
8
+ * that need it are the three a signed-out visitor touches on first paint.
9
+ *
10
+ * Returns `null` rather than throwing or returning a stub — a caller must decide what "no
11
+ * storage" means for it, and a silent no-op stub would make a failed write indistinguishable
12
+ * from a successful one (Root Cause #59).
13
+ */
14
+ export function getDefaultStorage() {
15
+ if (typeof window === "undefined")
16
+ return null;
17
+ return window.localStorage;
18
+ }
@@ -0,0 +1,19 @@
1
+ /**
2
+ * Join class names, dropping falsy entries.
3
+ *
4
+ * 🛑 This is deliberately NOT named `cn`, and it is deliberately not the same function as
5
+ * the `cn` exported from `style/tailwind`. That one runs `twMerge`, which DEDUPLICATES
6
+ * conflicting Tailwind utilities and therefore picks a winner between them. Both Tailwind
7
+ * configs here set `important: true`, and Root Cause #79 and #106 are both about which of
8
+ * two competing classes wins — so quietly moving twenty components from a plain join onto
9
+ * `twMerge` is a behaviour change in exactly the area that has already cost this project
10
+ * two incidents.
11
+ *
12
+ * Three features had a byte-identical private copy of the plain join before 2026-10-24
13
+ * (`features/filters/filterUtils.ts`, `features/forms/utils.ts`,
14
+ * `features/media/MediaLightbox.tsx`). This is that copy, once.
15
+ *
16
+ * Use `cn` from `style/tailwind` when conflict resolution is wanted; use this when the
17
+ * caller is simply assembling a list.
18
+ */
19
+ export declare function joinClasses(...classes: Array<string | undefined | null | false>): string;
@@ -0,0 +1,21 @@
1
+ /**
2
+ * Join class names, dropping falsy entries.
3
+ *
4
+ * 🛑 This is deliberately NOT named `cn`, and it is deliberately not the same function as
5
+ * the `cn` exported from `style/tailwind`. That one runs `twMerge`, which DEDUPLICATES
6
+ * conflicting Tailwind utilities and therefore picks a winner between them. Both Tailwind
7
+ * configs here set `important: true`, and Root Cause #79 and #106 are both about which of
8
+ * two competing classes wins — so quietly moving twenty components from a plain join onto
9
+ * `twMerge` is a behaviour change in exactly the area that has already cost this project
10
+ * two incidents.
11
+ *
12
+ * Three features had a byte-identical private copy of the plain join before 2026-10-24
13
+ * (`features/filters/filterUtils.ts`, `features/forms/utils.ts`,
14
+ * `features/media/MediaLightbox.tsx`). This is that copy, once.
15
+ *
16
+ * Use `cn` from `style/tailwind` when conflict resolution is wanted; use this when the
17
+ * caller is simply assembling a list.
18
+ */
19
+ export function joinClasses(...classes) {
20
+ return classes.filter(Boolean).join(" ");
21
+ }
@@ -0,0 +1,44 @@
1
+ /**
2
+ * URL search-param readers for route handlers.
3
+ *
4
+ * These were hand-copied into 27 route files before 2026-10-24 — `numParam` into 18
5
+ * (16 byte-identical) and `param` into 9 (all 9 identical). They are here so there is one
6
+ * definition, per the Duplication Decision Framework's "≥3 copies" and "same prop surface"
7
+ * triggers; `audit-duplicate-helpers` keeps it that way.
8
+ *
9
+ * Import this module directly (Root Cause #18) — these are deliberately NOT re-exported
10
+ * from `appkit/src/index.ts`, which is already 10k lines and is what the cold build parses.
11
+ */
12
+ /** Read a raw search param. `null` when absent — never `""` for a missing key. */
13
+ export declare function param(url: URL, key: string): string | null;
14
+ /**
15
+ * Read a numeric search param, falling back when absent or unparseable.
16
+ *
17
+ * 🛑 Uses `Number`, not `parseInt`, so `"12abc"` is rejected to the fallback rather than
18
+ * silently read as 12. That is the behaviour all 16 identical copies had, and a page size
19
+ * of 12 derived from a typo'd param is worse than the default.
20
+ */
21
+ export declare function numParam(url: URL, key: string, fallback: number): number;
22
+ /**
23
+ * Read an integer search param where "absent" and "zero" must stay distinguishable.
24
+ *
25
+ * Separate from `numParam` on purpose: a caller that needs to know whether the param was
26
+ * supplied at all cannot get that from a fallback. `parseInt` here is also deliberate —
27
+ * this one is for tier/depth values where a trailing fragment is tolerable.
28
+ */
29
+ /**
30
+ * Next's `searchParams` prop shape — a repeated query key arrives as an array.
31
+ *
32
+ * Six index page views declared this identically before 2026-10-24.
33
+ */
34
+ export type SearchParamsRecord = Record<string, string | string[]>;
35
+ /**
36
+ * Read one value out of Next's `searchParams`, taking the first of a repeated key.
37
+ *
38
+ * 🛑 Returns `""` for absent, NOT `null` — unlike `param` above, which reads a `URL`. The
39
+ * asymmetry is deliberate and matches what all six copies did: these feed filter builders
40
+ * that treat empty string as "no filter", and a `null` there would print as "null" in a
41
+ * Sieve clause and match nothing (Root Cause #62's shape).
42
+ */
43
+ export declare function firstParam(params: SearchParamsRecord, key: string): string;
44
+ export declare function intParamOrNull(url: URL, key: string): number | null;
@@ -0,0 +1,46 @@
1
+ /**
2
+ * URL search-param readers for route handlers.
3
+ *
4
+ * These were hand-copied into 27 route files before 2026-10-24 — `numParam` into 18
5
+ * (16 byte-identical) and `param` into 9 (all 9 identical). They are here so there is one
6
+ * definition, per the Duplication Decision Framework's "≥3 copies" and "same prop surface"
7
+ * triggers; `audit-duplicate-helpers` keeps it that way.
8
+ *
9
+ * Import this module directly (Root Cause #18) — these are deliberately NOT re-exported
10
+ * from `appkit/src/index.ts`, which is already 10k lines and is what the cold build parses.
11
+ */
12
+ /** Read a raw search param. `null` when absent — never `""` for a missing key. */
13
+ export function param(url, key) {
14
+ return url.searchParams.get(key);
15
+ }
16
+ /**
17
+ * Read a numeric search param, falling back when absent or unparseable.
18
+ *
19
+ * 🛑 Uses `Number`, not `parseInt`, so `"12abc"` is rejected to the fallback rather than
20
+ * silently read as 12. That is the behaviour all 16 identical copies had, and a page size
21
+ * of 12 derived from a typo'd param is worse than the default.
22
+ */
23
+ export function numParam(url, key, fallback) {
24
+ const v = url.searchParams.get(key);
25
+ const n = v !== null ? Number(v) : NaN;
26
+ return Number.isFinite(n) ? n : fallback;
27
+ }
28
+ /**
29
+ * Read one value out of Next's `searchParams`, taking the first of a repeated key.
30
+ *
31
+ * 🛑 Returns `""` for absent, NOT `null` — unlike `param` above, which reads a `URL`. The
32
+ * asymmetry is deliberate and matches what all six copies did: these feed filter builders
33
+ * that treat empty string as "no filter", and a `null` there would print as "null" in a
34
+ * Sieve clause and match nothing (Root Cause #62's shape).
35
+ */
36
+ export function firstParam(params, key) {
37
+ const v = params[key];
38
+ return Array.isArray(v) ? (v[0] ?? "") : (v ?? "");
39
+ }
40
+ export function intParamOrNull(url, key) {
41
+ const v = url.searchParams.get(key);
42
+ if (v === null)
43
+ return null;
44
+ const n = Number.parseInt(v, 10);
45
+ return Number.isFinite(n) ? n : null;
46
+ }
@@ -34,3 +34,16 @@ export declare function expandSieveParam(field: string, value: string | null, op
34
34
  /** Join Sieve clauses with comma (AND) — drops falsy values */
35
35
  export declare function sieveAnd(...clauses: (string | null | undefined | false)[]): string;
36
36
  export { sortBy } from "../constants/sort";
37
+ /**
38
+ * Keep only the Sieve clauses whose field is allow-listed, dropping the rest silently.
39
+ *
40
+ * 🛑 "Silently" is the correct behaviour here and is not Root Cause #59. `sieve.ts` sets
41
+ * `throwExceptions: false`, so an un-allowlisted field is *already* dropped one layer down —
42
+ * this function exists so the drop happens where the allow-list is declared rather than
43
+ * inside the query adapter, where Root Cause #62 showed it turns a scoped page into an
44
+ * unfiltered one. A raw `f=` is attacker-controlled input, so rejecting the request would
45
+ * also make a probe distinguishable from a valid query.
46
+ *
47
+ * Three route files carried a byte-identical private copy before 2026-10-24.
48
+ */
49
+ export declare function validateSieveFilters(raw: string, allowedFields: ReadonlySet<string>): string;
@@ -63,3 +63,25 @@ export function sieveAnd(...clauses) {
63
63
  return clauses.filter(Boolean).join(",");
64
64
  }
65
65
  export { sortBy } from "../constants/sort.js";
66
+ /**
67
+ * Keep only the Sieve clauses whose field is allow-listed, dropping the rest silently.
68
+ *
69
+ * 🛑 "Silently" is the correct behaviour here and is not Root Cause #59. `sieve.ts` sets
70
+ * `throwExceptions: false`, so an un-allowlisted field is *already* dropped one layer down —
71
+ * this function exists so the drop happens where the allow-list is declared rather than
72
+ * inside the query adapter, where Root Cause #62 showed it turns a scoped page into an
73
+ * unfiltered one. A raw `f=` is attacker-controlled input, so rejecting the request would
74
+ * also make a probe distinguishable from a valid query.
75
+ *
76
+ * Three route files carried a byte-identical private copy before 2026-10-24.
77
+ */
78
+ export function validateSieveFilters(raw, allowedFields) {
79
+ return raw
80
+ .split(",")
81
+ .map((c) => c.trim())
82
+ .filter((c) => {
83
+ const m = c.match(/^([^<>=!@]+)\s*(?:==|!=|<=|>=|<|>|@=\*?)/);
84
+ return m ? allowedFields.has(m[1].trim()) : false;
85
+ })
86
+ .join(",");
87
+ }
@@ -19,3 +19,15 @@ export declare function proseMirrorToHtml(value: string): string;
19
19
  * Supports ProseMirror JSON strings, existing HTML, and plain text.
20
20
  */
21
21
  export declare function normalizeRichTextHtml(value: string): string;
22
+ /**
23
+ * Normalize a description field of unknown shape into HTML, for a detail page.
24
+ *
25
+ * Every listing-type detail view reads `product.description`, which is typed loosely
26
+ * because the field holds a plain string on older documents and ProseMirror JSON on newer
27
+ * ones. Seven views had a byte-identical private copy of this before 2026-10-24.
28
+ *
29
+ * 🛑 `JSON.stringify` on a non-string is the fallback, not the intent — it exists so a
30
+ * document carrying an unexpected shape renders *something* legible instead of
31
+ * `[object Object]`. `normalizeRichTextHtml` then decides whether that is already markup.
32
+ */
33
+ export declare function toDescriptionHtml(raw: unknown): string;
@@ -161,3 +161,20 @@ export function normalizeRichTextHtml(value) {
161
161
  }
162
162
  return `<p>${escapeHtml(normalized).replace(/\r?\n/g, "<br />")}</p>`;
163
163
  }
164
+ /**
165
+ * Normalize a description field of unknown shape into HTML, for a detail page.
166
+ *
167
+ * Every listing-type detail view reads `product.description`, which is typed loosely
168
+ * because the field holds a plain string on older documents and ProseMirror JSON on newer
169
+ * ones. Seven views had a byte-identical private copy of this before 2026-10-24.
170
+ *
171
+ * 🛑 `JSON.stringify` on a non-string is the fallback, not the intent — it exists so a
172
+ * document carrying an unexpected shape renders *something* legible instead of
173
+ * `[object Object]`. `normalizeRichTextHtml` then decides whether that is already markup.
174
+ */
175
+ export function toDescriptionHtml(raw) {
176
+ if (!raw)
177
+ return "";
178
+ const s = typeof raw === "string" ? raw : JSON.stringify(raw);
179
+ return normalizeRichTextHtml(s);
180
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mohasinac/appkit",
3
- "version": "4.64.0",
3
+ "version": "4.65.0",
4
4
  "license": "MIT",
5
5
  "publishConfig": {
6
6
  "access": "public"
@@ -234,7 +234,7 @@
234
234
  "scripts": {
235
235
  "postinstall": "node scripts/dedupe-peer-deps.mjs",
236
236
  "build": "node scripts/dedupe-peer-deps.mjs && node -e \"const fs=require('fs');for(const t of ['tsconfig.build.tsbuildinfo','dist']){try{fs.rmSync(t,{recursive:true})}catch(e){}}\" && tsc -p tsconfig.build.json && node scripts/copy-assets.mjs && node scripts/fix-esm-extensions.mjs && tailwindcss -i src/tailwind-input.css -o dist/tailwind-utilities.css --minify && node scripts/bundle-css.mjs && node scripts/verify-css-build.mjs && node scripts/verify-esm-dist.mjs --quick && node scripts/write-build-stamp.mjs",
237
- "prepublishOnly": "npm run build",
237
+ "prepublishOnly": "node scripts/prepublish-gate.mjs && npm run build",
238
238
  "watch": "concurrently --kill-others --restart-tries 0 --names \"ts,css\" \"tsc -p tsconfig.build.json --watch\" \"npm run watch:css\"",
239
239
  "watch:css": "tailwindcss -i src/tailwind-input.css -o dist/tailwind-utilities.css --watch",
240
240
  "audit": "node scripts/audit-violations.mjs",