@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.
- package/dist/_internal/client/features/layout/SidebarCollapseToggle.js +1 -1
- package/dist/_internal/server/jobs/handlers/_helpers.d.ts +22 -0
- package/dist/_internal/server/jobs/handlers/_helpers.js +25 -1
- package/dist/_internal/shared/features/blog/schema.d.ts +2 -2
- package/dist/_internal/shared/features/blog/schema.js +2 -1
- package/dist/_internal/shared/features/brands/schema.d.ts +2 -2
- package/dist/_internal/shared/features/brands/schema.js +3 -1
- package/dist/_internal/shared/features/categories/bundle-schemas.d.ts +3 -3
- package/dist/_internal/shared/features/categories/bundle-schemas.js +2 -1
- package/dist/_internal/shared/features/events/schema.d.ts +2 -2
- package/dist/_internal/shared/features/events/schema.js +2 -1
- package/dist/_internal/shared/features/products/schema.d.ts +8 -8
- package/dist/_internal/shared/features/products/schema.js +3 -2
- package/dist/_internal/shared/features/reviews/schema.d.ts +1 -1
- package/dist/_internal/shared/features/reviews/schema.js +2 -1
- package/dist/_internal/shared/types/action-result.d.ts +0 -2
- package/dist/client/api/surface-error.d.ts +9 -0
- package/dist/client/api/surface-error.js +29 -3
- package/dist/client/api/useApiMutation.d.ts +10 -0
- package/dist/client/api/useApiMutation.js +2 -1
- package/dist/core/server-action.d.ts +3 -14
- package/dist/core/server-action.js +11 -2
- package/dist/errors/error-envelope.d.ts +73 -0
- package/dist/errors/error-envelope.js +96 -0
- package/dist/errors/error-handler.js +14 -7
- package/dist/features/account/components/NotificationBell.js +1 -1
- package/dist/features/account/components/UserSidebar.js +2 -2
- package/dist/features/account/components/UserSupportView.js +2 -6
- package/dist/features/account/hooks/useProfile.d.ts +6 -0
- package/dist/features/account/hooks/useProfile.js +1 -0
- package/dist/features/admin/components/AdminAdEditorView.js +1 -3
- package/dist/features/admin/components/AdminAddressClustersView.js +1 -3
- package/dist/features/admin/components/AdminAddressEditorView.js +2 -4
- package/dist/features/admin/components/AdminAddressesView.js +1 -3
- package/dist/features/admin/components/AdminAllEventEntriesView.js +1 -3
- package/dist/features/admin/components/AdminBidsView.js +1 -3
- package/dist/features/admin/components/AdminBlogEditorView.js +2 -2
- package/dist/features/admin/components/AdminBrandEditorView.js +4 -5
- package/dist/features/admin/components/AdminCarouselGroupEditorView.js +1 -3
- package/dist/features/admin/components/AdminCategoryEditorView.js +2 -4
- package/dist/features/admin/components/AdminContactEditorView.js +1 -3
- package/dist/features/admin/components/AdminContactView.js +2 -6
- package/dist/features/admin/components/AdminCouponEditorView.js +2 -4
- package/dist/features/admin/components/AdminEmployeeEditorView.js +3 -9
- package/dist/features/admin/components/AdminFaqEditorView.js +2 -4
- package/dist/features/admin/components/AdminFeatureEditorView.js +0 -9
- package/dist/features/admin/components/AdminFeatureFlagsView.js +2 -4
- package/dist/features/admin/components/AdminNavEditorView.js +1 -3
- package/dist/features/admin/components/AdminNavigationView.js +3 -3
- package/dist/features/admin/components/AdminNewsletterView.js +1 -3
- package/dist/features/admin/components/AdminNotificationsView.js +2 -6
- package/dist/features/admin/components/AdminOffersView.js +1 -3
- package/dist/features/admin/components/AdminOrderEditorView.js +1 -3
- package/dist/features/admin/components/AdminPaymentClustersView.js +1 -3
- package/dist/features/admin/components/AdminPaymentMethodsView.js +1 -3
- package/dist/features/admin/components/AdminPayoutMarkPaidModal.js +2 -4
- package/dist/features/admin/components/AdminProductEditorView.js +2 -4
- package/dist/features/admin/components/AdminReturnRequestsView.js +2 -6
- package/dist/features/admin/components/AdminReviewsView.js +2 -6
- package/dist/features/admin/components/AdminScammerEditorView.js +2 -6
- package/dist/features/admin/components/AdminSectionsView.js +2 -6
- package/dist/features/admin/components/AdminSessionsView.js +1 -3
- package/dist/features/admin/components/AdminSidebar.js +2 -2
- package/dist/features/admin/components/AdminSiteSettingsView.js +1 -1
- package/dist/features/admin/components/AdminStoreEditorView.js +1 -3
- package/dist/features/admin/components/AdminStoresView.js +2 -6
- package/dist/features/admin/components/AdminSublistingCategoryEditorView.js +2 -6
- package/dist/features/admin/components/AdminSupportTicketDetailView.js +7 -21
- package/dist/features/admin/components/AdminTesterChecklistItemEditorView.js +2 -4
- package/dist/features/admin/components/AdminUserEditorView.js +6 -18
- package/dist/features/admin/components/AdminUsersView.js +2 -6
- package/dist/features/admin/components/BrandQuickCreateForm.js +2 -4
- package/dist/features/admin/components/CategoryQuickCreateForm.js +2 -4
- package/dist/features/auctions/components/PlaceBidFormClient.js +1 -1
- package/dist/features/auth/components/ForgotPasswordView.js +1 -1
- package/dist/features/auth/components/LoginForm.js +1 -1
- package/dist/features/auth/components/RegisterForm.js +1 -1
- package/dist/features/auth/components/ResetPasswordView.js +1 -1
- package/dist/features/auth/components/SocialAuthButtons.js +1 -1
- package/dist/features/auth/repository/session.repository.js +13 -1
- package/dist/features/blog/actions/blog-actions.d.ts +14 -14
- package/dist/features/blog/actions/blog-actions.js +6 -8
- package/dist/features/blog/schemas/index.d.ts +1 -1
- package/dist/features/blog/schemas/index.js +2 -4
- package/dist/features/categories/api/[id]/route.js +2 -1
- package/dist/features/categories/api/route.js +2 -1
- package/dist/features/events/schemas/index.d.ts +1 -1
- package/dist/features/events/schemas/index.js +2 -4
- package/dist/features/faq/components/ContactCTA.js +1 -1
- package/dist/features/filters/AsyncFacetSection.js +1 -1
- package/dist/features/filters/FilterFacetSection.js +1 -1
- package/dist/features/filters/RangeFilter.js +1 -1
- package/dist/features/filters/SwitchFilter.js +1 -1
- package/dist/features/homepage/api/carousel/[id]/route.js +4 -3
- package/dist/features/homepage/api/carousel/route.js +4 -3
- package/dist/features/homepage/api/route.js +3 -2
- package/dist/features/homepage/components/GoogleReviewsSection.js +1 -1
- package/dist/features/homepage/components/HeroBanner.js +1 -1
- package/dist/features/homepage/components/HeroCarousel.js +1 -1
- package/dist/features/homepage/components/SocialPostCard.js +1 -1
- package/dist/features/homepage/components/WhatsAppCommunitySection.js +1 -1
- package/dist/features/layout/AppLayoutShell.js +4 -4
- package/dist/features/layout/BottomNavbar.js +2 -2
- package/dist/features/layout/MainNavbar.js +1 -1
- package/dist/features/layout/TitleBarLayout.js +24 -12
- package/dist/features/media/AvatarUpload.js +28 -10
- package/dist/features/media/modals/ImageCropModal.js +1 -1
- package/dist/features/media/modals/ImageEditor.js +1 -1
- package/dist/features/products/components/MakeOfferButton.js +1 -1
- package/dist/features/reviews/api/[id]/route.js +3 -2
- package/dist/features/reviews/api/route.js +3 -2
- package/dist/features/search/components/Search.js +2 -2
- package/dist/features/seller/components/SellerAnalyticsAlertsView.js +3 -3
- package/dist/features/seller/components/SellerSidebar.js +2 -2
- package/dist/features/store-extensions/components/ReviewDecisionModal.js +1 -1
- package/dist/features/stores/repository/store.repository.d.ts +21 -0
- package/dist/features/stores/repository/store.repository.js +32 -0
- package/dist/features/whatsapp-bot/components/SellerWhatsAppSettingsView.js +0 -2
- package/dist/index.d.ts +3 -2
- package/dist/index.js +4 -1
- package/dist/next/api/api-response.d.ts +29 -12
- package/dist/next/api/api-response.js +9 -8
- package/dist/next/api/apiHandler.d.ts +6 -1
- package/dist/next/api/apiHandler.js +13 -1
- package/dist/next/api/routeHandler.js +6 -6
- package/dist/next/middleware/pii-scrubber.js +5 -2
- package/dist/providers/db-firebase/base.repository.d.ts +14 -0
- package/dist/providers/db-firebase/base.repository.js +22 -0
- package/dist/providers/db-firebase/sieve.d.ts +14 -1
- package/dist/providers/db-firebase/sieve.js +3 -1
- package/dist/security/pii-encrypt.d.ts +0 -4
- package/dist/security/pii-encrypt.js +79 -13
- package/dist/security/pii-mask.d.ts +39 -1
- package/dist/security/pii-mask.js +33 -4
- package/dist/security/pii-redact.d.ts +0 -13
- package/dist/security/pii-redact.js +3 -1
- package/dist/security/pii-schemas.d.ts +0 -2
- package/dist/security/pii-schemas.js +16 -2
- package/dist/security/settings-encryption.js +31 -3
- package/dist/seed/stores-seed-data.js +18 -0
- package/dist/seed/test-utils.js +6 -2
- package/dist/ui/DataTable.js +2 -2
- package/dist/ui/components/CollapsibleSection.js +1 -1
- package/dist/ui/components/ImageGallery.js +1 -1
- package/dist/ui/components/ListingLayout.js +1 -1
- package/dist/ui/forms/FormShell.d.ts +0 -19
- package/dist/ui/forms/FormShell.js +4 -23
- package/dist/ui/forms/apply-zod-issues.d.ts +34 -0
- package/dist/ui/forms/apply-zod-issues.js +38 -0
- package/dist/ui/forms/index.d.ts +2 -1
- package/dist/ui/forms/index.js +2 -1
- package/dist/ui/icons/icon-registry.d.ts +8 -0
- package/dist/ui/icons/icon-registry.js +13 -1
- package/dist/utils/action-response.d.ts +1 -1
- package/dist/utils/media-url.d.ts +49 -0
- package/dist/utils/media-url.js +72 -1
- package/dist/validation/schemas.d.ts +15 -2
- package/dist/validation/schemas.js +18 -22
- package/package.json +1 -1
- 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
|
+
}
|
package/dist/ui/forms/index.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
export type { FormShellProviderProps, FormShellStep, FormShellNav, FormShellContextValue, UseFormShellStateResult, } from "./FormShell";
|
|
2
|
-
export { FormShellProvider, FormShellContext, useFormShell, useFormShellState
|
|
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";
|
package/dist/ui/forms/index.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
export { FormShellProvider, FormShellContext, useFormShell, useFormShellState
|
|
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.
|
|
@@ -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
|
package/dist/utils/media-url.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
/**
|
|
24
|
-
|
|
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 {
|
|
16
|
-
/**
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
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
|
-
.
|
|
27
|
-
.
|
|
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
package/scripts/seed-cli.mjs
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
// ---------------------------------------------------------------------------
|