@hachej/boring-core 0.1.48 → 0.1.50

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,11 +1,15 @@
1
1
  import {
2
2
  TopBarSlotProvider
3
3
  } from "./chunk-HYNKZSTF.js";
4
+ import {
5
+ canUseProtectedApi,
6
+ isRuntimeEmailVerificationEnabled
7
+ } from "./chunk-I4PGL4ZD.js";
4
8
  import {
5
9
  ConfigFetchError,
6
10
  ERROR_CODES,
7
11
  HttpError
8
- } from "./chunk-LIBHVT7V.js";
12
+ } from "./chunk-QZGYKLXB.js";
9
13
 
10
14
  // src/front/AppErrorBoundary.tsx
11
15
  import { Component } from "react";
@@ -660,11 +664,16 @@ function WorkspaceAuthProvider({
660
664
  const queryClient = useQueryClient();
661
665
  const routeWorkspaceId = id?.trim() ? id : workspaceIdFromPath(location.pathname, workspaceRoute, workspaceIdParam);
662
666
  const session = useSession();
663
- const isAuthenticated = Boolean(session.data?.user);
667
+ const config = useOptionalConfig();
668
+ const user = session.data?.user ?? null;
669
+ const canQueryProtectedApi = canUseProtectedApi(
670
+ user,
671
+ isRuntimeEmailVerificationEnabled(config)
672
+ );
664
673
  const workspacesQuery = useQuery2({
665
674
  queryKey: WORKSPACES_QUERY_KEY,
666
675
  queryFn: fetchWorkspaces,
667
- enabled: isAuthenticated
676
+ enabled: canQueryProtectedApi
668
677
  });
669
678
  const defaultWorkspace = routeWorkspaceId === null ? workspacesQuery.data?.find((workspace2) => workspace2.isDefault) ?? workspacesQuery.data?.[0] ?? null : null;
670
679
  const resolvedId = routeWorkspaceId ?? defaultWorkspace?.id ?? null;
@@ -677,13 +686,13 @@ function WorkspaceAuthProvider({
677
686
  }
678
687
  return fetchWorkspace(resolvedId);
679
688
  },
680
- enabled: isAuthenticated && resolvedId !== null
689
+ enabled: canQueryProtectedApi && resolvedId !== null
681
690
  });
682
691
  const detail = detailQuery.data ?? cachedDetail ?? null;
683
692
  const workspace = detailQuery.isError ? null : detail?.workspace ?? null;
684
693
  const role = detailQuery.isError ? null : detail?.role ?? null;
