@mohasinac/appkit 4.42.5 → 4.42.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.
@@ -19,6 +19,19 @@ export declare class AddressesRepository extends BaseRepository<AddressDocument>
19
19
  createWithId(id: string, data: Partial<AddressDocument>): Promise<AddressDocument>;
20
20
  update(id: string, data: Partial<AddressDocument>): Promise<AddressDocument>;
21
21
  listByOwner(ownerType: AddressOwnerType, ownerId: string): Promise<AddressDocument[]>;
22
+ /**
23
+ * 🛑 Requires the composite index `(ownerType ASC, createdAt DESC)`.
24
+ *
25
+ * The pre-existing `(ownerType, ownerId, createdAt DESC)` index does NOT serve
26
+ * this query: Firestore matches an index by its field sequence, and `ownerId`
27
+ * sits between the equality and the sort, so a query that filters on
28
+ * `ownerType` alone and orders by `createdAt` has no usable prefix. Without the
29
+ * two-field index this throws FAILED_PRECONDITION, which `mapToHttpError`
30
+ * renders as a **409** — and `/admin/address-clusters` then showed "Failed to
31
+ * load clusters." *and* "No shared addresses found." at the same time, so the
32
+ * page read as "there are no clusters" rather than as an error. Added
33
+ * 2026-10-02; see Recurrent Root Cause #2.
34
+ */
22
35
  listByOwnerType(ownerType: AddressOwnerType, limit?: number): Promise<AddressDocument[]>;
23
36
  countByOwner(ownerType: AddressOwnerType, ownerId: string): Promise<number>;
24
37
  createForOwner(ownerType: AddressOwnerType, ownerId: string, input: AddressCreateInput): Promise<AddressDocument>;
@@ -60,6 +60,19 @@ export class AddressesRepository extends BaseRepository {
60
60
  throw new DatabaseError(`Failed to list addresses for ${ownerType}:${ownerId}`, error);
61
61
  }
62
62
  }
