@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
@@ -0,0 +1,38 @@
1
+ /**
2
+ * `applyZodIssues` — pipe every issue from a validation failure into a form's
3
+ * error map, so they render inline on `<FieldInput>` etc. rather than as a
4
+ * toast.
5
+ *
6
+ * WHY THIS IS ITS OWN FILE
7
+ * ------------------------
8
+ * It used to live in `FormShell.tsx`, which is a `"use client"` React module.
9
+ * `client/api/surface-error.ts` needs this function to route server-side
10
+ * validation failures to inline field errors, and that file is deliberately
11
+ * hook-free and React-free (see its header) so it can be called from outside a
12
+ * hook boundary — e.g. inside Button's non-hook click wrapper. Importing
13
+ * FormShell there would have dragged React and the whole form runtime into it.
14
+ *
15
+ * This is a MOVE, not a barrel re-export: `FormShell.tsx` imports it from here.
16
+ *
17
+ * Keys are the full dotted/indexed path (`"video.duration"`, `"images.2"`),
18
+ * not just the top-level segment — a plain `issue.path[0]` key would collapse
19
+ * every nested issue under one object onto the same map entry, silently
20
+ * dropping all but the last. Top-level-only fields (the common case) are
21
+ * unaffected: a single-segment path joins to the same string as before.
22
+ *
23
+ * Root-level `.refine()` issues (empty `path: []`, no owning field) are still
24
+ * skipped — there is no field to attach them to, so the caller must surface
25
+ * those some other way (they fall through to a toast in `surfaceError`).
26
+ */
27
+ export function applyZodIssues(issues, setFieldError) {
28
+ for (const issue of issues) {
29
+ if (!issue.path || issue.path.length === 0)
30
+ continue;
31
+ const key = issue.path.map(String).join(".");
32
+ setFieldError(key, issue.message);
33
+ }
34
+ }
35
+ /** True when at least one issue carries a field path we can attach it to. */
36
+ export function hasAttachableIssue(issues) {
37
+ return Boolean(issues?.some((i) => i.path && i.path.length > 0));
38
+ }
@@ -1,5 +1,6 @@
1
1
  export type { FormShellProviderProps, FormShellStep, FormShellNav, FormShellContextValue, UseFormShellStateResult, } from "./FormShell";
2
- export { FormShellProvider, FormShellContext, useFormShell, useFormShellState, applyZodIssues } from "./FormShell";
2
+ export { FormShellProvider, FormShellContext, useFormShell, useFormShellState } from "./FormShell";
3
+ export { applyZodIssues, hasAttachableIssue } from "./apply-zod-issues";
3
4
  export type { FieldInputProps } from "./FieldInput";
4
5
  export { FieldInput } from "./FieldInput";
5
6
  export type { FieldSelectProps } from "./FieldSelect";
@@ -1,4 +1,5 @@
1
- export { FormShellProvider, FormShellContext, useFormShell, useFormShellState, applyZodIssues } from "./FormShell";
1
+ export { FormShellProvider, FormShellContext, useFormShell, useFormShellState } from "./FormShell";
2
+ export { applyZodIssues, hasAttachableIssue } from "./apply-zod-issues";
2
3
  export { FieldInput } from "./FieldInput";
3
4
  export { FieldSelect } from "./FieldSelect";
4
5
  export { FieldCheckbox } from "./FieldCheckbox";
@@ -50,6 +50,14 @@ export declare const ICONS: {
50
50
  readonly settings: import("react").ForwardRefExoticComponent<Omit<import("lucide-react").LucideProps, "ref"> & import("react").RefAttributes<SVGSVGElement>>;
51
51
  readonly expand: import("react").ForwardRefExoticComponent<Omit<import("lucide-react").LucideProps, "ref"> & import("react").RefAttributes<SVGSVGElement>>;
52
52
  readonly user: import("react").ForwardRefExoticComponent<Omit<import("lucide-react").LucideProps, "ref"> & import("react").RefAttributes<SVGSVGElement>>;
53
+ readonly notification: import("react").ForwardRefExoticComponent<Omit<import("lucide-react").LucideProps, "ref"> & import("react").RefAttributes<SVGSVGElement>>;
54
+ readonly compare: import("react").ForwardRefExoticComponent<Omit<import("lucide-react").LucideProps, "ref"> & import("react").RefAttributes<SVGSVGElement>>;
55
+ readonly help: import("react").ForwardRefExoticComponent<Omit<import("lucide-react").LucideProps, "ref"> & import("react").RefAttributes<SVGSVGElement>>;
56
+ readonly menu: import("react").ForwardRefExoticComponent<Omit<import("lucide-react").LucideProps, "ref"> & import("react").RefAttributes<SVGSVGElement>>;
57
+ readonly close: import("react").ForwardRefExoticComponent<Omit<import("lucide-react").LucideProps, "ref"> & import("react").RefAttributes<SVGSVGElement>>;
58
+ readonly themeLight: import("react").ForwardRefExoticComponent<Omit<import("lucide-react").LucideProps, "ref"> & import("react").RefAttributes<SVGSVGElement>>;
59
+ readonly themeDark: import("react").ForwardRefExoticComponent<Omit<import("lucide-react").LucideProps, "ref"> & import("react").RefAttributes<SVGSVGElement>>;
60
+ readonly deals: import("react").ForwardRefExoticComponent<Omit<import("lucide-react").LucideProps, "ref"> & import("react").RefAttributes<SVGSVGElement>>;
53
61
  };
