@open-mercato/ui 0.6.3-develop.3838.1.7f2657a47c → 0.6.3-develop.3857.1.da89d7530c

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,8 +1,10 @@
1
1
  "use client";
2
2
  import { jsx, jsxs } from "react/jsx-runtime";
3
3
  import * as React from "react";
4
+ import { X } from "lucide-react";
4
5
  import { useT } from "@open-mercato/shared/lib/i18n/context";
5
6
  import { Button } from "../../primitives/button.js";
7
+ import { IconButton } from "../../primitives/icon-button.js";
6
8
  function normalizeOptions(input) {
7
9
  if (!Array.isArray(input)) return [];
8
10
  return input.map((option) => {
@@ -38,11 +40,16 @@ function ComboboxInput({
38
40
  resolveDescription,
39
41
  autoFocus,
40
42
  disabled = false,
41
- allowCustomValues = true
43
+ allowCustomValues = true,
44
+ clearable = false,
45
+ clearLabel
42
46
  }) {
43
47
  const t = useT();
44
48
  const resolvedPlaceholder = placeholder ?? t("ui.inputs.comboboxInput.placeholder", "Type to search...");
45
49
  const loadingLabel = t("ui.inputs.comboboxInput.loading", "Loading suggestions\u2026");
50
+ const resolvedClearLabel = clearLabel ?? t("ui.inputs.comboboxInput.clear", "Clear value");
51
+ const blurCloseDelayMs = 250;
52
+ const blurCloseMaxDelayMs = 1e3;
46
53
  const [input, setInput] = React.useState("");
47
54
  const [asyncOptions, setAsyncOptions] = React.useState([]);
48
55
  const [resolvedOptions, setResolvedOptions] = React.useState([]);
@@ -51,25 +58,23 @@ function ComboboxInput({
51
58
  const [showSuggestions, setShowSuggestions] = React.useState(false);
52
59
  const [selectedIndex, setSelectedIndex] = React.useState(-1);
53
60
  const inputRef = React.useRef(null);
61
+ const loadingRef = React.useRef(false);
62
+ const blurCloseTimerRef = React.useRef(null);
63
+ const blurClosePendingRef = React.useRef(false);
54
64
  const suppressOpenOnFocusRef = React.useRef(Boolean(autoFocus && !disabled));
55
65
  const eagerFallbackLoadedValueRef = React.useRef(null);
56
66
  const staticOptions = React.useMemo(
57
67
  () => normalizeOptions([...seedOptions ?? [], ...suggestions ?? []]),
58
68
  [seedOptions, suggestions]
59
69
  );
60
- const knownLabelValues = React.useMemo(() => {
61
- const set = /* @__PURE__ */ new Set();
62
- for (const option of [...staticOptions, ...asyncOptions, ...resolvedOptions]) {
63
- if (option.label && option.label !== option.value) set.add(option.value);
70
+ const { knownLabelValues, coveredOptionValues } = React.useMemo(() => {
71
+ const known = /* @__PURE__ */ new Set();
72
+ const covered = /* @__PURE__ */ new Set();
73
+ for (const opt of [...staticOptions, ...asyncOptions, ...resolvedOptions]) {
74
+ covered.add(opt.value);
75
+ if (opt.label && opt.label !== opt.value) known.add(opt.value);
64
76
  }
65
- return set;
66
- }, [staticOptions, asyncOptions, resolvedOptions]);
67
- const coveredOptionValues = React.useMemo(() => {
68
- const set = /* @__PURE__ */ new Set();
69
- for (const option of [...staticOptions, ...asyncOptions, ...resolvedOptions]) {
70
- set.add(option.value);
71
- }
72
- return set;
77
+ return { knownLabelValues: known, coveredOptionValues: covered };
73
78
  }, [staticOptions, asyncOptions, resolvedOptions]);
74
79
  const optionMap = React.useMemo(() => {
75
80
  const map = /* @__PURE__ */ new Map();
@@ -97,6 +102,19 @@ function ComboboxInput({
97
102
  const availableOptions = React.useMemo(() => {
98
103
  return Array.from(optionMap.values());
99
104
  }, [optionMap]);
105
+ React.useEffect(() => {
106
+ loadingRef.current = loading;
107
+ }, [loading]);
108
+ const clearBlurCloseTimer = React.useCallback(() => {
109
+ if (blurCloseTimerRef.current === null) return;
110
+ window.clearTimeout(blurCloseTimerRef.current);
111
+ blurCloseTimerRef.current = null;
112
+ }, []);
113
+ const resetBlurCloseState = React.useCallback(() => {
114
+ blurClosePendingRef.current = false;
115
+ clearBlurCloseTimer();
116
+ }, [clearBlurCloseTimer]);
117
+ React.useEffect(() => resetBlurCloseState, [resetBlurCloseState]);
100
118
  const filteredSuggestions = React.useMemo(() => {
101
119
  const query = input.toLowerCase().trim();
102
120
  if (!query) return availableOptions;
@@ -110,17 +128,17 @@ function ComboboxInput({
110
128
  if (!loadSuggestions || !touched || disabled) return;
111
129
  const query = input.trim();
112
130
  let cancelled = false;
113
- const handle = window.setTimeout(async () => {
131
+ const handle = window.setTimeout(() => {
114
132
  setLoading(true);
115
- try {
116
- const items = await loadSuggestions(query);
133
+ Promise.resolve().then(() => loadSuggestions(query)).then((items) => {
117
134
  if (!cancelled) {
118
135
  const normalized = normalizeOptions(items);
119
136
  setAsyncOptions((prev) => areOptionsEqual(prev, normalized) ? prev : normalized);
120
137
  }
121
- } finally {
138
+ }).catch(() => {
139
+ }).finally(() => {
122
140
  if (!cancelled) setLoading(false);
123
- }
141
+ });
124
142
  }, 200);
125
143
  return () => {
126
144
  cancelled = true;
@@ -174,6 +192,7 @@ function ComboboxInput({
174
192
  const selectValue = React.useCallback(
175
193
  (nextValue) => {
176
194
  if (disabled) return;
195
+ resetBlurCloseState();
177
196
  const trimmed = nextValue.trim();
178
197
  onChange(trimmed);
179
198
  const option = optionMap.get(trimmed);
@@ -181,7 +200,7 @@ function ComboboxInput({
181
200
  setShowSuggestions(false);
182
201
  setSelectedIndex(-1);
183
202
  },
184
- [disabled, onChange, optionMap]
203
+ [disabled, onChange, optionMap, resetBlurCloseState]
185
204
  );
186
205
  const findOptionForInput = React.useCallback(
187
206
  (raw) => {
@@ -198,6 +217,10 @@ function ComboboxInput({
198
217
  const confirmSelection = React.useCallback(
199
218
  (raw) => {
200
219
  if (disabled) return;
220
+ if (clearable && raw.trim() === "") {
221
+ selectValue("");
222
+ return;
223
+ }
201
224
  const option = findOptionForInput(raw);
202
225
  if (option) {
203
226
  selectValue(option.value);
@@ -215,8 +238,36 @@ function ComboboxInput({
215
238
  }
216
239
  selectValue(raw);
217
240
  },
218
- [allowCustomValues, disabled, findOptionForInput, optionMap, selectValue, value]
241
+ [allowCustomValues, clearable, disabled, findOptionForInput, optionMap, selectValue, value]
219
242
  );
243
+ const handleClear = React.useCallback(() => {
244
+ if (disabled) return;
245
+ selectValue("");
246
+ inputRef.current?.focus();
247
+ }, [disabled, selectValue]);
248
+ const closeAfterBlur = React.useCallback(() => {
249
+ blurCloseTimerRef.current = null;
250
+ if (disabled) return;
251
+ blurClosePendingRef.current = false;
252
+ confirmSelection(input);
253
+ setShowSuggestions(false);
254
+ setSelectedIndex(-1);
255
+ }, [confirmSelection, disabled, input]);
256
+ const attemptBlurClose = React.useCallback(() => {
257
+ blurCloseTimerRef.current = null;
258
+ if (disabled) return;
259
+ if (loadingRef.current) {
260
+ blurCloseTimerRef.current = window.setTimeout(closeAfterBlur, blurCloseMaxDelayMs);
261
+ return;
262
+ }
263
+ closeAfterBlur();
264
+ }, [blurCloseMaxDelayMs, closeAfterBlur, disabled]);
265
+ React.useEffect(() => {
266
+ if (!blurClosePendingRef.current) return;
267
+ if (loading) return;
268
+ clearBlurCloseTimer();
269
+ closeAfterBlur();
270
+ }, [clearBlurCloseTimer, closeAfterBlur, loading]);
220
271
  const handleKeyDown = React.useCallback(
221
272
  (event) => {
222
273
  if (disabled) return;
@@ -246,13 +297,17 @@ function ComboboxInput({
246
297
  },
247
298
  [confirmSelection, disabled, filteredSuggestions, input, selectValue, selectedIndex, showSuggestions]
248
299
  );
300
+ const showClearButton = clearable && !disabled && (value !== "" || input !== "");
249
301
  return /* @__PURE__ */ jsxs("div", { className: "relative w-full", children: [
250
302
  /* @__PURE__ */ jsx(
251
303
  "input",
252
304
  {
253
305
  ref: inputRef,
254
306
  type: "text",
255
- className: "w-full h-9 rounded-md border border-input bg-background px-3 text-sm shadow-xs transition-colors outline-none placeholder:text-muted-foreground focus-visible:shadow-focus focus-visible:border-foreground disabled:bg-bg-disabled disabled:border-border-disabled disabled:text-muted-foreground disabled:cursor-not-allowed",
307
+ className: [
308
+ "w-full h-9 rounded-md border border-input bg-background px-3 text-sm shadow-xs transition-colors outline-none placeholder:text-muted-foreground focus-visible:shadow-focus focus-visible:border-foreground disabled:bg-bg-disabled disabled:border-border-disabled disabled:text-muted-foreground disabled:cursor-not-allowed",
309
+ showClearButton ? "pr-9" : ""
310
+ ].filter(Boolean).join(" "),
256
311
  value: input,
257
312
  placeholder: resolvedPlaceholder,
258
313
  autoFocus,
@@ -264,6 +319,10 @@ function ComboboxInput({
264
319
  suppressOpenOnFocusRef.current = false;
265
320
  return;
266
321
  }
322
+ resetBlurCloseState();
323
+ if (loadSuggestions && availableOptions.length === 0) {
324
+ setLoading(true);
325
+ }
267
326
  setShowSuggestions(true);
268
327
  },
269
328
  onChange: (event) => {
@@ -274,13 +333,29 @@ function ComboboxInput({
274
333
  },
275
334
  onKeyDown: handleKeyDown,
276
335
  onBlur: () => {
277
- setTimeout(() => {
278
- if (disabled) return;
279
- confirmSelection(input);
280
- }, 200);
336
+ blurClosePendingRef.current = true;
337
+ clearBlurCloseTimer();
338
+ if (loadingRef.current) {
339
+ blurCloseTimerRef.current = window.setTimeout(closeAfterBlur, blurCloseMaxDelayMs);
340
+ return;
341
+ }
342
+ blurCloseTimerRef.current = window.setTimeout(attemptBlurClose, blurCloseDelayMs);
281
343
  }
282
344
  }
283
345
  ),
346
+ showClearButton ? /* @__PURE__ */ jsx(
347
+ IconButton,
348
+ {
349
+ type: "button",
350
+ variant: "ghost",
351
+ size: "xs",
352
+ "aria-label": resolvedClearLabel,
353
+ className: "absolute right-1 top-1/2 -translate-y-1/2",
354
+ onMouseDown: (event) => event.preventDefault(),
355
+ onClick: handleClear,
356
+ children: /* @__PURE__ */ jsx(X, { className: "size-3" })
357
+ }
358
+ ) : null,
284
359
  showSuggestions && !disabled && (loading || filteredSuggestions.length > 0) && /* @__PURE__ */ jsx("div", { className: "absolute z-popover w-full mt-1 rounded-md border border-input bg-popover p-2 shadow-md max-h-48 sm:max-h-60 overflow-auto", children: loading && touched ? /* @__PURE__ */ jsx("div", { className: "px-2 py-1.5 text-xs text-muted-foreground", children: loadingLabel }) : /* @__PURE__ */ jsx("div", { className: "flex flex-col gap-1", children: filteredSuggestions.map((option, index) => /* @__PURE__ */ jsxs(
285
360
  Button,
286
361
  {
@@ -292,7 +367,10 @@ function ComboboxInput({
292
367
  index === selectedIndex ? "bg-muted" : ""
293
368
  ].filter(Boolean).join(" "),
294
369
  onMouseDown: (event) => event.preventDefault(),
295
- onClick: () => selectValue(option.value),
370
+ onClick: () => {
371
+ resetBlurCloseState();
372
+ selectValue(option.value);
373
+ },
296
374
  onMouseEnter: () => setSelectedIndex(index),
297
375
  children: [
298
376
  /* @__PURE__ */ jsx("span", { className: "font-medium text-foreground", children: option.label }),
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../src/backend/inputs/ComboboxInput.tsx"],
4
- "sourcesContent": ["\"use client\"\n\nimport * as React from 'react'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport { Button } from '../../primitives/button'\n\nexport type ComboboxOption = {\n value: string\n label: string\n description?: string | null\n}\n\nexport type ComboboxInputProps = {\n value: string\n onChange: (next: string) => void\n placeholder?: string\n suggestions?: Array<string | ComboboxOption>\n // Options to hydrate the option map up front (typically the linked entity's\n // display fields, already present in a record-detail payload). Merged with\n // `suggestions` so a pre-selected value renders its label without interaction.\n seedOptions?: ComboboxOption[]\n loadSuggestions?: (query?: string) => Promise<Array<string | ComboboxOption>>\n // Eagerly resolve a pre-selected `value` to a human label when it is not\n // covered by `suggestions`/`seedOptions`/`loadSuggestions` results. Runs once\n // per value, before any user interaction. May be sync or async.\n resolveLabel?: (value: string) => string | Promise<string>\n resolveDescription?: (value: string) => string | null | undefined\n autoFocus?: boolean\n disabled?: boolean\n allowCustomValues?: boolean\n}\n\nfunction normalizeOptions(input?: Array<string | ComboboxOption>): ComboboxOption[] {\n if (!Array.isArray(input)) return []\n return input\n .map((option) => {\n if (typeof option === 'string') {\n const trimmed = option.trim()\n if (!trimmed) return null\n return { value: trimmed, label: trimmed }\n }\n const value = typeof option.value === 'string' ? option.value.trim() : ''\n if (!value) return null\n return {\n value,\n label: option.label?.trim() || value,\n description: option.description ?? null,\n }\n })\n .filter((option): option is ComboboxOption => !!option)\n}\n\nfunction areOptionsEqual(a: ComboboxOption[], b: ComboboxOption[]): boolean {\n if (a.length !== b.length) return false\n return a.every((option, index) => {\n const next = b[index]\n return option.value === next.value\n && option.label === next.label\n && (option.description ?? null) === (next.description ?? null)\n })\n}\n\nexport function ComboboxInput({\n value,\n onChange,\n placeholder,\n suggestions,\n seedOptions,\n loadSuggestions,\n resolveLabel,\n resolveDescription,\n autoFocus,\n disabled = false,\n allowCustomValues = true,\n}: ComboboxInputProps) {\n const t = useT()\n const resolvedPlaceholder = placeholder ?? t('ui.inputs.comboboxInput.placeholder', 'Type to search...')\n const loadingLabel = t('ui.inputs.comboboxInput.loading', 'Loading suggestions\u2026')\n const [input, setInput] = React.useState('')\n const [asyncOptions, setAsyncOptions] = React.useState<ComboboxOption[]>([])\n const [resolvedOptions, setResolvedOptions] = React.useState<ComboboxOption[]>([])\n const [loading, setLoading] = React.useState(false)\n const [touched, setTouched] = React.useState(false)\n const [showSuggestions, setShowSuggestions] = React.useState(false)\n const [selectedIndex, setSelectedIndex] = React.useState(-1)\n const inputRef = React.useRef<HTMLInputElement>(null)\n const suppressOpenOnFocusRef = React.useRef(Boolean(autoFocus && !disabled))\n const eagerFallbackLoadedValueRef = React.useRef<string | null>(null)\n\n const staticOptions = React.useMemo(\n () => normalizeOptions([...(seedOptions ?? []), ...(suggestions ?? [])]),\n [seedOptions, suggestions]\n )\n\n // Options whose label is genuinely known (not a self-mapping `value === label`\n // placeholder). Used to decide whether eager resolution still needs to run.\n const knownLabelValues = React.useMemo(() => {\n const set = new Set<string>()\n for (const option of [...staticOptions, ...asyncOptions, ...resolvedOptions]) {\n if (option.label && option.label !== option.value) set.add(option.value)\n }\n return set\n }, [staticOptions, asyncOptions, resolvedOptions])\n\n const coveredOptionValues = React.useMemo(() => {\n const set = new Set<string>()\n for (const option of [...staticOptions, ...asyncOptions, ...resolvedOptions]) {\n set.add(option.value)\n }\n return set\n }, [staticOptions, asyncOptions, resolvedOptions])\n\n const optionMap = React.useMemo(() => {\n const map = new Map<string, ComboboxOption>()\n const register = (option: ComboboxOption) => {\n const existing = map.get(option.value)\n // Prefer an entry that carries a real label over a self-mapping placeholder.\n if (!existing || (existing.label === existing.value && option.label !== option.value)) {\n map.set(option.value, option)\n }\n }\n staticOptions.forEach(register)\n asyncOptions.forEach(register)\n resolvedOptions.forEach(register)\n if (value) {\n const existing = map.get(value)\n if (!existing) {\n map.set(value, {\n value,\n label: value,\n description: resolveDescription?.(value) ?? null,\n })\n }\n }\n return map\n }, [asyncOptions, resolvedOptions, resolveDescription, staticOptions, value])\n\n const availableOptions = React.useMemo(() => {\n return Array.from(optionMap.values())\n }, [optionMap])\n\n const filteredSuggestions = React.useMemo(() => {\n const query = input.toLowerCase().trim()\n if (!query) return availableOptions\n return availableOptions.filter((option) => {\n const labelMatch = option.label.toLowerCase().includes(query)\n const descMatch = option.description?.toLowerCase().includes(query)\n return labelMatch || Boolean(descMatch)\n })\n }, [availableOptions, input])\n\n React.useEffect(() => {\n if (!loadSuggestions || !touched || disabled) return\n const query = input.trim()\n let cancelled = false\n const handle = window.setTimeout(async () => {\n setLoading(true)\n try {\n const items = await loadSuggestions(query)\n if (!cancelled) {\n const normalized = normalizeOptions(items)\n setAsyncOptions((prev) => areOptionsEqual(prev, normalized) ? prev : normalized)\n }\n } finally {\n if (!cancelled) setLoading(false)\n }\n }, 200)\n return () => {\n cancelled = true\n window.clearTimeout(handle)\n }\n }, [disabled, input, loadSuggestions, touched])\n\n // Eagerly resolve a pre-selected value to its label without requiring the user\n // to focus the field. Runs once per value when it is not already covered.\n const eagerResolveLabel = typeof resolveLabel === 'function' ? resolveLabel : undefined\n React.useEffect(() => {\n if (!value || disabled) return\n let cancelled = false\n const apply = (label?: string | null, description?: string | null) => {\n const clean = typeof label === 'string' ? label.trim() : ''\n if (cancelled || !clean || clean === value) return\n setResolvedOptions((prev) => {\n if (prev.some((option) => option.value === value && option.label === clean)) return prev\n return [...prev.filter((option) => option.value !== value), { value, label: clean, description: description ?? null }]\n })\n }\n if (eagerResolveLabel) {\n if (knownLabelValues.has(value)) return\n Promise.resolve()\n .then(() => eagerResolveLabel(value))\n .then((label) => apply(label, resolveDescription?.(value)))\n .catch(() => {})\n return () => { cancelled = true }\n }\n if (coveredOptionValues.has(value)) return\n if (eagerFallbackLoadedValueRef.current === value) return\n eagerFallbackLoadedValueRef.current = value\n // Fallback: pull the first page of async suggestions so a remount that lost\n // its option cache can still recover the label without user interaction.\n if (loadSuggestions) {\n setLoading(true)\n Promise.resolve()\n .then(() => loadSuggestions())\n .then((items) => {\n if (cancelled) return\n const normalized = normalizeOptions(items)\n setAsyncOptions((prev) => areOptionsEqual(prev, normalized) ? prev : normalized)\n })\n .catch(() => {})\n .finally(() => { if (!cancelled) setLoading(false) })\n }\n return () => { cancelled = true }\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [value, disabled, knownLabelValues, coveredOptionValues, eagerResolveLabel, loadSuggestions])\n\n // Sync input with value when value changes externally and input is not focused.\n React.useEffect(() => {\n if (document.activeElement !== inputRef.current) {\n const option = optionMap.get(value)\n setInput(option?.label ?? value ?? '')\n }\n }, [value, optionMap])\n\n const selectValue = React.useCallback(\n (nextValue: string) => {\n if (disabled) return\n const trimmed = nextValue.trim()\n onChange(trimmed)\n const option = optionMap.get(trimmed)\n setInput(option?.label ?? trimmed)\n setShowSuggestions(false)\n setSelectedIndex(-1)\n },\n [disabled, onChange, optionMap]\n )\n\n const findOptionForInput = React.useCallback(\n (raw: string): ComboboxOption | null => {\n const query = raw.trim().toLowerCase()\n if (!query) return null\n for (const option of optionMap.values()) {\n if (option.value === raw.trim()) return option\n if (option.label.toLowerCase() === query) return option\n }\n return null\n },\n [optionMap]\n )\n\n const confirmSelection = React.useCallback(\n (raw: string) => {\n if (disabled) return\n const option = findOptionForInput(raw)\n if (option) {\n selectValue(option.value)\n return\n }\n if (!allowCustomValues) {\n // Revert to the current value's label \u2014 but only if we actually know it.\n // Baking the raw value back in while eager resolution is still pending\n // would freeze a placeholder (e.g. a UUID) into the visible input.\n setShowSuggestions(false)\n const currentOption = optionMap.get(value)\n if (currentOption && currentOption.label !== currentOption.value) {\n setInput(currentOption.label)\n } else if (!value) {\n setInput('')\n }\n return\n }\n selectValue(raw)\n },\n [allowCustomValues, disabled, findOptionForInput, optionMap, selectValue, value]\n )\n\n const handleKeyDown = React.useCallback(\n (event: React.KeyboardEvent<HTMLInputElement>) => {\n if (disabled) return\n\n if (event.key === 'ArrowDown') {\n event.preventDefault()\n if (!showSuggestions) {\n setShowSuggestions(true)\n setSelectedIndex(0)\n } else {\n setSelectedIndex((prev) => Math.min(prev + 1, filteredSuggestions.length - 1))\n }\n } else if (event.key === 'ArrowUp') {\n event.preventDefault()\n setSelectedIndex((prev) => Math.max(prev - 1, -1))\n } else if (event.key === 'Enter') {\n event.preventDefault()\n if (selectedIndex >= 0 && filteredSuggestions[selectedIndex]) {\n selectValue(filteredSuggestions[selectedIndex].value)\n } else {\n confirmSelection(input)\n }\n } else if (event.key === 'Escape') {\n event.preventDefault()\n setShowSuggestions(false)\n setSelectedIndex(-1)\n }\n },\n [confirmSelection, disabled, filteredSuggestions, input, selectValue, selectedIndex, showSuggestions]\n )\n\n return (\n <div className=\"relative w-full\">\n {/* Use raw <input> here instead of the DS Input primitive: ComboboxInput's\n focus / suggestions-popup interplay relies on the trigger being a plain\n input element. The DS wrapper introduces a <div> that desyncs autocomplete\n on this specific surface. Keeps the rest of the form on Input primitive. */}\n <input\n ref={inputRef}\n type=\"text\"\n className=\"w-full h-9 rounded-md border border-input bg-background px-3 text-sm shadow-xs transition-colors outline-none placeholder:text-muted-foreground focus-visible:shadow-focus focus-visible:border-foreground disabled:bg-bg-disabled disabled:border-border-disabled disabled:text-muted-foreground disabled:cursor-not-allowed\"\n value={input}\n placeholder={resolvedPlaceholder}\n autoFocus={autoFocus}\n data-crud-focus-target=\"\"\n disabled={disabled}\n onFocus={() => {\n setTouched(true)\n if (suppressOpenOnFocusRef.current) {\n suppressOpenOnFocusRef.current = false\n return\n }\n setShowSuggestions(true)\n }}\n onChange={(event) => {\n setTouched(true)\n setInput(event.target.value)\n setShowSuggestions(true)\n setSelectedIndex(-1)\n }}\n onKeyDown={handleKeyDown}\n onBlur={() => {\n // Delay to allow click on suggestions\n setTimeout(() => {\n if (disabled) return\n confirmSelection(input)\n }, 200)\n }}\n />\n\n {showSuggestions && !disabled && (loading || filteredSuggestions.length > 0) && (\n <div className=\"absolute z-popover w-full mt-1 rounded-md border border-input bg-popover p-2 shadow-md max-h-48 sm:max-h-60 overflow-auto\">\n {loading && touched ? (\n <div className=\"px-2 py-1.5 text-xs text-muted-foreground\">{loadingLabel}</div>\n ) : (\n <div className=\"flex flex-col gap-1\">\n {filteredSuggestions.map((option, index) => (\n <Button\n key={option.value}\n type=\"button\"\n variant=\"ghost\"\n size=\"sm\"\n className={[\n 'w-full h-auto justify-start font-normal text-left flex flex-col items-start rounded-lg p-2',\n index === selectedIndex ? 'bg-muted' : '',\n ]\n .filter(Boolean)\n .join(' ')}\n onMouseDown={(event) => event.preventDefault()}\n onClick={() => selectValue(option.value)}\n onMouseEnter={() => setSelectedIndex(index)}\n >\n <span className=\"font-medium text-foreground\">{option.label}</span>\n {option.description ? (\n <span className=\"text-xs text-muted-foreground\">{option.description}</span>\n ) : null}\n </Button>\n ))}\n </div>\n )}\n </div>\n )}\n </div>\n )\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["\"use client\"\n\nimport * as React from 'react'\nimport { X } from 'lucide-react'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport { Button } from '../../primitives/button'\nimport { IconButton } from '../../primitives/icon-button'\n\nexport type ComboboxOption = {\n value: string\n label: string\n description?: string | null\n}\n\nexport type ComboboxInputProps = {\n value: string\n onChange: (next: string) => void\n placeholder?: string\n suggestions?: Array<string | ComboboxOption>\n // Options to hydrate the option map up front (typically the linked entity's\n // display fields, already present in a record-detail payload). Merged with\n // `suggestions` so a pre-selected value renders its label without interaction.\n seedOptions?: ComboboxOption[]\n loadSuggestions?: (query?: string) => Promise<Array<string | ComboboxOption>>\n // Eagerly resolve a pre-selected `value` to a human label when it is not\n // covered by `suggestions`/`seedOptions`/`loadSuggestions` results. Runs once\n // per value, before any user interaction. May be sync or async.\n resolveLabel?: (value: string) => string | Promise<string>\n resolveDescription?: (value: string) => string | null | undefined\n autoFocus?: boolean\n disabled?: boolean\n allowCustomValues?: boolean\n clearable?: boolean\n clearLabel?: string\n}\n\nfunction normalizeOptions(input?: Array<string | ComboboxOption>): ComboboxOption[] {\n if (!Array.isArray(input)) return []\n return input\n .map((option) => {\n if (typeof option === 'string') {\n const trimmed = option.trim()\n if (!trimmed) return null\n return { value: trimmed, label: trimmed }\n }\n const value = typeof option.value === 'string' ? option.value.trim() : ''\n if (!value) return null\n return {\n value,\n label: option.label?.trim() || value,\n description: option.description ?? null,\n }\n })\n .filter((option): option is ComboboxOption => !!option)\n}\n\nfunction areOptionsEqual(a: ComboboxOption[], b: ComboboxOption[]): boolean {\n if (a.length !== b.length) return false\n return a.every((option, index) => {\n const next = b[index]\n return option.value === next.value\n && option.label === next.label\n && (option.description ?? null) === (next.description ?? null)\n })\n}\n\nexport function ComboboxInput({\n value,\n onChange,\n placeholder,\n suggestions,\n seedOptions,\n loadSuggestions,\n resolveLabel,\n resolveDescription,\n autoFocus,\n disabled = false,\n allowCustomValues = true,\n clearable = false,\n clearLabel,\n}: ComboboxInputProps) {\n const t = useT()\n const resolvedPlaceholder = placeholder ?? t('ui.inputs.comboboxInput.placeholder', 'Type to search...')\n const loadingLabel = t('ui.inputs.comboboxInput.loading', 'Loading suggestions\u2026')\n const resolvedClearLabel = clearLabel ?? t('ui.inputs.comboboxInput.clear', 'Clear value')\n const blurCloseDelayMs = 250\n const blurCloseMaxDelayMs = 1000\n const [input, setInput] = React.useState('')\n const [asyncOptions, setAsyncOptions] = React.useState<ComboboxOption[]>([])\n const [resolvedOptions, setResolvedOptions] = React.useState<ComboboxOption[]>([])\n const [loading, setLoading] = React.useState(false)\n const [touched, setTouched] = React.useState(false)\n const [showSuggestions, setShowSuggestions] = React.useState(false)\n const [selectedIndex, setSelectedIndex] = React.useState(-1)\n const inputRef = React.useRef<HTMLInputElement>(null)\n const loadingRef = React.useRef(false)\n const blurCloseTimerRef = React.useRef<number | null>(null)\n const blurClosePendingRef = React.useRef(false)\n const suppressOpenOnFocusRef = React.useRef(Boolean(autoFocus && !disabled))\n const eagerFallbackLoadedValueRef = React.useRef<string | null>(null)\n\n const staticOptions = React.useMemo(\n () => normalizeOptions([...(seedOptions ?? []), ...(suggestions ?? [])]),\n [seedOptions, suggestions]\n )\n\n // Single pass over all option sources to build both coverage sets at once.\n // knownLabelValues: values with a genuine label (not a self-mapping placeholder) \u2014\n // used to decide whether eager resolution still needs to run.\n // coveredOptionValues: all values present in any source (even self-mapped).\n const { knownLabelValues, coveredOptionValues } = React.useMemo(() => {\n const known = new Set<string>()\n const covered = new Set<string>()\n for (const opt of [...staticOptions, ...asyncOptions, ...resolvedOptions]) {\n covered.add(opt.value)\n if (opt.label && opt.label !== opt.value) known.add(opt.value)\n }\n return { knownLabelValues: known, coveredOptionValues: covered }\n }, [staticOptions, asyncOptions, resolvedOptions])\n\n const optionMap = React.useMemo(() => {\n const map = new Map<string, ComboboxOption>()\n const register = (option: ComboboxOption) => {\n const existing = map.get(option.value)\n // Prefer an entry that carries a real label over a self-mapping placeholder.\n if (!existing || (existing.label === existing.value && option.label !== option.value)) {\n map.set(option.value, option)\n }\n }\n staticOptions.forEach(register)\n asyncOptions.forEach(register)\n resolvedOptions.forEach(register)\n if (value) {\n const existing = map.get(value)\n if (!existing) {\n map.set(value, {\n value,\n label: value,\n description: resolveDescription?.(value) ?? null,\n })\n }\n }\n return map\n }, [asyncOptions, resolvedOptions, resolveDescription, staticOptions, value])\n\n const availableOptions = React.useMemo(() => {\n return Array.from(optionMap.values())\n }, [optionMap])\n\n React.useEffect(() => {\n loadingRef.current = loading\n }, [loading])\n\n const clearBlurCloseTimer = React.useCallback(() => {\n if (blurCloseTimerRef.current === null) return\n window.clearTimeout(blurCloseTimerRef.current)\n blurCloseTimerRef.current = null\n }, [])\n\n const resetBlurCloseState = React.useCallback(() => {\n blurClosePendingRef.current = false\n clearBlurCloseTimer()\n }, [clearBlurCloseTimer])\n\n React.useEffect(() => resetBlurCloseState, [resetBlurCloseState])\n\n const filteredSuggestions = React.useMemo(() => {\n const query = input.toLowerCase().trim()\n if (!query) return availableOptions\n return availableOptions.filter((option) => {\n const labelMatch = option.label.toLowerCase().includes(query)\n const descMatch = option.description?.toLowerCase().includes(query)\n return labelMatch || Boolean(descMatch)\n })\n }, [availableOptions, input])\n\n React.useEffect(() => {\n if (!loadSuggestions || !touched || disabled) return\n const query = input.trim()\n let cancelled = false\n const handle = window.setTimeout(() => {\n setLoading(true)\n Promise.resolve()\n .then(() => loadSuggestions(query))\n .then((items) => {\n if (!cancelled) {\n const normalized = normalizeOptions(items)\n setAsyncOptions((prev) => areOptionsEqual(prev, normalized) ? prev : normalized)\n }\n })\n .catch(() => {})\n .finally(() => {\n if (!cancelled) setLoading(false)\n })\n }, 200)\n return () => {\n cancelled = true\n window.clearTimeout(handle)\n }\n }, [disabled, input, loadSuggestions, touched])\n\n // Eagerly resolve a pre-selected value to its label without requiring the user\n // to focus the field. Runs once per value when it is not already covered.\n const eagerResolveLabel = typeof resolveLabel === 'function' ? resolveLabel : undefined\n React.useEffect(() => {\n if (!value || disabled) return\n let cancelled = false\n const apply = (label?: string | null, description?: string | null) => {\n const clean = typeof label === 'string' ? label.trim() : ''\n if (cancelled || !clean || clean === value) return\n setResolvedOptions((prev) => {\n if (prev.some((option) => option.value === value && option.label === clean)) return prev\n return [...prev.filter((option) => option.value !== value), { value, label: clean, description: description ?? null }]\n })\n }\n if (eagerResolveLabel) {\n if (knownLabelValues.has(value)) return\n Promise.resolve()\n .then(() => eagerResolveLabel(value))\n .then((label) => apply(label, resolveDescription?.(value)))\n .catch(() => {})\n return () => { cancelled = true }\n }\n if (coveredOptionValues.has(value)) return\n if (eagerFallbackLoadedValueRef.current === value) return\n eagerFallbackLoadedValueRef.current = value\n // Fallback: pull the first page of async suggestions so a remount that lost\n // its option cache can still recover the label without user interaction.\n // Note: if the loader is paginated and the value falls outside the first page,\n // the fallback silently fails and the raw value remains visible.\n if (loadSuggestions) {\n setLoading(true)\n Promise.resolve()\n .then(() => loadSuggestions())\n .then((items) => {\n if (cancelled) return\n const normalized = normalizeOptions(items)\n setAsyncOptions((prev) => areOptionsEqual(prev, normalized) ? prev : normalized)\n })\n .catch(() => {})\n .finally(() => { if (!cancelled) setLoading(false) })\n }\n return () => { cancelled = true }\n // eslint-disable-next-line react-hooks/exhaustive-deps \u2014 resolveDescription intentionally excluded:\n // including it would re-run the effect on every render when the prop is an inline function\n }, [value, disabled, knownLabelValues, coveredOptionValues, eagerResolveLabel, loadSuggestions])\n\n // Sync input with value when value changes externally and input is not focused.\n React.useEffect(() => {\n if (document.activeElement !== inputRef.current) {\n const option = optionMap.get(value)\n setInput(option?.label ?? value ?? '')\n }\n }, [value, optionMap])\n\n const selectValue = React.useCallback(\n (nextValue: string) => {\n if (disabled) return\n resetBlurCloseState()\n const trimmed = nextValue.trim()\n onChange(trimmed)\n const option = optionMap.get(trimmed)\n setInput(option?.label ?? trimmed)\n setShowSuggestions(false)\n setSelectedIndex(-1)\n },\n [disabled, onChange, optionMap, resetBlurCloseState]\n )\n\n const findOptionForInput = React.useCallback(\n (raw: string): ComboboxOption | null => {\n const query = raw.trim().toLowerCase()\n if (!query) return null\n for (const option of optionMap.values()) {\n if (option.value === raw.trim()) return option\n if (option.label.toLowerCase() === query) return option\n }\n return null\n },\n [optionMap]\n )\n\n const confirmSelection = React.useCallback(\n (raw: string) => {\n if (disabled) return\n if (clearable && raw.trim() === '') {\n selectValue('')\n return\n }\n const option = findOptionForInput(raw)\n if (option) {\n selectValue(option.value)\n return\n }\n if (!allowCustomValues) {\n // Revert to the current value's label \u2014 but only if we actually know it.\n // Baking the raw value back in while eager resolution is still pending\n // would freeze a placeholder (e.g. a UUID) into the visible input.\n setShowSuggestions(false)\n const currentOption = optionMap.get(value)\n if (currentOption && currentOption.label !== currentOption.value) {\n setInput(currentOption.label)\n } else if (!value) {\n setInput('')\n }\n return\n }\n selectValue(raw)\n },\n [allowCustomValues, clearable, disabled, findOptionForInput, optionMap, selectValue, value]\n )\n\n const handleClear = React.useCallback(() => {\n if (disabled) return\n selectValue('')\n inputRef.current?.focus()\n }, [disabled, selectValue])\n\n const closeAfterBlur = React.useCallback(() => {\n blurCloseTimerRef.current = null\n if (disabled) return\n blurClosePendingRef.current = false\n confirmSelection(input)\n setShowSuggestions(false)\n setSelectedIndex(-1)\n }, [confirmSelection, disabled, input])\n\n const attemptBlurClose = React.useCallback(() => {\n blurCloseTimerRef.current = null\n if (disabled) return\n if (loadingRef.current) {\n blurCloseTimerRef.current = window.setTimeout(closeAfterBlur, blurCloseMaxDelayMs)\n return\n }\n closeAfterBlur()\n }, [blurCloseMaxDelayMs, closeAfterBlur, disabled])\n\n React.useEffect(() => {\n if (!blurClosePendingRef.current) return\n if (loading) return\n clearBlurCloseTimer()\n closeAfterBlur()\n }, [clearBlurCloseTimer, closeAfterBlur, loading])\n\n const handleKeyDown = React.useCallback(\n (event: React.KeyboardEvent<HTMLInputElement>) => {\n if (disabled) return\n\n if (event.key === 'ArrowDown') {\n event.preventDefault()\n if (!showSuggestions) {\n setShowSuggestions(true)\n setSelectedIndex(0)\n } else {\n setSelectedIndex((prev) => Math.min(prev + 1, filteredSuggestions.length - 1))\n }\n } else if (event.key === 'ArrowUp') {\n event.preventDefault()\n setSelectedIndex((prev) => Math.max(prev - 1, -1))\n } else if (event.key === 'Enter') {\n event.preventDefault()\n if (selectedIndex >= 0 && filteredSuggestions[selectedIndex]) {\n selectValue(filteredSuggestions[selectedIndex].value)\n } else {\n confirmSelection(input)\n }\n } else if (event.key === 'Escape') {\n event.preventDefault()\n setShowSuggestions(false)\n setSelectedIndex(-1)\n }\n },\n [confirmSelection, disabled, filteredSuggestions, input, selectValue, selectedIndex, showSuggestions]\n )\n\n const showClearButton = clearable && !disabled && (value !== '' || input !== '')\n\n return (\n <div className=\"relative w-full\">\n {/* Use raw <input> here instead of the DS Input primitive: ComboboxInput's\n focus / suggestions-popup interplay relies on the trigger being a plain\n input element. The DS wrapper introduces a <div> that desyncs autocomplete\n on this specific surface. Keeps the rest of the form on Input primitive. */}\n <input\n ref={inputRef}\n type=\"text\"\n className={[\n 'w-full h-9 rounded-md border border-input bg-background px-3 text-sm shadow-xs transition-colors outline-none placeholder:text-muted-foreground focus-visible:shadow-focus focus-visible:border-foreground disabled:bg-bg-disabled disabled:border-border-disabled disabled:text-muted-foreground disabled:cursor-not-allowed',\n showClearButton ? 'pr-9' : '',\n ]\n .filter(Boolean)\n .join(' ')}\n value={input}\n placeholder={resolvedPlaceholder}\n autoFocus={autoFocus}\n data-crud-focus-target=\"\"\n disabled={disabled}\n onFocus={() => {\n setTouched(true)\n if (suppressOpenOnFocusRef.current) {\n suppressOpenOnFocusRef.current = false\n return\n }\n resetBlurCloseState()\n if (loadSuggestions && availableOptions.length === 0) {\n setLoading(true)\n }\n setShowSuggestions(true)\n }}\n onChange={(event) => {\n setTouched(true)\n setInput(event.target.value)\n setShowSuggestions(true)\n setSelectedIndex(-1)\n }}\n onKeyDown={handleKeyDown}\n onBlur={() => {\n // Delay closing so clicks on the popup can resolve first. If async\n // suggestions are still loading, keep the dropdown open instead of\n // closing before the first payload arrives.\n blurClosePendingRef.current = true\n clearBlurCloseTimer()\n if (loadingRef.current) {\n blurCloseTimerRef.current = window.setTimeout(closeAfterBlur, blurCloseMaxDelayMs)\n return\n }\n blurCloseTimerRef.current = window.setTimeout(attemptBlurClose, blurCloseDelayMs)\n }}\n />\n\n {showClearButton ? (\n <IconButton\n type=\"button\"\n variant=\"ghost\"\n size=\"xs\"\n aria-label={resolvedClearLabel}\n className=\"absolute right-1 top-1/2 -translate-y-1/2\"\n onMouseDown={(event) => event.preventDefault()}\n onClick={handleClear}\n >\n <X className=\"size-3\" />\n </IconButton>\n ) : null}\n\n {showSuggestions && !disabled && (loading || filteredSuggestions.length > 0) && (\n <div className=\"absolute z-popover w-full mt-1 rounded-md border border-input bg-popover p-2 shadow-md max-h-48 sm:max-h-60 overflow-auto\">\n {loading && touched ? (\n <div className=\"px-2 py-1.5 text-xs text-muted-foreground\">{loadingLabel}</div>\n ) : (\n <div className=\"flex flex-col gap-1\">\n {filteredSuggestions.map((option, index) => (\n <Button\n key={option.value}\n type=\"button\"\n variant=\"ghost\"\n size=\"sm\"\n className={[\n 'w-full h-auto justify-start font-normal text-left flex flex-col items-start rounded-lg p-2',\n index === selectedIndex ? 'bg-muted' : '',\n ]\n .filter(Boolean)\n .join(' ')}\n onMouseDown={(event) => event.preventDefault()}\n onClick={() => {\n resetBlurCloseState()\n selectValue(option.value)\n }}\n onMouseEnter={() => setSelectedIndex(index)}\n >\n <span className=\"font-medium text-foreground\">{option.label}</span>\n {option.description ? (\n <span className=\"text-xs text-muted-foreground\">{option.description}</span>\n ) : null}\n </Button>\n ))}\n </div>\n )}\n </div>\n )}\n </div>\n )\n}\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -6,23 +6,33 @@ import { Button } from "../../primitives/button.js";
6
6
  import { apiCall } from "../utils/apiCall.js";
7
7
  import { flash } from "../FlashMessages.js";
8
8
  import { useT } from "@open-mercato/shared/lib/i18n/context";
9
- const upgradeActionsEnabled = process.env.NEXT_PUBLIC_UPGRADE_ACTIONS_ENABLED === "true" || process.env.UPGRADE_ACTIONS_ENABLED === "true";
9
+ import { useBackendChrome } from "../BackendChromeProvider.js";
10
+ import { hasFeature } from "@open-mercato/shared/security/features";
11
+ function upgradeActionsEnabled() {
12
+ return process.env.NEXT_PUBLIC_UPGRADE_ACTIONS_ENABLED === "true" || process.env.UPGRADE_ACTIONS_ENABLED === "true";
13
+ }
14
+ const forbiddenRedirectOptOutHeader = { "x-om-forbidden-redirect": "0" };
10
15
  function UpgradeActionBanner() {
11
16
  const t = useT();
17
+ const { payload, isReady } = useBackendChrome();
18
+ const canManageConfigs = isReady && hasFeature(payload?.grantedFeatures, "configs.manage");
12
19
  const [action, setAction] = React.useState(null);
13
20
  const [loading, setLoading] = React.useState(false);
14
21
  const cancelledRef = React.useRef(false);
15
22
  const loadNextAction = React.useCallback(async () => {
16
- if (!upgradeActionsEnabled) return;
23
+ if (!canManageConfigs) return;
24
+ if (!upgradeActionsEnabled()) return;
17
25
  if (typeof window === "undefined" || typeof fetch === "undefined") return;
18
- const call = await apiCall("/api/configs/upgrade-actions");
26
+ const call = await apiCall("/api/configs/upgrade-actions", {
27
+ headers: forbiddenRedirectOptOutHeader
28
+ });
19
29
  if (cancelledRef.current) return;
20
30
  if (!call.ok || !call.result || !Array.isArray(call.result.actions) || !call.result.actions.length) {
21
31
  setAction(null);
22
32
  return;
23
33
  }
24
34
  setAction(call.result.actions[0]);
25
- }, []);
35
+ }, [canManageConfigs]);
26
36
  React.useEffect(() => {
27
37
  cancelledRef.current = false;
28
38
  void loadNextAction();
@@ -30,14 +40,14 @@ function UpgradeActionBanner() {
30
40
  cancelledRef.current = true;
31
41
  };
32
42
  }, [loadNextAction]);
33
- if (!upgradeActionsEnabled || !action) return null;
43
+ if (!upgradeActionsEnabled() || !canManageConfigs || !action) return null;
34
44
  async function handleRun() {
35
- if (!upgradeActionsEnabled || !action || loading) return;
45
+ if (!upgradeActionsEnabled() || !action || loading) return;
36
46
  setLoading(true);
37
47
  try {
38
48
  const response = await apiCall("/api/configs/upgrade-actions", {
39
49
  method: "POST",
40
- headers: { "Content-Type": "application/json" },
50
+ headers: { ...forbiddenRedirectOptOutHeader, "Content-Type": "application/json" },
41
51
  body: JSON.stringify({ actionId: action.id })
42
52
  });
43
53
  if (!response.ok) {
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../src/backend/upgrades/UpgradeActionBanner.tsx"],
4
- "sourcesContent": ["\"use client\"\nimport * as React from 'react'\nimport { Sparkles } from 'lucide-react'\nimport { Button } from '../../primitives/button'\nimport { apiCall } from '../utils/apiCall'\nimport { flash } from '../FlashMessages'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\n\nconst upgradeActionsEnabled =\n process.env.NEXT_PUBLIC_UPGRADE_ACTIONS_ENABLED === 'true' ||\n process.env.UPGRADE_ACTIONS_ENABLED === 'true'\n\ntype UpgradeActionPayload = {\n id: string\n version: string\n message: string\n ctaLabel: string\n successMessage?: string\n loadingLabel?: string\n}\n\ntype UpgradeActionResponse = {\n version: string\n actions?: UpgradeActionPayload[]\n error?: string\n}\n\ntype RunActionResponse = {\n status?: 'completed' | 'already_completed'\n message?: string\n error?: string\n}\n\nexport function UpgradeActionBanner() {\n const t = useT()\n const [action, setAction] = React.useState<UpgradeActionPayload | null>(null)\n const [loading, setLoading] = React.useState(false)\n const cancelledRef = React.useRef(false)\n\n const loadNextAction = React.useCallback(async () => {\n if (!upgradeActionsEnabled) return\n if (typeof window === 'undefined' || typeof fetch === 'undefined') return\n const call = await apiCall<UpgradeActionResponse>('/api/configs/upgrade-actions')\n if (cancelledRef.current) return\n if (!call.ok || !call.result || !Array.isArray(call.result.actions) || !call.result.actions.length) {\n setAction(null)\n return\n }\n setAction(call.result.actions[0]!)\n }, [])\n\n React.useEffect(() => {\n cancelledRef.current = false\n void loadNextAction()\n return () => {\n cancelledRef.current = true\n }\n }, [loadNextAction])\n\n if (!upgradeActionsEnabled || !action) return null\n\n async function handleRun() {\n if (!upgradeActionsEnabled || !action || loading) return\n setLoading(true)\n try {\n const response = await apiCall<RunActionResponse>('/api/configs/upgrade-actions', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ actionId: action.id }),\n })\n if (!response.ok) {\n const baseError =\n (response.result && typeof response.result.error === 'string' && response.result.error) ||\n t('upgrades.runFailed', 'We could not run this upgrade action.')\n const detail = response.result && typeof (response.result as any).details === 'string' ? (response.result as any).details : null\n const errorMessage = detail ? `${baseError} (${detail})` : baseError\n flash(errorMessage, 'error')\n return\n }\n const message =\n response.result?.message ||\n action.successMessage ||\n t('upgrades.v034.success', 'Example catalog products and categories installed.')\n flash(message, 'success')\n setAction(null)\n await loadNextAction()\n } catch (error) {\n const message = error instanceof Error ? error.message : t('upgrades.runFailed', 'We could not run this upgrade action.')\n flash(message, 'error')\n } finally {\n setLoading(false)\n }\n }\n\n const loadingLabel = action.loadingLabel || t('upgrades.v034.loading', 'Installing\u2026')\n const title = action.ctaLabel || action.message\n const description = action.message && action.message !== title ? action.message : null\n\n return (\n <div className=\"mb-4 flex flex-col gap-3 rounded-md border border-amber-300 bg-amber-50 px-3 py-3 text-sm text-amber-900 md:flex-row md:items-center md:justify-between\">\n <div className=\"flex items-start gap-2 text-sm\">\n <Sparkles className=\"mt-0.5 size-4 text-amber-700\" aria-hidden=\"true\" />\n <div className=\"flex flex-col gap-1\">\n <div className=\"font-medium text-amber-950\">\n {title}\n </div>\n {description ? (\n <div className=\"text-xs text-amber-900/80\">\n {description}\n </div>\n ) : null}\n <div className=\"text-xs text-amber-900/80\">{t('upgrades.versionLabel', { version: action.version })}</div>\n </div>\n </div>\n <div className=\"flex flex-wrap items-center gap-2\">\n <Button\n variant=\"outline\"\n size=\"sm\"\n onClick={() => { void handleRun() }}\n disabled={loading}\n className=\"border-amber-300 text-amber-900 hover:bg-amber-100\"\n >\n {loading ? loadingLabel : action.ctaLabel}\n </Button>\n </div>\n </div>\n )\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["\"use client\"\nimport * as React from 'react'\nimport { Sparkles } from 'lucide-react'\nimport { Button } from '../../primitives/button'\nimport { apiCall } from '../utils/apiCall'\nimport { flash } from '../FlashMessages'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport { useBackendChrome } from '../BackendChromeProvider'\nimport { hasFeature } from '@open-mercato/shared/security/features'\n\nfunction upgradeActionsEnabled() {\n return (\n process.env.NEXT_PUBLIC_UPGRADE_ACTIONS_ENABLED === 'true' ||\n process.env.UPGRADE_ACTIONS_ENABLED === 'true'\n )\n}\n\ntype UpgradeActionPayload = {\n id: string\n version: string\n message: string\n ctaLabel: string\n successMessage?: string\n loadingLabel?: string\n}\n\ntype UpgradeActionResponse = {\n version: string\n actions?: UpgradeActionPayload[]\n error?: string\n}\n\ntype RunActionResponse = {\n status?: 'completed' | 'already_completed'\n message?: string\n error?: string\n}\n\nconst forbiddenRedirectOptOutHeader = { 'x-om-forbidden-redirect': '0' } as const\n\nexport function UpgradeActionBanner() {\n const t = useT()\n const { payload, isReady } = useBackendChrome()\n const canManageConfigs = isReady && hasFeature(payload?.grantedFeatures, 'configs.manage')\n const [action, setAction] = React.useState<UpgradeActionPayload | null>(null)\n const [loading, setLoading] = React.useState(false)\n const cancelledRef = React.useRef(false)\n\n const loadNextAction = React.useCallback(async () => {\n if (!canManageConfigs) return\n if (!upgradeActionsEnabled()) return\n if (typeof window === 'undefined' || typeof fetch === 'undefined') return\n const call = await apiCall<UpgradeActionResponse>('/api/configs/upgrade-actions', {\n headers: forbiddenRedirectOptOutHeader,\n })\n if (cancelledRef.current) return\n if (!call.ok || !call.result || !Array.isArray(call.result.actions) || !call.result.actions.length) {\n setAction(null)\n return\n }\n setAction(call.result.actions[0]!)\n }, [canManageConfigs])\n\n React.useEffect(() => {\n cancelledRef.current = false\n void loadNextAction()\n return () => {\n cancelledRef.current = true\n }\n }, [loadNextAction])\n\n if (!upgradeActionsEnabled() || !canManageConfigs || !action) return null\n\n async function handleRun() {\n if (!upgradeActionsEnabled() || !action || loading) return\n setLoading(true)\n try {\n const response = await apiCall<RunActionResponse>('/api/configs/upgrade-actions', {\n method: 'POST',\n headers: { ...forbiddenRedirectOptOutHeader, 'Content-Type': 'application/json' },\n body: JSON.stringify({ actionId: action.id }),\n })\n if (!response.ok) {\n const baseError =\n (response.result && typeof response.result.error === 'string' && response.result.error) ||\n t('upgrades.runFailed', 'We could not run this upgrade action.')\n const detail = response.result && typeof (response.result as any).details === 'string' ? (response.result as any).details : null\n const errorMessage = detail ? `${baseError} (${detail})` : baseError\n flash(errorMessage, 'error')\n return\n }\n const message =\n response.result?.message ||\n action.successMessage ||\n t('upgrades.v034.success', 'Example catalog products and categories installed.')\n flash(message, 'success')\n setAction(null)\n await loadNextAction()\n } catch (error) {\n const message = error instanceof Error ? error.message : t('upgrades.runFailed', 'We could not run this upgrade action.')\n flash(message, 'error')\n } finally {\n setLoading(false)\n }\n }\n\n const loadingLabel = action.loadingLabel || t('upgrades.v034.loading', 'Installing\u2026')\n const title = action.ctaLabel || action.message\n const description = action.message && action.message !== title ? action.message : null\n\n return (\n <div className=\"mb-4 flex flex-col gap-3 rounded-md border border-amber-300 bg-amber-50 px-3 py-3 text-sm text-amber-900 md:flex-row md:items-center md:justify-between\">\n <div className=\"flex items-start gap-2 text-sm\">\n <Sparkles className=\"mt-0.5 size-4 text-amber-700\" aria-hidden=\"true\" />\n <div className=\"flex flex-col gap-1\">\n <div className=\"font-medium text-amber-950\">\n {title}\n </div>\n {description ? (\n <div className=\"text-xs text-amber-900/80\">\n {description}\n </div>\n ) : null}\n <div className=\"text-xs text-amber-900/80\">{t('upgrades.versionLabel', { version: action.version })}</div>\n </div>\n </div>\n <div className=\"flex flex-wrap items-center gap-2\">\n <Button\n variant=\"outline\"\n size=\"sm\"\n onClick={() => { void handleRun() }}\n disabled={loading}\n className=\"border-amber-300 text-amber-900 hover:bg-amber-100\"\n >\n {loading ? loadingLabel : action.ctaLabel}\n </Button>\n </div>\n </div>\n )\n}\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -69,25 +69,27 @@ function setAuthRedirectConfig(cfg) {
69
69
  DEFAULT_FORBIDDEN_ROLES = [...cfg.defaultForbiddenRoles].map(String);
70
70
  }
71
71
  }
72
- function redirectToForbiddenLogin(options) {
72
+ function formatForbiddenAccessMessage(options) {
73
+ const features = options?.requiredFeatures?.filter(Boolean) ?? [];
74
+ const roles = options?.requiredRoles?.filter(Boolean) ?? [];
75
+ const effectiveRoles = roles.length ? roles : DEFAULT_FORBIDDEN_ROLES.filter(Boolean);
76
+ if (features.length) {
77
+ return `Access denied: you are missing the required permission "${features.join(", ")}". Contact your administrator.`;
78
+ }
79
+ if (effectiveRoles.length) {
80
+ return `Access denied: this area requires the role "${effectiveRoles.join(", ")}". Contact your administrator.`;
81
+ }
82
+ return "Access denied: you do not have permission to perform this action.";
83
+ }
84
+ function notifyForbiddenAccess(options) {
73
85
  if (typeof window === "undefined") return;
74
- if (window.location.pathname.startsWith("/login")) return;
75
86
  if (/\/[^/]+\/portal(\/|$)/.test(window.location.pathname)) return;
76
87
  try {
77
- const current = window.location.pathname + window.location.search;
78
- const features = options?.requiredFeatures?.filter(Boolean) ?? [];
79
- const roles = options?.requiredRoles?.filter(Boolean) ?? [];
80
- const fallbackRoles = DEFAULT_FORBIDDEN_ROLES.filter(Boolean);
81
- const effectiveRoles = roles.length ? roles : fallbackRoles;
82
- const query = features.length ? `requireFeature=${encodeURIComponent(features.join(","))}` : effectiveRoles.length ? `requireRole=${encodeURIComponent(effectiveRoles.map(String).join(","))}` : "";
83
- const url = query ? `/login?${query}&redirect=${encodeURIComponent(current)}` : `/login?redirect=${encodeURIComponent(current)}`;
84
- flash("Insufficient permissions. Redirecting to login\u2026", "warning");
85
- setTimeout(() => {
86
- window.location.href = url;
87
- }, 60);
88
+ flash(formatForbiddenAccessMessage(options), "warning");
88
89
  } catch {
89
90
  }
90
91
  }
92
+ const redirectToForbiddenLogin = notifyForbiddenAccess;
91
93
  async function apiFetch(input, init) {
92
94
  const originalFetch = typeof window !== "undefined" ? window.__omOriginalFetch : void 0;
93
95
  const fallbackFetch = globalThis.fetch;
@@ -143,7 +145,7 @@ async function apiFetch(input, init) {
143
145
  }
144
146
  const hasAclHints = Boolean(roles && roles.length || features && features.length);
145
147
  if (hasAclHints) {
146
- redirectToForbiddenLogin({ requiredRoles: roles, requiredFeatures: features });
148
+ notifyForbiddenAccess({ requiredRoles: roles, requiredFeatures: features });
147
149
  }
148
150
  let msg = "Forbidden";
149
151
  if (aclData && typeof aclData === "object") {
@@ -187,6 +189,7 @@ export {
187
189
  ForbiddenError,
188
190
  UnauthorizedError,
189
191
  apiFetch,
192
+ notifyForbiddenAccess,
190
193
  redirectToForbiddenLogin,
191
194
  redirectToSessionRefresh,
192
195
  setAuthRedirectConfig,
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../src/backend/utils/api.ts"],
4
- "sourcesContent": ["\"use client\"\n// Simple fetch wrapper that redirects to session refresh on 401 (Unauthorized)\n// Used across UI data utilities to avoid duplication.\nimport { flash } from '../FlashMessages'\nimport { deserializeOperationMetadata } from '@open-mercato/shared/lib/commands/operationMetadata'\nimport { readJsonSafe } from '@open-mercato/shared/lib/http/readJsonSafe'\nimport { pushOperation } from '../operations/store'\nimport { pushPartialIndexWarning } from '../indexes/store'\nimport { createScopedHeaderStack } from './scopedHeaderStack'\n\nconst scopedHeaders = createScopedHeaderStack()\n\nfunction mergeHeaders(base: HeadersInit | undefined, scoped: Record<string, string>): Headers {\n const headers = new Headers(base ?? {})\n for (const [key, value] of Object.entries(scoped)) {\n if (headers.has(key)) continue\n headers.set(key, value)\n }\n return headers\n}\n\nfunction readRedirectOverride(headers: Headers, headerName: string): boolean {\n return headers.get(headerName) === '0'\n}\n\nfunction isSameOriginRequest(input: RequestInfo | URL): boolean {\n if (typeof window === 'undefined') return false\n const host = window.location?.host\n if (!host) return false\n let urlString: string\n if (typeof input === 'string') urlString = input\n else if (input instanceof URL) urlString = input.toString()\n else if (typeof Request !== 'undefined' && input instanceof Request) urlString = input.url\n else return false\n if (!/^[a-z][a-z0-9+.-]*:/i.test(urlString)) return true\n try {\n return new URL(urlString).host === host\n } catch {\n return false\n }\n}\n\nexport async function withScopedApiHeaders<T>(headers: Record<string, string>, run: () => Promise<T>): Promise<T> {\n return scopedHeaders.withScopedHeaders(headers, run)\n}\n\nexport class UnauthorizedError extends Error {\n readonly status = 401\n constructor(message = 'Unauthorized') {\n super(message)\n this.name = 'UnauthorizedError'\n }\n}\n\nexport function redirectToSessionRefresh() {\n if (typeof window === 'undefined') return\n const current = window.location.pathname + window.location.search\n // Avoid redirect loops if already on an auth/session route\n if (window.location.pathname.startsWith('/api/auth')) return\n // Portal routes have their own customer auth \u2014 never redirect to staff login\n if (/\\/[^/]+\\/portal(\\/|$)/.test(window.location.pathname)) return\n try {\n flash('Session expired. Redirecting to sign in\u2026', 'warning')\n setTimeout(() => {\n window.location.href = `/api/auth/session/refresh?redirect=${encodeURIComponent(current)}`\n }, 20)\n } catch {\n // no-op\n }\n}\n\nexport class ForbiddenError extends Error {\n readonly status = 403\n constructor(message = 'Forbidden') {\n super(message)\n this.name = 'ForbiddenError'\n }\n}\n\nlet DEFAULT_FORBIDDEN_ROLES: string[] = ['admin']\n\nexport function setAuthRedirectConfig(cfg: { defaultForbiddenRoles?: readonly string[] }) {\n if (cfg?.defaultForbiddenRoles && cfg.defaultForbiddenRoles.length) {\n DEFAULT_FORBIDDEN_ROLES = [...cfg.defaultForbiddenRoles].map(String)\n }\n}\n\nexport function redirectToForbiddenLogin(options?: { requiredRoles?: string[] | null; requiredFeatures?: string[] | null }) {\n if (typeof window === 'undefined') return\n if (window.location.pathname.startsWith('/login')) return\n // Portal routes have their own customer auth \u2014 never redirect to staff login\n if (/\\/[^/]+\\/portal(\\/|$)/.test(window.location.pathname)) return\n try {\n const current = window.location.pathname + window.location.search\n const features = options?.requiredFeatures?.filter(Boolean) ?? []\n const roles = options?.requiredRoles?.filter(Boolean) ?? []\n const fallbackRoles = DEFAULT_FORBIDDEN_ROLES.filter(Boolean)\n const effectiveRoles = roles.length ? roles : fallbackRoles\n const query = features.length\n ? `requireFeature=${encodeURIComponent(features.join(','))}`\n : effectiveRoles.length\n ? `requireRole=${encodeURIComponent(effectiveRoles.map(String).join(','))}`\n : ''\n const url = query\n ? `/login?${query}&redirect=${encodeURIComponent(current)}`\n : `/login?redirect=${encodeURIComponent(current)}`\n flash('Insufficient permissions. Redirecting to login\u2026', 'warning')\n setTimeout(() => { window.location.href = url }, 60)\n } catch {\n // no-op\n }\n}\n\nexport async function apiFetch(input: RequestInfo | URL, init?: RequestInit): Promise<Response> {\n type FetchType = (input: RequestInfo | URL, init?: RequestInit) => Promise<Response>\n const originalFetch =\n typeof window !== 'undefined'\n ? (window as Window & { __omOriginalFetch?: FetchType }).__omOriginalFetch\n : undefined\n const fallbackFetch = (globalThis as typeof globalThis & { fetch?: FetchType }).fetch\n const baseFetch = originalFetch ?? fallbackFetch\n if (!baseFetch) {\n return new Response(\n JSON.stringify({ error: 'Fetch API is not available in this runtime' }),\n { status: 503, headers: { 'content-type': 'application/json' } },\n )\n }\n const scoped = scopedHeaders.resolveScopedHeaders()\n const baseInit: RequestInit = Object.keys(scoped).length\n ? { ...(init ?? {}), headers: mergeHeaders(init?.headers, scoped) }\n : init ?? {}\n // Only auto-inject credentials: 'include' for same-origin requests so cookies\n // round-trip across Next.js proxy.ts rewrites (custom-domain portal flows)\n // without leaking session cookies to third-party hosts.\n const mergedInit: RequestInit = baseInit.credentials\n ? baseInit\n : isSameOriginRequest(input)\n ? { ...baseInit, credentials: 'include' }\n : baseInit\n const requestHeaders = new Headers(mergedInit?.headers)\n const disableUnauthorizedRedirect = readRedirectOverride(requestHeaders, 'x-om-unauthorized-redirect')\n const disableForbiddenRedirect = readRedirectOverride(requestHeaders, 'x-om-forbidden-redirect')\n const res = await baseFetch(input, mergedInit)\n const pathname = typeof window !== 'undefined' ? window.location.pathname : ''\n const onLoginPage = pathname.startsWith('/login')\n const onPortalRoute = /\\/[^/]+\\/portal(\\/|$)/.test(pathname)\n if (res.status === 401) {\n // Trigger same redirect flow as protected pages\n // Skip for staff login page and all portal routes (portal has its own auth)\n if (!onLoginPage && !onPortalRoute && !disableUnauthorizedRedirect) {\n redirectToSessionRefresh()\n // Throw a typed error for callers that might still handle it\n throw new UnauthorizedError(await res.text().catch(() => 'Unauthorized'))\n }\n return res\n }\n if (res.status === 403) {\n // Try to read requiredRoles from JSON body; ignore if not JSON\n let roles: string[] | null = null\n let features: string[] | null = null\n let payload: unknown = null\n const aclData = await readJsonSafe<Record<string, unknown>>(res.clone(), null)\n if (aclData && typeof aclData === 'object') {\n if (Array.isArray(aclData.requiredRoles)) {\n roles = aclData.requiredRoles.map((r) => String(r))\n }\n if (Array.isArray(aclData.requiredFeatures)) {\n features = aclData.requiredFeatures.map((f) => String(f))\n }\n payload = aclData\n }\n // Only redirect if not already on login page or a portal route\n if (!onLoginPage && !onPortalRoute && !disableForbiddenRedirect) {\n const target =\n typeof input === 'string'\n ? input\n : input instanceof URL\n ? input.toString()\n : (typeof Request !== 'undefined' && input instanceof Request)\n ? input.url\n : 'unknown'\n try {\n // eslint-disable-next-line no-console\n console.warn('[apiFetch] Forbidden response', {\n url: target,\n status: res.status,\n requiredRoles: roles,\n requiredFeatures: features,\n details: payload,\n })\n } catch {}\n const hasAclHints = Boolean((roles && roles.length) || (features && features.length))\n if (hasAclHints) {\n redirectToForbiddenLogin({ requiredRoles: roles, requiredFeatures: features })\n }\n let msg = 'Forbidden'\n if (aclData && typeof aclData === 'object') {\n if (typeof aclData.error === 'string') {\n msg = aclData.error\n } else if (typeof aclData.message === 'string') {\n msg = aclData.message\n }\n } else {\n msg = await res.clone().text().catch(() => 'Forbidden')\n }\n throw new ForbiddenError(msg)\n }\n // If already on login, just return the response for the caller to handle\n }\n try {\n const header = res.headers.get('x-om-operation')\n const metadata = deserializeOperationMetadata(header)\n if (metadata) pushOperation(metadata)\n } catch {\n // ignore malformed headers\n }\n try {\n const warningRaw = res.headers.get('x-om-partial-index')\n if (warningRaw) {\n const parsed = JSON.parse(warningRaw) as Record<string, unknown>\n if (parsed && typeof parsed === 'object' && parsed.type === 'partial_index') {\n const entity = typeof parsed.entity === 'string' ? parsed.entity : String(parsed.entity ?? '')\n if (entity) {\n const baseCount = typeof parsed.baseCount === 'number' ? parsed.baseCount : null\n const indexedCount = typeof parsed.indexedCount === 'number' ? parsed.indexedCount : null\n const scope = parsed.scope === 'global' ? 'global' : 'scoped'\n const entityLabel =\n typeof parsed.entityLabel === 'string' && parsed.entityLabel.trim()\n ? parsed.entityLabel.trim()\n : entity\n pushPartialIndexWarning({ entity, entityLabel, baseCount, indexedCount, scope })\n }\n }\n }\n } catch {\n // ignore malformed headers\n }\n return res\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["\"use client\"\n// Simple fetch wrapper that redirects to session refresh on 401 (Unauthorized)\n// Used across UI data utilities to avoid duplication.\nimport { flash } from '../FlashMessages'\nimport { deserializeOperationMetadata } from '@open-mercato/shared/lib/commands/operationMetadata'\nimport { readJsonSafe } from '@open-mercato/shared/lib/http/readJsonSafe'\nimport { pushOperation } from '../operations/store'\nimport { pushPartialIndexWarning } from '../indexes/store'\nimport { createScopedHeaderStack } from './scopedHeaderStack'\n\nconst scopedHeaders = createScopedHeaderStack()\n\nfunction mergeHeaders(base: HeadersInit | undefined, scoped: Record<string, string>): Headers {\n const headers = new Headers(base ?? {})\n for (const [key, value] of Object.entries(scoped)) {\n if (headers.has(key)) continue\n headers.set(key, value)\n }\n return headers\n}\n\nfunction readRedirectOverride(headers: Headers, headerName: string): boolean {\n return headers.get(headerName) === '0'\n}\n\nfunction isSameOriginRequest(input: RequestInfo | URL): boolean {\n if (typeof window === 'undefined') return false\n const host = window.location?.host\n if (!host) return false\n let urlString: string\n if (typeof input === 'string') urlString = input\n else if (input instanceof URL) urlString = input.toString()\n else if (typeof Request !== 'undefined' && input instanceof Request) urlString = input.url\n else return false\n if (!/^[a-z][a-z0-9+.-]*:/i.test(urlString)) return true\n try {\n return new URL(urlString).host === host\n } catch {\n return false\n }\n}\n\nexport async function withScopedApiHeaders<T>(headers: Record<string, string>, run: () => Promise<T>): Promise<T> {\n return scopedHeaders.withScopedHeaders(headers, run)\n}\n\nexport class UnauthorizedError extends Error {\n readonly status = 401\n constructor(message = 'Unauthorized') {\n super(message)\n this.name = 'UnauthorizedError'\n }\n}\n\nexport function redirectToSessionRefresh() {\n if (typeof window === 'undefined') return\n const current = window.location.pathname + window.location.search\n // Avoid redirect loops if already on an auth/session route\n if (window.location.pathname.startsWith('/api/auth')) return\n // Portal routes have their own customer auth \u2014 never redirect to staff login\n if (/\\/[^/]+\\/portal(\\/|$)/.test(window.location.pathname)) return\n try {\n flash('Session expired. Redirecting to sign in\u2026', 'warning')\n setTimeout(() => {\n window.location.href = `/api/auth/session/refresh?redirect=${encodeURIComponent(current)}`\n }, 20)\n } catch {\n // no-op\n }\n}\n\nexport class ForbiddenError extends Error {\n readonly status = 403\n constructor(message = 'Forbidden') {\n super(message)\n this.name = 'ForbiddenError'\n }\n}\n\nlet DEFAULT_FORBIDDEN_ROLES: string[] = ['admin']\n\nexport function setAuthRedirectConfig(cfg: { defaultForbiddenRoles?: readonly string[] }) {\n if (cfg?.defaultForbiddenRoles && cfg.defaultForbiddenRoles.length) {\n DEFAULT_FORBIDDEN_ROLES = [...cfg.defaultForbiddenRoles].map(String)\n }\n}\n\nfunction formatForbiddenAccessMessage(options?: { requiredRoles?: string[] | null; requiredFeatures?: string[] | null }): string {\n const features = options?.requiredFeatures?.filter(Boolean) ?? []\n const roles = options?.requiredRoles?.filter(Boolean) ?? []\n const effectiveRoles = roles.length ? roles : DEFAULT_FORBIDDEN_ROLES.filter(Boolean)\n if (features.length) {\n return `Access denied: you are missing the required permission \"${features.join(', ')}\". Contact your administrator.`\n }\n if (effectiveRoles.length) {\n return `Access denied: this area requires the role \"${effectiveRoles.join(', ')}\". Contact your administrator.`\n }\n return 'Access denied: you do not have permission to perform this action.'\n}\n\n/**\n * Signal a forbidden access attempt for an authenticated user via a flash banner.\n *\n * Authenticated 403 responses must never redirect to `/login` \u2014 that creates an\n * infinite loop because the login page detects the active session and bounces\n * the user back to the failing destination (see GH #2070). Pages that need an\n * inline banner should catch `ForbiddenError` and render `AccessDeniedMessage`\n * from `@open-mercato/ui/backend/detail`.\n */\nexport function notifyForbiddenAccess(options?: { requiredRoles?: string[] | null; requiredFeatures?: string[] | null }) {\n if (typeof window === 'undefined') return\n // Portal routes have their own customer auth \u2014 keep the existing no-op contract.\n if (/\\/[^/]+\\/portal(\\/|$)/.test(window.location.pathname)) return\n try {\n flash(formatForbiddenAccessMessage(options), 'warning')\n } catch {\n // no-op\n }\n}\n\n/**\n * @deprecated Renamed to {@link notifyForbiddenAccess}. The previous name\n * implied a `/login` redirect that no longer happens (see GH #2070). Kept as an\n * exported alias for one minor version so third-party module imports keep\n * building; update imports to `notifyForbiddenAccess`.\n */\nexport const redirectToForbiddenLogin = notifyForbiddenAccess\n\nexport async function apiFetch(input: RequestInfo | URL, init?: RequestInit): Promise<Response> {\n type FetchType = (input: RequestInfo | URL, init?: RequestInit) => Promise<Response>\n const originalFetch =\n typeof window !== 'undefined'\n ? (window as Window & { __omOriginalFetch?: FetchType }).__omOriginalFetch\n : undefined\n const fallbackFetch = (globalThis as typeof globalThis & { fetch?: FetchType }).fetch\n const baseFetch = originalFetch ?? fallbackFetch\n if (!baseFetch) {\n return new Response(\n JSON.stringify({ error: 'Fetch API is not available in this runtime' }),\n { status: 503, headers: { 'content-type': 'application/json' } },\n )\n }\n const scoped = scopedHeaders.resolveScopedHeaders()\n const baseInit: RequestInit = Object.keys(scoped).length\n ? { ...(init ?? {}), headers: mergeHeaders(init?.headers, scoped) }\n : init ?? {}\n // Only auto-inject credentials: 'include' for same-origin requests so cookies\n // round-trip across Next.js proxy.ts rewrites (custom-domain portal flows)\n // without leaking session cookies to third-party hosts.\n const mergedInit: RequestInit = baseInit.credentials\n ? baseInit\n : isSameOriginRequest(input)\n ? { ...baseInit, credentials: 'include' }\n : baseInit\n const requestHeaders = new Headers(mergedInit?.headers)\n const disableUnauthorizedRedirect = readRedirectOverride(requestHeaders, 'x-om-unauthorized-redirect')\n const disableForbiddenRedirect = readRedirectOverride(requestHeaders, 'x-om-forbidden-redirect')\n const res = await baseFetch(input, mergedInit)\n const pathname = typeof window !== 'undefined' ? window.location.pathname : ''\n const onLoginPage = pathname.startsWith('/login')\n const onPortalRoute = /\\/[^/]+\\/portal(\\/|$)/.test(pathname)\n if (res.status === 401) {\n // Trigger same redirect flow as protected pages\n // Skip for staff login page and all portal routes (portal has its own auth)\n if (!onLoginPage && !onPortalRoute && !disableUnauthorizedRedirect) {\n redirectToSessionRefresh()\n // Throw a typed error for callers that might still handle it\n throw new UnauthorizedError(await res.text().catch(() => 'Unauthorized'))\n }\n return res\n }\n if (res.status === 403) {\n // Try to read requiredRoles from JSON body; ignore if not JSON\n let roles: string[] | null = null\n let features: string[] | null = null\n let payload: unknown = null\n const aclData = await readJsonSafe<Record<string, unknown>>(res.clone(), null)\n if (aclData && typeof aclData === 'object') {\n if (Array.isArray(aclData.requiredRoles)) {\n roles = aclData.requiredRoles.map((r) => String(r))\n }\n if (Array.isArray(aclData.requiredFeatures)) {\n features = aclData.requiredFeatures.map((f) => String(f))\n }\n payload = aclData\n }\n // Only redirect if not already on login page or a portal route\n if (!onLoginPage && !onPortalRoute && !disableForbiddenRedirect) {\n const target =\n typeof input === 'string'\n ? input\n : input instanceof URL\n ? input.toString()\n : (typeof Request !== 'undefined' && input instanceof Request)\n ? input.url\n : 'unknown'\n try {\n // eslint-disable-next-line no-console\n console.warn('[apiFetch] Forbidden response', {\n url: target,\n status: res.status,\n requiredRoles: roles,\n requiredFeatures: features,\n details: payload,\n })\n } catch {}\n const hasAclHints = Boolean((roles && roles.length) || (features && features.length))\n if (hasAclHints) {\n notifyForbiddenAccess({ requiredRoles: roles, requiredFeatures: features })\n }\n let msg = 'Forbidden'\n if (aclData && typeof aclData === 'object') {\n if (typeof aclData.error === 'string') {\n msg = aclData.error\n } else if (typeof aclData.message === 'string') {\n msg = aclData.message\n }\n } else {\n msg = await res.clone().text().catch(() => 'Forbidden')\n }\n throw new ForbiddenError(msg)\n }\n // If already on login, just return the response for the caller to handle\n }\n try {\n const header = res.headers.get('x-om-operation')\n const metadata = deserializeOperationMetadata(header)\n if (metadata) pushOperation(metadata)\n } catch {\n // ignore malformed headers\n }\n try {\n const warningRaw = res.headers.get('x-om-partial-index')\n if (warningRaw) {\n const parsed = JSON.parse(warningRaw) as Record<string, unknown>\n if (parsed && typeof parsed === 'object' && parsed.type === 'partial_index') {\n const entity = typeof parsed.entity === 'string' ? parsed.entity : String(parsed.entity ?? '')\n if (entity) {\n const baseCount = typeof parsed.baseCount === 'number' ? parsed.baseCount : null\n const indexedCount = typeof parsed.indexedCount === 'number' ? parsed.indexedCount : null\n const scope = parsed.scope === 'global' ? 'global' : 'scoped'\n const entityLabel =\n typeof parsed.entityLabel === 'string' && parsed.entityLabel.trim()\n ? parsed.entityLabel.trim()\n : entity\n pushPartialIndexWarning({ entity, entityLabel, baseCount, indexedCount, scope })\n }\n }\n }\n } catch {\n // ignore malformed headers\n }\n return res\n}\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }