@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.
Files changed (165) hide show
  1. package/dist/_internal/client/features/layout/SidebarCollapseToggle.js +1 -1
  2. package/dist/_internal/shared/features/blog/schema.d.ts +2 -2
  3. package/dist/_internal/shared/features/blog/schema.js +2 -1
  4. package/dist/_internal/shared/features/brands/schema.d.ts +2 -2
  5. package/dist/_internal/shared/features/brands/schema.js +3 -1
  6. package/dist/_internal/shared/features/categories/bundle-schemas.d.ts +3 -3
  7. package/dist/_internal/shared/features/categories/bundle-schemas.js +2 -1
  8. package/dist/_internal/shared/features/events/schema.d.ts +2 -2
  9. package/dist/_internal/shared/features/events/schema.js +2 -1
  10. package/dist/_internal/shared/features/products/schema.d.ts +8 -8
  11. package/dist/_internal/shared/features/products/schema.js +3 -2
  12. package/dist/_internal/shared/features/reviews/schema.d.ts +1 -1
  13. package/dist/_internal/shared/features/reviews/schema.js +2 -1
  14. package/dist/_internal/shared/types/action-result.d.ts +0 -2
  15. package/dist/client/api/surface-error.d.ts +9 -0
  16. package/dist/client/api/surface-error.js +29 -3
  17. package/dist/client/api/useApiMutation.d.ts +10 -0
  18. package/dist/client/api/useApiMutation.js +2 -1
  19. package/dist/core/server-action.d.ts +3 -14
  20. package/dist/core/server-action.js +11 -2
  21. package/dist/errors/error-envelope.d.ts +73 -0
  22. package/dist/errors/error-envelope.js +96 -0
  23. package/dist/errors/error-handler.js +14 -7
  24. package/dist/features/account/components/NotificationBell.js +1 -1
  25. package/dist/features/account/components/UserSidebar.js +2 -2
  26. package/dist/features/account/components/UserSupportView.js +2 -6
  27. package/dist/features/account/hooks/useProfile.d.ts +6 -0
  28. package/dist/features/account/hooks/useProfile.js +1 -0
  29. package/dist/features/admin/components/AdminAdEditorView.js +1 -3
  30. package/dist/features/admin/components/AdminAddressClustersView.js +1 -3
  31. package/dist/features/admin/components/AdminAddressEditorView.js +2 -4
  32. package/dist/features/admin/components/AdminAddressesView.js +1 -3
  33. package/dist/features/admin/components/AdminAllEventEntriesView.js +1 -3
  34. package/dist/features/admin/components/AdminBidsView.js +1 -3
  35. package/dist/features/admin/components/AdminBlogEditorView.js +2 -2
  36. package/dist/features/admin/components/AdminBrandEditorView.js +4 -5
  37. package/dist/features/admin/components/AdminCarouselGroupEditorView.js +1 -3
  38. package/dist/features/admin/components/AdminCategoryEditorView.js +2 -4
  39. package/dist/features/admin/components/AdminContactEditorView.js +1 -3
  40. package/dist/features/admin/components/AdminContactView.js +2 -6
  41. package/dist/features/admin/components/AdminCouponEditorView.js +2 -4
  42. package/dist/features/admin/components/AdminEmployeeEditorView.js +3 -9
  43. package/dist/features/admin/components/AdminFaqEditorView.js +2 -4
  44. package/dist/features/admin/components/AdminFeatureEditorView.js +0 -9
  45. package/dist/features/admin/components/AdminFeatureFlagsView.js +2 -4
  46. package/dist/features/admin/components/AdminNavEditorView.js +1 -3
  47. package/dist/features/admin/components/AdminNavigationView.js +3 -3
  48. package/dist/features/admin/components/AdminNewsletterView.js +1 -3
  49. package/dist/features/admin/components/AdminNotificationsView.js +2 -6
  50. package/dist/features/admin/components/AdminOffersView.js +1 -3
  51. package/dist/features/admin/components/AdminOrderEditorView.js +1 -3
  52. package/dist/features/admin/components/AdminPaymentClustersView.js +1 -3
  53. package/dist/features/admin/components/AdminPaymentMethodsView.js +1 -3
  54. package/dist/features/admin/components/AdminPayoutMarkPaidModal.js +2 -4
  55. package/dist/features/admin/components/AdminProductEditorView.js +2 -4
  56. package/dist/features/admin/components/AdminReturnRequestsView.js +2 -6
  57. package/dist/features/admin/components/AdminReviewsView.js +2 -6
  58. package/dist/features/admin/components/AdminScammerEditorView.js +2 -6
  59. package/dist/features/admin/components/AdminSectionsView.js +2 -6
  60. package/dist/features/admin/components/AdminSessionsView.js +1 -3
  61. package/dist/features/admin/components/AdminSidebar.js +2 -2
  62. package/dist/features/admin/components/AdminSiteSettingsView.js +1 -1
  63. package/dist/features/admin/components/AdminStoreEditorView.js +1 -3
  64. package/dist/features/admin/components/AdminStoresView.js +2 -6
  65. package/dist/features/admin/components/AdminSublistingCategoryEditorView.js +2 -6
  66. package/dist/features/admin/components/AdminSupportTicketDetailView.js +7 -21
  67. package/dist/features/admin/components/AdminTesterChecklistItemEditorView.js +2 -4
  68. package/dist/features/admin/components/AdminUserEditorView.js +6 -18
  69. package/dist/features/admin/components/AdminUsersView.js +2 -6
  70. package/dist/features/admin/components/BrandQuickCreateForm.js +2 -4
  71. package/dist/features/admin/components/CategoryQuickCreateForm.js +2 -4
  72. package/dist/features/auctions/components/PlaceBidFormClient.js +1 -1
  73. package/dist/features/auth/components/ForgotPasswordView.js +1 -1
  74. package/dist/features/auth/components/LoginForm.js +1 -1
  75. package/dist/features/auth/components/RegisterForm.js +1 -1
  76. package/dist/features/auth/components/ResetPasswordView.js +1 -1
  77. package/dist/features/auth/components/SocialAuthButtons.js +1 -1
  78. package/dist/features/auth/repository/session.repository.js +13 -1
  79. package/dist/features/auth/repository/token.repository.d.ts +25 -2
  80. package/dist/features/auth/repository/token.repository.js +26 -18
  81. package/dist/features/blog/actions/blog-actions.d.ts +14 -14
  82. package/dist/features/blog/actions/blog-actions.js +6 -8
  83. package/dist/features/blog/schemas/index.d.ts +1 -1
  84. package/dist/features/blog/schemas/index.js +2 -4
  85. package/dist/features/categories/api/[id]/route.js +2 -1
  86. package/dist/features/categories/api/route.js +2 -1
  87. package/dist/features/events/schemas/index.d.ts +1 -1
  88. package/dist/features/events/schemas/index.js +2 -4
  89. package/dist/features/faq/components/ContactCTA.js +1 -1
  90. package/dist/features/filters/AsyncFacetSection.js +1 -1
  91. package/dist/features/filters/FilterFacetSection.js +1 -1
  92. package/dist/features/filters/RangeFilter.js +1 -1
  93. package/dist/features/filters/SwitchFilter.js +1 -1
  94. package/dist/features/homepage/api/carousel/[id]/route.js +4 -3
  95. package/dist/features/homepage/api/carousel/route.js +4 -3
  96. package/dist/features/homepage/api/route.js +3 -2
  97. package/dist/features/homepage/components/GoogleReviewsSection.js +1 -1
  98. package/dist/features/homepage/components/HeroBanner.js +1 -1
  99. package/dist/features/homepage/components/HeroCarousel.js +1 -1
  100. package/dist/features/homepage/components/SocialPostCard.js +1 -1
  101. package/dist/features/homepage/components/WhatsAppCommunitySection.js +1 -1
  102. package/dist/features/layout/AppLayoutShell.js +4 -4
  103. package/dist/features/layout/BottomNavbar.js +2 -2
  104. package/dist/features/layout/MainNavbar.js +1 -1
  105. package/dist/features/layout/TitleBarLayout.js +24 -12
  106. package/dist/features/media/AvatarUpload.js +28 -10
  107. package/dist/features/media/modals/ImageCropModal.js +1 -1
  108. package/dist/features/media/modals/ImageEditor.js +1 -1
  109. package/dist/features/products/components/MakeOfferButton.js +1 -1
  110. package/dist/features/reviews/api/[id]/route.js +3 -2
  111. package/dist/features/reviews/api/route.js +3 -2
  112. package/dist/features/search/components/Search.js +2 -2
  113. package/dist/features/seller/components/SellerAnalyticsAlertsView.js +3 -3
  114. package/dist/features/seller/components/SellerSidebar.js +2 -2
  115. package/dist/features/store-extensions/components/ReviewDecisionModal.js +1 -1
  116. package/dist/features/stores/repository/store.repository.d.ts +21 -0
  117. package/dist/features/stores/repository/store.repository.js +32 -0
  118. package/dist/features/whatsapp-bot/components/SellerWhatsAppSettingsView.js +0 -2
  119. package/dist/index.d.ts +3 -4
  120. package/dist/index.js +4 -7
  121. package/dist/next/api/api-response.d.ts +29 -12
  122. package/dist/next/api/api-response.js +9 -8
  123. package/dist/next/api/apiHandler.d.ts +6 -1
  124. package/dist/next/api/apiHandler.js +13 -1
  125. package/dist/next/api/routeHandler.js +6 -6
  126. package/dist/next/middleware/pii-scrubber.js +5 -2
  127. package/dist/providers/db-firebase/base.repository.d.ts +14 -0
  128. package/dist/providers/db-firebase/base.repository.js +22 -0
  129. package/dist/providers/db-firebase/sieve.d.ts +14 -1
  130. package/dist/providers/db-firebase/sieve.js +48 -4
  131. package/dist/security/browser.d.ts +1 -1
  132. package/dist/security/browser.js +1 -1
  133. package/dist/security/index.d.ts +2 -2
  134. package/dist/security/index.js +2 -2
  135. package/dist/security/pii-encrypt.d.ts +12 -10
  136. package/dist/security/pii-encrypt.js +91 -29
  137. package/dist/security/pii-mask.d.ts +39 -1
  138. package/dist/security/pii-mask.js +33 -4
  139. package/dist/security/pii-redact.d.ts +0 -13
  140. package/dist/security/pii-redact.js +3 -1
  141. package/dist/security/pii-schemas.d.ts +0 -6
  142. package/dist/security/pii-schemas.js +29 -6
  143. package/dist/security/settings-encryption.js +31 -3
  144. package/dist/seed/test-utils.js +6 -2
  145. package/dist/server.d.ts +1 -1
  146. package/dist/server.js +1 -1
  147. package/dist/ui/DataTable.js +2 -2
  148. package/dist/ui/components/CollapsibleSection.js +1 -1
  149. package/dist/ui/components/ImageGallery.js +1 -1
  150. package/dist/ui/components/ListingLayout.js +1 -1
  151. package/dist/ui/forms/FormShell.d.ts +0 -19
  152. package/dist/ui/forms/FormShell.js +4 -23
  153. package/dist/ui/forms/apply-zod-issues.d.ts +34 -0
  154. package/dist/ui/forms/apply-zod-issues.js +38 -0
  155. package/dist/ui/forms/index.d.ts +2 -1
  156. package/dist/ui/forms/index.js +2 -1
  157. package/dist/ui/icons/icon-registry.d.ts +8 -0
  158. package/dist/ui/icons/icon-registry.js +13 -1
  159. package/dist/utils/action-response.d.ts +1 -1
  160. package/dist/utils/media-url.d.ts +49 -0
  161. package/dist/utils/media-url.js +72 -1
  162. package/dist/validation/schemas.d.ts +15 -2
  163. package/dist/validation/schemas.js +18 -22
  164. package/package.json +1 -1
  165. 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
