@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.
- package/dist/.build-stamp.json +3 -3
- package/dist/_internal/server/features/checkout/actions.js +18 -1
- package/dist/_internal/server/features/search/index-build/sources.js +27 -12
- package/dist/_internal/server/features/seo/sitemap.js +78 -12
- package/dist/_internal/server/features/site-settings/adapters.d.ts +1 -1
- package/dist/_internal/server/features/site-settings/adapters.js +16 -1
- package/dist/_internal/server/features/stores/adapters.d.ts +1 -1
- package/dist/_internal/server/features/stores/adapters.js +14 -0
- package/dist/_internal/server/features/stores/tabs.d.ts +19 -15
- package/dist/_internal/server/features/stores/tabs.js +20 -21
- package/dist/_internal/server/jobs/core/analyticsAlertEvaluate.js +9 -4
- package/dist/_internal/server/jobs/core/categoryTaxonomyBackfill.js +22 -0
- package/dist/_internal/server/jobs/core/countersReconcile.js +0 -1
- package/dist/_internal/server/jobs/handlers/onCatalogueSubmittedForApproval.js +1 -1
- package/dist/_internal/shared/features/site-settings/types.d.ts +10 -2
- package/dist/_internal/shared/features/sticker-trace/trace/segment.js +13 -2
- package/dist/_internal/shared/listing-types/feature-flags.d.ts +54 -0
- package/dist/_internal/shared/listing-types/feature-flags.js +72 -0
- package/dist/client.d.ts +5 -2
- package/dist/client.js +8 -1
- package/dist/constants/field-names.d.ts +1 -0
- package/dist/constants/field-names.js +14 -0
- package/dist/features/account/api/[userId]/addresses/route.js +1 -5
- package/dist/features/addresses/repository/addresses.repository.js +1 -1
- package/dist/features/admin/api/bids/route.js +1 -5
- package/dist/features/admin/api/coupons/route.js +1 -5
- package/dist/features/admin/api/products/route.js +1 -5
- package/dist/features/admin/api/reviews/route.js +1 -5
- package/dist/features/admin/components/AdminCarouselEditorView.js +0 -1
- package/dist/features/admin/components/AdminFeatureEditorView.js +1 -2
- package/dist/features/admin/components/AdminGuideHubView.js +1 -1
- package/dist/features/admin/components/AdminProductEditorView.js +4 -1
- package/dist/features/admin/components/AdminSiteSettingsView.js +19 -3
- package/dist/features/admin/components/OwnerQuickActions.d.ts +7 -1
- package/dist/features/admin/components/OwnerQuickActions.js +1 -1
- package/dist/features/admin/schemas/admin-editor-forms.js +0 -1
- package/dist/features/admin/schemas/firestore.d.ts +52 -0
- package/dist/features/admin/schemas/firestore.js +20 -0
- package/dist/features/auctions/api/route.js +1 -8
- package/dist/features/auctions/components/AuctionDetailPageView.js +1 -7
- package/dist/features/auth/hooks/useAuth.js +1 -1
- package/dist/features/before-after/api/route.js +1 -5
- package/dist/features/blog/api/route.js +1 -8
- package/dist/features/blog/components/BlogIndexPageView.d.ts +2 -3
- package/dist/features/blog/components/BlogIndexPageView.js +6 -9
- package/dist/features/cart/actions/cart-actions.js +1 -1
- package/dist/features/cart/api/route.js +1 -5
- package/dist/features/cart/utils/guest-cart.js +1 -5
- package/dist/features/catalogue/repository/catalogue.repository.js +1 -1
- package/dist/features/categories/api/route.js +5 -23
- package/dist/features/categories/components/BundlesListView.d.ts +2 -3
- package/dist/features/categories/components/BundlesListView.js +2 -5
- package/dist/features/categories/components/CategoriesIndexListing.js +47 -5
- package/dist/features/categories/components/CategoriesIndexPageView.d.ts +2 -3
- package/dist/features/categories/components/CategoriesIndexPageView.js +0 -32
- package/dist/features/categories/constants/category-tabs.d.ts +1 -1
- package/dist/features/categories/constants/category-tabs.js +7 -0
- package/dist/features/categories/hooks/useCategories.d.ts +6 -0
- package/dist/features/categories/hooks/useCategories.js +73 -2
- package/dist/features/categories/types/index.d.ts +1 -1
- package/dist/features/classified/components/ClassifiedDetailPageView.js +2 -8
- package/dist/features/collections/api/route.js +1 -5
- package/dist/features/consultation/api/route.js +1 -5
- package/dist/features/contact/email.js +2 -5
- package/dist/features/corporate/api/route.js +1 -5
- package/dist/features/digital-codes/components/DigitalCodeDetailPageView.js +1 -7
- package/dist/features/events/components/AdminEventEditorView.js +1 -1
- package/dist/features/events/components/EventsListPageView.d.ts +2 -3
- package/dist/features/events/components/EventsListPageView.js +8 -11
- package/dist/features/faq/constants/help-guides.d.ts +8 -5
- package/dist/features/faq/constants/help-guides.js +9 -5
- package/dist/features/filters/AsyncFacetSection.js +2 -2
- package/dist/features/filters/FilterFacetSection.js +5 -5
- package/dist/features/filters/RangeFilter.js +2 -2
- package/dist/features/filters/SwitchFilter.js +2 -2
- package/dist/features/filters/filterUtils.d.ts +0 -2
- package/dist/features/filters/filterUtils.js +0 -4
- package/dist/features/filters/index.d.ts +1 -1
- package/dist/features/filters/index.js +1 -1
- package/dist/features/forms/Checkbox.js +4 -3
- package/dist/features/forms/Input.js +5 -4
- package/dist/features/forms/Radio.js +6 -5
- package/dist/features/forms/Select.js +3 -2
- package/dist/features/forms/Slider.js +3 -2
- package/dist/features/forms/Textarea.js +3 -2
- package/dist/features/forms/Toggle.js +3 -3
- package/dist/features/forms/index.d.ts +1 -1
- package/dist/features/forms/index.js +1 -1
- package/dist/features/forms/utils.d.ts +0 -2
- package/dist/features/forms/utils.js +0 -4
- package/dist/features/grouped/schemas/grouped-listing-form.d.ts +6 -6
- package/dist/features/grouped/schemas/index.d.ts +2 -2
- package/dist/features/history/utils/guest-history.js +1 -5
- package/dist/features/homepage/components/CustomCardsSection.js +8 -2
- package/dist/features/homepage/components/FeaturedBundlesSection.d.ts +6 -1
- package/dist/features/homepage/components/FeaturedBundlesSection.js +1 -1
- package/dist/features/layout/AppLayoutShell.d.ts +23 -1
- package/dist/features/layout/AppLayoutShell.js +13 -12
- package/dist/features/layout/BottomNavItem.d.ts +9 -1
- package/dist/features/layout/BottomNavItem.js +18 -4
- package/dist/features/layout/index.d.ts +1 -1
- package/dist/features/live/components/LiveItemDetailPageView.js +1 -7
- package/dist/features/media/MediaLightbox.js +3 -5
- package/dist/features/orders/components/OrderCompletionPanel.d.ts +1 -1
- package/dist/features/orders/components/OrderCompletionPanel.js +1 -1
- package/dist/features/orders/components/RaiseDisputeForm.js +1 -1
- package/dist/features/orders/components/RefundRequestView.js +1 -1
- package/dist/features/parts/schemas/firestore.d.ts +274 -0
- package/dist/features/parts/schemas/firestore.js +19 -0
- package/dist/features/payments/repository/saved-payment-methods.repository.js +1 -1
- package/dist/features/pre-orders/api/admin/route.js +1 -5
- package/dist/features/pre-orders/components/PreOrderDetailPageView.js +1 -7
- package/dist/features/products/api/route.js +1 -3
- package/dist/features/products/components/CustomSectionTabContent.js +0 -5
- package/dist/features/products/components/GroupMemberPicker.js +2 -2
- package/dist/features/products/components/GroupSettingsPanel.js +1 -1
- package/dist/features/products/components/PrizeDrawDetailPageView.js +1 -7
- package/dist/features/products/components/PrizeDrawEntryActions.d.ts +1 -1
- package/dist/features/products/components/PrizeDrawEntryActions.js +1 -1
- package/dist/features/products/components/ProductDetailPageView.js +1 -7
- package/dist/features/products/repository/products.repository.js +0 -1
- package/dist/features/promotions/api/route.js +1 -8
- package/dist/features/reviews/components/ReviewsIndexPageView.d.ts +2 -3
- package/dist/features/reviews/components/ReviewsIndexPageView.js +9 -12
- package/dist/features/seller/api/coupons/route.js +1 -5
- package/dist/features/seller/api/products/route.js +1 -5
- package/dist/features/seller/components/SellerProductShell.js +1 -2
- package/dist/features/seller/hooks/useSellerListingData.js +0 -30
- package/dist/features/stores/api/[storeSlug]/auctions/route.js +1 -10
- package/dist/features/stores/api/[storeSlug]/products/route.js +1 -10
- package/dist/features/stores/components/InteractiveStoreCard.js +9 -1
- package/dist/features/stores/components/StoreGuideHubView.js +1 -1
- package/dist/features/stores/components/StoresIndexPageView.d.ts +2 -3
- package/dist/features/stores/components/StoresIndexPageView.js +6 -9
- package/dist/features/stores/schemas/firestore.d.ts +27 -0
- package/dist/features/stores/schemas/firestore.js +2 -0
- package/dist/features/wishlist/utils/guest-wishlist.js +1 -5
- package/dist/index.d.ts +4 -1
- package/dist/index.js +28 -1
- package/dist/providers/db-firebase/admin-app-lite.js +14 -2
- package/dist/providers/db-firebase/admin.js +8 -2
- package/dist/react/hooks/useBrowsePageFlags.d.ts +26 -0
- package/dist/react/hooks/useBrowsePageFlags.js +18 -0
- package/dist/schemas/registry.d.ts +4 -4
- package/dist/security/csp.d.ts +32 -2
- package/dist/security/csp.js +32 -2
- package/dist/styles.css +1 -1
- package/dist/tailwind-utilities.css +1 -1
- package/dist/ui/components/Button.js +1 -1
- package/dist/ui/components/Chart.js +0 -1
- package/dist/ui/rich-text/RichText.js +1 -3
- package/dist/ui/rich-text/RichTextRenderer.js +1 -3
- package/dist/utils/browser-storage.d.ts +14 -0
- package/dist/utils/browser-storage.js +18 -0
- package/dist/utils/classnames.d.ts +19 -0
- package/dist/utils/classnames.js +21 -0
- package/dist/utils/search-params.d.ts +44 -0
- package/dist/utils/search-params.js +46 -0
- package/dist/utils/sieve-builder.d.ts +13 -0
- package/dist/utils/sieve-builder.js +22 -0
- package/dist/utils/string.formatter.d.ts +12 -0
- package/dist/utils/string.formatter.js +17 -0
- package/package.json +2 -2
|
@@ -43,33 +43,3 @@ export function toCurrency(value) {
|
|
|
43
43
|
}
|
|
44
44
|
return formatCurrency(value);
|
|
45
45
|
}
|
|
46
|
-
/*
|
|
47
|
-
* 🛑 `toRelativeDate` WAS HERE AND IS GONE. Import it from
|
|
48
|
-
* `features/admin/hooks/useAdminListingData` — the defining module now, and
|
|
49
|
-
* the one 44 files already use (Root Cause #18: import from where a symbol is
|
|
50
|
-
* defined, never from a convenience copy).
|
|
51
|
-
*
|
|
52
|
-
* This copy had drifted from that one and the drift was live: for a FUTURE
|
|
53
|
-
* date, `deltaMs` is negative, so `deltaMs < minute` matched and this version
|
|
54
|
-
* answered **"just now"** while the admin version answered **"1m ago"** — two
|
|
55
|
-
* different wrong answers to "when is this deadline", depending on which file
|
|
56
|
-
* a view happened to import from. The admin one is fixed to say "in 48h"; a
|
|
57
|
-
* second implementation is how that fix would have been half-applied.
|
|
58
|
-
*
|
|
59
|
-
* The three sibling helpers below are also duplicated there. They are left
|
|
60
|
-
* alone deliberately: none of them is wrong, and collapsing them is an import
|
|
61
|
-
* rewrite across both feature trees rather than a bug fix.
|
|
62
|
-
*/
|
|
63
|
-
function parseDate(value) {
|
|
64
|
-
if (value instanceof Date)
|
|
65
|
-
return value;
|
|
66
|
-
if (typeof value === "string") {
|
|
67
|
-
const parsed = new Date(value);
|
|
68
|
-
return Number.isNaN(parsed.getTime()) ? null : parsed;
|
|
69
|
-
}
|
|
70
|
-
if (typeof value === "number") {
|
|
71
|
-
const parsed = new Date(value);
|
|
72
|
-
return Number.isNaN(parsed.getTime()) ? null : parsed;
|
|
73
|
-
}
|
|
74
|
-
return null;
|
|
75
|
-
}
|
|
@@ -17,19 +17,10 @@ import { normalizeError } from "../../../../../errors/normalize.js";
|
|
|
17
17
|
import { NextResponse } from "next/server.js";
|
|
18
18
|
import { getProviders } from "../../../../../contracts/index.js";
|
|
19
19
|
const SAFE_STORE_AUCTION_FILTER_FIELDS = new Set(["price", "category"]);
|
|
20
|
-
function validateSieveFilters(raw, allowedFields) {
|
|
21
|
-
return raw
|
|
22
|
-
.split(",")
|
|
23
|
-
.map((c) => c.trim())
|
|
24
|
-
.filter((c) => {
|
|
25
|
-
const m = c.match(/^([^<>=!@]+)\s*(?:==|!=|<=|>=|<|>|@=\*?)/);
|
|
26
|
-
return m ? allowedFields.has(m[1].trim()) : false;
|
|
27
|
-
})
|
|
28
|
-
.join(",");
|
|
29
|
-
}
|
|
30
20
|
import { parseListingParams } from "../../../../../utils/listing-params.js";
|
|
31
21
|
import { SIEVE_OP, sieveFilter } from "../../../../../utils/sieve-builder.js";
|
|
32
22
|
import { PRODUCT_FIELDS } from "../../../../../constants/field-names.js";
|
|
23
|
+
import { validateSieveFilters } from "../../../../../utils/sieve-builder.js";
|
|
33
24
|
const DEFAULT_PAGE = 1;
|
|
34
25
|
const DEFAULT_PAGE_SIZE = 24;
|
|
35
26
|
const DEFAULT_SORT = "-createdAt";
|
|
@@ -19,6 +19,7 @@ import { getProviders } from "../../../../../contracts/index.js";
|
|
|
19
19
|
import { parseListingParams } from "../../../../../utils/listing-params.js";
|
|
20
20
|
import { SIEVE_OP, sieveFilter } from "../../../../../utils/sieve-builder.js";
|
|
21
21
|
import { PRODUCT_FIELDS } from "../../../../../constants/field-names.js";
|
|
22
|
+
import { validateSieveFilters } from "../../../../../utils/sieve-builder.js";
|
|
22
23
|
const DEFAULT_PAGE = 1;
|
|
23
24
|
const DEFAULT_PAGE_SIZE = 24;
|
|
24
25
|
const DEFAULT_SORT = "-createdAt";
|
|
@@ -28,16 +29,6 @@ const SAFE_STORE_PRODUCT_FILTER_FIELDS = new Set([
|
|
|
28
29
|
"brand",
|
|
29
30
|
"condition",
|
|
30
31
|
]);
|
|
31
|
-
function validateSieveFilters(raw, allowedFields) {
|
|
32
|
-
return raw
|
|
33
|
-
.split(",")
|
|
34
|
-
.map((c) => c.trim())
|
|
35
|
-
.filter((c) => {
|
|
36
|
-
const m = c.match(/^([^<>=!@]+)\s*(?:==|!=|<=|>=|<|>|@=\*?)/);
|
|
37
|
-
return m ? allowedFields.has(m[1].trim()) : false;
|
|
38
|
-
})
|
|
39
|
-
.join(",");
|
|
40
|
-
}
|
|
41
32
|
// --- GET /api/stores/[storeSlug]/products -------------------------------------
|
|
42
33
|
export async function GET(request, context) {
|
|
43
34
|
try {
|
|
@@ -16,6 +16,14 @@ export function InteractiveStoreCard({ store, href, selectable, isSelected = fal
|
|
|
16
16
|
const [logoBroken, setLogoBroken] = useState(false);
|
|
17
17
|
const hasLogo = Boolean(store.storeLogoURL) && !logoBroken;
|
|
18
18
|
const hasBanner = Boolean(store.storeBannerURL);
|
|
19
|
+
/*
|
|
20
|
+
* The rating badge used to live ONLY as an overlay on the banner, so making
|
|
21
|
+
* the banner conditional would have silently dropped the rating from every
|
|
22
|
+
* bannerless store — a hidden second effect of a layout change (Root Cause
|
|
23
|
+
* #57's family). It renders in the stats row instead when there is no banner
|
|
24
|
+
* to sit on, so the card never loses the number.
|
|
25
|
+
*/
|
|
26
|
+
const hasRating = store.averageRating != null && store.averageRating > 0;
|
|
19
27
|
const longPress = useLongPress(() => onSelect?.(store.id, !isSelected));
|
|
20
28
|
const card = (_jsxs(Div, { className: [
|
|
21
29
|
"group relative flex flex-col h-full overflow-hidden rounded-xl border bg-[var(--appkit-color-surface)] shadow-sm transition-shadow hover:shadow-lg",
|
|
@@ -26,7 +34,7 @@ export function InteractiveStoreCard({ store, href, selectable, isSelected = fal
|
|
|
26
34
|
].join(" "), onMouseDown: !isSelected ? longPress.onMouseDown : undefined, onMouseUp: !isSelected ? longPress.onMouseUp : undefined, onMouseLeave: !isSelected ? longPress.onMouseLeave : undefined, onTouchStart: !isSelected ? longPress.onTouchStart : undefined, onTouchEnd: !isSelected ? longPress.onTouchEnd : undefined, onTouchCancel: !isSelected ? longPress.onTouchCancel : undefined, children: [onSelect && (_jsx(BaseListingCard.Checkbox, { selected: isSelected, onSelect: (e) => {
|
|
27
35
|
e.preventDefault();
|
|
28
36
|
onSelect(store.id, !isSelected);
|
|
29
|
-
}, label: isSelected ? "Deselect store" : "Select store", position: "top-2 left-2", className: selectable || isSelected ? "opacity-100" : "opacity-0 pointer-events-none group-hover:opacity-100 group-hover:pointer-events-auto transition-opacity" })), _jsxs(Link, { href: href, className: "flex flex-col flex-1 min-h-0", children: [_jsxs(Div, { surface: "muted", className: `relative aspect-video w-full ${__O.hidden} flex-shrink-0`, children: [
|
|
37
|
+
}, label: isSelected ? "Deselect store" : "Select store", position: "top-2 left-2", className: selectable || isSelected ? "opacity-100" : "opacity-0 pointer-events-none group-hover:opacity-100 group-hover:pointer-events-auto transition-opacity" })), _jsxs(Link, { href: href, className: "flex flex-col flex-1 min-h-0", children: [hasBanner && (_jsxs(Div, { surface: "muted", className: `relative aspect-video w-full ${__O.hidden} flex-shrink-0`, children: [_jsx(MediaImage, { src: store.storeBannerURL, alt: `${store.storeName} banner`, size: "banner", className: "h-full w-full object-cover transition-transform duration-500 group-hover:scale-105" }), _jsx(Scrim, { direction: "bottom-up", intensity: "subtle", className: "absolute inset-x-0 bottom-0 h-12" }), hasRating && (_jsxs(Div, { className: CLS_RATING_BADGE, children: ["\u2605 ", store.averageRating.toFixed(1)] }))] })), _jsxs(Stack, { paddingY: "b-md", className: "flex-1", padding: "x-md", children: [_jsx(Row, { className: hasBanner ? "-mt-5 mb-2" : "mt-3 mb-2", align: "end", justify: "between", children: _jsx(Div, { className: "flex-shrink-0", children: hasLogo ? (_jsx(Div, { className: "relative h-10 w-10 border-2 border-[var(--appkit-color-border-subtle)] bg-[var(--appkit-color-surface)]", rounded: "lg", shadow: "md", overflow: "hidden", children: _jsx(MediaImage, { src: store.storeLogoURL, alt: store.storeName, size: "thumbnail", onError: () => setLogoBroken(true) }) })) : (_jsx(Row, { textWeight: "bold", textSize: "base", className: "h-10 w-10 border-2 border-white bg-primary/10 dark:bg-primary/20 text-primary", align: "center", justify: "center", rounded: "lg", shadow: "md", children: initial })) }) }), _jsx(Heading, { level: 3, className: `group-hover:text-primary transition-colors`, truncate: true, size: "sm", weight: "bold", color: "primary", children: store.storeName }), _jsx(SellerTierBadge, { isVerified: store.isVerified, sellerTier: store.sellerTier, size: "xs" }), store.storeDescription ? (_jsx(RichText, { html: normalizeRichTextHtml(store.storeDescription), proseClass: "prose prose-sm max-w-none dark:prose-invert prose-p:my-0", className: "mt-1 line-clamp-2 text-[length:var(--appkit-text-xs)] text-[var(--appkit-color-text-muted)] flex-1" })) : (_jsx(Div, { className: "flex-1" })), _jsxs(Row, { color: "muted", textSize: "xs", gap: "sm", className: "mt-2.5", wrap: true, children: [!hasBanner && hasRating && (_jsxs(Span, { layout: "flex", gap: "2xs", className: "text-warning", children: [_jsx(Span, { "aria-hidden": "true", children: "\u2605" }), " ", store.averageRating.toFixed(1)] })), store.totalProducts != null && store.totalProducts > 0 && (_jsxs(Span, { layout: "flex", gap: "2xs", children: [_jsx(Span, { "aria-hidden": "true", children: "\uD83D\uDCE6" }), " ", store.totalProducts, " ", labels.products ?? "products"] })), store.itemsSold != null && store.itemsSold > 0 && (_jsxs(Span, { layout: "flex", gap: "2xs", children: [_jsx(Span, { "aria-hidden": "true", children: "\uD83D\uDECD\uFE0F" }), " ", store.itemsSold, " ", labels.sold ?? "sold"] }))] }), _jsxs(Row, { border: "top-subtle", paddingY: "y-xs-tall", className: "mt-3", align: "center", justify: "between", children: [_jsxs(Span, { size: "xs", weight: "semibold", className: "text-primary group-hover:underline transition-colors", children: [labels.visitStore ?? "Visit store", " \u2192"] }), store.totalReviews != null && store.totalReviews > 0 && (_jsxs(Span, { size: "xs", color: "muted", children: [store.totalReviews, " ", labels.reviews ?? "reviews"] }))] })] })] })] }));
|
|
30
38
|
if (!renderOwnerActions)
|
|
31
39
|
return card;
|
|
32
40
|
/*
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import { Package, ShoppingBag, Wallet, Settings, Shield, Headphones, CheckCircle2, Circle, BookOpen
|
|
2
|
+
import { Package, ShoppingBag, Wallet, Settings, Shield, Headphones, CheckCircle2, Circle, BookOpen } from "lucide-react";
|
|
3
3
|
import { Div, Grid, Heading, Progress, Row, Section, Stack, Text, TextLink } from "../../../ui/index.js";
|
|
4
4
|
import { SellerStandingPanel } from "./SellerStandingPanel.js";
|
|
5
5
|
import { ROUTES } from "../../../next/routing/route-map.js";
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import React from "react";
|
|
2
|
-
type
|
|
2
|
+
import type { SearchParamsRecord } from "../../../utils/search-params";
|
|
3
3
|
export interface StoresIndexPageViewProps {
|
|
4
|
-
searchParams?:
|
|
4
|
+
searchParams?: SearchParamsRecord;
|
|
5
5
|
/**
|
|
6
6
|
* Restrict to verified stores and retitle the page. This is what /sellers is.
|
|
7
7
|
*
|
|
@@ -19,4 +19,3 @@ export interface StoresIndexPageViewProps {
|
|
|
19
19
|
heading?: string;
|
|
20
20
|
}
|
|
21
21
|
export declare function StoresIndexPageView({ searchParams, verifiedOnly, heading, }: StoresIndexPageViewProps): Promise<React.JSX.Element>;
|
|
22
|
-
export {};
|
|
@@ -6,14 +6,11 @@ import { StoresIndexListing } from "./StoresIndexListing.js";
|
|
|
6
6
|
import { safeRead } from "../../../errors/safe-read.js";
|
|
7
7
|
import { hidePublicTestData } from "../../../_internal/server/features/tester/visibility.js";
|
|
8
8
|
import { STORE_FIELDS } from "../../../constants/field-names.js";
|
|
9
|
-
|
|
10
|
-
const v = params[key];
|
|
11
|
-
return Array.isArray(v) ? v[0] ?? "" : v ?? "";
|
|
12
|
-
}
|
|
9
|
+
import { firstParam } from "../../../utils/search-params.js";
|
|
13
10
|
export async function StoresIndexPageView({ searchParams = {}, verifiedOnly = false, heading = "Stores", }) {
|
|
14
|
-
const sort =
|
|
15
|
-
const page = Number(
|
|
16
|
-
const pageSize = Number(
|
|
11
|
+
const sort = firstParam(searchParams, "sort") || "-createdAt";
|
|
12
|
+
const page = Number(firstParam(searchParams, "page")) || 1;
|
|
13
|
+
const pageSize = Number(firstParam(searchParams, "pageSize")) || 24;
|
|
17
14
|
/*
|
|
18
15
|
* 🛑 `q` MUST be read here, and passed as the third argument exactly as
|
|
19
16
|
* /api/stores does — `listStores(model, true, { search })`.
|
|
@@ -29,14 +26,14 @@ export async function StoresIndexPageView({ searchParams = {}, verifiedOnly = fa
|
|
|
29
26
|
* frozen for that query key. Root Cause #30 — the SSR builder has to compute
|
|
30
27
|
* the same filters the client would, because it never gets a second chance.
|
|
31
28
|
*/
|
|
32
|
-
const q =
|
|
29
|
+
const q = firstParam(searchParams, "q").trim();
|
|
33
30
|
/*
|
|
34
31
|
* The rating facet must be applied HERE too, not only by the client. This
|
|
35
32
|
* result is handed to <StoresIndexListing initialData=…>, and public listing
|
|
36
33
|
* hooks set staleTime: Infinity when given SSR data — so an unfiltered first
|
|
37
34
|
* paint is frozen for that query key and never self-corrects (Root Cause #30).
|
|
38
35
|
*/
|
|
39
|
-
const ratingRaw =
|
|
36
|
+
const ratingRaw = firstParam(searchParams, "rating");
|
|
40
37
|
const minRating = ratingRaw
|
|
41
38
|
? Math.max(...ratingRaw.split("|").map(Number).filter((n) => Number.isFinite(n)))
|
|
42
39
|
: undefined;
|
|
@@ -415,6 +415,31 @@ export interface StoreDocument extends BaseDocument {
|
|
|
415
415
|
* than trusting that nobody adds it.
|
|
416
416
|
*/
|
|
417
417
|
returnsDirectToSeller?: boolean;
|
|
418
|
+
/**
|
|
419
|
+
* Does this store publish a WhatsApp catalogue feed?
|
|
420
|
+
*
|
|
421
|
+
* 🛑 **ABSENT MEANS ENABLED**, and the polarity is load-bearing rather than a
|
|
422
|
+
* convenience. Every store that predates this field is already feeding a live
|
|
423
|
+
* Commerce Manager data source; a `=== true` reader would turn all of them off
|
|
424
|
+
* at once, and Meta EMPTIES a connected catalogue the first time a scheduled
|
|
425
|
+
* fetch returns a file with no rows. So the only state that disables a feed is
|
|
426
|
+
* somebody deliberately writing `false` — the same `!== false` kill-switch
|
|
427
|
+
* polarity as `siteSettings.search.indexEnabled`, and for the same reason.
|
|
428
|
+
*
|
|
429
|
+
* 🛑 Disabling is DESTRUCTIVE on Meta's side and that is the point. The feed
|
|
430
|
+
* route answers a disabled store with **200 and a header row only**, which is
|
|
431
|
+
* what makes the seller's next scheduled fetch clear their WhatsApp catalogue.
|
|
432
|
+
* Its 404 branch (unpublished / suspended / missing store) exists to avoid
|
|
433
|
+
* exactly that outcome for a TEMPORARY state. The distinction is intent: a
|
|
434
|
+
* suspension is not a decision to unpublish a catalogue, and a seller flipping
|
|
435
|
+
* this switch is.
|
|
436
|
+
*
|
|
437
|
+
* 🛑 PRIVATE in the public store projection. It describes a seller's own
|
|
438
|
+
* distribution plumbing, not the goods — and publishing it would advertise
|
|
439
|
+
* which stores are reachable on WhatsApp, which `whatsappNotifySubscribed`
|
|
440
|
+
* is private for too.
|
|
441
|
+
*/
|
|
442
|
+
whatsappFeedEnabled?: boolean;
|
|
418
443
|
/**
|
|
419
444
|
* Who changed what, when, and why. See § "Status History" in CLAUDE.md.
|
|
420
445
|
*
|
|
@@ -479,6 +504,8 @@ export declare const STORE_FIELDS: {
|
|
|
479
504
|
readonly TIER_LOCKED_BY_ADMIN: "tierLockedByAdmin";
|
|
480
505
|
readonly SUSPENSION_REASON: "suspensionReason";
|
|
481
506
|
readonly CAPABILITIES: "capabilities";
|
|
507
|
+
/** Per-store WhatsApp catalogue feed kill switch. Absent means ENABLED. */
|
|
508
|
+
readonly WHATSAPP_FEED_ENABLED: "whatsappFeedEnabled";
|
|
482
509
|
readonly STATUS_VALUES: {
|
|
483
510
|
readonly PENDING: "pending";
|
|
484
511
|
readonly ACTIVE: "active";
|
|
@@ -92,6 +92,8 @@ export const STORE_FIELDS = {
|
|
|
92
92
|
TIER_LOCKED_BY_ADMIN: "tierLockedByAdmin",
|
|
93
93
|
SUSPENSION_REASON: "suspensionReason",
|
|
94
94
|
CAPABILITIES: "capabilities",
|
|
95
|
+
/** Per-store WhatsApp catalogue feed kill switch. Absent means ENABLED. */
|
|
96
|
+
WHATSAPP_FEED_ENABLED: "whatsappFeedEnabled",
|
|
95
97
|
STATUS_VALUES: {
|
|
96
98
|
PENDING: "pending",
|
|
97
99
|
ACTIVE: "active",
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { normalizeError } from "../../../errors/normalize.js";
|
|
2
|
+
import { getDefaultStorage } from "../../../utils/browser-storage.js";
|
|
2
3
|
const DEFAULT_GUEST_WISHLIST_KEY = process.env.NEXT_PUBLIC_APP_ID
|
|
3
4
|
? `${process.env.NEXT_PUBLIC_APP_ID}_guest_wishlist`
|
|
4
5
|
: "guest_wishlist";
|
|
@@ -12,11 +13,6 @@ function dispatchGuestWishlistChange() {
|
|
|
12
13
|
return;
|
|
13
14
|
window.dispatchEvent(new Event(GUEST_WISHLIST_CHANGE_EVENT));
|
|
14
15
|
}
|
|
15
|
-
function getDefaultStorage() {
|
|
16
|
-
if (typeof window === "undefined")
|
|
17
|
-
return null;
|
|
18
|
-
return window.localStorage;
|
|
19
|
-
}
|
|
20
16
|
function readItems(storage, key) {
|
|
21
17
|
if (!storage)
|
|
22
18
|
return [];
|
package/dist/index.d.ts
CHANGED
|
@@ -2188,7 +2188,8 @@ export { getProductSortOptions } from "./features/products/index";
|
|
|
2188
2188
|
export { getProductTableColumns } from "./features/products/index";
|
|
2189
2189
|
export { normalizeListingType, isAuctionListing, isPreOrderListing, isStandardListing, isPrizeDrawListing, isClassifiedListing, isDigitalCodeListing, isLiveListing, isArtListing, isStickersListing, } from "./features/products/index";
|
|
2190
2190
|
export { isFinalSale, acceptsChangeOfMindReturns, } from "./features/products/constants/final-sale";
|
|
2191
|
-
export { isListingTypeEnabled, isCategoryTypeEnabled, enabledListingTypes, enabledCategoryTypes, ALL_LISTING_TYPES, isListingType, } from "./_internal/shared/listing-types/feature-flags";
|
|
2191
|
+
export { isListingTypeEnabled, isCategoryTypeEnabled, enabledListingTypes, enabledCategoryTypes, ALL_LISTING_TYPES, isListingType, isBrowsePageEnabled, enabledBrowsePages, ALL_BROWSE_PAGES, isBrowsePageKey, } from "./_internal/shared/listing-types/feature-flags";
|
|
2192
|
+
export type { BrowsePageKey } from "./_internal/shared/listing-types/feature-flags";
|
|
2192
2193
|
export { ADVANCED_LISTING_TYPE_CAPABILITY, ADVANCED_LISTING_TYPES, BASIC_LISTING_TYPES, LISTING_TYPE_TIER, assertListingTypeAllowed, hasAdvancedListingTypes, isAdvancedListingType, isListingTypeAllowedFor, listingTypesAllowedFor, } from "./_internal/shared/listing-types/tiers";
|
|
2193
2194
|
export type { ListingTypeAccessContext, ListingTypeTier, } from "./_internal/shared/listing-types/tiers";
|
|
2194
2195
|
export { actionTracker, setActionTrackerSink, resetActionTrackerSink, type ActionEvent, type ActionTrackerSink, } from "./_internal/shared/listing-types/action-tracker";
|
|
@@ -2852,6 +2853,8 @@ export { TAX_CODES_COLLECTION, TAX_CODE_PREFIX, TAX_CODE_FIELDS, TAX_CODE_SIEVE_
|
|
|
2852
2853
|
export type { TaxCodeDocument, TaxCodeCreateInput, TaxCodeUpdateInput, GstRate, HsnChapter, } from "./features/tax-codes/schemas/firestore";
|
|
2853
2854
|
export { taxCodeFormSchema, GST_RATE_OPTIONS, HSN_CHAPTER_OPTIONS } from "./features/tax-codes/schemas/tax-code-form";
|
|
2854
2855
|
export type { TaxCodeFormValues } from "./features/tax-codes/schemas/tax-code-form";
|
|
2856
|
+
export { EVENT_LIKES_COLLECTION } from "./features/events/schemas/likes";
|
|
2857
|
+
export { DISPUTE_REASON_COLLECTION } from "./features/dispute-reasons/schemas/firestore";
|
|
2855
2858
|
export { taxCodeCreateSchema, taxCodeUpdateSchema, } from "./features/tax-codes/schemas/tax-codes.validators";
|
|
2856
2859
|
export type { TaxCodeCreatePayload, TaxCodeUpdatePayload, } from "./features/tax-codes/schemas/tax-codes.validators";
|
|
2857
2860
|
export type { ProductFeatureDocument, ProductFeatureCreateInput, ProductFeatureUpdateInput, ProductFeatureScope, ProductFeatureCategory, ProductFeatureProductType, ProductFeatureIcon, } from "./features/products/schemas/product-features";
|
package/dist/index.js
CHANGED
|
@@ -4075,7 +4075,13 @@ export { normalizeListingType, isAuctionListing, isPreOrderListing, isStandardLi
|
|
|
4075
4075
|
// `finalSale` field. See the module docstring for why `!p.finalSale` is wrong.
|
|
4076
4076
|
export { isFinalSale, acceptsChangeOfMindReturns, } from "./features/products/constants/final-sale.js";
|
|
4077
4077
|
// SB-UNI-X4 2026-05-13 â€" per-type feature-flag helpers.
|
|
4078
|
-
export { isListingTypeEnabled, isCategoryTypeEnabled, enabledListingTypes, enabledCategoryTypes, ALL_LISTING_TYPES, isListingType,
|
|
4078
|
+
export { isListingTypeEnabled, isCategoryTypeEnabled, enabledListingTypes, enabledCategoryTypes, ALL_LISTING_TYPES, isListingType,
|
|
4079
|
+
// Browse-section gates — /categories, /brands, /franchise, /stores.
|
|
4080
|
+
// Exported HERE rather than from server.ts for the reason given in that
|
|
4081
|
+
// file's ALL_LISTING_TYPES note: `server-entry.ts` re-exports index, so a
|
|
4082
|
+
// Server Component already reaches these from the bare specifier and a
|
|
4083
|
+
// second path would leak an `_internal/` symbol through a third barrel.
|
|
4084
|
+
isBrowsePageEnabled, enabledBrowsePages, ALL_BROWSE_PAGES, isBrowsePageKey, } from "./_internal/shared/listing-types/feature-flags.js";
|
|
4079
4085
|
// Basic/advanced listing-type split — which types a store may create.
|
|
4080
4086
|
// Pure (no firebase-admin in its import chain), so it is safe in index.ts.
|
|
4081
4087
|
export { ADVANCED_LISTING_TYPE_CAPABILITY, ADVANCED_LISTING_TYPES, BASIC_LISTING_TYPES, LISTING_TYPE_TIER, assertListingTypeAllowed, hasAdvancedListingTypes, isAdvancedListingType, isListingTypeAllowedFor, listingTypesAllowedFor, } from "./_internal/shared/listing-types/tiers.js";
|
|
@@ -5140,6 +5146,27 @@ export { PRODUCT_FEATURES_COLLECTION, PRODUCT_FEATURE_PREFIX, MAX_STORE_CUSTOM_F
|
|
|
5140
5146
|
// Tax codes feature schemas (B3) — HSN + GST rate as an entity.
|
|
5141
5147
|
export { TAX_CODES_COLLECTION, TAX_CODE_PREFIX, TAX_CODE_FIELDS, TAX_CODE_SIEVE_FIELDS, TAX_CODE_UPDATABLE_FIELDS, GST_RATE_VALUES, createTaxCodeId, isValidHsnCode, } from "./features/tax-codes/schemas/firestore.js";
|
|
5142
5148
|
export { taxCodeFormSchema, GST_RATE_OPTIONS, HSN_CHAPTER_OPTIONS } from "./features/tax-codes/schemas/tax-code-form.js";
|
|
5149
|
+
/*
|
|
5150
|
+
* 🛑 THE SEED LOADER DESTRUCTURES THESE TWO OFF THE BARREL, so an unexported
|
|
5151
|
+
* collection name is not a tidiness issue — it is a collection that can
|
|
5152
|
+
* never load.
|
|
5153
|
+
*
|
|
5154
|
+
* `seed-cli.mjs` reads `COLLECTION_MAP` from one big destructure of
|
|
5155
|
+
* `@mohasinac/appkit`. A name missing there is `undefined`, which Firestore
|
|
5156
|
+
* rejects as `Value for argument "collectionPath" is not a valid resource
|
|
5157
|
+
* path` — so `appkit-seed status` and `load` both error on that row alone
|
|
5158
|
+
* while every other collection succeeds. Measured 2026-10-24: eventLikes
|
|
5159
|
+
* (23 rows) and disputeReasons (11 rows) were both stuck at 0 in the
|
|
5160
|
+
* database for exactly this reason, and the error names the path, never the
|
|
5161
|
+
* missing export.
|
|
5162
|
+
*
|
|
5163
|
+
* Exported straight from the defining schema module rather than through a
|
|
5164
|
+
* feature index (Root Cause #18). Both files carry ONLY `import type`, which
|
|
5165
|
+
* the compiler erases, so neither drags a runtime chain into the client
|
|
5166
|
+
* bundle — the check that matters before adding anything here.
|
|
5167
|
+
*/
|
|
5168
|
+
export { EVENT_LIKES_COLLECTION } from "./features/events/schemas/likes.js";
|
|
5169
|
+
export { DISPUTE_REASON_COLLECTION } from "./features/dispute-reasons/schemas/firestore.js";
|
|
5143
5170
|
// Wire contracts for /api/admin/tax-codes. BOTH are .strict(): z.object()
|
|
5144
5171
|
// strips unknown keys (a PATCH returning 200 having written nothing),
|
|
5145
5172
|
// .passthrough() writes whatever arrives.
|
|
@@ -30,7 +30,13 @@ export function getAdminAppLite() {
|
|
|
30
30
|
process.env.NEXT_PUBLIC_FIREBASE_STORAGE_BUCKET?.trim();
|
|
31
31
|
let app;
|
|
32
32
|
if (nodeFs().existsSync(keyPath)) {
|
|
33
|
-
|
|
33
|
+
/*
|
|
34
|
+
* 🛑 A `const sa = JSON.parse(readFileSync(keyPath))` sat here and was removed
|
|
35
|
+
* 2026-10-24: the parsed object was never used, because `cert(keyPath)` below
|
|
36
|
+
* takes the PATH and reads and validates the file itself. The only thing the
|
|
37
|
+
* extra parse bought was a second read of a service-account key on every
|
|
38
|
+
* cold start.
|
|
39
|
+
*/
|
|
34
40
|
const dbUrl = resolveRtdbUrl();
|
|
35
41
|
app = initializeApp({ credential: cert(keyPath), databaseURL: dbUrl, ...(storageBucket && { storageBucket }) });
|
|
36
42
|
}
|
|
@@ -63,7 +69,13 @@ export function getAdminAppLite() {
|
|
|
63
69
|
const firebaseConfig = process.env.FIREBASE_CONFIG
|
|
64
70
|
? JSON.parse(process.env.FIREBASE_CONFIG)
|
|
65
71
|
: {};
|
|
66
|
-
|
|
72
|
+
/*
|
|
73
|
+
* 🛑 A `projectId` was computed here from FIREBASE_CONFIG/GCLOUD_PROJECT and
|
|
74
|
+
* never passed, which was CORRECT but dead: this is the ADC branch, and
|
|
75
|
+
* `initializeApp()` with no credential is exactly what makes the SDK resolve
|
|
76
|
+
* both credential and project from the metadata server. Removed 2026-10-24
|
|
77
|
+
* rather than wired, because passing it would override that resolution.
|
|
78
|
+
*/
|
|
67
79
|
const dbUrl = firebaseConfig.databaseURL ??
|
|
68
80
|
resolveRtdbUrl();
|
|
69
81
|
app = initializeApp({
|
|
@@ -83,7 +83,10 @@ export function getAdminApp() {
|
|
|
83
83
|
let app;
|
|
84
84
|
try {
|
|
85
85
|
if (nodeFs().existsSync(keyPath)) {
|
|
86
|
-
|
|
86
|
+
/*
|
|
87
|
+
* 🛑 Removed 2026-10-24 — see the matching note in admin-app-lite.ts. The
|
|
88
|
+
* parsed service account was never used; `cert(keyPath)` reads the file.
|
|
89
|
+
*/
|
|
87
90
|
// Was NEXT_PUBLIC-only here while the other two branches honoured
|
|
88
91
|
// FIREBASE_ADMIN_DATABASE_URL, so setting it had no effect on this path.
|
|
89
92
|
const dbUrl = resolveRtdbUrl();
|
|
@@ -118,7 +121,10 @@ export function getAdminApp() {
|
|
|
118
121
|
const firebaseConfig = process.env.FIREBASE_CONFIG
|
|
119
122
|
? JSON.parse(process.env.FIREBASE_CONFIG)
|
|
120
123
|
: {};
|
|
121
|
-
|
|
124
|
+
/*
|
|
125
|
+
* 🛑 Removed 2026-10-24 — see admin-app-lite.ts. Computed, never passed, and
|
|
126
|
+
* passing it would defeat ADC's own project resolution.
|
|
127
|
+
*/
|
|
122
128
|
// FIREBASE_CONFIG is injected by the Functions runtime and is
|
|
123
129
|
// authoritative there; env vars are the fallback for other runtimes.
|
|
124
130
|
const dbUrl = firebaseConfig.databaseURL || resolveRtdbUrl();
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { type BrowsePageKey } from "../../_internal/shared/listing-types/feature-flags";
|
|
2
|
+
/**
|
|
3
|
+
* Which public browse sections are open, as configured in
|
|
4
|
+
* `siteSettings.listings.browsePages`.
|
|
5
|
+
*
|
|
6
|
+
* The sibling of `useListingTypeFlags`, and built the same way on purpose:
|
|
7
|
+
* keyed on the union via `ALL_BROWSE_PAGES` so a new section cannot be
|
|
8
|
+
* silently missed, and DEFAULTING TO OPEN while settings load so the first
|
|
9
|
+
* paint does not briefly hide navigation that is actually enabled.
|
|
10
|
+
*
|
|
11
|
+
* 🛑 The permissive default matters more here than it does for listing types.
|
|
12
|
+
* This hook decides whether a nav entry RENDERS, so a `false` default would
|
|
13
|
+
* blank the navbar on every cold load until the settings request resolved —
|
|
14
|
+
* visible flicker on every page of the site rather than one empty browse tab.
|
|
15
|
+
*/
|
|
16
|
+
export type BrowsePageFlagsShape = Partial<Record<BrowsePageKey, boolean>>;
|
|
17
|
+
export type BrowsePageFlags = Record<BrowsePageKey, boolean> & {
|
|
18
|
+
/** True when the section is open (or the setting has not loaded yet). */
|
|
19
|
+
isEnabled: (page: BrowsePageKey) => boolean;
|
|
20
|
+
/**
|
|
21
|
+
* True when a link may be shown — i.e. it points at no browse section, or at
|
|
22
|
+
* one that is open. Pass an href straight from a nav or footer constant.
|
|
23
|
+
*/
|
|
24
|
+
allowsHref: (href: string | undefined | null) => boolean;
|
|
25
|
+
};
|
|
26
|
+
export declare function useBrowsePageFlags(): BrowsePageFlags;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { useSiteSettings } from "../../core/hooks/useSiteSettings.js";
|
|
3
|
+
import { ALL_BROWSE_PAGES, browsePageForPath, } from "../../_internal/shared/listing-types/feature-flags.js";
|
|
4
|
+
export function useBrowsePageFlags() {
|
|
5
|
+
const { data } = useSiteSettings();
|
|
6
|
+
const bp = data?.listings?.browsePages;
|
|
7
|
+
// Closed only when EXPLICITLY false — a missing flag, or settings that have
|
|
8
|
+
// not loaded, means open.
|
|
9
|
+
const flags = Object.fromEntries(ALL_BROWSE_PAGES.map((page) => [page, bp?.[page] !== false]));
|
|
10
|
+
return {
|
|
11
|
+
...flags,
|
|
12
|
+
isEnabled: (page) => flags[page],
|
|
13
|
+
allowsHref: (href) => {
|
|
14
|
+
const page = browsePageForPath(href);
|
|
15
|
+
return page === null || flags[page];
|
|
16
|
+
},
|
|
17
|
+
};
|
|
18
|
+
}
|
|
@@ -2573,7 +2573,7 @@ export declare const SCHEMAS: {
|
|
|
2573
2573
|
isActive: boolean;
|
|
2574
2574
|
isFeatured: boolean;
|
|
2575
2575
|
productIds: string[];
|
|
2576
|
-
groupTheme: "set" | "
|
|
2576
|
+
groupTheme: "set" | "character" | "related" | "lineage" | "generic";
|
|
2577
2577
|
minActiveMembers: number;
|
|
2578
2578
|
visibilityStatus: "hidden" | "visible";
|
|
2579
2579
|
activeMemberCount: number;
|
|
@@ -2596,7 +2596,7 @@ export declare const SCHEMAS: {
|
|
|
2596
2596
|
isActive: boolean;
|
|
2597
2597
|
isFeatured: boolean;
|
|
2598
2598
|
productIds: string[];
|
|
2599
|
-
groupTheme: "set" | "
|
|
2599
|
+
groupTheme: "set" | "character" | "related" | "lineage" | "generic";
|
|
2600
2600
|
minActiveMembers: number;
|
|
2601
2601
|
visibilityStatus: "hidden" | "visible";
|
|
2602
2602
|
activeMemberCount: number;
|
|
@@ -5332,7 +5332,7 @@ export declare const SCHEMAS: {
|
|
|
5332
5332
|
isActive: boolean;
|
|
5333
5333
|
isFeatured: boolean;
|
|
5334
5334
|
productIds: string[];
|
|
5335
|
-
groupTheme: "set" | "
|
|
5335
|
+
groupTheme: "set" | "character" | "related" | "lineage" | "generic";
|
|
5336
5336
|
minActiveMembers: number;
|
|
5337
5337
|
description?: string | undefined;
|
|
5338
5338
|
coverImage?: string | undefined;
|
|
@@ -5341,7 +5341,7 @@ export declare const SCHEMAS: {
|
|
|
5341
5341
|
isActive: boolean;
|
|
5342
5342
|
isFeatured: boolean;
|
|
5343
5343
|
productIds: string[];
|
|
5344
|
-
groupTheme: "set" | "
|
|
5344
|
+
groupTheme: "set" | "character" | "related" | "lineage" | "generic";
|
|
5345
5345
|
minActiveMembers: number;
|
|
5346
5346
|
description?: string | undefined;
|
|
5347
5347
|
coverImage?: string | undefined;
|
package/dist/security/csp.d.ts
CHANGED
|
@@ -1,9 +1,39 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Content Security Policy helpers.
|
|
3
|
-
*
|
|
2
|
+
* Content Security Policy helpers — **NOT WIRED. Do not wire as-is.**
|
|
3
|
+
*
|
|
4
|
+
* 🛑 Measured 2026-10-24: these two functions are exported from three barrels
|
|
5
|
+
* (`security/index.ts`, `security/browser.ts`, and appkit's main `index.ts`) and imported by
|
|
6
|
+
* **nothing** — not elsewhere in appkit, not in the consumer. That is Root Cause #87's shape:
|
|
7
|
+
* a module whose absence is silent because nothing registers it. For the whole time it
|
|
8
|
+
* existed, letitrip.in shipped no CSP at all.
|
|
9
|
+
*
|
|
10
|
+
* 🛑 **And wiring `buildCSP` unchanged would break the live site.** The policy below predates
|
|
11
|
+
* four integrations the app now depends on, and every one of them fails *silently, in someone
|
|
12
|
+
* else's browser*:
|
|
13
|
+
*
|
|
14
|
+
* - **Razorpay** — no `checkout.razorpay.com` / `api.razorpay.com` in `script-src`,
|
|
15
|
+
* `frame-src` or `connect-src`. Checkout would not load.
|
|
16
|
+
* - **YouTube embeds** — no `www.youtube-nocookie.com` in `frame-src`. Every product and
|
|
17
|
+
* blog video embed would be blocked.
|
|
18
|
+
* - **Firebase RTDB** — no `wss://*.firebasedatabase.app` in `connect-src` (only the older
|
|
19
|
+
* `firebaseio.com`). The auth/payment/bulk signal channels would stop.
|
|
20
|
+
* - **`frame-ancestors`** and **`object-src`** are absent entirely.
|
|
21
|
+
*
|
|
22
|
+
* **What ships instead, today:** a `Content-Security-Policy-Report-Only` header built inline
|
|
23
|
+
* in the consumer's `next.config.js`, covering all four of the above. Report-Only on purpose —
|
|
24
|
+
* an enforced policy guessed without report data is how you discover the gaps above from a
|
|
25
|
+
* user. `audit-security-headers` requires a CSP in either spelling and accepts Report-Only as
|
|
26
|
+
* a valid end state.
|
|
27
|
+
*
|
|
28
|
+
* **Why this file is kept rather than deleted:** the *nonce* approach here is the correct
|
|
29
|
+
* destination — `'nonce-…'` is strictly better than the `'unsafe-inline'` the inline policy
|
|
30
|
+
* currently needs. Getting there means generating a nonce in `src/proxy.ts`, threading it
|
|
31
|
+
* through `requestHeaders`, and attaching it to every inline script Next emits. That is its
|
|
32
|
+
* own change, and the policy string needs the four additions above first.
|
|
4
33
|
*
|
|
5
34
|
* @example
|
|
6
35
|
* ```ts
|
|
36
|
+
* // The intended shape, once the nonce is threaded through proxy.ts:
|
|
7
37
|
* const nonce = generateNonce();
|
|
8
38
|
* const csp = buildCSP(nonce);
|
|
9
39
|
* response.headers.set("Content-Security-Policy", csp);
|
package/dist/security/csp.js
CHANGED
|
@@ -1,9 +1,39 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Content Security Policy helpers.
|
|
3
|
-
*
|
|
2
|
+
* Content Security Policy helpers — **NOT WIRED. Do not wire as-is.**
|
|
3
|
+
*
|
|
4
|
+
* 🛑 Measured 2026-10-24: these two functions are exported from three barrels
|
|
5
|
+
* (`security/index.ts`, `security/browser.ts`, and appkit's main `index.ts`) and imported by
|
|
6
|
+
* **nothing** — not elsewhere in appkit, not in the consumer. That is Root Cause #87's shape:
|
|
7
|
+
* a module whose absence is silent because nothing registers it. For the whole time it
|
|
8
|
+
* existed, letitrip.in shipped no CSP at all.
|
|
9
|
+
*
|
|
10
|
+
* 🛑 **And wiring `buildCSP` unchanged would break the live site.** The policy below predates
|
|
11
|
+
* four integrations the app now depends on, and every one of them fails *silently, in someone
|
|
12
|
+
* else's browser*:
|
|
13
|
+
*
|
|
14
|
+
* - **Razorpay** — no `checkout.razorpay.com` / `api.razorpay.com` in `script-src`,
|
|
15
|
+
* `frame-src` or `connect-src`. Checkout would not load.
|
|
16
|
+
* - **YouTube embeds** — no `www.youtube-nocookie.com` in `frame-src`. Every product and
|
|
17
|
+
* blog video embed would be blocked.
|
|
18
|
+
* - **Firebase RTDB** — no `wss://*.firebasedatabase.app` in `connect-src` (only the older
|
|
19
|
+
* `firebaseio.com`). The auth/payment/bulk signal channels would stop.
|
|
20
|
+
* - **`frame-ancestors`** and **`object-src`** are absent entirely.
|
|
21
|
+
*
|
|
22
|
+
* **What ships instead, today:** a `Content-Security-Policy-Report-Only` header built inline
|
|
23
|
+
* in the consumer's `next.config.js`, covering all four of the above. Report-Only on purpose —
|
|
24
|
+
* an enforced policy guessed without report data is how you discover the gaps above from a
|
|
25
|
+
* user. `audit-security-headers` requires a CSP in either spelling and accepts Report-Only as
|
|
26
|
+
* a valid end state.
|
|
27
|
+
*
|
|
28
|
+
* **Why this file is kept rather than deleted:** the *nonce* approach here is the correct
|
|
29
|
+
* destination — `'nonce-…'` is strictly better than the `'unsafe-inline'` the inline policy
|
|
30
|
+
* currently needs. Getting there means generating a nonce in `src/proxy.ts`, threading it
|
|
31
|
+
* through `requestHeaders`, and attaching it to every inline script Next emits. That is its
|
|
32
|
+
* own change, and the policy string needs the four additions above first.
|
|
4
33
|
*
|
|
5
34
|
* @example
|
|
6
35
|
* ```ts
|
|
36
|
+
* // The intended shape, once the nonce is threaded through proxy.ts:
|
|
7
37
|
* const nonce = generateNonce();
|
|
8
38
|
* const csp = buildCSP(nonce);
|
|
9
39
|
* response.headers.set("Content-Security-Policy", csp);
|