685
694
  const routeStatus = (() => {
686
- if (!isAuthenticated) return { status: "idle", workspaceId: routeWorkspaceId };
695
+ if (!canQueryProtectedApi) return { status: "idle", workspaceId: routeWorkspaceId };
687
696
  if (routeWorkspaceId === null) return { status: "idle", workspaceId: null };
688
697
  if (detailQuery.isError) return routeStatusFromError(routeWorkspaceId, detailQuery.error);
689
698
  if (detailQuery.isPending && !detail) return { status: "loading", workspaceId: routeWorkspaceId };
@@ -709,11 +718,16 @@ var UserContext = createContext5(null);
709
718
  var STALE_MS = 6e4;
710
719
  function UserIdentityProvider({ children }) {
711
720
  const { data: session } = useSession();
721
+ const config = useOptionalConfig();
722
+ const canFetchIdentity = canUseProtectedApi(
723
+ session?.user,
724
+ isRuntimeEmailVerificationEnabled(config)
725
+ );
712
726
  const [identity, setIdentity] = useState6(null);
713
727
  const fetchedForRef = useRef3(null);
714
728
  const lastFetchRef = useRef3(0);
715
729
  useEffect7(() => {
716
- if (!session?.user) {
730
+ if (!session?.user || !canFetchIdentity) {
717
731
  setIdentity(null);
718
732
  fetchedForRef.current = null;
719
733
  return;
@@ -736,7 +750,7 @@ function UserIdentityProvider({ children }) {
736
750
  return () => {
737
751
  cancelled = true;
738
752
  };
739
- }, [session?.user?.id]);
753
+ }, [canFetchIdentity, session?.user?.id]);
740
754
  return /* @__PURE__ */ jsx6(UserContext.Provider, { value: identity, children });
741
755
  }
742
756
  function useUser() {
@@ -892,6 +906,7 @@ function SignInPage() {
892
906
  // src/front/auth/SignUpPage.tsx
893
907
  import { useState as useState9 } from "react";
894
908
  import { useForm as useForm2 } from "react-hook-form";
909
+ import { useNavigate } from "react-router-dom";
895
910
  import { zodResolver as zodResolver2 } from "@hookform/resolvers/zod";
896
911
  import { z as z2 } from "zod";
897
912
  import { Button as Button4, Card as Card2, CardContent as CardContent2, CardDescription as CardDescription2, CardFooter as CardFooter2, CardHeader as CardHeader2, CardTitle as CardTitle2, Input as Input2, Label as Label2 } from "@hachej/boring-ui-kit";
@@ -909,6 +924,7 @@ function readGoogleAuthError2() {
909
924
  }
910
925
  function SignUpPage() {
911
926
  const signUp = useSignUp();
927
+ const navigate = useNavigate();
912
928
  const config = useOptionalConfig();
913
929
  const [serverError, setServerError] = useState9(null);
914
930
  const [oauthError, setOauthError] = useState9(() => readGoogleAuthError2());
@@ -935,6 +951,8 @@ function SignUpPage() {
935
951
  );
936
952
  if (result.error) {
937
953
  setServerError(result.error.message ?? "Sign up failed");
954
+ } else if (isRuntimeEmailVerificationEnabled(config)) {
955
+ navigate(routes.verifyEmail, { replace: true });
938
956
  } else {
939
957
  setSuccess(true);
940
958
  }
@@ -1214,16 +1232,23 @@ function getCookie(name) {
1214
1232
  function deleteCookie(name) {
1215
1233
  document.cookie = `${name}=; path=/; expires=Thu, 01 Jan 1970 00:00:00 GMT`;
1216
1234
  }
1235
+ function navigateToSignIn() {
1236
+ if (typeof window === "undefined") return;
1237
+ window.history.replaceState({}, "", routes.signin);
1238
+ window.dispatchEvent(new PopStateEvent("popstate"));
1239
+ }
1217
1240
  function VerifyEmailPage() {
1218
1241
  const session = useSession();
1219
1242
  const verifyEmail = useVerifyEmail();
1220
1243
  const sendVerificationEmail = useSendVerificationEmail();
1244
+ const signOut = useSignOut();
1221
1245
  const token = typeof window !== "undefined" ? new URLSearchParams(window.location.search).get("token") : null;
1222
1246
  const [status, setStatus] = useState12(token ? "verifying" : "no-token");
1223
1247
  const [inviteWarning, setInviteWarning] = useState12(null);
1224
1248
  const [cooldown, setCooldown] = useState12(0);
1225
1249
  const [resendEmail, setResendEmail] = useState12("");
1226
1250
  const [resendSent, setResendSent] = useState12(false);
1251
+ const [isSigningOut, setIsSigningOut] = useState12(false);
1227
1252
  const verifiedRef = useRef4(false);
1228
1253
  const sessionEmail = session.data?.user?.email ?? null;
1229
1254
  useEffect8(() => {
@@ -1278,6 +1303,15 @@ function VerifyEmailPage() {
1278
1303
  setResendSent(true);
1279
1304
  setCooldown(60);
1280
1305
  }, [sessionEmail, resendEmail, cooldown, sendVerificationEmail]);
1306
+ const handleBackToSignIn = useCallback3(async () => {
1307
+ if (isSigningOut) return;
1308
+ setIsSigningOut(true);
1309
+ try {
1310
+ await signOut();
1311
+ } finally {
1312
+ navigateToSignIn();
1313
+ }
1314
+ }, [isSigningOut, signOut]);
1281
1315
  const resendButton = /* @__PURE__ */ jsx12(
1282
1316
  Button7,
1283
1317
  {
@@ -1329,17 +1363,38 @@ function VerifyEmailPage() {
1329
1363
  /* @__PURE__ */ jsx12(CardDescription5, { children: "This verification link is no longer valid. Request a new one below." })
1330
1364
  ] }),
1331
1365
  /* @__PURE__ */ jsx12(CardContent5, { children: resendSection }),
1332
- /* @__PURE__ */ jsx12(CardFooter5, { children: /* @__PURE__ */ jsx12("a", { href: routes.signin, className: "text-sm text-muted-foreground hover:underline", children: "Back to sign in" }) })
1366
+ /* @__PURE__ */ jsx12(CardFooter5, { children: /* @__PURE__ */ jsx12(
1367
+ Button7,
1368
+ {
1369
+ type: "button",
1370
+ variant: "link",
1371
+ className: "h-auto p-0 text-sm text-muted-foreground hover:underline",
1372
+ disabled: isSigningOut,
1373
+ onClick: handleBackToSignIn,
1374
+ children: isSigningOut ? "Signing out\u2026" : "Back to sign in"
1375
+ }
1376
+ ) })
1333
1377
  ] }) });
1334
1378
  }
1379
+ const isWaitingForEmailClick = status === "no-token" && Boolean(sessionEmail);
1335
1380
  return /* @__PURE__ */ jsx12("div", { className: "flex min-h-screen items-center justify-center p-4", children: /* @__PURE__ */ jsxs5(Card5, { className: "w-full max-w-sm", children: [
1336
1381
  inviteWarning && /* @__PURE__ */ jsx12("div", { role: "status", className: "px-6 pt-4", children: /* @__PURE__ */ jsx12("div", { className: "rounded-md bg-muted px-3 py-2 text-sm text-muted-foreground", children: inviteWarning }) }),
1337
1382
  /* @__PURE__ */ jsxs5(CardHeader5, { children: [
1338
- /* @__PURE__ */ jsx12(CardTitle5, { children: "Invalid verification link" }),
1339
- /* @__PURE__ */ jsx12(CardDescription5, { children: status === "no-token" ? "No verification token found. Check the link in your email." : "This verification link is invalid. Request a new one below." })
1383
+ /* @__PURE__ */ jsx12(CardTitle5, { children: isWaitingForEmailClick ? "Check your email" : "Invalid verification link" }),
1384
+ /* @__PURE__ */ jsx12(CardDescription5, { children: isWaitingForEmailClick ? "We sent a verification link to your email address. Please check your inbox to continue." : status === "no-token" ? "No verification token found. Check the link in your email." : "This verification link is invalid. Request a new one below." })
1340
1385
  ] }),
1341
1386
  /* @__PURE__ */ jsx12(CardContent5, { children: resendSection }),
1342
- /* @__PURE__ */ jsx12(CardFooter5, { children: /* @__PURE__ */ jsx12("a", { href: routes.signin, className: "text-sm text-muted-foreground hover:underline", children: "Back to sign in" }) })
1387
+ /* @__PURE__ */ jsx12(CardFooter5, { children: /* @__PURE__ */ jsx12(
1388
+ Button7,
1389
+ {
1390
+ type: "button",
1391
+ variant: "link",
1392
+ className: "h-auto p-0 text-sm text-muted-foreground hover:underline",
1393
+ disabled: isSigningOut,
1394
+ onClick: handleBackToSignIn,
1395
+ children: isSigningOut ? "Signing out\u2026" : "Back to sign in"
1396
+ }
1397
+ ) })
1343
1398
  ] }) });
1344
1399
  }
1345
1400
 
@@ -1399,6 +1454,9 @@ function formatMemberSince(value) {
1399
1454
  });
1400
1455
  }
