@mohasinac/appkit 4.41.6 → 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
  }
@@ -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.
@@ -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";
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";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mohasinac/appkit",
3
- "version": "4.41.6",
3
+ "version": "4.41.7",
4
4
  "license": "MIT",
5
5
  "publishConfig": {
6
6
  "access": "public"