@mohasinac/appkit 4.29.0 → 4.30.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 (160) hide show
  1. package/dist/_internal/client/features/layout/SidebarCollapseToggle.js +1 -1
  2. package/dist/_internal/server/jobs/handlers/_helpers.d.ts +22 -0
  3. package/dist/_internal/server/jobs/handlers/_helpers.js +25 -1
  4. package/dist/_internal/shared/features/blog/schema.d.ts +2 -2
  5. package/dist/_internal/shared/features/blog/schema.js +2 -1
  6. package/dist/_internal/shared/features/brands/schema.d.ts +2 -2
  7. package/dist/_internal/shared/features/brands/schema.js +3 -1
  8. package/dist/_internal/shared/features/categories/bundle-schemas.d.ts +3 -3
  9. package/dist/_internal/shared/features/categories/bundle-schemas.js +2 -1
  10. package/dist/_internal/shared/features/events/schema.d.ts +2 -2
  11. package/dist/_internal/shared/features/events/schema.js +2 -1
  12. package/dist/_internal/shared/features/products/schema.d.ts +8 -8
  13. package/dist/_internal/shared/features/products/schema.js +3 -2
  14. package/dist/_internal/shared/features/reviews/schema.d.ts +1 -1
  15. package/dist/_internal/shared/features/reviews/schema.js +2 -1
  16. package/dist/_internal/shared/types/action-result.d.ts +0 -2
  17. package/dist/client/api/surface-error.d.ts +9 -0
  18. package/dist/client/api/surface-error.js +29 -3
  19. package/dist/client/api/useApiMutation.d.ts +10 -0
  20. package/dist/client/api/useApiMutation.js +2 -1
  21. package/dist/core/server-action.d.ts +3 -14
  22. package/dist/core/server-action.js +11 -2
  23. package/dist/errors/error-envelope.d.ts +73 -0
  24. package/dist/errors/error-envelope.js +96 -0
  25. package/dist/errors/error-handler.js +14 -7
  26. package/dist/features/account/components/NotificationBell.js +1 -1
  27. package/dist/features/account/components/UserSidebar.js +2 -2
  28. package/dist/features/account/components/UserSupportView.js +2 -6
  29. package/dist/features/account/hooks/useProfile.d.ts +6 -0
  30. package/dist/features/account/hooks/useProfile.js +1 -0
  31. package/dist/features/admin/components/AdminAdEditorView.js +1 -3
  32. package/dist/features/admin/components/AdminAddressClustersView.js +1 -3
  33. package/dist/features/admin/components/AdminAddressEditorView.js +2 -4
  34. package/dist/features/admin/components/AdminAddressesView.js +1 -3
  35. package/dist/features/admin/components/AdminAllEventEntriesView.js +1 -3
  36. package/dist/features/admin/components/AdminBidsView.js +1 -3
  37. package/dist/features/admin/components/AdminBlogEditorView.js +2 -2
  38. package/dist/features/admin/components/AdminBrandEditorView.js +4 -5
  39. package/dist/features/admin/components/AdminCarouselGroupEditorView.js +1 -3
  40. package/dist/features/admin/components/AdminCategoryEditorView.js +2 -4
  41. package/dist/features/admin/components/AdminContactEditorView.js +1 -3
  42. package/dist/features/admin/components/AdminContactView.js +2 -6
  43. package/dist/features/admin/components/AdminCouponEditorView.js +2 -4
  44. package/dist/features/admin/components/AdminEmployeeEditorView.js +3 -9
  45. package/dist/features/admin/components/AdminFaqEditorView.js +2 -4
  46. package/dist/features/admin/components/AdminFeatureEditorView.js +0 -9
  47. package/dist/features/admin/components/AdminFeatureFlagsView.js +2 -4
  48. package/dist/features/admin/components/AdminNavEditorView.js +1 -3
  49. package/dist/features/admin/components/AdminNavigationView.js +3 -3
  50. package/dist/features/admin/components/AdminNewsletterView.js +1 -3
  51. package/dist/features/admin/components/AdminNotificationsView.js +2 -6
  52. package/dist/features/admin/components/AdminOffersView.js +1 -3
  53. package/dist/features/admin/components/AdminOrderEditorView.js +1 -3
  54. package/dist/features/admin/components/AdminPaymentClustersView.js +1 -3
  55. package/dist/features/admin/components/AdminPaymentMethodsView.js +1 -3
  56. package/dist/features/admin/components/AdminPayoutMarkPaidModal.js +2 -4
  57. package/dist/features/admin/components/AdminProductEditorView.js +2 -4
  58. package/dist/features/admin/components/AdminReturnRequestsView.js +2 -6
  59. package/dist/features/admin/components/AdminReviewsView.js +2 -6
  60. package/dist/features/admin/components/AdminScammerEditorView.js +2 -6
  61. package/dist/features/admin/components/AdminSectionsView.js +2 -6
  62. package/dist/features/admin/components/AdminSessionsView.js +1 -3
  63. package/dist/features/admin/components/AdminSidebar.js +2 -2
  64. package/dist/features/admin/components/AdminSiteSettingsView.js +1 -1
  65. package/dist/features/admin/components/AdminStoreEditorView.js +1 -3
  66. package/dist/features/admin/components/AdminStoresView.js +2 -6
  67. package/dist/features/admin/components/AdminSublistingCategoryEditorView.js +2 -6
  68. package/dist/features/admin/components/AdminSupportTicketDetailView.js +7 -21
  69. package/dist/features/admin/components/AdminTesterChecklistItemEditorView.js +2 -4
  70. package/dist/features/admin/components/AdminUserEditorView.js +6 -18
  71. package/dist/features/admin/components/AdminUsersView.js +2 -6
  72. package/dist/features/admin/components/BrandQuickCreateForm.js +2 -4
  73. package/dist/features/admin/components/CategoryQuickCreateForm.js +2 -4
  74. package/dist/features/auctions/components/PlaceBidFormClient.js +1 -1
  75. package/dist/features/auth/components/ForgotPasswordView.js +1 -1
  76. package/dist/features/auth/components/LoginForm.js +1 -1
  77. package/dist/features/auth/components/RegisterForm.js +1 -1
  78. package/dist/features/auth/components/ResetPasswordView.js +1 -1
  79. package/dist/features/auth/components/SocialAuthButtons.js +1 -1
  80. package/dist/features/auth/repository/session.repository.js +13 -1
  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 -2
  120. package/dist/index.js +4 -1
  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 +3 -1
  131. package/dist/security/pii-encrypt.d.ts +0 -4
  132. package/dist/security/pii-encrypt.js +79 -13
  133. package/dist/security/pii-mask.d.ts +39 -1
  134. package/dist/security/pii-mask.js +33 -4
  135. package/dist/security/pii-redact.d.ts +0 -13
  136. package/dist/security/pii-redact.js +3 -1
  137. package/dist/security/pii-schemas.d.ts +0 -2
  138. package/dist/security/pii-schemas.js +16 -2
  139. package/dist/security/settings-encryption.js +31 -3
  140. package/dist/seed/stores-seed-data.js +18 -0
  141. package/dist/seed/test-utils.js +6 -2
  142. package/dist/ui/DataTable.js +2 -2
  143. package/dist/ui/components/CollapsibleSection.js +1 -1
  144. package/dist/ui/components/ImageGallery.js +1 -1
  145. package/dist/ui/components/ListingLayout.js +1 -1
  146. package/dist/ui/forms/FormShell.d.ts +0 -19
  147. package/dist/ui/forms/FormShell.js +4 -23
  148. package/dist/ui/forms/apply-zod-issues.d.ts +34 -0
  149. package/dist/ui/forms/apply-zod-issues.js +38 -0
  150. package/dist/ui/forms/index.d.ts +2 -1
  151. package/dist/ui/forms/index.js +2 -1
  152. package/dist/ui/icons/icon-registry.d.ts +8 -0
  153. package/dist/ui/icons/icon-registry.js +13 -1
  154. package/dist/utils/action-response.d.ts +1 -1
  155. package/dist/utils/media-url.d.ts +49 -0
  156. package/dist/utils/media-url.js +72 -1
  157. package/dist/validation/schemas.d.ts +15 -2
  158. package/dist/validation/schemas.js +18 -22
  159. package/package.json +1 -1
  160. package/scripts/seed-cli.mjs +10 -2
