@mohasinac/appkit 4.46.0 → 4.48.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (175) hide show
  1. package/dist/.build-stamp.json +4 -4
  2. package/dist/_internal/server/features/checkout/expand-order-items.js +3 -0
  3. package/dist/_internal/server/features/messages/actions.d.ts +68 -0
  4. package/dist/_internal/server/features/messages/actions.js +249 -0
  5. package/dist/_internal/server/features/parts/buildable.d.ts +47 -0
  6. package/dist/_internal/server/features/parts/buildable.js +159 -0
  7. package/dist/_internal/server/features/products/list-public.d.ts +17 -0
  8. package/dist/_internal/server/features/products/list-public.js +92 -5
  9. package/dist/_internal/server/features/products/og.d.ts +7 -0
  10. package/dist/_internal/server/features/products/og.js +38 -0
  11. package/dist/_internal/server/features/site-settings/adapters.d.ts +1 -1
  12. package/dist/_internal/server/features/site-settings/adapters.js +6 -0
  13. package/dist/_internal/server/jobs/core/cleanupRtdbEvents.js +28 -0
  14. package/dist/_internal/server/jobs/core/notificationPrune.js +87 -3
  15. package/dist/_internal/server/jobs/core/priceIndexRollup.js +36 -1
  16. package/dist/_internal/shared/actions/action-registry.js +8 -0
  17. package/dist/_internal/shared/features/notifications/action-url.js +8 -0
  18. package/dist/_internal/shared/features/notifications/email-eligibility.js +3 -0
  19. package/dist/_internal/shared/features/site-settings/types.d.ts +4 -0
  20. package/dist/client.d.ts +10 -0
  21. package/dist/client.js +8 -0
  22. package/dist/constants/api-endpoints.d.ts +48 -0
  23. package/dist/constants/api-endpoints.js +30 -0
  24. package/dist/constants/notification-types.d.ts +1 -1
  25. package/dist/features/about/components/GstExplainerView.d.ts +28 -0
  26. package/dist/features/about/components/GstExplainerView.js +59 -0
  27. package/dist/features/about/components/GuidesHubView.d.ts +28 -0
  28. package/dist/features/about/components/GuidesHubView.js +58 -0
  29. package/dist/features/about/components/PublicProfileView.js +5 -0
  30. package/dist/features/about/components/SpotAFakeView.d.ts +29 -0
  31. package/dist/features/about/components/SpotAFakeView.js +49 -0
  32. package/dist/features/admin/actions/notification-actions.js +4 -0
  33. package/dist/features/admin/schemas/firestore.d.ts +13 -3
  34. package/dist/features/admin/schemas/firestore.js +8 -0
  35. package/dist/features/blog/repository/blog.repository.js +21 -7
  36. package/dist/features/categories/api/route.js +41 -1
  37. package/dist/features/categories/components/BreadcrumbTrail.d.ts +58 -13
  38. package/dist/features/categories/components/BreadcrumbTrail.js +63 -26
  39. package/dist/features/categories/components/CategoryDetailPageView.js +92 -38
  40. package/dist/features/categories/components/ContextualTrail.d.ts +62 -0
  41. package/dist/features/categories/components/ContextualTrail.js +81 -0
  42. package/dist/features/categories/components/LastCategoryBeacon.d.ts +18 -0
  43. package/dist/features/categories/components/LastCategoryBeacon.js +33 -0
  44. package/dist/features/categories/repository/categories.repository.d.ts +21 -0
  45. package/dist/features/categories/repository/categories.repository.js +36 -3
  46. package/dist/features/categories/repository/price-index.repository.d.ts +25 -0
  47. package/dist/features/email/notification-templates.js +4 -0
  48. package/dist/features/events/repository/events.repository.js +10 -3
  49. package/dist/features/faq/constants/read-caps.d.ts +29 -0
  50. package/dist/features/faq/constants/read-caps.js +29 -0
  51. package/dist/features/faq/repository/faqs.repository.d.ts +0 -2
  52. package/dist/features/faq/repository/faqs.repository.js +19 -16
  53. package/dist/features/homepage/components/AdSlot.js +10 -4
  54. package/dist/features/homepage/hooks/useActiveAd.d.ts +18 -1
  55. package/dist/features/homepage/hooks/useActiveAd.js +42 -2
  56. package/dist/features/homepage/lib/section-renderer.js +12 -2
  57. package/dist/features/layout/AppLayoutShell.d.ts +4 -1
  58. package/dist/features/layout/AppLayoutShell.js +2 -2
  59. package/dist/features/layout/TitleBarLayout.d.ts +4 -1
  60. package/dist/features/layout/TitleBarLayout.js +8 -4
  61. package/dist/features/media/AvatarUpload.js +8 -1
  62. package/dist/features/media/MediaAvatar.js +1 -1
  63. package/dist/features/media/MediaImage.js +3 -1
  64. package/dist/features/media/MediaLightbox.js +4 -1
  65. package/dist/features/media/MediaPickerModal.js +6 -1
  66. package/dist/features/media/MediaVideo.js +3 -1
  67. package/dist/features/messages/components/ConversationThreadView.d.ts +3 -0
  68. package/dist/features/messages/components/ConversationThreadView.js +75 -0
  69. package/dist/features/messages/components/ConversationsInboxView.d.ts +1 -0
  70. package/dist/features/messages/components/ConversationsInboxView.js +39 -0
  71. package/dist/features/messages/components/MessageSellerButton.d.ts +17 -0
  72. package/dist/features/messages/components/MessageSellerButton.js +33 -0
  73. package/dist/features/messages/hooks/useConversationChannel.d.ts +24 -0
  74. package/dist/features/messages/hooks/useConversationChannel.js +89 -0
  75. package/dist/features/messages/hooks/useMessages.d.ts +347 -0
  76. package/dist/features/messages/hooks/useMessages.js +113 -0
  77. package/dist/features/messages/repository/conversations.repository.d.ts +44 -0
  78. package/dist/features/messages/repository/conversations.repository.js +173 -0
  79. package/dist/features/messages/repository/messages.repository.d.ts +42 -0
  80. package/dist/features/messages/repository/messages.repository.js +142 -0
  81. package/dist/features/messages/schemas/firestore.d.ts +99 -0
  82. package/dist/features/messages/schemas/firestore.js +46 -0
  83. package/dist/features/orders/repository/orders.repository.js +21 -7
  84. package/dist/features/orders/schemas/firestore.d.ts +11 -0
  85. package/dist/features/products/components/CityCollectiblesView.d.ts +31 -0
  86. package/dist/features/products/components/CityCollectiblesView.js +82 -0
  87. package/dist/features/products/components/MarketPriceAnchor.d.ts +45 -0
  88. package/dist/features/products/components/MarketPriceAnchor.js +62 -0
  89. package/dist/features/products/components/ProductDetailPageView.js +71 -5
  90. package/dist/features/products/constants/meetup-cities.d.ts +47 -0
  91. package/dist/features/products/constants/meetup-cities.js +49 -0
  92. package/dist/features/products/repository/category-assignment.d.ts +82 -11
  93. package/dist/features/products/repository/category-assignment.js +93 -12
  94. package/dist/features/products/repository/products.repository.d.ts +12 -1
  95. package/dist/features/products/repository/products.repository.js +66 -34
  96. package/dist/features/products/schemas/firestore.d.ts +24 -2
  97. package/dist/features/products/schemas/firestore.js +13 -1
  98. package/dist/features/products/schemas/index.d.ts +2 -2
  99. package/dist/features/promotions/repository/coupons.repository.js +11 -4
  100. package/dist/features/reviews/repository/reviews.repository.js +11 -4
  101. package/dist/features/scams/repository/scammer.repository.js +21 -7
  102. package/dist/features/seller/components/CategoryInlineSelect.d.ts +17 -1
  103. package/dist/features/seller/components/CategoryInlineSelect.js +14 -3
  104. package/dist/features/seller/components/SellerProductShell.d.ts +27 -2
  105. package/dist/features/seller/components/SellerProductShell.js +32 -3
  106. package/dist/features/seller/repository/offer.repository.js +21 -6
  107. package/dist/features/seller/schemas/product-form.d.ts +3 -3
  108. package/dist/features/seller/schemas/product-form.js +1 -1
  109. package/dist/features/server-errors/repository/server-errors.repository.js +11 -0
  110. package/dist/features/server-errors/schemas/firestore.d.ts +17 -0
  111. package/dist/features/server-errors/schemas/firestore.js +2 -0
  112. package/dist/features/store-extensions/repository/store-extensions.repositories.d.ts +17 -1
  113. package/dist/features/store-extensions/repository/store-extensions.repositories.js +31 -0
  114. package/dist/features/store-extensions/schemas/firestore.d.ts +1 -1
  115. package/dist/features/store-extensions/schemas/firestore.js +4 -0
  116. package/dist/features/stores/repository/store.repository.js +11 -3
  117. package/dist/features/tester/seed-data/authored/community-support__messages.d.ts +2 -0
  118. package/dist/features/tester/seed-data/authored/community-support__messages.js +200 -0
  119. package/dist/features/tester/seed-data/authored/index.js +2 -0
  120. package/dist/features/tester/seed-data/tester-checklist-seed-data.js +64 -1
  121. package/dist/index.d.ts +8 -2
  122. package/dist/index.js +1 -1
  123. package/dist/next/routing/route-map.d.ts +14 -0
  124. package/dist/next/routing/route-map.js +26 -0
  125. package/dist/providers/db-firebase/rtdb-paths.d.ts +8 -0
  126. package/dist/providers/db-firebase/rtdb-paths.js +8 -0
  127. package/dist/repositories/index.d.ts +1 -0
  128. package/dist/repositories/index.js +4 -0
  129. package/dist/schemas/registry.d.ts +3 -3
  130. package/dist/security/index.d.ts +1 -1
  131. package/dist/security/index.js +1 -1
  132. package/dist/security/pii-schemas.d.ts +17 -0
  133. package/dist/security/pii-schemas.js +15 -7
  134. package/dist/seed/_helpers/category-content-generated.d.ts +24 -0
  135. package/dist/seed/_helpers/category-content-generated.js +1853 -0
  136. package/dist/seed/_helpers/category-models.d.ts +1 -1
  137. package/dist/seed/_helpers/category-models.js +25 -1
  138. package/dist/seed/_helpers/category-names.d.ts +54 -0
  139. package/dist/seed/_helpers/category-names.js +118 -0
  140. package/dist/seed/_helpers/character-features.d.ts +1 -1
  141. package/dist/seed/_helpers/character-features.js +5 -4
  142. package/dist/seed/_helpers/final-sale-fixtures.js +1 -1
  143. package/dist/seed/categories-seed-data.js +55 -1
  144. package/dist/seed/grouped-listings-seed-data.js +2 -2
  145. package/dist/seed/products-art-seed-data.js +2 -1
  146. package/dist/seed/products-auctions-seed-data.js +42 -22
  147. package/dist/seed/products-classifieds-seed-data.js +21 -3
  148. package/dist/seed/products-digital-codes-seed-data.js +2 -1
  149. package/dist/seed/products-live-items-seed-data.js +2 -1
  150. package/dist/seed/products-preorders-seed-data.js +26 -11
  151. package/dist/seed/products-prize-draws-seed-data.js +8 -1
  152. package/dist/seed/products-standard-seed-data.js +26 -25
  153. package/dist/seed/products-stickers-seed-data.js +12 -11
  154. package/dist/server.d.ts +8 -0
  155. package/dist/server.js +15 -0
  156. package/dist/styles.css +113 -1
  157. package/dist/tailwind-utilities.css +1 -1
  158. package/dist/tokens/themes/sunset.js +17 -0
  159. package/dist/tokens/tokens.css +5 -0
  160. package/dist/ui/components/Breadcrumb.d.ts +8 -1
  161. package/dist/ui/components/Breadcrumb.js +2 -2
  162. package/dist/ui/components/Breadcrumb.style.css +37 -0
  163. package/dist/ui/components/Details.d.ts +11 -1
  164. package/dist/ui/components/Details.js +2 -2
  165. package/dist/ui/components/ImageMosaic.d.ts +43 -0
  166. package/dist/ui/components/ImageMosaic.js +46 -0
  167. package/dist/ui/components/ImageMosaic.style.css +69 -0
  168. package/dist/ui/components/SplitTile.d.ts +34 -0
  169. package/dist/ui/components/SplitTile.js +28 -0
  170. package/dist/ui/components/index.style.css +1 -0
  171. package/dist/ui/index.d.ts +4 -0
  172. package/dist/ui/index.js +4 -0
  173. package/dist/utils/search-txt-query.d.ts +77 -0
  174. package/dist/utils/search-txt-query.js +50 -28
  175. package/package.json +1 -1