63
+ /**
64
+ * 🛑 Requires the composite index `(ownerType ASC, createdAt DESC)`.
65
+ *
66
+ * The pre-existing `(ownerType, ownerId, createdAt DESC)` index does NOT serve
67
+ * this query: Firestore matches an index by its field sequence, and `ownerId`
68
+ * sits between the equality and the sort, so a query that filters on
69
+ * `ownerType` alone and orders by `createdAt` has no usable prefix. Without the
70
+ * two-field index this throws FAILED_PRECONDITION, which `mapToHttpError`
71
+ * renders as a **409** — and `/admin/address-clusters` then showed "Failed to
72
+ * load clusters." *and* "No shared addresses found." at the same time, so the
73
+ * page read as "there are no clusters" rather than as an error. Added
74
+ * 2026-10-02; see Recurrent Root Cause #2.
75
+ */
63
76
  async listByOwnerType(ownerType, limit = 500) {
64
77
  try {
65
78
  const snapshot = await this.getCollection()
@@ -91,8 +91,27 @@ export function AdminBundleEditorView({ bundleId, onSaved, onDeleted, scope = "a
91
91
  const brandsQuery = useQuery({
92
92
  queryKey: ["admin", "brands", "picker"],
93
93
  queryFn: async () => {
94
+ /*
95
+ * 🛑 `res.items`, NOT `res.data.items` — `apiClient` ALREADY unwraps the
96
+ * envelope (`ApiClient.ts` ends `return data.data as T`), so what arrives
97
+ * here is `{ items, total, page, pageSize, totalPages, hasMore }` and
98
+ * there is no second `.data` to reach through.
99
+ *
100
+ * This read was `res?.data?.items ?? []` behind a hand-written
101
+ * `as { data?: { items?: CategoryDocument[] } }`. The cast cannot be wrong
102
+ * at runtime — it only silenced the one check that would have caught it —
103
+ * and `?? []` turned the miss into an empty list rather than an error. So
104
+ * the Brand select offered ONLY "No specific brand", for every admin,
105
+ * forever: verified in production 2026-10-02 by polling the live
106
+ * `<select name="brandSlug">` for 15s (optionCount stayed 1) while
107
+ * `GET /api/admin/brands` returned 200 with 5 brand rows in `data.items`.
108
+ *
109
+ * Nothing was wrong with the endpoint, the query, the memo or its
110
+ * dependency array — I checked each before concluding. Root Cause #98's
111
+ * family: a cast at a boundary asserting a shape that is not there.
112
+ */
94
113
  const res = (await apiClient.get(ADMIN_ENDPOINTS.BRANDS));
95
- return res?.data?.items ?? [];
114
+ return res?.items ?? [];
96
115
  },
97
116
  });
98
117
  const brandOptions = React.useMemo(() => [
@@ -105,8 +124,28 @@ export function AdminBundleEditorView({ bundleId, onSaved, onDeleted, scope = "a
105
124
  const bundleQuery = useQuery({
106
125
  queryKey: ["bundle", scope, bundleId],
107
126
  queryFn: async () => {
127
+ /*
128
+ * 🛑 The response IS the bundle — do not reach through a second `.data`.
129
+ *
130
+ * Same defect as `brandsQuery` above and far more dangerous. This was
131
+ * `res?.data ?? null` behind `as { data?: CategoryDocument }`, so every
132
+ * edit of an existing bundle resolved to `null`, `bundleToForm(null)`
133
+ * returned `EMPTY_FORM`, and the editor rendered BLANK — then saving it
134
+ * would have written that blank over a live bundle.
135
+ *
136
+ * Verified in production 2026-10-02: /admin/bundles/
137
+ * bundle-every-generation-starter-pack/edit polled for 14s with
138
+ * `input[name="name"]` and `input[name="priceRupees"]` both `""` and no
139
+ * member count, while `GET /api/admin/bundles/{id}` returned 200 with the
140
+ * real bundle (`{ success, data: <bundle> }`, no nested `data`). I did not
141
+ * press Save.
142
+ *
143
+ * Root Cause #98: an `ActionResult`/envelope spread as if it were the
144
+ * payload, where the destructive half is the status field silently
145
+ * defaulting on the way back out.
146
+ */
108
147
  const res = (await apiClient.get(endpoints.byId(encodeURIComponent(bundleId))));
109
- return res?.data ?? null;
148
+ return res ?? null;
110
149
  },
111
150
  enabled: isEdit,
112
151
  });
@@ -10,7 +10,7 @@ export declare const BUNDLE_RULE_TYPE_OPTIONS: {
10
10
  */
11
11
  export declare const bundleFormSchema: z.ZodEffects<z.ZodObject<{
12
12
  name: z.ZodString;
13
- priceRupees: z.ZodEffects<z.ZodString, string, string>;
13
+ priceRupees: z.ZodEffects<z.ZodNumber, number, number>;
14
14
  description: z.ZodUnion<[z.ZodOptional<z.ZodString>, z.ZodLiteral<"">]>;
15
15
  ruleType: z.ZodEnum<["static", "dynamic"]>;
16
16
  productIds: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
@@ -61,7 +61,7 @@ export declare const bundleFormSchema: z.ZodEffects<z.ZodObject<{
61
61
  }, "strip", z.ZodTypeAny, {
62
62
  name: string;
63
63
  isActive: boolean;
64
- priceRupees: string;
64
+ priceRupees: number;
65
65
  ruleType: "static" | "dynamic";
66
66
  brandSlug?: string | undefined;
67
67
  description?: string | undefined;
@@ -81,7 +81,7 @@ export declare const bundleFormSchema: z.ZodEffects<z.ZodObject<{
81
81
  }, {
82
82
  name: string;
83
83
  isActive: boolean;
84
- priceRupees: string;
84
+ priceRupees: number;
85
85
  ruleType: "static" | "dynamic";
86
86
  brandSlug?: string | undefined;
87
87
  description?: string | undefined;
@@ -101,7 +101,7 @@ export declare const bundleFormSchema: z.ZodEffects<z.ZodObject<{
101
101
  }>, {
102
102
  name: string;
103
103
  isActive: boolean;
104
- priceRupees: string;
104
+ priceRupees: number;
105
105
  ruleType: "static" | "dynamic";
106
106
  brandSlug?: string | undefined;
107
107
  description?: string | undefined;
@@ -121,7 +121,7 @@ export declare const bundleFormSchema: z.ZodEffects<z.ZodObject<{
121
121
  }, {
122
122
  name: string;
123
123
  isActive: boolean;
124
- priceRupees: string;
124
+ priceRupees: number;
125
125
  ruleType: "static" | "dynamic";
126
126
  brandSlug?: string | undefined;
127
127
  description?: string | undefined;
@@ -73,10 +73,39 @@ export const bundleFormSchema = z
73
73
  section: "basics", sectionLabel: "Bundle", sectionRequired: true,
74
74
  quick: true, order: 1, row: "pair", label: "Name",
75
75
  }),
76
- priceRupees: annotate(z
77
- .string()
78
- .regex(/^\d+(\.\d{1,2})?$/, BUNDLE_COPY.adminEditor.errors.priceInvalid)
79
- .refine((v) => Number(v) > 0, BUNDLE_COPY.adminEditor.errors.priceInvalid), {
76
+ /*
77
+ * 🛑 `z.coerce.number()`, NOT `z.string()`, because `kind: "number"` below
78
+ * decides the runtime type and the schema does not get a vote.
79
+ *
80
+ * `build-sections.tsx`'s number control is
81
+ * `onChange={(v) => set(v === "" ? undefined : Number(v))}` — it writes a
82
+ * NUMBER into the draft on every keystroke. This field used to be
83
+ * `z.string().regex(...)`, so a save with 1500 visibly in the box was
84
+ * refused with "This field is required" sitting directly under it, and the
85
+ * summary read "Bundle: This field is required" — naming the section and no
86
+ * field. A bundle therefore could not be created at all. Verified in
87
+ * production 2026-10-02 by walking up from the role=alert element to its
88
+ * owner, `input[name="priceRupees"]`.
89
+ *
90
+ * Do NOT "fix" this by removing the coercion in `build-sections.tsx`:
91
+ * ~10 of the 30 `kind: "number"` fields are plain `z.number()` and depend on
92
+ * it. Two other fields still declare `z.string()` under the same control and
93
+ * have the same latent defect — `purchasedItemNumber` (admin-user-form) and
94
+ * `maxBudget` (item-request-create-form); neither is driven by a test yet,
95
+ * so neither was changed blind.
96
+ *
97
+ * `coerce` keeps every existing caller working: `EMPTY_FORM.priceRupees` is
98
+ * `""` (→ the control sends `undefined` → "This field is required", which is
99
+ * the right message for an empty price), `bundleToForm` supplies a string,
100
+ * and the submit still does `Number(form.priceRupees)`.
101
+ *
102
+ * The two-decimal rule is kept as a refine over `String(n)` rather than
103
+ * `multipleOf(0.01)`, which is not float-safe.
104
+ */
105
+ priceRupees: annotate(z.coerce
106
+ .number({ message: BUNDLE_COPY.adminEditor.errors.priceInvalid })
107
+ .positive(BUNDLE_COPY.adminEditor.errors.priceInvalid)
108
+ .refine((n) => Number.isFinite(n) && /^\d+(\.\d{1,2})?$/.test(String(n)), BUNDLE_COPY.adminEditor.errors.priceInvalid), {
80
109
  section: "basics", quick: true, order: 2, row: "pair",
81
110
  kind: "number", label: "Bundle price (₹)",
82
111
  help: "What the buyer pays for the whole bundle — normally less than the members' total.",
@@ -4,6 +4,7 @@ import { useRef, useState, useEffect, useCallback } from "react";
4
4
  import Link from "next/link";
5
5
  import { Div, Li, Nav, Row, Span, Ul } from "../../ui";
6
6
  import { NavbarChevron } from "../../ui/components/NavbarChevron";
7
+ import { findActiveNavItem } from "../../_internal/client/features/layout/navActive";
7
8
  const __O = {
8
9
  hidden: "overflow-hidden",
9
10
  };
@@ -48,8 +49,28 @@ export function NavbarLayout({ items, activeHref, id = "main-navbar", ariaLabel
48
49
  };
49
50
  }, [updateArrows, items]);
50
51
  const scroll = (dir) => scrollRef.current?.scrollBy({ left: dir === "left" ? -240 : 240, behavior: "smooth" });
52
+ // 🛑 Resolve the active item with `findActiveNavItem`, NOT `activeHref === item.href`.
53
+ //
54
+ // Strict equality is what this used, and it meant the top navbar marked
55
+ // nothing at all on any detail page: /products marked "Products", but
56
+ // /products/product-beyblade-original-dranzer-s marked nothing — verified in
57
+ // production 2026-10-02, where the Products and Home links had byte-identical
58
+ // computed styles (colour rgb(91,91,99), weight 500, transparent background,
59
+ // 0px bottom border), so there was no visual marker either. A visitor who
60
+ // drills from a listing into an item loses their place in the navigation.
61
+ //
62
+ // The sidebars and the mobile drawer were always correct, because they
63
+ // already called this helper — which is why the mobile bottom nav highlighted
64
+ // "Auctions" on /auctions while the desktop navbar above it highlighted
65
+ // nothing. One algorithm, four surfaces.
66
+ //
67
+ // `findActiveNavItem` prefix-matches (`activeHref.startsWith(`${href}/`)`) and
68
+ // resolves ties by LONGEST href, so a root entry like "/" cannot light up on
69
+ // every page and a more specific entry always beats a shorter one.
70
+ const activeItem = findActiveNavItem(items, activeHref);
71
+ const isItemActive = (item) => activeItem?.href === item.href;
51
72
  if (inline) {
52
- return (_jsx(Ul, { "aria-label": ariaLabel, className: "hidden xl:flex items-center justify-center gap-[var(--appkit-space-0-5)] xl:gap-[var(--appkit-space-1)] w-full", children: items.map((item) => (_jsx(Li, { children: renderItem ? (renderItem(item, activeHref === item.href)) : (_jsx(DefaultNavItem, { item: item, isActive: activeHref === item.href })) }, item.href))) }));
73
+ return (_jsx(Ul, { "aria-label": ariaLabel, className: "hidden xl:flex items-center justify-center gap-[var(--appkit-space-0-5)] xl:gap-[var(--appkit-space-1)] w-full", children: items.map((item) => (_jsx(Li, { children: renderItem ? (renderItem(item, isItemActive(item))) : (_jsx(DefaultNavItem, { item: item, isActive: isItemActive(item) })) }, item.href))) }));
53
74
  }
54
- return (_jsx(Nav, { border: "subtle", id: id, "aria-label": ariaLabel, className: "hidden lg:block bg-[var(--appkit-color-bg)]/95 backdrop-blur-md border-b", children: _jsxs(Row, { className: "container mx-auto sm:px-[var(--appkit-space-6)] lg:px-[var(--appkit-space-8)] max-w-[1920px] h-10", padding: "x-md", align: "center", reverse: "hand", children: [_jsxs(Div, { className: `relative flex-1 ${__O.hidden}`, children: [canScrollLeft && (_jsx(NavbarChevron, { direction: "left", onClick: () => scroll("left") })), _jsx(Ul, { ref: scrollRef, className: "flex items-center justify-center gap-[var(--appkit-space-0-5)] lg:gap-[var(--appkit-space-1)] overflow-x-auto [&::-webkit-scrollbar]:hidden [-ms-overflow-style:none] [scrollbar-width:none]", children: items.map((item) => (_jsx(Li, { className: "shrink-0", children: renderItem ? (renderItem(item, activeHref === item.href)) : (_jsx(DefaultNavItem, { item: item, isActive: activeHref === item.href })) }, item.href))) }), canScrollRight && (_jsx(NavbarChevron, { direction: "right", onClick: () => scroll("right") }))] }), rightSlot && _jsx(Div, { className: "shrink-0", children: rightSlot })] }) }));
75
+ return (_jsx(Nav, { border: "subtle", id: id, "aria-label": ariaLabel, className: "hidden lg:block bg-[var(--appkit-color-bg)]/95 backdrop-blur-md border-b", children: _jsxs(Row, { className: "container mx-auto sm:px-[var(--appkit-space-6)] lg:px-[var(--appkit-space-8)] max-w-[1920px] h-10", padding: "x-md", align: "center", reverse: "hand", children: [_jsxs(Div, { className: `relative flex-1 ${__O.hidden}`, children: [canScrollLeft && (_jsx(NavbarChevron, { direction: "left", onClick: () => scroll("left") })), _jsx(Ul, { ref: scrollRef, className: "flex items-center justify-center gap-[var(--appkit-space-0-5)] lg:gap-[var(--appkit-space-1)] overflow-x-auto [&::-webkit-scrollbar]:hidden [-ms-overflow-style:none] [scrollbar-width:none]", children: items.map((item) => (_jsx(Li, { className: "shrink-0", children: renderItem ? (renderItem(item, isItemActive(item))) : (_jsx(DefaultNavItem, { item: item, isActive: isItemActive(item) })) }, item.href))) }), canScrollRight && (_jsx(NavbarChevron, { direction: "right", onClick: () => scroll("right") }))] }), rightSlot && _jsx(Div, { className: "shrink-0", children: rightSlot })] }) }));
55
76
  }
@@ -1,63 +1,67 @@
1
- {
2
- "mediaEditor": {
3
- "cropTitle": "Crop Image",
4
- "cropInstruction": "Drag to reposition. Use the zoom slider or +/- keys to zoom.",
5
- "cropPreviewAlt": "Preview",
6
- "cropZoom": "Zoom",
7
- "cropZoomIn": "Zoom in",
8
- "cropZoomOut": "Zoom out",
9
- "cropWarningTitle": "Low zoom",
10
- "cropWarningMessage": "Zoom in for better quality.",
11
- "cropPosition": "Position",
12
- "cropReset": "Reset",
13
- "cropSave": "Save Crop",
14
- "cropCancel": "Cancel",
15
- "trimTitle": "Trim Video",
16
- "trimInstruction": "Set the start and end times, then click Trim.",
17
- "trimStart": "Start",
18
- "trimEnd": "End",
19
- "trimDuration": "Duration: {duration}s",
20
- "trimQuality": "Quality",
21
- "trimQualityLow": "Low",
22
- "trimQualityMedium": "Medium",
23
- "trimQualityHigh": "High",
24
- "trimSave": "Trim Video",
25
- "trimSkip": "Skip",
26
- "trimming": "Trimming\u2026",
27
- "thumbnailTitle": "Select Thumbnail",
28
- "thumbnailInstruction": "Seek to the frame you want to use as the thumbnail.",
29
- "thumbnailCapture": "Use This Frame",
30
- "thumbnailSkip": "Skip",
31
- "thumbnailCapturing": "Capturing\u2026",
32
- "thumbnailUploading": "Uploading\u2026",
33
- "cropSuccess": "Image cropped",
34
- "trimSuccess": "Video trimmed",
35
- "thumbnailSuccess": "Thumbnail saved",
36
- "cropError": "Failed to crop image",
37
- "trimError": "Failed to trim video",
38
- "thumbnailError": "Failed to capture thumbnail",
39
- "editImage": "Edit / Crop",
40
- "editVideo": "Trim / Thumbnail"
41
- },
42
- "camera": {
43
- "takePhoto": "Take Photo",
44
- "startRecording": "Start Recording",
45
- "stopRecording": "Stop Recording",
46
- "flipCamera": "Flip Camera",
47
- "switchToCamera": "Use Camera",
48
- "switchToUpload": "Upload File",
49
- "cameraError": "Camera unavailable",
50
- "permissionDenied": "Camera permission denied. Please allow access in your browser settings.",
51
- "notSupported": "Camera is not supported on this device",
52
- "starting": "Starting camera..."
53
- },
54
- "upload": {
55
- "change": "Change",
56
- "remove": "Remove",
57
- "clickToUpload": "Click to upload",
58
- "uploading": "Uploading\u2026",
59
- "uploadingProgress": "Uploading {progress}%\u2026",
60
- "replaceFile": "Replace file",
61
- "chooseFile": "Choose file to upload"
62
- }
63
- }
1
+ {
2
+ "mediaEditor": {
3
+ "cropTitle": "Crop Image",
4
+ "cropInstruction": "Drag to reposition. Use the zoom slider or +/- keys to zoom.",
5
+ "cropPreviewAlt": "Preview",
6
+ "cropZoom": "Zoom",
7
+ "cropZoomIn": "Zoom in",
8
+ "cropZoomOut": "Zoom out",
9
+ "cropRotateLeft": "Rotate left 90°",
10
+ "cropRotateRight": "Rotate right 90°",
11
+ "cropFlipH": "Flip horizontally",
12
+ "cropFlipV": "Flip vertically",
13
+ "cropWarningTitle": "Low zoom",
14
+ "cropWarningMessage": "Zoom in for better quality.",
15
+ "cropPosition": "Position",
16
+ "cropReset": "Reset",
17
+ "cropSave": "Save Crop",
18
+ "cropCancel": "Cancel",
19
+ "trimTitle": "Trim Video",
20
+ "trimInstruction": "Set the start and end times, then click Trim.",
21
+ "trimStart": "Start",
22
+ "trimEnd": "End",
23
+ "trimDuration": "Duration: {duration}s",
24
+ "trimQuality": "Quality",
25
+ "trimQualityLow": "Low",
26
+ "trimQualityMedium": "Medium",
27
+ "trimQualityHigh": "High",
28
+ "trimSave": "Trim Video",
29
+ "trimSkip": "Skip",
30
+ "trimming": "Trimming…",
31
+ "thumbnailTitle": "Select Thumbnail",
32
+ "thumbnailInstruction": "Seek to the frame you want to use as the thumbnail.",
33
+ "thumbnailCapture": "Use This Frame",
34
+ "thumbnailSkip": "Skip",
35
+ "thumbnailCapturing": "Capturing…",
36
+ "thumbnailUploading": "Uploading…",
37
+ "cropSuccess": "Image cropped",
38
+ "trimSuccess": "Video trimmed",
39
+ "thumbnailSuccess": "Thumbnail saved",
40
+ "cropError": "Failed to crop image",
41
+ "trimError": "Failed to trim video",
42
+ "thumbnailError": "Failed to capture thumbnail",
43
+ "editImage": "Edit / Crop",
44
+ "editVideo": "Trim / Thumbnail"
45
+ },
46
+ "camera": {
47
+ "takePhoto": "Take Photo",
48
+ "startRecording": "Start Recording",
49
+ "stopRecording": "Stop Recording",
50
+ "flipCamera": "Flip Camera",
51
+ "switchToCamera": "Use Camera",
52
+ "switchToUpload": "Upload File",
53
+ "cameraError": "Camera unavailable",
54
+ "permissionDenied": "Camera permission denied. Please allow access in your browser settings.",
55
+ "notSupported": "Camera is not supported on this device",
56
+ "starting": "Starting camera..."
57
+ },
58
+ "upload": {
59
+ "change": "Change",
60
+ "remove": "Remove",
61
+ "clickToUpload": "Click to upload",
62
+ "uploading": "Uploading…",
63
+ "uploadingProgress": "Uploading {progress}%…",
64
+ "replaceFile": "Replace file",
65
+ "chooseFile": "Choose file to upload"
66
+ }
67
+ }
@@ -64,6 +64,33 @@ export const MEDIA_URL_MESSAGE = "Must be a stored media reference (/media/<slug
64
64
  * validated before stops validating now.
65
65
  */
66
66
  export function isStoredMediaRef(value) {
67
+ /*
68
+ * 🛑 Type-guard first: this predicate is reached through `.refine()` from two
69
+ * schemas and from several hand-written call sites, so it receives whatever
70
+ * the document actually holds — not whatever the signature says.
71
+ *
72
+ * Without this line a non-string reached `value.startsWith(...)` and threw
73
+ * `TypeError: e.startsWith is not a function`, which killed the whole render
74
+ * of the panel it was validating. Measured on production 2026-10-02: expanding
75
+ * the Branding section of /admin/site threw exactly that and the section body
76
+ * never appeared — so the Site Settings editor looked like it rendered no
77
+ * fields for ANY section, and five checklist cases were blocked behind what
78
+ * read as a missing feature.
79
+ *
80
+ * `!value` already caught null/undefined/"" and 0; an object or an array is
81
+ * truthy with `length === undefined`, so `undefined > MAX` is false and
82
+ * execution fell straight through to `.startsWith`.
83
+ *
84
+ * A VALIDATOR MUST REJECT, NEVER THROW. Returning false is the correct answer
85
+ * for a non-string, and the warn keeps the real defect findable — some caller
86
+ * is passing the wrong shape (a `{ url }` object where its `.url` was meant
87
+ * is the obvious candidate) and that is a separate fix at the call site.
88
+ */
89
+ if (typeof value !== "string") {
90
+ // eslint-disable-next-line no-console -- client-side; the server path has its own recorder
91
+ console.warn("[isStoredMediaRef] expected a string, received", Object.prototype.toString.call(value));
92
+ return false;
93
+ }
67
94
  if (!value || value.length > MEDIA_URL_MAX_LENGTH)
68
95
  return false;
69
96
  if (value.startsWith(MEDIA_PROXY_PREFIX))
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mohasinac/appkit",
3
- "version": "4.42.5",
3
+ "version": "4.42.7",
4
4
  "license": "MIT",
5
5
  "publishConfig": {
6
6
  "access": "public"