54
62
  /** Every valid `iconKey`. A typo is a compile error, not a silent blank. */
55
63
  export type IconKey = keyof typeof ICONS;
@@ -39,7 +39,7 @@
39
39
  * @tag consumers:Button,RowActionMenu,BulkActionBar,action-registry
40
40
  * @tag sideEffects:none
41
41
  */
42
- import { Archive, ArrowLeft, Ban, Check, CheckCircle, ChevronDown, Copy, Download, Edit, Eye, EyeOff, FileDown, Filter, Flag, Gavel, Heart, Link as LinkIcon, Lock, Mail, MessageSquare, Package, Pause, Play, Plus, RefreshCw, Reply, RotateCcw, Save, Search, Send, Settings, Share2, ShieldCheck, ShoppingCart, Star, Trash2, TrendingUp, Truck, Upload, User, X, XCircle, } from "lucide-react";
42
+ import { Archive, ArrowLeft, Ban, Bell, Check, CheckCircle, ChevronDown, Copy, Download, Edit, Eye, EyeOff, FileDown, Filter, Flag, Gavel, HelpCircle, Heart, Link as LinkIcon, Lock, Mail, Menu, MessageSquare, Moon, Package, Pause, Play, Plus, RefreshCw, Reply, RotateCcw, Save, Search, Send, Settings, Share2, ShieldCheck, ShoppingCart, Star, Sun, Scale, Tag, Trash2, TrendingUp, Truck, Upload, User, X, XCircle, } from "lucide-react";
43
43
  /**
44
44
  * Semantic name → component.
45
45
  *
@@ -98,6 +98,18 @@ export const ICONS = {
98
98
  settings: Settings,
99
99
  expand: ChevronDown,
100
100
  user: User,
101
+ // Header chrome. Added 2026-08-27 when TitleBarLayout stopped hand-inlining
102
+ // ten raw <svg> elements — a raw <svg> carries no `shrink-0`, so inside a
103
+ // padded flex control it silently shrank rather than rendering at its stated
104
+ // size, and no audit could see a size it never declared.
105
+ notification: Bell,
106
+ compare: Scale,
107
+ help: HelpCircle,
108
+ menu: Menu,
109
+ close: X,
110
+ themeLight: Sun,
111
+ themeDark: Moon,
112
+ deals: Tag,
101
113
  };
102
114
  /**
103
115
  * The ONLY sanctioned glyph sizes.
@@ -1,4 +1,4 @@
1
- import type { ActionResult } from "../core/server-action";
1
+ import type { ActionResult } from "../_internal/shared/types/action-result";
2
2
  export type { ActionResult };
3
3
  type ActionErrorResult = Extract<ActionResult<never>, {
4
4
  ok: false;
@@ -7,6 +7,55 @@
7
7
  export declare const FIREBASE_STORAGE_HOST = "firebasestorage.googleapis.com";
8
8
  /** Google Cloud Storage host (used by public-asset URLs). */
9
9
  export declare const GCS_HOST = "storage.googleapis.com";
