@mohasinac/appkit 4.27.0 → 4.29.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/_internal/client/features/lottery/LotteryAdminEditView.d.ts +16 -16
- package/dist/_internal/client/features/lottery/LotteryAdminEditView.js +29 -6
- package/dist/_internal/client/features/lottery/LotteryListView.js +13 -1
- package/dist/_internal/server/features/checkout/actions.js +6 -4
- package/dist/_internal/server/features/classified/metadata.js +3 -1
- package/dist/_internal/server/features/digital-code/metadata.js +3 -1
- package/dist/_internal/server/features/live/metadata.js +3 -1
- package/dist/_internal/server/features/refunds/actions.js +3 -2
- package/dist/_internal/server/features/reviews/actions.js +3 -2
- package/dist/_internal/server/features/seo/sitemap.js +189 -40
- package/dist/_internal/server/features/stores/adapters.d.ts +1 -1
- package/dist/_internal/server/features/stores/adapters.js +5 -0
- package/dist/_internal/server/jobs/core/listingProcessor.d.ts +0 -1
- package/dist/_internal/server/jobs/core/listingProcessor.js +19 -1
- package/dist/_internal/shared/actions/action-registry.js +14 -0
- package/dist/_internal/shared/checkout/order-math.d.ts +25 -3
- package/dist/_internal/shared/features/checkout/config.d.ts +3 -3
- package/dist/_internal/shared/features/checkout/config.js +6 -3
- package/dist/client.d.ts +32 -2
- package/dist/client.js +37 -2
- package/dist/constants/api-endpoints.d.ts +21 -3
- package/dist/constants/api-endpoints.js +14 -1
- package/dist/constants/field-names.d.ts +2 -2
- package/dist/constants/field-names.js +2 -2
- package/dist/core/newsletter.repository.js +14 -1
- package/dist/features/about/components/FAQPageView.js +1 -1
- package/dist/features/about/components/FeesView.js +1 -1
- package/dist/features/account/components/AddressForm.d.ts +2 -0
- package/dist/features/account/components/AddressForm.js +5 -1
- package/dist/features/account/components/UserBidsView.js +1 -1
- package/dist/features/account/components/UserSidebar.js +1 -1
- package/dist/features/account/components/UserSupportView.js +24 -2
- package/dist/features/account/hooks/useAddresses.d.ts +29 -1
- package/dist/features/account/hooks/useAddresses.js +4 -1
- package/dist/features/account/hooks/useProfile.d.ts +1 -1
- package/dist/features/account/hooks/useProfile.js +1 -1
- package/dist/features/addresses/repository/addresses.repository.d.ts +13 -0
- package/dist/features/addresses/repository/addresses.repository.js +19 -0
- package/dist/features/admin/actions/chat-actions.js +5 -1
- package/dist/features/admin/components/AdminAdsView.js +1 -1
- package/dist/features/admin/components/AdminBidsView.js +6 -1
- package/dist/features/admin/components/AdminCarouselGroupEditorView.d.ts +9 -1
- package/dist/features/admin/components/AdminCarouselGroupEditorView.js +42 -10
- package/dist/features/admin/components/AdminCarouselView.js +1 -1
- package/dist/features/admin/components/AdminContentGuideView.js +1 -1
- package/dist/features/admin/components/AdminFeatureEditorView.js +1 -1
- package/dist/features/admin/components/AdminGroupedListingsView.js +14 -2
- package/dist/features/admin/components/AdminSidebar.js +1 -1
- package/dist/features/admin/components/AdminViewCards.js +2 -2
- package/dist/features/admin/components/DataTable.js +2 -2
- package/dist/features/admin/schemas/firestore.js +9 -3
- package/dist/features/auctions/actions/bid-actions.js +45 -4
- package/dist/features/auctions/components/AuctionBottomActions.d.ts +21 -6
- package/dist/features/auctions/components/AuctionBottomActions.js +42 -11
- package/dist/features/auctions/components/AuctionDetailPageView.d.ts +8 -3
- package/dist/features/auctions/components/AuctionDetailPageView.js +2 -2
- package/dist/features/auctions/components/MarketplaceAuctionCard.js +4 -5
- package/dist/features/auctions/components/PlaceBidFormClient.d.ts +27 -2
- package/dist/features/auctions/components/PlaceBidFormClient.js +42 -20
- package/dist/features/auth/components/SocialAuthButtons.js +1 -1
- package/dist/features/auth/hooks/useAuth.d.ts +23 -1
- package/dist/features/auth/hooks/useAuth.js +27 -1
- package/dist/features/auth/permissions/constants.d.ts +1 -1
- package/dist/features/auth/permissions/constants.js +3 -0
- package/dist/features/auth/repository/session.repository.d.ts +16 -1
- package/dist/features/auth/repository/session.repository.js +23 -0
- package/dist/features/auth/repository/user.repository.js +13 -2
- package/dist/features/blog/components/BlogListView.js +2 -2
- package/dist/features/blog/repository/blog.repository.d.ts +4 -0
- package/dist/features/blog/repository/blog.repository.js +1 -0
- package/dist/features/blog/schemas/firestore.d.ts +3 -1
- package/dist/features/blog/schemas/firestore.js +1 -0
- package/dist/features/cart/components/CartPriceBreakdown.d.ts +11 -1
- package/dist/features/cart/components/CartPriceBreakdown.js +3 -3
- package/dist/features/cart/components/StoreAddonsPicker.d.ts +9 -0
- package/dist/features/cart/components/StoreAddonsPicker.js +15 -5
- package/dist/features/cart/components/index.d.ts +1 -1
- package/dist/features/cart/components/index.js +1 -1
- package/dist/features/cart/hooks/useGuestCartMerge.d.ts +8 -1
- package/dist/features/cart/hooks/useGuestCartMerge.js +2 -1
- package/dist/features/cart/utils/line-total.d.ts +25 -0
- package/dist/features/cart/utils/line-total.js +26 -0
- package/dist/features/catalogue/repository/catalogue.repository.d.ts +12 -0
- package/dist/features/catalogue/repository/catalogue.repository.js +15 -0
- package/dist/features/categories/components/CategoryTree.js +2 -2
- package/dist/features/contact/components/ContactForm.js +1 -1
- package/dist/features/copilot/hooks/useCopilotFeedback.d.ts +1 -1
- package/dist/features/copilot/hooks/useCopilotFeedback.js +1 -1
- package/dist/features/events/components/AdminEventEditorView.js +16 -9
- package/dist/features/events/components/AdminEventEntriesView.js +7 -13
- package/dist/features/events/components/EventRafflesSection.js +14 -2
- package/dist/features/events/components/EventStatusBadge.js +1 -1
- package/dist/features/events/components/event-entry-detail-fields.d.ts +24 -0
- package/dist/features/events/components/event-entry-detail-fields.js +13 -0
- package/dist/features/events/repository/events.repository.js +1 -0
- package/dist/features/events/schemas/firestore.d.ts +3 -1
- package/dist/features/events/schemas/firestore.js +1 -0
- package/dist/features/faq/components/ContactCTA.js +1 -1
- package/dist/features/faq/components/FAQHelpfulButtons.js +2 -2
- package/dist/features/faq/repository/faqs.repository.d.ts +16 -1
- package/dist/features/faq/repository/faqs.repository.js +64 -54
- package/dist/features/faq/schemas/firestore.d.ts +3 -2
- package/dist/features/faq/schemas/firestore.js +2 -2
- package/dist/features/faq/schemas/index.d.ts +3 -3
- package/dist/features/faq/schemas/index.js +1 -1
- package/dist/features/filters/FilterFacetSection.js +2 -2
- package/dist/features/forms/Toggle.js +1 -1
- package/dist/features/grouped/components/GroupedListingEditorView.d.ts +18 -0
- package/dist/features/grouped/components/GroupedListingEditorView.js +192 -0
- package/dist/features/homepage/components/AdvertisementBanner.js +2 -2
- package/dist/features/homepage/components/BrandsSection.js +1 -1
- package/dist/features/homepage/components/CTABannerSection.js +1 -1
- package/dist/features/homepage/components/HomepageCustomerReviewsSection.js +1 -1
- package/dist/features/homepage/components/HomepageSectionBoundary.d.ts +39 -0
- package/dist/features/homepage/components/HomepageSectionBoundary.js +49 -0
- package/dist/features/homepage/components/ShopByCategorySection.js +1 -1
- package/dist/features/homepage/components/WelcomeSection.js +1 -1
- package/dist/features/homepage/lib/section-renderer.js +2 -1
- package/dist/features/layout/AppLayoutShell.js +5 -5
- package/dist/features/layout/BottomActions.js +5 -5
- package/dist/features/layout/BottomSheet.js +1 -1
- package/dist/features/layout/MainNavbar.js +1 -1
- package/dist/features/layout/NavbarLayout.js +1 -1
- package/dist/features/layout/TitleBarLayout.js +3 -3
- package/dist/features/layout/hooks/useFormBottomActions.d.ts +11 -1
- package/dist/features/layout/hooks/useFormBottomActions.js +16 -2
- package/dist/features/lottery/schemas/config-write.d.ts +141 -0
- package/dist/features/lottery/schemas/config-write.js +139 -0
- package/dist/features/media/MediaPickerModal.js +3 -1
- package/dist/features/media/MediaVideo.js +4 -2
- package/dist/features/media/components/MediaSlider.js +1 -1
- package/dist/features/messages/repository/conversations.repository.js +15 -5
- package/dist/features/orders/components/OrdersList.js +2 -2
- package/dist/features/payments/repository/payout.repository.d.ts +11 -1
- package/dist/features/payments/repository/payout.repository.js +14 -6
- package/dist/features/pre-orders/components/MarketplacePreorderCard.js +3 -2
- package/dist/features/pre-orders/components/PreOrderBadge.js +1 -1
- package/dist/features/pre-orders/components/PreOrderDetailPageView.js +1 -1
- package/dist/features/products/columns/productTableColumns.js +3 -3
- package/dist/features/products/components/CompareOverlay.js +1 -1
- package/dist/features/products/components/PrizeDrawDetailPageView.js +1 -1
- package/dist/features/products/components/PrizeDrawEntryActions.js +2 -2
- package/dist/features/products/components/PrizeDrawsSection.js +5 -1
- package/dist/features/products/components/ProductDetailActions.js +11 -7
- package/dist/features/products/components/ProductDetailPageView.js +3 -3
- package/dist/features/products/components/ProductFeatureBadges.js +2 -2
- package/dist/features/products/components/ProductFeaturesSelector.js +1 -1
- package/dist/features/products/components/ProductFilters.js +15 -8
- package/dist/features/products/components/ProductGrid.js +8 -17
- package/dist/features/products/components/ProductTabs.js +7 -4
- package/dist/features/products/components/ProductTabsShell.js +2 -7
- package/dist/features/products/constants/action-defs.d.ts +21 -4
- package/dist/features/products/constants/action-defs.js +7 -3
- package/dist/features/products/hooks/useRelatedProducts.js +2 -1
- package/dist/features/products/repository/products.repository.d.ts +1 -1
- package/dist/features/products/repository/products.repository.js +38 -16
- package/dist/features/products/schemas/firestore.d.ts +3 -2
- package/dist/features/products/schemas/firestore.js +1 -1
- package/dist/features/reviews/actions/review-actions.d.ts +8 -1
- package/dist/features/reviews/actions/review-actions.js +14 -3
- package/dist/features/reviews/components/ReviewDetailShell.js +10 -4
- package/dist/features/reviews/components/ReviewModal.js +1 -1
- package/dist/features/reviews/repository/reviews.repository.d.ts +29 -1
- package/dist/features/reviews/repository/reviews.repository.js +54 -13
- package/dist/features/reviews/schemas/firestore.d.ts +9 -1
- package/dist/features/reviews/schemas/firestore.js +1 -0
- package/dist/features/scams/components/ScamRegistryView.js +2 -2
- package/dist/features/search/components/Search.js +3 -3
- package/dist/features/search/components/SearchResultsSection.js +1 -1
- package/dist/features/seller/components/SellerAddressesView.js +3 -1
- package/dist/features/seller/components/SellerBidsView.js +6 -1
- package/dist/features/seller/components/SellerSidebar.js +1 -1
- package/dist/features/shell/SectionForm.d.ts +21 -1
- package/dist/features/shell/SectionForm.js +15 -3
- package/dist/features/shell/build-sections.js +10 -1
- package/dist/features/shell/field-ui-meta.d.ts +21 -0
- package/dist/features/shipments/components/AdminShipmentLotItemsView.js +23 -6
- package/dist/features/site-settings/components/ActionPermissionsManager.js +1 -1
- package/dist/features/store-extensions/repository/store-extensions.repositories.d.ts +21 -4
- package/dist/features/store-extensions/repository/store-extensions.repositories.js +24 -4
- package/dist/features/store-extensions/schemas/shipping-config-form.js +13 -0
- package/dist/features/store-extensions/schemas/store-category-form.js +1 -1
- package/dist/features/stores/actions/store-query-actions.js +25 -1
- package/dist/features/stores/components/InteractiveStoreCard.js +1 -1
- package/dist/features/stores/components/StoreHeader.js +1 -1
- package/dist/features/stores/components/StoreNavTabs.js +20 -8
- package/dist/features/stores/repository/store.repository.js +1 -0
- package/dist/features/stores/schemas/firestore.d.ts +2 -0
- package/dist/features/support/repository/support.repository.d.ts +12 -0
- package/dist/features/support/repository/support.repository.js +15 -0
- package/dist/features/support/schemas/ticket-create-form.d.ts +33 -0
- package/dist/features/support/schemas/ticket-create-form.js +82 -0
- package/dist/features/tester/repository/tester-checklist-item.repository.d.ts +0 -1
- package/dist/features/tester/repository/tester-checklist-item.repository.js +6 -15
- package/dist/features/tester/schemas/firestore.d.ts +5 -4
- package/dist/features/tester/schemas/firestore.js +3 -3
- package/dist/features/tester/seed-data/products-tester-seed-data.js +2 -2
- package/dist/features/tester/seed-data/tester-checklist-seed-data.js +138 -1
- package/dist/features/wishlist/components/WishlistHeartButton.d.ts +55 -0
- package/dist/features/wishlist/components/WishlistHeartButton.js +52 -0
- package/dist/features/wishlist/components/index.d.ts +2 -1
- package/dist/features/wishlist/components/index.js +1 -1
- package/dist/index.d.ts +10 -3
- package/dist/index.js +19 -4
- package/dist/monitoring/error-tracking.js +39 -5
- package/dist/monitoring/server-logger.js +70 -32
- package/dist/next/components/ErrorView.js +1 -1
- package/dist/next/routing/route-map.d.ts +39 -7
- package/dist/next/routing/route-map.js +34 -4
- package/dist/providers/db-firebase/base.repository.d.ts +30 -0
- package/dist/providers/db-firebase/base.repository.js +56 -9
- package/dist/react/ErrorBoundary.js +9 -1
- package/dist/react/contexts/SessionContext.js +6 -2
- package/dist/react/hooks/useAuthGate.d.ts +22 -0
- package/dist/react/hooks/useAuthGate.js +86 -7
- package/dist/schemas/registry.d.ts +3 -3
- package/dist/security/index.d.ts +1 -1
- package/dist/security/index.js +1 -1
- package/dist/security/pii-encrypt.d.ts +13 -5
- package/dist/security/pii-encrypt.js +132 -6
- package/dist/security/pii-schemas.d.ts +32 -4
- package/dist/security/pii-schemas.js +38 -5
- package/dist/seed/_helpers/product-search-txt.d.ts +18 -0
- package/dist/seed/_helpers/product-search-txt.js +33 -0
- package/dist/seed/_helpers/search-txt-wrappers.d.ts +20 -0
- package/dist/seed/_helpers/search-txt-wrappers.js +73 -0
- package/dist/seed/blog-posts-seed-data.d.ts +4 -0
- package/dist/seed/blog-posts-seed-data.js +7 -1
- package/dist/seed/events-seed-data.d.ts +5 -1
- package/dist/seed/events-seed-data.js +7 -1
- package/dist/seed/faq-seed-data.d.ts +16 -0
- package/dist/seed/faq-seed-data.js +22 -1
- package/dist/seed/products-art-seed-data.js +49 -7
- package/dist/seed/products-auctions-seed-data.js +12 -4
- package/dist/seed/products-classifieds-seed-data.d.ts +5 -0
- package/dist/seed/products-classifieds-seed-data.js +65 -9
- package/dist/seed/products-digital-codes-seed-data.d.ts +5 -0
- package/dist/seed/products-digital-codes-seed-data.js +65 -9
- package/dist/seed/products-live-items-seed-data.d.ts +5 -0
- package/dist/seed/products-live-items-seed-data.js +33 -5
- package/dist/seed/products-preorders-seed-data.d.ts +1 -1
- package/dist/seed/products-preorders-seed-data.js +6 -2
- package/dist/seed/products-prize-draws-seed-data.d.ts +5 -0
- package/dist/seed/products-prize-draws-seed-data.js +57 -8
- package/dist/seed/products-standard-seed-data.js +6 -2
- package/dist/seed/products-stickers-seed-data.js +49 -7
- package/dist/seed/reviews-seed-data.js +2 -1
- package/dist/seed/site-settings-seed-data.js +5 -3
- package/dist/seed/stores-seed-data.d.ts +4 -0
- package/dist/seed/stores-seed-data.js +7 -1
- package/dist/seo/json-ld.d.ts +44 -2
- package/dist/seo/json-ld.js +57 -11
- package/dist/styles.css +368 -90
- package/dist/tailwind-utilities.css +1 -1
- package/dist/tokens/index.d.ts +1 -1
- package/dist/tokens/index.js +1 -1
- package/dist/tokens/themes/default-dark.js +6 -0
- package/dist/tokens/themes/default-light.js +5 -0
- package/dist/tokens/tokens.css +84 -0
- package/dist/ui/DataTable.js +1 -1
- package/dist/ui/DataTable.style.css +3 -3
- package/dist/ui/components/BulkActionBar.style.css +1 -3
- package/dist/ui/components/Button.js +15 -6
- package/dist/ui/components/Dropdown.style.css +1 -4
- package/dist/ui/components/FilterChipGroup.js +1 -1
- package/dist/ui/components/FilterDrawer.style.css +1 -1
- package/dist/ui/components/Icon.d.ts +53 -0
- package/dist/ui/components/Icon.js +41 -0
- package/dist/ui/components/IconButton.d.ts +13 -0
- package/dist/ui/components/IconButton.js +13 -0
- package/dist/ui/components/IconButton.style.css +18 -10
- package/dist/ui/components/ListingLayout.style.css +2 -10
- package/dist/ui/components/ListingToolbar.js +6 -6
- package/dist/ui/components/Modal.js +1 -1
- package/dist/ui/components/PaginatedSelect.style.css +3 -3
- package/dist/ui/components/Pagination.style.css +1 -6
- package/dist/ui/components/RichTextEditor.js +1 -1
- package/dist/ui/components/RowActionMenu.style.css +1 -1
- package/dist/ui/components/SideDrawer.style.css +1 -1
- package/dist/ui/components/Slider.js +1 -1
- package/dist/ui/components/TabBarShell.d.ts +78 -0
- package/dist/ui/components/TabBarShell.js +95 -0
- package/dist/ui/components/Tabs.js +12 -8
- package/dist/ui/components/Tabs.style.css +249 -41
- package/dist/ui/components/ViewToggle.style.css +1 -5
- package/dist/ui/components/tabs-overflow.d.ts +17 -0
- package/dist/ui/components/tabs-overflow.js +142 -0
- package/dist/ui/icons/icon-registry.d.ts +38 -0
- package/dist/ui/icons/icon-registry.js +37 -0
- package/dist/ui/index.d.ts +6 -0
- package/dist/ui/index.js +3 -0
- package/dist/utils/index.d.ts +1 -1
- package/dist/utils/index.js +1 -1
- package/dist/utils/media-url.d.ts +27 -4
- package/dist/utils/media-url.js +37 -6
- package/dist/utils/search-txt.d.ts +71 -0
- package/dist/utils/search-txt.js +118 -0
- package/package.json +1 -1
|
@@ -1,19 +1,57 @@
|
|
|
1
1
|
"use client";
|
|
2
|
-
import { useState, useCallback } from "react";
|
|
2
|
+
import { useState, useCallback, useRef, useEffect } from "react";
|
|
3
3
|
import { useAuth } from "../contexts/SessionContext";
|
|
4
4
|
import { useSiteSettings } from "../../core/hooks/useSiteSettings";
|
|
5
5
|
import { ACTION_META } from "../../features/products/constants/action-defs";
|
|
6
6
|
const DEFAULT_MSG = "You need to be signed in to continue.";
|
|
7
7
|
const DISABLED_MSG = "This action is currently unavailable.";
|
|
8
8
|
const PERMISSION_MSG = "You don't have permission to perform this action.";
|
|
9
|
+
/**
|
|
10
|
+
* Ceiling on how long a click may sit parked waiting for the session to resolve.
|
|
11
|
+
*
|
|
12
|
+
* `SessionContext` settles after Firebase persistence resolves AND one
|
|
13
|
+
* `/api/user/profile` round-trip returns. If that never happens (offline, a
|
|
14
|
+
* dead adapter) we fall back to the signed-out answer rather than eating the
|
|
15
|
+
* click forever — a button that never responds is worse than a wrong modal.
|
|
16
|
+
*/
|
|
17
|
+
const AUTH_RESOLVE_TIMEOUT_MS = 8000;
|
|
18
|
+
/**
|
|
19
|
+
* The single auth/permission gate behind every CTA in the app.
|
|
20
|
+
*
|
|
21
|
+
* 🛑 `loading` is load-bearing and must never be dropped again.
|
|
22
|
+
*
|
|
23
|
+
* This hook used to read only `user` from the session. `SessionProvider` is
|
|
24
|
+
* mounted with `initialUser={null}` (the root locale layout is static and must
|
|
25
|
+
* stay that way), so on every hard load `user` is `null` and `loading` is
|
|
26
|
+
* `true` until Firebase persistence + a profile fetch resolve. Reading `user`
|
|
27
|
+
* alone made that window indistinguishable from "signed out": a signed-in user
|
|
28
|
+
* clicking a wishlist heart in the first ~800ms got "You need to be signed in".
|
|
29
|
+
*
|
|
30
|
+
* A click that arrives during that window is PARKED and replayed once the
|
|
31
|
+
* session settles — not dropped. Dropping it is indistinguishable from a dead
|
|
32
|
+
* button, which is the failure mode this hook is supposed to prevent.
|
|
33
|
+
*/
|
|
9
34
|
export function useAuthGate() {
|
|
10
|
-
const { user } = useAuth();
|
|
35
|
+
const { user, loading } = useAuth();
|
|
11
36
|
const { data: settings } = useSiteSettings();
|
|
12
37
|
const [state, setState] = useState({ open: false, message: "" });
|
|
13
|
-
const
|
|
38
|
+
const [parked, setParked] = useState(false);
|
|
39
|
+
const parkedRef = useRef(null);
|
|
40
|
+
const timerRef = useRef(null);
|
|
41
|
+
const clearTimer = useCallback(() => {
|
|
42
|
+
if (timerRef.current) {
|
|
43
|
+
clearTimeout(timerRef.current);
|
|
44
|
+
timerRef.current = null;
|
|
45
|
+
}
|
|
46
|
+
}, []);
|
|
47
|
+
/**
|
|
48
|
+
* The gate decision itself. Only ever called once the session has resolved,
|
|
49
|
+
* and re-created whenever `user`/`settings` change — which is what keeps a
|
|
50
|
+
* replayed click from being judged against a stale session.
|
|
51
|
+
*/
|
|
52
|
+
const evaluate = useCallback((actionId, fn) => {
|
|
14
53
|
const meta = ACTION_META[actionId];
|
|
15
|
-
const enabled = settings
|
|
16
|
-
?.actionConfig?.[actionId]?.enabled ??
|
|
54
|
+
const enabled = settings?.actionConfig?.[actionId]?.enabled ??
|
|
17
55
|
meta?.defaultEnabled ??
|
|
18
56
|
true;
|
|
19
57
|
if (!enabled) {
|
|
@@ -24,12 +62,53 @@ export function useAuthGate() {
|
|
|
24
62
|
setState({ open: true, message: meta.authMessage ?? DEFAULT_MSG });
|
|
25
63
|
return;
|
|
26
64
|
}
|
|
27
|
-
if (meta?.requiredPermission &&
|
|
65
|
+
if (meta?.requiredPermission &&
|
|
66
|
+
!user?.permissions?.includes(meta.requiredPermission)) {
|
|
28
67
|
setState({ open: true, message: PERMISSION_MSG });
|
|
29
68
|
return;
|
|
30
69
|
}
|
|
31
70
|
void fn();
|
|
32
71
|
}, [user, settings]);
|
|
72
|
+
const requireAuth = useCallback((actionId, fn) => {
|
|
73
|
+
if (loading) {
|
|
74
|
+
// Park only {actionId, fn} — the caller's click closure is preserved
|
|
75
|
+
// verbatim, while the gate's own view of the session is re-read at
|
|
76
|
+
// drain time via `evaluate`.
|
|
77
|
+
parkedRef.current = { actionId, fn };
|
|
78
|
+
setParked(true);
|
|
79
|
+
clearTimer();
|
|
80
|
+
timerRef.current = setTimeout(() => {
|
|
81
|
+
parkedRef.current = null;
|
|
82
|
+
setParked(false);
|
|
83
|
+
setState({
|
|
84
|
+
open: true,
|
|
85
|
+
message: ACTION_META[actionId]?.authMessage ?? DEFAULT_MSG,
|
|
86
|
+
});
|
|
87
|
+
}, AUTH_RESOLVE_TIMEOUT_MS);
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
evaluate(actionId, fn);
|
|
91
|
+
}, [loading, evaluate, clearTimer]);
|
|
92
|
+
// Drain the parked click with a FRESH user/settings closure. The ref is
|
|
93
|
+
// nulled before `evaluate` runs, so an extra effect pass is a no-op.
|
|
94
|
+
useEffect(() => {
|
|
95
|
+
if (loading)
|
|
96
|
+
return;
|
|
97
|
+
const request = parkedRef.current;
|
|
98
|
+
if (!request)
|
|
99
|
+
return;
|
|
100
|
+
parkedRef.current = null;
|
|
101
|
+
clearTimer();
|
|
102
|
+
setParked(false);
|
|
103
|
+
evaluate(request.actionId, request.fn);
|
|
104
|
+
}, [loading, evaluate, clearTimer]);
|
|
105
|
+
useEffect(() => () => clearTimer(), [clearTimer]);
|
|
33
106
|
const closeModal = useCallback(() => setState((s) => ({ ...s, open: false })), []);
|
|
34
|
-
return {
|
|
107
|
+
return {
|
|
108
|
+
requireAuth,
|
|
109
|
+
isAuthResolving: loading || parked,
|
|
110
|
+
modalOpen: state.open,
|
|
111
|
+
modalMessage: state.message,
|
|
112
|
+
closeModal,
|
|
113
|
+
};
|
|
35
114
|
}
|
|
@@ -2017,7 +2017,7 @@ export declare const SCHEMAS: {
|
|
|
2017
2017
|
order: import("zod").ZodNumber;
|
|
2018
2018
|
priority: import("zod").ZodNumber;
|
|
2019
2019
|
tags: import("zod").ZodArray<import("zod").ZodString, "many">;
|
|
2020
|
-
|
|
2020
|
+
searchTxt: import("zod").ZodOptional<import("zod").ZodArray<import("zod").ZodString, "many">>;
|
|
2021
2021
|
relatedFAQs: import("zod").ZodArray<import("zod").ZodString, "many">;
|
|
2022
2022
|
useSiteSettings: import("zod").ZodBoolean;
|
|
2023
2023
|
variables: import("zod").ZodOptional<import("zod").ZodRecord<import("zod").ZodString, import("zod").ZodString>>;
|
|
@@ -2099,7 +2099,7 @@ export declare const SCHEMAS: {
|
|
|
2099
2099
|
metaTitle?: string | undefined;
|
|
2100
2100
|
metaDescription?: string | undefined;
|
|
2101
2101
|
};
|
|
2102
|
-
|
|
2102
|
+
searchTxt?: string[] | undefined;
|
|
2103
2103
|
variables?: Record<string, string> | undefined;
|
|
2104
2104
|
}, {
|
|
2105
2105
|
id: string;
|
|
@@ -2138,7 +2138,7 @@ export declare const SCHEMAS: {
|
|
|
2138
2138
|
metaTitle?: string | undefined;
|
|
2139
2139
|
metaDescription?: string | undefined;
|
|
2140
2140
|
};
|
|
2141
|
-
|
|
2141
|
+
searchTxt?: string[] | undefined;
|
|
2142
2142
|
variables?: Record<string, string> | undefined;
|
|
2143
2143
|
}>;
|
|
2144
2144
|
readonly groupedListings: import("zod").ZodObject<{
|
package/dist/security/index.d.ts
CHANGED
|
@@ -4,7 +4,7 @@ export type { RateLimitConfig, RateLimitResult } from "./rate-limit";
|
|
|
4
4
|
export { requireAuth, requireRole, requireOwnership, requireEmailVerified, requireActiveAccount, canChangeRole, getRoleLevel, } from "./authorization";
|
|
5
5
|
export type { UserRole } from "./authorization";
|
|
6
6
|
export { redactPii, safeDisplayName, safeDisplayEmail, maskIp } from "./pii-redact";
|
|
7
|
-
export { encryptValue, decryptValue, hmacBlindIndex, encryptPiiFields, decryptPiiFields, encryptPii, decryptPii, piiBlindIndex, addPiiIndices, getPiiConfigError, encryptShippingAddress, decryptShippingAddress, encryptPayoutDetails, decryptPayoutDetails, encryptShippingConfig, decryptShippingConfig, encryptPayoutBankAccount, decryptPayoutBankAccount, } from "./pii-encrypt";
|
|
7
|
+
export { encryptValue, decryptValue, hmacBlindIndex, encryptPiiFields, decryptPiiFields, encryptPii, decryptPii, piiBlindIndex, addPiiIndices, piiIndicesFor, getPiiConfigError, encryptShippingAddress, decryptShippingAddress, encryptPayoutDetails, decryptPayoutDetails, encryptShippingConfig, decryptShippingConfig, encryptPayoutBankAccount, decryptPayoutBankAccount, } from "./pii-encrypt";
|
|
8
8
|
export { isPiiEncrypted, maskName, maskEmail, maskPublicReview, maskPublicBid, maskPublicEventEntry, maskOfferForSeller, ENC_PREFIX, HMAC_PREFIX, } from "./pii-mask";
|
|
9
9
|
export { USER_PII_FIELDS, USER_PII_INDEX_MAP, ADDRESS_PII_FIELDS, ORDER_PII_FIELDS, PAYOUT_PII_FIELDS, PAYOUT_PII_INDEX_MAP, BID_PII_FIELDS, NEWSLETTER_PII_FIELDS, NEWSLETTER_PII_INDEX_MAP, TOKEN_PII_FIELDS, TOKEN_PII_INDEX_MAP, REVIEW_PII_FIELDS, REVIEW_PII_INDEX_MAP, OFFER_PII_FIELDS, CHAT_PII_FIELDS, EVENT_ENTRY_PII_FIELDS, LOTTERY_ENTRY_PII_FIELDS, PAYMENT_METHOD_PII_FIELDS, } from "./pii-schemas";
|
|
10
10
|
export { encryptSecret, decryptSecret, isSecretEncrypted, maskSecret, } from "./settings-encryption";
|
package/dist/security/index.js
CHANGED
|
@@ -2,7 +2,7 @@ export { generateNonce, buildCSP } from "./csp";
|
|
|
2
2
|
export { rateLimit, applyRateLimit, rateLimitByIdentifier, RateLimitPresets, clearRateLimitStore, getClientIP, hashGuestIdentity, } from "./rate-limit";
|
|
3
3
|
export { requireAuth, requireRole, requireOwnership, requireEmailVerified, requireActiveAccount, canChangeRole, getRoleLevel, } from "./authorization";
|
|
4
4
|
export { redactPii, safeDisplayName, safeDisplayEmail, maskIp } from "./pii-redact";
|
|
5
|
-
export { encryptValue, decryptValue, hmacBlindIndex, encryptPiiFields, decryptPiiFields, encryptPii, decryptPii, piiBlindIndex, addPiiIndices, getPiiConfigError, encryptShippingAddress, decryptShippingAddress, encryptPayoutDetails, decryptPayoutDetails, encryptShippingConfig, decryptShippingConfig, encryptPayoutBankAccount, decryptPayoutBankAccount, } from "./pii-encrypt";
|
|
5
|
+
export { encryptValue, decryptValue, hmacBlindIndex, encryptPiiFields, decryptPiiFields, encryptPii, decryptPii, piiBlindIndex, addPiiIndices, piiIndicesFor, getPiiConfigError, encryptShippingAddress, decryptShippingAddress, encryptPayoutDetails, decryptPayoutDetails, encryptShippingConfig, decryptShippingConfig, encryptPayoutBankAccount, decryptPayoutBankAccount, } from "./pii-encrypt";
|
|
6
6
|
// Crypto-free display-masking helpers -- sourced from pii-mask.ts (not
|
|
7
7
|
// pii-encrypt.ts) so this barrel stays safe to re-export from the
|
|
8
8
|
// universal client+server index.ts. See pii-mask.ts's header comment.
|
|
@@ -21,11 +21,6 @@ export declare function decryptValue(ciphertext: string): string;
|
|
|
21
21
|
* Used for equality lookups without decrypting (e.g. find-by-email).
|
|
22
22
|
*/
|
|
23
23
|
export declare function hmacBlindIndex(value: string): string;
|
|
24
|
-
/**
|
|
25
|
-
* Encrypt specified PII fields in a Firestore document.
|
|
26
|
-
* For each encrypted field, a `<field>Index` blind-index sibling is added.
|
|
27
|
-
* Already-encrypted values (prefix `enc:v1:`) are left unchanged.
|
|
28
|
-
*/
|
|
29
24
|
export declare function encryptPiiFields<T extends object>(doc: T, piiFields: string[]): T;
|
|
30
25
|
/**
|
|
31
26
|
* Decrypt specified PII fields in a Firestore document.
|
|
@@ -43,6 +38,19 @@ export declare function piiBlindIndex(plaintext: string): string;
|
|
|
43
38
|
* For each mapping entry (e.g. { email: "emailIndex" }), if the source field
|
|
44
39
|
* is a non-empty string, computes the HMAC and stores it as the index field.
|
|
45
40
|
*/
|
|
41
|
+
/**
|
|
42
|
+
* Blind-index fields ONLY — derived from `source` (which must be plaintext),
|
|
43
|
+
* with none of `source`'s own keys carried along.
|
|
44
|
+
*
|
|
45
|
+
* Use this, never `addPiiIndices`, when merging indices onto an already-encrypted
|
|
46
|
+
* object. `addPiiIndices` returns `{...source, ...indices}`, so spreading its
|
|
47
|
+
* result over ciphertext silently restores the plaintext — which is exactly how
|
|
48
|
+
* `payouts.sellerEmail`, `payouts.upiId` and `reviews.userName` ended up stored
|
|
49
|
+
* in cleartext beside a valid index.
|
|
50
|
+
*
|
|
51
|
+
* @param source plaintext document — hashing ciphertext produces a useless index
|
|
52
|
+
*/
|
|
53
|
+
export declare function piiIndicesFor(source: object, mapping: Record<string, string>): Record<string, string>;
|
|
46
54
|
export declare function addPiiIndices<T extends object>(obj: T, mapping: Record<string, string>): T;
|
|
47
55
|
/**
|
|
48
56
|
* Encrypt PII inside a shipping address stored in an order.
|
|
@@ -99,17 +99,92 @@ export function hmacBlindIndex(value) {
|
|
|
99
99
|
* For each encrypted field, a `<field>Index` blind-index sibling is added.
|
|
100
100
|
* Already-encrypted values (prefix `enc:v1:`) are left unchanged.
|
|
101
101
|
*/
|
|
102
|
+
/** Read `a.b.c`, returning undefined if any hop is missing or not an object. */
|
|
103
|
+
function getPath(obj, path) {
|
|
104
|
+
return path.split(".").reduce((acc, key) => {
|
|
105
|
+
if (acc === null || typeof acc !== "object")
|
|
106
|
+
return undefined;
|
|
107
|
+
return acc[key];
|
|
108
|
+
}, obj);
|
|
109
|
+
}
|
|
110
|
+
/** Set `a.b.c`, cloning each object along the path so the input is untouched. */
|
|
111
|
+
function setPath(obj, path, value) {
|
|
112
|
+
const keys = path.split(".");
|
|
113
|
+
const last = keys.pop();
|
|
114
|
+
let cursor = obj;
|
|
115
|
+
for (const key of keys) {
|
|
116
|
+
const next = cursor[key];
|
|
117
|
+
if (next === null || typeof next !== "object")
|
|
118
|
+
return;
|
|
119
|
+
cursor[key] = { ...next };
|
|
120
|
+
cursor = cursor[key];
|
|
121
|
+
}
|
|
122
|
+
cursor[last] = value;
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* Encrypt the named fields, adding a blind index alongside each top-level one.
|
|
126
|
+
*
|
|
127
|
+
* Supports dotted paths (`deviceInfo.ip`, `whatsappConfig.accessToken`). This
|
|
128
|
+
* was a flat top-level loop that skipped anything non-string, which is why
|
|
129
|
+
* nested PII stayed in plaintext — and why the exported
|
|
130
|
+
* `STORE_SECRET_FIELDS = ["whatsappConfig.accessToken"]` was unusable by the
|
|
131
|
+
* very function it was written for.
|
|
132
|
+
*
|
|
133
|
+
* Idempotent: a value already carrying `enc:v1:` is left alone, so re-running
|
|
134
|
+
* over a partially-migrated document is safe.
|
|
135
|
+
*/
|
|
136
|
+
/**
|
|
137
|
+
* Encrypt `itemPath` on every element of the array at `arrayPath`.
|
|
138
|
+
*
|
|
139
|
+
* Spelled `messages[].body` in a PII field list. Without this, array-of-object
|
|
140
|
+
* PII — conversation message bodies, status-history notes — is written in
|
|
141
|
+
* plaintext and `mapDoc`'s decrypt never touches it on the way back out: a leak
|
|
142
|
+
* with no error and no visible symptom.
|
|
143
|
+
*/
|
|
144
|
+
function encryptInArray(root, arrayPath, itemPath) {
|
|
145
|
+
const arr = getPath(root, arrayPath);
|
|
146
|
+
if (!Array.isArray(arr))
|
|
147
|
+
return;
|
|
148
|
+
const next = arr.map((el) => {
|
|
149
|
+
if (el === null || typeof el !== "object")
|
|
150
|
+
return el;
|
|
151
|
+
const clone = { ...el };
|
|
152
|
+
const value = itemPath ? getPath(clone, itemPath) : undefined;
|
|
153
|
+
if (typeof value !== "string" || !value || value.startsWith(ENC_PREFIX)) {
|
|
154
|
+
return clone;
|
|
155
|
+
}
|
|
156
|
+
setPath(clone, itemPath, encryptValue(value));
|
|
157
|
+
return clone;
|
|
158
|
+
});
|
|
159
|
+
setPath(root, arrayPath, next);
|
|
160
|
+
}
|
|
102
161
|
export function encryptPiiFields(doc, piiFields) {
|
|
103
162
|
const result = { ...doc };
|
|
104
|
-
const source = doc;
|
|
105
163
|
for (const field of piiFields) {
|
|
106
|
-
|
|
164
|
+
// `messages[].body` — encrypt one property on every element of an array.
|
|
165
|
+
if (field.includes("[]")) {
|
|
166
|
+
const [arrayPath, rest] = field.split("[]");
|
|
167
|
+
encryptInArray(result, arrayPath, rest.replace(/^\./, ""));
|
|
168
|
+
continue;
|
|
169
|
+
}
|
|
170
|
+
const nested = field.includes(".");
|
|
171
|
+
const value = nested
|
|
172
|
+
? getPath(result, field)
|
|
173
|
+
: result[field];
|
|
107
174
|
if (typeof value !== "string" || !value)
|
|
108
175
|
continue;
|
|
109
176
|
if (value.startsWith(ENC_PREFIX))
|
|
110
177
|
continue; // already encrypted
|
|
111
|
-
|
|
112
|
-
|
|
178
|
+
if (nested) {
|
|
179
|
+
// Blind index deliberately omitted for nested paths: `deviceInfo.ipIndex`
|
|
180
|
+
// would be a nested sibling nothing queries, and an index only earns its
|
|
181
|
+
// storage when some lookup actually uses it.
|
|
182
|
+
setPath(result, field, encryptValue(value));
|
|
183
|
+
}
|
|
184
|
+
else {
|
|
185
|
+
result[field] = encryptValue(value);
|
|
186
|
+
result[`${field}Index`] = hmacBlindIndex(value);
|
|
187
|
+
}
|
|
113
188
|
}
|
|
114
189
|
return result;
|
|
115
190
|
}
|
|
@@ -119,9 +194,37 @@ export function encryptPiiFields(doc, piiFields) {
|
|
|
119
194
|
*/
|
|
120
195
|
export function decryptPiiFields(doc, piiFields) {
|
|
121
196
|
const result = { ...doc };
|
|
122
|
-
const source = doc;
|
|
123
197
|
for (const field of piiFields) {
|
|
124
|
-
|
|
198
|
+
// MUST mirror encryptPiiFields' path handling. This was a flat top-level
|
|
199
|
+
// loop while encrypt understood dotted and array paths — so a nested field
|
|
200
|
+
// would be encrypted on write and never decrypted on read, surfacing
|
|
201
|
+
// ciphertext to callers. Any path form added to one must be added to both.
|
|
202
|
+
if (field.includes("[]")) {
|
|
203
|
+
const [arrayPath, rest] = field.split("[]");
|
|
204
|
+
const itemPath = rest.replace(/^\./, "");
|
|
205
|
+
const arr = getPath(result, arrayPath);
|
|
206
|
+
if (!Array.isArray(arr))
|
|
207
|
+
continue;
|
|
208
|
+
setPath(result, arrayPath, arr.map((el) => {
|
|
209
|
+
if (el === null || typeof el !== "object")
|
|
210
|
+
return el;
|
|
211
|
+
const clone = { ...el };
|
|
212
|
+
const value = itemPath ? getPath(clone, itemPath) : undefined;
|
|
213
|
+
if (typeof value === "string" && value.startsWith(ENC_PREFIX)) {
|
|
214
|
+
setPath(clone, itemPath, decryptValue(value));
|
|
215
|
+
}
|
|
216
|
+
return clone;
|
|
217
|
+
}));
|
|
218
|
+
continue;
|
|
219
|
+
}
|
|
220
|
+
if (field.includes(".")) {
|
|
221
|
+
const value = getPath(result, field);
|
|
222
|
+
if (typeof value === "string" && value.startsWith(ENC_PREFIX)) {
|
|
223
|
+
setPath(result, field, decryptValue(value));
|
|
224
|
+
}
|
|
225
|
+
continue;
|
|
226
|
+
}
|
|
227
|
+
const value = result[field];
|
|
125
228
|
if (typeof value === "string" && value.startsWith(ENC_PREFIX)) {
|
|
126
229
|
result[field] = decryptValue(value);
|
|
127
230
|
}
|
|
@@ -152,6 +255,29 @@ export function piiBlindIndex(plaintext) {
|
|
|
152
255
|
* For each mapping entry (e.g. { email: "emailIndex" }), if the source field
|
|
153
256
|
* is a non-empty string, computes the HMAC and stores it as the index field.
|
|
154
257
|
*/
|
|
258
|
+
/**
|
|
259
|
+
* Blind-index fields ONLY — derived from `source` (which must be plaintext),
|
|
260
|
+
* with none of `source`'s own keys carried along.
|
|
261
|
+
*
|
|
262
|
+
* Use this, never `addPiiIndices`, when merging indices onto an already-encrypted
|
|
263
|
+
* object. `addPiiIndices` returns `{...source, ...indices}`, so spreading its
|
|
264
|
+
* result over ciphertext silently restores the plaintext — which is exactly how
|
|
265
|
+
* `payouts.sellerEmail`, `payouts.upiId` and `reviews.userName` ended up stored
|
|
266
|
+
* in cleartext beside a valid index.
|
|
267
|
+
*
|
|
268
|
+
* @param source plaintext document — hashing ciphertext produces a useless index
|
|
269
|
+
*/
|
|
270
|
+
export function piiIndicesFor(source, mapping) {
|
|
271
|
+
const access = source;
|
|
272
|
+
const indices = {};
|
|
273
|
+
for (const [sourceField, indexField] of Object.entries(mapping)) {
|
|
274
|
+
const val = access[sourceField];
|
|
275
|
+
if (typeof val === "string" && val) {
|
|
276
|
+
indices[indexField] = piiBlindIndex(val);
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
return indices;
|
|
280
|
+
}
|
|
155
281
|
export function addPiiIndices(obj, mapping) {
|
|
156
282
|
const result = { ...obj };
|
|
157
283
|
const access = result;
|
|
@@ -16,16 +16,44 @@ export declare const ORDER_PII_FIELDS: readonly ["userName", "userEmail", "selle
|
|
|
16
16
|
export declare const PAYOUT_PII_FIELDS: readonly ["sellerEmail", "upiId"];
|
|
17
17
|
/** Blind-index mapping for queryable payout PII */
|
|
18
18
|
export declare const PAYOUT_PII_INDEX_MAP: Record<string, string>;
|
|
19
|
-
/**
|
|
20
|
-
|
|
19
|
+
/**
|
|
20
|
+
* PII fields in bids.
|
|
21
|
+
*
|
|
22
|
+
* `userEmail` only. `userName` is deliberately NOT encrypted: the seller bid
|
|
23
|
+
* search (`/api/store/bids`) does a prefix match on it, and encryption and
|
|
24
|
+
* partial-match search are mutually exclusive over the same field — ciphertext
|
|
25
|
+
* has no usable prefix. Encrypting it would silently break that search.
|
|
26
|
+
* Bidder names are already masked on the public side by `maskPublicBid`.
|
|
27
|
+
*/
|
|
28
|
+
export declare const BID_PII_FIELDS: readonly ["userEmail"];
|
|
21
29
|
/** PII fields in newsletter subscribers */
|
|
22
30
|
export declare const NEWSLETTER_PII_FIELDS: readonly [];
|
|
23
31
|
/** Blind-index mapping for newsletter subscribers */
|
|
24
32
|
export declare const NEWSLETTER_PII_INDEX_MAP: Record<string, string>;
|
|
25
|
-
/**
|
|
26
|
-
|
|
33
|
+
/**
|
|
34
|
+
* PII fields in tokens (email verification / password reset).
|
|
35
|
+
*
|
|
36
|
+
* Was an empty array, so the encrypt/decrypt machinery wrapped around these
|
|
37
|
+
* repositories was a no-op and every verification/reset email sat in cleartext.
|
|
38
|
+
* Safe to enable because `findByEmail` already queries the blind index first
|
|
39
|
+
* and falls back to plaintext equality, so legacy rows keep resolving while new
|
|
40
|
+
* ones are encrypted.
|
|
41
|
+
*/
|
|
42
|
+
export declare const TOKEN_PII_FIELDS: readonly ["email"];
|
|
27
43
|
/** Blind-index mapping for token email queries */
|
|
28
44
|
export declare const TOKEN_PII_INDEX_MAP: Record<string, string>;
|
|
45
|
+
/**
|
|
46
|
+
* PII fields in conversations — buyer↔seller message bodies.
|
|
47
|
+
*
|
|
48
|
+
* `messages[].body` uses the array-path form, and `lastMessage` is the
|
|
49
|
+
* denormalised copy of the newest body: encrypting one without the other would
|
|
50
|
+
* leave every conversation's latest message readable at rest, which is most of
|
|
51
|
+
* the exposure.
|
|
52
|
+
*
|
|
53
|
+
* Participant display names are deliberately NOT here — decision D1 keeps names
|
|
54
|
+
* searchable, and both sides already know who they are talking to.
|
|
55
|
+
*/
|
|
56
|
+
export declare const CONVERSATION_PII_FIELDS: readonly ["lastMessage", "messages[].body"];
|
|
29
57
|
/** PII fields in reviews */
|
|
30
58
|
export declare const REVIEW_PII_FIELDS: readonly ["userName"];
|
|
31
59
|
/** Blind-index mapping for queryable review PII */
|
|
@@ -30,16 +30,49 @@ export const PAYOUT_PII_FIELDS = ["sellerEmail", "upiId"];
|
|
|
30
30
|
export const PAYOUT_PII_INDEX_MAP = {
|
|
31
31
|
sellerEmail: "sellerEmailIndex",
|
|
32
32
|
};
|
|
33
|
-
/**
|
|
34
|
-
|
|
33
|
+
/**
|
|
34
|
+
* PII fields in bids.
|
|
35
|
+
*
|
|
36
|
+
* `userEmail` only. `userName` is deliberately NOT encrypted: the seller bid
|
|
37
|
+
* search (`/api/store/bids`) does a prefix match on it, and encryption and
|
|
38
|
+
* partial-match search are mutually exclusive over the same field — ciphertext
|
|
39
|
+
* has no usable prefix. Encrypting it would silently break that search.
|
|
40
|
+
* Bidder names are already masked on the public side by `maskPublicBid`.
|
|
41
|
+
*/
|
|
42
|
+
export const BID_PII_FIELDS = ["userEmail"];
|
|
35
43
|
/** PII fields in newsletter subscribers */
|
|
36
44
|
export const NEWSLETTER_PII_FIELDS = [];
|
|
37
45
|
/** Blind-index mapping for newsletter subscribers */
|
|
38
46
|
export const NEWSLETTER_PII_INDEX_MAP = {};
|
|
39
|
-
/**
|
|
40
|
-
|
|
47
|
+
/**
|
|
48
|
+
* PII fields in tokens (email verification / password reset).
|
|
49
|
+
*
|
|
50
|
+
* Was an empty array, so the encrypt/decrypt machinery wrapped around these
|
|
51
|
+
* repositories was a no-op and every verification/reset email sat in cleartext.
|
|
52
|
+
* Safe to enable because `findByEmail` already queries the blind index first
|
|
53
|
+
* and falls back to plaintext equality, so legacy rows keep resolving while new
|
|
54
|
+
* ones are encrypted.
|
|
55
|
+
*/
|
|
56
|
+
export const TOKEN_PII_FIELDS = ["email"];
|
|
41
57
|
/** Blind-index mapping for token email queries */
|
|
42
|
-
export const TOKEN_PII_INDEX_MAP = {
|
|
58
|
+
export const TOKEN_PII_INDEX_MAP = {
|
|
59
|
+
email: "emailIndex",
|
|
60
|
+
};
|
|
61
|
+
/**
|
|
62
|
+
* PII fields in conversations — buyer↔seller message bodies.
|
|
63
|
+
*
|
|
64
|
+
* `messages[].body` uses the array-path form, and `lastMessage` is the
|
|
65
|
+
* denormalised copy of the newest body: encrypting one without the other would
|
|
66
|
+
* leave every conversation's latest message readable at rest, which is most of
|
|
67
|
+
* the exposure.
|
|
68
|
+
*
|
|
69
|
+
* Participant display names are deliberately NOT here — decision D1 keeps names
|
|
70
|
+
* searchable, and both sides already know who they are talking to.
|
|
71
|
+
*/
|
|
72
|
+
export const CONVERSATION_PII_FIELDS = [
|
|
73
|
+
"lastMessage",
|
|
74
|
+
"messages[].body",
|
|
75
|
+
];
|
|
43
76
|
/** PII fields in reviews */
|
|
44
77
|
export const REVIEW_PII_FIELDS = ["userName"];
|
|
45
78
|
/** Blind-index mapping for queryable review PII */
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { ProductDocument } from "../../features/products/schemas";
|
|
2
|
+
/**
|
|
3
|
+
* Derive `searchTxt` for a seeded product from its own content.
|
|
4
|
+
*
|
|
5
|
+
* Applied to the WHOLE array at each file's export rather than written per
|
|
6
|
+
* record. Four of the product seed files built records as inline literals with
|
|
7
|
+
* an inline `searchTxt:`, and in every one of them the last record — the
|
|
8
|
+
* "sold"/"depleted" fixture appended at the bottom — had been added without it.
|
|
9
|
+
* Those are exactly the rows the "Sold & Ended" scope exists to exercise, so
|
|
10
|
+
* they were invisible to search in the one view built to show them.
|
|
11
|
+
*
|
|
12
|
+
* The `...p` spread comes FIRST so a record cannot override or omit the field:
|
|
13
|
+
* that ordering is the actual guarantee, not the convention around it.
|
|
14
|
+
*
|
|
15
|
+
* Pure function of the record's content — deterministic, so this does not
|
|
16
|
+
* reintroduce the `appkit-seed` idempotency problem of Root Cause #25.
|
|
17
|
+
*/
|
|
18
|
+
export declare function withProductSearchTxt(p: Partial<ProductDocument>): Partial<ProductDocument>;
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { buildSearchTxt } from "../../utils/search-txt";
|
|
2
|
+
/**
|
|
3
|
+
* Derive `searchTxt` for a seeded product from its own content.
|
|
4
|
+
*
|
|
5
|
+
* Applied to the WHOLE array at each file's export rather than written per
|
|
6
|
+
* record. Four of the product seed files built records as inline literals with
|
|
7
|
+
* an inline `searchTxt:`, and in every one of them the last record — the
|
|
8
|
+
* "sold"/"depleted" fixture appended at the bottom — had been added without it.
|
|
9
|
+
* Those are exactly the rows the "Sold & Ended" scope exists to exercise, so
|
|
10
|
+
* they were invisible to search in the one view built to show them.
|
|
11
|
+
*
|
|
12
|
+
* The `...p` spread comes FIRST so a record cannot override or omit the field:
|
|
13
|
+
* that ordering is the actual guarantee, not the convention around it.
|
|
14
|
+
*
|
|
15
|
+
* Pure function of the record's content — deterministic, so this does not
|
|
16
|
+
* reintroduce the `appkit-seed` idempotency problem of Root Cause #25.
|
|
17
|
+
*/
|
|
18
|
+
export function withProductSearchTxt(p) {
|
|
19
|
+
return {
|
|
20
|
+
...p,
|
|
21
|
+
searchTxt: buildSearchTxt([
|
|
22
|
+
p.title,
|
|
23
|
+
p.description,
|
|
24
|
+
p.brand,
|
|
25
|
+
p.brandSlug,
|
|
26
|
+
p.categoryNames,
|
|
27
|
+
p.tags,
|
|
28
|
+
p.features,
|
|
29
|
+
p.condition,
|
|
30
|
+
p.specifications?.map((s) => `${s.name} ${s.value}`),
|
|
31
|
+
]),
|
|
32
|
+
};
|
|
33
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { StoreDocument } from "../../features/stores/schemas";
|
|
2
|
+
import type { EventDocument } from "../../features/events/schemas";
|
|
3
|
+
import type { BlogPostDocument } from "../../features/blog/schemas";
|
|
4
|
+
import type { ReviewDocument } from "../../features/reviews/schemas";
|
|
5
|
+
/** stores — name, description, category. No payout details, no contact. */
|
|
6
|
+
export declare function withStoreSearchTxt<T extends Partial<StoreDocument>>(p: T): T;
|
|
7
|
+
/** events — title, description, type, tags. */
|
|
8
|
+
export declare function withEventSearchTxt<T extends Partial<EventDocument>>(p: T): T;
|
|
9
|
+
/**
|
|
10
|
+
* blogPosts — title, excerpt, category, tags.
|
|
11
|
+
*
|
|
12
|
+
* The full `content` body is deliberately EXCLUDED. FAQ answers are short
|
|
13
|
+
* (measured: max 449 tokens, avg 200, nothing near the 600 cap), but blog bodies
|
|
14
|
+
* are an order of magnitude larger and would blow the cap — at which point
|
|
15
|
+
* `buildSearchTxt` truncates silently and the tail of every post becomes
|
|
16
|
+
* unsearchable with no error anywhere. The excerpt is the searchable summary.
|
|
17
|
+
*/
|
|
18
|
+
export declare function withBlogSearchTxt<T extends Partial<BlogPostDocument>>(p: T): T;
|
|
19
|
+
/** reviews — title, comment, productTitle. `userName` is PII and excluded. */
|
|
20
|
+
export declare function withReviewSearchTxt<T extends Partial<ReviewDocument>>(p: T): T;
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { buildSearchTxt } from "../../utils/search-txt";
|
|
2
|
+
/**
|
|
3
|
+
* Per-collection `searchTxt` derivation for seed data.
|
|
4
|
+
*
|
|
5
|
+
* Applied to the WHOLE array at each file's export, never per record. Writing
|
|
6
|
+
* the field inside object literals is how five product seed files shipped their
|
|
7
|
+
* last fixture — the sold/depleted row — with no tokens at all: the field was
|
|
8
|
+
* added to every record except the one appended at the bottom. The `...p` spread
|
|
9
|
+
* comes FIRST here so a record cannot override or omit it.
|
|
10
|
+
*
|
|
11
|
+
* All of these are pure functions of the record's own content, so they do not
|
|
12
|
+
* reintroduce the `appkit-seed` idempotency problem of Root Cause #25.
|
|
13
|
+
*
|
|
14
|
+
* 🛑 **No PII in any of these.** `searchTxt` stores readable fragments of the
|
|
15
|
+
* source text, so feeding it an encrypted field would undo the encryption.
|
|
16
|
+
* Decision D1: names stay searchable, emails/phones/addresses do not.
|
|
17
|
+
*
|
|
18
|
+
* Each wrapper is typed against its own document rather than a shared
|
|
19
|
+
* `Record<string, unknown>`: that shape trips `audit-unknown-leakage`, and more
|
|
20
|
+
* importantly it would silently accept a field that no longer exists after a
|
|
21
|
+
* rename, which is exactly the class of drift this migration is fixing.
|
|
22
|
+
*/
|
|
23
|
+
const stripHtml = (s) => (s ?? "").replace(/<[^>]+>/g, " ");
|
|
24
|
+
/** stores — name, description, category. No payout details, no contact. */
|
|
25
|
+
export function withStoreSearchTxt(p) {
|
|
26
|
+
return {
|
|
27
|
+
...p,
|
|
28
|
+
searchTxt: buildSearchTxt([
|
|
29
|
+
p.storeName,
|
|
30
|
+
stripHtml(p.storeDescription),
|
|
31
|
+
p.storeCategory,
|
|
32
|
+
]),
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
/** events — title, description, type, tags. */
|
|
36
|
+
export function withEventSearchTxt(p) {
|
|
37
|
+
return {
|
|
38
|
+
...p,
|
|
39
|
+
searchTxt: buildSearchTxt([
|
|
40
|
+
p.title,
|
|
41
|
+
stripHtml(p.description),
|
|
42
|
+
p.type,
|
|
43
|
+
p.tags ?? [],
|
|
44
|
+
]),
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* blogPosts — title, excerpt, category, tags.
|
|
49
|
+
*
|
|
50
|
+
* The full `content` body is deliberately EXCLUDED. FAQ answers are short
|
|
51
|
+
* (measured: max 449 tokens, avg 200, nothing near the 600 cap), but blog bodies
|
|
52
|
+
* are an order of magnitude larger and would blow the cap — at which point
|
|
53
|
+
* `buildSearchTxt` truncates silently and the tail of every post becomes
|
|
54
|
+
* unsearchable with no error anywhere. The excerpt is the searchable summary.
|
|
55
|
+
*/
|
|
56
|
+
export function withBlogSearchTxt(p) {
|
|
57
|
+
return {
|
|
58
|
+
...p,
|
|
59
|
+
searchTxt: buildSearchTxt([
|
|
60
|
+
p.title,
|
|
61
|
+
stripHtml(p.excerpt),
|
|
62
|
+
p.category,
|
|
63
|
+
p.tags ?? [],
|
|
64
|
+
]),
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
/** reviews — title, comment, productTitle. `userName` is PII and excluded. */
|
|
68
|
+
export function withReviewSearchTxt(p) {
|
|
69
|
+
return {
|
|
70
|
+
...p,
|
|
71
|
+
searchTxt: buildSearchTxt([p.title, stripHtml(p.comment), p.productTitle]),
|
|
72
|
+
};
|
|
73
|
+
}
|
|
@@ -1,2 +1,6 @@
|
|
|
1
1
|
import type { BlogPostDocument } from "../features/blog/schemas";
|
|
2
|
+
/**
|
|
3
|
+
* Tokens derived here, not per record — see _helpers/search-txt-wrappers.ts for
|
|
4
|
+
* why the wrapper form is what makes omission impossible.
|
|
5
|
+
*/
|
|
2
6
|
export declare const blogPostsSeedData: Partial<BlogPostDocument>[];
|
|
@@ -13,10 +13,11 @@
|
|
|
13
13
|
* @tag consumers:seed/index.ts,seed/runner.ts
|
|
14
14
|
* @tag sideEffects:none
|
|
15
15
|
*/
|
|
16
|
+
import { withBlogSearchTxt } from "./_helpers/search-txt-wrappers";
|
|
16
17
|
import { seedExtMedia } from "./_helpers/media";
|
|
17
18
|
const NOW = new Date();
|
|
18
19
|
const daysAgo = (n) => new Date(NOW.getTime() - n * 86400000);
|
|
19
|
-
|
|
20
|
+
const _rawblogPostsSeedData = [
|
|
20
21
|
// ── GUIDES (6) ────────────────────────────────────────────────────────────
|
|
21
22
|
{
|
|
22
23
|
id: "blog-spot-genuine-takara-tomy-beyblade",
|
|
@@ -434,3 +435,8 @@ export const blogPostsSeedData = [
|
|
|
434
435
|
updatedAt: daysAgo(60),
|
|
435
436
|
},
|
|
436
437
|
];
|
|
438
|
+
/**
|
|
439
|
+
* Tokens derived here, not per record — see _helpers/search-txt-wrappers.ts for
|
|
440
|
+
* why the wrapper form is what makes omission impossible.
|
|
441
|
+
*/
|
|
442
|
+
export const blogPostsSeedData = _rawblogPostsSeedData.map(withBlogSearchTxt);
|
|
@@ -1,3 +1,7 @@
|
|
|
1
1
|
import type { EventDocument, EventEntryDocument } from "../features/events/schemas";
|
|
2
|
-
export declare const eventsSeedData: EventDocument[];
|
|
3
2
|
export declare const eventEntriesSeedData: EventEntryDocument[];
|
|
3
|
+
/**
|
|
4
|
+
* Tokens derived here, not per record — see _helpers/search-txt-wrappers.ts for
|
|
5
|
+
* why the wrapper form is what makes omission impossible.
|
|
6
|
+
*/
|
|
7
|
+
export declare const eventsSeedData: EventDocument[];
|