zudoku 0.53.6 → 0.54.1

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 (197) hide show
  1. package/dist/config/config.d.ts +1 -0
  2. package/dist/config/validators/InputNavigationSchema.d.ts +190 -158
  3. package/dist/config/validators/InputNavigationSchema.js +4 -1
  4. package/dist/config/validators/InputNavigationSchema.js.map +1 -1
  5. package/dist/config/validators/ProtectedRoutesSchema.d.ts +12 -0
  6. package/dist/config/validators/ProtectedRoutesSchema.js +19 -0
  7. package/dist/config/validators/ProtectedRoutesSchema.js.map +1 -0
  8. package/dist/config/validators/validate.d.ts +32 -19
  9. package/dist/config/validators/validate.js +6 -2
  10. package/dist/config/validators/validate.js.map +1 -1
  11. package/dist/flat-config.d.ts +6 -2
  12. package/dist/lib/authentication/components/CallbackHandler.js +11 -9
  13. package/dist/lib/authentication/components/CallbackHandler.js.map +1 -1
  14. package/dist/lib/authentication/components/OAuthErrorPage.d.ts +3 -0
  15. package/dist/lib/authentication/components/OAuthErrorPage.js +99 -0
  16. package/dist/lib/authentication/components/OAuthErrorPage.js.map +1 -0
  17. package/dist/lib/authentication/errors.d.ts +6 -12
  18. package/dist/lib/authentication/errors.js +2 -1
  19. package/dist/lib/authentication/errors.js.map +1 -1
  20. package/dist/lib/authentication/hook.d.ts +1 -0
  21. package/dist/lib/authentication/hook.js.map +1 -1
  22. package/dist/lib/authentication/providers/azureb2c.js +4 -2
  23. package/dist/lib/authentication/providers/azureb2c.js.map +1 -1
  24. package/dist/lib/authentication/providers/clerk.js +4 -2
  25. package/dist/lib/authentication/providers/clerk.js.map +1 -1
  26. package/dist/lib/authentication/providers/openid.js +3 -1
  27. package/dist/lib/authentication/providers/openid.js.map +1 -1
  28. package/dist/lib/components/Heading.js +1 -1
  29. package/dist/lib/components/Heading.js.map +1 -1
  30. package/dist/lib/components/MobileTopNavigation.js +5 -3
  31. package/dist/lib/components/MobileTopNavigation.js.map +1 -1
  32. package/dist/lib/components/TopNavigation.js +4 -3
  33. package/dist/lib/components/TopNavigation.js.map +1 -1
  34. package/dist/lib/components/context/ZudokuContext.js +21 -13
  35. package/dist/lib/components/context/ZudokuContext.js.map +1 -1
  36. package/dist/lib/components/navigation/NavigationItem.d.ts +1 -1
  37. package/dist/lib/components/navigation/NavigationItem.js +8 -1
  38. package/dist/lib/components/navigation/NavigationItem.js.map +1 -1
  39. package/dist/lib/components/navigation/utils.d.ts +3 -1
  40. package/dist/lib/components/navigation/utils.js +6 -3
  41. package/dist/lib/components/navigation/utils.js.map +1 -1
  42. package/dist/lib/core/RouteGuard.js +9 -9
  43. package/dist/lib/core/RouteGuard.js.map +1 -1
  44. package/dist/lib/core/ZudokuContext.d.ts +2 -1
  45. package/dist/lib/core/ZudokuContext.js +13 -1
  46. package/dist/lib/core/ZudokuContext.js.map +1 -1
  47. package/dist/lib/core/plugins.d.ts +2 -1
  48. package/dist/lib/core/plugins.js.map +1 -1
  49. package/dist/lib/plugins/api-keys/CreateApiKey.js +7 -3
  50. package/dist/lib/plugins/api-keys/CreateApiKey.js.map +1 -1
  51. package/dist/lib/plugins/api-keys/SettingsApiKeys.js +3 -1
  52. package/dist/lib/plugins/api-keys/SettingsApiKeys.js.map +1 -1
  53. package/dist/lib/plugins/api-keys/index.d.ts +1 -0
  54. package/dist/lib/plugins/api-keys/index.js +3 -7
  55. package/dist/lib/plugins/api-keys/index.js.map +1 -1
  56. package/dist/lib/plugins/openapi/graphql/gql.d.ts +1 -1
  57. package/dist/lib/plugins/openapi/graphql/gql.js +1 -1
  58. package/dist/lib/plugins/openapi/graphql/gql.js.map +1 -1
  59. package/dist/lib/plugins/openapi/graphql/graphql.d.ts +1 -0
  60. package/dist/lib/plugins/openapi/graphql/graphql.js +1 -0
  61. package/dist/lib/plugins/openapi/graphql/graphql.js.map +1 -1
  62. package/dist/lib/plugins/openapi/index.js +42 -10
  63. package/dist/lib/plugins/openapi/index.js.map +1 -1
  64. package/dist/lib/ui/ActionButton.js +1 -1
  65. package/dist/lib/ui/ActionButton.js.map +1 -1
  66. package/dist/lib/ui/Badge.d.ts +1 -1
  67. package/dist/lib/ui/Button.d.ts +2 -2
  68. package/dist/lib/ui/Command.d.ts +1 -1
  69. package/dist/lib/util/invariant.d.ts +6 -5
  70. package/dist/lib/util/invariant.js +1 -1
  71. package/dist/lib/util/invariant.js.map +1 -1
  72. package/dist/vite/dev-server.js +1 -1
  73. package/dist/vite/dev-server.js.map +1 -1
  74. package/dist/vite/prerender/worker.js +5 -1
  75. package/dist/vite/prerender/worker.js.map +1 -1
  76. package/dist/vite/shadcn-registry.d.ts +8 -8
  77. package/lib/{Command-C9AC5cf-.js → Command-BYukybsa.js} +2 -2
  78. package/lib/{Command-C9AC5cf-.js.map → Command-BYukybsa.js.map} +1 -1
  79. package/lib/{Dialog-DMWw1doX.js → Dialog-u9Uz9sTt.js} +4 -4
  80. package/lib/{Dialog-DMWw1doX.js.map → Dialog-u9Uz9sTt.js.map} +1 -1
  81. package/lib/{MdxPage-DVI4iYgW.js → MdxPage-Bsko6_kb.js} +11 -11
  82. package/lib/{MdxPage-DVI4iYgW.js.map → MdxPage-Bsko6_kb.js.map} +1 -1
  83. package/lib/OAuthErrorPage-DJzGiIBt.js +150 -0
  84. package/lib/OAuthErrorPage-DJzGiIBt.js.map +1 -0
  85. package/lib/{OasProvider-CbwsKPNc.js → OasProvider-DQQRt3oS.js} +3 -3
  86. package/lib/{OasProvider-CbwsKPNc.js.map → OasProvider-DQQRt3oS.js.map} +1 -1
  87. package/lib/{OperationList-Bn9ggxw8.js → OperationList-DpmkHf26.js} +45 -43
  88. package/lib/{OperationList-Bn9ggxw8.js.map → OperationList-DpmkHf26.js.map} +1 -1
  89. package/lib/{Pagination-bavPec-z.js → Pagination-kqFNgtnI.js} +3 -3
  90. package/lib/{Pagination-bavPec-z.js.map → Pagination-kqFNgtnI.js.map} +1 -1
  91. package/lib/{RouteGuard-Vnlz_t51.js → RouteGuard-0wPUKdxJ.js} +166 -165
  92. package/lib/{RouteGuard-Vnlz_t51.js.map → RouteGuard-0wPUKdxJ.js.map} +1 -1
  93. package/lib/{SchemaList-DETyCVqu.js → SchemaList-DS-pMd6B.js} +8 -8
  94. package/lib/{SchemaList-DETyCVqu.js.map → SchemaList-DS-pMd6B.js.map} +1 -1
  95. package/lib/{SchemaView-Dvxo2RNe.js → SchemaView-BnN6WHjw.js} +4 -4
  96. package/lib/{SchemaView-Dvxo2RNe.js.map → SchemaView-BnN6WHjw.js.map} +1 -1
  97. package/lib/Select-BmTTKNPp.js +273 -0
  98. package/lib/Select-BmTTKNPp.js.map +1 -0
  99. package/lib/{SignUp-ClYhZq9H.js → SignUp-BwOSCD-6.js} +9 -9
  100. package/lib/{SignUp-ClYhZq9H.js.map → SignUp-BwOSCD-6.js.map} +1 -1
  101. package/lib/{Slot-B31yZlfB.js → Slot-DAyXieeZ.js} +1352 -1349
  102. package/lib/{Slot-B31yZlfB.js.map → Slot-DAyXieeZ.js.map} +1 -1
  103. package/lib/{SyntaxHighlight-bm761HDo.js → SyntaxHighlight-BMKR4pl6.js} +3 -3
  104. package/lib/{SyntaxHighlight-bm761HDo.js.map → SyntaxHighlight-BMKR4pl6.js.map} +1 -1
  105. package/lib/{Toc-D4oBWE8D.js → Toc-BKDRCQzU.js} +2 -2
  106. package/lib/{Toc-D4oBWE8D.js.map → Toc-BKDRCQzU.js.map} +1 -1
  107. package/lib/ZudokuContext-CLl5w57E.js +1278 -0
  108. package/lib/ZudokuContext-CLl5w57E.js.map +1 -0
  109. package/lib/{chunk-DQRVZFIR-DHK7_Ilc.js → chunk-QMGIS6GS-CEOk3lro.js} +3 -3
  110. package/lib/chunk-QMGIS6GS-CEOk3lro.js.map +1 -0
  111. package/lib/{circular-CRbFI6Zl.js → circular-8GWQDvCW.js} +2 -2
  112. package/lib/{circular-CRbFI6Zl.js.map → circular-8GWQDvCW.js.map} +1 -1
  113. package/lib/{createServer-DNyGJJNX.js → createServer-BsezSzvV.js} +5 -5
  114. package/lib/{createServer-DNyGJJNX.js.map → createServer-BsezSzvV.js.map} +1 -1
  115. package/lib/{errors-C1GlNcV3.js → errors-Cs7hKmdL.js} +11 -10
  116. package/lib/errors-Cs7hKmdL.js.map +1 -0
  117. package/lib/hook-DbUCLQNg.js +247 -0
  118. package/lib/hook-DbUCLQNg.js.map +1 -0
  119. package/lib/{index-D09PbNex.js → index-A5Qdwj1B.js} +1521 -1420
  120. package/lib/index-A5Qdwj1B.js.map +1 -0
  121. package/lib/{index-C_PXQ8Bx.js → index-Bg7Js3jB.js} +832 -912
  122. package/lib/index-Bg7Js3jB.js.map +1 -0
  123. package/lib/{index-CZTEgYDd.js → index-BkW9tJ6j.js} +2 -2
  124. package/lib/{index-CZTEgYDd.js.map → index-BkW9tJ6j.js.map} +1 -1
  125. package/lib/index.esm-CdzlRw50.js +1254 -0
  126. package/lib/index.esm-CdzlRw50.js.map +1 -0
  127. package/lib/{invariant-DAFpPywt.js → invariant-Bm-FVUQE.js} +2 -6
  128. package/lib/invariant-Bm-FVUQE.js.map +1 -0
  129. package/lib/ui/ActionButton.js +9 -9
  130. package/lib/ui/ActionButton.js.map +1 -1
  131. package/lib/ui/Command.js +1 -1
  132. package/lib/ui/Form.js +1 -1
  133. package/lib/ui/SyntaxHighlight.js +3 -3
  134. package/lib/{useExposedProps-BIYjecPD.js → useExposedProps-KcgXHKeE.js} +2 -2
  135. package/lib/{useExposedProps-BIYjecPD.js.map → useExposedProps-KcgXHKeE.js.map} +1 -1
  136. package/lib/zudoku.auth-auth0.js +1 -1
  137. package/lib/zudoku.auth-azureb2c.js +25 -17
  138. package/lib/zudoku.auth-azureb2c.js.map +1 -1
  139. package/lib/zudoku.auth-clerk.js +24 -21
  140. package/lib/zudoku.auth-clerk.js.map +1 -1
  141. package/lib/zudoku.auth-openid.js +213 -205
  142. package/lib/zudoku.auth-openid.js.map +1 -1
  143. package/lib/zudoku.auth-supabase.js +2 -2
  144. package/lib/zudoku.components.js +29 -28
  145. package/lib/zudoku.components.js.map +1 -1
  146. package/lib/zudoku.hooks.js +16 -15
  147. package/lib/zudoku.hooks.js.map +1 -1
  148. package/lib/zudoku.plugin-api-catalog.js +26 -25
  149. package/lib/zudoku.plugin-api-catalog.js.map +1 -1
  150. package/lib/zudoku.plugin-api-keys.js +409 -297
  151. package/lib/zudoku.plugin-api-keys.js.map +1 -1
  152. package/lib/zudoku.plugin-custom-pages.js +1 -1
  153. package/lib/zudoku.plugin-markdown.js +1 -1
  154. package/lib/zudoku.plugin-openapi.js +7 -6
  155. package/lib/zudoku.plugin-openapi.js.map +1 -1
  156. package/lib/zudoku.plugin-redirect.js +1 -1
  157. package/lib/zudoku.plugin-search-pagefind.js +28 -27
  158. package/lib/zudoku.plugin-search-pagefind.js.map +1 -1
  159. package/lib/zudoku.plugins.js.map +1 -1
  160. package/package.json +4 -4
  161. package/src/lib/authentication/components/CallbackHandler.tsx +22 -15
  162. package/src/lib/authentication/components/OAuthErrorPage.tsx +171 -0
  163. package/src/lib/authentication/errors.ts +27 -13
  164. package/src/lib/authentication/hook.ts +2 -0
  165. package/src/lib/authentication/providers/azureb2c.tsx +8 -3
  166. package/src/lib/authentication/providers/clerk.tsx +4 -1
  167. package/src/lib/authentication/providers/openid.tsx +7 -1
  168. package/src/lib/components/Heading.tsx +1 -1
  169. package/src/lib/components/MobileTopNavigation.tsx +6 -3
  170. package/src/lib/components/TopNavigation.tsx +4 -4
  171. package/src/lib/components/context/ZudokuContext.ts +25 -18
  172. package/src/lib/components/navigation/NavigationItem.tsx +9 -1
  173. package/src/lib/components/navigation/utils.ts +9 -3
  174. package/src/lib/core/RouteGuard.tsx +13 -13
  175. package/src/lib/core/ZudokuContext.ts +18 -5
  176. package/src/lib/core/plugins.ts +2 -1
  177. package/src/lib/plugins/api-keys/CreateApiKey.tsx +12 -1
  178. package/src/lib/plugins/api-keys/SettingsApiKeys.tsx +24 -4
  179. package/src/lib/plugins/api-keys/index.tsx +7 -8
  180. package/src/lib/plugins/openapi/graphql/gql.ts +3 -3
  181. package/src/lib/plugins/openapi/graphql/graphql.ts +2 -0
  182. package/src/lib/plugins/openapi/index.tsx +66 -16
  183. package/src/lib/ui/ActionButton.tsx +3 -1
  184. package/src/lib/util/invariant.ts +7 -5
  185. package/lib/Alert-CWApD0CL.js +0 -161
  186. package/lib/Alert-CWApD0CL.js.map +0 -1
  187. package/lib/CallbackHandler-Dr5Lva9x.js +0 -38
  188. package/lib/CallbackHandler-Dr5Lva9x.js.map +0 -1
  189. package/lib/chunk-DQRVZFIR-DHK7_Ilc.js.map +0 -1
  190. package/lib/errors-C1GlNcV3.js.map +0 -1
  191. package/lib/hook-CZjW2buS.js +0 -1510
  192. package/lib/hook-CZjW2buS.js.map +0 -1
  193. package/lib/index-C_PXQ8Bx.js.map +0 -1
  194. package/lib/index-D09PbNex.js.map +0 -1
  195. package/lib/index.esm-Cp4wkyud.js +0 -1236
  196. package/lib/index.esm-Cp4wkyud.js.map +0 -1
  197. package/lib/invariant-DAFpPywt.js.map +0 -1