@@ -14,5 +14,5 @@ export function SidebarCollapseToggle({ expanded, onToggle, expandedLabel = "Col
14
14
  // Chevron rotation direction mirrors along with the dock side so it keeps
15
15
  // pointing "into" the collapsed panel regardless of which edge it's on.
16
16
  const chevronRotated = edge === "left" ? !expanded : expanded;
17
- return (_jsx(IconButton, { type: "button", onClick: onToggle, "aria-label": expanded ? expandedLabel : collapsedLabel, "aria-expanded": expanded, variant: "ghost", style: HANDLE_STYLE, className: `w-9 shrink-0 ${roundedClass} shadow-lg transition-all duration-200 hover:shadow-xl hover:brightness-110 active:scale-[0.96]`, icon: _jsx("svg", { className: `w-4 h-4 drop-shadow-sm transition-transform duration-300 ${chevronRotated ? "rotate-180" : ""}`, style: CHEVRON_STYLE, fill: "none", stroke: "currentColor", viewBox: "0 0 24 24", "aria-hidden": "true", children: _jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2.5, d: "M15 19l-7-7 7-7" }) }) }));
17
+ return (_jsx(IconButton, { type: "button", onClick: onToggle, "aria-label": expanded ? expandedLabel : collapsedLabel, "aria-expanded": expanded, variant: "ghost", style: HANDLE_STYLE, className: `w-9 shrink-0 ${roundedClass} shadow-lg transition-all duration-200 hover:shadow-xl hover:brightness-110 active:scale-[0.96]`, icon: _jsx("svg", { className: `w-4 h-4 drop-shadow-sm transition-transform duration-300 ${chevronRotated ? "rotate-180" : ""} shrink-0`, style: CHEVRON_STYLE, fill: "none", stroke: "currentColor", viewBox: "0 0 24 24", "aria-hidden": "true", children: _jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2.5, d: "M15 19l-7-7 7-7" }) }) }));
18
18
  }
@@ -10,5 +10,27 @@ export { getAdminRealtimeDb };
10
10
  * Query docs tagged isTestData:true in a collection. `cutoff: null` returns every
11
11
  * tester-sandbox doc regardless of expiry (manual force-purge); a real Date returns
12
12
  * only the ones past testDataExpiresAt (the scheduled 7-day sweep).