@@ -0,0 +1,200 @@
1
+ /*
2
+ * WHY: Authored six-part procedures for the D9 chat pages (/messages).
3
+ * WHAT: 10 case(s), keyed by full checklist id.
4
+ *
5
+ * 🛑 CHAT IS OFF BY DEFAULT AND `undefined` READS AS OFF.
6
+ *
7
+ * `siteSettings.chat.enabled` gates every write path server-side
8
+ * (`assertChatEnabled`) AND the nav icon AND the Message Seller button. So the
9
+ * FIRST case here turns it on, and every case after it depends on that having
10
+ * happened — which is deliberate: the off-state behaviour is itself a case
11
+ * worth having, because "the feature silently half-works when disabled" is the
12
+ * shape the original chat feature died of.
13
+ *
14
+ * 🛑 AND IT IS A REAL SETTING, NOT A FIXTURE. The seed does not write it, so a
15
+ * fresh run starts with chat OFF. A case that assumes the icon is present
16
+ * without having turned it on is testing the wrong state and will answer `no`
17
+ * for a reason that is not a defect.
18
+ *
19
+ * 🛑 THE OLD FEATURE'S TWO BUGS ARE THE TWO MOST VALUABLE CASES HERE, and
20
+ * neither is visible from the UI alone:
21
+ *
22
+ * 1. `useConversation`/`useConversations` never called `signInWithToken`, so
23
+ * every RTDB rule evaluated false and the live channel was permission-
24
+ * denied — swallowed into `setIsConnected(false)` and a literal
25
+ * `// ignore`. The channel LOOKED implemented and was inert. Case
26
+ * `live-ping-surfaces-failure` is the control: the error must be VISIBLE.
27
+ * 2. `listByBuyer`/`listByStore` had no `.limit()`, and every message wrote
28
+ * 2–3 pings each triggering an unpaginated refetch for both participants.
29
+ * Twenty messages ≈ 1,600 reads. Case `inbox-read-is-bounded` is the
30
+ * negative control the plan names explicitly.
31
+ *
32
+ * 🛑 TWO ACCOUNTS ARE REQUIRED and both are seeded: admin@letitrip.in (admin)
33
+ * and the owner of store-beyblade-arena, user-tyson-blader. A conversation is
34
+ * between two DIFFERENT users and the server refuses a self-conversation, so a
35
+ * single-account run cannot exercise anything past case 2.
36
+ *
37
+ * audit-tester-created-id: the conversation id is generated by Firestore and is
38
+ * therefore not knowable in advance — every case that needs it says to read it
39
+ * off the URL after the Message Seller button navigates, which is the only
40
+ * honest instruction. It is not a seed fixture and must not become one: the
41
+ * point is to exercise the create path.
42
+ *
43
+ * @tag domain:tester
44
+ * @tag layer:seed
45
+ * @tag pattern:none
46
+ * @tag access:server-only
47
+ * @tag consumers:seed-data/authored/index.ts
48
+ * @tag sideEffects:none
49
+ */
50
+ export const authored = {
51
+ "checklist-community-support-messages-chat-off-hides-everything": {
52
+ roles: ["admin", "buyer", "guest"],
53
+ startPage: "/admin/site",
54
+ steps: [
55
+ "Sign in as admin@letitrip.in / TempPass123!.",
56
+ "Open /admin/site and confirm the Chat toggle is OFF (its stored value is absent on a fresh seed, which reads as off).",
57
+ "Open /products/product-beyblade-burst-valkyrie and look at the 'Sold by' card.",
58
+ "Look at the header icon row, between the notification bell and the wishlist heart.",
59
+ "Navigate directly to /messages.",
60
+ ],
61
+ expectedBehaviour: "With chat off, nothing chat-related renders anywhere: no Message Seller button on the store card, no Messages icon in the header, and no /api/messages/unread-count request fires at all (the hook is gated on the setting, not merely rendered-and-empty). /messages itself still resolves — it is a real route — and shows the signed-out or empty state rather than 404ing.",
62
+ expectedUiState: "The 'Sold by' card shows ONLY 'Visit Store →'. The header icon row has no speech-bubble icon. /messages renders its own chrome with no conversation rows.",
63
+ endResult: "Nothing persists; this is the baseline state. Reload and the three absences hold.",
64
+ },
65
+ "checklist-community-support-messages-enable-chat-reveals-entry-points": {
66
+ roles: ["admin", "buyer"],
67
+ startPage: "/admin/site",
68
+ steps: [
69
+ "Sign in as admin@letitrip.in / TempPass123!.",
70
+ "Open /admin/site, turn the Chat toggle ON, and save.",
71
+ "Reload /products/product-beyblade-burst-valkyrie.",
72
+ "Look at the 'Sold by' card and at the header icon row.",
73
+ ],
74
+ inputs: { "chat.enabled": true },
75
+ expectedBehaviour: "Turning it on makes both entry points appear WITHOUT a code change: the gate is a live settings read, not a build-time flag. The Messages icon appears in the header with no badge (zero unread).",
76
+ expectedUiState: "The 'Sold by' card now shows a 'Message Seller' button beside 'Visit Store →'. A speech-bubble icon appears in the header icon row.",
77
+ expectedData: { "siteSettings.chat.enabled": true },
78
+ endResult: "Reload: both still present. The setting is stored, so it survives a sign-out and back in.",
79
+ },
80
+ "checklist-community-support-messages-message-seller-creates-thread": {
81
+ roles: ["buyer", "seller"],
82
+ startPage: "/products/product-beyblade-burst-valkyrie",
83
+ steps: [
84
+ "With chat enabled (previous case), sign in as admin@letitrip.in / TempPass123!.",
85
+ "Open /products/product-beyblade-burst-valkyrie and click 'Message Seller' on the 'Sold by' card.",
86
+ "Read the URL you land on and note the conversation id — it is a Firestore-generated id, not a slug.",
87
+ "Click 'Message Seller' on the SAME product a second time (go back and click again).",
88
+ "Compare the URL to the one you noted.",
89
+ ],
90
+ expectedBehaviour: "The first click resolves store-beyblade-arena's storeId to its owner's uid SERVER-SIDE (a product page knows the store slug, never the owner's Auth UID) and creates one conversation. The second click finds the existing thread and does NOT create a second one — the thread is between two USERS, not per listing, so a buyer with three questions about three items gets one thread.",
91
+ expectedUiState: "Both clicks land on /messages/{sameId} showing an empty thread with a composer and a 'Block' control in the header.",
92
+ expectedData: { "conversations: documents between this pair": 1 },
93
+ endResult: "The conversation persists. /messages now lists it. Clicking Message Seller again forever reuses it.",
94
+ },
95
+ "checklist-community-support-messages-send-and-recipient-sees-it": {
96
+ roles: ["buyer", "seller"],
97
+ startPage: "/messages",
98
+ steps: [
99
+ "Signed in as admin@letitrip.in, open the conversation created in the previous case.",
100
+ "Type 'Does this come with the original launcher?' and press Enter.",
101
+ "Watch the bubble while it sends.",
102
+ "Sign out, sign in as the owner of store-beyblade-arena (user-tyson-blader), and open /messages.",
103
+ "Open the conversation and read the message.",
104
+ ],
105
+ inputs: { body: "Does this come with the original launcher?" },
106
+ expectedBehaviour: "The message appears immediately as a pending bubble (optimistic), then resolves to a sent one. Server-side the body is PII-stripped and encrypted at rest, a truncated preview is denormalised onto the conversation, the recipient's unread counter is incremented with a real Firestore increment (not a read-modify-write), and a bell-only `message_received` notification is written — no email, because EMAIL_ELIGIBLE_TYPES says false for it.",
107
+ expectedUiState: "Sender side: the bubble is right-aligned and reads 'Sending…' briefly, then settles. Recipient side: /messages shows the thread with an unread badge of 1 and the preview text; opening it shows the message left-aligned and clears the badge.",
108
+ expectedData: { "recipient unread before opening": 1, "recipient unread after opening": 0 },
109
+ endResult: "Reload on both sides: the message is still there, the badge stays cleared for the recipient, and the preview on the inbox row matches the message.",
110
+ },
111
+ "checklist-community-support-messages-pii-is-stripped-from-the-body": {
112
+ roles: ["buyer"],
113
+ startPage: "/messages",
114
+ steps: [
115
+ "Open any conversation you are a participant in.",
116
+ "Send exactly: 'call me on 9876543210 or pay to tyson@paytm or tyson@okhdfc'",
117
+ "Read the message as it appears in the thread.",
118
+ "Reload the page and read it again.",
119
+ ],
120
+ inputs: { body: "call me on 9876543210 or pay to tyson@paytm or tyson@okhdfc" },
121
+ expectedBehaviour: "All three identifiers are replaced BEFORE the write, so the stored body never contains them. The rule order is specific-before-general and that ordering IS the fix: a bare `name@bank` VPA has no TLD dot so it matches neither the email nor the phone rule, and running phone first would mangle '9876543210@upi' into '[number removed]@upi' before the UPI rule ever saw it.",
122
+ expectedUiState: "The message renders as 'call me on [number removed] or pay to [email removed] or pay to [UPI removed]' — the ten-digit run, the dotted VPA and the bare VPA each replaced, none left through.",
123
+ expectedData: { "stored body contains '9876543210'": false, "stored body contains 'okhdfc'": false },
124
+ endResult: "Reload: the redacted form persists. The original text is not recoverable from the UI, which is the point — the redaction happened on the way in, not on the way out.",
125
+ },
126
+ "checklist-community-support-messages-retry-does-not-double-post": {
127
+ roles: ["buyer"],
128
+ startPage: "/messages",
129
+ steps: [
130
+ "Open a conversation and open DevTools → Network, set throttling to Offline.",
131
+ "Type 'retry test' and press Enter.",
132
+ "Observe the bubble's state.",
133
+ "Set throttling back to online and click the failed bubble's retry control.",
134
+ "Count the messages in the thread.",
135
+ ],
136
+ inputs: { body: "retry test" },
137
+ expectedBehaviour: "The failed send keeps its ORIGINAL clientMessageId — generated once at compose time, not per attempt — so the retry resolves to the same message server-side rather than creating a second one. The server uses the clientMessageId as the message document id and returns the existing document on a replay.",
138
+ expectedUiState: "Offline: the bubble reads 'Failed — tap to retry' rather than disappearing or silently succeeding. After retry: exactly ONE 'retry test' bubble in the thread, no duplicate.",
139
+ expectedData: { "messages with body 'retry test'": 1 },
140
+ endResult: "Reload: one message. A silent failure or a duplicate would both be defects — the first is how the old feature hid, the second is what idempotency exists to prevent.",
141
+ },
142
+ "checklist-community-support-messages-soft-ban-blocks-sending": {
143
+ roles: ["admin", "buyer"],
144
+ startPage: "/admin/users",
145
+ steps: [
146
+ "Sign in as admin@letitrip.in and open /admin/users.",
147
+ "Find the account you have been testing with as the buyer, open its editor, and apply a soft ban with action 'Send messages' and a reason.",
148
+ "Sign in as that banned account and open a conversation you are a participant in.",
149
+ "Type anything and try to send.",
150
+ "Read the refusal.",
151
+ ],
152
+ inputs: { "softBan.action": "send_messages", "softBan.reason": "QA test ban" },
153
+ expectedBehaviour: "The send is refused server-side with the ban's own reason quoted back. 🛑 This is the case that matters most about this flag: `send_messages` was a live BannedAction with NO READER anywhere in the codebase — an admin could set it and nothing checked it. The check lives in the domain function, so every caller (route, future job) is covered rather than just the one route someone remembered.",
154
+ expectedUiState: "An error surfaces naming the restriction and the reason — 'You are restricted from sending messages. Reason: QA test ban.' — not a generic failure and not a silently-dropped message.",
155
+ expectedData: { "messages added to the thread": 0 },
156
+ endResult: "Reload: no message was added. Remove the ban and the same send succeeds, which is what proves the ban was the cause rather than something else.",
157
+ },
158
+ "checklist-community-support-messages-block-stops-the-other-side": {
159
+ roles: ["buyer", "seller"],
160
+ startPage: "/messages",
161
+ steps: [
162
+ "Sign in as the seller (user-tyson-blader) and open the shared conversation.",
163
+ "Click 'Block' in the thread header and confirm.",
164
+ "Sign out, sign back in as the buyer (admin@letitrip.in), open the same conversation.",
165
+ "Try to send a message.",
166
+ ],
167
+ expectedBehaviour: "Blocking records the blocker's uid on the conversation. A send from the blocked party is refused with a clear reason rather than silently vanishing — a blocked sender who thinks their message went through is worse than one who is told.",
168
+ expectedUiState: "Blocker side: a confirmation, then the block persists. Blocked side: the send fails with 'The other participant is not accepting messages from you.'",
169
+ expectedData: { "messages added after the block": 0 },
170
+ endResult: "Reload on both sides: the block holds and the thread's existing history is still readable to both. Blocking stops new messages; it does not delete the past.",
171
+ },
172
+ "checklist-community-support-messages-live-ping-surfaces-failure": {
173
+ roles: ["buyer"],
174
+ startPage: "/messages",
175
+ steps: [
176
+ "Open a conversation and leave it open.",
177
+ "Open DevTools → Network and block requests to the Firebase Realtime Database host (or set throttling to Offline then back, which drops the subscription).",
178
+ "Watch the top of the thread for a notice.",
179
+ "In a second browser profile signed in as the other participant, send a message.",
180
+ "Reload the first tab.",
181
+ ],
182
+ expectedBehaviour: "🛑 THE CONTROL FOR THE BUG THAT KILLED THE OLD FEATURE. When the realtime channel cannot connect, the failure is SURFACED, never swallowed. The old hooks swallowed permission-denied into `setIsConnected(false)` and a literal `// ignore`, so the channel looked implemented and was inert for its entire life. The message is still delivered — it is durably in Firestore the moment the send returns — and the subscription only controls whether the open tab learns about it without a reload.",
183
+ expectedUiState: "A visible warning strip above the thread reading 'Live updates are unavailable for this conversation. New messages may not appear until you reload.' — NOT a silent nothing. After reload, the other participant's message is present.",
184
+ endResult: "The message was never lost; only the live notification was. That distinction is what the warning communicates, and a tester seeing no warning at all is the defect.",
185
+ },
186
+ "checklist-community-support-messages-inbox-read-is-bounded": {
187
+ roles: ["buyer"],
188
+ startPage: "/messages",
189
+ steps: [
190
+ "Open DevTools → Network and filter to /api/messages.",
191
+ "Load /messages and note the request and its response shape.",
192
+ "Open one conversation and note the thread request's response shape.",
193
+ "Scroll the thread to its oldest message.",
194
+ ],
195
+ expectedBehaviour: "🛑 THE NEGATIVE CONTROL THE PLAN NAMES. Both list reads are bounded and cursor-paginated: the inbox returns at most 50 conversations with a `nextCursor`, the thread at most 50 messages (default 30) with its own `nextCursor`. The absence of a `.limit()` is literally what turned 20 messages into ~1,600 reads the first time this feature existed. The unread badge sums the conversation list the inbox already holds and never issues a list query of its own.",
196
+ expectedUiState: "Each response carries `{ items: [...], nextCursor }` with `items.length` at or below the limit — never an unbounded array, and never a response whose size grows with total thread count.",
197
+ expectedData: { "inbox items.length <= 50": true, "thread items.length <= 50": true },
198
+ endResult: "Reload: the same bounded shape. A response returning every message in a thread is the defect this case exists to catch, and it is invisible from the rendered page — only the network panel shows it.",
199
+ },
200
+ };
@@ -71,6 +71,7 @@ import { authored as a_buying__user_dashboard_extras } from "./buying__user-dash
71
71
  import { authored as a_buying__offers } from "./buying__offers.js";
