zudoku 0.1.1-dev.50 → 0.1.1-dev.52

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 (68) hide show
  1. package/dist/config/config.d.ts +19 -2
  2. package/dist/config/validators/auth.d.ts +2 -0
  3. package/dist/config/validators/auth.js +2 -0
  4. package/dist/config/validators/auth.js.map +1 -0
  5. package/dist/config/validators/validate.d.ts +2 -0
  6. package/dist/config/validators/validate.js +4 -0
  7. package/dist/config/validators/validate.js.map +1 -0
  8. package/dist/lib/authentication/Callback.d.ts +4 -0
  9. package/dist/lib/authentication/Callback.js +20 -0
  10. package/dist/lib/authentication/Callback.js.map +1 -0
  11. package/dist/lib/authentication/auth0.d.ts +5 -0
  12. package/dist/lib/authentication/auth0.js +9 -0
  13. package/dist/lib/authentication/auth0.js.map +1 -0
  14. package/dist/lib/authentication/authentication.d.ts +7 -10
  15. package/dist/lib/authentication/clerk.d.ts +3 -4
  16. package/dist/lib/authentication/clerk.js +13 -6
  17. package/dist/lib/authentication/clerk.js.map +1 -1
  18. package/dist/lib/authentication/openid.d.ts +5 -11
  19. package/dist/lib/authentication/openid.js +90 -73
  20. package/dist/lib/authentication/openid.js.map +1 -1
  21. package/dist/lib/components/DevPortal.d.ts +2 -2
  22. package/dist/lib/components/DevPortal.js +5 -1
  23. package/dist/lib/components/DevPortal.js.map +1 -1
  24. package/dist/lib/components/Layout.js +1 -4
  25. package/dist/lib/components/Layout.js.map +1 -1
  26. package/dist/lib/core/DevPortalContext.d.ts +4 -7
  27. package/dist/lib/core/DevPortalContext.js +6 -6
  28. package/dist/lib/core/DevPortalContext.js.map +1 -1
  29. package/dist/lib/core/plugins.d.ts +1 -6
  30. package/dist/lib/plugins/api-key/index.js +6 -3
  31. package/dist/lib/plugins/api-key/index.js.map +1 -1
  32. package/dist/lib/plugins/openapi/MakeRequest.js +9 -4
  33. package/dist/lib/plugins/openapi/MakeRequest.js.map +1 -1
  34. package/dist/lib/plugins/openapi/playground/Playground.d.ts +2 -1
  35. package/dist/lib/plugins/openapi/playground/Playground.js +1 -3
  36. package/dist/lib/plugins/openapi/playground/Playground.js.map +1 -1
  37. package/dist/lib/plugins/openapi/playground/QueryParams.js +7 -19
  38. package/dist/lib/plugins/openapi/playground/QueryParams.js.map +1 -1
  39. package/dist/vite/config.js +2 -0
  40. package/dist/vite/config.js.map +1 -1
  41. package/dist/vite/plugin-auth.js +1 -1
  42. package/dist/vite/plugin-auth.js.map +1 -1
  43. package/lib/DevPortalProvider-Dn9HNUG9.js +4559 -0
  44. package/lib/Spinner-D8DBhJkj.js +7329 -0
  45. package/lib/zudoku.auth-auth0.js +976 -0
  46. package/lib/zudoku.auth-clerk.js +21 -12
  47. package/lib/zudoku.components.js +180 -174
  48. package/lib/zudoku.plugins.js +6823 -6971
  49. package/package.json +4 -1
  50. package/src/lib/authentication/Callback.tsx +31 -0
  51. package/src/lib/authentication/auth0.tsx +18 -0
  52. package/src/lib/authentication/authentication.ts +7 -14
  53. package/src/lib/authentication/{clerk.ts → clerk.tsx} +17 -9
  54. package/src/lib/authentication/openid.tsx +206 -0
  55. package/src/lib/components/DevPortal.tsx +10 -3
  56. package/src/lib/components/Layout.tsx +1 -5
  57. package/src/lib/core/DevPortalContext.ts +10 -13
  58. package/src/lib/core/plugins.ts +4 -4
  59. package/src/lib/plugins/api-key/index.tsx +9 -3
  60. package/src/lib/plugins/openapi/MakeRequest.tsx +9 -4
  61. package/src/lib/plugins/openapi/playground/Playground.tsx +3 -4
  62. package/src/lib/plugins/openapi/playground/QueryParams.tsx +19 -39
  63. package/dist/lib/core/types/combine.d.ts +0 -4
  64. package/dist/lib/core/types/combine.js +0 -2
  65. package/dist/lib/core/types/combine.js.map +0 -1
  66. package/lib/Spinner-9_-7nYgL.js +0 -11855
  67. package/src/lib/authentication/openid.ts +0 -194
  68. package/src/lib/core/types/combine.ts +0 -16
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "zudoku",
3
- "version": "0.1.1-dev.50",
3
+ "version": "0.1.1-dev.52",
4
4
  "type": "module",
