@hachej/boring-core 0.1.5 → 0.1.6

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.
@@ -3,7 +3,7 @@ import {
3
3
  UserMenu,
4
4
  WorkspaceSwitcher,
5
5
  useCurrentWorkspace
6
- } from "../../chunk-VTOS4C7B.js";
6
+ } from "../../chunk-A5TMALZR.js";
7
7
  import "../../chunk-HYNKZSTF.js";
8
8
  import "../../chunk-H5KU6R6Y.js";
9
9
  import "../../chunk-MLKGABMK.js";
@@ -9,7 +9,7 @@ import {
9
9
  registerRoutes,
10
10
  registerSettingsRoutes,
11
11
  registerWorkspaceRoutes
12
- } from "../../chunk-CZ4HIXII.js";
12
+ } from "../../chunk-RIEZ2IPH.js";
13
13
  import {
14
14
  PostgresUserStore,
15
15
  PostgresWorkspaceStore,
@@ -328,11 +328,23 @@ async function createCoreWorkspaceAgentServer(options = {}) {
328
328
  plugins: options.plugins,
329
329
  excludeDefaults: options.excludeDefaults
330
330
  });
331
- await provisionWorkspaceAgentServer({
332
- workspaceRoot,
333
- provisioningContributions: pluginCollection.provisioningContributions,
334
- force: options.forceProvisioning
335
- });
331
+ const provisionedWorkspaceRoots = /* @__PURE__ */ new Map();
332
+ const ensureWorkspaceProvisioned = (root) => {
333
+ if (pluginCollection.provisioningContributions.length === 0) return Promise.resolve();
334
+ const existing = provisionedWorkspaceRoots.get(root);
335
+ if (existing) return existing;
336
+ const pending = provisionWorkspaceAgentServer({
337
+ workspaceRoot: root,
338
+ provisioningContributions: pluginCollection.provisioningContributions,
339
+ force: options.forceProvisioning
340
+ }).catch((error) => {
341
+ provisionedWorkspaceRoots.delete(root);
342
+ throw error;
343
+ });
344
+ provisionedWorkspaceRoots.set(root, pending);
345
+ return pending;
346
+ };
347
+ await ensureWorkspaceProvisioned(workspaceRoot);
336
348
  const bridges = /* @__PURE__ */ new Map();