72
72
  import { authored as a_buying__cart } from "./buying__cart.js";
73
73
  import { authored as a_community_support__public_profile } from "./community-support__public-profile.js";
74
+ import { authored as a_community_support__messages } from "./community-support__messages.js";
74
75
  import { authored as a_community_support__support_tickets } from "./community-support__support-tickets.js";
75
76
  import { authored as a_content_discovery__blog } from "./content-discovery__blog.js";
76
77
  import { authored as a_content_discovery__coupons } from "./content-discovery__coupons.js";
@@ -191,6 +192,7 @@ export const AUTHORED_CASES = {
191
192
  ...a_buying__offers,
192
193
  ...a_buying__cart,
193
194
  ...a_community_support__public_profile,
195
+ ...a_community_support__messages,
194
196
  ...a_community_support__support_tickets,
195
197
  ...a_content_discovery__blog,
196
198
  ...a_content_discovery__coupons,
@@ -505,7 +505,7 @@ const rawTesterChecklistItems = [
505
505
  {
506
506
  key: "tester-fixtures-hidden-from-the-public",
507
507
  label: "SIGNED OUT, no product on /products has an id containing \"tester\" — the sandbox is invisible to the public",
508
- description: "Fixed 2026-09-01. BEFORE: the public products endpoint had no viewer concept, so an ANONYMOUS caller got 25 tester fixtures in its first 40 rows — stickers-beyblade-original-classic-sheet, prizedraw-beyblade-metal-closed-revealed, live-golden-retriever-puppy — and they filled the public marketplace. AFTER: open /products in a private window and scan the grid; nothing should be named \"Tester …\". Then sign in AS A TESTER and reload — the sandbox items must come back. Both halves matter: hiding them from everyone would break the tester programme, and a fixture that never reappears is the opposite bug.",
508
+ description: "Fixed 2026-09-01. BEFORE: the public products endpoint had no viewer concept, so an ANONYMOUS caller got 25 tester fixtures in its first 40 rows — sticker-beyblade-original-classic-sheet, prizedraw-beyblade-metal-closed-revealed, live-golden-retriever-puppy — and they filled the public marketplace. AFTER: open /products in a private window and scan the grid; nothing should be named \"Tester …\". Then sign in AS A TESTER and reload — the sandbox items must come back. Both halves matter: hiding them from everyone would break the tester programme, and a fixture that never reappears is the opposite bug.",
509
509
  href: "/products",
510
510
  },
511
511
  {
@@ -3519,6 +3519,69 @@ const rawTesterChecklistItems = [
3519
3519
  },
3520
3520
  ]),
3521
3521
  ...group("community-support", "Community & Support", [
3522
+ {
3523
+ /*
3524
+ * D9 chat. 🛑 Chat is OFF by default (`siteSettings.chat.enabled`, and
3525
+ * `undefined` reads as off), so case 1 asserts the off-state and case 2
3526
+ * turns it on — every case after depends on that order. Steps live in
3527
+ * ./authored/community-support__messages.ts.
3528
+ */
3529
+ pageKey: "messages",
3530
+ pageLabel: "Messages (chat)",
3531
+ href: "/messages",
3532
+ cases: [
3533
+ {
3534
+ key: "chat-off-hides-everything",
3535
+ label: "With chat disabled, no Message Seller button, no header icon, and no unread-count request — absent, not disabled",
3536
+ description: "The baseline. `siteSettings.chat.enabled` is undefined on a fresh seed and must read as OFF. A feature that half-renders when disabled is the shape the original chat died of.",
3537
+ },
3538
+ {
3539
+ key: "enable-chat-reveals-entry-points",
3540
+ label: "Turning the Chat setting on makes both entry points appear with no code change or redeploy",
3541
+ },
3542
+ {
3543
+ key: "message-seller-creates-thread",
3544
+ label: "Message Seller creates ONE conversation, and clicking it again reuses that same thread rather than making a second",
3545
+ description: "A conversation is between two USERS, not per listing — a buyer with three questions about three items gets one thread. The storeId→ownerId resolution happens server-side because a product page never knows the owner's Auth UID.",
3546
+ },
3547
+ {
3548
+ key: "send-and-recipient-sees-it",
3549
+ label: "A sent message reaches the other participant with an unread badge, and opening the thread clears it",
3550
+ roles: ["buyer", "seller"],
3551
+ },
3552
+ {
3553
+ key: "pii-is-stripped-from-the-body",
3554
+ label: "A phone number, a dotted VPA and a bare name@bank VPA are ALL redacted before the message is stored",
3555
+ description: "The rule order is the fix: specific before general. A bare `name@bank` has no TLD dot so it matches neither the email nor the phone rule, and running phone first mangles `9876543210@upi` before the UPI rule sees it.",
3556
+ },
3557
+ {
3558
+ key: "retry-does-not-double-post",
3559
+ label: "Retrying a failed send posts the SAME message once, never a duplicate",
3560
+ description: "clientMessageId is generated once at compose time, not per attempt, and is the message's document id — so a replay resolves to the existing document.",
3561
+ },
3562
+ {
3563
+ key: "soft-ban-blocks-sending",
3564
+ label: "A user soft-banned from 'Send messages' is refused with the ban's own reason",
3565
+ description: "send_messages was a live BannedAction with NO reader anywhere — an admin could set it and nothing checked it. The check now lives in the domain function so every caller is covered.",
3566
+ roles: ["admin", "buyer"],
3567
+ },
3568
+ {
3569
+ key: "block-stops-the-other-side",
3570
+ label: "Blocking a participant refuses their next message with a clear reason, and leaves existing history readable",
3571
+ roles: ["buyer", "seller"],
3572
+ },
3573
+ {
3574
+ key: "live-ping-surfaces-failure",
3575
+ label: "When the realtime channel cannot connect, a visible warning says so — the failure is never swallowed",
3576
+ description: "THE control for the bug that killed the old feature: it swallowed permission-denied into setIsConnected(false) and a literal `// ignore`, so the channel looked implemented and was inert for its whole life. The message is still delivered; only the live notification is lost.",
3577
+ },
3578
+ {
3579
+ key: "inbox-read-is-bounded",
3580
+ label: "The inbox and thread reads are both cursor-paginated and capped — open 50 threads and the read count does not scale with them",
3581
+ description: "The negative control. The absence of a .limit() is literally what turned 20 messages into ~1,600 reads the first time this existed. Visible only in the network panel, never on the rendered page.",
3582
+ },
3583
+ ],
3584
+ },
3522
3585
  {
3523
3586
  pageKey: "support-tickets",
3524
3587
  pageLabel: "Support Tickets",
package/dist/index.d.ts CHANGED
@@ -652,8 +652,8 @@ export { groupedListingsSeedData } from "./seed/index";
652
652
  export type { CouponDiscountType } from "./seed/index";
653
653
  export type { FirestoreIndexConfig } from "./seed/index";
654
654
  export type { HomepageSectionType } from "./seed/index";
655
- export type { NotificationType } from "./features/admin/schemas/firestore";
656
- export { NOTIFICATION_TYPE_VALUES } from "./features/admin/schemas/firestore";
655
+ export type { NotificationType, NotificationRelatedType } from "./features/admin/schemas/firestore";
656
+ export { NOTIFICATION_TYPE_VALUES, NOTIFICATION_RELATED_TYPE_VALUES } from "./features/admin/schemas/firestore";
657
657
  export type { PayoutStatus } from "./seed/index";
658
658
  export type { SeedAddressDocument } from "./seed/index";
659
659
  export type { SeedBaseOrderDocument } from "./seed/index";
@@ -3228,3 +3228,9 @@ export { resolveReturnAddress, RTO_ENTRY_CUSTODY, rtoCostBearer, RETURN_ABUSE_SC
3228
3228
  export type { ReturnAddressResolution, ResolveReturnAddressInput, ReturnAbuseSignal, ReturnAbuseCounters, RtoCostBearer, } from "./_internal/shared/features/returns/destination";
3229
3229
  export { COURIER_RATING_MIN_CONFIDENT_SAMPLES, courierRatingDisplay, isValidCourierVote, courierVoteId, rollUpCourierVotes, } from "./_internal/shared/features/shipping/courier-rating";
3230
3230
  export type { CourierRatingPair, CourierRatingDisplay, CourierRatingVote, CourierStats, } from "./_internal/shared/features/shipping/courier-rating";
3231
+ /**
3232
+ * D9 — chat. Plain data types, safe for client components that render a
3233
+ * conversation/message prop — the write path (actions) stays server-only, see
3234
+ * server.ts.
3235
+ */
3236
+ export type { ConversationDocument, ConversationStatus, ConversationLastMessage, ParticipantRole, MessageDocument, MessageProductRef, } from "./features/messages/schemas/firestore";
package/dist/index.js CHANGED
@@ -1364,7 +1364,7 @@ export { historySeedData } from "./seed/index.js";
1364
1364
  // SB-UNI-B â€" sublistingCategoriesSeedData merged into categoriesSeedData with categoryType:"sublisting".
1365
1365
  // groupedListingsSeedData - Model for grouped listings seed data.
1366
1366
  export { groupedListingsSeedData } from "./seed/index.js";
1367
- export { NOTIFICATION_TYPE_VALUES } from "./features/admin/schemas/firestore.js";
1367
+ export { NOTIFICATION_TYPE_VALUES, NOTIFICATION_RELATED_TYPE_VALUES } from "./features/admin/schemas/firestore.js";
1368
1368
  // [UTIL]-Pure utility â€" no framework or runtime dependency; safe to import from any environment.
1369
1369
  // ErrorCategory - Shared export for error category.
1370
1370
  export { ErrorCategory } from "./monitoring/index.js";
@@ -86,6 +86,10 @@ export declare const DEFAULT_ROUTE_MAP: {
86
86
  readonly HOW_REVIEWS_WORK: "/how-reviews-work";
87
87
  readonly HOW_PAYOUTS_WORK: "/how-payouts-work";
88
88
  readonly FEES: "/fees";
89
+ readonly GST: "/gst";
90
+ readonly SPOT_A_FAKE: "/spot-a-fake";
91
+ readonly GUIDES: "/guides";
92
+ readonly CITY_COLLECTIBLES: (city: string) => string;
89
93
  readonly EVENTS: "/events";
90
94
  readonly EVENT_DETAIL: (id: string) => string;
91
95
  readonly EVENT_PARTICIPATE: (id: string) => string;
@@ -175,6 +179,9 @@ export declare const DEFAULT_ROUTE_MAP: {
175
179
  readonly CATALOGUE: "/user/catalogue";
176
180
  readonly CATALOGUE_NEW: "/user/catalogue/new";
177
181
  readonly CATALOGUE_EDIT: (id: string) => string;
182
+ /** D9 — one universal inbox, same real-top-level-path precedent as WISHLIST/CART. */
183
+ readonly MESSAGES: "/messages";
184
+ readonly MESSAGE_THREAD: (conversationId: string) => string;
178
185
  };
179
186
  readonly STORE: {
180
187
  readonly DASHBOARD: "/store";
@@ -493,6 +500,10 @@ export declare const ROUTES: {
493
500
  readonly HOW_REVIEWS_WORK: "/how-reviews-work";
494
501
  readonly HOW_PAYOUTS_WORK: "/how-payouts-work";
495
502
  readonly FEES: "/fees";
503
+ readonly GST: "/gst";
504
+ readonly SPOT_A_FAKE: "/spot-a-fake";
505
+ readonly GUIDES: "/guides";
506
+ readonly CITY_COLLECTIBLES: (city: string) => string;
496
507
  readonly EVENTS: "/events";
497
508
  readonly EVENT_DETAIL: (id: string) => string;
498
509
  readonly EVENT_PARTICIPATE: (id: string) => string;
@@ -582,6 +593,9 @@ export declare const ROUTES: {
582
593
  readonly CATALOGUE: "/user/catalogue";
583
594
  readonly CATALOGUE_NEW: "/user/catalogue/new";
584
595
  readonly CATALOGUE_EDIT: (id: string) => string;
596
+ /** D9 — one universal inbox, same real-top-level-path precedent as WISHLIST/CART. */
597
+ readonly MESSAGES: "/messages";
598
+ readonly MESSAGE_THREAD: (conversationId: string) => string;
585
599
  };
586
600
  readonly STORE: {
587
601
  readonly DASHBOARD: "/store";
@@ -79,6 +79,29 @@ export const DEFAULT_ROUTE_MAP = {
79
79
  HOW_REVIEWS_WORK: "/how-reviews-work",
80
80
  HOW_PAYOUTS_WORK: "/how-payouts-work",
81
81
  FEES: "/fees",
82
+ /*
83
+ * D6b — the GST explainer. A differentiator rather than table stakes:
84
+ * exactly ONE of the seventeen crawled Indian collectibles sites states
85
+ * tax-inclusive pricing to the buyer in words, and sixteen say nothing.
86
+ */
87
+ GST: "/gst",
88
+ /*
89
+ * D6 — the anti-counterfeit guide. Framed on what the crawl actually
90
+ * measured: the biggest copy-seller in this market makes no authenticity
91
+ * claim at all, so the signals are silence + price, then code, then weight.
92
+ */
93
+ SPOT_A_FAKE: "/spot-a-fake",
94
+ /* D6 — the content hub. Fourteen explainers whose only entry point was the
95
+ * footer; a footer link is reachable but it is not a destination, so it
96
+ * cannot rank and gives a crawler no page establishing the cluster. */
97
+ GUIDES: "/guides",
98
+ /*
99
+ * D5 — local SEO, one page per city a classified listing actually names.
100
+ * 🛑 STATIC (`generateStaticParams`): a city page that awaited
101
+ * `searchParams` would be dynamic, and this is the surface most likely to
102
+ * be crawled widely (Root Cause #94).
103
+ */
104
+ CITY_COLLECTIBLES: (city) => `/collectibles/${city}`,
82
105
  EVENTS: "/events",
83
106
  EVENT_DETAIL: (id) => `/events/${id}`,
84
107
  EVENT_PARTICIPATE: (id) => `/events/${id}/participate`,
@@ -197,6 +220,9 @@ export const DEFAULT_ROUTE_MAP = {
197
220
  CATALOGUE: "/user/catalogue",
198
221
  CATALOGUE_NEW: "/user/catalogue/new",
199
222
  CATALOGUE_EDIT: (id) => `/user/catalogue/${id}/edit`,
223
+ /** D9 — one universal inbox, same real-top-level-path precedent as WISHLIST/CART. */
224
+ MESSAGES: "/messages",
225
+ MESSAGE_THREAD: (conversationId) => `/messages/${conversationId}`,
200
226
  },
201
227
  STORE: {
202
228
  DASHBOARD: "/store",
@@ -26,4 +26,12 @@ export declare const RTDB_PATHS: {
26
26
  * `cleanupRtdbEvents` alongside the other three.
27
27
  */
28
28
  readonly EMAIL_EVENTS: "email_events";
29
+ /**
30
+ * D9 — new-message ping per conversation. RTDB-ping-plus-REST: the actual
31
+ * message content never crosses this channel, only "something changed,
32
+ * refetch" — `firestore.rules` forbids client Firestore access entirely, so
33
+ * this plus the bounded REST route is the only option, not a cost choice.
34
+ * Pruned by `cleanupRtdbEvents`'s sixth block.
35
+ */
36
+ readonly CHAT_EVENTS: "chat_events";
29
37
  };
@@ -26,4 +26,12 @@ export const RTDB_PATHS = {
26
26
  * `cleanupRtdbEvents` alongside the other three.
27
27
  */
28
28
  EMAIL_EVENTS: "email_events",
29
+ /**
30
+ * D9 — new-message ping per conversation. RTDB-ping-plus-REST: the actual
31
+ * message content never crosses this channel, only "something changed,
32
+ * refetch" — `firestore.rules` forbids client Firestore access entirely, so
33
+ * this plus the bounded REST route is the only option, not a cost choice.
34
+ * Pruned by `cleanupRtdbEvents`'s sixth block.
35
+ */
36
+ CHAT_EVENTS: "chat_events",
29
37
  };
@@ -64,3 +64,4 @@ export { MediaAssetsRepository, mediaAssetsRepository, } from "../features/media
64
64
  export type { MediaAssetDocument, MediaAssetCreateInput, } from "../features/media/repository/media-assets.repository";
65
65
  export { PageViewsRepository, pageViewsRepository, PAGE_VIEW_ENTITY_TYPES, } from "../features/analytics/repository/page-views.repository";
66
66
  export type { PageViewDocument, PageViewEntityType, RecordPageViewInput, } from "../features/analytics/repository/page-views.repository";
67
+ export { ConversationsRepository, conversationsRepository, } from "../features/messages/repository/conversations.repository";
@@ -77,3 +77,7 @@ export { LotteryEntryRepository, lotteryEntryRepository, } from "../features/lot
77
77
  // Media assets — short ID → storagePath mapping for SEO-friendly /media/{id} URLs
78
78
  export { MediaAssetsRepository, mediaAssetsRepository, } from "../features/media/repository/media-assets.repository.js";
79
79
  export { PageViewsRepository, pageViewsRepository, PAGE_VIEW_ENTITY_TYPES, } from "../features/analytics/repository/page-views.repository.js";
80
+ // D9 — chat. Messages themselves are a subcollection (functional module, see
81
+ // features/messages/repository/messages.repository.ts and server.ts), not a
82
+ // BaseRepository class — there is no subcollection form of BaseRepository.
83
+ export { ConversationsRepository, conversationsRepository, } from "../features/messages/repository/conversations.repository.js";
@@ -4660,7 +4660,7 @@ export declare const SCHEMAS: {
4660
4660
  youtubeId: import("zod").ZodOptional<import("zod").ZodString>;
4661
4661
  externalVideoUrl: import("zod").ZodOptional<import("zod").ZodString>;
4662
4662
  price: import("zod").ZodOptional<import("zod").ZodNumber>;
4663
- compareAtPrice: import("zod").ZodOptional<import("zod").ZodNumber>;
4663
+ originalPrice: import("zod").ZodOptional<import("zod").ZodNumber>;
4664
4664
  stockQuantity: import("zod").ZodOptional<import("zod").ZodNumber>;
4665
4665
  featured: import("zod").ZodOptional<import("zod").ZodBoolean>;
4666
4666
  isPromoted: import("zod").ZodOptional<import("zod").ZodBoolean>;
@@ -4750,7 +4750,7 @@ export declare const SCHEMAS: {
4750
4750
  youtubeId: import("zod").ZodOptional<import("zod").ZodString>;
4751
4751
  externalVideoUrl: import("zod").ZodOptional<import("zod").ZodString>;
4752
4752
  price: import("zod").ZodOptional<import("zod").ZodNumber>;
4753
- compareAtPrice: import("zod").ZodOptional<import("zod").ZodNumber>;
4753
+ originalPrice: import("zod").ZodOptional<import("zod").ZodNumber>;
4754
4754
  stockQuantity: import("zod").ZodOptional<import("zod").ZodNumber>;
4755
4755
  featured: import("zod").ZodOptional<import("zod").ZodBoolean>;
4756
4756
  isPromoted: import("zod").ZodOptional<import("zod").ZodBoolean>;
@@ -4840,7 +4840,7 @@ export declare const SCHEMAS: {
4840
4840
  youtubeId: import("zod").ZodOptional<import("zod").ZodString>;
4841
4841
  externalVideoUrl: import("zod").ZodOptional<import("zod").ZodString>;
4842
4842
  price: import("zod").ZodOptional<import("zod").ZodNumber>;
4843
- compareAtPrice: import("zod").ZodOptional<import("zod").ZodNumber>;
4843
+ originalPrice: import("zod").ZodOptional<import("zod").ZodNumber>;
4844
4844
  stockQuantity: import("zod").ZodOptional<import("zod").ZodNumber>;
4845
4845
  featured: import("zod").ZodOptional<import("zod").ZodBoolean>;
4846
4846
  isPromoted: import("zod").ZodOptional<import("zod").ZodBoolean>;
@@ -6,5 +6,5 @@ export type { UserRole } from "./authorization";
6
6
  export { redactPii, safeDisplayName, safeDisplayEmail, maskIp } from "./pii-redact";
7
7
  export { encryptValue, decryptValue, hmacBlindIndex, encryptPiiFields, decryptPiiFields, encryptPii, decryptPii, piiBlindIndex, piiIndicesFor, getPiiConfigError, 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, TOKEN_PII_FIELDS, TOKEN_PII_INDEX_MAP, REVIEW_PII_FIELDS, REVIEW_PII_INDEX_MAP, OFFER_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, EVENT_ENTRY_PII_FIELDS, LOTTERY_ENTRY_PII_FIELDS, PAYMENT_METHOD_PII_FIELDS, CONVERSATION_PII_FIELDS, MESSAGE_PII_FIELDS, } from "./pii-schemas";
10
10
  export { encryptSecret, decryptSecret, isSecretEncrypted, maskSecret, } from "./settings-encryption";
@@ -7,7 +7,7 @@ export { encryptValue, decryptValue, hmacBlindIndex, encryptPiiFields, decryptPi
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.js";
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, EVENT_ENTRY_PII_FIELDS, LOTTERY_ENTRY_PII_FIELDS, PAYMENT_METHOD_PII_FIELDS, } from "./pii-schemas.js";
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, EVENT_ENTRY_PII_FIELDS, LOTTERY_ENTRY_PII_FIELDS, PAYMENT_METHOD_PII_FIELDS, CONVERSATION_PII_FIELDS, MESSAGE_PII_FIELDS, } from "./pii-schemas.js";
11
11
  export { encryptSecret, decryptSecret, isSecretEncrypted, maskSecret, } from "./settings-encryption.js";
12
12
  // RBAC system — DELETED 2026-08-29.
13
13
  //
@@ -56,6 +56,23 @@ export declare const BID_PII_FIELDS: readonly ["userEmail"];
56
56
  export declare const TOKEN_PII_FIELDS: readonly ["email"];
57
57
  /** Blind-index mapping for token email queries */
58
58
  export declare const TOKEN_PII_INDEX_MAP: Record<string, string>;
59
+ /**
60
+ * D9 — chat. Re-created against the SUBCOLLECTION redesign, not the old
61
+ * embedded-array one the comment below used to describe.
62
+ *
63
+ * `messages/{id}` is a true Firestore subcollection now (one document per
64
+ * message, not `conversations.messages[]`), so there is no array to walk and
65
+ * the old `messages[].body` `[]`-syntax path does not apply here — `body` is a
66
+ * plain top-level field on each message document. Only `lastMessage.body`, a
67
+ * nested object on the PARENT `conversations` document, needs the dotted-path
68
+ * form `encryptPiiFields` already supports.
69
+ *
70
+ * No blind index on either: nothing queries chat by body content, and a
71
+ * `searchTxt` over an encrypted body would hand back only what the encryption
72
+ * withheld — chat search, if it exists, indexes participant names only.
73
+ */
74
+ export declare const CONVERSATION_PII_FIELDS: readonly ["lastMessage.body"];
75
+ export declare const MESSAGE_PII_FIELDS: readonly ["body"];
59
76
  /** PII fields in reviews */
60
77
  export declare const REVIEW_PII_FIELDS: readonly ["userName"];
61
78
  /** Blind-index mapping for queryable review PII */
@@ -85,15 +85,23 @@ export const TOKEN_PII_FIELDS = ["email"];
85
85
  export const TOKEN_PII_INDEX_MAP = {
86
86
  email: "emailIndex",
87
87
  };
88
- /*
89
- * `CONVERSATION_PII_FIELDS` (`lastMessage`, `messages[].body`) was removed with
90
- * the conversations feature on 2026-08-31.
88
+ /**
89
+ * D9 — chat. Re-created against the SUBCOLLECTION redesign, not the old
90
+ * embedded-array one the comment below used to describe.
91
+ *
92
+ * `messages/{id}` is a true Firestore subcollection now (one document per
93
+ * message, not `conversations.messages[]`), so there is no array to walk and
94
+ * the old `messages[].body` `[]`-syntax path does not apply here — `body` is a
95
+ * plain top-level field on each message document. Only `lastMessage.body`, a
96
+ * nested object on the PARENT `conversations` document, needs the dotted-path
97
+ * form `encryptPiiFields` already supports.
91
98
  *
92
- * 🛑 Its array-path form was the ONLY user of `encryptPiiFields`' `[]` syntax
93
- * outside status history. If a future schema needs to encrypt a field inside an
94
- * array, check that machinery still works before relying on it — a path that no
95
- * caller exercises is a path nothing proves.
99
+ * No blind index on either: nothing queries chat by body content, and a
100
+ * `searchTxt` over an encrypted body would hand back only what the encryption
101
+ * withheld — chat search, if it exists, indexes participant names only.
96
102
  */
103
+ export const CONVERSATION_PII_FIELDS = ["lastMessage.body"];
104
+ export const MESSAGE_PII_FIELDS = ["body"];
97
105
  /** PII fields in reviews */
98
106
  export const REVIEW_PII_FIELDS = ["userName"];
99
107
  /** Blind-index mapping for queryable review PII */
@@ -0,0 +1,24 @@
1
+ /**
2
+ * GENERATED by scripts/gen-category-content.mjs — do not edit by hand.
3
+ *
4
+ * `contentBody` per category, composed from FACTS ONLY (I7). Every clause
5
+ * traces to a committed source: canonical-beys.json for a model leaf,
6
+ * part-glossary.json for a part leaf, the forest's own child lists above them.
7
+ *
8
+ * 🛑 A node with nothing factual to say is ABSENT from this map rather than
9
+ * carrying an empty body — `CategoryHighlightsAndFaqSection` renders nothing
10
+ * when its inputs are empty, and an empty paragraph reads as a defect.
11
+ *
12
+ * 🛑 No price band appears here. `MARKET_BANDS` omits a band rather than
13
+ * guessing one, and a body that stated an unmeasured "typical price" would be
14
+ * the same guess with better typography.
15
+ *
16
+ * 🛑 No listing COUNT appears here either. Counts live in `categoryCounts` and
17
+ * move; a sentence saying "48 listings" is wrong the next day and nothing
18
+ * reports it.
19
+ *
20
+ * Model leaves with a body: 217 of 220
21
+ * Part leaves with a body: 534 of 752
22
+ */
23
+ export declare const GENERATED_CATEGORY_CONTENT: Record<string, string>;
24
+ export declare const GENERATED_CATEGORY_CONTENT_COUNT = 751;