- return processor.apply(effective, baseQuery, {
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) => deserializeTimestamps({ id: d.id, ...d.data() }));
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, 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, } from "./pii-schemas";
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";
@@ -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, 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, } from "./pii-schemas";
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";
@@ -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, addPiiIndices, piiIndicesFor, getPiiConfigError, encryptShippingAddress, decryptShippingAddress, encryptPayoutDetails, decryptPayoutDetails, encryptShippingConfig, decryptShippingConfig, encryptPayoutBankAccount, decryptPayoutBankAccount, } from "./pii-encrypt";
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, 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";
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";
@@ -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, addPiiIndices, piiIndicesFor, getPiiConfigError, encryptShippingAddress, decryptShippingAddress, encryptPayoutDetails, decryptPayoutDetails, encryptShippingConfig, decryptShippingConfig, encryptPayoutBankAccount, decryptPayoutBankAccount, } from "./pii-encrypt";
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, 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
+ 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
- * 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.
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
- throw new Error(`Invalid PII ciphertext: ${ciphertext}`);
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`, `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.
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, so re-running
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, decryptValue(value));
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, decryptValue(value));
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] = decryptValue(value);
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
- * 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.
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
- /** Check if a value is encrypted by our PII system. */
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
- /** Check if a value is encrypted by our PII system. */
18
- export function isPiiEncrypted(value) {
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 (isPiiEncrypted(name))
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 (isPiiEncrypted(email))
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
- const ENC_TOKEN_PREFIX = "enc:v1:";
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
- /** PII fields in newsletter subscribers */
44
- export const NEWSLETTER_PII_FIELDS = [];
45
- /** Blind-index mapping for newsletter subscribers */
46
- export const NEWSLETTER_PII_INDEX_MAP = {};
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
- /** Store OAuth bearer tokens — encrypted via SETTINGS_ENCRYPTION_KEY */
103
- export const STORE_SECRET_FIELDS = ["whatsappConfig.accessToken"];
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"];