337
349
  const getUiBridge = (workspaceId) => {
338
350
  let bridge = bridges.get(workspaceId);
@@ -343,11 +355,16 @@ async function createCoreWorkspaceAgentServer(options = {}) {
343
355
  return bridge;
344
356
  };
345
357
  const resolveWorkspaceId = async (request) => options.getWorkspaceId ? await options.getWorkspaceId(request) : await resolveAuthorizedWorkspaceId(request, workspaceStore);
346
- const resolveRoot = async (workspaceId, request) => options.getWorkspaceRoot ? await options.getWorkspaceRoot(workspaceId, request) : await resolveWorkspaceRoot(workspaceRoot, workspaceId);
358
+ const resolveRoot = async (workspaceId, request) => {
359
+ const root = options.getWorkspaceRoot ? await options.getWorkspaceRoot(workspaceId, request) : await resolveWorkspaceRoot(workspaceRoot, workspaceId);
360
+ await ensureWorkspaceProvisioned(root);
361
+ return root;
362
+ };
347
363
  await app.register(registerAgentRoutes, {
348
364
  workspaceRoot,
349
365
  sessionId: options.sessionId,
350
366
  templatePath: options.templatePath,
367
+ getTemplatePath: options.getTemplatePath,
351
368
  mode: options.mode,
352
369
  version: options.version,
353
370
  extraTools: [
@@ -1809,11 +1809,11 @@ function AuthGate({
1809
1809
  clearTimeout(redirectTimerRef.current);
1810
1810
  redirectTimerRef.current = null;
1811
1811
  }
1812
- const pathname = normalizePath(currentLocation.pathname);
1812
+ const pathname2 = normalizePath(currentLocation.pathname);
1813
1813
  const currentPath = buildCurrentPath(currentLocation);
1814
1814
  if (session.data) {
1815
1815
  nullSinceRef.current = null;
1816
- if (pathname === routes.signin) {
1816
+ if (pathname2 === routes.signin) {
1817
1817
  const destination = readSafeRedirect(currentLocation.search) ?? "/";
1818
1818
  if (destination !== currentPath) {
1819
1819
  goTo(destination, { replace: true });
@@ -1821,7 +1821,11 @@ function AuthGate({
1821
1821
  }
1822
1822
  return;
1823
1823
  }
1824
- if (session.isPending || isPublicPath(pathname, normalizedPublicPaths)) {
1824
+ if (isPublicPath(pathname2, normalizedPublicPaths)) {
1825
+ nullSinceRef.current = null;
1826
+ return;
1827
+ }
1828
+ if (session.isPending) {
1825
1829
  nullSinceRef.current = null;
1826
1830
  return;
1827
1831
  }
@@ -1840,6 +1844,8 @@ function AuthGate({
1840
1844
  }, remainingMs);
1841
1845
  redirectTimerRef.current.unref?.();
1842
1846
  }, [currentLocation, goTo, graceMs, normalizedPublicPaths, readNow, session.data, session.isPending]);
1847
+ const pathname = normalizePath(currentLocation.pathname);
1848
+ if (session.isPending && !isPublicPath(pathname, normalizedPublicPaths)) return null;
1843
1849
  return /* @__PURE__ */ jsx14(Fragment, { children });
1844
1850
  }
1845
1851
 
@@ -2752,7 +2758,7 @@ function MembersPage() {
2752
2758
  }
2753
2759
 
2754
2760
  // src/front/workspace/WorkspaceSettingsPage.tsx
2755
- import { useCallback as useCallback8, useState as useState18 } from "react";
2761
+ import { useCallback as useCallback8, useEffect as useEffect10, useState as useState18 } from "react";
2756
2762
  import { useQuery as useQuery6, useMutation as useMutation4, useQueryClient as useQueryClient6 } from "@tanstack/react-query";
2757
2763
  import { useNavigate as useNavigate4 } from "react-router-dom";
2758
2764
  import {
@@ -2775,6 +2781,7 @@ import {
2775
2781
  } from "@hachej/boring-ui-kit";
2776
2782
  import {
2777
2783
  AlertCircle,
2784
+ FileImage,
2778
2785
  HardDrive,
2779
2786
  RefreshCw,
2780
2787
  Settings2 as Settings22,
@@ -2852,6 +2859,7 @@ function roleLabel(role) {
2852
2859
  var WORKSPACE_NAV_ITEMS = [
2853
2860
  { href: "#general", label: "General", description: "Name and access" },
2854
2861
  { href: "#runtime", label: "Runtime", description: "Provisioning state" },
2862
+ { href: "#files", label: "Files", description: "Markdown assets" },
2855
2863
  { href: "#danger-zone", label: "Danger zone", description: "Permanent actions" }
2856
2864
  ];
2857
2865
  function WorkspaceSettingsPage({ topBar } = {}) {
@@ -2866,8 +2874,11 @@ function WorkspaceSettingsPage({ topBar } = {}) {
2866
2874
  const [deleteConfirmName, setDeleteConfirmName] = useState18("");
2867
2875
  const [deleteDialogOpen, setDeleteDialogOpen] = useState18(false);
2868
2876
  const [deleteError, setDeleteError] = useState18(null);
2877
+ const [imageUploadDirValue, setImageUploadDirValue] = useState18(null);
2878
+ const [fileSettingsError, setFileSettingsError] = useState18(null);
2869
2879
  const displayName = nameValue ?? workspace?.name ?? "";
2870
2880
  const encodedWorkspaceId = encodeURIComponent(workspaceId);
2881
+ const requestHeaders = workspaceId ? { "x-boring-workspace-id": workspaceId } : void 0;
2871
2882
  const runtimeQuery = useQuery6({
2872
2883
  queryKey: ["runtime", workspaceId],
2873
2884
  queryFn: async () => {
@@ -2884,6 +2895,50 @@ function WorkspaceSettingsPage({ topBar } = {}) {
2884
2895
  },
2885
2896
  enabled: workspaceId.length > 0
2886
2897
  });
2898
+ const fileSettingsQuery = useQuery6({
2899
+ queryKey: ["workspace-file-settings", workspaceId],
2900
+ queryFn: async () => {
2901
+ try {
2902
+ const data = await apiFetchJson(
2903
+ "/api/v1/workspace-settings",
2904
+ { headers: requestHeaders }
2905
+ );
2906
+ return data.settings;
2907
+ } catch (err) {
2908
+ const detail = getHttpErrorDetail(err);
2909
+ if (detail.status === 404) return null;
2910
+ throw err;
2911
+ }
2912
+ },
2913
+ enabled: workspaceId.length > 0,
2914
+ retry: false
2915
+ });
2916
+ useEffect10(() => {
2917
+ const next = fileSettingsQuery.data?.markdown?.imageUploadDir;
2918
+ if (typeof next === "string") setImageUploadDirValue(next);
2919
+ }, [fileSettingsQuery.data?.markdown?.imageUploadDir]);
2920
+ const fileSettingsMutation = useMutation4({
2921
+ mutationFn: async (imageUploadDir) => {
2922
+ const data = await apiFetchJson(
2923
+ "/api/v1/workspace-settings",
2924
+ {
2925
+ method: "PUT",
2926
+ headers: { ...requestHeaders, "Content-Type": "application/json" },
2927
+ body: JSON.stringify({ settings: { markdown: { imageUploadDir } } })
2928
+ }
2929
+ );
2930
+ return data.settings;
2931
+ },
2932
+ onSuccess: (settings) => {
2933
+ setFileSettingsError(null);
2934
+ setImageUploadDirValue(settings.markdown?.imageUploadDir ?? "assets/images");
2935
+ queryClient.invalidateQueries({ queryKey: ["workspace-file-settings", workspaceId] });
2936
+ },
2937
+ onError: (err) => {
2938
+ const detail = getHttpErrorDetail(err);
2939
+ setFileSettingsError(detail.message);
2940
+ }
2941
+ });
2887
2942
  const renameMutation = useMutation4({
2888
2943
  mutationFn: async (name) => {
2889
2944
  await apiFetch(`/api/v1/workspaces/${encodedWorkspaceId}`, {
@@ -2950,15 +3005,28 @@ function WorkspaceSettingsPage({ topBar } = {}) {
2950
3005
  setDeleteError(null);
2951
3006
  deleteMutation.mutate();
2952
3007
  }, [deleteMutation]);
3008
+ const handleSaveFileSettings = useCallback8(() => {
3009
+ const trimmed = (imageUploadDirValue ?? "").trim();
3010
+ if (!trimmed) return;
3011
+ fileSettingsMutation.mutate(trimmed);
3012
+ }, [fileSettingsMutation, imageUploadDirValue]);
2953
3013
  const runtime = runtimeQuery.data ?? null;
2954
3014
  const hasRuntime = runtime !== null && runtimeQuery.isSuccess;
3015
+ const fileSettings = fileSettingsQuery.data ?? null;
3016
+ const hasFileSettings = fileSettings !== null && fileSettingsQuery.isSuccess;
3017
+ const currentImageUploadDir = fileSettings?.markdown?.imageUploadDir ?? "assets/images";
3018
+ const fileSettingsChanged = imageUploadDirValue !== null && imageUploadDirValue.trim() !== currentImageUploadDir;
2955
3019
  const nameChanged = nameValue !== null && nameValue.trim() !== workspace?.name;
2956
3020
  const canEditName = role !== "viewer";
2957
3021
  const canDeleteWorkspace = role === "owner" || role === null;
2958
3022
  const workspaceName = workspace?.name ?? "Workspace";
2959
3023
  const workspaceInitial2 = (workspace?.name?.trim()?.[0] ?? "W").toUpperCase();
2960
3024
  const topBarNode = topBar === void 0 ? /* @__PURE__ */ jsx19(SettingsTopBar2, { workspaceId, workspaceName }) : topBar;
2961
- const navItems = hasRuntime ? WORKSPACE_NAV_ITEMS : WORKSPACE_NAV_ITEMS.filter((item) => item.href !== "#runtime");
3025
+ const navItems = WORKSPACE_NAV_ITEMS.filter((item) => {
3026
+ if (item.href === "#runtime") return hasRuntime;
3027
+ if (item.href === "#files") return hasFileSettings;
3028
+ return true;
3029
+ });
2962
3030
  return /* @__PURE__ */ jsxs13("main", { className: "boring-settings-shell", children: [
2963
3031
  topBarNode,
2964
3032
  /* @__PURE__ */ jsx19("div", { className: "boring-settings-scroll", children: /* @__PURE__ */ jsxs13("div", { className: "boring-settings-layout", children: [
@@ -3091,6 +3159,48 @@ function WorkspaceSettingsPage({ topBar } = {}) {
3091
3159
  ] })
3092
3160
  }
3093
3161
  ),
3162
+ hasFileSettings && /* @__PURE__ */ jsx19(
3163
+ UiSettingsPanel2,
3164
+ {
3165
+ id: "files",
3166
+ testId: "file-settings-card",
3167
+ icon: /* @__PURE__ */ jsx19(FileImage, { className: "h-3.5 w-3.5", "aria-hidden": "true" }),
3168
+ title: "Files",
3169
+ description: "Configure where markdown editor image uploads are stored. Direct/local workspaces can also edit .boring/settings.",
3170
+ footer: /* @__PURE__ */ jsx19(
3171
+ Button14,
3172
+ {
3173
+ "data-testid": "save-file-settings",
3174
+ size: "sm",
3175
+ disabled: !fileSettingsChanged || fileSettingsMutation.isPending || !canEditName,
3176
+ onClick: handleSaveFileSettings,
3177
+ children: fileSettingsMutation.isPending ? "Saving..." : "Save file settings"
3178
+ }
3179
+ ),
3180
+ children: /* @__PURE__ */ jsxs13("div", { className: "space-y-4", children: [
3181
+ fileSettingsError && /* @__PURE__ */ jsx19(Notice5, { "data-testid": "file-settings-error", role: "alert", tone: "error", description: fileSettingsError }),
3182
+ /* @__PURE__ */ jsxs13("div", { className: "space-y-2", children: [
3183
+ /* @__PURE__ */ jsx19(Label9, { htmlFor: "markdown-image-upload-dir", className: "text-[12px]", children: "Markdown image upload path" }),
3184
+ /* @__PURE__ */ jsx19(
3185
+ Input9,
3186
+ {
3187
+ id: "markdown-image-upload-dir",
3188
+ "data-testid": "markdown-image-upload-dir-input",
3189
+ className: "h-8 font-mono text-[13px]",
3190
+ value: imageUploadDirValue ?? currentImageUploadDir,
3191
+ onChange: (e) => setImageUploadDirValue(e.target.value),
3192
+ disabled: !canEditName
3193
+ }
3194
+ ),
3195
+ /* @__PURE__ */ jsxs13(FieldNote, { children: [
3196
+ "Relative workspace path used by markdown image uploads. Stored in ",
3197
+ /* @__PURE__ */ jsx19("code", { children: ".boring/settings" }),
3198
+ "."
3199
+ ] })
3200
+ ] })
3201
+ ] })
3202
+ }
3203
+ ),
3094
3204
  /* @__PURE__ */ jsx19(
3095
3205
  UiSettingsPanel2,
3096
3206
  {
@@ -672,11 +672,16 @@ async function createCoreApp(config, options) {
672
672
  ],
673
673
  styleSrc: [
674
674
  "'self'",
675
+ "https://fonts.googleapis.com",
675
676
  (request) => `'nonce-${request.cspNonce ?? ""}'`
676
677
  ],
678
+ // React/DockView use style attributes for runtime layout sizing.
679
+ // Keep stylesheet loading nonce/domain-bound, but allow attributes
680
+ // so production CSP does not collapse workspace/workbench panes.
681
+ styleSrcAttr: ["'unsafe-inline'"],
677
682
  imgSrc: ["'self'", "data:", "blob:"],
678
683
  connectSrc: ["'self'"],
679
- fontSrc: ["'self'"],
684
+ fontSrc: ["'self'", "https://fonts.gstatic.com", "data:"],
680
685
  objectSrc: ["'none'"],
681
686
  frameAncestors: ["'none'"],
682
687
  baseUri: ["'self'"],
@@ -410,7 +410,7 @@ interface AuthGateProps {
410
410
  }) => void;
411
411
  now?: () => number;
412
412
  }
413
- declare function AuthGate({ children, publicPaths, graceMs, location, navigate, now, }: AuthGateProps): react_jsx_runtime.JSX.Element;
413
+ declare function AuthGate({ children, publicPaths, graceMs, location, navigate, now, }: AuthGateProps): react_jsx_runtime.JSX.Element | null;
414
414
 
415
415
  interface CoreCommand {
416
416
  id: string;
@@ -56,7 +56,7 @@ import {
56
56
  useViewportBreakpoint,
57
57
  useWorkspaceMembers,
58
58
  useWorkspaceRole
59
- } from "../chunk-VTOS4C7B.js";
59
+ } from "../chunk-A5TMALZR.js";
60
60
  import {
61
61
  TopBarSlotProvider,
62
62
  useTopBarSlot
@@ -24,7 +24,7 @@ import {
24
24
  requireWorkspaceMember,
25
25
  validateConfig,
26
26
  validatePasswordStrength
27
- } from "../chunk-CZ4HIXII.js";
27
+ } from "../chunk-RIEZ2IPH.js";
28
28
  import {
29
29
  createDatabase,
30
30
  runMigrations
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hachej/boring-core",
3
- "version": "0.1.5",
3
+ "version": "0.1.6",
4
4
  "type": "module",
5
5
  "license": "MIT",
6
6
  "description": "Foundation package for boring-ui-v2 apps: DB, auth, config, HTTP app factory, and frontend app shell.",
@@ -78,9 +78,9 @@
78
78
  "react-router-dom": "^7.14.2",
79
79
  "smol-toml": "^1.6.1",
80
80
  "zod": "^3.25.76",
81
- "@hachej/boring-ui-kit": "0.1.5",
82
- "@hachej/boring-agent": "0.1.5",
83
- "@hachej/boring-workspace": "0.1.5"
81
+ "@hachej/boring-agent": "0.1.6",
82
+ "@hachej/boring-workspace": "0.1.6",
83
+ "@hachej/boring-ui-kit": "0.1.6"
84
84
  },
85
85
  "devDependencies": {
86
86
  "@testing-library/jest-dom": "^6.9.1",