@mohasinac/appkit 4.29.1 → 4.30.1
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/auth/repository/token.repository.d.ts +25 -2
- package/dist/features/auth/repository/token.repository.js +26 -18
- 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 -4
- package/dist/index.js +4 -7
- 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 +48 -4
- package/dist/security/browser.d.ts +1 -1
- package/dist/security/browser.js +1 -1
- package/dist/security/index.d.ts +2 -2
- package/dist/security/index.js +2 -2
- package/dist/security/pii-encrypt.d.ts +12 -10
- package/dist/security/pii-encrypt.js +91 -29
- 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 -6
- package/dist/security/pii-schemas.js +29 -6
- package/dist/security/settings-encryption.js +31 -3
- package/dist/seed/test-utils.js +6 -2
- package/dist/server.d.ts +1 -1
- package/dist/server.js +1 -1
- 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 +21 -6
|
@@ -76,12 +76,24 @@ import { createFirebaseAdapter } from "@mohasinac/sievejs/adapters/firebase";
|
|
|
76
76
|
* (OR semantics). The stock adapter tries `whereOr` which Firestore Admin SDK
|
|
77
77
|
* doesn't support. Firestore's `.where(field, "in", [v1, v2])` is the correct
|
|
78
78
|
* equivalent and supports up to 30 values.
|
|
79
|
+
*
|
|
80
|
+
* It also REPORTS every case-insensitive condition it sees, because those are
|
|
81
|
+
* the ones the stock adapter throws on — see `buildFilteredSieveQuery`.
|
|
82
|
+
* Reading `operatorIsCaseInsensitive` off the parsed condition is deliberate:
|
|
83
|
+
* re-scanning the raw filter string for a trailing `*` cannot tell an operator
|
|
84
|
+
* suffix from a `*` inside a value (`title==foo*bar`).
|
|
79
85
|
*/
|
|
80
|
-
function createEnhancedFirebaseAdapter() {
|
|
86
|
+
function createEnhancedFirebaseAdapter(onCaseInsensitive) {
|
|
81
87
|
const base = createFirebaseAdapter();
|
|
82
88
|
return {
|
|
83
89
|
...base,
|
|
84
90
|
applyFilterGroup(query, group) {
|
|
91
|
+
if (onCaseInsensitive) {
|
|
92
|
+
for (const c of group) {
|
|
93
|
+
if (c.operatorIsCaseInsensitive)
|
|
94
|
+
onCaseInsensitive(c);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
85
97
|
if (group.length > 1 &&
|
|
86
98
|
group.every((c) => c.parsedOperator === "equals" &&
|
|
87
99
|
!c.operatorIsNegated &&
|
|
@@ -94,6 +106,9 @@ function createEnhancedFirebaseAdapter() {
|
|
|
94
106
|
};
|
|
95
107
|
}
|
|
96
108
|
import { FirebaseRepository } from "./base";
|
|
109
|
+
// Defining module, not the barrel — `errors/index` re-exports a lot, and this
|
|
110
|
+
// file is imported by every repository (Root Cause #18).
|
|
111
|
+
import { ValidationError } from "../../errors/validation-error";
|
|
97
112
|
import { deserializeTimestamps, getFirestoreCount } from "./helpers";
|
|
98
113
|
import { expandFilterAliases as _expandFilterAliases, } from "./filter-aliases";
|
|
99
114
|
// Re-export the pure alias helper + types so consumers that already imported
|
|
@@ -138,6 +153,22 @@ function withAliasesExpanded(model, aliases) {
|
|
|
138
153
|
*/
|
|
139
154
|
function buildFilteredSieveQuery(baseQuery, model, fields, merged, aliases) {
|
|
140
155
|
const effective = withAliasesExpanded(model, aliases);
|
|
156
|
+
// Case-insensitive operators (`@=*`, `_=*`, `==*`, `!=*`, `_-=*`) are the one
|
|
157
|
+
// shape the Firebase adapter throws on, and `throwExceptions: false` turns
|
|
158
|
+
// that throw into SILENCE — measured, not assumed:
|
|
159
|
+
//
|
|
160
|
+
// filters="status==published,title==foo" → where, where, orderBy ✅
|
|
161
|
+
// filters="title@=*dranzer,status==published" → NOTHING APPLIED ✗
|
|
162
|
+
// filters="status==published,title@=*dranzer" → where(status) only, no sort
|
|
163
|
+
//
|
|
164
|
+
// The processor catches the adapter's throw and returns the query as of that
|
|
165
|
+
// point, so the remaining clauses AND the entire sort phase are discarded.
|
|
166
|
+
// With the CI clause first that is the bare collection — every facet the user
|
|
167
|
+
// picked, gone, and a 200 carrying the whole table as "search results".
|
|
168
|
+
// `ignoreUnsupported: true` does NOT help; it drops the clause just as
|
|
169
|
+
// quietly. So the only honest options are "throw" or "keep lying", and a
|
|
170
|
+
// caller asking for an operator this backend cannot express is a 400.
|
|
171
|
+
const caseInsensitive = [];
|
|
141
172
|
const processor = new SieveProcessorBase({
|
|
142
173
|
// MUST be the enhanced adapter, not the stock one. This is the path
|
|
143
174
|
// `BaseRepository.sieveQuery` delegates to, so it is what ProductRepository
|
|
@@ -148,14 +179,25 @@ function buildFilteredSieveQuery(baseQuery, model, fields, merged, aliases) {
|
|
|
148
179
|
// not implement; `throwExceptions: false` then swallowed it and the clause
|
|
149
180
|
// vanished, returning the ENTIRE collection instead of the selected subset.
|
|
150
181
|
// See __tests__/sieve-or-group.test.ts.
|
|
151
|
-
adapter: createEnhancedFirebaseAdapter(),
|
|
182
|
+
adapter: createEnhancedFirebaseAdapter((c) => caseInsensitive.push(c)),
|
|
152
183
|
autoLoadConfig: false,
|
|
153
184
|
options: merged,
|
|
154
185
|
fields,
|
|
155
186
|
});
|
|
156
|
-
|
|
187
|
+
const query = processor.apply(effective, baseQuery, {
|
|
157
188
|
applyPagination: false,
|
|
158
189
|
});
|
|
190
|
+
// Thrown AFTER apply so the message can name every offending clause, not just
|
|
191
|
+
// the first one the adapter happened to reach.
|
|
192
|
+
if (caseInsensitive.length > 0) {
|
|
193
|
+
const clauses = caseInsensitive
|
|
194
|
+
.map((c) => `${c.field} (${c.parsedOperator})`)
|
|
195
|
+
.join(", ");
|
|
196
|
+
throw new ValidationError(`Case-insensitive filter operators are not supported by Firestore. ` +
|
|
197
|
+
`Offending clause(s): ${clauses}. Use searchTxt token search instead ` +
|
|
198
|
+
`— see appkit/src/utils/search-txt.ts.`, { fields: caseInsensitive.map((c) => c.field) });
|
|
199
|
+
}
|
|
200
|
+
return query;
|
|
159
201
|
}
|
|
160
202
|
/**
|
|
161
203
|
* Count matching documents WITHOUT reading any of them.
|
|
@@ -189,7 +231,9 @@ export async function applySieveToFirestore(params) {
|
|
|
189
231
|
// the full Sieve DSL from the base.
|
|
190
232
|
const pagedQ = filteredQ.offset((page - 1) * pageSize).limit(pageSize);
|
|
191
233
|
const snap = await pagedQ.get();
|
|
192
|
-
const items = snap.docs.map((d) =>
|
|
234
|
+
const items = snap.docs.map((d) => params.mapDoc
|
|
235
|
+
? params.mapDoc(d)
|
|
236
|
+
: deserializeTimestamps({ id: d.id, ...d.data() }));
|
|
193
237
|
const totalPages = total === 0 ? 0 : Math.max(1, Math.ceil(total / pageSize));
|
|
194
238
|
return {
|
|
195
239
|
items,
|
|
@@ -2,6 +2,6 @@ export { generateNonce, buildCSP } from "./csp";
|
|
|
2
2
|
export { requireAuth, requireRole, requireOwnership, requireEmailVerified, requireActiveAccount, canChangeRole, getRoleLevel, } from "./authorization";
|
|
3
3
|
export type { UserRole } from "./authorization";
|
|
4
4
|
export { redactPii } from "./pii-redact";
|
|
5
|
-
export { USER_PII_FIELDS, USER_PII_INDEX_MAP, ADDRESS_PII_FIELDS, ORDER_PII_FIELDS, PAYOUT_PII_FIELDS, PAYOUT_PII_INDEX_MAP, BID_PII_FIELDS,
|
|
5
|
+
export { USER_PII_FIELDS, USER_PII_INDEX_MAP, ADDRESS_PII_FIELDS, ORDER_PII_FIELDS, PAYOUT_PII_FIELDS, PAYOUT_PII_INDEX_MAP, BID_PII_FIELDS, TOKEN_PII_FIELDS, TOKEN_PII_INDEX_MAP, REVIEW_PII_FIELDS, REVIEW_PII_INDEX_MAP, OFFER_PII_FIELDS, CHAT_PII_FIELDS, EVENT_ENTRY_PII_FIELDS, } from "./pii-schemas";
|
|
6
6
|
export type { Permission, RoleDefinition, RbacConfig, ResolvedUser, RbacHookReturn, } from "./rbac";
|
|
7
7
|
export { DEFAULT_ROLES, resolvePermissions, hasPermission, hasAllPermissions, hasAnyPermission, createRbacHook, Can, createRbacMiddleware, } from "./rbac";
|
package/dist/security/browser.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export { generateNonce, buildCSP } from "./csp";
|
|
2
2
|
export { requireAuth, requireRole, requireOwnership, requireEmailVerified, requireActiveAccount, canChangeRole, getRoleLevel, } from "./authorization";
|
|
3
3
|
export { redactPii } from "./pii-redact";
|
|
4
|
-
export { USER_PII_FIELDS, USER_PII_INDEX_MAP, ADDRESS_PII_FIELDS, ORDER_PII_FIELDS, PAYOUT_PII_FIELDS, PAYOUT_PII_INDEX_MAP, BID_PII_FIELDS,
|
|
4
|
+
export { USER_PII_FIELDS, USER_PII_INDEX_MAP, ADDRESS_PII_FIELDS, ORDER_PII_FIELDS, PAYOUT_PII_FIELDS, PAYOUT_PII_INDEX_MAP, BID_PII_FIELDS, TOKEN_PII_FIELDS, TOKEN_PII_INDEX_MAP, REVIEW_PII_FIELDS, REVIEW_PII_INDEX_MAP, OFFER_PII_FIELDS, CHAT_PII_FIELDS, EVENT_ENTRY_PII_FIELDS, } from "./pii-schemas";
|
|
5
5
|
export { DEFAULT_ROLES, resolvePermissions, hasPermission, hasAllPermissions, hasAnyPermission, createRbacHook, Can, createRbacMiddleware, } from "./rbac";
|
package/dist/security/index.d.ts
CHANGED
|
@@ -4,9 +4,9 @@ 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,
|
|
7
|
+
export { encryptValue, decryptValue, hmacBlindIndex, encryptPiiFields, decryptPiiFields, encryptPii, decryptPii, piiBlindIndex, 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
|
-
export { USER_PII_FIELDS, USER_PII_INDEX_MAP, ADDRESS_PII_FIELDS, ORDER_PII_FIELDS, PAYOUT_PII_FIELDS, PAYOUT_PII_INDEX_MAP, BID_PII_FIELDS,
|
|
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, 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";
|
|
11
11
|
export type { Permission, RoleDefinition, RbacConfig, ResolvedUser, RbacHookReturn, } from "./rbac";
|
|
12
12
|
export { DEFAULT_ROLES, resolvePermissions, hasPermission, hasAllPermissions, hasAnyPermission, createRbacHook, Can, createRequirePermission, createRequirePermissionSync, createRbacMiddleware, } from "./rbac";
|
package/dist/security/index.js
CHANGED
|
@@ -2,11 +2,11 @@ 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,
|
|
5
|
+
export { encryptValue, decryptValue, hmacBlindIndex, encryptPiiFields, decryptPiiFields, encryptPii, decryptPii, piiBlindIndex, 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.
|
|
9
9
|
export { isPiiEncrypted, maskName, maskEmail, maskPublicReview, maskPublicBid, maskPublicEventEntry, maskOfferForSeller, ENC_PREFIX, HMAC_PREFIX, } from "./pii-mask";
|
|
10
|
-
export { USER_PII_FIELDS, USER_PII_INDEX_MAP, ADDRESS_PII_FIELDS, ORDER_PII_FIELDS, PAYOUT_PII_FIELDS, PAYOUT_PII_INDEX_MAP, BID_PII_FIELDS,
|
|
10
|
+
export { USER_PII_FIELDS, USER_PII_INDEX_MAP, ADDRESS_PII_FIELDS, ORDER_PII_FIELDS, PAYOUT_PII_FIELDS, PAYOUT_PII_INDEX_MAP, BID_PII_FIELDS, 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";
|
|
11
11
|
export { encryptSecret, decryptSecret, isSecretEncrypted, maskSecret, } from "./settings-encryption";
|
|
12
12
|
export { DEFAULT_ROLES, resolvePermissions, hasPermission, hasAllPermissions, hasAnyPermission, createRbacHook, Can, createRequirePermission, createRequirePermissionSync, createRbacMiddleware, } from "./rbac";
|
|
@@ -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;
|
|
@@ -42,16 +38,22 @@ export declare function piiBlindIndex(plaintext: string): string;
|
|
|
42
38
|
* Blind-index fields ONLY — derived from `source` (which must be plaintext),
|
|
43
39
|
* with none of `source`'s own keys carried along.
|
|
44
40
|
*
|
|
45
|
-
*
|
|
46
|
-
*
|
|
47
|
-
*
|
|
48
|
-
*
|
|
49
|
-
*
|
|
41
|
+
* This is the ONLY index helper. Its predecessor, `addPiiIndices`, returned
|
|
42
|
+
* `{...source, ...indices}`, so spreading its result over ciphertext silently
|
|
43
|
+
* restored the plaintext — which is how `payouts.sellerEmail`, `payouts.upiId`
|
|
44
|
+
* and `reviews.userName` ended up stored in cleartext beside a valid index, and
|
|
45
|
+
* how both token repositories wrote every verification and password-reset email
|
|
46
|
+
* in cleartext until it was deleted. Returning indices only makes that class of
|
|
47
|
+
* bug unrepresentable rather than merely documented.
|
|
48
|
+
*
|
|
49
|
+
* Merge order is still the caller's responsibility:
|
|
50
|
+
* `{ ...encryptPiiFields(data, FIELDS), ...piiIndicesFor(data, MAP) }`
|
|
51
|
+
* — both arguments read the PLAINTEXT `data`; hashing ciphertext yields an
|
|
52
|
+
* index nothing can look up.
|
|
50
53
|
*
|
|
51
54
|
* @param source plaintext document — hashing ciphertext produces a useless index
|
|
52
55
|
*/
|
|
53
56
|
export declare function piiIndicesFor(source: object, mapping: Record<string, string>): Record<string, string>;
|
|
54
|
-
export declare function addPiiIndices<T extends object>(obj: T, mapping: Record<string, string>): T;
|
|
55
57
|
/**
|
|
56
58
|
* Encrypt PII inside a shipping address stored in an order.
|
|
57
59
|
* Supports string and object address shapes.
|
|
@@ -9,7 +9,9 @@
|
|
|
9
9
|
* Ciphertext format: "enc:v1:<iv_b64>:<ciphertext_b64>:<authtag_b64>"
|
|
10
10
|
* Blind index format: "hmac-sha256:<sha256_hex>"
|
|
11
11
|
*/
|
|
12
|
-
import { ENC_PREFIX, HMAC_PREFIX, isPiiEncrypted } from "./pii-mask";
|
|
12
|
+
import { ENC_PREFIX, HMAC_PREFIX, isPiiEncrypted, encEnvelopeKind } from "./pii-mask";
|
|
13
|
+
import { serverLogger } from "../monitoring/server-logger";
|
|
14
|
+
import { normalizeError } from "../errors/normalize";
|
|
13
15
|
// Re-exported for backward-compat with existing direct imports of these
|
|
14
16
|
// symbols from this file — the actual crypto-free definitions live in
|
|
15
17
|
// pii-mask.ts (see that file's header comment for why the split exists).
|
|
@@ -50,10 +52,32 @@ function getEncKey() {
|
|
|
50
52
|
const hex = normalizePiiSecretValue(process.env.PII_ENCRYPTION_KEY);
|
|
51
53
|
return Buffer.from(hex, "hex");
|
|
52
54
|
}
|
|
55
|
+
/**
|
|
56
|
+
* Blind-index key. Normalised and validated exactly like `getEncKey`.
|
|
57
|
+
*
|
|
58
|
+
* It previously read the env var RAW while `getEncKey` normalised the same
|
|
59
|
+
* value — so the two keys were derived from one variable by two different code
|
|
60
|
+
* paths. `Buffer.from(x, "hex")` does not throw on bad input; it truncates
|
|
61
|
+
* silently at the first non-hex byte. A BOM or trailing CR (a problem this repo
|
|
62
|
+
* has hit before — see scripts/_fix-vercel-env-bom.mjs) would therefore give
|
|
63
|
+
* the encryptor a 32-byte key and the indexer a 0-byte one, and every blind
|
|
64
|
+
* index written from that moment would be unmatchable against the existing
|
|
65
|
+
* ones. `findByEmail` would simply stop finding people — indistinguishable
|
|
66
|
+
* from "no such user", with nothing logged.
|
|
67
|
+
*
|
|
68
|
+
* Verified before changing: on the current environment the raw and normalised
|
|
69
|
+
* reads produce byte-identical keys, so this is a no-op for every index
|
|
70
|
+
* already in Firestore.
|
|
71
|
+
*/
|
|
53
72
|
function getHmacKey() {
|
|
54
|
-
const raw = process.env["PII_HMAC_KEY"] ?? process.env["PII_ENCRYPTION_KEY"] ?? "";
|
|
73
|
+
const raw = normalizePiiSecretValue(process.env["PII_HMAC_KEY"] ?? process.env["PII_ENCRYPTION_KEY"] ?? "");
|
|
55
74
|
if (!raw)
|
|
56
75
|
throw new Error("PII_HMAC_KEY env var is not set — cannot generate blind PII index");
|
|
76
|
+
if (!/^[0-9a-fA-F]{64}$/.test(raw))
|
|
77
|
+
// Fail loudly at the boundary. A startup error is recoverable; a silently
|
|
78
|
+
// truncated key is not.
|
|
79
|
+
throw new Error("PII_HMAC_KEY (or its PII_ENCRYPTION_KEY fallback) must be a 64-character hex string — " +
|
|
80
|
+
`got ${raw.length} chars. Buffer.from would truncate this silently and desync every blind index.`);
|
|
57
81
|
return Buffer.from(raw, "hex");
|
|
58
82
|
}
|
|
59
83
|
/** Encrypt a plaintext string with AES-256-GCM. */
|
|
@@ -70,10 +94,19 @@ export function encryptValue(plaintext) {
|
|
|
70
94
|
export function decryptValue(ciphertext) {
|
|
71
95
|
if (!ciphertext.startsWith(ENC_PREFIX))
|
|
72
96
|
return ciphertext;
|
|
97
|
+
// Name the actual problem. A settings-encrypted value shares this prefix but
|
|
98
|
+
// uses dot separators and a different key; it used to fail here as a bare
|
|
99
|
+
// part-count error, which reads like corruption rather than "wrong system".
|
|
100
|
+
if (encEnvelopeKind(ciphertext) === "settings") {
|
|
101
|
+
throw new Error("Value is settings-encrypted (SETTINGS_ENCRYPTION_KEY, dot-separated), not PII-encrypted — decryptValue cannot read it");
|
|
102
|
+
}
|
|
73
103
|
const inner = ciphertext.slice(ENC_PREFIX.length);
|
|
74
104
|
const parts = inner.split(":");
|
|
75
105
|
if (parts.length !== 3)
|
|
76
|
-
|
|
106
|
+
// NEVER interpolate the value. This used to embed the whole ciphertext,
|
|
107
|
+
// which then flowed into DatabaseError and serverLogger.error — encrypted
|
|
108
|
+
// PII sitting in the logs on every malformed row. Shape only.
|
|
109
|
+
throw new Error(`Invalid PII ciphertext: expected 3 colon-separated parts after ${ENC_PREFIX}, got ${parts.length} (${ciphertext.length} chars)`);
|
|
77
110
|
const [ivB64, encB64, tagB64] = parts;
|
|
78
111
|
const { createDecipheriv } = nodeCrypto();
|
|
79
112
|
const key = getEncKey();
|
|
@@ -124,14 +157,20 @@ function setPath(obj, path, value) {
|
|
|
124
157
|
/**
|
|
125
158
|
* Encrypt the named fields, adding a blind index alongside each top-level one.
|
|
126
159
|
*
|
|
127
|
-
* Supports dotted paths (`deviceInfo.ip`, `
|
|
128
|
-
*
|
|
129
|
-
*
|
|
130
|
-
*
|
|
131
|
-
*
|
|
160
|
+
* Supports dotted paths (`deviceInfo.ip`, `location.city`). This was a flat
|
|
161
|
+
* top-level loop that skipped anything non-string, which is why nested PII
|
|
162
|
+
* stayed in plaintext.
|
|
163
|
+
*
|
|
164
|
+
* 🛑 That dotted-path support is also what made it possible to point this
|
|
165
|
+
* function at a field belonging to the OTHER crypto system. A store's
|
|
166
|
+
* `whatsappConfig.accessToken` is encrypted with SETTINGS_ENCRYPTION_KEY;
|
|
167
|
+
* passing it here would encrypt it with the PII key while
|
|
168
|
+
* `StoreRepository.decryptSecrets` kept decrypting with the settings one.
|
|
169
|
+
* Only ever pass fields from this file's own `*_PII_FIELDS` lists.
|
|
132
170
|
*
|
|
133
|
-
* Idempotent: a value already carrying `enc:v1:` is left alone
|
|
134
|
-
* over a partially-migrated document is safe
|
|
171
|
+
* Idempotent: a value already carrying `enc:v1:` is left alone — whichever
|
|
172
|
+
* system wrote it — so re-running over a partially-migrated document is safe
|
|
173
|
+
* and a settings-encrypted value is never double-wrapped.
|
|
135
174
|
*/
|
|
136
175
|
/**
|
|
137
176
|
* Encrypt `itemPath` on every element of the array at `arrayPath`.
|
|
@@ -192,6 +231,33 @@ export function encryptPiiFields(doc, piiFields) {
|
|
|
192
231
|
* Decrypt specified PII fields in a Firestore document.
|
|
193
232
|
* Fields that are not encrypted (no `enc:v1:` prefix) are passed through.
|
|
194
233
|
*/
|
|
234
|
+
/**
|
|
235
|
+
* Decrypt one value, or return a sentinel and warn.
|
|
236
|
+
*
|
|
237
|
+
* A single corrupt or foreign-format row must not take out the page. Before
|
|
238
|
+
* the sieve read path started calling `mapDoc` (which is what decrypts),
|
|
239
|
+
* a bad value was merely rendered as ciphertext — cosmetic. Once decryption
|
|
240
|
+
* actually runs on list reads, an unguarded throw inside a 100-row admin page
|
|
241
|
+
* turns that cosmetic defect into a 500 for the whole list.
|
|
242
|
+
*
|
|
243
|
+
* Same fail-soft posture `SiteSettingsRepository.getDecryptedCredentials`
|
|
244
|
+
* already takes for the settings key.
|
|
245
|
+
*
|
|
246
|
+
* The warning names the FIELD, never the value.
|
|
247
|
+
*/
|
|
248
|
+
function decryptOrWarn(value, field) {
|
|
249
|
+
try {
|
|
250
|
+
return decryptValue(value);
|
|
251
|
+
}
|
|
252
|
+
catch (err) {
|
|
253
|
+
const normalized = normalizeError(err);
|
|
254
|
+
serverLogger.warn("PII decrypt failed — field left empty", {
|
|
255
|
+
field,
|
|
256
|
+
reason: normalized.message,
|
|
257
|
+
});
|
|
258
|
+
return "";
|
|
259
|
+
}
|
|
260
|
+
}
|
|
195
261
|
export function decryptPiiFields(doc, piiFields) {
|
|
196
262
|
const result = { ...doc };
|
|
197
263
|
for (const field of piiFields) {
|
|
@@ -211,7 +277,7 @@ export function decryptPiiFields(doc, piiFields) {
|
|
|
211
277
|
const clone = { ...el };
|
|
212
278
|
const value = itemPath ? getPath(clone, itemPath) : undefined;
|
|
213
279
|
if (typeof value === "string" && value.startsWith(ENC_PREFIX)) {
|
|
214
|
-
setPath(clone, itemPath,
|
|
280
|
+
setPath(clone, itemPath, decryptOrWarn(value, field));
|
|
215
281
|
}
|
|
216
282
|
return clone;
|
|
217
283
|
}));
|
|
@@ -220,13 +286,13 @@ export function decryptPiiFields(doc, piiFields) {
|
|
|
220
286
|
if (field.includes(".")) {
|
|
221
287
|
const value = getPath(result, field);
|
|
222
288
|
if (typeof value === "string" && value.startsWith(ENC_PREFIX)) {
|
|
223
|
-
setPath(result, field,
|
|
289
|
+
setPath(result, field, decryptOrWarn(value, field));
|
|
224
290
|
}
|
|
225
291
|
continue;
|
|
226
292
|
}
|
|
227
293
|
const value = result[field];
|
|
228
294
|
if (typeof value === "string" && value.startsWith(ENC_PREFIX)) {
|
|
229
|
-
result[field] =
|
|
295
|
+
result[field] = decryptOrWarn(value, field);
|
|
230
296
|
}
|
|
231
297
|
}
|
|
232
298
|
return result;
|
|
@@ -259,11 +325,18 @@ export function piiBlindIndex(plaintext) {
|
|
|
259
325
|
* Blind-index fields ONLY — derived from `source` (which must be plaintext),
|
|
260
326
|
* with none of `source`'s own keys carried along.
|
|
261
327
|
*
|
|
262
|
-
*
|
|
263
|
-
*
|
|
264
|
-
*
|
|
265
|
-
*
|
|
266
|
-
*
|
|
328
|
+
* This is the ONLY index helper. Its predecessor, `addPiiIndices`, returned
|
|
329
|
+
* `{...source, ...indices}`, so spreading its result over ciphertext silently
|
|
330
|
+
* restored the plaintext — which is how `payouts.sellerEmail`, `payouts.upiId`
|
|
331
|
+
* and `reviews.userName` ended up stored in cleartext beside a valid index, and
|
|
332
|
+
* how both token repositories wrote every verification and password-reset email
|
|
333
|
+
* in cleartext until it was deleted. Returning indices only makes that class of
|
|
334
|
+
* bug unrepresentable rather than merely documented.
|
|
335
|
+
*
|
|
336
|
+
* Merge order is still the caller's responsibility:
|
|
337
|
+
* `{ ...encryptPiiFields(data, FIELDS), ...piiIndicesFor(data, MAP) }`
|
|
338
|
+
* — both arguments read the PLAINTEXT `data`; hashing ciphertext yields an
|
|
339
|
+
* index nothing can look up.
|
|
267
340
|
*
|
|
268
341
|
* @param source plaintext document — hashing ciphertext produces a useless index
|
|
269
342
|
*/
|
|
@@ -278,17 +351,6 @@ export function piiIndicesFor(source, mapping) {
|
|
|
278
351
|
}
|
|
279
352
|
return indices;
|
|
280
353
|
}
|
|
281
|
-
export function addPiiIndices(obj, mapping) {
|
|
282
|
-
const result = { ...obj };
|
|
283
|
-
const access = result;
|
|
284
|
-
for (const [sourceField, indexField] of Object.entries(mapping)) {
|
|
285
|
-
const val = access[sourceField];
|
|
286
|
-
if (typeof val === "string" && val) {
|
|
287
|
-
access[indexField] = piiBlindIndex(val);
|
|
288
|
-
}
|
|
289
|
-
}
|
|
290
|
-
return result;
|
|
291
|
-
}
|
|
292
354
|
// --- Nested-object PII helpers ----------------------------------------------
|
|
293
355
|
/**
|
|
294
356
|
* Encrypt PII inside a shipping address stored in an order.
|
|
@@ -14,7 +14,45 @@
|
|
|
14
14
|
*/
|
|
15
15
|
export declare const ENC_PREFIX = "enc:v1:";
|
|
16
16
|
export declare const HMAC_PREFIX = "hmac-sha256:";
|
|
17
|
-
/**
|
|
17
|
+
/**
|
|
18
|
+
* Which crypto system produced an `enc:v1:` value.
|
|
19
|
+
*
|
|
20
|
+
* TWO systems share this prefix and always have:
|
|
21
|
+
*
|
|
22
|
+
* PII `enc:v1:` + iv `:` ciphertext `:` tag — PII_ENCRYPTION_KEY
|
|
23
|
+
* settings `enc:v1:` + iv `.` tag `.` ciphertext — SETTINGS_ENCRYPTION_KEY
|
|
24
|
+
*
|
|
25
|
+
* Different separator, different field order, different key. `isPiiEncrypted`
|
|
26
|
+
* checked only the prefix and so could not tell them apart, which made every
|
|
27
|
+
* "is this ours" test wrong for one of the two — feed a settings value to
|
|
28
|
+
* `decryptValue` and it throws on the part count; feed a PII value to
|
|
29
|
+
* `decryptSecret` and it throws on a different part count. Neither error names
|
|
30
|
+
* the actual problem.
|
|
31
|
+
*
|
|
32
|
+
* Changing either prefix is NOT an option: `enc:v1:` is stamped into every
|
|
33
|
+
* encrypted value across twelve collections, and a new prefix orphans all of
|
|
34
|
+
* it. It does not need changing. Base64's alphabet is `A-Za-z0-9+/=`, which
|
|
35
|
+
* contains neither `.` nor `:` — so the two formats are ALREADY unambiguous in
|
|
36
|
+
* the data, and this function simply reads what is there. No migration.
|
|
37
|
+
*/
|
|
38
|
+
export type EncEnvelopeKind = "pii" | "settings" | null;
|
|
39
|
+
export declare function encEnvelopeKind(value: unknown): EncEnvelopeKind;
|
|
40
|
+
/**
|
|
41
|
+
* Does this value carry the `enc:v1:` prefix at all, whichever system wrote it
|
|
42
|
+
* and even if malformed?
|
|
43
|
+
*
|
|
44
|
+
* This is the check the DISPLAY masks want. `maskName`/`maskEmail` exist so
|
|
45
|
+
* ciphertext never reaches the UI, and for that purpose "settings-encrypted"
|
|
46
|
+
* and "malformed" are just as unshowable as "PII-encrypted". Narrowing them to
|
|
47
|
+
* `kind === "pii"` would let the other two render verbatim.
|
|
48
|
+
*/
|
|
49
|
+
export declare function hasEncPrefix(value: unknown): boolean;
|
|
50
|
+
/**
|
|
51
|
+
* Check if a value is encrypted by our PII system SPECIFICALLY.
|
|
52
|
+
*
|
|
53
|
+
* Use for crypto routing — "may I hand this to decryptValue". For "is this
|
|
54
|
+
* unshowable", use `hasEncPrefix`.
|
|
55
|
+
*/
|
|
18
56
|
export declare function isPiiEncrypted(value: string): boolean;
|
|
19
57
|
/**
|
|
20
58
|
* Mask a person's name for public display.
|
|
@@ -14,10 +14,39 @@
|
|
|
14
14
|
*/
|
|
15
15
|
export const ENC_PREFIX = "enc:v1:";
|
|
16
16
|
export const HMAC_PREFIX = "hmac-sha256:";
|
|
17
|
-
|
|
18
|
-
|
|
17
|
+
export function encEnvelopeKind(value) {
|
|
18
|
+
if (typeof value !== "string" || !value.startsWith(ENC_PREFIX))
|
|
19
|
+
return null;
|
|
20
|
+
const inner = value.slice(ENC_PREFIX.length);
|
|
21
|
+
const colons = inner.split(":").length - 1;
|
|
22
|
+
const dots = inner.split(".").length - 1;
|
|
23
|
+
if (colons === 2 && dots === 0)
|
|
24
|
+
return "pii";
|
|
25
|
+
if (dots === 2 && colons === 0)
|
|
26
|
+
return "settings";
|
|
27
|
+
return null; // carries the prefix but neither shape — malformed
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Does this value carry the `enc:v1:` prefix at all, whichever system wrote it
|
|
31
|
+
* and even if malformed?
|
|
32
|
+
*
|
|
33
|
+
* This is the check the DISPLAY masks want. `maskName`/`maskEmail` exist so
|
|
34
|
+
* ciphertext never reaches the UI, and for that purpose "settings-encrypted"
|
|
35
|
+
* and "malformed" are just as unshowable as "PII-encrypted". Narrowing them to
|
|
36
|
+
* `kind === "pii"` would let the other two render verbatim.
|
|
37
|
+
*/
|
|
38
|
+
export function hasEncPrefix(value) {
|
|
19
39
|
return typeof value === "string" && value.startsWith(ENC_PREFIX);
|
|
20
40
|
}
|
|
41
|
+
/**
|
|
42
|
+
* Check if a value is encrypted by our PII system SPECIFICALLY.
|
|
43
|
+
*
|
|
44
|
+
* Use for crypto routing — "may I hand this to decryptValue". For "is this
|
|
45
|
+
* unshowable", use `hasEncPrefix`.
|
|
46
|
+
*/
|
|
47
|
+
export function isPiiEncrypted(value) {
|
|
48
|
+
return encEnvelopeKind(value) === "pii";
|
|
49
|
+
}
|
|
21
50
|
/**
|
|
22
51
|
* Mask a person's name for public display.
|
|
23
52
|
* Each word is reduced to its first letter followed by "***".
|
|
@@ -31,7 +60,7 @@ export function isPiiEncrypted(value) {
|
|
|
31
60
|
export function maskName(name) {
|
|
32
61
|
if (!name || typeof name !== "string")
|
|
33
62
|
return "Anonymous";
|
|
34
|
-
if (
|
|
63
|
+
if (hasEncPrefix(name))
|
|
35
64
|
return "Anonymous";
|
|
36
65
|
return name
|
|
37
66
|
.trim()
|
|
@@ -47,7 +76,7 @@ export function maskName(name) {
|
|
|
47
76
|
export function maskEmail(email) {
|
|
48
77
|
if (!email || typeof email !== "string")
|
|
49
78
|
return "***@***.***";
|
|
50
|
-
if (
|
|
79
|
+
if (hasEncPrefix(email))
|
|
51
80
|
return "***@***.***";
|
|
52
81
|
const atIdx = email.indexOf("@");
|
|
53
82
|
if (atIdx < 0)
|
|
@@ -1,16 +1,3 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* PII Redaction Utility
|
|
3
|
-
*
|
|
4
|
-
* Recursively sanitizes objects before logging by masking or redacting
|
|
5
|
-
* known PII (Personally Identifiable Information) field names.
|
|
6
|
-
*
|
|
7
|
-
* - Secrets (password, token, apiKey, etc.) → "[REDACTED]"
|
|
8
|
-
* - Emails → partial mask: "mo***@gmail.com"
|
|
9
|
-
* - Phones → last 4 digits: "***9876"
|
|
10
|
-
* - Names → first char + asterisks: "J********"
|
|
11
|
-
* - Addresses, financial IDs → "[REDACTED]"
|
|
12
|
-
* - IPs → first octet only: "192.***.***.***"
|
|
13
|
-
*/
|
|
14
1
|
/**
|
|
15
2
|
* Mask an IPv4 address by zeroing the last three octets. Returns
|
|
16
3
|
* `[REDACTED]` for IPv6 / malformed input.
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { ENC_PREFIX } from "./pii-mask";
|
|
1
2
|
/**
|
|
2
3
|
* PII Redaction Utility
|
|
3
4
|
*
|
|
@@ -138,7 +139,8 @@ export function redactPii(data, depth = 0) {
|
|
|
138
139
|
// ---------------------------------------------------------------------------
|
|
139
140
|
// UI display helpers
|
|
140
141
|
// ---------------------------------------------------------------------------
|
|
141
|
-
|
|
142
|
+
// One declaration of the prefix, shared with the crypto that produces it.
|
|
143
|
+
const ENC_TOKEN_PREFIX = ENC_PREFIX;
|
|
142
144
|
/**
|
|
143
145
|
* Safe display name for untrusted / possibly-encrypted PII fields.
|
|
144
146
|
*
|
|
@@ -26,10 +26,6 @@ export declare const PAYOUT_PII_INDEX_MAP: Record<string, string>;
|
|
|
26
26
|
* Bidder names are already masked on the public side by `maskPublicBid`.
|
|
27
27
|
*/
|
|
28
28
|
export declare const BID_PII_FIELDS: readonly ["userEmail"];
|
|
29
|
-
/** PII fields in newsletter subscribers */
|
|
30
|
-
export declare const NEWSLETTER_PII_FIELDS: readonly [];
|
|
31
|
-
/** Blind-index mapping for newsletter subscribers */
|
|
32
|
-
export declare const NEWSLETTER_PII_INDEX_MAP: Record<string, string>;
|
|
33
29
|
/**
|
|
34
30
|
* PII fields in tokens (email verification / password reset).
|
|
35
31
|
*
|
|
@@ -66,7 +62,5 @@ export declare const CHAT_PII_FIELDS: readonly ["buyerName", "sellerName"];
|
|
|
66
62
|
export declare const EVENT_ENTRY_PII_FIELDS: readonly ["userDisplayName", "userEmail", "ipAddress"];
|
|
67
63
|
/** PII fields in lottery entries */
|
|
68
64
|
export declare const LOTTERY_ENTRY_PII_FIELDS: readonly ["userPhone", "userEmail", "userDisplayName"];
|
|
69
|
-
/** Store OAuth bearer tokens — encrypted via SETTINGS_ENCRYPTION_KEY */
|
|
70
|
-
export declare const STORE_SECRET_FIELDS: readonly ["whatsappConfig.accessToken"];
|
|
71
65
|
/** PII fields in savedPaymentMethods — full identifier (UPI VPA, card PAN, etc.) */
|
|
72
66
|
export declare const PAYMENT_METHOD_PII_FIELDS: readonly ["identifier"];
|
|
@@ -40,10 +40,19 @@ export const PAYOUT_PII_INDEX_MAP = {
|
|
|
40
40
|
* Bidder names are already masked on the public side by `maskPublicBid`.
|
|
41
41
|
*/
|
|
42
42
|
export const BID_PII_FIELDS = ["userEmail"];
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
43
|
+
/*
|
|
44
|
+
* NEWSLETTER_PII_FIELDS / NEWSLETTER_PII_INDEX_MAP were DELETED here.
|
|
45
|
+
*
|
|
46
|
+
* They were an empty `[]` and `{}` with no reader anywhere — only three barrel
|
|
47
|
+
* re-exports. `NewsletterRepository` does not extend `BaseRepository`; it holds
|
|
48
|
+
* its OWN module-local `NEWSLETTER_PII_FIELDS = ["email"]` and encrypts through
|
|
49
|
+
* that, so newsletter emails ARE encrypted at rest.
|
|
50
|
+
*
|
|
51
|
+
* The empty pair therefore did nothing except read, convincingly, as proof that
|
|
52
|
+
* newsletter PII was unencrypted — a shadow constant of exactly the shape in
|
|
53
|
+
* Root Cause #53, where the symbol a grep finds is not the one doing the work.
|
|
54
|
+
* Deleting it is the fix; populating it would have been a no-op.
|
|
55
|
+
*/
|
|
47
56
|
/**
|
|
48
57
|
* PII fields in tokens (email verification / password reset).
|
|
49
58
|
*
|
|
@@ -99,7 +108,21 @@ export const LOTTERY_ENTRY_PII_FIELDS = [
|
|
|
99
108
|
"userEmail",
|
|
100
109
|
"userDisplayName",
|
|
101
110
|
];
|
|
102
|
-
|
|
103
|
-
|
|
111
|
+
// `STORE_SECRET_FIELDS = ["whatsappConfig.accessToken"]` was deleted here.
|
|
112
|
+
//
|
|
113
|
+
// It had ZERO consumers, and its presence in this file was the whole hazard:
|
|
114
|
+
// it sat in the PII-field registry, formatted identically to the entries above
|
|
115
|
+
// that ARE assigned to a repository's `piiFields` — while its own doc-comment
|
|
116
|
+
// said it was encrypted with SETTINGS_ENCRYPTION_KEY, the other key entirely.
|
|
117
|
+
//
|
|
118
|
+
// Wiring it into a `piiFields` override would have encrypted every store's Meta
|
|
119
|
+
// bearer token with the PII key while `StoreRepository.decryptSecrets` kept
|
|
120
|
+
// decrypting with the settings key: a hard throw on every store read, and
|
|
121
|
+
// live seller tokens unrecoverable without a bespoke migration. Dotted-path
|
|
122
|
+
// support was added to `encryptPiiFields` in 2026-08, which is exactly what
|
|
123
|
+
// made that one-line mistake possible.
|
|
124
|
+
//
|
|
125
|
+
// The token is handled where it belongs — `encryptSecret`/`decryptSecret` in
|
|
126
|
+
// `StoreRepository`. A constant does not need to exist for that.
|
|
104
127
|
/** PII fields in savedPaymentMethods — full identifier (UPI VPA, card PAN, etc.) */
|
|
105
128
|
export const PAYMENT_METHOD_PII_FIELDS = ["identifier"];
|