@@ -2,7 +2,6 @@ import { useQuery, useSuspenseQuery } from "@tanstack/react-query";
2
2
  import { createContext, useContext } from "react";
3
3
  import { matchPath, useLocation } from "react-router";
4
4
  import { type NavigationItem } from "../../../config/validators/NavigationSchema.js";
5
- import { useAuth } from "../../authentication/hook.js";
6
5
  import type { ZudokuContext } from "../../core/ZudokuContext.js";
7
6
  import { joinUrl } from "../../util/joinUrl.js";
8
7
  import { CACHE_KEYS } from "../cache.js";
@@ -45,14 +44,28 @@ const getItemPath = (item: NavigationItem) => {
45
44
  return undefined;
46
45
  }
47
46
  };
47
+
48
+ const extractAllPaths = (items: NavigationItem[]) => {
49
+ const paths = new Set<string>();
50
+
51
+ const collectPaths = (items: NavigationItem[]) => {
52
+ for (const item of items) {
53
+ const itemPath = getItemPath(item)?.split("?").at(0)?.split("#").at(0);
54
+
55
+ if (itemPath) paths.add(itemPath);
56
+ if (item.type === "category") {
57
+ collectPaths(item.items);
58
+ }
59
+ }
60
+ };
61
+ collectPaths(items);
62
+
63
+ return [...paths];
64
+ };
65
+
48
66
  export const useCurrentNavigation = () => {
49
- const { getPluginNavigation, navigation, options } = useZudoku();
67
+ const { getPluginNavigation, navigation } = useZudoku();
50
68
  const location = useLocation();
51
- const auth = useAuth();
52
-
53
- const isProtectedRoute = options.protectedRoutes?.some((route) =>
54
- matchPath(route, location.pathname),
55
- );
56
69
 
57
70
  const navItem = traverseNavigation(navigation, (item, parentCategories) => {
58
71
  if (getItemPath(item) === location.pathname) {
@@ -67,12 +80,8 @@ export const useCurrentNavigation = () => {
67
80
 
68
81
  let topNavItem = navItem;
69
82
  if (!navItem && data.length > 0) {
70
- // Extract base paths from plugin navigation items
71
- const pluginBasePaths = data.flatMap((item) => {
72
- return getItemPath(item)?.split("?").at(0)?.split("#").at(0) ?? [];
73
- });
83
+ const pluginBasePaths = extractAllPaths(data);
74
84
 
75
- // Find top-level nav item that matches any plugin base path
76
85
  topNavItem = navigation
77
86
  .flatMap((item) => {
78
87
  const itemPath = getItemPath(item);
@@ -88,13 +97,11 @@ export const useCurrentNavigation = () => {
88
97
  })?.item;
89
98
  }
90
99
 
91
- const hasNavigation =
92
- auth.isAuthEnabled && !auth.isAuthenticated && isProtectedRoute;
93
-
94
100
  return {
95
- navigation: hasNavigation
96
- ? []
97
- : [...(navItem?.type === "category" ? navItem.items : []), ...data],
101
+ navigation: [
102
+ ...(navItem?.type === "category" ? navItem.items : []),
103
+ ...data,
104
+ ],
98
105
  topNavItem,
99
106
  };
100
107
  };
@@ -9,13 +9,15 @@ import {
9
9
  TooltipTrigger,
10
10
  } from "zudoku/ui/Tooltip.js";
11
11
  import type { NavigationItem as NavigationItemType } from "../../../config/validators/NavigationSchema.js";
12
+ import { useAuth } from "../../authentication/hook.js";
12
13
  import { cn } from "../../util/cn.js";
13
14
  import { joinUrl } from "../../util/joinUrl.js";
14
15
  import { AnchorLink } from "../AnchorLink.js";
15
16
  import { useViewportAnchor } from "../context/ViewportAnchorContext.js";
17
+ import { useZudoku } from "../context/ZudokuContext.js";
16
18
  import { NavigationBadge } from "./NavigationBadge.js";
17
19
  import { NavigationCategory } from "./NavigationCategory.js";
18
- import { navigationListItem } from "./utils.js";
20
+ import { isHiddenItem, navigationListItem } from "./utils.js";
19
21
 
20
22
  const TruncatedLabel = ({
21
23
  label,
@@ -74,6 +76,12 @@ export const NavigationItem = ({
74
76
  }) => {
75
77
  const location = useLocation();
76
78
  const { activeAnchor } = useViewportAnchor();
79
+ const auth = useAuth();
80
+ const context = useZudoku();
81
+
82
+ if (!isHiddenItem(auth, context)(item)) {
83
+ return null;
84
+ }
77
85
 
78
86
  switch (item.type) {
79
87
  case "category":
@@ -4,6 +4,8 @@ import type {
4
4
  NavigationCategory,
5
5
  NavigationItem,
6
6
  } from "../../../config/validators/NavigationSchema.js";
7
+ import type { UseAuthReturn } from "../../authentication/hook.js";
8
+ import type { ZudokuContext } from "../../core/ZudokuContext.js";
7
9
  import { joinUrl } from "../../util/joinUrl.js";
8
10
  import { useCurrentNavigation } from "../context/ZudokuContext.js";
9
11
 
@@ -133,14 +135,18 @@ export const navigationListItem = cva(
133
135
  );
134
136
 
135
137
  export const isHiddenItem =
136
- (isAuthenticated?: boolean) =>
138
+ (auth: UseAuthReturn, context: ZudokuContext) =>
137
139
  (item: NavigationItem): boolean => {
140
+ if (typeof item.display === "function") {
141
+ return item.display({ context, auth });
142
+ }
143
+
138
144
  if (item.display === "hide") return false;
139
145
  if (!item.label) return false;
140
146
 
141
147
  return (
142
- (item.display === "auth" && isAuthenticated) ||
143
- (item.display === "anon" && !isAuthenticated) ||
148
+ (item.display === "auth" && auth.isAuthenticated) ||
149
+ (item.display === "anon" && !auth.isAuthenticated) ||
144
150
  !item.display ||
145
151
  item.display === "always"
146
152
  );
@@ -24,14 +24,18 @@ export const RouteGuard = () => {
24
24
  const location = useLocation();
25
25
  const latestPath = useLatest(location.pathname);
26
26
  const shouldBypass = use(BypassProtectedRoutesContext);
27
+ const { protectedRoutes } = zudoku.options;
27
28
 
28
- const { protectedRoutes = [] } = zudoku.options;
29
+ const authCheckFn =
30
+ !shouldBypass && protectedRoutes
31
+ ? Object.entries(protectedRoutes).find(([path]) =>
32
+ matchPath({ path, end: true }, location.pathname),
33
+ )?.[1]
34
+ : undefined;
29
35
 
30
- const isProtected =
31
- !shouldBypass &&
32
- protectedRoutes.some((path) =>
33
- matchPath({ path, end: true }, location.pathname),
34
- );
36
+ const isProtectedRoute = authCheckFn !== undefined;
37
+ const needsToSignIn =
38
+ isProtectedRoute && !authCheckFn({ auth, context: zudoku });
35
39
 
36
40
  useQuery({
37
41
  queryKey: ["login-redirect"],
@@ -42,14 +46,10 @@ export const RouteGuard = () => {
42
46
  });
43
47
  return true;
44
48
  },
45
- enabled:
46
- typeof window !== "undefined" &&
47
- isProtected &&
48
- !auth.isPending &&
49
- !auth.isAuthenticated,
49
+ enabled: typeof window !== "undefined" && needsToSignIn && !auth.isPending,
50
50
  });
51
51
 
52
- if (isProtected && !auth.isAuthenticated) {
52
+ if (needsToSignIn) {
53
53
  return (
54
54
  <Dialog
55
55
  open={true}
@@ -71,7 +71,7 @@ export const RouteGuard = () => {
71
71
  );
72
72
  }
73
73
 
74
- if (isProtected && !auth.isAuthEnabled) {
74
+ if (isProtectedRoute && !auth.isAuthEnabled) {
75
75
  throw new ZudokuError("Authentication is not enabled", {
76
76
  title: "Authentication is not enabled",
77
77
  developerHint:
@@ -5,6 +5,10 @@ import type { Location } from "react-router";
5
5
  import type { BundledTheme, HighlighterCore } from "shiki";
6
6
  import type { z } from "zod/v4";
7
7
  import type { Navigation } from "../../config/validators/NavigationSchema.js";
8
+ import {
9
+ type ProtectedRoutesInput,
10
+ ProtectedRoutesSchema,
11
+ } from "../../config/validators/ProtectedRoutesSchema.js";
8
12
  import type { FooterSchema } from "../../config/validators/validate.js";
9
13
  import type { AuthenticationPlugin } from "../authentication/authentication.js";
10
14
  import { type AuthState, useAuthState } from "../authentication/state.js";
@@ -94,7 +98,7 @@ export type ZudokuContextOptions = {
94
98
  components?: MdxComponentsType;
95
99
  };
96
100
  overrides?: ComponentsContextType;
97
- protectedRoutes?: string[];
101
+ protectedRoutes?: ProtectedRoutesInput;
98
102
  syntaxHighlighting?: {
99
103
  highlighter: HighlighterCore;
100
104
  themes?: { light: BundledTheme; dark: BundledTheme };
@@ -113,12 +117,21 @@ export class ZudokuContext {
113
117
  private emitter = createNanoEvents<ZudokuEvents>();
114
118
 
115
119
  constructor(options: ZudokuContextOptions, queryClient: QueryClient) {
116
- const protectedRoutes = (options.protectedRoutes ?? []).concat(
117
- options.plugins?.flatMap((plugin) =>
118
- isNavigationPlugin(plugin) ? (plugin.getProtectedRoutes?.() ?? []) : [],
119
- ) ?? [],
120
+ const pluginProtectedRoutes = Object.fromEntries(
121
+ (options.plugins ?? []).flatMap((plugin) => {
122
+ if (!isNavigationPlugin(plugin)) return [];
123
+ const routes = plugin.getProtectedRoutes?.();
124
+ if (!routes) return [];
125
+
126
+ return Object.entries(ProtectedRoutesSchema.parse(routes) ?? {});
127
+ }),
120
128
  );
121
129
 
130
+ const protectedRoutes = {
131
+ ...pluginProtectedRoutes,
132
+ ...ProtectedRoutesSchema.parse(options.protectedRoutes),
133
+ };
134
+
122
135
  this.queryClient = queryClient;
123
136
  this.options = { ...options, protectedRoutes };
124
137
  this.plugins = options.plugins ?? [];
@@ -2,6 +2,7 @@ import type { LucideIcon } from "lucide-react";
2
2
  import type { ReactElement } from "react";
3
3
  import type { Location, RouteObject } from "react-router";
4
4
  import type { Navigation } from "../../config/validators/NavigationSchema.js";
5
+ import type { ProtectedRoutesInput } from "../../config/validators/ProtectedRoutesSchema.js";
5
6
  import type { AuthenticationPlugin } from "../authentication/authentication.js";
6
7
  import type { MdxComponentsType } from "../util/MdxComponents.js";
7
8
  import type {
@@ -24,7 +25,7 @@ export type { AuthenticationPlugin, RouteObject };
24
25
  export interface NavigationPlugin {
25
26
  getRoutes: () => RouteObject[];
26
27
  getNavigation?: (path: string, context: ZudokuContext) => Promise<Navigation>;
27
- getProtectedRoutes?: () => string[];
28
+ getProtectedRoutes?: () => ProtectedRoutesInput;
28
29
  }
29
30
 
30
31
  export const createApiIdentityPlugin = (
@@ -2,6 +2,7 @@ import { useMutation, useQueryClient } from "@tanstack/react-query";
2
2
  import { useForm } from "react-hook-form";
3
3
  import { useNavigate } from "react-router";
4
4
  import { ActionButton } from "zudoku/ui/ActionButton.js";
5
+ import { Alert, AlertDescription, AlertTitle } from "zudoku/ui/Alert.js";
5
6
  import { DialogClose, DialogFooter } from "zudoku/ui/Dialog.js";
6
7
  import {
7
8
  Select,
@@ -33,6 +34,7 @@ export const CreateApiKey = ({
33
34
  expiresOn: "30",
34
35
  },
35
36
  });
37
+
36
38
  const createKeyMutation = useMutation({
37
39
  mutationFn: ({ description, expiresOn }: CreateApiKey) => {
38
40
  if (!service.createKey) {
@@ -43,7 +45,10 @@ export const CreateApiKey = ({
43
45
  expiresOn !== "never" ? addDaysToDate(Number(expiresOn)) : undefined;
44
46
 
45
47
  return service.createKey(
46
- { description: description || "Secret Key", expiresOn: expiresOnDate },
48
+ {
49
+ description: description || "Secret Key",
50
+ expiresOn: expiresOnDate,
51
+ },
47
52
  context,
48
53
  );
49
54
  },
@@ -68,6 +73,12 @@ export const CreateApiKey = ({
68
73
  ),
69
74
  )}
70
75
  >
76
+ {createKeyMutation.error && (
77
+ <Alert variant="destructive" className="mb-4">
78
+ <AlertTitle>Error</AlertTitle>
79
+ <AlertDescription>{createKeyMutation.error.message}</AlertDescription>
80
+ </Alert>
81
+ )}
71
82
  <div className="flex gap-2 flex-col text-sm font-medium">
72
83
  Name
73
84
  <Input {...form.register("description")} />
@@ -35,6 +35,7 @@ import { Button } from "../../ui/Button.js";
35
35
  import { Input } from "../../ui/Input.js";
36
36
  import { cn } from "../../util/cn.js";
37
37
  import { useCopyToClipboard } from "../../util/useCopyToClipboard.js";
38
+ import { CreateApiKey } from "./CreateApiKey.js";
38
39
  import { type ApiConsumer, type ApiKey, type ApiKeyService } from "./index.js";
39
40
 
40
41
  export const SettingsApiKeys = ({ service }: { service: ApiKeyService }) => {
@@ -50,6 +51,8 @@ export const SettingsApiKeys = ({ service }: { service: ApiKeyService }) => {
50
51
  retry: false,
51
52
  });
52
53
 
54
+ const [isCreateApiKeyOpen, setIsCreateApiKeyOpen] = useState(false);
55
+
53
56
  const deleteKeyMutation = useMutation({
54
57
  mutationFn: ({
55
58
  consumerId,
@@ -180,11 +183,28 @@ export const SettingsApiKeys = ({ service }: { service: ApiKeyService }) => {
180
183
  <Slot.Target name="api-keys-list-page" />
181
184
 
182
185
  <div className="flex justify-between pb-3">
183
- <h1 className="font-medium text-2xl">API Keys</h1>
186
+ <h1 className="font-medium text-2xl">
187
+ API Keys {typeof service.createKey}
188
+ </h1>
189
+
184
190
  {service.createKey && (
185
- <Button asChild>
186
- <Link to="/settings/api-keys/new">Create API Key</Link>
187
- </Button>
191
+ <Dialog
192
+ open={isCreateApiKeyOpen}
193
+ onOpenChange={setIsCreateApiKeyOpen}
194
+ >
195
+ <DialogTrigger asChild>
196
+ <Button variant="outline">Create API Key</Button>
197
+ </DialogTrigger>
198
+ <DialogContent>
199
+ <DialogHeader>
200
+ <DialogTitle>Create API Key</DialogTitle>
201
+ </DialogHeader>
202
+ <CreateApiKey
203
+ service={service}
204
+ onOpenChange={setIsCreateApiKeyOpen}
205
+ />
206
+ </DialogContent>
207
+ </Dialog>
188
208
  )}
189
209
  </div>
190
210
  <p>Create, manage, and monitor your API keys</p>
@@ -27,7 +27,7 @@ export type ApiKeyService = {
27
27
  ) => Promise<void>;
28
28
  getUsage?: (apiKeys: string[], context: ZudokuContext) => Promise<void>;
29
29
  createKey?: (
30
- apiKey: { description: string; expiresOn?: string },
30
+ apiKey: { description: string; expiresOn?: string; expiresValue?: string },
31
31
  context: ZudokuContext,
32
32
  ) => Promise<void>;
33
33
  };
@@ -74,7 +74,10 @@ const throwIfProblemJson = async (response: Response) => {
74
74
  }
75
75
  };
76
76
 
77
- const createDefaultHandler = (deploymentName: string): ApiKeyService => {
77
+ const createDefaultHandler = (
78
+ deploymentName: string,
79
+ options: ApiKeyPluginOptions,
80
+ ): ApiKeyService => {
78
81
  return {
79
82
  deleteKey: async (consumerId, keyId, context) => {
80
83
  const request = new Request(
@@ -159,6 +162,7 @@ const createDefaultHandler = (deploymentName: string): ApiKeyService => {
159
162
  key: consumer.apiKeys.data.at(0),
160
163
  }));
161
164
  },
165
+ ...options,
162
166
  };
163
167
  };
164
168
 
@@ -170,7 +174,7 @@ export const apiKeyPlugin = (
170
174
  ): ZudokuPlugin & ApiIdentityPlugin & ProfileMenuPlugin => {
171
175
  const service: ApiKeyService =
172
176
  "deploymentName" in options
173
- ? createDefaultHandler(options.deploymentName)
177
+ ? createDefaultHandler(options.deploymentName, options)
174
178
  : options;
175
179
 
176
180
  return {
@@ -203,7 +207,6 @@ export const apiKeyPlugin = (
203
207
  }
204
208
  },
205
209
  getRoutes: (): RouteObject[] => {
206
- // TODO: Make lazy
207
210
  return [
208
211
  {
209
212
  element: <ProtectedRoute />,
@@ -213,10 +216,6 @@ export const apiKeyPlugin = (
213
216
  path: "/settings/api-keys",
214
217
  element: <SettingsApiKeys service={service} />,
215
218
  },
216
- // {
217
- // path: "/settings/api-keys/new",
218
- // element: <CreateApiKey service={service} />,
219
- // },
220
219
  ],
221
220
  },
222
221
  ];
@@ -19,7 +19,7 @@ type Documents = {
19
19
  "\n query OperationsForTag(\n $input: JSON!\n $type: SchemaType!\n $tag: String\n $untagged: Boolean\n ) {\n schema(input: $input, type: $type) {\n servers {\n url\n }\n description\n summary\n title\n url\n version\n tag(slug: $tag, untagged: $untagged) {\n name\n description\n operations {\n slug\n ...OperationsFragment\n }\n next {\n name\n slug\n }\n prev {\n name\n slug\n }\n }\n }\n }\n": typeof types.OperationsForTagDocument;
20
20
  "\n query GetSchemas($input: JSON!, $type: SchemaType!) {\n schema(input: $input, type: $type) {\n title\n description\n summary\n components {\n schemas {\n name\n schema\n extensions\n }\n }\n }\n }\n": typeof types.GetSchemasDocument;
21
21
  "\n query getServerQuery($input: JSON!, $type: SchemaType!) {\n schema(input: $input, type: $type) {\n url\n servers {\n url\n }\n }\n }\n": typeof types.GetServerQueryDocument;
22
- "\n query GetNavigationOperations($input: JSON!, $type: SchemaType!) {\n schema(input: $input, type: $type) {\n tags {\n slug\n name\n extensions\n operations {\n summary\n slug\n method\n operationId\n path\n }\n }\n components {\n schemas {\n __typename\n }\n }\n }\n }\n": typeof types.GetNavigationOperationsDocument;
22
+ "\n query GetNavigationOperations($input: JSON!, $type: SchemaType!) {\n schema(input: $input, type: $type) {\n extensions\n tags {\n slug\n name\n extensions\n operations {\n summary\n slug\n method\n operationId\n path\n }\n }\n components {\n schemas {\n __typename\n }\n }\n }\n }\n": typeof types.GetNavigationOperationsDocument;
23
23
  };
24
24
  const documents: Documents = {
25
25
  "\n query ServersQuery($input: JSON!, $type: SchemaType!) {\n schema(input: $input, type: $type) {\n url\n servers {\n url\n }\n }\n }\n":
@@ -34,7 +34,7 @@ const documents: Documents = {
34
34
  types.GetSchemasDocument,
35
35
  "\n query getServerQuery($input: JSON!, $type: SchemaType!) {\n schema(input: $input, type: $type) {\n url\n servers {\n url\n }\n }\n }\n":
36
36
  types.GetServerQueryDocument,
37
- "\n query GetNavigationOperations($input: JSON!, $type: SchemaType!) {\n schema(input: $input, type: $type) {\n tags {\n slug\n name\n extensions\n operations {\n summary\n slug\n method\n operationId\n path\n }\n }\n components {\n schemas {\n __typename\n }\n }\n }\n }\n":
37
+ "\n query GetNavigationOperations($input: JSON!, $type: SchemaType!) {\n schema(input: $input, type: $type) {\n extensions\n tags {\n slug\n name\n extensions\n operations {\n summary\n slug\n method\n operationId\n path\n }\n }\n components {\n schemas {\n __typename\n }\n }\n }\n }\n":
38
38
  types.GetNavigationOperationsDocument,
39
39
  };
40
40
 
@@ -78,7 +78,7 @@ export function graphql(
78
78
  * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
79
79
  */
80
80
  export function graphql(
81
- source: "\n query GetNavigationOperations($input: JSON!, $type: SchemaType!) {\n schema(input: $input, type: $type) {\n tags {\n slug\n name\n extensions\n operations {\n summary\n slug\n method\n operationId\n path\n }\n }\n components {\n schemas {\n __typename\n }\n }\n }\n }\n",
81
+ source: "\n query GetNavigationOperations($input: JSON!, $type: SchemaType!) {\n schema(input: $input, type: $type) {\n extensions\n tags {\n slug\n name\n extensions\n operations {\n summary\n slug\n method\n operationId\n path\n }\n }\n components {\n schemas {\n __typename\n }\n }\n }\n }\n",
82
82
  ): typeof import("./graphql.js").GetNavigationOperationsDocument;
83
83
 
84
84
  export function graphql(source: string) {
@@ -383,6 +383,7 @@ export type GetNavigationOperationsQuery = {
383
383
  __typename?: "Query";
384
384
  schema: {
385
385
  __typename?: "Schema";
386
+ extensions?: any | null;
386
387
  tags: Array<{
387
388
  __typename?: "SchemaTag";
388
389
  slug?: string | null;
@@ -645,6 +646,7 @@ export const GetServerQueryDocument = new TypedDocumentString(`
645
646
  export const GetNavigationOperationsDocument = new TypedDocumentString(`
646
647
  query GetNavigationOperations($input: JSON!, $type: SchemaType!) {
647
648
  schema(input: $input, type: $type) {
649
+ extensions
648
650
  tags {
649
651
  slug
650
652
  name
@@ -1,6 +1,7 @@
1
1
  import { CirclePlayIcon, LogInIcon } from "lucide-react";
2
2
  import { type ReactNode } from "react";
3
3
  import { matchPath } from "react-router";
4
+ import type { NavigationItem } from "../../../config/validators/NavigationSchema.js";
4
5
  import { useAuth } from "../../authentication/hook.js";
5
6
  import { type ZudokuPlugin } from "../../core/plugins.js";
6
7
  import { Button } from "../../ui/Button.js";
@@ -18,6 +19,7 @@ import { getRoutes, getVersions } from "./util/getRoutes.js";
18
19
  export const GetNavigationOperationsQuery = graphql(`
19
20
  query GetNavigationOperations($input: JSON!, $type: SchemaType!) {
20
21
  schema(input: $input, type: $type) {
22
+ extensions
21
23
  tags {
22
24
  slug
23
25
  name
@@ -139,25 +141,73 @@ export const openApiPlugin = (config: OasPluginConfig): ZudokuPlugin => {
139
141
  });
140
142
  const data = await context.queryClient.ensureQueryData(query);
141
143
 
142
- const categories = data.schema.tags.flatMap((tag) => {
143
- if (!tag.name || tag.operations.length === 0) return [];
144
+ const tagCategories = new Map(
145
+ data.schema.tags
146
+ .filter((tag) => tag.name && tag.operations.length > 0)
147
+ .map((tag) => {
148
+ if (!tag.name) {
149
+ throw new Error(`Tag ${tag.slug} has no name`);
150
+ }
151
+
152
+ const categoryPath = joinUrl(basePath, versionParam, tag.slug);
153
+
154
+ const isCollapsed =
155
+ tag.extensions?.["x-zudoku-collapsed"] ??
156
+ !config.options?.expandAllTags;
157
+ const isCollapsible =
158
+ tag.extensions?.["x-zudoku-collapsible"] ?? true;
159
+
160
+ return [
161
+ tag.name,
162
+ createNavigationCategory({
163
+ label: tag.name,
164
+ path: categoryPath,
165
+ operations: tag.operations,
166
+ collapsed: isCollapsed,
167
+ collapsible: isCollapsible,
168
+ }),
169
+ ];
170
+ }),
171
+ );
144
172
 
145
- const categoryPath = joinUrl(basePath, versionParam, tag.slug);
173
+ const tagGroups =
174
+ (data.schema.extensions?.["x-tagGroups"] as
175
+ | { name: string; tags: string[] }[]
176
+ | undefined) ?? [];
146
177
 
147
- const isCollapsed =
148
- tag.extensions?.["x-zudoku-collapsed"] ??
149
- !config.options?.expandAllTags;
150
- const isCollapsible =
151
- tag.extensions?.["x-zudoku-collapsible"] ?? true;
178
+ const groupedTags = new Set(
179
+ tagGroups.flatMap((group) =>
180
+ group.tags.filter((name) => tagCategories.has(name)),
181
+ ),
182
+ );
152
183
 
153
- return createNavigationCategory({
154
- label: tag.name,
155
- path: categoryPath,
156
- operations: tag.operations,
157
- collapsed: isCollapsed,
158
- collapsible: isCollapsible,
159
- });
160
- });
184
+ const groupedCategories: NavigationItem[] = tagGroups.flatMap(
185
+ (group) => {
186
+ const items = group.tags
187
+ .map((name) => tagCategories.get(name))
188
+ .filter(Boolean) as NavigationItem[];
189
+
190
+ if (items.length === 0) {
191
+ return [];
192
+ }
193
+ return [
194
+ {
195
+ type: "category",
196
+ label: group.name,
197
+ items,
198
+ collapsible: true,
199
+ collapsed: !config.options?.expandAllTags,
200
+ },
201
+ ];
202
+ },
203
+ );
204
+
205
+ const categories: NavigationItem[] = [
206
+ ...groupedCategories,
207
+ ...Array.from(tagCategories.entries())
208
+ .filter(([name]) => !groupedTags.has(name))
209
+ .map(([, cat]) => cat),
210
+ ];
161
211
 
162
212
  const untaggedOperations = data.schema.tags.find(
163
213
  (tag) => !tag.name,
@@ -19,7 +19,9 @@ export const ActionButton = forwardRef<HTMLButtonElement, ActionButtonProps>(
19
19
  <Spinner />
20
20
  </div>
21
21
  )}
22
- <div className={cn(isPending && "invisible")}>{children}</div>
22
+ <span className={cn("block", isPending && "invisible")}>
23
+ {children}
24
+ </span>
23
25
  </Button>
24
26
  );
25
27
  },
@@ -18,17 +18,19 @@ export default function invariant(
18
18
  throw new ZudokuError(provided ?? "Invariant failed");
19
19
  }
20
20
 
21
+ export type ZudokuErrorOptions = {
22
+ developerHint?: string;
23
+ title?: string;
24
+ cause?: Error;
25
+ };
26
+
21
27
  export class ZudokuError extends Error {
22
28
  public developerHint: string | undefined;
23
29
  public title: string | undefined;
24
30
 
25
31
  constructor(
26
32
  message: string,
27
- {
28
- developerHint,
29
- title,
30
- cause,
31
- }: { developerHint?: string; title?: string; cause?: Error } = {},
33
+ { developerHint, title, cause }: ZudokuErrorOptions = {},
32
34
  ) {
33
35
  super(message, { cause });
34
36
  this.name = "ZudokuError";