13
+ *
14
+ * ## 🛑 `isTestData` without `testDataExpiresAt` means PERMANENT, never purge
15
+ *
16
+ * The flag has two uses, and only one of them is a sandbox fixture:
17
+ *
18
+ * · a fixture — `isTestData: true` PLUS `testDataExpiresAt`. Disposable,
19
+ * swept on the 7-day cycle. Every doc in `features/tester/seed-data/` is
20
+ * this shape; the invariant was checked across all six files.
21
+ * · permanently hidden — `isTestData: true` and NO expiry. Real, permanent
22
+ * content that simply must not reach the public or the sitemap.
23
+ * `store-tester-qa-seller` is the case that forced this distinction.
24
+ *
25
+ * The scheduled path already separates them: `testDataExpiresAt <= cutoff`
26
+ * excludes docs lacking the field. The FORCE path did not — `cutoff: null`
27
+ * drops that clause and returned everything, so `tester:purge-sandbox --force`
28
+ * would have deleted a permanent seed store. Hence the filter below.
29
+ *
30
+ * Filtered in memory rather than as a query clause: a Firestore inequality on
31
+ * `testDataExpiresAt` would exclude every doc missing it, which is the trap
32
+ * documented in CLAUDE.md's anti-pattern table — correct here by luck, wrong
33
+ * the moment someone reuses the shape. In-memory keeps the intent legible, and
34
+ * the read is already bounded at 200.
13
35
  */
14
36
  export declare function getTestDataRefs(db: FirebaseFirestore.Firestore, collection: string, cutoff: Date | null): Promise<FirebaseFirestore.DocumentReference[]>;
@@ -22,6 +22,28 @@ export { getAdminRealtimeDb };
22
22
  * Query docs tagged isTestData:true in a collection. `cutoff: null` returns every
23
23
  * tester-sandbox doc regardless of expiry (manual force-purge); a real Date returns
24
24
  * only the ones past testDataExpiresAt (the scheduled 7-day sweep).
25
+ *
26
+ * ## 🛑 `isTestData` without `testDataExpiresAt` means PERMANENT, never purge
27
+ *
28
+ * The flag has two uses, and only one of them is a sandbox fixture:
29
+ *
30
+ * · a fixture — `isTestData: true` PLUS `testDataExpiresAt`. Disposable,
31
+ * swept on the 7-day cycle. Every doc in `features/tester/seed-data/` is
32
+ * this shape; the invariant was checked across all six files.
33
+ * · permanently hidden — `isTestData: true` and NO expiry. Real, permanent
34
+ * content that simply must not reach the public or the sitemap.
35
+ * `store-tester-qa-seller` is the case that forced this distinction.
36
+ *
37
+ * The scheduled path already separates them: `testDataExpiresAt <= cutoff`
38
+ * excludes docs lacking the field. The FORCE path did not — `cutoff: null`
39
+ * drops that clause and returned everything, so `tester:purge-sandbox --force`
40
+ * would have deleted a permanent seed store. Hence the filter below.
41
+ *
42
+ * Filtered in memory rather than as a query clause: a Firestore inequality on
43
+ * `testDataExpiresAt` would exclude every doc missing it, which is the trap
44
+ * documented in CLAUDE.md's anti-pattern table — correct here by luck, wrong
45
+ * the moment someone reuses the shape. In-memory keeps the intent legible, and
46
+ * the read is already bounded at 200.
25
47
  */