5
5
  "files": [
6
6
  "dist",
@@ -21,6 +21,9 @@
21
21
  "./auth/clerk": {
22
22
  "import": "./lib/zudoku.auth-clerk.js"
23
23
  },
24
+ "./auth/auth0": {
25
+ "import": "./lib/zudoku.auth-auth0.js"
26
+ },
24
27
  "./plugins": {
25
28
  "import": "./lib/zudoku.plugins.js"
26
29
  },
@@ -0,0 +1,31 @@
1
+ import { useQuery } from "@tanstack/react-query";
2
+ import { Navigate } from "react-router-dom";
3
+ import { useDevPortal } from "../components/context/DevPortalProvider.js";
4
+ import {
5
+ DevPortalContext,
6
+ SessionProfileState,
7
+ } from "../core/DevPortalContext.js";
8
+
9
+ export function Callback({
10
+ handleCallback,
11
+ }: {
12
+ handleCallback: (
13
+ url: URL,
14
+ context: DevPortalContext,
15
+ ) => Promise<SessionProfileState>;
16
+ }) {
17
+ const context = useDevPortal();
18
+ const callback = useQuery({
19
+ queryFn: () => handleCallback(new URL(window.location.href), context),
20
+ retry: 0,
21
+ queryKey: ["auth-callback"],
22
+ });
23
+
24
+ if (callback.isPending) {
25
+ return <div>Loading...</div>;
26
+ } else if (callback.error) {
27
+ return <div>Error: {JSON.stringify(callback.error)}</div>;
28
+ }
29
+
30
+ return <Navigate to="/" replace={true} />;
31
+ }
@@ -0,0 +1,18 @@
1
+ import { Auth0AuthenticationConfig } from "../../config/config.js";
2
+ import { NavigationPlugin } from "../core/plugins.js";
3
+ import { AuthenticationProvider } from "./authentication.js";
4
+ import openIdAuth from "./openid.js";
5
+
6
+ const auth0Provider = ({
7
+ domain,
8
+ clientId,
9
+ audience,
10
+ }: Auth0AuthenticationConfig): AuthenticationProvider & NavigationPlugin =>
11
+ openIdAuth({
12
+ type: "openid",
13
+ issuer: `https://${domain}`,
14
+ clientId: clientId,
15
+ audience: audience,
16
+ });
17
+
18
+ export default auth0Provider;
@@ -1,18 +1,11 @@
1
- import { DevPortalContext } from "../core/DevPortalContext.js";
1
+ import { DevPortalContext } from "../../lib/core/DevPortalContext.js";
2
+ import { NavigationPlugin } from "../../lib/core/plugins.js";
2
3
 
3
- export interface AuthProvider {
4
- initialize(context: DevPortalContext): Promise<unknown>;
4
+ export interface AuthenticationProvider extends NavigationPlugin {
5
+ initialize?(context: DevPortalContext): Promise<unknown>;
5
6
  login(context: DevPortalContext): Promise<unknown>;
7
+ signRequest(request: Request, context: DevPortalContext): Promise<Request>;
8
+ /** @deprecated **/
6
9
  getToken?: (context: DevPortalContext) => Promise<string | undefined>;
7
- handleAuthenticationResponse?: (
8
- path: Path,
9
- context: DevPortalContext,
10
- ) => Promise<unknown>;
11
- signOut(context: DevPortalContext): void;
10
+ logout(context: DevPortalContext): void;
12
11
  }
13
-
14
- export type Path = {
15
- pathname: string;
16
- search: string;
17
- hash: string;
18
- };
@@ -1,10 +1,13 @@
1
1
  import type { Clerk } from "@clerk/clerk-js";
2
- import { type AuthProvider } from "./authentication.js";
2
+ import { ClerkAuthenticationConfig } from "../../config/config.js";
3
+ import { AuthenticationProvider } from "./authentication.js";
3
4
 
4
- const clerkAuth = ({ clerkPubKey }: { clerkPubKey: string }): AuthProvider => {
5
+ const clerkAuth = ({
6
+ clerkPubKey,
7
+ }: ClerkAuthenticationConfig): AuthenticationProvider => {
5
8
  let clerkApi: Clerk;
6
9
 
7
- const clerkIsLoaded = (async () => {
10
+ const ensureLoaded = (async () => {
8
11
  const { Clerk } = await import("@clerk/clerk-js");
9
12
  clerkApi = new Clerk(clerkPubKey);
10
13
 
@@ -13,7 +16,7 @@ const clerkAuth = ({ clerkPubKey }: { clerkPubKey: string }): AuthProvider => {
13
16
 
14
17
  return {
15
18
  initialize: async (context) => {
16
- await clerkIsLoaded;
19
+ await ensureLoaded;
17
20
 
18
21
  if (clerkApi.session) {
19
22
  await context.setUserProfile({
@@ -29,19 +32,24 @@ const clerkAuth = ({ clerkPubKey }: { clerkPubKey: string }): AuthProvider => {
29
32
  });
30
33
  }
31
34
  },
32
-
35
+ async signRequest(request: Request): Promise<Request> {
36
+ await ensureLoaded;
37
+ const token = await clerkApi.session?.getToken();
38
+ request.headers.set("Authorization", `Bearer ${token}`);
39
+ return request;
40
+ },
33
41
  getToken: async () => {
34
- await clerkIsLoaded;
42
+ await ensureLoaded;
35
43
  const token = await clerkApi.session?.getToken();
36
44
  return token ?? undefined;
37
45
  },
38
-
39
- signOut() {
40
- clerkApi.signOut();
46
+ logout: async () => {
47
+ await clerkApi.signOut();
41
48
  },
42
49
  login: async () => {
43
50
  await clerkApi.redirectToSignIn();
44
51
  },
52
+ getRoutes: () => [],
45
53
  };
46
54
  };
47
55
 
@@ -0,0 +1,206 @@
1
+ import logger from "loglevel";
2
+ import * as oauth from "oauth4webapi";
3
+ import { OpenIDAuthenticationConfig } from "../../config/config.js";
4
+ import { DevPortalContext } from "../core/DevPortalContext.js";
5
+ import { NavigationPlugin } from "../core/plugins.js";
6
+ import { AuthenticationProvider } from "./authentication.js";
7
+ import { Callback } from "./Callback.js";
8
+
9
+ const CALLBACK_URL_PATH = "/oauth/callback";
10
+
11
+ async function getAuthServerByIssuer(issuer: URL | string) {
12
+ const issuerUrl = typeof issuer === "string" ? new URL(issuer) : issuer;
13
+ const response = await oauth.discoveryRequest(issuerUrl);
14
+ const server = await oauth.processDiscoveryResponse(issuerUrl, response);
15
+ return server;
16
+ }
17
+
18
+ async function getAuthServer({
19
+ issuer,
20
+ authorizationEndpoint,
21
+ tokenEndpoint,
22
+ }: Pick<
23
+ OpenIDAuthenticationConfig,
24
+ "issuer" | "authorizationEndpoint" | "tokenEndpoint"
25
+ >) {
26
+ return await getAuthServerByIssuer(issuer);
27
+ // : ({
28
+ // issuer: new URL(authorizationEndpoint!).origin,
29
+ // authorization_endpoint: authorizationEndpoint,
30
+ // token_endpoint: tokenEndpoint,
31
+ // code_challenge_methods_supported: [],
32
+ // } satisfies oauth.AuthorizationServer);
33
+ }
34
+
35
+ const openIdAuth = ({
36
+ issuer,
37
+ authorizationEndpoint,
38
+ tokenEndpoint,
39
+ clientId,
40
+ }: OpenIDAuthenticationConfig): AuthenticationProvider & NavigationPlugin => {
41
+ const client: oauth.Client = {
42
+ client_id: clientId,
43
+ token_endpoint_auth_method: "none",
44
+ };
45
+
46
+ async function handleCallback(url: URL, context: DevPortalContext) {
47
+ const searchParams = url.searchParams;
48
+ const state = searchParams.get("state");
49
+
50
+ // one eternity later, the user lands back on the redirect_uri
51
+ // Authorization Code Grant Request & Response
52
+ const codeVerifier = await context.sessionStorage.get("codeVerifier");
53
+
54
+ if (!codeVerifier) {
55
+ return {
56
+ isLoggedIn: false,
57
+ };
58
+ }
59
+
60
+ const authServer = await getAuthServer({
61
+ issuer: issuer,
62
+ authorizationEndpoint: authorizationEndpoint,
63
+ tokenEndpoint: tokenEndpoint,
64
+ });
65
+
66
+ const params = oauth.validateAuthResponse(
67
+ authServer,
68
+ client,
69
+ searchParams,
70
+ state ?? undefined,
71
+ );
72
+ if (oauth.isOAuth2Error(params)) {
73
+ logger.error("Error Response", params);
74
+ throw new Error(); // Handle OAuth 2.0 redirect error
75
+ }
76
+
77
+ const redirectUrl = new URL(url);
78
+ redirectUrl.pathname = CALLBACK_URL_PATH;
79
+ redirectUrl.search = "";
80
+
81
+ const response = await oauth.authorizationCodeGrantRequest(
82
+ authServer,
83
+ client,
84
+ params,
85
+ redirectUrl.toString(),
86
+ codeVerifier,
87
+ );
88
+
89
+ // @todo do we need to do these
90
+ // const challenges = oauth.parseWwwAuthenticateChallenges(response);
91
+ // if (challenges) {
92
+ // for (const challenge of challenges) {
93
+ // console.error("WWW-Authenticate Challenge", challenge);
94
+ // }
95
+ // throw new Error(); // Handle WWW-Authenticate Challenges as needed
96
+ // }
97
+ const oauthResult = await oauth.processAuthorizationCodeOAuth2Response(
98
+ authServer,
99
+ client,
100
+ response,
101
+ );
102
+
103
+ if (oauth.isOAuth2Error(oauthResult)) {
104
+ logger.error("Error Response", oauthResult);
105
+ throw new Error(oauthResult.error);
106
+ }
107
+
108
+ const userInfoResponse = await oauth.userInfoRequest(
109
+ authServer,
110
+ client,
111
+ oauthResult.access_token,
112
+ );
113
+ const userInfo = await userInfoResponse.json();
114
+
115
+ const profile = {
116
+ sub: userInfo.sub,
117
+ email: userInfo.email,
118
+ name: userInfo.name,
119
+ email_verified: userInfo.email_verified ?? false,
120
+ picture: userInfo.picture,
121
+ isLoggedIn: true,
122
+ };
123
+ void context.setUserProfile(profile);
124
+
125
+ return profile;
126
+ }
127
+
128
+ return {
129
+ logout: async (context) => {
130
+ await context.setUserProfile({ isLoggedIn: false });
131
+ },
132
+ login: async (context) => {
133
+ const code_challenge_method = "S256";
134
+ const authorizationServer = await getAuthServer({
135
+ issuer: issuer,
136
+ authorizationEndpoint,
137
+ tokenEndpoint,
138
+ });
139
+
140
+ if (!authorizationServer.authorization_endpoint) {
141
+ throw new Error("No authorization endpoint");
142
+ }
143
+
144
+ /**
145
+ * The following MUST be generated for every redirect to the authorization_endpoint. You must store
146
+ * the codeVerifier and nonce in the end-user session such that it can be recovered as the user
147
+ * gets redirected from the authorization server back to your application.
148
+ */
149
+ const codeVerifier = oauth.generateRandomCodeVerifier();
150
+ const codeChallenge =
151
+ await oauth.calculatePKCECodeChallenge(codeVerifier);
152
+
153
+ await context.sessionStorage.set("codeVerifier", codeVerifier);
154
+
155
+ // redirect user to as.authorization_endpoint
156
+ const authorizationUrl = new URL(
157
+ authorizationServer.authorization_endpoint,
158
+ );
159
+
160
+ const redirectUrl = new URL(context.url);
161
+ redirectUrl.pathname = CALLBACK_URL_PATH;
162
+ redirectUrl.search = "";
163
+
164
+ authorizationUrl.searchParams.set("client_id", client.client_id);
165
+ authorizationUrl.searchParams.set("redirect_uri", redirectUrl.toString());
166
+ authorizationUrl.searchParams.set("response_type", "code");
167
+ authorizationUrl.searchParams.set("scope", "openid+profile+email");
168
+ authorizationUrl.searchParams.set("code_challenge", codeChallenge);
169
+ authorizationUrl.searchParams.set(
170
+ "code_challenge_method",
171
+ code_challenge_method,
172
+ );
173
+
174
+ /**
175
+ * We cannot be sure the AS supports PKCE so we're going to use state too. Use of PKCE is
176
+ * backwards compatible even if the AS doesn't support it which is why we're using it regardless.
177
+ */
178
+ if (
179
+ authorizationServer.code_challenge_methods_supported?.includes(
180
+ "S256",
181
+ ) !== true
182
+ ) {
183
+ const state = oauth.generateRandomState();
184
+ authorizationUrl.searchParams.set("state", state);
185
+ }
186
+
187
+ // now redirect the user to authorizationUrl.href
188
+ location.href = authorizationUrl.href;
189
+ },
190
+ signRequest(request: Request, context: DevPortalContext): Promise<Request> {
191
+ // check if token is expired
192
+
193
+ return Promise.resolve(request);
194
+ },
195
+ getRoutes: () => {
196
+ return [
197
+ {
198
+ path: CALLBACK_URL_PATH,
199
+ element: <Callback handleCallback={handleCallback} />,
200
+ },
201
+ ];
202
+ },
203
+ } satisfies AuthenticationProvider & NavigationPlugin;
204
+ };
205
+
206
+ export default openIdAuth;
@@ -1,7 +1,7 @@
1
+ /* eslint-disable react/destructuring-assignment */
1
2
  import { MDXProvider } from "@mdx-js/react";
2
3
  import { QueryClientProvider } from "@tanstack/react-query";
3
4
  import { memo, Suspense, useEffect, useMemo } from "react";
4
- import { type AuthProvider } from "../authentication/authentication.js";
5
5
  import {
6
6
  DevPortalContext,
7
7
  queryClient,
@@ -9,6 +9,8 @@ import {
9
9
  } from "../core/DevPortalContext.js";
10
10
  import { HelmetProvider } from "../core/helmet.js";
11
11
  import { type DevPortalPlugin } from "../core/plugins.js";
12
+
13
+ import { AuthenticationProvider } from "../authentication/authentication.js";
12
14
  import {
13
15
  MdxComponents,
14
16
  type MdxComponentsType,
@@ -39,7 +41,7 @@ export type DevPortalProps = {
39
41
  logo: string;
40
42
  favicon: string;
41
43
  }>;
42
- authentication?: AuthProvider;
44
+ authentication?: AuthenticationProvider;
43
45
  navigation: NavigationItem[];
44
46
  plugins?: DevPortalPlugin[];
45
47
  mdxComponents?: MdxComponentsType;
@@ -78,7 +80,12 @@ const DevPortalInner = (props: DevPortalProps) => {
78
80
  </div>
79
81
  }
80
82
  >
81
- <Router plugins={props.plugins} />
83
+ <Router
84
+ plugins={[
85
+ ...(props.plugins ?? []),
86
+ ...(props.authentication ? [props.authentication] : []),
87
+ ]}
88
+ />
82
89
  </Suspense>
83
90
  </ViewportAnchorProvider>
84
91
  </ComponentsProvider>
@@ -12,11 +12,7 @@ import { Spinner } from "./Spinner.js";
12
12
  export const Layout = ({ children }: { children?: ReactNode }) => {
13
13
  const location = useLocation();
14
14
  const { setActiveAnchor } = useViewportAnchor();
15
- const { meta, handleAuthenticationResponse } = useDevPortal();
16
-
17
- useEffect(() => {
18
- void handleAuthenticationResponse(location);
19
- }, [handleAuthenticationResponse, location]);
15
+ const { meta } = useDevPortal();
20
16
 
21
17
  useScrollToAnchor();
22
18
  useScrollToTop();
@@ -1,7 +1,7 @@
1
1
  import { QueryClient } from "@tanstack/react-query";
2
2
  import { type ReactNode } from "react";
3
3
  import { create } from "zustand";
4
- import { type AuthProvider } from "../authentication/authentication.js";
4
+ import { type AuthenticationProvider } from "../authentication/authentication.js";
5
5
  import {
6
6
  type DevPortalPath,
7
7
  type DevPortalProps,
@@ -62,6 +62,7 @@ export type SessionProfileState = {
62
62
  name?: string;
63
63
  email_verified?: string;
64
64
  picture?: string;
65
+ [key: string]: string | boolean | undefined;
65
66
  };
66
67
 
67
68
  export type RoutingState = {
@@ -85,9 +86,13 @@ export class DevPortalContext {
85
86
 
86
87
  public navigation: NavigationItem[];
87
88
  public meta: DevPortalProps["meta"];
88
- public authentication?: AuthProvider;
89
+ public authentication?: AuthenticationProvider;
89
90
  public state: typeof useDevPortalState;
90
91
 
92
+ public get url(): URL {
93
+ return new URL(window.location.href);
94
+ }
95
+
91
96
  constructor(private config: DevPortalProps) {
92
97
  this.plugins = config.plugins ?? [];
93
98
  this.navigation = config.navigation;
@@ -101,7 +106,7 @@ export class DevPortalContext {
101
106
  this.plugins
102
107
  .filter(needsInitialization)
103
108
  .forEach((plugin) => plugin.initialize(this));
104
- this.authentication?.initialize(this);
109
+ await this.authentication?.initialize?.(this);
105
110
  };
106
111
 
107
112
  setUserProfile = async (profile: Partial<SessionProfileState>) => {
@@ -109,7 +114,7 @@ export class DevPortalContext {
109
114
  };
110
115
 
111
116
  invalidateCache = async (key: DevPortalCacheKey[]) => {
112
- queryClient.invalidateQueries({ queryKey: key });
117
+ await queryClient.invalidateQueries({ queryKey: key });
113
118
  };
114
119
 
115
120
  getApiIdentities = async () => {
@@ -135,15 +140,7 @@ export class DevPortalContext {
135
140
  throw new Error("No authentication configured");
136
141
  }
137
142
 
138
- return this.authentication.signOut(this);
139
- };
140
-
141
- handleAuthenticationResponse = async (path: {
142
- pathname: string;
143
- search: string;
144
- hash: string;
145
- }) => {
146
- this.config.authentication?.handleAuthenticationResponse?.(path, this);
143
+ return this.authentication.logout(this);
147
144
  };
148
145
 
149
146
  getNavigation = async (path: string) => {
@@ -4,15 +4,15 @@ import {
4
4
  type ApiIdentity,
5
5
  type NavigationCategory,
6
6
  } from "./DevPortalContext.js";
7
- import { type Combine } from "./types/combine.js";
8
7
 
9
8
  export type PluginNavigationCategory = {
10
9
  path: string;
11
10
  } & NavigationCategory;
12
11
 
13
- export type DevPortalPlugin = Combine<
14
- [NavigationPlugin, ApiIdentityPlugin, InitializationPlugin]
15
- >;
12
+ export type DevPortalPlugin =
13
+ | NavigationPlugin
14
+ | ApiIdentityPlugin
15
+ | InitializationPlugin;
16
16
 
17
17
  export interface NavigationPlugin {
18
18
  getRoutes: () => RouteObject[];
@@ -61,14 +61,16 @@ const createDefaultHandler = (endpoint: string): ApiKeyService => {
61
61
  throw new Error("Not authenticated");
62
62
  }
63
63
 
64
- await fetch(endpoint + `/v1/developer/api-keys`, {
64
+ const request = new Request(endpoint + `/v1/developer/api-keys`, {
65
65
  method: "POST",
66
66
  headers: {
67
- Authorization: `Bearer ${accessToken}`,
68
67
  "Content-Type": "application/json",
69
68
  },
70
69
  body: JSON.stringify(apiKey),
71
70
  });
71
+
72
+ await context.authentication?.signRequest(request, context);
73
+ await fetch(request);
72
74
  },
73
75
  getKeys: async (context) => {
74
76
  const accessToken = await context.authentication?.getToken?.(context);
@@ -77,12 +79,16 @@ const createDefaultHandler = (endpoint: string): ApiKeyService => {
77
79
  return [];
78
80
  }
79
81
 
80
- const keys = await fetch(endpoint + `/v1/developer/api-keys`, {
82
+ const request = new Request(endpoint + `/v1/developer/api-keys`, {
81
83
  headers: {
82
84
  Authorization: `Bearer ${accessToken}`,
83
85
  },
84
86
  });
85
87
 
88
+ await context.authentication?.signRequest(request, context);
89
+
90
+ const keys = await fetch(request);
91
+
86
92
  return await keys.json();
87
93
  },
88
94
  };
@@ -29,10 +29,15 @@ export const MakeRequest = ({
29
29
  }));
30
30
  const queryParams = operation.parameters
31
31
  ?.filter((p) => p.in === "query")
32
- .map((p) => ({
33
- name: p.name,
34
- value: "",
35
- }));
32
+ .sort((a, b) => (a.required && !b.required ? -1 : 1))
33
+ .map((p) => {
34
+ return {
35
+ name: p.name,
36
+ value: "",
37
+ active: p.required ?? false,
38
+ isRequired: p.required ?? false,
39
+ };
40
+ });
36
41
  const pathParams = operation.parameters
37
42
  ?.filter((p) => p.in === "path")
38
43
  .map((p) => ({
@@ -65,7 +65,8 @@ export type Header = {
65
65
  export type QueryParam = {
66
66
  name: string;
67
67
  value: string;
68
- active?: boolean;
68
+ active: boolean;
69
+ isRequired: boolean;
69
70
  };
70
71
  export type PathParam = {
71
72
  name: string;
@@ -200,9 +201,7 @@ const Playground = ({
200
201
  </VisuallyHidden>
201
202
  <FormProvider {...{ register, control, handleSubmit, watch, ...form }}>
202
203
  <form
203
- onSubmit={handleSubmit((data) => {
204
- queryMutation.mutateAsync(data);
205
- })}
204
+ onSubmit={handleSubmit((data) => queryMutation.mutateAsync(data))}
206
205
  >
207
206
  <div className="grid grid-cols-2 text-sm h-full">
208
207
  <div className="flex flex-col gap-4 p-8 bg-muted/50 after:bg-muted-foreground/20 relative after:absolute after:w-px after:inset-0 after:left-auto">
@@ -1,51 +1,13 @@
1
- import logger from "loglevel";
2
1
  import { XIcon } from "lucide-react";
3
- import { useEffect } from "react";
4
2
  import {
5
3
  Control,
6
4
  Controller,
7
5
  useFieldArray,
8
6
  useFormContext,
9
- useWatch,
10
7
  } from "react-hook-form";
11
8
  import { InlineInput } from "./InlineInput.js";
12
9
  import { PlaygroundForm } from "./Playground.js";
13
10
 
14
- const QueryParamActive = ({
15
- i,
16
- control,
17
- }: {
18
- i: number;
19
- control: Control<PlaygroundForm>;
20
- }) => {
21
- const value = useWatch({ control, name: `queryParams.${i}.value` });
22
- const active = useWatch({ control, name: `queryParams.${i}.active` });
23
- const form = useFormContext<PlaygroundForm>();
24
-
25
- useEffect(() => {
26
- if (value) {
27
- logger.log(`queryParams.${i}.active`, active);
28
- form.setValue(`queryParams.${i}.active`, true);
29
- }
30
- }, [value]);
31
-
32
- return (
33
- <Controller
34
- name={`queryParams.${i}.active`}
35
- render={({ field }) => {
36
- return (
37
- <input
38
- type="checkbox"
39
- id={`queryParams.${i}.active`}
40
- {...field}
41
- checked={field.value}
42
- />
43
- );
44
- }}
45
- />
46
- );
47
- };
48
-
49
11
  export const QueryParams = ({
50
12
  control,
51
13
  }: {
@@ -55,13 +17,29 @@ export const QueryParams = ({
55
17
  control,
56
18
  name: "queryParams",
57
19
  });
20
+ const form = useFormContext<PlaygroundForm>();
21
+
22
+ const requiredFields = form
23
+ .getValues(`queryParams`)
24
+ .map((param) => param.isRequired);
58
25
 
59
26
  return fields.map((field, i) => (
60
27
  <div
61
28
  key={field.id}
62
29
  className="px-2 grid-cols-subgrid col-span-full grid items-center gap-x-2 has-[:focus]:bg-muted hover:bg-accent rounded overflow-hidden group"
63
30
  >
64
- <QueryParamActive i={i} control={control} />
31
+ <Controller
32
+ control={control}
33
+ name={`queryParams.${i}.active`}
34
+ render={({ field }) => (
35
+ <input
36
+ type="checkbox"
37
+ id={`queryParams.${i}.active`}
38
+ checked={field.value}
39
+ onChange={field.onChange}
40
+ />
41
+ )}
42
+ />
65
43
 
66
44
  <Controller
67
45
  control={control}
@@ -71,8 +49,10 @@ export const QueryParams = ({
71
49
  <label
72
50
  className="flex items-center"
73
51
  htmlFor={`queryParams.${i}.active`}
52
+ title={requiredFields[i] ? "Required field" : undefined}
74
53
  >
75
54
  {field.value}
55
+ {requiredFields[i] && <sup className="text-destructive">*</sup>}
76
56
  </label>
77
57
  </InlineInput>
78
58
  );
@@ -1,4 +0,0 @@
1
- type UnionToIntersection<U> = (U extends any ? (k: U) => void : never) extends (k: infer I) => void ? I : never;
2
- type AnyCombination<T, U = T> = T extends any ? U extends any ? T | (T & U) : never : never;
3
- export type Combine<T extends any[]> = UnionToIntersection<T[number]> | AnyCombination<T[number]>;
4
- export {};
@@ -1,2 +0,0 @@
1
- export {};
2
- //# sourceMappingURL=combine.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"combine.js","sourceRoot":"","sources":["../../../../src/lib/core/types/combine.ts"],"names":[],"mappings":""}