@emdash-cms/admin 0.13.0 → 0.15.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (92) hide show
  1. package/dist/{LocaleDirectionProvider-rC0KuZjM.js → LocaleDirectionProvider-C-w4lOU2.js} +17 -17
  2. package/dist/{LocaleDirectionProvider-rC0KuZjM.js.map → LocaleDirectionProvider-C-w4lOU2.js.map} +1 -1
  3. package/dist/index.d.ts +56 -2
  4. package/dist/index.d.ts.map +1 -1
  5. package/dist/index.js +1559 -573
  6. package/dist/index.js.map +1 -1
  7. package/dist/locales/ar/messages.mjs +1 -1
  8. package/dist/locales/de/messages.mjs +1 -1
  9. package/dist/locales/en/messages.mjs +1 -1
  10. package/dist/locales/es-419/messages.mjs +1 -1
  11. package/dist/locales/es-ES/messages.mjs +1 -1
  12. package/dist/locales/eu/messages.mjs +1 -1
  13. package/dist/locales/fa/messages.mjs +1 -1
  14. package/dist/locales/fr/messages.mjs +1 -1
  15. package/dist/locales/id/messages.mjs +1 -1
  16. package/dist/locales/index.js +1 -1
  17. package/dist/locales/ja/messages.mjs +1 -1
  18. package/dist/locales/ko/messages.mjs +1 -1
  19. package/dist/locales/pl/messages.mjs +1 -1
  20. package/dist/locales/pseudo/messages.mjs +1 -1
  21. package/dist/locales/pt-BR/messages.mjs +1 -1
  22. package/dist/locales/zh-CN/messages.mjs +1 -1
  23. package/dist/locales/zh-TW/messages.mjs +1 -1
  24. package/dist/messages-B1vNWgt7.js +6 -0
  25. package/dist/messages-B1vNWgt7.js.map +1 -0
  26. package/dist/messages-B4SjKBpv.js +6 -0
  27. package/dist/messages-B4SjKBpv.js.map +1 -0
  28. package/dist/messages-BMFLZDHo.js +6 -0
  29. package/dist/messages-BMFLZDHo.js.map +1 -0
  30. package/dist/messages-BWDz4uQu.js +6 -0
  31. package/dist/messages-BWDz4uQu.js.map +1 -0
  32. package/dist/messages-BXt6OWYC.js +6 -0
  33. package/dist/messages-BXt6OWYC.js.map +1 -0
  34. package/dist/messages-CA-O-TjM.js +6 -0
  35. package/dist/messages-CA-O-TjM.js.map +1 -0
  36. package/dist/messages-CX3dih9c.js +6 -0
  37. package/dist/messages-CX3dih9c.js.map +1 -0
  38. package/dist/messages-CXZHQHKt.js +6 -0
  39. package/dist/messages-CXZHQHKt.js.map +1 -0
  40. package/dist/messages-Cz2f6Su-.js +6 -0
  41. package/dist/messages-Cz2f6Su-.js.map +1 -0
  42. package/dist/messages-DW1BWiJ5.js +6 -0
  43. package/dist/messages-DW1BWiJ5.js.map +1 -0
  44. package/dist/messages-DpMKfmmV.js +6 -0
  45. package/dist/messages-DpMKfmmV.js.map +1 -0
  46. package/dist/messages-hYPn9ilH.js +6 -0
  47. package/dist/messages-hYPn9ilH.js.map +1 -0
  48. package/dist/messages-qYhBOhtE.js +6 -0
  49. package/dist/messages-qYhBOhtE.js.map +1 -0
  50. package/dist/messages-rxCTcw_6.js +6 -0
  51. package/dist/messages-rxCTcw_6.js.map +1 -0
  52. package/dist/messages-wpqHOBfn.js +6 -0
  53. package/dist/messages-wpqHOBfn.js.map +1 -0
  54. package/dist/messages-zuRDwn8n.js +6 -0
  55. package/dist/messages-zuRDwn8n.js.map +1 -0
  56. package/dist/{plugins-qrjvPK4g.js → plugins-BZzztFdK.js} +3 -3
  57. package/dist/plugins-BZzztFdK.js.map +1 -0
  58. package/dist/styles.css +1 -1
  59. package/package.json +5 -4
  60. package/dist/messages-BB7Ta5GU.js +0 -6
  61. package/dist/messages-BB7Ta5GU.js.map +0 -1
  62. package/dist/messages-BMyaLK4h.js +0 -6
  63. package/dist/messages-BMyaLK4h.js.map +0 -1
  64. package/dist/messages-BRIi_T5q.js +0 -6
  65. package/dist/messages-BRIi_T5q.js.map +0 -1
  66. package/dist/messages-Be0FUVz_.js +0 -6
  67. package/dist/messages-Be0FUVz_.js.map +0 -1
  68. package/dist/messages-BvE8RBIJ.js +0 -6
  69. package/dist/messages-BvE8RBIJ.js.map +0 -1
  70. package/dist/messages-CBHhMxhB.js +0 -6
  71. package/dist/messages-CBHhMxhB.js.map +0 -1
  72. package/dist/messages-CQJpnG4Q.js +0 -6
  73. package/dist/messages-CQJpnG4Q.js.map +0 -1
  74. package/dist/messages-CZpDTTb_.js +0 -6
  75. package/dist/messages-CZpDTTb_.js.map +0 -1
  76. package/dist/messages-CemjGhIt.js +0 -6
  77. package/dist/messages-CemjGhIt.js.map +0 -1
  78. package/dist/messages-Czya1QvU.js +0 -6
  79. package/dist/messages-Czya1QvU.js.map +0 -1
  80. package/dist/messages-DR7ouL8H.js +0 -6
  81. package/dist/messages-DR7ouL8H.js.map +0 -1
  82. package/dist/messages-DyiZEfUj.js +0 -6
  83. package/dist/messages-DyiZEfUj.js.map +0 -1
  84. package/dist/messages-JMuy3Tpu.js +0 -6
  85. package/dist/messages-JMuy3Tpu.js.map +0 -1
  86. package/dist/messages-h9IORlKh.js +0 -6
  87. package/dist/messages-h9IORlKh.js.map +0 -1
  88. package/dist/messages-iu8SG58F.js +0 -6
  89. package/dist/messages-iu8SG58F.js.map +0 -1
  90. package/dist/messages-uD16X789.js +0 -6
  91. package/dist/messages-uD16X789.js.map +0 -1
  92. package/dist/plugins-qrjvPK4g.js.map +0 -1
package/dist/index.js CHANGED
@@ -1,7 +1,7 @@
1
- import { c as fetchAuthMode, d as throwResponseError, i as fetchPlugins, l as fetchManifest, n as enablePlugin, o as API_BASE, r as fetchPlugin, s as apiFetch, t as disablePlugin, u as parseApiResponse } from "./plugins-qrjvPK4g.js";
2
- import { a as SUPPORTED_LOCALES, i as DEFAULT_LOCALE, l as resolveLocale, n as useLocale, o as SUPPORTED_LOCALE_CODES, t as LocaleDirectionProvider } from "./LocaleDirectionProvider-rC0KuZjM.js";
1
+ import { c as fetchAuthMode, d as throwResponseError, i as fetchPlugins, l as fetchManifest, n as enablePlugin, o as API_BASE, r as fetchPlugin, s as apiFetch, t as disablePlugin, u as parseApiResponse } from "./plugins-BZzztFdK.js";
2
+ import { a as SUPPORTED_LOCALES, i as DEFAULT_LOCALE, l as resolveLocale, n as useLocale, o as SUPPORTED_LOCALE_CODES, t as LocaleDirectionProvider } from "./LocaleDirectionProvider-C-w4lOU2.js";
3
3
  import "./locales/index.js";
4
- import { Badge, Button, Checkbox, CommandPalette, Dialog, Input, InputArea, Label, LinkButton, Loader, Popover, Select, Sidebar as KumoSidebar, Switch, Tabs, Toast, Toasty, Tooltip, buttonVariants, useSidebar } from "@cloudflare/kumo";
4
+ import { Autocomplete, Badge, Button, Checkbox, CommandPalette, Dialog, Input, InputArea, Label, LinkButton, Loader, Popover, Select, Sidebar as KumoSidebar, Switch, Tabs, Toast, Toasty, Tooltip, buttonVariants, useSidebar } from "@cloudflare/kumo";
5
5
  import { i18n } from "@lingui/core";
6
6
  import { I18nProvider, Trans, useLingui } from "@lingui/react";
7
7
  import { QueryClient, QueryClientProvider, useInfiniteQuery, useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
@@ -27,12 +27,13 @@ import { TableHeader } from "@tiptap/extension-table-header";
27
27
  import { TableRow } from "@tiptap/extension-table-row";
28
28
  import TextAlign from "@tiptap/extension-text-align";
29
29
  import Typography from "@tiptap/extension-typography";
30
- import { EditorContent, Node as Node$2, NodeViewWrapper, ReactNodeViewRenderer, mergeAttributes as mergeAttributes$1, useEditor, useEditorState } from "@tiptap/react";
30
+ import { EditorContent, Node as Node$2, NodeViewContent, NodeViewWrapper, ReactNodeViewRenderer, mergeAttributes as mergeAttributes$1, useEditor, useEditorState } from "@tiptap/react";
31
31
  import { BubbleMenu } from "@tiptap/react/menus";
32
32
  import StarterKit from "@tiptap/starter-kit";
33
33
  import { Plugin, PluginKey } from "@tiptap/pm/state";
34
34
  import { Decoration, DecorationSet } from "@tiptap/pm/view";
35
35
  import { createPortal } from "react-dom";
36
+ import CodeBlock$1 from "@tiptap/extension-code-block";
36
37
  import { DragHandle } from "@tiptap/extension-drag-handle-react";
37
38
  import { isAllowedUri } from "@tiptap/extension-link";
38
39
  import { BlockRenderer } from "@emdash-cms/blocks";
@@ -1956,6 +1957,7 @@ async function fetchBylines(options) {
1956
1957
  if (options?.search) params.set("search", options.search);
1957
1958
  if (options?.isGuest !== void 0) params.set("isGuest", String(options.isGuest));
1958
1959
  if (options?.userId) params.set("userId", options.userId);
1960
+ if (options?.locale) params.set("locale", options.locale);
1959
1961
  if (options?.cursor) params.set("cursor", options.cursor);
1960
1962
  if (options?.limit) params.set("limit", String(options.limit));
1961
1963
  return parseApiResponse(await apiFetch(`${API_BASE}/admin/bylines${params.toString() ? `?${params}` : ""}`), "Failed to fetch bylines");
@@ -1984,6 +1986,26 @@ async function deleteByline(id) {
1984
1986
  message: "Failed to delete byline"
1985
1987
  }));
1986
1988
  }
1989
+ /**
1990
+ * Fetch every translation of a byline (siblings sharing the same
1991
+ * translation_group).
1992
+ */
1993
+ async function fetchBylineTranslations(id) {
1994
+ return parseApiResponse(await apiFetch(`${API_BASE}/admin/bylines/${id}/translations`), "Failed to fetch byline translations");
1995
+ }
1996
+ /**
1997
+ * Create a new locale variant of a byline. The new row joins the source's
1998
+ * `translation_group`. Body defaults — slug, display name, avatar, website —
1999
+ * inherit from the source when omitted, so editors only have to fill in the
2000
+ * localized bio (and optionally a localized display name).
2001
+ */
2002
+ async function createBylineTranslation(id, input) {
2003
+ return parseApiResponse(await apiFetch(`${API_BASE}/admin/bylines/${id}/translations`, {
2004
+ method: "POST",
2005
+ headers: { "Content-Type": "application/json" },
2006
+ body: JSON.stringify(input)
2007
+ }), "Failed to create byline translation");
2008
+ }
1987
2009
 
1988
2010
  //#endregion
1989
2011
  //#region src/lib/api/menus.ts