26
48
  export async function getTestDataRefs(db, collection, cutoff) {
27
49
  let q = db
@@ -30,5 +52,7 @@ export async function getTestDataRefs(db, collection, cutoff) {
30
52
  if (cutoff)
31
53
  q = q.where("testDataExpiresAt", "<=", cutoff);
32
54
  const snap = await q.limit(200).get();
33
- return snap.docs.map((d) => d.ref);
55
+ return snap.docs
56
+ .filter((d) => cutoff !== null || d.get("testDataExpiresAt") != null)
57
+ .map((d) => d.ref);
34
58
  }
@@ -4,7 +4,7 @@ export declare const blogPostInputSchema: z.ZodObject<{
4
4
  slug: z.ZodString;
5
5
  excerpt: z.ZodOptional<z.ZodString>;
6
6
  content: z.ZodString;
7
- coverImage: z.ZodOptional<z.ZodString>;
7
+ coverImage: z.ZodOptional<z.ZodEffects<z.ZodString, string, string>>;
8
8
  youtubeId: z.ZodOptional<z.ZodString>;
9
9
  category: z.ZodString;
10
10
  tags: z.ZodDefault<z.ZodArray<z.ZodString, "many">>;
@@ -44,7 +44,7 @@ export declare const blogPostUpdateSchema: z.ZodObject<Omit<{
44
44
  slug: z.ZodOptional<z.ZodString>;
45
45
  excerpt: z.ZodOptional<z.ZodOptional<z.ZodString>>;
46
46
  content: z.ZodOptional<z.ZodString>;
47
- coverImage: z.ZodOptional<z.ZodOptional<z.ZodString>>;
47
+ coverImage: z.ZodOptional<z.ZodOptional<z.ZodEffects<z.ZodString, string, string>>>;
48
48
  youtubeId: z.ZodOptional<z.ZodOptional<z.ZodString>>;
49
49
  category: z.ZodOptional<z.ZodString>;
50
50
  tags: z.ZodOptional<z.ZodDefault<z.ZodArray<z.ZodString, "many">>>;
@@ -1,11 +1,12 @@
1
1
  import { z } from "zod";
2
2
  import { BLOG_TITLE_MAX_LENGTH, BLOG_EXCERPT_MAX_LENGTH } from "./config";
3
+ import { mediaUrlSchema } from "../../../../validation/schemas";
3
4
  export const blogPostInputSchema = z.object({
4
5
  title: z.string().min(3).max(BLOG_TITLE_MAX_LENGTH),
5
6
  slug: z.string().min(1).regex(/^blog-[a-z0-9-]+$/, "Slug must start with 'blog-'"),
6
7
  excerpt: z.string().max(BLOG_EXCERPT_MAX_LENGTH).optional(),
7
8
  content: z.string().max(100000),
8
- coverImage: z.string().url().optional(),
9
+ coverImage: mediaUrlSchema.optional(),
9
10
  youtubeId: z.string().optional(),
10
11
  category: z.string().min(1),
11
12
  tags: z.array(z.string()).max(10).default([]),
@@ -3,7 +3,7 @@ export declare const brandInputSchema: z.ZodObject<{
3
3
  name: z.ZodString;
4
4
  slug: z.ZodString;
5
5
  description: z.ZodOptional<z.ZodString>;
6
- logoURL: z.ZodOptional<z.ZodString>;
6
+ logoURL: z.ZodOptional<z.ZodEffects<z.ZodString, string, string>>;
7
7
  website: z.ZodOptional<z.ZodString>;
8
8
  country: z.ZodOptional<z.ZodString>;
9
9
  founded: z.ZodOptional<z.ZodNumber>;
@@ -34,7 +34,7 @@ export declare const brandUpdateSchema: z.ZodObject<Omit<{
34
34
  name: z.ZodOptional<z.ZodString>;
35
35
  slug: z.ZodOptional<z.ZodString>;
36
36
  description: z.ZodOptional<z.ZodOptional<z.ZodString>>;
37
- logoURL: z.ZodOptional<z.ZodOptional<z.ZodString>>;
37
+ logoURL: z.ZodOptional<z.ZodOptional<z.ZodEffects<z.ZodString, string, string>>>;
38
38
  website: z.ZodOptional<z.ZodOptional<z.ZodString>>;
39
39
  country: z.ZodOptional<z.ZodOptional<z.ZodString>>;
40
40
  founded: z.ZodOptional<z.ZodOptional<z.ZodNumber>>;
@@ -1,10 +1,12 @@
1
1
  import { z } from "zod";
2
2
  import { BRAND_NAME_MAX_LENGTH, BRAND_DESCRIPTION_MAX_LENGTH } from "./config";
3
+ import { mediaUrlSchema } from "../../../../validation/schemas";
3
4
  export const brandInputSchema = z.object({
4
5
  name: z.string().min(1).max(BRAND_NAME_MAX_LENGTH),
5
6
  slug: z.string().min(1).regex(/^brand-[a-z0-9-]+$/, "Slug must start with 'brand-' and contain only lowercase letters, numbers, and hyphens"),
6
7
  description: z.string().max(BRAND_DESCRIPTION_MAX_LENGTH).optional(),
7
- logoURL: z.string().url().optional(),
8
+ logoURL: mediaUrlSchema.optional(),
9
+ // `website` is a genuine EXTERNAL link, not stored media — stays `.url()`.
8
10
  website: z.string().url().optional(),
9
11
  country: z.string().optional(),
10
12
  founded: z.number().int().min(1800).max(new Date().getFullYear()).optional(),
@@ -60,7 +60,7 @@ export declare const bundleQueryRuleSchema: z.ZodDiscriminatedUnion<"type", [z.Z
60
60
  orderBy?: "price-asc" | "price-desc" | "createdAt-desc" | undefined;
61
61
  }>]>;
62
62
  export declare const bundleDisplaySchema: z.ZodObject<{
63
- coverImage: z.ZodOptional<z.ZodString>;
63
+ coverImage: z.ZodOptional<z.ZodEffects<z.ZodString, string, string>>;
64
64
  icon: z.ZodOptional<z.ZodString>;
65
65
  color: z.ZodOptional<z.ZodString>;
66
66
  }, "strip", z.ZodTypeAny, {
@@ -148,7 +148,7 @@ export declare const bundleCreateSchema: z.ZodObject<{
148
148
  drawCount?: number | undefined;
149
149
  }>, "many">>;
150
150
  display: z.ZodOptional<z.ZodObject<{
151
- coverImage: z.ZodOptional<z.ZodString>;
151
+ coverImage: z.ZodOptional<z.ZodEffects<z.ZodString, string, string>>;
152
152
  icon: z.ZodOptional<z.ZodString>;
153
153
  color: z.ZodOptional<z.ZodString>;
154
154
  }, "strip", z.ZodTypeAny, {
@@ -295,7 +295,7 @@ export declare const bundleUpdateSchema: z.ZodObject<{
295
295
  drawCount?: number | undefined;
296
296
  }>, "many">>;
297
297
  display: z.ZodOptional<z.ZodObject<{
298
- coverImage: z.ZodOptional<z.ZodString>;
298
+ coverImage: z.ZodOptional<z.ZodEffects<z.ZodString, string, string>>;
299
299
  icon: z.ZodOptional<z.ZodString>;
300
300
  color: z.ZodOptional<z.ZodString>;
301
301
  }, "strip", z.ZodTypeAny, {
@@ -9,6 +9,7 @@
9
9
  */
10
10
  import { z } from "zod";
11
11
  import { BUNDLE_MAX_ITEMS, BUNDLE_DRAW_COUNT_MAX, BUNDLE_KIND_SPECIAL, BUNDLE_KIND_BRAND, } from "./bundle-config";
12
+ import { mediaUrlSchema } from "../../../../validation/schemas";
12
13
  const productIdsSchema = z.array(z.string().min(1)).min(0).max(BUNDLE_MAX_ITEMS);
13
14
  /**
14
15
  * Per-member detail — mirrors BundleItemDetail in the Firestore schema.
@@ -39,7 +40,7 @@ export const bundleQueryRuleSchema = z.discriminatedUnion("type", [
39
40
  }),
40
41
  ]);
41
42
  export const bundleDisplaySchema = z.object({
42
- coverImage: z.string().url().optional(),
43
+ coverImage: mediaUrlSchema.optional(),
43
44
  icon: z.string().optional(),
44
45
  color: z.string().optional(),
45
46
  });
@@ -8,7 +8,7 @@ export declare const eventInputSchema: z.ZodObject<{
8
8
  startsAt: z.ZodString;
9
9
  endsAt: z.ZodString;
10
10
  maxEntries: z.ZodOptional<z.ZodNumber>;
11
- imageUrl: z.ZodOptional<z.ZodString>;
11
+ imageUrl: z.ZodOptional<z.ZodEffects<z.ZodString, string, string>>;
12
12
  location: z.ZodOptional<z.ZodString>;
13
13
  isOnline: z.ZodDefault<z.ZodBoolean>;
14
14
  entryFee: z.ZodDefault<z.ZodNumber>;
@@ -48,7 +48,7 @@ export declare const eventUpdateSchema: z.ZodObject<Omit<{
48
48
  startsAt: z.ZodOptional<z.ZodString>;
49
49
  endsAt: z.ZodOptional<z.ZodString>;
50
50
  maxEntries: z.ZodOptional<z.ZodOptional<z.ZodNumber>>;
51
- imageUrl: z.ZodOptional<z.ZodOptional<z.ZodString>>;
51
+ imageUrl: z.ZodOptional<z.ZodOptional<z.ZodEffects<z.ZodString, string, string>>>;
52
52
  location: z.ZodOptional<z.ZodOptional<z.ZodString>>;
53
53
  isOnline: z.ZodOptional<z.ZodDefault<z.ZodBoolean>>;
54
54
  entryFee: z.ZodOptional<z.ZodDefault<z.ZodNumber>>;
@@ -1,5 +1,6 @@
1
1
  import { z } from "zod";
2
2
  import { EVENT_TITLE_MAX_LENGTH, EVENT_DESCRIPTION_MAX_LENGTH } from "./config";
3
+ import { mediaUrlSchema } from "../../../../validation/schemas";
3
4
  export const eventInputSchema = z.object({
4
5
  title: z.string().min(3).max(EVENT_TITLE_MAX_LENGTH),
5
6
  slug: z.string().min(1).regex(/^event-[a-z0-9-]+$/, "Slug must start with 'event-'"),
@@ -9,7 +10,7 @@ export const eventInputSchema = z.object({
9
10
  startsAt: z.string().datetime({ offset: true }),
10
11
  endsAt: z.string().datetime({ offset: true }),
11
12
  maxEntries: z.number().int().min(1).optional(),
12
- imageUrl: z.string().url().optional(),
13
+ imageUrl: mediaUrlSchema.optional(),
13
14
  location: z.string().max(300).optional(),
14
15
  isOnline: z.boolean().default(false),
15
16
  entryFee: z.number().min(0).default(0),
@@ -8,8 +8,8 @@ export declare const productInputSchema: z.ZodObject<{
8
8
  currency: z.ZodDefault<z.ZodString>;
9
9
  stockQuantity: z.ZodDefault<z.ZodNumber>;
10
10
  availableQuantity: z.ZodDefault<z.ZodNumber>;
11
- mainImage: z.ZodString;
12
- images: z.ZodDefault<z.ZodArray<z.ZodString, "many">>;
11
+ mainImage: z.ZodEffects<z.ZodString, string, string>;
12
+ images: z.ZodDefault<z.ZodArray<z.ZodEffects<z.ZodString, string, string>, "many">>;
13
13
  condition: z.ZodOptional<z.ZodEnum<["new", "like_new", "good", "fair", "poor", "used", "refurbished", "broken"]>>;
14
14
  tags: z.ZodDefault<z.ZodArray<z.ZodString, "many">>;
15
15
  seoTitle: z.ZodOptional<z.ZodString>;
@@ -81,8 +81,8 @@ export declare const productUpdateSchema: z.ZodObject<{
81
81
  currency: z.ZodOptional<z.ZodDefault<z.ZodString>>;
82
82
  stockQuantity: z.ZodOptional<z.ZodDefault<z.ZodNumber>>;
83
83
  availableQuantity: z.ZodOptional<z.ZodDefault<z.ZodNumber>>;
84
- mainImage: z.ZodOptional<z.ZodString>;
85
- images: z.ZodOptional<z.ZodDefault<z.ZodArray<z.ZodString, "many">>>;
84
+ mainImage: z.ZodOptional<z.ZodEffects<z.ZodString, string, string>>;
85
+ images: z.ZodOptional<z.ZodDefault<z.ZodArray<z.ZodEffects<z.ZodString, string, string>, "many">>>;
86
86
  condition: z.ZodOptional<z.ZodOptional<z.ZodEnum<["new", "like_new", "good", "fair", "poor", "used", "refurbished", "broken"]>>>;
87
87
  tags: z.ZodOptional<z.ZodDefault<z.ZodArray<z.ZodString, "many">>>;
88
88
  seoTitle: z.ZodOptional<z.ZodOptional<z.ZodString>>;
@@ -154,8 +154,8 @@ export declare const auctionInputSchema: z.ZodObject<{
154
154
  currency: z.ZodDefault<z.ZodString>;
155
155
  stockQuantity: z.ZodDefault<z.ZodNumber>;
156
156
  availableQuantity: z.ZodDefault<z.ZodNumber>;
157
- mainImage: z.ZodString;
158
- images: z.ZodDefault<z.ZodArray<z.ZodString, "many">>;
157
+ mainImage: z.ZodEffects<z.ZodString, string, string>;
158
+ images: z.ZodDefault<z.ZodArray<z.ZodEffects<z.ZodString, string, string>, "many">>;
159
159
  condition: z.ZodOptional<z.ZodEnum<["new", "like_new", "good", "fair", "poor", "used", "refurbished", "broken"]>>;
160
160
  tags: z.ZodDefault<z.ZodArray<z.ZodString, "many">>;
161
161
  seoTitle: z.ZodOptional<z.ZodString>;
@@ -255,8 +255,8 @@ export declare const preOrderInputSchema: z.ZodObject<{
255
255
  currency: z.ZodDefault<z.ZodString>;
256
256
  stockQuantity: z.ZodDefault<z.ZodNumber>;
257
257
  availableQuantity: z.ZodDefault<z.ZodNumber>;
258
- mainImage: z.ZodString;
259
- images: z.ZodDefault<z.ZodArray<z.ZodString, "many">>;
258
+ mainImage: z.ZodEffects<z.ZodString, string, string>;
259
+ images: z.ZodDefault<z.ZodArray<z.ZodEffects<z.ZodString, string, string>, "many">>;
260
260
  condition: z.ZodOptional<z.ZodEnum<["new", "like_new", "good", "fair", "poor", "used", "refurbished", "broken"]>>;
261
261
  tags: z.ZodDefault<z.ZodArray<z.ZodString, "many">>;
262
262
  seoTitle: z.ZodOptional<z.ZodString>;
@@ -1,4 +1,5 @@
1
1
  import { z } from "zod";
2
+ import { mediaUrlSchema } from "../../../../validation/schemas";
2
3
  const priceSchema = z.number().min(0, "Price must be non-negative");
3
4
  const conditionSchema = z.enum(["new", "like_new", "good", "fair", "poor", "used", "refurbished", "broken"]);
4
5
  export const productInputSchema = z.object({
@@ -10,8 +11,8 @@ export const productInputSchema = z.object({
10
11
  currency: z.string().default("INR"),
11
12
  stockQuantity: z.number().int().min(0).default(0),
12
13
  availableQuantity: z.number().int().min(0).default(0),
13
- mainImage: z.string().url("mainImage must be a valid URL"),
14
- images: z.array(z.string().url()).max(20).default([]),
14
+ mainImage: mediaUrlSchema,
15
+ images: z.array(mediaUrlSchema).max(20).default([]),
15
16
  condition: conditionSchema.optional(),
16
17
  tags: z.array(z.string()).max(20).default([]),
17
18
  seoTitle: z.string().max(70).optional(),
@@ -5,7 +5,7 @@ export declare const createReviewSchema: z.ZodObject<{
5
5
  rating: z.ZodNumber;
6
6
  title: z.ZodString;
7
7
  comment: z.ZodString;
8
- images: z.ZodDefault<z.ZodArray<z.ZodString, "many">>;
8
+ images: z.ZodDefault<z.ZodArray<z.ZodEffects<z.ZodString, string, string>, "many">>;
9
9
  orderId: z.ZodOptional<z.ZodString>;
10
10
  }, "strip", z.ZodTypeAny, {
11
11
  title: string;
@@ -1,4 +1,5 @@
1
1
  import { z } from "zod";
2
+ import { mediaUrlSchema } from "../../../../validation/schemas";
2
3
  import { REVIEW_BODY_MAX_LENGTH, REVIEW_BODY_MIN_LENGTH, REVIEW_TITLE_MAX_LENGTH, REVIEW_MIN_RATING, REVIEW_MAX_RATING, REVIEW_IMAGES_MAX, REVIEW_REPLY_MAX_LENGTH, } from "./config";
3
4
  export const createReviewSchema = z.object({
4
5
  productId: z.string().min(1),
@@ -16,7 +17,7 @@ export const createReviewSchema = z.object({
16
17
  * never consulted it.
17
18
  */
18
19
  comment: z.string().min(REVIEW_BODY_MIN_LENGTH).max(REVIEW_BODY_MAX_LENGTH),
19
- images: z.array(z.string().url()).max(REVIEW_IMAGES_MAX).default([]),
20
+ images: z.array(mediaUrlSchema).max(REVIEW_IMAGES_MAX).default([]),
20
21
  orderId: z.string().optional(),
21
22
  });
22
23
  export const replyToReviewSchema = z.object({
@@ -14,8 +14,6 @@ export type ActionResult<T> = {
14
14
  code?: string;
15
15
  error: string;
16
16
  issues?: unknown[];
17
- /** Legacy server-action shape — kept for compat. */
18
- fieldErrors?: Record<string, string[]>;
19
17
  /** Legacy debug field — only populated in dev. */
20
18
  debug?: {
21
19
  stack?: string;
@@ -18,6 +18,15 @@ export interface SurfaceErrorOptions {
18
18
  setFieldError?: (field: string, message: string) => void;
19
19
  /** next-intl style translator. Falls back to the raw error message when undefined. */
20
20
  translate?: (key: string) => string;
21
+ /**
22
+ * Curated copy preferred over `err.message` when the error carries no
23
+ * mapped display message. Lets a caller keep wording better than the raw
24
+ * server string WITHOUT adding a second toast of its own.
25
+ *
26
+ * Never overrides field-level `issues` — those are more specific than any
27
+ * caller-supplied sentence.
28
+ */
29
+ fallbackMessage?: string;
21
30
  /** Optional reporter for the clientErrors / serverErrors collection. */
22
31
  report?: (payload: {
23
32
  code: string;
@@ -1,5 +1,6 @@
1
1
  import { isApiError } from "./ApiError";
2
2
  import { ApiClientError } from "../../http/ApiClient";
3
+ import { applyZodIssues, hasAttachableIssue } from "../../ui/forms/apply-zod-issues";
3
4
  import { normalizeError } from "../../errors/normalize";
4
5
  import { getErrorDisplay } from "../../errors/error-display-map";
5
6
  /** True for `ApiError` instances and for `ApiClientError` (the lower-level
@@ -9,11 +10,36 @@ import { getErrorDisplay } from "../../errors/error-display-map";
9
10
  function hasStableErrorCode(err) {
10
11
  return isApiError(err) || (err instanceof ApiClientError && typeof err.code === "string");
11
12
  }
13
+ /** Field-level issues carried by either error shape, when present. */
14
+ function issuesOf(err) {
15
+ if (isApiError(err))
16
+ return err.issues;
17
+ if (err instanceof ApiClientError)
18
+ return err.issues;
19
+ return undefined;
20
+ }
12
21
  export function surfaceError(err, opts) {
13
- const { showToast, setFieldError, translate, report } = opts;
22
+ const { showToast, setFieldError, translate, report, fallbackMessage } = opts;
23
+ // ISSUES FIRST. A validation failure names the fields it is about, and the
24
+ // right place for "photoURL must be …" is under the photoURL input — not a
25
+ // toast that says "Validation failed" and nothing else.
26
+ //
27
+ // This branch could not fire before: `errorResponse` serialised Zod issues
28
+ // under a `details` key that `ApiClientError` never read, so `err.issues`
29
+ // was always undefined and every schema rejection on every route fell
30
+ // through to the generic toast below.
31
+ //
32
+ // `ERROR_DISPLAY_MAP` deliberately has no `field` for VALIDATION_FAILED —
33
+ // a validation failure is not tied to one fixed field, which is exactly why
34
+ // the routing has to come from the issues themselves.
35
+ const issues = issuesOf(err);
36
+ if (setFieldError && hasAttachableIssue(issues)) {
37
+ applyZodIssues(issues, (name, message) => setFieldError(name, message ?? ""));
38
+ return; // inline errors only — no toast
39
+ }
14
40
  if (hasStableErrorCode(err)) {
15
41
  const display = getErrorDisplay(err.code);
16
- const message = translate?.(display.messageKey) ?? err.message;
42
+ const message = translate?.(display.messageKey) ?? fallbackMessage ?? err.message;
17
43
  if (display.field && setFieldError) {
18
44
  setFieldError(display.field, message);
19
45
  return;
@@ -24,7 +50,7 @@ export function surfaceError(err, opts) {
24
50
  // Non-ApiError — this is a programming error or unhandled exception.
25
51
  // Report it (workstream 13) so clientErrors picks it up.
26
52
  const message = err instanceof Error ? err.message : "Something went wrong. Please try again.";
27
- const fallback = translate?.("errors.codes.CLIENT_UNHANDLED") ?? message;
53
+ const fallback = fallbackMessage ?? translate?.("errors.codes.CLIENT_UNHANDLED") ?? message;
28
54
  showToast(fallback, "error");
29
55
  if (report) {
30
56
  try {
@@ -16,6 +16,16 @@ export interface UseApiMutationOptions<TData, TError, TVariables, TContext> exte
16
16
  /** When set, a `loading` toast is shown on mutate and finalized on settle. */
17
17
  loadingMessage?: string;
18
18
  successMessage?: string;
19
+ /**
20
+ * Curated failure copy, preferred over `err.message` by the ONE surface.
21
+ *
22
+ * Exists so a caller with wording better than the raw server message
23
+ * ("Could not mark notification as read.") does not have to add a second
24
+ * toast in its own `onError` to say it — which is what produced two and
25
+ * three stacked banners for a single failure. A server error that carries
26
+ * field `issues` still wins: those route to inline field errors.
27
+ */
28
+ errorMessage?: string;
19
29
  /** Translation function (next-intl style); falls back to raw message. */
20
30
  translate?: (key: string) => string;
21
31
  /** Form field error setter (FormShell context). Optional. */
@@ -6,7 +6,7 @@ import { surfaceError } from "./surface-error";
6
6
  export function useApiMutation(options) {
7
7
  const { showToast, updateToast } = useToast();
8
8
  const loadingIdRef = React.useRef(null);
9
- const { loadingMessage, successMessage, translate, setFieldError, reportClientError, onMutate, onError, onSuccess, onSettled, ...rest } = options;
9
+ const { loadingMessage, successMessage, errorMessage, translate, setFieldError, reportClientError, onMutate, onError, onSuccess, onSettled, ...rest } = options;
10
10
  return useMutation({
11
11
  ...rest,
12
12
  onMutate: (async (variables, ctx) => {
@@ -20,6 +20,7 @@ export function useApiMutation(options) {
20
20
  showToast,
21
21
  setFieldError,
22
22
  translate,
23
+ fallbackMessage: errorMessage,
23
24
  report: reportClientError,
24
25
  });
25
26
  onError?.(err, variables, context, ctx);
@@ -1,15 +1,5 @@
1
- export type ActionResult<TOutput> = {
2
- ok: true;
3
- data: TOutput;
4
- } | {
5
- ok: false;
6
- error: string;
7
- code?: string;
8
- fieldErrors?: Record<string, string[]>;
9
- debug?: {
10
- stack?: string;
11
- };
12
- };
1
+ export type { ActionResult } from "../_internal/shared/types/action-result";
2
+ import type { ActionResult } from "../_internal/shared/types/action-result";
13
3
  export interface ActionMiddlewareContext<TInput = unknown, TOutput = unknown> {
14
4
  actionName: string;
15
5
  input: TInput;
@@ -50,7 +40,7 @@ export interface ServerActionOptions<TInput, TOutput> {
50
40
  name?: string;
51
41
  /**
52
42
  * Zod-compatible schema for input validation.
53
- * If validation fails, returns `{ ok: false, fieldErrors }` before the handler runs.
43
+ * If validation fails, returns `{ ok: false, issues }` before the handler runs.
54
44
  */
55
45
  schema?: SafeParseSchema<TInput>;
56
46
  /**
@@ -79,4 +69,3 @@ export interface ServerActionOptions<TInput, TOutput> {
79
69
  * and can be exported directly from an `actions/` file.
80
70
  */
81
71
  export declare function createServerAction<TInput = unknown, TOutput = unknown>(options: ServerActionOptions<TInput, TOutput>): (rawInput: TInput) => Promise<ActionResult<TOutput>>;
82
- export {};
@@ -1,4 +1,6 @@
1
1
  import { normalizeError } from "../errors/normalize";
2
+ import { toApiIssues } from "../errors/error-envelope";
3
+ import { HTTP_ERROR_CODES } from "../errors/error-mapping";
2
4
  /**
3
5
  * createServerAction — @mohasinac/appkit/core
4
6
  *
@@ -65,8 +67,15 @@ export function createServerAction(options) {
65
67
  if (options.schema) {
66
68
  const parsed = options.schema.safeParse(rawInput);
67
69
  if (!parsed.success) {
68
- const fieldErrors = parsed.error.flatten?.().fieldErrors ?? {};
69
- return { ok: false, error: "Validation failed", fieldErrors };
70
+ // `issues` (not the old `fieldErrors`, which had zero readers) so a
71
+ // server action's validation failure routes to inline field errors
72
+ // through the same path an HTTP route's does.
73
+ return {
74
+ ok: false,
75
+ error: "Validation failed",
76
+ code: HTTP_ERROR_CODES.VALIDATION_FAILED,
77
+ issues: toApiIssues(parsed.error.issues),
78
+ };
70
79
  }
71
80
  input = parsed.data;
72
81
  }
@@ -0,0 +1,73 @@
1
+ /**
2
+ * The ONE shape of an HTTP error body.
3
+ *
4
+ * WHY THIS FILE EXISTS
5
+ * --------------------
6
+ * There were four independent producers of an error response, and the client
7
+ * read a shape none of them fully emitted:
8
+ *
9
+ * - `next/api/api-response.ts` → `{success, error, details}` — no `code`
10
+ * - `next/api/routeHandler.ts` → `{ok, success, code, error, issues, requestId}`
11
+ * - `errors/error-handler.ts` → `{..., data: {issues}}` — issues nested
12
+ * - `core/server-action.ts` → `{ok, error, fieldErrors}`
13
+ *
14
+ * while `http/ApiClient.ts`'s `ApiClientError` reads exactly three keys off the
15
+ * body: `code`, `issues`, `requestId`.
16
+ *
17
+ * The consequence was silent and app-wide. On every route built with
18
+ * `createApiHandler`, a Zod rejection serialised its issues under `details`,
19
+ * which nothing reads — so `err.code` was `undefined`, `surfaceError`'s
20
+ * `hasStableErrorCode()` check failed, the `ERROR_DISPLAY_MAP` branch was
21
+ * skipped, and the user got a bare toast reading "Validation failed" with no
22
+ * field name. `applyZodIssues(err.issues, …)` was dead code everywhere.
23
+ *
24
+ * Every producer now builds its body here, so the wire shape and the reader
25
+ * cannot drift again. `scripts/audit-api-error-envelope.mjs` enforces it in
26
+ * both directions.
27
+ */
28
+ import type { ApiIssue } from "../client/api/ApiError";
29
+ export interface ApiErrorEnvelope {
30
+ ok: false;
31
+ /**
32
+ * @deprecated Mirrors `ok` for consumers that still read `body.success`.
33
+ * `audit-server-action-envelope.mjs` is designed to start failing once the
34
+ * last reader is gone, at which point this key is removed.
35
+ */
36
+ success: false;
37
+ code: string;
38
+ error: string;
39
+ issues?: ApiIssue[];
40
+ requestId?: string;
41
+ }
42
+ /**
43
+ * Default stable code for an HTTP status, for producers that don't carry one.
44
+ *
45
+ * Mirrors the mapping already applied in `mapToHttpError`'s status branch —
46
+ * extracted so there is one table rather than two that can disagree about
47
+ * what a 409 is called.
48
+ */
49
+ export declare function codeForStatus(status: number): string;
50
+ /**
51
+ * Narrow raw Zod issues to the wire shape.
52
+ *
53
+ * Deliberately picks ONLY `path`, `message` and `code`. A `ZodIssue` can also
54
+ * carry `received` / `expected` / `unionErrors`, and `received` is the value
55
+ * the user actually submitted — i.e. potentially their email, phone or
56
+ * address. Spreading the issue wholesale would publish that in an error body
57
+ * and into any client-error log that captures it.
58
+ *
59
+ * The `unknown` parameter is the point: this is the sole entry point for issue
60
+ * arrays arriving from THREE mutually unassignable sources — zod v3
61
+ * `ZodIssue[]` (appkit), zod v4 `$ZodIssue[]` (the consumer app, a different
62
+ * major), and the `unknown[]` that `mapToHttpError` already returns. Narrowing
63
+ * it to any one of them makes the other two uncallable. This function exists
64
+ * precisely to be the place that narrows.
65
+ */
66
+ export declare function toApiIssues(input: unknown): ApiIssue[] | undefined;
67
+ export declare function buildErrorEnvelope(input: {
68
+ message: string;
69
+ status?: number;
70
+ code?: string;
71
+ issues?: ApiIssue[];
72
+ requestId?: string;
73
+ }): ApiErrorEnvelope;