10
+ /**
11
+ * The canonical stored form of every media reference: `/media/<slug>`.
12
+ *
13
+ * `POST /api/media/finalize` mints exactly this, `next.config.js` rewrites
14
+ * `/media/:path*` → `/api/media/:path*`, and CLAUDE.md § Media Architecture
15
+ * makes it mandatory ("never write raw firebasestorage.googleapis.com URLs
16
+ * into Firestore"). Exported so validation can name the same prefix the
17
+ * resolver below branches on, instead of re-typing the literal.
18
+ */
19
+ export declare const MEDIA_PROXY_PREFIX = "/media/";
20
+ /** Cap for any stored media reference. Matches the previous schema bound. */
21
+ export declare const MEDIA_URL_MAX_LENGTH = 2048;
22
+ /**
23
+ * Absolute hosts a stored media reference may point at.
24
+ *
25
+ * These exist for BACK-COMPAT only — rows written before the `/media/` proxy,
26
+ * and `seedExtMedia()` fixtures pointing at third-party sample assets. New
27
+ * writes should always be producing `/media/<slug>`. This list was previously
28
+ * duplicated in `appkit/src/validation/schemas.ts` and
29
+ * `src/validation/request-schemas.ts`; it lives here now so there is one copy.
30
+ */
31
+ export declare const APPROVED_MEDIA_DOMAINS: readonly string[];
32
+ export declare const MEDIA_URL_MESSAGE = "Must be a stored media reference (/media/<slug>) or a URL on an approved CDN domain";
33
+ /**
34
+ * Is `value` something we are willing to PERSIST as a media reference?
35
+ *
36
+ * The single definition of that rule — `mediaUrlSchema` in both validation
37
+ * modules refines on it, and everything that stores an image/video URL should
38
+ * go through one of those.
39
+ *
40
+ * Accepts:
41
+ * - `/media/<slug>` — the canonical form (`/api/media/finalize` output);
42
+ * - an absolute URL on an approved host — see `APPROVED_MEDIA_DOMAINS`.
43
+ *
44
+ * Rejects:
45
+ * - `blob:` / `data:` — valid only in the tab that created them. Persisting
46
+ * one stores a reference that is already dead by the time anyone reads it;
47
+ * - `/api/media/ext?url=…` — a RENDER-time transform produced by
48
+ * `resolveMediaUrl` below. Storing it double-wraps on the next render and
49
+ * pins the value to a proxy route rather than to the asset;
50
+ * - anything else, including bare relative paths that are not `/media/`.
51
+ *
52
+ * NOTE this is deliberately a strict SUPERSET of the rule it replaced (which
53
+ * was `.url()` + an approved-host check). That old rule rejected the canonical
54
+ * `/media/<slug>` form outright — which is what made every avatar save fail —
55
+ * while happily accepting the raw Storage URLs CLAUDE.md bans. No value that
56
+ * validated before stops validating now.
57
+ */
58
+ export declare function isStoredMediaRef(value: string): boolean;
10
59
  export interface ResolveMediaUrlOptions {
11
60
  /**
12
61
  * When false, a URL we do not own is returned UNCHANGED instead of being sent
@@ -9,7 +9,78 @@ import { normalizeError } from "../errors/normalize";
9
9
  export const FIREBASE_STORAGE_HOST = "firebasestorage.googleapis.com";
10
10
  /** Google Cloud Storage host (used by public-asset URLs). */
11
11
  export const GCS_HOST = "storage.googleapis.com";
12
- const PROXY_PREFIX = "/media/";
12
+ /**
13
+ * The canonical stored form of every media reference: `/media/<slug>`.
14
+ *
15
+ * `POST /api/media/finalize` mints exactly this, `next.config.js` rewrites
16
+ * `/media/:path*` → `/api/media/:path*`, and CLAUDE.md § Media Architecture
17
+ * makes it mandatory ("never write raw firebasestorage.googleapis.com URLs
18
+ * into Firestore"). Exported so validation can name the same prefix the
19
+ * resolver below branches on, instead of re-typing the literal.
20
+ */
21
+ export const MEDIA_PROXY_PREFIX = "/media/";
22
+ const PROXY_PREFIX = MEDIA_PROXY_PREFIX;
23
+ /** Cap for any stored media reference. Matches the previous schema bound. */
24
+ export const MEDIA_URL_MAX_LENGTH = 2048;
25
+ /**
26
+ * Absolute hosts a stored media reference may point at.
27
+ *
28
+ * These exist for BACK-COMPAT only — rows written before the `/media/` proxy,
29
+ * and `seedExtMedia()` fixtures pointing at third-party sample assets. New
30
+ * writes should always be producing `/media/<slug>`. This list was previously
31
+ * duplicated in `appkit/src/validation/schemas.ts` and
32
+ * `src/validation/request-schemas.ts`; it lives here now so there is one copy.
33
+ */
34
+ export const APPROVED_MEDIA_DOMAINS = [
35
+ FIREBASE_STORAGE_HOST,
36
+ GCS_HOST,
37
+ "res.cloudinary.com",
38
+ "images.unsplash.com",
39
+ ];
40
+ export const MEDIA_URL_MESSAGE = "Must be a stored media reference (/media/<slug>) or a URL on an approved CDN domain";
41
+ /**
42
+ * Is `value` something we are willing to PERSIST as a media reference?
43
+ *
44
+ * The single definition of that rule — `mediaUrlSchema` in both validation
45
+ * modules refines on it, and everything that stores an image/video URL should
46
+ * go through one of those.
47
+ *
48
+ * Accepts:
49
+ * - `/media/<slug>` — the canonical form (`/api/media/finalize` output);
50
+ * - an absolute URL on an approved host — see `APPROVED_MEDIA_DOMAINS`.
51
+ *
52
+ * Rejects:
53
+ * - `blob:` / `data:` — valid only in the tab that created them. Persisting
54
+ * one stores a reference that is already dead by the time anyone reads it;
55
+ * - `/api/media/ext?url=…` — a RENDER-time transform produced by
56
+ * `resolveMediaUrl` below. Storing it double-wraps on the next render and
57
+ * pins the value to a proxy route rather than to the asset;
58
+ * - anything else, including bare relative paths that are not `/media/`.
59
+ *
60
+ * NOTE this is deliberately a strict SUPERSET of the rule it replaced (which
61
+ * was `.url()` + an approved-host check). That old rule rejected the canonical
62
+ * `/media/<slug>` form outright — which is what made every avatar save fail —
63
+ * while happily accepting the raw Storage URLs CLAUDE.md bans. No value that
64
+ * validated before stops validating now.
65
+ */
66
+ export function isStoredMediaRef(value) {
67
+ if (!value || value.length > MEDIA_URL_MAX_LENGTH)
68
+ return false;
69
+ if (value.startsWith(MEDIA_PROXY_PREFIX))
70
+ return true;
71
+ if (value.startsWith("blob:") || value.startsWith("data:"))
72
+ return false;
73
+ if (value.startsWith(MEDIA_ENDPOINTS.EXT))
74
+ return false;
75
+ try {
76
+ const { hostname } = new URL(value);
77
+ return APPROVED_MEDIA_DOMAINS.some((domain) => hostname === domain || hostname.endsWith(`.${domain}`));
78
+ }
79
+ catch (_err) {
80
+ void normalizeError(_err); // not an absolute URL, and not /media/ — reject
81
+ return false;
82
+ }
83
+ }
13
84
  /**
14
85
  * Our own Storage bucket, as it appears as the first path segment of a
15
86
  * `storage.googleapis.com` URL. Only URLs pointing at THIS bucket may have that
@@ -20,8 +20,21 @@ export declare const paginationQuerySchema: z.ZodObject<{
20
20
  }>;
21
21
  export declare const objectIdSchema: z.ZodString;
22
22
  export declare const urlSchema: z.ZodString;
23
- /** Default approved media domains. Extend via `createMediaUrlSchema`. */
24
- export declare const APPROVED_MEDIA_DOMAINS: string[];
23
+ /**
24
+ * A media reference we are willing to PERSIST.
25
+ *
26
+ * The rule lives in `isStoredMediaRef` (`utils/media-url.ts`) so this schema
27
+ * and its zod-4 twin in `src/validation/request-schemas.ts` cannot drift —
28
+ * the two Zod major versions mean the schema object can't be shared, but the
29
+ * predicate can.
30
+ *
31
+ * This used to be `.url()` + an approved-host refine, which rejected the
32
+ * canonical `/media/<slug>` form that `/api/media/finalize` actually mints.
33
+ * That is what made every avatar save fail with a bare "Validation failed".
34
+ *
35
+ * For a genuine EXTERNAL link (a brand website, a social handle, a tracking
36
+ * URL) use `urlSchema` above — not this.
37
+ */
25
38
  export declare const mediaUrlSchema: z.ZodEffects<z.ZodString, string, string>;
26
39
  export declare const dateStringSchema: z.ZodString;
27
40
  /** Discount percentage — 0–100. */
@@ -1,5 +1,4 @@
1
1
  import { z } from "zod";
2
- import { normalizeError } from "../errors/normalize";
3
2
  // ============================================
4
3
  // COMMON SCHEMAS
5
4
  // ============================================
@@ -12,29 +11,26 @@ export const paginationQuerySchema = z.object({
12
11
  });
13
12
  export const objectIdSchema = z.string().regex(/^[a-z0-9-]+$/);
14
13
  export const urlSchema = z.string().url().max(2048);
15
- import { FIREBASE_STORAGE_HOST, GCS_HOST } from "../utils/media-url";
16
- /** Default approved media domains. Extend via `createMediaUrlSchema`. */
17
- export const APPROVED_MEDIA_DOMAINS = [
18
- FIREBASE_STORAGE_HOST,
19
- GCS_HOST,
20
- "res.cloudinary.com",
21
- "images.unsplash.com",
22
- // Add site-specific CDN domains by calling createMediaUrlSchema(extraDomains)
23
- ];
14
+ import { isStoredMediaRef, MEDIA_URL_MAX_LENGTH, MEDIA_URL_MESSAGE, } from "../utils/media-url";
15
+ /**
16
+ * A media reference we are willing to PERSIST.
17
+ *
18
+ * The rule lives in `isStoredMediaRef` (`utils/media-url.ts`) so this schema
19
+ * and its zod-4 twin in `src/validation/request-schemas.ts` cannot drift —
20
+ * the two Zod major versions mean the schema object can't be shared, but the
21
+ * predicate can.
22
+ *
23
+ * This used to be `.url()` + an approved-host refine, which rejected the
24
+ * canonical `/media/<slug>` form that `/api/media/finalize` actually mints.
25
+ * That is what made every avatar save fail with a bare "Validation failed".
26
+ *
27
+ * For a genuine EXTERNAL link (a brand website, a social handle, a tracking
28
+ * URL) use `urlSchema` above — not this.
29
+ */
24
30
  export const mediaUrlSchema = z
25
31
  .string()
26
- .url()
27
- .max(2048)
28
- .refine((url) => {
29
- try {
30
- const { hostname } = new URL(url);
31
- return APPROVED_MEDIA_DOMAINS.some((domain) => hostname === domain || hostname.endsWith(`.${domain}`));
32
- }
33
- catch (_err) {
34
- void normalizeError(_err);
35
- return false;
36
- }
37
- }, { message: "Image or video URL must be hosted on an approved CDN domain" });
32
+ .max(MEDIA_URL_MAX_LENGTH)
33
+ .refine(isStoredMediaRef, { message: MEDIA_URL_MESSAGE });
38
34
  export const dateStringSchema = z.string().datetime();
39
35
  // ============================================
40
36
  // SHARED PRIMITIVES (W2-4 — 2026-05-23)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mohasinac/appkit",
3
- "version": "4.29.0",
3
+ "version": "4.30.0",
4
4
  "license": "MIT",
5
5
  "publishConfig": {
6
6
  "access": "public"
@@ -108,9 +108,17 @@ if (!SERVICE_ACCOUNT_OVERRIDE && missing.length > 0) {
108
108
 
109
109
  // PII keys are mandatory for collections that encrypt fields
110
110
  const PII_REQUIRED = ["users", "addresses", "storeAddresses", "products", "orders", "reviews", "bids", "payouts", "eventEntries"];
111
- const PII_KEY_PRESENT = Boolean(process.env.PII_SECRET || process.env.PII_ENCRYPTION_KEY);
111
+ // PII_ENCRYPTION_KEY ONLY. `PII_SECRET` used to be accepted here as an
112
+ // alternative and is read by no runtime code — so a machine with only that set
113
+ // passed this check and then threw at the first encrypt, mid-load, with some
114
+ // collections already written.
115
+ const PII_KEY_PRESENT = Boolean(process.env.PII_ENCRYPTION_KEY);
112
116
  if (ACTION === "load" && !PII_KEY_PRESENT) {
113
- console.warn("⚠ PII_SECRET / PII_ENCRYPTION_KEY not set — collections with PII fields will fail.");
117
+ // A hard stop, not a warning. Loading without the key does not skip the PII
118
+ // collections — it writes some and then throws, leaving a half-seeded
119
+ // database that looks like a successful run until someone reads it.
120
+ console.error("✖ PII_ENCRYPTION_KEY is not set — users, addresses, orders, reviews, bids, payouts and eventEntries all encrypt fields at write time.");
121
+ process.exit(2);
114
122
  }
115
123
 
116
124
  // ---------------------------------------------------------------------------