1401
1456
  function SettingsTopBar() {
1457
+ const config = useOptionalConfig();
1458
+ const appTitle = config?.appName ?? "Boring UI";
1459
+ const appInitial = (appTitle.trim().charAt(0) || "B").toUpperCase();
1402
1460
  return /* @__PURE__ */ jsx13(
1403
1461
  "header",
1404
1462
  {
@@ -1410,10 +1468,10 @@ function SettingsTopBar() {
1410
1468
  {
1411
1469
  "aria-hidden": "true",
1412
1470
  className: "flex h-7 w-7 shrink-0 items-center justify-center rounded-md bg-foreground text-[12px] font-semibold text-background",
1413
- children: "S"
1471
+ children: appInitial
1414
1472
  }
1415
1473
  ),
1416
- /* @__PURE__ */ jsx13("span", { className: "truncate text-[13px] font-medium tracking-tight text-foreground", children: "Sovereign Workspace" }),
1474
+ /* @__PURE__ */ jsx13("span", { className: "truncate text-[13px] font-medium tracking-tight text-foreground", children: appTitle }),
1417
1475
  /* @__PURE__ */ jsx13("span", { "aria-hidden": "true", className: "text-muted-foreground/30", children: "/" }),
1418
1476
  /* @__PURE__ */ jsx13("span", { className: "truncate text-[13px] text-muted-foreground", children: "Account settings" })
1419
1477
  ] })
