@mohasinac/appkit 4.39.0 → 4.39.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.
@@ -37,7 +37,7 @@ declare class ReviewRepository extends BaseRepository<ReviewDocument> {
37
37
  * it can be reasoned about).
38
38
  */
39
39
  findApprovedByProduct(productId: string, limit?: number): Promise<ReviewDocument[]>;
40
- findApprovedByStore(storeId: string): Promise<ReviewDocument[]>;
40
+ findApprovedByStore(storeId: string, limit?: number): Promise<ReviewDocument[]>;
41
41
  findByUser(userId: string): Promise<ReviewDocument[]>;
42
42
  findApprovedByUser(userId: string): Promise<ReviewDocument[]>;
43
43
  findPending(): Promise<ReviewDocument[]>;
@@ -101,13 +101,13 @@ class ReviewRepository extends BaseRepository {
101
101
  const snapshot = await query.get();
102
102
  return snapshot.docs.map((doc) => this.mapDoc(doc));
103
103
  }
104
- async findApprovedByStore(storeId) {
104
+ async findApprovedByStore(storeId, limit = 50) {
105
105
  const snapshot = await this.db
106
106
  .collection(this.collection)
107
107
  .where(REVIEW_FIELDS.STORE_ID, "==", storeId)
108
108
  .where(REVIEW_FIELDS.STATUS, "==", "approved")
109
109
  .orderBy(REVIEW_FIELDS.CREATED_AT, "desc")
110
- .limit(50)
110
+ .limit(limit)
111
111
  .get();
112
112
  return snapshot.docs.map((doc) => this.mapDoc(doc));
113
113
  }
@@ -20,6 +20,8 @@
20
20
  */
21
21
  import { NextResponse } from "next/server.js";
22
22
  import { getProviders } from "../../../../../contracts";
23
+ import { reviewRepository } from "../../../../reviews/repository/reviews.repository";
24
+ import { maskPublicReview } from "../../../../../security/pii-mask";
23
25
  import { normalizeError } from "../../../../../errors/normalize";
24
26
  /**
25
27
  * Upper bound on the reviews scanned to build a store's aggregate. The average
@@ -81,14 +83,22 @@ export async function GET(request, context) {
81
83
  * the store's reviews are one equality away. This also turns 21 Firestore
82
84
  * queries into 1, well inside Rule #6's ~3-round-trip budget.
83
85
  */
84
- const reviewsRepo = db.getRepository("reviews");
85
- const reviewsResult = await reviewsRepo.findAll({
86
- filters: `storeId==${store.id},status==approved`,
87
- sort: "createdAt",
88
- order: "desc",
89
- perPage: STORE_REVIEW_SCAN_CAP,
90
- });
91
- const allFlat = reviewsResult.data;
86
+ /*
87
+ * 🛑 `reviewRepository`, NOT `db.getRepository("reviews")`.
88
+ *
89
+ * The generic provider repository returns the RAW document. `userName` is
90
+ * PII-encrypted at rest, so it came back as the literal string
91
+ * `enc:v1:…` — measured in production the moment this endpoint started
92
+ * returning rows at all — and so did `userNameIndex`, the HMAC blind index,
93
+ * `searchTxt`, `reportCount` and the moderation `status`. Only
94
+ * `reviewRepository.mapDoc` runs `decryptPiiFields`.
95
+ *
96
+ * This was INVISIBLE until the itemsSold fix (Root Cause #100): the
97
+ * endpoint returned zero reviews, so there was nothing to leak. A fix that
98
+ * makes a surface work for the first time is a fix that exposes whatever
99
+ * the surface does next.
100
+ */
101
+ const allFlat = (await reviewRepository.findApprovedByStore(store.id, STORE_REVIEW_SCAN_CAP));
92
102
  // Compute aggregate metrics from ALL reviews (unfiltered)
93
103
  const ratingDistribution = { 1: 0, 2: 0, 3: 0, 4: 0, 5: 0 };
94
104
  let ratingSum = 0;
@@ -184,11 +194,38 @@ export async function GET(request, context) {
184
194
  for (const p of fetched)
185
195
  if (p)
186
196
  productMap.set(p.id, p);
187
- const reviewsWithProduct = pageSlice.map((review) => ({
188
- ...review,
189
- productTitle: productMap.get(review.productId)?.title ?? review.productTitle,
190
- productMainImage: productMap.get(review.productId)?.mainImage ?? null,
191
- }));
197
+ /*
198
+ * 🛑 AN ALLOW-LIST, not a spread. `...review` published every field the
199
+ * document happens to carry — including `userNameIndex` (an HMAC blind
200
+ * index, which lets a caller confirm a guessed name), `searchTxt`,
201
+ * `reportCount` and the moderation `status`. Root Cause #70: a payload
202
+ * built by spreading publishes everything nobody thought to remove, and is
203
+ * blind to fields added later.
204
+ *
205
+ * `userName` is MASKED, matching every other public review surface —
206
+ * `maskPublicReview` is what `listReviewsForProduct` already applies.
207
+ */
208
+ const reviewsWithProduct = pageSlice.map((review) => {
209
+ const r = review;
210
+ const masked = maskPublicReview({ ...r, userName: String(r.userName ?? "") });
211
+ return {
212
+ id: r.id,
213
+ rating: r.rating,
214
+ title: r.title,
215
+ comment: r.comment ?? r.body,
216
+ images: r.images ?? [],
217
+ video: r.video,
218
+ verified: r.verified ?? r.isVerifiedPurchase,
219
+ helpfulCount: r.helpfulCount ?? 0,
220
+ sellerReply: r.sellerReply,
221
+ sellerRepliedAt: r.sellerRepliedAt,
222
+ createdAt: r.createdAt,
223
+ userName: masked.userName,
224
+ productId: r.productId,
225
+ productTitle: productMap.get(review.productId)?.title ?? review.productTitle,
226
+ productMainImage: productMap.get(review.productId)?.mainImage ?? null,
227
+ };
228
+ });
192
229
  return NextResponse.json({
193
230
  success: true,
194
231
  data: {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mohasinac/appkit",
3
- "version": "4.39.0",
3
+ "version": "4.39.1",
4
4
  "license": "MIT",
5
5
  "publishConfig": {
6
6
  "access": "public"