use correct token for jwt filter & proper validation of auth tokens

This commit is contained in:
Dario Ghunney Ware 2025-12-12 17:49:11 +00:00
parent ae83342683
commit a545d5f3a3
8 changed files with 91 additions and 79 deletions

View File

@ -46,6 +46,7 @@ dependencies {
api 'org.springframework.boot:spring-boot-starter-security'
api 'org.springframework.boot:spring-boot-starter-data-jpa'
api 'org.springframework.boot:spring-boot-starter-oauth2-client'
api 'org.springframework.security:spring-security-oauth2-resource-server'
api 'org.springframework.boot:spring-boot-starter-mail'
api 'org.springframework.boot:spring-boot-starter-cache'
api 'com.github.ben-manes.caffeine:caffeine'

View File

@ -12,6 +12,7 @@ import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.oauth2.core.user.OAuth2User;
import org.springframework.security.oauth2.jwt.Jwt;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
@ -371,6 +372,13 @@ public class ProprietaryUIDataController {
if (principal instanceof UserDetails detailsUser) {
username = detailsUser.getUsername();
} else if (principal instanceof Jwt jwt) {
username = jwt.getSubject();
switch (jwt.getClaimAsString("authType")) {
case "OAUTH2" -> isOAuth2Login = true;
case "SAML2" -> isSaml2Login = true;
}
} else if (principal instanceof OAuth2User oAuth2User) {
username = oAuth2User.getName();
isOAuth2Login = true;

View File

@ -1,6 +1,8 @@
package stirling.software.proprietary.security;
import java.io.IOException;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.security.cert.X509Certificate;
import java.security.interfaces.RSAPrivateKey;
import java.util.ArrayList;
@ -67,28 +69,18 @@ public class CustomLogoutSuccessHandler extends SimpleUrlLogoutSuccessHandler {
String authType = null;
if (authentication instanceof JwtAuthenticationToken jwtAuth) {
authType =
(String)
jwtAuth.getToken()
.getClaims()
.getOrDefault("authType", null);
(String) jwtAuth.getToken().getClaims().getOrDefault("authType", null);
log.debug("JWT-based logout detected with authType: {}", authType);
}
if ("SAML2".equals(authType)) {
// Handle SAML2 logout redirection
if (authentication instanceof Saml2Authentication samlAuthentication) {
getRedirect_saml2(request, response, samlAuthentication);
} else {
log.info("SAML2 logout via JWT - redirecting to login page");
getRedirectStrategy().sendRedirect(request, response, LOGOUT_PATH);
}
} else if ("OAUTH2".equals(authType)) {
if (authentication instanceof OAuth2AuthenticationToken oAuthToken) {
getRedirect_oauth2(request, response, oAuthToken);
} else {
log.info("OAuth2 logout via JWT - attempting OIDC logout");
handleJwtOAuth2Logout(request, response);
}
if (authentication instanceof Saml2Authentication samlAuthentication) {
getRedirect_saml2(request, response, samlAuthentication);
} else if (authentication instanceof OAuth2AuthenticationToken oAuthToken) {
log.info("OAuth2 logout via JWT - attempting OIDC logout");
getRedirect_oauth2(request, response, oAuthToken);
} else if (authentication
instanceof JwtAuthenticationToken jwtAuthenticationToken) {
getRedirectJwt(request, response, jwtAuthenticationToken);
} else if (authentication instanceof UsernamePasswordAuthenticationToken
|| authentication instanceof JwtAuthenticationToken) {
// Handle Username/Password logout (or JWT without OAUTH2/SAML2 authType)
@ -192,7 +184,10 @@ public class CustomLogoutSuccessHandler extends SimpleUrlLogoutSuccessHandler {
}
// Redirect for JWT-based OAuth2 authentication logout
private void handleJwtOAuth2Logout(HttpServletRequest request, HttpServletResponse response)
private void getRedirectJwt(
HttpServletRequest request,
HttpServletResponse response,
JwtAuthenticationToken jwtAuthenticationToken)
throws IOException {
OAUTH2 oauth = securityProperties.getOauth2();
String path = checkForErrors(request);
@ -238,7 +233,7 @@ public class CustomLogoutSuccessHandler extends SimpleUrlLogoutSuccessHandler {
}
logoutUrlBuilder
.append("post_logout_redirect_uri=")
.append(response.encodeRedirectURL(redirectUrl));
.append(URLEncoder.encode(redirectUrl, StandardCharsets.UTF_8));
String logoutUrl = logoutUrlBuilder.toString();
log.info("JWT-based OAuth2 logout URL: {}", logoutUrl);
@ -271,6 +266,7 @@ public class CustomLogoutSuccessHandler extends SimpleUrlLogoutSuccessHandler {
"keycloak".equalsIgnoreCase(oAuthToken.getAuthorizedClientRegistrationId());
if (isKeycloak) {
KeycloakProvider keycloak = oauth.getClient().getKeycloak();
if (keycloak.getIssuer() != null && !keycloak.getIssuer().isBlank()) {
issuer = keycloak.getIssuer();
clientId = keycloak.getClientId();
@ -304,7 +300,7 @@ public class CustomLogoutSuccessHandler extends SimpleUrlLogoutSuccessHandler {
logoutUrlBuilder.append("id_token_hint=").append(idToken);
logoutUrlBuilder
.append("&post_logout_redirect_uri=")
.append(response.encodeRedirectURL(redirectUrl));
.append(URLEncoder.encode(redirectUrl, StandardCharsets.UTF_8));
// client_id is optional when id_token_hint is present, but included for
// compatibility
@ -322,7 +318,7 @@ public class CustomLogoutSuccessHandler extends SimpleUrlLogoutSuccessHandler {
}
logoutUrlBuilder
.append("post_logout_redirect_uri=")
.append(response.encodeRedirectURL(redirectUrl));
.append(URLEncoder.encode(redirectUrl, StandardCharsets.UTF_8));
log.warn("OIDC logout without id_token_hint - user may see confirmation screen");
}
@ -373,7 +369,7 @@ public class CustomLogoutSuccessHandler extends SimpleUrlLogoutSuccessHandler {
*/
private String checkForErrors(HttpServletRequest request) {
String errorMessage;
String path = "?logout=true";
String path = "logout=true";
if (request.getParameter("oAuth2AuthenticationErrorWeb") != null) {
path = "errorOAuth=userAlreadyExistsWeb";
@ -495,6 +491,7 @@ public class CustomLogoutSuccessHandler extends SimpleUrlLogoutSuccessHandler {
ApplicationProperties.Security.OAUTH2 oauth, String issuer) {
if (oauth != null && oauth.getClient() != null) {
String configuredEndpoint = oauth.getClient().getEndSessionEndpoint();
if (configuredEndpoint != null && !configuredEndpoint.isBlank()) {
log.debug("Using configured end_session_endpoint: {}", configuredEndpoint);
return configuredEndpoint;

View File

@ -23,6 +23,7 @@ import org.springframework.security.saml2.provider.service.registration.RelyingP
import org.springframework.security.saml2.provider.service.web.authentication.OpenSaml4AuthenticationRequestResolver;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.security.web.authentication.logout.LogoutFilter;
import org.springframework.security.web.authentication.rememberme.PersistentTokenRepository;
import org.springframework.security.web.savedrequest.NullRequestCache;
import org.springframework.security.web.servlet.util.matcher.PathPatternRequestMatcher;
@ -201,7 +202,7 @@ public class SecurityConfiguration {
http.addFilterBefore(
userAuthenticationFilter, UsernamePasswordAuthenticationFilter.class)
.addFilterBefore(rateLimitingFilter, UsernamePasswordAuthenticationFilter.class)
.addFilterBefore(jwtAuthenticationFilter, UserAuthenticationFilter.class);
.addFilterBefore(jwtAuthenticationFilter, LogoutFilter.class);
http.sessionManagement(
sessionManagement ->

View File

@ -25,6 +25,7 @@ import stirling.software.proprietary.audit.AuditEventType;
import stirling.software.proprietary.audit.AuditLevel;
import stirling.software.proprietary.audit.Audited;
import stirling.software.proprietary.security.model.AuthenticationType;
import stirling.software.proprietary.security.model.Authority;
import stirling.software.proprietary.security.model.User;
import stirling.software.proprietary.security.model.api.user.UsernameAndPass;
import stirling.software.proprietary.security.service.CustomUserDetailsService;
@ -155,11 +156,30 @@ public class AuthController {
.body(Map.of("error", "Not authenticated"));
}
UserDetails userDetails = (UserDetails) auth.getPrincipal();
User user = (User) userDetails;
Object principal = auth.getPrincipal();
User user;
if (principal instanceof User u) {
user = u;
} else {
// JWT case - get User from Authority
user =
auth.getAuthorities().stream()
.filter(Authority.class::isInstance)
.map(Authority.class::cast)
.findFirst()
.map(Authority::getUser)
.orElseThrow(
() ->
new IllegalStateException(
"User not found in authentication"));
}
return ResponseEntity.ok(Map.of("user", buildUserResponse(user)));
} catch (IllegalStateException e) {
log.error("User not found in authentication context", e);
return ResponseEntity.status(HttpStatus.UNAUTHORIZED)
.body(Map.of("error", "User not found"));
} catch (Exception e) {
log.error("Get current user error", e);
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
@ -216,6 +236,7 @@ public class AuthController {
*/
private Map<String, Object> buildUserResponse(User user) {
Map<String, Object> userMap = new HashMap<>();
userMap.put("id", user.getId());
userMap.put("email", user.getUsername()); // Use username as email
userMap.put("username", user.getUsername());

View File

@ -6,10 +6,9 @@ import static stirling.software.proprietary.security.model.AuthenticationType.OA
import static stirling.software.proprietary.security.model.AuthenticationType.SAML2;
import static stirling.software.proprietary.security.model.AuthenticationType.WEB;
import io.jsonwebtoken.Jwts;
import java.io.IOException;
import java.sql.SQLException;
import java.time.Instant;
import java.util.Map;
import java.util.Optional;
@ -24,6 +23,8 @@ import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
import org.springframework.web.filter.OncePerRequestFilter;
import io.jsonwebtoken.Jwts;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
@ -184,6 +185,12 @@ public class JwtAuthenticationFilter extends OncePerRequestFilter {
UserDetails userDetails = userDetailsService.loadUserByUsername(username);
if (userDetails != null) {
// Convert timestamp claims from Long to Instant (jjwt stores as Long,
// Spring Security Jwt expects Instant)
convertTimestampClaim(claims, "iat");
convertTimestampClaim(claims, "exp");
convertTimestampClaim(claims, "nbf");
Jwt jwt =
Jwt.withTokenValue(jwtToken)
.headers(headers -> headers.put("alg", Jwts.SIG.RS256.getId()))
@ -228,6 +235,11 @@ public class JwtAuthenticationFilter extends OncePerRequestFilter {
}
}
private void convertTimestampClaim(Map<String, Object> claims, String claimName) {
Long timestamp = (Long) claims.get(claimName);
claims.put(claimName, Instant.ofEpochSecond(timestamp));
}
private void handleAuthenticationFailure(
HttpServletRequest request,
HttpServletResponse response,

View File

@ -79,13 +79,14 @@ public class JwtService implements JwtServiceInterface {
}
KeyPair keyPair = keyPairOpt.get();
Date now = new Date();
var builder =
Jwts.builder()
.claims(claims)
.subject(username)
.issuer(ISSUER)
.issuedAt(new Date())
.issuedAt(now)
.notBefore(now)
.expiration(new Date(System.currentTimeMillis() + EXPIRATION))
.signWith(keyPair.getPrivate(), Jwts.SIG.RS256);
@ -251,14 +252,10 @@ public class JwtService implements JwtServiceInterface {
@Override
public String extractToken(HttpServletRequest request) {
// Extract from Authorization header Bearer token
String authHeader = request.getHeader("Authorization");
if (authHeader != null && authHeader.startsWith("Bearer ")) {
String token = authHeader.substring(7); // Remove "Bearer " prefix
return token;
}
return null;
return (authHeader != null && authHeader.startsWith("Bearer "))
? authHeader.substring(7)
: null;
}
@Override

View File

@ -457,7 +457,6 @@
}
],
"license": "MIT",
"peer": true,
"engines": {
"node": ">=18"
},
@ -501,7 +500,6 @@
}
],
"license": "MIT",
"peer": true,
"engines": {
"node": ">=18"
}
@ -582,7 +580,6 @@
"resolved": "https://registry.npmjs.org/@embedpdf/core/-/core-1.5.0.tgz",
"integrity": "sha512-Yrh9XoVaT8cUgzgqpJ7hx5wg6BqQrCFirqqlSwVb+Ly9oNn4fZbR9GycIWmzJOU5XBnaOJjXfQSaDyoNP0woNA==",
"license": "MIT",
"peer": true,
"dependencies": {
"@embedpdf/engines": "1.5.0",
"@embedpdf/models": "1.5.0"
@ -682,7 +679,6 @@
"resolved": "https://registry.npmjs.org/@embedpdf/plugin-history/-/plugin-history-1.5.0.tgz",
"integrity": "sha512-p7PTNNaIr4gH3jLwX+eLJe1DeUXgi21kVGN6SRx/pocH8esg4jqoOeD/YiRRZoZnPOiy0jBXVhkPkwSmY7a2hQ==",
"license": "MIT",
"peer": true,
"dependencies": {
"@embedpdf/models": "1.5.0"
},
@ -699,7 +695,6 @@
"resolved": "https://registry.npmjs.org/@embedpdf/plugin-interaction-manager/-/plugin-interaction-manager-1.5.0.tgz",
"integrity": "sha512-ckHgTfvkW6c5Ta7Mc+Dl9C2foVnvEpqEJ84wyBnqrU0OWbe/jsiPhyKBVeartMGqNI/kVfaQTXupyrKhekAVmg==",
"license": "MIT",
"peer": true,
"dependencies": {
"@embedpdf/models": "1.5.0"
},
@ -717,7 +712,6 @@
"resolved": "https://registry.npmjs.org/@embedpdf/plugin-loader/-/plugin-loader-1.5.0.tgz",
"integrity": "sha512-P4YpIZfaW69etYIjphyaL4cGl2pB14h3OdTE0tRQ2pZYZHFLTvlt4q9B3PVSdhlSrHK5nob7jfLGon2U7xCslg==",
"license": "MIT",
"peer": true,
"dependencies": {
"@embedpdf/models": "1.5.0"
},
@ -771,7 +765,6 @@
"resolved": "https://registry.npmjs.org/@embedpdf/plugin-render/-/plugin-render-1.5.0.tgz",
"integrity": "sha512-ywwSj0ByrlkvrJIHKRzqxARkOZriki8VJUC+T4MV8fGyF4CzvCRJyKlPktahFz+VxhoodqTh7lBCib68dH+GvA==",
"license": "MIT",
"peer": true,
"dependencies": {
"@embedpdf/models": "1.5.0"
},
@ -806,7 +799,6 @@
"resolved": "https://registry.npmjs.org/@embedpdf/plugin-scroll/-/plugin-scroll-1.5.0.tgz",
"integrity": "sha512-RNmTZCZ8X1mA8cw9M7TMDuhO9GtkOalGha2bBL3En3D1IlDRS7PzNNMSMV7eqT7OQICSTltlpJ8p8Qi5esvL/Q==",
"license": "MIT",
"peer": true,
"dependencies": {
"@embedpdf/models": "1.5.0"
},
@ -843,7 +835,6 @@
"resolved": "https://registry.npmjs.org/@embedpdf/plugin-selection/-/plugin-selection-1.5.0.tgz",
"integrity": "sha512-zrxLBAZQoPswDuf9q9DrYaQc6B0Ysc2U1hueTjNH/4+ydfl0BFXZkKR63C2e3YmWtXvKjkoIj0GyPzsiBORLUw==",
"license": "MIT",
"peer": true,
"dependencies": {
"@embedpdf/models": "1.5.0"
},
@ -919,7 +910,6 @@
"resolved": "https://registry.npmjs.org/@embedpdf/plugin-viewport/-/plugin-viewport-1.5.0.tgz",
"integrity": "sha512-G8GDyYRhfehw72+r4qKkydnA5+AU8qH67g01Y12b0DzI0VIzymh/05Z4dK8DsY3jyWPXJfw2hlg5+KDHaMBHgQ==",
"license": "MIT",
"peer": true,
"dependencies": {
"@embedpdf/models": "1.5.0"
},
@ -1075,7 +1065,6 @@
"resolved": "https://registry.npmjs.org/@emotion/react/-/react-11.14.0.tgz",
"integrity": "sha512-O000MLDBDdk/EohJPFUqvnp4qnHeYkVP5B0xEG0D/L7cOKP9kefu2DXn8dj74cQfsEzUqh+sr1RzFqiL1o+PpA==",
"license": "MIT",
"peer": true,
"dependencies": {
"@babel/runtime": "^7.18.3",
"@emotion/babel-plugin": "^11.13.5",
@ -1119,7 +1108,6 @@
"resolved": "https://registry.npmjs.org/@emotion/styled/-/styled-11.14.1.tgz",
"integrity": "sha512-qEEJt42DuToa3gurlH4Qqc1kVpNq8wO8cJtDzU46TjlzWjDlsVyevtYCRijVq3SrHsROS+gVQ8Fnea108GnKzw==",
"license": "MIT",
"peer": true,
"dependencies": {
"@babel/runtime": "^7.18.3",
"@emotion/babel-plugin": "^11.13.5",
@ -2150,7 +2138,6 @@
"resolved": "https://registry.npmjs.org/@mantine/core/-/core-8.3.6.tgz",
"integrity": "sha512-paTl+0x+O/QtgMtqVJaG8maD8sfiOdgPmLOyG485FmeGZ1L3KMdEkhxZtmdGlDFsLXhmMGQ57ducT90bvhXX5A==",
"license": "MIT",
"peer": true,
"dependencies": {
"@floating-ui/react": "^0.27.16",
"clsx": "^2.1.1",
@ -2201,7 +2188,6 @@
"resolved": "https://registry.npmjs.org/@mantine/hooks/-/hooks-8.3.6.tgz",
"integrity": "sha512-liHfaWXHAkLjJy+Bkr29UsCwAoDQ/a64WrM67lksx8F0qqyjR5RQH8zVlhuOjdpQnwtlUkE/YiTvbJiPcoI0bw==",
"license": "MIT",
"peer": true,
"peerDependencies": {
"react": "^18.x || ^19.x"
}
@ -2269,7 +2255,6 @@
"resolved": "https://registry.npmjs.org/@mui/material/-/material-7.3.5.tgz",
"integrity": "sha512-8VVxFmp1GIm9PpmnQoCoYo0UWHoOrdA57tDL62vkpzEgvb/d71Wsbv4FRg7r1Gyx7PuSo0tflH34cdl/NvfHNQ==",
"license": "MIT",
"peer": true,
"dependencies": {
"@babel/runtime": "^7.28.4",
"@mui/core-downloads-tracker": "^7.3.5",
@ -3202,7 +3187,6 @@
"resolved": "https://registry.npmjs.org/@stripe/stripe-js/-/stripe-js-7.9.0.tgz",
"integrity": "sha512-ggs5k+/0FUJcIgNY08aZTqpBTtbExkJMYMLSMwyucrhtWexVOEY1KJmhBsxf+E/Q15f5rbwBpj+t0t2AW2oCsQ==",
"license": "MIT",
"peer": true,
"engines": {
"node": ">=12.16"
}
@ -3321,6 +3305,7 @@
"resolved": "https://registry.npmjs.org/@sveltejs/acorn-typescript/-/acorn-typescript-1.0.6.tgz",
"integrity": "sha512-4awhxtMh4cx9blePWl10HRHj8Iivtqj+2QdDCSMDzxG+XKa9+VCNupQuCuvzEhYPzZSrX+0gC+0lHA/0fFKKQQ==",
"license": "MIT",
"peer": true,
"peerDependencies": {
"acorn": "^8.9.0"
}
@ -4097,7 +4082,6 @@
"integrity": "sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@babel/code-frame": "^7.10.4",
"@babel/runtime": "^7.12.5",
@ -4426,7 +4410,6 @@
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.2.tgz",
"integrity": "sha512-6mDvHUFSjyT2B2yeNx2nUgMxh9LtOWvkhIU3uePn2I2oyNymUAX1NIsdgviM4CH+JSrp2D2hsMvJOkxY+0wNRA==",
"license": "MIT",
"peer": true,
"dependencies": {
"csstype": "^3.0.2"
}
@ -4437,7 +4420,6 @@
"integrity": "sha512-9KQPoO6mZCi7jcIStSnlOWn2nEF3mNmyr3rIAsGnAbQKYbRLyqmeSc39EVgtxXVia+LMT8j3knZLAZAh+xLmrw==",
"dev": true,
"license": "MIT",
"peer": true,
"peerDependencies": {
"@types/react": "^19.2.0"
}
@ -4507,7 +4489,6 @@
"integrity": "sha512-6m1I5RmHBGTnUGS113G04DMu3CpSdxCAU/UvtjNWL4Nuf3MW9tQhiJqRlHzChIkhy6kZSAQmc+I1bcGjE3yNKg==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@typescript-eslint/scope-manager": "8.46.3",
"@typescript-eslint/types": "8.46.3",
@ -5221,6 +5202,7 @@
"resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.24.tgz",
"integrity": "sha512-BM8kBhtlkkbnyl4q+HiF5R5BL0ycDPfihowulm02q3WYp2vxgPcJuZO866qa/0u3idbMntKEtVNuAUp5bw4teg==",
"license": "MIT",
"peer": true,
"dependencies": {
"@vue/shared": "3.5.24"
}
@ -5230,6 +5212,7 @@
"resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.5.24.tgz",
"integrity": "sha512-RYP/byyKDgNIqfX/gNb2PB55dJmM97jc9wyF3jK7QUInYKypK2exmZMNwnjueWwGceEkP6NChd3D2ZVEp9undQ==",
"license": "MIT",
"peer": true,
"dependencies": {
"@vue/reactivity": "3.5.24",
"@vue/shared": "3.5.24"
@ -5240,6 +5223,7 @@
"resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.5.24.tgz",
"integrity": "sha512-Z8ANhr/i0XIluonHVjbUkjvn+CyrxbXRIxR7wn7+X7xlcb7dJsfITZbkVOeJZdP8VZwfrWRsWdShH6pngMxRjw==",
"license": "MIT",
"peer": true,
"dependencies": {
"@vue/reactivity": "3.5.24",
"@vue/runtime-core": "3.5.24",
@ -5252,6 +5236,7 @@
"resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.5.24.tgz",
"integrity": "sha512-Yh2j2Y4G/0/4z/xJ1Bad4mxaAk++C2v4kaa8oSYTMJBJ00/ndPuxCnWeot0/7/qafQFLh5pr6xeV6SdMcE/G1w==",
"license": "MIT",
"peer": true,
"dependencies": {
"@vue/compiler-ssr": "3.5.24",
"@vue/shared": "3.5.24"
@ -5278,7 +5263,6 @@
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz",
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
"license": "MIT",
"peer": true,
"bin": {
"acorn": "bin/acorn"
},
@ -5686,6 +5670,7 @@
"resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-4.1.0.tgz",
"integrity": "sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==",
"license": "Apache-2.0",
"peer": true,
"engines": {
"node": ">= 0.4"
}
@ -5962,7 +5947,6 @@
}
],
"license": "MIT",
"peer": true,
"dependencies": {
"baseline-browser-mapping": "^2.8.19",
"caniuse-lite": "^1.0.30001751",
@ -7010,8 +6994,7 @@
"resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1521046.tgz",
"integrity": "sha512-vhE6eymDQSKWUXwwA37NtTTVEzjtGVfDr3pRbsWEQ5onH/Snp2c+2xZHWJJawG/0hCCJLRGt4xVtEVUVILol4w==",
"dev": true,
"license": "BSD-3-Clause",
"peer": true
"license": "BSD-3-Clause"
},
"node_modules/dezalgo": {
"version": "1.0.4",
@ -7406,7 +7389,6 @@
"integrity": "sha512-BhHmn2yNOFA9H9JmmIVKJmd288g9hrVRDkdoIgRCRuSySRUHH7r/DI6aAXW9T1WwUuY3DFgrcaqB+deURBLR5g==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@eslint-community/eslint-utils": "^4.8.0",
"@eslint-community/regexpp": "^4.12.1",
@ -7577,7 +7559,6 @@
"integrity": "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@rtsao/scc": "^1.1.0",
"array-includes": "^3.1.9",
@ -7744,7 +7725,8 @@
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/esm-env/-/esm-env-1.2.2.tgz",
"integrity": "sha512-Epxrv+Nr/CaL4ZcFGPJIYLWFom+YeV1DqMLHJoEd9SYRxNbaFruBwfEX/kkHUJf55j2+TUbmDcmuilbP1TmXHA==",
"license": "MIT"
"license": "MIT",
"peer": true
},
"node_modules/espree": {
"version": "10.4.0",
@ -7809,6 +7791,7 @@
"resolved": "https://registry.npmjs.org/esrap/-/esrap-2.1.2.tgz",
"integrity": "sha512-DgvlIQeowRNyvLPWW4PT7Gu13WznY288Du086E751mwwbsgr29ytBiYeLzAGIo0qk3Ujob0SDk8TiSaM5WQzNg==",
"license": "MIT",
"peer": true,
"dependencies": {
"@jridgewell/sourcemap-codec": "^1.4.15"
}
@ -8899,7 +8882,6 @@
}
],
"license": "MIT",
"peer": true,
"dependencies": {
"@babel/runtime": "^7.27.6"
},
@ -9376,6 +9358,7 @@
"resolved": "https://registry.npmjs.org/is-reference/-/is-reference-3.0.3.tgz",
"integrity": "sha512-ixkJoqQvAP88E6wLydLGGqCJsrFUnqoH6HnaczB8XmDH1oaWU+xxdptvikTgaEhtZ53Ky6YXiBuUI2WXLMCwjw==",
"license": "MIT",
"peer": true,
"dependencies": {
"@types/estree": "^1.0.6"
}
@ -9696,7 +9679,6 @@
"integrity": "sha512-Pcfm3eZ+eO4JdZCXthW9tCDT3nF4K+9dmeZ+5X39n+Kqz0DDIABRP5CAEOHRFZk8RGuC2efksTJxrjp8EXCunQ==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@acemir/cssom": "^0.9.19",
"@asamuzakjp/dom-selector": "^6.7.3",
@ -10283,7 +10265,8 @@
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/locate-character/-/locate-character-3.0.0.tgz",
"integrity": "sha512-SW13ws7BjaeJ6p7Q6CO2nchbYEc3X3J6WrmTTDto7yMPqVSZTUyY5Tjbid+Ab8gLnATtygYtiDIJGQRRn2ZOiA==",
"license": "MIT"
"license": "MIT",
"peer": true
},
"node_modules/locate-path": {
"version": "6.0.0",
@ -11442,7 +11425,6 @@
}
],
"license": "MIT",
"peer": true,
"dependencies": {
"nanoid": "^3.3.11",
"picocolors": "^1.1.1",
@ -11722,7 +11704,6 @@
"resolved": "https://registry.npmjs.org/preact/-/preact-10.27.2.tgz",
"integrity": "sha512-5SYSgFKSyhCbk6SrXyMpqjb5+MQBgfvEKE/OC+PujcY34sOpqtr+0AZQtPYx5IA6VxynQ7rUPCtKzyovpj9Bpg==",
"license": "MIT",
"peer": true,
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/preact"
@ -12105,7 +12086,6 @@
"resolved": "https://registry.npmjs.org/react/-/react-19.2.0.tgz",
"integrity": "sha512-tmbWg6W31tQLeB5cdIBOicJDJRR2KzXsV7uSK9iNfLWQ5bIZfxuPEHp7M8wiHyHnn0DD1i7w3Zmin0FtkrwoCQ==",
"license": "MIT",
"peer": true,
"engines": {
"node": ">=0.10.0"
}
@ -12115,7 +12095,6 @@
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.0.tgz",
"integrity": "sha512-UlbRu4cAiGaIewkPyiRGJk0imDN2T3JjieT6spoL2UeSf5od4n5LB/mQ4ejmxhCFT1tYe8IvaFulzynWovsEFQ==",
"license": "MIT",
"peer": true,
"dependencies": {
"scheduler": "^0.27.0"
},
@ -13627,6 +13606,7 @@
"resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.2.tgz",
"integrity": "sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==",
"license": "Apache-2.0",
"peer": true,
"engines": {
"node": ">= 0.4"
}
@ -13835,7 +13815,6 @@
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
"dev": true,
"license": "MIT",
"peer": true,
"engines": {
"node": ">=12"
},
@ -14137,7 +14116,6 @@
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
"devOptional": true,
"license": "Apache-2.0",
"peer": true,
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
@ -14219,7 +14197,6 @@
"dev": true,
"hasInstallScript": true,
"license": "MIT",
"peer": true,
"dependencies": {
"napi-postinstall": "^0.3.0"
},
@ -14424,7 +14401,6 @@
"integrity": "sha512-BxAKBWmIbrDgrokdGZH1IgkIk/5mMHDreLDmCJ0qpyJaAteP8NvMhkwr/ZCQNqNH97bw/dANTE9PDzqwJghfMQ==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"esbuild": "^0.25.0",
"fdir": "^6.5.0",
@ -14595,7 +14571,6 @@
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
"dev": true,
"license": "MIT",
"peer": true,
"engines": {
"node": ">=12"
},
@ -14609,7 +14584,6 @@
"integrity": "sha512-LUCP5ev3GURDysTWiP47wRRUpLKMOfPh+yKTx3kVIEiu5KOMeqzpnYNsKyOoVrULivR8tLcks4+lga33Whn90A==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@types/chai": "^5.2.2",
"@vitest/expect": "3.2.4",
@ -15221,7 +15195,8 @@
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/zimmerframe/-/zimmerframe-1.1.4.tgz",
"integrity": "sha512-B58NGBEoc8Y9MWWCQGl/gq9xBCe4IiKM0a2x7GZdQKOW5Exr8S1W24J6OgM1njK8xCRGvAJIL/MxXHf6SkmQKQ==",
"license": "MIT"
"license": "MIT",
"peer": true
},
"node_modules/zod": {
"version": "3.25.76",