@mohasinac/appkit 4.41.5 → 4.41.7

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.
@@ -1,5 +1,6 @@
1
1
  "use client";
2
2
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { IMAGE_ACCEPT_ATTR } from "../../../shared/media/limits";
3
4
  import { useCallback, useEffect, useState } from "react";
4
5
  import { normalizeError } from "../../../../errors/normalize";
5
6
  import { Alert, Button, Div, Row, Span, Stack, Text } from "../../../../ui";
@@ -63,5 +64,5 @@ export function DigitalContentPoolManager({ productId, api, canUploadFiles = fal
63
64
  await api.addCodes(productId, codes);
64
65
  setBulk("");
65
66
  });
66
- }, children: "Add codes" }), _jsx(FilePickerButton, { variant: "secondary", size: "sm", disabled: busy, accept: canUploadFiles ? undefined : "image/*", onFile: (file) => void run(() => api.addAsset(productId, file)), children: canUploadFiles ? "Upload QR or file" : "Upload QR image" })] }), _jsx(Div, { children: entries === null ? (_jsx(Text, { size: "sm", color: "muted", children: "Loading\u2026" })) : entries.length === 0 ? (_jsx(Text, { size: "sm", color: "muted", children: "Nothing in the pool yet." })) : (_jsx(Stack, { gap: "xs", children: entries.map((e) => (_jsxs(Row, { gap: "sm", align: "center", justify: "between", children: [_jsxs(Text, { size: "sm", children: [KIND_LABEL[e.contentKind], e.fileName ? ` — ${e.fileName}` : "", _jsxs(Span, { color: "muted", children: [" \u00B7 ", e.status] })] }), e.status !== "claimed" && (_jsx(Button, { type: "button", variant: "ghost", size: "sm", disabled: busy, onClick: () => void run(() => api.remove(productId, e.id)), children: "Remove" }))] }, e.id))) })) })] }));
67
+ }, children: "Add codes" }), _jsx(FilePickerButton, { variant: "secondary", size: "sm", disabled: busy, accept: canUploadFiles ? undefined : IMAGE_ACCEPT_ATTR, onFile: (file) => void run(() => api.addAsset(productId, file)), children: canUploadFiles ? "Upload QR or file" : "Upload QR image" })] }), _jsx(Div, { children: entries === null ? (_jsx(Text, { size: "sm", color: "muted", children: "Loading\u2026" })) : entries.length === 0 ? (_jsx(Text, { size: "sm", color: "muted", children: "Nothing in the pool yet." })) : (_jsx(Stack, { gap: "xs", children: entries.map((e) => (_jsxs(Row, { gap: "sm", align: "center", justify: "between", children: [_jsxs(Text, { size: "sm", children: [KIND_LABEL[e.contentKind], e.fileName ? ` — ${e.fileName}` : "", _jsxs(Span, { color: "muted", children: [" \u00B7 ", e.status] })] }), e.status !== "claimed" && (_jsx(Button, { type: "button", variant: "ghost", size: "sm", disabled: busy, onClick: () => void run(() => api.remove(productId, e.id)), children: "Remove" }))] }, e.id))) })) })] }));
67
68
  }
@@ -0,0 +1,46 @@
1
+ import type { LotteryConfig, ClientLotteryConfig } from "../../../../features/lottery/types";
2
+ /** A spin prize as a public visitor may see it: what it is, not what it is worth. */
3
+ export interface ClientSpinPrize {
4
+ id: string;
5
+ label: string;
6
+ isActive: boolean;
7
+ }
8
+ /** Allow-list, not a spread: `weight` and `couponId` are deliberately absent. */
9
+ export declare function toClientSpinPrize(prize: {
10
+ id: string;
11
+ label: string;
12
+ isActive?: boolean;
13
+ }): ClientSpinPrize;
14
+ /**
15
+ * The three fields this projection touches; every other field passes through.
16
+ *
17
+ * 🛑 `lotteryConfig` accepts EITHER shape, and that is not laxness — it is the
18
+ * reason this leak survived review. `EventItem.lotteryConfig` is declared as
19
+ * `ClientLotteryConfig`, i.e. the already-projected type, while the repository
20
+ * hands back the STORED document with `price`, `weight` and `bookedByUserId`
21
+ * still on every slot. The type asserted the projection had happened; nothing
22
+ * had performed it. Accepting both means the adapter can be applied to a value
23
+ * whose type is lying, which is precisely where it is needed.
24
+ */
25
+ export interface PublicEventInput {
26
+ createdBy?: string;
27
+ lotteryConfig?: LotteryConfig | ClientLotteryConfig;
28
+ spinPrizes?: {
29
+ id: string;
30
+ label: string;
31
+ isActive?: boolean;
32
+ }[];
33
+ }
34
+ /** What a caller gets back: `createdBy` gone, the two nested shapes narrowed. */
35
+ export type PublicEvent<T extends PublicEventInput> = Omit<T, "createdBy" | "lotteryConfig" | "spinPrizes"> & {
36
+ lotteryConfig?: ClientLotteryConfig;
37
+ spinPrizes?: ClientSpinPrize[];
38
+ };
39
+ /**
40
+ * Project one stored event into its public shape.
41
+ *
42
+ * The return TYPE is narrowed too, not just the runtime value — so a caller
43
+ * that goes looking for `slot.weight` on the result gets a compile error rather
44
+ * than `undefined` at runtime, and the next projection bug is caught by tsc.
45
+ */
46
+ export declare function toPublicEvent<T extends PublicEventInput>(event: T): PublicEvent<T>;
@@ -0,0 +1,51 @@
1
+ /**
2
+ * Events adapters — the public projection for an event document.
3
+ *
4
+ * 🛑 An event is published by two separate routes (appkit's own `eventIdGET`
5
+ * and the consumer's `/api/events` list), and both previously built their
6
+ * payload by SPREADING the stored document and deleting `createdBy`. A
7
+ * deny-list keyed on a handful of remembered field names publishes everything
8
+ * nobody thought to delete — see § Public Data Projections and Root Cause #70.
9
+ *
10
+ * Two nested structures on `EventDocument` carry fields that decide outcomes
11
+ * and must never leave the server:
12
+ *
13
+ * - `lotteryConfig.slots[]` — per-slot `price` and `weight` (the weighting IS
14
+ * the odds), plus the internal `bookedByUserId`.
15
+ * - `spinPrizes[]` — per-prize `weight` (again, the odds) and
16
+ * `couponId` (an internal coupon document id).
17
+ *
18
+ * `SpinWheelView` reads `id`, `label` and `isActive` and nothing else, so
19
+ * dropping the other two costs the client nothing. The prize a spin actually
20
+ * wins is resolved server-side in `assignSpinPrize` and returned as a coupon
21
+ * CODE on the entry, never as the coupon's document id.
22
+ *
23
+ * Both routes call `toPublicEvent` so the two can no longer disagree
24
+ * (Root Cause #75 — a rule reachable by copy-paste gets copied, and the copy
25
+ * never learns about the branch added later).
26
+ */
27
+ import { toClientLotteryConfig } from "../lottery/adapters";
28
+ /** Allow-list, not a spread: `weight` and `couponId` are deliberately absent. */
29
+ export function toClientSpinPrize(prize) {
30
+ return {
31
+ id: prize.id,
32
+ label: prize.label,
33
+ isActive: prize.isActive !== false,
34
+ };
35
+ }
36
+ /**
37
+ * Project one stored event into its public shape.
38
+ *
39
+ * The return TYPE is narrowed too, not just the runtime value — so a caller
40
+ * that goes looking for `slot.weight` on the result gets a compile error rather
41
+ * than `undefined` at runtime, and the next projection bug is caught by tsc.
42
+ */
43
+ export function toPublicEvent(event) {
44
+ const { createdBy: _createdBy, lotteryConfig, spinPrizes, ...rest } = event;
45
+ const out = rest;
46
+ if (lotteryConfig)
47
+ out.lotteryConfig = toClientLotteryConfig(lotteryConfig);
48
+ if (Array.isArray(spinPrizes))
49
+ out.spinPrizes = spinPrizes.map(toClientSpinPrize);
50
+ return out;
51
+ }
@@ -3,7 +3,13 @@
3
3
  * before any slot/config data leaves the server.
4
4
  */
5
5
  import type { LotteryConfig, LotterySlot, ClientLotteryConfig, ClientLotterySlot } from "../../../../features/lottery/types";
6
- /** Strip price and weight from a single slot for client consumption. */
7
- export declare function toClientLotterySlot(slot: LotterySlot): ClientLotterySlot;
6
+ /**
7
+ * Strip price and weight from a single slot for client consumption.
8
+ *
9
+ * Takes the loose shape rather than `LotterySlot` so it can be applied to a
10
+ * value already TYPED as projected but not actually projected — which is the
11
+ * state `EventItem.lotteryConfig` is in when it comes off the repository.
12
+ */
13
+ export declare function toClientLotterySlot(slot: LotterySlot | ClientLotterySlot): ClientLotterySlot;
8
14
  /** Strip price/weight from the entire config for client consumption. */
9
- export declare function toClientLotteryConfig(config: LotteryConfig): ClientLotteryConfig;
15
+ export declare function toClientLotteryConfig(config: LotteryConfig | ClientLotteryConfig): ClientLotteryConfig;
@@ -1,4 +1,10 @@
1
- /** Strip price and weight from a single slot for client consumption. */
1
+ /**
2
+ * Strip price and weight from a single slot for client consumption.
3
+ *
4
+ * Takes the loose shape rather than `LotterySlot` so it can be applied to a
5
+ * value already TYPED as projected but not actually projected — which is the
6
+ * state `EventItem.lotteryConfig` is in when it comes off the repository.
7
+ */
2
8
  export function toClientLotterySlot(slot) {
3
9
  // Allow-list, not a spread: every field named here is a deliberate public
4
10
  // exposure with a known reader (§ Public Data Projections). `image` is read
@@ -18,6 +18,17 @@ export declare function assertPrizeDrawNotLocked(product: Pick<ProductDocument,
18
18
  * seller from swapping in a cheaper item after a buyer has already won.
19
19
  */
20
20
  export declare function assertPrizeDrawWonItemsImmutable(currentProduct: Pick<ProductDocument, "prizeDrawItems">, input: unknown): void;
21
+ /**
22
+ * Anti-scam guard — an auction's economic terms (starting bid, reserve, close
23
+ * time) stop being editable the moment a real bid exists. Moving the goalposts
24
+ * under a live bidder is the one auction edit that must never pass.
25
+ *
26
+ * 🛑 It compares VALUES, not key presence. The seller editor round-trips the
27
+ * whole draft, so every save resubmits `startingBid` and `auctionEndDate`
28
+ * unchanged — rejecting on presence alone would make a live auction entirely
29
+ * unsaveable, and the seller could no longer fix so much as a typo.
30
+ */
31
+ export declare function assertAuctionTermsMutable(existing: Pick<ProductDocument, "listingType" | "bidsHaveStarted" | "bidCount" | "startingBid" | "reservePrice" | "auctionEndDate">, input: unknown): void;
21
32
  /** Assert the product has available stock for a purchase. */
22
33
  export declare function assertInStock(product: ProductDocument, quantity?: number): void;
23
34
  /** Derive effective price (respects currentBid for auctions, base price otherwise). */
@@ -66,6 +66,43 @@ export function assertPrizeDrawWonItemsImmutable(currentProduct, input) {
66
66
  }
67
67
  }
68
68
  }
