@mohasinac/appkit 4.36.0 → 4.37.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,9 +37,9 @@ import type { PublicSiteSettings } from "../../../shared/features/site-settings/
37
37
  * was traced to a call site; anything without one belongs in the private list,
38
38
  * because an unused public field is pure attack surface.
39
39
  */
40
- export declare const PUBLIC_SITE_SETTINGS_FIELDS: readonly ["contact", "payment", "listings", "notificationChannels", "announcementBar", "navConfig", "actionConfig", "background", "watermark"];
40
+ export declare const PUBLIC_SITE_SETTINGS_FIELDS: readonly ["contact", "payment", "listings", "notificationChannels", "announcementBar", "navConfig", "actionConfig", "background", "watermark", "disabledRoutes"];
41
41
  /** Everything else, with the reason it stays server-side. */
42
- export declare const PRIVATE_SITE_SETTINGS_FIELDS: readonly ["id", "createdAt", "updatedAt", "credentials", "emailSettings", "legalPages", "commissions", "emi", "gst", "laborRate", "adSettings", "integrations", "platformLimits", "auctionConfig", "disabledRoutes", "siteName", "tagline", "motto", "logo", "favicon", "seo", "socialLinks", "features", "shipping", "returns", "faq", "aboutContent", "navbarConfig", "footerConfig", "theme", "featuredResults", "messaging"];
42
+ export declare const PRIVATE_SITE_SETTINGS_FIELDS: readonly ["id", "createdAt", "updatedAt", "credentials", "emailSettings", "legalPages", "commissions", "emi", "gst", "laborRate", "adSettings", "integrations", "platformLimits", "auctionConfig", "siteName", "tagline", "motto", "logo", "favicon", "seo", "socialLinks", "features", "shipping", "returns", "faq", "aboutContent", "navbarConfig", "footerConfig", "theme", "featuredResults", "messaging"];
43
43
  export interface ToPublicSiteSettingsOptions {
44
44
  /**
45
45
  * The resolved marker → wordmark → text watermark chain. Injected because
@@ -13,6 +13,26 @@ export const PUBLIC_SITE_SETTINGS_FIELDS = [
13
13
  "actionConfig", // useAuthGate
14
14
  "background", // DashboardLayoutClient (background.light / .dark)
15
15
  "watermark", // MediaVideo, as the fallback behind effectiveWatermark
16
+ /*
17
+ * src/proxy.ts — the admin "disable a nav item and that route 404s" gate.
18
+ *
19
+ * 🛑 It was PRIVATE until 2026-09-13, justified by "read by
20
+ * app/[locale]/layout.tsx (RSC)". That was true once and stopped being true in
21
+ * Root Cause #82, which moved the gate OUT of the root layout (a dynamic API
22
+ * there made every page on the site uncacheable) and into the proxy. The
23
+ * proxy cannot do a Firestore read on the Edge, so it fetches
24
+ * /api/site-settings over HTTP — where a private field simply does not exist.
25
+ *
26
+ * The gate has therefore been a silent no-op ever since: getDisabledRoutes()
27
+ * reads `body.data.disabledRoutes`, gets undefined, and returns []. It fails
28
+ * OPEN, so nothing 404s that should not; the admin capability just does
29
+ * nothing. The stale comment on the private entry was the only trace.
30
+ *
31
+ * Publishing it is the right call rather than reverting the move: it is a
32
+ * list of route paths an admin has switched off, not a secret, and the
33
+ * alternative is putting a per-request Firestore read back on the Edge.
34
+ */
35
+ "disabledRoutes",
16
36
  ];
17
37
  /** Everything else, with the reason it stays server-side. */
