@mohasinac/appkit 4.29.1 → 4.30.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/layout/SidebarCollapseToggle.js +1 -1
- package/dist/_internal/shared/features/blog/schema.d.ts +2 -2
- package/dist/_internal/shared/features/blog/schema.js +2 -1
- package/dist/_internal/shared/features/brands/schema.d.ts +2 -2
- package/dist/_internal/shared/features/brands/schema.js +3 -1
- package/dist/_internal/shared/features/categories/bundle-schemas.d.ts +3 -3
- package/dist/_internal/shared/features/categories/bundle-schemas.js +2 -1
- package/dist/_internal/shared/features/events/schema.d.ts +2 -2
- package/dist/_internal/shared/features/events/schema.js +2 -1
- package/dist/_internal/shared/features/products/schema.d.ts +8 -8
- package/dist/_internal/shared/features/products/schema.js +3 -2
- package/dist/_internal/shared/features/reviews/schema.d.ts +1 -1
- package/dist/_internal/shared/features/reviews/schema.js +2 -1
- package/dist/_internal/shared/types/action-result.d.ts +0 -2
- package/dist/client/api/surface-error.d.ts +9 -0
- package/dist/client/api/surface-error.js +29 -3
- package/dist/client/api/useApiMutation.d.ts +10 -0
- package/dist/client/api/useApiMutation.js +2 -1
- package/dist/core/server-action.d.ts +3 -14
- package/dist/core/server-action.js +11 -2
- package/dist/errors/error-envelope.d.ts +73 -0
- package/dist/errors/error-envelope.js +96 -0
- package/dist/errors/error-handler.js +14 -7
- package/dist/features/account/components/NotificationBell.js +1 -1
- package/dist/features/account/components/UserSidebar.js +2 -2
- package/dist/features/account/components/UserSupportView.js +2 -6
- package/dist/features/account/hooks/useProfile.d.ts +6 -0
- package/dist/features/account/hooks/useProfile.js +1 -0
- package/dist/features/admin/components/AdminAdEditorView.js +1 -3
- package/dist/features/admin/components/AdminAddressClustersView.js +1 -3
- package/dist/features/admin/components/AdminAddressEditorView.js +2 -4
- package/dist/features/admin/components/AdminAddressesView.js +1 -3
- package/dist/features/admin/components/AdminAllEventEntriesView.js +1 -3
- package/dist/features/admin/components/AdminBidsView.js +1 -3
- package/dist/features/admin/components/AdminBlogEditorView.js +2 -2
- package/dist/features/admin/components/AdminBrandEditorView.js +4 -5
- package/dist/features/admin/components/AdminCarouselGroupEditorView.js +1 -3
- package/dist/features/admin/components/AdminCategoryEditorView.js +2 -4
- package/dist/features/admin/components/AdminContactEditorView.js +1 -3
- package/dist/features/admin/components/AdminContactView.js +2 -6
- package/dist/features/admin/components/AdminCouponEditorView.js +2 -4
- package/dist/features/admin/components/AdminEmployeeEditorView.js +3 -9
- package/dist/features/admin/components/AdminFaqEditorView.js +2 -4
- package/dist/features/admin/components/AdminFeatureEditorView.js +0 -9
- package/dist/features/admin/components/AdminFeatureFlagsView.js +2 -4
- package/dist/features/admin/components/AdminNavEditorView.js +1 -3
- package/dist/features/admin/components/AdminNavigationView.js +3 -3
- package/dist/features/admin/components/AdminNewsletterView.js +1 -3
- package/dist/features/admin/components/AdminNotificationsView.js +2 -6
- package/dist/features/admin/components/AdminOffersView.js +1 -3
- package/dist/features/admin/components/AdminOrderEditorView.js +1 -3
- package/dist/features/admin/components/AdminPaymentClustersView.js +1 -3
- package/dist/features/admin/components/AdminPaymentMethodsView.js +1 -3
- package/dist/features/admin/components/AdminPayoutMarkPaidModal.js +2 -4
- package/dist/features/admin/components/AdminProductEditorView.js +2 -4
- package/dist/features/admin/components/AdminReturnRequestsView.js +2 -6
- package/dist/features/admin/components/AdminReviewsView.js +2 -6
- package/dist/features/admin/components/AdminScammerEditorView.js +2 -6
- package/dist/features/admin/components/AdminSectionsView.js +2 -6
- package/dist/features/admin/components/AdminSessionsView.js +1 -3
- package/dist/features/admin/components/AdminSidebar.js +2 -2
- package/dist/features/admin/components/AdminSiteSettingsView.js +1 -1
- package/dist/features/admin/components/AdminStoreEditorView.js +1 -3
- package/dist/features/admin/components/AdminStoresView.js +2 -6
- package/dist/features/admin/components/AdminSublistingCategoryEditorView.js +2 -6
- package/dist/features/admin/components/AdminSupportTicketDetailView.js +7 -21
- package/dist/features/admin/components/AdminTesterChecklistItemEditorView.js +2 -4
- package/dist/features/admin/components/AdminUserEditorView.js +6 -18
- package/dist/features/admin/components/AdminUsersView.js +2 -6
- package/dist/features/admin/components/BrandQuickCreateForm.js +2 -4
- package/dist/features/admin/components/CategoryQuickCreateForm.js +2 -4
- package/dist/features/auctions/components/PlaceBidFormClient.js +1 -1
- package/dist/features/auth/components/ForgotPasswordView.js +1 -1
- package/dist/features/auth/components/LoginForm.js +1 -1
- package/dist/features/auth/components/RegisterForm.js +1 -1
- package/dist/features/auth/components/ResetPasswordView.js +1 -1
- package/dist/features/auth/components/SocialAuthButtons.js +1 -1
- package/dist/features/auth/repository/session.repository.js +13 -1
- package/dist/features/blog/actions/blog-actions.d.ts +14 -14
- package/dist/features/blog/actions/blog-actions.js +6 -8
- package/dist/features/blog/schemas/index.d.ts +1 -1
- package/dist/features/blog/schemas/index.js +2 -4
- package/dist/features/categories/api/[id]/route.js +2 -1
- package/dist/features/categories/api/route.js +2 -1
- package/dist/features/events/schemas/index.d.ts +1 -1
- package/dist/features/events/schemas/index.js +2 -4
- package/dist/features/faq/components/ContactCTA.js +1 -1
- package/dist/features/filters/AsyncFacetSection.js +1 -1
- package/dist/features/filters/FilterFacetSection.js +1 -1
- package/dist/features/filters/RangeFilter.js +1 -1
- package/dist/features/filters/SwitchFilter.js +1 -1
- package/dist/features/homepage/api/carousel/[id]/route.js +4 -3
- package/dist/features/homepage/api/carousel/route.js +4 -3
- package/dist/features/homepage/api/route.js +3 -2
- package/dist/features/homepage/components/GoogleReviewsSection.js +1 -1
- package/dist/features/homepage/components/HeroBanner.js +1 -1
- package/dist/features/homepage/components/HeroCarousel.js +1 -1
- package/dist/features/homepage/components/SocialPostCard.js +1 -1
- package/dist/features/homepage/components/WhatsAppCommunitySection.js +1 -1
- package/dist/features/layout/AppLayoutShell.js +4 -4
- package/dist/features/layout/BottomNavbar.js +2 -2
- package/dist/features/layout/MainNavbar.js +1 -1
- package/dist/features/layout/TitleBarLayout.js +24 -12
- package/dist/features/media/AvatarUpload.js +28 -10
- package/dist/features/media/modals/ImageCropModal.js +1 -1
- package/dist/features/media/modals/ImageEditor.js +1 -1
- package/dist/features/products/components/MakeOfferButton.js +1 -1
- package/dist/features/reviews/api/[id]/route.js +3 -2
- package/dist/features/reviews/api/route.js +3 -2
- package/dist/features/search/components/Search.js +2 -2
- package/dist/features/seller/components/SellerAnalyticsAlertsView.js +3 -3
- package/dist/features/seller/components/SellerSidebar.js +2 -2
- package/dist/features/store-extensions/components/ReviewDecisionModal.js +1 -1
- package/dist/features/stores/repository/store.repository.d.ts +21 -0
- package/dist/features/stores/repository/store.repository.js +32 -0
- package/dist/features/whatsapp-bot/components/SellerWhatsAppSettingsView.js +0 -2
- package/dist/index.d.ts +3 -2
- package/dist/index.js +4 -1
- package/dist/next/api/api-response.d.ts +29 -12
- package/dist/next/api/api-response.js +9 -8
- package/dist/next/api/apiHandler.d.ts +6 -1
- package/dist/next/api/apiHandler.js +13 -1
- package/dist/next/api/routeHandler.js +6 -6
- package/dist/next/middleware/pii-scrubber.js +5 -2
- package/dist/providers/db-firebase/base.repository.d.ts +14 -0
- package/dist/providers/db-firebase/base.repository.js +22 -0
- package/dist/providers/db-firebase/sieve.d.ts +14 -1
- package/dist/providers/db-firebase/sieve.js +3 -1
- package/dist/security/pii-encrypt.d.ts +0 -4
- package/dist/security/pii-encrypt.js +79 -13
- package/dist/security/pii-mask.d.ts +39 -1
- package/dist/security/pii-mask.js +33 -4
- package/dist/security/pii-redact.d.ts +0 -13
- package/dist/security/pii-redact.js +3 -1
- package/dist/security/pii-schemas.d.ts +0 -2
- package/dist/security/pii-schemas.js +16 -2
- package/dist/security/settings-encryption.js +31 -3
- package/dist/seed/test-utils.js +6 -2
- package/dist/ui/DataTable.js +2 -2
- package/dist/ui/components/CollapsibleSection.js +1 -1
- package/dist/ui/components/ImageGallery.js +1 -1
- package/dist/ui/components/ListingLayout.js +1 -1
- package/dist/ui/forms/FormShell.d.ts +0 -19
- package/dist/ui/forms/FormShell.js +4 -23
- package/dist/ui/forms/apply-zod-issues.d.ts +34 -0
- package/dist/ui/forms/apply-zod-issues.js +38 -0
- package/dist/ui/forms/index.d.ts +2 -1
- package/dist/ui/forms/index.js +2 -1
- package/dist/ui/icons/icon-registry.d.ts +8 -0
- package/dist/ui/icons/icon-registry.js +13 -1
- package/dist/utils/action-response.d.ts +1 -1
- package/dist/utils/media-url.d.ts +49 -0
- package/dist/utils/media-url.js +72 -1
- package/dist/validation/schemas.d.ts +15 -2
- package/dist/validation/schemas.js +18 -22
- package/package.json +1 -1
- package/scripts/seed-cli.mjs +10 -2
|
@@ -81,6 +81,7 @@ export function SellerAnalyticsAlertsView({ labels = {}, }) {
|
|
|
81
81
|
});
|
|
82
82
|
const alerts = data ?? [];
|
|
83
83
|
const createMutation = useApiMutation({
|
|
84
|
+
errorMessage: "Failed to create alert",
|
|
84
85
|
mutationFn: async () => {
|
|
85
86
|
/*
|
|
86
87
|
* Parsed against the SAME schema the route uses, before the request.
|
|
@@ -112,22 +113,21 @@ export function SellerAnalyticsAlertsView({ labels = {}, }) {
|
|
|
112
113
|
setShowForm(false);
|
|
113
114
|
setDraft(EMPTY_DRAFT);
|
|
114
115
|
},
|
|
115
|
-
onError: (err) => showToast(err?.message ?? "Failed to create alert", "error"),
|
|
116
116
|
});
|
|
117
117
|
const toggleMutation = useApiMutation({
|
|
118
|
+
errorMessage: "Failed to update alert",
|
|
118
119
|
mutationFn: async ({ id, isActive }) => apiClient.patch(SELLER_ENDPOINTS.ANALYTICS_ALERT_BY_ID(id), { isActive }),
|
|
119
120
|
onSuccess: () => {
|
|
120
121
|
void queryClient.invalidateQueries({ queryKey: ["seller", "analytics-alerts"] });
|
|
121
122
|
},
|
|
122
|
-
onError: () => showToast("Failed to update alert", "error"),
|
|
123
123
|
});
|
|
124
124
|
const deleteMutation = useApiMutation({
|
|
125
|
+
errorMessage: "Failed to delete alert",
|
|
125
126
|
mutationFn: async (id) => apiClient.delete(SELLER_ENDPOINTS.ANALYTICS_ALERT_BY_ID(id)),
|
|
126
127
|
onSuccess: () => {
|
|
127
128
|
showToast("Alert deleted", "success");
|
|
128
129
|
void queryClient.invalidateQueries({ queryKey: ["seller", "analytics-alerts"] });
|
|
129
130
|
},
|
|
130
|
-
onError: () => showToast("Failed to delete alert", "error"),
|
|
131
131
|
});
|
|
132
132
|
const toggleChannel = useCallback((ch) => {
|
|
133
133
|
setDraft((d) => ({
|
|
@@ -62,13 +62,13 @@ function GroupsContent({ groups, activeHref, storeName, storeLogoURL, onItemClic
|
|
|
62
62
|
const hasActive = !!findActiveNavItem(group.items, activeHref);
|
|
63
63
|
return (_jsxs(Div, { className: "mb-0.5", children: [_jsx(Button, { type: "button", variant: "ghost", onClick: () => toggle(group.title), paddingX: "md", paddingY: "sm", weight: "semibold", rounded: "none", className: `w-full text-[0.6875rem] uppercase tracking-widest transition-colors ${hasActive && !isOpen
|
|
64
64
|
? CLS_NAV_ICON_ACTIVE
|
|
65
|
-
: "text-[var(--appkit-color-text-faint)] hover:text-[var(--appkit-color-text-muted)]"}`, children: _jsxs(Row, { align: "center", justify: "between", className: "w-full", children: [_jsx(Span, { children: group.title }), _jsx("svg", { className: `w-3 h-3 transition-transform duration-150 ${isOpen ? "rotate-180" : ""}`, fill: "none", stroke: "currentColor", viewBox: "0 0 24 24", "aria-hidden": "true", children: _jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2.5, d: "M19 9l-7 7-7-7" }) })] }) }), isOpen && (_jsx(Ul, { paddingX: "x-sm", paddingY: "y-bottom-xs", spacing: "2xs", children: group.items.map((item) => (_jsx(Li, { children: _jsx(NavLink, { item: item, isActive: activeItem?.href === item.href, onClick: onItemClick }) }, item.href))) }))] }, group.title));
|
|
65
|
+
: "text-[var(--appkit-color-text-faint)] hover:text-[var(--appkit-color-text-muted)]"}`, children: _jsxs(Row, { align: "center", justify: "between", className: "w-full", children: [_jsx(Span, { children: group.title }), _jsx("svg", { className: `w-3 h-3 transition-transform duration-150 ${isOpen ? "rotate-180" : ""} shrink-0`, fill: "none", stroke: "currentColor", viewBox: "0 0 24 24", "aria-hidden": "true", children: _jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2.5, d: "M19 9l-7 7-7-7" }) })] }) }), isOpen && (_jsx(Ul, { paddingX: "x-sm", paddingY: "y-bottom-xs", spacing: "2xs", children: group.items.map((item) => (_jsx(Li, { children: _jsx(NavLink, { item: item, isActive: activeItem?.href === item.href, onClick: onItemClick }) }, item.href))) }))] }, group.title));
|
|
66
66
|
})] })] }));
|
|
67
67
|
}
|
|
68
68
|
function DrawerPanel({ title, onClose, children }) {
|
|
69
69
|
const { hand } = useHandMode();
|
|
70
70
|
const { edgeClass, borderClass } = getSidebarOverlayClasses(hand);
|
|
71
|
-
return (_jsxs(Div, { className: "hidden lg:block", children: [_jsx(Div, { surface: "overlay-xs", className: "fixed inset-0 z-40 backdrop-blur-sm", onClick: onClose, "aria-hidden": "true" }), _jsxs(Stack, { border: "default", shadow: "2xl", role: "dialog", "aria-modal": "true", "aria-label": title, className: `fixed top-0 ${edgeClass} z-50 h-full w-64 ${borderClass}`, surface: "default", children: [_jsxs(Row, { border: "bottom-subtle", paddingY: "y-sm-tall", className: "shrink-0", padding: "x-md", align: "center", justify: "between", children: [_jsx(Span, { size: "xs", weight: "semibold", transform: "uppercase", color: "muted", children: title }), _jsx(IconButton, { "aria-label": "Close", variant: "ghost", size: "sm", onClick: onClose, icon: _jsx("svg", { className: "w-4 h-4", fill: "none", stroke: "currentColor", viewBox: "0 0 24 24", "aria-hidden": "true", children: _jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2, d: "M6 18L18 6M6 6l12 12" }) }) })] }), _jsx(Div, { className: `flex-1 ${__O.yAuto}`, children: children })] })] }));
|
|
71
|
+
return (_jsxs(Div, { className: "hidden lg:block", children: [_jsx(Div, { surface: "overlay-xs", className: "fixed inset-0 z-40 backdrop-blur-sm", onClick: onClose, "aria-hidden": "true" }), _jsxs(Stack, { border: "default", shadow: "2xl", role: "dialog", "aria-modal": "true", "aria-label": title, className: `fixed top-0 ${edgeClass} z-50 h-full w-64 ${borderClass}`, surface: "default", children: [_jsxs(Row, { border: "bottom-subtle", paddingY: "y-sm-tall", className: "shrink-0", padding: "x-md", align: "center", justify: "between", children: [_jsx(Span, { size: "xs", weight: "semibold", transform: "uppercase", color: "muted", children: title }), _jsx(IconButton, { "aria-label": "Close", variant: "ghost", size: "sm", onClick: onClose, icon: _jsx("svg", { className: "w-4 h-4 shrink-0", fill: "none", stroke: "currentColor", viewBox: "0 0 24 24", "aria-hidden": "true", children: _jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2, d: "M6 18L18 6M6 6l12 12" }) }) })] }), _jsx(Div, { className: `flex-1 ${__O.yAuto}`, children: children })] })] }));
|
|
72
72
|
}
|
|
73
73
|
export function StoreSidebar({ items, groups, activeHref, storeName, storeLogoURL, mobileOpen = false, desktopOpen = false, variant = "overlay", onCloseMobile, onToggle, renderFooter, }) {
|
|
74
74
|
const close = onCloseMobile ?? (() => { });
|
|
@@ -44,7 +44,7 @@ import { Stack } from "../../../ui/components/Layout";
|
|
|
44
44
|
import { Form } from "../../../ui/components/Form";
|
|
45
45
|
import { FieldTextarea } from "../../../ui/forms/FieldTextarea";
|
|
46
46
|
import { FormErrorSummary } from "../../../ui/forms/FormErrorSummary";
|
|
47
|
-
import { applyZodIssues } from "../../../ui/forms/
|
|
47
|
+
import { applyZodIssues } from "../../../ui/forms/apply-zod-issues";
|
|
48
48
|
import { useToast } from "../../../ui/components/Toast";
|
|
49
49
|
import { normalizeError } from "../../../errors/normalize";
|
|
50
50
|
export function ReviewDecisionModal({ isOpen, onClose, title, schema, status, noteField, noteLabel, noteHelp, notePlaceholder, confirmLabel, confirmVariant = "primary", onConfirm, }) {
|
|
@@ -19,6 +19,27 @@ export declare class StoreRepository extends BaseRepository<StoreDocument> {
|
|
|
19
19
|
private encryptSecrets;
|
|
20
20
|
private decryptSecrets;
|
|
21
21
|
protected mapDoc<D = StoreDocument>(snap: DocumentSnapshot): D;
|
|
22
|
+
/**
|
|
23
|
+
* LIST reads never carry the WhatsApp token — not even encrypted.
|
|
24
|
+
*
|
|
25
|
+
* `mapDoc` above decrypts it, and `listStores(model, activeOnly)` backs the
|
|
26
|
+
* PUBLIC `/stores` page. Once `sieveQuery` started routing list rows through
|
|
27
|
+
* `mapDoc` (it previously mapped documents itself and skipped every
|
|
28
|
+
* override), that decrypt would have put a plaintext Meta OAuth bearer token
|
|
29
|
+
* on a public listing — the exact shape of Root Cause #70.
|
|
30
|
+
*
|
|
31
|
+
* So the list path deliberately does NOT call `decryptSecrets`, and drops
|
|
32
|
+
* `accessToken` outright: a list has no legitimate use for it, and a value
|
|
33
|
+
* that is never in the payload cannot be leaked by a projection someone
|
|
34
|
+
* forgets to write.
|
|
35
|
+
*
|
|
36
|
+
* Detail reads are unchanged — `findBySlug`/`findById` still decrypt, so the
|
|
37
|
+
* WhatsApp settings and catalog-sync routes keep working exactly as before.
|
|
38
|
+
* Removing the decrypt from `mapDoc` itself is a separate, riskier change
|
|
39
|
+
* that CLAUDE.md explicitly defers; this closes the public-exposure half
|
|
40
|
+
* without touching the encryption round-trip at all.
|
|
41
|
+
*/
|
|
42
|
+
protected mapDocForList<D = StoreDocument>(snap: DocumentSnapshot): D;
|
|
22
43
|
update(id: string, data: Partial<StoreDocument>): Promise<StoreDocument>;
|
|
23
44
|
/**
|
|
24
45
|
* Create a new store.
|
|
@@ -36,6 +36,33 @@ export class StoreRepository extends BaseRepository {
|
|
|
36
36
|
mapDoc(snap) {
|
|
37
37
|
return this.decryptSecrets(super.mapDoc(snap));
|
|
38
38
|
}
|
|
39
|
+
/**
|
|
40
|
+
* LIST reads never carry the WhatsApp token — not even encrypted.
|
|
41
|
+
*
|
|
42
|
+
* `mapDoc` above decrypts it, and `listStores(model, activeOnly)` backs the
|
|
43
|
+
* PUBLIC `/stores` page. Once `sieveQuery` started routing list rows through
|
|
44
|
+
* `mapDoc` (it previously mapped documents itself and skipped every
|
|
45
|
+
* override), that decrypt would have put a plaintext Meta OAuth bearer token
|
|
46
|
+
* on a public listing — the exact shape of Root Cause #70.
|
|
47
|
+
*
|
|
48
|
+
* So the list path deliberately does NOT call `decryptSecrets`, and drops
|
|
49
|
+
* `accessToken` outright: a list has no legitimate use for it, and a value
|
|
50
|
+
* that is never in the payload cannot be leaked by a projection someone
|
|
51
|
+
* forgets to write.
|
|
52
|
+
*
|
|
53
|
+
* Detail reads are unchanged — `findBySlug`/`findById` still decrypt, so the
|
|
54
|
+
* WhatsApp settings and catalog-sync routes keep working exactly as before.
|
|
55
|
+
* Removing the decrypt from `mapDoc` itself is a separate, riskier change
|
|
56
|
+
* that CLAUDE.md explicitly defers; this closes the public-exposure half
|
|
57
|
+
* without touching the encryption round-trip at all.
|
|
58
|
+
*/
|
|
59
|
+
mapDocForList(snap) {
|
|
60
|
+
const doc = super.mapDoc(snap);
|
|
61
|
+
if (!doc.whatsappConfig)
|
|
62
|
+
return doc;
|
|
63
|
+
const { accessToken: _dropped, ...safeConfig } = doc.whatsappConfig;
|
|
64
|
+
return { ...doc, whatsappConfig: safeConfig };
|
|
65
|
+
}
|
|
39
66
|
async update(id, data) {
|
|
40
67
|
return super.update(id, this.encryptSecrets(data));
|
|
41
68
|
}
|
|
@@ -179,6 +206,11 @@ export class StoreRepository extends BaseRepository {
|
|
|
179
206
|
baseQuery,
|
|
180
207
|
model,
|
|
181
208
|
fields: sieveFields,
|
|
209
|
+
// Called DIRECTLY, not through `sieveQuery`, so the mapper has to be
|
|
210
|
+
// passed by hand — and it is the list mapper, never `mapDoc`. With
|
|
211
|
+
// `activeOnly` this is the public /stores listing; `mapDoc` decrypts the
|
|
212
|
+
// WhatsApp bearer token and a list has no use for it.
|
|
213
|
+
mapDoc: (snap) => this.mapDocForList(snap),
|
|
182
214
|
});
|
|
183
215
|
}
|
|
184
216
|
/**
|
|
@@ -123,7 +123,6 @@ export function SellerWhatsAppSettingsView({ hasCapability }) {
|
|
|
123
123
|
},
|
|
124
124
|
onError: (err) => {
|
|
125
125
|
const msg = err instanceof Error ? err.message : "Failed to save settings";
|
|
126
|
-
showToast(msg, "error");
|
|
127
126
|
},
|
|
128
127
|
});
|
|
129
128
|
// Catalog sync mutation (push: site → WhatsApp)
|
|
@@ -136,7 +135,6 @@ export function SellerWhatsAppSettingsView({ hasCapability }) {
|
|
|
136
135
|
},
|
|
137
136
|
onError: (err) => {
|
|
138
137
|
const msg = err instanceof Error ? err.message : "Sync failed";
|
|
139
|
-
showToast(msg, "error");
|
|
140
138
|
},
|
|
141
139
|
});
|
|
142
140
|
// Catalog import (pull: WhatsApp → site). Runs as the `whatsappCatalogImport`
|
package/dist/index.d.ts
CHANGED
|
@@ -411,7 +411,7 @@ export { unitOfWork } from "./core/index";
|
|
|
411
411
|
export { useSiteSettings } from "./core/index";
|
|
412
412
|
export type { ActionMiddleware } from "./core/index";
|
|
413
413
|
export type { ActionMiddlewareContext } from "./core/index";
|
|
414
|
-
export type { ActionResult } from "./
|
|
414
|
+
export type { ActionResult } from "./_internal/shared/types/action-result";
|
|
415
415
|
export { handleActionError } from "./utils/action-response";
|
|
416
416
|
export type { CacheEntry } from "./core/index";
|
|
417
417
|
export type { CacheOptions } from "./core/index";
|
|
@@ -799,6 +799,7 @@ export { createPiiRedactorMiddleware } from "./next/index";
|
|
|
799
799
|
export { createRouteHandler } from "./next/index";
|
|
800
800
|
export { createRouteMap } from "./next/index";
|
|
801
801
|
export { errorResponse } from "./next/index";
|
|
802
|
+
export { buildErrorEnvelope, toApiIssues, codeForStatus, type ApiErrorEnvelope, } from "./errors/error-envelope";
|
|
802
803
|
export { getBooleanParam } from "./next/index";
|
|
803
804
|
export { getNumberParam } from "./next/index";
|
|
804
805
|
export { getOptionalSessionCookie } from "./next/index";
|
|
@@ -3139,7 +3140,7 @@ export { formatMonthYear } from "./utils/date.formatter";
|
|
|
3139
3140
|
export { generateMediaFilename } from "./utils/id-generators";
|
|
3140
3141
|
export { deriveContextTypeFromFilename } from "./utils/id-generators";
|
|
3141
3142
|
export type { MediaFilenameContext } from "./utils/id-generators";
|
|
3142
|
-
export { resolveMediaUrl, resolveVideoUrl, FIREBASE_STORAGE_HOST, GCS_HOST } from "./utils/media-url";
|
|
3143
|
+
export { resolveMediaUrl, resolveVideoUrl, FIREBASE_STORAGE_HOST, GCS_HOST, isStoredMediaRef, MEDIA_PROXY_PREFIX, MEDIA_URL_MAX_LENGTH, MEDIA_URL_MESSAGE, APPROVED_MEDIA_DOMAINS, } from "./utils/media-url";
|
|
3143
3144
|
export { generateFAQId } from "./utils/id-generators";
|
|
3144
3145
|
export type { GenerateFAQIdInput } from "./utils/id-generators";
|
|
3145
3146
|
export { generateCroppedImageFilename } from "./utils/id-generators";
|
package/dist/index.js
CHANGED
|
@@ -1640,6 +1640,9 @@ export { createRouteMap } from "./next/index";
|
|
|
1640
1640
|
// [SERVER-ONLY]-Server-only â€" uses Node.js, Next.js server internals, or third-party server SDKs (auth, email, payment, shipping).
|
|
1641
1641
|
// errorResponse - Model for error response.
|
|
1642
1642
|
export { errorResponse } from "./next/index";
|
|
1643
|
+
// The one error-body shape, plus the Zod-issue narrower every route needs to
|
|
1644
|
+
// put field errors on the wire under the key ApiClientError actually reads.
|
|
1645
|
+
export { buildErrorEnvelope, toApiIssues, codeForStatus, } from "./errors/error-envelope";
|
|
1643
1646
|
// [SERVER-ONLY]-Server-only â€" uses Node.js, Next.js server internals, or third-party server SDKs (auth, email, payment, shipping).
|
|
1644
1647
|
// getBooleanParam - Helper for get boolean param.
|
|
1645
1648
|
export { getBooleanParam } from "./next/index";
|
|
@@ -5550,7 +5553,7 @@ export { formatCurrency, formatFileSize } from "./utils/number.formatter";
|
|
|
5550
5553
|
export { formatMonthYear } from "./utils/date.formatter";
|
|
5551
5554
|
export { generateMediaFilename } from "./utils/id-generators"; // generateProductImageFilename already exported from "./utils/id-generators";
|
|
5552
5555
|
export { deriveContextTypeFromFilename } from "./utils/id-generators";
|
|
5553
|
-
export { resolveMediaUrl, resolveVideoUrl, FIREBASE_STORAGE_HOST, GCS_HOST } from "./utils/media-url";
|
|
5556
|
+
export { resolveMediaUrl, resolveVideoUrl, FIREBASE_STORAGE_HOST, GCS_HOST, isStoredMediaRef, MEDIA_PROXY_PREFIX, MEDIA_URL_MAX_LENGTH, MEDIA_URL_MESSAGE, APPROVED_MEDIA_DOMAINS, } from "./utils/media-url";
|
|
5554
5557
|
export { generateFAQId } from "./utils/id-generators";
|
|
5555
5558
|
export { generateCroppedImageFilename } from "./utils/id-generators";
|
|
5556
5559
|
// Messages / Conversations feature schemas
|
|
@@ -6,25 +6,42 @@
|
|
|
6
6
|
*/
|
|
7
7
|
import { NextResponse } from "next/server";
|
|
8
8
|
import type { JsonValue } from "@mohasinac/appkit";
|
|
9
|
+
import type { ApiIssue } from "../../client/api/ApiError";
|
|
10
|
+
import { type ApiErrorEnvelope } from "../../errors/error-envelope";
|
|
9
11
|
export interface ApiSuccessResponse<T = unknown> {
|
|
10
12
|
success: true;
|
|
11
13
|
data?: T;
|
|
12
14
|
message?: string;
|
|
13
15
|
meta?: Record<string, JsonValue>;
|
|
14
16
|
}
|
|
15
|
-
export
|
|
16
|
-
success: false;
|
|
17
|
-
error: string;
|
|
18
|
-
details?: unknown;
|
|
19
|
-
}
|
|
17
|
+
export type ApiErrorResponse = ApiErrorEnvelope;
|
|
20
18
|
export declare function successResponse<T>(data?: T, message?: string, status?: number, meta?: Record<string, JsonValue>): NextResponse<ApiSuccessResponse<T>>;
|
|
21
|
-
|
|
19
|
+
/**
|
|
20
|
+
* Options for an error response.
|
|
21
|
+
*
|
|
22
|
+
* This replaced a bare `details?: unknown` third positional argument that was
|
|
23
|
+
* carrying three mutually incompatible things across the codebase — a stable
|
|
24
|
+
* code string, an array of Zod issues, and arbitrary server-side context —
|
|
25
|
+
* all serialised under a `details` key that no client ever read. Narrowing the
|
|
26
|
+
* type is what let `tsc` enumerate every call site so each could be given the
|
|
27
|
+
* meaning it actually intended.
|
|
28
|
+
*
|
|
29
|
+
* Server-side context does NOT belong here: it goes to `serverLogger`. Only
|
|
30
|
+
* things the client can act on cross the wire.
|
|
31
|
+
*/
|
|
32
|
+
export interface ErrorResponseOptions {
|
|
33
|
+
/** Stable machine code. Defaults to one derived from `status`. */
|
|
34
|
+
code?: string;
|
|
35
|
+
/** Field-level validation issues — routed to inline field errors by `surfaceError`. */
|
|
36
|
+
issues?: ApiIssue[];
|
|
37
|
+
requestId?: string;
|
|
38
|
+
}
|
|
39
|
+
export declare function errorResponse(error: string, status?: number, opts?: ErrorResponseOptions): NextResponse<ApiErrorResponse>;
|
|
22
40
|
/** Pre-built shorthand factories for common HTTP error responses. */
|
|
23
41
|
export declare const ApiErrors: {
|
|
24
|
-
unauthorized: (message?: string) => NextResponse<
|
|
25
|
-
forbidden: (message?: string) => NextResponse<
|
|
26
|
-
notFound: (resource?: string) => NextResponse<
|
|
27
|
-
badRequest: (message?: string,
|
|
28
|
-
internalError: (message?: string) => NextResponse<
|
|
29
|
-
validationError: (details: Error) => NextResponse<ApiErrorResponse>;
|
|
42
|
+
unauthorized: (message?: string) => NextResponse<ApiErrorEnvelope>;
|
|
43
|
+
forbidden: (message?: string) => NextResponse<ApiErrorEnvelope>;
|
|
44
|
+
notFound: (resource?: string) => NextResponse<ApiErrorEnvelope>;
|
|
45
|
+
badRequest: (message?: string, opts?: ErrorResponseOptions) => NextResponse<ApiErrorEnvelope>;
|
|
46
|
+
internalError: (message?: string) => NextResponse<ApiErrorEnvelope>;
|
|
30
47
|
};
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
* No app-specific dependencies — safe to use in any project.
|
|
6
6
|
*/
|
|
7
7
|
import { NextResponse } from "next/server";
|
|
8
|
+
import { buildErrorEnvelope, } from "../../errors/error-envelope";
|
|
8
9
|
export function successResponse(data, message, status = 200, meta) {
|
|
9
10
|
return NextResponse.json({
|
|
10
11
|
success: true,
|
|
@@ -13,12 +14,8 @@ export function successResponse(data, message, status = 200, meta) {
|
|
|
13
14
|
...(meta && { meta }),
|
|
14
15
|
}, { status });
|
|
15
16
|
}
|
|
16
|
-
export function errorResponse(error, status = 400,
|
|
17
|
-
return NextResponse.json({
|
|
18
|
-
success: false,
|
|
19
|
-
error,
|
|
20
|
-
...(details !== undefined && { details }),
|
|
21
|
-
},
|
|
17
|
+
export function errorResponse(error, status = 400, opts) {
|
|
18
|
+
return NextResponse.json(buildErrorEnvelope({ message: error, status, ...opts }),
|
|
22
19
|
// `no-store` — without this, Vercel's edge CDN can cache a transient
|
|
23
20
|
// error response (cold-start 500, not-yet-synced 404) on a public GET
|
|
24
21
|
// route indefinitely. Same root cause as the /media 404-caching bug.
|
|
@@ -29,7 +26,11 @@ export const ApiErrors = {
|
|
|
29
26
|
unauthorized: (message = "Unauthorized") => errorResponse(message, 401),
|
|
30
27
|
forbidden: (message = "Forbidden") => errorResponse(message, 403),
|
|
31
28
|
notFound: (resource = "Resource") => errorResponse(`${resource} not found`, 404),
|
|
32
|
-
badRequest: (message = "Bad request",
|
|
29
|
+
badRequest: (message = "Bad request", opts) => errorResponse(message, 400, opts),
|
|
33
30
|
internalError: (message = "Internal server error") => errorResponse(message, 500),
|
|
34
|
-
validationError
|
|
31
|
+
// `validationError` used to live here as a 422 taking a raw `Error`. It had
|
|
32
|
+
// ZERO call sites, and its status disagreed with every other validation
|
|
33
|
+
// producer (`apiHandler`, `routeHandler` and `mapToHttpError` all use 400).
|
|
34
|
+
// Deleted rather than aligned — a fourth spelling of "the body was invalid"
|
|
35
|
+
// is what this whole file is trying to stop having.
|
|
35
36
|
};
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { ApiIssue } from "../../client/api/ApiError";
|
|
1
2
|
interface SafeParseSchema<TInput> {
|
|
2
3
|
safeParse: (input: unknown) => {
|
|
3
4
|
success: true;
|
|
@@ -40,7 +41,11 @@ export interface ApiHandlerFactoryDeps<TRole, TRateLimitConfig, TUser> {
|
|
|
40
41
|
applyRateLimit: (request: Request, config: TRateLimitConfig) => Promise<ApiRateLimitResult>;
|
|
41
42
|
requireAuthFromRequest: (request: Request) => Promise<TUser>;
|
|
42
43
|
requireRoleFromRequest: (request: Request, roles: TRole[]) => Promise<TUser>;
|
|
43
|
-
errorResponse: (message: string, status: number,
|
|
44
|
+
errorResponse: (message: string, status: number, opts?: {
|
|
45
|
+
code?: string;
|
|
46
|
+
issues?: ApiIssue[];
|
|
47
|
+
requestId?: string;
|
|
48
|
+
}) => Response;
|
|
44
49
|
handleApiError: (error: unknown) => Response;
|
|
45
50
|
getRateLimitExceededMessage: () => string;
|
|
46
51
|
logTiming: (entry: {
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { normalizeError } from "../../errors/normalize";
|
|
2
|
+
import { toApiIssues } from "../../errors/error-envelope";
|
|
3
|
+
import { HTTP_ERROR_CODES } from "../../errors/error-mapping";
|
|
2
4
|
async function validateSchema(request, schema) {
|
|
3
5
|
if (typeof schema.safeParse === "function") {
|
|
4
6
|
const body = await request.json();
|
|
@@ -49,7 +51,17 @@ async function applyRateLimitCheck(request, config, applyRateLimit, getRateLimit
|
|
|
49
51
|
async function applySchemaValidation(request, schema, errorResponse) {
|
|
50
52
|
const schemaResult = await validateSchema(request, schema);
|
|
51
53
|
if (schemaResult.validationError) {
|
|
52
|
-
|
|
54
|
+
// The issues travel as `issues`, which is the key ApiClientError reads.
|
|
55
|
+
// They used to be passed positionally and serialised under `details`,
|
|
56
|
+
// where nothing read them — so every schema rejection on every route
|
|
57
|
+
// reached the user as a bare "Validation failed" with no field name.
|
|
58
|
+
return {
|
|
59
|
+
invalid: true,
|
|
60
|
+
response: errorResponse("Validation failed", 400, {
|
|
61
|
+
code: HTTP_ERROR_CODES.VALIDATION_FAILED,
|
|
62
|
+
issues: toApiIssues(schemaResult.issues),
|
|
63
|
+
}),
|
|
64
|
+
};
|
|
53
65
|
}
|
|
54
66
|
return { invalid: false, data: schemaResult.data };
|
|
55
67
|
}
|
|
@@ -32,6 +32,7 @@ import { NextResponse } from "next/server.js";
|
|
|
32
32
|
import { getProviders } from "../../contracts";
|
|
33
33
|
import { isEffectiveAdminUser } from "../../features/auth/role-predicates";
|
|
34
34
|
import { mapToHttpError, HTTP_ERROR_CODES } from "../../errors/error-mapping";
|
|
35
|
+
import { buildErrorEnvelope, toApiIssues } from "../../errors/error-envelope";
|
|
35
36
|
import { serverErrorsRepository } from "../../features/server-errors/repository/server-errors.repository";
|
|
36
37
|
import { userRepository } from "../../repositories";
|
|
37
38
|
import { SCHEMAS } from "../../schemas/registry";
|
|
@@ -121,14 +122,13 @@ function resolveBodySchema(schema) {
|
|
|
121
122
|
* this helper and the audit re-runs to enforce the cleanup.
|
|
122
123
|
*/
|
|
123
124
|
function errorJson(status, code, message, requestId, issues) {
|
|
124
|
-
return NextResponse.json({
|
|
125
|
-
|
|
126
|
-
|
|
125
|
+
return NextResponse.json(buildErrorEnvelope({
|
|
126
|
+
message,
|
|
127
|
+
status,
|
|
127
128
|
code,
|
|
128
|
-
|
|
129
|
-
...(issues ? { issues } : {}),
|
|
129
|
+
issues: toApiIssues(issues),
|
|
130
130
|
requestId,
|
|
131
|
-
}, {
|
|
131
|
+
}), {
|
|
132
132
|
status,
|
|
133
133
|
// `no-store` on every error envelope — without this, Vercel's edge CDN
|
|
134
134
|
// can cache a transient failure (cold-start 500, not-yet-synced 404)
|
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
import { normalizeError } from "../../errors/normalize";
|
|
2
2
|
import { NextResponse } from "next/server";
|
|
3
|
-
|
|
3
|
+
import { hasEncPrefix } from "../../security/pii-mask";
|
|
4
|
+
// Scrubbing wants EVERY `enc:v1:` value gone, whichever of the two crypto
|
|
5
|
+
// systems wrote it and even if malformed — so `hasEncPrefix`, not
|
|
6
|
+
// `isPiiEncrypted`, which is now specifically the PII format.
|
|
4
7
|
function scrubValue(value) {
|
|
5
|
-
if (
|
|
8
|
+
if (hasEncPrefix(value)) {
|
|
6
9
|
return "[encrypted]";
|
|
7
10
|
}
|
|
8
11
|
if (Array.isArray(value))
|
|
@@ -7,6 +7,20 @@ export declare abstract class BaseRepository<T extends DocumentData> {
|
|
|
7
7
|
protected get db(): Firestore;
|
|
8
8
|
protected getCollection(): CollectionReference<DocumentData, DocumentData>;
|
|
9
9
|
protected mapDoc<D = T>(snap: DocumentSnapshot): D;
|
|
10
|
+
/**
|
|
11
|
+
* How a row is mapped on a LIST read (anything through `sieveQuery`).
|
|
12
|
+
*
|
|
13
|
+
* Defaults to `mapDoc`, so a repository that overrides `mapDoc` to decrypt,
|
|
14
|
+
* project or normalise now gets that on list reads too — Sieve used to map
|
|
15
|
+
* documents itself and never call `mapDoc` at all, which is why
|
|
16
|
+
* `/api/admin/users` served ciphertext emails.
|
|
17
|
+
*
|
|
18
|
+
* It is a SEPARATE hook because a list is not a detail read. A repository
|
|
19
|
+
* whose `mapDoc` decrypts a secret must not hand that secret to a list —
|
|
20
|
+
* `StoreRepository` overrides this to drop the WhatsApp token, since
|
|
21
|
+
* `listStores(activeOnly)` backs the PUBLIC /stores page.
|
|
22
|
+
*/
|
|
23
|
+
protected mapDocForList<D = T>(snap: DocumentSnapshot): D;
|
|
10
24
|
findById(id: string): Promise<T | null>;
|
|
11
25
|
findByIdOrFail(id: string): Promise<T>;
|
|
12
26
|
findBy(field: string, value: unknown): Promise<T[]>;
|
|
@@ -34,6 +34,22 @@ export class BaseRepository {
|
|
|
34
34
|
...(snap.data() ?? {}),
|
|
35
35
|
});
|
|
36
36
|
}
|
|
37
|
+
/**
|
|
38
|
+
* How a row is mapped on a LIST read (anything through `sieveQuery`).
|
|
39
|
+
*
|
|
40
|
+
* Defaults to `mapDoc`, so a repository that overrides `mapDoc` to decrypt,
|
|
41
|
+
* project or normalise now gets that on list reads too — Sieve used to map
|
|
42
|
+
* documents itself and never call `mapDoc` at all, which is why
|
|
43
|
+
* `/api/admin/users` served ciphertext emails.
|
|
44
|
+
*
|
|
45
|
+
* It is a SEPARATE hook because a list is not a detail read. A repository
|
|
46
|
+
* whose `mapDoc` decrypts a secret must not hand that secret to a list —
|
|
47
|
+
* `StoreRepository` overrides this to drop the WhatsApp token, since
|
|
48
|
+
* `listStores(activeOnly)` backs the PUBLIC /stores page.
|
|
49
|
+
*/
|
|
50
|
+
mapDocForList(snap) {
|
|
51
|
+
return this.mapDoc(snap);
|
|
52
|
+
}
|
|
37
53
|
async findById(id) {
|
|
38
54
|
try {
|
|
39
55
|
const doc = await this.getCollection().doc(id).get();
|
|
@@ -219,6 +235,12 @@ export class BaseRepository {
|
|
|
219
235
|
model,
|
|
220
236
|
fields,
|
|
221
237
|
options: sieveOptions,
|
|
238
|
+
// Load-bearing. Without this every Sieve-backed read silently skips the
|
|
239
|
+
// subclass's mapDoc — decryption, projection and normalisation all lost.
|
|
240
|
+
// audit-repository-fields asserts this argument stays here, because one
|
|
241
|
+
// edit removing it would unwire all 14 affected repositories at once
|
|
242
|
+
// while the per-file rule still passed.
|
|
243
|
+
mapDoc: (snap) => this.mapDocForList(snap),
|
|
222
244
|
});
|
|
223
245
|
}
|
|
224
246
|
/**
|
|
@@ -66,7 +66,7 @@
|
|
|
66
66
|
* Add nested-field indexes explicitly; Firestore does not auto-index nested
|
|
67
67
|
* paths in composite indexes declared at the root level.
|
|
68
68
|
*/
|
|
69
|
-
import type { CollectionReference, DocumentData, Query } from "firebase-admin/firestore";
|
|
69
|
+
import type { CollectionReference, DocumentData, Query, QueryDocumentSnapshot } from "firebase-admin/firestore";
|
|
70
70
|
import { FirebaseRepository } from "./base";
|
|
71
71
|
import { expandFilterAliases as _expandFilterAliases, type SieveFilterAlias, type SieveFilterAliases } from "./filter-aliases";
|
|
72
72
|
export { _expandFilterAliases as expandFilterAliases, type SieveFilterAlias, type SieveFilterAliases, };
|
|
@@ -153,6 +153,19 @@ export declare function applySieveToFirestore<T extends DocumentData>(params: {
|
|
|
153
153
|
model: SieveModel;
|
|
154
154
|
fields: SieveFields;
|
|
155
155
|
options?: SieveOptions;
|
|
156
|
+
/**
|
|
157
|
+
* Row mapper. Defaults to the same plain `deserializeTimestamps({id, ...data})`
|
|
158
|
+
* this function always did — so it is a provable no-op for the ~40
|
|
159
|
+
* repositories that don't override `mapDoc`.
|
|
160
|
+
*
|
|
161
|
+
* It exists because this function mapped documents ITSELF and never called
|
|
162
|
+
* the repository's `mapDoc`, so every read that went through Sieve skipped
|
|
163
|
+
* whatever that override did: decryption, projection, normalisation. The
|
|
164
|
+
* visible consequence was `/api/admin/users` serving `enc:v1:…` ciphertext
|
|
165
|
+
* as each user's email address, since `UserRepository.mapDoc` is what
|
|
166
|
+
* decrypts and `list()` goes through here.
|
|
167
|
+
*/
|
|
168
|
+
mapDoc?: (snap: QueryDocumentSnapshot) => T;
|
|
156
169
|
}): Promise<SieveResult<T>>;
|
|
157
170
|
export declare abstract class FirebaseSieveRepository<T extends DocumentData> extends FirebaseRepository<T> {
|
|
158
171
|
/**
|
|
@@ -189,7 +189,9 @@ export async function applySieveToFirestore(params) {
|
|
|
189
189
|
// the full Sieve DSL from the base.
|
|
190
190
|
const pagedQ = filteredQ.offset((page - 1) * pageSize).limit(pageSize);
|
|
191
191
|
const snap = await pagedQ.get();
|
|
192
|
-
const items = snap.docs.map((d) =>
|
|
192
|
+
const items = snap.docs.map((d) => params.mapDoc
|
|
193
|
+
? params.mapDoc(d)
|
|
194
|
+
: deserializeTimestamps({ id: d.id, ...d.data() }));
|
|
193
195
|
const totalPages = total === 0 ? 0 : Math.max(1, Math.ceil(total / pageSize));
|
|
194
196
|
return {
|
|
195
197
|
items,
|
|
@@ -22,10 +22,6 @@ export declare function decryptValue(ciphertext: string): string;
|
|
|
22
22
|
*/
|
|
23
23
|
export declare function hmacBlindIndex(value: string): string;
|
|
24
24
|
export declare function encryptPiiFields<T extends object>(doc: T, piiFields: string[]): T;
|
|
25
|
-
/**
|
|
26
|
-
* Decrypt specified PII fields in a Firestore document.
|
|
27
|
-
* Fields that are not encrypted (no `enc:v1:` prefix) are passed through.
|
|
28
|
-
*/
|
|
29
25
|
export declare function decryptPiiFields<T extends object>(doc: T, piiFields: string[]): T;
|
|
30
26
|
/** Alias for encryptValue — encrypts a PII plaintext string. */
|
|
31
27
|
export declare function encryptPii(plaintext: string | null | undefined): string | null | undefined;
|