69
+ /**
70
+ * Anti-scam guard — an auction's economic terms (starting bid, reserve, close
71
+ * time) stop being editable the moment a real bid exists. Moving the goalposts
72
+ * under a live bidder is the one auction edit that must never pass.
73
+ *
74
+ * 🛑 It compares VALUES, not key presence. The seller editor round-trips the
75
+ * whole draft, so every save resubmits `startingBid` and `auctionEndDate`
76
+ * unchanged — rejecting on presence alone would make a live auction entirely
77
+ * unsaveable, and the seller could no longer fix so much as a typo.
78
+ */
79
+ export function assertAuctionTermsMutable(existing, input) {
80
+ if ((existing.listingType ?? "standard") !== "auction")
81
+ return;
82
+ const bidding = existing.bidsHaveStarted === true || (existing.bidCount ?? 0) > 0;
83
+ if (!bidding)
84
+ return;
85
+ if (typeof input !== "object" || input === null)
86
+ return;
87
+ const patch = input;
88
+ const time = (v) => {
89
+ if (v == null)
90
+ return null;
91
+ const d = v instanceof Date ? v : new Date(v);
92
+ return Number.isNaN(d.getTime()) ? null : d.getTime();
93
+ };
94
+ const changed = [];
95
+ if ("startingBid" in patch && patch.startingBid !== existing.startingBid)
96
+ changed.push("starting bid");
97
+ if ("reservePrice" in patch && patch.reservePrice !== existing.reservePrice)
98
+ changed.push("reserve price");
99
+ if ("auctionEndDate" in patch &&
100
+ time(patch.auctionEndDate) !== time(existing.auctionEndDate))
101
+ changed.push("end date");
102
+ if (changed.length > 0) {
103
+ throw new ValidationError(`Bidding has already started on this auction, so its ${changed.join(" and ")} can no longer be changed. Everything else on the listing is still editable.`);
104
+ }
105
+ }
69
106
  /** Assert the product has available stock for a purchase. */