@@ -5525,6 +5547,392 @@ function Suggestion({ pluginKey = SuggestionPluginKey, editor, char = "@", allow
5525
5547
  }
5526
5548
  var index_default = Suggestion;
5527
5549
 
5550
+ //#endregion
5551
+ //#region src/components/editor/codeBlockLanguages.ts
5552
+ const CODE_BLOCK_LANGUAGES = [
5553
+ {
5554
+ id: "plaintext",
5555
+ label: "Plain text",
5556
+ aliases: [
5557
+ "text",
5558
+ "plain",
5559
+ "txt"
5560
+ ]
5561
+ },
5562
+ {
5563
+ id: "astro",
5564
+ label: "Astro"
5565
+ },
5566
+ {
5567
+ id: "bash",
5568
+ label: "Bash",
5569
+ aliases: [
5570
+ "sh",
5571
+ "shell",
5572
+ "zsh"
5573
+ ]
5574
+ },
5575
+ {
5576
+ id: "c",
5577
+ label: "C"
5578
+ },
5579
+ {
5580
+ id: "cpp",
5581
+ label: "C++",
5582
+ aliases: ["c++"]
5583
+ },
5584
+ {
5585
+ id: "csharp",
5586
+ label: "C#",
5587
+ aliases: ["cs", "c#"]
5588
+ },
5589
+ {
5590
+ id: "css",
5591
+ label: "CSS"
5592
+ },
5593
+ {
5594
+ id: "diff",
5595
+ label: "Diff",
5596
+ aliases: ["patch"]
5597
+ },
5598
+ {
5599
+ id: "dockerfile",
5600
+ label: "Dockerfile",
5601
+ aliases: ["docker"]
5602
+ },
5603
+ {
5604
+ id: "go",
5605
+ label: "Go",
5606
+ aliases: ["golang"]
5607
+ },
5608
+ {
5609
+ id: "graphql",
5610
+ label: "GraphQL",
5611
+ aliases: ["gql"]
5612
+ },
5613
+ {
5614
+ id: "html",
5615
+ label: "HTML"
5616
+ },
5617
+ {
5618
+ id: "java",
5619
+ label: "Java"
5620
+ },
5621
+ {
5622
+ id: "javascript",
5623
+ label: "JavaScript",
5624
+ aliases: ["js"]
5625
+ },
5626
+ {
5627
+ id: "json",
5628
+ label: "JSON"
5629
+ },
5630
+ {
5631
+ id: "jsx",
5632
+ label: "JSX"
5633
+ },
5634
+ {
5635
+ id: "kotlin",
5636
+ label: "Kotlin",
5637
+ aliases: ["kt"]
5638
+ },
5639
+ {
5640
+ id: "markdown",
5641
+ label: "Markdown",
5642
+ aliases: ["md"]
5643
+ },
5644
+ {
5645
+ id: "mdx",
5646
+ label: "MDX"
5647
+ },
5648
+ {
5649
+ id: "php",
5650
+ label: "PHP"
5651
+ },
5652
+ {
5653
+ id: "python",
5654
+ label: "Python",
5655
+ aliases: ["py"]
5656
+ },
5657
+ {
5658
+ id: "ruby",
5659
+ label: "Ruby",
5660
+ aliases: ["rb"]
5661
+ },
5662
+ {
5663
+ id: "rust",
5664
+ label: "Rust",
5665
+ aliases: ["rs"]
5666
+ },
5667
+ {
5668
+ id: "scss",
5669
+ label: "SCSS",
5670
+ aliases: ["sass"]
5671
+ },
5672
+ {
5673
+ id: "sql",
5674
+ label: "SQL"
5675
+ },
5676
+ {
5677
+ id: "svelte",
5678
+ label: "Svelte"
5679
+ },
5680
+ {
5681
+ id: "swift",
5682
+ label: "Swift"
5683
+ },
5684
+ {
5685
+ id: "toml",
5686
+ label: "TOML"
5687
+ },
5688
+ {
5689
+ id: "tsx",
5690
+ label: "TSX"
5691
+ },
5692
+ {
5693
+ id: "typescript",
5694
+ label: "TypeScript",
5695
+ aliases: ["ts"]
5696
+ },
5697
+ {
5698
+ id: "vue",
5699
+ label: "Vue"
5700
+ },
5701
+ {
5702
+ id: "xml",
5703
+ label: "XML"
5704
+ },
5705
+ {
5706
+ id: "yaml",
5707
+ label: "YAML",
5708
+ aliases: ["yml"]
5709
+ }
5710
+ ];
5711
+ /**
5712
+ * Look up a language by id or alias. Case-insensitive.
5713
+ * Returns the canonical entry, or `null` if not found in the curated list.
5714
+ */
5715
+ function findLanguage(value) {
5716
+ if (!value) return null;
5717
+ const needle = value.trim().toLowerCase();
5718
+ if (!needle) return null;
5719
+ for (const lang of CODE_BLOCK_LANGUAGES) {
5720
+ if (lang.id === needle) return lang;
5721
+ if (lang.aliases?.includes(needle)) return lang;
5722
+ }
5723
+ return null;
5724
+ }
5725
+ /**
5726
+ * Normalize a user-entered language string to a canonical id where possible.
5727
+ * Unknown inputs are sanitized to a single safe class token: lowercased,
5728
+ * trimmed, with any character outside `[a-z0-9_-]` (including whitespace,
5729
+ * dots, slashes, etc.) collapsed to `-`. This keeps the stored value safe
5730
+ * to interpolate into a `language-{id}` CSS class without splitting on
5731
+ * whitespace.
5732
+ *
5733
+ * Examples:
5734
+ * normalizeLanguage("TypeScript") -> "typescript" (canonical id)
5735
+ * normalizeLanguage("ts") -> "typescript" (alias)
5736
+ * normalizeLanguage("Objective C") -> "objective-c" (sanitized)
5737
+ * normalizeLanguage("F#") -> "f-" (sanitized)
5738
+ * normalizeLanguage("") -> undefined
5739
+ */
5740
+ const DISALLOWED_CHARS_RE = /[^a-z0-9_-]+/g;
5741
+ const LEADING_TRAILING_HYPHENS_RE = /^-+|-+$/g;
5742
+ function normalizeLanguage(value) {
5743
+ if (!value) return void 0;
5744
+ const trimmed = value.trim();
5745
+ if (!trimmed) return void 0;
5746
+ const match = findLanguage(trimmed);
5747
+ if (match) return match.id;
5748
+ return trimmed.toLowerCase().replace(DISALLOWED_CHARS_RE, "-").replace(LEADING_TRAILING_HYPHENS_RE, "") || void 0;
5749
+ }
5750
+ /**
5751
+ * Human-readable label for a stored language id. Falls back to the id itself
5752
+ * for unknown values so the editor never shows "undefined".
5753
+ */
5754
+ function languageLabel(value) {
5755
+ if (!value) return "Plain text";
5756
+ const match = findLanguage(value);
5757
+ if (match) return match.label;
5758
+ return value;
5759
+ }
5760
+
5761
+ //#endregion
5762
+ //#region src/components/editor/CodeBlockNode.tsx
5763
+ /**
5764
+ * Code block node with language picker.
5765
+ *
5766
+ * Wraps the base `@tiptap/extension-code-block` with a React node view that
5767
+ * overlays a small language chip in the top-right corner. Clicking the chip
5768
+ * opens a popover with a Kumo Autocomplete: a free-form text input plus a
5769
+ * filtered list of curated language suggestions. The value is persisted on
5770
+ * the node's `language` attribute and round-trips through Portable Text as
5771
+ * `block.language`.
5772
+ *
5773
+ * The picker accepts arbitrary strings (not restricted to the curated list)
5774
+ * so that less common languages can still be used. Free-form input is
5775
+ * sanitized to a single safe CSS class token via `normalizeLanguage` so the
5776
+ * frontend's `language-{id}` class stays well-formed.
5777
+ */
5778
+ const LANGUAGE_ITEMS = CODE_BLOCK_LANGUAGES.map((l) => l.label);
5779
+ function filterLanguages(item, query) {
5780
+ if (!query) return true;
5781
+ const needle = query.toLowerCase();
5782
+ const lang = CODE_BLOCK_LANGUAGES.find((l) => l.label === item);
5783
+ if (!lang) return false;
5784
+ if (lang.label.toLowerCase().includes(needle)) return true;
5785
+ if (lang.id.toLowerCase().includes(needle)) return true;
5786
+ return lang.aliases?.some((alias) => alias.toLowerCase().includes(needle)) ?? false;
5787
+ }
5788
+ function CodeBlockNodeView({ node, updateAttributes, selected }) {
5789
+ const { _: _t } = useLingui();
5790
+ const [isEditing, setIsEditing] = React$1.useState(false);
5791
+ const storedLanguage = typeof node.attrs.language === "string" ? node.attrs.language : "";
5792
+ const [draft, setDraft] = React$1.useState(() => languageLabel(storedLanguage));
5793
+ const popoverRef = React$1.useRef(null);
5794
+ React$1.useEffect(() => {
5795
+ if (!isEditing) setDraft(languageLabel(storedLanguage));
5796
+ }, [storedLanguage, isEditing]);
5797
+ const openPicker = React$1.useCallback(() => {
5798
+ setDraft(storedLanguage ? languageLabel(storedLanguage) : "");
5799
+ setIsEditing(true);
5800
+ }, [storedLanguage]);
5801
+ const closePicker = React$1.useCallback(() => {
5802
+ setIsEditing(false);
5803
+ setDraft(languageLabel(storedLanguage));
5804
+ }, [storedLanguage]);
5805
+ const commit = React$1.useCallback((value) => {
5806
+ updateAttributes({ language: normalizeLanguage(value ?? draft) ?? null });
5807
+ setIsEditing(false);
5808
+ }, [draft, updateAttributes]);
5809
+ const handleKeyDown = (e) => {
5810
+ if (e.key === "Enter") {
5811
+ e.preventDefault();
5812
+ commit();
5813
+ } else if (e.key === "Escape") {
5814
+ e.preventDefault();
5815
+ closePicker();
5816
+ }
5817
+ };
5818
+ React$1.useEffect(() => {
5819
+ if (!isEditing) return void 0;
5820
+ const onMouseDown = (event) => {
5821
+ const target = event.target instanceof Node ? event.target : null;
5822
+ if (popoverRef.current && target && !popoverRef.current.contains(target)) closePicker();
5823
+ };
5824
+ document.addEventListener("mousedown", onMouseDown);
5825
+ return () => document.removeEventListener("mousedown", onMouseDown);
5826
+ }, [isEditing, closePicker]);
5827
+ const label = languageLabel(storedLanguage);
5828
+ const chipPersistent = isEditing || Boolean(storedLanguage) || selected;
5829
+ return /* @__PURE__ */ jsxs(NodeViewWrapper, {
5830
+ className: "group relative my-4",
5831
+ "data-language": storedLanguage || void 0,
5832
+ children: [/* @__PURE__ */ jsx("pre", {
5833
+ className: "emdash-code-block",
5834
+ children: /* @__PURE__ */ jsx(NodeViewContent, { as: "code" })
5835
+ }), /* @__PURE__ */ jsx("div", {
5836
+ className: "absolute end-2 top-2 select-none",
5837
+ contentEditable: false,
5838
+ children: isEditing ? /* @__PURE__ */ jsxs("div", {
5839
+ ref: popoverRef,
5840
+ className: "flex items-center gap-1 rounded-md border bg-kumo-overlay p-1 shadow-lg",
5841
+ onKeyDown: handleKeyDown,
5842
+ children: [
5843
+ /* @__PURE__ */ jsxs(Autocomplete, {
5844
+ items: LANGUAGE_ITEMS,
5845
+ value: draft,
5846
+ onValueChange: (next) => setDraft(next),
5847
+ filter: filterLanguages,
5848
+ children: [/* @__PURE__ */ jsx(Autocomplete.InputGroup, {
5849
+ size: "sm",
5850
+ placeholder: _t({
5851
+ id: "vXIe7J",
5852
+ message: "Language"
5853
+ })
5854
+ }), /* @__PURE__ */ jsxs(Autocomplete.Content, {
5855
+ sideOffset: 4,
5856
+ children: [/* @__PURE__ */ jsx(Autocomplete.List, { children: (item) => /* @__PURE__ */ jsx(Autocomplete.Item, {
5857
+ value: item,
5858
+ children: item
5859
+ }, item) }), /* @__PURE__ */ jsx(Autocomplete.Empty, { children: _t({
5860
+ id: "4TVVsS",
5861
+ message: "No matches"
5862
+ }) })]
5863
+ })]
5864
+ }),
5865
+ /* @__PURE__ */ jsx(Button, {
5866
+ type: "button",
5867
+ variant: "ghost",
5868
+ shape: "square",
5869
+ className: "h-7 w-7",
5870
+ onMouseDown: (e) => e.preventDefault(),
5871
+ onClick: () => commit(),
5872
+ title: _t({
5873
+ id: "Ek42QX",
5874
+ message: "Apply language"
5875
+ }),
5876
+ "aria-label": _t({
5877
+ id: "Ek42QX",
5878
+ message: "Apply language"
5879
+ }),
5880
+ children: /* @__PURE__ */ jsx(Check, { className: "h-4 w-4" })
5881
+ }),
5882
+ /* @__PURE__ */ jsx(Button, {
5883
+ type: "button",
5884
+ variant: "ghost",
5885
+ shape: "square",
5886
+ className: "h-7 w-7",
5887
+ onMouseDown: (e) => e.preventDefault(),
5888
+ onClick: closePicker,
5889
+ title: _t({
5890
+ id: "dEgA5A",
5891
+ message: "Cancel"
5892
+ }),
5893
+ "aria-label": _t({
5894
+ id: "dEgA5A",
5895
+ message: "Cancel"
5896
+ }),
5897
+ children: /* @__PURE__ */ jsx(X, { className: "h-4 w-4" })
5898
+ })
5899
+ ]
5900
+ }) : /* @__PURE__ */ jsx("button", {
5901
+ type: "button",
5902
+ tabIndex: chipPersistent ? 0 : -1,
5903
+ onMouseDown: (e) => e.preventDefault(),
5904
+ onClick: openPicker,
5905
+ className: "rounded-md border bg-kumo-overlay/90 px-2 py-1 text-xs text-kumo-subtle opacity-0 transition-opacity hover:text-kumo-strong focus:opacity-100 focus:outline-none focus:ring-2 focus:ring-kumo-brand group-hover:opacity-100 data-[persistent=true]:opacity-100",
5906
+ "data-persistent": chipPersistent ? "true" : "false",
5907
+ title: _t({
5908
+ id: "9cbOH7",
5909
+ message: "Set language"
5910
+ }),
5911
+ "aria-label": _t({
5912
+ id: "q/A3pF",
5913
+ message: "Set language (current: {label})",
5914
+ values: { label }
5915
+ }),
5916
+ "aria-hidden": chipPersistent ? void 0 : true,
5917
+ children: storedLanguage ? label : _t({
5918
+ id: "9cbOH7",
5919
+ message: "Set language"
5920
+ })
5921
+ })
5922
+ })]
5923
+ });
5924
+ }
5925
+ /**
5926
+ * TipTap extension: code block with an inline language picker node view.
5927
+ *
5928
+ * Drop-in replacement for StarterKit's default `codeBlock`. Configure
5929
+ * `StarterKit.configure({ codeBlock: false })` and add this extension to
5930
+ * the editor's extensions array.
5931
+ */
5932
+ const CodeBlockExtension = CodeBlock$1.extend({ addNodeView() {
5933
+ return ReactNodeViewRenderer(CodeBlockNodeView);
5934
+ } });
5935
+
5528
5936
  //#endregion
5529
5937
  //#region src/components/editor/BlockMenu.tsx
5530
5938
  const blockTransforms = [
@@ -8153,6 +8561,7 @@ function PortableTextEditor({ value, onChange, placeholder = "Start writing...",
8153
8561
  color: "#3b82f6",
8154
8562
  width: 2
8155
8563
  },
8564
+ codeBlock: false,
8156
8565
  link: {
8157
8566
  openOnClick: false,
8158
8567
  enableClickSelection: true,
@@ -8160,6 +8569,7 @@ function PortableTextEditor({ value, onChange, placeholder = "Start writing...",
8160
8569
  },
8161
8570
  underline: {}
8162
8571
  }),
8572
+ CodeBlockExtension,
8163
8573
  ImageExtension,
8164
8574
  MarkdownLinkExtension,
8165
8575
  PluginBlockExtension,
@@ -8262,8 +8672,7 @@ function PortableTextEditor({ value, onChange, placeholder = "Start writing...",
8262
8672
  if (!editor || !pluginBlockModal) return;
8263
8673
  const { id, ...data } = values;
8264
8674
  const editPos = editingBlockPosRef.current;
8265
- if (editPos !== null) {
8266
- const { tr } = editor.state;
8675
+ if (editPos !== null) editor.chain().command(({ tr }) => {
8267
8676
  const node = tr.doc.nodeAt(editPos);
8268
8677
  if (node?.type.name === "pluginBlock") {
8269
8678
  tr.setNodeMarkup(editPos, void 0, {
@@ -8271,9 +8680,11 @@ function PortableTextEditor({ value, onChange, placeholder = "Start writing...",
8271
8680
  id: typeof id === "string" ? id : node.attrs.id,
8272
8681
  data
8273
8682
  });
8274
- editor.view.dispatch(tr);
8683
+ return true;
8275
8684
  }
8276
- } else editor.chain().focus().insertContent({
8685
+ return false;
8686
+ }).run();
8687
+ else editor.chain().focus().insertContent({
8277
8688
  type: "pluginBlock",
8278
8689
  attrs: {
8279
8690
  blockType: pluginBlockModal.type,
@@ -10373,7 +10784,7 @@ function formatScheduledDate(dateStr) {
10373
10784
  /**
10374
10785
  * Content editor with dynamic field rendering
10375
10786
  */
10376
- function ContentEditor({ collection, collectionLabel, item, fields, isNew, isSaving, onSave, onAutosave, isAutosaving, lastAutosaveAt, onPublish, onUnpublish, onDiscardDraft, onSchedule, onUnschedule, isScheduling, supportsDrafts = false, supportsRevisions = false, supportsPreview = false, currentUser, users, onAuthorChange, availableBylines, selectedBylines, onBylinesChange, onQuickCreateByline, onQuickEditByline, onDelete, isDeleting, i18n, translations, onTranslate, pluginBlocks, hasSeo = false, onSeoChange, manifest }) {
10787
+ function ContentEditor({ collection, collectionLabel, item, fields, isNew, entryLocale, isSaving, onSave, onAutosave, isAutosaving, lastAutosaveAt, onPublish, onUnpublish, onDiscardDraft, onSchedule, onUnschedule, isScheduling, supportsDrafts = false, supportsRevisions = false, supportsPreview = false, currentUser, users, onAuthorChange, availableBylines, availableBylinesLoaded, selectedBylines, onBylinesChange, onQuickCreateByline, onQuickEditByline, onDelete, isDeleting, i18n, translations, onTranslate, pluginBlocks, hasSeo = false, onSeoChange, manifest }) {
10377
10788
  const { _: _t } = useLingui();
10378
10789
  const navigate = useNavigate$1();
10379
10790
  const [formData, setFormData] = React$1.useState(item?.data || {});
@@ -10384,6 +10795,7 @@ function ContentEditor({ collection, collectionLabel, item, fields, isNew, isSav
10384
10795
  bylineId: entry.byline.id,
10385
10796
  roleLabel: entry.roleLabel
10386
10797
  })) ?? []);
10798
+ const [bylinesTouched, setBylinesTouched] = React$1.useState(false);
10387
10799
  const [portableTextEditor, setPortableTextEditor] = React$1.useState(null);
10388
10800
  const [blockSidebarPanel, setBlockSidebarPanel] = React$1.useState(null);
10389
10801
  const handleBlockSidebarOpen = React$1.useCallback((panel) => {
@@ -10425,6 +10837,7 @@ function ContentEditor({ collection, collectionLabel, item, fields, isNew, isSav
10425
10837
  bylines: nextBylines
10426
10838
  }));
10427
10839
  pendingAutosaveStateRef.current = null;
10840
+ setBylinesTouched(false);
10428
10841
  }
10429
10842
  const itemDataString = React$1.useMemo(() => item ? JSON.stringify(item.data) : "", [item?.data]);
10430
10843
  React$1.useEffect(() => {
@@ -10446,6 +10859,7 @@ function ContentEditor({ collection, collectionLabel, item, fields, isNew, isSav
10446
10859
  })) ?? []
10447
10860
  }));
10448
10861
  pendingAutosaveStateRef.current = null;
10862
+ setBylinesTouched(false);
10449
10863
  }
10450
10864
  }, [
10451
10865
  item?.updatedAt,
@@ -10455,6 +10869,7 @@ function ContentEditor({ collection, collectionLabel, item, fields, isNew, isSav
10455
10869
  ]);
10456
10870
  const activeBylines = isNew ? selectedBylines ?? [] : internalBylines;
10457
10871
  const handleBylinesChange = React$1.useCallback((next) => {
10872
+ setBylinesTouched(true);
10458
10873
  if (isNew) {
10459
10874
  onBylinesChange?.(next);
10460
10875
  return;
@@ -10497,13 +10912,13 @@ function ContentEditor({ collection, collectionLabel, item, fields, isNew, isSav
10497
10912
  if (hasInvalidUrls(formDataRef.current)) return;
10498
10913
  const payload = {
10499
10914
  data: formDataRef.current,
10500
- slug: slugRef.current || void 0,
10501
- bylines: activeBylines
10915
+ slug: slugRef.current || void 0
10502
10916
  };
10917
+ if (bylinesTouched) payload.bylines = activeBylines;
10503
10918
  pendingAutosaveStateRef.current = serializeEditorState({
10504
10919
  data: payload.data,
10505
10920
  slug: payload.slug || "",
10506
- bylines: payload.bylines
10921
+ bylines: activeBylines
10507
10922
  });
10508
10923
  onAutosave(payload);
10509
10924
  }, AUTOSAVE_DELAY);
@@ -10519,6 +10934,7 @@ function ContentEditor({ collection, collectionLabel, item, fields, isNew, isSav
10519
10934
  isSaving,
10520
10935
  isAutosaving,
10521
10936
  activeBylines,
10937
+ bylinesTouched,
10522
10938
  hasInvalidUrls
10523
10939
  ]);
10524
10940
  const handleSubmit = (e) => {
@@ -10528,11 +10944,12 @@ function ContentEditor({ collection, collectionLabel, item, fields, isNew, isSav
10528
10944
  clearTimeout(autosaveTimeoutRef.current);
10529
10945
  autosaveTimeoutRef.current = null;
10530
10946
  }
10531
- onSave?.({
10947
+ const payload = {
10532
10948
  data: formData,
10533
- slug: slug || void 0,
10534
- bylines: activeBylines
10535
- });
10949
+ slug: slug || void 0
10950
+ };
10951
+ if (isNew || bylinesTouched) payload.bylines = activeBylines;
10952
+ onSave?.(payload);
10536
10953
  };
10537
10954
  const [isLoadingPreview, setIsLoadingPreview] = React$1.useState(false);
10538
10955
  const urlPattern = manifest?.collections[collection]?.urlPattern;
@@ -11066,9 +11483,12 @@ function ContentEditor({ collection, collectionLabel, item, fields, isNew, isSav
11066
11483
  }), /* @__PURE__ */ jsx(BylineCreditsEditor, {
11067
11484
  credits: activeBylines,
11068
11485
  bylines: availableBylines ?? [],
11486
+ bylinesLoaded: availableBylinesLoaded,
11069
11487
  onChange: handleBylinesChange,
11070
11488
  onQuickCreate: onQuickCreateByline,
11071
- onQuickEdit: onQuickEditByline
11489
+ onQuickEdit: onQuickEditByline,
11490
+ entryLocale: item?.locale ?? entryLocale,
11491
+ i18n
11072
11492
  })]
11073
11493
  }),
11074
11494
  i18n && item && !isNew && /* @__PURE__ */ jsx("div", {
@@ -11725,7 +12145,7 @@ function FileFieldRenderer({ id, label, value, onChange, required, allowedMimeTy
11725
12145
  ]
11726
12146
  });
11727
12147
  }
11728
- function BylineCreditsEditor({ credits, bylines, onChange, onQuickCreate, onQuickEdit }) {
12148
+ function BylineCreditsEditor({ credits, bylines, onChange, onQuickCreate, onQuickEdit, entryLocale, i18n, bylinesLoaded = true }) {
11729
12149
  const { _: _t7 } = useLingui();
11730
12150
  const [selectedBylineId, setSelectedBylineId] = React$1.useState("");
11731
12151
  const [quickName, setQuickName] = React$1.useState("");
@@ -11768,6 +12188,27 @@ function BylineCreditsEditor({ credits, bylines, onChange, onQuickCreate, onQuic
11768
12188
  return /* @__PURE__ */ jsxs("div", {
11769
12189
  className: "space-y-3",
11770
12190
  children: [
12191
+ !!i18n && i18n.locales.length > 1 && bylinesLoaded && bylines.length === 0 && !!entryLocale && /* @__PURE__ */ jsxs("div", {
12192
+ className: "rounded border border-dashed p-3 text-sm space-y-2",
12193
+ children: [/* @__PURE__ */ jsx("p", {
12194
+ className: "text-kumo-subtle",
12195
+ children: _t7({
12196
+ id: "Tmb8Fw",
12197
+ message: "No bylines available in {entryLocale}. Create a variant from the Bylines page before crediting one on this entry.",
12198
+ values: { entryLocale }
12199
+ })
12200
+ }), /* @__PURE__ */ jsx(RouterLinkButton, {
12201
+ to: "/bylines",
12202
+ search: { locale: entryLocale ?? void 0 },
12203
+ variant: "secondary",
12204
+ size: "sm",
12205
+ children: _t7({
12206
+ id: "yXD9HA",
12207
+ message: "Manage bylines in {entryLocale}",
12208
+ values: { entryLocale }
12209
+ })
12210
+ })]
12211
+ }),
11771
12212
  /* @__PURE__ */ jsxs("div", {
11772
12213
  className: "flex gap-2",
11773
12214
  children: [/* @__PURE__ */ jsx(Select, {
@@ -17600,10 +18041,10 @@ function formatInstallCount(count) {
17600
18041
  * Lists each requested capability with a human-readable explanation.
17601
18042
  * User must explicitly confirm before the action proceeds.
17602
18043
  */
17603
- function CapabilityConsentDialog({ mode, pluginName, capabilities, allowedHosts, newCapabilities = [], auditVerdict, isPending = false, error, onConfirm, onCancel }) {
18044
+ function CapabilityConsentDialog({ mode, pluginName, capabilities, allowedHosts, newCapabilities = [], newlyPublicRoutes = [], auditVerdict, isPending = false, error, onConfirm, onCancel }) {
17604
18045
  const { _: _t } = useLingui();
17605
18046
  const newSet = new Set(newCapabilities);
17606
- const isUpdate = mode === "update" || newCapabilities.length > 0;
18047
+ const isUpdate = mode === "update" || newCapabilities.length > 0 || newlyPublicRoutes.length > 0;
17607
18048
  return /* @__PURE__ */ jsxs("div", {
17608
18049
  className: "fixed inset-0 z-50 flex items-center justify-center",
17609
18050
  role: "dialog",
@@ -17644,31 +18085,60 @@ function CapabilityConsentDialog({ mode, pluginName, capabilities, allowedHosts,
17644
18085
  }),
17645
18086
  /* @__PURE__ */ jsxs("div", {
17646
18087
  className: "px-6 py-4 space-y-3",
17647
- children: [capabilities.map((cap) => {
17648
- const isNew = newSet.has(cap);
17649
- return /* @__PURE__ */ jsxs("div", {
17650
- className: cn("flex items-start gap-3 rounded-md p-2 text-sm", isNew ? "bg-warning/10 border border-warning/30" : "bg-kumo-tint/50"),
17651
- children: [/* @__PURE__ */ jsx(ShieldCheck, { className: cn("mt-0.5 h-4 w-4 shrink-0", isNew ? "text-warning" : "text-kumo-subtle") }), /* @__PURE__ */ jsxs("div", { children: [/* @__PURE__ */ jsx("span", {
17652
- className: cn(isNew && "font-medium"),
17653
- children: describeCapability(cap, allowedHosts)
17654
- }), isNew && /* @__PURE__ */ jsx("span", {
17655
- className: "ms-2 text-xs text-warning font-medium",
17656
- children: _t({
17657
- id: "p6NueD",
17658
- message: "NEW"
18088
+ children: [
18089
+ capabilities.map((cap) => {
18090
+ const isNew = newSet.has(cap);
18091
+ return /* @__PURE__ */ jsxs("div", {
18092
+ className: cn("flex items-start gap-3 rounded-md p-2 text-sm", isNew ? "bg-warning/10 border border-warning/30" : "bg-kumo-tint/50"),
18093
+ children: [/* @__PURE__ */ jsx(ShieldCheck, { className: cn("mt-0.5 h-4 w-4 shrink-0", isNew ? "text-warning" : "text-kumo-subtle") }), /* @__PURE__ */ jsxs("div", { children: [/* @__PURE__ */ jsx("span", {
18094
+ className: cn(isNew && "font-medium"),
18095
+ children: describeCapability(cap, allowedHosts)
18096
+ }), isNew && /* @__PURE__ */ jsx("span", {
18097
+ className: "ms-2 text-xs text-warning font-medium",
18098
+ children: _t({
18099
+ id: "p6NueD",
18100
+ message: "NEW"
18101
+ })
18102
+ })] })]
18103
+ }, cap);
18104
+ }),
18105
+ newlyPublicRoutes.length > 0 && /* @__PURE__ */ jsxs("div", {
18106
+ className: "rounded-md border border-warning/30 bg-warning/10 p-3 text-sm",
18107
+ children: [
18108
+ /* @__PURE__ */ jsxs("div", {
18109
+ className: "flex items-center gap-2 font-medium text-warning",
18110
+ children: [/* @__PURE__ */ jsx(Warning, { className: "h-4 w-4 shrink-0" }), _t({
18111
+ id: "NXruDe",
18112
+ message: "New public routes"
18113
+ })]
18114
+ }),
18115
+ /* @__PURE__ */ jsx("p", {
18116
+ className: "mt-1 text-xs text-kumo-subtle",
18117
+ children: _t({
18118
+ id: "qCUL4n",
18119
+ message: "This update exposes the following routes without authentication:"
18120
+ })
18121
+ }),
18122
+ /* @__PURE__ */ jsx("ul", {
18123
+ className: "mt-2 space-y-1 ps-5 text-xs",
18124
+ children: newlyPublicRoutes.map((route) => /* @__PURE__ */ jsx("li", {
18125
+ className: "list-disc font-mono",
18126
+ children: route
18127
+ }, route))
17659
18128
  })
17660
- })] })]
17661
- }, cap);
17662
- }), auditVerdict && auditVerdict !== "pass" && /* @__PURE__ */ jsxs("div", {
17663
- className: cn("flex items-center gap-2 rounded-md p-3 text-sm mt-2", auditVerdict === "warn" ? "bg-warning/10 text-warning" : "bg-kumo-danger/10 text-kumo-danger"),
17664
- children: [auditVerdict === "warn" ? /* @__PURE__ */ jsx(Warning, { className: "h-4 w-4 shrink-0" }) : /* @__PURE__ */ jsx(ShieldWarning, { className: "h-4 w-4 shrink-0" }), /* @__PURE__ */ jsx("span", { children: auditVerdict === "warn" ? _t({
17665
- id: "zPUuMZ",
17666
- message: "Security audit flagged potential concerns with this plugin."
17667
- }) : _t({
17668
- id: "nvVzbM",
17669
- message: "Security audit flagged this plugin as potentially unsafe."
17670
- }) })]
17671
- })]
18129
+ ]
18130
+ }),
18131
+ auditVerdict && auditVerdict !== "pass" && /* @__PURE__ */ jsxs("div", {
18132
+ className: cn("flex items-center gap-2 rounded-md p-3 text-sm mt-2", auditVerdict === "warn" ? "bg-warning/10 text-warning" : "bg-kumo-danger/10 text-kumo-danger"),
18133
+ children: [auditVerdict === "warn" ? /* @__PURE__ */ jsx(Warning, { className: "h-4 w-4 shrink-0" }) : /* @__PURE__ */ jsx(ShieldWarning, { className: "h-4 w-4 shrink-0" }), /* @__PURE__ */ jsx("span", { children: auditVerdict === "warn" ? _t({
18134
+ id: "zPUuMZ",
18135
+ message: "Security audit flagged potential concerns with this plugin."
18136
+ }) : _t({
18137
+ id: "nvVzbM",
18138
+ message: "Security audit flagged this plugin as potentially unsafe."
18139
+ }) })]
18140
+ })
18141
+ ]
17672
18142
  }),
17673
18143
  /* @__PURE__ */ jsx(DialogError, {
17674
18144
  message: error,
@@ -17707,6 +18177,332 @@ function CapabilityConsentDialog({ mode, pluginName, capabilities, allowedHosts,
17707
18177
  });
17708
18178
  }
17709
18179
 
18180
+ //#endregion
18181
+ //#region src/lib/api/registry.ts
18182
+ let cachedDiscovery = null;
18183
+ async function getDiscoveryClient(config) {
18184
+ if (cachedDiscovery && cachedDiscovery.config.aggregatorUrl === config.aggregatorUrl && cachedDiscovery.config.acceptLabelers === config.acceptLabelers) return cachedDiscovery.client;
18185
+ const DiscoveryClient = (await import("@emdash-cms/registry-client/discovery")).DiscoveryClient;
18186
+ const discovery = new DiscoveryClient({
18187
+ aggregatorUrl: config.aggregatorUrl,
18188
+ acceptLabelers: config.acceptLabelers
18189
+ });
18190
+ const wrapped = {
18191
+ async searchPackages(opts) {
18192
+ return discovery.searchPackages({
18193
+ q: opts.q,
18194
+ cursor: opts.cursor,
18195
+ limit: opts.limit
18196
+ });
18197
+ },
18198
+ async resolvePackage(handle, slug) {
18199
+ return discovery.resolvePackage({
18200
+ handle,
18201
+ slug
18202
+ });
18203
+ },
18204
+ async getPackage(did, slug) {
18205
+ return discovery.getPackage({
18206
+ did,
18207
+ slug
18208
+ });
18209
+ },
18210
+ async getLatestRelease(did, slug) {
18211
+ return discovery.getLatestRelease({
18212
+ did,
18213
+ package: slug
18214
+ });
18215
+ },
18216
+ async listReleases(did, slug, opts) {
18217
+ return discovery.listReleases({
18218
+ did,
18219
+ package: slug,
18220
+ cursor: opts?.cursor,
18221
+ limit: opts?.limit
18222
+ });
18223
+ }
18224
+ };
18225
+ cachedDiscovery = {
18226
+ config,
18227
+ client: wrapped
18228
+ };
18229
+ return wrapped;
18230
+ }
18231
+ /**
18232
+ * Returns whether a release should be considered installable given the
18233
+ * configured policy. Currently implements the minimum-release-age check
18234
+ * described in RFC 0001's "Pre-label gap and launch tempo" section,
18235
+ * plus the `minimumReleaseAgeExclude` allowlist.
18236
+ *
18237
+ * Returns `false` (release blocked) when the policy is configured but
18238
+ * the release is missing a valid `indexedAt` -- we fail closed rather
18239
+ * than silently letting unbounded-age releases through.
18240
+ */
18241
+ function releasePassesPolicy(release, pkg, policy, now = Date.now()) {
18242
+ if (!policy?.minimumReleaseAgeSeconds) return true;
18243
+ if (releaseExemptFromMinimumAge(policy.minimumReleaseAgeExclude, pkg.did, pkg.slug)) return true;
18244
+ const indexedAt = Date.parse(release.indexedAt);
18245
+ if (!Number.isFinite(indexedAt)) return false;
18246
+ return (now - indexedAt) / 1e3 >= policy.minimumReleaseAgeSeconds;
18247
+ }
18248
+ /**
18249
+ * Canonicalize a capabilities list for set-style comparison. Mirrors
18250
+ * the server-side helper `canonicalCapabilitiesForDriftCheck` in
18251
+ * `packages/core/src/registry/config.ts` -- both sides must produce
18252
+ * the same canonical shape so the install handler's drift check is
18253
+ * stable across reorderings, duplicates, and junk entries.
18254
+ *
18255
+ * Filters non-strings, deduplicates, and sorts lexically.
18256
+ */
18257
+ function canonicalCapabilitiesForDriftCheck(value) {
18258
+ if (!Array.isArray(value)) return [];
18259
+ const seen = /* @__PURE__ */ new Set();
18260
+ for (const entry of value) if (typeof entry === "string" && entry.length > 0) seen.add(entry);
18261
+ return [...seen].toSorted();
18262
+ }
18263
+ /**
18264
+ * Matches a `(publisher_did, slug)` against the
18265
+ * `minimumReleaseAgeExclude` allowlist. Mirrors the server-side helper
18266
+ * of the same name in `packages/core/src/registry/config.ts`.
18267
+ *
18268
+ * DID-only on purpose: handles are aggregator-supplied envelope data
18269
+ * and accepting them as a trust input would let a compromised
18270
+ * aggregator bypass the holdback by claiming any handle for any
18271
+ * package. DIDs are tied to the AT URI of the record itself.
18272
+ *
18273
+ * Entries from the config list have already been lowercased at
18274
+ * manifest build time, so this only needs to lowercase the runtime
18275
+ * values for comparison.
18276
+ */
18277
+ function releaseExemptFromMinimumAge(exclude, publisherDid, slug) {
18278
+ if (!exclude || exclude.length === 0) return false;
18279
+ const didLower = publisherDid.toLowerCase();
18280
+ const fullDid = `${didLower}/${slug.toLowerCase()}`;
18281
+ for (const entry of exclude) {
18282
+ if (entry === didLower) return true;
18283
+ if (entry === fullDid) return true;
18284
+ }
18285
+ return false;
18286
+ }
18287
+ async function searchRegistryPackages(config, opts) {
18288
+ return (await getDiscoveryClient(config)).searchPackages(opts);
18289
+ }
18290
+ async function resolveRegistryPackage(config, handle, slug) {
18291
+ return (await getDiscoveryClient(config)).resolvePackage(handle, slug);
18292
+ }
18293
+ async function getRegistryPackage(config, did, slug) {
18294
+ return (await getDiscoveryClient(config)).getPackage(did, slug);
18295
+ }
18296
+ async function listRegistryReleases(config, did, slug, opts) {
18297
+ return (await getDiscoveryClient(config)).listReleases(did, slug, opts);
18298
+ }
18299
+ /**
18300
+ * Resolve a publisher DID to its claimed handle using the same
18301
+ * `LocalActorResolver` pattern as `@emdash-cms/plugin-cli` and
18302
+ * `@emdash-cms/auth-atproto`. Bidirectional verification (handle's
18303
+ * domain points back to the same DID) is part of the resolver --
18304
+ * `LocalActorResolver` returns the sentinel `"handle.invalid"` when
18305
+ * the `alsoKnownAs` handle is present but doesn't round-trip.
18306
+ *
18307
+ * Three distinct outcomes the UI can render:
18308
+ *
18309
+ * - `{ status: "ok", handle }` — verified handle, round-trip OK.
18310
+ * - `{ status: "invalid" }` — DID claims a handle but it doesn't
18311
+ * resolve back. The publisher's handle setup is broken; the admin
18312
+ * should see a clear "Invalid handle" indicator rather than the
18313
+ * raw DID.
18314
+ * - `{ status: "missing" }` — no handle claimed at all (no
18315
+ * `alsoKnownAs`), or the DID document couldn't be fetched (network
18316
+ * error, unsupported DID method).
18317
+ */
18318
+ let actorResolver = null;
18319
+ async function getActorResolver() {
18320
+ if (actorResolver) return actorResolver;
18321
+ const { CompositeDidDocumentResolver, CompositeHandleResolver, DohJsonHandleResolver, LocalActorResolver, PlcDidDocumentResolver, WebDidDocumentResolver, WellKnownHandleResolver } = await import("@atcute/identity-resolver");
18322
+ actorResolver = new LocalActorResolver({
18323
+ handleResolver: new CompositeHandleResolver({ methods: {
18324
+ dns: new DohJsonHandleResolver({ dohUrl: "https://cloudflare-dns.com/dns-query" }),
18325
+ http: new WellKnownHandleResolver()
18326
+ } }),
18327
+ didDocumentResolver: new CompositeDidDocumentResolver({ methods: {
18328
+ plc: new PlcDidDocumentResolver(),
18329
+ web: new WebDidDocumentResolver()
18330
+ } })
18331
+ });
18332
+ return actorResolver;
18333
+ }
18334
+ /**
18335
+ * localStorage-backed cache for DID→handle resolutions. Handles are
18336
+ * stable for hours-to-days in practice, but bound the cache so a
18337
+ * compromised handle eventually flips back to "invalid" without a
18338
+ * forced refresh. 24h matches the typical atproto handle TTL.
18339
+ *
18340
+ * Failures (network errors, unsupported DID method) are *not* cached --
18341
+ * those should retry on the next render.
18342
+ */
18343
+ const HANDLE_CACHE_TTL_MS = 1440 * 60 * 1e3;
18344
+ const HANDLE_CACHE_KEY_PREFIX = "emdash:did-handle:";
18345
+ function isCachedResolution(value) {
18346
+ if (typeof value !== "object" || value === null) return false;
18347
+ const candidate = value;
18348
+ return typeof candidate.expiresAt === "number" && typeof candidate.resolution === "object" && candidate.resolution !== null;
18349
+ }
18350
+ function readHandleCache(did) {
18351
+ if (typeof localStorage === "undefined") return null;
18352
+ try {
18353
+ const raw = localStorage.getItem(`${HANDLE_CACHE_KEY_PREFIX}${did}`);
18354
+ if (!raw) return null;
18355
+ const parsed = JSON.parse(raw);
18356
+ if (!isCachedResolution(parsed) || parsed.expiresAt < Date.now()) return null;
18357
+ return parsed.resolution;
18358
+ } catch {
18359
+ return null;
18360
+ }
18361
+ }
18362
+ function writeHandleCache(did, resolution) {
18363
+ if (typeof localStorage === "undefined") return;
18364
+ try {
18365
+ const entry = {
18366
+ resolution,
18367
+ expiresAt: Date.now() + HANDLE_CACHE_TTL_MS
18368
+ };
18369
+ localStorage.setItem(`${HANDLE_CACHE_KEY_PREFIX}${did}`, JSON.stringify(entry));
18370
+ } catch {}
18371
+ }
18372
+ async function resolveDidToHandle(did) {
18373
+ const cached = readHandleCache(did);
18374
+ if (cached) return cached;
18375
+ let result;
18376
+ try {
18377
+ const resolved = await (await getActorResolver()).resolve(did);
18378
+ if (resolved.handle === "handle.invalid") result = { status: "invalid" };
18379
+ else if (resolved.handle) result = {
18380
+ status: "ok",
18381
+ handle: resolved.handle
18382
+ };
18383
+ else result = { status: "missing" };
18384
+ } catch (err) {
18385
+ console.warn(`[registry] DID->handle resolution failed for ${did}:`, err);
18386
+ return { status: "missing" };
18387
+ }
18388
+ writeHandleCache(did, result);
18389
+ return result;
18390
+ }
18391
+ const INSTALL_ENDPOINT = `${API_BASE}/admin/plugins/registry/install`;
18392
+ /**
18393
+ * Install a plugin from the registry.
18394
+ *
18395
+ * Posts to the EmDash server, which re-resolves the same `(handle,
18396
+ * slug)` against the aggregator, re-verifies the bundle's checksum
18397
+ * against the signed release record, and writes the install. Surfaces
18398
+ * structured error codes (`RELEASE_YANKED`, `CHECKSUM_MISMATCH`,
18399
+ * `DECLARED_ACCESS_DRIFT`, etc.) that callers map to localized
18400
+ * messages.
18401
+ */
18402
+ async function installRegistryPlugin(body) {
18403
+ return parseApiResponse(await apiFetch(INSTALL_ENDPOINT, {
18404
+ method: "POST",
18405
+ headers: { "Content-Type": "application/json" },
18406
+ body: JSON.stringify(body)
18407
+ }), i18n._({
18408
+ id: "/LWKyx",
18409
+ message: "Failed to install plugin"
18410
+ }));
18411
+ }
18412
+ /**
18413
+ * Server-side escalation gate raised by the update endpoint when the
18414
+ * target version widens the trust contract. Carries the diff the user
18415
+ * needs to see in the consent dialog before the call is retried with the
18416
+ * matching `confirm*` flag.
18417
+ */
18418
+ var RegistryUpdateEscalationError = class extends Error {
18419
+ code;
18420
+ capabilityChanges;
18421
+ routeVisibilityChanges;
18422
+ constructor(code, message, capabilityChanges, routeVisibilityChanges) {
18423
+ super(message);
18424
+ this.name = "RegistryUpdateEscalationError";
18425
+ this.code = code;
18426
+ this.capabilityChanges = capabilityChanges;
18427
+ this.routeVisibilityChanges = routeVisibilityChanges;
18428
+ }
18429
+ };
18430
+ /**
18431
+ * Update a registry-source plugin to a newer version.
18432
+ * `POST /_emdash/api/admin/plugins/registry/:id/update`
18433
+ *
18434
+ * Called without `confirm*` flags first, this throws
18435
+ * `RegistryUpdateEscalationError` when the target version widens
18436
+ * permissions; the caller renders a consent dialog populated from the
18437
+ * error's diff, then re-calls with the matching `confirm*` flag once
18438
+ * the user agrees.
18439
+ */
18440
+ async function updateRegistryPlugin(pluginId, opts = {}) {
18441
+ const response = await apiFetch(`${API_BASE}/admin/plugins/registry/${encodeURIComponent(pluginId)}/update`, {
18442
+ method: "POST",
18443
+ headers: { "Content-Type": "application/json" },
18444
+ body: JSON.stringify(opts)
18445
+ });
18446
+ if (response.ok) return;
18447
+ const escalation = parseEscalation(await response.clone().json().catch(() => void 0));
18448
+ if (escalation) throw escalation;
18449
+ await throwResponseError(response, i18n._({
18450
+ id: "8m84rO",
18451
+ message: "Failed to update plugin"
18452
+ }));
18453
+ }
18454
+ function parseEscalation(body) {
18455
+ if (!body || typeof body !== "object" || !("error" in body)) return null;
18456
+ const error = body.error;
18457
+ if (!error || typeof error !== "object" || !("code" in error)) return null;
18458
+ const code = error.code;
18459
+ if (code !== "CAPABILITY_ESCALATION" && code !== "ROUTE_VISIBILITY_ESCALATION") return null;
18460
+ const details = "details" in error && error.details && typeof error.details === "object" ? error.details : {};
18461
+ const capabilityChanges = normaliseCapabilityChanges("capabilityChanges" in details ? details.capabilityChanges : void 0);
18462
+ const routeVisibilityChanges = normaliseRouteVisibilityChanges("routeVisibilityChanges" in details ? details.routeVisibilityChanges : void 0);
18463
+ return new RegistryUpdateEscalationError(code, "message" in error && typeof error.message === "string" ? error.message : i18n._({
18464
+ id: "dgD7yY",
18465
+ message: "Plugin update requires re-consent"
18466
+ }), capabilityChanges, routeVisibilityChanges);
18467
+ }
18468
+ function normaliseCapabilityChanges(value) {
18469
+ if (!value || typeof value !== "object") return {
18470
+ added: [],
18471
+ removed: []
18472
+ };
18473
+ const v = value;
18474
+ return {
18475
+ added: Array.isArray(v.added) ? v.added.filter((s) => typeof s === "string") : [],
18476
+ removed: Array.isArray(v.removed) ? v.removed.filter((s) => typeof s === "string") : []
18477
+ };
18478
+ }
18479
+ function normaliseRouteVisibilityChanges(value) {
18480
+ if (!value || typeof value !== "object") return void 0;
18481
+ const v = value;
18482
+ if (!Array.isArray(v.newlyPublic)) return void 0;
18483
+ const newlyPublic = v.newlyPublic.filter((s) => typeof s === "string");
18484
+ return newlyPublic.length > 0 ? { newlyPublic } : void 0;
18485
+ }
18486
+ /**
18487
+ * Uninstall a registry-source plugin.
18488
+ * `POST /_emdash/api/admin/plugins/registry/:id/uninstall`
18489
+ *
18490
+ * The server refuses to uninstall non-registry sources, so calling this
18491
+ * with a marketplace or config plugin id is a no-op error rather than a
18492
+ * destructive cross-source action.
18493
+ */
18494
+ async function uninstallRegistryPlugin(pluginId, opts = {}) {
18495
+ const response = await apiFetch(`${API_BASE}/admin/plugins/registry/${encodeURIComponent(pluginId)}/uninstall`, {
18496
+ method: "POST",
18497
+ headers: { "Content-Type": "application/json" },
18498
+ body: JSON.stringify(opts)
18499
+ });
18500
+ if (!response.ok) await throwResponseError(response, i18n._({
18501
+ id: "IDxl65",
18502
+ message: "Failed to uninstall plugin"
18503
+ }));
18504
+ }
18505
+
17710
18506
  //#endregion
17711
18507
  //#region src/components/PluginManager.tsx
17712
18508
  /**
@@ -17796,7 +18592,7 @@ function PluginManager({ manifest }) {
17796
18592
  if (!updates) return /* @__PURE__ */ new Map();
17797
18593
  return new Map(updates.map((u) => [u.pluginId, u]));
17798
18594
  }, [updates]);
17799
- const hasMarketplacePlugins = plugins?.some((p) => p.source === "marketplace");
18595
+ const hasUpdatableSources = plugins?.some((p) => p.source === "marketplace" || p.source === "registry");
17800
18596
  if (isLoading) return /* @__PURE__ */ jsxs("div", {
17801
18597
  className: "space-y-6",
17802
18598
  children: [/* @__PURE__ */ jsx("h1", {
@@ -17844,7 +18640,7 @@ function PluginManager({ manifest }) {
17844
18640
  }), /* @__PURE__ */ jsxs("div", {
17845
18641
  className: "flex items-center gap-3",
17846
18642
  children: [
17847
- hasMarketplacePlugins && /* @__PURE__ */ jsx(Button, {
18643
+ hasUpdatableSources && /* @__PURE__ */ jsx(Button, {
17848
18644
  variant: "ghost",
17849
18645
  onClick: () => void refetchUpdates(),
17850
18646
  disabled: isCheckingUpdates,
@@ -17939,15 +18735,17 @@ function PluginCard({ plugin, updateInfo, onEnable, onDisable, isToggling, hasMa
17939
18735
  const [expanded, setExpanded] = React$1.useState(false);
17940
18736
  const [showUpdateConsent, setShowUpdateConsent] = React$1.useState(false);
17941
18737
  const [showUninstallConfirm, setShowUninstallConfirm] = React$1.useState(false);
18738
+ const [registryEscalation, setRegistryEscalation] = React$1.useState(null);
17942
18739
  const queryClient = useQueryClient();
17943
18740
  const toastManager = Toast.useToastManager();
17944
18741
  const isMarketplace = plugin.source === "marketplace";
17945
18742
  const isRegistry = plugin.source === "registry";
17946
18743
  const hasUpdate = !!updateInfo && updateInfo.installed !== updateInfo.latest;
17947
18744
  const updateMutation = useMutation({
17948
- mutationFn: () => updateMarketplacePlugin(plugin.id, { confirmCapabilities: true }),
18745
+ mutationFn: (opts) => isRegistry ? updateRegistryPlugin(plugin.id, opts) : updateMarketplacePlugin(plugin.id, { confirmCapabilities: true }),
17949
18746
  onSuccess: () => {
17950
18747
  setShowUpdateConsent(false);
18748
+ setRegistryEscalation(null);
17951
18749
  queryClient.invalidateQueries({ queryKey: ["plugins"] });
17952
18750
  queryClient.invalidateQueries({ queryKey: ["plugin-updates"] });
17953
18751
  queryClient.invalidateQueries({ queryKey: ["manifest"] });
@@ -17965,10 +18763,29 @@ function PluginCard({ plugin, updateInfo, onEnable, onDisable, isToggling, hasMa
17965
18763
  }
17966
18764
  })
17967
18765
  });
18766
+ },
18767
+ onError: (err) => {
18768
+ if (err instanceof RegistryUpdateEscalationError) {
18769
+ setRegistryEscalation(err);
18770
+ setShowUpdateConsent(true);
18771
+ }
17968
18772
  }
17969
18773
  });
18774
+ const handleUpdateClick = () => {
18775
+ if (isRegistry) {
18776
+ setRegistryEscalation(null);
18777
+ updateMutation.mutate({});
18778
+ } else setShowUpdateConsent(true);
18779
+ };
18780
+ const handleUpdateConfirm = () => {
18781
+ if (isRegistry) {
18782
+ const opts = { confirmCapabilityChanges: true };
18783
+ if (registryEscalation?.code === "ROUTE_VISIBILITY_ESCALATION") opts.confirmRouteVisibilityChanges = true;
18784
+ updateMutation.mutate(opts);
18785
+ } else updateMutation.mutate({});
18786
+ };
17970
18787
  const uninstallMutation = useMutation({
17971
- mutationFn: (deleteData) => uninstallMarketplacePlugin(plugin.id, { deleteData }),
18788
+ mutationFn: (deleteData) => isRegistry ? uninstallRegistryPlugin(plugin.id, { deleteData }) : uninstallMarketplacePlugin(plugin.id, { deleteData }),
17972
18789
  onSuccess: () => {
17973
18790
  setShowUninstallConfirm(false);
17974
18791
  queryClient.invalidateQueries({ queryKey: ["plugins"] });
@@ -18097,7 +18914,7 @@ function PluginCard({ plugin, updateInfo, onEnable, onDisable, isToggling, hasMa
18097
18914
  hasUpdate && /* @__PURE__ */ jsx(Button, {
18098
18915
  variant: "outline",
18099
18916
  size: "sm",
18100
- onClick: () => setShowUpdateConsent(true),
18917
+ onClick: handleUpdateClick,
18101
18918
  disabled: updateMutation.isPending,
18102
18919
  children: updateMutation.isPending ? _t2({
18103
18920
  id: "RXbEvi",
@@ -18261,7 +19078,7 @@ function PluginCard({ plugin, updateInfo, onEnable, onDisable, isToggling, hasMa
18261
19078
  ] })
18262
19079
  ]
18263
19080
  }),
18264
- isMarketplace && /* @__PURE__ */ jsx("div", {
19081
+ (isMarketplace || isRegistry) && /* @__PURE__ */ jsx("div", {
18265
19082
  className: "pt-2 border-t",
18266
19083
  children: /* @__PURE__ */ jsx(Button, {
18267
19084
  variant: "ghost",
@@ -18274,13 +19091,6 @@ function PluginCard({ plugin, updateInfo, onEnable, onDisable, isToggling, hasMa
18274
19091
  message: "Uninstall"
18275
19092
  })
18276
19093
  })
18277
- }),
18278
- isRegistry && /* @__PURE__ */ jsx("div", {
18279
- className: "pt-2 border-t text-xs text-kumo-subtle",
18280
- children: _t2({
18281
- id: "YvXVtr",
18282
- message: "Uninstall is not yet available for registry plugins. Disable the plugin to stop it from running; full uninstall will land in a follow-up."
18283
- })
18284
19094
  })
18285
19095
  ]
18286
19096
  })]
@@ -18289,12 +19099,14 @@ function PluginCard({ plugin, updateInfo, onEnable, onDisable, isToggling, hasMa
18289
19099
  mode: "update",
18290
19100
  pluginName: plugin.name,
18291
19101
  capabilities: plugin.capabilities,
18292
- newCapabilities: [],
19102
+ newCapabilities: registryEscalation?.capabilityChanges.added ?? [],
19103
+ newlyPublicRoutes: registryEscalation?.routeVisibilityChanges?.newlyPublic ?? [],
18293
19104
  isPending: updateMutation.isPending,
18294
- error: getMutationError(updateMutation.error),
18295
- onConfirm: () => updateMutation.mutate(),
19105
+ error: updateMutation.error instanceof RegistryUpdateEscalationError ? null : getMutationError(updateMutation.error),
19106
+ onConfirm: handleUpdateConfirm,
18296
19107
  onCancel: () => {
18297
19108
  setShowUpdateConsent(false);
19109
+ setRegistryEscalation(null);
18298
19110
  updateMutation.reset();
18299
19111
  }
18300
19112
  }),
@@ -21951,235 +22763,6 @@ function Redirects() {
21951
22763
  });
21952
22764
  }
21953
22765
 
21954
- //#endregion
21955
- //#region src/lib/api/registry.ts
21956
- let cachedDiscovery = null;
21957
- async function getDiscoveryClient(config) {
21958
- if (cachedDiscovery && cachedDiscovery.config.aggregatorUrl === config.aggregatorUrl && cachedDiscovery.config.acceptLabelers === config.acceptLabelers) return cachedDiscovery.client;
21959
- const DiscoveryClient = (await import("@emdash-cms/registry-client/discovery")).DiscoveryClient;
21960
- const discovery = new DiscoveryClient({
21961
- aggregatorUrl: config.aggregatorUrl,
21962
- acceptLabelers: config.acceptLabelers
21963
- });
21964
- const wrapped = {
21965
- async searchPackages(opts) {
21966
- return await discovery.searchPackages({
21967
- q: opts.q,
21968
- cursor: opts.cursor,
21969
- limit: opts.limit
21970
- });
21971
- },
21972
- async resolvePackage(handle, slug) {
21973
- return await discovery.resolvePackage({
21974
- handle,
21975
- slug
21976
- });
21977
- },
21978
- async getPackage(did, slug) {
21979
- return await discovery.getPackage({
21980
- did,
21981
- slug
21982
- });
21983
- },
21984
- async getLatestRelease(did, slug) {
21985
- return await discovery.getLatestRelease({
21986
- did,
21987
- package: slug
21988
- });
21989
- },
21990
- async listReleases(did, slug, cursor) {
21991
- return await discovery.listReleases({
21992
- did,
21993
- package: slug,
21994
- cursor
21995
- });
21996
- }
21997
- };
21998
- cachedDiscovery = {
21999
- config,
22000
- client: wrapped
22001
- };
22002
- return wrapped;
22003
- }
22004
- /**
22005
- * Returns whether a release should be considered installable given the
22006
- * configured policy. Currently implements the minimum-release-age check
22007
- * described in RFC 0001's "Pre-label gap and launch tempo" section,
22008
- * plus the `minimumReleaseAgeExclude` allowlist.
22009
- *
22010
- * Returns `false` (release blocked) when the policy is configured but
22011
- * the release is missing a valid `indexedAt` -- we fail closed rather
22012
- * than silently letting unbounded-age releases through.
22013
- */
22014
- function releasePassesPolicy(release, pkg, policy, now = Date.now()) {
22015
- if (!policy?.minimumReleaseAgeSeconds) return true;
22016
- if (releaseExemptFromMinimumAge(policy.minimumReleaseAgeExclude, pkg.did, pkg.slug)) return true;
22017
- const indexedAt = Date.parse(release.indexedAt);
22018
- if (!Number.isFinite(indexedAt)) return false;
22019
- return (now - indexedAt) / 1e3 >= policy.minimumReleaseAgeSeconds;
22020
- }
22021
- /**
22022
- * Canonicalize a capabilities list for set-style comparison. Mirrors
22023
- * the server-side helper `canonicalCapabilitiesForDriftCheck` in
22024
- * `packages/core/src/registry/config.ts` -- both sides must produce
22025
- * the same canonical shape so the install handler's drift check is
22026
- * stable across reorderings, duplicates, and junk entries.
22027
- *
22028
- * Filters non-strings, deduplicates, and sorts lexically.
22029
- */
22030
- function canonicalCapabilitiesForDriftCheck(value) {
22031
- if (!Array.isArray(value)) return [];
22032
- const seen = /* @__PURE__ */ new Set();
22033
- for (const entry of value) if (typeof entry === "string" && entry.length > 0) seen.add(entry);
22034
- return [...seen].toSorted();
22035
- }
22036
- /**
22037
- * Matches a `(publisher_did, slug)` against the
22038
- * `minimumReleaseAgeExclude` allowlist. Mirrors the server-side helper
22039
- * of the same name in `packages/core/src/registry/config.ts`.
22040
- *
22041
- * DID-only on purpose: handles are aggregator-supplied envelope data
22042
- * and accepting them as a trust input would let a compromised
22043
- * aggregator bypass the holdback by claiming any handle for any
22044
- * package. DIDs are tied to the AT URI of the record itself.
22045
- *
22046
- * Entries from the config list have already been lowercased at
22047
- * manifest build time, so this only needs to lowercase the runtime
22048
- * values for comparison.
22049
- */
22050
- function releaseExemptFromMinimumAge(exclude, publisherDid, slug) {
22051
- if (!exclude || exclude.length === 0) return false;
22052
- const didLower = publisherDid.toLowerCase();
22053
- const fullDid = `${didLower}/${slug.toLowerCase()}`;
22054
- for (const entry of exclude) {
22055
- if (entry === didLower) return true;
22056
- if (entry === fullDid) return true;
22057
- }
22058
- return false;
22059
- }
22060
- async function searchRegistryPackages(config, opts) {
22061
- return (await getDiscoveryClient(config)).searchPackages(opts);
22062
- }
22063
- async function resolveRegistryPackage(config, handle, slug) {
22064
- return (await getDiscoveryClient(config)).resolvePackage(handle, slug);
22065
- }
22066
- async function getRegistryPackage(config, did, slug) {
22067
- return (await getDiscoveryClient(config)).getPackage(did, slug);
22068
- }
22069
- async function getLatestRegistryRelease(config, did, slug) {
22070
- return (await getDiscoveryClient(config)).getLatestRelease(did, slug);
22071
- }
22072
- /**
22073
- * Resolve a publisher DID to its claimed handle using the same
22074
- * `LocalActorResolver` pattern as `@emdash-cms/plugin-cli` and
22075
- * `@emdash-cms/auth-atproto`. Bidirectional verification (handle's
22076
- * domain points back to the same DID) is part of the resolver --
22077
- * `LocalActorResolver` returns the sentinel `"handle.invalid"` when
22078
- * the `alsoKnownAs` handle is present but doesn't round-trip.
22079
- *
22080
- * Three distinct outcomes the UI can render:
22081
- *
22082
- * - `{ status: "ok", handle }` — verified handle, round-trip OK.
22083
- * - `{ status: "invalid" }` — DID claims a handle but it doesn't
22084
- * resolve back. The publisher's handle setup is broken; the admin
22085
- * should see a clear "Invalid handle" indicator rather than the
22086
- * raw DID.
22087
- * - `{ status: "missing" }` — no handle claimed at all (no
22088
- * `alsoKnownAs`), or the DID document couldn't be fetched (network
22089
- * error, unsupported DID method).
22090
- */
22091
- let actorResolver = null;
22092
- async function getActorResolver() {
22093
- if (actorResolver) return actorResolver;
22094
- const { CompositeDidDocumentResolver, CompositeHandleResolver, DohJsonHandleResolver, LocalActorResolver, PlcDidDocumentResolver, WebDidDocumentResolver, WellKnownHandleResolver } = await import("@atcute/identity-resolver");
22095
- actorResolver = new LocalActorResolver({
22096
- handleResolver: new CompositeHandleResolver({ methods: {
22097
- dns: new DohJsonHandleResolver({ dohUrl: "https://cloudflare-dns.com/dns-query" }),
22098
- http: new WellKnownHandleResolver()
22099
- } }),
22100
- didDocumentResolver: new CompositeDidDocumentResolver({ methods: {
22101
- plc: new PlcDidDocumentResolver(),
22102
- web: new WebDidDocumentResolver()
22103
- } })
22104
- });
22105
- return actorResolver;
22106
- }
22107
- /**
22108
- * localStorage-backed cache for DID→handle resolutions. Handles are
22109
- * stable for hours-to-days in practice, but bound the cache so a
22110
- * compromised handle eventually flips back to "invalid" without a
22111
- * forced refresh. 24h matches the typical atproto handle TTL.
22112
- *
22113
- * Failures (network errors, unsupported DID method) are *not* cached --
22114
- * those should retry on the next render.
22115
- */
22116
- const HANDLE_CACHE_TTL_MS = 1440 * 60 * 1e3;
22117
- const HANDLE_CACHE_KEY_PREFIX = "emdash:did-handle:";
22118
- function readHandleCache(did) {
22119
- if (typeof localStorage === "undefined") return null;
22120
- try {
22121
- const raw = localStorage.getItem(`${HANDLE_CACHE_KEY_PREFIX}${did}`);
22122
- if (!raw) return null;
22123
- const parsed = JSON.parse(raw);
22124
- if (!parsed || typeof parsed.expiresAt !== "number" || parsed.expiresAt < Date.now()) return null;
22125
- return parsed.resolution;
22126
- } catch {
22127
- return null;
22128
- }
22129
- }
22130
- function writeHandleCache(did, resolution) {
22131
- if (typeof localStorage === "undefined") return;
22132
- try {
22133
- const entry = {
22134
- resolution,
22135
- expiresAt: Date.now() + HANDLE_CACHE_TTL_MS
22136
- };
22137
- localStorage.setItem(`${HANDLE_CACHE_KEY_PREFIX}${did}`, JSON.stringify(entry));
22138
- } catch {}
22139
- }
22140
- async function resolveDidToHandle(did) {
22141
- const cached = readHandleCache(did);
22142
- if (cached) return cached;
22143
- let result;
22144
- try {
22145
- const resolved = await (await getActorResolver()).resolve(did);
22146
- if (resolved.handle === "handle.invalid") result = { status: "invalid" };
22147
- else if (resolved.handle) result = {
22148
- status: "ok",
22149
- handle: resolved.handle
22150
- };
22151
- else result = { status: "missing" };
22152
- } catch (err) {
22153
- console.warn(`[registry] DID->handle resolution failed for ${did}:`, err);
22154
- return { status: "missing" };
22155
- }
22156
- writeHandleCache(did, result);
22157
- return result;
22158
- }
22159
- const INSTALL_ENDPOINT = `${API_BASE}/admin/plugins/registry/install`;
22160
- /**
22161
- * Install a plugin from the registry.
22162
- *
22163
- * Posts to the EmDash server, which re-resolves the same `(handle,
22164
- * slug)` against the aggregator, re-verifies the bundle's checksum
22165
- * against the signed release record, and writes the install. Surfaces
22166
- * structured error codes (`RELEASE_YANKED`, `CHECKSUM_MISMATCH`,
22167
- * `DECLARED_ACCESS_DRIFT`, etc.) that callers map to localized
22168
- * messages.
22169
- */
22170
- async function installRegistryPlugin(body) {
22171
- const response = await apiFetch(INSTALL_ENDPOINT, {
22172
- method: "POST",
22173
- headers: { "Content-Type": "application/json" },
22174
- body: JSON.stringify(body)
22175
- });
22176
- if (!response.ok) await throwResponseError(response, i18n._({
22177
- id: "/LWKyx",
22178
- message: "Failed to install plugin"
22179
- }));
22180
- return (await response.json()).data;
22181
- }
22182
-
22183
22766
  //#endregion
22184
22767
  //#region src/components/PublisherHandle.tsx
22185
22768
  /**
@@ -22392,7 +22975,9 @@ function RegistryBrowse({ config, installedRegistryUris = /* @__PURE__ */ new Se
22392
22975
  function RegistryPackageCard({ pkg, installed }) {
22393
22976
  const { _: _t2 } = useLingui();
22394
22977
  const linkSegment = usePublisherHandle(pkg.did, pkg.handle).handle ?? pkg.did;
22395
- const profile = pkg.profile;
22978
+ const name = pkg.profile?.name;
22979
+ const description = pkg.profile?.description;
22980
+ const license = pkg.profile?.license;
22396
22981
  const verified = (pkg.labels ?? []).some((l) => l.val === "verified");
22397
22982
  return /* @__PURE__ */ jsx(Link$1, {
22398
22983
  to: "/plugins/marketplace/$pluginId",
@@ -22410,7 +22995,7 @@ function RegistryPackageCard({ pkg, installed }) {
22410
22995
  className: "flex items-center gap-2",
22411
22996
  children: [/* @__PURE__ */ jsx("h2", {
22412
22997
  className: "truncate font-semibold",
22413
- children: profile.name ?? pkg.slug
22998
+ children: name ?? pkg.slug
22414
22999
  }), verified ? /* @__PURE__ */ jsx(ShieldCheck, {
22415
23000
  className: "h-4 w-4 shrink-0 text-kumo-brand",
22416
23001
  "aria-label": _t2({
@@ -22424,9 +23009,13 @@ function RegistryPackageCard({ pkg, installed }) {
22424
23009
  aggregatorHandle: pkg.handle,
22425
23010
  variant: "card"
22426
23011
  }),
22427
- profile.description ? /* @__PURE__ */ jsx("p", {
23012
+ description ? /* @__PURE__ */ jsx("p", {
22428
23013
  className: "mt-2 line-clamp-2 text-sm text-kumo-default",
22429
- children: profile.description
23014
+ children: description
23015
+ }) : null,
23016
+ license ? /* @__PURE__ */ jsx("p", {
23017
+ className: "mt-2 text-xs text-kumo-subtle",
23018
+ children: license
22430
23019
  }) : null,
22431
23020
  installed ? /* @__PURE__ */ jsx("div", {
22432
23021
  className: "mt-3",
@@ -22467,7 +23056,7 @@ function RegistryPluginDetail({ pluginId, config }) {
22467
23056
  const { data: installedPlugins } = useQuery({
22468
23057
  queryKey: ["plugins"],
22469
23058
  queryFn: async () => {
22470
- const { fetchPlugins } = await import("./plugins-qrjvPK4g.js").then((n) => n.a);
23059
+ const { fetchPlugins } = await import("./plugins-BZzztFdK.js").then((n) => n.a);
22471
23060
  return fetchPlugins();
22472
23061
  }
22473
23062
  });
@@ -22488,20 +23077,63 @@ function RegistryPluginDetail({ pluginId, config }) {
22488
23077
  enabled: Boolean(publisher && slug)
22489
23078
  });
22490
23079
  const handleResult = usePublisherHandle(pkg?.did ?? "", pkg?.handle);
22491
- const { data: release } = useQuery({
23080
+ const { data: releasesData } = useQuery({
22492
23081
  queryKey: [
22493
23082
  "registry",
22494
- "latest-release",
23083
+ "releases",
22495
23084
  config.aggregatorUrl,
23085
+ config.acceptLabelers,
22496
23086
  pkg?.did,
22497
23087
  slug
22498
23088
  ],
22499
- queryFn: () => getLatestRegistryRelease(config, pkg.did, slug),
23089
+ queryFn: () => listRegistryReleases(config, pkg.did, slug, { limit: 100 }),
22500
23090
  enabled: Boolean(pkg?.did && slug)
22501
23091
  });
22502
- const ext = (release?.release)?.extensions?.["com.emdashcms.experimental.package.releaseExtension"];
23092
+ const releases = React$1.useMemo(() => (releasesData?.releases ?? []).filter((r) => r.release !== null && !isYanked(r)), [releasesData]);
23093
+ const hasFilteredAllReleases = (releasesData?.releases.length ?? 0) > 0 && releases.length === 0;
23094
+ const defaultVersion = React$1.useMemo(() => {
23095
+ if (!pkg || releases.length === 0) return void 0;
23096
+ return (releases.find((r) => releasePassesPolicy(r, {
23097
+ did: pkg.did,
23098
+ slug
23099
+ }, config.policy)) ?? releases[0])?.version;
23100
+ }, [
23101
+ pkg,
23102
+ releases,
23103
+ slug,
23104
+ config.policy
23105
+ ]);
23106
+ const [selectedVersion, setSelectedVersion] = React$1.useState(void 0);
23107
+ const [prevPluginId, setPrevPluginId] = React$1.useState(pluginId);
23108
+ if (prevPluginId !== pluginId) {
23109
+ setPrevPluginId(pluginId);
23110
+ setSelectedVersion(void 0);
23111
+ }
23112
+ if (selectedVersion !== void 0 && releases.length > 0 && !releases.some((r) => r.version === selectedVersion)) setSelectedVersion(void 0);
23113
+ const effectiveVersion = selectedVersion ?? defaultVersion;
23114
+ const release = React$1.useMemo(() => releases.find((r) => r.version === effectiveVersion), [releases, effectiveVersion]);
23115
+ const isPreRelease = release ? isPreReleaseVersion(release.version) : false;
23116
+ const ext = (release?.release?.extensions)?.["com.emdashcms.experimental.package.releaseExtension"];
22503
23117
  const capabilities = Array.isArray(ext?.capabilities) ? canonicalCapabilitiesForDriftCheck(ext?.capabilities) : canonicalCapabilitiesForDriftCheck(declaredAccessToCapabilityList(ext?.declaredAccess));
22504
- const profile = pkg?.profile;
23118
+ const pkgProfile = pkg?.profile ?? null;
23119
+ const displayName = pkgProfile?.name;
23120
+ const description = pkgProfile?.description;
23121
+ const licenseText = pkgProfile?.license;
23122
+ const keywordList = pkgProfile?.keywords ?? [];
23123
+ const authorList = (pkgProfile?.authors ?? []).map((a) => ({
23124
+ name: a.name,
23125
+ url: safeExternalHref(a.url),
23126
+ email: safeEmail(a.email)
23127
+ }));
23128
+ const securityList = (pkgProfile?.security ?? []).flatMap((c) => {
23129
+ const url = safeExternalHref(c.url);
23130
+ const email = safeEmail(c.email);
23131
+ return url || email ? [{
23132
+ url,
23133
+ email
23134
+ }] : [];
23135
+ });
23136
+ const repoHref = safeExternalHref(release?.release?.repo);
22505
23137
  const verified = (pkg?.labels ?? []).some((l) => l.val === "verified");
22506
23138
  const policyOk = release && pkg ? releasePassesPolicy(release, {
22507
23139
  did: pkg.did,
@@ -22566,7 +23198,7 @@ function RegistryPluginDetail({ pluginId, config }) {
22566
23198
  children: [
22567
23199
  /* @__PURE__ */ jsx(BackLink, {}),
22568
23200
  /* @__PURE__ */ jsxs("div", {
22569
- className: "flex items-start gap-4",
23201
+ className: "flex flex-wrap items-start gap-4",
22570
23202
  children: [
22571
23203
  /* @__PURE__ */ jsx("div", {
22572
23204
  className: "rounded-xl bg-kumo-subtle p-3 text-kumo-subtle",
@@ -22582,7 +23214,7 @@ function RegistryPluginDetail({ pluginId, config }) {
22582
23214
  className: "flex items-center gap-2",
22583
23215
  children: [/* @__PURE__ */ jsx("h1", {
22584
23216
  className: "truncate text-3xl font-bold",
22585
- children: profile?.name ?? slug
23217
+ children: displayName ?? slug
22586
23218
  }), verified ? /* @__PURE__ */ jsx(ShieldCheck, {
22587
23219
  className: "h-5 w-5 shrink-0 text-kumo-brand",
22588
23220
  "aria-label": _t({
@@ -22606,40 +23238,88 @@ function RegistryPluginDetail({ pluginId, config }) {
22606
23238
  })
22607
23239
  ]
22608
23240
  }),
22609
- release ? /* @__PURE__ */ jsxs("p", {
22610
- className: "text-xs text-kumo-subtle",
22611
- children: [
22612
- _t({
22613
- id: "YYdC3A",
22614
- message: "Version {0}",
22615
- values: { 0: release.version }
22616
- }),
22617
- " · ",
22618
- _t({
22619
- id: "294VT5",
22620
- message: "indexed {0}",
22621
- values: { 0: formatDate(release.indexedAt) }
22622
- })
22623
- ]
23241
+ release ? /* @__PURE__ */ jsxs("div", {
23242
+ className: "mt-1 flex flex-wrap items-center gap-2",
23243
+ children: [/* @__PURE__ */ jsxs("p", {
23244
+ className: "text-xs text-kumo-subtle",
23245
+ children: [
23246
+ _t({
23247
+ id: "YYdC3A",
23248
+ message: "Version {0}",
23249
+ values: { 0: release.version }
23250
+ }),
23251
+ " · ",
23252
+ _t({
23253
+ id: "294VT5",
23254
+ message: "indexed {0}",
23255
+ values: { 0: formatDate(release.indexedAt) }
23256
+ })
23257
+ ]
23258
+ }), isPreRelease ? /* @__PURE__ */ jsx(Badge, { children: _t({
23259
+ id: "omrja/",
23260
+ message: "Pre-release"
23261
+ }) }) : null]
22624
23262
  }) : null
22625
23263
  ]
22626
23264
  }),
22627
- /* @__PURE__ */ jsx("div", { children: isInstalled ? /* @__PURE__ */ jsx(Button, {
22628
- variant: "secondary",
22629
- disabled: true,
22630
- children: _t({
22631
- id: "eQkgKV",
22632
- message: "Installed"
22633
- })
22634
- }) : /* @__PURE__ */ jsx(Button, {
22635
- variant: "primary",
22636
- disabled: !release || !policyOk || handleResult.status === "invalid",
22637
- onClick: () => setShowConsent(true),
22638
- children: _t({
22639
- id: "TKQ7K+",
22640
- message: "Install"
22641
- })
22642
- }) })
23265
+ /* @__PURE__ */ jsxs("div", {
23266
+ className: "flex min-w-0 flex-wrap items-center gap-2",
23267
+ children: [releases.length > 1 ? /* @__PURE__ */ jsx(Select, {
23268
+ "aria-label": _t({
23269
+ id: "eE0JZ4",
23270
+ message: "Version"
23271
+ }),
23272
+ className: "w-full max-w-[220px]",
23273
+ value: effectiveVersion ?? "",
23274
+ onValueChange: (v) => setSelectedVersion(v ?? void 0),
23275
+ renderValue: (v) => typeof v === "string" ? v : "",
23276
+ children: releases.map((r) => {
23277
+ const preRelease = isPreReleaseVersion(r.version);
23278
+ const policyBlocked = !releasePassesPolicy(r, {
23279
+ did: pkg.did,
23280
+ slug
23281
+ }, config.policy);
23282
+ return /* @__PURE__ */ jsx(Select.Option, {
23283
+ value: r.version,
23284
+ children: /* @__PURE__ */ jsxs("span", {
23285
+ className: "flex items-center gap-2",
23286
+ children: [
23287
+ /* @__PURE__ */ jsx("span", { children: r.version }),
23288
+ preRelease ? /* @__PURE__ */ jsx("span", {
23289
+ className: "text-xs text-kumo-subtle",
23290
+ children: _t({
23291
+ id: "D2C1D+",
23292
+ message: "(pre-release)"
23293
+ })
23294
+ }) : null,
23295
+ policyBlocked ? /* @__PURE__ */ jsx("span", {
23296
+ className: "text-xs text-kumo-subtle",
23297
+ children: _t({
23298
+ id: "W3SUIF",
23299
+ message: "(too new)"
23300
+ })
23301
+ }) : null
23302
+ ]
23303
+ })
23304
+ }, r.version);
23305
+ })
23306
+ }) : null, isInstalled ? /* @__PURE__ */ jsx(Button, {
23307
+ variant: "secondary",
23308
+ disabled: true,
23309
+ children: _t({
23310
+ id: "eQkgKV",
23311
+ message: "Installed"
23312
+ })
23313
+ }) : /* @__PURE__ */ jsx(Button, {
23314
+ variant: "primary",
23315
+ disabled: !release || !policyOk || handleResult.status === "invalid",
23316
+ onClick: () => setShowConsent(true),
23317
+ children: _t({
23318
+ id: "TKQ7K+",
23319
+ message: "Install"
23320
+ })
23321
+ })]
23322
+ })
22643
23323
  ]
22644
23324
  }),
22645
23325
  handleResult.status === "invalid" ? /* @__PURE__ */ jsxs("div", {
@@ -22659,6 +23339,23 @@ function RegistryPluginDetail({ pluginId, config }) {
22659
23339
  })
22660
23340
  })] })]
22661
23341
  }) : null,
23342
+ hasFilteredAllReleases ? /* @__PURE__ */ jsxs("div", {
23343
+ className: "flex items-start gap-3 rounded-md border border-kumo-warning bg-kumo-warning/10 p-4 text-kumo-warning",
23344
+ role: "status",
23345
+ children: [/* @__PURE__ */ jsx(Warning, { className: "mt-0.5 h-5 w-5 shrink-0" }), /* @__PURE__ */ jsxs("div", { children: [/* @__PURE__ */ jsx("p", {
23346
+ className: "font-medium",
23347
+ children: _t({
23348
+ id: "rqsq95",
23349
+ message: "No installable releases"
23350
+ })
23351
+ }), /* @__PURE__ */ jsx("p", {
23352
+ className: "mt-1 text-sm text-kumo-default",
23353
+ children: _t({
23354
+ id: "o+La9d",
23355
+ message: "Every published release of this plugin has been withdrawn or could not be verified. Check back later, or contact the publisher."
23356
+ })
23357
+ })] })]
23358
+ }) : null,
22662
23359
  release && !policyOk ? /* @__PURE__ */ jsxs("div", {
22663
23360
  className: "flex items-start gap-3 rounded-md border border-kumo-warning bg-kumo-warning/10 p-4 text-kumo-warning",
22664
23361
  role: "status",
@@ -22677,10 +23374,82 @@ function RegistryPluginDetail({ pluginId, config }) {
22677
23374
  })
22678
23375
  })] })]
22679
23376
  }) : null,
22680
- profile?.description ? /* @__PURE__ */ jsx("p", {
23377
+ description ? /* @__PURE__ */ jsx("p", {
22681
23378
  className: "text-base text-kumo-default",
22682
- children: profile.description
23379
+ children: description
23380
+ }) : null,
23381
+ licenseText || repoHref || keywordList.length > 0 ? /* @__PURE__ */ jsxs("section", {
23382
+ className: "flex flex-wrap items-center gap-2",
23383
+ children: [
23384
+ licenseText ? /* @__PURE__ */ jsx(LicenseBadge, { license: licenseText }) : null,
23385
+ keywordList.map((k) => /* @__PURE__ */ jsx(Badge, { children: k }, k)),
23386
+ repoHref ? /* @__PURE__ */ jsx(LinkButton, {
23387
+ href: repoHref,
23388
+ external: true,
23389
+ variant: "secondary",
23390
+ children: _t({
23391
+ id: "n/I+da",
23392
+ message: "View source"
23393
+ })
23394
+ }) : null
23395
+ ]
22683
23396
  }) : null,
23397
+ authorList.length > 0 ? /* @__PURE__ */ jsxs("section", { children: [/* @__PURE__ */ jsx("h2", {
23398
+ className: "text-sm font-semibold text-kumo-subtle",
23399
+ children: _t({
23400
+ id: "dpqPBg",
23401
+ message: "Authors"
23402
+ })
23403
+ }), /* @__PURE__ */ jsx("ul", {
23404
+ className: "mt-2 space-y-1",
23405
+ children: authorList.map((a, i) => /* @__PURE__ */ jsxs("li", {
23406
+ className: "flex flex-wrap items-center gap-x-3 gap-y-1 text-sm",
23407
+ children: [
23408
+ /* @__PURE__ */ jsx("span", {
23409
+ className: "font-medium text-kumo-default",
23410
+ children: a.name
23411
+ }),
23412
+ a.url ? /* @__PURE__ */ jsx("a", {
23413
+ href: a.url,
23414
+ target: "_blank",
23415
+ rel: "noreferrer",
23416
+ className: "text-kumo-brand hover:underline",
23417
+ children: _t({
23418
+ id: "On0aF2",
23419
+ message: "Website"
23420
+ })
23421
+ }) : null,
23422
+ a.email ? /* @__PURE__ */ jsx("a", {
23423
+ href: `mailto:${a.email}`,
23424
+ className: "text-kumo-brand hover:underline",
23425
+ children: a.email
23426
+ }) : null
23427
+ ]
23428
+ }, `${a.name}-${i}`))
23429
+ })] }) : null,
23430
+ securityList.length > 0 ? /* @__PURE__ */ jsxs("section", { children: [/* @__PURE__ */ jsx("h2", {
23431
+ className: "text-sm font-semibold text-kumo-subtle",
23432
+ children: _t({
23433
+ id: "FDZXmz",
23434
+ message: "Security contacts"
23435
+ })
23436
+ }), /* @__PURE__ */ jsx("ul", {
23437
+ className: "mt-2 space-y-1",
23438
+ children: securityList.map((c, i) => /* @__PURE__ */ jsxs("li", {
23439
+ className: "flex flex-wrap items-center gap-x-3 gap-y-1 text-sm",
23440
+ children: [c.email ? /* @__PURE__ */ jsx("a", {
23441
+ href: `mailto:${c.email}`,
23442
+ className: "text-kumo-brand hover:underline",
23443
+ children: c.email
23444
+ }) : null, c.url ? /* @__PURE__ */ jsx("a", {
23445
+ href: c.url,
23446
+ target: "_blank",
23447
+ rel: "noreferrer",
23448
+ className: "text-kumo-brand hover:underline",
23449
+ children: c.url
23450
+ }) : null]
23451
+ }, `${c.email ?? c.url ?? "contact"}-${i}`))
23452
+ })] }) : null,
22684
23453
  capabilities.length > 0 ? /* @__PURE__ */ jsxs("section", { children: [/* @__PURE__ */ jsx("h2", {
22685
23454
  className: "text-sm font-semibold text-kumo-subtle",
22686
23455
  children: _t({
@@ -22693,7 +23462,7 @@ function RegistryPluginDetail({ pluginId, config }) {
22693
23462
  })] }) : null,
22694
23463
  showConsent && release ? /* @__PURE__ */ jsx(CapabilityConsentDialog, {
22695
23464
  mode: "install",
22696
- pluginName: profile?.name ?? slug,
23465
+ pluginName: displayName ?? slug,
22697
23466
  capabilities,
22698
23467
  isPending: installMutation.isPending,
22699
23468
  error: getMutationError(installMutation.error),
@@ -22737,6 +23506,33 @@ function declaredAccessToCapabilityList(declaredAccess) {
22737
23506
  }
22738
23507
  return out;
22739
23508
  }
23509
+ const PRE_RELEASE_VERSION_RE = /^\d+\.\d+\.\d+-/;
23510
+ /**
23511
+ * Detects semver pre-release identifiers (`1.0.0-alpha.1`, `2.0.0-rc.2`). The
23512
+ * release lexicon does not enforce semver shape, so a permissive `includes("-")`
23513
+ * check would light up the badge for malformed values like `-1.0.0` or
23514
+ * `abc-def`. Require a `MAJOR.MINOR.PATCH-` prefix instead.
23515
+ */
23516
+ function isPreReleaseVersion(version) {
23517
+ return PRE_RELEASE_VERSION_RE.test(version);
23518
+ }
23519
+ const YANKED_LABEL_VALUE = "security:yanked";
23520
+ /**
23521
+ * Aggregators forward labels applied by their configured labellers. `security:yanked`
23522
+ * is a hard-enforcement label that publishers can self-apply (or that a labeller
23523
+ * applies on their behalf) to retract a release after publication. Sites whose
23524
+ * `acceptLabelers` config includes the labeller never see yanked releases at all
23525
+ * (server filtering), but sites without it receive yanked releases interleaved
23526
+ * with installable ones — filter them out so they never reach the picker.
23527
+ *
23528
+ * `neg` (negated labels) is intentionally ignored to match the server install
23529
+ * handler, which only checks `l.val === "security:yanked"`. Diverging here would
23530
+ * let the UI surface an install affordance the server will reject with
23531
+ * `RELEASE_YANKED`. Honoring `neg` on both sides is a separate follow-up.
23532
+ */
23533
+ function isYanked(release) {
23534
+ return (release.labels ?? []).some((l) => l.val === YANKED_LABEL_VALUE);
23535
+ }
22740
23536
  function formatDate(iso) {
22741
23537
  try {
22742
23538
  return new Date(iso).toLocaleDateString();
@@ -22744,6 +23540,61 @@ function formatDate(iso) {
22744
23540
  return iso;
22745
23541
  }
22746
23542
  }
23543
+ /**
23544
+ * SPDX page URL for a license, or `null` when the value isn't a single
23545
+ * SPDX identifier (compound expressions like "MIT OR Apache-2.0" and the
23546
+ * literal "proprietary" have no canonical spdx.org page).
23547
+ */
23548
+ /**
23549
+ * Validate an untrusted aggregator-supplied URL for use in an `href`.
23550
+ * Returns the normalised URL only when it is an absolute `http(s)` URL;
23551
+ * everything else (relative, `javascript:`, `data:`, garbage, non-string)
23552
+ * returns `null`. The profile/release records are pass-throughs from a
23553
+ * remote service, so an unsanitised `href` is stored XSS in the
23554
+ * authenticated admin origin.
23555
+ */
23556
+ function safeExternalHref(value) {
23557
+ if (typeof value !== "string" || value.length === 0) return null;
23558
+ let parsed;
23559
+ try {
23560
+ parsed = new URL(value);
23561
+ } catch {
23562
+ return null;
23563
+ }
23564
+ if (parsed.protocol !== "https:" && parsed.protocol !== "http:") return null;
23565
+ return parsed.href;
23566
+ }
23567
+ const EMAIL_RE = /^[^\s@<>()[\]\\,;:"?&=%/]+@[^\s@<>()[\]\\,;:"?&=%/]+\.[^\s@<>()[\]\\,;:"?&=%/]+$/;
23568
+ function safeEmail(value) {
23569
+ if (typeof value !== "string") return null;
23570
+ const email = value.trim();
23571
+ if (email.length === 0 || email.length > 320) return null;
23572
+ return EMAIL_RE.test(email) ? email : null;
23573
+ }
23574
+ const SPDX_SINGLE_ID_RE = /^[A-Za-z0-9.+-]+$/;
23575
+ function spdxLicenseHref(license) {
23576
+ const id = license.trim();
23577
+ if (!SPDX_SINGLE_ID_RE.test(id)) return null;
23578
+ if (id.toLowerCase() === "proprietary") return null;
23579
+ return `https://spdx.org/licenses/${id}.html`;
23580
+ }
23581
+ function LicenseBadge({ license }) {
23582
+ const { _: _t3 } = useLingui();
23583
+ const href = spdxLicenseHref(license);
23584
+ if (!href) return /* @__PURE__ */ jsx(Badge, { children: license });
23585
+ return /* @__PURE__ */ jsx("a", {
23586
+ href,
23587
+ target: "_blank",
23588
+ rel: "noreferrer",
23589
+ "aria-label": _t3({
23590
+ id: "4Q1BOX",
23591
+ message: "View the {license} license on spdx.org",
23592
+ values: { license }
23593
+ }),
23594
+ className: "hover:opacity-80",
23595
+ children: /* @__PURE__ */ jsx(Badge, { children: license })
23596
+ });
23597
+ }
22747
23598
  function formatHoldback(seconds) {
22748
23599
  if (seconds <= 0) return "0s";
22749
23600
  if (seconds < 3600) return `${Math.round(seconds / 60)} min`;
@@ -34067,6 +34918,13 @@ function AuthorMappingStep({ authorMappings, emdashUsers, onMappingChange, onCon
34067
34918
 
34068
34919
  //#endregion
34069
34920
  //#region src/routes/bylines.tsx
34921
+ /**
34922
+ * True when the load-more snapshot still matches the current filter state.
34923
+ * Used to discard appends from requests whose filters have changed mid-flight.
34924
+ */
34925
+ function loadMoreSnapshotMatches(snapshot, current) {
34926
+ return snapshot.search === current.search && snapshot.guestFilter === current.guestFilter && snapshot.locale === current.locale;
34927
+ }
34070
34928
  function toFormState(byline) {
34071
34929
  if (!byline) return {
34072
34930
  slug: "",
@@ -34092,21 +34950,39 @@ function getUserLabel(user) {
34092
34950
  function BylinesPage() {
34093
34951
  const { _: _t } = useLingui();
34094
34952
  const queryClient = useQueryClient();
34953
+ const navigate = useNavigate$1();
34954
+ const { locale: routeLocale } = useSearch({ from: "/_admin/bylines" });
34095
34955
  const [search, setSearch] = React$1.useState("");
34096
34956
  const [guestFilter, setGuestFilter] = React$1.useState("all");
34097
34957
  const [selectedId, setSelectedId] = React$1.useState(null);
34098
34958
  const [showDeleteConfirm, setShowDeleteConfirm] = React$1.useState(false);
34099
34959
  const [allItems, setAllItems] = React$1.useState([]);
34100
34960
  const [nextCursor, setNextCursor] = React$1.useState(void 0);
34961
+ const { data: manifest } = useQuery({
34962
+ queryKey: ["manifest"],
34963
+ queryFn: fetchManifest
34964
+ });
34965
+ const i18n = useI18nConfig(manifest);
34966
+ const isMultiLocale = !!i18n && i18n.locales.length > 1;
34967
+ const activeLocale = routeLocale ?? i18n?.defaultLocale ?? void 0;
34968
+ const handleLocaleChange = (locale) => {
34969
+ navigate({
34970
+ to: "/bylines",
34971
+ search: { locale: locale || void 0 }
34972
+ });
34973
+ setSelectedId(null);
34974
+ };
34101
34975
  const { data, isLoading, error } = useQuery({
34102
34976
  queryKey: [
34103
34977
  "bylines",
34104
34978
  search,
34105
- guestFilter
34979
+ guestFilter,
34980
+ activeLocale ?? null
34106
34981
  ],
34107
34982
  queryFn: () => fetchBylines({
34108
34983
  search: search || void 0,
34109
34984
  isGuest: guestFilter === "all" ? void 0 : guestFilter === "guest",
34985
+ locale: activeLocale,
34110
34986
  limit: 50
34111
34987
  })
34112
34988
  });
@@ -34122,28 +34998,44 @@ function BylinesPage() {
34122
34998
  });
34123
34999
  const users = usersData?.items ?? [];
34124
35000
  const loadMoreMutation = useMutation({
34125
- mutationFn: async () => {
34126
- if (!nextCursor) return null;
34127
- return fetchBylines({
34128
- search: search || void 0,
34129
- isGuest: guestFilter === "all" ? void 0 : guestFilter === "guest",
34130
- limit: 50,
34131
- cursor: nextCursor
34132
- });
35001
+ mutationFn: async (snapshot) => {
35002
+ return {
35003
+ result: await fetchBylines({
35004
+ search: snapshot.search || void 0,
35005
+ isGuest: snapshot.guestFilter === "all" ? void 0 : snapshot.guestFilter === "guest",
35006
+ locale: snapshot.locale,
35007
+ limit: 50,
35008
+ cursor: snapshot.cursor
35009
+ }),
35010
+ snapshot
35011
+ };
34133
35012
  },
34134
- onSuccess: (result) => {
34135
- if (result) {
34136
- setAllItems((prev) => [...prev, ...result.items]);
34137
- setNextCursor(result.nextCursor);
34138
- }
35013
+ onSuccess: ({ result, snapshot }) => {
35014
+ if (!loadMoreSnapshotMatches(snapshot, {
35015
+ search,
35016
+ guestFilter,
35017
+ locale: activeLocale
35018
+ })) return;
35019
+ setAllItems((prev) => [...prev, ...result.items]);
35020
+ setNextCursor(result.nextCursor);
34139
35021
  }
34140
35022
  });
34141
35023
  const items = allItems;
34142
- const selected = items.find((item) => item.id === selectedId) ?? null;
35024
+ const { data: selectedRemote } = useQuery({
35025
+ queryKey: ["byline", selectedId],
35026
+ queryFn: () => selectedId ? fetchByline(selectedId) : Promise.resolve(null),
35027
+ enabled: !!selectedId
35028
+ });
35029
+ const selected = selectedRemote ?? items.find((item) => item.id === selectedId) ?? null;
34143
35030
  const [form, setForm] = React$1.useState(() => toFormState(null));
34144
35031
  React$1.useEffect(() => {
34145
35032
  setForm(toFormState(selected));
34146
- }, [selectedId, selected]);
35033
+ }, [selected]);
35034
+ const { data: translationsData } = useQuery({
35035
+ queryKey: ["byline-translations", selectedId],
35036
+ queryFn: () => selectedId ? fetchBylineTranslations(selectedId) : Promise.resolve({ items: [] }),
35037
+ enabled: !!selectedId && isMultiLocale
35038
+ });
34147
35039
  const createMutation = useMutation({
34148
35040
  mutationFn: () => createByline({
34149
35041
  slug: form.slug,
@@ -34151,7 +35043,8 @@ function BylinesPage() {
34151
35043
  bio: form.bio || null,
34152
35044
  websiteUrl: form.websiteUrl || null,
34153
35045
  userId: form.userId,
34154
- isGuest: form.isGuest
35046
+ isGuest: form.isGuest,
35047
+ locale: activeLocale
34155
35048
  }),
34156
35049
  onSuccess: (created) => {
34157
35050
  queryClient.invalidateQueries({ queryKey: ["bylines"] });
@@ -34172,6 +35065,7 @@ function BylinesPage() {
34172
35065
  },
34173
35066
  onSuccess: () => {
34174
35067
  queryClient.invalidateQueries({ queryKey: ["bylines"] });
35068
+ if (selectedId) queryClient.invalidateQueries({ queryKey: ["byline", selectedId] });
34175
35069
  }
34176
35070
  });
34177
35071
  const deleteMutation = useMutation({
@@ -34185,6 +35079,26 @@ function BylinesPage() {
34185
35079
  setShowDeleteConfirm(false);
34186
35080
  }
34187
35081
  });
35082
+ const [pendingTranslationLocale, setPendingTranslationLocale] = React$1.useState(null);
35083
+ const translateMutation = useMutation({
35084
+ mutationFn: (targetLocale) => {
35085
+ if (!selectedId) throw new Error("No byline selected");
35086
+ setPendingTranslationLocale(targetLocale);
35087
+ return createBylineTranslation(selectedId, { locale: targetLocale });
35088
+ },
35089
+ onSettled: () => {
35090
+ setPendingTranslationLocale(null);
35091
+ },
35092
+ onSuccess: (created) => {
35093
+ queryClient.invalidateQueries({ queryKey: ["bylines"] });
35094
+ if (selectedId) queryClient.invalidateQueries({ queryKey: ["byline-translations", selectedId] });
35095
+ navigate({
35096
+ to: "/bylines",
35097
+ search: { locale: created.locale }
35098
+ });
35099
+ setSelectedId(created.id);
35100
+ }
35101
+ });
34188
35102
  if (isLoading) return /* @__PURE__ */ jsx("div", {
34189
35103
  className: "flex items-center justify-center min-h-[30vh]",
34190
35104
  children: /* @__PURE__ */ jsx(Loader, {})
@@ -34198,230 +35112,273 @@ function BylinesPage() {
34198
35112
  })
34199
35113
  });
34200
35114
  const isSaving = createMutation.isPending || updateMutation.isPending;
34201
- const mutationError = createMutation.error || updateMutation.error || deleteMutation.error;
35115
+ const mutationError = createMutation.error || updateMutation.error || deleteMutation.error || translateMutation.error;
34202
35116
  return /* @__PURE__ */ jsxs("div", {
34203
- className: "grid grid-cols-1 gap-6 lg:grid-cols-[320px_1fr]",
35117
+ className: "space-y-4",
34204
35118
  children: [
35119
+ isMultiLocale && i18n && activeLocale ? /* @__PURE__ */ jsxs("div", {
35120
+ className: "flex items-center justify-between",
35121
+ children: [/* @__PURE__ */ jsx("div", { children: /* @__PURE__ */ jsx("h1", {
35122
+ className: "text-2xl font-semibold",
35123
+ children: _t({
35124
+ id: "xLGa7J",
35125
+ message: "Bylines"
35126
+ })
35127
+ }) }), /* @__PURE__ */ jsx(LocaleSwitcher, {
35128
+ locales: i18n.locales,
35129
+ defaultLocale: i18n.defaultLocale,
35130
+ value: activeLocale,
35131
+ onChange: handleLocaleChange
35132
+ })]
35133
+ }) : null,
34205
35134
  /* @__PURE__ */ jsxs("div", {
34206
- className: "rounded-lg border p-4",
35135
+ className: "grid grid-cols-1 gap-6 lg:grid-cols-[320px_1fr]",
34207
35136
  children: [/* @__PURE__ */ jsxs("div", {
34208
- className: "mb-4 space-y-2",
34209
- children: [/* @__PURE__ */ jsx(Input, {
34210
- placeholder: _t({
34211
- id: "4L2LjM",
34212
- message: "Search bylines"
34213
- }),
34214
- value: search,
34215
- onChange: (e) => setSearch(e.target.value)
34216
- }), /* @__PURE__ */ jsxs("div", {
34217
- className: "flex items-center gap-2",
34218
- children: [/* @__PURE__ */ jsx("div", {
34219
- className: "flex-1",
34220
- children: /* @__PURE__ */ jsx(Select, {
34221
- "aria-label": _t({
34222
- id: "kYxs0a",
34223
- message: "Filter byline type"
34224
- }),
34225
- value: guestFilter,
34226
- onValueChange: (v) => setGuestFilter(v ?? "all"),
34227
- items: {
34228
- all: _t({
34229
- id: "VK3dVJ",
34230
- message: "All bylines"
34231
- }),
34232
- guest: _t({
34233
- id: "+0jFzA",
34234
- message: "Guest only"
35137
+ className: "rounded-lg border p-4",
35138
+ children: [/* @__PURE__ */ jsxs("div", {
35139
+ className: "mb-4 space-y-2",
35140
+ children: [/* @__PURE__ */ jsx(Input, {
35141
+ placeholder: _t({
35142
+ id: "4L2LjM",
35143
+ message: "Search bylines"
35144
+ }),
35145
+ value: search,
35146
+ onChange: (e) => setSearch(e.target.value)
35147
+ }), /* @__PURE__ */ jsxs("div", {
35148
+ className: "flex items-center gap-2",
35149
+ children: [/* @__PURE__ */ jsx("div", {
35150
+ className: "flex-1",
35151
+ children: /* @__PURE__ */ jsx(Select, {
35152
+ "aria-label": _t({
35153
+ id: "kYxs0a",
35154
+ message: "Filter byline type"
34235
35155
  }),
34236
- linked: _t({
34237
- id: "RZ5T1J",
34238
- message: "Linked only"
34239
- })
35156
+ value: guestFilter,
35157
+ onValueChange: (v) => setGuestFilter(v ?? "all"),
35158
+ items: {
35159
+ all: _t({
35160
+ id: "VK3dVJ",
35161
+ message: "All bylines"
35162
+ }),
35163
+ guest: _t({
35164
+ id: "+0jFzA",
35165
+ message: "Guest only"
35166
+ }),
35167
+ linked: _t({
35168
+ id: "RZ5T1J",
35169
+ message: "Linked only"
35170
+ })
35171
+ },
35172
+ className: "w-full"
35173
+ })
35174
+ }), /* @__PURE__ */ jsx(Button, {
35175
+ variant: "secondary",
35176
+ onClick: () => {
35177
+ setSelectedId(null);
35178
+ setForm(toFormState(null));
34240
35179
  },
34241
- className: "w-full"
34242
- })
34243
- }), /* @__PURE__ */ jsx(Button, {
34244
- variant: "secondary",
34245
- onClick: () => {
34246
- setSelectedId(null);
34247
- setForm(toFormState(null));
34248
- },
34249
- children: _t({
34250
- id: "isRobC",
34251
- message: "New"
34252
- })
35180
+ children: _t({
35181
+ id: "isRobC",
35182
+ message: "New"
35183
+ })
35184
+ })]
34253
35185
  })]
35186
+ }), /* @__PURE__ */ jsxs("div", {
35187
+ className: "space-y-2 max-h-[70vh] overflow-auto",
35188
+ children: [
35189
+ items.map((item) => {
35190
+ return /* @__PURE__ */ jsxs("button", {
35191
+ type: "button",
35192
+ onClick: () => setSelectedId(item.id),
35193
+ className: `w-full rounded border p-3 text-start ${item.id === selectedId ? "border-kumo-brand bg-kumo-brand/10" : "border-kumo-line"}`,
35194
+ children: [/* @__PURE__ */ jsx("p", {
35195
+ className: "font-medium",
35196
+ children: item.displayName
35197
+ }), /* @__PURE__ */ jsxs("p", {
35198
+ className: "text-xs text-kumo-subtle",
35199
+ children: [item.slug, item.isGuest ? _t({
35200
+ id: "89z9Fh",
35201
+ message: " - Guest"
35202
+ }) : item.userId ? _t({
35203
+ id: "X4uMa1",
35204
+ message: " - Linked"
35205
+ }) : ""]
35206
+ })]
35207
+ }, item.id);
35208
+ }),
35209
+ items.length === 0 && /* @__PURE__ */ jsx("p", {
35210
+ className: "text-sm text-kumo-subtle",
35211
+ children: _t({
35212
+ id: "Qc/a/A",
35213
+ message: "No bylines found"
35214
+ })
35215
+ }),
35216
+ nextCursor && /* @__PURE__ */ jsx(Button, {
35217
+ variant: "secondary",
35218
+ className: "w-full mt-2",
35219
+ onClick: () => loadMoreMutation.mutate({
35220
+ search,
35221
+ guestFilter,
35222
+ locale: activeLocale,
35223
+ cursor: nextCursor
35224
+ }),
35225
+ disabled: loadMoreMutation.isPending,
35226
+ children: loadMoreMutation.isPending ? _t({
35227
+ id: "Z3FXyt",
35228
+ message: "Loading..."
35229
+ }) : _t({
35230
+ id: "yQ2kGp",
35231
+ message: "Load more"
35232
+ })
35233
+ })
35234
+ ]
34254
35235
  })]
34255
35236
  }), /* @__PURE__ */ jsxs("div", {
34256
- className: "space-y-2 max-h-[70vh] overflow-auto",
35237
+ className: "rounded-lg border p-6 space-y-6",
34257
35238
  children: [
34258
- items.map((item) => {
34259
- return /* @__PURE__ */ jsxs("button", {
34260
- type: "button",
34261
- onClick: () => setSelectedId(item.id),
34262
- className: `w-full rounded border p-3 text-start ${item.id === selectedId ? "border-kumo-brand bg-kumo-brand/10" : "border-kumo-line"}`,
34263
- children: [/* @__PURE__ */ jsx("p", {
34264
- className: "font-medium",
34265
- children: item.displayName
34266
- }), /* @__PURE__ */ jsxs("p", {
34267
- className: "text-xs text-kumo-subtle",
34268
- children: [item.slug, item.isGuest ? _t({
34269
- id: "89z9Fh",
34270
- message: " - Guest"
34271
- }) : item.userId ? _t({
34272
- id: "X4uMa1",
34273
- message: " - Linked"
34274
- }) : ""]
34275
- })]
34276
- }, item.id);
34277
- }),
34278
- items.length === 0 && /* @__PURE__ */ jsx("p", {
34279
- className: "text-sm text-kumo-subtle",
34280
- children: _t({
34281
- id: "Qc/a/A",
34282
- message: "No bylines found"
34283
- })
34284
- }),
34285
- nextCursor && /* @__PURE__ */ jsx(Button, {
34286
- variant: "secondary",
34287
- className: "w-full mt-2",
34288
- onClick: () => loadMoreMutation.mutate(),
34289
- disabled: loadMoreMutation.isPending,
34290
- children: loadMoreMutation.isPending ? _t({
34291
- id: "Z3FXyt",
34292
- message: "Loading..."
35239
+ /* @__PURE__ */ jsx("h2", {
35240
+ className: "text-lg font-semibold",
35241
+ children: selected ? _t({
35242
+ id: "N6j2JH",
35243
+ message: "Edit {0}",
35244
+ values: { 0: selected.displayName }
34293
35245
  }) : _t({
34294
- id: "yQ2kGp",
34295
- message: "Load more"
35246
+ id: "d6HWlw",
35247
+ message: "Create byline"
34296
35248
  })
34297
- })
34298
- ]
34299
- })]
34300
- }),
34301
- /* @__PURE__ */ jsxs("div", {
34302
- className: "rounded-lg border p-6",
34303
- children: [/* @__PURE__ */ jsx("h2", {
34304
- className: "text-lg font-semibold mb-4",
34305
- children: selected ? _t({
34306
- id: "N6j2JH",
34307
- message: "Edit {0}",
34308
- values: { 0: selected.displayName }
34309
- }) : _t({
34310
- id: "d6HWlw",
34311
- message: "Create byline"
34312
- })
34313
- }), /* @__PURE__ */ jsxs("div", {
34314
- className: "space-y-4",
34315
- children: [
34316
- /* @__PURE__ */ jsx(Input, {
34317
- label: _t({
34318
- id: "pfa8F0",
34319
- message: "Display name"
34320
- }),
34321
- value: form.displayName,
34322
- onChange: (e) => setForm((prev) => ({
34323
- ...prev,
34324
- displayName: e.target.value
34325
- }))
34326
- }),
34327
- /* @__PURE__ */ jsx(Input, {
34328
- label: _t({
34329
- id: "L85WcV",
34330
- message: "Slug"
34331
- }),
34332
- value: form.slug,
34333
- onChange: (e) => setForm((prev) => ({
34334
- ...prev,
34335
- slug: e.target.value
34336
- }))
34337
- }),
34338
- /* @__PURE__ */ jsx(Input, {
34339
- label: _t({
34340
- id: "XYcw7G",
34341
- message: "Website URL"
34342
- }),
34343
- value: form.websiteUrl,
34344
- onChange: (e) => setForm((prev) => ({
34345
- ...prev,
34346
- websiteUrl: e.target.value
34347
- }))
34348
- }),
34349
- /* @__PURE__ */ jsx(InputArea, {
34350
- label: _t({
34351
- id: "E+oKCO",
34352
- message: "Bio"
34353
- }),
34354
- value: form.bio,
34355
- onChange: (e) => setForm((prev) => ({
34356
- ...prev,
34357
- bio: e.target.value
34358
- })),
34359
- rows: 5
34360
35249
  }),
34361
- /* @__PURE__ */ jsx(Select, {
34362
- label: _t({
34363
- id: "nti0N3",
34364
- message: "Linked user"
34365
- }),
34366
- value: form.userId ?? "",
34367
- onValueChange: (v) => {
34368
- const val = v || null;
34369
- setForm((prev) => ({
34370
- ...prev,
34371
- userId: val,
34372
- isGuest: val ? false : prev.isGuest
34373
- }));
34374
- },
34375
- items: {
34376
- "": _t({
34377
- id: "n9o2+L",
34378
- message: "No linked user"
35250
+ /* @__PURE__ */ jsxs("div", {
35251
+ className: "space-y-4",
35252
+ children: [
35253
+ /* @__PURE__ */ jsx(Input, {
35254
+ label: _t({
35255
+ id: "pfa8F0",
35256
+ message: "Display name"
35257
+ }),
35258
+ value: form.displayName,
35259
+ onChange: (e) => setForm((prev) => ({
35260
+ ...prev,
35261
+ displayName: e.target.value
35262
+ }))
34379
35263
  }),
34380
- ...Object.fromEntries(users.map((u) => [u.id, getUserLabel(u)]))
34381
- },
34382
- className: "w-full"
34383
- }),
34384
- /* @__PURE__ */ jsx(Switch, {
34385
- label: _t({
34386
- id: "6CRaxp",
34387
- message: "Guest byline"
34388
- }),
34389
- checked: form.isGuest,
34390
- onCheckedChange: (checked) => setForm((prev) => ({
34391
- ...prev,
34392
- isGuest: checked,
34393
- userId: checked ? null : prev.userId
34394
- }))
35264
+ /* @__PURE__ */ jsx(Input, {
35265
+ label: _t({
35266
+ id: "L85WcV",
35267
+ message: "Slug"
35268
+ }),
35269
+ value: form.slug,
35270
+ onChange: (e) => setForm((prev) => ({
35271
+ ...prev,
35272
+ slug: e.target.value
35273
+ }))
35274
+ }),
35275
+ /* @__PURE__ */ jsx(Input, {
35276
+ label: _t({
35277
+ id: "XYcw7G",
35278
+ message: "Website URL"
35279
+ }),
35280
+ value: form.websiteUrl,
35281
+ onChange: (e) => setForm((prev) => ({
35282
+ ...prev,
35283
+ websiteUrl: e.target.value
35284
+ }))
35285
+ }),
35286
+ /* @__PURE__ */ jsx(InputArea, {
35287
+ label: _t({
35288
+ id: "E+oKCO",
35289
+ message: "Bio"
35290
+ }),
35291
+ value: form.bio,
35292
+ onChange: (e) => setForm((prev) => ({
35293
+ ...prev,
35294
+ bio: e.target.value
35295
+ })),
35296
+ rows: 5
35297
+ }),
35298
+ /* @__PURE__ */ jsx(Select, {
35299
+ label: _t({
35300
+ id: "nti0N3",
35301
+ message: "Linked user"
35302
+ }),
35303
+ value: form.userId ?? "",
35304
+ onValueChange: (v) => {
35305
+ const val = v || null;
35306
+ setForm((prev) => ({
35307
+ ...prev,
35308
+ userId: val,
35309
+ isGuest: val ? false : prev.isGuest
35310
+ }));
35311
+ },
35312
+ items: {
35313
+ "": _t({
35314
+ id: "n9o2+L",
35315
+ message: "No linked user"
35316
+ }),
35317
+ ...Object.fromEntries(users.map((u) => [u.id, getUserLabel(u)]))
35318
+ },
35319
+ className: "w-full"
35320
+ }),
35321
+ /* @__PURE__ */ jsx(Switch, {
35322
+ label: _t({
35323
+ id: "6CRaxp",
35324
+ message: "Guest byline"
35325
+ }),
35326
+ checked: form.isGuest,
35327
+ onCheckedChange: (checked) => setForm((prev) => ({
35328
+ ...prev,
35329
+ isGuest: checked,
35330
+ userId: checked ? null : prev.userId
35331
+ }))
35332
+ }),
35333
+ /* @__PURE__ */ jsx(DialogError, { message: getMutationError(mutationError) }),
35334
+ /* @__PURE__ */ jsxs("div", {
35335
+ className: "flex gap-2 pt-2",
35336
+ children: [/* @__PURE__ */ jsx(Button, {
35337
+ onClick: () => {
35338
+ if (selected) updateMutation.mutate();
35339
+ else createMutation.mutate();
35340
+ },
35341
+ disabled: !form.displayName || !form.slug || isSaving,
35342
+ children: isSaving ? _t({
35343
+ id: "XvjC4F",
35344
+ message: "Saving..."
35345
+ }) : selected ? _t({
35346
+ id: "tfDRzk",
35347
+ message: "Save"
35348
+ }) : _t({
35349
+ id: "hYgDIe",
35350
+ message: "Create"
35351
+ })
35352
+ }), selected && /* @__PURE__ */ jsx(Button, {
35353
+ variant: "destructive",
35354
+ onClick: () => setShowDeleteConfirm(true),
35355
+ disabled: deleteMutation.isPending,
35356
+ children: _t({
35357
+ id: "cnGeoo",
35358
+ message: "Delete"
35359
+ })
35360
+ })]
35361
+ })
35362
+ ]
34395
35363
  }),
34396
- /* @__PURE__ */ jsx(DialogError, { message: getMutationError(mutationError) }),
34397
- /* @__PURE__ */ jsxs("div", {
34398
- className: "flex gap-2 pt-2",
34399
- children: [/* @__PURE__ */ jsx(Button, {
34400
- onClick: () => {
34401
- if (selected) updateMutation.mutate();
34402
- else createMutation.mutate();
35364
+ selected && isMultiLocale && i18n ? /* @__PURE__ */ jsx("div", {
35365
+ className: "border-t pt-6",
35366
+ children: /* @__PURE__ */ jsx(TranslationsPanel, {
35367
+ locales: i18n.locales,
35368
+ defaultLocale: i18n.defaultLocale,
35369
+ currentLocale: selected.locale,
35370
+ translations: translationsData?.items ?? [],
35371
+ onOpen: (summary) => {
35372
+ navigate({
35373
+ to: "/bylines",
35374
+ search: { locale: summary.locale }
35375
+ });
35376
+ setSelectedId(summary.id);
34403
35377
  },
34404
- disabled: !form.displayName || !form.slug || isSaving,
34405
- children: isSaving ? _t({
34406
- id: "XvjC4F",
34407
- message: "Saving..."
34408
- }) : selected ? _t({
34409
- id: "tfDRzk",
34410
- message: "Save"
34411
- }) : _t({
34412
- id: "hYgDIe",
34413
- message: "Create"
34414
- })
34415
- }), selected && /* @__PURE__ */ jsx(Button, {
34416
- variant: "destructive",
34417
- onClick: () => setShowDeleteConfirm(true),
34418
- disabled: deleteMutation.isPending,
34419
- children: _t({
34420
- id: "cnGeoo",
34421
- message: "Delete"
34422
- })
34423
- })]
34424
- })
35378
+ onCreate: (locale) => translateMutation.mutate(locale),
35379
+ pendingLocale: pendingTranslationLocale
35380
+ })
35381
+ }) : null
34425
35382
  ]
34426
35383
  })]
34427
35384
  }),
@@ -35989,10 +36946,11 @@ function ContentNewPage() {
35989
36946
  queryKey: ["manifest"],
35990
36947
  queryFn: fetchManifest
35991
36948
  });
36949
+ const pickerLocale = locale ?? manifest?.i18n?.defaultLocale;
35992
36950
  const createMutation = useMutation({
35993
36951
  mutationFn: (data) => createContent(collection, {
35994
36952
  ...data,
35995
- locale
36953
+ locale: pickerLocale
35996
36954
  }),
35997
36955
  onSuccess: (result) => {
35998
36956
  queryClient.invalidateQueries({ queryKey: ["content", collection] });
@@ -36006,14 +36964,23 @@ function ContentNewPage() {
36006
36964
  }
36007
36965
  });
36008
36966
  const pluginBlocks = React$1.useMemo(() => manifest ? getPluginBlocks(manifest) : [], [manifest]);
36009
- const { data: bylinesData } = useQuery({
36010
- queryKey: ["bylines"],
36011
- queryFn: () => fetchBylines({ limit: 100 })
36967
+ const { data: bylinesData, isSuccess: bylinesLoaded } = useQuery({
36968
+ queryKey: [
36969
+ "bylines",
36970
+ "picker",
36971
+ pickerLocale ?? null
36972
+ ],
36973
+ queryFn: () => fetchBylines({
36974
+ locale: pickerLocale,
36975
+ limit: 100
36976
+ }),
36977
+ enabled: !!manifest
36012
36978
  });
36013
36979
  const createBylineMutation = useMutation({
36014
36980
  mutationFn: (input) => createByline({
36015
36981
  ...input,
36016
- isGuest: true
36982
+ isGuest: true,
36983
+ locale: pickerLocale
36017
36984
  }),
36018
36985
  onSuccess: () => {
36019
36986
  queryClient.invalidateQueries({ queryKey: ["bylines"] });
@@ -36039,10 +37006,13 @@ function ContentNewPage() {
36039
37006
  collectionLabel: collectionConfig.labelSingular || collectionConfig.label,
36040
37007
  fields: collectionConfig.fields,
36041
37008
  isNew: true,
37009
+ entryLocale: pickerLocale,
37010
+ i18n: manifest?.i18n,
36042
37011
  isSaving: createMutation.isPending,
36043
37012
  onSave: handleSave,
36044
37013
  pluginBlocks,
36045
37014
  availableBylines: bylinesData?.items,
37015
+ availableBylinesLoaded: bylinesLoaded,
36046
37016
  selectedBylines,
36047
37017
  onBylinesChange: setSelectedBylines,
36048
37018
  onQuickCreateByline: async (input) => {
@@ -36152,14 +37122,24 @@ function ContentEditPage() {
36152
37122
  enabled: !!currentUser && currentUser.role >= ROLE_EDITOR,
36153
37123
  staleTime: 300 * 1e3
36154
37124
  });
36155
- const { data: bylinesData } = useQuery({
36156
- queryKey: ["bylines"],
36157
- queryFn: () => fetchBylines({ limit: 100 })
37125
+ const itemLocale = rawItem?.locale ?? void 0;
37126
+ const { data: bylinesData, isSuccess: bylinesLoaded } = useQuery({
37127
+ queryKey: [
37128
+ "bylines",
37129
+ "picker",
37130
+ itemLocale ?? null
37131
+ ],
37132
+ queryFn: () => fetchBylines({
37133
+ locale: itemLocale,
37134
+ limit: 100
37135
+ }),
37136
+ enabled: !!itemLocale
36158
37137
  });
36159
37138
  const createBylineMutation = useMutation({
36160
37139
  mutationFn: (input) => createByline({
36161
37140
  ...input,
36162
- isGuest: true
37141
+ isGuest: true,
37142
+ locale: itemLocale
36163
37143
  }),
36164
37144
  onSuccess: () => {
36165
37145
  queryClient.invalidateQueries({ queryKey: ["bylines"] });
@@ -36542,6 +37522,7 @@ function ContentEditPage() {
36542
37522
  hasSeo: collectionConfig.hasSeo,
36543
37523
  onSeoChange: handleSeoChange,
36544
37524
  availableBylines: bylinesData?.items,
37525
+ availableBylinesLoaded: bylinesLoaded,
36545
37526
  onQuickCreateByline: async (input) => {
36546
37527
  return await createBylineMutation.mutateAsync(input);
36547
37528
  },
@@ -36834,7 +37815,7 @@ function MarketplaceBrowsePage() {
36834
37815
  const { data: plugins } = useQuery({
36835
37816
  queryKey: ["plugins"],
36836
37817
  queryFn: async () => {
36837
- const { fetchPlugins } = await import("./plugins-qrjvPK4g.js").then((n) => n.a);
37818
+ const { fetchPlugins } = await import("./plugins-BZzztFdK.js").then((n) => n.a);
36838
37819
  return fetchPlugins();
36839
37820
  }
36840
37821
  });
@@ -36865,7 +37846,7 @@ function MarketplaceDetailPage() {
36865
37846
  const { data: plugins } = useQuery({
36866
37847
  queryKey: ["plugins"],
36867
37848
  queryFn: async () => {
36868
- const { fetchPlugins } = await import("./plugins-qrjvPK4g.js").then((n) => n.a);
37849
+ const { fetchPlugins } = await import("./plugins-BZzztFdK.js").then((n) => n.a);
36869
37850
  return fetchPlugins();
36870
37851
  }
36871
37852
  });
@@ -36949,10 +37930,15 @@ const usersRoute = createRoute({
36949
37930
  path: "/users",
36950
37931
  component: UsersPage
36951
37932
  });
37933
+ function parseBylinesLocaleSearch(search) {
37934
+ if (typeof search.locale === "string" && search.locale.length > 0) return { locale: search.locale };
37935
+ return { locale: void 0 };
37936
+ }
36952
37937
  const bylinesRoute = createRoute({
36953
37938
  getParentRoute: () => adminLayoutRoute,
36954
37939
  path: "/bylines",
36955
- component: BylinesPage
37940
+ component: BylinesPage,
37941
+ validateSearch: parseBylinesLocaleSearch
36956
37942
  });
36957
37943
  const contentTypesListRoute = createRoute({
36958
37944
  getParentRoute: () => adminLayoutRoute,
@@ -37337,5 +38323,5 @@ function AdminApp({ pluginAdmins = EMPTY_PLUGINS, authProviders = EMPTY_AUTH_PRO
37337
38323
  }
37338
38324
 
37339
38325
  //#endregion
37340
- export { API_BASE, API_TOKEN_SCOPES, AdminApp, AdminApp as App, AuthProviderProvider, CAPABILITY_LABELS, ContentEditor, ContentList, DEFAULT_LOCALE, Dashboard, EditorHeader, Header, Link, LoginPage, MediaLibrary, MediaPickerModal, PasskeyLogin, PasskeyRegistration, PluginAdminProvider, PortableTextEditor, SUPPORTED_LOCALES, SUPPORTED_LOCALE_CODES, SaveButton, Settings, SetupWizard, Shell, KumoSidebar as Sidebar, SidebarNav, analyzeWpPluginSite, analyzeWxr, apiFetch, bulkCommentAction, checkPluginUpdates, cn, compareRevisions, completeSignup, createAdminRouter, createAllowedDomain, createApiToken, createByline, createCollection, createContent, createField, createMenu, createMenuItem, createMenuTranslation, createRedirect, createSection, createTaxonomy, createTerm, createTermTranslation, createWidget, createWidgetArea, deleteAllowedDomain, deleteByline, deleteCollection, deleteComment, deleteContent, deleteField, deleteFromProvider, deleteMedia, deleteMenu, deleteMenuItem, deletePasskey, deleteRedirect, deleteSection, deleteTerm, deleteWidget, deleteWidgetArea, describeCapability, disablePlugin, disableUser, discardDraft, duplicateContent, enablePlugin, enableUser, executeWpPluginImport, executeWxrImport, fetch404Summary, fetchAllowedDomains, fetchApiTokens, fetchAuthMode, fetchByline, fetchBylines, fetchCollection, fetchCollections, fetchComment, fetchCommentCounts, fetchComments, fetchContent, fetchContentList, fetchDashboardStats, fetchEmailSettings, fetchFields, fetchManifest, fetchMarketplacePlugin, fetchMediaList, fetchMediaProviders, fetchMenu, fetchMenuTranslations, fetchMenus, fetchOrphanedTables, fetchPasskeys, fetchPlugin, fetchPlugins, fetchProviderMedia, fetchRedirects, fetchRevision, fetchRevisions, fetchSection, fetchSections, fetchSettings, fetchTaxonomyDef, fetchTaxonomyDefs, fetchTermTranslations, fetchTerms, fetchTheme, fetchTranslations, fetchTrashedContent, fetchUser, fetchUsers, fetchWidgetArea, fetchWidgetAreas, fetchWidgetComponents, generatePreviewUrl, getDraftStatus, getPreviewUrl, hasAllowedDomains, importWxrMedia, installMarketplacePlugin, inviteUser, parseApiResponse, permanentDeleteContent, prepareWxrImport, probeImportUrl, publishContent, registerOrphanedTable, renamePasskey, reorderFields, reorderMenuItems, reorderWidgets, requestSignup, resolveLocale, restoreContent, restoreRevision, revokeApiToken, rewriteContentUrls, scheduleContent, searchMarketplace, searchThemes, sendRecoveryLink, sendTestEmail, setSearchEnabled, throwResponseError, uninstallMarketplacePlugin, unpublishContent, unscheduleContent, updateAllowedDomain, updateByline, updateCollection, updateCommentStatus, updateContent, updateField, updateMarketplacePlugin, updateMedia, updateMenu, updateMenuItem, updateRedirect, updateSection, updateSettings, updateTerm, updateUser, updateWidget, uploadMedia, uploadToProvider, useAuthProviderList, useAuthProviders, useCurrentUser, useLocale, useNavigate, useParams, usePluginAdmins, usePluginField, usePluginHasPages, usePluginHasWidgets, usePluginPage, usePluginWidget, validateInviteToken, verifySignupToken };
38326
+ export { API_BASE, API_TOKEN_SCOPES, AdminApp, AdminApp as App, AuthProviderProvider, CAPABILITY_LABELS, ContentEditor, ContentList, DEFAULT_LOCALE, Dashboard, EditorHeader, Header, Link, LoginPage, MediaLibrary, MediaPickerModal, PasskeyLogin, PasskeyRegistration, PluginAdminProvider, PortableTextEditor, SUPPORTED_LOCALES, SUPPORTED_LOCALE_CODES, SaveButton, Settings, SetupWizard, Shell, KumoSidebar as Sidebar, SidebarNav, analyzeWpPluginSite, analyzeWxr, apiFetch, bulkCommentAction, checkPluginUpdates, cn, compareRevisions, completeSignup, createAdminRouter, createAllowedDomain, createApiToken, createByline, createBylineTranslation, createCollection, createContent, createField, createMenu, createMenuItem, createMenuTranslation, createRedirect, createSection, createTaxonomy, createTerm, createTermTranslation, createWidget, createWidgetArea, deleteAllowedDomain, deleteByline, deleteCollection, deleteComment, deleteContent, deleteField, deleteFromProvider, deleteMedia, deleteMenu, deleteMenuItem, deletePasskey, deleteRedirect, deleteSection, deleteTerm, deleteWidget, deleteWidgetArea, describeCapability, disablePlugin, disableUser, discardDraft, duplicateContent, enablePlugin, enableUser, executeWpPluginImport, executeWxrImport, fetch404Summary, fetchAllowedDomains, fetchApiTokens, fetchAuthMode, fetchByline, fetchBylineTranslations, fetchBylines, fetchCollection, fetchCollections, fetchComment, fetchCommentCounts, fetchComments, fetchContent, fetchContentList, fetchDashboardStats, fetchEmailSettings, fetchFields, fetchManifest, fetchMarketplacePlugin, fetchMediaList, fetchMediaProviders, fetchMenu, fetchMenuTranslations, fetchMenus, fetchOrphanedTables, fetchPasskeys, fetchPlugin, fetchPlugins, fetchProviderMedia, fetchRedirects, fetchRevision, fetchRevisions, fetchSection, fetchSections, fetchSettings, fetchTaxonomyDef, fetchTaxonomyDefs, fetchTermTranslations, fetchTerms, fetchTheme, fetchTranslations, fetchTrashedContent, fetchUser, fetchUsers, fetchWidgetArea, fetchWidgetAreas, fetchWidgetComponents, generatePreviewUrl, getDraftStatus, getPreviewUrl, hasAllowedDomains, importWxrMedia, installMarketplacePlugin, inviteUser, parseApiResponse, permanentDeleteContent, prepareWxrImport, probeImportUrl, publishContent, registerOrphanedTable, renamePasskey, reorderFields, reorderMenuItems, reorderWidgets, requestSignup, resolveLocale, restoreContent, restoreRevision, revokeApiToken, rewriteContentUrls, scheduleContent, searchMarketplace, searchThemes, sendRecoveryLink, sendTestEmail, setSearchEnabled, throwResponseError, uninstallMarketplacePlugin, unpublishContent, unscheduleContent, updateAllowedDomain, updateByline, updateCollection, updateCommentStatus, updateContent, updateField, updateMarketplacePlugin, updateMedia, updateMenu, updateMenuItem, updateRedirect, updateSection, updateSettings, updateTerm, updateUser, updateWidget, uploadMedia, uploadToProvider, useAuthProviderList, useAuthProviders, useCurrentUser, useLocale, useNavigate, useParams, usePluginAdmins, usePluginField, usePluginHasPages, usePluginHasWidgets, usePluginPage, usePluginWidget, validateInviteToken, verifySignupToken };
37341
38327
  //# sourceMappingURL=index.js.map