@@ -1740,7 +1798,7 @@ function UserSettingsPage({ topBar, extraSections = [] } = {}) {
1740
1798
 
1741
1799
  // src/front/auth/InviteAcceptPage.tsx
1742
1800
  import { useCallback as useCallback5, useState as useState14 } from "react";
1743
- import { useParams as useParams2, useNavigate } from "react-router-dom";
1801
+ import { useParams as useParams2, useNavigate as useNavigate2 } from "react-router-dom";
1744
1802
  import { useQuery as useQuery3, useMutation, useQueryClient as useQueryClient2 } from "@tanstack/react-query";
1745
1803
  import {
1746
1804
  Button as Button9,
@@ -1757,7 +1815,7 @@ import { jsx as jsx14, jsxs as jsxs7 } from "react/jsx-runtime";
1757
1815
  function InviteAcceptPage() {
1758
1816
  const { token } = useParams2();
1759
1817
  const session = useSession();
1760
- const navigate = useNavigate();
1818
+ const navigate = useNavigate2();
1761
1819
  const queryClient = useQueryClient2();
1762
1820
  const [acceptError, setAcceptError] = useState14(null);
1763
1821
  const isSignedIn = Boolean(session.data);
@@ -1891,6 +1949,15 @@ import { matchPath as matchPath2 } from "react-router-dom";
1891
1949
  import { Fragment as Fragment3, jsx as jsx15 } from "react/jsx-runtime";
1892
1950
  var DEFAULT_GRACE_MS = 3e4;
1893
1951
  var UNSAFE_REDIRECT_RE = /[\0\r\n<>"'`]/;
1952
+ var UNVERIFIED_ALLOWED_PATHS = /* @__PURE__ */ new Set([
1953
+ routes.signup,
1954
+ routes.forgotPassword,
1955
+ routes.resetPassword,
1956
+ routes.verifyEmail,
1957
+ routes.authError,
1958
+ routes.callbackGithub,
1959
+ routes.callbackGoogle
1960
+ ]);
1894
1961
  function normalizePath(pathname) {
1895
1962
  if (!pathname) return "/";
1896
1963
  return pathname.startsWith("/") ? pathname : `/${pathname}`;
@@ -1921,6 +1988,9 @@ function isPublicPath(pathname, publicPaths) {
1921
1988
  return normalizedPath === candidate || normalizedPath.startsWith(`${candidate}/`);
1922
1989
  });
1923
1990
  }
1991
+ function shouldBlockUnverifiedUser(pathname, hasSession, requireEmailVerification, isEmailVerified) {
1992
+ return hasSession && requireEmailVerification && !isEmailVerified && !UNVERIFIED_ALLOWED_PATHS.has(normalizePath(pathname));
1993
+ }
1924
1994
  function readSafeRedirect(search) {
1925
1995
  const redirect = new URLSearchParams(normalizeSearch(search)).get("redirect");
1926
1996
  if (!redirect) return null;
@@ -1950,7 +2020,8 @@ function AuthGate({
1950
2020
  graceMs = DEFAULT_GRACE_MS,
1951
2021
  location,
1952
2022
  navigate,
1953
- now
2023
+ now,
2024
+ requireEmailVerification = false
1954
2025
  }) {
1955
2026
  const session = useSession();
1956
2027
  const nullSinceRef = useRef5(null);
@@ -1962,6 +2033,7 @@ function AuthGate({
1962
2033
  () => publicPaths.map(normalizePublicPath),
1963
2034
  [publicPaths]
1964
2035
  );
2036
+ const isEmailVerified = session.data?.user?.emailVerified ?? false;
1965
2037
  useEffect9(() => {
1966
2038
  return () => {
1967
2039
  if (!redirectTimerRef.current) return;
@@ -1978,6 +2050,10 @@ function AuthGate({
1978
2050
  const currentPath = buildCurrentPath(currentLocation);
1979
2051
  if (session.data) {
1980
2052
  nullSinceRef.current = null;
2053
+ if (shouldBlockUnverifiedUser(pathname2, true, requireEmailVerification, isEmailVerified)) {
2054
+ goTo(routes.verifyEmail, { replace: true });
2055
+ return;
2056
+ }
1981
2057
  if (pathname2 === routes.signin) {
1982
2058
  const destination = readSafeRedirect(currentLocation.search) ?? "/";
1983
2059
  if (destination !== currentPath) {
@@ -2008,15 +2084,16 @@ function AuthGate({
2008
2084
  goTo(`${routes.signin}?redirect=${encodeURIComponent(currentPath)}`, { replace: true });
2009
2085
  }, remainingMs);
2010
2086
  redirectTimerRef.current.unref?.();
2011
- }, [currentLocation, goTo, graceMs, normalizedPublicPaths, readNow, session.data, session.isPending]);
2087
+ }, [currentLocation, goTo, graceMs, isEmailVerified, normalizedPublicPaths, readNow, requireEmailVerification, session.data, session.isPending]);
2012
2088
  const pathname = normalizePath(currentLocation.pathname);
2089
+ if (shouldBlockUnverifiedUser(pathname, Boolean(session.data), requireEmailVerification, isEmailVerified)) return null;
2013
2090
  if (session.isPending && !isPublicPath(pathname, normalizedPublicPaths)) return null;
2014
2091
  return /* @__PURE__ */ jsx15(Fragment3, { children });
2015
2092
  }
2016
2093
 
2017
2094
  // src/front/CoreFront.tsx
2018
2095
  import { Suspense, useCallback as useCallback9, useMemo as useMemo6 } from "react";
2019
- import { BrowserRouter, Routes, Route, useLocation as useLocation2, useNavigate as useNavigate5 } from "react-router-dom";
2096
+ import { BrowserRouter, Routes, Route, useLocation as useLocation2, useNavigate as useNavigate6 } from "react-router-dom";
2020
2097
  import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
2021
2098
  import { Helmet, HelmetProvider } from "react-helmet-async";
2022
2099
 
@@ -2040,7 +2117,7 @@ import {
2040
2117
  Settings,
2041
2118
  Sun
2042
2119
  } from "lucide-react";
2043
- import { useNavigate as useNavigate2 } from "react-router-dom";
2120
+ import { useNavigate as useNavigate3 } from "react-router-dom";
2044
2121
  import { jsx as jsx16, jsxs as jsxs8 } from "react/jsx-runtime";
2045
2122
  var THEME_ORDER = ["light", "dark", "system"];
2046
2123
  function labelForTheme(preference) {
@@ -2062,7 +2139,7 @@ function initialsFor2(name, email) {
2062
2139
  function UserMenu() {
2063
2140
  const identity = useUser();
2064
2141
  const signOut = useSignOut();
2065
- const navigate = useNavigate2();
2142
+ const navigate = useNavigate3();
2066
2143
  const { preference, setTheme } = useTheme();
2067
2144
  const [isSigningOut, setIsSigningOut] = useState15(false);
2068
2145
  const user = identity?.user;
@@ -2192,7 +2269,7 @@ import {
2192
2269
  useToast
2193
2270
  } from "@hachej/boring-ui-kit";
2194
2271
  import { ChevronsUpDown, LayoutGrid, Plus, Settings as Settings2 } from "lucide-react";
2195
- import { useNavigate as useNavigate3 } from "react-router-dom";
2272
+ import { useNavigate as useNavigate4 } from "react-router-dom";
2196
2273
  import { z as z6 } from "zod";
2197
2274
  import { Fragment as Fragment4, jsx as jsx17, jsxs as jsxs9 } from "react/jsx-runtime";
2198
2275
  var workspaceNameSchema = z6.object({
@@ -2238,10 +2315,12 @@ function OpenInNewTabIcon({ className }) {
2238
2315
  ] });
2239
2316
  }
2240
2317
  function WorkspaceSwitcher({
2241
- appTitle = "Sovereign Workspace",
2318
+ appTitle,
2242
2319
  workspacePathPrefix = "/workspace"
2243
2320
  }) {
2244
- const navigate = useNavigate3();
2321
+ const config = useOptionalConfig();
2322
+ const resolvedAppTitle = appTitle ?? config?.appName ?? "Boring UI";
2323
+ const navigate = useNavigate4();
2245
2324
  const queryClient = useQueryClient3();
2246
2325
  const { toast } = useToastCompat();
2247
2326
  const currentWorkspace = useCurrentWorkspace();
@@ -2328,7 +2407,7 @@ function WorkspaceSwitcher({
2328
2407
  {
2329
2408
  "aria-hidden": "true",
2330
2409
  className: "flex h-7 w-7 shrink-0 items-center justify-center rounded-md bg-foreground text-[12px] font-semibold text-background",
2331
- children: appTitle.charAt(0).toUpperCase()
2410
+ children: resolvedAppTitle.charAt(0).toUpperCase()
2332
2411
  }
2333
2412
  ),
2334
2413
  /* @__PURE__ */ jsx17("span", { className: "text-[13px] font-medium text-foreground", children: "Create your first workspace" })
@@ -2348,11 +2427,11 @@ function WorkspaceSwitcher({
2348
2427
  {
2349
2428
  "aria-hidden": "true",
2350
2429
  className: "flex h-7 w-7 shrink-0 items-center justify-center rounded-md bg-foreground text-[12px] font-semibold text-background",
2351
- children: appTitle.charAt(0).toUpperCase()
2430
+ children: resolvedAppTitle.charAt(0).toUpperCase()
2352
2431
  }
2353
2432
  ),
2354
2433
  /* @__PURE__ */ jsxs9("span", { className: "flex min-w-0 items-center gap-1.5", children: [
2355
- /* @__PURE__ */ jsx17("span", { className: "truncate text-[13px] font-medium text-foreground", children: appTitle }),
2434
+ /* @__PURE__ */ jsx17("span", { className: "truncate text-[13px] font-medium text-foreground", children: resolvedAppTitle }),
2356
2435
  /* @__PURE__ */ jsx17("span", { "aria-hidden": "true", className: "text-muted-foreground/30", children: "/" }),
2357
2436
  /* @__PURE__ */ jsx17("span", { className: "truncate text-[13px] font-normal text-muted-foreground", children: switcherLabel })
2358
2437
  ] }),
@@ -2985,7 +3064,7 @@ function MembersPage() {
2985
3064
  // src/front/workspace/WorkspaceSettingsPage.tsx
2986
3065
  import { useCallback as useCallback8, useEffect as useEffect10, useState as useState19 } from "react";
2987
3066
  import { useQuery as useQuery6, useMutation as useMutation4, useQueryClient as useQueryClient6 } from "@tanstack/react-query";
2988
- import { useNavigate as useNavigate4 } from "react-router-dom";
3067
+ import { useNavigate as useNavigate5 } from "react-router-dom";
2989
3068
  import {
2990
3069
  AlertDialog as AlertDialog3,
2991
3070
  AlertDialogCancel as AlertDialogCancel3,
@@ -3020,7 +3099,10 @@ var STATE_TONES = {
3020
3099
  error: "danger"
3021
3100
  };
3022
3101
  function SettingsTopBar2({ workspaceId, workspaceName }) {
3023
- const navigate = useNavigate4();
3102
+ const config = useOptionalConfig();
3103
+ const appTitle = config?.appName ?? "Boring UI";
3104
+ const appInitial = (appTitle.trim().charAt(0) || "B").toUpperCase();
3105
+ const navigate = useNavigate5();
3024
3106
  const workspaceHref = workspaceId ? `/workspace/${encodeURIComponent(workspaceId)}` : "/";
3025
3107
  return /* @__PURE__ */ jsx21(
3026
3108
  "header",
@@ -3038,10 +3120,10 @@ function SettingsTopBar2({ workspaceId, workspaceName }) {
3038
3120
  title: "Back to workspace",
3039
3121
  onClick: () => navigate(workspaceHref),
3040
3122
  className: "shrink-0 bg-foreground text-[12px] font-semibold text-background hover:bg-foreground/90",
3041
- children: "S"
3123
+ children: appInitial
3042
3124
  }
3043
3125
  ),
3044
- /* @__PURE__ */ jsx21("span", { className: "truncate text-[13px] font-medium tracking-tight text-foreground", children: "Sovereign Workspace" }),
3126
+ /* @__PURE__ */ jsx21("span", { className: "truncate text-[13px] font-medium tracking-tight text-foreground", children: appTitle }),
3045
3127
  /* @__PURE__ */ jsx21("span", { "aria-hidden": "true", className: "text-muted-foreground/30", children: "/" }),
3046
3128
  /* @__PURE__ */ jsx21("span", { className: "truncate text-[13px] text-muted-foreground", children: workspaceName }),
3047
3129
  /* @__PURE__ */ jsx21("span", { "aria-hidden": "true", className: "text-muted-foreground/30", children: "/" }),
@@ -3091,7 +3173,7 @@ function WorkspaceSettingsPage({ topBar } = {}) {
3091
3173
  const workspace = useCurrentWorkspace();
3092
3174
  const role = useWorkspaceRole();
3093
3175
  const queryClient = useQueryClient6();
3094
- const navigate = useNavigate4();
3176
+ const navigate = useNavigate5();
3095
3177
  const workspaceId = workspace?.id ?? "";
3096
3178
  const [nameValue, setNameValue] = useState19(null);
3097
3179
  const [nameError, setNameError] = useState19(null);
@@ -3533,8 +3615,9 @@ function createDefaultQueryClient() {
3533
3615
  });
3534
3616
  }
3535
3617
  function RouterAuthGate({ children, publicPaths }) {
3618
+ const config = useConfig();
3536
3619
  const location = useLocation2();
3537
- const navigate = useNavigate5();
3620
+ const navigate = useNavigate6();
3538
3621
  const authLocation = useMemo6(
3539
3622
  () => ({ pathname: location.pathname, search: location.search, hash: location.hash }),
3540
3623
  [location.hash, location.pathname, location.search]
@@ -3551,6 +3634,7 @@ function RouterAuthGate({ children, publicPaths }) {
3551
3634
  location: authLocation,
3552
3635
  navigate: navigateWithinRouter,
3553
3636
  publicPaths,
3637
+ requireEmailVerification: isRuntimeEmailVerificationEnabled(config),
3554
3638
  children
3555
3639
  }
3556
3640
  );
@@ -3605,7 +3689,7 @@ function CoreFront({ children, authPages, cspNonce, workspaceRoute, workspaceIdP
3605
3689
 
3606
3690
  // src/front/commands/CoreCommandContributions.tsx
3607
3691
  import { useMemo as useMemo7, useState as useState20 } from "react";
3608
- import { useNavigate as useNavigate6 } from "react-router-dom";
3692
+ import { useNavigate as useNavigate7 } from "react-router-dom";
3609
3693
 
3610
3694
  // src/front/workspace/commands.ts
3611
3695
  function getWorkspaceCommands(workspaceId, navigate) {
@@ -3643,7 +3727,7 @@ function toPaletteCommand(command) {
3643
3727
  };
3644
3728
  }
3645
3729
  function useCoreCommands() {
3646
- const navigate = useNavigate6();
3730
+ const navigate = useNavigate7();
3647
3731
  const signOut = useSignOut();
3648
3732
  const workspace = useCurrentWorkspace();
3649
3733
  const [isSigningOut, setIsSigningOut] = useState20(false);
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  ERROR_CODES,
3
3
  HttpError
4
- } from "./chunk-LIBHVT7V.js";
4
+ } from "./chunk-QZGYKLXB.js";
5
5
  import {
6
6
  __export
7
7
  } from "./chunk-MLKGABMK.js";
@@ -0,0 +1,17 @@
1
+ // src/shared/authPolicy.ts
2
+ function isCoreEmailVerificationEnabled(config) {
3
+ return Boolean(config.auth.mail);
4
+ }
5
+ function isRuntimeEmailVerificationEnabled(config) {
6
+ return config?.features.emailVerification === true;
7
+ }
8
+ function canUseProtectedApi(user, requireEmailVerification) {
9
+ if (!user) return false;
10
+ return !requireEmailVerification || user.emailVerified === true;
11
+ }
12
+
13
+ export {
14
+ isCoreEmailVerificationEnabled,
15
+ isRuntimeEmailVerificationEnabled,
16
+ canUseProtectedApi
17
+ };
@@ -5,6 +5,7 @@ var ERROR_CODES = {
5
5
  FORBIDDEN: "forbidden",
6
6
  WEAK_PASSWORD: "weak_password",
7
7
  EMAIL_IN_USE: "email_in_use",
8
+ EMAIL_NOT_VERIFIED: "email_not_verified",
8
9
  // Workspace membership
9
10
  NOT_MEMBER: "not_member",
10
11
  LAST_OWNER: "last_owner",
@@ -12,16 +12,19 @@ import {
12
12
  workspaceRuntimes,
13
13
  workspaceSettings,
14
14
  workspaces
15
- } from "./chunk-FZC3VL5D.js";
15
+ } from "./chunk-BVZ2YT3M.js";
16
16
  import {
17
17
  noopTelemetry,
18
18
  safeCapture
19
19
  } from "./chunk-AQBXNPMD.js";
20
+ import {
21
+ isCoreEmailVerificationEnabled
22
+ } from "./chunk-I4PGL4ZD.js";
20
23
  import {
21
24
  ConfigValidationError,
22
25
  ERROR_CODES,
23
26
  HttpError
24
- } from "./chunk-LIBHVT7V.js";
27
+ } from "./chunk-QZGYKLXB.js";
25
28
 
26
29
  // src/server/config/schema.ts
27
30
  import { z } from "zod";
@@ -290,7 +293,8 @@ function buildRuntimeConfigPayload(config) {
290
293
  githubOauth: config.features.githubOauth,
291
294
  googleOauth: isGoogleOauthUsable(config),
292
295
  invitesEnabled: config.features.invitesEnabled,
293
- sendWelcomeEmail: config.features.sendWelcomeEmail
296
+ sendWelcomeEmail: config.features.sendWelcomeEmail,
297
+ emailVerification: isCoreEmailVerificationEnabled(config)
294
298
  }
295
299
  };
296
300
  }
@@ -419,7 +423,7 @@ function registerCapabilities(app) {
419
423
  contributors.set(name, fn);
420
424
  }
421
425
  );
422
- const hasMail = !!app.config.auth.mail;
426
+ const emailVerificationEnabled = isCoreEmailVerificationEnabled(app.config);
423
427
  app.registerCapabilitiesContributor("core", () => {
424
428
  const googleOauth = isGoogleOauthUsable(app.config);
425
429
  const core = {
@@ -428,15 +432,15 @@ function registerCapabilities(app) {
428
432
  invitesEnabled: app.config.features.invitesEnabled,
429
433
  githubOauth: app.config.features.githubOauth,
430
434
  googleOauth,
431
- emailFlows: hasMail
435
+ emailFlows: emailVerificationEnabled
432
436
  },
433
437
  auth: {
434
438
  emailPassword: true,
435
439
  github: false,
436
440
  google: googleOauth,
437
- emailVerification: hasMail,
438
- passwordReset: hasMail,
439
- magicLink: hasMail
441
+ emailVerification: emailVerificationEnabled,
442
+ passwordReset: emailVerificationEnabled,
443
+ magicLink: emailVerificationEnabled
440
444
  }
441
445
  };
442
446
  return { core };
@@ -1747,7 +1751,8 @@ function buildMailTransport(config) {
1747
1751
  function createAuth(config, db, opts) {
1748
1752
  const transport = buildMailTransport(config);
1749
1753
  const telemetry = opts?.telemetry ?? noopTelemetry;
1750
- const emailVerificationConfig = transport ? {
1754
+ const emailVerificationEnabled = isCoreEmailVerificationEnabled(config);
1755
+ const emailVerificationConfig = emailVerificationEnabled && transport ? {
1751
1756
  sendOnSignUp: true,
1752
1757
  sendVerificationEmail: async (data) => {
1753
1758
  const email = await renderVerifyEmail({
@@ -1930,7 +1935,8 @@ var authHookPlugin = async (app, opts) => {
1930
1935
  request.user = {
1931
1936
  id: result.user.id,
1932
1937
  email: result.user.email,
1933
- name: result.user.name ?? null
1938
+ name: result.user.name ?? null,
1939
+ emailVerified: Boolean(result.user.emailVerified)
1934
1940
  };
1935
1941
  }
1936
1942
  } catch {
@@ -1938,7 +1944,8 @@ var authHookPlugin = async (app, opts) => {
1938
1944
  }
1939
1945
  }
1940
1946
  const path = request.url.split("?")[0];
1941
- if (path.startsWith("/api/v1/") && !publicPatterns.some((re) => re.test(path)) && !request.user) {
1947
+ const isProtectedApi = path.startsWith("/api/v1/") && !publicPatterns.some((re) => re.test(path));
1948
+ if (isProtectedApi && !request.user) {
1942
1949
  throw new HttpError({
1943
1950
  status: 401,
1944
1951
  code: ERROR_CODES.UNAUTHORIZED,
@@ -1946,6 +1953,14 @@ var authHookPlugin = async (app, opts) => {
1946
1953
  requestId: request.id
1947
1954
  });
1948
1955
  }
1956
+ if (isProtectedApi && isCoreEmailVerificationEnabled(app.config) && request.user?.emailVerified !== true) {
1957
+ throw new HttpError({
1958
+ status: 403,
1959
+ code: ERROR_CODES.EMAIL_NOT_VERIFIED,
1960
+ message: "Email verification required",
1961
+ requestId: request.id
1962
+ });
1963
+ }
1949
1964
  });
1950
1965
  };
1951
1966
  var authHook = fp2(authHookPlugin, { name: "auth-hook" });
@@ -1,4 +1,4 @@
1
- import { C as CoreConfig, W as Workspace, E as ERROR_CODES, M as MemberRole, a as WorkspaceMember, U as User, b as WorkspaceInvite, c as WorkspaceRuntime, d as WorkspaceRuntimeResourceSelector, e as WorkspaceRuntimeResource, f as WorkspaceRuntimeResourceInput, g as CapabilitiesResponse } from './types-CWtJ4kgd.js';
1
+ import { C as CoreConfig, c as Workspace, E as ERROR_CODES, M as MemberRole, d as WorkspaceMember, U as User, e as WorkspaceInvite, f as WorkspaceRuntime, W as WorkspaceRuntimeResourceSelector, a as WorkspaceRuntimeResource, b as WorkspaceRuntimeResourceInput, g as CapabilitiesResponse } from './types-Bb7I-83I.js';
2
2
  import { drizzle } from 'drizzle-orm/postgres-js';
3
3
  import postgres from 'postgres';
4
4
 
@@ -131,6 +131,7 @@ declare module 'fastify' {
131
131
  id: string;
132
132
  email: string;
133
133
  name: string | null;
134
+ emailVerified: boolean;
134
135
  } | null;
135
136
  cspNonce?: string;
136
137
  }
@@ -142,4 +143,4 @@ declare function createDatabase(config: CoreConfig): {
142
143
  sql: postgres.Sql;
143
144
  };
144
145
 
145
- export { type AuthProvider as A, type CreateCoreAppOptions as C, type Database as D, type ProvisionContext as P, type UserStore as U, type WorkspaceStore as W, type WorkspaceProvisioner as a, type CapabilitiesContributor as b, createDatabase as c, type ProvisionResult as d };
146
+ export { type AuthProvider as A, type CreateCoreAppOptions as C, type Database as D, type ProvisionContext as P, type UserStore as U, type WorkspaceStore as W, type WorkspaceProvisioner as a, type CapabilitiesContributor as b, type ProvisionResult as c, createDatabase as d };
@@ -1,7 +1,7 @@
1
1
  import * as react_jsx_runtime from 'react/jsx-runtime';
2
2
  import * as react from 'react';
3
3
  import { Component, ReactNode, ErrorInfo } from 'react';
4
- import { R as RuntimeConfig, g as CapabilitiesResponse, a as WorkspaceMember, U as User, W as Workspace, M as MemberRole, S as SessionState } from '../types-CWtJ4kgd.js';
4
+ import { R as RuntimeConfig, g as CapabilitiesResponse, d as WorkspaceMember, U as User, c as Workspace, M as MemberRole, S as SessionState } from '../types-Bb7I-83I.js';
5
5
  import * as _tanstack_react_query from '@tanstack/react-query';
6
6
  import * as better_auth_react from 'better-auth/react';
7
7
  import { createAuthClient } from 'better-auth/react';
@@ -461,8 +461,9 @@ interface AuthGateProps {
461
461
  replace?: boolean;
462
462
  }) => void;
463
463
  now?: () => number;
464
+ requireEmailVerification?: boolean;
464
465
  }
465
- declare function AuthGate({ children, publicPaths, graceMs, location, navigate, now, }: AuthGateProps): react_jsx_runtime.JSX.Element | null;
466
+ declare function AuthGate({ children, publicPaths, graceMs, location, navigate, now, requireEmailVerification, }: AuthGateProps): react_jsx_runtime.JSX.Element | null;
466
467
 
467
468
  interface CoreCommand {
468
469
  id: string;
@@ -58,12 +58,13 @@ import {
58
58
  useWorkspaceMembers,
59
59
  useWorkspaceRole,
60
60
  useWorkspaceRouteStatus
61
- } from "../chunk-VYXEXOCO.js";
61
+ } from "../chunk-2ZTKRLVG.js";
62
62
  import {
63
63
  TopBarSlotProvider,
64
64
  useTopBarSlot
65
65
  } from "../chunk-HYNKZSTF.js";
66
- import "../chunk-LIBHVT7V.js";
66
+ import "../chunk-I4PGL4ZD.js";
67
+ import "../chunk-QZGYKLXB.js";
67
68
  import "../chunk-MLKGABMK.js";
68
69
  export {
69
70
  AppErrorBoundary,
@@ -1,7 +1,7 @@
1
- import { U as UserStore, W as WorkspaceStore } from '../../connection-C5SiqoNc.js';
2
- export { D as Database, c as createDatabase } from '../../connection-C5SiqoNc.js';
3
- export { F as FinishReservationInput, G as GrantOnceInput, I as InsufficientCreditError, M as MeteringBalance, P as PostgresMeteringStore, R as RecordUsageInput, a as RecordUsageResult, b as ReservationFinalStatus, c as ReserveInput, d as ReserveResult, e as RunMigrationsOptions, r as runMigrations } from '../../PostgresMeteringStore-DhOVVtau.js';
4
- import { U as User, W as Workspace, E as ERROR_CODES, M as MemberRole, a as WorkspaceMember, b as WorkspaceInvite, c as WorkspaceRuntime, d as WorkspaceRuntimeResourceSelector, e as WorkspaceRuntimeResource, f as WorkspaceRuntimeResourceInput } from '../../types-CWtJ4kgd.js';
1
+ import { U as UserStore, W as WorkspaceStore } from '../../connection-B1iC6B-w.js';
2
+ export { D as Database, d as createDatabase } from '../../connection-B1iC6B-w.js';
3
+ export { F as FinishReservationInput, G as GrantOnceInput, I as InsufficientCreditError, M as MeteringBalance, P as PostgresMeteringStore, R as RecordUsageInput, a as RecordUsageResult, b as ReservationFinalStatus, c as ReserveInput, d as ReserveResult, e as RunMigrationsOptions, r as runMigrations } from '../../PostgresMeteringStore-qjKGmVFr.js';
4
+ import { U as User, c as Workspace, E as ERROR_CODES, M as MemberRole, d as WorkspaceMember, e as WorkspaceInvite, f as WorkspaceRuntime, W as WorkspaceRuntimeResourceSelector, a as WorkspaceRuntimeResource, b as WorkspaceRuntimeResourceInput } from '../../types-Bb7I-83I.js';
5
5
  import { PostgresJsDatabase } from 'drizzle-orm/postgres-js';
6
6
  import 'postgres';
7
7
 
@@ -7,8 +7,8 @@ import {
7
7
  PostgresWorkspaceStore,
8
8
  createDatabase,
9
9
  runMigrations
10
- } from "../../chunk-FZC3VL5D.js";
11
- import "../../chunk-LIBHVT7V.js";
10
+ } from "../../chunk-BVZ2YT3M.js";
11
+ import "../../chunk-QZGYKLXB.js";
12
12
  import "../../chunk-MLKGABMK.js";
13
13
  export {
14
14
  InsufficientCreditError,