70
107
  export function assertInStock(product, quantity = 1) {
71
108
  if ((product.availableQuantity ?? 0) < quantity) {
@@ -1527,6 +1527,17 @@ export const ACTIONS = {
1527
1527
  description: "Approve a return request and initiate the refund flow.",
1528
1528
  kind: "primary",
1529
1529
  permissions: ["admin", "moderator"],
1530
+ // Required by Rule #7 — approving starts a real refund, which is money
1531
+ // leaving the platform and is not undoable from this screen. Its absence
1532
+ // also CRASHED /admin/return-requests outright: the view reads
1533
+ // `confirmation!.title`, so a missing block threw "Cannot read properties
1534
+ // of undefined (reading 'title')" during render and the page never painted.
1535
+ confirmation: {
1536
+ title: "Approve return request?",
1537
+ body: "The order will be marked Refunded, the buyer will be notified, and the refund process will begin. This cannot be undone from here.",
1538
+ confirmLabel: "Approve return",
1539
+ confirmKind: "primary",
1540
+ },
1530
1541
  },
1531
1542
  "reject-return": {
1532
1543
  iconKey: "reject",
@@ -1,181 +1,20 @@
1
1
  import { z } from "zod";
2
- export declare const productInputSchema: z.ZodObject<{
3
- title: z.ZodString;
4
- description: z.ZodOptional<z.ZodString>;
5
- category: z.ZodString;
6
- brandSlug: z.ZodOptional<z.ZodString>;
7
- price: z.ZodNumber;
8
- currency: z.ZodDefault<z.ZodString>;
9
- stockQuantity: z.ZodDefault<z.ZodNumber>;
10
- availableQuantity: z.ZodDefault<z.ZodNumber>;
11
- mainImage: z.ZodEffects<z.ZodString, string, string>;
12
- images: z.ZodDefault<z.ZodArray<z.ZodEffects<z.ZodString, string, string>, "many">>;
13
- condition: z.ZodOptional<z.ZodEnum<["new", "like_new", "good", "fair", "poor", "used", "refurbished", "broken"]>>;
14
- tags: z.ZodDefault<z.ZodArray<z.ZodString, "many">>;
15
- seoTitle: z.ZodOptional<z.ZodString>;
16
- seoDescription: z.ZodOptional<z.ZodString>;
17
- seoKeywords: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
18
- shippingInfo: z.ZodOptional<z.ZodString>;
19
- returnPolicy: z.ZodOptional<z.ZodString>;
20
- finalSale: z.ZodOptional<z.ZodBoolean>;
21
- shippingPaidBy: z.ZodOptional<z.ZodEnum<["seller", "buyer"]>>;
22
- insurance: z.ZodOptional<z.ZodBoolean>;
23
- insuranceCost: z.ZodOptional<z.ZodNumber>;
24
- allowOffers: z.ZodOptional<z.ZodBoolean>;
25
- minOfferPercent: z.ZodOptional<z.ZodNumber>;
26
- features: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
2
+ export declare const productInputSchema: z.ZodObject<{} & {
3
+ [x: string]: z.ZodOptional<z.ZodUnknown>;
27
4
  }, "strip", z.ZodTypeAny, {
28
- currency: string;
29
- title: string;
30
- price: number;
31
- category: string;
32
- stockQuantity: number;
33
- availableQuantity: number;
34
- mainImage: string;
35
- images: string[];
36
- tags: string[];
37
- brandSlug?: string | undefined;
38
- features?: string[] | undefined;
39
- description?: string | undefined;
40
- shippingInfo?: string | undefined;
41
- returnPolicy?: string | undefined;
42
- condition?: "new" | "used" | "refurbished" | "like_new" | "good" | "fair" | "poor" | "broken" | undefined;
43
- insurance?: boolean | undefined;
44
- insuranceCost?: number | undefined;
45
- shippingPaidBy?: "buyer" | "seller" | undefined;
46
- seoTitle?: string | undefined;
47
- seoDescription?: string | undefined;
48
- seoKeywords?: string[] | undefined;
49
- finalSale?: boolean | undefined;
50
- allowOffers?: boolean | undefined;
51
- minOfferPercent?: number | undefined;
5
+ [x: string]: unknown;
52
6
  }, {
53
- title: string;
54
- price: number;
55
- category: string;
56
- mainImage: string;
57
- currency?: string | undefined;
58
- brandSlug?: string | undefined;
59
- stockQuantity?: number | undefined;
60
- features?: string[] | undefined;
61
- description?: string | undefined;
62
- availableQuantity?: number | undefined;
63
- images?: string[] | undefined;
64
- tags?: string[] | undefined;
65
- shippingInfo?: string | undefined;
66
- returnPolicy?: string | undefined;
67
- condition?: "new" | "used" | "refurbished" | "like_new" | "good" | "fair" | "poor" | "broken" | undefined;
68
- insurance?: boolean | undefined;
69
- insuranceCost?: number | undefined;
70
- shippingPaidBy?: "buyer" | "seller" | undefined;
71
- seoTitle?: string | undefined;
72
- seoDescription?: string | undefined;
73
- seoKeywords?: string[] | undefined;
74
- finalSale?: boolean | undefined;
75
- allowOffers?: boolean | undefined;
76
- minOfferPercent?: number | undefined;
7
+ [x: string]: unknown;
77
8
  }>;
78
9
  export declare const productUpdateSchema: z.ZodObject<{
79
- title: z.ZodOptional<z.ZodString>;
80
- description: z.ZodOptional<z.ZodOptional<z.ZodString>>;
81
- category: z.ZodOptional<z.ZodString>;
82
- brandSlug: z.ZodOptional<z.ZodOptional<z.ZodString>>;
83
- price: z.ZodOptional<z.ZodNumber>;
84
- currency: z.ZodOptional<z.ZodDefault<z.ZodString>>;
85
- stockQuantity: z.ZodOptional<z.ZodDefault<z.ZodNumber>>;
86
- availableQuantity: z.ZodOptional<z.ZodDefault<z.ZodNumber>>;
87
- mainImage: z.ZodOptional<z.ZodEffects<z.ZodString, string, string>>;
88
- images: z.ZodOptional<z.ZodDefault<z.ZodArray<z.ZodEffects<z.ZodString, string, string>, "many">>>;
89
- condition: z.ZodOptional<z.ZodOptional<z.ZodEnum<["new", "like_new", "good", "fair", "poor", "used", "refurbished", "broken"]>>>;
90
- tags: z.ZodOptional<z.ZodDefault<z.ZodArray<z.ZodString, "many">>>;
91
- seoTitle: z.ZodOptional<z.ZodOptional<z.ZodString>>;
92
- seoDescription: z.ZodOptional<z.ZodOptional<z.ZodString>>;
93
- seoKeywords: z.ZodOptional<z.ZodOptional<z.ZodArray<z.ZodString, "many">>>;
94
- shippingInfo: z.ZodOptional<z.ZodOptional<z.ZodString>>;
95
- returnPolicy: z.ZodOptional<z.ZodOptional<z.ZodString>>;
96
- finalSale: z.ZodOptional<z.ZodOptional<z.ZodBoolean>>;
97
- shippingPaidBy: z.ZodOptional<z.ZodOptional<z.ZodEnum<["seller", "buyer"]>>>;
98
- insurance: z.ZodOptional<z.ZodOptional<z.ZodBoolean>>;
99
- insuranceCost: z.ZodOptional<z.ZodOptional<z.ZodNumber>>;
100
- allowOffers: z.ZodOptional<z.ZodOptional<z.ZodBoolean>>;
101
- minOfferPercent: z.ZodOptional<z.ZodOptional<z.ZodNumber>>;
102
- features: z.ZodOptional<z.ZodOptional<z.ZodArray<z.ZodString, "many">>>;
10
+ [x: string]: z.ZodOptional<z.ZodOptional<z.ZodUnknown>>;
103
11
  }, "strip", z.ZodTypeAny, {
104
- currency?: string | undefined;
105
- title?: string | undefined;
106
- price?: number | undefined;
107
- category?: string | undefined;
108
- brandSlug?: string | undefined;
109
- stockQuantity?: number | undefined;
110
- features?: string[] | undefined;
111
- description?: string | undefined;
112
- availableQuantity?: number | undefined;
113
- mainImage?: string | undefined;
114
- images?: string[] | undefined;
115
- tags?: string[] | undefined;
116
- shippingInfo?: string | undefined;
117
- returnPolicy?: string | undefined;
118
- condition?: "new" | "used" | "refurbished" | "like_new" | "good" | "fair" | "poor" | "broken" | undefined;
119
- insurance?: boolean | undefined;
120
- insuranceCost?: number | undefined;
121
- shippingPaidBy?: "buyer" | "seller" | undefined;
122
- seoTitle?: string | undefined;
123
- seoDescription?: string | undefined;
124
- seoKeywords?: string[] | undefined;
125
- finalSale?: boolean | undefined;
126
- allowOffers?: boolean | undefined;
127
- minOfferPercent?: number | undefined;
12
+ [x: string]: unknown;
128
13
  }, {
129
- currency?: string | undefined;
130
- title?: string | undefined;
131
- price?: number | undefined;
132
- category?: string | undefined;
133
- brandSlug?: string | undefined;
134
- stockQuantity?: number | undefined;
135
- features?: string[] | undefined;
136
- description?: string | undefined;
137
- availableQuantity?: number | undefined;
138
- mainImage?: string | undefined;
139
- images?: string[] | undefined;
140
- tags?: string[] | undefined;
141
- shippingInfo?: string | undefined;
142
- returnPolicy?: string | undefined;
143
- condition?: "new" | "used" | "refurbished" | "like_new" | "good" | "fair" | "poor" | "broken" | undefined;
144
- insurance?: boolean | undefined;
145
- insuranceCost?: number | undefined;
146
- shippingPaidBy?: "buyer" | "seller" | undefined;
147
- seoTitle?: string | undefined;
148
- seoDescription?: string | undefined;
149
- seoKeywords?: string[] | undefined;
150
- finalSale?: boolean | undefined;
151
- allowOffers?: boolean | undefined;
152
- minOfferPercent?: number | undefined;
14
+ [x: string]: unknown;
153
15
  }>;
154
16
  export declare const auctionInputSchema: z.ZodObject<{
155
- title: z.ZodString;
156
- description: z.ZodOptional<z.ZodString>;
157
- category: z.ZodString;
158
- brandSlug: z.ZodOptional<z.ZodString>;
159
- price: z.ZodNumber;
160
- currency: z.ZodDefault<z.ZodString>;
161
- stockQuantity: z.ZodDefault<z.ZodNumber>;
162
- availableQuantity: z.ZodDefault<z.ZodNumber>;
163
- mainImage: z.ZodEffects<z.ZodString, string, string>;
164
- images: z.ZodDefault<z.ZodArray<z.ZodEffects<z.ZodString, string, string>, "many">>;
165
- condition: z.ZodOptional<z.ZodEnum<["new", "like_new", "good", "fair", "poor", "used", "refurbished", "broken"]>>;
166
- tags: z.ZodDefault<z.ZodArray<z.ZodString, "many">>;
167
- seoTitle: z.ZodOptional<z.ZodString>;
168
- seoDescription: z.ZodOptional<z.ZodString>;
169
- seoKeywords: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
170
- shippingInfo: z.ZodOptional<z.ZodString>;
171
- returnPolicy: z.ZodOptional<z.ZodString>;
172
- finalSale: z.ZodOptional<z.ZodBoolean>;
173
- shippingPaidBy: z.ZodOptional<z.ZodEnum<["seller", "buyer"]>>;
174
- insurance: z.ZodOptional<z.ZodBoolean>;
175
- insuranceCost: z.ZodOptional<z.ZodNumber>;
176
- allowOffers: z.ZodOptional<z.ZodBoolean>;
177
- minOfferPercent: z.ZodOptional<z.ZodNumber>;
178
- features: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
17
+ [x: string]: z.ZodOptional<z.ZodUnknown>;
179
18
  } & {
180
19
  listingType: z.ZodLiteral<"auction">;
181
20
  startingBid: z.ZodNumber;
@@ -187,99 +26,30 @@ export declare const auctionInputSchema: z.ZodObject<{
187
26
  auctionExtensionMinutes: z.ZodOptional<z.ZodNumber>;
188
27
  auctionShippingPaidBy: z.ZodOptional<z.ZodEnum<["seller", "winner"]>>;
189
28
  }, "strip", z.ZodTypeAny, {
190
- currency: string;
191
- title: string;
192
- price: number;
193
- listingType: "auction";
194
- category: string;
195
- stockQuantity: number;
196
- availableQuantity: number;
197
- mainImage: string;
198
- images: string[];
199
- tags: string[];
200
- auctionEndDate: string;
201
- startingBid: number;
202
- autoExtendable: boolean;
203
- brandSlug?: string | undefined;
204
- features?: string[] | undefined;
205
- description?: string | undefined;
206
- shippingInfo?: string | undefined;
207
- returnPolicy?: string | undefined;
208
- condition?: "new" | "used" | "refurbished" | "like_new" | "good" | "fair" | "poor" | "broken" | undefined;
209
- insurance?: boolean | undefined;
210
- insuranceCost?: number | undefined;
211
- shippingPaidBy?: "buyer" | "seller" | undefined;
212
- reservePrice?: number | undefined;
213
- buyNowPrice?: number | undefined;
214
- minBidIncrement?: number | undefined;
215
- auctionExtensionMinutes?: number | undefined;
216
- auctionShippingPaidBy?: "seller" | "winner" | undefined;
217
- seoTitle?: string | undefined;
218
- seoDescription?: string | undefined;
219
- seoKeywords?: string[] | undefined;
220
- finalSale?: boolean | undefined;
221
- allowOffers?: boolean | undefined;
222
- minOfferPercent?: number | undefined;
29
+ [x: string]: unknown;
30
+ listingType?: unknown;
31
+ startingBid?: unknown;
32
+ reservePrice?: unknown;
33
+ buyNowPrice?: unknown;
34
+ minBidIncrement?: unknown;
35
+ auctionEndDate?: unknown;
36
+ autoExtendable?: unknown;
37
+ auctionExtensionMinutes?: unknown;
38
+ auctionShippingPaidBy?: unknown;
223
39
  }, {
224
- title: string;
225
- price: number;
226
- listingType: "auction";
227
- category: string;
228
- mainImage: string;
229
- auctionEndDate: string;
230
- startingBid: number;
231
- currency?: string | undefined;
232
- brandSlug?: string | undefined;
233
- stockQuantity?: number | undefined;
234
- features?: string[] | undefined;
235
- description?: string | undefined;
236
- availableQuantity?: number | undefined;
237
- images?: string[] | undefined;
238
- tags?: string[] | undefined;
239
- shippingInfo?: string | undefined;
240
- returnPolicy?: string | undefined;
241
- condition?: "new" | "used" | "refurbished" | "like_new" | "good" | "fair" | "poor" | "broken" | undefined;
242
- insurance?: boolean | undefined;
243
- insuranceCost?: number | undefined;
244
- shippingPaidBy?: "buyer" | "seller" | undefined;
245
- reservePrice?: number | undefined;
246
- buyNowPrice?: number | undefined;
247
- minBidIncrement?: number | undefined;
248
- autoExtendable?: boolean | undefined;
249
- auctionExtensionMinutes?: number | undefined;
250
- auctionShippingPaidBy?: "seller" | "winner" | undefined;
251
- seoTitle?: string | undefined;
252
- seoDescription?: string | undefined;
253
- seoKeywords?: string[] | undefined;
254
- finalSale?: boolean | undefined;
255
- allowOffers?: boolean | undefined;
256
- minOfferPercent?: number | undefined;
40
+ [x: string]: unknown;
41
+ listingType?: unknown;
42
+ startingBid?: unknown;
43
+ reservePrice?: unknown;
44
+ buyNowPrice?: unknown;
45
+ minBidIncrement?: unknown;
46
+ auctionEndDate?: unknown;
47
+ autoExtendable?: unknown;
48
+ auctionExtensionMinutes?: unknown;
49
+ auctionShippingPaidBy?: unknown;
257
50
  }>;
258
51
  export declare const preOrderInputSchema: z.ZodObject<{
259
- title: z.ZodString;
260
- description: z.ZodOptional<z.ZodString>;
261
- category: z.ZodString;
262
- brandSlug: z.ZodOptional<z.ZodString>;
263
- price: z.ZodNumber;
264
- currency: z.ZodDefault<z.ZodString>;
265
- stockQuantity: z.ZodDefault<z.ZodNumber>;
266
- availableQuantity: z.ZodDefault<z.ZodNumber>;
267
- mainImage: z.ZodEffects<z.ZodString, string, string>;
268
- images: z.ZodDefault<z.ZodArray<z.ZodEffects<z.ZodString, string, string>, "many">>;
269
- condition: z.ZodOptional<z.ZodEnum<["new", "like_new", "good", "fair", "poor", "used", "refurbished", "broken"]>>;
270
- tags: z.ZodDefault<z.ZodArray<z.ZodString, "many">>;
271
- seoTitle: z.ZodOptional<z.ZodString>;
272
- seoDescription: z.ZodOptional<z.ZodString>;
273
- seoKeywords: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
274
- shippingInfo: z.ZodOptional<z.ZodString>;
275
- returnPolicy: z.ZodOptional<z.ZodString>;
276
- finalSale: z.ZodOptional<z.ZodBoolean>;
277
- shippingPaidBy: z.ZodOptional<z.ZodEnum<["seller", "buyer"]>>;
278
- insurance: z.ZodOptional<z.ZodBoolean>;
279
- insuranceCost: z.ZodOptional<z.ZodNumber>;
280
- allowOffers: z.ZodOptional<z.ZodBoolean>;
281
- minOfferPercent: z.ZodOptional<z.ZodNumber>;
282
- features: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
52
+ [x: string]: z.ZodOptional<z.ZodUnknown>;
283
53
  } & {
284
54
  listingType: z.ZodLiteral<"pre-order">;
285
55
  preOrderDeliveryDate: z.ZodString;
@@ -288,67 +58,21 @@ export declare const preOrderInputSchema: z.ZodObject<{
288
58
  preOrderProductionStatus: z.ZodDefault<z.ZodEnum<["upcoming", "in_production", "ready_to_ship"]>>;
289
59
  preOrderCancellable: z.ZodDefault<z.ZodBoolean>;
290
60
  }, "strip", z.ZodTypeAny, {
291
- currency: string;
292
- title: string;
293
- price: number;
294
- listingType: "pre-order";
295
- category: string;
296
- stockQuantity: number;
297
- availableQuantity: number;
298
- mainImage: string;
299
- images: string[];
300
- tags: string[];
301
- preOrderDeliveryDate: string;
302
- preOrderProductionStatus: "upcoming" | "in_production" | "ready_to_ship";
303
- preOrderCancellable: boolean;
304
- brandSlug?: string | undefined;
305
- features?: string[] | undefined;
306
- description?: string | undefined;
307
- shippingInfo?: string | undefined;
308
- returnPolicy?: string | undefined;
309
- condition?: "new" | "used" | "refurbished" | "like_new" | "good" | "fair" | "poor" | "broken" | undefined;
310
- insurance?: boolean | undefined;
311
- insuranceCost?: number | undefined;
312
- shippingPaidBy?: "buyer" | "seller" | undefined;
313
- preOrderDepositPercent?: number | undefined;
314
- preOrderMaxQuantity?: number | undefined;
315
- seoTitle?: string | undefined;
316
- seoDescription?: string | undefined;
317
- seoKeywords?: string[] | undefined;
318
- finalSale?: boolean | undefined;
319
- allowOffers?: boolean | undefined;
320
- minOfferPercent?: number | undefined;
61
+ [x: string]: unknown;
62
+ listingType?: unknown;
63
+ preOrderDeliveryDate?: unknown;
64
+ preOrderDepositPercent?: unknown;
65
+ preOrderMaxQuantity?: unknown;
66
+ preOrderProductionStatus?: unknown;
67
+ preOrderCancellable?: unknown;
321
68
  }, {
322
- title: string;
323
- price: number;
324
- listingType: "pre-order";
325
- category: string;
326
- mainImage: string;
327
- preOrderDeliveryDate: string;
328
- currency?: string | undefined;
329
- brandSlug?: string | undefined;
330
- stockQuantity?: number | undefined;
331
- features?: string[] | undefined;
332
- description?: string | undefined;
333
- availableQuantity?: number | undefined;
334
- images?: string[] | undefined;
335
- tags?: string[] | undefined;
336
- shippingInfo?: string | undefined;
337
- returnPolicy?: string | undefined;
338
- condition?: "new" | "used" | "refurbished" | "like_new" | "good" | "fair" | "poor" | "broken" | undefined;
339
- insurance?: boolean | undefined;
340
- insuranceCost?: number | undefined;
341
- shippingPaidBy?: "buyer" | "seller" | undefined;
342
- preOrderDepositPercent?: number | undefined;
343
- preOrderMaxQuantity?: number | undefined;
344
- preOrderProductionStatus?: "upcoming" | "in_production" | "ready_to_ship" | undefined;
345
- preOrderCancellable?: boolean | undefined;
346
- seoTitle?: string | undefined;
347
- seoDescription?: string | undefined;
348
- seoKeywords?: string[] | undefined;
349
- finalSale?: boolean | undefined;
350
- allowOffers?: boolean | undefined;
351
- minOfferPercent?: number | undefined;
69
+ [x: string]: unknown;
70
+ listingType?: unknown;
71
+ preOrderDeliveryDate?: unknown;
72
+ preOrderDepositPercent?: unknown;
73
+ preOrderMaxQuantity?: unknown;
74
+ preOrderProductionStatus?: unknown;
75
+ preOrderCancellable?: unknown;
352
76
  }>;
353
77
  export declare const setFeaturedSchema: z.ZodObject<{
354
78
  productId: z.ZodString;
@@ -1,9 +1,10 @@
1
1
  import { z } from "zod";
2
2
  import { mediaUrlSchema } from "../../../../validation/schemas";
3
3
  import { PRODUCT_MAX_IMAGES } from "../../media/limits";
4
+ import { PRODUCT_UPDATABLE_FIELDS } from "../../../../features/products/schemas/firestore";
4
5
  const priceSchema = z.number().min(0, "Price must be non-negative");
5
6
  const conditionSchema = z.enum(["new", "like_new", "good", "fair", "poor", "used", "refurbished", "broken"]);
6
- export const productInputSchema = z.object({
7
+ const productBaseShape = z.object({
7
8
  title: z.string().min(3, "Title must be at least 3 characters").max(200),
8
9
  description: z.string().max(10000).optional(),
9
10
  category: z.string().min(1, "Category is required"),
@@ -30,6 +31,44 @@ export const productInputSchema = z.object({
30
31
  minOfferPercent: z.number().min(0).max(100).optional(),
31
32
  features: z.array(z.string()).max(50).optional(),
32
33
  });
34
+ /**
35
+ * Every other field a seller legitimately owns on their own listing.
36
+ *
37
+ * 🛑 Declared, never `.passthrough()`. `sellerUpdateProduct` hands its parsed
38
+ * input straight to `productRepository.updateProduct` with no field filter, so
39
+ * passthrough would let a seller forge `currentBid` / `bidCount` / `viewCount`
40
+ * or reassign `storeId` to another store. The three sibling product-update
41
+ * routes DO use `.passthrough()`; they get away with it because they are
42
+ * admin-gated. This one is not.
43
+ *
44
+ * Derived from `PRODUCT_UPDATABLE_FIELDS` — the codebase's existing declaration
45
+ * of what a seller may change — so the two cannot drift (Root Cause #61).
46
+ *
47
+ * Why this exists: `productInputSchema` declared 24 fields while that constant
48
+ * lists 55, and `z.object()` SILENTLY STRIPS whatever it does not declare. The
49
+ * parse SUCCEEDED, so nothing surfaced anywhere — the seller saw a success
50
+ * toast and 38 fields were discarded on the way to Firestore, including
51
+ * `status` (so Publish wrote nothing at all), `listingType`, and all four
52
+ * per-type blocks that `draftToProductInput` had just carefully assembled.
53
+ * Update-side sibling of Root Cause #101.
54
+ */
55
+ const EXTRA_WRITABLE_FIELDS = [
56
+ ...PRODUCT_UPDATABLE_FIELDS,
57
+ // Absent from that constant, but plainly the seller's own listing data —
58
+ // their media, their tax codes. Omitting them is an oversight in the list,
59
+ // not a policy: there is no integrity risk in a seller setting either.
60
+ "video",
61
+ "gstRate",
62
+ "hsnCode",
63
+ // Auction terms. Writable while the listing is still a draft; once a real
64
+ // bid exists, `sellerUpdateProduct` refuses them — changing the starting bid
65
+ // or the end date under a live bidder is the one case that must not pass.
66
+ "startingBid",
67
+ "auctionEndDate",
68
+ ];
69
+ const declaredKeys = new Set(Object.keys(productBaseShape.shape));
70
+ const extraWritableShape = Object.fromEntries(EXTRA_WRITABLE_FIELDS.filter((field) => !declaredKeys.has(field)).map((field) => [field, z.unknown().optional()]));
71
+ export const productInputSchema = productBaseShape.extend(extraWritableShape);
33
72
  export const productUpdateSchema = productInputSchema.partial();
34
73
  export const auctionInputSchema = productInputSchema.extend({
35
74
  // SB1-G — canonical discriminator. Locked to "auction" via literal.
@@ -37,6 +37,18 @@ export type AllowedImageMime = (typeof ALLOWED_IMAGE_MIMES)[number];
37
37
  export type AllowedVideoMime = (typeof ALLOWED_VIDEO_MIMES)[number];
38
38
  export type AllowedDocMime = (typeof ALLOWED_DOC_MIMES)[number];
39
39
  export type AllowedMime = (typeof ALLOWED_MIMES)[number];
40
+ /**
41
+ * The `accept` attribute for an image picker.
42
+ *
43
+ * 🛑 Never `image/*` — that wildcard matches `image/svg+xml`, which
44
+ * `ALLOWED_IMAGE_MIMES` deliberately excludes as an XSS surface. The server
45
+ * does reject it (`classifyMime` returns null and `/api/media/finalize` 422s),
46
+ * but only AFTER the picker has accepted it and the crop editor has opened on
47
+ * it — so the user frames and crops an image that was never going to upload,
48
+ * and the refusal arrives at the end looking like a failure rather than a rule.
49
+ * Deriving the attribute from the allowlist makes the picker refuse it first.
50
+ */
51
+ export declare const IMAGE_ACCEPT_ATTR: string;
40
52
  export declare const ALLOWED_TYPES_LABEL = "JPEG, PNG, GIF, WebP, MP4, WebM, QuickTime, 3GP, MKV, PDF";
41
53
  export declare const MIME_TO_EXT: Record<string, string>;
42
54
  export declare function classifyMime(mime: string): MediaKind | null;
@@ -76,6 +76,18 @@ export const ALLOWED_MIMES = [
76
76
  ...ALLOWED_VIDEO_MIMES,
77
77
  ...ALLOWED_DOC_MIMES,
78
78
  ];
79
+ /**
80
+ * The `accept` attribute for an image picker.
81
+ *
82
+ * 🛑 Never `image/*` — that wildcard matches `image/svg+xml`, which
83
+ * `ALLOWED_IMAGE_MIMES` deliberately excludes as an XSS surface. The server
84
+ * does reject it (`classifyMime` returns null and `/api/media/finalize` 422s),
85
+ * but only AFTER the picker has accepted it and the crop editor has opened on
86
+ * it — so the user frames and crops an image that was never going to upload,
87
+ * and the refusal arrives at the end looking like a failure rather than a rule.
88
+ * Deriving the attribute from the allowlist makes the picker refuse it first.
89
+ */
90
+ export const IMAGE_ACCEPT_ATTR = ALLOWED_IMAGE_MIMES.join(",");
79
91
  export const ALLOWED_TYPES_LABEL = "JPEG, PNG, GIF, WebP, MP4, WebM, QuickTime, 3GP, MKV, PDF";
80
92
  // Server-detected MIME → canonical filename extension. The browser-supplied
81
93
  // File.type is untrusted; the server picks the ext from the detected MIME.
package/dist/client.d.ts CHANGED
@@ -402,7 +402,7 @@ export type { ListingTypeCapability } from "./_internal/shared/listing-types/cap
402
402
  export { LISTING_TYPE_REGISTRY, pluginFor, detectListingTypeFromSlug } from "./_internal/shared/listing-types/_registry";
403
403
  export type { ListingTypePlugin } from "./_internal/shared/listing-types/_registry";
404
404
  export type { ListingType } from "./features/products/types/index";
405
- export { MEGABYTE, MAX_IMAGE_BYTES, MAX_PDF_BYTES, MAX_VIDEO_BYTES, MAX_LABEL, MAX_BYTES, ALLOWED_IMAGE_MIMES, ALLOWED_VIDEO_MIMES, ALLOWED_DOC_MIMES, ALLOWED_MIMES, ALLOWED_TYPES_LABEL, MIME_TO_EXT, PDF_MAGIC, VIDEO_CONVERSION_HINTS, classifyMime, isAllowedMime, maxBytesFor, getConversionHint, PRODUCT_MAX_IMAGES, PRODUCT_MAX_VIDEOS, PRODUCT_IMAGE_INDEX_MAX, } from "./_internal/shared/media/limits";
405
+ export { MEGABYTE, MAX_IMAGE_BYTES, MAX_PDF_BYTES, MAX_VIDEO_BYTES, MAX_LABEL, MAX_BYTES, ALLOWED_IMAGE_MIMES, IMAGE_ACCEPT_ATTR, ALLOWED_VIDEO_MIMES, ALLOWED_DOC_MIMES, ALLOWED_MIMES, ALLOWED_TYPES_LABEL, MIME_TO_EXT, PDF_MAGIC, VIDEO_CONVERSION_HINTS, classifyMime, isAllowedMime, maxBytesFor, getConversionHint, PRODUCT_MAX_IMAGES, PRODUCT_MAX_VIDEOS, PRODUCT_IMAGE_INDEX_MAX, } from "./_internal/shared/media/limits";
406
406
  export type { MediaKind, AllowedImageMime, AllowedVideoMime, AllowedDocMime, AllowedMime, } from "./_internal/shared/media/limits";
407
407
  export { ScamAwarenessModal } from "./features/scams/components/ScamAwarenessModal";
408
408
  export type { ScamAwarenessModalProps } from "./features/scams/components/ScamAwarenessModal";
package/dist/client.js CHANGED
@@ -416,7 +416,7 @@ export { DashboardLayoutClient, RoleGuard } from "./_internal/client/features/la
416
416
  export { LISTING_TYPE_CAPABILITIES, capabilityFor, canAddToCart, canBid, supportsShipping, requiresVendorVerified, requiresJurisdictionCheck, hasInstantFulfillment, assertNever, } from "./_internal/shared/listing-types/capabilities";
417
417
  export { LISTING_TYPE_REGISTRY, pluginFor, detectListingTypeFromSlug } from "./_internal/shared/listing-types/_registry";
418
418
  // Media upload limits — shared by client uploaders + server sign/finalize routes.
419
- export { MEGABYTE, MAX_IMAGE_BYTES, MAX_PDF_BYTES, MAX_VIDEO_BYTES, MAX_LABEL, MAX_BYTES, ALLOWED_IMAGE_MIMES, ALLOWED_VIDEO_MIMES, ALLOWED_DOC_MIMES, ALLOWED_MIMES, ALLOWED_TYPES_LABEL, MIME_TO_EXT, PDF_MAGIC, VIDEO_CONVERSION_HINTS, classifyMime, isAllowedMime, maxBytesFor, getConversionHint, PRODUCT_MAX_IMAGES, PRODUCT_MAX_VIDEOS, PRODUCT_IMAGE_INDEX_MAX, } from "./_internal/shared/media/limits";
419
+ export { MEGABYTE, MAX_IMAGE_BYTES, MAX_PDF_BYTES, MAX_VIDEO_BYTES, MAX_LABEL, MAX_BYTES, ALLOWED_IMAGE_MIMES, IMAGE_ACCEPT_ATTR, ALLOWED_VIDEO_MIMES, ALLOWED_DOC_MIMES, ALLOWED_MIMES, ALLOWED_TYPES_LABEL, MIME_TO_EXT, PDF_MAGIC, VIDEO_CONVERSION_HINTS, classifyMime, isAllowedMime, maxBytesFor, getConversionHint, PRODUCT_MAX_IMAGES, PRODUCT_MAX_VIDEOS, PRODUCT_IMAGE_INDEX_MAX, } from "./_internal/shared/media/limits";
420
420
  export { ScamAwarenessModal } from "./features/scams/components/ScamAwarenessModal";
421
421
  export { SellerTrustBadge } from "./features/scams/components/SellerTrustBadge";
422
422
  // [CLIENT-ONLY] — Admin panel components for action/nav permission management.
@@ -1,5 +1,6 @@
1
1
  "use client";
2
2
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
3
+ import { IMAGE_ACCEPT_ATTR } from "../../../_internal/shared/media/limits";
3
4
  import { Div, Input, Label, RichTextEditor, Select, Stack, TagInput, Text, Textarea, } from "../../../ui";
4
5
  import { Checkbox } from "../../forms/index";
5
6
  import { MediaUploadField, MediaUploadList } from "../../media/index";
@@ -11,7 +12,7 @@ function renderBlogPostMetaFields(props) {
11
12
  }
12
13
  function renderBlogPostMediaFields(props) {
13
14
  const { update, isReadonly, labels, onUploadCover, onUploadContentImage, onUploadAdditionalImage, onAbort, coverImageUrl, contentImages, additionalImages } = props;
14
- return (_jsxs(_Fragment, { children: [_jsx(MediaUploadField, { label: labels.coverImage, value: coverImageUrl || "", onChange: (url) => update({ coverImage: url ? { url, type: "image" } : null }), onChangeField: (media) => update({ coverImage: media }), onUpload: onUploadCover, onAbort: onAbort, accept: "image/*", maxSizeMB: 10, disabled: isReadonly, helperText: labels.coverImageHelper }), _jsx(MediaUploadList, { label: labels.contentImages, value: contentImages, onChange: (media) => update({ contentImages: media }), onUpload: onUploadContentImage, onAbort: onAbort, accept: "image/*,video/*", maxItems: 10, maxSizeMB: 10, disabled: isReadonly, helperText: labels.contentImagesHelper }), _jsx(MediaUploadList, { label: labels.additionalImages, value: additionalImages, onChange: (media) => update({ additionalImages: media }), onUpload: onUploadAdditionalImage, onAbort: onAbort, accept: "image/*,video/*", maxItems: 5, maxSizeMB: 10, disabled: isReadonly, helperText: labels.additionalImagesHelper }), isReadonly && coverImageUrl && _jsx(Text, { size: "xs", variant: "secondary", children: coverImageUrl })] }));
15
+ return (_jsxs(_Fragment, { children: [_jsx(MediaUploadField, { label: labels.coverImage, value: coverImageUrl || "", onChange: (url) => update({ coverImage: url ? { url, type: "image" } : null }), onChangeField: (media) => update({ coverImage: media }), onUpload: onUploadCover, onAbort: onAbort, accept: IMAGE_ACCEPT_ATTR, maxSizeMB: 10, disabled: isReadonly, helperText: labels.coverImageHelper }), _jsx(MediaUploadList, { label: labels.contentImages, value: contentImages, onChange: (media) => update({ contentImages: media }), onUpload: onUploadContentImage, onAbort: onAbort, accept: "image/*,video/*", maxItems: 10, maxSizeMB: 10, disabled: isReadonly, helperText: labels.contentImagesHelper }), _jsx(MediaUploadList, { label: labels.additionalImages, value: additionalImages, onChange: (media) => update({ additionalImages: media }), onUpload: onUploadAdditionalImage, onAbort: onAbort, accept: "image/*,video/*", maxItems: 5, maxSizeMB: 10, disabled: isReadonly, helperText: labels.additionalImagesHelper }), isReadonly && coverImageUrl && _jsx(Text, { size: "xs", variant: "secondary", children: coverImageUrl })] }));
15
16
  }
16
17
  export function BlogPostForm({ value, onChange, categoryOptions, statusOptions, labels, onUploadCover, onUploadContentImage, onUploadAdditionalImage, onAbort, isReadonly = false, renderContentField, renderContentReadonly, }) {
17
18
  const coverImageUrl = getMediaUrl(value.coverImage) ?? null;
@@ -1,5 +1,6 @@
1
1
  "use client";
2
2
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { IMAGE_ACCEPT_ATTR } from "../../../_internal/shared/media/limits";
3
4
  /**
4
5
  * Add or edit an item in a user's personal catalogue.
5
6
  *
@@ -82,7 +83,7 @@ export function CatalogueItemEditorView({ item, onSaved }) {
82
83
  // `mainImage` is derived, so it moves with the gallery rather
83
84
  // than pointing at a photo the user has since removed.
84
85
  onChange({ images: urls, mainImage: urls[0] ?? "" });
85
- }, onUpload: handleUpload, accept: "image/*", maxItems: 8, helperText: "Photos older than 30 days must be refreshed before this item can be listed." })),
86
+ }, onUpload: handleUpload, accept: IMAGE_ACCEPT_ATTR, maxItems: 8, helperText: "Photos older than 30 days must be refreshed before this item can be listed." })),
86
87
  categorySlugs: ({ values, onChange }) => (
87
88
  // Explicit `<string>`: with `multiple`, `value` is `V[]`, so an
88
89
  // inferred V reads the array itself as the option type.
@@ -18,7 +18,7 @@ import { NextResponse } from "next/server.js";
18
18
  import { getProviders } from "../../../../contracts";
19
19
  import { EVENT_FIELDS } from "../../schemas";
20
20
  import { normalizeError } from "../../../../errors/normalize";
21
- import { toClientLotteryConfig } from "../../../../_internal/server/features/lottery/adapters";
21
+ import { toPublicEvent } from "../../../../_internal/server/features/events/adapters";
22
22
  // --- GET /api/events/[id] -----------------------------------------------------
23
23
  export async function GET(_request, context) {
24
24
  try {
@@ -34,22 +34,12 @@ export async function GET(_request, context) {
34
34
  event.status === EVENT_FIELDS.STATUS_VALUES.PAUSED) {
35
35
  return NextResponse.json({ success: false, error: "Event not found" }, { status: 404 });
36
36
  }
37
- // Strip internal field.
38
- //
39
- // 🛑 This is a deny-list spread, so anything NOT named here is published by
40
- // default — which is how a lottery event's stored `lotteryConfig` reached
41
- // anonymous callers whole. `toClientLotteryConfig` is the allow-list that
42
- // belongs on this boundary: it drops the per-slot `price` and `weight` (the
43
- // weighting is how the odds are set) and the internal `bookedByUserId`,
44
- // while deliberately keeping `bookedByDisplayName` and
45
- // `bookedByUserLotteryNumber`, which the public slot grid renders.
46
- const { createdBy: _createdBy, ...rawPublicEvent } = event;
47
- const publicEvent = rawPublicEvent.lotteryConfig
48
- ? {
49
- ...rawPublicEvent,
50
- lotteryConfig: toClientLotteryConfig(rawPublicEvent.lotteryConfig),
51
- }
52
- : rawPublicEvent;
37
+ // 🛑 Do not hand-roll this projection here. `toPublicEvent` drops
38
+ // `createdBy` AND allow-lists the two nested structures that decide
39
+ // outcomes — `lotteryConfig.slots[]` (price / weight / bookedByUserId) and
40
+ // `spinPrizes[]` (weight / couponId). Both were published to anonymous
41
+ // callers by the deny-list spread that used to live here.
42
+ const publicEvent = toPublicEvent(event);
53
43
  const entriesRepo = db.getRepository("eventEntries");
54
44
  // For poll events with resultsVisibility='always': compute live vote tallies
55
45
  let pollResults = [];
@@ -1,5 +1,6 @@
1
1
  "use client";
2
2
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { IMAGE_ACCEPT_ATTR } from "../../../_internal/shared/media/limits";
3
4
  import { normalizeError } from "../../../errors/normalize";
4
5
  import { useEffect, useState, useRef } from "react";
5
6
  import { z } from "zod";
@@ -159,7 +160,7 @@ export function CharacterHotspotForm({ initial, onUploadImage, onSave, onAfterSa
159
160
  className: "mt-1 text-center text-[10px]", style: { color: "var(--color-muted)" }, children: s.label })] }, s.key))) }), step === "image" && (_jsxs(Div, { className: CLS_STEP, style: {
160
161
  borderColor: "var(--border-ink)",
161
162
  background: "var(--surface-elevated)",
162
- }, children: [_jsx(Heading, { level: 2, size: "lg", weight: "bold", children: "Upload Background Image" }), _jsx(Text, { style: { color: "var(--color-muted)" }, size: "sm", children: "Choose a wide panoramic image that shows all the characters. You will place pins on it in the next step." }), _jsxs(Label, { layout: "flex-col-center", gap: "md", rounded: "lg", padding: "xl", className: "cursor-pointer border-2 border-dashed transition-colors hover:opacity-80", style: { borderColor: "var(--border-ink)" }, children: [_jsx(Span, { size: "3xl", children: "\uD83D\uDDBC" }), _jsx(Span, { weight: "medium", children: uploading ? "Uploading…" : "Click to choose image" }), _jsx(Span, { size: "xs", style: { color: "var(--color-muted)" }, children: "JPG, PNG, WebP \u2014 wide landscape images work best" }), _jsx(Input, { type: "file", accept: "image/*", className: "hidden", onChange: handleImageUpload, disabled: uploading })] }), imageUrl && (_jsxs(Stack, { gap: "xs", children: [_jsx(Div, { className: CLS_IMG_WRAP, style: { paddingTop: "37.5%" }, children: _jsx(Image, { src: resolveMediaUrl(imageUrl), alt: imageAlt, fill: true, className: "object-cover", sizes: "680px" }) }), _jsx(Text, { className: CLS_SUCCESS_TEXT, children: "\u2713 Image uploaded" })] })), _jsxs(Stack, { gap: "xs", children: [_jsx(Label, { size: "sm", weight: "bold", children: "Image Alt Text" }), _jsx(Input, { type: "text", value: imageAlt, onChange: (e) => setImageAlt(e.target.value), placeholder: "DC, Marvel and Anime characters", className: CLS_INPUT, style: {
163
+ }, children: [_jsx(Heading, { level: 2, size: "lg", weight: "bold", children: "Upload Background Image" }), _jsx(Text, { style: { color: "var(--color-muted)" }, size: "sm", children: "Choose a wide panoramic image that shows all the characters. You will place pins on it in the next step." }), _jsxs(Label, { layout: "flex-col-center", gap: "md", rounded: "lg", padding: "xl", className: "cursor-pointer border-2 border-dashed transition-colors hover:opacity-80", style: { borderColor: "var(--border-ink)" }, children: [_jsx(Span, { size: "3xl", children: "\uD83D\uDDBC" }), _jsx(Span, { weight: "medium", children: uploading ? "Uploading…" : "Click to choose image" }), _jsx(Span, { size: "xs", style: { color: "var(--color-muted)" }, children: "JPG, PNG, WebP \u2014 wide landscape images work best" }), _jsx(Input, { type: "file", accept: IMAGE_ACCEPT_ATTR, className: "hidden", onChange: handleImageUpload, disabled: uploading })] }), imageUrl && (_jsxs(Stack, { gap: "xs", children: [_jsx(Div, { className: CLS_IMG_WRAP, style: { paddingTop: "37.5%" }, children: _jsx(Image, { src: resolveMediaUrl(imageUrl), alt: imageAlt, fill: true, className: "object-cover", sizes: "680px" }) }), _jsx(Text, { className: CLS_SUCCESS_TEXT, children: "\u2713 Image uploaded" })] })), _jsxs(Stack, { gap: "xs", children: [_jsx(Label, { size: "sm", weight: "bold", children: "Image Alt Text" }), _jsx(Input, { type: "text", value: imageAlt, onChange: (e) => setImageAlt(e.target.value), placeholder: "DC, Marvel and Anime characters", className: CLS_INPUT, style: {
163
164
  borderColor: "var(--border-ink)",
164
165
  background: "var(--surface-elevated)",
165
166
  color: "var(--color-black)",
@@ -319,7 +320,7 @@ export function CharacterHotspotForm({ initial, onUploadImage, onSave, onAfterSa
319
320
  }, children: "+" }), pin.name && (_jsx(Div, { textWeight: "bold", className: "pointer-events-none absolute left-8 top-1/2 -translate-y-1/2 whitespace-nowrap text-[9px] text-white", rounded: "default", style: { paddingInline: "0.375rem", paddingBlock: "0.125rem", background: "black" }, children: pin.name }))] }, pin.id)))] }), pins.length > 0 ? (_jsx(Ul, { rounded: "lg", className: "divide-y border", style: { borderColor: "var(--border-ink)" }, children: pins.map((pin, i) => (_jsx(Li, { padding: "inline", children: _jsxs(Row, { gap: "sm", children: [_jsx(Span, { layout: "flex-center", color: "inverse", className: "h-6 w-6 shrink-0", rounded: "full", size: "xs", weight: "bold", style: { background: pin.accent || DEFAULT_ACCENT_HEX }, children: i + 1 }), _jsxs(Div, { className: "min-w-0 flex-1", children: [_jsx(Text, { className: "truncate", size: "sm", weight: "bold", children: pin.name || (_jsx(Span, { className: "italic", style: { color: "var(--color-muted)" }, children: "Unnamed" })) }), _jsxs(Text, { style: { color: "var(--color-muted)" }, size: "xs", children: [pin.universe, " \u00B7 ", pin.xPct.toFixed(0), "%,", " ", pin.yPct.toFixed(0), "%"] })] }), _jsx(Button, { type: "button", onClick: () => deletePin(pin.id), variant: "ghost", className: CLS_DELETE_PIN, title: "Remove pin", children: "\u2715" })] }) }, pin.id))) })) : (_jsx(Div, { textSize: "sm", className: "border-2 border-dashed text-left", rounded: "lg", padding: "xl", style: {
320
321
  borderColor: "var(--border-ink)",
321
322
  color: "var(--color-muted)",
322
- }, children: "No pins yet \u2014 add some using the button below." })), _jsxs(Details, { rounded: "lg", padding: "md", className: "border", style: { borderColor: "var(--border-ink)" }, children: [_jsx(Summary, { size: "sm", weight: "medium", children: "Image Settings" }), _jsxs(Stack, { className: "mt-3", gap: "3", children: [_jsxs(Label, { layout: "inline-flex", gap: "md", rounded: "default", padding: "toolbar", className: "cursor-pointer border-2 border-dashed transition-colors hover:opacity-80", size: "sm", weight: "medium", style: { borderColor: "var(--border-ink)" }, children: [uploading ? "Uploading…" : "Replace Image", _jsx(Input, { type: "file", accept: "image/*", className: "hidden", onChange: handleImageUpload, disabled: uploading })] }), _jsxs(Stack, { gap: "xs", children: [_jsx(Label, { size: "sm", weight: "bold", children: "Image Alt Text" }), _jsx(Input, { type: "text", value: imageAlt, onChange: (e) => setImageAlt(e.target.value), className: CLS_INPUT, style: {
323
+ }, children: "No pins yet \u2014 add some using the button below." })), _jsxs(Details, { rounded: "lg", padding: "md", className: "border", style: { borderColor: "var(--border-ink)" }, children: [_jsx(Summary, { size: "sm", weight: "medium", children: "Image Settings" }), _jsxs(Stack, { className: "mt-3", gap: "3", children: [_jsxs(Label, { layout: "inline-flex", gap: "md", rounded: "default", padding: "toolbar", className: "cursor-pointer border-2 border-dashed transition-colors hover:opacity-80", size: "sm", weight: "medium", style: { borderColor: "var(--border-ink)" }, children: [uploading ? "Uploading…" : "Replace Image", _jsx(Input, { type: "file", accept: IMAGE_ACCEPT_ATTR, className: "hidden", onChange: handleImageUpload, disabled: uploading })] }), _jsxs(Stack, { gap: "xs", children: [_jsx(Label, { size: "sm", weight: "bold", children: "Image Alt Text" }), _jsx(Input, { type: "text", value: imageAlt, onChange: (e) => setImageAlt(e.target.value), className: CLS_INPUT, style: {
323
324
  borderColor: "var(--border-ink)",
324
325
  background: "var(--surface-elevated)",
325
326
  color: "var(--color-black)",
@@ -1,5 +1,6 @@
1
1
  "use client";
2
2
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
3
+ import { IMAGE_ACCEPT_ATTR } from "../../_internal/shared/media/limits";
3
4
  import { useEffect, useRef, useState } from "react";
4
5
  import { Alert, Button, Div, Input, Progress, Row, Stack, Text, useToast } from "../../ui";
5
6
  import { useMediaUpload } from "./hooks/useMedia";
@@ -131,6 +132,6 @@ export function AvatarUpload({ currentPhotoURL, currentCropData, userId, display
131
132
  fileInputRef.current.value = "";
132
133
  };
133
134
  const hasPending = pendingCropData !== null && pendingUploadFile !== null;
134
- return (_jsxs(_Fragment, { children: [_jsxs(Stack, { gap: "md", children: [_jsxs(Row, { align: "start", gap: "lg", children: [_jsx(Div, { className: "shrink-0", children: _jsx(AvatarDisplay, { cropData: hasPending ? pendingCropData : cropData, size: "2xl", className: `border-4 ${hasPending ? "border-primary/60 ring-2 ring-primary/20" : "border-[var(--appkit-color-border)]"}`, displayName: displayName }) }), _jsxs(Stack, { className: "flex-1", gap: "3", children: [_jsx(Text, { variant: "secondary", size: "xs", children: "JPG, PNG, WEBP or GIF up to 10MB." }), isUploading ? (_jsx(Progress, { value: 50, variant: "primary", size: "sm", label: t.uploading })) : null, hasPending && !isUploading ? (_jsxs(Stack, { gap: "sm", children: [_jsx(Text, { variant: "secondary", className: "text-primary", size: "xs", children: t.readyToSave }), _jsxs(Row, { gap: "3", children: [_jsx(Button, { type: "button", onClick: handleConfirmSave, size: "sm", children: t.saveAvatar }), _jsx(Button, { type: "button", variant: "secondary", size: "sm", onClick: handleCancelPending, children: t.cancelChange })] })] })) : null, !hasPending && !isUploading ? (_jsxs(Row, { gap: "3", children: [_jsx(Button, { type: "button", onClick: () => fileInputRef.current?.click(), size: "sm", children: previewUrl ? t.changePhoto : t.chooseImage }), previewUrl ? (_jsx(Button, { type: "button", variant: "secondary", size: "sm", onClick: handleRemovePhoto, children: t.removePhoto })) : null] })) : null, _jsx(Input, { ref: fileInputRef, bare: true, type: "file", accept: "image/*", onChange: handleFileSelect, className: "hidden", "aria-label": t.changePhoto })] })] }), uploadApiError ? (_jsx(Alert, { variant: "error", children: uploadApiError.message })) : null] }), showCropModal && tempImageUrl ? (_jsx(ImageCropModal, { isOpen: showCropModal, imageUrl: tempImageUrl, onClose: handleCropCancel, onSave: handleCropSave, initialCropData: cropData || undefined })) : null] }));
135
+ return (_jsxs(_Fragment, { children: [_jsxs(Stack, { gap: "md", children: [_jsxs(Row, { align: "start", gap: "lg", children: [_jsx(Div, { className: "shrink-0", children: _jsx(AvatarDisplay, { cropData: hasPending ? pendingCropData : cropData, size: "2xl", className: `border-4 ${hasPending ? "border-primary/60 ring-2 ring-primary/20" : "border-[var(--appkit-color-border)]"}`, displayName: displayName }) }), _jsxs(Stack, { className: "flex-1", gap: "3", children: [_jsx(Text, { variant: "secondary", size: "xs", children: "JPG, PNG, WEBP or GIF up to 10MB." }), isUploading ? (_jsx(Progress, { value: 50, variant: "primary", size: "sm", label: t.uploading })) : null, hasPending && !isUploading ? (_jsxs(Stack, { gap: "sm", children: [_jsx(Text, { variant: "secondary", className: "text-primary", size: "xs", children: t.readyToSave }), _jsxs(Row, { gap: "3", children: [_jsx(Button, { type: "button", onClick: handleConfirmSave, size: "sm", children: t.saveAvatar }), _jsx(Button, { type: "button", variant: "secondary", size: "sm", onClick: handleCancelPending, children: t.cancelChange })] })] })) : null, !hasPending && !isUploading ? (_jsxs(Row, { gap: "3", children: [_jsx(Button, { type: "button", onClick: () => fileInputRef.current?.click(), size: "sm", children: previewUrl ? t.changePhoto : t.chooseImage }), previewUrl ? (_jsx(Button, { type: "button", variant: "secondary", size: "sm", onClick: handleRemovePhoto, children: t.removePhoto })) : null] })) : null, _jsx(Input, { ref: fileInputRef, bare: true, type: "file", accept: IMAGE_ACCEPT_ATTR, onChange: handleFileSelect, className: "hidden", "aria-label": t.changePhoto })] })] }), uploadApiError ? (_jsx(Alert, { variant: "error", children: uploadApiError.message })) : null] }), showCropModal && tempImageUrl ? (_jsx(ImageCropModal, { isOpen: showCropModal, imageUrl: tempImageUrl, onClose: handleCropCancel, onSave: handleCropSave, initialCropData: cropData || undefined })) : null] }));
135
136
  }
136
137
  export default AvatarUpload;
@@ -1,5 +1,6 @@
1
1
  "use client";
2
2
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
3
+ import { IMAGE_ACCEPT_ATTR } from "../../../_internal/shared/media/limits";
3
4
  import { normalizeError } from "../../../errors/normalize";
4
5
  /**
5
6
  * ImageUpload — canonical image upload component for @mohasinac/feat-media.
@@ -185,5 +186,5 @@ export function ImageUpload({ currentImage, onUpload, onChange, accept = "image/
185
186
  };
186
187
  return (_jsxs(Stack, { gap: "3", children: [label && (_jsx(Label, { className: "block", color: "muted", size: "sm", weight: "medium", children: label })), _jsx(Div, { className: "relative", children: preview ? (_jsxs(Stack, { gap: "sm", children: [_jsxs(Div, { className: `relative h-32 max-w-xs mx-auto ${__O.hidden}`, rounded: "lg", border: "default", children: [_jsx(MediaImage, { src: preview, alt: "Preview", size: "card" }), uploading && progress > 0 && (_jsx(Div, { className: "absolute inset-x-0 bottom-0", children: _jsx(Progress, { value: progress, size: "sm" }) }))] }), !uploading && (_jsxs(Row, { wrap: true, gap: "sm", children: [(enableCrop || enableAdvancedCrop) && (_jsx(Button, { type: "button", onClick: () => openEditor(preview), variant: "secondary", size: "sm", children: tMediaEditor("editImage") })), _jsx(Button, { type: "button", onClick: () => fileInputRef.current?.click(), variant: "outline", size: "sm", children: tUpload("change") }), _jsx(Button, { type: "button", onClick: handleRemove, variant: "danger", size: "sm", children: tUpload("remove") })] }))] })) : (_jsxs(_Fragment, { children: [showCamera && isCameraSupported && (_jsx(CameraCapture, { mode: "photo", facingMode: "environment", onCapture: handleCameraCapture, onError: setError })), !showCamera && (_jsxs(Stack, { gap: "sm", children: [_jsxs(Row, { gap: "sm", wrap: true, children: [showFileInput && (_jsx(Button, { type: "button", onClick: () => fileInputRef.current?.click(), disabled: uploading, variant: "outline", size: "sm", children: uploading ? tUpload("uploading") : tUpload("clickToUpload") })), captureSource !== "file-only" && isCameraSupported && (_jsx(Button, { type: "button", onClick: () => setCaptureMode("camera"), disabled: uploading, variant: "ghost", size: "sm", children: t("switchToCamera") })), captureSource !== "file-only" && !isCameraSupported && (_jsx(Button, { type: "button", onClick: () => mobileCaptureRef.current?.click(), disabled: uploading, variant: "ghost", size: "sm", children: t("switchToCamera") }))] }), _jsxs(Text, { size: "xs", variant: "secondary", children: [accept === "image/*"
187
188
  ? `JPG PNG GIF WebP`
188
- : accept.split(",").map((a) => a.split("/")[1]?.toUpperCase() ?? a).join(" "), " ", "\u2014 max ", maxSizeMB, "MB"] })] }))] })) }), _jsx("input", { ref: fileInputRef, type: "file", accept: accept, onChange: handleFileChange, className: "hidden", "aria-hidden": "true", "data-testid": "media-upload-input" }), showCamera && !isCameraSupported && (_jsx("input", { ref: mobileCaptureRef, type: "file", accept: "image/*", capture: "environment", onChange: handleFileChange, className: "hidden", "aria-hidden": "true", "data-testid": "media-upload-capture-input" })), helperText && !error && (_jsx(Text, { variant: "secondary", size: "xs", children: helperText })), error && _jsx(Alert, { variant: "error", children: error }), uploading && (_jsxs(Row, { gap: "sm", children: [_jsx(Spinner, { size: "sm" }), _jsx(Text, { size: "sm", variant: "secondary", children: tUpload("uploadingProgress", { progress }) })] })), cropPreviewUrl && !enableAdvancedCrop && (_jsx(ImageCropModal, { isOpen: showCropModal, imageUrl: cropPreviewUrl, onClose: handleCropClose, onSave: handleCropSave })), cropPreviewUrl && enableAdvancedCrop && (_jsx(ImageEditor, { isOpen: showAdvancedEditor, imageUrl: cropPreviewUrl, onClose: handleAdvancedEditorClose, onSave: handleAdvancedEditorSave, aspectRatio: cropAspectRatio }))] }));
189
+ : accept.split(",").map((a) => a.split("/")[1]?.toUpperCase() ?? a).join(" "), " ", "\u2014 max ", maxSizeMB, "MB"] })] }))] })) }), _jsx("input", { ref: fileInputRef, type: "file", accept: accept, onChange: handleFileChange, className: "hidden", "aria-hidden": "true", "data-testid": "media-upload-input" }), showCamera && !isCameraSupported && (_jsx("input", { ref: mobileCaptureRef, type: "file", accept: IMAGE_ACCEPT_ATTR, capture: "environment", onChange: handleFileChange, className: "hidden", "aria-hidden": "true", "data-testid": "media-upload-capture-input" })), helperText && !error && (_jsx(Text, { variant: "secondary", size: "xs", children: helperText })), error && _jsx(Alert, { variant: "error", children: error }), uploading && (_jsxs(Row, { gap: "sm", children: [_jsx(Spinner, { size: "sm" }), _jsx(Text, { size: "sm", variant: "secondary", children: tUpload("uploadingProgress", { progress }) })] })), cropPreviewUrl && !enableAdvancedCrop && (_jsx(ImageCropModal, { isOpen: showCropModal, imageUrl: cropPreviewUrl, onClose: handleCropClose, onSave: handleCropSave })), cropPreviewUrl && enableAdvancedCrop && (_jsx(ImageEditor, { isOpen: showAdvancedEditor, imageUrl: cropPreviewUrl, onClose: handleAdvancedEditorClose, onSave: handleAdvancedEditorSave, aspectRatio: cropAspectRatio }))] }));
189
190
  }
@@ -17,7 +17,7 @@ import { userRepository } from "../../auth/repository/user.repository";
17
17
  import { storeRepository } from "../../stores/repository/store.repository";
18
18
  import { productRepository } from "../../products/repository/products.repository";
19
19
  import { ProductStatusValues } from "../../products/schemas";
20
- import { assertPrizeDrawNotLocked, assertPrizeDrawWonItemsImmutable, } from "../../../_internal/server/features/products/service";
20
+ import { assertPrizeDrawNotLocked, assertPrizeDrawWonItemsImmutable, assertAuctionTermsMutable, } from "../../../_internal/server/features/products/service";
21
21
  import { orderRepository } from "../../orders/repository/orders.repository";
22
22
  import { OrderStatusValues } from "../../orders/schemas";
23
23
  import { payoutRepository } from "../../payments/repository/payout.repository";
@@ -561,6 +561,7 @@ export async function sellerUpdateProduct(userId, userRole, productId, input) {
561
561
  assertPrizeDrawNotLocked(existing, "unpublished or archived");
562
562
  }
563
563
  assertPrizeDrawWonItemsImmutable(existing, input.prizeDrawItems);
564
+ assertAuctionTermsMutable(existing, input);
564
565
  const finalizedData = await finalizeProductMediaReferences(input);
565
566
  const updated = await productRepository.updateProduct(productId, finalizedData);
566
567
  serverLogger.info("sellerUpdateProduct", { userId, productId });
package/dist/index.d.ts CHANGED
@@ -3140,7 +3140,7 @@ export { LISTING_TYPE_REGISTRY, pluginFor, detectListingTypeFromSlug } from "./_
3140
3140
  export type { ListingTypePlugin } from "./_internal/shared/listing-types/_registry";
3141
3141
  export { getListingRule, getCategoryRule, pickOrderType, getSplitKey, runSyncPreflight, CHECKOUT_RULES, CATEGORY_CHECKOUT_RULES, CHECKOUT_MAX_ORDERS_PER_TX, PRIZE_DRAW_MAX_REVEALS_PER_ORDER, BUNDLE_MAX_QTY_PER_TX, STANDARD_MAX_QTY_PER_LINE, } from "./_internal/shared/checkout/rules";
3142
3142
  export type { ListingCheckoutRule, CategoryCheckoutRule, CartItemProductPair, OrderItemInput, RefundPolicy, } from "./_internal/shared/checkout/rules";
3143
- export { MEGABYTE, MAX_IMAGE_BYTES, MAX_PDF_BYTES, MAX_VIDEO_BYTES, MAX_LABEL, MAX_BYTES, ALLOWED_IMAGE_MIMES, ALLOWED_VIDEO_MIMES, ALLOWED_DOC_MIMES, ALLOWED_MIMES, ALLOWED_TYPES_LABEL, MIME_TO_EXT, PDF_MAGIC, VIDEO_CONVERSION_HINTS, classifyMime, isAllowedMime, maxBytesFor, getConversionHint, PRODUCT_MAX_IMAGES, PRODUCT_MAX_VIDEOS, PRODUCT_IMAGE_INDEX_MAX, } from "./_internal/shared/media/limits";
3143
+ export { MEGABYTE, MAX_IMAGE_BYTES, MAX_PDF_BYTES, MAX_VIDEO_BYTES, MAX_LABEL, MAX_BYTES, ALLOWED_IMAGE_MIMES, IMAGE_ACCEPT_ATTR, ALLOWED_VIDEO_MIMES, ALLOWED_DOC_MIMES, ALLOWED_MIMES, ALLOWED_TYPES_LABEL, MIME_TO_EXT, PDF_MAGIC, VIDEO_CONVERSION_HINTS, classifyMime, isAllowedMime, maxBytesFor, getConversionHint, PRODUCT_MAX_IMAGES, PRODUCT_MAX_VIDEOS, PRODUCT_IMAGE_INDEX_MAX, } from "./_internal/shared/media/limits";
3144
3144
  export type { MediaKind, AllowedImageMime, AllowedVideoMime, AllowedDocMime, AllowedMime, } from "./_internal/shared/media/limits";
3145
3145
  export { PRODUCT_FIELDS, PRODUCT_STATUS_TRANSITIONS, ORDER_FIELDS, REVIEW_FIELDS, BID_FIELDS, AD_FIELDS, AD_TRACKED_FIELDS, BLOG_FIELDS, BRAND_FIELDS, WISHLIST_FIELDS, HISTORY_FIELDS, COUPON_USAGE_FIELDS, CAROUSEL_FIELDS, FAQ_FIELDS, HOMEPAGE_SECTION_FIELDS, SITE_SETTINGS_FIELDS, COMMON_FIELDS, OAUTH_STATE_VALUES, SCHEMA_DEFAULTS, } from "./constants/field-names";
3146
3146
  export { SORT_DIR, sortBy } from "./constants/sort";
package/dist/index.js CHANGED
@@ -5509,7 +5509,7 @@ export { LISTING_TYPE_REGISTRY, pluginFor, detectListingTypeFromSlug } from "./_
5509
5509
  // Checkout rule registry â€" pure TS, safe in both client and server bundles.
5510
5510
  export { getListingRule, getCategoryRule, pickOrderType, getSplitKey, runSyncPreflight, CHECKOUT_RULES, CATEGORY_CHECKOUT_RULES, CHECKOUT_MAX_ORDERS_PER_TX, PRIZE_DRAW_MAX_REVEALS_PER_ORDER, BUNDLE_MAX_QTY_PER_TX, STANDARD_MAX_QTY_PER_LINE, } from "./_internal/shared/checkout/rules";
5511
5511
  // Media upload limits â€" pure constants, safe in both client and server bundles.
5512
- export { MEGABYTE, MAX_IMAGE_BYTES, MAX_PDF_BYTES, MAX_VIDEO_BYTES, MAX_LABEL, MAX_BYTES, ALLOWED_IMAGE_MIMES, ALLOWED_VIDEO_MIMES, ALLOWED_DOC_MIMES, ALLOWED_MIMES, ALLOWED_TYPES_LABEL, MIME_TO_EXT, PDF_MAGIC, VIDEO_CONVERSION_HINTS, classifyMime, isAllowedMime, maxBytesFor, getConversionHint, PRODUCT_MAX_IMAGES, PRODUCT_MAX_VIDEOS, PRODUCT_IMAGE_INDEX_MAX, } from "./_internal/shared/media/limits";
5512
+ export { MEGABYTE, MAX_IMAGE_BYTES, MAX_PDF_BYTES, MAX_VIDEO_BYTES, MAX_LABEL, MAX_BYTES, ALLOWED_IMAGE_MIMES, IMAGE_ACCEPT_ATTR, ALLOWED_VIDEO_MIMES, ALLOWED_DOC_MIMES, ALLOWED_MIMES, ALLOWED_TYPES_LABEL, MIME_TO_EXT, PDF_MAGIC, VIDEO_CONVERSION_HINTS, classifyMime, isAllowedMime, maxBytesFor, getConversionHint, PRODUCT_MAX_IMAGES, PRODUCT_MAX_VIDEOS, PRODUCT_IMAGE_INDEX_MAX, } from "./_internal/shared/media/limits";
5513
5513
  // ── Field name constants — canonical source of truth for all Firestore field names ──
5514
5514
  // NOTE: STORE_FIELDS, CATEGORY_FIELDS, EVENT_FIELDS, EVENT_ENTRY_FIELDS, PAYOUT_FIELDS,
5515
5515
  // COUPON_FIELDS, CART_FIELDS, USER_FIELDS, ADDRESS_FIELDS, NOTIFICATION_FIELDS,
package/dist/server.d.ts CHANGED
@@ -528,7 +528,7 @@ export { LISTING_TYPE_REGISTRY, pluginFor, detectListingTypeFromSlug } from "./_
528
528
  export type { ListingTypePlugin } from "./_internal/shared/listing-types/_registry";
529
529
  export { applyMediaContextGuards, CONTEXT_LIMITS, } from "./_internal/server/features/media/contextGuards";
530
530
  export type { GuardResult, GuardError, GuardSuccess } from "./_internal/server/features/media/contextGuards";
531
- export { MEGABYTE, MAX_IMAGE_BYTES, MAX_PDF_BYTES, MAX_VIDEO_BYTES, MAX_LABEL, MAX_BYTES, ALLOWED_IMAGE_MIMES, ALLOWED_VIDEO_MIMES, ALLOWED_DOC_MIMES, ALLOWED_MIMES, ALLOWED_TYPES_LABEL, MIME_TO_EXT, PDF_MAGIC, VIDEO_CONVERSION_HINTS, classifyMime, isAllowedMime, maxBytesFor, getConversionHint, PRODUCT_MAX_IMAGES, PRODUCT_MAX_VIDEOS, PRODUCT_IMAGE_INDEX_MAX, } from "./_internal/shared/media/limits";
531
+ export { MEGABYTE, MAX_IMAGE_BYTES, MAX_PDF_BYTES, MAX_VIDEO_BYTES, MAX_LABEL, MAX_BYTES, ALLOWED_IMAGE_MIMES, IMAGE_ACCEPT_ATTR, ALLOWED_VIDEO_MIMES, ALLOWED_DOC_MIMES, ALLOWED_MIMES, ALLOWED_TYPES_LABEL, MIME_TO_EXT, PDF_MAGIC, VIDEO_CONVERSION_HINTS, classifyMime, isAllowedMime, maxBytesFor, getConversionHint, PRODUCT_MAX_IMAGES, PRODUCT_MAX_VIDEOS, PRODUCT_IMAGE_INDEX_MAX, } from "./_internal/shared/media/limits";
532
532
  export type { MediaKind, AllowedImageMime, AllowedVideoMime, AllowedDocMime, AllowedMime, } from "./_internal/shared/media/limits";
533
533
  export { getServerPermissions, checkPermission, checkAnyPermission, makeAdminSectionLayout, } from "./_internal/server/features/auth/permissions";
534
534
  export type { ResolvedPermissions, AdminSectionLayoutOpts, } from "./_internal/server/features/auth/permissions";
@@ -566,6 +566,8 @@ export { renderLiveItemOg, renderLiveItemOgImage, renderLiveItemOgFromDoc, type
566
566
  export { getLotteryEventCached, listLotteryEvents, getLotteryEntriesForAdmin, getLotteryEntriesForUser } from "./_internal/server/features/lottery/data";
567
567
  export type { LotteryEventClient } from "./_internal/server/features/lottery/data";
568
568
  export { toClientLotterySlot, toClientLotteryConfig } from "./_internal/server/features/lottery/adapters";
569
+ export { toPublicEvent, toClientSpinPrize } from "./_internal/server/features/events/adapters";
570
+ export type { ClientSpinPrize } from "./_internal/server/features/events/adapters";
569
571
  export { buildLotteryMetadata } from "./_internal/server/features/lottery/metadata";
570
572
  export { submitLotteryPullAction, flagLotteryEntryAction, reopenLotterySlotAction, createLotteryEventAction, updateLotteryEventAction, setProductLotteryModeAction, } from "./_internal/server/features/lottery/actions";
571
573
  export { lotteryEntryRepository } from "./features/lottery/repository/lottery-entry.repository";
package/dist/server.js CHANGED
@@ -1407,7 +1407,7 @@ export { LISTING_TYPE_REGISTRY, pluginFor, detectListingTypeFromSlug } from "./_
1407
1407
  // Media context guards — shared by /api/media/sign + legacy /api/media/upload.
1408
1408
  export { applyMediaContextGuards, CONTEXT_LIMITS, } from "./_internal/server/features/media/contextGuards";
1409
1409
  // Media upload limits — shared by /api/media/sign + /api/media/finalize + /api/media/upload.
1410
- export { MEGABYTE, MAX_IMAGE_BYTES, MAX_PDF_BYTES, MAX_VIDEO_BYTES, MAX_LABEL, MAX_BYTES, ALLOWED_IMAGE_MIMES, ALLOWED_VIDEO_MIMES, ALLOWED_DOC_MIMES, ALLOWED_MIMES, ALLOWED_TYPES_LABEL, MIME_TO_EXT, PDF_MAGIC, VIDEO_CONVERSION_HINTS, classifyMime, isAllowedMime, maxBytesFor, getConversionHint, PRODUCT_MAX_IMAGES, PRODUCT_MAX_VIDEOS, PRODUCT_IMAGE_INDEX_MAX, } from "./_internal/shared/media/limits";
1410
+ export { MEGABYTE, MAX_IMAGE_BYTES, MAX_PDF_BYTES, MAX_VIDEO_BYTES, MAX_LABEL, MAX_BYTES, ALLOWED_IMAGE_MIMES, IMAGE_ACCEPT_ATTR, ALLOWED_VIDEO_MIMES, ALLOWED_DOC_MIMES, ALLOWED_MIMES, ALLOWED_TYPES_LABEL, MIME_TO_EXT, PDF_MAGIC, VIDEO_CONVERSION_HINTS, classifyMime, isAllowedMime, maxBytesFor, getConversionHint, PRODUCT_MAX_IMAGES, PRODUCT_MAX_VIDEOS, PRODUCT_IMAGE_INDEX_MAX, } from "./_internal/shared/media/limits";
1411
1411
  // ── RBAC server helpers ───────────────────────────────────────────────────────
1412
1412
  // getServerPermissions, checkPermission, makeAdminSectionLayout
1413
1413
  export { getServerPermissions, checkPermission, checkAnyPermission, makeAdminSectionLayout, } from "./_internal/server/features/auth/permissions";
@@ -1464,6 +1464,7 @@ export { renderLiveItemOg, renderLiveItemOgImage, renderLiveItemOgFromDoc } from
1464
1464
  // ── Lottery feature — server data + adapters + actions ──────────────────────────
1465
1465
  export { getLotteryEventCached, listLotteryEvents, getLotteryEntriesForAdmin, getLotteryEntriesForUser } from "./_internal/server/features/lottery/data";
1466
1466
  export { toClientLotterySlot, toClientLotteryConfig } from "./_internal/server/features/lottery/adapters";
1467
+ export { toPublicEvent, toClientSpinPrize } from "./_internal/server/features/events/adapters";
1467
1468
  export { buildLotteryMetadata } from "./_internal/server/features/lottery/metadata";
1468
1469
  export { submitLotteryPullAction, flagLotteryEntryAction, reopenLotterySlotAction, createLotteryEventAction, updateLotteryEventAction, setProductLotteryModeAction, } from "./_internal/server/features/lottery/actions";
1469
1470
  export { lotteryEntryRepository } from "./features/lottery/repository/lottery-entry.repository";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mohasinac/appkit",
3
- "version": "4.41.5",
3
+ "version": "4.41.7",
4
4
  "license": "MIT",
5
5
  "publishConfig": {
6
6
  "access": "public"