18
38
  export const PRIVATE_SITE_SETTINGS_FIELDS = [
@@ -30,7 +50,6 @@ export const PRIVATE_SITE_SETTINGS_FIELDS = [
30
50
  "integrations", // analytics/tag IDs are injected server-side into markup, not fetched by the client
31
51
  "platformLimits", // server-enforced caps; no client reader
32
52
  "auctionConfig", // read by AuctionDetailPageView, a Server Component doing its own settings read
33
- "disabledRoutes", // read by app/[locale]/layout.tsx (RSC) to gate routing
34
53
  "siteName", // delivered as an SSR prop / rendered server-side
35
54
  "tagline", // admin chrome only
36
55
  "motto", // rendered server-side
@@ -81,6 +100,11 @@ export function toPublicSiteSettings(doc, opts) {
81
100
  actionConfig: doc.actionConfig,
82
101
  background: doc.background,
83
102
  watermark: doc.watermark,
103
+ // Defaulted to [] rather than passed through: src/proxy.ts fails OPEN on a
104
+ // missing value, so an absent field and an empty list already behave the
105
+ // same — but emitting `undefined` would drop the key from the JSON entirely
106
+ // and leave the next person wondering whether the projection ran at all.
107
+ disabledRoutes: doc.disabledRoutes ?? [],
84
108
  effectiveWatermark: opts?.effectiveWatermark,
85
109
  };
86
110
  }
@@ -44,6 +44,18 @@ export interface PublicSiteSettings {
44
44
  actionConfig?: SiteSettingsDocument["actionConfig"];
45
45
  background?: SiteSettingsDocument["background"];
46
46
  watermark?: SiteSettingsDocument["watermark"];
47
+ /**
48
+ * Routes an admin has switched off. Read by `src/proxy.ts`, which rewrites a
49
+ * disabled path to the 404 page.
50
+ *
51
+ * 🛑 Not optional, and always emitted (defaulted to `[]`). The proxy fetches
52
+ * this over HTTP and fails OPEN on a missing value, so the difference between
53
+ * "no routes disabled" and "the field never arrived" is invisible at runtime —
54
+ * which is exactly how this capability sat dead after Root Cause #82 moved the
55
+ * gate out of the root layout. A key that is always present makes the two
56
+ * distinguishable from the wire alone.
57
+ */
58
+ disabledRoutes: string[];
47
59
  /** Derived, not a stored field — the resolved marker → wordmark → text chain. */
48
60
  effectiveWatermark?: MediaVideoWatermark;
49
61
  }
@@ -155,16 +155,24 @@ export const moneyFlowsPages = [
155
155
  {
156
156
  pageKey: "auction-win-to-payment",
157
157
  pageLabel: "Win an auction → get notified → pay",
158
- href: "/auctions/auction-tester-sandbox-cycle-1",
158
+ href: "/auctions/auction-money-flows-closing",
159
159
  cases: [
160
160
  {
161
161
  key: "winning-bid-recorded",
162
162
  label: "Placing the highest bid on a closing auction records you as the winner",
163
163
  roles: ["buyer"],
164
- startPage: "/auctions/auction-tester-sandbox-cycle-1",
164
+ // Batch-scoped fixture, seeded by
165
+ // seed-data/fixtures/money-flows__auction-win-to-payment.mjs and closing
166
+ // ~3 minutes after this batch starts.
167
+ //
168
+ // It used to cite auction-tester-sandbox-cycle-1, which 46 OTHER
169
+ // citations need LIVE — one fixture with two contradictory
170
+ // requirements, so this case could never pass its final step and the
171
+ // rig notes recorded it as unpassable-by-design.
172
+ startPage: "/auctions/auction-money-flows-closing",
165
173
  steps: [
166
174
  STEP_SIGNIN_BUYER,
167
- "Open /auctions/auction-tester-sandbox-cycle-1, which starts at ₹15,000 with a ₹1,000 increment.",
175
+ "Open /auctions/auction-money-flows-closing, which starts at ₹15,000 with a ₹1,000 increment and closes about three minutes from now.",
168
176
  "Note the current bid and the end time.",
169
177
  "Enter 16000 as the bid amount.",
170
178
  "Click 'Place Bid'.",
@@ -180,7 +188,7 @@ export const moneyFlowsPages = [
180
188
  roles: ["buyer"],
181
189
  startPage: "/user/notifications",
182
190
  steps: [
183
- "Sign in as the buyer who won auction-tester-sandbox-cycle-1.",
191
+ "Sign in as the buyer who won auction-money-flows-closing.",
184
192
  "Open the notifications bell in the header.",
185
193
  "Find the auction-won notification.",
186
194
  "Read its text.",
@@ -197,7 +205,7 @@ export const moneyFlowsPages = [
197
205
  roles: ["buyer"],
198
206
  startPage: "/user/orders",
199
207
  steps: [
200
- "Sign in as the buyer who won auction-tester-sandbox-cycle-1.",
208
+ "Sign in as the buyer who won auction-money-flows-closing.",
201
209
  "Open /user/orders.",
202
210
  "Locate the won auction.",
203
211
  "Click its payment call to action.",
@@ -9,3 +9,4 @@ export { bidsTesterSeedData } from "./bids-tester-seed-data";
9
9
  export { cartsTesterSeedData } from "./carts-tester-seed-data";
10
10
  export { ordersTesterSeedData } from "./orders-tester-seed-data";
11
11
  export { offersTesterSeedData } from "./offers-tester-seed-data";
12
+ export { testerResponsesSeedData } from "./tester-responses-seed-data";
@@ -9,3 +9,4 @@ export { bidsTesterSeedData } from "./bids-tester-seed-data";
9
9
  export { cartsTesterSeedData } from "./carts-tester-seed-data";
10
10
  export { ordersTesterSeedData } from "./orders-tester-seed-data";
11
11
  export { offersTesterSeedData } from "./offers-tester-seed-data";
12
+ export { testerResponsesSeedData } from "./tester-responses-seed-data";
@@ -0,0 +1,2 @@
1
+ import type { TesterChecklistResponseDocument } from "../schemas/firestore";
2
+ export declare const testerResponsesSeedData: Partial<TesterChecklistResponseDocument>[];
@@ -0,0 +1,95 @@
1
+ /*
2
+ * WHY: Three `admin/bug-hunter-rewards` cases need a tester submission that
3
+ * ALREADY EXISTS before the tester arrives — you cannot test "confirm this
4
+ * reported bug" or "reopen this confirmed case" against an empty inbox.
5
+ *
6
+ * `testerChecklistResponses` is DERIVED tier, so a run wipes it at setup
7
+ * and on a fresh project there are genuinely zero submissions. That is
8
+ * correct — stale verdicts from a previous run must not leak into the next
9
+ * one — but it left those three cases permanently unrunnable. Measured
10
+ * 2026-09-11: 0 pass / 0 fail / 5 blocked on that page, with both
11
+ * calibration controls answered correctly, so the batch was trustworthy and
12
+ * the emptiness was real.
13
+ *
14
+ * The fix is NOT to stop wiping the collection. It is to restore these
15
+ * fixtures after the wipe — `SEED_TRANSACTIONAL` in
16
+ * tester/scripts/lib/collections.mjs is a RESTORE list, orthogonal to the
17
+ * delete tiers, so a collection can be both wiped and re-seeded.
18
+ *
19
+ * WHAT: Two response documents against the two demo-fixture checklist items that
20
+ * already live in the catalogue.
21
+ *
22
+ * 🛑 THE DOC ID IS DETERMINISTIC: `${testerId}__${checklistItemId}`.
23
+ * That is the same id the live answer path computes, and it is what makes an
24
+ * answer an UPSERT rather than a duplicate — `set({merge:true})`, never
25
+ * `add()`. Generating a random id here would (a) create a second, competing
26
+ * response for the same (tester, case) pair the moment a real tester answers,
27
+ * and (b) break idempotency, so every `appkit-seed load` would pile up
28
+ * another copy — Root Cause #25 exactly.
29
+ *
30
+ * @tag domain:tester
31
+ * @tag layer:seed
32
+ * @tag access:server-only
33
+ * @tag consumers:appkit/scripts/seed-cli.mjs, seed/manifest.ts
34
+ * @tag sideEffects:none
35
+ */
36
+ /** The seeded QA persona that the demo fixtures credit as "Mock User 18". */
37
+ const TESTER_ID = "user-tester-qa";
38
+ const TESTER_NAME = "Mock User 18";
39
+ /** Same shape the answer route computes — see the callout above. */
40
+ const responseId = (testerId, checklistItemId) => `${testerId}__${checklistItemId}`;
41
+ const DEMO_V1 = "checklist-admin-bug-hunter-rewards-demo-fixture";
42
+ const DEMO_V2 = "checklist-admin-bug-hunter-rewards-demo-fixture-v2";
43
+ const seededAt = new Date("2026-08-18T09:30:00.000Z");
44
+ export const testerResponsesSeedData = [
45
+ /*
46
+ * The ALREADY-CONFIRMED submission. The v1 catalogue item carries
47
+ * bugConfirmed:true / bugHunterId:"user-tester-qa" / bugHunterName:"Mock User
48
+ * 18", and until now there was no response row behind that credit — so
49
+ * /admin/tester-feedback showed "No feedback submitted yet" while the
50
+ * leaderboard showed a confirmed bug. This is the missing half.
51
+ *
52
+ * Backs: confirm-bug-idempotent (needs a row that is already confirmed) and
53
+ * reopen-case (needs the v1 credit to be visible from a real submission).
54
+ *
55
+ * `status: "reviewed"` because an admin has already acted on it.
56
+ */
57
+ {
58
+ id: responseId(TESTER_ID, DEMO_V1),
59
+ testerId: TESTER_ID,
60
+ testerDisplayName: TESTER_NAME,
61
+ checklistItemId: DEMO_V1,
62
+ groupKey: "admin",
63
+ pageKey: "bug-hunter-rewards",
64
+ phase: 1,
65
+ answer: "no",
66
+ comment: "Seed fixture — the reported bug behind the v1 demo case. Already confirmed and credited, then reopened as v2. Exists so an admin can see what a confirmed submission looks like without waiting for a real tester to file one.",
67
+ status: "reviewed",
68
+ createdAt: seededAt,
69
+ updatedAt: seededAt,
70
+ },
71
+ /*
72
+ * The UNCONFIRMED submission, against the active v2 retest. This is the row
73
+ * "Mark as Bug" is meant to be tried on end-to-end.
74
+ *
75
+ * 🛑 status MUST stay "new" and the case MUST stay un-confirmed, or the
76
+ * confirm-bug case has nothing left to do — it would open an already-confirmed
77
+ * row and correctly report that it could not perform its own steps. A fixture
78
+ * that pre-completes the thing under test is worse than no fixture, because it
79
+ * looks like coverage.
80
+ */
81
+ {
82
+ id: responseId(TESTER_ID, DEMO_V2),
83
+ testerId: TESTER_ID,
84
+ testerDisplayName: TESTER_NAME,
85
+ checklistItemId: DEMO_V2,
86
+ groupKey: "admin",
87
+ pageKey: "bug-hunter-rewards",
88
+ phase: 1,
89
+ answer: "no",
90
+ comment: "Seed fixture — a fresh 'No' on the reopened v2 case, deliberately NOT yet confirmed. This is the row to try Mark as Bug against.",
91
+ status: "new",
92
+ createdAt: seededAt,
93
+ updatedAt: seededAt,
94
+ },
95
+ ];
package/dist/index.d.ts CHANGED
@@ -594,6 +594,7 @@ export { eventEntriesSeedData } from "./seed/index";
594
594
  export { eventsSeedData } from "./seed/index";
595
595
  export { faqSeedData } from "./seed/index";
596
596
  export { testerChecklistSeedData } from "./seed/index";
597
+ export { testerResponsesSeedData } from "./seed/index";
597
598
  export { storesTesterSeedData } from "./seed/index";
598
599
  export { categoriesTesterSeedData } from "./seed/index";
599
600
  export { productsTesterSeedData } from "./seed/index";
package/dist/index.js CHANGED
@@ -1189,6 +1189,8 @@ export { faqSeedData } from "./seed/index";
1189
1189
  // [SERVER-ONLY]-Server-only — uses Node.js, Next.js server internals, or third-party server SDKs (auth, email, payment, shipping).
1190
1190
  // testerChecklistSeedData - Model for tester checklist seed data.
1191
1191
  export { testerChecklistSeedData } from "./seed/index";
1192
+ // testerResponsesSeedData - Seeded tester submissions behind the bug-hunter demo fixtures.
1193
+ export { testerResponsesSeedData } from "./seed/index";
1192
1194
  // [SERVER-ONLY]-Server-only — uses Node.js, Next.js server internals, or third-party server SDKs (auth, email, payment, shipping).
1193
1195
  // storesTesterSeedData - Model for the shared tester sandbox store seed data.
1194
1196
  export { storesTesterSeedData } from "./seed/index";
@@ -78,7 +78,7 @@ export { historySeedData } from "./history-seed-data";
78
78
  export { groupedListingsSeedData } from "./grouped-listings-seed-data";
79
79
  export { scammersSeedData } from "./scammers-seed-data";
80
80
  export { supportTicketsSeedData } from "./support-tickets-seed-data";
81
- export { testerChecklistSeedData, storesTesterSeedData, categoriesTesterSeedData, productsTesterSeedData, blogTesterSeedData, eventsTesterSeedData, couponsTesterSeedData, bidsTesterSeedData, cartsTesterSeedData, ordersTesterSeedData, } from "../features/tester/seed-data";
81
+ export { testerChecklistSeedData, testerResponsesSeedData, storesTesterSeedData, categoriesTesterSeedData, productsTesterSeedData, blogTesterSeedData, eventsTesterSeedData, couponsTesterSeedData, bidsTesterSeedData, cartsTesterSeedData, ordersTesterSeedData, } from "../features/tester/seed-data";
82
82
  export { productFeaturesSeedData } from "./product-features-seed-data";
83
83
  export { offersSeedData } from "./offers-seed-data";
84
84
  export { payoutMethodsSeedData, shippingConfigsSeedData, analyticsCardsSeedData, analyticsAlertsSeedData, storeCategoriesSeedData, listingTemplatesSeedData, moderationQueueSeedData, reportsSeedData, itemRequestsSeedData, storeWhatsAppConfigSeedData, storeGoogleConfigSeedData, } from "./store-extensions-seed-data";
@@ -69,7 +69,12 @@ export { scammersSeedData } from "./scammers-seed-data";
69
69
  export { supportTicketsSeedData } from "./support-tickets-seed-data";
70
70
  // Tester sandbox seed fixtures live in ../features/tester/seed-data (isolated from this
71
71
  // folder on purpose) — re-exported here so every other seed consumer keeps one import surface.
72
- export { testerChecklistSeedData, storesTesterSeedData, categoriesTesterSeedData, productsTesterSeedData, blogTesterSeedData, eventsTesterSeedData, couponsTesterSeedData, bidsTesterSeedData, cartsTesterSeedData, ordersTesterSeedData, } from "../features/tester/seed-data";
72
+ export { testerChecklistSeedData,
73
+ // 🛑 Adding a tester seed export means FOUR hops, not one: the tester barrel,
74
+ // THIS re-export, appkit/src/index.ts, and seed-cli's own import. Missing the
75
+ // middle two is silent — seed-cli logs "no seed data, skipping" and the run
76
+ // continues, which is exactly what happened on the first attempt here.
77
+ testerResponsesSeedData, storesTesterSeedData, categoriesTesterSeedData, productsTesterSeedData, blogTesterSeedData, eventsTesterSeedData, couponsTesterSeedData, bidsTesterSeedData, cartsTesterSeedData, ordersTesterSeedData, } from "../features/tester/seed-data";
73
78
  export { productFeaturesSeedData } from "./product-features-seed-data";
74
79
  export { offersSeedData } from "./offers-seed-data";
75
80
  // S-STORE Extensions (Tier S-STORE) — 11 new collections
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mohasinac/appkit",
3
- "version": "4.36.0",
3
+ "version": "4.37.1",
4
4
  "license": "MIT",
5
5
  "publishConfig": {
6
6
  "access": "public"
@@ -192,7 +192,7 @@ const {
192
192
  wishlistsSeedData, historySeedData,
193
193
  groupedListingsSeedData,
194
194
  scammersSeedData, productFeaturesSeedData,
195
- testerChecklistSeedData, cartsTesterSeedData, offersTesterSeedData,
195
+ testerChecklistSeedData, testerResponsesSeedData, cartsTesterSeedData, offersTesterSeedData,
196
196
  supportTicketsSeedData, catalogueSeedData,
197
197
  shipmentsSeedData, shipmentLotsSeedData, shipmentItemsSeedData,
198
198
  storesTesterSeedData, categoriesTesterSeedData, productsTesterSeedData,
@@ -210,6 +210,7 @@ const {
210
210
  GROUPED_LISTINGS_COLLECTION, SCAMMER_COLLECTION,
211
211
  WISHLIST_COLLECTION, HISTORY_COLLECTION, PRODUCT_FEATURES_COLLECTION,
212
212
  TESTER_CHECKLIST_ITEM_COLLECTION,
213
+ TESTER_CHECKLIST_RESPONSE_COLLECTION,
213
214
  // PII field/index constants (plain data, still in the main barrel — the
214
215
  // crypto functions themselves come from appkitServer above)
215
216
  USER_PII_FIELDS, USER_PII_INDEX_MAP,
@@ -266,6 +267,17 @@ const COLLECTION_MAP = {
266
267
  scammerProfiles: SCAMMER_COLLECTION,
267
268
  productFeatures: PRODUCT_FEATURES_COLLECTION,
268
269
  testerChecklistItems: TESTER_CHECKLIST_ITEM_COLLECTION,
270
+ /*
271
+ * Tester submissions. DERIVED tier in the tester's own map (wiped at setup so
272
+ * a prior run's verdicts never leak into the next), AND listed in
273
+ * SEED_TRANSACTIONAL so these two demo fixtures are restored afterwards —
274
+ * that list is a RESTORE list, orthogonal to the delete tiers.
275
+ *
276
+ * Without the fixtures, three admin/bug-hunter-rewards cases have nothing to
277
+ * confirm or reopen and can only report blocked. Measured 2026-09-11:
278
+ * 0 pass / 0 fail / 5 blocked, both controls correct.
279
+ */
280
+ testerChecklistResponses: TESTER_CHECKLIST_RESPONSE_COLLECTION,
269
281
  };
270
282
 
271
283
  const SEED_DATA_MAP = {
@@ -325,6 +337,7 @@ const SEED_DATA_MAP = {
325
337
  scammerProfiles: scammersSeedData,
326
338
  productFeatures: productFeaturesSeedData,
327
339
  testerChecklistItems: testerChecklistSeedData,
340
+ testerChecklistResponses: testerResponsesSeedData,
328
341
  };
329
342
 
330
343
  const ALL_COLLECTIONS = Object.keys(COLLECTION_MAP);