From 65e3530080b13a7b9fa2896ac7e771308763a418 Mon Sep 17 00:00:00 2001 From: Christian Llontop Date: Mon, 15 Dec 2025 19:47:08 -0500 Subject: [PATCH 1/2] Implement OAuth2 and OpenID Connect authentication flows --- .../try-api/enhanced-credentials-form.tsx | 141 +++-- .../react/src/components/try-api/index.ts | 1 + .../src/components/try-api/oauth2-form.tsx | 491 ++++++++++++++++++ packages/react/src/contexts/auth-context.tsx | 24 + packages/react/src/core/types/index.ts | 29 +- packages/react/src/hooks/index.ts | 1 + .../react/src/hooks/use-curl-generator.ts | 6 + packages/react/src/hooks/use-oauth2.ts | 406 +++++++++++++++ packages/react/src/services/index.ts | 14 + packages/react/src/services/oauth2-client.ts | 425 +++++++++++++++ .../react/src/utils/map-security-schemes.ts | 52 +- 11 files changed, 1529 insertions(+), 61 deletions(-) create mode 100644 packages/react/src/components/try-api/oauth2-form.tsx create mode 100644 packages/react/src/hooks/use-oauth2.ts create mode 100644 packages/react/src/services/oauth2-client.ts diff --git a/packages/react/src/components/try-api/enhanced-credentials-form.tsx b/packages/react/src/components/try-api/enhanced-credentials-form.tsx index fc2e282..0f87b5c 100644 --- a/packages/react/src/components/try-api/enhanced-credentials-form.tsx +++ b/packages/react/src/components/try-api/enhanced-credentials-form.tsx @@ -1,9 +1,10 @@ import type { AuthCredentials } from "@/core/types"; import { useAuth, useOpenAPI } from "@/hooks"; import { Button, Card, Input, Select } from "@rhinolabs/ui"; -import { Code, Eye, EyeOff, IdCard, Key, Shield } from "lucide-react"; +import { Code, Eye, EyeOff, IdCard, Key, KeyRound, Shield } from "lucide-react"; import type React from "react"; import { useEffect, useState } from "react"; +import { OAuth2Form } from "./oauth2-form"; interface AuthTypeConfig { type: AuthCredentials["type"]; @@ -39,6 +40,22 @@ const authConfigs: AuthTypeConfig[] = [ placeholder: "Enter password", fieldName: "password", }, + { + type: "oauth2", + icon: KeyRound, + label: "OAuth 2.0", + description: "Authenticate using OAuth2 flows", + placeholder: "", + fieldName: "", + }, + { + type: "openIdConnect", + icon: KeyRound, + label: "OpenID Connect", + description: "Authenticate using OpenID Connect", + placeholder: "", + fieldName: "", + }, ]; export const EnhancedCredentialsForm: React.FC = () => { @@ -117,65 +134,75 @@ export const EnhancedCredentialsForm: React.FC = () => { - {/* Credential Input Fields */} - {credentials.type === "basic" && ( -
- - updateCredentials({ username: e.target.value })} - className="font-mono text-sm bg-card text-foreground" - /> -
- )} + {/* OAuth2/OpenIdConnect Form */} + {credentials.type === "oauth2" || + credentials.type === "openIdConnect" ? ( + + ) : ( + <> + {/* Credential Input Fields */} + {credentials.type === "basic" && ( +
+ + + updateCredentials({ username: e.target.value }) + } + className="font-mono text-sm bg-card text-foreground" + /> +
+ )} -
- -
- updateCredentials({ value: e.target.value })} - className="flex-1 font-mono text-sm bg-card text-foreground" - /> +
+ +
+ updateCredentials({ value: e.target.value })} + className="flex-1 font-mono text-sm bg-card text-foreground" + /> - -
-
+ +
+
- {/* Status Indicator */} - {credentials.value && ( -
-
- - Credentials configured - -
+ {/* Status Indicator */} + {credentials.value && ( +
+
+ + Credentials configured + +
+ )} + )} diff --git a/packages/react/src/components/try-api/index.ts b/packages/react/src/components/try-api/index.ts index 7c3138d..209a250 100644 --- a/packages/react/src/components/try-api/index.ts +++ b/packages/react/src/components/try-api/index.ts @@ -2,6 +2,7 @@ export { TryApiPanel } from "./try-api-panel"; export { CredentialsForm } from "./credentials-form"; export { EnhancedCredentialsForm } from "./enhanced-credentials-form"; +export { OAuth2Form } from "./oauth2-form"; export { CurlDisplay } from "./curl-display"; export { EnhancedCurlDisplay } from "./enhanced-curl-display"; export { ResponseDisplay } from "./response-display"; diff --git a/packages/react/src/components/try-api/oauth2-form.tsx b/packages/react/src/components/try-api/oauth2-form.tsx new file mode 100644 index 0000000..57b43ec --- /dev/null +++ b/packages/react/src/components/try-api/oauth2-form.tsx @@ -0,0 +1,491 @@ +"use client"; + +import type { OAuth2FlowType } from "@/core/types"; +import { useAuth, useOpenAPI } from "@/hooks"; +import { useOAuth2 } from "@/hooks/use-oauth2"; +import { + getAvailableOAuth2Flows, + getOAuth2FlowDescription, + getOAuth2FlowLabel, +} from "@/services/oauth2-client"; +import type { OAuth2Flow } from "@/types/api/openapi/security"; +import { getOAuth2Scheme } from "@/utils/map-security-schemes"; +import { Badge, Button, Checkbox, Input, Label, Select } from "@rhinolabs/ui"; +import { + AlertCircle, + Check, + Clock, + Eye, + EyeOff, + KeyRound, + Loader2, + LogOut, + RefreshCw, +} from "lucide-react"; +import type React from "react"; +import { useEffect, useMemo, useState } from "react"; + +interface OAuth2FormProps { + onAuthenticated?: () => void; +} + +export const OAuth2Form: React.FC = ({ onAuthenticated }) => { + const { spec } = useOpenAPI(); + const { credentials, updateCredentials } = useAuth(); + const { + isAuthenticating, + error, + tokenExpiresAt, + startAuthCodeFlow, + authenticateWithClientCredentials, + authenticateWithPassword, + refreshAccessToken, + isTokenExpired, + clearOAuth2Auth, + } = useOAuth2(); + + const [showSecret, setShowSecret] = useState(false); + const [showPassword, setShowPassword] = useState(false); + const [username, setUsername] = useState(""); + const [password, setPassword] = useState(""); + + // Get OAuth2 scheme from spec + const oauth2Scheme = useMemo(() => getOAuth2Scheme(spec), [spec]); + + // Get available flows from the scheme + const availableFlows = useMemo(() => { + if (!oauth2Scheme?.flows) return []; + return getAvailableOAuth2Flows(oauth2Scheme.flows); + }, [oauth2Scheme]); + + // Current selected flow + const selectedFlow = credentials.oauth2?.flow || availableFlows[0]; + + // Get flow config from scheme + const flowConfig = useMemo((): OAuth2Flow | undefined => { + if (!oauth2Scheme?.flows || !selectedFlow) return undefined; + return oauth2Scheme.flows[selectedFlow]; + }, [oauth2Scheme, selectedFlow]); + + // Available scopes from the flow + const availableScopes = useMemo(() => { + if (!flowConfig?.scopes) return []; + return Object.entries(flowConfig.scopes).map(([scope, description]) => ({ + scope, + description, + })); + }, [flowConfig]); + + // Selected scopes + const selectedScopes = credentials.oauth2?.scopes || []; + + // Auto-select first flow if none selected + useEffect(() => { + if (availableFlows.length > 0 && !credentials.oauth2?.flow) { + updateCredentials({ + oauth2: { + ...credentials.oauth2, + flow: availableFlows[0], + clientId: credentials.oauth2?.clientId || "", + scopes: credentials.oauth2?.scopes || [], + }, + }); + } + }, [availableFlows, credentials.oauth2, updateCredentials]); + + // Handle flow change + const handleFlowChange = (flow: OAuth2FlowType) => { + updateCredentials({ + oauth2: { + ...credentials.oauth2, + flow, + clientId: credentials.oauth2?.clientId || "", + scopes: credentials.oauth2?.scopes || [], + }, + }); + }; + + // Handle scope toggle + const handleScopeToggle = (scope: string, checked: boolean) => { + const currentScopes = credentials.oauth2?.scopes || []; + const newScopes = checked + ? [...currentScopes, scope] + : currentScopes.filter((s) => s !== scope); + + updateCredentials({ + oauth2: { + ...credentials.oauth2, + flow: credentials.oauth2?.flow || "authorizationCode", + clientId: credentials.oauth2?.clientId || "", + scopes: newScopes, + }, + }); + }; + + // Handle authentication + const handleAuthenticate = async () => { + if (!flowConfig) return; + + try { + switch (selectedFlow) { + case "authorizationCode": + await startAuthCodeFlow({ + authorizationUrl: flowConfig.authorizationUrl || "", + tokenUrl: flowConfig.tokenUrl || "", + clientId: credentials.oauth2?.clientId || "", + clientSecret: credentials.oauth2?.clientSecret, + scopes: selectedScopes, + }); + break; + + case "clientCredentials": + await authenticateWithClientCredentials({ + tokenUrl: flowConfig.tokenUrl || "", + clientId: credentials.oauth2?.clientId || "", + clientSecret: credentials.oauth2?.clientSecret || "", + scopes: selectedScopes, + }); + onAuthenticated?.(); + break; + + case "password": + await authenticateWithPassword({ + tokenUrl: flowConfig.tokenUrl || "", + clientId: credentials.oauth2?.clientId || "", + clientSecret: credentials.oauth2?.clientSecret, + username, + password, + scopes: selectedScopes, + }); + onAuthenticated?.(); + break; + } + } catch (err) { + console.error("Authentication failed:", err); + } + }; + + // Handle token refresh + const handleRefresh = async () => { + await refreshAccessToken(); + }; + + // Handle logout + const handleLogout = () => { + clearOAuth2Auth(); + setUsername(""); + setPassword(""); + }; + + // Format expiration time + const formatExpiration = (expiresAt: number) => { + const now = Date.now(); + const diff = expiresAt - now; + if (diff <= 0) return "Expired"; + + const minutes = Math.floor(diff / 60000); + const hours = Math.floor(minutes / 60); + + if (hours > 0) return `${hours}h ${minutes % 60}m`; + return `${minutes}m`; + }; + + if (!oauth2Scheme) { + return ( +
+ No OAuth2 configuration found in the API specification. +
+ ); + } + + const isAuthenticated = Boolean(credentials.value); + const tokenExpired = isTokenExpired(); + + return ( +
+ {/* Flow Selector */} + {availableFlows.length > 1 && ( +
+ + +
+ )} + + {/* Client ID */} +
+ + + updateCredentials({ + oauth2: { + ...credentials.oauth2, + flow: credentials.oauth2?.flow || "authorizationCode", + clientId: e.target.value, + scopes: credentials.oauth2?.scopes || [], + }, + }) + } + className="font-mono text-sm bg-card text-foreground" + disabled={isAuthenticated} + /> +
+ + {/* Client Secret (for flows that need it) */} + {(selectedFlow === "clientCredentials" || + selectedFlow === "authorizationCode") && ( +
+ +
+ + updateCredentials({ + oauth2: { + ...credentials.oauth2, + flow: credentials.oauth2?.flow || "authorizationCode", + clientId: credentials.oauth2?.clientId || "", + clientSecret: e.target.value, + scopes: credentials.oauth2?.scopes || [], + }, + }) + } + className="font-mono text-sm bg-card text-foreground pr-10" + disabled={isAuthenticated} + /> + +
+
+ )} + + {/* Username/Password (for password flow) */} + {selectedFlow === "password" && ( + <> +
+ + setUsername(e.target.value)} + className="font-mono text-sm bg-card text-foreground" + disabled={isAuthenticated} + /> +
+
+ +
+ setPassword(e.target.value)} + className="font-mono text-sm bg-card text-foreground pr-10" + disabled={isAuthenticated} + /> + +
+
+ + )} + + {/* Scopes Selector */} + {availableScopes.length > 0 && ( +
+ +
+ {availableScopes.map(({ scope, description }) => ( +
+ + handleScopeToggle(scope, checked as boolean) + } + disabled={isAuthenticated} + /> +
+ + {description && ( + + {description} + + )} +
+
+ ))} +
+
+ )} + + {/* Error Display */} + {error && ( +
+ + {error} +
+ )} + + {/* Token Status */} + {isAuthenticated && ( +
+ {tokenExpired ? ( + <> + + Token expired + + ) : ( + <> + + Authenticated + {tokenExpiresAt && ( + + + {formatExpiration(tokenExpiresAt)} + + )} + + )} +
+ )} + + {/* Action Buttons */} +
+ {!isAuthenticated ? ( + + ) : ( + <> + {credentials.oauth2?.refreshToken && ( + + )} + + + )} +
+ + {/* Auth Code Flow Info */} + {selectedFlow === "authorizationCode" && !isAuthenticated && ( +

+ Clicking Authenticate will open a popup for authorization. Make sure + popups are enabled. +

+ )} +
+ ); +}; diff --git a/packages/react/src/contexts/auth-context.tsx b/packages/react/src/contexts/auth-context.tsx index e4f9995..f887524 100644 --- a/packages/react/src/contexts/auth-context.tsx +++ b/packages/react/src/contexts/auth-context.tsx @@ -57,6 +57,24 @@ const authTypeDefaults: Record< basic: { location: "header", }, + oauth2: { + prefix: "Bearer ", + location: "header", + oauth2: { + flow: "authorizationCode", + clientId: "", + scopes: [], + }, + }, + openIdConnect: { + prefix: "Bearer ", + location: "header", + openIdConnect: { + discoveryUrl: "", + clientId: "", + scopes: [], + }, + }, }; /** @@ -168,6 +186,12 @@ export function AuthProvider({ children }: { children: ReactNode }) { headers.Authorization = `Basic ${encoded}`; } break; + + case "oauth2": + case "openIdConnect": + // OAuth2 and OpenID Connect use Bearer token in Authorization header + headers.Authorization = `Bearer ${credentials.value}`; + break; } return headers; diff --git a/packages/react/src/core/types/index.ts b/packages/react/src/core/types/index.ts index 0fd271e..535d37b 100644 --- a/packages/react/src/core/types/index.ts +++ b/packages/react/src/core/types/index.ts @@ -26,13 +26,40 @@ export interface RequestConfig { timeout?: number; } +export type OAuth2FlowType = + | "implicit" + | "password" + | "clientCredentials" + | "authorizationCode"; + +export interface OAuth2Config { + flow: OAuth2FlowType; + clientId: string; + clientSecret?: string; // Only for clientCredentials and authorizationCode (confidential clients) + authorizationUrl?: string; + tokenUrl?: string; + scopes: string[]; + refreshToken?: string; + expiresAt?: number; // Unix timestamp when token expires +} + +export interface OpenIdConnectConfig { + discoveryUrl: string; + clientId: string; + scopes: string[]; +} + export interface AuthCredentials { - type: "apiKey" | "bearer" | "basic"; + type: "apiKey" | "bearer" | "basic" | "oauth2" | "openIdConnect"; value: string; username?: string; // For basic auth keyName?: string; // For API key location (x-api-key, api-key, etc.) location?: "header" | "query"; // Where to send the credential prefix?: string; // e.g., "Bearer ", "Token ", etc. + // OAuth2 specific configuration + oauth2?: OAuth2Config; + // OpenID Connect specific configuration + openIdConnect?: OpenIdConnectConfig; } export interface ParameterValue { diff --git a/packages/react/src/hooks/index.ts b/packages/react/src/hooks/index.ts index 7cb8f17..97d2d92 100644 --- a/packages/react/src/hooks/index.ts +++ b/packages/react/src/hooks/index.ts @@ -7,6 +7,7 @@ export { useSidebarData } from "./use-sidebar-data"; export { useEndpointHeader } from "./use-endpoint-header"; export { useEndpointParameter } from "./use-endpoint-parameter"; export { useEndpointResponses } from "./use-endpoint-responses"; +export { useOAuth2 } from "./use-oauth2"; // Re-export context hooks export { useAuth } from "@/contexts/auth-context"; diff --git a/packages/react/src/hooks/use-curl-generator.ts b/packages/react/src/hooks/use-curl-generator.ts index 1d4ee14..b3e2266 100644 --- a/packages/react/src/hooks/use-curl-generator.ts +++ b/packages/react/src/hooks/use-curl-generator.ts @@ -131,6 +131,12 @@ function generateAuthHeaders( headers.Authorization = `Basic ${encoded}`; } break; + + case "oauth2": + case "openIdConnect": + // OAuth2 and OpenID Connect use Bearer token in Authorization header + headers.Authorization = `Bearer ${credentials.value}`; + break; } return headers; diff --git a/packages/react/src/hooks/use-oauth2.ts b/packages/react/src/hooks/use-oauth2.ts new file mode 100644 index 0000000..64f0588 --- /dev/null +++ b/packages/react/src/hooks/use-oauth2.ts @@ -0,0 +1,406 @@ +"use client"; + +import { useAuth } from "@/contexts/auth-context"; +import { + type AuthCodeFlowConfig, + type ClientCredentialsConfig, + OAuth2Client, + type OAuth2TokenResponse, + type OpenIdConfiguration, + type PasswordFlowConfig, +} from "@/services/oauth2-client"; +import { useCallback, useEffect, useRef, useState } from "react"; + +interface UseOAuth2State { + isAuthenticating: boolean; + error: string | null; + tokenExpiresAt: number | null; +} + +interface UseOAuth2Return extends UseOAuth2State { + /** + * Start Authorization Code flow with PKCE + * Opens a popup for user authentication + */ + startAuthCodeFlow: ( + config: Omit, + ) => Promise; + + /** + * Authenticate using Client Credentials flow + * For machine-to-machine authentication + */ + authenticateWithClientCredentials: ( + config: ClientCredentialsConfig, + ) => Promise; + + /** + * Authenticate using Password flow + * Direct username/password authentication + */ + authenticateWithPassword: (config: PasswordFlowConfig) => Promise; + + /** + * Refresh the current access token + */ + refreshAccessToken: () => Promise; + + /** + * Check if token is expired or about to expire + */ + isTokenExpired: () => boolean; + + /** + * Discover OpenID Connect configuration + */ + discoverOpenIdConfig: (discoveryUrl: string) => Promise; + + /** + * Clear OAuth2 authentication state + */ + clearOAuth2Auth: () => void; +} + +/** + * Hook for managing OAuth2 authentication flows + * + * @example + * ```tsx + * const { startAuthCodeFlow, isAuthenticating, error } = useOAuth2(); + * + * const handleAuth = async () => { + * await startAuthCodeFlow({ + * authorizationUrl: 'https://auth.example.com/authorize', + * tokenUrl: 'https://auth.example.com/token', + * clientId: 'my-client-id', + * scopes: ['read', 'write'], + * }); + * }; + * ``` + */ +export function useOAuth2(): UseOAuth2Return { + const { credentials, updateCredentials, clearCredentials } = useAuth(); + const [state, setState] = useState({ + isAuthenticating: false, + error: null, + tokenExpiresAt: credentials.oauth2?.expiresAt || null, + }); + + const oauth2ClientRef = useRef(new OAuth2Client()); + const pendingAuthRef = useRef<{ + config: Omit; + codeVerifier: string; + state: string; + } | null>(null); + + // Get redirect URI for OAuth2 callbacks + const getRedirectUri = useCallback(() => { + if (typeof window === "undefined") return ""; + return `${window.location.origin}${window.location.pathname}`; + }, []); + + // Handle OAuth2 callback messages from popup + useEffect(() => { + const handleMessage = async (event: MessageEvent) => { + // Verify origin + if (event.origin !== window.location.origin) return; + + const { type, code, state: returnedState, error } = event.data || {}; + + if (type !== "oauth2-callback") return; + + if (error) { + setState((prev) => ({ + ...prev, + isAuthenticating: false, + error: error, + })); + return; + } + + if (!pendingAuthRef.current) { + setState((prev) => ({ + ...prev, + isAuthenticating: false, + error: "No pending authentication request", + })); + return; + } + + // Verify state for CSRF protection + if (returnedState !== pendingAuthRef.current.state) { + setState((prev) => ({ + ...prev, + isAuthenticating: false, + error: "State mismatch - possible CSRF attack", + })); + pendingAuthRef.current = null; + return; + } + + try { + // Exchange code for token + const tokenResponse = + await oauth2ClientRef.current.exchangeCodeForToken( + { + tokenUrl: pendingAuthRef.current.config.tokenUrl, + clientId: pendingAuthRef.current.config.clientId, + clientSecret: pendingAuthRef.current.config.clientSecret, + redirectUri: getRedirectUri(), + }, + code, + pendingAuthRef.current.codeVerifier, + ); + + handleTokenResponse(tokenResponse, pendingAuthRef.current.config); + pendingAuthRef.current = null; + } catch (err) { + setState((prev) => ({ + ...prev, + isAuthenticating: false, + error: err instanceof Error ? err.message : "Token exchange failed", + })); + pendingAuthRef.current = null; + } + }; + + window.addEventListener("message", handleMessage); + return () => window.removeEventListener("message", handleMessage); + }, [getRedirectUri]); + + // Handle successful token response + const handleTokenResponse = useCallback( + ( + response: OAuth2TokenResponse, + config: { tokenUrl: string; clientId: string; scopes: string[] }, + ) => { + const expiresAt = response.expires_in + ? Date.now() + response.expires_in * 1000 + : undefined; + + updateCredentials({ + type: "oauth2", + value: response.access_token, + oauth2: { + flow: "authorizationCode", + clientId: config.clientId, + tokenUrl: config.tokenUrl, + scopes: config.scopes, + refreshToken: response.refresh_token, + expiresAt, + }, + }); + + setState({ + isAuthenticating: false, + error: null, + tokenExpiresAt: expiresAt || null, + }); + }, + [updateCredentials], + ); + + // Start Authorization Code flow + const startAuthCodeFlow = useCallback( + async (config: Omit) => { + setState((prev) => ({ ...prev, isAuthenticating: true, error: null })); + + try { + const fullConfig: AuthCodeFlowConfig = { + ...config, + redirectUri: getRedirectUri(), + }; + + const { state, codeVerifier } = + await oauth2ClientRef.current.startAuthCodeFlow(fullConfig); + + // Store pending auth info for callback handling + pendingAuthRef.current = { + config, + codeVerifier, + state, + }; + } catch (err) { + setState((prev) => ({ + ...prev, + isAuthenticating: false, + error: + err instanceof Error ? err.message : "Failed to start auth flow", + })); + } + }, + [getRedirectUri], + ); + + // Authenticate with Client Credentials + const authenticateWithClientCredentials = useCallback( + async (config: ClientCredentialsConfig) => { + setState((prev) => ({ ...prev, isAuthenticating: true, error: null })); + + try { + const response = + await oauth2ClientRef.current.getClientCredentialsToken(config); + + const expiresAt = response.expires_in + ? Date.now() + response.expires_in * 1000 + : undefined; + + updateCredentials({ + type: "oauth2", + value: response.access_token, + oauth2: { + flow: "clientCredentials", + clientId: config.clientId, + clientSecret: config.clientSecret, + tokenUrl: config.tokenUrl, + scopes: config.scopes, + expiresAt, + }, + }); + + setState({ + isAuthenticating: false, + error: null, + tokenExpiresAt: expiresAt || null, + }); + } catch (err) { + setState((prev) => ({ + ...prev, + isAuthenticating: false, + error: + err instanceof Error + ? err.message + : "Client credentials authentication failed", + })); + } + }, + [updateCredentials], + ); + + // Authenticate with Password + const authenticateWithPassword = useCallback( + async (config: PasswordFlowConfig) => { + setState((prev) => ({ ...prev, isAuthenticating: true, error: null })); + + try { + const response = await oauth2ClientRef.current.getPasswordToken(config); + + const expiresAt = response.expires_in + ? Date.now() + response.expires_in * 1000 + : undefined; + + updateCredentials({ + type: "oauth2", + value: response.access_token, + oauth2: { + flow: "password", + clientId: config.clientId, + tokenUrl: config.tokenUrl, + scopes: config.scopes, + refreshToken: response.refresh_token, + expiresAt, + }, + }); + + setState({ + isAuthenticating: false, + error: null, + tokenExpiresAt: expiresAt || null, + }); + } catch (err) { + setState((prev) => ({ + ...prev, + isAuthenticating: false, + error: + err instanceof Error + ? err.message + : "Password authentication failed", + })); + } + }, + [updateCredentials], + ); + + // Refresh access token + const refreshAccessToken = useCallback(async () => { + if (!credentials.oauth2?.refreshToken || !credentials.oauth2?.tokenUrl) { + setState((prev) => ({ + ...prev, + error: "No refresh token available", + })); + return; + } + + setState((prev) => ({ ...prev, isAuthenticating: true, error: null })); + + try { + const response = await oauth2ClientRef.current.refreshToken( + credentials.oauth2.tokenUrl, + credentials.oauth2.refreshToken, + credentials.oauth2.clientId, + credentials.oauth2.clientSecret, + ); + + const expiresAt = response.expires_in + ? Date.now() + response.expires_in * 1000 + : undefined; + + updateCredentials({ + value: response.access_token, + oauth2: { + ...credentials.oauth2, + refreshToken: + response.refresh_token || credentials.oauth2.refreshToken, + expiresAt, + }, + }); + + setState({ + isAuthenticating: false, + error: null, + tokenExpiresAt: expiresAt || null, + }); + } catch (err) { + setState((prev) => ({ + ...prev, + isAuthenticating: false, + error: err instanceof Error ? err.message : "Token refresh failed", + })); + } + }, [credentials.oauth2, updateCredentials]); + + // Check if token is expired + const isTokenExpired = useCallback(() => { + if (!state.tokenExpiresAt) return false; + // Consider token expired 60 seconds before actual expiration + return Date.now() > state.tokenExpiresAt - 60000; + }, [state.tokenExpiresAt]); + + // Discover OpenID Connect configuration + const discoverOpenIdConfig = useCallback(async (discoveryUrl: string) => { + return oauth2ClientRef.current.discoverOpenIdConfig(discoveryUrl); + }, []); + + // Clear OAuth2 authentication + const clearOAuth2Auth = useCallback(() => { + clearCredentials(); + oauth2ClientRef.current.clearAuthState(); + pendingAuthRef.current = null; + setState({ + isAuthenticating: false, + error: null, + tokenExpiresAt: null, + }); + }, [clearCredentials]); + + return { + ...state, + startAuthCodeFlow, + authenticateWithClientCredentials, + authenticateWithPassword, + refreshAccessToken, + isTokenExpired, + discoverOpenIdConfig, + clearOAuth2Auth, + }; +} diff --git a/packages/react/src/services/index.ts b/packages/react/src/services/index.ts index fcaa627..10cf8cc 100644 --- a/packages/react/src/services/index.ts +++ b/packages/react/src/services/index.ts @@ -1,3 +1,17 @@ // Export all services export { ApiClient } from "./api-client"; export { OpenApiService } from "./openapi-service"; +export { + OAuth2Client, + oauth2Client, + getAvailableOAuth2Flows, + getOAuth2FlowLabel, + getOAuth2FlowDescription, +} from "./oauth2-client"; +export type { + OAuth2TokenResponse, + OpenIdConfiguration, + AuthCodeFlowConfig, + ClientCredentialsConfig, + PasswordFlowConfig, +} from "./oauth2-client"; diff --git a/packages/react/src/services/oauth2-client.ts b/packages/react/src/services/oauth2-client.ts new file mode 100644 index 0000000..c020679 --- /dev/null +++ b/packages/react/src/services/oauth2-client.ts @@ -0,0 +1,425 @@ +import type { OAuth2FlowType } from "@/core/types"; + +/** + * OAuth2 Token Response from the authorization server + */ +export interface OAuth2TokenResponse { + access_token: string; + token_type: string; + expires_in?: number; + refresh_token?: string; + scope?: string; +} + +/** + * OpenID Connect Discovery Configuration + */ +export interface OpenIdConfiguration { + issuer: string; + authorization_endpoint: string; + token_endpoint: string; + userinfo_endpoint?: string; + jwks_uri?: string; + scopes_supported?: string[]; + response_types_supported?: string[]; + grant_types_supported?: string[]; +} + +/** + * Configuration for Authorization Code Flow + */ +export interface AuthCodeFlowConfig { + authorizationUrl: string; + tokenUrl: string; + clientId: string; + clientSecret?: string; + redirectUri: string; + scopes: string[]; + state?: string; +} + +/** + * Configuration for Client Credentials Flow + */ +export interface ClientCredentialsConfig { + tokenUrl: string; + clientId: string; + clientSecret: string; + scopes: string[]; +} + +/** + * Configuration for Password Flow (Resource Owner) + */ +export interface PasswordFlowConfig { + tokenUrl: string; + clientId: string; + clientSecret?: string; + username: string; + password: string; + scopes: string[]; +} + +/** + * PKCE (Proof Key for Code Exchange) utilities for secure Authorization Code flow + */ +function generateCodeVerifier(): string { + const array = new Uint8Array(32); + crypto.getRandomValues(array); + return base64UrlEncode(array); +} + +async function generateCodeChallenge(verifier: string): Promise { + const encoder = new TextEncoder(); + const data = encoder.encode(verifier); + const digest = await crypto.subtle.digest("SHA-256", data); + return base64UrlEncode(new Uint8Array(digest)); +} + +function base64UrlEncode(buffer: Uint8Array): string { + const base64 = btoa(String.fromCharCode(...buffer)); + return base64.replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, ""); +} + +function generateState(): string { + const array = new Uint8Array(16); + crypto.getRandomValues(array); + return base64UrlEncode(array); +} + +/** + * OAuth2 Client for handling different OAuth2 flows + * + * Supports: + * - Authorization Code Flow (with PKCE for SPAs) + * - Client Credentials Flow + * - Password Flow (Resource Owner) + * - Token Refresh + * - OpenID Connect Discovery + */ +export class OAuth2Client { + private codeVerifier: string | null = null; + private state: string | null = null; + + /** + * Start Authorization Code Flow with PKCE + * Opens a popup or redirects to the authorization server + * + * @param config - Authorization Code flow configuration + * @param usePopup - Whether to use popup (true) or redirect (false) + * @returns Promise that resolves when popup is opened or redirect is initiated + */ + async startAuthCodeFlow( + config: AuthCodeFlowConfig, + usePopup = true, + ): Promise<{ authUrl: string; state: string; codeVerifier: string }> { + // Generate PKCE code verifier and challenge + this.codeVerifier = generateCodeVerifier(); + const codeChallenge = await generateCodeChallenge(this.codeVerifier); + + // Generate state for CSRF protection + this.state = config.state || generateState(); + + // Build authorization URL + const params = new URLSearchParams({ + response_type: "code", + client_id: config.clientId, + redirect_uri: config.redirectUri, + scope: config.scopes.join(" "), + state: this.state, + code_challenge: codeChallenge, + code_challenge_method: "S256", + }); + + const authUrl = `${config.authorizationUrl}?${params.toString()}`; + + if (usePopup) { + // Open popup for authorization + const popup = window.open( + authUrl, + "oauth2-popup", + "width=600,height=700,menubar=no,toolbar=no,location=no,status=no", + ); + + if (!popup) { + throw new Error( + "Failed to open popup. Please allow popups for this site.", + ); + } + } + + return { + authUrl, + state: this.state, + codeVerifier: this.codeVerifier, + }; + } + + /** + * Exchange authorization code for tokens + * + * @param config - Token exchange configuration + * @param code - Authorization code received from callback + * @param codeVerifier - PKCE code verifier used in authorization request + * @returns OAuth2 token response + */ + async exchangeCodeForToken( + config: Pick< + AuthCodeFlowConfig, + "tokenUrl" | "clientId" | "clientSecret" | "redirectUri" + >, + code: string, + codeVerifier: string, + ): Promise { + const body = new URLSearchParams({ + grant_type: "authorization_code", + code, + redirect_uri: config.redirectUri, + client_id: config.clientId, + code_verifier: codeVerifier, + }); + + // Add client_secret if provided (for confidential clients) + if (config.clientSecret) { + body.append("client_secret", config.clientSecret); + } + + const response = await fetch(config.tokenUrl, { + method: "POST", + headers: { + "Content-Type": "application/x-www-form-urlencoded", + }, + body: body.toString(), + }); + + if (!response.ok) { + const error = await response.json().catch(() => ({})); + throw new Error( + error.error_description || error.error || "Token exchange failed", + ); + } + + return response.json(); + } + + /** + * Get token using Client Credentials Flow + * Used for machine-to-machine authentication + * + * @param config - Client credentials configuration + * @returns OAuth2 token response + */ + async getClientCredentialsToken( + config: ClientCredentialsConfig, + ): Promise { + const body = new URLSearchParams({ + grant_type: "client_credentials", + client_id: config.clientId, + client_secret: config.clientSecret, + scope: config.scopes.join(" "), + }); + + const response = await fetch(config.tokenUrl, { + method: "POST", + headers: { + "Content-Type": "application/x-www-form-urlencoded", + }, + body: body.toString(), + }); + + if (!response.ok) { + const error = await response.json().catch(() => ({})); + throw new Error( + error.error_description || + error.error || + "Client credentials authentication failed", + ); + } + + return response.json(); + } + + /** + * Get token using Password Flow (Resource Owner Password Credentials) + * Note: This flow is less secure and should only be used when other flows are not possible + * + * @param config - Password flow configuration + * @returns OAuth2 token response + */ + async getPasswordToken( + config: PasswordFlowConfig, + ): Promise { + const body = new URLSearchParams({ + grant_type: "password", + username: config.username, + password: config.password, + client_id: config.clientId, + scope: config.scopes.join(" "), + }); + + // Add client_secret if provided + if (config.clientSecret) { + body.append("client_secret", config.clientSecret); + } + + const response = await fetch(config.tokenUrl, { + method: "POST", + headers: { + "Content-Type": "application/x-www-form-urlencoded", + }, + body: body.toString(), + }); + + if (!response.ok) { + const error = await response.json().catch(() => ({})); + throw new Error( + error.error_description || + error.error || + "Password authentication failed", + ); + } + + return response.json(); + } + + /** + * Refresh an access token using a refresh token + * + * @param tokenUrl - Token endpoint URL + * @param refreshToken - Refresh token + * @param clientId - Client ID + * @param clientSecret - Optional client secret + * @returns OAuth2 token response with new access token + */ + async refreshToken( + tokenUrl: string, + refreshToken: string, + clientId: string, + clientSecret?: string, + ): Promise { + const body = new URLSearchParams({ + grant_type: "refresh_token", + refresh_token: refreshToken, + client_id: clientId, + }); + + if (clientSecret) { + body.append("client_secret", clientSecret); + } + + const response = await fetch(tokenUrl, { + method: "POST", + headers: { + "Content-Type": "application/x-www-form-urlencoded", + }, + body: body.toString(), + }); + + if (!response.ok) { + const error = await response.json().catch(() => ({})); + throw new Error( + error.error_description || error.error || "Token refresh failed", + ); + } + + return response.json(); + } + + /** + * Discover OpenID Connect configuration from the well-known endpoint + * + * @param discoveryUrl - OpenID Connect discovery URL (typically ends with /.well-known/openid-configuration) + * @returns OpenID Connect configuration + */ + async discoverOpenIdConfig( + discoveryUrl: string, + ): Promise { + // Ensure URL ends with well-known endpoint + let url = discoveryUrl; + if (!url.includes(".well-known")) { + url = `${url.replace(/\/$/, "")}/.well-known/openid-configuration`; + } + + const response = await fetch(url); + + if (!response.ok) { + throw new Error("Failed to discover OpenID Connect configuration"); + } + + return response.json(); + } + + /** + * Get the stored state for CSRF verification + */ + getState(): string | null { + return this.state; + } + + /** + * Get the stored code verifier for PKCE + */ + getCodeVerifier(): string | null { + return this.codeVerifier; + } + + /** + * Clear stored PKCE and state values + */ + clearAuthState(): void { + this.codeVerifier = null; + this.state = null; + } +} + +/** + * Helper to determine which flows are available from an OAuth2 security scheme + */ +export function getAvailableOAuth2Flows(flows: { + implicit?: unknown; + password?: unknown; + clientCredentials?: unknown; + authorizationCode?: unknown; +}): OAuth2FlowType[] { + const available: OAuth2FlowType[] = []; + + if (flows.authorizationCode) available.push("authorizationCode"); + if (flows.clientCredentials) available.push("clientCredentials"); + if (flows.password) available.push("password"); + if (flows.implicit) available.push("implicit"); + + return available; +} + +/** + * Get human-readable label for OAuth2 flow type + */ +export function getOAuth2FlowLabel(flow: OAuth2FlowType): string { + const labels: Record = { + authorizationCode: "Authorization Code (Recommended)", + clientCredentials: "Client Credentials", + password: "Password", + implicit: "Implicit (Deprecated)", + }; + return labels[flow]; +} + +/** + * Get description for OAuth2 flow type + */ +export function getOAuth2FlowDescription(flow: OAuth2FlowType): string { + const descriptions: Record = { + authorizationCode: + "Secure flow for web applications. Opens authorization page in a popup.", + clientCredentials: + "For machine-to-machine authentication. Requires client ID and secret.", + password: + "Direct username/password authentication. Less secure, use only when necessary.", + implicit: + "Legacy flow for SPAs. Deprecated in favor of Authorization Code with PKCE.", + }; + return descriptions[flow]; +} + +// Export singleton instance for convenience +export const oauth2Client = new OAuth2Client(); diff --git a/packages/react/src/utils/map-security-schemes.ts b/packages/react/src/utils/map-security-schemes.ts index af0fc5a..8bdd562 100644 --- a/packages/react/src/utils/map-security-schemes.ts +++ b/packages/react/src/utils/map-security-schemes.ts @@ -1,4 +1,8 @@ import type { AuthCredentials, OpenApiDocument } from "@/core/types"; +import type { + OAuth2SecurityScheme, + OpenIdConnectSecurityScheme, +} from "@/types/api/openapi/security"; /** * Maps OpenAPI security schemes to Docutopia auth types @@ -10,7 +14,7 @@ import type { AuthCredentials, OpenApiDocument } from "@/core/types"; * ```ts * const spec = await loadSpec(url); * const authTypes = getAvailableAuthTypes(spec); - * // ["apiKey", "bearer"] + * // ["apiKey", "bearer", "oauth2"] * ``` */ export function getAvailableAuthTypes( @@ -33,13 +37,55 @@ export function getAvailableAuthTypes( } break; - // oauth2 and openIdConnect are not yet supported by Docutopia case "oauth2": + availableTypes.add("oauth2"); + break; + case "openIdConnect": - // TODO: Add support for OAuth2 and OpenID Connect + availableTypes.add("openIdConnect"); break; } } return Array.from(availableTypes); } + +/** + * Gets the OAuth2 security scheme from the OpenAPI spec + * + * @param spec - OpenAPI document specification + * @returns OAuth2 security scheme or null if not found + */ +export function getOAuth2Scheme( + spec: OpenApiDocument, +): OAuth2SecurityScheme | null { + const schemes = spec.components?.securitySchemes || {}; + + for (const scheme of Object.values(schemes)) { + if (scheme.type === "oauth2") { + return scheme as OAuth2SecurityScheme; + } + } + + return null; +} + +/** + * Gets the OpenID Connect security scheme from the OpenAPI spec + * + * @param spec - OpenAPI document specification + * @returns OpenID Connect security scheme or null if not found + */ +export function getOpenIdConnectScheme( + spec: OpenApiDocument, +): OpenIdConnectSecurityScheme | null { + const schemes = spec.components?.securitySchemes || {}; + + for (const scheme of Object.values(schemes)) { + if (scheme.type === "openIdConnect") { + return scheme as OpenIdConnectSecurityScheme; + } + } + + return null; +} From 0eb80fc0c47aeafa6db9128684b3c180caf1d89c Mon Sep 17 00:00:00 2001 From: Christian Llontop Date: Tue, 16 Dec 2025 19:14:33 -0500 Subject: [PATCH 2/2] Add OAuth2 mock server and local test API configuration --- package.json | 4 +- pnpm-lock.yaml | 573 ++++++++++++++++++++++++++- scripts/oauth/mock-server.ts | 23 ++ scripts/oauth/oauth2-local-test.json | 277 +++++++++++++ 4 files changed, 869 insertions(+), 8 deletions(-) create mode 100644 scripts/oauth/mock-server.ts create mode 100644 scripts/oauth/oauth2-local-test.json diff --git a/package.json b/package.json index 327a6cb..ebca657 100644 --- a/package.json +++ b/package.json @@ -20,11 +20,13 @@ "build": "pnpm -r build", "dev": "pnpm -r --parallel dev", "lint": "biome check .", - "lint:fix": "biome check . --fix --unsafe" + "lint:fix": "biome check . --fix --unsafe", + "oauth2:mock": "tsx scripts/oauth/mock-server.ts" }, "devDependencies": { "@biomejs/biome": "1.9.4", "@types/node": "^22.10.3", + "oauth2-mock-server": "^8.2.0", "tsx": "^4.19.2" }, "license": "MIT", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 411baf4..1763343 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -14,6 +14,9 @@ importers: '@types/node': specifier: ^22.10.3 version: 22.13.4 + oauth2-mock-server: + specifier: ^8.2.0 + version: 8.2.0 tsx: specifier: ^4.19.2 version: 4.19.2 @@ -50,7 +53,7 @@ importers: version: link:../../packages/nextjs next: specifier: ^15.5.9 - version: 15.5.9(@babel/core@7.28.4)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + version: 15.5.9(react-dom@19.0.0(react@19.0.0))(react@19.0.0) react: specifier: ^19.0.0 version: 19.0.0 @@ -149,7 +152,7 @@ importers: version: 5.0.4(vite@7.1.11(@types/node@22.13.4)(jiti@2.6.1)(lightningcss@1.29.1)(tsx@4.19.2)(yaml@2.8.1)) next: specifier: ^15.5.9 - version: 15.5.9(@babel/core@7.28.4)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + version: 15.5.9(react-dom@19.0.0(react@19.0.0))(react@19.0.0) tsc-alias: specifier: ^1.8.16 version: 1.8.16 @@ -1965,6 +1968,10 @@ packages: abstract-logging@2.0.1: resolution: {integrity: sha512-2BjRTZxTPvheOvGbBslFSYOUkr+SjPtOnrLP33f+VIWLzezQpZcqVg7ja3L4dBXmzzgwT+a029jRx5PCi3JuiA==} + accepts@2.0.0: + resolution: {integrity: sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==} + engines: {node: '>= 0.6'} + acorn@8.15.0: resolution: {integrity: sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==} engines: {node: '>=0.4.0'} @@ -2045,6 +2052,10 @@ packages: resolution: {integrity: sha512-zoKGUdu6vb2jd3YOq0nnhEDQVbPcHhco3UImJrv5dSkvxTc2pl2WjOPsjZXDwPDSl5eghIMuY3R6J9NDKF3KcQ==} hasBin: true + basic-auth@2.0.1: + resolution: {integrity: sha512-NF+epuEdnUYVlGuhaxbbq+dvJttwLnGY+YixlXlME5KpQ5W3CnXA5cVTneY3SPbPDRkcjMbifrwmFYcClgOZeg==} + engines: {node: '>= 0.8'} + binary-extensions@2.3.0: resolution: {integrity: sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==} engines: {node: '>=8'} @@ -2052,6 +2063,10 @@ packages: bl@5.1.0: resolution: {integrity: sha512-tv1ZJHLfTDnXE6tMHv73YgSJaWR2AFuPwMntBe7XL/GBFHnT0CLnsHMogfk5+GzCDC5ZWarSCYaIGATZt9dNsQ==} + body-parser@2.2.1: + resolution: {integrity: sha512-nfDwkulwiZYQIGwxdy0RUmowMhKcFVcYXUU7m4QlKYim1rUtg83xm2yjZ40QjDuc291AJjjeSc9b++AWHSgSHw==} + engines: {node: '>=18'} + brace-expansion@2.0.2: resolution: {integrity: sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==} @@ -2067,6 +2082,18 @@ packages: buffer@6.0.3: resolution: {integrity: sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==} + bytes@3.1.2: + resolution: {integrity: sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==} + engines: {node: '>= 0.8'} + + call-bind-apply-helpers@1.0.2: + resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==} + engines: {node: '>= 0.4'} + + call-bound@1.0.4: + resolution: {integrity: sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==} + engines: {node: '>= 0.4'} + caniuse-lite@1.0.30001760: resolution: {integrity: sha512-7AAMPcueWELt1p3mi13HR/LHH0TJLT11cnwDJEs3xA4+CK/PLKeO9Kl1oru24htkyUKtkGCvAx4ohB0Ttry8Dw==} @@ -2152,13 +2179,33 @@ packages: confbox@0.2.2: resolution: {integrity: sha512-1NB+BKqhtNipMsov4xI/NnhCKp9XG9NamYp5PVm9klAT0fsrNPjaFICsCFhNhwZJKNh7zB/3q8qXz0E9oaMNtQ==} + content-disposition@1.0.1: + resolution: {integrity: sha512-oIXISMynqSqm241k6kcQ5UwttDILMK4BiurCfGEREw6+X9jkkpEe5T9FZaApyLGGOnFuyMWZpdolTXMtvEJ08Q==} + engines: {node: '>=18'} + + content-type@1.0.5: + resolution: {integrity: sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==} + engines: {node: '>= 0.6'} + convert-source-map@2.0.0: resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} + cookie-signature@1.2.2: + resolution: {integrity: sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==} + engines: {node: '>=6.6.0'} + + cookie@0.7.2: + resolution: {integrity: sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==} + engines: {node: '>= 0.6'} + cookie@1.0.2: resolution: {integrity: sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA==} engines: {node: '>=18'} + cors@2.8.5: + resolution: {integrity: sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==} + engines: {node: '>= 0.10'} + cross-spawn@7.0.6: resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} engines: {node: '>= 8'} @@ -2238,6 +2285,10 @@ packages: defaults@1.0.4: resolution: {integrity: sha512-eFuaLoy/Rxalv2kr+lqMlUnrDWV+3j4pljOIJgLIhI058IQfWJ7vXhyEIHu+HtC738klGALYxOKDO0bQP3tg8A==} + depd@2.0.0: + resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==} + engines: {node: '>= 0.8'} + dependency-graph@1.0.0: resolution: {integrity: sha512-cW3gggJ28HZ/LExwxP2B++aiKxhJXMSIt9K48FOXQkm+vuG5gyatXnLsONRJdzO/7VfjDIiaOOa/bs4l464Lwg==} engines: {node: '>=4'} @@ -2272,6 +2323,13 @@ packages: dom-helpers@5.2.1: resolution: {integrity: sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==} + dunder-proto@1.0.1: + resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} + engines: {node: '>= 0.4'} + + ee-first@1.1.1: + resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==} + electron-to-chromium@1.5.239: resolution: {integrity: sha512-1y5w0Zsq39MSPmEjHjbizvhYoTaulVtivpxkp5q5kaPmQtsK6/2nvAzGRxNMS9DoYySp9PkW0MAQDwU1m764mg==} @@ -2291,6 +2349,10 @@ packages: emoji-regex@8.0.0: resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} + encodeurl@2.0.0: + resolution: {integrity: sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==} + engines: {node: '>= 0.8'} + enhanced-resolve@5.18.1: resolution: {integrity: sha512-ZSW3ma5GkcQBIpwZTSRAI8N71Uuwgs93IezB7mf7R60tC8ZbJideoDNKjHn2O9KIlx6rkGTTEk1xUCK2E1Y2Yg==} engines: {node: '>=10.13.0'} @@ -2299,6 +2361,18 @@ packages: resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==} engines: {node: '>=0.12'} + es-define-property@1.0.1: + resolution: {integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==} + engines: {node: '>= 0.4'} + + es-errors@1.3.0: + resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==} + engines: {node: '>= 0.4'} + + es-object-atoms@1.1.1: + resolution: {integrity: sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==} + engines: {node: '>= 0.4'} + esbuild@0.23.1: resolution: {integrity: sha512-VVNz/9Sa0bs5SELtn3f7qhJCDPCF5oMEl5cO9/SSinpE9hbPVvxbd572HH5AKiP7WD8INO53GgfDDhRjkylHEg==} engines: {node: '>=18'} @@ -2313,6 +2387,9 @@ packages: resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} engines: {node: '>=6'} + escape-html@1.0.3: + resolution: {integrity: sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==} + escape-string-regexp@5.0.0: resolution: {integrity: sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==} engines: {node: '>=12'} @@ -2323,6 +2400,10 @@ packages: estree-walker@2.0.2: resolution: {integrity: sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==} + etag@1.8.1: + resolution: {integrity: sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==} + engines: {node: '>= 0.6'} + eventemitter3@4.0.7: resolution: {integrity: sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==} @@ -2330,6 +2411,10 @@ packages: resolution: {integrity: sha512-UduyVP7TLB5IcAQl+OzLyLcS/l32W/GLg+AhHJ+ow40FOk2U3SAllPwR44v4vmdFwIWqpdwxxpQbF1n5ta9seA==} engines: {node: ^14.18.0 || ^16.14.0 || >=18.0.0} + express@5.2.1: + resolution: {integrity: sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==} + engines: {node: '>= 18'} + exsolve@1.0.7: resolution: {integrity: sha512-VO5fQUzZtI6C+vx4w/4BWJpg3s/5l+6pRQEHzFRM8WFi4XffSP1Z+4qi7GbjWbvRQEbdIco5mIMq+zX4rPuLrw==} @@ -2382,6 +2467,10 @@ packages: resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} engines: {node: '>=8'} + finalhandler@2.1.1: + resolution: {integrity: sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==} + engines: {node: '>= 18.0.0'} + find-my-way@9.3.0: resolution: {integrity: sha512-eRoFWQw+Yv2tuYlK2pjFS2jGXSxSppAs3hSQjfxVKxM5amECzIgYYc1FEI8ZmhSh/Ig+FrKEz43NLRKJjYCZVg==} engines: {node: '>=20'} @@ -2390,6 +2479,14 @@ packages: resolution: {integrity: sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==} engines: {node: '>=12.20.0'} + forwarded@0.2.0: + resolution: {integrity: sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==} + engines: {node: '>= 0.6'} + + fresh@2.0.0: + resolution: {integrity: sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==} + engines: {node: '>= 0.8'} + fs-extra@11.3.0: resolution: {integrity: sha512-Z4XaCL6dUDHfP/jT25jJKMmtxvuwbkrD1vNSMFlo9lNLY2c5FHYSQgHPRZUjAB26TpDEoW9HCOgplrdbaPV/ew==} engines: {node: '>=14.14'} @@ -2410,10 +2507,18 @@ packages: resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==} engines: {node: 6.* || 8.* || >= 10.*} + get-intrinsic@1.3.0: + resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==} + engines: {node: '>= 0.4'} + get-nonce@1.0.1: resolution: {integrity: sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q==} engines: {node: '>=6'} + get-proto@1.0.1: + resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==} + engines: {node: '>= 0.4'} + get-stream@6.0.1: resolution: {integrity: sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==} engines: {node: '>=10'} @@ -2429,6 +2534,10 @@ packages: resolution: {integrity: sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==} engines: {node: '>=10'} + gopd@1.2.0: + resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==} + engines: {node: '>= 0.4'} + graceful-fs@4.2.11: resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} @@ -2436,6 +2545,10 @@ packages: resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} engines: {node: '>=8'} + has-symbols@1.1.0: + resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==} + engines: {node: '>= 0.4'} + hasown@2.0.2: resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} engines: {node: '>= 0.4'} @@ -2444,10 +2557,18 @@ packages: resolution: {integrity: sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==} hasBin: true + http-errors@2.0.1: + resolution: {integrity: sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==} + engines: {node: '>= 0.8'} + human-signals@4.3.1: resolution: {integrity: sha512-nZXjEF2nbo7lIw3mgYjItAfgQXog3OjJogSbKa2CQIIvSGWcKgeJnQlNXip6NglNzYH45nSRiEVimMvYL8DDqQ==} engines: {node: '>=14.18.0'} + iconv-lite@0.7.1: + resolution: {integrity: sha512-2Tth85cXwGFHfvRgZWszZSvdo+0Xsqmw8k8ZwxScfcBneNUraK+dxRxRm24nszx80Y0TVio8kKLt5sLE7ZCLlw==} + engines: {node: '>=0.10.0'} + ieee754@1.2.1: resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==} @@ -2472,6 +2593,10 @@ packages: resolution: {integrity: sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==} engines: {node: '>=12'} + ipaddr.js@1.9.1: + resolution: {integrity: sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==} + engines: {node: '>= 0.10'} + ipaddr.js@2.2.0: resolution: {integrity: sha512-Ag3wB2o37wslZS19hZqorUnrnzSkpOVy+IiiDEiTqNubEYpYuHWIf6K4psgN2ZWKExS4xhVCrRVfb/wfW8fWJA==} engines: {node: '>= 10'} @@ -2516,6 +2641,13 @@ packages: resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} engines: {node: '>=0.12.0'} + is-plain-obj@4.1.0: + resolution: {integrity: sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==} + engines: {node: '>=12'} + + is-promise@4.0.0: + resolution: {integrity: sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==} + is-stream@3.0.0: resolution: {integrity: sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} @@ -2538,6 +2670,9 @@ packages: jju@1.4.0: resolution: {integrity: sha512-8wb9Yw966OSxApiCt0K3yNJL8pnNeIv+OEq2YMidz4FKP6nonSRoOXc80iXY4JaN2FC11B9qsNmDsm+ZOfMROA==} + jose@6.1.3: + resolution: {integrity: sha512-0TpaTfihd4QMNwrz/ob2Bp7X04yuxJkjRGi4aKmOqwhov54i6u79oCv7T+C7lo70MKH6BesI3vscD1yb/yzKXQ==} + js-tokens@4.0.0: resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} @@ -2679,6 +2814,10 @@ packages: magic-string@0.30.19: resolution: {integrity: sha512-2N21sPY9Ws53PZvsEpVtNuSW+ScYbQdp4b9qUaL+9QkHUrGFKo56Lg9Emg5s9V/qrtNBmiR01sYhUOwu3H+VOw==} + math-intrinsics@1.1.0: + resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} + engines: {node: '>= 0.4'} + mdast-util-from-markdown@2.0.2: resolution: {integrity: sha512-uZhTV/8NBuw0WHkPTrCqDOl0zVe1BIng5ZtHoDk49ME1qqcjYmmLmOf0gELgcRMxN4w2iuIeVso5/6QymSrgmA==} @@ -2694,6 +2833,14 @@ packages: mdast-util-to-string@4.0.0: resolution: {integrity: sha512-0H44vDimn51F0YwvxSJSm0eCDOJTRlmN0R1yBh4HLj9wiV1Dn0QoXGbvFAWj2hSItVTlCmBF1hqKlIyUBVFLPg==} + media-typer@1.1.0: + resolution: {integrity: sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==} + engines: {node: '>= 0.8'} + + merge-descriptors@2.0.0: + resolution: {integrity: sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==} + engines: {node: '>=18'} + merge-stream@2.0.0: resolution: {integrity: sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==} @@ -2768,6 +2915,14 @@ packages: resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==} engines: {node: '>=8.6'} + mime-db@1.54.0: + resolution: {integrity: sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==} + engines: {node: '>= 0.6'} + + mime-types@3.0.2: + resolution: {integrity: sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==} + engines: {node: '>=18'} + mimic-fn@2.1.0: resolution: {integrity: sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==} engines: {node: '>=6'} @@ -2810,6 +2965,10 @@ packages: engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} hasBin: true + negotiator@1.0.0: + resolution: {integrity: sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==} + engines: {node: '>= 0.6'} + next-themes@0.4.6: resolution: {integrity: sha512-pZvgD5L0IEvX5/9GWyHMf3m8BKiVQwsCMHfoFosXtXBMnaS0ZnIJ9ST4b4NqLVKDEm8QBxoNNGNaBv2JNF6XNA==} peerDependencies: @@ -2857,10 +3016,19 @@ packages: resolution: {integrity: sha512-ppwTtiJZq0O/ai0z7yfudtBpWIoxM8yE6nHi1X47eFR2EWORqfbu6CnPlNsjeN683eT0qG6H/Pyf9fCcvjnnnQ==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + oauth2-mock-server@8.2.0: + resolution: {integrity: sha512-bXkYmOdsIEj9kAKzofd8UdnilFT25KwCNNz150SfqQQXgcMRzw0VB3J4WVUpkPlxA+7DzIBJJoeZHgR5ZZnNPg==} + engines: {node: ^20.19 || ^22.12 || ^24} + hasBin: true + object-assign@4.1.1: resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} engines: {node: '>=0.10.0'} + object-inspect@1.13.4: + resolution: {integrity: sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==} + engines: {node: '>= 0.4'} + obliterator@2.0.5: resolution: {integrity: sha512-42CPE9AhahZRsMNslczq0ctAEtqk8Eka26QofnqC346BZdHDySk3LWka23LI7ULIw11NmltpiLagIq8gBozxTw==} @@ -2868,6 +3036,13 @@ packages: resolution: {integrity: sha512-0eJJY6hXLGf1udHwfNftBqH+g73EU4B504nZeKpz1sYRKafAghwxEJunB2O7rDZkL4PGfsMVnTXZ2EjibbqcsA==} engines: {node: '>=14.0.0'} + on-finished@2.4.1: + resolution: {integrity: sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==} + engines: {node: '>= 0.8'} + + once@1.4.0: + resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} + onetime@5.1.2: resolution: {integrity: sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==} engines: {node: '>=6'} @@ -2886,6 +3061,10 @@ packages: parse-entities@4.0.2: resolution: {integrity: sha512-GG2AQYWoLgL877gQIKeRPGO1xF9+eG1ujIb5soS5gPvLQ1y2o8FL90w2QWNdf9I361Mpp7726c+lj3U0qK1uGw==} + parseurl@1.3.3: + resolution: {integrity: sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==} + engines: {node: '>= 0.8'} + path-browserify@1.0.1: resolution: {integrity: sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==} @@ -2900,6 +3079,9 @@ packages: path-parse@1.0.7: resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==} + path-to-regexp@8.3.0: + resolution: {integrity: sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA==} + path-type@4.0.0: resolution: {integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==} engines: {node: '>=8'} @@ -2999,10 +3181,18 @@ packages: prop-types@15.8.1: resolution: {integrity: sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==} + proxy-addr@2.0.7: + resolution: {integrity: sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==} + engines: {node: '>= 0.10'} + punycode@2.3.1: resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} engines: {node: '>=6'} + qs@6.14.0: + resolution: {integrity: sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==} + engines: {node: '>=0.6'} + quansync@0.2.11: resolution: {integrity: sha512-AifT7QEbW9Nri4tAwR5M/uzpBuqfZf+zwaEM/QkzEjj7NBuFD2rBuy0K3dE+8wltbezDV7JMA0WfnCPYRSYbXA==} @@ -3016,6 +3206,14 @@ packages: quick-format-unescaped@4.0.4: resolution: {integrity: sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg==} + range-parser@1.2.1: + resolution: {integrity: sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==} + engines: {node: '>= 0.6'} + + raw-body@3.0.2: + resolution: {integrity: sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==} + engines: {node: '>= 0.10'} + react-day-picker@8.10.1: resolution: {integrity: sha512-TMx7fNbhLk15eqcMt+7Z7S2KF7mfTId/XJDjKE8f+IUcFn0l08/kI4FiYTL/0yuOLmEcbR4Fwe3GJf/NiiMnPA==} peerDependencies: @@ -3169,9 +3367,16 @@ packages: engines: {node: '>=18.0.0', npm: '>=8.0.0'} hasBin: true + router@2.2.0: + resolution: {integrity: sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==} + engines: {node: '>= 18'} + run-parallel@1.2.0: resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} + safe-buffer@5.1.2: + resolution: {integrity: sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==} + safe-buffer@5.2.1: resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} @@ -3182,6 +3387,9 @@ packages: resolution: {integrity: sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==} engines: {node: '>=10'} + safer-buffer@2.1.2: + resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} + scheduler@0.25.0: resolution: {integrity: sha512-xFVuu11jh+xcO7JOAGJNOXld8/TcEHK/4CituBUeUb5hqxJLj9YuemAEuvm9gQ/+pgXYfbQuqAkiYu+u7YEsNA==} @@ -3202,9 +3410,20 @@ packages: engines: {node: '>=10'} hasBin: true + send@1.2.1: + resolution: {integrity: sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==} + engines: {node: '>= 18'} + + serve-static@2.2.1: + resolution: {integrity: sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==} + engines: {node: '>= 18'} + set-cookie-parser@2.7.1: resolution: {integrity: sha512-IOc8uWeOZgnb3ptbCURJWNjWUPcO3ZnTTdzsurqERrP6nPyv+paC55vJM0LpOlT2ne+Ix+9+CRG1MNLlyZ4GjQ==} + setprototypeof@1.2.0: + resolution: {integrity: sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==} + sharp@0.34.5: resolution: {integrity: sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} @@ -3217,6 +3436,22 @@ packages: resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} engines: {node: '>=8'} + side-channel-list@1.0.0: + resolution: {integrity: sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==} + engines: {node: '>= 0.4'} + + side-channel-map@1.0.1: + resolution: {integrity: sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==} + engines: {node: '>= 0.4'} + + side-channel-weakmap@1.0.2: + resolution: {integrity: sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==} + engines: {node: '>= 0.4'} + + side-channel@1.1.0: + resolution: {integrity: sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==} + engines: {node: '>= 0.4'} + signal-exit@3.0.7: resolution: {integrity: sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==} @@ -3255,6 +3490,10 @@ packages: sprintf-js@1.0.3: resolution: {integrity: sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==} + statuses@2.0.2: + resolution: {integrity: sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==} + engines: {node: '>= 0.8'} + stdin-discarder@0.1.0: resolution: {integrity: sha512-xhV7w8S+bUwlPTb4bAOUQhv8/cSS5offJuX8GQGq32ONF0ZtDWKfkdomM3HMRA+LhX6um/FZ0COqlwsjD53LeQ==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} @@ -3346,6 +3585,10 @@ packages: resolution: {integrity: sha512-/m8M+2BJUpoJdgAHoG+baCwBT+tf2VraSfkBgl0Y00qIWt41DJ8R5B8nsEw0I58YwF5IZH6z24/2TobDKnqSWw==} engines: {node: '>=12'} + toidentifier@1.0.1: + resolution: {integrity: sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==} + engines: {node: '>=0.6'} + tsc-alias@1.8.16: resolution: {integrity: sha512-QjCyu55NFyRSBAl6+MTFwplpFcnm2Pq01rR/uxfqJoLMm6X3O14KEGtaSDZpJYaE1bJBGDjD0eSuiIWPe2T58g==} engines: {node: '>=16.20.2'} @@ -3359,6 +3602,10 @@ packages: engines: {node: '>=18.0.0'} hasBin: true + type-is@2.0.1: + resolution: {integrity: sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==} + engines: {node: '>= 0.6'} + typescript@5.7.3: resolution: {integrity: sha512-84MVSjMEHP+FQRPy3pX9sTVV/INIex71s9TL2Gm5FG/WG1SqXeKyZ0k7/blY/4FdOzI12CBy1vGc4og/eus0fw==} engines: {node: '>=14.17'} @@ -3391,6 +3638,10 @@ packages: resolution: {integrity: sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==} engines: {node: '>= 10.0.0'} + unpipe@1.0.0: + resolution: {integrity: sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==} + engines: {node: '>= 0.8'} + update-browserslist-db@1.1.4: resolution: {integrity: sha512-q0SPT4xyU84saUX+tomz1WLkxUbuaJnR1xWt17M7fJtEJigJeWUNGUqrauFXsHnqev9y9JTRGwk13tFBuKby4A==} hasBin: true @@ -3428,6 +3679,10 @@ packages: util-deprecate@1.0.2: resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} + vary@1.1.2: + resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==} + engines: {node: '>= 0.8'} + vaul@1.1.2: resolution: {integrity: sha512-ZFkClGpWyI2WUQjdLJ/BaGuV6AVQiJ3uELGk3OYtP+B6yCO7Cmn9vPFXVJkRaGkOJu3m8bQMgtyzNHixULceQA==} peerDependencies: @@ -3508,6 +3763,9 @@ packages: resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==} engines: {node: '>=10'} + wrappy@1.0.2: + resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} + y18n@5.0.8: resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==} engines: {node: '>=10'} @@ -5184,6 +5442,11 @@ snapshots: abstract-logging@2.0.1: {} + accepts@2.0.0: + dependencies: + mime-types: 3.0.2 + negotiator: 1.0.0 + acorn@8.15.0: {} ajv-draft-04@1.0.0(ajv@8.13.0): @@ -5250,6 +5513,10 @@ snapshots: baseline-browser-mapping@2.8.19: {} + basic-auth@2.0.1: + dependencies: + safe-buffer: 5.1.2 + binary-extensions@2.3.0: {} bl@5.1.0: @@ -5258,6 +5525,20 @@ snapshots: inherits: 2.0.4 readable-stream: 3.6.2 + body-parser@2.2.1: + dependencies: + bytes: 3.1.2 + content-type: 1.0.5 + debug: 4.4.3 + http-errors: 2.0.1 + iconv-lite: 0.7.1 + on-finished: 2.4.1 + qs: 6.14.0 + raw-body: 3.0.2 + type-is: 2.0.1 + transitivePeerDependencies: + - supports-color + brace-expansion@2.0.2: dependencies: balanced-match: 1.0.2 @@ -5279,6 +5560,18 @@ snapshots: base64-js: 1.5.1 ieee754: 1.2.1 + bytes@3.1.2: {} + + call-bind-apply-helpers@1.0.2: + dependencies: + es-errors: 1.3.0 + function-bind: 1.1.2 + + call-bound@1.0.4: + dependencies: + call-bind-apply-helpers: 1.0.2 + get-intrinsic: 1.3.0 + caniuse-lite@1.0.30001760: {} ccount@2.0.1: {} @@ -5365,10 +5658,23 @@ snapshots: confbox@0.2.2: {} + content-disposition@1.0.1: {} + + content-type@1.0.5: {} + convert-source-map@2.0.0: {} + cookie-signature@1.2.2: {} + + cookie@0.7.2: {} + cookie@1.0.2: {} + cors@2.8.5: + dependencies: + object-assign: 4.1.1 + vary: 1.1.2 + cross-spawn@7.0.6: dependencies: path-key: 3.1.1 @@ -5435,6 +5741,8 @@ snapshots: dependencies: clone: 1.0.4 + depd@2.0.0: {} + dependency-graph@1.0.0: {} dequal@2.0.3: {} @@ -5461,6 +5769,14 @@ snapshots: '@babel/runtime': 7.27.4 csstype: 3.1.3 + dunder-proto@1.0.1: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-errors: 1.3.0 + gopd: 1.2.0 + + ee-first@1.1.1: {} + electron-to-chromium@1.5.239: {} embla-carousel-react@8.5.2(react@19.0.0): @@ -5477,6 +5793,8 @@ snapshots: emoji-regex@8.0.0: {} + encodeurl@2.0.0: {} + enhanced-resolve@5.18.1: dependencies: graceful-fs: 4.2.11 @@ -5484,6 +5802,14 @@ snapshots: entities@4.5.0: {} + es-define-property@1.0.1: {} + + es-errors@1.3.0: {} + + es-object-atoms@1.1.1: + dependencies: + es-errors: 1.3.0 + esbuild@0.23.1: optionalDependencies: '@esbuild/aix-ppc64': 0.23.1 @@ -5542,6 +5868,8 @@ snapshots: escalade@3.2.0: {} + escape-html@1.0.3: {} + escape-string-regexp@5.0.0: {} estree-util-visit@2.0.0: @@ -5551,6 +5879,8 @@ snapshots: estree-walker@2.0.2: {} + etag@1.8.1: {} + eventemitter3@4.0.7: {} execa@7.2.0: @@ -5565,6 +5895,39 @@ snapshots: signal-exit: 3.0.7 strip-final-newline: 3.0.0 + express@5.2.1: + dependencies: + accepts: 2.0.0 + body-parser: 2.2.1 + content-disposition: 1.0.1 + content-type: 1.0.5 + cookie: 0.7.2 + cookie-signature: 1.2.2 + debug: 4.4.3 + depd: 2.0.0 + encodeurl: 2.0.0 + escape-html: 1.0.3 + etag: 1.8.1 + finalhandler: 2.1.1 + fresh: 2.0.0 + http-errors: 2.0.1 + merge-descriptors: 2.0.0 + mime-types: 3.0.2 + on-finished: 2.4.1 + once: 1.4.0 + parseurl: 1.3.3 + proxy-addr: 2.0.7 + qs: 6.14.0 + range-parser: 1.2.1 + router: 2.2.0 + send: 1.2.1 + serve-static: 2.2.1 + statuses: 2.0.2 + type-is: 2.0.1 + vary: 1.1.2 + transitivePeerDependencies: + - supports-color + exsolve@1.0.7: {} fast-decode-uri-component@1.0.1: {} @@ -5633,6 +5996,17 @@ snapshots: dependencies: to-regex-range: 5.0.1 + finalhandler@2.1.1: + dependencies: + debug: 4.4.3 + encodeurl: 2.0.0 + escape-html: 1.0.3 + on-finished: 2.4.1 + parseurl: 1.3.3 + statuses: 2.0.2 + transitivePeerDependencies: + - supports-color + find-my-way@9.3.0: dependencies: fast-deep-equal: 3.1.3 @@ -5643,6 +6017,10 @@ snapshots: dependencies: fetch-blob: 3.2.0 + forwarded@0.2.0: {} + + fresh@2.0.0: {} + fs-extra@11.3.0: dependencies: graceful-fs: 4.2.11 @@ -5658,8 +6036,26 @@ snapshots: get-caller-file@2.0.5: {} + get-intrinsic@1.3.0: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-define-property: 1.0.1 + es-errors: 1.3.0 + es-object-atoms: 1.1.1 + function-bind: 1.1.2 + get-proto: 1.0.1 + gopd: 1.2.0 + has-symbols: 1.1.0 + hasown: 2.0.2 + math-intrinsics: 1.1.0 + get-nonce@1.0.1: {} + get-proto@1.0.1: + dependencies: + dunder-proto: 1.0.1 + es-object-atoms: 1.1.1 + get-stream@6.0.1: {} get-tsconfig@4.10.0: @@ -5679,18 +6075,34 @@ snapshots: merge2: 1.4.1 slash: 3.0.0 + gopd@1.2.0: {} + graceful-fs@4.2.11: {} has-flag@4.0.0: {} + has-symbols@1.1.0: {} + hasown@2.0.2: dependencies: function-bind: 1.1.2 he@1.2.0: {} + http-errors@2.0.1: + dependencies: + depd: 2.0.0 + inherits: 2.0.4 + setprototypeof: 1.2.0 + statuses: 2.0.2 + toidentifier: 1.0.1 + human-signals@4.3.1: {} + iconv-lite@0.7.1: + dependencies: + safer-buffer: 2.1.2 + ieee754@1.2.1: {} ignore@5.3.2: {} @@ -5706,6 +6118,8 @@ snapshots: internmap@2.0.3: {} + ipaddr.js@1.9.1: {} + ipaddr.js@2.2.0: {} is-alphabetical@2.0.1: {} @@ -5739,6 +6153,10 @@ snapshots: is-number@7.0.0: {} + is-plain-obj@4.1.0: {} + + is-promise@4.0.0: {} + is-stream@3.0.0: {} is-unicode-supported@1.3.0: {} @@ -5752,6 +6170,8 @@ snapshots: jju@1.4.0: {} + jose@6.1.3: {} + js-tokens@4.0.0: {} js-yaml@4.1.1: @@ -5874,6 +6294,8 @@ snapshots: dependencies: '@jridgewell/sourcemap-codec': 1.5.5 + math-intrinsics@1.1.0: {} + mdast-util-from-markdown@2.0.2: dependencies: '@types/mdast': 4.0.4 @@ -5929,6 +6351,10 @@ snapshots: dependencies: '@types/mdast': 4.0.4 + media-typer@1.1.0: {} + + merge-descriptors@2.0.0: {} + merge-stream@2.0.0: {} merge2@1.4.1: {} @@ -6071,6 +6497,12 @@ snapshots: braces: 3.0.3 picomatch: 2.3.1 + mime-db@1.54.0: {} + + mime-types@3.0.2: + dependencies: + mime-db: 1.54.0 + mimic-fn@2.1.0: {} mimic-fn@4.0.0: {} @@ -6104,12 +6536,14 @@ snapshots: nanoid@3.3.8: {} + negotiator@1.0.0: {} + next-themes@0.4.6(react-dom@19.0.0(react@19.0.0))(react@19.0.0): dependencies: react: 19.0.0 react-dom: 19.0.0(react@19.0.0) - next@15.5.9(@babel/core@7.28.4)(react-dom@19.0.0(react@19.0.0))(react@19.0.0): + next@15.5.9(react-dom@19.0.0(react@19.0.0))(react@19.0.0): dependencies: '@next/env': 15.5.9 '@swc/helpers': 0.5.15 @@ -6117,7 +6551,7 @@ snapshots: postcss: 8.4.31 react: 19.0.0 react-dom: 19.0.0(react@19.0.0) - styled-jsx: 5.1.6(@babel/core@7.28.4)(react@19.0.0) + styled-jsx: 5.1.6(react@19.0.0) optionalDependencies: '@next/swc-darwin-arm64': 15.5.7 '@next/swc-darwin-x64': 15.5.7 @@ -6148,12 +6582,32 @@ snapshots: dependencies: path-key: 4.0.0 + oauth2-mock-server@8.2.0: + dependencies: + basic-auth: 2.0.1 + cors: 2.8.5 + express: 5.2.1 + is-plain-obj: 4.1.0 + jose: 6.1.3 + transitivePeerDependencies: + - supports-color + object-assign@4.1.1: {} + object-inspect@1.13.4: {} + obliterator@2.0.5: {} on-exit-leak-free@2.1.2: {} + on-finished@2.4.1: + dependencies: + ee-first: 1.1.1 + + once@1.4.0: + dependencies: + wrappy: 1.0.2 + onetime@5.1.2: dependencies: mimic-fn: 2.1.0 @@ -6186,6 +6640,8 @@ snapshots: is-decimal: 2.0.1 is-hexadecimal: 2.0.1 + parseurl@1.3.3: {} + path-browserify@1.0.1: {} path-key@3.1.1: {} @@ -6194,6 +6650,8 @@ snapshots: path-parse@1.0.7: {} + path-to-regexp@8.3.0: {} + path-type@4.0.0: {} pathe@2.0.3: {} @@ -6310,8 +6768,17 @@ snapshots: object-assign: 4.1.1 react-is: 16.13.1 + proxy-addr@2.0.7: + dependencies: + forwarded: 0.2.0 + ipaddr.js: 1.9.1 + punycode@2.3.1: {} + qs@6.14.0: + dependencies: + side-channel: 1.1.0 + quansync@0.2.11: {} queue-lit@1.5.2: {} @@ -6320,6 +6787,15 @@ snapshots: quick-format-unescaped@4.0.4: {} + range-parser@1.2.1: {} + + raw-body@3.0.2: + dependencies: + bytes: 3.1.2 + http-errors: 2.0.1 + iconv-lite: 0.7.1 + unpipe: 1.0.0 + react-day-picker@8.10.1(date-fns@4.1.0)(react@19.0.0): dependencies: date-fns: 4.1.0 @@ -6487,10 +6963,22 @@ snapshots: '@rollup/rollup-win32-x64-msvc': 4.52.5 fsevents: 2.3.3 + router@2.2.0: + dependencies: + debug: 4.4.3 + depd: 2.0.0 + is-promise: 4.0.0 + parseurl: 1.3.3 + path-to-regexp: 8.3.0 + transitivePeerDependencies: + - supports-color + run-parallel@1.2.0: dependencies: queue-microtask: 1.2.3 + safe-buffer@5.1.2: {} + safe-buffer@5.2.1: {} safe-regex2@5.0.0: @@ -6499,6 +6987,8 @@ snapshots: safe-stable-stringify@2.5.0: {} + safer-buffer@2.1.2: {} + scheduler@0.25.0: {} secure-json-parse@4.1.0: {} @@ -6511,8 +7001,35 @@ snapshots: semver@7.7.3: {} + send@1.2.1: + dependencies: + debug: 4.4.3 + encodeurl: 2.0.0 + escape-html: 1.0.3 + etag: 1.8.1 + fresh: 2.0.0 + http-errors: 2.0.1 + mime-types: 3.0.2 + ms: 2.1.3 + on-finished: 2.4.1 + range-parser: 1.2.1 + statuses: 2.0.2 + transitivePeerDependencies: + - supports-color + + serve-static@2.2.1: + dependencies: + encodeurl: 2.0.0 + escape-html: 1.0.3 + parseurl: 1.3.3 + send: 1.2.1 + transitivePeerDependencies: + - supports-color + set-cookie-parser@2.7.1: {} + setprototypeof@1.2.0: {} + sharp@0.34.5: dependencies: '@img/colour': 1.0.0 @@ -6551,6 +7068,34 @@ snapshots: shebang-regex@3.0.0: {} + side-channel-list@1.0.0: + dependencies: + es-errors: 1.3.0 + object-inspect: 1.13.4 + + side-channel-map@1.0.1: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + object-inspect: 1.13.4 + + side-channel-weakmap@1.0.2: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + object-inspect: 1.13.4 + side-channel-map: 1.0.1 + + side-channel@1.1.0: + dependencies: + es-errors: 1.3.0 + object-inspect: 1.13.4 + side-channel-list: 1.0.0 + side-channel-map: 1.0.1 + side-channel-weakmap: 1.0.2 + signal-exit@3.0.7: {} sisteransi@1.0.5: {} @@ -6576,6 +7121,8 @@ snapshots: sprintf-js@1.0.3: {} + statuses@2.0.2: {} + stdin-discarder@0.1.0: dependencies: bl: 5.1.0 @@ -6609,12 +7156,10 @@ snapshots: strip-json-comments@3.1.1: {} - styled-jsx@5.1.6(@babel/core@7.28.4)(react@19.0.0): + styled-jsx@5.1.6(react@19.0.0): dependencies: client-only: 0.0.1 react: 19.0.0 - optionalDependencies: - '@babel/core': 7.28.4 supports-color@8.1.1: dependencies: @@ -6651,6 +7196,8 @@ snapshots: toad-cache@3.7.0: {} + toidentifier@1.0.1: {} + tsc-alias@1.8.16: dependencies: chokidar: 3.6.0 @@ -6670,6 +7217,12 @@ snapshots: optionalDependencies: fsevents: 2.3.3 + type-is@2.0.1: + dependencies: + content-type: 1.0.5 + media-typer: 1.1.0 + mime-types: 3.0.2 + typescript@5.7.3: {} typescript@5.8.2: {} @@ -6699,6 +7252,8 @@ snapshots: universalify@2.0.1: {} + unpipe@1.0.0: {} + update-browserslist-db@1.1.4(browserslist@4.27.0): dependencies: browserslist: 4.27.0 @@ -6730,6 +7285,8 @@ snapshots: util-deprecate@1.0.2: {} + vary@1.1.2: {} + vaul@1.1.2(@types/react-dom@19.0.2(@types/react@19.0.4))(@types/react@19.0.4)(react-dom@19.0.0(react@19.0.0))(react@19.0.0): dependencies: '@radix-ui/react-dialog': 1.1.6(@types/react-dom@19.0.2(@types/react@19.0.4))(@types/react@19.0.4)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) @@ -6833,6 +7390,8 @@ snapshots: string-width: 4.2.3 strip-ansi: 6.0.1 + wrappy@1.0.2: {} + y18n@5.0.8: {} yallist@3.1.1: {} diff --git a/scripts/oauth/mock-server.ts b/scripts/oauth/mock-server.ts new file mode 100644 index 0000000..0914e86 --- /dev/null +++ b/scripts/oauth/mock-server.ts @@ -0,0 +1,23 @@ +import { OAuth2Server } from "oauth2-mock-server"; + +const server = new OAuth2Server(); + +async function start() { + // Generate RSA key for signing tokens + await server.issuer.keys.generate("RS256"); + + // Start server on port 8080 + await server.start(8080, "localhost"); + + console.log("OAuth2 Mock Server running on http://localhost:8080"); + console.log("Endpoints:"); + console.log(" Authorization: http://localhost:8080/authorize"); + console.log(" Token: http://localhost:8080/token"); + console.log(" JWKS: http://localhost:8080/.well-known/jwks.json"); + console.log( + " OpenID Config: http://localhost:8080/.well-known/openid-configuration", + ); + console.log("\nPress Ctrl+C to stop the server"); +} + +start().catch(console.error); diff --git a/scripts/oauth/oauth2-local-test.json b/scripts/oauth/oauth2-local-test.json new file mode 100644 index 0000000..78c9b85 --- /dev/null +++ b/scripts/oauth/oauth2-local-test.json @@ -0,0 +1,277 @@ +{ + "openapi": "3.1.0", + "info": { + "title": "OAuth2 Local Test API", + "description": "API para testear OAuth2 con mock server local (oauth2-mock-server)", + "version": "1.0.0", + "contact": { + "name": "Docutopia", + "url": "https://github.com/rhinolabs/docutopia" + } + }, + "servers": [ + { + "url": "http://localhost:3001/api", + "description": "Local development server" + } + ], + "components": { + "securitySchemes": { + "oauth2": { + "type": "oauth2", + "description": "OAuth2 authentication with local mock server", + "flows": { + "authorizationCode": { + "authorizationUrl": "http://localhost:8080/authorize", + "tokenUrl": "http://localhost:8080/token", + "scopes": { + "read": "Read access to resources", + "write": "Write access to resources", + "admin": "Admin access" + } + }, + "clientCredentials": { + "tokenUrl": "http://localhost:8080/token", + "scopes": { + "api": "Full API access" + } + }, + "password": { + "tokenUrl": "http://localhost:8080/token", + "scopes": { + "read": "Read access", + "write": "Write access" + } + } + } + } + }, + "schemas": { + "User": { + "type": "object", + "properties": { + "id": { + "type": "string", + "format": "uuid" + }, + "name": { + "type": "string" + }, + "email": { + "type": "string", + "format": "email" + } + }, + "required": ["id", "name", "email"] + }, + "Error": { + "type": "object", + "properties": { + "code": { + "type": "integer" + }, + "message": { + "type": "string" + } + } + } + } + }, + "security": [ + { + "oauth2": ["read"] + } + ], + "paths": { + "/users": { + "get": { + "summary": "List users", + "description": "Get a list of all users", + "operationId": "listUsers", + "tags": ["Users"], + "security": [ + { + "oauth2": ["read"] + } + ], + "responses": { + "200": { + "description": "Successful response", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/User" + } + } + } + } + }, + "401": { + "description": "Unauthorized", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + } + } + }, + "post": { + "summary": "Create user", + "description": "Create a new user", + "operationId": "createUser", + "tags": ["Users"], + "security": [ + { + "oauth2": ["write"] + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "email": { + "type": "string", + "format": "email" + } + }, + "required": ["name", "email"] + } + } + } + }, + "responses": { + "201": { + "description": "User created", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/User" + } + } + } + }, + "401": { + "description": "Unauthorized" + } + } + } + }, + "/users/{userId}": { + "get": { + "summary": "Get user by ID", + "description": "Get a specific user by their ID", + "operationId": "getUserById", + "tags": ["Users"], + "security": [ + { + "oauth2": ["read"] + } + ], + "parameters": [ + { + "name": "userId", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + } + ], + "responses": { + "200": { + "description": "Successful response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/User" + } + } + } + }, + "404": { + "description": "User not found" + } + } + } + }, + "/admin/settings": { + "get": { + "summary": "Get admin settings", + "description": "Get application settings (admin only)", + "operationId": "getAdminSettings", + "tags": ["Admin"], + "security": [ + { + "oauth2": ["admin"] + } + ], + "responses": { + "200": { + "description": "Successful response", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "maintenance": { + "type": "boolean" + }, + "maxUsers": { + "type": "integer" + } + } + } + } + } + }, + "403": { + "description": "Forbidden - Admin access required" + } + } + } + }, + "/health": { + "get": { + "summary": "Health check", + "description": "Check API health status (no auth required)", + "operationId": "healthCheck", + "tags": ["System"], + "security": [], + "responses": { + "200": { + "description": "API is healthy", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "status": { + "type": "string", + "enum": ["healthy", "degraded"] + }, + "timestamp": { + "type": "string", + "format": "date-time" + } + } + } + } + } + } + } + } + } + } +}